diff --git a/.c8rc.json b/.c8rc.json index c93196c..f0bdf92 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -1,4 +1,4 @@ { - "include": ["src/**/*.mjs"], + "include": ["src/**/*.ts"], "reporter": ["lcov", "text-summary"] } 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 f08cdea..619c71d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,18 +4,32 @@ module.exports = { root: true, extends: ["plugin:@eslint-community/mysticatea/es2020"], + parserOptions: { + project: "./tsconfig.json", + }, rules: { "@eslint-community/mysticatea/prettier": "off", + "@eslint-community/mysticatea/ts/naming-convention": "off", + "@eslint-community/mysticatea/ts/prefer-readonly-parameter-types": + "off", + "@eslint-community/mysticatea/node/no-missing-import": [ + "error", + { allowModules: ["estree", "unbuild"] }, + ], + }, + settings: { + node: { + tryExtensions: [".js", ".json", ".mjs", ".node", ".ts", ".tsx"], + }, }, overrides: [ { - files: ["src/**/*.mjs", "test/**/*.mjs"], + files: ["src/**/*.ts", "test/**/*.ts"], extends: ["plugin:@eslint-community/mysticatea/+modules"], rules: { "init-declarations": "off", - - "@eslint-community/mysticatea/node/no-unsupported-features/es-syntax": - ["error", { ignores: ["modules"] }], + "no-duplicate-imports": "off", + "no-shadow": "off", }, }, ], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c34526..7a8ba30 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/.vscode/settings.json b/.vscode/settings.json index 66662a4..b4e2b44 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,15 @@ { + "eslint.validate": ["javascript", "typescript"], "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": ["source.organizeImports", "source.fixAll"] }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": ["source.organizeImports", "source.fixAll"] + }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 0000000..172ae50 --- /dev/null +++ b/build.config.ts @@ -0,0 +1,14 @@ +import { defineBuildConfig } from "unbuild" + +export default defineBuildConfig({ + externals: ["estree"], + hooks: { + "rollup:options"(_ctx, options) { + for (const output of [options.output].flat()) { + if (output!.format === "cjs") { + output!.exports = "named" + } + } + }, + }, +}) diff --git a/package.json b/package.json index 9933426..8734dae 100644 --- a/package.json +++ b/package.json @@ -18,19 +18,20 @@ "sideEffects": false, "exports": { ".": { - "import": "./index.mjs", - "require": "./index.js" + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" }, "./package.json": "./package.json" }, - "main": "index", - "module": "index.mjs", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", "files": [ - "index.*" + "dist" ], "scripts": { "prebuild": "npm run -s clean", - "build": "rollup -c", + "build": "unbuild", "clean": "rimraf .nyc_output coverage index.*", "coverage": "opener ./coverage/lcov-report/index.html", "docs:build": "vitepress build docs", @@ -38,29 +39,35 @@ "format": "npm run -s format:prettier -- --write", "format:prettier": "prettier .", "format:check": "npm run -s format:prettier -- --check", - "lint": "eslint .", - "test": "c8 mocha --reporter dot \"test/*.mjs\"", + "lint": "tsc && eslint .", + "test": "c8 mocha --require=esbuild-register --reporter dot \"test/*.ts\"", "preversion": "npm test && npm run -s build", "postversion": "git push && git push --tags", "prewatch": "npm run -s clean", - "watch": "warun \"{src,test}/**/*.mjs\" -- npm run -s test:mocha" + "watch": "warun \"{src,test}/**/*.ts\" -- npm run -s test:mocha" }, "dependencies": { "eslint-visitor-keys": "^3.4.2" }, "devDependencies": { "@eslint-community/eslint-plugin-mysticatea": "^15.5.0", + "@types/eslint": "^8.21.0", + "@types/estree": "^1.0.0", + "@types/mocha": "^10.0.1", + "@types/node": "^12.20.55", + "@typescript-eslint/types": "^5.50.0", "c8": "^8.0.1", "dot-prop": "^7.2.0", + "esbuild-register": "^3.4.2", "eslint": "^8.46.0", "mocha": "^9.2.2", "npm-run-all": "^4.1.5", "opener": "^1.5.2", "prettier": "2.8.8", "rimraf": "^3.0.2", - "rollup": "^2.79.1", - "rollup-plugin-sourcemaps": "^0.6.3", "semver": "^7.5.4", + "typescript": "^4.9.5", + "unbuild": "^1.1.1", "vitepress": "^1.0.0-beta.7", "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/src/find-variable.mjs b/src/find-variable.mjs deleted file mode 100644 index c52bf76..0000000 --- a/src/find-variable.mjs +++ /dev/null @@ -1,29 +0,0 @@ -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. - */ -export function findVariable(initialScope, nameOrNode) { - let name = "" - let scope = initialScope - - if (typeof nameOrNode === "string") { - name = nameOrNode - } else { - name = nameOrNode.name - scope = getInnermostScope(scope, nameOrNode) - } - - while (scope != null) { - const variable = scope.set.get(name) - if (variable != null) { - return variable - } - scope = scope.upper - } - - return null -} diff --git a/src/find-variable.ts b/src/find-variable.ts new file mode 100644 index 0000000..df8cfac --- /dev/null +++ b/src/find-variable.ts @@ -0,0 +1,34 @@ +import type { Scope } from "eslint" +import type * as ESTree from "estree" +import { getInnermostScope } from "./get-innermost-scope" + +/** + * Find the variable of a given name. + * @param initialScope The scope to start finding. + * @param nameOrNode The variable name to find. If this is a Node object then it should be an Identifier node. + * @returns The found variable or null. + */ +export function findVariable( + initialScope: Scope.Scope, + nameOrNode: ESTree.Identifier | string, +): Scope.Variable | null { + let name = "" + let scope: Scope.Scope | null = initialScope + + if (typeof nameOrNode === "string") { + name = nameOrNode + } else { + name = nameOrNode.name + scope = getInnermostScope(scope, nameOrNode) + } + + while (scope != null) { + const variable = scope.set.get(name) + if (variable != null) { + return variable + } + scope = scope.upper + } + + return null +} diff --git a/src/get-function-head-location.mjs b/src/get-function-head-location.mjs deleted file mode 100644 index 6e0b79d..0000000 --- a/src/get-function-head-location.mjs +++ /dev/null @@ -1,47 +0,0 @@ -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. - */ -function getOpeningParenOfParams(node, sourceCode) { - return 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. - */ -export function getFunctionHeadLocation(node, sourceCode) { - const parent = node.parent - let start = null - let end = null - - if (node.type === "ArrowFunctionExpression") { - const arrowToken = sourceCode.getTokenBefore(node.body, isArrowToken) - - 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 - } else { - start = node.loc.start - end = getOpeningParenOfParams(node, sourceCode).loc.start - } - - return { - start: { ...start }, - end: { ...end }, - } -} diff --git a/src/get-function-head-location.ts b/src/get-function-head-location.ts new file mode 100644 index 0000000..abbac78 --- /dev/null +++ b/src/get-function-head-location.ts @@ -0,0 +1,56 @@ +import type { SourceCode } from "eslint" +import type * as ESTree from "estree" +import { getParent } from "./get-parent" +import { isArrowToken, isOpeningParenToken } from "./token-predicate" + +/** + * Get the `(` token of the given function node. + * @param node - The function node to get. + * @param sourceCode - The source code object to get tokens. + * @returns `(` token. + */ +function getOpeningParenOfParams( + node: ESTree.Function, + sourceCode: SourceCode, +) { + return node.type !== "ArrowFunctionExpression" && node.id + ? sourceCode.getTokenAfter(node.id, isOpeningParenToken)! + : sourceCode.getFirstToken(node, isOpeningParenToken)! +} + +/** + * Get the location of the given function node for reporting. + * @param node - The function node to get. + * @param sourceCode - The source code object to get tokens. + * @returns The location of the function node for reporting. + */ +export function getFunctionHeadLocation( + node: ESTree.Function, + sourceCode: SourceCode, +): ESTree.SourceLocation { + const parent = getParent(node)! + let start: ESTree.Position | null = null + let end: ESTree.Position | null = null + + if (node.type === "ArrowFunctionExpression") { + const arrowToken = sourceCode.getTokenBefore(node.body, isArrowToken)! + + 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 + } else { + start = node.loc!.start + end = getOpeningParenOfParams(node, sourceCode).loc.start + } + + return { + start: { ...start }, + end: { ...end }, + } +} diff --git a/src/get-function-name-with-kind.mjs b/src/get-function-name-with-kind.ts similarity index 81% rename from src/get-function-name-with-kind.mjs rename to src/get-function-name-with-kind.ts index bf8e17c..620a7c2 100644 --- a/src/get-function-name-with-kind.mjs +++ b/src/get-function-name-with-kind.ts @@ -1,14 +1,20 @@ -import { getPropertyName } from "./get-property-name.mjs" +import type { SourceCode } from "eslint" +import type * as ESTree from "estree" +import { getParent } from "./get-parent" +import { getPropertyName } from "./get-property-name" /** * 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. - * @returns {string} The name and kind of the function node. + * @param node - The function node to get. + * @param sourceCode The source code object to get the code of computed property keys. + * @returns The name and kind of the function node. */ // eslint-disable-next-line complexity -export function getFunctionNameWithKind(node, sourceCode) { - const parent = node.parent +export function getFunctionNameWithKind( + node: ESTree.Function, + sourceCode?: SourceCode, +): string { + const parent = getParent(node)! const tokens = [] const isObjectMethod = parent.type === "Property" && parent.value === node const isClassMethod = @@ -68,7 +74,7 @@ export function getFunctionNameWithKind(node, sourceCode) { } } } - } else if (node.id) { + } else if (node.type !== "ArrowFunctionExpression" && node.id) { tokens.push(`'${node.id.name}'`) } else if ( parent.type === "VariableDeclarator" && diff --git a/src/get-innermost-scope.mjs b/src/get-innermost-scope.ts similarity index 51% rename from src/get-innermost-scope.mjs rename to src/get-innermost-scope.ts index d62ec69..ce3cff8 100644 --- a/src/get-innermost-scope.mjs +++ b/src/get-innermost-scope.ts @@ -1,18 +1,24 @@ +import type { Scope } from "eslint" +import type * as ESTree from "estree" + /** * 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 initialScope The initial scope to search. + * @param node The location to search. + * @returns The innermost scope. */ -export function getInnermostScope(initialScope, node) { - const location = node.range[0] +export function getInnermostScope( + initialScope: Scope.Scope, + node: ESTree.Node, +): Scope.Scope { + const location = node.range![0] let scope = initialScope let found = false do { found = false for (const childScope of scope.childScopes) { - const range = childScope.block.range + const range = childScope.block.range! if (range[0] <= location && location < range[1]) { scope = childScope diff --git a/src/get-parent.ts b/src/get-parent.ts new file mode 100644 index 0000000..955a6c2 --- /dev/null +++ b/src/get-parent.ts @@ -0,0 +1,5 @@ +import type * as ESTree from "estree" +export function getParent(node: ESTree.Node): ESTree.Node | null { + // eslint-disable-next-line @eslint-community/mysticatea/ts/no-unsafe-member-access + return (node as any).parent as ESTree.Node | null +} diff --git a/src/get-property-name.mjs b/src/get-property-name.mjs deleted file mode 100644 index 5ae3d3a..0000000 --- a/src/get-property-name.mjs +++ /dev/null @@ -1,38 +0,0 @@ -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. - * @returns {string|null} The property name of the node. - */ -export function getPropertyName(node, initialScope) { - switch (node.type) { - case "MemberExpression": - if (node.computed) { - return getStringIfConstant(node.property, initialScope) - } - if (node.property.type === "PrivateIdentifier") { - return null - } - return node.property.name - - case "Property": - case "MethodDefinition": - case "PropertyDefinition": - if (node.computed) { - return getStringIfConstant(node.key, initialScope) - } - if (node.key.type === "Literal") { - return String(node.key.value) - } - if (node.key.type === "PrivateIdentifier") { - return null - } - return node.key.name - - // no default - } - - return null -} diff --git a/src/get-property-name.ts b/src/get-property-name.ts new file mode 100644 index 0000000..d84cbc5 --- /dev/null +++ b/src/get-property-name.ts @@ -0,0 +1,45 @@ +import type { Scope } from "eslint" +import type * as ESTree from "estree" +import { getStringIfConstant } from "./get-string-if-constant" + +/** + * Get the property name from a MemberExpression node or a Property node. + * @param node The node to get. + * @param 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 The property name of the node. + */ +export function getPropertyName( + node: + | ESTree.MemberExpression + | ESTree.MethodDefinition + | ESTree.Property + | ESTree.PropertyDefinition, + initialScope?: Scope.Scope, +): string | null { + switch (node.type) { + case "MemberExpression": + if (node.computed) { + return getStringIfConstant(node.property, initialScope) + } + if (node.property.type === "PrivateIdentifier") { + return null + } + return (node.property as ESTree.Identifier).name + + case "Property": + case "MethodDefinition": + case "PropertyDefinition": + if (node.computed) { + return getStringIfConstant(node.key, initialScope) + } + if (node.key.type === "Literal") { + return String(node.key.value) + } + if (node.key.type === "PrivateIdentifier") { + return null + } + return (node.key as ESTree.Identifier).name + default: + return null + } +} diff --git a/src/get-static-value.mjs b/src/get-static-value.ts similarity index 74% rename from src/get-static-value.mjs rename to src/get-static-value.ts index 074f298..f8ba115 100644 --- a/src/get-static-value.mjs +++ b/src/get-static-value.ts @@ -1,8 +1,23 @@ -/* globals globalThis, global, self, window */ - -import { findVariable } from "./find-variable.mjs" - -const globalObject = +/* globals globalThis, global */ +/* eslint + @eslint-community/mysticatea/eslint-comments/no-use:0, + @eslint-community/mysticatea/ts/no-unsafe-assignment:0, + @eslint-community/mysticatea/ts/no-unsafe-argument:0, + @eslint-community/mysticatea/ts/no-unsafe-return:0, + @eslint-community/mysticatea/ts/no-unsafe-member-access:0, + @eslint-community/mysticatea/ts/no-unsafe-call:0, + --- + The logic within this source makes heavy use of any. +*/ +import type { Scope } from "eslint" +import type * as ESTree from "estree" +import { findVariable } from "./find-variable" + +type AnyFunction = (...any: any[]) => any +declare const self: typeof globalThis +declare const window: typeof globalThis + +const globalObject: Record = typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" @@ -63,7 +78,7 @@ const builtinNames = Object.freeze( "WeakSet", ]), ) -const callAllowed = new Set( +const callAllowed = new Set( [ Array.isArray, Array.of, @@ -95,14 +110,14 @@ const callAllowed = new Set( escape, isFinite, isNaN, - isPrototypeOf, + globalObject.isPrototypeOf, Map, Map.prototype.entries, Map.prototype.get, Map.prototype.has, Map.prototype.keys, Map.prototype.values, - ...Object.getOwnPropertyNames(Math) + ...(Object.getOwnPropertyNames(Math) as (keyof Math)[]) .filter((k) => k !== "random") .map((k) => Math[k]) .filter((f) => typeof f === "function"), @@ -162,7 +177,7 @@ const callAllowed = new Set( Symbol.for, Symbol.keyFor, unescape, - ].filter((f) => typeof f === "function"), + ].filter((f): f is AnyFunction => typeof f === "function"), ) const callPassThrough = new Set([ Object.freeze, @@ -170,8 +185,7 @@ const callPassThrough = new Set([ Object.seal, ]) -/** @type {ReadonlyArray]>} */ -const getterAllowed = [ +const getterAllowed: readonly (readonly [Function, ReadonlySet])[] = [ [Map, new Set(["size"])], [ RegExp, @@ -192,10 +206,10 @@ const getterAllowed = [ /** * Get the property descriptor. - * @param {object} object The object to get. - * @param {string|number|symbol} name The property name to get. + * @param object The object to get. + * @param name The property name to get. */ -function getPropertyDescriptor(object, name) { +function getPropertyDescriptor(object: any, name: number | string | symbol) { let x = object while ((typeof x === "object" || typeof x === "function") && x !== null) { const d = Object.getOwnPropertyDescriptor(x, name) @@ -209,21 +223,24 @@ function getPropertyDescriptor(object, name) { /** * Check if a property is getter or not. - * @param {object} object The object to check. - * @param {string|number|symbol} name The property name to check. + * @param object The object to check. + * @param name The property name to check. */ -function isGetter(object, name) { +function isGetter(object: any, name: number | string | symbol) { const d = getPropertyDescriptor(object, name) - return d != null && d.get != null + return d?.get != null } /** * 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 nodeList The node list to get values. + * @param initialScope The initial scope to find variables. * @returns {any[]|null} The value list if all nodes are constant. Otherwise, null. */ -function getElementValues(nodeList, initialScope) { +function getElementValues( + nodeList: (ESTree.Node | null)[], + initialScope: Scope.Scope | null, +): any[] | null { const valueList = [] for (let i = 0; i < nodeList.length; ++i) { @@ -236,7 +253,7 @@ function getElementValues(nodeList, initialScope) { if (argument == null) { return null } - valueList.push(...argument.value) + valueList.push(...(argument.value as any[])) } else { const element = getStaticValueR(elementNode, initialScope) if (element == null) { @@ -254,7 +271,7 @@ function getElementValues(nodeList, initialScope) { * @param {import("eslint").Scope.Variable} variable * @returns {boolean} */ -function isEffectivelyConst(variable) { +function isEffectivelyConst(variable: Scope.Variable): boolean { const refs = variable.references const inits = refs.filter((r) => r.init).length @@ -267,12 +284,18 @@ function isEffectivelyConst(variable) { } const operations = Object.freeze({ - ArrayExpression(node, initialScope) { + ArrayExpression( + node: ESTree.ArrayExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const elements = getElementValues(node.elements, initialScope) return elements != null ? { value: elements } : null }, - AssignmentExpression(node, initialScope) { + AssignmentExpression( + node: ESTree.AssignmentExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { if (node.operator === "=") { return getStaticValueR(node.right, initialScope) } @@ -280,7 +303,10 @@ const operations = Object.freeze({ }, //eslint-disable-next-line complexity - BinaryExpression(node, initialScope) { + BinaryExpression( + node: ESTree.BinaryExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { if (node.operator === "in" || node.operator === "instanceof") { // Not supported. return null @@ -313,6 +339,7 @@ const operations = Object.freeze({ case ">>>": return { value: left.value >>> right.value } case "+": + // eslint-disable-next-line @eslint-community/mysticatea/ts/restrict-plus-operands return { value: left.value + right.value } case "-": return { value: left.value - right.value } @@ -338,7 +365,10 @@ const operations = Object.freeze({ return null }, - CallExpression(node, initialScope) { + CallExpression( + node: ESTree.SimpleCallExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const calleeNode = node.callee const args = getElementValues(node.arguments, initialScope) @@ -391,7 +421,10 @@ const operations = Object.freeze({ return null }, - ConditionalExpression(node, initialScope) { + ConditionalExpression( + node: ESTree.ConditionalExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const test = getStaticValueR(node.test, initialScope) if (test != null) { return test.value @@ -401,11 +434,17 @@ const operations = Object.freeze({ return null }, - ExpressionStatement(node, initialScope) { + ExpressionStatement( + node: ESTree.ExpressionStatement, + initialScope: Scope.Scope | null, + ): StaticValue | null { return getStaticValueR(node.expression, initialScope) }, - Identifier(node, initialScope) { + Identifier( + node: ESTree.Identifier, + initialScope: Scope.Scope | null, + ): StaticValue | null { if (initialScope != null) { const variable = findVariable(initialScope, node) @@ -423,6 +462,7 @@ const operations = Object.freeze({ if (variable != null && variable.defs.length === 1) { const def = variable.defs[0] if ( + def.type === "Variable" && def.parent && def.type === "Variable" && (def.parent.kind === "const" || @@ -430,28 +470,38 @@ const operations = Object.freeze({ // TODO(mysticatea): don't support destructuring here. def.node.id.type === "Identifier" ) { - return getStaticValueR(def.node.init, initialScope) + return getStaticValueR(def.node.init!, initialScope) } } } return null }, - Literal(node) { + Literal( + node: ESTree.Literal, + initialScope: Scope.Scope | null, + ): StaticValue | 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 } return { value: node.value } }, - LogicalExpression(node, initialScope) { + LogicalExpression( + node: ESTree.LogicalExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const left = getStaticValueR(node.left, initialScope) if (left != null) { if ( - (node.operator === "||" && Boolean(left.value) === true) || - (node.operator === "&&" && Boolean(left.value) === false) || + (node.operator === "||" && Boolean(left.value)) || + (node.operator === "&&" && !left.value) || (node.operator === "??" && left.value != null) ) { return left @@ -466,7 +516,10 @@ const operations = Object.freeze({ return null }, - MemberExpression(node, initialScope) { + MemberExpression( + node: ESTree.MemberExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { if (node.property.type === "PrivateIdentifier") { return null } @@ -487,7 +540,7 @@ const operations = Object.freeze({ object.value instanceof classFn && allowed.has(property.value) ) { - return { value: object.value[property.value] } + return { value: (object.value as any)[property.value] } } } } @@ -495,7 +548,10 @@ const operations = Object.freeze({ return null }, - ChainExpression(node, initialScope) { + ChainExpression( + node: ESTree.ChainExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const expression = getStaticValueR(node.expression, initialScope) if (expression != null) { return { value: expression.value } @@ -503,7 +559,10 @@ const operations = Object.freeze({ return null }, - NewExpression(node, initialScope) { + NewExpression( + node: ESTree.NewExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const callee = getStaticValueR(node.callee, initialScope) const args = getElementValues(node.arguments, initialScope) @@ -517,8 +576,11 @@ const operations = Object.freeze({ return null }, - ObjectExpression(node, initialScope) { - const object = {} + ObjectExpression( + node: ESTree.ObjectExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { + const object: Record = {} for (const propertyNode of node.properties) { if (propertyNode.type === "Property") { @@ -536,7 +598,8 @@ const operations = Object.freeze({ object[key.value] = value.value } else if ( propertyNode.type === "SpreadElement" || - propertyNode.type === "ExperimentalSpreadProperty" + // Experimental node type + (propertyNode as any).type === "ExperimentalSpreadProperty" ) { const argument = getStaticValueR( propertyNode.argument, @@ -554,12 +617,18 @@ const operations = Object.freeze({ return { value: object } }, - SequenceExpression(node, initialScope) { + SequenceExpression( + node: ESTree.SequenceExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const last = node.expressions[node.expressions.length - 1] return getStaticValueR(last, initialScope) }, - TaggedTemplateExpression(node, initialScope) { + TaggedTemplateExpression( + node: ESTree.TaggedTemplateExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { const tag = getStaticValueR(node.tag, initialScope) const expressions = getElementValues( node.quasi.expressions, @@ -568,7 +637,9 @@ const operations = Object.freeze({ if (tag != null && expressions != null) { const func = tag.value - const strings = node.quasi.quasis.map((q) => q.value.cooked) + const strings = node.quasi.quasis.map( + (q) => q.value.cooked, + ) as string[] & { raw: readonly string[] } strings.raw = node.quasi.quasis.map((q) => q.value.raw) if (func === String.raw) { @@ -579,20 +650,26 @@ const operations = Object.freeze({ return null }, - TemplateLiteral(node, initialScope) { + TemplateLiteral( + node: ESTree.TemplateLiteral, + initialScope: Scope.Scope | null, + ): StaticValue | null { const expressions = getElementValues(node.expressions, initialScope) if (expressions != null) { 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 } } return null }, - UnaryExpression(node, initialScope) { + UnaryExpression( + node: ESTree.UnaryExpression, + initialScope: Scope.Scope | null, + ): StaticValue | null { if (node.operator === "delete") { // Not supported. return null @@ -623,15 +700,29 @@ const operations = Object.freeze({ }, }) +type StaticValueR = + | { + optional: true + value: undefined + } + | { + optional?: false + value: any + } + /** * 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 The node to get. + * @param initialScope The scope to start finding variable. + * @returns The static value of the node, or `null`. */ -function getStaticValueR(node, initialScope) { +function getStaticValueR( + node: ESTree.Node, + initialScope: Scope.Scope | null, +): StaticValueR | null { if (node != null && Object.hasOwnProperty.call(operations, node.type)) { - return operations[node.type](node, initialScope) + const get = operations[node.type as keyof typeof operations] + return get(node as never, initialScope) } return null } @@ -642,7 +733,10 @@ function getStaticValueR(node, initialScope) { * @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`. */ -function getStaticPropertyNameValue(node, initialScope) { +function getStaticPropertyNameValue( + node: ESTree.MemberExpression | ESTree.Property, + initialScope: Scope.Scope | null, +) { const nameNode = node.type === "Property" ? node.key : node.property if (node.computed) { @@ -654,7 +748,7 @@ function getStaticPropertyNameValue(node, initialScope) { } if (nameNode.type === "Literal") { - if (nameNode.bigint) { + if ("bigint" in nameNode && nameNode.bigint) { return { value: nameNode.bigint } } return { value: String(nameNode.value) } @@ -663,13 +757,25 @@ function getStaticPropertyNameValue(node, initialScope) { return null } +export type StaticValue = StaticValueOptional | StaticValueProvided +export type StaticValueProvided = { + value: unknown +} +export type StaticValueOptional = { + optional?: true + value: undefined +} + /** * 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 node The node to get. + * @param 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 The static value of the node, or `null`. */ -export function getStaticValue(node, initialScope = null) { +export function getStaticValue( + node: ESTree.Node, + initialScope: Scope.Scope | null = null, +): StaticValue | null { try { return getStaticValueR(node, initialScope) } catch (_error) { diff --git a/src/get-string-if-constant.mjs b/src/get-string-if-constant.mjs deleted file mode 100644 index ab03363..0000000 --- a/src/get-string-if-constant.mjs +++ /dev/null @@ -1,22 +0,0 @@ -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. - * @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) { - return `/${node.regex.pattern}/${node.regex.flags}` - } - if (node.bigint) { - return node.bigint - } - } - - const evaluated = getStaticValue(node, initialScope) - return evaluated && String(evaluated.value) -} diff --git a/src/get-string-if-constant.ts b/src/get-string-if-constant.ts new file mode 100644 index 0000000..a2573ac --- /dev/null +++ b/src/get-string-if-constant.ts @@ -0,0 +1,27 @@ +import type { Scope } from "eslint" +import type * as ESTree from "estree" +import { getStaticValue } from "./get-static-value" + +/** + * Get the value of a given node if it's a literal or a template literal. + * @param node The node to get. + * @param 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 The value of the node, or `null`. + */ +export function getStringIfConstant( + node: ESTree.Node, + initialScope: Scope.Scope | null = null, +): string | 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) { + return `/${node.regex.pattern}/${node.regex.flags}` + } + if ("bigint" in node && node.bigint) { + return node.bigint + } + } + + const evaluated = getStaticValue(node, initialScope) + return evaluated && String(evaluated.value) +} diff --git a/src/has-side-effect.mjs b/src/has-side-effect.ts similarity index 56% rename from src/has-side-effect.mjs rename to src/has-side-effect.ts index 3d792f3..4434b92 100644 --- a/src/has-side-effect.mjs +++ b/src/has-side-effect.ts @@ -1,4 +1,7 @@ +import type { SourceCode } from "eslint" +import type { VisitorKeys } from "eslint-visitor-keys" import { getKeys, KEYS } from "eslint-visitor-keys" +import type * as ESTree from "estree" const typeConversionBinaryOps = Object.freeze( new Set([ @@ -26,30 +29,44 @@ 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 x The value to check. + * @returns `true` if the value is an ASTNode. */ -function isNode(x) { +function isNode(x: any): x is ESTree.Node { + // eslint-disable-next-line @eslint-community/mysticatea/ts/no-unsafe-member-access return x !== null && typeof x === "object" && typeof x.type === "string" } -const visitor = Object.freeze( - Object.assign(Object.create(null), { - $visit(node, options, visitorKeys) { - const { type } = node +function freeze(o: T): T { + return Object.freeze(o) as T +} - if (typeof this[type] === "function") { - return this[type](node, options, visitorKeys) +const visitor = freeze( + Object.assign(Object.create(null) as {}, { + $visit( + node: ESTree.Node, + options: Required, + visitorKeys: VisitorKeys, + ): boolean { + const type = node.type as keyof typeof visitor + + if (this[type]) { + return this[type](node as never, options, visitorKeys) } return this.$visitChildren(node, options, visitorKeys) }, - $visitChildren(node, options, visitorKeys) { + $visitChildren( + node: ESTree.Node, + options: Required, + visitorKeys: VisitorKeys, + ): boolean { const { type } = node for (const key of visitorKeys[type] || getKeys(node)) { - const value = node[key] + // eslint-disable-next-line @eslint-community/mysticatea/ts/no-unsafe-assignment, @eslint-community/mysticatea/ts/no-unsafe-member-access + const value = (node as any)[key] if (Array.isArray(value)) { for (const element of value) { @@ -80,7 +97,11 @@ const visitor = Object.freeze( AwaitExpression() { return true }, - BinaryExpression(node, options, visitorKeys) { + BinaryExpression( + node: ESTree.BinaryExpression, + options: Required, + visitorKeys: VisitorKeys, + ) { if ( options.considerImplicitTypeConversion && typeConversionBinaryOps.has(node.operator) && @@ -99,7 +120,11 @@ const visitor = Object.freeze( ImportExpression() { return true }, - MemberExpression(node, options, visitorKeys) { + MemberExpression( + node: ESTree.MemberExpression, + options: Required, + visitorKeys: VisitorKeys, + ) { if (options.considerGetters) { return true } @@ -112,7 +137,11 @@ const visitor = Object.freeze( } return this.$visitChildren(node, options, visitorKeys) }, - MethodDefinition(node, options, visitorKeys) { + MethodDefinition( + node: ESTree.MethodDefinition, + options: Required, + visitorKeys: VisitorKeys, + ) { if ( options.considerImplicitTypeConversion && node.computed && @@ -125,7 +154,11 @@ const visitor = Object.freeze( NewExpression() { return true }, - Property(node, options, visitorKeys) { + Property( + node: ESTree.Property, + options: Required, + visitorKeys: VisitorKeys, + ) { if ( options.considerImplicitTypeConversion && node.computed && @@ -135,7 +168,11 @@ const visitor = Object.freeze( } return this.$visitChildren(node, options, visitorKeys) }, - PropertyDefinition(node, options, visitorKeys) { + PropertyDefinition( + node: ESTree.PropertyDefinition, + options: Required, + visitorKeys: VisitorKeys, + ) { if ( options.considerImplicitTypeConversion && node.computed && @@ -145,7 +182,11 @@ const visitor = Object.freeze( } return this.$visitChildren(node, options, visitorKeys) }, - UnaryExpression(node, options, visitorKeys) { + UnaryExpression( + node: ESTree.UnaryExpression, + options: Required, + visitorKeys: VisitorKeys, + ) { if (node.operator === "delete") { return true } @@ -167,21 +208,38 @@ const visitor = Object.freeze( }), ) +/** + * Options for `hasSideEffect`, optionally. + */ +export type HasSideEffectOptions = { + /** + * If `true` then it considers member accesses as the node which has side effects. Default is `false`. + */ + considerGetters?: boolean + + /** + * If `true` then it considers implicit type conversion as the node which has side effects. Default is `false`. + */ + considerImplicitTypeConversion?: boolean +} + /** * 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`. - * @returns {boolean} `true` if the node has a certain side effect. + * @param node The node to get. + * @param sourceCode The source code object. + * @param options The option object. + * @param options.considerGetters If `true` then it considers member accesses as the node which has side effects. Default is `false`. + * @param options.considerImplicitTypeConversion If `true` then it considers implicit type conversion as the node which has side effects. Default is `false`. + * @returns `true` if the node has a certain side effect. */ export function hasSideEffect( - node, - sourceCode, - { considerGetters = false, considerImplicitTypeConversion = false } = {}, -) { + node: ESTree.Node, + sourceCode: SourceCode, + { + considerGetters = false, + considerImplicitTypeConversion = false, + }: HasSideEffectOptions = {}, +): boolean { return visitor.$visit( node, { considerGetters, considerImplicitTypeConversion }, diff --git a/src/index.mjs b/src/index.ts similarity index 62% rename from src/index.mjs rename to src/index.ts index 5f0bc06..22c621b 100644 --- a/src/index.mjs +++ b/src/index.ts @@ -1,20 +1,41 @@ -import { findVariable } from "./find-variable.mjs" -import { getFunctionHeadLocation } from "./get-function-head-location.mjs" -import { getFunctionNameWithKind } from "./get-function-name-with-kind.mjs" -import { getInnermostScope } from "./get-innermost-scope.mjs" -import { getPropertyName } from "./get-property-name.mjs" -import { getStaticValue } from "./get-static-value.mjs" -import { getStringIfConstant } from "./get-string-if-constant.mjs" -import { hasSideEffect } from "./has-side-effect.mjs" -import { isParenthesized } from "./is-parenthesized.mjs" -import { PatternMatcher } from "./pattern-matcher.mjs" +import { findVariable } from "./find-variable" +import { getFunctionHeadLocation } from "./get-function-head-location" +import { getFunctionNameWithKind } from "./get-function-name-with-kind" +import { getInnermostScope } from "./get-innermost-scope" +import { getPropertyName } from "./get-property-name" +import type { + StaticValue, + StaticValueOptional, + StaticValueProvided, +} from "./get-static-value" +import { getStaticValue } from "./get-static-value" +import { getStringIfConstant } from "./get-string-if-constant" +import type { HasSideEffectOptions } from "./has-side-effect" +import { hasSideEffect } from "./has-side-effect" +import { isParenthesized } from "./is-parenthesized" +import type { PatternMatherOptions } from "./pattern-matcher" +import { PatternMatcher } from "./pattern-matcher" +import type { ReferenceTrackerOptions } from "./reference-tracker" import { CALL, CONSTRUCT, ESM, READ, ReferenceTracker, -} from "./reference-tracker.mjs" +} from "./reference-tracker" +import type { + ArrowToken, + ClosingBraceToken, + ClosingBracketToken, + ClosingParenToken, + ColonToken, + CommaToken, + OpeningBraceToken, + OpeningBracketToken, + OpeningParenToken, + PunctuatorToken, + SemicolonToken, +} from "./token-predicate" import { isArrowToken, isClosingBraceToken, @@ -38,7 +59,7 @@ import { isOpeningBracketToken, isOpeningParenToken, isSemicolonToken, -} from "./token-predicate.mjs" +} from "./token-predicate" export default { CALL, @@ -79,6 +100,25 @@ export default { READ, ReferenceTracker, } +export type { + StaticValue, + StaticValueOptional, + StaticValueProvided, + HasSideEffectOptions, + ReferenceTrackerOptions, + PunctuatorToken, + ArrowToken, + CommaToken, + SemicolonToken, + ColonToken, + OpeningParenToken, + ClosingParenToken, + OpeningBracketToken, + ClosingBracketToken, + OpeningBraceToken, + ClosingBraceToken, + PatternMatherOptions, +} export { CALL, CONSTRUCT, diff --git a/src/is-parenthesized.mjs b/src/is-parenthesized.ts similarity index 60% rename from src/is-parenthesized.mjs rename to src/is-parenthesized.ts index c862d1a..f900241 100644 --- a/src/is-parenthesized.mjs +++ b/src/is-parenthesized.ts @@ -1,14 +1,17 @@ -import { isClosingParenToken, isOpeningParenToken } from "./token-predicate.mjs" +import type { AST, SourceCode } from "eslint" +import type * as ESTree from "estree" +import { getParent } from "./get-parent" +import { isClosingParenToken, isOpeningParenToken } from "./token-predicate" /** * 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 node The AST node to check. + * @param sourceCode The source code object to get tokens. + * @returns The left parenthesis of the parent node syntax */ -function getParentSyntaxParen(node, sourceCode) { - const parent = node.parent +function getParentSyntaxParen(node: ESTree.Node, sourceCode: SourceCode) { + const parent = getParent(node)! switch (parent.type) { case "CallExpression": @@ -62,42 +65,58 @@ 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. - * @returns {boolean} `true` if the node is parenthesized the given times. + * @param times The number of parantheses. + * @param node The AST node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is parenthesized the given times. */ +export function isParenthesized( + times: number, + node: ESTree.Node, + sourceCode: SourceCode, +): boolean /** * 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. - * @returns {boolean} `true` if the node is parenthesized. + * @param node The AST node to check. + * @param sourceCode The source code object to get tokens. + * @returns `true` if the node is parenthesized. */ export function isParenthesized( - timesOrNode, - nodeOrSourceCode, - optionalSourceCode, -) { - let times, node, sourceCode, maybeLeftParen, maybeRightParen + node: ESTree.Node, + sourceCode: SourceCode, +): boolean +export function isParenthesized( + timesOrNode: ESTree.Node | number, + nodeOrSourceCode: ESTree.Node | SourceCode, + optionalSourceCode?: SourceCode, +): boolean { + let times: number | undefined = undefined, + node: ESTree.Node | undefined = undefined, + sourceCode: SourceCode | undefined = undefined, + maybeLeftParen: AST.Token | ESTree.Comment | ESTree.Node | null = null, + maybeRightParen: AST.Token | ESTree.Comment | ESTree.Node | null = null if (typeof timesOrNode === "number") { times = timesOrNode | 0 - node = nodeOrSourceCode - sourceCode = optionalSourceCode + node = nodeOrSourceCode as ESTree.Node + sourceCode = optionalSourceCode! if (!(times >= 1)) { throw new TypeError("'times' should be a positive integer.") } } else { times = 1 node = timesOrNode - sourceCode = nodeOrSourceCode + sourceCode = nodeOrSourceCode as SourceCode } + if (node == null) { + return false + } + const parent = getParent(node) if ( - node == null || // `Program` can't be parenthesized - node.parent == null || + parent == null || // `CatchClause.param` can't be parenthesized, example `try {} catch (error) {}` - (node.parent.type === "CatchClause" && node.parent.param === node) + (parent.type === "CatchClause" && parent.param === node) ) { return false } diff --git a/src/pattern-matcher.mjs b/src/pattern-matcher.ts similarity index 53% rename from src/pattern-matcher.mjs rename to src/pattern-matcher.ts index 35f5a17..b94b18a 100644 --- a/src/pattern-matcher.mjs +++ b/src/pattern-matcher.ts @@ -5,16 +5,18 @@ const placeholder = /\$(?:[$&`']|[1-9][0-9]?)/gu -/** @type {WeakMap} */ -const internal = new WeakMap() +const internal = new WeakMap< + PatternMatcher, + { pattern: RegExp; escaped: boolean } +>() /** * Check whether a given character is escaped or not. - * @param {string} str The string to check. - * @param {number} index The location of the character to check. - * @returns {boolean} `true` if the character is escaped. + * @param str The string to check. + * @param index The location of the character to check. + * @returns `true` if the character is escaped. */ -function isEscaped(str, index) { +function isEscaped(str: string, index: number) { let escaped = false for (let i = index - 1; i >= 0 && str.charCodeAt(i) === 0x5c; --i) { escaped = !escaped @@ -24,23 +26,20 @@ function isEscaped(str, index) { /** * Replace a given string by a given matcher. - * @param {PatternMatcher} matcher The pattern matcher. - * @param {string} str The string to be replaced. - * @param {string} replacement The new substring to replace each matched part. - * @returns {string} The replaced string. + * @param matcher The pattern matcher. + * @param str The string to be replaced. + * @param replacement The new substring to replace each matched part. + * @returns The replaced string. */ -function replaceS(matcher, str, replacement) { +function replaceS(matcher: PatternMatcher, str: string, replacement: string) { const chunks = [] let index = 0 - /** @type {RegExpExecArray} */ - let match = null - /** - * @param {string} key The placeholder. - * @returns {string} The replaced string. + * @param key The placeholder. + * @returns The replaced string. */ - function replacer(key) { + function replacer(key: string, match: RegExpExecArray) { switch (key) { case "$$": return "$" @@ -53,16 +52,16 @@ function replaceS(matcher, str, replacement) { default: { const i = key.slice(1) if (i in match) { - return match[i] + return match[Number(i)] } return key } } } - for (match of matcher.execAll(str)) { + for (const match of matcher.execAll(str)) { chunks.push(str.slice(index, match.index)) - chunks.push(replacement.replace(placeholder, replacer)) + chunks.push(replacement.replace(placeholder, (s) => replacer(s, match))) index = match.index + match[0].length } chunks.push(str.slice(index)) @@ -72,18 +71,30 @@ 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. - * @returns {string} The replaced string. + * @param matcher The pattern matcher. + * @param str The string to be replaced. + * @param replace The function to replace each matched part. + * @returns The replaced string. */ -function replaceF(matcher, str, replace) { +function replaceF( + matcher: PatternMatcher, + str: string, + replace: (substring: string, ...args: any[]) => string, +) { const chunks = [] let index = 0 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 as unknown as [string, ...string[]]), + match.index, + match.input, + ), + ), + ) index = match.index + match[0].length } chunks.push(str.slice(index)) @@ -91,16 +102,22 @@ function replaceF(matcher, str, replace) { return chunks.join("") } +export type PatternMatherOptions = { + escaped?: boolean +} /** * The class to find patterns as considering escape sequences. */ export class PatternMatcher { /** * Initialize this matcher. - * @param {RegExp} pattern The pattern to match. - * @param {{escaped:boolean}} options The options. + * @param pattern The pattern to match. + * @param options The options. */ - constructor(pattern, { escaped = false } = {}) { + public constructor( + pattern: RegExp, + { escaped = false }: PatternMatherOptions = {}, + ) { if (!(pattern instanceof RegExp)) { throw new TypeError("'pattern' should be a RegExp instance.") } @@ -116,11 +133,11 @@ export class PatternMatcher { /** * Find the pattern in a given string. - * @param {string} str The string to find. - * @returns {IterableIterator} The iterator which iterate the matched information. + * @param str The string to find. + * @returns The iterator which iterate the matched information. */ - *execAll(str) { - const { pattern, escaped } = internal.get(this) + public *execAll(str: string): IterableIterator { + const { pattern, escaped } = internal.get(this)! let match = null let lastIndex = 0 @@ -136,10 +153,10 @@ export class PatternMatcher { /** * Check whether the pattern is found in a given string. - * @param {string} str The string to check. - * @returns {boolean} `true` if the pattern was found in the string. + * @param str The string to check. + * @returns `true` if the pattern was found in the string. */ - test(str) { + public test(str: string): boolean { const it = this.execAll(str) const ret = it.next() return !ret.done @@ -147,11 +164,14 @@ 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`. - * @returns {string} The replaced string. + * @param str The string to be replaced. + * @param replacer The string or function to replace. This is the same as the 2nd argument of `String.prototype.replace`. + * @returns The replaced string. */ - [Symbol.replace](str, replacer) { + public [Symbol.replace]( + str: string, + replacer: string | ((substring: string, ...args: any[]) => string), + ): string { return typeof replacer === "function" ? replaceF(this, String(str), replacer) : replaceS(this, String(str), String(replacer)) diff --git a/src/reference-tracker.mjs b/src/reference-tracker.ts similarity index 54% rename from src/reference-tracker.mjs rename to src/reference-tracker.ts index 3f14b28..270fc5f 100644 --- a/src/reference-tracker.mjs +++ b/src/reference-tracker.ts @@ -1,9 +1,15 @@ -import { findVariable } from "./find-variable.mjs" -import { getPropertyName } from "./get-property-name.mjs" -import { getStringIfConstant } from "./get-string-if-constant.mjs" +import type { Scope } from "eslint" +import type * as ESTree from "estree" +import { findVariable } from "./find-variable" +import { getParent } from "./get-parent" +import { getPropertyName } from "./get-property-name" +import { getStringIfConstant } from "./get-string-if-constant" const IMPORT_TYPE = /^(?:Import|Export(?:All|Default|Named))Declaration$/u -const has = Function.call.bind(Object.hasOwnProperty) +const has = Function.call.bind(Object.hasOwnProperty) as ( + o: any, + k: string | symbol, +) => boolean export const READ = Symbol("read") export const CALL = Symbol("call") @@ -12,12 +18,28 @@ export const ESM = Symbol("esm") const requireCall = { require: { [CALL]: true } } +function isImportNodeAndHasSource(node: ESTree.Node): node is + | (ESTree.ExportAllDeclaration & { + source: ESTree.Literal & { value: string } + }) + | (ESTree.ImportDeclaration & { + source: ESTree.Literal & { value: string } + }) { + return ( + IMPORT_TYPE.test(node.type) && + (node as ESTree.ExportAllDeclaration | ESTree.ImportDeclaration) + .source != null + ) +} + /** * Check whether a given variable is modified or not. - * @param {Variable} variable The variable to check. - * @returns {boolean} `true` if the variable is modified. + * @param variable The variable to check. + * @returns `true` if the variable is modified. */ -function isModifiedGlobal(variable) { +function isModifiedGlobal( + variable: Scope.Variable | null | undefined, +): boolean { return ( variable == null || variable.defs.length !== 0 || @@ -31,10 +53,10 @@ function isModifiedGlobal(variable) { * @param {Node} node A node to check. * @returns {boolean} `true` if the node is passed through. */ -function isPassThrough(node) { - const parent = node.parent +function isPassThrough(node: ESTree.Node) { + const parent = getParent(node) - switch (parent && parent.type) { + switch (parent?.type) { case "ConditionalExpression": return parent.consequent === node || parent.alternate === node case "LogicalExpression": @@ -49,23 +71,105 @@ function isPassThrough(node) { } } +export type ReferenceTrackerOptions = { + /** + * The variable names for Global Object. Default is ["global","globalThis","self","window"] + */ + globalObjectNames?: string[] + + /** + * The mode to determine the ImportDeclaration's behavior for CJS modules. Default is "strict" + */ + mode?: "legacy" | "strict" +} + +export type TraceMap< + CallInfo = never, + ConstructInfo = never, + ReadInfo = never, +> = Record> + +export type TraceMapObject = { + [i: string]: TraceMapObject + [CALL]?: CallInfo + [CONSTRUCT]?: ConstructInfo + [ESM]?: boolean + [READ]?: ReadInfo +} + +export type TrackedReferenceWithCallInfo = { + info: CallInfo + node: ESTree.SimpleCallExpression + path: string[] + type: symbol +} +export type TrackedReferenceWithConstructInfo = { + info: ConstructInfo + node: ESTree.NewExpression + path: string[] + type: symbol +} + +export type TrackedReferenceWithReadInfo = { + info: ReadInfo + node: + | ESTree.AssignmentProperty + | ESTree.ExportAllDeclaration + | ESTree.ExportSpecifier + | ESTree.Expression + | ESTree.ImportDeclaration + | ESTree.ImportDefaultSpecifier + | ESTree.ImportSpecifier + | ESTree.RestElement + path: string[] + type: symbol +} + +export type TrackedReferences = + | (CallInfo extends never ? never : TrackedReferenceWithCallInfo) + | (ConstructInfo extends never + ? never + : TrackedReferenceWithConstructInfo) + | (ReadInfo extends never ? never : TrackedReferenceWithReadInfo) + +type TrackedReferencesInternal = + | TrackedReferenceWithCallInfo + | TrackedReferenceWithConstructInfo + | TrackedReferenceWithReadInfo + /** * The reference tracker. */ export class ReferenceTracker { + public static readonly READ = READ + + public static readonly CALL = CALL + + public static readonly CONSTRUCT = CONSTRUCT + + public static readonly ESM = ESM + + private readonly globalScope: Scope.Scope + + private readonly mode: "legacy" | "strict" + + private readonly globalObjectNames: string[] + + private readonly variableStack: Scope.Variable[] + /** * Initialize this tracker. - * @param {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. + * @param globalScope The global scope. + * @param options The options. + * @param options.mode The mode to determine the ImportDeclaration's behavior for CJS modules. Default is "strict" + * @param options.globalObjectNames The variable names for Global Object. Default is ["global","globalThis","self","window"] */ - constructor( - globalScope, + public constructor( + globalScope: Scope.Scope, { mode = "strict", globalObjectNames = ["global", "globalThis", "self", "window"], - } = {}, + }: ReferenceTrackerOptions = {}, ) { this.variableStack = [] this.globalScope = globalScope @@ -75,10 +179,69 @@ export class ReferenceTracker { /** * 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 The trace map. + * @returns The iterator to iterate references. + */ + public iterateGlobalReferences< + CallInfo = never, + ConstructInfo = never, + ReadInfo = never, + >( + traceMap: TraceMap, + ): IterableIterator> { + return this._iterateGlobalReferences(traceMap) as IterableIterator< + TrackedReferences + > + } + + /** + * Iterate the references of CommonJS modules. + * @param traceMap The trace map. + * @returns The iterator to iterate references. */ - *iterateGlobalReferences(traceMap) { + public iterateCjsReferences< + CallInfo = never, + ConstructInfo = never, + ReadInfo = never, + >( + traceMap: TraceMap, + ): IterableIterator> { + return this._iterateCjsReferences(traceMap) as IterableIterator< + TrackedReferences + > + } + + /** + * Iterate the references of ES modules. + * @param traceMap The trace map. + * @returns The iterator to iterate references. + */ + public iterateEsmReferences< + CallInfo = never, + ConstructInfo = never, + ReadInfo = never, + >( + traceMap: TraceMap, + ): IterableIterator> { + return this._iterateEsmReferences(traceMap) as IterableIterator< + TrackedReferences + > + } + + /** + * Iterate the references of global variables. + * @param traceMap The trace map. + * @returns The iterator to iterate references. + */ + private *_iterateGlobalReferences< + CallInfo = never, + ConstructInfo = never, + ReadInfo = never, + >( + traceMap: TraceMap, + ): IterableIterator< + TrackedReferencesInternal + > { for (const key of Object.keys(traceMap)) { const nextTraceMap = traceMap[key] const path = [key] @@ -97,7 +260,7 @@ export class ReferenceTracker { } for (const key of this.globalObjectNames) { - const path = [] + const path: string[] = [] const variable = this.globalScope.set.get(key) if (isModifiedGlobal(variable)) { @@ -115,12 +278,22 @@ 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 The trace map. + * @returns The iterator to iterate references. */ - *iterateCjsReferences(traceMap) { + private *_iterateCjsReferences< + CallInfo = never, + ConstructInfo = never, + ReadInfo = never, + >( + traceMap: TraceMap, + ): IterableIterator< + TrackedReferencesInternal + > { for (const { node } of this.iterateGlobalReferences(requireCall)) { - const key = getStringIfConstant(node.arguments[0]) + const key = getStringIfConstant( + (node as ESTree.CallExpression).arguments[0], + ) if (key == null || !has(traceMap, key)) { continue } @@ -142,14 +315,22 @@ 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 The trace map. + * @returns The iterator to iterate references. */ - *iterateEsmReferences(traceMap) { - const programNode = this.globalScope.block + public *_iterateEsmReferences< + CallInfo = never, + ConstructInfo = never, + ReadInfo = never, + >( + traceMap: TraceMap, + ): IterableIterator< + TrackedReferencesInternal + > { + const programNode = this.globalScope.block as ESTree.Program for (const node of programNode.body) { - if (!IMPORT_TYPE.test(node.type) || node.source == null) { + if (!isImportNodeAndHasSource(node)) { continue } const moduleId = node.source.value @@ -209,13 +390,23 @@ export class ReferenceTracker { /** * Iterate the references for a given variable. - * @param {Variable} variable The variable to iterate that references. - * @param {string[]} path The current path. - * @param {object} 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. + * @param variable The variable to iterate that references. + * @param path The current path. + * @param traceMap The trace map. + * @param shouldReport = The flag to report those references. + * @returns The iterator to iterate references. */ - *_iterateVariableReferences(variable, path, traceMap, shouldReport) { + private *_iterateVariableReferences( + variable: Scope.Variable | undefined, + path: string[], + traceMap: TraceMapObject, + shouldReport: boolean, + ): IterableIterator< + TrackedReferencesInternal + > { + if (!variable) { + return + } if (this.variableStack.includes(variable)) { return } @@ -240,18 +431,24 @@ export class ReferenceTracker { /** * Iterate the references for a given AST node. * @param 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 path The current path. + * @param traceMap The trace map. + * @returns The iterator to iterate references. */ //eslint-disable-next-line complexity - *_iteratePropertyReferences(rootNode, path, traceMap) { + private *_iteratePropertyReferences( + rootNode: ESTree.Node, + path: string[], + traceMap: TraceMapObject, + ): IterableIterator< + TrackedReferencesInternal + > { let node = rootNode while (isPassThrough(node)) { - node = node.parent + node = getParent(node)! } - const parent = node.parent + const parent = getParent(node)! if (parent.type === "MemberExpression") { if (parent.object === node) { const key = getPropertyName(parent) @@ -317,11 +514,17 @@ 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 path The current path. + * @param traceMap The trace map. + * @returns The iterator to iterate references. */ - *_iterateLhsReferences(patternNode, path, traceMap) { + private *_iterateLhsReferences( + patternNode: ESTree.Pattern, + path: string[], + traceMap: TraceMapObject, + ): IterableIterator< + TrackedReferencesInternal + > { if (patternNode.type === "Identifier") { const variable = findVariable(this.globalScope, patternNode) if (variable != null) { @@ -336,7 +539,10 @@ 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)) { continue @@ -352,11 +558,13 @@ export class ReferenceTracker { info: nextTraceMap[READ], } } - yield* this._iterateLhsReferences( - property.value, - nextPath, - nextTraceMap, - ) + if (property.type === "Property") { + yield* this._iterateLhsReferences( + property.value, + nextPath, + nextTraceMap, + ) + } } return } @@ -367,12 +575,22 @@ 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 specifierNode The ModuleSpecifier node to iterate references. + * @param path The current path. + * @param traceMap The trace map. + * @returns The iterator to iterate references. */ - *_iterateImportReferences(specifierNode, path, traceMap) { + private *_iterateImportReferences( + specifierNode: + | ESTree.ExportSpecifier + | ESTree.ImportDefaultSpecifier + | ESTree.ImportNamespaceSpecifier + | ESTree.ImportSpecifier, + path: string[], + traceMap: TraceMapObject, + ): IterableIterator< + TrackedReferencesInternal + > { const type = specifierNode.type if (type === "ImportSpecifier" || type === "ImportDefaultSpecifier") { @@ -395,7 +613,7 @@ export class ReferenceTracker { } } yield* this._iterateVariableReferences( - findVariable(this.globalScope, specifierNode.local), + findVariable(this.globalScope, specifierNode.local)!, path, nextTraceMap, false, @@ -406,7 +624,7 @@ export class ReferenceTracker { if (type === "ImportNamespaceSpecifier") { yield* this._iterateVariableReferences( - findVariable(this.globalScope, specifierNode.local), + findVariable(this.globalScope, specifierNode.local)!, path, traceMap, false, @@ -434,17 +652,12 @@ export class ReferenceTracker { } } -ReferenceTracker.READ = READ -ReferenceTracker.CALL = CALL -ReferenceTracker.CONSTRUCT = CONSTRUCT -ReferenceTracker.ESM = ESM - /** * This is a predicate function for Array#filter. * @param {string} name A name part. * @param {number} index The index of the name. * @returns {boolean} `false` if it's default. */ -function exceptDefault(name, index) { +function exceptDefault(name: string, index: number) { return !(index === 1 && name === "default") } diff --git a/src/token-predicate.mjs b/src/token-predicate.ts similarity index 63% rename from src/token-predicate.mjs rename to src/token-predicate.ts index 9814c1f..e02f11a 100644 --- a/src/token-predicate.mjs +++ b/src/token-predicate.ts @@ -1,37 +1,49 @@ -/** - * Negate the result of `this` calling. - * @param {Token} token The token to check. - * @returns {boolean} `true` if the result of `this(token)` is `false`. - */ -function negate0(token) { - return !this(token) //eslint-disable-line no-invalid-this +type Token = { type: string; value: string } +type Comment = Token & { type: "Block" | "Line" | "Shebang" } + +export type PunctuatorToken = { + type: "Punctuator" + value: Value } +export type ArrowToken = PunctuatorToken<"=>"> +export type CommaToken = PunctuatorToken<","> +export type SemicolonToken = PunctuatorToken<";"> +export type ColonToken = PunctuatorToken<":"> +export type OpeningParenToken = PunctuatorToken<"("> +export type ClosingParenToken = PunctuatorToken<")"> +export type OpeningBracketToken = PunctuatorToken<"["> +export type ClosingBracketToken = PunctuatorToken<"]"> +export type OpeningBraceToken = PunctuatorToken<"{"> +export type ClosingBraceToken = PunctuatorToken<"}"> /** * Creates the negate function of the given function. * @param {function(Token):boolean} f - The function to negate. * @returns {function(Token):boolean} Negated function. */ -function negate(f) { - return negate0.bind(f) +function negate(f: (token: Token) => boolean): (token: Token) => boolean { + return (t) => !f(t) } /** * Checks if the given token is a PunctuatorToken with the given value - * @param {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. + * @param token - The token to check. + * @param value - The value to check. + * @returns `true` if the token is a PunctuatorToken with the given value. */ -function isPunctuatorTokenWithValue(token, value) { +function isPunctuatorTokenWithValue( + token: Token, + value: V, +): token is PunctuatorToken { return token.type === "Punctuator" && token.value === value } /** * Checks if the given token is an arrow token or not. - * @param {Token} token - The token to check. - * @returns {boolean} `true` if the token is an arrow token. + * @param token - The token to check. + * @returns `true` if the token is an arrow token. */ -export function isArrowToken(token) { +export function isArrowToken(token: Token): token is ArrowToken { return isPunctuatorTokenWithValue(token, "=>") } @@ -40,7 +52,7 @@ export function isArrowToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a comma token. */ -export function isCommaToken(token) { +export function isCommaToken(token: Token): token is CommaToken { return isPunctuatorTokenWithValue(token, ",") } @@ -49,7 +61,7 @@ export function isCommaToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a semicolon token. */ -export function isSemicolonToken(token) { +export function isSemicolonToken(token: Token): token is SemicolonToken { return isPunctuatorTokenWithValue(token, ";") } @@ -58,7 +70,7 @@ export function isSemicolonToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a colon token. */ -export function isColonToken(token) { +export function isColonToken(token: Token): token is ColonToken { return isPunctuatorTokenWithValue(token, ":") } @@ -67,7 +79,7 @@ export function isColonToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is an opening parenthesis token. */ -export function isOpeningParenToken(token) { +export function isOpeningParenToken(token: Token): token is OpeningParenToken { return isPunctuatorTokenWithValue(token, "(") } @@ -76,7 +88,7 @@ export function isOpeningParenToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a closing parenthesis token. */ -export function isClosingParenToken(token) { +export function isClosingParenToken(token: Token): token is ClosingParenToken { return isPunctuatorTokenWithValue(token, ")") } @@ -85,7 +97,9 @@ export function isClosingParenToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is an opening square bracket token. */ -export function isOpeningBracketToken(token) { +export function isOpeningBracketToken( + token: Token, +): token is OpeningBracketToken { return isPunctuatorTokenWithValue(token, "[") } @@ -94,7 +108,9 @@ export function isOpeningBracketToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a closing square bracket token. */ -export function isClosingBracketToken(token) { +export function isClosingBracketToken( + token: Token, +): token is ClosingBracketToken { return isPunctuatorTokenWithValue(token, "]") } @@ -103,7 +119,7 @@ export function isClosingBracketToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is an opening brace token. */ -export function isOpeningBraceToken(token) { +export function isOpeningBraceToken(token: Token): token is OpeningBraceToken { return isPunctuatorTokenWithValue(token, "{") } @@ -112,7 +128,7 @@ export function isOpeningBraceToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a closing brace token. */ -export function isClosingBraceToken(token) { +export function isClosingBraceToken(token: Token): token is ClosingBraceToken { return isPunctuatorTokenWithValue(token, "}") } @@ -121,7 +137,7 @@ export function isClosingBraceToken(token) { * @param {Token} token - The token to check. * @returns {boolean} `true` if the token is a comment token. */ -export function isCommentToken(token) { +export function isCommentToken(token: Token): token is Comment { return ["Block", "Line", "Shebang"].includes(token.type) } diff --git a/test/find-variable.mjs b/test/find-variable.ts similarity index 80% rename from test/find-variable.mjs rename to test/find-variable.ts index cfb501f..4239681 100644 --- a/test/find-variable.mjs +++ b/test/find-variable.ts @@ -1,23 +1,34 @@ import assert from "assert" +import type { Scope } from "eslint" import eslint from "eslint" -import { findVariable } from "../src/index.mjs" +import type * as ESTree from "estree" +import { findVariable } from "../src/index" describe("The 'findVariable' function", () => { - function getVariable(code, selector, withString = null) { + function getVariable( + code: string, + selector: string, + withString: string | null = null, + ) { const linter = new eslint.Linter() - let variable = null + let variable: any = null - linter.defineRule("test", (context) => ({ - [selector](node) { - variable = findVariable(context.getScope(), withString || node) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + [selector](node: ESTree.Identifier) { + variable = findVariable( + context.getScope(), + withString ?? node, + ) + }, + }), + }) linter.verify(code, { parserOptions: { ecmaVersion: 2020 }, rules: { test: "error" }, }) - return variable + return variable as Scope.Variable } describe("should return the variable of a given Identifier node", () => { diff --git a/test/get-function-head-location.mjs b/test/get-function-head-location.ts similarity index 87% rename from test/get-function-head-location.mjs rename to test/get-function-head-location.ts index ace1724..6f2cce2 100644 --- a/test/get-function-head-location.mjs +++ b/test/get-function-head-location.ts @@ -1,7 +1,8 @@ import assert from "assert" import eslint from "eslint" +import type * as ESTree from "estree" import semver from "semver" -import { getFunctionHeadLocation } from "../src/index.mjs" +import { getFunctionHeadLocation } from "../src/index" describe("The 'getFunctionHeadLocation' function", () => { const expectedResults = { @@ -88,15 +89,17 @@ describe("The 'getFunctionHeadLocation' function", () => { : {}), } - for (const key of Object.keys(expectedResults)) { + for (const key of Object.keys( + expectedResults, + ) as (keyof typeof expectedResults)[]) { const expectedLoc = { start: { line: 1, - column: expectedResults[key][0], + column: expectedResults[key]![0], }, end: { line: 1, - column: expectedResults[key][1], + column: expectedResults[key]![1], }, } @@ -106,14 +109,16 @@ describe("The 'getFunctionHeadLocation' function", () => { const linter = new eslint.Linter() let actualLoc = null - linter.defineRule("test", (context) => ({ - ":function"(node) { - actualLoc = getFunctionHeadLocation( - node, - context.getSourceCode(), - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + ":function"(node: ESTree.Function) { + actualLoc = getFunctionHeadLocation( + node, + context.getSourceCode(), + ) + }, + }), + }) const messages = linter.verify( key, { @@ -125,14 +130,9 @@ describe("The 'getFunctionHeadLocation' function", () => { }, }, "test.js", - true, ) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) assert.deepStrictEqual(actualLoc, expectedLoc) }) } diff --git a/test/get-function-name-with-kind.mjs b/test/get-function-name-with-kind.ts similarity index 87% rename from test/get-function-name-with-kind.mjs rename to test/get-function-name-with-kind.ts index b29b6f4..cd08092 100644 --- a/test/get-function-name-with-kind.mjs +++ b/test/get-function-name-with-kind.ts @@ -1,7 +1,8 @@ import assert from "assert" import eslint from "eslint" +import type * as ESTree from "estree" import semver from "semver" -import { getFunctionNameWithKind } from "../src/index.mjs" +import { getFunctionNameWithKind } from "../src/index" describe("The 'getFunctionNameWithKind' function", () => { const expectedResults = { @@ -128,19 +129,26 @@ describe("The 'getFunctionNameWithKind' function", () => { : {}), } - for (const key of Object.keys(expectedResults)) { - const expectedResult1 = expectedResults[key].replace(/\s+\[.+?\]/gu, "") + for (const key of Object.keys( + expectedResults, + ) as (keyof typeof expectedResults)[]) { + const expectedResult1 = expectedResults[key]!.replace( + /\s+\[.+?\]/gu, + "", + ) const expectedResult2 = expectedResults[key] it(`should return "${expectedResult1}" for "${key}".`, () => { const linter = new eslint.Linter() let actualResult = null - linter.defineRule("test", () => ({ - ":function"(node) { - actualResult = getFunctionNameWithKind(node) - }, - })) + linter.defineRule("test", { + create: () => ({ + ":function"(node: ESTree.Function) { + actualResult = getFunctionNameWithKind(node) + }, + }), + }) const messages = linter.verify(key, { rules: { test: "error" }, parserOptions: { @@ -151,26 +159,24 @@ describe("The 'getFunctionNameWithKind' function", () => { }, }) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) assert.strictEqual(actualResult, expectedResult1) }) - it(`should return "${expectedResult2}" for "${key}" if sourceCode is present.`, () => { + it(`should return "${expectedResult2!}" for "${key}" if sourceCode is present.`, () => { const linter = new eslint.Linter() let actualResult = null - linter.defineRule("test", (context) => ({ - ":function"(node) { - actualResult = getFunctionNameWithKind( - node, - context.getSourceCode(), - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + ":function"(node: ESTree.Function) { + actualResult = getFunctionNameWithKind( + node, + context.getSourceCode(), + ) + }, + }), + }) const messages = linter.verify(key, { rules: { test: "error" }, parserOptions: { @@ -181,11 +187,7 @@ describe("The 'getFunctionNameWithKind' function", () => { }, }) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) assert.strictEqual(actualResult, expectedResult2) }) } diff --git a/test/get-innermost-scope.mjs b/test/get-innermost-scope.ts similarity index 66% rename from test/get-innermost-scope.mjs rename to test/get-innermost-scope.ts index c658c8b..b8dc8ed 100644 --- a/test/get-innermost-scope.mjs +++ b/test/get-innermost-scope.ts @@ -1,8 +1,15 @@ import assert from "assert" import eslint from "eslint" -import { getInnermostScope } from "../src/index.mjs" +import type * as ESTree from "estree" +import { getInnermostScope } from "../src/index" describe("The 'getInnermostScope' function", () => { + type TestCase = { + code: "let a = 0" + parserOptions: eslint.Linter.ParserOptions + selectNode: (node: ESTree.Program) => ESTree.Node + selectScope: (scope: eslint.Scope.Scope) => eslint.Scope.Scope + } let i = 0 for (const { code, parserOptions, selectNode, selectScope } of [ { @@ -38,34 +45,42 @@ describe("The 'getInnermostScope' function", () => { { code: "a; { b; { c; } d; } e;", parserOptions: {}, - selectNode: (node) => node.body[1].body[0], + selectNode: (node) => + (node.body[1] as ESTree.BlockStatement).body[0], selectScope: (scope) => scope.childScopes[0], }, { code: "a; { b; { c; } d; } e;", parserOptions: {}, - selectNode: (node) => node.body[1].body[2], + selectNode: (node) => + (node.body[1] as ESTree.BlockStatement).body[2], selectScope: (scope) => scope.childScopes[0], }, { code: "a; { b; { c; } d; } e;", parserOptions: {}, - selectNode: (node) => node.body[1].body[1].body[0], + selectNode: (node) => + ( + (node.body[1] as ESTree.BlockStatement) + .body[1] as ESTree.BlockStatement + ).body[0], selectScope: (scope) => scope.childScopes[0].childScopes[0], }, - ]) { + ] as TestCase[]) { it(`should return the innermost scope (${++i})`, () => { const linter = new eslint.Linter() let actualScope = null let expectedScope = null - linter.defineRule("test", (context) => ({ - Program(node) { - const scope = context.getScope() - actualScope = getInnermostScope(scope, selectNode(node)) - expectedScope = selectScope(scope) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + Program(node) { + const scope = context.getScope() + actualScope = getInnermostScope(scope, selectNode(node)) + expectedScope = selectScope(scope) + }, + }), + }) linter.verify(code, { parserOptions: { ecmaVersion: 2020, ...parserOptions }, rules: { test: "error" }, diff --git a/test/get-property-name.mjs b/test/get-property-name.ts similarity index 82% rename from test/get-property-name.mjs rename to test/get-property-name.ts index 4ced068..1837041 100644 --- a/test/get-property-name.mjs +++ b/test/get-property-name.ts @@ -1,7 +1,8 @@ import assert from "assert" import eslint from "eslint" +import type * as ESTree from "estree" import semver from "semver" -import { getPropertyName } from "../src/index.mjs" +import { getPropertyName } from "../src/index" describe("The 'getPropertyName' function", () => { for (const { code, expected } of [ @@ -59,13 +60,19 @@ describe("The 'getPropertyName' function", () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", () => ({ - "Property,PropertyDefinition,MethodDefinition,MemberExpression"( - node, - ) { - actual = getPropertyName(node) - }, - })) + linter.defineRule("test", { + create: () => ({ + "Property,PropertyDefinition,MethodDefinition,MemberExpression"( + node: + | ESTree.MemberExpression + | ESTree.MethodDefinition + | ESTree.Property + | ESTree.PropertyDefinition, + ) { + actual = getPropertyName(node) + }, + }), + }) const messages = linter.verify(code, { parserOptions: { ecmaVersion: semver.gte(eslint.Linter.version, "8.0.0") @@ -74,11 +81,7 @@ describe("The 'getPropertyName' function", () => { }, rules: { test: "error" }, }) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) assert.strictEqual(actual, expected) }) } diff --git a/test/get-static-value.mjs b/test/get-static-value.ts similarity index 96% rename from test/get-static-value.mjs rename to test/get-static-value.ts index b898f32..4fc7ffc 100644 --- a/test/get-static-value.mjs +++ b/test/get-static-value.ts @@ -1,7 +1,7 @@ import assert from "assert" import eslint from "eslint" import semver from "semver" -import { getStaticValue } from "../src/index.mjs" +import { getStaticValue } from "../src/index" describe("The 'getStaticValue' function", () => { for (const { code, expected, noScope = false } of [ @@ -114,7 +114,8 @@ describe("The 'getStaticValue' function", () => { { code: "Array.of(1, 2)", expected: { value: [1, 2] } }, { code: "[0,1,2].at(-1)", - expected: Array.prototype.at ? { value: 2 } : null, + expected: + typeof Array.prototype.at === "function" ? { value: 2 } : null, }, { code: "[0,1,2].concat([3,4], [5])", @@ -401,14 +402,16 @@ const aMap = Object.freeze({ const linter = new eslint.Linter() let actual = null - linter.defineRule("test", (context) => ({ - ExpressionStatement(node) { - actual = getStaticValue( - node, - noScope ? null : context.getScope(), - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + ExpressionStatement(node) { + actual = getStaticValue( + node, + noScope ? null : context.getScope(), + ) + }, + }), + }) const messages = linter.verify(code, { env: { es6: true }, parserOptions: { @@ -419,11 +422,7 @@ const aMap = Object.freeze({ rules: { test: "error" }, }) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) if (actual == null) { assert.strictEqual(actual, expected) } else { diff --git a/test/get-string-if-constant.mjs b/test/get-string-if-constant.ts similarity index 72% rename from test/get-string-if-constant.mjs rename to test/get-string-if-constant.ts index 0ea091c..e31fdfc 100644 --- a/test/get-string-if-constant.mjs +++ b/test/get-string-if-constant.ts @@ -1,6 +1,7 @@ import assert from "assert" import eslint from "eslint" -import { getStringIfConstant } from "../src/index.mjs" +import type * as ESTree from "estree" +import { getStringIfConstant } from "../src/index" describe("The 'getStringIfConstant' function", () => { for (const { code, expected } of [ @@ -25,11 +26,15 @@ describe("The 'getStringIfConstant' function", () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", () => ({ - "Program > ExpressionStatement > *"(node) { - actual = getStringIfConstant(node) - }, - })) + linter.defineRule("test", { + create: () => ({ + "Program > ExpressionStatement > *"( + node: ESTree.Expression, + ) { + actual = getStringIfConstant(node) + }, + }), + }) linter.verify(code, { parserOptions: { ecmaVersion: 2020 }, rules: { test: "error" }, @@ -53,11 +58,18 @@ describe("The 'getStringIfConstant' function", () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", (context) => ({ - "Program > ExpressionStatement > *"(node) { - actual = getStringIfConstant(node, context.getScope()) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + "Program > ExpressionStatement > *"( + node: ESTree.Expression, + ) { + actual = getStringIfConstant( + node, + context.getScope(), + ) + }, + }), + }) linter.verify(code, { parserOptions: { ecmaVersion: 2020 }, rules: { test: "error" }, diff --git a/test/has-side-effect.mjs b/test/has-side-effect.ts similarity index 91% rename from test/has-side-effect.mjs rename to test/has-side-effect.ts index c9f190f..fedb709 100644 --- a/test/has-side-effect.mjs +++ b/test/has-side-effect.ts @@ -2,7 +2,7 @@ import assert from "assert" import { getProperty } from "dot-prop" import eslint from "eslint" import semver from "semver" -import { hasSideEffect } from "../src/index.mjs" +import { hasSideEffect } from "../src/index" describe("The 'hasSideEffect' function", () => { for (const { code, key = "body[0].expression", options, expected } of [ @@ -300,21 +300,26 @@ describe("The 'hasSideEffect' function", () => { expected: false, }, ]) { - it(`should return ${expected} on the code \`${code}\` and the options \`${JSON.stringify( + it(`should return ${String( + expected, + )} on the code \`${code}\` and the options \`${JSON.stringify( options, )}\``, () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", (context) => ({ - Program(node) { - actual = hasSideEffect( - getProperty(node, key), - context.getSourceCode(), - options, - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + Program(node) { + actual = hasSideEffect( + // eslint-disable-next-line @eslint-community/mysticatea/ts/no-confusing-void-expression -- Its return type is not void. + getProperty(node, key)!, + context.getSourceCode(), + options, + ) + }, + }), + }) const messages = linter.verify(code, { env: { es6: true }, parserOptions: { @@ -325,11 +330,7 @@ describe("The 'hasSideEffect' function", () => { rules: { test: "error" }, }) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) assert.strictEqual(actual, expected) }) } diff --git a/test/is-parenthesized.mjs b/test/is-parenthesized.ts similarity index 81% rename from test/is-parenthesized.mjs rename to test/is-parenthesized.ts index 3bcb1e6..680559c 100644 --- a/test/is-parenthesized.mjs +++ b/test/is-parenthesized.ts @@ -1,7 +1,7 @@ import assert from "assert" import { getProperty } from "dot-prop" import eslint from "eslint" -import { isParenthesized } from "../src/index.mjs" +import { isParenthesized } from "../src/index" describe("The 'isParenthesized' function", () => { for (const { code, expected } of [ @@ -217,30 +217,31 @@ describe("The 'isParenthesized' function", () => { }, ]) { describe(`on the code \`${code}\``, () => { - for (const key of Object.keys(expected)) { - it(`should return ${expected[key]} at "${key}"`, () => { + for (const key of Object.keys( + expected, + ) as (keyof typeof expected)[]) { + it(`should return ${String(expected[key])} at "${key}"`, () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", (context) => ({ - Program(node) { - actual = isParenthesized( - getProperty(node, key), - context.getSourceCode(), - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + Program(node) { + actual = isParenthesized( + // eslint-disable-next-line @eslint-community/mysticatea/ts/no-confusing-void-expression -- Its return type is not void. + getProperty(node, key)!, + context.getSourceCode(), + ) + }, + }), + }) const messages = linter.verify(code, { env: { es6: true }, parserOptions: { ecmaVersion: 2020 }, rules: { test: "error" }, }) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) assert.strictEqual(actual, expected[key]) }) } @@ -292,31 +293,32 @@ describe("The 'isParenthesized' function", () => { }, ]) { describe(`on the code \`${code}\` and 2 times`, () => { - for (const key of Object.keys(expected)) { - it(`should return ${expected[key]} at "${key}"`, () => { + for (const key of Object.keys( + expected, + ) as (keyof typeof expected)[]) { + it(`should return ${String(expected[key])} at "${key}"`, () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", (context) => ({ - Program(node) { - actual = isParenthesized( - 2, - getProperty(node, key), - context.getSourceCode(), - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + Program(node) { + actual = isParenthesized( + 2, + // eslint-disable-next-line @eslint-community/mysticatea/ts/no-confusing-void-expression -- Its return type is not void. + getProperty(node, key)!, + context.getSourceCode(), + ) + }, + }), + }) const messages = linter.verify(code, { env: { es6: true }, parserOptions: { ecmaVersion: 2020 }, rules: { test: "error" }, }) - assert.strictEqual( - messages.length, - 0, - messages[0] && messages[0].message, - ) + assert.strictEqual(messages.length, 0, messages[0]?.message) assert.strictEqual(actual, expected[key]) }) } diff --git a/test/pattern-matcher.mjs b/test/pattern-matcher.ts similarity index 95% rename from test/pattern-matcher.mjs rename to test/pattern-matcher.ts index 8ae8b8c..fa0273e 100644 --- a/test/pattern-matcher.mjs +++ b/test/pattern-matcher.ts @@ -1,5 +1,5 @@ import assert from "assert" -import { PatternMatcher } from "../src/index.mjs" +import { PatternMatcher } from "../src/index" const NAMED_CAPTURE_GROUP_SUPPORTED = (() => { try { @@ -17,12 +17,16 @@ const NAMED_CAPTURE_GROUP_SUPPORTED = (() => { * @param {string} input The input. * @returns {RegExpExecArray} The created object. */ -function newRegExpExecArray(subStrings, index, input) { +function newRegExpExecArray( + subStrings: string[], + index: number, + input: string, +): RegExpExecArray { Object.assign(subStrings, { index, input }) if (NAMED_CAPTURE_GROUP_SUPPORTED) { - subStrings.groups = undefined + ;(subStrings as RegExpExecArray).groups = undefined } - return subStrings + return subStrings as RegExpExecArray } describe("The 'PatternMatcher' class:", () => { @@ -43,7 +47,7 @@ describe("The 'PatternMatcher' class:", () => { }, ]) { assert.throws( - () => new PatternMatcher(value), + () => new PatternMatcher(value as never), /^TypeError: 'pattern' should be a RegExp instance\.$/u, ) } @@ -237,7 +241,9 @@ describe("The 'PatternMatcher' class:", () => { { str: "-foofoofooabcfoo-", expected: true }, { str: String.raw`-foo\foofooabcfoo-`, expected: true }, ]) { - it(`should return ${expected} in ${JSON.stringify(str)}.`, () => { + it(`should return ${String(expected)} in ${JSON.stringify( + str, + )}.`, () => { const matcher = new PatternMatcher(/foo/gu) const actual = matcher.test(str) assert.deepStrictEqual(actual, expected) @@ -287,7 +293,7 @@ describe("The 'PatternMatcher' class:", () => { it(`should return ${expected} in ${JSON.stringify( str, )} and ${JSON.stringify(replacer)}.`, () => { - const matcher = new PatternMatcher(pattern || /[a-c]/gu) + const matcher = new PatternMatcher(pattern ?? /[a-c]/gu) const actual = str.replace(matcher, replacer) assert.deepStrictEqual(actual, expected) }) @@ -295,7 +301,7 @@ describe("The 'PatternMatcher' class:", () => { it("should pass the correct arguments to replacers.", () => { const matcher = new PatternMatcher(/(\w)(\d)/gu) - const actualArgs = [] + const actualArgs: (number | string)[][] = [] const actual = "abc1d2efg".replace(matcher, (...args) => { actualArgs.push(args) return "x" diff --git a/test/reference-tracker.mjs b/test/reference-tracker.ts similarity index 90% rename from test/reference-tracker.mjs rename to test/reference-tracker.ts index 87adb6a..a4c0f5e 100644 --- a/test/reference-tracker.mjs +++ b/test/reference-tracker.ts @@ -1,7 +1,7 @@ import assert from "assert" import eslint from "eslint" import semver from "semver" -import { CALL, CONSTRUCT, ESM, READ, ReferenceTracker } from "../src/index.mjs" +import { CALL, CONSTRUCT, ESM, READ, ReferenceTracker } from "../src/index" const config = { parserOptions: { @@ -10,7 +10,7 @@ const config = { }, globals: { Reflect: false }, rules: { test: "error" }, -} +} as const describe("The 'ReferenceTracker' class:", () => { describe("the 'iterateGlobalReferences' method", () => { @@ -514,28 +514,35 @@ describe("The 'ReferenceTracker' class:", () => { }, ] : []), - ]) { + ] as const) { it(description, () => { const linter = new eslint.Linter() - let actual = null - linter.defineRule("test", (context) => ({ - "Program:exit"() { - const tracker = new ReferenceTracker(context.getScope()) - actual = Array.from( - tracker.iterateGlobalReferences(traceMap), - ).map((x) => - Object.assign(x, { - node: { - type: x.node.type, - ...(x.node.optional - ? { optional: x.node.optional } - : {}), - }, - }), - ) - }, - })) + let actual: any = null + linter.defineRule("test", { + create: (context) => ({ + "Program:exit"() { + const tracker = new ReferenceTracker( + context.getScope(), + ) + actual = Array.from( + tracker.iterateGlobalReferences(traceMap), + ).map((x) => + Object.assign(x, { + node: { + type: x.node.type, + ...((x.node.type === "CallExpression" || + x.node.type === + "MemberExpression") && + x.node.optional + ? { optional: x.node.optional } + : {}), + }, + }), + ) + }, + }), + }) linter.verify(code, config) assert.deepStrictEqual(actual, expected) @@ -683,23 +690,30 @@ describe("The 'ReferenceTracker' class:", () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", (context) => ({ - "Program:exit"() { - const tracker = new ReferenceTracker(context.getScope()) - actual = Array.from( - tracker.iterateCjsReferences(traceMap), - ).map((x) => - Object.assign(x, { - node: { - type: x.node.type, - ...(x.node.optional - ? { optional: x.node.optional } - : {}), - }, - }), - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + "Program:exit"() { + const tracker = new ReferenceTracker( + context.getScope(), + ) + actual = Array.from( + tracker.iterateCjsReferences(traceMap), + ).map((x) => + Object.assign(x, { + node: { + type: x.node.type, + ...((x.node.type === "CallExpression" || + x.node.type === + "MemberExpression") && + x.node.optional + ? { optional: x.node.optional } + : {}), + }, + }), + ) + }, + }), + }) linter.verify(code, config) assert.deepStrictEqual(actual, expected) @@ -965,23 +979,30 @@ describe("The 'ReferenceTracker' class:", () => { const linter = new eslint.Linter() let actual = null - linter.defineRule("test", (context) => ({ - "Program:exit"() { - const tracker = new ReferenceTracker(context.getScope()) - actual = Array.from( - tracker.iterateEsmReferences(traceMap), - ).map((x) => - Object.assign(x, { - node: { - type: x.node.type, - ...(x.node.optional - ? { optional: x.node.optional } - : {}), - }, - }), - ) - }, - })) + linter.defineRule("test", { + create: (context) => ({ + "Program:exit"() { + const tracker = new ReferenceTracker( + context.getScope(), + ) + actual = Array.from( + tracker.iterateEsmReferences(traceMap), + ).map((x) => + Object.assign(x, { + node: { + type: x.node.type, + ...((x.node.type === "CallExpression" || + x.node.type === + "MemberExpression") && + x.node.optional + ? { optional: x.node.optional } + : {}), + }, + }), + ) + }, + }), + }) linter.verify(code, config) assert.deepStrictEqual(actual, expected) diff --git a/test/token-predicate.mjs b/test/token-predicate.ts similarity index 96% rename from test/token-predicate.mjs rename to test/token-predicate.ts index d2b5914..9cbead3 100644 --- a/test/token-predicate.mjs +++ b/test/token-predicate.ts @@ -22,7 +22,7 @@ import { isOpeningBracketToken, isOpeningParenToken, isSemicolonToken, -} from "../src/index.mjs" +} from "../src/index" describe("The predicate functions for tokens", () => { for (const { positive, negative, patterns } of [ @@ -132,12 +132,12 @@ describe("The predicate functions for tokens", () => { [{ type: "Line", value: ";" }, false], ], }, - ]) { + ] as const) { const baseName = positive.name.slice(2) describe(`'is${baseName}'`, () => { for (const [token, expected] of patterns) { - it(`should return ${expected} if ${JSON.stringify( + it(`should return ${String(expected)} if ${JSON.stringify( token, )} was given.`, () => { assert.strictEqual(positive(token), expected) @@ -147,7 +147,7 @@ describe("The predicate functions for tokens", () => { describe(`'isNot${baseName}'`, () => { for (const [token, expected] of patterns) { - it(`should return ${!expected} if ${JSON.stringify( + it(`should return ${String(!expected)} if ${JSON.stringify( token, )} was given.`, () => { assert.strictEqual(negative(token), !expected) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0e65348 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "baseUrl": ".", + "checkJs": true, + "declaration": true, + "diagnostics": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2022"], + "noEmit": true, + "module": "commonjs", + "moduleResolution": "node", + "newLine": "LF", + "noEmitOnError": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "pretty": true, + "sourceMap": true, + "sourceRoot": "src", + "strictFunctionTypes": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "target": "es2015", + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "test/**/*.ts", "build.config.ts"] +}