From 895f059117b9c4f0ddc24031ecefb090a955c324 Mon Sep 17 00:00:00 2001 From: Jeff Wainwright Date: Sat, 21 Dec 2024 00:16:43 -0800 Subject: [PATCH 1/4] feat: adds `looseGlobMatching` --- README.md | 8 ++++ index.js | 129 +++++++++++++++++++++++++++--------------------------- 2 files changed, 73 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index fe3052c..cebffa6 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,14 @@ index.js es-check [files...] ``` +**Loose Glob Matching** + +```sh + +--loose-glob-match allows for loose glob matching, default false + +``` + โš ๏ธ **NOTE:** This is primarily intended as a way to override the `files` setting in the `.escheckrc` file for specific invocations. Setting both the `[...files]` argument and `--files` flag is an error. ### Global Options diff --git a/index.js b/index.js index e5bda6e..2a651e5 100755 --- a/index.js +++ b/index.js @@ -25,28 +25,30 @@ program .version(pkg.version) .argument( '[ecmaVersion]', - 'ecmaVersion to check files against. Can be: es3, es4, es5, es6/es2015, es7/es2016, es8/es2017, es9/es2018, es10/es2019, es11/es2020, es12/es2021, es2022, es2023', + 'ecmaVersion to check files against. Can be: es3, es4, es5, es6/es2015, es7/es2016, es8/es2017, es9/es2018, es10/es2019, es11/es2020, es12/es2021, es13/es2022, es14/es2023', ) .argument('[files...]', 'a glob of files to to test the EcmaScript version against') .option('--module', 'use ES modules') - .option('--allow-hash-bang', 'if the code starts with #! treat it as a comment') + .option('--allow-hash-bang', '--allowHashBang', 'if the code starts with #! treat it as a comment', false) .option('--files ', 'a glob of files to to test the EcmaScript version against (alias for [files...])') .option('--not ', 'folder or file names to skip') - .option('--no-color', 'disable use of colors in output') - .option('-v, --verbose', 'verbose mode: will also output debug messages') - .option('--quiet', 'quiet mode: only displays warn and error messages') + .option('--no-color', '--noColor', 'disable use of colors in output', false) + .option('-v, --verbose', 'verbose mode: will also output debug messages', false) + .option('--quiet', 'quiet mode: only displays warn and error messages', false) + .option('--looseGlobMatching', 'doesn\'t fail if no files are found in some globs/files', false) .option( '--silent', 'silent mode: does not output anything, giving no indication of success or failure other than the exit code', ) .action((ecmaVersionArg, filesArg, options) => { + const noColor = options?.noColor || options?.['no-color'] || false; const logger = winston.createLogger() logger.add( new winston.transports.Console({ silent: options.silent, level: options.verbose ? 'silly' : options.quiet ? 'warn' : 'info', format: winston.format.combine( - ...(supportsColor.stdout ? [winston.format.colorize()] : []), + ...(supportsColor.stdout || !noColor ? [winston.format.colorize()] : []), winston.format.simple(), ), }), @@ -70,8 +72,9 @@ program const files = filesArg && filesArg.length ? filesArg : options.files ? options.files.split(',') : [].concat(config.files) const esmodule = options.module ? options.module : config.module - const allowHashBang = options.allowHashBang ? options.allowHashBang : config.allowHashBang + const allowHashBang = options.allowHashBang || options['allow-hash-bang'] || config.allowHashBang const pathsToIgnore = options.not ? options.not.split(',') : [].concat(config.not || []) + const looseGlobMatching = options.looseGlobMatching || options?.['loose-glob-matching'] || config.looseGlobMatching || false if (!expectedEcmaVersion) { logger.error( @@ -85,6 +88,26 @@ program process.exit(1) } + if (looseGlobMatching) { + logger.debug('ES-Check: loose-glob-matching is set') + } + + const globOpts = { nodir: true } + let allMatchedFiles = [] + files.forEach((pattern) => { + const globbedFiles = glob.sync(pattern, globOpts); + if (globbedFiles.length === 0 && !looseGlobMatching) { + logger.error(`ES-Check: Did not find any files to check for ${pattern}.`) + process.exit(1) + } + allMatchedFiles = allMatchedFiles.concat(globbedFiles); + }, []); + + if (allMatchedFiles.length === 0) { + logger.error(`ES-Check: Did not find any files to check for ${files}.`) + process.exit(1) + } + /** * @note define ecmaScript version */ @@ -100,50 +123,38 @@ program ecmaVersion = '5' break case 'es6': - ecmaVersion = '6' - break - case 'es7': - ecmaVersion = '7' - break - case 'es8': - ecmaVersion = '8' - break - case 'es9': - ecmaVersion = '9' - break - case 'es10': - ecmaVersion = '10' - break - case 'es11': - ecmaVersion = '11' - break - case 'es12': - ecmaVersion = '12' - break case 'es2015': ecmaVersion = '6' break + case 'es7': case 'es2016': ecmaVersion = '7' break + case 'es8': case 'es2017': ecmaVersion = '8' break + case 'es9': case 'es2018': ecmaVersion = '9' break + case 'es10': case 'es2019': ecmaVersion = '10' break + case 'es11': case 'es2020': ecmaVersion = '2020' break + case 'es12': case 'es2021': ecmaVersion = '2021' break + case 'es13': case 'es2022': ecmaVersion = '2022' break + case 'es14': case 'es2023': ecmaVersion = '2023' break @@ -153,9 +164,20 @@ program } const errArray = [] - const globOpts = { nodir: true } const acornOpts = { ecmaVersion: parseInt(ecmaVersion, 10), silent: true } + logger.debug(`ES-Check: Going to check files using version ${ecmaVersion}`) + + if (esmodule) { + acornOpts.sourceType = 'module' + logger.debug('ES-Check: esmodule is set') + } + + if (allowHashBang) { + acornOpts.allowHashBang = true + logger.debug('ES-Check: allowHashBang is set') + } + const expandedPathsToIgnore = pathsToIgnore.reduce((result, path) => { if (path.includes('*')) { return result.concat(glob.sync(path, globOpts)) @@ -174,43 +196,22 @@ program return globbedFiles } - logger.debug(`ES-Check: Going to check files using version ${ecmaVersion}`) - - if (esmodule) { - acornOpts.sourceType = 'module' - logger.debug('ES-Check: esmodule is set') - } - - if (allowHashBang) { - acornOpts.allowHashBang = true - logger.debug('ES-Check: allowHashBang is set') - } - - files.forEach((pattern) => { - const globbedFiles = glob.sync(pattern, globOpts) - - if (globbedFiles.length === 0) { - logger.error(`ES-Check: Did not find any files to check for ${pattern}.`) - process.exit(1) - } - - const filteredFiles = filterForIgnore(globbedFiles) - - filteredFiles.forEach((file) => { - const code = fs.readFileSync(file, 'utf8') - logger.debug(`ES-Check: checking ${file}`) - try { - acorn.parse(code, acornOpts) - } catch (err) { - logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${err}`) - const errorObj = { - err, - stack: err.stack, - file, - } - errArray.push(errorObj) + const filteredFiles = filterForIgnore(allMatchedFiles) + + filteredFiles.forEach((file) => { + const code = fs.readFileSync(file, 'utf8') + logger.debug(`ES-Check: checking ${file}`) + try { + acorn.parse(code, acornOpts) + } catch (err) { + logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${err}`) + const errorObj = { + err, + stack: err.stack, + file, } - }) + errArray.push(errorObj) + } }) if (errArray.length > 0) { From 228a859613e4c8df92239ea33111546fbdf50a8a Mon Sep 17 00:00:00 2001 From: Jeff Wainwright Date: Tue, 31 Dec 2024 01:09:43 -0800 Subject: [PATCH 2/4] chore: updates actions; adds walk --- .github/workflows/codeql-analysis.yml | 4 +- .github/workflows/node.js.yml | 2 +- .github/workflows/update.yml | 2 +- README.md | 29 +- constants.js | 489 ++++++++++++++++++++++++++ index.js | 8 +- package.json | 1 + pnpm-lock.yaml | 11 + test.js | 22 ++ tests/es2018.js | 3 + walk.js | 71 ++++ 11 files changed, 626 insertions(+), 16 deletions(-) create mode 100644 constants.js create mode 100644 tests/es2018.js create mode 100644 walk.js diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 07cd54f..d83936f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,10 +14,10 @@ name: 'CodeQL' on: push: - branches: [master] + branches: [main] pull_request: # The branches below must be a subset of the branches above - branches: [master] + branches: [main] schedule: - cron: '45 14 * * 4' diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 2ed45bf..f3b9f0d 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index 8e9a470..bbd896e 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index cebffa6..82b985c 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,12 @@ index.js es-check [files...] ```sh - 'define the ECMAScript version to check for against a glob of JavaScript files' required -[files...] 'a glob of files to test the ECMAScript version against' required +Usage: index [options] [ecmaVersion] [files...] + +Arguments: + ecmaVersion ecmaVersion to check files against. Can be: es3, es4, es5, es6/es2015, es7/es2016, es8/es2017, es9/es2018, es10/es2019, es11/es2020, es12/es2021, + es13/es2022, es14/es2023 + files a glob of files to to test the EcmaScript version against ``` @@ -126,7 +130,7 @@ index.js es-check [files...] ```sh ---allow-hash-bang supports files that start with hash bang, default false +--allowHashBang supports files that start with hash bang, default false ``` @@ -150,7 +154,7 @@ index.js es-check [files...] ```sh ---loose-glob-match allows for loose glob matching, default false +--looseGlobMatch allows for loose glob matching, default false ``` @@ -160,11 +164,18 @@ index.js es-check [files...] ```sh --h, --help Display help --V, --version Display version ---no-color Disable colors ---quiet Quiet mode - only displays warn and error messages --v, --verbose Verbose mode - will also output debug messages +Options: + -V, --version output the version number + --module use ES modules + --allow-hash-bang, --allowHashBang if the code starts with #! treat it as a comment (default: false) + --files a glob of files to to test the EcmaScript version against (alias for [files...]) + --not folder or file names to skip + --no-color, --noColor disable use of colors in output (default: false) + -v, --verbose verbose mode: will also output debug messages (default: false) + --quiet quiet mode: only displays warn and error messages (default: false) + --looseGlobMatching doesn't fail if no files are found in some globs/files (default: false) + --silent silent mode: does not output anything, giving no indication of success or failure other than the exit code (default: false) + -h, --help display help for command ``` diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..eaf40c9 --- /dev/null +++ b/constants.js @@ -0,0 +1,489 @@ +/** + * Map of ES features (by name) โ†’ earliest ES version + AST detection hints + * ES6 (2015) = 6, ES7 (2016) = 7, ES8 (2017) = 8, ES9 (2018) = 9, + * ES10 (2019) = 10, ES11 (2020) = 11, ES12 (2021) = 12, + * ES13 (2022) = 13, ES14 (2023) = 14, etc. + */ +const ES_FEATURES = { + // ---------------------------------------------------------- + // ES6 / ES2015 + // ---------------------------------------------------------- + ArraySpread: { + version: 6, + example: '[...arr]', + astInfo: { + nodeType: 'ArrayExpression', + childType: 'SpreadElement', + }, + }, + let: { + version: 6, + example: 'let x = 10;', + astInfo: { + nodeType: 'VariableDeclaration', + kind: 'let', + }, + }, + const: { + version: 6, + example: 'const x = 10;', + astInfo: { + nodeType: 'VariableDeclaration', + kind: 'const', + }, + }, + class: { + version: 6, + example: 'class MyClass {}', + astInfo: { + nodeType: 'ClassDeclaration', + }, + }, + extends: { + version: 6, + example: 'class MyClass extends OtherClass {}', + astInfo: { + nodeType: 'ClassDeclaration', + property: 'superClass', + }, + }, + import: { + version: 6, + example: 'import * as mod from "mod";', + astInfo: { + nodeType: 'ImportDeclaration', + }, + }, + export: { + version: 6, + example: 'export default x;', + astInfo: { + nodeType: 'ExportDeclaration', + }, + }, + ArrowFunctions: { + version: 6, + example: 'const fn = () => {};', + astInfo: { + nodeType: 'ArrowFunctionExpression', + }, + }, + TemplateLiterals: { + version: 6, + example: 'const str = `Hello, ${name}!`;', + astInfo: { + nodeType: 'TemplateLiteral', + }, + }, + Destructuring: { + version: 6, + example: 'const { x } = obj;', + astInfo: { + nodeType: 'ObjectPattern', + }, + }, + DefaultParams: { + version: 6, + example: 'function foo(x=10) {}', + astInfo: { + nodeType: 'AssignmentPattern', + }, + }, + RestSpread: { + version: 6, + example: 'function(...args) {}', + astInfo: { + nodeType: 'RestElement', + }, + }, + ForOf: { + version: 6, + example: 'for (const x of iterable) {}', + astInfo: { + nodeType: 'ForOfStatement', + }, + }, + Map: { + version: 6, + example: 'new Map()', + astInfo: { + nodeType: 'NewExpression', + callee: 'Map', + }, + }, + Set: { + version: 6, + example: 'new Set()', + astInfo: { + nodeType: 'NewExpression', + callee: 'Set', + }, + }, + WeakMap: { + version: 6, + example: 'new WeakMap()', + astInfo: { + nodeType: 'NewExpression', + callee: 'WeakMap', + }, + }, + WeakSet: { + version: 6, + example: 'new WeakSet()', + astInfo: { + nodeType: 'NewExpression', + callee: 'WeakSet', + }, + }, + Promise: { + version: 6, + example: 'new Promise((resolve, reject) => {})', + astInfo: { + nodeType: 'NewExpression', + callee: 'Promise', + }, + }, + Symbol: { + version: 6, + example: 'Symbol("desc")', + astInfo: { + nodeType: 'CallExpression', + callee: 'Symbol', + }, + }, + + // ---------------------------------------------------------- + // ES7 / ES2016 + // ---------------------------------------------------------- + ExponentOperator: { + version: 7, + example: 'a ** b', + astInfo: { + nodeType: 'BinaryExpression', + operator: '**', + }, + }, + ArrayPrototypeIncludes: { + version: 7, + example: 'arr.includes(x)', + astInfo: { + nodeType: 'CallExpression', + property: 'includes', + }, + }, + + // ---------------------------------------------------------- + // ES8 / ES2017 + // ---------------------------------------------------------- + AsyncAwait: { + version: 8, + example: 'async function foo() { await bar(); }', + astInfo: { + nodeType: 'AwaitExpression', + }, + }, + ObjectValues: { + version: 8, + example: 'Object.values(obj)', + astInfo: { + nodeType: 'CallExpression', + object: 'Object', + property: 'values', + }, + }, + ObjectEntries: { + version: 8, + example: 'Object.entries(obj)', + astInfo: { + nodeType: 'CallExpression', + object: 'Object', + property: 'entries', + }, + }, + StringPadStart: { + version: 8, + example: 'str.padStart(10)', + astInfo: { + nodeType: 'CallExpression', + property: 'padStart', + }, + }, + StringPadEnd: { + version: 8, + example: 'str.padEnd(10)', + astInfo: { + nodeType: 'CallExpression', + property: 'padEnd', + }, + }, + + // ---------------------------------------------------------- + // ES9 / ES2018 + // ---------------------------------------------------------- + ObjectSpread: { + version: 9, + example: 'const obj2 = { ...obj};'; + astInfo: { + nodeType: 'ObjectExpression', + childType: 'SpreadElement', + }, + }, + AsyncIteration: { + version: 9, + example: 'for await (const x of asyncIterable) {}', + astInfo: { + nodeType: 'ForAwaitStatement', + }, + }, + PromiseFinally: { + version: 9, + example: 'promise.finally(() => {})', + astInfo: { + nodeType: 'CallExpression', + property: 'finally', + }, + }, + + // ---------------------------------------------------------- + // ES10 / ES2019 + // ---------------------------------------------------------- + ArrayFlat: { + version: 10, + example: 'arr.flat()', + astInfo: { + nodeType: 'CallExpression', + property: 'flat', + }, + }, + ArrayFlatMap: { + version: 10, + example: 'arr.flatMap(x => x)', + astInfo: { + nodeType: 'CallExpression', + property: 'flatMap', + }, + }, + ObjectFromEntries: { + version: 10, + example: 'Object.fromEntries(entries)', + astInfo: { + nodeType: 'CallExpression', + object: 'Object', + property: 'fromEntries', + }, + }, + OptionalCatchBinding: { + version: 10, + example: 'try { ... } catch { ... }', + astInfo: { + nodeType: 'CatchClause', + noParam: true, + }, + }, + + // ---------------------------------------------------------- + // ES11 / ES2020 + // ---------------------------------------------------------- + BigInt: { + version: 11, + example: '123n', + astInfo: { + nodeType: 'BigIntLiteral', + }, + }, + DynamicImport: { + version: 11, + example: 'import("module.js")', + astInfo: { + nodeType: 'ImportExpression', + }, + }, + OptionalChaining: { + version: 11, + example: 'obj?.prop', + astInfo: { + nodeType: 'ChainExpression', + }, + }, + NullishCoalescing: { + version: 11, + example: 'a ?? b', + astInfo: { + nodeType: 'LogicalExpression', + operator: '??', + }, + }, + globalThis: { + version: 11, + example: 'globalThis', + astInfo: { + nodeType: 'Identifier', + name: 'globalThis', + }, + }, + PromiseAllSettled: { + version: 11, + example: 'Promise.allSettled(promises)', + astInfo: { + nodeType: 'CallExpression', + property: 'allSettled', + }, + }, + StringMatchAll: { + version: 11, + example: 'str.matchAll(regex)', + astInfo: { + nodeType: 'CallExpression', + property: 'matchAll', + }, + }, + + // ---------------------------------------------------------- + // ES12 / ES2021 + // ---------------------------------------------------------- + LogicalAssignment: { + version: 12, + example: 'x &&= y;', + astInfo: { + nodeType: 'AssignmentExpression', + operators: ['&&=', '||=', '??='], + }, + }, + NumericSeparators: { + version: 12, + example: '1_000_000', + astInfo: { + nodeType: 'NumericLiteralWithSeparator', + }, + }, + StringReplaceAll: { + version: 12, + example: 'str.replaceAll("a", "b")', + astInfo: { + nodeType: 'CallExpression', + property: 'replaceAll', + }, + }, + PromiseAny: { + version: 12, + example: 'Promise.any(promises)', + astInfo: { + nodeType: 'CallExpression', + property: 'any', + }, + }, + WeakRef: { + version: 12, + example: 'new WeakRef(obj)', + astInfo: { + nodeType: 'NewExpression', + callee: 'WeakRef', + }, + }, + FinalizationRegistry: { + version: 12, + example: 'new FinalizationRegistry(cb)', + astInfo: { + nodeType: 'NewExpression', + callee: 'FinalizationRegistry', + }, + }, + + // ---------------------------------------------------------- + // ES13 / ES2022 + // ---------------------------------------------------------- + TopLevelAwait: { + version: 13, + example: 'await foo()', + astInfo: { + nodeType: 'AwaitExpression', + topLevel: true, + }, + }, + PrivateClassFields: { + version: 13, + example: 'class MyClass { #x = 1; }', + astInfo: { + nodeType: 'PropertyDefinition', + isPrivate: true, + }, + }, + ClassStaticBlocks: { + version: 13, + example: 'class MyClass { static {} }', + astInfo: { + nodeType: 'StaticBlock', + }, + }, + ErgonomicBrandChecks: { + version: 13, + example: '#field in obj', + astInfo: { + nodeType: 'BinaryExpression', + operator: 'in', + leftIsPrivate: true, + }, + }, + ErrorCause: { + version: 13, + example: 'new Error("...", { cause: e })', + astInfo: { + nodeType: 'NewExpression', + callee: 'Error', + hasOptionsCause: true, + }, + }, + ArrayPrototypeAt: { + version: 13, + example: 'arr.at(-1)', + astInfo: { + nodeType: 'CallExpression', + property: 'at', + }, + }, + + // ---------------------------------------------------------- + // ES14 / ES2023 + // ---------------------------------------------------------- + Hashbang: { + version: 14, + example: '#!/usr/bin/env node', + astInfo: { + nodeType: 'Hashbang', + }, + }, + ArrayToReversed: { + version: 14, + example: 'arr.toReversed()', + astInfo: { + nodeType: 'CallExpression', + property: 'toReversed', + }, + }, + ArrayToSorted: { + version: 14, + example: 'arr.toSorted(compareFn)', + astInfo: { + nodeType: 'CallExpression', + property: 'toSorted', + }, + }, + ArrayToSpliced: { + version: 14, + example: 'arr.toSpliced(start, deleteCount, ...)', + astInfo: { + nodeType: 'CallExpression', + property: 'toSpliced', + }, + }, + ArrayWith: { + version: 14, + example: 'arr.with(index, value)', + astInfo: { + nodeType: 'CallExpression', + property: 'with', + }, + }, +}; + +module.exports = ES_FEATURES; diff --git a/index.js b/index.js index 2a651e5..cc6dcf3 100755 --- a/index.js +++ b/index.js @@ -29,18 +29,19 @@ program ) .argument('[files...]', 'a glob of files to to test the EcmaScript version against') .option('--module', 'use ES modules') - .option('--allow-hash-bang', '--allowHashBang', 'if the code starts with #! treat it as a comment', false) + .option('--allow-hash-bang, --allowHashBang', 'if the code starts with #! treat it as a comment', false) .option('--files ', 'a glob of files to to test the EcmaScript version against (alias for [files...])') .option('--not ', 'folder or file names to skip') - .option('--no-color', '--noColor', 'disable use of colors in output', false) + .option('--no-color, --noColor', 'disable use of colors in output', false) .option('-v, --verbose', 'verbose mode: will also output debug messages', false) .option('--quiet', 'quiet mode: only displays warn and error messages', false) .option('--looseGlobMatching', 'doesn\'t fail if no files are found in some globs/files', false) .option( '--silent', - 'silent mode: does not output anything, giving no indication of success or failure other than the exit code', + 'silent mode: does not output anything, giving no indication of success or failure other than the exit code', false ) .action((ecmaVersionArg, filesArg, options) => { + console.log({ options, ecmaVersionArg, filesArg }); const noColor = options?.noColor || options?.['no-color'] || false; const logger = winston.createLogger() logger.add( @@ -202,6 +203,7 @@ program const code = fs.readFileSync(file, 'utf8') logger.debug(`ES-Check: checking ${file}`) try { + console.log({ code, acornOpts }); acorn.parse(code, acornOpts) } catch (err) { logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${err}`) diff --git a/package.json b/package.json index eeb2209..180fc5f 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ }, "dependencies": { "acorn": "8.14.0", + "acorn-walk": "^8.3.4", "commander": "12.1.0", "fast-glob": "^3.3.2", "supports-color": "8.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad26da4..0ce84ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: acorn: specifier: 8.14.0 version: 8.14.0 + acorn-walk: + specifier: ^8.3.4 + version: 8.3.4 commander: specifier: 12.1.0 version: 12.1.0 @@ -492,6 +495,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + acorn@8.14.0: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} @@ -3346,6 +3353,10 @@ snapshots: dependencies: acorn: 8.14.0 + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + acorn@8.14.0: {} add-stream@1.0.0: {} diff --git a/test.js b/test.js index b450105..04e7200 100644 --- a/test.js +++ b/test.js @@ -233,3 +233,25 @@ describe('Es Check supports the --files flag', () => { }) }) }) + +describe('Es Check supports the es2018 flag', () => { + it('๐ŸŽ‰ Es Check should pass when checking a file with es2018 syntax as es2018', (done) => { + exec('node index.js es2018 ./tests/es2018.js', (err, stdout, stderr) => { + if (err) { + console.error(err.stack) + console.error(stdout.toString()) + console.error(stderr.toString()) + done(err) + return + } + done() + }) + }) + it.only('๐Ÿ‘Œ Es Check should fail when versions belows es2018 use version es2018+ features', (done) => { + exec('node index.js es6 ./tests/es2018.js -v', (err, stdout, stderr) => { + console.log({ err, stdout, stderr }) + assert(err) + done() + }) + }); +}); diff --git a/tests/es2018.js b/tests/es2018.js new file mode 100644 index 0000000..6c4017a --- /dev/null +++ b/tests/es2018.js @@ -0,0 +1,3 @@ +class Test { constructor() { this.test = 'test'; } }; +function test(...args) { console.log(args); }; +const at = [1, 2, 3].at(-1); diff --git a/walk.js b/walk.js new file mode 100644 index 0000000..fbae399 --- /dev/null +++ b/walk.js @@ -0,0 +1,71 @@ +const acorn = require('acorn'); +const walk = require('acorn-walk'); +const { ES_FEATURES } = require('./constants'); + +function detectFeatures(code, sourceType) { + const ast = acorn.parse(code, { + ecmaVersion: 'latest', + sourceType, + }); + + const foundFeatures = {}; + for (const featureName of Object.keys(ES_FEATURES)) { + foundFeatures[featureName] = false; + } + + const visitors = {}; + + for (const [featureName, { astInfo }] of Object.entries(ES_FEATURES)) { + const { nodeType } = astInfo; + + if (!visitors[nodeType]) { + visitors[nodeType] = function nodeVisitor(node) {}; + visitors[nodeType].checks = []; + } + + visitors[nodeType].checks.push({ featureName, astInfo }); + } + + for (const nodeType of Object.keys(visitors)) { + const originalVisitor = visitors[nodeType]; + + visitors[nodeType] = function(node) { + for (const { featureName, astInfo } of originalVisitor.checks) { + switch (nodeType) { + case 'VariableDeclaration': { + if (astInfo.kind && node.kind === astInfo.kind) { + foundFeatures[featureName] = true; + } + break; + } + case 'ArrowFunctionExpression': { + foundFeatures[featureName] = true; + break; + } + case 'ChainExpression': { + foundFeatures[featureName] = true; + break; + } + case 'LogicalExpression': { + if (astInfo.operator && node.operator === astInfo.operator) { + foundFeatures[featureName] = true; + } + break; + } + case 'NewExpression': { + if (astInfo.callee && node.callee.type === 'Identifier') { + if (node.callee.name === astInfo.callee) { + foundFeatures[featureName] = true; + } + } + break; + } + default: + break; + } + } + } + } + walk.simple(ast, visitors); + return foundFeatures; +} From 718363448fa73f304386313de464f17d882c4f4d Mon Sep 17 00:00:00 2001 From: Jeff Wainwright Date: Wed, 1 Jan 2025 19:00:36 -0800 Subject: [PATCH 3/4] chore: adds initial updated `detectFeatures` function --- README.md | 21 +++-- constants.js | 131 ++++++++++++++++-------------- detectFeatures.js | 87 ++++++++++++++++++++ index.js | 33 ++++++-- test.js | 4 +- tests/es6-2.js | 13 ++- utils.js | 199 ++++++++++++++++++++++++++++++++++++++++++++++ walk.js | 71 ----------------- 8 files changed, 406 insertions(+), 153 deletions(-) create mode 100644 detectFeatures.js create mode 100644 utils.js delete mode 100644 walk.js diff --git a/README.md b/README.md index 82b985c..231606a 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,13 @@ Ensuring that JavaScript files can pass ES Check is important in a [modular and --- -## Version 7 ๐ŸŽ‰ +## Version 8 ๐ŸŽ‰ -Thanks to the efforts of [Anders Kaseorg](https://github.com/andersk), ES Check has switched to [Commander](https://www.npmjs.com/package/commander)! There appears to be no breaking issues but this update is being published as a major release for your ease-of-use. Please reach out with observations or pull requests features/fixes! +**ES Check** version 8 is a major release update that can enforce actual ES version specific features checks; no longer just that a files are syntatically correct to the es version. To enable this feature, just pass the `--checkFeatures` flag. This feature will become default in version 9. Besides this, there are minor feature updates based on user feedbackโ€”glob matching updates and some optional fixes. -This update was made for security purposesโ€”dependencies not being maintained. - -Thanks to Anders for this deeper fix, to [Pavel Starosek](https://github.com/StudioMaX) for the initial issue and support, and to [Alexander Pepper](https://github.com/apepper) for digging into this issue more! +```sh +es-check es6 './dist/**/*.js' --checkFeatures +``` --- @@ -160,6 +160,13 @@ Arguments: โš ๏ธ **NOTE:** This is primarily intended as a way to override the `files` setting in the `.escheckrc` file for specific invocations. Setting both the `[...files]` argument and `--files` flag is an error. +**Check Features** + + +```sh +es-check es6 './dist/**/*.js' --checkFeatures +``` + ### Global Options ```sh @@ -174,7 +181,9 @@ Options: -v, --verbose verbose mode: will also output debug messages (default: false) --quiet quiet mode: only displays warn and error messages (default: false) --looseGlobMatching doesn't fail if no files are found in some globs/files (default: false) - --silent silent mode: does not output anything, giving no indication of success or failure other than the exit code (default: false) + --silent silent mode: does not output anything, giving no indication of success or + failure other than the exit code (default: false) + --checkFeatures check for actual ES version specific features (default: false) -h, --help display help for command ``` diff --git a/constants.js b/constants.js index eaf40c9..6261813 100644 --- a/constants.js +++ b/constants.js @@ -1,5 +1,5 @@ /** - * Map of ES features (by name) โ†’ earliest ES version + AST detection hints + * Map of ES features (by name) โ†’ earliest ES minVersion + AST detection hints * ES6 (2015) = 6, ES7 (2016) = 7, ES8 (2017) = 8, ES9 (2018) = 9, * ES10 (2019) = 10, ES11 (2020) = 11, ES12 (2021) = 12, * ES13 (2022) = 13, ES14 (2023) = 14, etc. @@ -9,7 +9,7 @@ const ES_FEATURES = { // ES6 / ES2015 // ---------------------------------------------------------- ArraySpread: { - version: 6, + minVersion: 6, example: '[...arr]', astInfo: { nodeType: 'ArrayExpression', @@ -17,7 +17,7 @@ const ES_FEATURES = { }, }, let: { - version: 6, + minVersion: 6, example: 'let x = 10;', astInfo: { nodeType: 'VariableDeclaration', @@ -25,7 +25,7 @@ const ES_FEATURES = { }, }, const: { - version: 6, + minVersion: 6, example: 'const x = 10;', astInfo: { nodeType: 'VariableDeclaration', @@ -33,14 +33,14 @@ const ES_FEATURES = { }, }, class: { - version: 6, + minVersion: 6, example: 'class MyClass {}', astInfo: { nodeType: 'ClassDeclaration', }, }, extends: { - version: 6, + minVersion: 6, example: 'class MyClass extends OtherClass {}', astInfo: { nodeType: 'ClassDeclaration', @@ -48,63 +48,63 @@ const ES_FEATURES = { }, }, import: { - version: 6, + minVersion: 6, example: 'import * as mod from "mod";', astInfo: { nodeType: 'ImportDeclaration', }, }, export: { - version: 6, + minVersion: 6, example: 'export default x;', astInfo: { nodeType: 'ExportDeclaration', }, }, ArrowFunctions: { - version: 6, + minVersion: 6, example: 'const fn = () => {};', astInfo: { nodeType: 'ArrowFunctionExpression', }, }, TemplateLiterals: { - version: 6, + minVersion: 6, example: 'const str = `Hello, ${name}!`;', astInfo: { nodeType: 'TemplateLiteral', }, }, Destructuring: { - version: 6, + minVersion: 6, example: 'const { x } = obj;', astInfo: { nodeType: 'ObjectPattern', }, }, DefaultParams: { - version: 6, + minVersion: 6, example: 'function foo(x=10) {}', astInfo: { nodeType: 'AssignmentPattern', }, }, RestSpread: { - version: 6, + minVersion: 6, example: 'function(...args) {}', astInfo: { nodeType: 'RestElement', }, }, ForOf: { - version: 6, + minVersion: 6, example: 'for (const x of iterable) {}', astInfo: { nodeType: 'ForOfStatement', }, }, Map: { - version: 6, + minVersion: 6, example: 'new Map()', astInfo: { nodeType: 'NewExpression', @@ -112,7 +112,7 @@ const ES_FEATURES = { }, }, Set: { - version: 6, + minVersion: 6, example: 'new Set()', astInfo: { nodeType: 'NewExpression', @@ -120,7 +120,7 @@ const ES_FEATURES = { }, }, WeakMap: { - version: 6, + minVersion: 6, example: 'new WeakMap()', astInfo: { nodeType: 'NewExpression', @@ -128,7 +128,7 @@ const ES_FEATURES = { }, }, WeakSet: { - version: 6, + minVersion: 6, example: 'new WeakSet()', astInfo: { nodeType: 'NewExpression', @@ -136,7 +136,7 @@ const ES_FEATURES = { }, }, Promise: { - version: 6, + minVersion: 6, example: 'new Promise((resolve, reject) => {})', astInfo: { nodeType: 'NewExpression', @@ -144,7 +144,7 @@ const ES_FEATURES = { }, }, Symbol: { - version: 6, + minVersion: 6, example: 'Symbol("desc")', astInfo: { nodeType: 'CallExpression', @@ -156,7 +156,7 @@ const ES_FEATURES = { // ES7 / ES2016 // ---------------------------------------------------------- ExponentOperator: { - version: 7, + minVersion: 7, example: 'a ** b', astInfo: { nodeType: 'BinaryExpression', @@ -164,7 +164,7 @@ const ES_FEATURES = { }, }, ArrayPrototypeIncludes: { - version: 7, + minVersion: 7, example: 'arr.includes(x)', astInfo: { nodeType: 'CallExpression', @@ -176,14 +176,14 @@ const ES_FEATURES = { // ES8 / ES2017 // ---------------------------------------------------------- AsyncAwait: { - version: 8, + minVersion: 8, example: 'async function foo() { await bar(); }', astInfo: { nodeType: 'AwaitExpression', }, }, ObjectValues: { - version: 8, + minVersion: 8, example: 'Object.values(obj)', astInfo: { nodeType: 'CallExpression', @@ -192,7 +192,7 @@ const ES_FEATURES = { }, }, ObjectEntries: { - version: 8, + minVersion: 8, example: 'Object.entries(obj)', astInfo: { nodeType: 'CallExpression', @@ -201,7 +201,7 @@ const ES_FEATURES = { }, }, StringPadStart: { - version: 8, + minVersion: 8, example: 'str.padStart(10)', astInfo: { nodeType: 'CallExpression', @@ -209,7 +209,7 @@ const ES_FEATURES = { }, }, StringPadEnd: { - version: 8, + minVersion: 8, example: 'str.padEnd(10)', astInfo: { nodeType: 'CallExpression', @@ -221,22 +221,22 @@ const ES_FEATURES = { // ES9 / ES2018 // ---------------------------------------------------------- ObjectSpread: { - version: 9, - example: 'const obj2 = { ...obj};'; + minVersion: 9, + example: 'const obj2 = { ...obj};', astInfo: { nodeType: 'ObjectExpression', childType: 'SpreadElement', }, }, AsyncIteration: { - version: 9, + minVersion: 9, example: 'for await (const x of asyncIterable) {}', astInfo: { nodeType: 'ForAwaitStatement', }, }, PromiseFinally: { - version: 9, + minVersion: 9, example: 'promise.finally(() => {})', astInfo: { nodeType: 'CallExpression', @@ -248,7 +248,7 @@ const ES_FEATURES = { // ES10 / ES2019 // ---------------------------------------------------------- ArrayFlat: { - version: 10, + minVersion: 10, example: 'arr.flat()', astInfo: { nodeType: 'CallExpression', @@ -256,7 +256,7 @@ const ES_FEATURES = { }, }, ArrayFlatMap: { - version: 10, + minVersion: 10, example: 'arr.flatMap(x => x)', astInfo: { nodeType: 'CallExpression', @@ -264,7 +264,7 @@ const ES_FEATURES = { }, }, ObjectFromEntries: { - version: 10, + minVersion: 10, example: 'Object.fromEntries(entries)', astInfo: { nodeType: 'CallExpression', @@ -273,7 +273,7 @@ const ES_FEATURES = { }, }, OptionalCatchBinding: { - version: 10, + minVersion: 10, example: 'try { ... } catch { ... }', astInfo: { nodeType: 'CatchClause', @@ -285,28 +285,28 @@ const ES_FEATURES = { // ES11 / ES2020 // ---------------------------------------------------------- BigInt: { - version: 11, + minVersion: 11, example: '123n', astInfo: { nodeType: 'BigIntLiteral', }, }, DynamicImport: { - version: 11, + minVersion: 11, example: 'import("module.js")', astInfo: { nodeType: 'ImportExpression', }, }, OptionalChaining: { - version: 11, + minVersion: 11, example: 'obj?.prop', astInfo: { nodeType: 'ChainExpression', }, }, NullishCoalescing: { - version: 11, + minVersion: 11, example: 'a ?? b', astInfo: { nodeType: 'LogicalExpression', @@ -314,7 +314,7 @@ const ES_FEATURES = { }, }, globalThis: { - version: 11, + minVersion: 11, example: 'globalThis', astInfo: { nodeType: 'Identifier', @@ -322,7 +322,7 @@ const ES_FEATURES = { }, }, PromiseAllSettled: { - version: 11, + minVersion: 11, example: 'Promise.allSettled(promises)', astInfo: { nodeType: 'CallExpression', @@ -330,7 +330,7 @@ const ES_FEATURES = { }, }, StringMatchAll: { - version: 11, + minVersion: 11, example: 'str.matchAll(regex)', astInfo: { nodeType: 'CallExpression', @@ -342,7 +342,7 @@ const ES_FEATURES = { // ES12 / ES2021 // ---------------------------------------------------------- LogicalAssignment: { - version: 12, + minVersion: 12, example: 'x &&= y;', astInfo: { nodeType: 'AssignmentExpression', @@ -350,14 +350,14 @@ const ES_FEATURES = { }, }, NumericSeparators: { - version: 12, + minVersion: 12, example: '1_000_000', astInfo: { nodeType: 'NumericLiteralWithSeparator', }, }, StringReplaceAll: { - version: 12, + minVersion: 12, example: 'str.replaceAll("a", "b")', astInfo: { nodeType: 'CallExpression', @@ -365,7 +365,7 @@ const ES_FEATURES = { }, }, PromiseAny: { - version: 12, + minVersion: 12, example: 'Promise.any(promises)', astInfo: { nodeType: 'CallExpression', @@ -373,7 +373,7 @@ const ES_FEATURES = { }, }, WeakRef: { - version: 12, + minVersion: 12, example: 'new WeakRef(obj)', astInfo: { nodeType: 'NewExpression', @@ -381,7 +381,7 @@ const ES_FEATURES = { }, }, FinalizationRegistry: { - version: 12, + minVersion: 12, example: 'new FinalizationRegistry(cb)', astInfo: { nodeType: 'NewExpression', @@ -393,7 +393,7 @@ const ES_FEATURES = { // ES13 / ES2022 // ---------------------------------------------------------- TopLevelAwait: { - version: 13, + minVersion: 13, example: 'await foo()', astInfo: { nodeType: 'AwaitExpression', @@ -401,7 +401,7 @@ const ES_FEATURES = { }, }, PrivateClassFields: { - version: 13, + minVersion: 13, example: 'class MyClass { #x = 1; }', astInfo: { nodeType: 'PropertyDefinition', @@ -409,14 +409,14 @@ const ES_FEATURES = { }, }, ClassStaticBlocks: { - version: 13, + minVersion: 13, example: 'class MyClass { static {} }', astInfo: { nodeType: 'StaticBlock', }, }, ErgonomicBrandChecks: { - version: 13, + minVersion: 13, example: '#field in obj', astInfo: { nodeType: 'BinaryExpression', @@ -425,7 +425,7 @@ const ES_FEATURES = { }, }, ErrorCause: { - version: 13, + minVersion: 13, example: 'new Error("...", { cause: e })', astInfo: { nodeType: 'NewExpression', @@ -434,7 +434,7 @@ const ES_FEATURES = { }, }, ArrayPrototypeAt: { - version: 13, + minVersion: 13, example: 'arr.at(-1)', astInfo: { nodeType: 'CallExpression', @@ -446,14 +446,14 @@ const ES_FEATURES = { // ES14 / ES2023 // ---------------------------------------------------------- Hashbang: { - version: 14, + minVersion: 14, example: '#!/usr/bin/env node', astInfo: { nodeType: 'Hashbang', }, }, ArrayToReversed: { - version: 14, + minVersion: 14, example: 'arr.toReversed()', astInfo: { nodeType: 'CallExpression', @@ -461,7 +461,7 @@ const ES_FEATURES = { }, }, ArrayToSorted: { - version: 14, + minVersion: 14, example: 'arr.toSorted(compareFn)', astInfo: { nodeType: 'CallExpression', @@ -469,7 +469,7 @@ const ES_FEATURES = { }, }, ArrayToSpliced: { - version: 14, + minVersion: 14, example: 'arr.toSpliced(start, deleteCount, ...)', astInfo: { nodeType: 'CallExpression', @@ -477,7 +477,7 @@ const ES_FEATURES = { }, }, ArrayWith: { - version: 14, + minVersion: 14, example: 'arr.with(index, value)', astInfo: { nodeType: 'CallExpression', @@ -486,4 +486,15 @@ const ES_FEATURES = { }, }; -module.exports = ES_FEATURES; +const NODE_TYPES = { + VARIABLE_DECLARATION: 'VariableDeclaration', + ARROW_FUNCTION_EXPRESSION: 'ArrowFunctionExpression', + CHAIN_EXPRESSION: 'ChainExpression', + LOGICAL_EXPRESSION: 'LogicalExpression', + NEW_EXPRESSION: 'NewExpression', +}; + +module.exports = { + ES_FEATURES, + NODE_TYPES, +}; diff --git a/detectFeatures.js b/detectFeatures.js new file mode 100644 index 0000000..c9227fa --- /dev/null +++ b/detectFeatures.js @@ -0,0 +1,87 @@ +const acorn = require('acorn'); +const walk = require('acorn-walk'); +const { ES_FEATURES } = require('./constants'); +const { checkMap } = require('./utils'); + +const detectFeatures = (code, ecmaVersion, sourceType) => { + const ast = acorn.parse(code, { + ecmaVersion: 'latest', + sourceType, + }); + + /** + * @note Flatten all checks + */ + const allChecks = Object.entries(ES_FEATURES).map(([featureName, { astInfo }]) => ({ + featureName, + nodeType: astInfo.nodeType, + astInfo, + })); + + /** + * @note A universal visitor for any node type: + * - Filters checks that match the current nodeโ€™s type + * - Calls the relevant checker function + * - If true => mark the feature as found + */ + const foundFeatures = Object.keys(ES_FEATURES).reduce((acc, f) => { + acc[f] = false; + return acc; + }, {}); + + const universalVisitor = (node) => { + allChecks + .filter(({ nodeType }) => nodeType === node.type) + .forEach(({ featureName, astInfo }) => { + const checker = checkMap[node.type] || checkMap.default; + if (checker(node, astInfo)) { + foundFeatures[featureName] = true; + } + }); + }; + + console.log({ foundFeatures, ecmaVersion }); + + /** + * @note Build the visitors object for acorn-walk. + * Each unique nodeType gets the same universalVisitor. + */ + const nodeTypes = [...new Set(allChecks.map((c) => c.nodeType))]; + const visitors = nodeTypes.reduce((acc, nt) => { + acc[nt] = universalVisitor; + return acc; + }, {}); + + walk.simple(ast, visitors); + + /** + * @note Check if any found feature requires a higher version than requested. + * We assume each entry in ES_FEATURES has a `minVersion` property. + */ + const unsupportedFeatures = Object.entries(ES_FEATURES).reduce((acc = [], [featureName, { minVersion }]) => { + // If feature is used but requires a newer version than ecmaVersion, it's unsupported + if (foundFeatures[featureName] && minVersion > ecmaVersion) { + acc.push(featureName); + } + return acc; + }, []); + + console.log({ unsupportedFeatures }); + + /** + * @note Fail if any unsupported features were used. + */ + if (unsupportedFeatures.length > 0) { + throw new Error( + `Unsupported features detected: ${unsupportedFeatures.join(', ')}. ` + + `These require a higher ES version than ${ecmaVersion}.` + ); + } + + return { + foundFeatures, + unsupportedFeatures, + } +}; + +module.exports = detectFeatures; diff --git a/index.js b/index.js index cc6dcf3..20097c0 100755 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const fs = require('fs') const path = require('path') const supportsColor = require('supports-color') const winston = require('winston') - +const detectFeatures = require('./detectFeatures') const pkg = require('./package.json') /** @@ -36,6 +36,7 @@ program .option('-v, --verbose', 'verbose mode: will also output debug messages', false) .option('--quiet', 'quiet mode: only displays warn and error messages', false) .option('--looseGlobMatching', 'doesn\'t fail if no files are found in some globs/files', false) + .option('--checkFeatures', 'check features of es version', false) .option( '--silent', 'silent mode: does not output anything, giving no indication of success or failure other than the exit code', false @@ -76,6 +77,7 @@ program const allowHashBang = options.allowHashBang || options['allow-hash-bang'] || config.allowHashBang const pathsToIgnore = options.not ? options.not.split(',') : [].concat(config.not || []) const looseGlobMatching = options.looseGlobMatching || options?.['loose-glob-matching'] || config.looseGlobMatching || false + const checkFeatures = options.checkFeatures || config.checkFeatures || false if (!expectedEcmaVersion) { logger.error( @@ -145,19 +147,19 @@ program break case 'es11': case 'es2020': - ecmaVersion = '2020' + ecmaVersion = '11' break case 'es12': case 'es2021': - ecmaVersion = '2021' + ecmaVersion = '12' break case 'es13': case 'es2022': - ecmaVersion = '2022' + ecmaVersion = '13' break case 'es14': case 'es2023': - ecmaVersion = '2023' + ecmaVersion = '14' break default: logger.error('Invalid ecmaScript version, please pass a valid version, use --help for help') @@ -212,7 +214,26 @@ program stack: err.stack, file, } - errArray.push(errorObj) + errArray.push(errorObj); + return; + } + + if (!checkFeatures) return; + const parseSourceType = acornOpts.sourceType || 'script'; + const esVersion = parseInt(ecmaVersion, 10); + const { foundFeatures, unsupportedFeatures } = detectFeatures(code, esVersion, parseSourceType); + const stringifiedFeatures = JSON.stringify(foundFeatures, null, 2); + logger.debug(`Features found in ${file}: ${stringifiedFeatures}`); + const isSupported = unsupportedFeatures.length === 0; + if (!isSupported) { + logger.error( + `ES-Check: The file "${file}" uses these unsupported features: ${unsupportedFeatures.join(', ')} + but your target is ES${ecmaVersion}.` + ); + errArray.push({ + err: new Error(`Unsupported features used: ${unsupportedFeatures.join(', ')}`), + file, + }); } }) diff --git a/test.js b/test.js index 04e7200..b984f0d 100644 --- a/test.js +++ b/test.js @@ -236,7 +236,7 @@ describe('Es Check supports the --files flag', () => { describe('Es Check supports the es2018 flag', () => { it('๐ŸŽ‰ Es Check should pass when checking a file with es2018 syntax as es2018', (done) => { - exec('node index.js es2018 ./tests/es2018.js', (err, stdout, stderr) => { + exec('node index.js es2018 ./tests/es2018.js --checkFeatures', (err, stdout, stderr) => { if (err) { console.error(err.stack) console.error(stdout.toString()) @@ -248,7 +248,7 @@ describe('Es Check supports the es2018 flag', () => { }) }) it.only('๐Ÿ‘Œ Es Check should fail when versions belows es2018 use version es2018+ features', (done) => { - exec('node index.js es6 ./tests/es2018.js -v', (err, stdout, stderr) => { + exec('node index.js es6 ./tests/es2018.js --checkFeatures', (err, stdout, stderr) => { console.log({ err, stdout, stderr }) assert(err) done() diff --git a/tests/es6-2.js b/tests/es6-2.js index 9325029..2114390 100644 --- a/tests/es6-2.js +++ b/tests/es6-2.js @@ -1,8 +1,5 @@ -const afunc = () => {}; -class Rectangle { - constructor(height, width) { - this.height = height; - this.width = width; - } -} -const square = new Rectangle(10, 10); +const arrowFn = () => { + let msg = 'Hello from arrow function in ES6'; + console.log(msg); +}; +arrowFn(); diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..aa18d1a --- /dev/null +++ b/utils.js @@ -0,0 +1,199 @@ +// utils.js + +/** + * Checks if node.kind === astInfo.kind (e.g., 'const', 'let'). + */ +function checkVarKindMatch(node, astInfo) { + if (!astInfo.kind) return false; + return node.kind === astInfo.kind; +} + +/** + * Checks if a NewExpression node's callee is an Identifier + * that matches astInfo.callee (e.g. "Promise", "WeakRef"). + */ +function checkCalleeMatch(node, astInfo) { + if (!astInfo.callee) return false; + // e.g. node.callee.type === 'Identifier' && node.callee.name === 'Promise' + if (!node.callee || node.callee.type !== 'Identifier') return false; + return node.callee.name === astInfo.callee; +} + +/** + * Checks if a LogicalExpression node's operator matches astInfo.operator (e.g., '??'). + */ +function checkOperatorMatch(node, astInfo) { + if (!astInfo.operator) return false; + return node.operator === astInfo.operator; +} + +/** + * For simple presence-based checks (e.g., ArrowFunctionExpression). + */ +function checkPresence() { + return true; +} + +/** + * A more "universal" check for a CallExpression, used for many ES features: + * - arrayMethod => property: 'flat', 'includes', 'at', etc. + * - objectMethod => object: 'Object', property: 'fromEntries', etc. + */ +function checkCallExpression(node, astInfo) { + // Must be `CallExpression` + if (node.type !== 'CallExpression') return false; + + // We might check if node.callee is a MemberExpression, e.g. array.includes(...) + // or if node.callee is an Identifier, e.g. Symbol(...). + if (node.callee.type === 'MemberExpression') { + const { object, property } = astInfo; + // e.g. object: 'Object', property: 'entries' + // => node.callee.object.name === 'Object' && node.callee.property.name === 'entries' + if (object) { + // Make sure node.callee.object is an Identifier with correct name + if ( + !node.callee.object || + node.callee.object.type !== 'Identifier' || + node.callee.object.name !== object + ) { + return false; + } + } + if (property) { + // e.g. property: 'includes' + if (!node.callee.property || node.callee.property.name !== property) { + return false; + } + } + return true; + } else if (node.callee.type === 'Identifier') { + // e.g. Symbol("desc") + const { callee } = astInfo; + // If astInfo.callee is "Symbol", check node.callee.name + if (callee && node.callee.name === callee) { + return true; + } + } + + return false; +} + +/** + * Check ObjectExpression for childType, e.g. 'SpreadElement' + */ +function checkObjectExpression(node, astInfo) { + // If we want to detect object spread, we might check if node.properties + // contain a SpreadElement + if (astInfo.childType === 'SpreadElement') { + return node.properties.some((p) => p.type === 'SpreadElement'); + } + return false; +} + +/** + * Check ClassDeclaration presence or superClass usage + */ +function checkClassDeclaration(node, astInfo) { + // Just having a ClassDeclaration means classes are used. + // If astInfo has `property: 'superClass'`, it means "extends" usage + if (astInfo.property === 'superClass') { + return !!node.superClass; // if superClass is not null, "extends" is used + } + return true; // default: any ClassDeclaration means the feature is used +} + +/** + * Example check for BinaryExpression (e.g., exponent operator `**`). + */ +function checkBinaryExpression(node, astInfo) { + if (!astInfo.operator) return false; + return node.operator === astInfo.operator; +} + +/** + * Example check for ForAwaitStatement + */ +function checkForAwaitStatement(node) { + // If we see a ForAwaitStatement at all, it's used (ES2018 async iteration) + return true; +} + +/** + * Example check for CatchClause with no param => optional catch binding + */ +function checkCatchClause(node, astInfo) { + if (astInfo.noParam) { + // ES2019 optional catch binding => catch {} + return !node.param; + } + return false; +} + +/** + * Example check for BigIntLiteral or numeric with underscore + * (Acorn might parse BigInt as node.type === 'Literal' with a bigint property) + */ +function checkBigIntLiteral(node) { + // Some Acorn versions: node.type === 'Literal' && typeof node.value === 'bigint' + // Others: node.type === 'BigIntLiteral' + // Adjust for your parserโ€™s shape. We'll do a basic check: + if (typeof node.value === 'bigint') { + return true; + } + return false; +} + +/** + * A single "catch-all" object mapping node types to specialized checkers + */ +const checkMap = { + // Existing from your snippet: + VariableDeclaration: (node, astInfo) => checkVarKindMatch(node, astInfo), + ArrowFunctionExpression: () => checkPresence(), + ChainExpression: () => checkPresence(), + LogicalExpression: (node, astInfo) => checkOperatorMatch(node, astInfo), + NewExpression: (node, astInfo) => checkCalleeMatch(node, astInfo), + + // ** Added Node Types ** + + // For "CallExpression": .includes, .flat, .at, etc. + CallExpression: (node, astInfo) => checkCallExpression(node, astInfo), + + // For "ObjectExpression": object spread + ObjectExpression: (node, astInfo) => checkObjectExpression(node, astInfo), + + // For "ClassDeclaration": classes, extends + ClassDeclaration: (node, astInfo) => checkClassDeclaration(node, astInfo), + + // For "BinaryExpression": exponent operator, etc. + BinaryExpression: (node, astInfo) => checkBinaryExpression(node, astInfo), + + // For "ForAwaitStatement": async iteration + ForAwaitStatement: (node) => checkForAwaitStatement(node), + + // For "CatchClause": optional catch binding + CatchClause: (node, astInfo) => checkCatchClause(node, astInfo), + + // For "Literal": numeric separators or bigints (depending on your parser) + // If your parser uses node.raw.includes('_'), it might detect numeric separators. + // For BigInt, you might check `typeof node.value === 'bigint'`. + Literal: (node, astInfo) => { + if (astInfo.nodeType === 'BigIntLiteral') { + return checkBigIntLiteral(node); + } + // or if checking numeric separators + // if (astInfo.nodeType === 'NumericLiteralWithSeparator' && node.raw.includes('_')) ... + return false; + }, + + // Provide a default if the nodeType is not in this map + default: () => false, +}; + +module.exports = { + checkVarKindMatch, + checkCalleeMatch, + checkOperatorMatch, + checkPresence, + checkMap, +}; diff --git a/walk.js b/walk.js deleted file mode 100644 index fbae399..0000000 --- a/walk.js +++ /dev/null @@ -1,71 +0,0 @@ -const acorn = require('acorn'); -const walk = require('acorn-walk'); -const { ES_FEATURES } = require('./constants'); - -function detectFeatures(code, sourceType) { - const ast = acorn.parse(code, { - ecmaVersion: 'latest', - sourceType, - }); - - const foundFeatures = {}; - for (const featureName of Object.keys(ES_FEATURES)) { - foundFeatures[featureName] = false; - } - - const visitors = {}; - - for (const [featureName, { astInfo }] of Object.entries(ES_FEATURES)) { - const { nodeType } = astInfo; - - if (!visitors[nodeType]) { - visitors[nodeType] = function nodeVisitor(node) {}; - visitors[nodeType].checks = []; - } - - visitors[nodeType].checks.push({ featureName, astInfo }); - } - - for (const nodeType of Object.keys(visitors)) { - const originalVisitor = visitors[nodeType]; - - visitors[nodeType] = function(node) { - for (const { featureName, astInfo } of originalVisitor.checks) { - switch (nodeType) { - case 'VariableDeclaration': { - if (astInfo.kind && node.kind === astInfo.kind) { - foundFeatures[featureName] = true; - } - break; - } - case 'ArrowFunctionExpression': { - foundFeatures[featureName] = true; - break; - } - case 'ChainExpression': { - foundFeatures[featureName] = true; - break; - } - case 'LogicalExpression': { - if (astInfo.operator && node.operator === astInfo.operator) { - foundFeatures[featureName] = true; - } - break; - } - case 'NewExpression': { - if (astInfo.callee && node.callee.type === 'Identifier') { - if (node.callee.name === astInfo.callee) { - foundFeatures[featureName] = true; - } - } - break; - } - default: - break; - } - } - } - } - walk.simple(ast, visitors); - return foundFeatures; -} From 87326710848771c80cae1bcfeae3ceb57089485a Mon Sep 17 00:00:00 2001 From: Jeff Wainwright Date: Wed, 1 Jan 2025 21:15:33 -0800 Subject: [PATCH 4/4] chore: adds initial detectFeature tests --- README.md | 19 ++++----- detectFeatures.js | 4 -- index.js | 2 - test.js | 98 ++++++++++++++++++++++++++++++++++++++++++++--- tests/es10.js | 11 ++++++ tests/es11.js | 5 +++ tests/es12.js | 3 ++ tests/es2018.js | 3 +- tests/es7.js | 2 + tests/es8.js | 7 ++++ tests/es9.js | 4 ++ utils.js | 85 ++++++++++++---------------------------- 12 files changed, 159 insertions(+), 84 deletions(-) create mode 100644 tests/es10.js create mode 100644 tests/es11.js create mode 100644 tests/es12.js create mode 100644 tests/es7.js create mode 100644 tests/es8.js create mode 100644 tests/es9.js diff --git a/README.md b/README.md index 231606a..a989ccd 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,13 @@ ES Check is a small utility using powerful tools that [Isaac Z. Schlueter](https ## Contributing -ES Check has 3 main dependencies: [acorn](https://github.com/ternjs/acorn/), [glob](https://www.npmjs.com/package/glob), and [caporal](https://github.com/mattallty/Caporal.js). To contribute, file an [issue](https://github.com/yowainwright/es-check/issues) or submit a pull request. To setup local development, run `./bin/setup.sh` or open the devcontainer in VSCode. +ES Check has 6 dependencies: [acorn and acorn-walk](https://github.com/ternjs/acorn/), [fast-glob](https://github.com/mrmlnc/fast-glob), [supports-color](github.com/chalk/supports-color), [winston](https://github.com/winstonjs/winston), and [commander](https://github.com/tj/commander). To contribute, file an [issue](https://github.com/yowainwright/es-check/issues) or submit a pull request. + +To update es versions, check out these lines of code [here](https://github.com/yowainwright/es-check/blob/main/index.js#L92-L153) and [here (in acorn.js)](https://github.com/acornjs/acorn/blob/3221fa54f9dea30338228b97210c4f1fd332652d/acorn/src/acorn.d.ts#L586). + +To update es feature detection, update these files [here](./utils.js) and [here](./constants.js) as enabled feature testing using [acorn walk](https://github.com/acornjs/acorn/blob/master/acorn-walk/README.md). + +[tests](./test.js) to go with new version and/or feature detection updates are great to have! ### Contributors @@ -254,14 +260,3 @@ ES Check has 3 main dependencies: [acorn](https://github.com/ternjs/acorn/), [gl - [Ben Junya](https://github.com/MrBenJ) - [Jeff Barczewski](https://github.com/jeffbski) - [Brandon Casey](https://github.com/BrandonOCasey) - -### Roadmap - -- Provide compilation step to support esm - - non-user-facing - - required to keep package dependencies up-to-date as more dependencies are ESM-only -- Provide checks for _theoretical_ keywork words - - Things like `Map` and `Object.assign` are not keywords that fail ECMAScript - compilation depending on specific versions of ECMAScript. However, they hint at additions to ECMAScript that previous version did not support. - - This feature will enhance an already built-in confiration feature to provide more out-of-the-box support for ECMAScript checking. - - If enabled, this feature will warn (or fail) based on _theoretical_ ECMAScript keywords. diff --git a/detectFeatures.js b/detectFeatures.js index c9227fa..5c30f82 100644 --- a/detectFeatures.js +++ b/detectFeatures.js @@ -40,8 +40,6 @@ const detectFeatures = (code, ecmaVersion, sourceType) => { }); }; - console.log({ foundFeatures, ecmaVersion }); - /** * @note Build the visitors object for acorn-walk. * Each unique nodeType gets the same universalVisitor. @@ -66,8 +64,6 @@ const detectFeatures = (code, ecmaVersion, sourceType) => { return acc; }, []); - console.log({ unsupportedFeatures }); - /** * @note Fail if any unsupported features were used. */ diff --git a/index.js b/index.js index 20097c0..10fb6aa 100755 --- a/index.js +++ b/index.js @@ -42,7 +42,6 @@ program 'silent mode: does not output anything, giving no indication of success or failure other than the exit code', false ) .action((ecmaVersionArg, filesArg, options) => { - console.log({ options, ecmaVersionArg, filesArg }); const noColor = options?.noColor || options?.['no-color'] || false; const logger = winston.createLogger() logger.add( @@ -205,7 +204,6 @@ program const code = fs.readFileSync(file, 'utf8') logger.debug(`ES-Check: checking ${file}`) try { - console.log({ code, acornOpts }); acorn.parse(code, acornOpts) } catch (err) { logger.debug(`ES-Check: failed to parse file: ${file} \n - error: ${err}`) diff --git a/test.js b/test.js index b984f0d..c3ec499 100644 --- a/test.js +++ b/test.js @@ -38,7 +38,7 @@ it('๐Ÿ‘Œ Es Check should fail when checking an array of es6 files as es5', (don }) it('๐ŸŽ‰ Es Check should pass when checking a glob of es6 files as es6', (done) => { - exec('node index.js es6 "./tests/*.js"', (err, stdout, stderr) => { + exec('node index.js es6 "./tests/es6.js"', (err, stdout, stderr) => { if (err) { console.error(err.stack) console.error(stdout.toString()) @@ -204,7 +204,7 @@ describe('Es Check skips folders and files included in the not flag', () => { describe('Es Check supports the --files flag', () => { it('๐ŸŽ‰ Es Check should pass when checking a glob with es6 modules as es6 using the --files flag', (done) => { - exec('node index.js es6 --files=./tests/*.js', (err, stdout, stderr) => { + exec('node index.js es6 --files=./tests/es6.js', (err, stdout, stderr) => { assert(stdout) if (err) { console.error(err.stack) @@ -218,7 +218,7 @@ describe('Es Check supports the --files flag', () => { }) it('๐Ÿ‘Œ Es Check should fail when checking a glob with es6 modules as es5 using the --files flag', (done) => { - exec('node index.js es5 --files=./tests/*.js', (err, stdout, stderr) => { + exec('node index.js es5 --files=./tests/es6.js', (err, stdout, stderr) => { assert(err) console.log(stdout) done() @@ -226,7 +226,7 @@ describe('Es Check supports the --files flag', () => { }) it('๐Ÿ‘Œ Es Check should fail when given both spread files and --files flag', (done) => { - exec('node index.js es6 ./tests/*.js --files=./tests/*.js', (err, stdout, stderr) => { + exec('node index.js es6 ./tests/es6.js --files=./tests/es6.js', (err, stdout, stderr) => { assert(err) console.log(stdout) done() @@ -247,7 +247,7 @@ describe('Es Check supports the es2018 flag', () => { done() }) }) - it.only('๐Ÿ‘Œ Es Check should fail when versions belows es2018 use version es2018+ features', (done) => { + it('๐Ÿ‘Œ Es Check should fail when versions belows es2018 use version es2018+ features', (done) => { exec('node index.js es6 ./tests/es2018.js --checkFeatures', (err, stdout, stderr) => { console.log({ err, stdout, stderr }) assert(err) @@ -255,3 +255,91 @@ describe('Es Check supports the es2018 flag', () => { }) }); }); + +describe('ES7 / ES2016 Feature Tests', () => { + it('๐ŸŽ‰ Es Check should pass when checking an ES7 file as es7', (done) => { + exec('node index.js es7 ./tests/es7.js --checkFeatures', (err, stdout, stderr) => { + if (err) { + console.error(err.stack); + console.error(stdout.toString()); + console.error(stderr.toString()); + return done(err); + } + done(); + }); + }); + + it('๐Ÿ‘Œ Es Check should fail when checking an ES7 file as es6', (done) => { + exec('node index.js es6 ./tests/es7.js --checkFeatures', (err, stdout, stderr) => { + console.log(stdout); + assert(err, 'Expected an error but command ran successfully'); + done(); + }); + }); +}); + +describe('ES10 / ES2019 Feature Tests', () => { + it('๐ŸŽ‰ Es Check should pass when checking an ES10 file as es10', (done) => { + exec('node index.js es10 ./tests/es10.js --checkFeatures', (err, stdout, stderr) => { + if (err) { + console.error(err.stack); + console.error(stdout.toString()); + console.error(stderr.toString()); + return done(err); + } + done(); + }); + }); + + it('๐Ÿ‘Œ Es Check should fail when checking an ES10 file as es6', (done) => { + exec('node index.js es6 ./tests/es10.js --checkFeatures', (err, stdout, stderr) => { + console.log(stdout); + assert(err, 'Expected an error but command ran successfully'); + done(); + }); + }); +}); + +describe('ES11 / ES2020 Feature Tests', () => { + it('๐ŸŽ‰ Es Check should pass when checking an ES11 file as es11', (done) => { + exec('node index.js es11 ./tests/es11.js --checkFeatures', (err, stdout, stderr) => { + if (err) { + console.error(err.stack); + console.error(stdout.toString()); + console.error(stderr.toString()); + return done(err); + } + done(); + }); + }); + + it('๐Ÿ‘Œ Es Check should fail when checking an ES11 file as es6', (done) => { + exec('node index.js es6 ./tests/es11.js --checkFeatures', (err, stdout, stderr) => { + console.log(stdout); + assert(err, 'Expected an error but command ran successfully'); + done(); + }); + }); +}); + +describe('ES12 / ES2021 Feature Tests', () => { + it('๐ŸŽ‰ Es Check should pass when checking an ES12 file as es12', (done) => { + exec('node index.js es12 ./tests/es12.js --checkFeatures', (err, stdout, stderr) => { + if (err) { + console.error(err.stack); + console.error(stdout.toString()); + console.error(stderr.toString()); + return done(err); + } + done(); + }); + }); + + it('๐Ÿ‘Œ Es Check should fail when checking an ES12 file as es6', (done) => { + exec('node index.js es6 ./tests/es12.js --checkFeatures', (err, stdout, stderr) => { + console.log(stdout); + assert(err, 'Expected an error but command ran successfully'); + done(); + }); + }); +}); diff --git a/tests/es10.js b/tests/es10.js new file mode 100644 index 0000000..0399c2c --- /dev/null +++ b/tests/es10.js @@ -0,0 +1,11 @@ +const flatArr = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]; +const flatArr2 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]; +const flatMap = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]].flatMap((item) => item); +const objectFromEntries = Object.fromEntries([['name', 'John'], ['age', 30]]); +const testTryCatch = () => { + try { + throw Error('error'); + } catch { + console.log('error'); + } +} diff --git a/tests/es11.js b/tests/es11.js new file mode 100644 index 0000000..c8be2a6 --- /dev/null +++ b/tests/es11.js @@ -0,0 +1,5 @@ +const bigIntTest = 1234567890123456789012345678901234567890n; +console.log(bigIntTest); +const opts = { a: { b: { c: 1 } } }; +const chained = opts?.a?.b?.c; +const nullishCoalescingTest = chained ?? 'default'; diff --git a/tests/es12.js b/tests/es12.js new file mode 100644 index 0000000..cc451ee --- /dev/null +++ b/tests/es12.js @@ -0,0 +1,3 @@ +const test = 1; +const logicalAssignmentTest = test ??= 4; +const testStringReplaceAll = 'test'.replaceAll('t', 'T'); diff --git a/tests/es2018.js b/tests/es2018.js index 6c4017a..529b836 100644 --- a/tests/es2018.js +++ b/tests/es2018.js @@ -1,3 +1,4 @@ class Test { constructor() { this.test = 'test'; } }; function test(...args) { console.log(args); }; -const at = [1, 2, 3].at(-1); +const obj = { a: 1, b: 2, c: 3 }; +const objSpread = { ...obj, d: 4 }; diff --git a/tests/es7.js b/tests/es7.js new file mode 100644 index 0000000..0685f44 --- /dev/null +++ b/tests/es7.js @@ -0,0 +1,2 @@ +const test = a ** b; +const arrTest = [1, 2, 3, 4, 5].includes(3); diff --git a/tests/es8.js b/tests/es8.js new file mode 100644 index 0000000..705e054 --- /dev/null +++ b/tests/es8.js @@ -0,0 +1,7 @@ +const test = async (a, b) => await a + b; +await test(1, 2); +const objValues = Object.values({ a: 1, b: 2, c: 3 }); +const objEntries = Object.entries({ a: 1, b: 2, c: 3 }); +const padStartStr = 'abc'.padStart(10, '123'); +const padEndStr = 'abc'.padEnd(10, '123'); +export { test, objValues, objEntries, padStartStr, padEndStr }; diff --git a/tests/es9.js b/tests/es9.js new file mode 100644 index 0000000..529b836 --- /dev/null +++ b/tests/es9.js @@ -0,0 +1,4 @@ +class Test { constructor() { this.test = 'test'; } }; +function test(...args) { console.log(args); }; +const obj = { a: 1, b: 2, c: 3 }; +const objSpread = { ...obj, d: 4 }; diff --git a/utils.js b/utils.js index aa18d1a..bec0c91 100644 --- a/utils.js +++ b/utils.js @@ -1,18 +1,16 @@ -// utils.js - /** - * Checks if node.kind === astInfo.kind (e.g., 'const', 'let'). + * @note Checks if node.kind === astInfo.kind (e.g., 'const', 'let'). */ -function checkVarKindMatch(node, astInfo) { +const checkVarKindMatch = (node, astInfo) => { if (!astInfo.kind) return false; return node.kind === astInfo.kind; } /** - * Checks if a NewExpression node's callee is an Identifier + * @note Checks if a NewExpression node's callee is an Identifier * that matches astInfo.callee (e.g. "Promise", "WeakRef"). */ -function checkCalleeMatch(node, astInfo) { +const checkCalleeMatch = (node, astInfo) => { if (!astInfo.callee) return false; // e.g. node.callee.type === 'Identifier' && node.callee.name === 'Promise' if (!node.callee || node.callee.type !== 'Identifier') return false; @@ -20,26 +18,26 @@ function checkCalleeMatch(node, astInfo) { } /** - * Checks if a LogicalExpression node's operator matches astInfo.operator (e.g., '??'). + * @note Checks if a LogicalExpression node's operator matches astInfo.operator (e.g., '??'). */ -function checkOperatorMatch(node, astInfo) { +const checkOperatorMatch = (node, astInfo) =>{ if (!astInfo.operator) return false; return node.operator === astInfo.operator; } /** - * For simple presence-based checks (e.g., ArrowFunctionExpression). + * @note For simple presence-based checks (e.g., ArrowFunctionExpression). */ -function checkPresence() { +const checkDefault = () => { return true; } /** - * A more "universal" check for a CallExpression, used for many ES features: + * @note A more "universal" check for a CallExpression, used for many ES features: * - arrayMethod => property: 'flat', 'includes', 'at', etc. * - objectMethod => object: 'Object', property: 'fromEntries', etc. */ -function checkCallExpression(node, astInfo) { +const checkCallExpression = (node, astInfo) => { // Must be `CallExpression` if (node.type !== 'CallExpression') return false; @@ -50,7 +48,6 @@ function checkCallExpression(node, astInfo) { // e.g. object: 'Object', property: 'entries' // => node.callee.object.name === 'Object' && node.callee.property.name === 'entries' if (object) { - // Make sure node.callee.object is an Identifier with correct name if ( !node.callee.object || node.callee.object.type !== 'Identifier' || @@ -79,9 +76,9 @@ function checkCallExpression(node, astInfo) { } /** - * Check ObjectExpression for childType, e.g. 'SpreadElement' + * @note Check ObjectExpression for childType, e.g. 'SpreadElement' */ -function checkObjectExpression(node, astInfo) { +const checkObjectExpression = (node, astInfo) => { // If we want to detect object spread, we might check if node.properties // contain a SpreadElement if (astInfo.childType === 'SpreadElement') { @@ -91,9 +88,9 @@ function checkObjectExpression(node, astInfo) { } /** - * Check ClassDeclaration presence or superClass usage + * @note Check ClassDeclaration presence or superClass usage */ -function checkClassDeclaration(node, astInfo) { +const checkClassDeclaration = (node, astInfo) => { // Just having a ClassDeclaration means classes are used. // If astInfo has `property: 'superClass'`, it means "extends" usage if (astInfo.property === 'superClass') { @@ -103,40 +100,31 @@ function checkClassDeclaration(node, astInfo) { } /** - * Example check for BinaryExpression (e.g., exponent operator `**`). + * @note Example check for BinaryExpression (e.g., exponent operator `**`). */ -function checkBinaryExpression(node, astInfo) { +const checkBinaryExpression = (node, astInfo) => { if (!astInfo.operator) return false; return node.operator === astInfo.operator; } -/** - * Example check for ForAwaitStatement - */ -function checkForAwaitStatement(node) { - // If we see a ForAwaitStatement at all, it's used (ES2018 async iteration) +const checkForAwaitStatement = (node) => { return true; } /** - * Example check for CatchClause with no param => optional catch binding + * @note Example check for CatchClause with no param => optional catch binding */ -function checkCatchClause(node, astInfo) { +const checkCatchClause = (node, astInfo) => { if (astInfo.noParam) { - // ES2019 optional catch binding => catch {} return !node.param; } return false; } /** - * Example check for BigIntLiteral or numeric with underscore - * (Acorn might parse BigInt as node.type === 'Literal' with a bigint property) + * @note Example check for BigIntLiteral or numeric with underscore */ -function checkBigIntLiteral(node) { - // Some Acorn versions: node.type === 'Literal' && typeof node.value === 'bigint' - // Others: node.type === 'BigIntLiteral' - // Adjust for your parserโ€™s shape. We'll do a basic check: +const checkBigIntLiteral = (node) =>{ if (typeof node.value === 'bigint') { return true; } @@ -144,49 +132,26 @@ function checkBigIntLiteral(node) { } /** - * A single "catch-all" object mapping node types to specialized checkers + * @note the "catch-all" object mapping node types to specialized checkers */ const checkMap = { - // Existing from your snippet: VariableDeclaration: (node, astInfo) => checkVarKindMatch(node, astInfo), - ArrowFunctionExpression: () => checkPresence(), - ChainExpression: () => checkPresence(), + ArrowFunctionExpression: () => checkDefault(), + ChainExpression: () => checkDefault(), LogicalExpression: (node, astInfo) => checkOperatorMatch(node, astInfo), NewExpression: (node, astInfo) => checkCalleeMatch(node, astInfo), - - // ** Added Node Types ** - - // For "CallExpression": .includes, .flat, .at, etc. CallExpression: (node, astInfo) => checkCallExpression(node, astInfo), - - // For "ObjectExpression": object spread ObjectExpression: (node, astInfo) => checkObjectExpression(node, astInfo), - - // For "ClassDeclaration": classes, extends ClassDeclaration: (node, astInfo) => checkClassDeclaration(node, astInfo), - - // For "BinaryExpression": exponent operator, etc. BinaryExpression: (node, astInfo) => checkBinaryExpression(node, astInfo), - - // For "ForAwaitStatement": async iteration ForAwaitStatement: (node) => checkForAwaitStatement(node), - - // For "CatchClause": optional catch binding CatchClause: (node, astInfo) => checkCatchClause(node, astInfo), - - // For "Literal": numeric separators or bigints (depending on your parser) - // If your parser uses node.raw.includes('_'), it might detect numeric separators. - // For BigInt, you might check `typeof node.value === 'bigint'`. Literal: (node, astInfo) => { if (astInfo.nodeType === 'BigIntLiteral') { return checkBigIntLiteral(node); } - // or if checking numeric separators - // if (astInfo.nodeType === 'NumericLiteralWithSeparator' && node.raw.includes('_')) ... return false; }, - - // Provide a default if the nodeType is not in this map default: () => false, }; @@ -194,6 +159,6 @@ module.exports = { checkVarKindMatch, checkCalleeMatch, checkOperatorMatch, - checkPresence, + checkDefault, checkMap, };