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..c02bc4c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,7 +4,12 @@ module.exports = { root: true, extends: ["plugin:@eslint-community/mysticatea/es2020"], + parserOptions: { + project: true, + }, rules: { + semi: ["error", "never"], + "semi-spacing": ["error", { before: false, after: true }], "@eslint-community/mysticatea/prettier": "off", "no-restricted-properties": [ "error", @@ -18,7 +23,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/.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 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..b22a0e2 100644 --- a/package.json +++ b/package.json @@ -18,27 +18,30 @@ "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: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", @@ -50,17 +53,23 @@ }, "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", + "type-coverage": "^2.27.1", + "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..9103186 --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,39 @@ +/** + * @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" + +/** @type {{ dependencies: Record }} */ +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", + }, + ], + // type-coverage:ignore-next-line + plugins: [dts()], + }, +] diff --git a/src/find-variable.mjs b/src/find-variable.mjs index c52bf76..831f9f1 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('./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) { 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..f50d7ba 100644 --- a/src/get-function-head-location.mjs +++ b/src/get-function-head-location.mjs @@ -2,46 +2,51 @@ 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 {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. */ function getOpeningParenOfParams(node, sourceCode) { - return node.id + return "id" in node && node.id ? sourceCode.getTokenAfter(node.id, isOpeningParenToken) : 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 {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 - let start = null - let end = null + 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 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" + 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..10ac08e 100644 --- a/src/get-function-name-with-kind.mjs +++ b/src/get-function-name-with-kind.mjs @@ -2,13 +2,14 @@ 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 {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. */ // 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 = @@ -25,10 +26,10 @@ export function getFunctionNameWithKind(node, sourceCode) { tokens.push("private") } } - if (node.async) { + if ("async" in node && node.async) { tokens.push("async") } - if (node.generator) { + if ("generator" in node && node.generator) { tokens.push("generator") } @@ -68,8 +69,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..5fad463 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('./types.mjs').Node} 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,12 @@ 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 !== 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 5ae3d3a..d7df86d 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 {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 {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 node.property.name + 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 node.key.name + result = "name" in node.key ? node.key.name : null // no default } - return null + return result } diff --git a/src/get-static-value.mjs b/src/get-static-value.mjs index 074f298..5cb9232 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.mjs @@ -2,13 +2,18 @@ import { findVariable } from "./find-variable.mjs" +/** @type {Record} */ const globalObject = typeof globalThis !== "undefined" ? globalThis - : typeof self !== "undefined" - ? self - : typeof window !== "undefined" - ? window + : // @ts-ignore + typeof self !== "undefined" + ? // @ts-ignore + self + : // @ts-ignore + typeof window !== "undefined" + ? // @ts-ignore + window : typeof global !== "undefined" ? global : {} @@ -95,6 +100,7 @@ const callAllowed = new Set( escape, isFinite, isNaN, + // @ts-ignore isPrototypeOf, Map, Map.prototype.entries, @@ -104,6 +110,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, @@ -219,8 +226,8 @@ 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('./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. */ function getElementValues(nodeList, initialScope) { @@ -266,13 +273,27 @@ function isEffectivelyConst(variable) { return false } +/** + * @callback VisitorCallback + * @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") { + 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) } @@ -281,6 +302,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 @@ -338,7 +362,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) @@ -392,6 +420,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 @@ -402,10 +433,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) @@ -423,7 +460,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)) && @@ -438,8 +475,15 @@ 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 } @@ -447,6 +491,9 @@ const operations = Object.freeze({ }, LogicalExpression(node, initialScope) { + if (node.type !== "LogicalExpression") { + return null + } const left = getStaticValueR(node.left, initialScope) if (left != null) { if ( @@ -467,6 +514,9 @@ const operations = Object.freeze({ }, MemberExpression(node, initialScope) { + if (node.type !== "MemberExpression") { + return null + } if (node.property.type === "PrivateIdentifier") { return null } @@ -487,6 +537,7 @@ const operations = Object.freeze({ object.value instanceof classFn && allowed.has(property.value) ) { + // @ts-ignore return { value: object.value[property.value] } } } @@ -496,6 +547,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 } @@ -504,6 +558,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) @@ -518,6 +575,10 @@ const operations = Object.freeze({ }, ObjectExpression(node, initialScope) { + if (node.type !== "ObjectExpression") { + return null + } + /** @type {Record} */ const object = {} for (const propertyNode of node.properties) { @@ -536,6 +597,7 @@ const operations = Object.freeze({ object[key.value] = value.value } else if ( propertyNode.type === "SpreadElement" || + // @ts-ignore propertyNode.type === "ExperimentalSpreadProperty" ) { const argument = getStaticValueR( @@ -555,11 +617,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, @@ -568,6 +636,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) @@ -580,12 +649,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 } } @@ -593,6 +665,9 @@ const operations = Object.freeze({ }, UnaryExpression(node, initialScope) { + if (node.type !== "UnaryExpression") { + return null + } if (node.operator === "delete") { // Not supported. return null @@ -623,38 +698,46 @@ 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 {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`. */ 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 } /** * 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('./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`. */ function getStaticPropertyNameValue(node, initialScope) { - const nameNode = node.type === "Property" ? node.key : node.property - - if (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) } - 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) } @@ -665,13 +748,13 @@ 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('./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`. */ 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..f74ca7f 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('./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`. */ 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 && node.regex) { return `/${node.regex.pattern}/${node.regex.flags}` } - if (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 3d792f3..026d89b 100644 --- a/src/has-side-effect.mjs +++ b/src/has-side-effect.mjs @@ -26,126 +26,158 @@ 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 { type: string }} `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 +/** + * @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 + */ - if (typeof this[type] === "function") { - return this[type](node, options, visitorKeys) - } +/** + * @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. + */ - return this.$visitChildren(node, options, visitorKeys) - }, +/** + * @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 + * @returns {boolean} + */ + +/** @type {Partial> & Record<'$visit' | '$visitChildren', VisitorCallback>} */ +const visitor = { + $visit(node, options, visitorKeys) { + const match = this[node.type] - $visitChildren(node, options, visitorKeys) { - const { type } = node + if (typeof match === "function") { + return match.call(this, node, options, visitorKeys) + } - for (const key of visitorKeys[type] || getKeys(node)) { - const value = node[key] + return this.$visitChildren(node, options, visitorKeys) + }, - if (Array.isArray(value)) { - for (const element of value) { - if ( - isNode(element) && - this.$visit(element, options, visitorKeys) - ) { - return true - } + $visitChildren(node, options, visitorKeys) { + const { type, ...remainder } = node + + for (const key of visitorKeys[type] || getKeys(node)) { + const value = /** @type {ValuesInObjectUnion} */ ( + remainder[/** @type {keyof typeof remainder} */ (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 +188,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..05ba4f5 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,64 @@ 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 + return internalIsParenthesized( + timesOrNode | 0, + // @ts-ignore + 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) { + /** @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 || // `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) @@ -102,7 +127,6 @@ export function isParenthesized( return false } - maybeLeftParen = maybeRightParen = node do { maybeLeftParen = sourceCode.getTokenBefore(maybeLeftParen) maybeRightParen = sourceCode.getTokenAfter(maybeRightParen) @@ -113,6 +137,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..6bc4c0a 100644 --- a/src/pattern-matcher.mjs +++ b/src/pattern-matcher.mjs @@ -30,11 +30,12 @@ function isEscaped(str, index) { * @returns {string} The replaced string. */ function replaceS(matcher, str, replacement) { + /** @type {string[]} */ const chunks = [] let index = 0 /** @type {RegExpExecArray} */ - let match = null + let match /** * @param {string} key The placeholder. @@ -51,11 +52,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,10 +72,11 @@ 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|number)[])=>string} replace The function to replace each matched part. * @returns {string} The replaced string. */ function replaceF(matcher, str, replace) { + /** @type {string[]} */ const chunks = [] let index = 0 @@ -98,7 +97,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 +119,15 @@ 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) || {} + /** @type {RegExpExecArray|null} */ let match = null let lastIndex = 0 + if (!pattern) { + return + } + pattern.lastIndex = 0 while ((match = pattern.exec(str)) != null) { if (escaped || !isEscaped(str, match.index)) { @@ -148,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) { diff --git a/src/reference-tracker.mjs b/src/reference-tracker.mjs index 3f14b28..63277f4 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,22 @@ export const ESM = Symbol("esm") 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 {RichNode} node + * @property {string[]} path + * @property {ReferenceType} type + * @property {unknown} 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) { @@ -28,13 +40,13 @@ 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 {RichNode} node A node to check. * @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 +67,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 +79,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 +112,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 +131,26 @@ 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 +165,40 @@ 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 +208,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 +219,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 +250,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 +280,32 @@ export class ReferenceTracker { /** * Iterate the references for a given AST node. - * @param rootNode The AST node to iterate references. + * @param {RichNode} 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 +322,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 +339,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) } @@ -316,10 +361,10 @@ export class ReferenceTracker { /** * Iterate the references for a given Pattern node. - * @param {Node} patternNode The Pattern node to iterate references. + * @param {RichNode} 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") { @@ -336,14 +381,20 @@ 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 || !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 +403,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 } @@ -367,10 +420,10 @@ export class ReferenceTracker { /** * Iterate the references for a given ModuleSpecifier node. - * @param {Node} specifierNode The ModuleSpecifier node to iterate references. + * @param {RichNode} 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 +433,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 +450,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/src/types.mjs b/src/types.mjs new file mode 100644 index 0000000..0820970 --- /dev/null +++ b/src/types.mjs @@ -0,0 +1 @@ +/** @typedef {import('estree').Node | import('estree').Expression} Node */ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ed04eb3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "files": ["rollup.config.mjs"], + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["es2022"], + "target": "es2022", + + "module": "nodenext", + "moduleResolution": "nodenext", + + "strict": true, + + "skipLibCheck": true, + + /* Clean up generated declarations */ + "removeComments": true, + "stripInternal": true, + + /* Make it a JS-targeted config */ + "allowJs": true, + "checkJs": true, + "noEmit": true, + + /* Extra strictness */ + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noUncheckedIndexedAccess": true, + + /* Additional non-type checks */ + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true + } +}