From c6a385fe2e359fa36fcc0ae3e47a016f869f6a33 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Mon, 4 Mar 2024 21:25:10 +0100 Subject: [PATCH 01/14] Initial type setup --- .eslintignore | 1 + .eslintrc.js | 5 ++++- .gitignore | 1 + declaration.tsconfig.json | 11 ++++++++++ package.json | 33 +++++++++++++++++------------- rollup.config.js | 27 ------------------------ rollup.config.mjs | 33 ++++++++++++++++++++++++++++++ tsconfig.json | 43 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 declaration.tsconfig.json delete mode 100644 rollup.config.js create mode 100644 rollup.config.mjs create mode 100644 tsconfig.json diff --git a/.eslintignore b/.eslintignore index c42664f..5eea4df 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,4 @@ !.vitepress /docs/.vitepress/dist /docs/.vitepress/cache +/dist diff --git a/.eslintrc.js b/.eslintrc.js index 4483568..803c59a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,9 @@ module.exports = { root: true, extends: ["plugin:@eslint-community/mysticatea/es2020"], + parserOptions: { + project: "./tsconfig.json", + }, rules: { "@eslint-community/mysticatea/prettier": "off", "no-restricted-properties": [ @@ -18,7 +21,7 @@ module.exports = { }, overrides: [ { - files: ["src/**/*.mjs", "test/**/*.mjs"], + files: ["src/**/*.mjs", "test/**/*.mjs", "rollup.config.mjs"], extends: ["plugin:@eslint-community/mysticatea/+modules"], rules: { "init-declarations": "off", diff --git a/.gitignore b/.gitignore index 2a9fa3b..82c80fd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /node_modules /index.* /test.* +/dist diff --git a/declaration.tsconfig.json b/declaration.tsconfig.json new file mode 100644 index 0000000..657175d --- /dev/null +++ b/declaration.tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig", + "files": [], + "exclude": ["tests/**/*.js"], + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "outDir": "dist" + } +} diff --git a/package.json b/package.json index b4d8bb7..11548c7 100644 --- a/package.json +++ b/package.json @@ -18,27 +18,27 @@ "sideEffects": false, "exports": { ".": { - "import": "./index.mjs", - "require": "./index.js" - }, - "./package.json": "./package.json" + "types": "./index.d.ts", + "default": "./index.js" + } }, - "main": "index", - "module": "index.mjs", + "main": "index.js", + "types": "index.d.ts", "files": [ - "index.*" + "index.d.ts", + "index.js" ], "scripts": { - "prebuild": "npm run -s clean", - "build": "rollup -c", - "clean": "rimraf .nyc_output coverage index.*", + "prebuild": "npm run -s clean ", + "build": "tsc -p declaration.tsconfig.json && rollup -c", + "clean": "rimraf .nyc_output coverage dist index.*", "coverage": "opener ./coverage/lcov-report/index.html", "docs:build": "vitepress build docs", "docs:watch": "vitepress dev docs", "format": "npm run -s format:prettier -- --write", "format:prettier": "prettier .", "format:check": "npm run -s format:prettier -- --check", - "lint": "eslint .", + "lint": "tsc && eslint .", "test": "c8 mocha --reporter dot \"test/*.mjs\"", "preversion": "npm test && npm run -s build", "postversion": "git push && git push --tags", @@ -50,17 +50,22 @@ }, "devDependencies": { "@eslint-community/eslint-plugin-mysticatea": "^15.5.1", + "@types/eslint": "^8.21.0", + "@types/estree": "^1.0.0", + "@types/mocha": "^10.0.1", + "@types/node": "^18.19.21", "c8": "^8.0.1", "dot-prop": "^7.2.0", "eslint": "^8.50.0", "mocha": "^9.2.2", - "npm-run-all": "^4.1.5", + "npm-run-all2": "^6.1.2", "opener": "^1.5.2", "prettier": "2.8.8", "rimraf": "^3.0.2", - "rollup": "^2.79.1", - "rollup-plugin-sourcemaps": "^0.6.3", + "rollup": "^4.12.0", + "rollup-plugin-dts": "^6.1.0", "semver": "^7.5.4", + "typescript": "^5.3.3", "vitepress": "^1.0.0-rc.20", "warun": "^1.0.0" }, diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index fe078f8..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @author Toru Nagashima - * See LICENSE file in root directory for full license. - */ -import sourcemaps from "rollup-plugin-sourcemaps" -import packageInfo from "./package.json" - -/** - * Define the output configuration. - * @param {string} ext The extension for generated files. - * @returns {object} The output configuration - */ -function config(ext) { - return { - input: "src/index.mjs", - output: { - exports: ext === ".mjs" ? undefined : "named", - file: `index${ext}`, - format: ext === ".mjs" ? "es" : "cjs", - sourcemap: true, - }, - plugins: [sourcemaps()], - external: Object.keys(packageInfo.dependencies), - } -} - -export default [config(".js"), config(".mjs")] diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 0000000..47e299c --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,33 @@ +/** + * @author Toru Nagashima + * See LICENSE file in root directory for full license. + */ + +import { readFileSync } from 'node:fs'; +import { URL } from 'node:url'; + +import { dts } from "rollup-plugin-dts"; + +const packageInfo = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); + +export default [ + { + input: "src/index.mjs", + output: { + exports: "named", + file: `index.js`, + format: "cjs", + sourcemap: true, + }, + external: Object.keys(packageInfo.dependencies), + }, + { + input: 'dist/index.d.mts', + output: [{ + exports: "named", + file: `index.d.ts`, + format: "cjs", + }], + plugins: [dts()], + }, +] diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e6a02bc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,43 @@ +{ + "files": ["rollup.config.mjs"], + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["es2022"], + "target": "es2022", + + "module": "nodenext", + "moduleResolution": "nodenext", + + "strict": true, + + "skipLibCheck": false, // See https://github.com/voxpelli/tsconfig/issues/1 + + /* Clean up generated declarations */ + "removeComments": true, + "stripInternal": true, + + /* Make it a JS-targeted config */ + "allowJs": true, + "checkJs": true, + "noEmit": true, + + /* New checks being tried out */ + // "exactOptionalPropertyTypes": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + + /* Additional non-type checks */ + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + + /* To make strict checking somewhat less strict during a transition stage, add one or more of: */ + /* + "noImplicitThis": false, + "noImplicitAny": false, + "strictNullChecks": false, + */ + } +} From d24cd96d3996bb32e93e14adad7a2ed6ad7609f2 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Tue, 5 Mar 2024 13:05:00 +0100 Subject: [PATCH 02/14] Type it up --- src/find-variable.mjs | 9 +- src/get-function-head-location.mjs | 44 ++--- src/get-function-name-with-kind.mjs | 12 +- src/get-innermost-scope.mjs | 10 +- src/get-property-name.mjs | 8 +- src/get-static-value.mjs | 45 +++-- src/get-string-if-constant.mjs | 8 +- src/has-side-effect.mjs | 252 +++++++++++++++------------- src/is-parenthesized.mjs | 57 +++++-- src/pattern-matcher.mjs | 21 +-- src/reference-tracker.mjs | 170 ++++++++++++------- src/token-predicate.mjs | 28 ++-- tsconfig.json | 11 +- 13 files changed, 388 insertions(+), 287 deletions(-) diff --git a/src/find-variable.mjs b/src/find-variable.mjs index c52bf76..c935a65 100644 --- a/src/find-variable.mjs +++ b/src/find-variable.mjs @@ -2,18 +2,19 @@ import { getInnermostScope } from "./get-innermost-scope.mjs" /** * Find the variable of a given name. - * @param {Scope} initialScope The scope to start finding. - * @param {string|Node} nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node. - * @returns {Variable|null} The found variable or null. + * @param {import('eslint').Scope.Scope} initialScope The scope to start finding. + * @param {string | import('estree').Node | import('estree').Expression} nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node. + * @returns {import('eslint').Scope.Variable|null} The found variable or null. */ export function findVariable(initialScope, nameOrNode) { let name = "" + /** @type {import('eslint').Scope.Scope|null} */ let scope = initialScope if (typeof nameOrNode === "string") { name = nameOrNode } else { - name = nameOrNode.name + name = 'name' in nameOrNode ? nameOrNode.name : '' scope = getInnermostScope(scope, nameOrNode) } diff --git a/src/get-function-head-location.mjs b/src/get-function-head-location.mjs index 6e0b79d..e37914e 100644 --- a/src/get-function-head-location.mjs +++ b/src/get-function-head-location.mjs @@ -2,46 +2,50 @@ import { isArrowToken, isOpeningParenToken } from "./token-predicate.mjs" /** * Get the `(` token of the given function node. - * @param {Node} node - The function node to get. - * @param {SourceCode} sourceCode - The source code object to get tokens. - * @returns {Token} `(` token. + * @param {import('eslint').Rule.Node} node - The function node to get. + * @param {import('eslint').SourceCode} sourceCode - The source code object to get tokens. + * @returns {import('eslint').AST.Token | null} `(` token. */ function getOpeningParenOfParams(node, sourceCode) { - return node.id - ? sourceCode.getTokenAfter(node.id, isOpeningParenToken) + return 'id' in node + ? (node.id ? sourceCode.getTokenAfter(node.id, isOpeningParenToken) : null) : sourceCode.getFirstToken(node, isOpeningParenToken) } /** * Get the location of the given function node for reporting. - * @param {Node} node - The function node to get. - * @param {SourceCode} sourceCode - The source code object to get tokens. - * @returns {string} The location of the function node for reporting. + * @param {import('eslint').Rule.Node} node - The function node to get. + * @param {import('eslint').SourceCode} sourceCode - The source code object to get tokens. + * @returns {import('eslint').AST.SourceLocation|null} The location of the function node for reporting. */ export function getFunctionHeadLocation(node, sourceCode) { const parent = node.parent - let start = null - let end = null + /** @type {import('eslint').AST.SourceLocation["start"]|undefined} */ + let start, + /** @type {import('eslint').AST.SourceLocation["end"]|undefined} */ + end if (node.type === "ArrowFunctionExpression") { const arrowToken = sourceCode.getTokenBefore(node.body, isArrowToken) - start = arrowToken.loc.start - end = arrowToken.loc.end + start = arrowToken?.loc.start + end = arrowToken?.loc.end } else if ( parent.type === "Property" || parent.type === "MethodDefinition" || parent.type === "PropertyDefinition" ) { - start = parent.loc.start - end = getOpeningParenOfParams(node, sourceCode).loc.start + start = parent.loc?.start + end = getOpeningParenOfParams(node, sourceCode)?.loc.start } else { - start = node.loc.start - end = getOpeningParenOfParams(node, sourceCode).loc.start + start = node.loc?.start + end = getOpeningParenOfParams(node, sourceCode)?.loc.start } - return { - start: { ...start }, - end: { ...end }, - } + return start && end + ? { + start: { ...start }, + end: { ...end }, + } + : null } diff --git a/src/get-function-name-with-kind.mjs b/src/get-function-name-with-kind.mjs index bf8e17c..e63871b 100644 --- a/src/get-function-name-with-kind.mjs +++ b/src/get-function-name-with-kind.mjs @@ -2,8 +2,8 @@ import { getPropertyName } from "./get-property-name.mjs" /** * Get the name and kind of the given function node. - * @param {ASTNode} node - The function node to get. - * @param {SourceCode} [sourceCode] The source code object to get the code of computed property keys. + * @param {import('eslint').Rule.Node} node - The function node to get. + * @param {import('eslint').SourceCode} [sourceCode] The source code object to get the code of computed property keys. * @returns {string} The name and kind of the function node. */ // eslint-disable-next-line complexity @@ -25,10 +25,10 @@ export function getFunctionNameWithKind(node, sourceCode) { tokens.push("private") } } - if (node.async) { + if ('async' in node) { tokens.push("async") } - if (node.generator) { + if ('generator' in node) { tokens.push("generator") } @@ -68,8 +68,8 @@ export function getFunctionNameWithKind(node, sourceCode) { } } } - } else if (node.id) { - tokens.push(`'${node.id.name}'`) + } else if ('id' in node && node.id) { + tokens.push(`'${'name' in node.id ? node.id.name : undefined}'`) } else if ( parent.type === "VariableDeclarator" && parent.id && diff --git a/src/get-innermost-scope.mjs b/src/get-innermost-scope.mjs index d62ec69..c757a0e 100644 --- a/src/get-innermost-scope.mjs +++ b/src/get-innermost-scope.mjs @@ -1,11 +1,11 @@ /** * Get the innermost scope which contains a given location. - * @param {Scope} initialScope The initial scope to search. - * @param {Node} node The location to search. - * @returns {Scope} The innermost scope. + * @param {import('eslint').Scope.Scope} initialScope The initial scope to search. + * @param {import('estree').Node | import('estree').Expression} node The location to search. + * @returns {import('eslint').Scope.Scope} The innermost scope. */ export function getInnermostScope(initialScope, node) { - const location = node.range[0] + const location = node.range ? node.range[0] : undefined let scope = initialScope let found = false @@ -14,7 +14,7 @@ export function getInnermostScope(initialScope, node) { for (const childScope of scope.childScopes) { const range = childScope.block.range - if (range[0] <= location && location < range[1]) { + if (range && location && range[0] <= location && location < range[1]) { scope = childScope found = true break diff --git a/src/get-property-name.mjs b/src/get-property-name.mjs index 5ae3d3a..6c15c7b 100644 --- a/src/get-property-name.mjs +++ b/src/get-property-name.mjs @@ -2,8 +2,8 @@ import { getStringIfConstant } from "./get-string-if-constant.mjs" /** * Get the property name from a MemberExpression node or a Property node. - * @param {Node} node The node to get. - * @param {Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. + * @param {import('estree').Node | import('estree').Expression} node The node to get. + * @param {import('eslint').Scope.Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. * @returns {string|null} The property name of the node. */ export function getPropertyName(node, initialScope) { @@ -15,7 +15,7 @@ export function getPropertyName(node, initialScope) { if (node.property.type === "PrivateIdentifier") { return null } - return node.property.name + return 'name' in node.property ? node.property.name : null case "Property": case "MethodDefinition": @@ -29,7 +29,7 @@ export function getPropertyName(node, initialScope) { if (node.key.type === "PrivateIdentifier") { return null } - return node.key.name + return 'name' in node.key ? node.key.name : null // no default } diff --git a/src/get-static-value.mjs b/src/get-static-value.mjs index 074f298..900d220 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.mjs @@ -2,12 +2,19 @@ import { findVariable } from "./find-variable.mjs" +/** @typedef {import('estree').Node | import('estree').Expression} Node */ + +/** @type {typeof globalThis} */ const globalObject = typeof globalThis !== "undefined" ? globalThis + // @ts-ignore : typeof self !== "undefined" + // @ts-ignore ? self + // @ts-ignore : typeof window !== "undefined" + // @ts-ignore ? window : typeof global !== "undefined" ? global @@ -95,6 +102,7 @@ const callAllowed = new Set( escape, isFinite, isNaN, + // @ts-ignore isPrototypeOf, Map, Map.prototype.entries, @@ -220,7 +228,7 @@ function isGetter(object, name) { /** * Get the element values of a given node list. * @param {Node[]} nodeList The node list to get values. - * @param {Scope|undefined} initialScope The initial scope to find variables. + * @param {import('eslint').Scope.Scope|undefined} initialScope The initial scope to find variables. * @returns {any[]|null} The value list if all nodes are constant. Otherwise, null. */ function getElementValues(nodeList, initialScope) { @@ -423,7 +431,7 @@ const operations = Object.freeze({ if (variable != null && variable.defs.length === 1) { const def = variable.defs[0] if ( - def.parent && + def?.parent && def.type === "Variable" && (def.parent.kind === "const" || isEffectivelyConst(variable)) && @@ -623,15 +631,18 @@ const operations = Object.freeze({ }, }) +/** @typedef {{ value: any, optional?: never }|{value:undefined,optional?:true}} StaticValue */ + /** * Get the value of a given node if it's a static value. - * @param {Node} node The node to get. - * @param {Scope|undefined} initialScope The scope to start finding variable. - * @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`. + * @param {Node | null | undefined} node The node to get. + * @param {import('eslint').Scope.Scope|undefined} initialScope The scope to start finding variable. + * @returns {StaticValue|null} The static value of the node, or `null`. */ function getStaticValueR(node, initialScope) { - if (node != null && Object.hasOwnProperty.call(operations, node.type)) { - return operations[node.type](node, initialScope) + if (node && Object.hasOwn(operations, node.type)) { + const cb = operations[node.type] + return cb ? cb(node, initialScope) : null } return null } @@ -639,22 +650,22 @@ function getStaticValueR(node, initialScope) { /** * Get the static value of property name from a MemberExpression node or a Property node. * @param {Node} node The node to get. - * @param {Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. - * @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the property name of the node, or `null`. + * @param {import('eslint').Scope.Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. + * @returns {StaticValue|null} The static value of the property name of the node, or `null`. */ function getStaticPropertyNameValue(node, initialScope) { - const nameNode = node.type === "Property" ? node.key : node.property + const nameNode = node.type === "Property" ? node.key : ('property' in node ? node.property : undefined) - if (node.computed) { + if ('computed' in node && node.computed) { return getStaticValueR(nameNode, initialScope) } - if (nameNode.type === "Identifier") { + if (nameNode?.type === "Identifier") { return { value: nameNode.name } } - if (nameNode.type === "Literal") { - if (nameNode.bigint) { + if (nameNode?.type === "Literal") { + if ('bigint' in nameNode && nameNode.bigint) { return { value: nameNode.bigint } } return { value: String(nameNode.value) } @@ -666,12 +677,12 @@ function getStaticPropertyNameValue(node, initialScope) { /** * Get the value of a given node if it's a static value. * @param {Node} node The node to get. - * @param {Scope} [initialScope] The scope to start finding variable. Optional. If this scope was given, this tries to resolve identifier references which are in the given node as much as possible. - * @returns {{value:any}|{value:undefined,optional?:true}|null} The static value of the node, or `null`. + * @param {import('eslint').Scope.Scope|null} [initialScope] The scope to start finding variable. Optional. If this scope was given, this tries to resolve identifier references which are in the given node as much as possible. + * @returns {StaticValue|null} The static value of the node, or `null`. */ export function getStaticValue(node, initialScope = null) { try { - return getStaticValueR(node, initialScope) + return getStaticValueR(node, initialScope || undefined) } catch (_error) { return null } diff --git a/src/get-string-if-constant.mjs b/src/get-string-if-constant.mjs index ab03363..4288283 100644 --- a/src/get-string-if-constant.mjs +++ b/src/get-string-if-constant.mjs @@ -2,17 +2,17 @@ import { getStaticValue } from "./get-static-value.mjs" /** * Get the value of a given node if it's a literal or a template literal. - * @param {Node} node The node to get. - * @param {Scope} [initialScope] The scope to start finding variable. Optional. If the node is an Identifier node and this scope was given, this checks the variable of the identifier, and returns the value of it if the variable is a constant. + * @param {import('estree').Node | import('estree').Expression} node The node to get. + * @param {import('eslint').Scope.Scope|null} [initialScope] The scope to start finding variable. Optional. If the node is an Identifier node and this scope was given, this checks the variable of the identifier, and returns the value of it if the variable is a constant. * @returns {string|null} The value of the node, or `null`. */ export function getStringIfConstant(node, initialScope = null) { // Handle the literals that the platform doesn't support natively. if (node && node.type === "Literal" && node.value === null) { - if (node.regex) { + if ('regex' in node) { return `/${node.regex.pattern}/${node.regex.flags}` } - if (node.bigint) { + if ('bigint' in node) { return node.bigint } } diff --git a/src/has-side-effect.mjs b/src/has-side-effect.mjs index 3d792f3..5239141 100644 --- a/src/has-side-effect.mjs +++ b/src/has-side-effect.mjs @@ -26,126 +26,145 @@ const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"])) /** * Check whether the given value is an ASTNode or not. - * @param {any} x The value to check. - * @returns {boolean} `true` if the value is an ASTNode. + * @param {unknown} x The value to check. + * @returns {x is import('eslint').Rule.Node} `true` if the value is an ASTNode. */ function isNode(x) { - return x !== null && typeof x === "object" && typeof x.type === "string" + return x !== null && typeof x === "object" && 'type' in x && typeof x.type === "string" } -const visitor = Object.freeze( - Object.assign(Object.create(null), { - $visit(node, options, visitorKeys) { - const { type } = node +/** + * @typedef VisitOptions + * @property {boolean} [considerGetters=false] If `true` then it considers member accesses as the node which has side effects. + * @property {boolean} [considerImplicitTypeConversion=false] If `true` then it considers implicit type conversion as the node which has side effects. + */ - if (typeof this[type] === "function") { - return this[type](node, options, visitorKeys) - } +/** + * @callback VisitorCallback + * @param {import('eslint').Rule.Node} node + * @param {VisitOptions} options + * @param {import('eslint').SourceCode.VisitorKeys | typeof KEYS} visitorKeys + * @returns {boolean} + */ + +/** @type {Partial> & Record<'$visit' | '$visitChildren', VisitorCallback>} */ +const visitor = { + $visit(node, options, visitorKeys) { + const match = this[node.type] - return this.$visitChildren(node, options, visitorKeys) - }, + if (typeof match === "function") { + return match(node, options, visitorKeys) + } - $visitChildren(node, options, visitorKeys) { - const { type } = node + return this.$visitChildren(node, options, visitorKeys) + }, - for (const key of visitorKeys[type] || getKeys(node)) { - const value = node[key] + $visitChildren(node, options, visitorKeys) { + const { type } = node - if (Array.isArray(value)) { - for (const element of value) { - if ( - isNode(element) && - this.$visit(element, options, visitorKeys) - ) { - return true - } + for (const key of visitorKeys[type] || getKeys(node)) { + const value = node[/** @type {keyof typeof node} */ (key)] + + if (Array.isArray(value)) { + for (const element of value) { + if ( + isNode(element) && + this.$visit(element, options, visitorKeys) + ) { + return true } - } else if ( - isNode(value) && - this.$visit(value, options, visitorKeys) - ) { - return true } + } else if ( + isNode(value) && + this.$visit(value, options, visitorKeys) + ) { + return true } + } - return false - }, - - ArrowFunctionExpression() { - return false - }, - AssignmentExpression() { + return false + }, + ArrowFunctionExpression() { + return false + }, + AssignmentExpression() { + return true + }, + AwaitExpression() { + return true + }, + BinaryExpression(node, options, visitorKeys) { + if ( + node.type === 'BinaryExpression' && + options.considerImplicitTypeConversion && + typeConversionBinaryOps.has(node.operator) && + (node.left.type !== "Literal" || node.right.type !== "Literal") + ) { return true - }, - AwaitExpression() { + } + return this.$visitChildren(node, options, visitorKeys) + }, + CallExpression() { + return true + }, + FunctionExpression() { + return false + }, + ImportExpression() { + return true + }, + MemberExpression(node, options, visitorKeys) { + if (options.considerGetters) { return true - }, - BinaryExpression(node, options, visitorKeys) { - if ( - options.considerImplicitTypeConversion && - typeConversionBinaryOps.has(node.operator) && - (node.left.type !== "Literal" || node.right.type !== "Literal") - ) { - return true - } - return this.$visitChildren(node, options, visitorKeys) - }, - CallExpression() { + } + if ( + node.type === 'MemberExpression' && + options.considerImplicitTypeConversion && + node.computed && + node.property.type !== "Literal" + ) { return true - }, - FunctionExpression() { - return false - }, - ImportExpression() { + } + return this.$visitChildren(node, options, visitorKeys) + }, + MethodDefinition(node, options, visitorKeys) { + if ( + node.type === 'MethodDefinition' && + options.considerImplicitTypeConversion && + node.computed && + node.key.type !== "Literal" + ) { return true - }, - MemberExpression(node, options, visitorKeys) { - if (options.considerGetters) { - return true - } - if ( - options.considerImplicitTypeConversion && - node.computed && - node.property.type !== "Literal" - ) { - return true - } - return this.$visitChildren(node, options, visitorKeys) - }, - MethodDefinition(node, options, visitorKeys) { - if ( - options.considerImplicitTypeConversion && - node.computed && - node.key.type !== "Literal" - ) { - return true - } - return this.$visitChildren(node, options, visitorKeys) - }, - NewExpression() { + } + return this.$visitChildren(node, options, visitorKeys) + }, + NewExpression() { + return true + }, + Property(node, options, visitorKeys) { + if ( + node.type === 'Property' && + options.considerImplicitTypeConversion && + node.computed && + node.key.type !== "Literal" + ) { return true - }, - Property(node, options, visitorKeys) { - if ( - options.considerImplicitTypeConversion && - node.computed && - node.key.type !== "Literal" - ) { - return true - } - return this.$visitChildren(node, options, visitorKeys) - }, - PropertyDefinition(node, options, visitorKeys) { - if ( - options.considerImplicitTypeConversion && - node.computed && - node.key.type !== "Literal" - ) { - return true - } - return this.$visitChildren(node, options, visitorKeys) - }, - UnaryExpression(node, options, visitorKeys) { + } + return this.$visitChildren(node, options, visitorKeys) + }, + PropertyDefinition(node, options, visitorKeys) { + if ( + node.type === 'PropertyDefinition' && + options.considerImplicitTypeConversion && + node.computed && + node.key.type !== "Literal" + ) { + return true + } + return this.$visitChildren(node, options, visitorKeys) + }, + UnaryExpression(node, options, visitorKeys) { + if (node.type === 'UnaryExpression') { if (node.operator === "delete") { return true } @@ -156,25 +175,22 @@ const visitor = Object.freeze( ) { return true } - return this.$visitChildren(node, options, visitorKeys) - }, - UpdateExpression() { - return true - }, - YieldExpression() { - return true - }, - }), -) + } + return this.$visitChildren(node, options, visitorKeys) + }, + UpdateExpression() { + return true + }, + YieldExpression() { + return true + }, +} /** * Check whether a given node has any side effect or not. - * @param {Node} node The node to get. - * @param {SourceCode} sourceCode The source code object. - * @param {object} [options] The option object. - * @param {boolean} [options.considerGetters=false] If `true` then it considers member accesses as the node which has side effects. - * @param {boolean} [options.considerImplicitTypeConversion=false] If `true` then it considers implicit type conversion as the node which has side effects. - * @param {object} [options.visitorKeys=KEYS] The keys to traverse nodes. Use `context.getSourceCode().visitorKeys`. + * @param {import('eslint').Rule.Node} node The node to get. + * @param {import('eslint').SourceCode} sourceCode The source code object. + * @param {VisitOptions} [options] The option object. * @returns {boolean} `true` if the node has a certain side effect. */ export function hasSideEffect( diff --git a/src/is-parenthesized.mjs b/src/is-parenthesized.mjs index c862d1a..9cd4f23 100644 --- a/src/is-parenthesized.mjs +++ b/src/is-parenthesized.mjs @@ -3,9 +3,9 @@ import { isClosingParenToken, isOpeningParenToken } from "./token-predicate.mjs" /** * Get the left parenthesis of the parent node syntax if it exists. * E.g., `if (a) {}` then the `(`. - * @param {Node} node The AST node to check. - * @param {SourceCode} sourceCode The source code object to get tokens. - * @returns {Token|null} The left parenthesis of the parent node syntax + * @param {import('eslint').Rule.Node} node The AST node to check. + * @param {import('eslint').SourceCode} sourceCode The source code object to get tokens. + * @returns {import('eslint').AST.Token|null} The left parenthesis of the parent node syntax */ function getParentSyntaxParen(node, sourceCode) { const parent = node.parent @@ -62,39 +62,61 @@ function getParentSyntaxParen(node, sourceCode) { /** * Check whether a given node is parenthesized or not. - * @param {number} times The number of parantheses. - * @param {Node} node The AST node to check. - * @param {SourceCode} sourceCode The source code object to get tokens. + * @overload + * @param {number} timesOrNode The number of parantheses. + * @param {import('eslint').Rule.Node} nodeOrSourceCode The AST node to check. + * @param {import('eslint').SourceCode} optionalSourceCode The source code object to get tokens. * @returns {boolean} `true` if the node is parenthesized the given times. */ /** * Check whether a given node is parenthesized or not. - * @param {Node} node The AST node to check. - * @param {SourceCode} sourceCode The source code object to get tokens. + * @overload + * @param {import('eslint').Rule.Node} timesOrNode The AST node to check. + * @param {import('eslint').SourceCode} nodeOrSourceCode The source code object to get tokens. * @returns {boolean} `true` if the node is parenthesized. */ +/** + * Check whether a given node is parenthesized or not. + * @param {import('eslint').Rule.Node|number} timesOrNode The number of parantheses. + * @param {import('eslint').SourceCode|import('eslint').Rule.Node} nodeOrSourceCode The AST node to check. + * @param {import('eslint').SourceCode} [optionalSourceCode] The source code object to get tokens. + * @returns {boolean} `true` if the node is parenthesized the given times. + */ export function isParenthesized( timesOrNode, nodeOrSourceCode, optionalSourceCode, ) { - let times, node, sourceCode, maybeLeftParen, maybeRightParen if (typeof timesOrNode === "number") { - times = timesOrNode | 0 - node = nodeOrSourceCode - sourceCode = optionalSourceCode - if (!(times >= 1)) { + if (!(timesOrNode >= 1)) { throw new TypeError("'times' should be a positive integer.") } - } else { - times = 1 - node = timesOrNode - sourceCode = nodeOrSourceCode + // @ts-ignore + return internalIsParenthesized(timesOrNode | 0, nodeOrSourceCode, optionalSourceCode) } + // @ts-ignore + return internalIsParenthesized(1, timesOrNode, nodeOrSourceCode) +} + +/** + * Check whether a given node is parenthesized or not. + * @param {number} times The number of parantheses. + * @param {import('eslint').Rule.Node} node The AST node to check. + * @param {import('eslint').SourceCode} sourceCode The source code object to get tokens. + * @returns {boolean} `true` if the node is parenthesized the given times. + */ +function internalIsParenthesized( + times, + node, + sourceCode, +) { + let maybeLeftParen, maybeRightParen + if ( node == null || // `Program` can't be parenthesized + !('parent' in node) || node.parent == null || // `CatchClause.param` can't be parenthesized, example `try {} catch (error) {}` (node.parent.type === "CatchClause" && node.parent.param === node) @@ -113,6 +135,7 @@ export function isParenthesized( isClosingParenToken(maybeRightParen) && // Avoid false positive such as `if (a) {}` maybeLeftParen !== getParentSyntaxParen(node, sourceCode) && + // eslint-disable-next-line no-param-reassign --times > 0 ) diff --git a/src/pattern-matcher.mjs b/src/pattern-matcher.mjs index 35f5a17..3d4a0ec 100644 --- a/src/pattern-matcher.mjs +++ b/src/pattern-matcher.mjs @@ -34,7 +34,7 @@ function replaceS(matcher, str, replacement) { let index = 0 /** @type {RegExpExecArray} */ - let match = null + let match /** * @param {string} key The placeholder. @@ -51,11 +51,8 @@ function replaceS(matcher, str, replacement) { case "$'": return str.slice(match.index + match[0].length) default: { - const i = key.slice(1) - if (i in match) { - return match[i] - } - return key + const i = parseInt(key.slice(1), 10) + return match[i] || key } } } @@ -74,7 +71,7 @@ function replaceS(matcher, str, replacement) { * Replace a given string by a given matcher. * @param {PatternMatcher} matcher The pattern matcher. * @param {string} str The string to be replaced. - * @param {(...strs[])=>string} replace The function to replace each matched part. + * @param {(...strs: string[])=>string} replace The function to replace each matched part. * @returns {string} The replaced string. */ function replaceF(matcher, str, replace) { @@ -83,7 +80,7 @@ function replaceF(matcher, str, replace) { for (const match of matcher.execAll(str)) { chunks.push(str.slice(index, match.index)) - chunks.push(String(replace(...match, match.index, match.input))) + chunks.push(String(replace(...match, String(match.index), match.input))) index = match.index + match[0].length } chunks.push(str.slice(index)) @@ -98,7 +95,7 @@ export class PatternMatcher { /** * Initialize this matcher. * @param {RegExp} pattern The pattern to match. - * @param {{escaped:boolean}} options The options. + * @param {{escaped?:boolean}} [options] The options. */ constructor(pattern, { escaped = false } = {}) { if (!(pattern instanceof RegExp)) { @@ -120,10 +117,14 @@ export class PatternMatcher { * @returns {IterableIterator} The iterator which iterate the matched information. */ *execAll(str) { - const { pattern, escaped } = internal.get(this) + const { pattern, escaped } = internal.get(this) || {} let match = null let lastIndex = 0 + if (!pattern) { + return + } + pattern.lastIndex = 0 while ((match = pattern.exec(str)) != null) { if (escaped || !isEscaped(str, match.index)) { diff --git a/src/reference-tracker.mjs b/src/reference-tracker.mjs index 3f14b28..5f793eb 100644 --- a/src/reference-tracker.mjs +++ b/src/reference-tracker.mjs @@ -3,7 +3,6 @@ import { getPropertyName } from "./get-property-name.mjs" import { getStringIfConstant } from "./get-string-if-constant.mjs" const IMPORT_TYPE = /^(?:Import|Export(?:All|Default|Named))Declaration$/u -const has = Function.call.bind(Object.hasOwnProperty) export const READ = Symbol("read") export const CALL = Symbol("call") @@ -12,9 +11,23 @@ export const ESM = Symbol("esm") const requireCall = { require: { [CALL]: true } } +/** @typedef {import('eslint').Rule.Node | import('estree').Node | import('estree').Expression} Node */ +/** @typedef {READ | CALL | CONSTRUCT} ReferenceType */ + +/** @typedef {Partial>} TraceMapLeaf */ +/** @typedef {{ [key: string]: TraceMap } & TraceMapLeaf} TraceMap */ + +/** + * @typedef Reference + * @property {Node} node + * @property {string[]} path + * @property {ReferenceType} type + * @property {any} info + */ + /** * Check whether a given variable is modified or not. - * @param {Variable} variable The variable to check. + * @param {import('eslint').Scope.Variable} variable The variable to check. * @returns {boolean} `true` if the variable is modified. */ function isModifiedGlobal(variable) { @@ -32,9 +45,9 @@ function isModifiedGlobal(variable) { * @returns {boolean} `true` if the node is passed through. */ function isPassThrough(node) { - const parent = node.parent + const parent = 'parent' in node ? node.parent : undefined - switch (parent && parent.type) { + switch (parent?.type) { case "ConditionalExpression": return parent.consequent === node || parent.alternate === node case "LogicalExpression": @@ -55,7 +68,7 @@ function isPassThrough(node) { export class ReferenceTracker { /** * Initialize this tracker. - * @param {Scope} globalScope The global scope. + * @param {import('eslint').Scope.Scope} globalScope The global scope. * @param {object} [options] The options. * @param {"legacy"|"strict"} [options.mode="strict"] The mode to determine the ImportDeclaration's behavior for CJS modules. * @param {string[]} [options.globalObjectNames=["global","globalThis","self","window"]] The variable names for Global Object. @@ -67,24 +80,27 @@ export class ReferenceTracker { globalObjectNames = ["global", "globalThis", "self", "window"], } = {}, ) { + /** @type {import('eslint').Scope.Variable[]} */ this.variableStack = [] + /** @type {import('eslint').Scope.Scope} */ this.globalScope = globalScope + /** @type {"legacy"|"strict"} */ this.mode = mode + /** @type {string[]} */ this.globalObjectNames = globalObjectNames.slice(0) } /** * Iterate the references of global variables. - * @param {object} traceMap The trace map. - * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references. + * @param {TraceMap} traceMap The trace map. + * @returns {IterableIterator} The iterator to iterate references. */ *iterateGlobalReferences(traceMap) { - for (const key of Object.keys(traceMap)) { - const nextTraceMap = traceMap[key] + for (const [key, nextTraceMap] of Object.entries(traceMap)) { const path = [key] const variable = this.globalScope.set.get(key) - if (isModifiedGlobal(variable)) { + if (!variable || isModifiedGlobal(variable)) { continue } @@ -97,10 +113,11 @@ export class ReferenceTracker { } for (const key of this.globalObjectNames) { + /** @type {string[]} */ const path = [] const variable = this.globalScope.set.get(key) - if (isModifiedGlobal(variable)) { + if (!variable || isModifiedGlobal(variable)) { continue } @@ -115,19 +132,23 @@ export class ReferenceTracker { /** * Iterate the references of CommonJS modules. - * @param {object} traceMap The trace map. - * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references. + * @param {TraceMap} traceMap The trace map. + * @returns {IterableIterator} The iterator to iterate references. */ *iterateCjsReferences(traceMap) { for (const { node } of this.iterateGlobalReferences(requireCall)) { - const key = getStringIfConstant(node.arguments[0]) - if (key == null || !has(traceMap, key)) { + const key = 'arguments' in node && node.arguments[0] ? getStringIfConstant(node.arguments[0]) : null + if (key == null || !Object.hasOwn(traceMap, key)) { continue } const nextTraceMap = traceMap[key] const path = [key] + if (!nextTraceMap) { + return + } + if (nextTraceMap[READ]) { yield { node, @@ -142,22 +163,30 @@ export class ReferenceTracker { /** * Iterate the references of ES modules. - * @param {object} traceMap The trace map. - * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references. + * @param {TraceMap} traceMap The trace map. + * @returns {IterableIterator} The iterator to iterate references. */ + // eslint-disable-next-line complexity *iterateEsmReferences(traceMap) { const programNode = this.globalScope.block + if (!('body' in programNode) || !(Symbol.iterator in programNode.body)) { + return + } + for (const node of programNode.body) { - if (!IMPORT_TYPE.test(node.type) || node.source == null) { + if (!IMPORT_TYPE.test(node.type) || !('source' in node) || node.source == null) { continue } const moduleId = node.source.value - if (!has(traceMap, moduleId)) { + if (typeof moduleId !== 'string' || !Object.hasOwn(traceMap, moduleId)) { continue } const nextTraceMap = traceMap[moduleId] + if (!nextTraceMap) { + continue + } const path = [moduleId] if (nextTraceMap[READ]) { @@ -167,7 +196,7 @@ export class ReferenceTracker { if (node.type === "ExportAllDeclaration") { for (const key of Object.keys(nextTraceMap)) { const exportTraceMap = nextTraceMap[key] - if (exportTraceMap[READ]) { + if (exportTraceMap && exportTraceMap[READ]) { yield { node, path: path.concat(key), @@ -178,7 +207,7 @@ export class ReferenceTracker { } } else { for (const specifier of node.specifiers) { - const esm = has(nextTraceMap, ESM) + const esm = Object.hasOwn(nextTraceMap, ESM) const it = this._iterateImportReferences( specifier, path, @@ -209,11 +238,11 @@ export class ReferenceTracker { /** * Iterate the references for a given variable. - * @param {Variable} variable The variable to iterate that references. + * @param {import('eslint').Scope.Variable} variable The variable to iterate that references. * @param {string[]} path The current path. - * @param {object} traceMap The trace map. + * @param {TraceMap} traceMap The trace map. * @param {boolean} shouldReport = The flag to report those references. - * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references. + * @returns {IterableIterator} The iterator to iterate references. */ *_iterateVariableReferences(variable, path, traceMap, shouldReport) { if (this.variableStack.includes(variable)) { @@ -239,28 +268,32 @@ export class ReferenceTracker { /** * Iterate the references for a given AST node. - * @param rootNode The AST node to iterate references. + * @param {Node} rootNode The AST node to iterate references. * @param {string[]} path The current path. - * @param {object} traceMap The trace map. - * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references. + * @param {TraceMap} traceMap The trace map. + * @returns {IterableIterator} The iterator to iterate references. */ //eslint-disable-next-line complexity *_iteratePropertyReferences(rootNode, path, traceMap) { let node = rootNode - while (isPassThrough(node)) { + + while (isPassThrough(node) && 'parent' in node) { node = node.parent } - const parent = node.parent - if (parent.type === "MemberExpression") { + const parent = 'parent' in node ? node.parent : undefined + if (parent?.type === "MemberExpression") { if (parent.object === node) { const key = getPropertyName(parent) - if (key == null || !has(traceMap, key)) { + if (key == null || !Object.hasOwn(traceMap, key)) { return } path = path.concat(key) //eslint-disable-line no-param-reassign const nextTraceMap = traceMap[key] + if (!nextTraceMap) { + return; + } if (nextTraceMap[READ]) { yield { node: parent, @@ -277,13 +310,13 @@ export class ReferenceTracker { } return } - if (parent.type === "CallExpression") { + if (parent?.type === "CallExpression") { if (parent.callee === node && traceMap[CALL]) { yield { node: parent, path, type: CALL, info: traceMap[CALL] } } return } - if (parent.type === "NewExpression") { + if (parent?.type === "NewExpression") { if (parent.callee === node && traceMap[CONSTRUCT]) { yield { node: parent, @@ -294,20 +327,20 @@ export class ReferenceTracker { } return } - if (parent.type === "AssignmentExpression") { + if (parent?.type === "AssignmentExpression") { if (parent.right === node) { yield* this._iterateLhsReferences(parent.left, path, traceMap) yield* this._iteratePropertyReferences(parent, path, traceMap) } return } - if (parent.type === "AssignmentPattern") { + if (parent?.type === "AssignmentPattern") { if (parent.right === node) { yield* this._iterateLhsReferences(parent.left, path, traceMap) } return } - if (parent.type === "VariableDeclarator") { + if (parent?.type === "VariableDeclarator") { if (parent.init === node) { yield* this._iterateLhsReferences(parent.id, path, traceMap) } @@ -318,8 +351,8 @@ export class ReferenceTracker { * Iterate the references for a given Pattern node. * @param {Node} patternNode The Pattern node to iterate references. * @param {string[]} path The current path. - * @param {object} traceMap The trace map. - * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references. + * @param {TraceMap} traceMap The trace map. + * @returns {IterableIterator} The iterator to iterate references. */ *_iterateLhsReferences(patternNode, path, traceMap) { if (patternNode.type === "Identifier") { @@ -338,12 +371,15 @@ export class ReferenceTracker { for (const property of patternNode.properties) { const key = getPropertyName(property) - if (key == null || !has(traceMap, key)) { + if (key == null || !Object.hasOwn(traceMap, key)) { continue } const nextPath = path.concat(key) const nextTraceMap = traceMap[key] + if (!nextTraceMap) { + return; + } if (nextTraceMap[READ]) { yield { node: property, @@ -352,11 +388,13 @@ export class ReferenceTracker { info: nextTraceMap[READ], } } - yield* this._iterateLhsReferences( - property.value, - nextPath, - nextTraceMap, - ) + if ('value' in property) { + yield* this._iterateLhsReferences( + property.value, + nextPath, + nextTraceMap, + ) + } } return } @@ -369,8 +407,8 @@ export class ReferenceTracker { * Iterate the references for a given ModuleSpecifier node. * @param {Node} specifierNode The ModuleSpecifier node to iterate references. * @param {string[]} path The current path. - * @param {object} traceMap The trace map. - * @returns {IterableIterator<{node:Node,path:string[],type:symbol,info:any}>} The iterator to iterate references. + * @param {TraceMap} traceMap The trace map. + * @returns {IterableIterator} The iterator to iterate references. */ *_iterateImportReferences(specifierNode, path, traceMap) { const type = specifierNode.type @@ -380,12 +418,15 @@ export class ReferenceTracker { type === "ImportDefaultSpecifier" ? "default" : specifierNode.imported.name - if (!has(traceMap, key)) { + if (!Object.hasOwn(traceMap, key)) { return } path = path.concat(key) //eslint-disable-line no-param-reassign const nextTraceMap = traceMap[key] + if (!nextTraceMap) { + return + } if (nextTraceMap[READ]) { yield { node: specifierNode, @@ -394,34 +435,43 @@ export class ReferenceTracker { info: nextTraceMap[READ], } } - yield* this._iterateVariableReferences( - findVariable(this.globalScope, specifierNode.local), - path, - nextTraceMap, - false, - ) + const variable = findVariable(this.globalScope, specifierNode.local) + if (variable) { + yield* this._iterateVariableReferences( + variable, + path, + nextTraceMap, + false, + ) + } return } if (type === "ImportNamespaceSpecifier") { - yield* this._iterateVariableReferences( - findVariable(this.globalScope, specifierNode.local), - path, - traceMap, - false, - ) + const variable = findVariable(this.globalScope, specifierNode.local) + if (variable) { + yield* this._iterateVariableReferences( + variable, + path, + traceMap, + false, + ) + } return } if (type === "ExportSpecifier") { const key = specifierNode.local.name - if (!has(traceMap, key)) { + if (!Object.hasOwn(traceMap, key)) { return } path = path.concat(key) //eslint-disable-line no-param-reassign const nextTraceMap = traceMap[key] + if (!nextTraceMap) { + return + } if (nextTraceMap[READ]) { yield { node: specifierNode, diff --git a/src/token-predicate.mjs b/src/token-predicate.mjs index 22889f1..e82d0a8 100644 --- a/src/token-predicate.mjs +++ b/src/token-predicate.mjs @@ -1,7 +1,7 @@ /** * Creates the negate function of the given function. - * @param {function(Token):boolean} f - The function to negate. - * @returns {function(Token):boolean} Negated function. + * @param {(token: import('eslint').AST.Token) => boolean} f - The function to negate. + * @returns {(token: import('eslint').AST.Token) => boolean} Negated function. */ function negate(f) { return (token) => !f(token) @@ -9,7 +9,7 @@ function negate(f) { /** * Checks if the given token is a PunctuatorToken with the given value - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @param {string} value - The value to check. * @returns {boolean} `true` if the token is a PunctuatorToken with the given value. */ @@ -19,7 +19,7 @@ function isPunctuatorTokenWithValue(token, value) { /** * Checks if the given token is an arrow token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is an arrow token. */ export function isArrowToken(token) { @@ -28,7 +28,7 @@ export function isArrowToken(token) { /** * Checks if the given token is a comma token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is a comma token. */ export function isCommaToken(token) { @@ -37,7 +37,7 @@ export function isCommaToken(token) { /** * Checks if the given token is a semicolon token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is a semicolon token. */ export function isSemicolonToken(token) { @@ -46,7 +46,7 @@ export function isSemicolonToken(token) { /** * Checks if the given token is a colon token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is a colon token. */ export function isColonToken(token) { @@ -55,7 +55,7 @@ export function isColonToken(token) { /** * Checks if the given token is an opening parenthesis token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is an opening parenthesis token. */ export function isOpeningParenToken(token) { @@ -64,7 +64,7 @@ export function isOpeningParenToken(token) { /** * Checks if the given token is a closing parenthesis token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is a closing parenthesis token. */ export function isClosingParenToken(token) { @@ -73,7 +73,7 @@ export function isClosingParenToken(token) { /** * Checks if the given token is an opening square bracket token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is an opening square bracket token. */ export function isOpeningBracketToken(token) { @@ -82,7 +82,7 @@ export function isOpeningBracketToken(token) { /** * Checks if the given token is a closing square bracket token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is a closing square bracket token. */ export function isClosingBracketToken(token) { @@ -91,7 +91,7 @@ export function isClosingBracketToken(token) { /** * Checks if the given token is an opening brace token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is an opening brace token. */ export function isOpeningBraceToken(token) { @@ -100,7 +100,7 @@ export function isOpeningBraceToken(token) { /** * Checks if the given token is a closing brace token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is a closing brace token. */ export function isClosingBraceToken(token) { @@ -109,7 +109,7 @@ export function isClosingBraceToken(token) { /** * Checks if the given token is a comment token or not. - * @param {Token} token - The token to check. + * @param {import('eslint').AST.Token} token - The token to check. * @returns {boolean} `true` if the token is a comment token. */ export function isCommentToken(token) { diff --git a/tsconfig.json b/tsconfig.json index e6a02bc..a28c4fb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "strict": true, - "skipLibCheck": false, // See https://github.com/voxpelli/tsconfig/issues/1 + "skipLibCheck": true, /* Clean up generated declarations */ "removeComments": true, @@ -21,8 +21,7 @@ "checkJs": true, "noEmit": true, - /* New checks being tried out */ - // "exactOptionalPropertyTypes": true, + /* Extra strictness */ "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, @@ -33,11 +32,7 @@ "noUnusedLocals": true, "noUnusedParameters": true, - /* To make strict checking somewhat less strict during a transition stage, add one or more of: */ - /* - "noImplicitThis": false, + /* TODO: Remove to ensure full strictness */ "noImplicitAny": false, - "strictNullChecks": false, - */ } } From b430a97f0c450333faa663c8aaab9cd83a8edba4 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 20:10:36 +0100 Subject: [PATCH 03/14] Have static value pass strict check --- src/get-static-value.mjs | 81 +++++++++++++++++++++++++++++++++++++--- tsconfig.json | 5 +-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/get-static-value.mjs b/src/get-static-value.mjs index 900d220..97bb480 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.mjs @@ -4,7 +4,7 @@ import { findVariable } from "./find-variable.mjs" /** @typedef {import('estree').Node | import('estree').Expression} Node */ -/** @type {typeof globalThis} */ +/** @type {Record} */ const globalObject = typeof globalThis !== "undefined" ? globalThis @@ -112,6 +112,7 @@ const callAllowed = new Set( Map.prototype.values, ...Object.getOwnPropertyNames(Math) .filter((k) => k !== "random") + // @ts-ignore .map((k) => Math[k]) .filter((f) => typeof f === "function"), Number, @@ -227,7 +228,7 @@ function isGetter(object, name) { /** * Get the element values of a given node list. - * @param {Node[]} nodeList The node list to get values. + * @param {(Node|null)[]} nodeList The node list to get values. * @param {import('eslint').Scope.Scope|undefined} initialScope The initial scope to find variables. * @returns {any[]|null} The value list if all nodes are constant. Otherwise, null. */ @@ -274,13 +275,27 @@ function isEffectivelyConst(variable) { return false } +/** + * @callback VisitorCallback + * @param {Node} node + * @param {import('eslint').Scope.Scope|undefined} initialScope + * @returns {StaticValue | null} + */ + +/** @type {Partial> } */ const operations = Object.freeze({ ArrayExpression(node, initialScope) { + if (node.type !== 'ArrayExpression') { + return null + } const elements = getElementValues(node.elements, initialScope) return elements != null ? { value: elements } : null }, AssignmentExpression(node, initialScope) { + if (node.type !== 'AssignmentExpression') { + return null + } if (node.operator === "=") { return getStaticValueR(node.right, initialScope) } @@ -289,6 +304,9 @@ const operations = Object.freeze({ //eslint-disable-next-line complexity BinaryExpression(node, initialScope) { + if (node.type !== 'BinaryExpression') { + return null + } if (node.operator === "in" || node.operator === "instanceof") { // Not supported. return null @@ -346,7 +364,11 @@ const operations = Object.freeze({ return null }, + // eslint-disable-next-line complexity CallExpression(node, initialScope) { + if (node.type !== 'CallExpression') { + return null + } const calleeNode = node.callee const args = getElementValues(node.arguments, initialScope) @@ -400,6 +422,9 @@ const operations = Object.freeze({ }, ConditionalExpression(node, initialScope) { + if (node.type !== 'ConditionalExpression') { + return null + } const test = getStaticValueR(node.test, initialScope) if (test != null) { return test.value @@ -410,10 +435,16 @@ const operations = Object.freeze({ }, ExpressionStatement(node, initialScope) { + if (node.type !== 'ExpressionStatement') { + return null + } return getStaticValueR(node.expression, initialScope) }, Identifier(node, initialScope) { + if (node.type !== 'Identifier') { + return null + } if (initialScope != null) { const variable = findVariable(initialScope, node) @@ -446,8 +477,17 @@ const operations = Object.freeze({ }, Literal(node) { + if (node.type !== 'Literal') { + return null + } //istanbul ignore if : this is implementation-specific behavior. - if ((node.regex != null || node.bigint != null) && node.value == null) { + if ( + ( + ('regex' in node && node.regex != null) || + ('bigint' in node && node.bigint != null) + ) && + node.value == null + ) { // It was a RegExp/BigInt literal, but Node.js didn't support it. return null } @@ -455,6 +495,9 @@ const operations = Object.freeze({ }, LogicalExpression(node, initialScope) { + if (node.type !== 'LogicalExpression') { + return null + } const left = getStaticValueR(node.left, initialScope) if (left != null) { if ( @@ -475,6 +518,9 @@ const operations = Object.freeze({ }, MemberExpression(node, initialScope) { + if (node.type !== 'MemberExpression') { + return null + } if (node.property.type === "PrivateIdentifier") { return null } @@ -495,6 +541,7 @@ const operations = Object.freeze({ object.value instanceof classFn && allowed.has(property.value) ) { + // @ts-ignore return { value: object.value[property.value] } } } @@ -504,6 +551,9 @@ const operations = Object.freeze({ }, ChainExpression(node, initialScope) { + if (node.type !== 'ChainExpression') { + return null + } const expression = getStaticValueR(node.expression, initialScope) if (expression != null) { return { value: expression.value } @@ -512,6 +562,9 @@ const operations = Object.freeze({ }, NewExpression(node, initialScope) { + if (node.type !== 'NewExpression') { + return null + } const callee = getStaticValueR(node.callee, initialScope) const args = getElementValues(node.arguments, initialScope) @@ -526,6 +579,10 @@ const operations = Object.freeze({ }, ObjectExpression(node, initialScope) { + if (node.type !== 'ObjectExpression') { + return null + } + /** @type {Record} */ const object = {} for (const propertyNode of node.properties) { @@ -544,6 +601,7 @@ const operations = Object.freeze({ object[key.value] = value.value } else if ( propertyNode.type === "SpreadElement" || + // @ts-ignore propertyNode.type === "ExperimentalSpreadProperty" ) { const argument = getStaticValueR( @@ -563,11 +621,17 @@ const operations = Object.freeze({ }, SequenceExpression(node, initialScope) { + if (node.type !== 'SequenceExpression') { + return null + } const last = node.expressions[node.expressions.length - 1] return getStaticValueR(last, initialScope) }, TaggedTemplateExpression(node, initialScope) { + if (node.type !== 'TaggedTemplateExpression') { + return null + } const tag = getStaticValueR(node.tag, initialScope) const expressions = getElementValues( node.quasi.expressions, @@ -576,6 +640,7 @@ const operations = Object.freeze({ if (tag != null && expressions != null) { const func = tag.value + /** @type {(string|null|undefined)[] & { raw?: string[] }} */ const strings = node.quasi.quasis.map((q) => q.value.cooked) strings.raw = node.quasi.quasis.map((q) => q.value.raw) @@ -588,12 +653,15 @@ const operations = Object.freeze({ }, TemplateLiteral(node, initialScope) { + if (node.type !== 'TemplateLiteral') { + return null + } const expressions = getElementValues(node.expressions, initialScope) if (expressions != null) { - let value = node.quasis[0].value.cooked + let value = node.quasis[0]?.value.cooked for (let i = 0; i < expressions.length; ++i) { value += expressions[i] - value += node.quasis[i + 1].value.cooked + value += node.quasis[i + 1]?.value.cooked || '' } return { value } } @@ -601,6 +669,9 @@ const operations = Object.freeze({ }, UnaryExpression(node, initialScope) { + if (node.type !== 'UnaryExpression') { + return null + } if (node.operator === "delete") { // Not supported. return null diff --git a/tsconfig.json b/tsconfig.json index a28c4fb..ed04eb3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,9 +30,6 @@ /* Additional non-type checks */ "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, - "noUnusedParameters": true, - - /* TODO: Remove to ensure full strictness */ - "noImplicitAny": false, + "noUnusedParameters": true } } From 94d9faf7868a9beeb573275b7b35dbcf85f6b8d7 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 20:26:28 +0100 Subject: [PATCH 04/14] Check and increase type-coverage --- package.json | 6 +++++- rollup.config.mjs | 2 ++ src/get-function-name-with-kind.mjs | 1 + src/get-static-value.mjs | 2 +- src/is-parenthesized.mjs | 6 ++++-- src/pattern-matcher.mjs | 3 +++ src/reference-tracker.mjs | 2 +- 7 files changed, 17 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 11548c7..b22a0e2 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,10 @@ "format": "npm run -s format:prettier -- --write", "format:prettier": "prettier .", "format:check": "npm run -s format:prettier -- --check", - "lint": "tsc && eslint .", + "lint:eslint": "eslint .", + "lint:tsc": "tsc", + "lint:type-coverage": "type-coverage --detail --strict --at-least 99 --ignore-files 'test/*' --ignore-files 'src/get-static-value.mjs'", + "lint": "run-p lint:*", "test": "c8 mocha --reporter dot \"test/*.mjs\"", "preversion": "npm test && npm run -s build", "postversion": "git push && git push --tags", @@ -65,6 +68,7 @@ "rollup": "^4.12.0", "rollup-plugin-dts": "^6.1.0", "semver": "^7.5.4", + "type-coverage": "^2.27.1", "typescript": "^5.3.3", "vitepress": "^1.0.0-rc.20", "warun": "^1.0.0" diff --git a/rollup.config.mjs b/rollup.config.mjs index 47e299c..bb82b15 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -8,6 +8,7 @@ import { URL } from 'node:url'; import { dts } from "rollup-plugin-dts"; +/** @type {{ dependencies: Record }} */ const packageInfo = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); export default [ @@ -28,6 +29,7 @@ export default [ file: `index.d.ts`, format: "cjs", }], + // type-coverage:ignore-next-line plugins: [dts()], }, ] diff --git a/src/get-function-name-with-kind.mjs b/src/get-function-name-with-kind.mjs index e63871b..08ed9f7 100644 --- a/src/get-function-name-with-kind.mjs +++ b/src/get-function-name-with-kind.mjs @@ -9,6 +9,7 @@ import { getPropertyName } from "./get-property-name.mjs" // eslint-disable-next-line complexity export function getFunctionNameWithKind(node, sourceCode) { const parent = node.parent + /** @type {string[]} */ const tokens = [] const isObjectMethod = parent.type === "Property" && parent.value === node const isClassMethod = diff --git a/src/get-static-value.mjs b/src/get-static-value.mjs index 97bb480..f7ebdd0 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.mjs @@ -749,7 +749,7 @@ function getStaticPropertyNameValue(node, initialScope) { * Get the value of a given node if it's a static value. * @param {Node} node The node to get. * @param {import('eslint').Scope.Scope|null} [initialScope] The scope to start finding variable. Optional. If this scope was given, this tries to resolve identifier references which are in the given node as much as possible. - * @returns {StaticValue|null} The static value of the node, or `null`. + * @returns {{ value: unknown, optional?: never }|{value:undefined,optional?:true}|null} The static value of the node, or `null`. */ export function getStaticValue(node, initialScope = null) { try { diff --git a/src/is-parenthesized.mjs b/src/is-parenthesized.mjs index 9cd4f23..ef7663f 100644 --- a/src/is-parenthesized.mjs +++ b/src/is-parenthesized.mjs @@ -111,7 +111,10 @@ function internalIsParenthesized( node, sourceCode, ) { - let maybeLeftParen, maybeRightParen + /** @type {import('eslint').Rule.Node | import('eslint').AST.Token | null} */ + let maybeLeftParen = node; + /** @type {import('eslint').Rule.Node | import('eslint').AST.Token | null} */ + let maybeRightParen = node; if ( node == null || @@ -124,7 +127,6 @@ function internalIsParenthesized( return false } - maybeLeftParen = maybeRightParen = node do { maybeLeftParen = sourceCode.getTokenBefore(maybeLeftParen) maybeRightParen = sourceCode.getTokenAfter(maybeRightParen) diff --git a/src/pattern-matcher.mjs b/src/pattern-matcher.mjs index 3d4a0ec..5601132 100644 --- a/src/pattern-matcher.mjs +++ b/src/pattern-matcher.mjs @@ -30,6 +30,7 @@ function isEscaped(str, index) { * @returns {string} The replaced string. */ function replaceS(matcher, str, replacement) { + /** @type {string[]} */ const chunks = [] let index = 0 @@ -75,6 +76,7 @@ function replaceS(matcher, str, replacement) { * @returns {string} The replaced string. */ function replaceF(matcher, str, replace) { + /** @type {string[]} */ const chunks = [] let index = 0 @@ -118,6 +120,7 @@ export class PatternMatcher { */ *execAll(str) { const { pattern, escaped } = internal.get(this) || {} + /** @type {RegExpExecArray|null} */ let match = null let lastIndex = 0 diff --git a/src/reference-tracker.mjs b/src/reference-tracker.mjs index 5f793eb..d268a97 100644 --- a/src/reference-tracker.mjs +++ b/src/reference-tracker.mjs @@ -22,7 +22,7 @@ const requireCall = { require: { [CALL]: true } } * @property {Node} node * @property {string[]} path * @property {ReferenceType} type - * @property {any} info + * @property {unknown} info */ /** From 5b7ea93d75bcf2b064e4675883b5168333b61e5a Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 21:49:37 +0100 Subject: [PATCH 05/14] Improve has-side-effect types --- src/has-side-effect.mjs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/has-side-effect.mjs b/src/has-side-effect.mjs index 5239141..97d7ecd 100644 --- a/src/has-side-effect.mjs +++ b/src/has-side-effect.mjs @@ -27,12 +27,18 @@ const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"])) /** * Check whether the given value is an ASTNode or not. * @param {unknown} x The value to check. - * @returns {x is import('eslint').Rule.Node} `true` if the value is an ASTNode. + * @returns {x is { type: string }} `true` if the value is an ASTNode. */ function isNode(x) { return x !== null && typeof x === "object" && 'type' in x && typeof x.type === "string" } +/** + * @see https://github.com/sindresorhus/type-fest/blob/906e7e77204c65f7512f9f54b3205f25c5c0c8e5/source/keys-of-union.d.ts#L38-L40 + * @template T + * @typedef {T extends unknown ? T[keyof T] : never} ValuesInObjectUnion + */ + /** * @typedef VisitOptions * @property {boolean} [considerGetters=false] If `true` then it considers member accesses as the node which has side effects. @@ -41,13 +47,13 @@ function isNode(x) { /** * @callback VisitorCallback - * @param {import('eslint').Rule.Node} node + * @param {import('estree').Node | import('estree').Expression | import('estree').Comment | import('estree').MaybeNamedClassDeclaration | import('estree').MaybeNamedFunctionDeclaration} node * @param {VisitOptions} options * @param {import('eslint').SourceCode.VisitorKeys | typeof KEYS} visitorKeys * @returns {boolean} */ -/** @type {Partial> & Record<'$visit' | '$visitChildren', VisitorCallback>} */ +/** @type {Partial> & Record<'$visit' | '$visitChildren', VisitorCallback>} */ const visitor = { $visit(node, options, visitorKeys) { const match = this[node.type] @@ -60,10 +66,10 @@ const visitor = { }, $visitChildren(node, options, visitorKeys) { - const { type } = node + const { type, ...remainder } = node for (const key of visitorKeys[type] || getKeys(node)) { - const value = node[/** @type {keyof typeof node} */ (key)] + const value = /** @type {ValuesInObjectUnion} */ (remainder[/** @type {keyof typeof remainder} */ (key)]) if (Array.isArray(value)) { for (const element of value) { From 722f27e3656407f62ea14555e130f24f8a61631d Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 21:54:31 +0100 Subject: [PATCH 06/14] Improve the types some more --- src/find-variable.mjs | 2 +- src/get-innermost-scope.mjs | 2 +- src/get-property-name.mjs | 2 +- src/get-static-value.mjs | 14 ++++++-------- src/get-string-if-constant.mjs | 2 +- src/has-side-effect.mjs | 2 +- src/reference-tracker.mjs | 15 ++++++--------- src/types.mjs | 2 ++ 8 files changed, 19 insertions(+), 22 deletions(-) create mode 100644 src/types.mjs diff --git a/src/find-variable.mjs b/src/find-variable.mjs index c935a65..eccb86e 100644 --- a/src/find-variable.mjs +++ b/src/find-variable.mjs @@ -3,7 +3,7 @@ import { getInnermostScope } from "./get-innermost-scope.mjs" /** * Find the variable of a given name. * @param {import('eslint').Scope.Scope} initialScope The scope to start finding. - * @param {string | import('estree').Node | import('estree').Expression} nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node. + * @param {string | import('./types.mjs').Node} nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node. * @returns {import('eslint').Scope.Variable|null} The found variable or null. */ export function findVariable(initialScope, nameOrNode) { diff --git a/src/get-innermost-scope.mjs b/src/get-innermost-scope.mjs index c757a0e..77c54a5 100644 --- a/src/get-innermost-scope.mjs +++ b/src/get-innermost-scope.mjs @@ -1,7 +1,7 @@ /** * Get the innermost scope which contains a given location. * @param {import('eslint').Scope.Scope} initialScope The initial scope to search. - * @param {import('estree').Node | import('estree').Expression} node The location to search. + * @param {import('./types.mjs').Node} node The location to search. * @returns {import('eslint').Scope.Scope} The innermost scope. */ export function getInnermostScope(initialScope, node) { diff --git a/src/get-property-name.mjs b/src/get-property-name.mjs index 6c15c7b..f237d03 100644 --- a/src/get-property-name.mjs +++ b/src/get-property-name.mjs @@ -2,7 +2,7 @@ import { getStringIfConstant } from "./get-string-if-constant.mjs" /** * Get the property name from a MemberExpression node or a Property node. - * @param {import('estree').Node | import('estree').Expression} node The node to get. + * @param {import('./types.mjs').Node} node The node to get. * @param {import('eslint').Scope.Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. * @returns {string|null} The property name of the node. */ diff --git a/src/get-static-value.mjs b/src/get-static-value.mjs index f7ebdd0..ce951ff 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.mjs @@ -2,8 +2,6 @@ import { findVariable } from "./find-variable.mjs" -/** @typedef {import('estree').Node | import('estree').Expression} Node */ - /** @type {Record} */ const globalObject = typeof globalThis !== "undefined" @@ -228,7 +226,7 @@ function isGetter(object, name) { /** * Get the element values of a given node list. - * @param {(Node|null)[]} nodeList The node list to get values. + * @param {(import('./types.mjs').Node|null)[]} nodeList The node list to get values. * @param {import('eslint').Scope.Scope|undefined} initialScope The initial scope to find variables. * @returns {any[]|null} The value list if all nodes are constant. Otherwise, null. */ @@ -277,12 +275,12 @@ function isEffectivelyConst(variable) { /** * @callback VisitorCallback - * @param {Node} node + * @param {import('./types.mjs').Node} node * @param {import('eslint').Scope.Scope|undefined} initialScope * @returns {StaticValue | null} */ -/** @type {Partial> } */ +/** @type {Readonly>> } */ const operations = Object.freeze({ ArrayExpression(node, initialScope) { if (node.type !== 'ArrayExpression') { @@ -706,7 +704,7 @@ const operations = Object.freeze({ /** * Get the value of a given node if it's a static value. - * @param {Node | null | undefined} node The node to get. + * @param {import('./types.mjs').Node | null | undefined} node The node to get. * @param {import('eslint').Scope.Scope|undefined} initialScope The scope to start finding variable. * @returns {StaticValue|null} The static value of the node, or `null`. */ @@ -720,7 +718,7 @@ function getStaticValueR(node, initialScope) { /** * Get the static value of property name from a MemberExpression node or a Property node. - * @param {Node} node The node to get. + * @param {import('./types.mjs').Node} node The node to get. * @param {import('eslint').Scope.Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. * @returns {StaticValue|null} The static value of the property name of the node, or `null`. */ @@ -747,7 +745,7 @@ function getStaticPropertyNameValue(node, initialScope) { /** * Get the value of a given node if it's a static value. - * @param {Node} node The node to get. + * @param {import('./types.mjs').Node} node The node to get. * @param {import('eslint').Scope.Scope|null} [initialScope] The scope to start finding variable. Optional. If this scope was given, this tries to resolve identifier references which are in the given node as much as possible. * @returns {{ value: unknown, optional?: never }|{value:undefined,optional?:true}|null} The static value of the node, or `null`. */ diff --git a/src/get-string-if-constant.mjs b/src/get-string-if-constant.mjs index 4288283..5cb61aa 100644 --- a/src/get-string-if-constant.mjs +++ b/src/get-string-if-constant.mjs @@ -2,7 +2,7 @@ import { getStaticValue } from "./get-static-value.mjs" /** * Get the value of a given node if it's a literal or a template literal. - * @param {import('estree').Node | import('estree').Expression} node The node to get. + * @param {import('./types.mjs').Node} node The node to get. * @param {import('eslint').Scope.Scope|null} [initialScope] The scope to start finding variable. Optional. If the node is an Identifier node and this scope was given, this checks the variable of the identifier, and returns the value of it if the variable is a constant. * @returns {string|null} The value of the node, or `null`. */ diff --git a/src/has-side-effect.mjs b/src/has-side-effect.mjs index 97d7ecd..406022a 100644 --- a/src/has-side-effect.mjs +++ b/src/has-side-effect.mjs @@ -47,7 +47,7 @@ function isNode(x) { /** * @callback VisitorCallback - * @param {import('estree').Node | import('estree').Expression | import('estree').Comment | import('estree').MaybeNamedClassDeclaration | import('estree').MaybeNamedFunctionDeclaration} node + * @param {import('./types.mjs').Node | import('estree').Comment | import('estree').MaybeNamedClassDeclaration | import('estree').MaybeNamedFunctionDeclaration} node * @param {VisitOptions} options * @param {import('eslint').SourceCode.VisitorKeys | typeof KEYS} visitorKeys * @returns {boolean} diff --git a/src/reference-tracker.mjs b/src/reference-tracker.mjs index d268a97..d157047 100644 --- a/src/reference-tracker.mjs +++ b/src/reference-tracker.mjs @@ -11,15 +11,12 @@ export const ESM = Symbol("esm") const requireCall = { require: { [CALL]: true } } -/** @typedef {import('eslint').Rule.Node | import('estree').Node | import('estree').Expression} Node */ /** @typedef {READ | CALL | CONSTRUCT} ReferenceType */ - -/** @typedef {Partial>} TraceMapLeaf */ -/** @typedef {{ [key: string]: TraceMap } & TraceMapLeaf} TraceMap */ +/** @typedef {{ [key: string]: TraceMap } & Partial>} TraceMap */ /** * @typedef Reference - * @property {Node} node + * @property {import('./types.mjs').RichNode} node * @property {string[]} path * @property {ReferenceType} type * @property {unknown} info @@ -41,7 +38,7 @@ function isModifiedGlobal(variable) { /** * Check if the value of a given node is passed through to the parent syntax as-is. * For example, `a` and `b` in (`a || b` and `c ? a : b`) are passed through. - * @param {Node} node A node to check. + * @param {import('./types.mjs').RichNode} node A node to check. * @returns {boolean} `true` if the node is passed through. */ function isPassThrough(node) { @@ -268,7 +265,7 @@ export class ReferenceTracker { /** * Iterate the references for a given AST node. - * @param {Node} rootNode The AST node to iterate references. + * @param {import('./types.mjs').RichNode} rootNode The AST node to iterate references. * @param {string[]} path The current path. * @param {TraceMap} traceMap The trace map. * @returns {IterableIterator} The iterator to iterate references. @@ -349,7 +346,7 @@ export class ReferenceTracker { /** * Iterate the references for a given Pattern node. - * @param {Node} patternNode The Pattern node to iterate references. + * @param {import('./types.mjs').RichNode} patternNode The Pattern node to iterate references. * @param {string[]} path The current path. * @param {TraceMap} traceMap The trace map. * @returns {IterableIterator} The iterator to iterate references. @@ -405,7 +402,7 @@ export class ReferenceTracker { /** * Iterate the references for a given ModuleSpecifier node. - * @param {Node} specifierNode The ModuleSpecifier node to iterate references. + * @param {import('./types.mjs').RichNode} specifierNode The ModuleSpecifier node to iterate references. * @param {string[]} path The current path. * @param {TraceMap} traceMap The trace map. * @returns {IterableIterator} The iterator to iterate references. diff --git a/src/types.mjs b/src/types.mjs new file mode 100644 index 0000000..7262212 --- /dev/null +++ b/src/types.mjs @@ -0,0 +1,2 @@ +/** @typedef {import('estree').Node | import('estree').Expression} Node */ +/** @typedef {import('eslint').Rule.Node | Node} RichNode */ From 5a7e9a270126c476c50519e5b8c3439b42b4b555 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 22:22:16 +0100 Subject: [PATCH 07/14] Fix tests --- src/get-function-head-location.mjs | 4 ++-- src/get-function-name-with-kind.mjs | 4 ++-- src/get-innermost-scope.mjs | 2 +- src/has-side-effect.mjs | 2 +- src/pattern-matcher.mjs | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/get-function-head-location.mjs b/src/get-function-head-location.mjs index e37914e..954fc9a 100644 --- a/src/get-function-head-location.mjs +++ b/src/get-function-head-location.mjs @@ -7,8 +7,8 @@ import { isArrowToken, isOpeningParenToken } from "./token-predicate.mjs" * @returns {import('eslint').AST.Token | null} `(` token. */ function getOpeningParenOfParams(node, sourceCode) { - return 'id' in node - ? (node.id ? sourceCode.getTokenAfter(node.id, isOpeningParenToken) : null) + return 'id' in node && node.id + ? sourceCode.getTokenAfter(node.id, isOpeningParenToken) : sourceCode.getFirstToken(node, isOpeningParenToken) } diff --git a/src/get-function-name-with-kind.mjs b/src/get-function-name-with-kind.mjs index 08ed9f7..5183aef 100644 --- a/src/get-function-name-with-kind.mjs +++ b/src/get-function-name-with-kind.mjs @@ -26,10 +26,10 @@ export function getFunctionNameWithKind(node, sourceCode) { tokens.push("private") } } - if ('async' in node) { + if ('async' in node && node.async) { tokens.push("async") } - if ('generator' in node) { + if ('generator' in node && node.generator) { tokens.push("generator") } diff --git a/src/get-innermost-scope.mjs b/src/get-innermost-scope.mjs index 77c54a5..00f7c13 100644 --- a/src/get-innermost-scope.mjs +++ b/src/get-innermost-scope.mjs @@ -14,7 +14,7 @@ export function getInnermostScope(initialScope, node) { for (const childScope of scope.childScopes) { const range = childScope.block.range - if (range && location && range[0] <= location && location < range[1]) { + if (range && location !== undefined && range[0] <= location && location < range[1]) { scope = childScope found = true break diff --git a/src/has-side-effect.mjs b/src/has-side-effect.mjs index 406022a..5dcabb6 100644 --- a/src/has-side-effect.mjs +++ b/src/has-side-effect.mjs @@ -59,7 +59,7 @@ const visitor = { const match = this[node.type] if (typeof match === "function") { - return match(node, options, visitorKeys) + return match.call(this, node, options, visitorKeys) } return this.$visitChildren(node, options, visitorKeys) diff --git a/src/pattern-matcher.mjs b/src/pattern-matcher.mjs index 5601132..6bc4c0a 100644 --- a/src/pattern-matcher.mjs +++ b/src/pattern-matcher.mjs @@ -72,7 +72,7 @@ function replaceS(matcher, str, replacement) { * Replace a given string by a given matcher. * @param {PatternMatcher} matcher The pattern matcher. * @param {string} str The string to be replaced. - * @param {(...strs: string[])=>string} replace The function to replace each matched part. + * @param {(...strs: (string|number)[])=>string} replace The function to replace each matched part. * @returns {string} The replaced string. */ function replaceF(matcher, str, replace) { @@ -82,7 +82,7 @@ function replaceF(matcher, str, replace) { for (const match of matcher.execAll(str)) { chunks.push(str.slice(index, match.index)) - chunks.push(String(replace(...match, String(match.index), match.input))) + chunks.push(String(replace(...match, match.index, match.input))) index = match.index + match[0].length } chunks.push(str.slice(index)) @@ -152,7 +152,7 @@ export class PatternMatcher { /** * Replace a given string. * @param {string} str The string to be replaced. - * @param {(string|((...strs:string[])=>string))} replacer The string or function to replace. This is the same as the 2nd argument of `String.prototype.replace`. + * @param {(string|((...strs:(string|number)[])=>string))} replacer The string or function to replace. This is the same as the 2nd argument of `String.prototype.replace`. * @returns {string} The replaced string. */ [Symbol.replace](str, replacer) { From fe80e3c4d610ac814c4b57bbf75edb116ec1a6be Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 22:49:50 +0100 Subject: [PATCH 08/14] Improve types --- src/get-function-head-location.mjs | 13 +++++++------ src/get-function-name-with-kind.mjs | 2 +- src/get-property-name.mjs | 12 ++++++++---- src/reference-tracker.mjs | 18 ++++++++++-------- src/types.mjs | 1 - 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/get-function-head-location.mjs b/src/get-function-head-location.mjs index 954fc9a..bab4d04 100644 --- a/src/get-function-head-location.mjs +++ b/src/get-function-head-location.mjs @@ -2,7 +2,7 @@ import { isArrowToken, isOpeningParenToken } from "./token-predicate.mjs" /** * Get the `(` token of the given function node. - * @param {import('eslint').Rule.Node} node - The function node to get. + * @param {Extract} node - The function node to get. * @param {import('eslint').SourceCode} sourceCode - The source code object to get tokens. * @returns {import('eslint').AST.Token | null} `(` token. */ @@ -14,12 +14,13 @@ function getOpeningParenOfParams(node, sourceCode) { /** * Get the location of the given function node for reporting. - * @param {import('eslint').Rule.Node} node - The function node to get. + * @param {Extract} node - The function node to get. * @param {import('eslint').SourceCode} sourceCode - The source code object to get tokens. * @returns {import('eslint').AST.SourceLocation|null} The location of the function node for reporting. */ export function getFunctionHeadLocation(node, sourceCode) { - const parent = node.parent + const parent = 'parent' in node ? node.parent : undefined + /** @type {import('eslint').AST.SourceLocation["start"]|undefined} */ let start, /** @type {import('eslint').AST.SourceLocation["end"]|undefined} */ @@ -31,9 +32,9 @@ export function getFunctionHeadLocation(node, sourceCode) { start = arrowToken?.loc.start end = arrowToken?.loc.end } else if ( - parent.type === "Property" || - parent.type === "MethodDefinition" || - parent.type === "PropertyDefinition" + parent?.type === "Property" || + parent?.type === "MethodDefinition" || + parent?.type === "PropertyDefinition" ) { start = parent.loc?.start end = getOpeningParenOfParams(node, sourceCode)?.loc.start diff --git a/src/get-function-name-with-kind.mjs b/src/get-function-name-with-kind.mjs index 5183aef..ee63463 100644 --- a/src/get-function-name-with-kind.mjs +++ b/src/get-function-name-with-kind.mjs @@ -2,7 +2,7 @@ import { getPropertyName } from "./get-property-name.mjs" /** * Get the name and kind of the given function node. - * @param {import('eslint').Rule.Node} node - The function node to get. + * @param {Extract} node - The function node to get. * @param {import('eslint').SourceCode} [sourceCode] The source code object to get the code of computed property keys. * @returns {string} The name and kind of the function node. */ diff --git a/src/get-property-name.mjs b/src/get-property-name.mjs index f237d03..bf44742 100644 --- a/src/get-property-name.mjs +++ b/src/get-property-name.mjs @@ -2,11 +2,14 @@ import { getStringIfConstant } from "./get-string-if-constant.mjs" /** * Get the property name from a MemberExpression node or a Property node. - * @param {import('./types.mjs').Node} node The node to get. + * @param {Extract} node The node to get. * @param {import('eslint').Scope.Scope} [initialScope] The scope to start finding variable. Optional. If the node is a computed property node and this scope was given, this checks the computed property name by the `getStringIfConstant` function with the scope, and returns the value of it. * @returns {string|null} The property name of the node. */ export function getPropertyName(node, initialScope) { + /** @type {string|null} */ + let result = null + switch (node.type) { case "MemberExpression": if (node.computed) { @@ -15,7 +18,8 @@ export function getPropertyName(node, initialScope) { if (node.property.type === "PrivateIdentifier") { return null } - return 'name' in node.property ? node.property.name : null + result = 'name' in node.property ? node.property.name : null + break case "Property": case "MethodDefinition": @@ -29,10 +33,10 @@ export function getPropertyName(node, initialScope) { if (node.key.type === "PrivateIdentifier") { return null } - return 'name' in node.key ? node.key.name : null + result = 'name' in node.key ? node.key.name : null // no default } - return null + return result } diff --git a/src/reference-tracker.mjs b/src/reference-tracker.mjs index d157047..a798ae1 100644 --- a/src/reference-tracker.mjs +++ b/src/reference-tracker.mjs @@ -14,9 +14,11 @@ const requireCall = { require: { [CALL]: true } } /** @typedef {READ | CALL | CONSTRUCT} ReferenceType */ /** @typedef {{ [key: string]: TraceMap } & Partial>} TraceMap */ +/** @typedef {import('eslint').Rule.Node | import('./types.mjs').Node} RichNode */ + /** * @typedef Reference - * @property {import('./types.mjs').RichNode} node + * @property {RichNode} node * @property {string[]} path * @property {ReferenceType} type * @property {unknown} info @@ -38,7 +40,7 @@ function isModifiedGlobal(variable) { /** * Check if the value of a given node is passed through to the parent syntax as-is. * For example, `a` and `b` in (`a || b` and `c ? a : b`) are passed through. - * @param {import('./types.mjs').RichNode} node A node to check. + * @param {RichNode} node A node to check. * @returns {boolean} `true` if the node is passed through. */ function isPassThrough(node) { @@ -265,7 +267,7 @@ export class ReferenceTracker { /** * Iterate the references for a given AST node. - * @param {import('./types.mjs').RichNode} rootNode The AST node to iterate references. + * @param {RichNode} rootNode The AST node to iterate references. * @param {string[]} path The current path. * @param {TraceMap} traceMap The trace map. * @returns {IterableIterator} The iterator to iterate references. @@ -289,7 +291,7 @@ export class ReferenceTracker { path = path.concat(key) //eslint-disable-line no-param-reassign const nextTraceMap = traceMap[key] if (!nextTraceMap) { - return; + return } if (nextTraceMap[READ]) { yield { @@ -346,7 +348,7 @@ export class ReferenceTracker { /** * Iterate the references for a given Pattern node. - * @param {import('./types.mjs').RichNode} patternNode The Pattern node to iterate references. + * @param {RichNode} patternNode The Pattern node to iterate references. * @param {string[]} path The current path. * @param {TraceMap} traceMap The trace map. * @returns {IterableIterator} The iterator to iterate references. @@ -366,7 +368,7 @@ export class ReferenceTracker { } if (patternNode.type === "ObjectPattern") { for (const property of patternNode.properties) { - const key = getPropertyName(property) + const key = property.type === 'Property' ? getPropertyName(property) : null if (key == null || !Object.hasOwn(traceMap, key)) { continue @@ -375,7 +377,7 @@ export class ReferenceTracker { const nextPath = path.concat(key) const nextTraceMap = traceMap[key] if (!nextTraceMap) { - return; + return } if (nextTraceMap[READ]) { yield { @@ -402,7 +404,7 @@ export class ReferenceTracker { /** * Iterate the references for a given ModuleSpecifier node. - * @param {import('./types.mjs').RichNode} specifierNode The ModuleSpecifier node to iterate references. + * @param {RichNode} specifierNode The ModuleSpecifier node to iterate references. * @param {string[]} path The current path. * @param {TraceMap} traceMap The trace map. * @returns {IterableIterator} The iterator to iterate references. diff --git a/src/types.mjs b/src/types.mjs index 7262212..0820970 100644 --- a/src/types.mjs +++ b/src/types.mjs @@ -1,2 +1 @@ /** @typedef {import('estree').Node | import('estree').Expression} Node */ -/** @typedef {import('eslint').Rule.Node | Node} RichNode */ From 82a113419a34a1aba31c5e7e302363f4f3706c1f Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 22:50:21 +0100 Subject: [PATCH 09/14] Enforce no semi colons --- .eslintrc.js | 2 ++ rollup.config.mjs | 8 ++++---- src/is-parenthesized.mjs | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 803c59a..81da88f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,8 @@ module.exports = { project: "./tsconfig.json", }, rules: { + 'semi': ['error', 'never'], + 'semi-spacing': ['error', { 'before': false, 'after': true }], "@eslint-community/mysticatea/prettier": "off", "no-restricted-properties": [ "error", diff --git a/rollup.config.mjs b/rollup.config.mjs index bb82b15..7f20c66 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -3,13 +3,13 @@ * See LICENSE file in root directory for full license. */ -import { readFileSync } from 'node:fs'; -import { URL } from 'node:url'; +import { readFileSync } from 'node:fs' +import { URL } from 'node:url' -import { dts } from "rollup-plugin-dts"; +import { dts } from "rollup-plugin-dts" /** @type {{ dependencies: Record }} */ -const packageInfo = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); +const packageInfo = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) export default [ { diff --git a/src/is-parenthesized.mjs b/src/is-parenthesized.mjs index ef7663f..66c9cf6 100644 --- a/src/is-parenthesized.mjs +++ b/src/is-parenthesized.mjs @@ -112,9 +112,9 @@ function internalIsParenthesized( sourceCode, ) { /** @type {import('eslint').Rule.Node | import('eslint').AST.Token | null} */ - let maybeLeftParen = node; + let maybeLeftParen = node /** @type {import('eslint').Rule.Node | import('eslint').AST.Token | null} */ - let maybeRightParen = node; + let maybeRightParen = node if ( node == null || From dbbf50d4eca9e2d6996df3bf37d61919cedb8364 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 23:03:51 +0100 Subject: [PATCH 10/14] Fix incorrect check in `getStringIfConstant` --- src/get-string-if-constant.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/get-string-if-constant.mjs b/src/get-string-if-constant.mjs index 5cb61aa..e600dcf 100644 --- a/src/get-string-if-constant.mjs +++ b/src/get-string-if-constant.mjs @@ -9,10 +9,10 @@ import { getStaticValue } from "./get-static-value.mjs" export function getStringIfConstant(node, initialScope = null) { // Handle the literals that the platform doesn't support natively. if (node && node.type === "Literal" && node.value === null) { - if ('regex' in node) { + if ('regex' in node && node.regex) { return `/${node.regex.pattern}/${node.regex.flags}` } - if ('bigint' in node) { + if ('bigint' in node && node.bigint) { return node.bigint } } From a3abcf98a913ed4841eee88cae2984e09c97fe23 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 23:10:46 +0100 Subject: [PATCH 11/14] Run prettier --- .eslintrc.js | 4 +- rollup.config.mjs | 22 +++++---- src/find-variable.mjs | 2 +- src/get-function-head-location.mjs | 14 +++--- src/get-function-name-with-kind.mjs | 8 +-- src/get-innermost-scope.mjs | 7 ++- src/get-property-name.mjs | 4 +- src/get-static-value.mjs | 75 +++++++++++++++-------------- src/get-string-if-constant.mjs | 4 +- src/has-side-effect.mjs | 29 ++++++----- src/is-parenthesized.mjs | 14 +++--- src/reference-tracker.mjs | 34 +++++++++---- 12 files changed, 126 insertions(+), 91 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 81da88f..32e1597 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,8 +8,8 @@ module.exports = { project: "./tsconfig.json", }, rules: { - 'semi': ['error', 'never'], - 'semi-spacing': ['error', { 'before': false, 'after': true }], + semi: ["error", "never"], + "semi-spacing": ["error", { before: false, after: true }], "@eslint-community/mysticatea/prettier": "off", "no-restricted-properties": [ "error", diff --git a/rollup.config.mjs b/rollup.config.mjs index 7f20c66..9103186 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -3,13 +3,15 @@ * See LICENSE file in root directory for full license. */ -import { readFileSync } from 'node:fs' -import { URL } from 'node:url' +import { readFileSync } from "node:fs" +import { URL } from "node:url" import { dts } from "rollup-plugin-dts" /** @type {{ dependencies: Record }} */ -const packageInfo = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')) +const packageInfo = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +) export default [ { @@ -23,12 +25,14 @@ export default [ external: Object.keys(packageInfo.dependencies), }, { - input: 'dist/index.d.mts', - output: [{ - exports: "named", - file: `index.d.ts`, - format: "cjs", - }], + input: "dist/index.d.mts", + output: [ + { + exports: "named", + file: `index.d.ts`, + format: "cjs", + }, + ], // type-coverage:ignore-next-line plugins: [dts()], }, diff --git a/src/find-variable.mjs b/src/find-variable.mjs index eccb86e..831f9f1 100644 --- a/src/find-variable.mjs +++ b/src/find-variable.mjs @@ -14,7 +14,7 @@ export function findVariable(initialScope, nameOrNode) { if (typeof nameOrNode === "string") { name = nameOrNode } else { - name = 'name' in nameOrNode ? nameOrNode.name : '' + name = "name" in nameOrNode ? nameOrNode.name : "" scope = getInnermostScope(scope, nameOrNode) } diff --git a/src/get-function-head-location.mjs b/src/get-function-head-location.mjs index bab4d04..f50d7ba 100644 --- a/src/get-function-head-location.mjs +++ b/src/get-function-head-location.mjs @@ -7,7 +7,7 @@ import { isArrowToken, isOpeningParenToken } from "./token-predicate.mjs" * @returns {import('eslint').AST.Token | null} `(` token. */ function getOpeningParenOfParams(node, sourceCode) { - return 'id' in node && node.id + return "id" in node && node.id ? sourceCode.getTokenAfter(node.id, isOpeningParenToken) : sourceCode.getFirstToken(node, isOpeningParenToken) } @@ -19,12 +19,12 @@ function getOpeningParenOfParams(node, sourceCode) { * @returns {import('eslint').AST.SourceLocation|null} The location of the function node for reporting. */ export function getFunctionHeadLocation(node, sourceCode) { - const parent = 'parent' in node ? node.parent : undefined + const parent = "parent" in node ? node.parent : undefined /** @type {import('eslint').AST.SourceLocation["start"]|undefined} */ let start, - /** @type {import('eslint').AST.SourceLocation["end"]|undefined} */ - end + /** @type {import('eslint').AST.SourceLocation["end"]|undefined} */ + end if (node.type === "ArrowFunctionExpression") { const arrowToken = sourceCode.getTokenBefore(node.body, isArrowToken) @@ -45,8 +45,8 @@ export function getFunctionHeadLocation(node, sourceCode) { return start && end ? { - start: { ...start }, - end: { ...end }, - } + start: { ...start }, + end: { ...end }, + } : null } diff --git a/src/get-function-name-with-kind.mjs b/src/get-function-name-with-kind.mjs index ee63463..10ac08e 100644 --- a/src/get-function-name-with-kind.mjs +++ b/src/get-function-name-with-kind.mjs @@ -26,10 +26,10 @@ export function getFunctionNameWithKind(node, sourceCode) { tokens.push("private") } } - if ('async' in node && node.async) { + if ("async" in node && node.async) { tokens.push("async") } - if ('generator' in node && node.generator) { + if ("generator" in node && node.generator) { tokens.push("generator") } @@ -69,8 +69,8 @@ export function getFunctionNameWithKind(node, sourceCode) { } } } - } else if ('id' in node && node.id) { - tokens.push(`'${'name' in node.id ? node.id.name : undefined}'`) + } else if ("id" in node && node.id) { + tokens.push(`'${"name" in node.id ? node.id.name : undefined}'`) } else if ( parent.type === "VariableDeclarator" && parent.id && diff --git a/src/get-innermost-scope.mjs b/src/get-innermost-scope.mjs index 00f7c13..5fad463 100644 --- a/src/get-innermost-scope.mjs +++ b/src/get-innermost-scope.mjs @@ -14,7 +14,12 @@ export function getInnermostScope(initialScope, node) { for (const childScope of scope.childScopes) { const range = childScope.block.range - if (range && location !== undefined && range[0] <= location && location < range[1]) { + if ( + range && + location !== undefined && + range[0] <= location && + location < range[1] + ) { scope = childScope found = true break diff --git a/src/get-property-name.mjs b/src/get-property-name.mjs index bf44742..d7df86d 100644 --- a/src/get-property-name.mjs +++ b/src/get-property-name.mjs @@ -18,7 +18,7 @@ export function getPropertyName(node, initialScope) { if (node.property.type === "PrivateIdentifier") { return null } - result = 'name' in node.property ? node.property.name : null + result = "name" in node.property ? node.property.name : null break case "Property": @@ -33,7 +33,7 @@ export function getPropertyName(node, initialScope) { if (node.key.type === "PrivateIdentifier") { return null } - result = 'name' in node.key ? node.key.name : null + result = "name" in node.key ? node.key.name : null // no default } diff --git a/src/get-static-value.mjs b/src/get-static-value.mjs index ce951ff..5cb9232 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.mjs @@ -6,14 +6,14 @@ import { findVariable } from "./find-variable.mjs" const globalObject = typeof globalThis !== "undefined" ? globalThis - // @ts-ignore - : typeof self !== "undefined" - // @ts-ignore - ? self - // @ts-ignore - : typeof window !== "undefined" - // @ts-ignore - ? window + : // @ts-ignore + typeof self !== "undefined" + ? // @ts-ignore + self + : // @ts-ignore + typeof window !== "undefined" + ? // @ts-ignore + window : typeof global !== "undefined" ? global : {} @@ -275,15 +275,15 @@ function isEffectivelyConst(variable) { /** * @callback VisitorCallback - * @param {import('./types.mjs').Node} node - * @param {import('eslint').Scope.Scope|undefined} initialScope + * @param {import('./types.mjs').Node} node + * @param {import('eslint').Scope.Scope|undefined} initialScope * @returns {StaticValue | null} */ /** @type {Readonly>> } */ const operations = Object.freeze({ ArrayExpression(node, initialScope) { - if (node.type !== 'ArrayExpression') { + if (node.type !== "ArrayExpression") { return null } const elements = getElementValues(node.elements, initialScope) @@ -291,7 +291,7 @@ const operations = Object.freeze({ }, AssignmentExpression(node, initialScope) { - if (node.type !== 'AssignmentExpression') { + if (node.type !== "AssignmentExpression") { return null } if (node.operator === "=") { @@ -302,7 +302,7 @@ const operations = Object.freeze({ //eslint-disable-next-line complexity BinaryExpression(node, initialScope) { - if (node.type !== 'BinaryExpression') { + if (node.type !== "BinaryExpression") { return null } if (node.operator === "in" || node.operator === "instanceof") { @@ -364,7 +364,7 @@ const operations = Object.freeze({ // eslint-disable-next-line complexity CallExpression(node, initialScope) { - if (node.type !== 'CallExpression') { + if (node.type !== "CallExpression") { return null } const calleeNode = node.callee @@ -420,7 +420,7 @@ const operations = Object.freeze({ }, ConditionalExpression(node, initialScope) { - if (node.type !== 'ConditionalExpression') { + if (node.type !== "ConditionalExpression") { return null } const test = getStaticValueR(node.test, initialScope) @@ -433,14 +433,14 @@ const operations = Object.freeze({ }, ExpressionStatement(node, initialScope) { - if (node.type !== 'ExpressionStatement') { + if (node.type !== "ExpressionStatement") { return null } return getStaticValueR(node.expression, initialScope) }, Identifier(node, initialScope) { - if (node.type !== 'Identifier') { + if (node.type !== "Identifier") { return null } if (initialScope != null) { @@ -475,15 +475,13 @@ const operations = Object.freeze({ }, Literal(node) { - if (node.type !== 'Literal') { + if (node.type !== "Literal") { return null } //istanbul ignore if : this is implementation-specific behavior. if ( - ( - ('regex' in node && node.regex != null) || - ('bigint' in node && node.bigint != null) - ) && + (("regex" in node && node.regex != null) || + ("bigint" in node && node.bigint != null)) && node.value == null ) { // It was a RegExp/BigInt literal, but Node.js didn't support it. @@ -493,7 +491,7 @@ const operations = Object.freeze({ }, LogicalExpression(node, initialScope) { - if (node.type !== 'LogicalExpression') { + if (node.type !== "LogicalExpression") { return null } const left = getStaticValueR(node.left, initialScope) @@ -516,7 +514,7 @@ const operations = Object.freeze({ }, MemberExpression(node, initialScope) { - if (node.type !== 'MemberExpression') { + if (node.type !== "MemberExpression") { return null } if (node.property.type === "PrivateIdentifier") { @@ -549,7 +547,7 @@ const operations = Object.freeze({ }, ChainExpression(node, initialScope) { - if (node.type !== 'ChainExpression') { + if (node.type !== "ChainExpression") { return null } const expression = getStaticValueR(node.expression, initialScope) @@ -560,7 +558,7 @@ const operations = Object.freeze({ }, NewExpression(node, initialScope) { - if (node.type !== 'NewExpression') { + if (node.type !== "NewExpression") { return null } const callee = getStaticValueR(node.callee, initialScope) @@ -577,7 +575,7 @@ const operations = Object.freeze({ }, ObjectExpression(node, initialScope) { - if (node.type !== 'ObjectExpression') { + if (node.type !== "ObjectExpression") { return null } /** @type {Record} */ @@ -619,7 +617,7 @@ const operations = Object.freeze({ }, SequenceExpression(node, initialScope) { - if (node.type !== 'SequenceExpression') { + if (node.type !== "SequenceExpression") { return null } const last = node.expressions[node.expressions.length - 1] @@ -627,7 +625,7 @@ const operations = Object.freeze({ }, TaggedTemplateExpression(node, initialScope) { - if (node.type !== 'TaggedTemplateExpression') { + if (node.type !== "TaggedTemplateExpression") { return null } const tag = getStaticValueR(node.tag, initialScope) @@ -651,7 +649,7 @@ const operations = Object.freeze({ }, TemplateLiteral(node, initialScope) { - if (node.type !== 'TemplateLiteral') { + if (node.type !== "TemplateLiteral") { return null } const expressions = getElementValues(node.expressions, initialScope) @@ -659,7 +657,7 @@ const operations = Object.freeze({ let value = node.quasis[0]?.value.cooked for (let i = 0; i < expressions.length; ++i) { value += expressions[i] - value += node.quasis[i + 1]?.value.cooked || '' + value += node.quasis[i + 1]?.value.cooked || "" } return { value } } @@ -667,7 +665,7 @@ const operations = Object.freeze({ }, UnaryExpression(node, initialScope) { - if (node.type !== 'UnaryExpression') { + if (node.type !== "UnaryExpression") { return null } if (node.operator === "delete") { @@ -723,9 +721,14 @@ function getStaticValueR(node, initialScope) { * @returns {StaticValue|null} The static value of the property name of the node, or `null`. */ function getStaticPropertyNameValue(node, initialScope) { - const nameNode = node.type === "Property" ? node.key : ('property' in node ? node.property : undefined) - - if ('computed' in node && node.computed) { + const nameNode = + node.type === "Property" + ? node.key + : "property" in node + ? node.property + : undefined + + if ("computed" in node && node.computed) { return getStaticValueR(nameNode, initialScope) } @@ -734,7 +737,7 @@ function getStaticPropertyNameValue(node, initialScope) { } if (nameNode?.type === "Literal") { - if ('bigint' in nameNode && nameNode.bigint) { + if ("bigint" in nameNode && nameNode.bigint) { return { value: nameNode.bigint } } return { value: String(nameNode.value) } diff --git a/src/get-string-if-constant.mjs b/src/get-string-if-constant.mjs index e600dcf..f74ca7f 100644 --- a/src/get-string-if-constant.mjs +++ b/src/get-string-if-constant.mjs @@ -9,10 +9,10 @@ import { getStaticValue } from "./get-static-value.mjs" export function getStringIfConstant(node, initialScope = null) { // Handle the literals that the platform doesn't support natively. if (node && node.type === "Literal" && node.value === null) { - if ('regex' in node && node.regex) { + if ("regex" in node && node.regex) { return `/${node.regex.pattern}/${node.regex.flags}` } - if ('bigint' in node && node.bigint) { + if ("bigint" in node && node.bigint) { return node.bigint } } diff --git a/src/has-side-effect.mjs b/src/has-side-effect.mjs index 5dcabb6..026d89b 100644 --- a/src/has-side-effect.mjs +++ b/src/has-side-effect.mjs @@ -30,7 +30,12 @@ const typeConversionUnaryOps = Object.freeze(new Set(["-", "+", "!", "~"])) * @returns {x is { type: string }} `true` if the value is an ASTNode. */ function isNode(x) { - return x !== null && typeof x === "object" && 'type' in x && typeof x.type === "string" + return ( + x !== null && + typeof x === "object" && + "type" in x && + typeof x.type === "string" + ) } /** @@ -47,9 +52,9 @@ function isNode(x) { /** * @callback VisitorCallback - * @param {import('./types.mjs').Node | import('estree').Comment | import('estree').MaybeNamedClassDeclaration | import('estree').MaybeNamedFunctionDeclaration} node - * @param {VisitOptions} options - * @param {import('eslint').SourceCode.VisitorKeys | typeof KEYS} visitorKeys + * @param {import('./types.mjs').Node | import('estree').Comment | import('estree').MaybeNamedClassDeclaration | import('estree').MaybeNamedFunctionDeclaration} node + * @param {VisitOptions} options + * @param {import('eslint').SourceCode.VisitorKeys | typeof KEYS} visitorKeys * @returns {boolean} */ @@ -69,7 +74,9 @@ const visitor = { const { type, ...remainder } = node for (const key of visitorKeys[type] || getKeys(node)) { - const value = /** @type {ValuesInObjectUnion} */ (remainder[/** @type {keyof typeof remainder} */ (key)]) + const value = /** @type {ValuesInObjectUnion} */ ( + remainder[/** @type {keyof typeof remainder} */ (key)] + ) if (Array.isArray(value)) { for (const element of value) { @@ -101,7 +108,7 @@ const visitor = { }, BinaryExpression(node, options, visitorKeys) { if ( - node.type === 'BinaryExpression' && + node.type === "BinaryExpression" && options.considerImplicitTypeConversion && typeConversionBinaryOps.has(node.operator) && (node.left.type !== "Literal" || node.right.type !== "Literal") @@ -124,7 +131,7 @@ const visitor = { return true } if ( - node.type === 'MemberExpression' && + node.type === "MemberExpression" && options.considerImplicitTypeConversion && node.computed && node.property.type !== "Literal" @@ -135,7 +142,7 @@ const visitor = { }, MethodDefinition(node, options, visitorKeys) { if ( - node.type === 'MethodDefinition' && + node.type === "MethodDefinition" && options.considerImplicitTypeConversion && node.computed && node.key.type !== "Literal" @@ -149,7 +156,7 @@ const visitor = { }, Property(node, options, visitorKeys) { if ( - node.type === 'Property' && + node.type === "Property" && options.considerImplicitTypeConversion && node.computed && node.key.type !== "Literal" @@ -160,7 +167,7 @@ const visitor = { }, PropertyDefinition(node, options, visitorKeys) { if ( - node.type === 'PropertyDefinition' && + node.type === "PropertyDefinition" && options.considerImplicitTypeConversion && node.computed && node.key.type !== "Literal" @@ -170,7 +177,7 @@ const visitor = { return this.$visitChildren(node, options, visitorKeys) }, UnaryExpression(node, options, visitorKeys) { - if (node.type === 'UnaryExpression') { + if (node.type === "UnaryExpression") { if (node.operator === "delete") { return true } diff --git a/src/is-parenthesized.mjs b/src/is-parenthesized.mjs index 66c9cf6..de74b4c 100644 --- a/src/is-parenthesized.mjs +++ b/src/is-parenthesized.mjs @@ -92,7 +92,11 @@ export function isParenthesized( throw new TypeError("'times' should be a positive integer.") } // @ts-ignore - return internalIsParenthesized(timesOrNode | 0, nodeOrSourceCode, optionalSourceCode) + return internalIsParenthesized( + timesOrNode | 0, + nodeOrSourceCode, + optionalSourceCode, + ) } // @ts-ignore @@ -106,11 +110,7 @@ export function isParenthesized( * @param {import('eslint').SourceCode} sourceCode The source code object to get tokens. * @returns {boolean} `true` if the node is parenthesized the given times. */ -function internalIsParenthesized( - times, - node, - sourceCode, -) { +function internalIsParenthesized(times, node, sourceCode) { /** @type {import('eslint').Rule.Node | import('eslint').AST.Token | null} */ let maybeLeftParen = node /** @type {import('eslint').Rule.Node | import('eslint').AST.Token | null} */ @@ -119,7 +119,7 @@ function internalIsParenthesized( if ( node == null || // `Program` can't be parenthesized - !('parent' in node) || + !("parent" in node) || node.parent == null || // `CatchClause.param` can't be parenthesized, example `try {} catch (error) {}` (node.parent.type === "CatchClause" && node.parent.param === node) diff --git a/src/reference-tracker.mjs b/src/reference-tracker.mjs index a798ae1..63277f4 100644 --- a/src/reference-tracker.mjs +++ b/src/reference-tracker.mjs @@ -44,7 +44,7 @@ function isModifiedGlobal(variable) { * @returns {boolean} `true` if the node is passed through. */ function isPassThrough(node) { - const parent = 'parent' in node ? node.parent : undefined + const parent = "parent" in node ? node.parent : undefined switch (parent?.type) { case "ConditionalExpression": @@ -136,7 +136,10 @@ export class ReferenceTracker { */ *iterateCjsReferences(traceMap) { for (const { node } of this.iterateGlobalReferences(requireCall)) { - const key = 'arguments' in node && node.arguments[0] ? getStringIfConstant(node.arguments[0]) : null + const key = + "arguments" in node && node.arguments[0] + ? getStringIfConstant(node.arguments[0]) + : null if (key == null || !Object.hasOwn(traceMap, key)) { continue } @@ -169,17 +172,27 @@ export class ReferenceTracker { *iterateEsmReferences(traceMap) { const programNode = this.globalScope.block - if (!('body' in programNode) || !(Symbol.iterator in programNode.body)) { + if ( + !("body" in programNode) || + !(Symbol.iterator in programNode.body) + ) { return } for (const node of programNode.body) { - if (!IMPORT_TYPE.test(node.type) || !('source' in node) || node.source == null) { + if ( + !IMPORT_TYPE.test(node.type) || + !("source" in node) || + node.source == null + ) { continue } const moduleId = node.source.value - if (typeof moduleId !== 'string' || !Object.hasOwn(traceMap, moduleId)) { + if ( + typeof moduleId !== "string" || + !Object.hasOwn(traceMap, moduleId) + ) { continue } const nextTraceMap = traceMap[moduleId] @@ -276,11 +289,11 @@ export class ReferenceTracker { *_iteratePropertyReferences(rootNode, path, traceMap) { let node = rootNode - while (isPassThrough(node) && 'parent' in node) { + while (isPassThrough(node) && "parent" in node) { node = node.parent } - const parent = 'parent' in node ? node.parent : undefined + const parent = "parent" in node ? node.parent : undefined if (parent?.type === "MemberExpression") { if (parent.object === node) { const key = getPropertyName(parent) @@ -368,7 +381,10 @@ export class ReferenceTracker { } if (patternNode.type === "ObjectPattern") { for (const property of patternNode.properties) { - const key = property.type === 'Property' ? getPropertyName(property) : null + const key = + property.type === "Property" + ? getPropertyName(property) + : null if (key == null || !Object.hasOwn(traceMap, key)) { continue @@ -387,7 +403,7 @@ export class ReferenceTracker { info: nextTraceMap[READ], } } - if ('value' in property) { + if ("value" in property) { yield* this._iterateLhsReferences( property.value, nextPath, From 77cff12e2ea964d46aecb1a403598c346e986e79 Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 23:12:45 +0100 Subject: [PATCH 12/14] Mimic #60 and move build to lint step on CI --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1c4e93..96b37a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,9 @@ jobs: - name: โ–ถ๏ธ Run lint script run: npm run lint + - name: ๐Ÿ— Build + run: npm run build + test: name: ๐Ÿงช Test (Node@${{ matrix.node }} - ESLint@${{ matrix.eslint }} - ${{ @@ -83,9 +86,6 @@ jobs: - name: ๐Ÿ“ฅ Install ESLint v${{ matrix.eslint }} run: npm install --save-dev eslint@${{ matrix.eslint }} - - name: ๐Ÿ— Build - run: npm run build - - name: โ–ถ๏ธ Run test script run: npm run test From 33adae48eafc4368d3d2048eadaffaf1bff1c9cb Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Fri, 8 Mar 2024 23:17:31 +0100 Subject: [PATCH 13/14] Prettier broke an ignore --- src/is-parenthesized.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/is-parenthesized.mjs b/src/is-parenthesized.mjs index de74b4c..05ba4f5 100644 --- a/src/is-parenthesized.mjs +++ b/src/is-parenthesized.mjs @@ -91,9 +91,9 @@ export function isParenthesized( if (!(timesOrNode >= 1)) { throw new TypeError("'times' should be a positive integer.") } - // @ts-ignore return internalIsParenthesized( timesOrNode | 0, + // @ts-ignore nodeOrSourceCode, optionalSourceCode, ) From fece1fae83ea29eacdafa2c470098dcf9bd9699c Mon Sep 17 00:00:00 2001 From: Pelle Wessman Date: Mon, 8 Apr 2024 21:17:00 +0200 Subject: [PATCH 14/14] Update .eslintrc.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg โœจ --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 32e1597..c02bc4c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,7 +5,7 @@ module.exports = { root: true, extends: ["plugin:@eslint-community/mysticatea/es2020"], parserOptions: { - project: "./tsconfig.json", + project: true, }, rules: { semi: ["error", "never"],