diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 9838612c..1c83bfa5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -22,10 +22,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20.x - - name: Install Packages - run: npm install - - name: Lint - run: npm run -s lint + - run: npm install + - run: npm run -s lint + - run: npm run -s test:types test: name: Test @@ -64,7 +63,6 @@ jobs: - name: Install Packages run: npm install - name: Install ESLint ${{ matrix.eslint }} - run: | - npm install --no-save --force eslint@${{ matrix.eslint }} + run: npm install --no-save --force eslint@${{ matrix.eslint }} - name: Test - run: npm run -s test:ci + run: npm run -s test:tests diff --git a/.gitignore b/.gitignore index 06e79ef5..ff9b474d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ /test.js .eslintcache .vscode -.idea/ \ No newline at end of file +.idea/ + +types/ diff --git a/lib/configs/_commons.js b/lib/configs/_commons.js index 1e2b070d..e280c3c4 100644 --- a/lib/configs/_commons.js +++ b/lib/configs/_commons.js @@ -1,21 +1,19 @@ "use strict" -module.exports = { - commonRules: { - "n/no-deprecated-api": "error", - "n/no-extraneous-import": "error", - "n/no-extraneous-require": "error", - "n/no-exports-assign": "error", - "n/no-missing-import": "error", - "n/no-missing-require": "error", - "n/no-process-exit": "error", - "n/no-unpublished-bin": "error", - "n/no-unpublished-import": "error", - "n/no-unpublished-require": "error", - "n/no-unsupported-features/es-builtins": "error", - "n/no-unsupported-features/es-syntax": "error", - "n/no-unsupported-features/node-builtins": "error", - "n/process-exit-as-throw": "error", - "n/hashbang": "error", - }, -} +module.exports.commonRules = /** @type {const} */ ({ + "n/no-deprecated-api": "error", + "n/no-extraneous-import": "error", + "n/no-extraneous-require": "error", + "n/no-exports-assign": "error", + "n/no-missing-import": "error", + "n/no-missing-require": "error", + "n/no-process-exit": "error", + "n/no-unpublished-bin": "error", + "n/no-unpublished-import": "error", + "n/no-unpublished-require": "error", + "n/no-unsupported-features/es-builtins": "error", + "n/no-unsupported-features/es-syntax": "error", + "n/no-unsupported-features/node-builtins": "error", + "n/process-exit-as-throw": "error", + "n/hashbang": "error", +}) diff --git a/lib/configs/recommended-module.js b/lib/configs/recommended-module.js index 0584fa63..648d4c61 100644 --- a/lib/configs/recommended-module.js +++ b/lib/configs/recommended-module.js @@ -3,7 +3,10 @@ const globals = require("globals") const { commonRules } = require("./_commons") -// eslintrc config: https://eslint.org/docs/latest/use/configure/configuration-files +/** + * https://eslint.org/docs/latest/use/configure/configuration-files + * @type {import('eslint').ESLint.ConfigData} + */ module.exports.eslintrc = { env: { node: true, @@ -30,7 +33,10 @@ module.exports.eslintrc = { }, } -// flat config: https://eslint.org/docs/latest/use/configure/configuration-files-new +/** + * https://eslint.org/docs/latest/use/configure/configuration-files-new + * @type {import('eslint').Linter.FlatConfig} + */ module.exports.flat = { languageOptions: { sourceType: "module", @@ -39,5 +45,7 @@ module.exports.flat = { ...module.exports.eslintrc.globals, }, }, - rules: module.exports.eslintrc.rules, + rules: + /** @type {import('eslint').Linter.RulesRecord} */ + (module.exports.eslintrc.rules), } diff --git a/lib/configs/recommended-script.js b/lib/configs/recommended-script.js index 0d8f1aa5..c1000fb5 100644 --- a/lib/configs/recommended-script.js +++ b/lib/configs/recommended-script.js @@ -3,7 +3,10 @@ const globals = require("globals") const { commonRules } = require("./_commons") -// eslintrc config: https://eslint.org/docs/latest/use/configure/configuration-files +/** + * https://eslint.org/docs/latest/use/configure/configuration-files + * @type {import('eslint').ESLint.ConfigData} + */ module.exports.eslintrc = { env: { node: true, @@ -27,7 +30,10 @@ module.exports.eslintrc = { }, } -// https://eslint.org/docs/latest/use/configure/configuration-files-new +/** + * https://eslint.org/docs/latest/use/configure/configuration-files-new + * @type {import('eslint').Linter.FlatConfig} + */ module.exports.flat = { languageOptions: { sourceType: "commonjs", @@ -36,5 +42,7 @@ module.exports.flat = { ...module.exports.eslintrc.globals, }, }, - rules: module.exports.eslintrc.rules, + rules: + /** @type {import('eslint').Linter.RulesRecord} */ + (module.exports.eslintrc.rules), } diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js index f0cdf423..073c9102 100644 --- a/lib/configs/recommended.js +++ b/lib/configs/recommended.js @@ -1,13 +1,22 @@ "use strict" -const getPackageJson = require("../util/get-package-json") +const { getPackageJson } = require("../util/get-package-json") const moduleConfig = require("./recommended-module") const scriptConfig = require("./recommended-script") const packageJson = getPackageJson() -const isModule = (packageJson && packageJson.type) === "module" + +const isModule = + packageJson != null && + typeof packageJson === "object" && + "type" in packageJson && + packageJson.type === "module" const recommendedConfig = isModule ? moduleConfig : scriptConfig +/** + * https://eslint.org/docs/latest/use/configure/configuration-files + * @type {import('eslint').ESLint.ConfigData} + */ module.exports.eslintrc = { ...recommendedConfig.eslintrc, overrides: [ diff --git a/lib/eslint-utils.d.ts b/lib/eslint-utils.d.ts new file mode 100644 index 00000000..0db31dba --- /dev/null +++ b/lib/eslint-utils.d.ts @@ -0,0 +1,80 @@ +declare module "eslint-plugin-es-x" { + export const rules: NonNullable; +} + +declare module "@eslint-community/eslint-utils" { + import * as estree from 'estree'; + import * as eslint from 'eslint'; + + type Node = estree.Node | estree.Expression; + + export const READ: unique symbol; + export const CALL: unique symbol; + export const CONSTRUCT: unique symbol; + export const ESM: unique symbol; + export class ReferenceTracker { + constructor(globalScope: eslint.Scope.Scope, { mode, globalObjectNames, }?: { + mode?: "legacy" | "strict" | undefined; + globalObjectNames?: string[] | undefined; + } | undefined); + variableStack: eslint.Scope.Variable[]; + globalScope: eslint.Scope.Scope; + mode: "legacy" | "strict"; + globalObjectNames: string[]; + iterateGlobalReferences(traceMap: TraceMap): IterableIterator>; + iterateCjsReferences(traceMap: TraceMap): IterableIterator>; + iterateEsmReferences(traceMap: TraceMap): IterableIterator>; + } + export namespace ReferenceTracker { + export { READ }; + export { CALL }; + export { CONSTRUCT }; + export { ESM }; + } + type ReferenceType = typeof READ | typeof CALL | typeof CONSTRUCT; + type TraceMap = { + [READ]?: Info; + [CALL]?: Info; + [CONSTRUCT]?: Info; + [key: string]: TraceMap; + } + type RichNode = eslint.Rule.Node | Node; + type Reference = { + node: RichNode; + path: string[]; + type: ReferenceType; + info: Info; + }; + + export function findVariable(initialScope: eslint.Scope.Scope, nameOrNode: string | Node): eslint.Scope.Variable | null; + + export function getFunctionHeadLocation(node: Extract, sourceCode: eslint.SourceCode): eslint.AST.SourceLocation | null; + + export function getFunctionNameWithKind(node: Extract, sourceCode?: eslint.SourceCode | undefined): string; + + export function getInnermostScope(initialScope: eslint.Scope.Scope, node: Node): eslint.Scope.Scope; + + export function getPropertyName(node: Extract, initialScope?: eslint.Scope.Scope | undefined): string | null; + + export function getStaticValue(node: Node, initialScope?: eslint.Scope.Scope | null | undefined): { + value: unknown; + optional?: never; + } | { + value: undefined; + optional?: true; + } | null; + + export function getStringIfConstant(node: Node, initialScope?: eslint.Scope.Scope | null | undefined): string | null; + + export function hasSideEffect(node: eslint.Rule.Node, sourceCode: eslint.SourceCode, { considerGetters, considerImplicitTypeConversion }?: VisitOptions | undefined): boolean; + type VisitOptions = { + considerGetters?: boolean | undefined; + considerImplicitTypeConversion?: boolean | undefined; + }; +} diff --git a/lib/index.js b/lib/index.js index 39331a60..49fd4c71 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,73 +5,89 @@ const esmConfig = require("./configs/recommended-module") const cjsConfig = require("./configs/recommended-script") const recommendedConfig = require("./configs/recommended") -const rules = { - "callback-return": require("./rules/callback-return"), - "exports-style": require("./rules/exports-style"), - "file-extension-in-import": require("./rules/file-extension-in-import"), - "global-require": require("./rules/global-require"), - "handle-callback-err": require("./rules/handle-callback-err"), - "no-callback-literal": require("./rules/no-callback-literal"), - "no-deprecated-api": require("./rules/no-deprecated-api"), - "no-exports-assign": require("./rules/no-exports-assign"), - "no-extraneous-import": require("./rules/no-extraneous-import"), - "no-extraneous-require": require("./rules/no-extraneous-require"), - "no-missing-import": require("./rules/no-missing-import"), - "no-missing-require": require("./rules/no-missing-require"), - "no-mixed-requires": require("./rules/no-mixed-requires"), - "no-new-require": require("./rules/no-new-require"), - "no-path-concat": require("./rules/no-path-concat"), - "no-process-env": require("./rules/no-process-env"), - "no-process-exit": require("./rules/no-process-exit"), - "no-restricted-import": require("./rules/no-restricted-import"), - "no-restricted-require": require("./rules/no-restricted-require"), - "no-sync": require("./rules/no-sync"), - "no-unpublished-bin": require("./rules/no-unpublished-bin"), - "no-unpublished-import": require("./rules/no-unpublished-import"), - "no-unpublished-require": require("./rules/no-unpublished-require"), - "no-unsupported-features/es-builtins": require("./rules/no-unsupported-features/es-builtins"), - "no-unsupported-features/es-syntax": require("./rules/no-unsupported-features/es-syntax"), - "no-unsupported-features/node-builtins": require("./rules/no-unsupported-features/node-builtins"), - "prefer-global/buffer": require("./rules/prefer-global/buffer"), - "prefer-global/console": require("./rules/prefer-global/console"), - "prefer-global/process": require("./rules/prefer-global/process"), - "prefer-global/text-decoder": require("./rules/prefer-global/text-decoder"), - "prefer-global/text-encoder": require("./rules/prefer-global/text-encoder"), - "prefer-global/url-search-params": require("./rules/prefer-global/url-search-params"), - "prefer-global/url": require("./rules/prefer-global/url"), - "prefer-node-protocol": require("./rules/prefer-node-protocol"), - "prefer-promises/dns": require("./rules/prefer-promises/dns"), - "prefer-promises/fs": require("./rules/prefer-promises/fs"), - "process-exit-as-throw": require("./rules/process-exit-as-throw"), - hashbang: require("./rules/hashbang"), +/** + * @typedef {{ + 'recommended-module': import('eslint').ESLint.ConfigData; + 'recommended-script': import('eslint').ESLint.ConfigData; + 'recommended': import('eslint').ESLint.ConfigData; + 'flat/recommended-module': import('eslint').Linter.FlatConfig; + 'flat/recommended-script': import('eslint').Linter.FlatConfig; + 'flat/recommended': import('eslint').Linter.FlatConfig; + 'flat/mixed-esm-and-cjs': import('eslint').Linter.FlatConfig[]; + }} Configs + */ - // Deprecated rules. - "no-hide-core-modules": require("./rules/no-hide-core-modules"), - shebang: require("./rules/shebang"), -} - -const mod = { +/** @type {import('eslint').ESLint.Plugin & { configs: Configs }} */ +const plugin = { meta: { name: pkg.name, version: pkg.version, }, - rules, + rules: /** @type {Record} */ ({ + "callback-return": require("./rules/callback-return"), + "exports-style": require("./rules/exports-style"), + "file-extension-in-import": require("./rules/file-extension-in-import"), + "global-require": require("./rules/global-require"), + "handle-callback-err": require("./rules/handle-callback-err"), + "no-callback-literal": require("./rules/no-callback-literal"), + "no-deprecated-api": require("./rules/no-deprecated-api"), + "no-exports-assign": require("./rules/no-exports-assign"), + "no-extraneous-import": require("./rules/no-extraneous-import"), + "no-extraneous-require": require("./rules/no-extraneous-require"), + "no-missing-import": require("./rules/no-missing-import"), + "no-missing-require": require("./rules/no-missing-require"), + "no-mixed-requires": require("./rules/no-mixed-requires"), + "no-new-require": require("./rules/no-new-require"), + "no-path-concat": require("./rules/no-path-concat"), + "no-process-env": require("./rules/no-process-env"), + "no-process-exit": require("./rules/no-process-exit"), + "no-restricted-import": require("./rules/no-restricted-import"), + "no-restricted-require": require("./rules/no-restricted-require"), + "no-sync": require("./rules/no-sync"), + "no-unpublished-bin": require("./rules/no-unpublished-bin"), + "no-unpublished-import": require("./rules/no-unpublished-import"), + "no-unpublished-require": require("./rules/no-unpublished-require"), + "no-unsupported-features/es-builtins": require("./rules/no-unsupported-features/es-builtins"), + "no-unsupported-features/es-syntax": require("./rules/no-unsupported-features/es-syntax"), + "no-unsupported-features/node-builtins": require("./rules/no-unsupported-features/node-builtins"), + "prefer-global/buffer": require("./rules/prefer-global/buffer"), + "prefer-global/console": require("./rules/prefer-global/console"), + "prefer-global/process": require("./rules/prefer-global/process"), + "prefer-global/text-decoder": require("./rules/prefer-global/text-decoder"), + "prefer-global/text-encoder": require("./rules/prefer-global/text-encoder"), + "prefer-global/url-search-params": require("./rules/prefer-global/url-search-params"), + "prefer-global/url": require("./rules/prefer-global/url"), + "prefer-node-protocol": require("./rules/prefer-node-protocol"), + "prefer-promises/dns": require("./rules/prefer-promises/dns"), + "prefer-promises/fs": require("./rules/prefer-promises/fs"), + "process-exit-as-throw": require("./rules/process-exit-as-throw"), + hashbang: require("./rules/hashbang"), + + // Deprecated rules. + "no-hide-core-modules": require("./rules/no-hide-core-modules"), + shebang: require("./rules/shebang"), + }), + configs: { + "recommended-module": { plugins: ["n"], ...esmConfig.eslintrc }, + "recommended-script": { plugins: ["n"], ...cjsConfig.eslintrc }, + recommended: { plugins: ["n"], ...recommendedConfig.eslintrc }, + "flat/recommended-module": { ...esmConfig.flat }, + "flat/recommended-script": { ...cjsConfig.flat }, + "flat/recommended": { ...recommendedConfig.flat }, + "flat/mixed-esm-and-cjs": [ + { files: ["**/*.js"], ...recommendedConfig.flat }, + { files: ["**/*.mjs"], ...esmConfig.flat }, + { files: ["**/*.cjs"], ...cjsConfig.flat }, + ], + }, } -// set configs, e.g. mod.configs["recommended-module"] -// do not defined in the mod obj - to avoid circular dependency -mod.configs = { - "recommended-module": { plugins: ["n"], ...esmConfig.eslintrc }, - "recommended-script": { plugins: ["n"], ...cjsConfig.eslintrc }, - recommended: { plugins: ["n"], ...recommendedConfig.eslintrc }, - "flat/recommended-module": { plugins: { n: mod }, ...esmConfig.flat }, - "flat/recommended-script": { plugins: { n: mod }, ...cjsConfig.flat }, - "flat/recommended": { plugins: { n: mod }, ...recommendedConfig.flat }, - "flat/mixed-esm-and-cjs": [ - { plugins: { n: mod }, files: ["**/*.js"], ...recommendedConfig.flat }, - { plugins: { n: mod }, files: ["**/*.mjs"], ...esmConfig.flat }, - { plugins: { n: mod }, files: ["**/*.cjs"], ...cjsConfig.flat }, - ], +plugin.configs["flat/recommended-module"].plugins = { n: plugin } +plugin.configs["flat/recommended-script"].plugins = { n: plugin } +plugin.configs["flat/recommended"].plugins = { n: plugin } + +for (const config of plugin.configs["flat/mixed-esm-and-cjs"]) { + config.plugins = { n: plugin } } -module.exports = mod +module.exports = plugin diff --git a/lib/rules/callback-return.js b/lib/rules/callback-return.js index 1b661532..42674134 100644 --- a/lib/rules/callback-return.js +++ b/lib/rules/callback-return.js @@ -4,6 +4,7 @@ */ "use strict" +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -30,9 +31,9 @@ module.exports = { /** * Find the closest parent matching a list of types. - * @param {ASTNode} node The node whose parents we are searching - * @param {Array} types The node types to match - * @returns {ASTNode} The matched node or undefined. + * @param {import('eslint').Rule.Node} node The node whose parents we are searching + * @param {string[]} types The node types to match + * @returns {import('eslint').Rule.Node | null} The matched node or undefined. */ function findClosestParentOfType(node, types) { if (!node.parent) { @@ -46,7 +47,7 @@ module.exports = { /** * Check to see if a node contains only identifers - * @param {ASTNode} node The node to check + * @param {import('estree').Expression | import('estree').Super} node The node to check * @returns {boolean} Whether or not the node contains only identifers */ function containsOnlyIdentifiers(node) { @@ -68,7 +69,7 @@ module.exports = { /** * Check to see if a CallExpression is in our callback list. - * @param {ASTNode} node The node to check against our callback names list. + * @param {import('estree').CallExpression} node The node to check against our callback names list. * @returns {boolean} Whether or not this function matches our callback name. */ function isCallback(node) { @@ -80,8 +81,8 @@ module.exports = { /** * Determines whether or not the callback is part of a callback expression. - * @param {ASTNode} node The callback node - * @param {ASTNode} parentNode The expression node + * @param {import('eslint').Rule.Node} node The callback node + * @param {import('estree').Statement} parentNode The expression node * @returns {boolean} Whether or not this is part of a callback expression */ function isCallbackExpression(node, parentNode) { @@ -116,25 +117,24 @@ module.exports = { } // find the closest block, return or loop - const closestBlock = - findClosestParentOfType(node, [ - "BlockStatement", - "ReturnStatement", - "ArrowFunctionExpression", - ]) || {} + const closestBlock = findClosestParentOfType(node, [ + "BlockStatement", + "ReturnStatement", + "ArrowFunctionExpression", + ]) // if our parent is a return we know we're ok - if (closestBlock.type === "ReturnStatement") { + if (closestBlock?.type === "ReturnStatement") { return } // arrow functions don't always have blocks and implicitly return - if (closestBlock.type === "ArrowFunctionExpression") { + if (closestBlock?.type === "ArrowFunctionExpression") { return } // block statements are part of functions and most if statements - if (closestBlock.type === "BlockStatement") { + if (closestBlock?.type === "BlockStatement") { // find the last item in the block const lastItem = closestBlock.body[closestBlock.body.length - 1] diff --git a/lib/rules/exports-style.js b/lib/rules/exports-style.js index 2ddd6ebc..6a157151 100644 --- a/lib/rules/exports-style.js +++ b/lib/rules/exports-style.js @@ -4,18 +4,23 @@ */ "use strict" +/** + * @typedef {import('estree').Node & { parent?: Node }} Node + */ + /*istanbul ignore next */ /** * This function is copied from https://github.com/eslint/eslint/blob/2355f8d0de1d6732605420d15ddd4f1eee3c37b6/lib/ast-utils.js#L648-L684 * - * @param {ASTNode} node - The node to get. - * @returns {string|null} The property name if static. Otherwise, null. + * @param {Node} node - The node to get. + * @returns {string | null | undefined} The property name if static. Otherwise, null. * @private */ function getStaticPropertyName(node) { + /** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */ let prop = null - switch (node && node.type) { + switch (node?.type) { case "Property": case "MethodDefinition": prop = node.key @@ -28,7 +33,7 @@ function getStaticPropertyName(node) { // no default } - switch (prop && prop.type) { + switch (prop?.type) { case "Literal": return String(prop.value) @@ -39,7 +44,12 @@ function getStaticPropertyName(node) { break case "Identifier": - if (!node.computed) { + if ( + !( + /** @type {import('estree').MemberExpression} */ (node) + .computed + ) + ) { return prop.name } break @@ -53,12 +63,13 @@ function getStaticPropertyName(node) { /** * Checks whether the given node is assignee or not. * - * @param {ASTNode} node - The node to check. + * @param {Node} node - The node to check. * @returns {boolean} `true` if the node is assignee. */ function isAssignee(node) { return ( - node.parent.type === "AssignmentExpression" && node.parent.left === node + node.parent?.type === "AssignmentExpression" && + node.parent.left === node ) } @@ -68,15 +79,15 @@ function isAssignee(node) { * This is used to distinguish 2 assignees belong to the same assignment. * If the node is not an assignee, this returns null. * - * @param {ASTNode} leafNode - The node to get. - * @returns {ASTNode|null} The top assignment expression node, or null. + * @param {Node} leafNode - The node to get. + * @returns {Node|null} The top assignment expression node, or null. */ function getTopAssignment(leafNode) { let node = leafNode // Skip MemberExpressions. while ( - node.parent.type === "MemberExpression" && + node.parent?.type === "MemberExpression" && node.parent.object === node ) { node = node.parent @@ -88,7 +99,7 @@ function getTopAssignment(leafNode) { } // Find the top. - while (node.parent.type === "AssignmentExpression") { + while (node.parent?.type === "AssignmentExpression") { node = node.parent } @@ -98,18 +109,18 @@ function getTopAssignment(leafNode) { /** * Gets top assignment nodes of the given node list. * - * @param {ASTNode[]} nodes - The node list to get. - * @returns {ASTNode[]} Gotten top assignment nodes. + * @param {Node[]} nodes - The node list to get. + * @returns {Node[]} Gotten top assignment nodes. */ function createAssignmentList(nodes) { - return nodes.map(getTopAssignment).filter(Boolean) + return /** @type {Node[]} */ (nodes.map(getTopAssignment).filter(Boolean)) } /** * Gets the reference of `module.exports` from the given scope. * - * @param {escope.Scope} scope - The scope to get. - * @returns {ASTNode[]} Gotten MemberExpression node list. + * @param {import('eslint').Scope.Scope} scope - The scope to get. + * @returns {Node[]} Gotten MemberExpression node list. */ function getModuleExportsNodes(scope) { const variable = scope.set.get("module") @@ -117,10 +128,14 @@ function getModuleExportsNodes(scope) { return [] } return variable.references - .map(reference => reference.identifier.parent) + .map( + reference => + /** @type {Node & { parent: Node }} */ (reference.identifier) + .parent + ) .filter( node => - node.type === "MemberExpression" && + node?.type === "MemberExpression" && getStaticPropertyName(node) === "exports" ) } @@ -128,17 +143,23 @@ function getModuleExportsNodes(scope) { /** * Gets the reference of `exports` from the given scope. * - * @param {escope.Scope} scope - The scope to get. - * @returns {ASTNode[]} Gotten Identifier node list. + * @param {import('eslint').Scope.Scope} scope - The scope to get. + * @returns {import('estree').Identifier[]} Gotten Identifier node list. */ function getExportsNodes(scope) { const variable = scope.set.get("exports") if (variable == null) { return [] } + return variable.references.map(reference => reference.identifier) } +/** + * @param {Node} property + * @param {import('eslint').SourceCode} sourceCode + * @returns {string | null} + */ function getReplacementForProperty(property, sourceCode) { if (property.type !== "Property" || property.kind !== "init") { // We don't have a nice syntax for adding these directly on the exports object. Give up on fixing the whole thing: @@ -152,7 +173,7 @@ function getReplacementForProperty(property, sourceCode) { } let fixedValue = sourceCode.getText(property.value) - if (property.method) { + if (property.value.type === "FunctionExpression" && property.method) { fixedValue = `function${ property.value.generator ? "*" : "" } ${fixedValue}` @@ -162,6 +183,7 @@ function getReplacementForProperty(property, sourceCode) { } const lines = sourceCode .getCommentsBefore(property) + // @ts-expect-error getText supports both BaseNode and BaseNodeWithoutComments .map(comment => sourceCode.getText(comment)) if (property.key.type === "Literal" || property.computed) { // String or dynamic key: @@ -180,28 +202,43 @@ function getReplacementForProperty(property, sourceCode) { lines.push( ...sourceCode .getCommentsAfter(property) + // @ts-expect-error getText supports both BaseNode and BaseNodeWithoutComments .map(comment => sourceCode.getText(comment)) ) return lines.join("\n") } -// Check for a top level module.exports = { ... } +/** + * Check for a top level module.exports = { ... } + * @param {Node} node + * @returns {node is {parent: import('estree').AssignmentExpression & {parent: import('estree').ExpressionStatement, right: import('estree').ObjectExpression}}} + */ function isModuleExportsObjectAssignment(node) { return ( - node.parent.type === "AssignmentExpression" && - node.parent.parent.type === "ExpressionStatement" && - node.parent.parent.parent.type === "Program" && + node.parent?.type === "AssignmentExpression" && + node.parent?.parent?.type === "ExpressionStatement" && + node.parent.parent.parent?.type === "Program" && node.parent.right.type === "ObjectExpression" ) } -// Check for module.exports.foo or module.exports.bar reference or assignment +/** + * Check for module.exports.foo or module.exports.bar reference or assignment + * @param {Node} node + * @returns {node is import('estree').MemberExpression} + */ function isModuleExportsReference(node) { return ( - node.parent.type === "MemberExpression" && node.parent.object === node + node.parent?.type === "MemberExpression" && node.parent.object === node ) } +/** + * @param {Node} node + * @param {import('eslint').SourceCode} sourceCode + * @param {import('eslint').Rule.RuleFixer} fixer + * @returns {import('eslint').Rule.Fix | null} + */ function fixModuleExports(node, sourceCode, fixer) { if (isModuleExportsReference(node)) { return fixer.replaceText(node, "exports") @@ -223,6 +260,7 @@ function fixModuleExports(node, sourceCode, fixer) { return fixer.replaceText(node.parent, statements.join("\n\n")) } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -269,14 +307,16 @@ module.exports = { * module.exports = foo * ^^^^^^^^^^^^^^^^ * - * @param {ASTNode} node - The node of `exports`/`module.exports`. - * @returns {Location} The location info of reports. + * @param {Node} node - The node of `exports`/`module.exports`. + * @returns {import('estree').SourceLocation} The location info of reports. */ function getLocation(node) { const token = sourceCode.getTokenAfter(node) return { - start: node.loc.start, - end: token.loc.end, + start: /** @type {import('estree').SourceLocation} */ (node.loc) + .start, + end: /** @type {import('estree').SourceLocation} */ (token?.loc) + ?.end, } } @@ -284,6 +324,7 @@ module.exports = { * Enforces `module.exports`. * This warns references of `exports`. * + * @param {import('eslint').Scope.Scope} globalScope * @returns {void} */ function enforceModuleExports(globalScope) { @@ -294,9 +335,11 @@ module.exports = { for (const node of exportsNodes) { // Skip if it's a batch assignment. + const topAssignment = getTopAssignment(node) if ( + topAssignment && assignList.length > 0 && - assignList.indexOf(getTopAssignment(node)) !== -1 + assignList.indexOf(topAssignment) !== -1 ) { continue } @@ -314,6 +357,7 @@ module.exports = { * Enforces `exports`. * This warns references of `module.exports`. * + * @param {import('eslint').Scope.Scope} globalScope * @returns {void} */ function enforceExports(globalScope) { @@ -327,7 +371,10 @@ module.exports = { for (const node of moduleExportsNodes) { // Skip if it's a batch assignment. if (assignList.length > 0) { - const found = assignList.indexOf(getTopAssignment(node)) + const topAssignment = getTopAssignment(node) + const found = topAssignment + ? assignList.indexOf(topAssignment) + : -1 if (found !== -1) { batchAssignList.push(assignList[found]) assignList.splice(found, 1) @@ -353,8 +400,12 @@ module.exports = { continue } + const topAssignment = getTopAssignment(node) // Check if it's a batch assignment. - if (batchAssignList.indexOf(getTopAssignment(node)) !== -1) { + if ( + topAssignment && + batchAssignList.indexOf(topAssignment) !== -1 + ) { continue } diff --git a/lib/rules/file-extension-in-import.js b/lib/rules/file-extension-in-import.js index d0b7508f..82177636 100644 --- a/lib/rules/file-extension-in-import.js +++ b/lib/rules/file-extension-in-import.js @@ -6,7 +6,7 @@ const path = require("path") const fs = require("fs") -const mapTypescriptExtension = require("../util/map-typescript-extension") +const { convertTsExtensionToJs } = require("../util/map-typescript-extension") const visitImport = require("../util/visit-import") /** @@ -29,6 +29,7 @@ function getExistingExtensions(filePath) { } } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -69,7 +70,10 @@ module.exports = { */ function verify({ filePath, name, node, moduleType }) { // Ignore if it's not resolved to a file or it's a bare module. - if (moduleType !== "relative" && moduleType !== "absolute") { + if ( + (moduleType !== "relative" && moduleType !== "absolute") || + filePath == null + ) { return } @@ -78,7 +82,7 @@ module.exports = { const actualExt = path.extname(filePath) const style = overrideStyle[actualExt] || defaultStyle - const expectedExt = mapTypescriptExtension( + const expectedExt = convertTsExtensionToJs( context, filePath, actualExt @@ -91,7 +95,8 @@ module.exports = { messageId: "requireExt", data: { ext: expectedExt }, fix(fixer) { - const index = node.range[1] - 1 + const index = + /** @type {[number, number]} */ (node.range)[1] - 1 return fixer.insertTextBeforeRange( [index, index], expectedExt @@ -108,22 +113,24 @@ module.exports = { ) { const otherExtensions = getExistingExtensions(filePath) - let fix = fixer => { - const index = name.lastIndexOf(currentExt) - const start = node.range[0] + 1 + index - const end = start + currentExt.length - return fixer.removeRange([start, end]) - } - - if (otherExtensions.length > 1) { - fix = undefined - } - context.report({ node, messageId: "forbidExt", data: { ext: currentExt }, - fix, + fix: + otherExtensions.length > 1 + ? undefined + : fixer => { + const index = name.lastIndexOf(currentExt) + const start = + /** @type {[number, number]} */ ( + node.range + )[0] + + 1 + + index + const end = start + currentExt.length + return fixer.removeRange([start, end]) + }, }) } } diff --git a/lib/rules/global-require.js b/lib/rules/global-require.js index d993e595..78151e1e 100644 --- a/lib/rules/global-require.js +++ b/lib/rules/global-require.js @@ -17,36 +17,31 @@ const ACCEPTABLE_PARENTS = [ /** * Finds the eslint-scope reference in the given scope. - * @param {Object} scope The scope to search. - * @param {ASTNode} node The identifier node. - * @returns {Reference|null} Returns the found reference or null if none were found. + * @param {import('eslint').Scope.Scope} scope The scope to search. + * @param {import('estree').Node} node The identifier node. + * @returns {import('eslint').Scope.Reference|undefined} Returns the found reference or null if none were found. */ function findReference(scope, node) { - const references = scope.references.filter( + return scope.references.find( reference => - reference.identifier.range[0] === node.range[0] && - reference.identifier.range[1] === node.range[1] + reference.identifier.range?.[0] === node.range?.[0] && + reference.identifier.range?.[1] === node.range?.[1] ) - - /* istanbul ignore else: correctly returns null */ - if (references.length === 1) { - return references[0] - } - return null } /** * Checks if the given identifier node is shadowed in the given scope. - * @param {Object} scope The current scope. - * @param {ASTNode} node The identifier node to check. + * @param {import('eslint').Scope.Scope} scope The current scope. + * @param {import('estree').Node} node The identifier node to check. * @returns {boolean} Whether or not the name is shadowed. */ function isShadowed(scope, node) { const reference = findReference(scope, node) - return reference && reference.resolved && reference.resolved.defs.length > 0 + return Boolean(reference?.resolved?.defs?.length) } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -72,7 +67,8 @@ module.exports = { sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 if ( - node.callee.name === "require" && + /** @type {import('estree').Identifier} */ (node.callee) + .name === "require" && !isShadowed(currentScope, node.callee) ) { const isGoodRequire = ( diff --git a/lib/rules/handle-callback-err.js b/lib/rules/handle-callback-err.js index 7973a51c..313659ed 100644 --- a/lib/rules/handle-callback-err.js +++ b/lib/rules/handle-callback-err.js @@ -4,6 +4,7 @@ */ "use strict" +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -54,8 +55,8 @@ module.exports = { /** * Get the parameters of a given function scope. - * @param {Object} scope The function scope. - * @returns {Array} All parameters of the given scope. + * @param {import('eslint').Scope.Scope} scope The function scope. + * @returns {import('eslint').Scope.Variable[]} All parameters of the given scope. */ function getParameters(scope) { return scope.variables.filter( @@ -66,7 +67,7 @@ module.exports = { /** * Check to see if we're handling the error object properly. - * @param {ASTNode} node The AST node to check. + * @param {import('estree').Node} node The AST node to check. * @returns {void} */ function checkForError(node) { diff --git a/lib/rules/hashbang.js b/lib/rules/hashbang.js index 8baee0a0..b7984ec2 100644 --- a/lib/rules/hashbang.js +++ b/lib/rules/hashbang.js @@ -5,56 +5,22 @@ "use strict" const path = require("path") -const matcher = require("ignore") +const matcher = require("ignore").default const getConvertPath = require("../util/get-convert-path") -const getPackageJson = require("../util/get-package-json") +const { getPackageJson } = require("../util/get-package-json") const getNpmignore = require("../util/get-npmignore") +const { isBinFile } = require("../util/is-bin-file") const NODE_SHEBANG = "#!/usr/bin/env node\n" const SHEBANG_PATTERN = /^(#!.+?)?(\r)?\n/u const NODE_SHEBANG_PATTERN = /^#!\/usr\/bin\/env(?: -\S+)*(?: [^\s=-]+=\S+)* node(?: [^\r\n]+?)?\n/u -function simulateNodeResolutionAlgorithm(filePath, binField) { - const possibilities = [filePath] - let newFilePath = filePath.replace(/\.js$/u, "") - possibilities.push(newFilePath) - newFilePath = newFilePath.replace(/[/\\]index$/u, "") - possibilities.push(newFilePath) - return possibilities.includes(binField) -} - -/** - * Checks whether or not a given path is a `bin` file. - * - * @param {string} filePath - A file path to check. - * @param {string|object|undefined} binField - A value of the `bin` field of `package.json`. - * @param {string} basedir - A directory path that `package.json` exists. - * @returns {boolean} `true` if the file is a `bin` file. - */ -function isBinFile(filePath, binField, basedir) { - if (!binField) { - return false - } - if (typeof binField === "string") { - return simulateNodeResolutionAlgorithm( - filePath, - path.resolve(basedir, binField) - ) - } - return Object.keys(binField).some(key => - simulateNodeResolutionAlgorithm( - filePath, - path.resolve(basedir, binField[key]) - ) - ) -} - /** * Gets the shebang line (includes a line ending) from a given code. * - * @param {SourceCode} sourceCode - A source code object to check. + * @param {import('eslint').SourceCode} sourceCode - A source code object to check. * @returns {{length: number, bom: boolean, shebang: string, cr: boolean}} * shebang's information. * `retv.shebang` is an empty string if shebang doesn't exist. @@ -109,12 +75,12 @@ module.exports = { return {} } - const p = getPackageJson(filePath) - if (!p) { + const packageJson = getPackageJson(filePath) + if (typeof packageJson?.filePath !== "string") { return {} } - const packageDirectory = path.dirname(p.filePath) + const packageDirectory = path.dirname(packageJson.filePath) const originalAbsolutePath = path.resolve(filePath) const originalRelativePath = path @@ -128,6 +94,7 @@ module.exports = { convertedRelativePath ) + /** @type {{ additionalExecutables: string[] }} */ const { additionalExecutables = [] } = context.options?.[0] ?? {} const executable = matcher() @@ -148,14 +115,17 @@ module.exports = { const needsShebang = isExecutable.ignored === true || - isBinFile(convertedAbsolutePath, p.bin, packageDirectory) + isBinFile(convertedAbsolutePath, packageJson?.bin, packageDirectory) const info = getShebangInfo(sourceCode) return { Program() { const loc = { start: { line: 1, column: 0 }, - end: { line: 1, column: sourceCode.lines.at(0).length }, + end: { + line: 1, + column: sourceCode.lines.at(0)?.length ?? 0, + }, } if ( diff --git a/lib/rules/no-callback-literal.js b/lib/rules/no-callback-literal.js index a8ee0471..150271e4 100644 --- a/lib/rules/no-callback-literal.js +++ b/lib/rules/no-callback-literal.js @@ -4,6 +4,9 @@ */ "use strict" +const callbackNames = ["callback", "cb"] + +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -22,21 +25,17 @@ module.exports = { }, create(context) { - const callbackNames = ["callback", "cb"] - - function isCallback(name) { - return callbackNames.indexOf(name) > -1 - } - return { CallExpression(node) { const errorArg = node.arguments[0] - const calleeName = node.callee.name + const calleeName = /** @type {import('estree').Identifier} */ ( + node.callee + ).name if ( errorArg && !couldBeError(errorArg) && - isCallback(calleeName) + callbackNames.includes(calleeName) ) { context.report({ node, @@ -50,7 +49,7 @@ module.exports = { /** * Determine if a node has a possiblity to be an Error object - * @param {ASTNode} node ASTNode to check + * @param {import('estree').Node} node ASTNode to check * @returns {boolean} True if there is a chance it contains an Error obj */ function couldBeError(node) { diff --git a/lib/rules/no-deprecated-api.js b/lib/rules/no-deprecated-api.js index 957c51cc..ef7e90a5 100644 --- a/lib/rules/no-deprecated-api.js +++ b/lib/rules/no-deprecated-api.js @@ -16,6 +16,18 @@ const getSemverRange = require("../util/get-semver-range") const extendTrackmapWithNodePrefix = require("../util/extend-trackmap-with-node-prefix") const unprefixNodeColon = require("../util/unprefix-node-colon") +/** + * @typedef DeprecatedInfo + * @property {string} since + * @property {string|{ name: string, supported: string }[]|null} replacedBy + */ +/** + * @typedef ParsedOptions + * @property {import('semver').Range} version + * @property {Set} ignoredGlobalItems + * @property {Set} ignoredModuleItems + */ +/** @type {import('@eslint-community/eslint-utils').TraceMap} */ const rawModules = { _linklist: { [READ]: { since: "5.0.0", replacedBy: null }, @@ -637,8 +649,8 @@ const globals = { /** * Makes a replacement message. * - * @param {string|array|null} replacedBy - The text of substitute way. - * @param {Range} version - The configured version range + * @param {DeprecatedInfo["replacedBy"]} replacedBy - The text of substitute way. + * @param {import('semver').Range} version - The configured version range * @returns {string} Replacement message. */ function toReplaceMessage(replacedBy, version) { @@ -648,7 +660,11 @@ function toReplaceMessage(replacedBy, version) { message = replacedBy .filter( ({ supported }) => - !version.intersects(getSemverRange(`<${supported}`)) + !version.intersects( + /** @type {import('semver').Range} */ ( + getSemverRange(`<${supported}`) + ) + ) ) .map(({ name }) => name) .join(" or ") @@ -674,9 +690,8 @@ function toName(type, path) { /** * Parses the options. - * @param {RuleContext} context The rule context. - * @returns {{version:Range,ignoredGlobalItems:Set,ignoredModuleItems:Set}} Parsed - * value. + * @param {import('eslint').Rule.RuleContext} context The rule context. + * @returns {ParsedOptions} Parsed options */ function parseOptions(context) { const raw = context.options[0] || {} @@ -687,6 +702,7 @@ function parseOptions(context) { return Object.freeze({ version, ignoredGlobalItems, ignoredModuleItems }) } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -738,15 +754,17 @@ module.exports = { /** * Reports a use of a deprecated API. * - * @param {ASTNode} node - A node to report. + * @param {import('estree').Node} node - A node to report. * @param {string} name - The name of a deprecated API. - * @param {{since: number, replacedBy: string}} info - Information of the API. + * @param {DeprecatedInfo} info - Information of the API. * @returns {void} */ function reportItem(node, name, info) { context.report({ node, - loc: node.loc, + loc: /** @type {NonNullable} */ ( + node.loc + ), messageId: "deprecated", data: { name, diff --git a/lib/rules/no-exports-assign.js b/lib/rules/no-exports-assign.js index 12128011..fcfa9453 100644 --- a/lib/rules/no-exports-assign.js +++ b/lib/rules/no-exports-assign.js @@ -6,6 +6,11 @@ const { findVariable } = require("@eslint-community/eslint-utils") +/** + * @param {import('estree').Node} node + * @param {import('eslint').Scope.Scope} scope + * @returns {boolean} + */ function isExports(node, scope) { let variable = null @@ -18,6 +23,11 @@ function isExports(node, scope) { ) } +/** + * @param {import('estree').Node} node + * @param {import('eslint').Scope.Scope} scope + * @returns {boolean} + */ function isModuleExports(node, scope) { let variable = null @@ -34,6 +44,7 @@ function isModuleExports(node, scope) { ) } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-extraneous-import.js b/lib/rules/no-extraneous-import.js index 3514b204..0147bf32 100644 --- a/lib/rules/no-extraneous-import.js +++ b/lib/rules/no-extraneous-import.js @@ -10,6 +10,7 @@ const getConvertPath = require("../util/get-convert-path") const getResolvePaths = require("../util/get-resolve-paths") const visitImport = require("../util/visit-import") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-extraneous-require.js b/lib/rules/no-extraneous-require.js index 20d6d52f..f733ee96 100644 --- a/lib/rules/no-extraneous-require.js +++ b/lib/rules/no-extraneous-require.js @@ -11,6 +11,7 @@ const getResolvePaths = require("../util/get-resolve-paths") const getTryExtensions = require("../util/get-try-extensions") const visitRequire = require("../util/visit-require") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-hide-core-modules.js b/lib/rules/no-hide-core-modules.js index 167b45b8..01e05b28 100644 --- a/lib/rules/no-hide-core-modules.js +++ b/lib/rules/no-hide-core-modules.js @@ -9,11 +9,12 @@ "use strict" const path = require("path") -const getPackageJson = require("../util/get-package-json") +const { getPackageJson } = require("../util/get-package-json") const mergeVisitorsInPlace = require("../util/merge-visitors-in-place") const visitImport = require("../util/visit-import") const visitRequire = require("../util/visit-require") +/** @type {Set} */ const CORE_MODULES = new Set([ "assert", "buffer", @@ -49,6 +50,7 @@ const CORE_MODULES = new Set([ ]) const BACK_SLASH = /\\/gu +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -89,12 +91,11 @@ module.exports = { const filePath = path.resolve(filename) const dirPath = path.dirname(filePath) const packageJson = getPackageJson(filePath) - const deps = new Set( - [].concat( - Object.keys((packageJson && packageJson.dependencies) || {}), - Object.keys((packageJson && packageJson.devDependencies) || {}) - ) - ) + /** @type {Set} */ + const deps = new Set([ + ...Object.keys(packageJson?.dependencies ?? {}), + ...Object.keys(packageJson?.devDependencies ?? {}), + ]) const options = context.options[0] || {} const allow = options.allow || [] const ignoreDirectDependencies = Boolean( @@ -103,6 +104,7 @@ module.exports = { const ignoreIndirectDependencies = Boolean( options.ignoreIndirectDependencies ) + /** @type {import('../util/import-target.js')[]} */ const targets = [] return [ @@ -135,7 +137,9 @@ module.exports = { context.report({ node: target.node, - loc: target.node.loc, + loc: /** @type {NonNullable} */ ( + target.node.loc + ), messageId: "unexpectedImport", data: { name: path @@ -146,10 +150,6 @@ module.exports = { } }, }, - ].reduce( - (mergedVisitor, thisVisitor) => - mergeVisitorsInPlace(mergedVisitor, thisVisitor), - {} - ) + ].reduce(mergeVisitorsInPlace, {}) }, } diff --git a/lib/rules/no-missing-import.js b/lib/rules/no-missing-import.js index 521ca57b..537b4e1d 100644 --- a/lib/rules/no-missing-import.js +++ b/lib/rules/no-missing-import.js @@ -11,6 +11,7 @@ const getTSConfig = require("../util/get-tsconfig") const getTypescriptExtensionMap = require("../util/get-typescript-extension-map") const visitImport = require("../util/visit-import") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-missing-require.js b/lib/rules/no-missing-require.js index 032e41de..cd2eabb7 100644 --- a/lib/rules/no-missing-require.js +++ b/lib/rules/no-missing-require.js @@ -12,6 +12,7 @@ const getTryExtensions = require("../util/get-try-extensions") const getTypescriptExtensionMap = require("../util/get-typescript-extension-map") const visitRequire = require("../util/visit-require") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-mixed-requires.js b/lib/rules/no-mixed-requires.js index 07717b3d..662d519e 100644 --- a/lib/rules/no-mixed-requires.js +++ b/lib/rules/no-mixed-requires.js @@ -65,6 +65,7 @@ const BUILTIN_MODULES = [ "zlib", ] +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -126,7 +127,7 @@ module.exports = { /** * Determines the type of a declaration statement. - * @param {ASTNode} initExpression The init node of the VariableDeclarator. + * @param {import('estree').Node | undefined | null} initExpression The init node of the VariableDeclarator. * @returns {string} The type of declaration represented by the expression. */ function getDeclarationType(initExpression) { @@ -162,7 +163,7 @@ module.exports = { /** * Determines the type of module that is loaded via require. - * @param {ASTNode} initExpression The init node of the VariableDeclarator. + * @param {import('estree').Expression | import('estree').Super} initExpression The init node of the VariableDeclarator. * @returns {string} The module type. */ function inferModuleType(initExpression) { @@ -170,12 +171,18 @@ module.exports = { // "var x = require('glob').Glob;" return inferModuleType(initExpression.object) } - if (initExpression.arguments.length === 0) { + + if ( + /** @type {import('estree').CallExpression} */ (initExpression) + .arguments.length === 0 + ) { // "var x = require();" return REQ_COMPUTED } - const arg = initExpression.arguments[0] + const arg = /** @type {import('estree').CallExpression} */ ( + initExpression + ).arguments[0] if (arg.type !== "Literal" || typeof arg.value !== "string") { // "var x = require(42);" @@ -198,10 +205,11 @@ module.exports = { /** * Check if the list of variable declarations is mixed, i.e. whether it * contains both require and other declarations. - * @param {ASTNode} declarations The list of VariableDeclarators. + * @param {import('estree').VariableDeclarator[]} declarations The list of VariableDeclarators. * @returns {boolean} True if the declarations are mixed, false if not. */ function isMixed(declarations) { + /** @type {Record} */ const contains = {} for (const declaration of declarations) { @@ -219,15 +227,22 @@ module.exports = { /** * Check if all require declarations in the given list are of the same * type. - * @param {ASTNode} declarations The list of VariableDeclarators. + * @param {import('estree').VariableDeclarator[]} declarations The list of VariableDeclarators. * @returns {boolean} True if the declarations are grouped, false if not. */ function isGrouped(declarations) { + /** @type {Record} */ const found = {} for (const declaration of declarations) { if (getDeclarationType(declaration.init) === DECL_REQUIRE) { - found[inferModuleType(declaration.init)] = true + found[ + inferModuleType( + /** @type {import('estree').Expression} */ ( + declaration.init + ) + ) + ] = true } } diff --git a/lib/rules/no-new-require.js b/lib/rules/no-new-require.js index d67903c5..e3a38493 100644 --- a/lib/rules/no-new-require.js +++ b/lib/rules/no-new-require.js @@ -4,6 +4,7 @@ */ "use strict" +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", diff --git a/lib/rules/no-path-concat.js b/lib/rules/no-path-concat.js index 0c1756be..3419c81f 100644 --- a/lib/rules/no-path-concat.js +++ b/lib/rules/no-path-concat.js @@ -13,10 +13,10 @@ const { /** * Get the first char of the specified template element. - * @param {TemplateLiteral} node The `TemplateLiteral` node to get. + * @param {import('estree').TemplateLiteral} node The `TemplateLiteral` node to get. * @param {number} i The number of template elements to get first char. - * @param {Set} sepNodes The nodes of `path.sep`. - * @param {import("escope").Scope} globalScope The global scope object. + * @param {Set} sepNodes The nodes of `path.sep`. + * @param {import("eslint").Scope.Scope} globalScope The global scope object. * @param {string[]} outNextChars The array to collect chars. * @returns {void} */ @@ -48,9 +48,9 @@ function collectFirstCharsOfTemplateElement( /** * Get the first char of a given node. - * @param {TemplateLiteral} node The `TemplateLiteral` node to get. - * @param {Set} sepNodes The nodes of `path.sep`. - * @param {import("escope").Scope} globalScope The global scope object. + * @param {import('estree').Node} node The `TemplateLiteral` node to get. + * @param {Set} sepNodes The nodes of `path.sep`. + * @param {import("eslint").Scope.Scope} globalScope The global scope object. * @param {string[]} outNextChars The array to collect chars. * @returns {void} */ @@ -122,18 +122,20 @@ function collectFirstChars(node, sepNodes, globalScope, outNextChars) { function isPathSeparator(c) { return c === "/" || c === path.sep } +/** @typedef {import('estree').Identifier & { parent: import('estree').Node }} Identifier */ /** * Check if the given Identifier node is followed by string concatenation with a * path separator. * @param {Identifier} node The `__dirname` or `__filename` node to check. - * @param {Set} sepNodes The nodes of `path.sep`. - * @param {import("escope").Scope} globalScope The global scope object. + * @param {Set} sepNodes The nodes of `path.sep`. + * @param {import("eslint").Scope.Scope} globalScope The global scope object. * @returns {boolean} `true` if the given Identifier node is followed by string * concatenation with a path separator. */ function isConcat(node, sepNodes, globalScope) { const parent = node.parent + /** @type {string[]} */ const nextChars = [] if ( @@ -160,6 +162,7 @@ function isConcat(node, sepNodes, globalScope) { return nextChars.some(isPathSeparator) } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -179,11 +182,12 @@ module.exports = { create(context) { return { - "Program:exit"(node) { + "Program:exit"(programNode) { const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 const globalScope = - sourceCode.getScope?.(node) ?? context.getScope() + sourceCode.getScope?.(programNode) ?? context.getScope() const tracker = new ReferenceTracker(globalScope) + /** @type {Set} */ const sepNodes = new Set() // Collect `paht.sep` references @@ -203,9 +207,17 @@ module.exports = { __dirname: { [READ]: true }, __filename: { [READ]: true }, })) { - if (isConcat(node, sepNodes, globalScope)) { + if ( + isConcat( + /** @type {Identifier} */ (node), + sepNodes, + globalScope + ) + ) { context.report({ - node: node.parent, + node: /** @type {import('estree').Node & { parent: import('estree').Node }}*/ ( + node + ).parent, messageId: "usePathFunctions", }) } diff --git a/lib/rules/no-process-env.js b/lib/rules/no-process-env.js index 6592adbe..daec0def 100644 --- a/lib/rules/no-process-env.js +++ b/lib/rules/no-process-env.js @@ -8,6 +8,14 @@ // Rule Definition //------------------------------------------------------------------------------ +const querySelector = [ + `MemberExpression`, + `[computed!=true]`, + `[object.name="process"]`, + `[property.name="env"]`, +] + +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -25,18 +33,9 @@ module.exports = { create(context) { return { - MemberExpression(node) { - const objectName = node.object.name - const propertyName = node.property.name - - if ( - objectName === "process" && - !node.computed && - propertyName && - propertyName === "env" - ) { - context.report({ node, messageId: "unexpectedProcessEnv" }) - } + /** @param {import('estree').MemberExpression} node */ + [querySelector.join("")](node) { + context.report({ node, messageId: "unexpectedProcessEnv" }) }, } }, diff --git a/lib/rules/no-process-exit.js b/lib/rules/no-process-exit.js index 9be6f8b2..c295aec2 100644 --- a/lib/rules/no-process-exit.js +++ b/lib/rules/no-process-exit.js @@ -4,6 +4,14 @@ */ "use strict" +const querySelector = [ + `CallExpression > `, + `MemberExpression.callee`, + `[object.name="process"]`, + `[property.name="exit"]`, +] + +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -21,9 +29,8 @@ module.exports = { create(context) { return { - "CallExpression > MemberExpression.callee[object.name = 'process'][property.name = 'exit']"( - node - ) { + /** @param {import('estree').MemberExpression & { parent: import('estree').CallExpression}} node */ + [querySelector.join("")](node) { context.report({ node: node.parent, messageId: "noProcessExit", diff --git a/lib/rules/no-restricted-import.js b/lib/rules/no-restricted-import.js index f033c475..9969e5ed 100644 --- a/lib/rules/no-restricted-import.js +++ b/lib/rules/no-restricted-import.js @@ -7,6 +7,7 @@ const { checkForRestriction, messages } = require("../util/check-restricted") const visit = require("../util/visit-import") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", diff --git a/lib/rules/no-restricted-require.js b/lib/rules/no-restricted-require.js index 2bc34d03..d491b63a 100644 --- a/lib/rules/no-restricted-require.js +++ b/lib/rules/no-restricted-require.js @@ -8,6 +8,7 @@ const { checkForRestriction, messages } = require("../util/check-restricted") const visit = require("../util/visit-require") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", diff --git a/lib/rules/no-sync.js b/lib/rules/no-sync.js index 242b0cc4..f32f0fed 100644 --- a/lib/rules/no-sync.js +++ b/lib/rules/no-sync.js @@ -11,7 +11,7 @@ const allowedAtRootLevelSelector = [ ":function MemberExpression > Identifier[name=/Sync$/]", // readFileSync() ":function :not(MemberExpression) > Identifier[name=/Sync$/]", -] +].join(",") const disallowedAtRootLevelSelector = [ // fs.readFileSync() @@ -20,8 +20,9 @@ const disallowedAtRootLevelSelector = [ "MemberExpression > Identifier[name=/Sync$/]", // readFileSync() ":not(MemberExpression) > Identifier[name=/Sync$/]", -] +].join(",") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: "suggestion", @@ -54,6 +55,11 @@ module.exports = { : disallowedAtRootLevelSelector return { + /** + * [node description] + * @param {import('estree').Identifier & {parent: import('estree').Node}} node + * @returns {void} + */ [selector](node) { context.report({ node: node.parent, diff --git a/lib/rules/no-unpublished-bin.js b/lib/rules/no-unpublished-bin.js index 213cb9cc..f679758a 100644 --- a/lib/rules/no-unpublished-bin.js +++ b/lib/rules/no-unpublished-bin.js @@ -7,28 +7,10 @@ const path = require("path") const getConvertPath = require("../util/get-convert-path") const getNpmignore = require("../util/get-npmignore") -const getPackageJson = require("../util/get-package-json") - -/** - * Checks whether or not a given path is a `bin` file. - * - * @param {string} filePath - A file path to check. - * @param {string|object|undefined} binField - A value of the `bin` field of `package.json`. - * @param {string} basedir - A directory path that `package.json` exists. - * @returns {boolean} `true` if the file is a `bin` file. - */ -function isBinFile(filePath, binField, basedir) { - if (!binField) { - return false - } - if (typeof binField === "string") { - return filePath === path.resolve(basedir, binField) - } - return Object.keys(binField).some( - key => filePath === path.resolve(basedir, binField[key]) - ) -} +const { getPackageJson } = require("../util/get-package-json") +const { isBinFile } = require("../util/is-bin-file") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -63,20 +45,20 @@ module.exports = { rawFilePath = path.resolve(rawFilePath) // Find package.json - const p = getPackageJson(rawFilePath) - if (!p) { - return + const packageJson = getPackageJson(rawFilePath) + if (typeof packageJson?.filePath !== "string") { + return {} } // Convert by convertPath option - const basedir = path.dirname(p.filePath) + const basedir = path.dirname(packageJson.filePath) const relativePath = getConvertPath(context)( path.relative(basedir, rawFilePath).replace(/\\/gu, "/") ) const filePath = path.join(basedir, relativePath) // Check this file is bin. - if (!isBinFile(filePath, p.bin, basedir)) { + if (!isBinFile(filePath, packageJson.bin, basedir)) { return } diff --git a/lib/rules/no-unpublished-import.js b/lib/rules/no-unpublished-import.js index 1049d3a7..90166940 100644 --- a/lib/rules/no-unpublished-import.js +++ b/lib/rules/no-unpublished-import.js @@ -10,6 +10,7 @@ const getConvertPath = require("../util/get-convert-path") const getResolvePaths = require("../util/get-resolve-paths") const visitImport = require("../util/visit-import") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-unpublished-require.js b/lib/rules/no-unpublished-require.js index efd63145..e9edcf60 100644 --- a/lib/rules/no-unpublished-require.js +++ b/lib/rules/no-unpublished-require.js @@ -11,6 +11,7 @@ const getResolvePaths = require("../util/get-resolve-paths") const getTryExtensions = require("../util/get-try-extensions") const visitRequire = require("../util/visit-require") +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-unsupported-features/es-builtins.js b/lib/rules/no-unsupported-features/es-builtins.js index 9122ef0f..a2b881bd 100644 --- a/lib/rules/no-unsupported-features/es-builtins.js +++ b/lib/rules/no-unsupported-features/es-builtins.js @@ -12,7 +12,8 @@ const { const enumeratePropertyNames = require("../../util/enumerate-property-names") const getConfiguredNodeVersion = require("../../util/get-configured-node-version") -const trackMap = { +/** @type {Record<'globals' | 'modules', import('../../unsupported-features/types.js').SupportVersionTraceMap>} */ +const traceMap = { globals: { // Core js builtins AggregateError: { @@ -616,8 +617,10 @@ const trackMap = { [READ]: { supported: ["17.0.0"] }, }, }, + modules: {}, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -637,7 +640,7 @@ module.exports = { type: "array", items: { enum: Array.from( - enumeratePropertyNames(trackMap.globals) + enumeratePropertyNames(traceMap.globals) ), }, uniqueItems: true, @@ -651,7 +654,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkUnsupportedBuiltins(context, trackMap) + checkUnsupportedBuiltins(context, traceMap) }, } }, diff --git a/lib/rules/no-unsupported-features/es-syntax.js b/lib/rules/no-unsupported-features/es-syntax.js index 2c0c65ed..d0490272 100644 --- a/lib/rules/no-unsupported-features/es-syntax.js +++ b/lib/rules/no-unsupported-features/es-syntax.js @@ -17,18 +17,19 @@ const features = require("./es-syntax.json") const ignoreKeys = new Set() /** - * @typedef {Object} ESSyntax - * @property {string[]} aliases + * @typedef ESSyntax + * @property {string[]} [aliases] * @property {string | null} supported * @property {string} [strictMode] * @property {string} [deprecated] */ /** - * @typedef {Object} RuleMap + * @typedef RuleMap * @property {string} ruleId * @property {string} feature * @property {string[]} ignoreNames * @property {import("semver").Range} supported + * @property {import("semver").Range} [strictMode] * @property {boolean} deprecated */ @@ -57,18 +58,23 @@ const ruleMap = Object.entries(features).map(([ruleId, meta]) => { ruleId: ruleId, feature: ruleIdNegated, ignoreNames: ignoreNames, - supported: getSemverRange(meta.supported ?? "<0"), - strictMode: getSemverRange(meta.strictMode), + supported: /** @type {import("semver").Range} */ ( + getSemverRange(meta.supported ?? "<0") + ), + strictMode: meta.strictMode + ? getSemverRange(meta.strictMode) + : undefined, deprecated: Boolean(meta.deprecated), } }) /** * Parses the options. - * @param {RuleContext} context The rule context. - * @returns {{version:Range,ignores:Set}} Parsed value. + * @param {import('eslint').Rule.RuleContext} context The rule context. + * @returns {{version: import('semver').Range,ignores:Set}} Parsed value. */ function parseOptions(context) { + /** @type {{ ignores?: string[] }} */ const raw = context.options[0] || {} const version = getConfiguredNodeVersion(context) const ignores = new Set(raw.ignores || []) @@ -78,20 +84,25 @@ function parseOptions(context) { /** * Find the scope that a given node belongs to. - * @param {Scope} initialScope The initial scope to find. - * @param {Node} node The AST node. - * @returns {Scope} The scope that the node belongs to. + * @param {import('eslint').Scope.Scope} initialScope The initial scope to find. + * @param {import('estree').Node} node The AST node. + * @returns {import('eslint').Scope.Scope} The scope that the node belongs to. */ function normalizeScope(initialScope, node) { let scope = getInnermostScope(initialScope, node) - while (scope && scope.block === node) { + while (scope?.block === node && scope.upper) { scope = scope.upper } return scope } +/** + * @param {import('eslint').Rule.RuleContext} context + * @param {import('estree').Node} node + * @returns {boolean} + */ function isStrict(context, node) { const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 @@ -100,8 +111,8 @@ function isStrict(context, node) { /** * Define the visitor object as merging the rules of eslint-plugin-es-x. - * @param {RuleContext} context The rule context. - * @param {{version:Range,ignores:Set}} options The options. + * @param {import('eslint').Rule.RuleContext} context The rule context. + * @param {ReturnType} options The options. * @returns {object} The defined visitor. */ function defineVisitor(context, options) { @@ -117,7 +128,10 @@ function defineVisitor(context, options) { ) === false ) .map(rule => { - const esRule = esRules[rule.ruleId] + const esRule = /** @type {import('eslint').Rule.RuleModule} */ ( + esRules[rule.ruleId] + ) + /** @type {Partial} */ const esContext = { report(descriptor) { delete descriptor.fix @@ -131,7 +145,14 @@ function defineVisitor(context, options) { descriptor.data.supported = rule.supported.raw if (rule.strictMode != null) { - if (isStrict(context, descriptor.node) === false) { + if ( + isStrict( + context, + /** @type {{ node: import('estree').Node}} */ ( + descriptor + ).node + ) === false + ) { descriptor.data.supported = rule.strictMode.raw } else if ( rangeSubset(options.version, rule.supported) @@ -140,22 +161,25 @@ function defineVisitor(context, options) { } } - descriptor.messageId = + const messageId = rule.supported.raw === "<0" ? "not-supported-yet" : "not-supported-till" - super.report(descriptor) + super.report({ ...descriptor, messageId }) }, } Object.setPrototypeOf(esContext, context) - return esRule.create(esContext) + return esRule.create( + /** @type {import('eslint').Rule.RuleContext} */ (esContext) + ) }) .reduce(mergeVisitorsInPlace, {}) } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/no-unsupported-features/es-syntax.json b/lib/rules/no-unsupported-features/es-syntax.json index 87ca77a7..8fb47cf8 100644 --- a/lib/rules/no-unsupported-features/es-syntax.json +++ b/lib/rules/no-unsupported-features/es-syntax.json @@ -133,11 +133,11 @@ }, "no-date-prototype-getyear-setyear": { "supported": ">=0.10.0", - "deprecated": true + "supported": ">=0.10.0" }, "no-date-prototype-togmtstring": { "supported": ">=0.10.0", - "deprecated": true + "supported": ">=0.10.0" }, "no-default-parameters": { "supported": ">=6.0.0" @@ -153,7 +153,7 @@ }, "no-escape-unescape": { "supported": ">=0.10.0", - "deprecated": true + "supported": ">=0.10.0" }, "no-exponential-operators": { "supported": ">=7.0.0" @@ -241,7 +241,7 @@ }, "no-legacy-object-prototype-accessor-methods": { "supported": ">=0.10.0", - "deprecated": true + "supported": ">=0.10.0" }, "no-logical-assignment-operators": { "supported": ">=15.0.0" @@ -524,7 +524,7 @@ }, "no-string-create-html-methods": { "supported": ">=0.10.0", - "deprecated": true + "supported": ">=0.10.0" }, "no-string-fromcodepoint": { "supported": ">=4.0.0" diff --git a/lib/rules/no-unsupported-features/node-builtins.js b/lib/rules/no-unsupported-features/node-builtins.js index 1ade4477..be648555 100644 --- a/lib/rules/no-unsupported-features/node-builtins.js +++ b/lib/rules/no-unsupported-features/node-builtins.js @@ -16,10 +16,15 @@ const { NodeBuiltinModules, } = require("../../unsupported-features/node-builtins.js") -const trackMap = { +/** + * @typedef TraceMap + * @property {import('@eslint-community/eslint-utils').TraceMap} globals + * @property {import('@eslint-community/eslint-utils').TraceMap} modules + */ +const traceMap = { globals: { queueMicrotask: { - [READ]: { supported: ["12.0.0"], experimental: "11.0.0" }, + [READ]: { supported: ["12.0.0"], experimental: ["11.0.0"] }, }, require: { resolve: { @@ -29,28 +34,29 @@ const trackMap = { }, modules: NodeBuiltinModules, } -Object.assign(trackMap.globals, { - Buffer: trackMap.modules.buffer.Buffer, +Object.assign(traceMap.globals, { + Buffer: traceMap.modules.buffer.Buffer, TextDecoder: { - ...trackMap.modules.util.TextDecoder, + ...traceMap.modules.util.TextDecoder, [READ]: { supported: ["11.0.0"] }, }, TextEncoder: { - ...trackMap.modules.util.TextEncoder, + ...traceMap.modules.util.TextEncoder, [READ]: { supported: ["11.0.0"] }, }, URL: { - ...trackMap.modules.url.URL, + ...traceMap.modules.url.URL, [READ]: { supported: ["10.0.0"] }, }, URLSearchParams: { - ...trackMap.modules.url.URLSearchParams, + ...traceMap.modules.url.URLSearchParams, [READ]: { supported: ["10.0.0"] }, }, - console: trackMap.modules.console, - process: trackMap.modules.process, + console: traceMap.modules.console, + process: traceMap.modules.process, }) +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -71,8 +77,8 @@ module.exports = { items: { enum: Array.from( new Set([ - ...enumeratePropertyNames(trackMap.globals), - ...enumeratePropertyNames(trackMap.modules), + ...enumeratePropertyNames(traceMap.globals), + ...enumeratePropertyNames(traceMap.modules), ]) ), }, @@ -87,7 +93,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkUnsupportedBuiltins(context, trackMap) + checkUnsupportedBuiltins(context, traceMap) }, } }, diff --git a/lib/rules/prefer-global/buffer.js b/lib/rules/prefer-global/buffer.js index 7494dc68..46e358fb 100644 --- a/lib/rules/prefer-global/buffer.js +++ b/lib/rules/prefer-global/buffer.js @@ -7,17 +7,17 @@ const { READ } = require("@eslint-community/eslint-utils") const checkForPreferGlobal = require("../../util/check-prefer-global") -const trackMap = { +const traceMap = { globals: { Buffer: { [READ]: true }, }, modules: { - buffer: { - Buffer: { [READ]: true }, - }, + buffer: { Buffer: { [READ]: true } }, + "node:buffer": { Buffer: { [READ]: true } }, }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -40,7 +40,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkForPreferGlobal(context, trackMap) + checkForPreferGlobal(context, traceMap) }, } }, diff --git a/lib/rules/prefer-global/console.js b/lib/rules/prefer-global/console.js index 73aca52b..3e8c2848 100644 --- a/lib/rules/prefer-global/console.js +++ b/lib/rules/prefer-global/console.js @@ -7,15 +7,17 @@ const { READ } = require("@eslint-community/eslint-utils") const checkForPreferGlobal = require("../../util/check-prefer-global") -const trackMap = { +const traceMap = { globals: { console: { [READ]: true }, }, modules: { console: { [READ]: true }, + "node:console": { [READ]: true }, }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -37,7 +39,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkForPreferGlobal(context, trackMap) + checkForPreferGlobal(context, traceMap) }, } }, diff --git a/lib/rules/prefer-global/process.js b/lib/rules/prefer-global/process.js index 59510158..329f2288 100644 --- a/lib/rules/prefer-global/process.js +++ b/lib/rules/prefer-global/process.js @@ -7,15 +7,17 @@ const { READ } = require("@eslint-community/eslint-utils") const checkForPreferGlobal = require("../../util/check-prefer-global") -const trackMap = { +const traceMap = { globals: { process: { [READ]: true }, }, modules: { process: { [READ]: true }, + "node:process": { [READ]: true }, }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -37,7 +39,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkForPreferGlobal(context, trackMap) + checkForPreferGlobal(context, traceMap) }, } }, diff --git a/lib/rules/prefer-global/text-decoder.js b/lib/rules/prefer-global/text-decoder.js index b0c6c7d6..1d342004 100644 --- a/lib/rules/prefer-global/text-decoder.js +++ b/lib/rules/prefer-global/text-decoder.js @@ -7,17 +7,17 @@ const { READ } = require("@eslint-community/eslint-utils") const checkForPreferGlobal = require("../../util/check-prefer-global") -const trackMap = { +const traceMap = { globals: { TextDecoder: { [READ]: true }, }, modules: { - util: { - TextDecoder: { [READ]: true }, - }, + util: { TextDecoder: { [READ]: true } }, + "node:util": { TextDecoder: { [READ]: true } }, }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -40,7 +40,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkForPreferGlobal(context, trackMap) + checkForPreferGlobal(context, traceMap) }, } }, diff --git a/lib/rules/prefer-global/text-encoder.js b/lib/rules/prefer-global/text-encoder.js index a527470e..645f5ea7 100644 --- a/lib/rules/prefer-global/text-encoder.js +++ b/lib/rules/prefer-global/text-encoder.js @@ -7,17 +7,17 @@ const { READ } = require("@eslint-community/eslint-utils") const checkForPreferGlobal = require("../../util/check-prefer-global") -const trackMap = { +const traceMap = { globals: { TextEncoder: { [READ]: true }, }, modules: { - util: { - TextEncoder: { [READ]: true }, - }, + util: { TextEncoder: { [READ]: true } }, + "node:util": { TextEncoder: { [READ]: true } }, }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -40,7 +40,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkForPreferGlobal(context, trackMap) + checkForPreferGlobal(context, traceMap) }, } }, diff --git a/lib/rules/prefer-global/url-search-params.js b/lib/rules/prefer-global/url-search-params.js index 4c1d1699..afad6dd3 100644 --- a/lib/rules/prefer-global/url-search-params.js +++ b/lib/rules/prefer-global/url-search-params.js @@ -7,17 +7,17 @@ const { READ } = require("@eslint-community/eslint-utils") const checkForPreferGlobal = require("../../util/check-prefer-global") -const trackMap = { +const traceMap = { globals: { URLSearchParams: { [READ]: true }, }, modules: { - url: { - URLSearchParams: { [READ]: true }, - }, + url: { URLSearchParams: { [READ]: true } }, + "node:url": { URLSearchParams: { [READ]: true } }, }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -40,7 +40,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkForPreferGlobal(context, trackMap) + checkForPreferGlobal(context, traceMap) }, } }, diff --git a/lib/rules/prefer-global/url.js b/lib/rules/prefer-global/url.js index 7d2806d1..403630fe 100644 --- a/lib/rules/prefer-global/url.js +++ b/lib/rules/prefer-global/url.js @@ -7,17 +7,17 @@ const { READ } = require("@eslint-community/eslint-utils") const checkForPreferGlobal = require("../../util/check-prefer-global") -const trackMap = { +const traceMap = { globals: { URL: { [READ]: true }, }, modules: { - url: { - URL: { [READ]: true }, - }, + url: { URL: { [READ]: true } }, + "node:url": { URL: { [READ]: true } }, }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -39,7 +39,7 @@ module.exports = { create(context) { return { "Program:exit"() { - checkForPreferGlobal(context, trackMap) + checkForPreferGlobal(context, traceMap) }, } }, diff --git a/lib/rules/prefer-node-protocol.js b/lib/rules/prefer-node-protocol.js index f6133a98..85fba1cf 100644 --- a/lib/rules/prefer-node-protocol.js +++ b/lib/rules/prefer-node-protocol.js @@ -13,6 +13,14 @@ const mergeVisitorsInPlace = require("../util/merge-visitors-in-place") const messageId = "preferNodeProtocol" +const supportedRangeForEsm = /** @type {import('semver').Range} */ ( + getSemverRange("^12.20.0 || >= 14.13.1") +) +const supportedRangeForCjs = /** @type {import('semver').Range} */ ( + getSemverRange("^14.18.0 || >= 16.0.0") +) + +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -37,6 +45,13 @@ module.exports = { type: "suggestion", }, create(context) { + /** + * @param {import('estree').Node} node + * @param {object} options + * @param {string} options.name + * @param {number} options.argumentsLength + * @returns {node is import('estree').CallExpression} + */ function isCallExpression(node, { name, argumentsLength }) { if (node?.type !== "CallExpression") { return false @@ -60,33 +75,46 @@ module.exports = { return true } + /** + * @param {import('estree').Node} node + * @returns {node is import('estree').Literal} + */ function isStringLiteral(node) { return node?.type === "Literal" && typeof node.type === "string" } + /** + * @param {import('estree').Node | undefined} node + * @returns {node is import('estree').CallExpression} + */ function isStaticRequire(node) { return ( + node != null && isCallExpression(node, { name: "require", argumentsLength: 1, - }) && isStringLiteral(node.arguments[0]) + }) && + isStringLiteral(node.arguments[0]) ) } + /** + * @param {import('eslint').Rule.RuleContext} context + * @param {import('../util/import-target.js').ModuleStyle} moduleStyle + * @returns {boolean} + */ function isEnablingThisRule(context, moduleStyle) { const version = getConfiguredNodeVersion(context) - const supportedVersionForEsm = "^12.20.0 || >= 14.13.1" // Only check Node.js version because this rule is meaningless if configured Node.js version doesn't match semver range. - if (!version.intersects(getSemverRange(supportedVersionForEsm))) { + if (!version.intersects(supportedRangeForEsm)) { return false } - const supportedVersionForCjs = "^14.18.0 || >= 16.0.0" // Only check when using `require` if ( moduleStyle === "require" && - !version.intersects(getSemverRange(supportedVersionForCjs)) + !version.intersects(supportedRangeForCjs) ) { return false } @@ -94,6 +122,7 @@ module.exports = { return true } + /** @type {import('../util/import-target.js')[]} */ const targets = [] return [ visitImport(context, { includeCore: true }, importTargets => { @@ -117,7 +146,7 @@ module.exports = { continue } - const { value } = node + const { value } = /** @type {{ value: string }}*/ (node) if ( typeof value !== "string" || value.startsWith("node:") || @@ -134,7 +163,8 @@ module.exports = { moduleName: value, }, fix(fixer) { - const firstCharacterIndex = node.range[0] + 1 + const firstCharacterIndex = + (node?.range?.[0] ?? 0) + 1 return fixer.replaceTextRange( [firstCharacterIndex, firstCharacterIndex], "node:" diff --git a/lib/rules/prefer-promises/dns.js b/lib/rules/prefer-promises/dns.js index 4062a5a5..165143a9 100644 --- a/lib/rules/prefer-promises/dns.js +++ b/lib/rules/prefer-promises/dns.js @@ -10,7 +10,8 @@ const { ReferenceTracker, } = require("@eslint-community/eslint-utils") -const trackMap = { +/** @type {import('@eslint-community/eslint-utils').TraceMap} */ +const traceMap = { dns: { lookup: { [CALL]: true }, lookupService: { [CALL]: true }, @@ -32,8 +33,9 @@ const trackMap = { setServers: { [CALL]: true }, }, } -trackMap["node:dns"] = trackMap.dns +traceMap["node:dns"] = traceMap.dns +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -57,8 +59,8 @@ module.exports = { const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 const tracker = new ReferenceTracker(scope, { mode: "legacy" }) const references = [ - ...tracker.iterateCjsReferences(trackMap), - ...tracker.iterateEsmReferences(trackMap), + ...tracker.iterateCjsReferences(traceMap), + ...tracker.iterateEsmReferences(traceMap), ] for (const { node, path } of references) { diff --git a/lib/rules/prefer-promises/fs.js b/lib/rules/prefer-promises/fs.js index f0404348..d828e51d 100644 --- a/lib/rules/prefer-promises/fs.js +++ b/lib/rules/prefer-promises/fs.js @@ -6,7 +6,8 @@ const { CALL, ReferenceTracker } = require("@eslint-community/eslint-utils") -const trackMap = { +/** @type {import('@eslint-community/eslint-utils').TraceMap} */ +const traceMap = { fs: { access: { [CALL]: true }, copyFile: { [CALL]: true }, @@ -34,8 +35,9 @@ const trackMap = { readFile: { [CALL]: true }, }, } -trackMap["node:fs"] = trackMap.fs +traceMap["node:fs"] = traceMap.fs +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { @@ -58,8 +60,8 @@ module.exports = { const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 const tracker = new ReferenceTracker(scope, { mode: "legacy" }) const references = [ - ...tracker.iterateCjsReferences(trackMap), - ...tracker.iterateEsmReferences(trackMap), + ...tracker.iterateCjsReferences(traceMap), + ...tracker.iterateEsmReferences(traceMap), ] for (const { node, path } of references) { diff --git a/lib/rules/process-exit-as-throw.js b/lib/rules/process-exit-as-throw.js index 5b9944de..6ca95955 100644 --- a/lib/rules/process-exit-as-throw.js +++ b/lib/rules/process-exit-as-throw.js @@ -5,26 +5,28 @@ */ "use strict" +/** @type {typeof import('../types-code-path-analysis/code-path-analyzer.js')} */ const CodePathAnalyzer = safeRequire( "eslint/lib/linter/code-path-analysis/code-path-analyzer", "eslint/lib/code-path-analysis/code-path-analyzer" ) +/** @type {typeof import('../types-code-path-analysis/code-path-segment.js')} */ const CodePathSegment = safeRequire( "eslint/lib/linter/code-path-analysis/code-path-segment", "eslint/lib/code-path-analysis/code-path-segment" ) +/** @type {typeof import('../types-code-path-analysis/code-path.js')} */ const CodePath = safeRequire( "eslint/lib/linter/code-path-analysis/code-path", "eslint/lib/code-path-analysis/code-path" ) -const originalLeaveNode = - CodePathAnalyzer && CodePathAnalyzer.prototype.leaveNode +const originalLeaveNode = CodePathAnalyzer?.prototype?.leaveNode /** * Imports a specific module. * @param {...string} moduleNames - module names to import. - * @returns {object|null} The imported object, or null. + * @returns {*} The imported object, or null. */ function safeRequire(...moduleNames) { for (const moduleName of moduleNames) { @@ -41,8 +43,8 @@ function safeRequire(...moduleNames) { /** * Copied from https://github.com/eslint/eslint/blob/16fad5880bb70e9dddbeab8ed0f425ae51f5841f/lib/code-path-analysis/code-path-analyzer.js#L137 * - * @param {CodePathAnalyzer} analyzer - The instance. - * @param {ASTNode} node - The current AST node. + * @param {import('../types-code-path-analysis/code-path-analyzer.js')} analyzer - The instance. + * @param {import('eslint').Rule.Node} node - The current AST node. * @returns {void} */ function forwardCurrentToHead(analyzer, node) { @@ -95,7 +97,7 @@ function forwardCurrentToHead(analyzer, node) { /** * Checks whether a given node is `process.exit()` or not. * - * @param {ASTNode} node - A node to check. + * @param {import('eslint').Rule.Node} node - A node to check. * @returns {boolean} `true` if the node is `process.exit()`. */ function isProcessExit(node) { @@ -114,8 +116,8 @@ function isProcessExit(node) { * The function to override `CodePathAnalyzer.prototype.leaveNode` in order to * address `process.exit()` as throw. * - * @this CodePathAnalyzer - * @param {ASTNode} node - A node to be left. + * @this {import('../types-code-path-analysis/code-path-analyzer.js')} + * @param {import('eslint').Rule.Node} node - A node to be left. * @returns {void} */ function overrideLeaveNode(node) { @@ -144,6 +146,7 @@ const visitor = }, } +/** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { docs: { diff --git a/lib/rules/shebang.js b/lib/rules/shebang.js index 86ac0236..585947be 100644 --- a/lib/rules/shebang.js +++ b/lib/rules/shebang.js @@ -12,7 +12,7 @@ module.exports = { ...hashbang.meta, deprecated: true, replacedBy: ["n/hashbang"], - docs: { ...hashbang.meta.docs, recommended: false }, + docs: { ...hashbang.meta?.docs, recommended: false }, }, create: hashbang.create, } diff --git a/lib/types-code-path-analysis/code-path-analyzer.d.ts b/lib/types-code-path-analysis/code-path-analyzer.d.ts new file mode 100644 index 00000000..1d4f652d --- /dev/null +++ b/lib/types-code-path-analysis/code-path-analyzer.d.ts @@ -0,0 +1,47 @@ +export = CodePathAnalyzer; + +interface EventGenerator { + emitter: import('node:events').EventEmitter; + enterNode(node: import('eslint').Rule.Node): void; + leaveNode(node: import('eslint').Rule.Node): void; +} + +/** + * The class to analyze code paths. + * This class implements the EventGenerator interface. + */ +declare class CodePathAnalyzer { + /** + * @param {EventGenerator} eventGenerator An event generator to wrap. + */ + constructor(eventGenerator: EventGenerator); + original: EventGenerator; + emitter: any; + codePath: any; + idGenerator: IdGenerator; + currentNode: any; + /** + * This is called on a code path looped. + * Then this raises a looped event. + * @param {CodePathSegment} fromSegment A segment of prev. + * @param {CodePathSegment} toSegment A segment of next. + * @returns {void} + */ + onLooped(fromSegment: CodePathSegment, toSegment: CodePathSegment): void; + /** + * Does the process to enter a given AST node. + * This updates state of analysis and calls `enterNode` of the wrapped. + * @param {ASTNode} node A node which is entering. + * @returns {void} + */ + enterNode(node: import('eslint').Rule.Node): void; + /** + * Does the process to leave a given AST node. + * This updates state of analysis and calls `leaveNode` of the wrapped. + * @param {ASTNode} node A node which is leaving. + * @returns {void} + */ + leaveNode(node: import('eslint').Rule.Node): void; +} +import IdGenerator = require("./id-generator"); +import CodePathSegment = require("./code-path-segment"); diff --git a/lib/types-code-path-analysis/code-path-segment.d.ts b/lib/types-code-path-analysis/code-path-segment.d.ts new file mode 100644 index 00000000..a78e61ef --- /dev/null +++ b/lib/types-code-path-analysis/code-path-segment.d.ts @@ -0,0 +1,115 @@ +export = CodePathSegment; +/** + * A code path segment. + * + * Each segment is arranged in a series of linked lists (implemented by arrays) + * that keep track of the previous and next segments in a code path. In this way, + * you can navigate between all segments in any code path so long as you have a + * reference to any segment in that code path. + * + * When first created, the segment is in a detached state, meaning that it knows the + * segments that came before it but those segments don't know that this new segment + * follows it. Only when `CodePathSegment#markUsed()` is called on a segment does it + * officially become part of the code path by updating the previous segments to know + * that this new segment follows. + */ +declare class CodePathSegment { + /** + * Creates the root segment. + * @param {string} id An identifier. + * @returns {CodePathSegment} The created segment. + */ + static newRoot(id: string): CodePathSegment; + /** + * Creates a new segment and appends it after the given segments. + * @param {string} id An identifier. + * @param {CodePathSegment[]} allPrevSegments An array of the previous segments + * to append to. + * @returns {CodePathSegment} The created segment. + */ + static newNext(id: string, allPrevSegments: CodePathSegment[]): CodePathSegment; + /** + * Creates an unreachable segment and appends it after the given segments. + * @param {string} id An identifier. + * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. + * @returns {CodePathSegment} The created segment. + */ + static newUnreachable(id: string, allPrevSegments: CodePathSegment[]): CodePathSegment; + /** + * Creates a segment that follows given segments. + * This factory method does not connect with `allPrevSegments`. + * But this inherits `reachable` flag. + * @param {string} id An identifier. + * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. + * @returns {CodePathSegment} The created segment. + */ + static newDisconnected(id: string, allPrevSegments: CodePathSegment[]): CodePathSegment; + /** + * Marks a given segment as used. + * + * And this function registers the segment into the previous segments as a next. + * @param {CodePathSegment} segment A segment to mark. + * @returns {void} + */ + static markUsed(segment: CodePathSegment): void; + /** + * Marks a previous segment as looped. + * @param {CodePathSegment} segment A segment. + * @param {CodePathSegment} prevSegment A previous segment to mark. + * @returns {void} + */ + static markPrevSegmentAsLooped(segment: CodePathSegment, prevSegment: CodePathSegment): void; + /** + * Creates a new array based on an array of segments. If any segment in the + * array is unused, then it is replaced by all of its previous segments. + * All used segments are returned as-is without replacement. + * @param {CodePathSegment[]} segments The array of segments to flatten. + * @returns {CodePathSegment[]} The flattened array. + */ + static flattenUnusedSegments(segments: CodePathSegment[]): CodePathSegment[]; + /** + * Creates a new instance. + * @param {string} id An identifier. + * @param {CodePathSegment[]} allPrevSegments An array of the previous segments. + * This array includes unreachable segments. + * @param {boolean} reachable A flag which shows this is reachable. + */ + constructor(id: string, allPrevSegments: CodePathSegment[], reachable: boolean); + /** + * The identifier of this code path. + * Rules use it to store additional information of each rule. + * @type {string} + */ + id: string; + /** + * An array of the next reachable segments. + * @type {CodePathSegment[]} + */ + nextSegments: CodePathSegment[]; + /** + * An array of the previous reachable segments. + * @type {CodePathSegment[]} + */ + prevSegments: CodePathSegment[]; + /** + * An array of all next segments including reachable and unreachable. + * @type {CodePathSegment[]} + */ + allNextSegments: CodePathSegment[]; + /** + * An array of all previous segments including reachable and unreachable. + * @type {CodePathSegment[]} + */ + allPrevSegments: CodePathSegment[]; + /** + * A flag which shows this is reachable. + * @type {boolean} + */ + reachable: boolean; + /** + * Checks a given previous segment is coming from the end of a loop. + * @param {CodePathSegment} segment A previous segment to check. + * @returns {boolean} `true` if the segment is coming from the end of a loop. + */ + isLoopedPrevSegment(segment: CodePathSegment): boolean; +} diff --git a/lib/types-code-path-analysis/code-path-state.d.ts b/lib/types-code-path-analysis/code-path-state.d.ts new file mode 100644 index 00000000..ee7716ae --- /dev/null +++ b/lib/types-code-path-analysis/code-path-state.d.ts @@ -0,0 +1,895 @@ +import IdGenerator = require("./id-generator") + +export = CodePathState; +/** + * A class which manages state to analyze code paths. + */ +declare class CodePathState { + /** + * Creates a new instance. + * @param {IdGenerator} idGenerator An id generator to generate id for code + * path segments. + * @param {Function} onLooped A callback function to notify looping. + */ + constructor(idGenerator: IdGenerator, onLooped: Function); + /** + * The ID generator to use when creating new segments. + * @type {IdGenerator} + */ + idGenerator: IdGenerator; + /** + * A callback function to call when there is a loop. + * @type {Function} + */ + notifyLooped: Function; + /** + * The root fork context for this state. + * @type {ForkContext} + */ + forkContext: ForkContext; + /** + * Context for logical expressions, conditional expressions, `if` statements, + * and loops. + * @type {ChoiceContext} + */ + choiceContext: ChoiceContext; + /** + * Context for `switch` statements. + * @type {SwitchContext} + */ + switchContext: SwitchContext; + /** + * Context for `try` statements. + * @type {TryContext} + */ + tryContext: TryContext; + /** + * Context for loop statements. + * @type {LoopContext} + */ + loopContext: LoopContext; + /** + * Context for `break` statements. + * @type {BreakContext} + */ + breakContext: BreakContext; + /** + * Context for `ChainExpression` nodes. + * @type {ChainContext} + */ + chainContext: ChainContext; + /** + * An array that tracks the current segments in the state. The array + * starts empty and segments are added with each `onCodePathSegmentStart` + * event and removed with each `onCodePathSegmentEnd` event. Effectively, + * this is tracking the code path segment traversal as the state is + * modified. + * @type {Array} + */ + currentSegments: Array; + /** + * Tracks the starting segment for this path. This value never changes. + * @type {CodePathSegment} + */ + initialSegment: CodePathSegment; + /** + * The final segments of the code path which are either `return` or `throw`. + * This is a union of the segments in `returnedForkContext` and `thrownForkContext`. + * @type {Array} + */ + finalSegments: Array; + /** + * The final segments of the code path which are `return`. These + * segments are also contained in `finalSegments`. + * @type {Array} + */ + returnedForkContext: Array; + /** + * The final segments of the code path which are `throw`. These + * segments are also contained in `finalSegments`. + * @type {Array} + */ + thrownForkContext: Array; + /** + * A passthrough property exposing the current pointer as part of the API. + * @type {CodePathSegment[]} + */ + get headSegments(): CodePathSegment[]; + /** + * The parent forking context. + * This is used for the root of new forks. + * @type {ForkContext} + */ + get parentForkContext(): ForkContext; + /** + * Creates and stacks new forking context. + * @param {boolean} forkLeavingPath A flag which shows being in a + * "finally" block. + * @returns {ForkContext} The created context. + */ + pushForkContext(forkLeavingPath: boolean): ForkContext; + /** + * Pops and merges the last forking context. + * @returns {ForkContext} The last context. + */ + popForkContext(): ForkContext; + /** + * Creates a new path. + * @returns {void} + */ + forkPath(): void; + /** + * Creates a bypass path. + * This is used for such as IfStatement which does not have "else" chunk. + * @returns {void} + */ + forkBypassPath(): void; + /** + * Creates a context for ConditionalExpression, LogicalExpression, AssignmentExpression (logical assignments only), + * IfStatement, WhileStatement, DoWhileStatement, or ForStatement. + * + * LogicalExpressions have cases that it goes different paths between the + * `true` case and the `false` case. + * + * For Example: + * + * if (a || b) { + * foo(); + * } else { + * bar(); + * } + * + * In this case, `b` is evaluated always in the code path of the `else` + * block, but it's not so in the code path of the `if` block. + * So there are 3 paths. + * + * a -> foo(); + * a -> b -> foo(); + * a -> b -> bar(); + * @param {string} kind A kind string. + * If the new context is LogicalExpression's or AssignmentExpression's, this is `"&&"` or `"||"` or `"??"`. + * If it's IfStatement's or ConditionalExpression's, this is `"test"`. + * Otherwise, this is `"loop"`. + * @param {boolean} isForkingAsResult Indicates if the result of the choice + * creates a fork. + * @returns {void} + */ + pushChoiceContext(kind: string, isForkingAsResult: boolean): void; + /** + * Pops the last choice context and finalizes it. + * This is called upon leaving a node that represents a choice. + * @throws {Error} (Unreachable.) + * @returns {ChoiceContext} The popped context. + */ + popChoiceContext(): ChoiceContext; + /** + * Creates a code path segment to represent right-hand operand of a logical + * expression. + * This is called in the preprocessing phase when entering a node. + * @throws {Error} (Unreachable.) + * @returns {void} + */ + makeLogicalRight(): void; + /** + * Makes a code path segment of the `if` block. + * @returns {void} + */ + makeIfConsequent(): void; + /** + * Makes a code path segment of the `else` block. + * @returns {void} + */ + makeIfAlternate(): void; + /** + * Pushes a new `ChainExpression` context to the stack. This method is + * called when entering a `ChainExpression` node. A chain context is used to + * count forking in the optional chain then merge them on the exiting from the + * `ChainExpression` node. + * @returns {void} + */ + pushChainContext(): void; + /** + * Pop a `ChainExpression` context from the stack. This method is called on + * exiting from each `ChainExpression` node. This merges all forks of the + * last optional chaining. + * @returns {void} + */ + popChainContext(): void; + /** + * Create a choice context for optional access. + * This method is called on entering to each `(Call|Member)Expression[optional=true]` node. + * This creates a choice context as similar to `LogicalExpression[operator="??"]` node. + * @returns {void} + */ + makeOptionalNode(): void; + /** + * Create a fork. + * This method is called on entering to the `arguments|property` property of each `(Call|Member)Expression` node. + * @returns {void} + */ + makeOptionalRight(): void; + /** + * Creates a context object of SwitchStatement and stacks it. + * @param {boolean} hasCase `true` if the switch statement has one or more + * case parts. + * @param {string|null} label The label text. + * @returns {void} + */ + pushSwitchContext(hasCase: boolean, label: string | null): void; + /** + * Pops the last context of SwitchStatement and finalizes it. + * + * - Disposes all forking stack for `case` and `default`. + * - Creates the next code path segment from `context.brokenForkContext`. + * - If the last `SwitchCase` node is not a `default` part, creates a path + * to the `default` body. + * @returns {void} + */ + popSwitchContext(): void; + /** + * Makes a code path segment for a `SwitchCase` node. + * @param {boolean} isCaseBodyEmpty `true` if the body is empty. + * @param {boolean} isDefaultCase `true` if the body is the default case. + * @returns {void} + */ + makeSwitchCaseBody(isCaseBodyEmpty: boolean, isDefaultCase: boolean): void; + /** + * Creates a context object of TryStatement and stacks it. + * @param {boolean} hasFinalizer `true` if the try statement has a + * `finally` block. + * @returns {void} + */ + pushTryContext(hasFinalizer: boolean): void; + /** + * Pops the last context of TryStatement and finalizes it. + * @returns {void} + */ + popTryContext(): void; + /** + * Makes a code path segment for a `catch` block. + * @returns {void} + */ + makeCatchBlock(): void; + /** + * Makes a code path segment for a `finally` block. + * + * In the `finally` block, parallel paths are created. The parallel paths + * are used as leaving-paths. The leaving-paths are paths from `return` + * statements and `throw` statements in a `try` block or a `catch` block. + * @returns {void} + */ + makeFinallyBlock(): void; + /** + * Makes a code path segment from the first throwable node to the `catch` + * block or the `finally` block. + * @returns {void} + */ + makeFirstThrowablePathInTryBlock(): void; + /** + * Creates a context object of a loop statement and stacks it. + * @param {string} type The type of the node which was triggered. One of + * `WhileStatement`, `DoWhileStatement`, `ForStatement`, `ForInStatement`, + * and `ForStatement`. + * @param {string|null} label A label of the node which was triggered. + * @throws {Error} (Unreachable - unknown type.) + * @returns {void} + */ + pushLoopContext(type: string, label: string | null): void; + /** + * Pops the last context of a loop statement and finalizes it. + * @throws {Error} (Unreachable - unknown type.) + * @returns {void} + */ + popLoopContext(): void; + /** + * Makes a code path segment for the test part of a WhileStatement. + * @param {boolean|undefined} test The test value (only when constant). + * @returns {void} + */ + makeWhileTest(test: boolean | undefined): void; + /** + * Makes a code path segment for the body part of a WhileStatement. + * @returns {void} + */ + makeWhileBody(): void; + /** + * Makes a code path segment for the body part of a DoWhileStatement. + * @returns {void} + */ + makeDoWhileBody(): void; + /** + * Makes a code path segment for the test part of a DoWhileStatement. + * @param {boolean|undefined} test The test value (only when constant). + * @returns {void} + */ + makeDoWhileTest(test: boolean | undefined): void; + /** + * Makes a code path segment for the test part of a ForStatement. + * @param {boolean|undefined} test The test value (only when constant). + * @returns {void} + */ + makeForTest(test: boolean | undefined): void; + /** + * Makes a code path segment for the update part of a ForStatement. + * @returns {void} + */ + makeForUpdate(): void; + /** + * Makes a code path segment for the body part of a ForStatement. + * @returns {void} + */ + makeForBody(): void; + /** + * Makes a code path segment for the left part of a ForInStatement and a + * ForOfStatement. + * @returns {void} + */ + makeForInOfLeft(): void; + /** + * Makes a code path segment for the right part of a ForInStatement and a + * ForOfStatement. + * @returns {void} + */ + makeForInOfRight(): void; + /** + * Makes a code path segment for the body part of a ForInStatement and a + * ForOfStatement. + * @returns {void} + */ + makeForInOfBody(): void; + /** + * Creates new context in which a `break` statement can be used. This occurs inside of a loop, + * labeled statement, or switch statement. + * @param {boolean} breakable Indicates if we are inside a statement where + * `break` without a label will exit the statement. + * @param {string|null} label The label associated with the statement. + * @returns {BreakContext} The new context. + */ + pushBreakContext(breakable: boolean, label: string | null): BreakContext; + /** + * Removes the top item of the break context stack. + * @returns {Object} The removed context. + */ + popBreakContext(): any; + /** + * Makes a path for a `break` statement. + * + * It registers the head segment to a context of `break`. + * It makes new unreachable segment, then it set the head with the segment. + * @param {string|null} label A label of the break statement. + * @returns {void} + */ + makeBreak(label: string | null): void; + /** + * Makes a path for a `continue` statement. + * + * It makes a looping path. + * It makes new unreachable segment, then it set the head with the segment. + * @param {string|null} label A label of the continue statement. + * @returns {void} + */ + makeContinue(label: string | null): void; + /** + * Makes a path for a `return` statement. + * + * It registers the head segment to a context of `return`. + * It makes new unreachable segment, then it set the head with the segment. + * @returns {void} + */ + makeReturn(): void; + /** + * Makes a path for a `throw` statement. + * + * It registers the head segment to a context of `throw`. + * It makes new unreachable segment, then it set the head with the segment. + * @returns {void} + */ + makeThrow(): void; + /** + * Makes the final path. + * @returns {void} + */ + makeFinal(): void; +} +declare namespace CodePathState { + export { LoopContext }; +} +import ForkContext = require("./fork-context"); +/** + * Represents a choice in the code path. + * + * Choices are created by logical operators such as `&&`, loops, conditionals, + * and `if` statements. This is the point at which the code path has a choice of + * which direction to go. + * + * The result of a choice might be in the left (test) expression of another choice, + * and in that case, may create a new fork. For example, `a || b` is a choice + * but does not create a new fork because the result of the expression is + * not used as the test expression in another expression. In this case, + * `isForkingAsResult` is false. In the expression `a || b || c`, the `a || b` + * expression appears as the test expression for `|| c`, so the + * result of `a || b` creates a fork because execution may or may not + * continue to `|| c`. `isForkingAsResult` for `a || b` in this case is true + * while `isForkingAsResult` for `|| c` is false. (`isForkingAsResult` is always + * false for `if` statements, conditional expressions, and loops.) + * + * All of the choices except one (`??`) operate on a true/false fork, meaning if + * true go one way and if false go the other (tracked by `trueForkContext` and + * `falseForkContext`). The `??` operator doesn't operate on true/false because + * the left expression is evaluated to be nullish or not, so only if nullish do + * we fork to the right expression (tracked by `nullishForkContext`). + */ +declare class ChoiceContext { + /** + * Creates a new instance. + * @param {ChoiceContext} upperContext The previous `ChoiceContext`. + * @param {string} kind The kind of choice. If it's a logical or assignment expression, this + * is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or + * conditional expression, this is `"test"`; otherwise, this is `"loop"`. + * @param {boolean} isForkingAsResult Indicates if the result of the choice + * creates a fork. + * @param {ForkContext} forkContext The containing `ForkContext`. + */ + constructor(upperContext: ChoiceContext, kind: string, isForkingAsResult: boolean, forkContext: ForkContext); + /** + * The previous `ChoiceContext` + * @type {ChoiceContext} + */ + upper: ChoiceContext; + /** + * The kind of choice. If it's a logical or assignment expression, this + * is `"&&"` or `"||"` or `"??"`; if it's an `if` statement or + * conditional expression, this is `"test"`; otherwise, this is `"loop"`. + * @type {string} + */ + kind: string; + /** + * Indicates if the result of the choice forks the code path. + * @type {boolean} + */ + isForkingAsResult: boolean; + /** + * The fork context for the `true` path of the choice. + * @type {ForkContext} + */ + trueForkContext: ForkContext; + /** + * The fork context for the `false` path of the choice. + * @type {ForkContext} + */ + falseForkContext: ForkContext; + /** + * The fork context for when the choice result is `null` or `undefined`. + * @type {ForkContext} + */ + nullishForkContext: ForkContext; + /** + * Indicates if any of `trueForkContext`, `falseForkContext`, or + * `nullishForkContext` have been updated with segments from a child context. + * @type {boolean} + */ + processed: boolean; +} +/** + * Represents the context for any loop. + * @typedef {WhileLoopContext|DoWhileLoopContext|ForLoopContext|ForInLoopContext|ForOfLoopContext} LoopContext + */ +/** + * Represents the context for a `switch` statement. + */ +declare class SwitchContext { + /** + * Creates a new instance. + * @param {SwitchContext} upperContext The previous context. + * @param {boolean} hasCase Indicates if there is at least one `case` statement. + * `default` doesn't count. + */ + constructor(upperContext: SwitchContext, hasCase: boolean); + /** + * The previous context. + * @type {SwitchContext} + */ + upper: SwitchContext; + /** + * Indicates if there is at least one `case` statement. `default` doesn't count. + * @type {boolean} + */ + hasCase: boolean; + /** + * The `default` keyword. + * @type {Array|null} + */ + defaultSegments: Array | null; + /** + * The default case body starting segments. + * @type {Array|null} + */ + defaultBodySegments: Array | null; + /** + * Indicates if a `default` case and is empty exists. + * @type {boolean} + */ + foundEmptyDefault: boolean; + /** + * Indicates that a `default` exists and is the last case. + * @type {boolean} + */ + lastIsDefault: boolean; + /** + * The number of fork contexts created. This is equivalent to the + * number of `case` statements plus a `default` statement (if present). + * @type {number} + */ + forkCount: number; +} +/** + * Represents the context for a `try` statement. + */ +declare class TryContext { + /** + * Creates a new instance. + * @param {TryContext} upperContext The previous context. + * @param {boolean} hasFinalizer Indicates if the `try` statement has a + * `finally` block. + * @param {ForkContext} forkContext The enclosing fork context. + */ + constructor(upperContext: TryContext, hasFinalizer: boolean, forkContext: ForkContext); + /** + * The previous context. + * @type {TryContext} + */ + upper: TryContext; + /** + * Indicates if the `try` statement has a `finally` block. + * @type {boolean} + */ + hasFinalizer: boolean; + /** + * Tracks the traversal position inside of the `try` statement. This is + * used to help determine the context necessary to create paths because + * a `try` statement may or may not have `catch` or `finally` blocks, + * and code paths behave differently in those blocks. + * @type {"try"|"catch"|"finally"} + */ + position: "try" | "catch" | "finally"; + /** + * If the `try` statement has a `finally` block, this affects how a + * `return` statement behaves in the `try` block. Without `finally`, + * `return` behaves as usual and doesn't require a fork; with `finally`, + * `return` forks into the `finally` block, so we need a fork context + * to track it. + * @type {ForkContext|null} + */ + returnedForkContext: ForkContext | null; + /** + * When a `throw` occurs inside of a `try` block, the code path forks + * into the `catch` or `finally` blocks, and this fork context tracks + * that path. + * @type {ForkContext} + */ + thrownForkContext: ForkContext; + /** + * Indicates if the last segment in the `try` block is reachable. + * @type {boolean} + */ + lastOfTryIsReachable: boolean; + /** + * Indicates if the last segment in the `catch` block is reachable. + * @type {boolean} + */ + lastOfCatchIsReachable: boolean; +} +/** + * Represents the context for any loop. + */ +type LoopContext = WhileLoopContext | DoWhileLoopContext | ForLoopContext | ForInLoopContext | ForOfLoopContext; +/** + * Represents the context in which a `break` statement can be used. + * + * A `break` statement without a label is only valid in a few places in + * JavaScript: any type of loop or a `switch` statement. Otherwise, `break` + * without a label causes a syntax error. For these contexts, `breakable` is + * set to `true` to indicate that a `break` without a label is valid. + * + * However, a `break` statement with a label is also valid inside of a labeled + * statement. For example, this is valid: + * + * a : { + * break a; + * } + * + * The `breakable` property is set false for labeled statements to indicate + * that `break` without a label is invalid. + */ +declare class BreakContext { + /** + * Creates a new instance. + * @param {BreakContext} upperContext The previous `BreakContext`. + * @param {boolean} breakable Indicates if we are inside a statement where + * `break` without a label will exit the statement. + * @param {string|null} label The label for the statement. + * @param {ForkContext} forkContext The current fork context. + */ + constructor(upperContext: BreakContext, breakable: boolean, label: string | null, forkContext: ForkContext); + /** + * The previous `BreakContext` + * @type {BreakContext} + */ + upper: BreakContext; + /** + * Indicates if we are inside a statement where `break` without a label + * will exit the statement. + * @type {boolean} + */ + breakable: boolean; + /** + * The label associated with the statement. + * @type {string|null} + */ + label: string | null; + /** + * The fork context for the `break`. + * @type {ForkContext} + */ + brokenForkContext: ForkContext; +} +/** + * Represents the context for `ChainExpression` nodes. + */ +declare class ChainContext { + /** + * Creates a new instance. + * @param {ChainContext} upperContext The previous `ChainContext`. + */ + constructor(upperContext: ChainContext); + /** + * The previous `ChainContext` + * @type {ChainContext} + */ + upper: ChainContext; + /** + * The number of choice contexts inside of the `ChainContext`. + * @type {number} + */ + choiceContextCount: number; +} +import CodePathSegment = require("./code-path-segment"); +/** + * Represents the context for a `while` loop. + */ +declare class WhileLoopContext extends LoopContextBase { + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext: LoopContext | null, label: string | null, breakContext: BreakContext); + /** + * The segments representing the test condition where `continue` will + * jump to. The test condition will typically have just one segment but + * it's possible for there to be more than one. + * @type {Array|null} + */ + continueDestSegments: Array | null; +} +/** + * Represents the context for a `do-while` loop. + */ +declare class DoWhileLoopContext extends LoopContextBase { + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + * @param {ForkContext} forkContext The enclosing fork context. + */ + constructor(upperContext: LoopContext | null, label: string | null, breakContext: BreakContext, forkContext: ForkContext); + /** + * The segments at the start of the loop body. This is the only loop + * where the test comes at the end, so the first iteration always + * happens and we need a reference to the first statements. + * @type {Array|null} + */ + entrySegments: Array | null; + /** + * The fork context to follow when a `continue` is found. + * @type {ForkContext} + */ + continueForkContext: ForkContext; +} +/** + * Represents the context for a `for` loop. + */ +declare class ForLoopContext extends LoopContextBase { + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext: LoopContext | null, label: string | null, breakContext: BreakContext); + /** + * The end of the init expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * an init expression. + * @type {Array|null} + */ + endOfInitSegments: Array | null; + /** + * The start of the test expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * a test expression. + * @type {Array|null} + */ + testSegments: Array | null; + /** + * The end of the test expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * a test expression. + * @type {Array|null} + */ + endOfTestSegments: Array | null; + /** + * The start of the update expression. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * an update expression. + * @type {Array|null} + */ + updateSegments: Array | null; + /** + * The end of the update expresion. This may change during the lifetime + * of the instance as we traverse the loop because some loops don't have + * an update expression. + * @type {Array|null} + */ + endOfUpdateSegments: Array | null; + /** + * The segments representing the test condition where `continue` will + * jump to. The test condition will typically have just one segment but + * it's possible for there to be more than one. This may change during the + * lifetime of the instance as we traverse the loop because some loops + * don't have an update expression. When there is an update expression, this + * will end up pointing to that expression; otherwise it will end up pointing + * to the test expression. + * @type {Array|null} + */ + continueDestSegments: Array | null; +} +/** + * Represents the context for a `for-in` loop. + * + * Terminology: + * - "left" means the part of the loop to the left of the `in` keyword. For + * example, in `for (var x in y)`, the left is `var x`. + * - "right" means the part of the loop to the right of the `in` keyword. For + * example, in `for (var x in y)`, the right is `y`. + */ +declare class ForInLoopContext extends LoopContextBase { + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext: LoopContext | null, label: string | null, breakContext: BreakContext); + /** + * The segments that came immediately before the start of the loop. + * This allows you to traverse backwards out of the loop into the + * surrounding code. This is necessary to evaluate the right expression + * correctly, as it must be evaluated in the same way as the left + * expression, but the pointer to these segments would otherwise be + * lost if not stored on the instance. Once the right expression has + * been evaluated, this property is no longer used. + * @type {Array|null} + */ + prevSegments: Array | null; + /** + * Segments representing the start of everything to the left of the + * `in` keyword. This can be used to move forward towards + * `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are + * effectively the head and tail of a doubly-linked list. + * @type {Array|null} + */ + leftSegments: Array | null; + /** + * Segments representing the end of everything to the left of the + * `in` keyword. This can be used to move backward towards `leftSegments`. + * `leftSegments` and `endOfLeftSegments` are effectively the head + * and tail of a doubly-linked list. + * @type {Array|null} + */ + endOfLeftSegments: Array | null; + /** + * The segments representing the left expression where `continue` will + * jump to. In `for-in` loops, `continue` must always re-execute the + * left expression each time through the loop. This contains the same + * segments as `leftSegments`, but is duplicated here so each loop + * context has the same property pointing to where `continue` should + * end up. + * @type {Array|null} + */ + continueDestSegments: Array | null; +} +/** + * Represents the context for a `for-of` loop. + */ +declare class ForOfLoopContext extends LoopContextBase { + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext: LoopContext | null, label: string | null, breakContext: BreakContext); + /** + * The segments that came immediately before the start of the loop. + * This allows you to traverse backwards out of the loop into the + * surrounding code. This is necessary to evaluate the right expression + * correctly, as it must be evaluated in the same way as the left + * expression, but the pointer to these segments would otherwise be + * lost if not stored on the instance. Once the right expression has + * been evaluated, this property is no longer used. + * @type {Array|null} + */ + prevSegments: Array | null; + /** + * Segments representing the start of everything to the left of the + * `of` keyword. This can be used to move forward towards + * `endOfLeftSegments`. `leftSegments` and `endOfLeftSegments` are + * effectively the head and tail of a doubly-linked list. + * @type {Array|null} + */ + leftSegments: Array | null; + /** + * Segments representing the end of everything to the left of the + * `of` keyword. This can be used to move backward towards `leftSegments`. + * `leftSegments` and `endOfLeftSegments` are effectively the head + * and tail of a doubly-linked list. + * @type {Array|null} + */ + endOfLeftSegments: Array | null; + /** + * The segments representing the left expression where `continue` will + * jump to. In `for-in` loops, `continue` must always re-execute the + * left expression each time through the loop. This contains the same + * segments as `leftSegments`, but is duplicated here so each loop + * context has the same property pointing to where `continue` should + * end up. + * @type {Array|null} + */ + continueDestSegments: Array | null; +} +/** + * Base class for all loop contexts. + */ +declare class LoopContextBase { + /** + * Creates a new instance. + * @param {LoopContext|null} upperContext The previous `LoopContext`. + * @param {string} type The AST node's `type` for the loop. + * @param {string|null} label The label for the loop from an enclosing `LabeledStatement`. + * @param {BreakContext} breakContext The context for breaking the loop. + */ + constructor(upperContext: LoopContext | null, type: string, label: string | null, breakContext: BreakContext); + /** + * The previous `LoopContext`. + * @type {LoopContext} + */ + upper: LoopContext; + /** + * The AST node's `type` for the loop. + * @type {string} + */ + type: string; + /** + * The label for the loop from an enclosing `LabeledStatement`. + * @type {string|null} + */ + label: string | null; + /** + * The fork context for when `break` is encountered. + * @type {ForkContext} + */ + brokenForkContext: ForkContext; +} diff --git a/lib/types-code-path-analysis/code-path.d.ts b/lib/types-code-path-analysis/code-path.d.ts new file mode 100644 index 00000000..55e773cf --- /dev/null +++ b/lib/types-code-path-analysis/code-path.d.ts @@ -0,0 +1,111 @@ +import CodePathSegment = require("./code-path-segment"); + +export = CodePath; +/** + * A code path. + */ +declare class CodePath { + /** + * Gets the state of a given code path. + * @param {CodePath} codePath A code path to get. + * @returns {CodePathState} The state of the code path. + */ + static getState(codePath: CodePath): CodePathState; + /** + * Creates a new instance. + * @param {Object} options Options for the function (see below). + * @param {string} options.id An identifier. + * @param {string} options.origin The type of code path origin. + * @param {CodePath|null} options.upper The code path of the upper function scope. + * @param {Function} options.onLooped A callback function to notify looping. + */ + constructor({ id, origin, upper, onLooped }: { + id: string; + origin: string; + upper: CodePath | null; + onLooped: Function; + }); + /** + * The identifier of this code path. + * Rules use it to store additional information of each rule. + * @type {string} + */ + id: string; + /** + * The reason that this code path was started. May be "program", + * "function", "class-field-initializer", or "class-static-block". + * @type {string} + */ + origin: string; + /** + * The code path of the upper function scope. + * @type {CodePath|null} + */ + upper: CodePath | null; + /** + * The code paths of nested function scopes. + * @type {CodePath[]} + */ + childCodePaths: CodePath[]; + /** + * The initial code path segment. This is the segment that is at the head + * of the code path. + * This is a passthrough to the underlying `CodePathState`. + * @type {CodePathSegment} + */ + get initialSegment(): CodePathSegment; + /** + * Final code path segments. These are the terminal (tail) segments in the + * code path, which is the combination of `returnedSegments` and `thrownSegments`. + * All segments in this array are reachable. + * This is a passthrough to the underlying `CodePathState`. + * @type {CodePathSegment[]} + */ + get finalSegments(): CodePathSegment[]; + /** + * Final code path segments that represent normal completion of the code path. + * For functions, this means both explicit `return` statements and implicit returns, + * such as the last reachable segment in a function that does not have an + * explicit `return` as this implicitly returns `undefined`. For scripts, + * modules, class field initializers, and class static blocks, this means + * all lines of code have been executed. + * These segments are also present in `finalSegments`. + * This is a passthrough to the underlying `CodePathState`. + * @type {CodePathSegment[]} + */ + get returnedSegments(): CodePathSegment[]; + /** + * Final code path segments that represent `throw` statements. + * This is a passthrough to the underlying `CodePathState`. + * These segments are also present in `finalSegments`. + * @type {CodePathSegment[]} + */ + get thrownSegments(): CodePathSegment[]; + /** + * Traverses all segments in this code path. + * + * codePath.traverseSegments((segment, controller) => { + * // do something. + * }); + * + * This method enumerates segments in order from the head. + * + * The `controller` argument has two methods: + * + * - `skip()` - skips the following segments in this branch + * - `break()` - skips all following segments in the traversal + * + * A note on the parameters: the `options` argument is optional. This means + * the first argument might be an options object or the callback function. + * @param {Object} [optionsOrCallback] Optional first and last segments to traverse. + * @param {CodePathSegment} [optionsOrCallback.first] The first segment to traverse. + * @param {CodePathSegment} [optionsOrCallback.last] The last segment to traverse. + * @param {Function} callback A callback function. + * @returns {void} + */ + traverseSegments(optionsOrCallback: { + first?: CodePathSegment; + last?: CodePathSegment; + } | Function, callback?: Function): void; +} +import CodePathState = require("./code-path-state"); diff --git a/lib/types-code-path-analysis/debug-helpers.d.ts b/lib/types-code-path-analysis/debug-helpers.d.ts new file mode 100644 index 00000000..05609b06 --- /dev/null +++ b/lib/types-code-path-analysis/debug-helpers.d.ts @@ -0,0 +1,15 @@ +import CodePath = require("./code-path"); + +declare const debug: any; +export declare let enabled: boolean; +export { debug as dump }; +export declare let dumpState: any; +export declare let dumpDot: any; +/** + * Makes a DOT code of a given code path. + * The DOT code can be visualized with Graphvis. + * @param {CodePath} codePath A code path to make DOT. + * @param {Object} traceMap Optional. A map to check whether or not segments had been done. + * @returns {string} A DOT code of the code path. + */ +export declare function makeDotArrows(codePath: CodePath, traceMap: any): string; diff --git a/lib/types-code-path-analysis/fork-context.d.ts b/lib/types-code-path-analysis/fork-context.d.ts new file mode 100644 index 00000000..b8901f70 --- /dev/null +++ b/lib/types-code-path-analysis/fork-context.d.ts @@ -0,0 +1,136 @@ +import IdGenerator = require("./id-generator"); + +export = ForkContext; +/** + * Manages the forking of code paths. + */ +declare class ForkContext { + /** + * Creates a new root context, meaning that there are no parent + * fork contexts. + * @param {IdGenerator} idGenerator An identifier generator for segments. + * @returns {ForkContext} New fork context. + */ + static newRoot(idGenerator: IdGenerator): ForkContext; + /** + * Creates an empty fork context preceded by a given context. + * @param {ForkContext} parentContext The parent fork context. + * @param {boolean} shouldForkLeavingPath Indicates that we are inside of + * a `finally` block and should therefore fork the path that leaves + * `finally`. + * @returns {ForkContext} New fork context. + */ + static newEmpty(parentContext: ForkContext, shouldForkLeavingPath: boolean): ForkContext; + /** + * Creates a new instance. + * @param {IdGenerator} idGenerator An identifier generator for segments. + * @param {ForkContext|null} upper The preceding fork context. + * @param {number} count The number of parallel segments in each element + * of `segmentsList`. + */ + constructor(idGenerator: IdGenerator, upper: ForkContext | null, count: number); + /** + * The ID generator that will generate segment IDs for any new + * segments that are created. + * @type {IdGenerator} + */ + idGenerator: IdGenerator; + /** + * The preceding fork context. + * @type {ForkContext|null} + */ + upper: ForkContext | null; + /** + * The number of elements in each element of `segmentsList`. In most + * cases, this is 1 but can be 2 when there is a `finally` present, + * which forks the code path outside of normal flow. In the case of nested + * `finally` blocks, this can be a multiple of 2. + * @type {number} + */ + count: number; + /** + * The segments within this context. Each element in this array has + * `count` elements that represent one step in each fork. For example, + * when `segmentsList` is `[[a, b], [c, d], [e, f]]`, there is one path + * a->c->e and one path b->d->f, and `count` is 2 because each element + * is an array with two elements. + * @type {Array>} + */ + segmentsList: Array>; + /** + * The segments that begin this fork context. + * @type {Array} + */ + get head(): CodePathSegment[]; + /** + * Indicates if the context contains no segments. + * @type {boolean} + */ + get empty(): boolean; + /** + * Indicates if there are any segments that are reachable. + * @type {boolean} + */ + get reachable(): boolean; + /** + * Creates new segments in this context and appends them to the end of the + * already existing `CodePathSegment`s specified by `startIndex` and + * `endIndex`. + * @param {number} startIndex The index of the first segment in the context + * that should be specified as previous segments for the newly created segments. + * @param {number} endIndex The index of the last segment in the context + * that should be specified as previous segments for the newly created segments. + * @returns {Array} An array of the newly created segments. + */ + makeNext(startIndex: number, endIndex: number): Array; + /** + * Creates new unreachable segments in this context and appends them to the end of the + * already existing `CodePathSegment`s specified by `startIndex` and + * `endIndex`. + * @param {number} startIndex The index of the first segment in the context + * that should be specified as previous segments for the newly created segments. + * @param {number} endIndex The index of the last segment in the context + * that should be specified as previous segments for the newly created segments. + * @returns {Array} An array of the newly created segments. + */ + makeUnreachable(startIndex: number, endIndex: number): Array; + /** + * Creates new segments in this context and does not append them to the end + * of the already existing `CodePathSegment`s specified by `startIndex` and + * `endIndex`. The `startIndex` and `endIndex` are only used to determine if + * the new segments should be reachable. If any of the segments in this range + * are reachable then the new segments are also reachable; otherwise, the new + * segments are unreachable. + * @param {number} startIndex The index of the first segment in the context + * that should be considered for reachability. + * @param {number} endIndex The index of the last segment in the context + * that should be considered for reachability. + * @returns {Array} An array of the newly created segments. + */ + makeDisconnected(startIndex: number, endIndex: number): Array; + /** + * Adds segments to the head of this context. + * @param {Array} segments The segments to add. + * @returns {void} + */ + add(segments: Array): void; + /** + * Replaces the head segments with the given segments. + * The current head segments are removed. + * @param {Array} replacementHeadSegments The new head segments. + * @returns {void} + */ + replaceHead(replacementHeadSegments: Array): void; + /** + * Adds all segments of a given fork context into this context. + * @param {ForkContext} otherForkContext The fork context to add from. + * @returns {void} + */ + addAll(otherForkContext: ForkContext): void; + /** + * Clears all segments in this context. + * @returns {void} + */ + clear(): void; +} +import CodePathSegment = require("./code-path-segment"); diff --git a/lib/types-code-path-analysis/id-generator.d.ts b/lib/types-code-path-analysis/id-generator.d.ts new file mode 100644 index 00000000..d6fe3160 --- /dev/null +++ b/lib/types-code-path-analysis/id-generator.d.ts @@ -0,0 +1,17 @@ +export = IdGenerator; +/** + * A generator for unique ids. + */ +declare class IdGenerator { + /** + * @param {string} prefix Optional. A prefix of generated ids. + */ + constructor(prefix: string); + prefix: string; + n: number; + /** + * Generates id. + * @returns {string} A generated id. + */ + next(): string; +} diff --git a/lib/unsupported-features/node-builtins-modules/assert.js b/lib/unsupported-features/node-builtins-modules/assert.js index d2b0a2ef..c62e0e5a 100644 --- a/lib/unsupported-features/node-builtins-modules/assert.js +++ b/lib/unsupported-features/node-builtins-modules/assert.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const assert = { assert: { [READ]: { supported: ["0.5.9"] } }, deepEqual: { [READ]: { supported: ["0.1.21"] } }, @@ -41,26 +41,26 @@ const assert = { } assert.strict = { - [READ]: { supported: ["9.9.0", "8.13.0"] }, ...assert, + [READ]: { supported: ["9.9.0", "8.13.0"] }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { assert: { - [READ]: { supported: ["0.1.21"] }, ...assert, + [READ]: { supported: ["0.1.21"] }, }, "node:assert": { - [READ]: { supported: ["14.13.1", "12.20.0"] }, ...assert, + [READ]: { supported: ["14.13.1", "12.20.0"] }, }, "assert/strict": { - [READ]: { supported: ["15.0.0"] }, ...assert.strict, + [READ]: { supported: ["15.0.0"] }, }, "node:assert/strict": { - [READ]: { supported: ["15.0.0"] }, ...assert.strict, + [READ]: { supported: ["15.0.0"] }, }, } diff --git a/lib/unsupported-features/node-builtins-modules/async_hooks.js b/lib/unsupported-features/node-builtins-modules/async_hooks.js index db72b395..9591d623 100644 --- a/lib/unsupported-features/node-builtins-modules/async_hooks.js +++ b/lib/unsupported-features/node-builtins-modules/async_hooks.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const async_hooks = { createHook: { [READ]: { experimental: ["8.1.0"] } }, executionAsyncResource: { [READ]: { experimental: ["13.9.0", "12.17.0"] } }, @@ -25,7 +25,7 @@ const async_hooks = { }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { async_hooks: { [READ]: { diff --git a/lib/unsupported-features/node-builtins-modules/buffer.js b/lib/unsupported-features/node-builtins-modules/buffer.js index 896843e5..dddd12ba 100644 --- a/lib/unsupported-features/node-builtins-modules/buffer.js +++ b/lib/unsupported-features/node-builtins-modules/buffer.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const buffer = { constants: { [READ]: { supported: ["8.2.0"] } }, INSPECT_MAX_BYTES: { [READ]: { supported: ["0.5.4"] } }, @@ -42,7 +42,7 @@ const buffer = { }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { buffer: { [READ]: { supported: ["0.1.90"] }, diff --git a/lib/unsupported-features/node-builtins-modules/child_process.js b/lib/unsupported-features/node-builtins-modules/child_process.js index c224be9f..8e6d1d21 100644 --- a/lib/unsupported-features/node-builtins-modules/child_process.js +++ b/lib/unsupported-features/node-builtins-modules/child_process.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const child_process = { exec: { [READ]: { supported: ["0.1.90"] } }, execFile: { [READ]: { supported: ["0.1.91"] } }, @@ -14,7 +14,7 @@ const child_process = { ChildProcess: { [READ]: { supported: ["2.2.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { child_process: { [READ]: { supported: ["0.1.90"] }, diff --git a/lib/unsupported-features/node-builtins-modules/cluster.js b/lib/unsupported-features/node-builtins-modules/cluster.js index e46e8a9e..69b9c9cf 100644 --- a/lib/unsupported-features/node-builtins-modules/cluster.js +++ b/lib/unsupported-features/node-builtins-modules/cluster.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const cluster = { isMaster: { [READ]: { supported: ["0.8.1"], deprecated: ["16.0.0"] } }, isPrimary: { [READ]: { supported: ["16.0.0"] } }, @@ -18,7 +18,7 @@ const cluster = { Worker: { [READ]: { supported: ["0.7.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { cluster: { [READ]: { supported: ["0.7.0"] }, diff --git a/lib/unsupported-features/node-builtins-modules/console.js b/lib/unsupported-features/node-builtins-modules/console.js index fb5c32e1..89a4adf7 100644 --- a/lib/unsupported-features/node-builtins-modules/console.js +++ b/lib/unsupported-features/node-builtins-modules/console.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const console = { profile: { [READ]: { supported: ["8.0.0"] } }, profileEnd: { [READ]: { supported: ["8.0.0"] } }, @@ -34,7 +34,7 @@ const console = { // timelineEnd: { [READ]: { supported: ["8.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { console: { [READ]: { supported: ["0.1.100"] }, diff --git a/lib/unsupported-features/node-builtins-modules/crypto.js b/lib/unsupported-features/node-builtins-modules/crypto.js index b9cf5f7e..a7ebf460 100644 --- a/lib/unsupported-features/node-builtins-modules/crypto.js +++ b/lib/unsupported-features/node-builtins-modules/crypto.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const WebCrypto = { [READ]: { experimental: ["15.0.0"], supported: ["19.0.0"] }, subtle: { @@ -24,7 +24,7 @@ const WebCrypto = { randomUUID: { [READ]: { supported: ["16.7.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const crypto = { constants: { [READ]: { supported: ["6.3.0"] } }, fips: { [READ]: { supported: ["6.0.0"], deprecated: ["10.0.0"] } }, @@ -116,7 +116,7 @@ const crypto = { X509Certificate: { [READ]: { supported: ["15.6.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { crypto: { [READ]: { supported: ["0.1.92"] }, diff --git a/lib/unsupported-features/node-builtins-modules/dgram.js b/lib/unsupported-features/node-builtins-modules/dgram.js index 9b6dfa0a..af10dbac 100644 --- a/lib/unsupported-features/node-builtins-modules/dgram.js +++ b/lib/unsupported-features/node-builtins-modules/dgram.js @@ -2,13 +2,13 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const dgram = { createSocket: { [READ]: { supported: ["0.1.99"] } }, Socket: { [READ]: { supported: ["0.1.99"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { dgram: { [READ]: { supported: ["0.1.99"] }, diff --git a/lib/unsupported-features/node-builtins-modules/diagnostics_channel.js b/lib/unsupported-features/node-builtins-modules/diagnostics_channel.js index 3016a16a..8fa84989 100644 --- a/lib/unsupported-features/node-builtins-modules/diagnostics_channel.js +++ b/lib/unsupported-features/node-builtins-modules/diagnostics_channel.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const diagnostics_channel = { hasSubscribers: { [READ]: { supported: ["15.1.0", "14.17.0"] } }, channel: { [READ]: { supported: ["15.1.0", "14.17.0"] } }, @@ -13,7 +13,7 @@ const diagnostics_channel = { TracingChannel: { [READ]: { experimental: ["19.9.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { diagnostics_channel: { [READ]: { diff --git a/lib/unsupported-features/node-builtins-modules/dns.js b/lib/unsupported-features/node-builtins-modules/dns.js index fb040147..13cd0d51 100644 --- a/lib/unsupported-features/node-builtins-modules/dns.js +++ b/lib/unsupported-features/node-builtins-modules/dns.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const dns = { Resolver: { [READ]: { supported: ["8.3.0"] } }, getServers: { [READ]: { supported: ["0.11.3"] } }, @@ -56,7 +56,7 @@ const dns = { }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { dns: { ...dns, [READ]: { supported: ["0.1.16"] } }, "node:dns": { ...dns, [READ]: { supported: ["14.13.1", "12.20.0"] } }, diff --git a/lib/unsupported-features/node-builtins-modules/domain.js b/lib/unsupported-features/node-builtins-modules/domain.js index 288a7878..166947f1 100644 --- a/lib/unsupported-features/node-builtins-modules/domain.js +++ b/lib/unsupported-features/node-builtins-modules/domain.js @@ -2,13 +2,13 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const domain = { create: { [READ]: { supported: ["0.7.8"] } }, Domain: { [READ]: { supported: ["0.7.8"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { domain: { [READ]: { diff --git a/lib/unsupported-features/node-builtins-modules/events.js b/lib/unsupported-features/node-builtins-modules/events.js index 126a338a..c2c6e44a 100644 --- a/lib/unsupported-features/node-builtins-modules/events.js +++ b/lib/unsupported-features/node-builtins-modules/events.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const EventEmitterStatic = { defaultMaxListeners: { [READ]: { supported: ["0.11.2"] } }, errorMonitor: { [READ]: { supported: ["13.6.0", "12.17.0"] } }, @@ -27,7 +27,7 @@ const EventEmitterStatic = { addAbortListener: { [READ]: { experimental: ["20.5.0", "18.18.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const events = { Event: { [READ]: { experimental: ["14.5.0"], supported: ["15.4.0"] } }, EventTarget: { @@ -54,7 +54,7 @@ const events = { ...EventEmitterStatic, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { events: { [READ]: { supported: ["0.1.26"] }, diff --git a/lib/unsupported-features/node-builtins-modules/fs.js b/lib/unsupported-features/node-builtins-modules/fs.js index 7845b892..a60ca457 100644 --- a/lib/unsupported-features/node-builtins-modules/fs.js +++ b/lib/unsupported-features/node-builtins-modules/fs.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const promises_api = { constants: { [READ]: { supported: ["18.4.0", "16.17.0"] } }, access: { [READ]: { supported: ["10.0.0"] } }, @@ -38,7 +38,7 @@ const promises_api = { FileHandle: { [READ]: { supported: ["10.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const callback_api = { access: { [READ]: { supported: ["0.11.15"] } }, appendFile: { [READ]: { supported: ["0.6.7"] } }, @@ -94,7 +94,7 @@ const callback_api = { writev: { [READ]: { supported: ["12.9.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const synchronous_api = { accessSync: { [READ]: { supported: ["0.11.15"] } }, appendFileSync: { [READ]: { supported: ["0.6.7"] } }, @@ -144,7 +144,7 @@ const synchronous_api = { writevSync: { [READ]: { supported: ["12.9.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const fs = { promises: { [READ]: { @@ -167,7 +167,7 @@ const fs = { common_objects: { [READ]: { supported: ["0.1.8"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { fs: { [READ]: { supported: ["0.1.8"] }, diff --git a/lib/unsupported-features/node-builtins-modules/http.js b/lib/unsupported-features/node-builtins-modules/http.js index 0c430449..dd422e5f 100644 --- a/lib/unsupported-features/node-builtins-modules/http.js +++ b/lib/unsupported-features/node-builtins-modules/http.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const http = { METHODS: { [READ]: { supported: ["0.11.8"] } }, STATUS_CODES: { [READ]: { supported: ["0.1.22"] } }, @@ -22,7 +22,7 @@ const http = { OutgoingMessage: { [READ]: { supported: ["0.1.17"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { http: { [READ]: { supported: ["0.0.1"] }, diff --git a/lib/unsupported-features/node-builtins-modules/http2.js b/lib/unsupported-features/node-builtins-modules/http2.js index 4d599200..9dbac8c1 100644 --- a/lib/unsupported-features/node-builtins-modules/http2.js +++ b/lib/unsupported-features/node-builtins-modules/http2.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const http2 = { constants: { [READ]: { supported: ["8.4.0"] } }, sensitiveHeaders: { [READ]: { supported: ["15.0.0", "14.18.0"] } }, @@ -24,7 +24,7 @@ const http2 = { Http2ServerResponse: { [READ]: { supported: ["8.4.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { http2: { [READ]: { diff --git a/lib/unsupported-features/node-builtins-modules/https.js b/lib/unsupported-features/node-builtins-modules/https.js index 55fb6e48..05783ff0 100644 --- a/lib/unsupported-features/node-builtins-modules/https.js +++ b/lib/unsupported-features/node-builtins-modules/https.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const http = { globalAgent: { [READ]: { supported: ["0.5.9"] } }, createServer: { [READ]: { supported: ["0.3.4"] } }, @@ -12,7 +12,7 @@ const http = { Server: { [READ]: { supported: ["0.3.4"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { http: { [READ]: { supported: ["0.3.4"] }, diff --git a/lib/unsupported-features/node-builtins-modules/inspector.js b/lib/unsupported-features/node-builtins-modules/inspector.js index 0eaed414..408122fa 100644 --- a/lib/unsupported-features/node-builtins-modules/inspector.js +++ b/lib/unsupported-features/node-builtins-modules/inspector.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const common_objects = { console: { [READ]: { supported: ["8.0.0"] } }, close: { [READ]: { supported: ["9.0.0"] } }, @@ -11,19 +11,19 @@ const common_objects = { waitForDebugger: { [READ]: { supported: ["12.7.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const promises_api = { Session: { [READ]: { supported: ["19.0.0"] } }, ...common_objects, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const callback_api = { Session: { [READ]: { supported: ["8.0.0"] } }, ...common_objects, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { inspector: { [READ]: { diff --git a/lib/unsupported-features/node-builtins-modules/module.js b/lib/unsupported-features/node-builtins-modules/module.js index 00ff4192..359206f2 100644 --- a/lib/unsupported-features/node-builtins-modules/module.js +++ b/lib/unsupported-features/node-builtins-modules/module.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const Module = { builtinModules: { [READ]: { supported: ["9.3.0", "8.10.0", "6.13.0"] } }, createRequire: { [READ]: { supported: ["12.2.0"] } }, @@ -21,7 +21,7 @@ const Module = { Module.Module = Module -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { module: { [READ]: { supported: ["0.3.7"] }, diff --git a/lib/unsupported-features/node-builtins-modules/net.js b/lib/unsupported-features/node-builtins-modules/net.js index 6d80dddc..be0413fa 100644 --- a/lib/unsupported-features/node-builtins-modules/net.js +++ b/lib/unsupported-features/node-builtins-modules/net.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const net = { connect: { [READ]: { supported: ["0.0.1"] } }, createConnection: { [READ]: { supported: ["0.0.1"] } }, @@ -24,7 +24,7 @@ const net = { Socket: { [READ]: { supported: ["0.3.4"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { net: { [READ]: { supported: ["0.0.1"] }, diff --git a/lib/unsupported-features/node-builtins-modules/os.js b/lib/unsupported-features/node-builtins-modules/os.js index 778bad13..6489d9e6 100644 --- a/lib/unsupported-features/node-builtins-modules/os.js +++ b/lib/unsupported-features/node-builtins-modules/os.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const os = { EOL: { [READ]: { supported: ["0.7.8"] } }, constants: { @@ -32,7 +32,7 @@ const os = { version: { [READ]: { supported: ["13.11.0", "12.17.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { os: { [READ]: { supported: ["0.3.3"] }, diff --git a/lib/unsupported-features/node-builtins-modules/path.js b/lib/unsupported-features/node-builtins-modules/path.js index 2df72e6a..e0456255 100644 --- a/lib/unsupported-features/node-builtins-modules/path.js +++ b/lib/unsupported-features/node-builtins-modules/path.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const path = { delimiter: { [READ]: { supported: ["0.9.3"] } }, sep: { [READ]: { supported: ["0.7.9"] } }, @@ -19,7 +19,7 @@ const path = { toNamespacedPath: { [READ]: { supported: ["9.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { path: { [READ]: { supported: ["0.1.16"] }, diff --git a/lib/unsupported-features/node-builtins-modules/perf_hooks.js b/lib/unsupported-features/node-builtins-modules/perf_hooks.js index 298b6b59..e83d3ebe 100644 --- a/lib/unsupported-features/node-builtins-modules/perf_hooks.js +++ b/lib/unsupported-features/node-builtins-modules/perf_hooks.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const perf_hooks = { performance: { [READ]: { supported: ["8.5.0"] } }, createHistogram: { [READ]: { supported: ["15.9.0", "14.18.0"] } }, @@ -20,7 +20,7 @@ const perf_hooks = { RecordableHistogram: { [READ]: { supported: ["15.9.0", "14.18.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { perf_hooks: { [READ]: { supported: ["8.5.0"] }, diff --git a/lib/unsupported-features/node-builtins-modules/process.js b/lib/unsupported-features/node-builtins-modules/process.js index aaacd0fd..c81a0774 100644 --- a/lib/unsupported-features/node-builtins-modules/process.js +++ b/lib/unsupported-features/node-builtins-modules/process.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const process = { allowedNodeEnvironmentFlags: { [READ]: { supported: ["10.10.0"] } }, arch: { [READ]: { supported: ["0.5.0"] } }, @@ -117,7 +117,7 @@ const process = { uptime: { [READ]: { supported: ["0.5.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { process: { [READ]: { supported: ["0.1.3"] }, diff --git a/lib/unsupported-features/node-builtins-modules/punycode.js b/lib/unsupported-features/node-builtins-modules/punycode.js index 248ee4f3..05475129 100644 --- a/lib/unsupported-features/node-builtins-modules/punycode.js +++ b/lib/unsupported-features/node-builtins-modules/punycode.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const punycode = { ucs2: { [READ]: { supported: ["0.7.0"] } }, version: { [READ]: { supported: ["0.6.1"] } }, @@ -12,7 +12,7 @@ const punycode = { toUnicode: { [READ]: { supported: ["0.6.1"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { punycode: { [READ]: { diff --git a/lib/unsupported-features/node-builtins-modules/querystring.js b/lib/unsupported-features/node-builtins-modules/querystring.js index 5e2c0c76..0cc34017 100644 --- a/lib/unsupported-features/node-builtins-modules/querystring.js +++ b/lib/unsupported-features/node-builtins-modules/querystring.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const querystring = { decode: { [READ]: { supported: ["0.1.99"] } }, encode: { [READ]: { supported: ["0.1.99"] } }, @@ -12,7 +12,7 @@ const querystring = { unescape: { [READ]: { supported: ["0.1.25"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { querystring: { [READ]: { supported: ["0.1.25"] }, diff --git a/lib/unsupported-features/node-builtins-modules/readline.js b/lib/unsupported-features/node-builtins-modules/readline.js index f8817796..ace56ec2 100644 --- a/lib/unsupported-features/node-builtins-modules/readline.js +++ b/lib/unsupported-features/node-builtins-modules/readline.js @@ -2,14 +2,14 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const promises_api = { createInterface: { [READ]: { supported: ["17.0.0"] } }, Interface: { [READ]: { supported: ["17.0.0"] } }, Readline: { [READ]: { supported: ["17.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const readline = { promises: { [READ]: { experimental: ["17.0.0"] }, @@ -25,7 +25,7 @@ const readline = { InterfaceConstructor: { [READ]: { supported: ["0.1.104"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { readline: { [READ]: { supported: ["0.1.98"] }, diff --git a/lib/unsupported-features/node-builtins-modules/stream.js b/lib/unsupported-features/node-builtins-modules/stream.js index c7b799e4..c8209ed0 100644 --- a/lib/unsupported-features/node-builtins-modules/stream.js +++ b/lib/unsupported-features/node-builtins-modules/stream.js @@ -4,7 +4,7 @@ const { READ } = require("@eslint-community/eslint-utils") // TODO: https://nodejs.org/docs/latest/api/webstreams.html -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const Readable = { [READ]: { supported: ["0.9.4"] }, from: { [READ]: { supported: ["12.3.0", "10.17.0"] } }, @@ -13,14 +13,14 @@ const Readable = { toWeb: { [READ]: { experimental: ["17.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const Writable = { [READ]: { supported: ["0.9.4"] }, fromWeb: { [READ]: { experimental: ["17.0.0"] } }, toWeb: { [READ]: { experimental: ["17.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const Duplex = { [READ]: { supported: ["0.9.4"] }, from: { [READ]: { experimental: ["16.8.0"] } }, @@ -30,13 +30,13 @@ const Duplex = { const Transform = Duplex -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const StreamPromise = { pipeline: { [READ]: { supported: ["15.0.0"] } }, finished: { [READ]: { supported: ["15.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const Stream = { promises: { [READ]: { supported: ["15.0.0"] }, @@ -58,7 +58,7 @@ const Stream = { setDefaultHighWaterMark: { [READ]: { supported: ["19.9.0", "18.17.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const StreamWeb = { ReadableStream: { [READ]: { supported: ["16.5.0"] }, @@ -91,7 +91,7 @@ const StreamConsumer = { text: { [READ]: { supported: ["16.7.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { stream: { [READ]: { supported: ["0.9.4"] }, @@ -120,12 +120,6 @@ module.exports = { ...StreamWeb, }, - "stream/consumers": { - [READ]: { supported: ["16.7.0"] }, - ...StreamConsumer, - }, - "node:stream/consumers": { - [READ]: { supported: ["16.7.0"] }, - ...StreamConsumer, - }, + "stream/consumers": { ...StreamConsumer }, + "node:stream/consumers": { ...StreamConsumer }, } diff --git a/lib/unsupported-features/node-builtins-modules/string_decoder.js b/lib/unsupported-features/node-builtins-modules/string_decoder.js index f6842aa9..efa15b2e 100644 --- a/lib/unsupported-features/node-builtins-modules/string_decoder.js +++ b/lib/unsupported-features/node-builtins-modules/string_decoder.js @@ -2,12 +2,12 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const string_decoder = { StringDecoder: { [READ]: { supported: ["0.1.99"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { string_decoder: { [READ]: { supported: ["0.1.99"] }, diff --git a/lib/unsupported-features/node-builtins-modules/test.js b/lib/unsupported-features/node-builtins-modules/test.js index aa4613ed..a21f7379 100644 --- a/lib/unsupported-features/node-builtins-modules/test.js +++ b/lib/unsupported-features/node-builtins-modules/test.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const test = { run: { [READ]: { supported: ["18.9.0", "16.19.0"] } }, skip: { [READ]: { supported: ["20.2.0", "18.17.0"] } }, @@ -34,7 +34,7 @@ const test = { test.test = test -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { "node:test": { [READ]: { diff --git a/lib/unsupported-features/node-builtins-modules/timers.js b/lib/unsupported-features/node-builtins-modules/timers.js index 192f6c3a..0ae31b97 100644 --- a/lib/unsupported-features/node-builtins-modules/timers.js +++ b/lib/unsupported-features/node-builtins-modules/timers.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const promises_api = { setTimeout: { [READ]: { supported: ["15.0.0"] } }, setImmediate: { [READ]: { supported: ["15.0.0"] } }, @@ -13,7 +13,7 @@ const promises_api = { }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const timers = { Immediate: { [READ]: { supported: ["0.9.1"] } }, Timeout: { [READ]: { supported: ["0.9.1"] } }, @@ -31,7 +31,7 @@ const timers = { // enroll: [Function: deprecated] } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { timers: { [READ]: { supported: ["0.9.1"] }, diff --git a/lib/unsupported-features/node-builtins-modules/tls.js b/lib/unsupported-features/node-builtins-modules/tls.js index 6301d2c4..159aa5d9 100644 --- a/lib/unsupported-features/node-builtins-modules/tls.js +++ b/lib/unsupported-features/node-builtins-modules/tls.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const tls = { rootCertificates: { [READ]: { supported: ["12.3.0"] } }, DEFAULT_ECDH_CURVE: { [READ]: { supported: ["0.11.13"] } }, @@ -24,7 +24,7 @@ const tls = { TLSSocket: { [READ]: { supported: ["0.11.4"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { tls: { [READ]: { supported: ["0.3.2"] }, diff --git a/lib/unsupported-features/node-builtins-modules/trace_events.js b/lib/unsupported-features/node-builtins-modules/trace_events.js index 2704da56..677d5f7b 100644 --- a/lib/unsupported-features/node-builtins-modules/trace_events.js +++ b/lib/unsupported-features/node-builtins-modules/trace_events.js @@ -2,13 +2,13 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const trace_events = { createTracing: { [READ]: { supported: ["10.0.0"] } }, getEnabledCategories: { [READ]: { supported: ["10.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { trace_events: { [READ]: { experimental: ["10.0.0"] }, diff --git a/lib/unsupported-features/node-builtins-modules/tty.js b/lib/unsupported-features/node-builtins-modules/tty.js index ace7ccac..3cf7a271 100644 --- a/lib/unsupported-features/node-builtins-modules/tty.js +++ b/lib/unsupported-features/node-builtins-modules/tty.js @@ -2,14 +2,14 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const tty = { isatty: { [READ]: { supported: ["0.5.8"] } }, ReadStream: { [READ]: { supported: ["0.5.8"] } }, WriteStream: { [READ]: { supported: ["0.5.8"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { tty: { [READ]: { supported: ["0.5.8"] }, diff --git a/lib/unsupported-features/node-builtins-modules/url.js b/lib/unsupported-features/node-builtins-modules/url.js index e346a92f..20d67a3f 100644 --- a/lib/unsupported-features/node-builtins-modules/url.js +++ b/lib/unsupported-features/node-builtins-modules/url.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const url = { domainToASCII: { [READ]: { supported: ["7.4.0", "6.13.0"] } }, domainToUnicode: { [READ]: { supported: ["7.4.0", "6.13.0"] } }, @@ -20,7 +20,7 @@ const url = { Url: { [READ]: { supported: ["0.1.25"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { url: { [READ]: { supported: ["0.1.25"] }, diff --git a/lib/unsupported-features/node-builtins-modules/util.js b/lib/unsupported-features/node-builtins-modules/util.js index 53cceeef..a4db99be 100644 --- a/lib/unsupported-features/node-builtins-modules/util.js +++ b/lib/unsupported-features/node-builtins-modules/util.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const types = { [READ]: { supported: ["10.0.0"] }, isExternal: { [READ]: { supported: ["10.0.0"] } }, @@ -52,7 +52,7 @@ const types = { }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const deprecated = { _extend: { [READ]: { supported: ["0.7.5"], deprecated: ["6.0.0"] } }, isArray: { [READ]: { supported: ["0.6.0"], deprecated: ["4.0.0"] } }, @@ -75,7 +75,7 @@ const deprecated = { log: { [READ]: { supported: ["0.3.0"], deprecated: ["6.0.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const util = { promisify: { [READ]: { supported: ["8.0.0"] }, @@ -115,7 +115,7 @@ const util = { ...deprecated, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { util: util, "node:util": { ...util, [READ]: { supported: ["14.13.1", "12.20.0"] } }, diff --git a/lib/unsupported-features/node-builtins-modules/v8.js b/lib/unsupported-features/node-builtins-modules/v8.js index d88f783b..75193ca3 100644 --- a/lib/unsupported-features/node-builtins-modules/v8.js +++ b/lib/unsupported-features/node-builtins-modules/v8.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const v8 = { serialize: { [READ]: { supported: ["8.0.0"] } }, deserialize: { [READ]: { supported: ["8.0.0"] } }, @@ -44,7 +44,7 @@ const v8 = { GCProfiler: { [READ]: { supported: ["19.6.0", "18.15.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { v8: { ...v8, [READ]: { supported: ["1.0.0"] } }, "node:v8": { ...v8, [READ]: { supported: ["14.13.1", "12.20.0"] } }, diff --git a/lib/unsupported-features/node-builtins-modules/vm.js b/lib/unsupported-features/node-builtins-modules/vm.js index 3cbd063d..b1f7772d 100644 --- a/lib/unsupported-features/node-builtins-modules/vm.js +++ b/lib/unsupported-features/node-builtins-modules/vm.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const vm = { compileFunction: { [READ]: { supported: ["10.10.0"] } }, createContext: { [READ]: { supported: ["0.3.1"] } }, @@ -18,7 +18,7 @@ const vm = { SyntheticModule: { [READ]: { experimental: ["13.0.0", "12.16.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { vm: vm, "node:vm": { ...vm, [READ]: { supported: ["14.13.1", "12.20.0"] } }, diff --git a/lib/unsupported-features/node-builtins-modules/wasi.js b/lib/unsupported-features/node-builtins-modules/wasi.js index 01f5f8ec..d9e02563 100644 --- a/lib/unsupported-features/node-builtins-modules/wasi.js +++ b/lib/unsupported-features/node-builtins-modules/wasi.js @@ -2,12 +2,12 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const wasi = { WASI: { [READ]: { supported: ["13.3.0", "12.16.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { wasi: wasi, "node:wasi": { ...wasi, [READ]: { supported: ["14.13.1", "12.20.0"] } }, diff --git a/lib/unsupported-features/node-builtins-modules/worker_threads.js b/lib/unsupported-features/node-builtins-modules/worker_threads.js index d86ab049..5129ff78 100644 --- a/lib/unsupported-features/node-builtins-modules/worker_threads.js +++ b/lib/unsupported-features/node-builtins-modules/worker_threads.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const worker_threads = { isMainThread: { [READ]: { supported: ["10.5.0"] } }, parentPort: { [READ]: { supported: ["10.5.0"] } }, @@ -34,7 +34,7 @@ const worker_threads = { Worker: { [READ]: { supported: ["10.5.0"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { worker_threads: { ...worker_threads, diff --git a/lib/unsupported-features/node-builtins-modules/zlib.js b/lib/unsupported-features/node-builtins-modules/zlib.js index 82ab9cc0..3da814f8 100644 --- a/lib/unsupported-features/node-builtins-modules/zlib.js +++ b/lib/unsupported-features/node-builtins-modules/zlib.js @@ -2,7 +2,7 @@ const { READ } = require("@eslint-community/eslint-utils") -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ const zlib = { constants: { [READ]: { supported: ["7.0.0"] } }, createBrotliCompress: { [READ]: { supported: ["11.7.0", "10.16.0"] } }, @@ -43,7 +43,7 @@ const zlib = { Unzip: { [READ]: { supported: ["0.5.8"] } }, } -/** @type {import('../types.js').SupportVersionTree} */ +/** @type {import('../types.js').SupportVersionTraceMap} */ module.exports = { zlib: zlib, "node:zlib": { diff --git a/lib/unsupported-features/node-builtins.js b/lib/unsupported-features/node-builtins.js index 93ed53d6..a416c4ee 100644 --- a/lib/unsupported-features/node-builtins.js +++ b/lib/unsupported-features/node-builtins.js @@ -1,6 +1,6 @@ "use strict" -/** @type {import('./types.js').SupportVersionTree} */ +/** @type {import('./types.js').SupportVersionTraceMap} */ const NodeBuiltinModules = { ...require("./node-builtins-modules/assert.js"), ...require("./node-builtins-modules/async_hooks.js"), diff --git a/lib/unsupported-features/types.js b/lib/unsupported-features/types.js index 48b63748..faae70ce 100644 --- a/lib/unsupported-features/types.js +++ b/lib/unsupported-features/types.js @@ -1,16 +1,33 @@ "use strict" /** - * @typedef {Object} SupportInfo - * @property {string[]} experimental The node versions in which experimental support was added - * @property {string[]} supported The node versions in which stable support was added - * @property {string[]} deprecated The node versions in which support was removed + * @typedef {( + * | import("@eslint-community/eslint-utils").READ + * | import("@eslint-community/eslint-utils").CALL + * | import("@eslint-community/eslint-utils").CONSTRUCT + * )} UTIL_SYMBOL */ + +/** + * @typedef SupportInfo + * @property {string[]} [experimental] The node versions in which experimental support was added + * @property {string[]} [supported] The node versions in which stable support was added + * @property {string[]} [deprecated] The node versions in which support was removed + */ + +/** + * @typedef DeprecatedInfo + * @property {string} since + * @property {string|{ name: string, supported: string }[]|null} replacedBy + */ + +/** @typedef {import('@eslint-community/eslint-utils').TraceMap} DeprecatedInfoTraceMap */ +/** @typedef {import('@eslint-community/eslint-utils').TraceMap} SupportVersionTraceMap */ + /** - * @typedef {{ - * [key: readonly unique symbol]: SupportInfo | undefined; - * [key: string]: SupportVersionTree | undefined; - * }} SupportVersionTree + * @typedef SupportVersionBuiltins + * @property {SupportVersionTraceMap} globals + * @property {SupportVersionTraceMap} modules */ module.exports = {} diff --git a/lib/util/check-existence.js b/lib/util/check-existence.js index a29e1278..a7899a75 100644 --- a/lib/util/check-existence.js +++ b/lib/util/check-existence.js @@ -8,20 +8,22 @@ const path = require("path") const exists = require("./exists") const getAllowModules = require("./get-allow-modules") const isTypescript = require("./is-typescript") -const mapTypescriptExtension = require("../util/map-typescript-extension") +const { convertJsExtensionToTs } = require("../util/map-typescript-extension") /** * Reports a missing file from ImportTarget - * @param {RuleContext} context - A context to report. + * @param {import('eslint').Rule.RuleContext} context - A context to report. * @param {import('../util/import-target.js')} target - A list of target information to check. * @returns {void} */ function markMissing(context, target) { context.report({ node: target.node, - loc: target.node.loc, + loc: /** @type {import('eslint').AST.SourceLocation} */ ( + target.node.loc + ), messageId: "notFound", - data: target, + data: /** @type {Record} */ (target), }) } @@ -31,14 +33,14 @@ function markMissing(context, target) { * It looks up the target according to the logic of Node.js. * See Also: https://nodejs.org/api/modules.html * - * @param {RuleContext} context - A context to report. + * @param {import('eslint').Rule.RuleContext} context - A context to report. * @param {import('../util/import-target.js')[]} targets - A list of target information to check. * @returns {void} */ exports.checkExistence = function checkExistence(context, targets) { const allowed = new Set(getAllowModules(context)) - for (const target of targets) { + target: for (const target of targets) { if ( target.moduleName != null && !allowed.has(target.moduleName) && @@ -48,35 +50,37 @@ exports.checkExistence = function checkExistence(context, targets) { continue } - if (target.moduleName != null) { + if ( + target.moduleName != null || + target.filePath == null || + exists(target.filePath) + ) { continue } - let missingFile = - target.filePath == null ? false : !exists(target.filePath) + if (isTypescript(context) === false) { + markMissing(context, target) + continue + } - if (missingFile && isTypescript(context)) { - const parsed = path.parse(target.filePath) - const pathWithoutExt = path.resolve(parsed.dir, parsed.name) + const parsed = path.parse(target.filePath) + const pathWithoutExt = path.resolve(parsed.dir, parsed.name) - const reversedExts = mapTypescriptExtension( - context, - target.filePath, - parsed.ext, - true - ) - const reversedPaths = reversedExts.map( - reversedExt => pathWithoutExt + reversedExt - ) - missingFile = reversedPaths.every( - reversedPath => - target.moduleName == null && !exists(reversedPath) - ) - } + const reversedExtensions = convertJsExtensionToTs( + context, + target.filePath, + parsed.ext + ) - if (missingFile) { - markMissing(context, target) + for (const reversedExtension of reversedExtensions) { + const reversedPath = pathWithoutExt + reversedExtension + + if (exists(reversedPath)) { + continue target + } } + + markMissing(context, target) } } diff --git a/lib/util/check-extraneous.js b/lib/util/check-extraneous.js index 3736694b..a41f11f1 100644 --- a/lib/util/check-extraneous.js +++ b/lib/util/check-extraneous.js @@ -5,16 +5,16 @@ "use strict" const getAllowModules = require("./get-allow-modules") -const getPackageJson = require("./get-package-json") +const { getPackageJson } = require("./get-package-json") /** * Checks whether or not each requirement target is published via package.json. * * It reads package.json and checks the target exists in `dependencies`. * - * @param {RuleContext} context - A context to report. + * @param {import('eslint').Rule.RuleContext} context - A context to report. * @param {string} filePath - The current file path. - * @param {ImportTarget[]} targets - A list of target information to check. + * @param {import('./import-target.js')[]} targets - A list of target information to check. * @returns {void} */ exports.checkExtraneous = function checkExtraneous(context, filePath, targets) { @@ -43,9 +43,11 @@ exports.checkExtraneous = function checkExtraneous(context, filePath, targets) { if (extraneous) { context.report({ node: target.node, - loc: target.node.loc, + loc: /** @type {import('eslint').AST.SourceLocation} */ ( + target.node.loc + ), messageId: "extraneous", - data: target, + data: /** @type {Record} */ (target), }) } } diff --git a/lib/util/check-prefer-global.js b/lib/util/check-prefer-global.js index 96aa58a0..ca685de8 100644 --- a/lib/util/check-prefer-global.js +++ b/lib/util/check-prefer-global.js @@ -5,7 +5,12 @@ "use strict" const { ReferenceTracker } = require("@eslint-community/eslint-utils") -const extendTrackmapWithNodePrefix = require("./extend-trackmap-with-node-prefix") + +/** + * @typedef TraceMap + * @property {import('@eslint-community/eslint-utils').TraceMap} globals + * @property {import('@eslint-community/eslint-utils').TraceMap} modules + */ /** * Verifier for `prefer-global/*` rules. @@ -13,12 +18,12 @@ const extendTrackmapWithNodePrefix = require("./extend-trackmap-with-node-prefix class Verifier { /** * Initialize this instance. - * @param {RuleContext} context The rule context to report. - * @param {{modules:object,globals:object}} trackMap The track map. + * @param {import('eslint').Rule.RuleContext} context The rule context to report. + * @param {TraceMap} traceMap The track map. */ - constructor(context, trackMap) { + constructor(context, traceMap) { this.context = context - this.trackMap = trackMap + this.traceMap = traceMap this.verify = context.options[0] === "never" ? this.verifyToPreferModules @@ -30,7 +35,7 @@ class Verifier { * @returns {void} */ verifyToPreferGlobals() { - const { context, trackMap } = this + const { context, traceMap } = this const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 const scope = sourceCode.getScope?.(sourceCode.ast) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 @@ -38,11 +43,9 @@ class Verifier { mode: "legacy", }) - const modules = extendTrackmapWithNodePrefix(trackMap.modules) - for (const { node } of [ - ...tracker.iterateCjsReferences(modules), - ...tracker.iterateEsmReferences(modules), + ...tracker.iterateCjsReferences(traceMap.modules), + ...tracker.iterateEsmReferences(traceMap.modules), ]) { context.report({ node, messageId: "preferGlobal" }) } @@ -53,20 +56,25 @@ class Verifier { * @returns {void} */ verifyToPreferModules() { - const { context, trackMap } = this + const { context, traceMap } = this const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 const scope = sourceCode.getScope?.(sourceCode.ast) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 const tracker = new ReferenceTracker(scope) for (const { node } of tracker.iterateGlobalReferences( - trackMap.globals + traceMap.globals )) { context.report({ node, messageId: "preferModule" }) } } } -module.exports = function checkForPreferGlobal(context, trackMap) { - new Verifier(context, trackMap).verify() +/** + * @param {import('eslint').Rule.RuleContext} context + * @param {TraceMap} traceMap + * @returns {void} + */ +module.exports = function checkForPreferGlobal(context, traceMap) { + new Verifier(context, traceMap).verify() } diff --git a/lib/util/check-publish.js b/lib/util/check-publish.js index 7b4535a0..2a10b9d7 100644 --- a/lib/util/check-publish.js +++ b/lib/util/check-publish.js @@ -8,49 +8,48 @@ const path = require("path") const getAllowModules = require("./get-allow-modules") const getConvertPath = require("./get-convert-path") const getNpmignore = require("./get-npmignore") -const getPackageJson = require("./get-package-json") +const { getPackageJson } = require("./get-package-json") /** * Checks whether or not each requirement target is published via package.json. * * It reads package.json and checks the target exists in `dependencies`. * - * @param {RuleContext} context - A context to report. + * @param {import('eslint').Rule.RuleContext} context - A context to report. * @param {string} filePath - The current file path. - * @param {ImportTarget[]} targets - A list of target information to check. + * @param {import('./import-target.js')[]} targets - A list of target information to check. * @returns {void} */ exports.checkPublish = function checkPublish(context, filePath, targets) { - const packageInfo = getPackageJson(filePath) - if (!packageInfo) { + const packageJson = getPackageJson(filePath) + if (typeof packageJson?.filePath !== "string") { return } // Private packages are never published so we don't need to check the imported dependencies either. // More information: https://docs.npmjs.com/cli/v8/configuring-npm/package-json#private - if (packageInfo.private === true) { + if (packageJson.private === true) { return } const allowed = new Set(getAllowModules(context)) const convertPath = getConvertPath(context) - const basedir = path.dirname(packageInfo.filePath) + const basedir = path.dirname(packageJson.filePath) + /** @type {(fullPath: string) => string} */ const toRelative = fullPath => { const retv = path.relative(basedir, fullPath).replace(/\\/gu, "/") return convertPath(retv) } const npmignore = getNpmignore(filePath) const devDependencies = new Set( - Object.keys(packageInfo.devDependencies || {}) - ) - const dependencies = new Set( - [].concat( - Object.keys(packageInfo.dependencies || {}), - Object.keys(packageInfo.peerDependencies || {}), - Object.keys(packageInfo.optionalDependencies || {}) - ) + Object.keys(packageJson.devDependencies ?? {}) ) + const dependencies = new Set([ + ...Object.keys(packageJson?.dependencies ?? {}), + ...Object.keys(packageJson?.peerDependencies ?? {}), + ...Object.keys(packageJson?.optionalDependencies ?? {}), + ]) if (!npmignore.match(toRelative(filePath))) { // This file is published, so this cannot import private files. @@ -74,7 +73,9 @@ exports.checkPublish = function checkPublish(context, filePath, targets) { if (isPrivateFile() || isDevPackage()) { context.report({ node: target.node, - loc: target.node.loc, + loc: /** @type {import('estree').SourceLocation} */ ( + target.node.loc + ), messageId: "notPublished", data: { name: target.moduleName || target.name }, }) diff --git a/lib/util/check-restricted.js b/lib/util/check-restricted.js index 6373ffe1..d825aa68 100644 --- a/lib/util/check-restricted.js +++ b/lib/util/check-restricted.js @@ -9,16 +9,16 @@ const { Minimatch } = require("minimatch") /** @typedef {import("../util/import-target")} ImportTarget */ /** - * @typedef {Object} DefinitionData + * @typedef DefinitionData * @property {string | string[]} name The name to disallow. * @property {string} [message] The custom message to show. */ /** * Check if matched or not. - * @param {InstanceType} matcher The matcher. + * @param {Minimatch} matcher The matcher. * @param {boolean} absolute The flag that the matcher is for absolute paths. - * @param {ImportTarget} importee The importee information. + * @param {import('./import-target.js')} importee The importee information. */ function match(matcher, absolute, { filePath, name }) { if (absolute) { @@ -54,7 +54,7 @@ class Restriction { /** * Check if a given importee is disallowed. - * @param {ImportTarget} importee The importee to check. + * @param {import('./import-target.js')} importee The importee to check. * @returns {boolean} `true` if the importee is disallowed. */ match(importee) { @@ -82,8 +82,8 @@ function createRestriction(def) { /** * Create restrictions. - * @param {(string | DefinitionData | GlobDefinition)[]} defs Definitions. - * @returns {(Restriction | GlobRestriction)[]} Created restrictions. + * @param {(string | DefinitionData)[]} defs Definitions. + * @returns {(Restriction)[]} Created restrictions. */ function createRestrictions(defs) { return (defs || []).map(createRestriction) @@ -91,8 +91,8 @@ function createRestrictions(defs) { /** * Checks if given importees are disallowed or not. - * @param {RuleContext} context - A context to report. - * @param {ImportTarget[]} targets - A list of target information to check. + * @param {import('eslint').Rule.RuleContext} context - A context to report. + * @param {import('./import-target.js')[]} targets - A list of target information to check. * @returns {void} */ exports.checkForRestriction = function checkForRestriction(context, targets) { diff --git a/lib/util/check-unsupported-builtins.js b/lib/util/check-unsupported-builtins.js index 5ef32d31..db2ae46f 100644 --- a/lib/util/check-unsupported-builtins.js +++ b/lib/util/check-unsupported-builtins.js @@ -10,16 +10,13 @@ const getConfiguredNodeVersion = require("./get-configured-node-version") const getSemverRange = require("./get-semver-range") const unprefixNodeColon = require("./unprefix-node-colon") -/** - * @typedef {Object} SupportInfo - * @property {string[]} supported The stably supported version. If `null` is present, it hasn't been supported yet. - * @property {string} [experimental] The added version as experimental. - */ - /** * Parses the options. - * @param {RuleContext} context The rule context. - * @returns {{version:Range,ignores:Set}} Parsed value. + * @param {import('eslint').Rule.RuleContext} context The rule context. + * @returns {Readonly<{ + * version: import('semver').Range, + * ignores: Set + * }>} Parsed value. */ function parseOptions(context) { const raw = context.options[0] || {} @@ -31,8 +28,8 @@ function parseOptions(context) { /** * Check if it has been supported. - * @param {SupportInfo} info The support info. - * @param {Range} configured The configured version range. + * @param {import('../unsupported-features/types.js').SupportInfo} info The support info. + * @param {import('semver').Range} configured The configured version range. */ function isSupported({ supported }, configured) { if (supported == null || supported.length === 0) { @@ -49,17 +46,21 @@ function isSupported({ supported }, configured) { ].join("||") ) + if (range == null) { + return false + } + return configured.intersects(range) } /** * Get the formatted text of a given supported version. - * @param {SupportInfo} info The support info. - * @returns {null|string} + * @param {import('../unsupported-features/types.js').SupportInfo} info The support info. + * @returns {string | undefined} */ function supportedVersionToString({ supported }) { if (supported == null || supported.length === 0) { - return null + return } const [latest, ...backported] = rsort(supported) @@ -75,22 +76,22 @@ function supportedVersionToString({ supported }) { /** * Verify the code to report unsupported APIs. - * @param {RuleContext} context The rule context. - * @param {{modules:object,globals:object}} trackMap The map for APIs to report. + * @param {import('eslint').Rule.RuleContext} context The rule context. + * @param {import('../unsupported-features/types.js').SupportVersionBuiltins} traceMap The map for APIs to report. * @returns {void} */ module.exports.checkUnsupportedBuiltins = function checkUnsupportedBuiltins( context, - trackMap + traceMap ) { const options = parseOptions(context) const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 const scope = sourceCode.getScope?.(sourceCode.ast) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 const tracker = new ReferenceTracker(scope, { mode: "legacy" }) const references = [ - ...tracker.iterateCjsReferences(trackMap.modules || {}), - ...tracker.iterateEsmReferences(trackMap.modules || {}), - ...tracker.iterateGlobalReferences(trackMap.globals || {}), + ...tracker.iterateCjsReferences(traceMap.modules ?? {}), + ...tracker.iterateEsmReferences(traceMap.modules ?? {}), + ...tracker.iterateGlobalReferences(traceMap.globals ?? {}), ] for (const { node, path, info } of references) { @@ -101,26 +102,17 @@ module.exports.checkUnsupportedBuiltins = function checkUnsupportedBuiltins( continue } const supportedVersion = supportedVersionToString(info) - if (supportedVersion == null) { - context.report({ - node, - messageId: "not-supported-yet", - data: { - name: path.join("."), - version: options.version.raw, - }, - }) - } else { - context.report({ - node, - messageId: "not-supported-till", - data: { - name: path.join("."), - supported: supportedVersionToString(info), - version: options.version.raw, - }, - }) - } + context.report({ + node, + messageId: supportedVersion + ? "not-supported-till" + : "not-supported-yet", + data: { + name: path.join("."), + supported: /** @type string */ (supportedVersion), + version: options.version.raw, + }, + }) } } diff --git a/lib/util/enumerate-property-names.js b/lib/util/enumerate-property-names.js index fbf332c4..de802c46 100644 --- a/lib/util/enumerate-property-names.js +++ b/lib/util/enumerate-property-names.js @@ -7,27 +7,35 @@ const { CALL, CONSTRUCT, READ } = require("@eslint-community/eslint-utils") const unprefixNodeColon = require("./unprefix-node-colon") +/** @typedef {import('../unsupported-features/types.js').DeprecatedInfoTraceMap} DeprecatedInfoTraceMap */ +/** @typedef {import('../unsupported-features/types.js').SupportVersionTraceMap} SupportVersionTraceMap */ + /** + * @template {SupportVersionTraceMap | DeprecatedInfoTraceMap} TraceMap * Enumerate property names of a given object recursively. - * @param {import('../unsupported-features/types.js').SupportVersionTree} trackMap The map for APIs to enumerate. + * @param {TraceMap} traceMap The map for APIs to enumerate. * @param {string[]} [path] The path to the current map. - * @param {WeakSet} [recursionSet] A WeakSet used to block recursion (eg Module, Module.Module, Module.Module.Module) + * @param {WeakSet} [recursionSet] A WeakSet used to block recursion (eg Module, Module.Module, Module.Module.Module) * @returns {IterableIterator} The property names of the map. */ function* enumeratePropertyNames( - trackMap, + traceMap, path = [], recursionSet = new WeakSet() ) { - if (recursionSet.has(trackMap)) { + if (recursionSet.has(traceMap)) { return } - for (const key of Object.getOwnPropertyNames(trackMap)) { - const childValue = trackMap[key] + for (const key of Object.getOwnPropertyNames(traceMap)) { + const childValue = traceMap[key] const childPath = [...path, key] const childName = unprefixNodeColon(childPath.join(".")) + if (childValue == null) { + continue + } + if (childValue[CALL]) { yield `${childName}()` } @@ -43,7 +51,7 @@ function* enumeratePropertyNames( yield* enumeratePropertyNames( childValue, childPath, - recursionSet.add(trackMap) + recursionSet.add(traceMap) ) } } diff --git a/lib/util/exists.js b/lib/util/exists.js index b0324ae1..c120cd13 100644 --- a/lib/util/exists.js +++ b/lib/util/exists.js @@ -50,7 +50,10 @@ module.exports = function exists(filePath) { fs.statSync(relativePath).isFile() && existsCaseSensitive(relativePath) } catch (error) { - if (error.code !== "ENOENT") { + if ( + error instanceof Error && + ("code" in error === false || error.code !== "ENOENT") + ) { throw error } result = false diff --git a/lib/util/extend-trackmap-with-node-prefix.js b/lib/util/extend-trackmap-with-node-prefix.js index d103e9d1..db549eba 100644 --- a/lib/util/extend-trackmap-with-node-prefix.js +++ b/lib/util/extend-trackmap-with-node-prefix.js @@ -1,20 +1,22 @@ "use strict" -const isCoreModule = require("is-core-module") +const isBuiltinModule = require("is-builtin-module") /** - * Extend trackMap.modules with `node:` prefixed modules - * @param {Object} modules Like `{assert: foo}` - * @returns {Object} Like `{assert: foo}, "node:assert": foo}` + * Extend traceMap.modules with `node:` prefixed modules + * @template {import('@eslint-community/eslint-utils').TraceMap<*>} TraceMap + * @param {TraceMap} modules Like `{assert: foo}` + * @returns {TraceMap} Like `{assert: foo}, "node:assert": foo}` */ -module.exports = function extendTrackMapWithNodePrefix(modules) { +module.exports = function extendTraceMapWithNodePrefix(modules) { const ret = { ...modules, ...Object.fromEntries( Object.entries(modules) .map(([name, value]) => [`node:${name}`, value]) - // Note: "999" arbitrary to check current/future Node.js version - .filter(([name]) => isCoreModule(name, "999")) + .filter(([name]) => + isBuiltinModule(/** @type {string} */ (name)) + ) ), } return ret diff --git a/lib/util/get-allow-modules.js b/lib/util/get-allow-modules.js index 54efcef4..c022257a 100644 --- a/lib/util/get-allow-modules.js +++ b/lib/util/get-allow-modules.js @@ -4,16 +4,19 @@ */ "use strict" -const DEFAULT_VALUE = Object.freeze([]) +/** + * @type {string[]} + */ +const DEFAULT_VALUE = [] /** * Gets `allowModules` property from a given option object. * - * @param {object|undefined} option - An option object to get. + * @param {{allowModules:? string[]}|undefined} option - An option object to get. * @returns {string[]|null} The `allowModules` value, or `null`. */ function get(option) { - if (option && option.allowModules && Array.isArray(option.allowModules)) { + if (Array.isArray(option?.allowModules)) { return option.allowModules.map(String) } return null @@ -26,15 +29,14 @@ function get(option) { * 2. This checks `settings.n` | `settings.node` property, then returns it if exists. * 3. This returns `[]`. * - * @param {RuleContext} context - The rule context. + * @param {import('eslint').Rule.RuleContext} context - The rule context. * @returns {string[]} A list of extensions. */ module.exports = function getAllowModules(context) { return ( - get(context.options && context.options[0]) || - get( - context.settings && (context.settings.n || context.settings.node) - ) || + get(context.options[0]) ?? + get(context.settings?.n) ?? + get(context.settings?.node) ?? DEFAULT_VALUE ) } diff --git a/lib/util/get-configured-node-version.js b/lib/util/get-configured-node-version.js index b9e0750d..3a66b0af 100644 --- a/lib/util/get-configured-node-version.js +++ b/lib/util/get-configured-node-version.js @@ -4,31 +4,35 @@ */ "use strict" -const { Range } = require("semver") // eslint-disable-line no-unused-vars -const getPackageJson = require("./get-package-json") +const { getPackageJson } = require("./get-package-json") const getSemverRange = require("./get-semver-range") /** * Gets `version` property from a given option object. * - * @param {object|undefined} option - An option object to get. - * @returns {string[]|null} The `allowModules` value, or `null`. + * @param {Record|undefined} option - An option object to get. + * @returns {import("semver").Range|undefined} The `allowModules` value, or `null`. */ -function get(option) { - if (option && option.version) { - return option.version +function getVersionRange(option) { + if (option?.version) { + return getSemverRange(option.version) } - return null } - +/** + * @typedef {{ [EngineName in 'npm' | 'node' | string]?: string }} Engines + */ /** * Get the `engines.node` field of package.json. - * @param {string} filename The path to the current linting file. - * @returns {Range|null} The range object of the `engines.node` field. + * @param {import('eslint').Rule.RuleContext} context The path to the current linting file. + * @returns {import("semver").Range|undefined} The range object of the `engines.node` field. */ -function getEnginesNode(filename) { +function getEnginesNode(context) { + const filename = context.filename ?? context.getFilename() const info = getPackageJson(filename) - return getSemverRange(info && info.engines && info.engines.node) + const engines = /** @type {Engines | undefined} */ (info?.engines) + if (typeof engines?.node === "string") { + return getSemverRange(engines?.node) + } } /** @@ -38,21 +42,17 @@ function getEnginesNode(filename) { * 2. Look package.json up and parse `engines.node` then return it if it's valid. * 3. Return `>=16.0.0`. * - * @param {string|undefined} version The version range text. - * @param {string} filename The path to the current linting file. + * @param {import('eslint').Rule.RuleContext} context The version range text. * This will be used to look package.json up if `version` is not a valid version range. - * @returns {Range} The configured version range. + * @returns {import("semver").Range} The configured version range. */ module.exports = function getConfiguredNodeVersion(context) { - const version = - get(context.options && context.options[0]) || - get(context.settings && (context.settings.n || context.settings.node)) - const filePath = context.filename ?? context.getFilename() - return ( - getSemverRange(version) || - getEnginesNode(filePath) || - getSemverRange(">=16.0.0") + getVersionRange(context.options?.[0]) ?? + getVersionRange(context.settings?.n) ?? + getVersionRange(context.settings?.node) ?? + getEnginesNode(context) ?? + /** @type {import("semver").Range} */ (getSemverRange(">=16.0.0")) ) } diff --git a/lib/util/get-convert-path.js b/lib/util/get-convert-path.js index 6adf9ae4..7f5a813b 100644 --- a/lib/util/get-convert-path.js +++ b/lib/util/get-convert-path.js @@ -7,42 +7,52 @@ const { Minimatch } = require("minimatch") /** - * @param {any} x - An any value. - * @returns {any} Always `x`. + * @typedef PathConvertion + * @property {string[]} include + * @property {string[]} exclude + * @property {[string, string]} replace */ +/** @typedef {PathConvertion[] | Record} ConvertPath */ +/** @typedef {{match: (filePath: string) => boolean, convert: (filePath: string) => string}} Converter */ + +/** @type {Converter['convert']} */ function identity(x) { return x } /** - * Converts old-style value to new-style value. + * Ensures the given value is a string array. * - * @param {any} x - The value to convert. - * @returns {({include: string[], exclude: string[], replace: string[]})[]} Normalized value. + * @param {unknown[]} x - The value to ensure. + * @returns {string[]} The string array. */ -function normalizeValue(x) { +function toStringArray(x) { if (Array.isArray(x)) { - return x + return x.map(String) } - - return Object.keys(x).map(pattern => ({ - include: [pattern], - exclude: [], - replace: x[pattern], - })) + return [] } /** - * Ensures the given value is a string array. + * Converts old-style value to new-style value. * - * @param {any} x - The value to ensure. - * @returns {string[]} The string array. + * @param {ConvertPath} x - The value to convert. + * @returns {PathConvertion[]} Normalized value. */ -function toStringArray(x) { +function normalizeValue(x) { if (Array.isArray(x)) { - return x.map(String) + return x.map(({ include, exclude, replace }) => ({ + include: toStringArray(include), + exclude: toStringArray(exclude), + replace: replace, + })) } - return [] + + return Object.entries(x).map(([pattern, replace]) => ({ + include: [pattern], + exclude: [], + replace: replace, + })) } /** @@ -61,7 +71,7 @@ function makeMatcher(pattern) { * * @param {string[]} includePatterns - The glob patterns to include files. * @param {string[]} excludePatterns - The glob patterns to exclude files. - * @returns {function} Created predicate function. + * @returns {Converter['match']} Created predicate function. */ function createMatch(includePatterns, excludePatterns) { const include = includePatterns.map(makeMatcher) @@ -77,7 +87,7 @@ function createMatch(includePatterns, excludePatterns) { * * @param {RegExp} fromRegexp - A `RegExp` object to replace. * @param {string} toStr - A new string to replace. - * @returns {function} A function which replaces a given path. + * @returns {Converter['convert']} A function which replaces a given path. */ function defineConvert(fromRegexp, toStr) { return filePath => filePath.replace(fromRegexp, toStr) @@ -87,8 +97,8 @@ function defineConvert(fromRegexp, toStr) { * Combines given converters. * The result function converts a given path with the first matched converter. * - * @param {{match: function, convert: function}} converters - A list of converters to combine. - * @returns {function} A function which replaces a given path. + * @param {Converter[]} converters - A list of converters to combine. + * @returns {Converter['convert']} A function which replaces a given path. */ function combine(converters) { return filePath => { @@ -104,27 +114,21 @@ function combine(converters) { /** * Parses `convertPath` property from a given option object. * - * @param {object|undefined} option - An option object to get. - * @returns {function|null} A function which converts a path., or `null`. + * @param {{convertPath?: ConvertPath}|undefined} option - An option object to get. + * @returns {Converter['convert']|null} A function which converts a path., or `null`. */ function parse(option) { - if ( - !option || - !option.convertPath || - typeof option.convertPath !== "object" - ) { + if (option?.convertPath == null) { return null } const converters = [] for (const pattern of normalizeValue(option.convertPath)) { - const include = toStringArray(pattern.include) - const exclude = toStringArray(pattern.exclude) const fromRegexp = new RegExp(String(pattern.replace[0])) const toStr = String(pattern.replace[1]) converters.push({ - match: createMatch(include, exclude), + match: createMatch(pattern.include, pattern.exclude), convert: defineConvert(fromRegexp, toStr), }) } @@ -139,15 +143,14 @@ function parse(option) { * 2. This checks `settings.n` | `settings.node` property, then returns it if exists. * 3. This returns a function of identity. * - * @param {RuleContext} context - The rule context. - * @returns {function} A function which converts a path. + * @param {import('eslint').Rule.RuleContext} context - The rule context. + * @returns {Converter['convert']} A function which converts a path. */ module.exports = function getConvertPath(context) { return ( - parse(context.options && context.options[0]) || - parse( - context.settings && (context.settings.n || context.settings.node) - ) || + parse(context.options?.[0]) ?? + parse(context.settings?.n) ?? + parse(context.settings?.node) ?? identity ) } diff --git a/lib/util/get-npmignore.js b/lib/util/get-npmignore.js index 605316b7..e9165084 100644 --- a/lib/util/get-npmignore.js +++ b/lib/util/get-npmignore.js @@ -6,10 +6,10 @@ const fs = require("fs") const path = require("path") -const ignore = require("ignore") +const ignore = require("ignore").default const Cache = require("./cache") const exists = require("./exists") -const getPackageJson = require("./get-package-json") +const { getPackageJson } = require("./get-package-json") const cache = new Cache() const PARENT_RELATIVE_PATH = /^\.\./u @@ -29,27 +29,31 @@ function isAncestorFiles(filePath) { } /** - * @param {function} f - A function. - * @param {function} g - A function. - * @returns {function} A logical-and function of `f` and `g`. + * @param {(filePath: string) => boolean} f - A function. + * @param {(filePath: string) => boolean} g - A function. + * @returns {(filePath: string) => boolean} A logical-and function of `f` and `g`. */ function and(f, g) { return filePath => f(filePath) && g(filePath) } /** - * @param {function} f - A function. - * @param {function} g - A function. - * @param {function|null} h - A function. - * @returns {function} A logical-or function of `f`, `g`, and `h`. + * @param {(filePath: string) => boolean} f - A function. + * @param {(filePath: string) => boolean} g - A function. + * @param {(filePath: string) => boolean} [h] - A function. + * @returns {(filePath: string) => boolean} A logical-or function of `f`, `g`, and `h`. */ function or(f, g, h) { - return filePath => f(filePath) || g(filePath) || (h && h(filePath)) + if (h == null) { + return filePath => f(filePath) || g(filePath) + } + + return filePath => f(filePath) || g(filePath) || h(filePath) } /** - * @param {function} f - A function. - * @returns {function} A logical-not function of `f`. + * @param {(filePath: string) => boolean} f - A function. + * @returns {(filePath: string) => boolean} A logical-not function of `f`. */ function not(f) { return filePath => !f(filePath) @@ -58,13 +62,19 @@ function not(f) { /** * Creates a function which checks whether or not a given file is ignoreable. * - * @param {object} p - An object of package.json. - * @returns {function} A function which checks whether or not a given file is ignoreable. + * @param {import('type-fest').JsonObject} packageJson - An object of package.json. + * @returns {(filePath: string) => boolean} A function which checks whether or not a given file is ignoreable. */ -function filterNeverIgnoredFiles(p) { - const basedir = path.dirname(p.filePath) +function filterNeverIgnoredFiles(packageJson) { + if (typeof packageJson?.filePath !== "string") { + return () => false + } + + const basedir = path.dirname(packageJson.filePath) const mainFilePath = - typeof p.main === "string" ? path.join(basedir, p.main) : null + typeof packageJson.main === "string" + ? path.join(basedir, packageJson.main) + : null return filePath => path.join(basedir, filePath) !== mainFilePath && @@ -75,11 +85,11 @@ function filterNeverIgnoredFiles(p) { /** * Creates a function which checks whether or not a given file should be ignored. * - * @param {string[]|null} files - File names of whitelist. - * @returns {function|null} A function which checks whether or not a given file should be ignored. + * @param {unknown} files - File names of whitelist. + * @returns {((filePath: string) => boolean) | null} A function which checks whether or not a given file should be ignored. */ function parseWhiteList(files) { - if (!files || !Array.isArray(files)) { + if (Array.isArray(files) === false) { return null } @@ -114,7 +124,7 @@ function parseWhiteList(files) { * * @param {string} basedir - The directory path "package.json" exists. * @param {boolean} filesFieldExists - `true` if `files` field of `package.json` exists. - * @returns {function|null} A function which checks whether or not a given file should be ignored. + * @returns {((filePath: string) => boolean)|null} A function which checks whether or not a given file should be ignored. */ function parseNpmignore(basedir, filesFieldExists) { let filePath = path.join(basedir, ".npmignore") @@ -142,7 +152,7 @@ function parseNpmignore(basedir, filesFieldExists) { * - `.npmignore` * * @param {string} startPath - A file path to lookup. - * @returns {object} + * @returns {{ match: (filePath: string) => boolean }} * An object to check whther or not a given path should be ignored. * The object has a method `match`. * `match` returns `true` if a given file path should be ignored. @@ -150,38 +160,41 @@ function parseNpmignore(basedir, filesFieldExists) { module.exports = function getNpmignore(startPath) { const retv = { match: isAncestorFiles } - const p = getPackageJson(startPath) - if (p) { - const data = cache.get(p.filePath) - if (data) { - return data - } + const packageJson = getPackageJson(startPath) + if (typeof packageJson?.filePath !== "string") { + return retv + } - const filesIgnore = parseWhiteList(p.files) - const npmignoreIgnore = parseNpmignore( - path.dirname(p.filePath), - Boolean(filesIgnore) - ) + const data = cache.get(packageJson.filePath) + if (data) { + return data + } - if (filesIgnore && npmignoreIgnore) { - retv.match = and( - filterNeverIgnoredFiles(p), - or(isAncestorFiles, filesIgnore, npmignoreIgnore) - ) - } else if (filesIgnore) { - retv.match = and( - filterNeverIgnoredFiles(p), - or(isAncestorFiles, filesIgnore) - ) - } else if (npmignoreIgnore) { - retv.match = and( - filterNeverIgnoredFiles(p), - or(isAncestorFiles, npmignoreIgnore) - ) - } + const filesIgnore = parseWhiteList(packageJson.files) - cache.set(p.filePath, retv) + const npmignoreIgnore = parseNpmignore( + path.dirname(packageJson.filePath), + Boolean(filesIgnore) + ) + + if (filesIgnore && npmignoreIgnore) { + retv.match = and( + filterNeverIgnoredFiles(packageJson), + or(isAncestorFiles, filesIgnore, npmignoreIgnore) + ) + } else if (filesIgnore) { + retv.match = and( + filterNeverIgnoredFiles(packageJson), + or(isAncestorFiles, filesIgnore) + ) + } else if (npmignoreIgnore) { + retv.match = and( + filterNeverIgnoredFiles(packageJson), + or(isAncestorFiles, npmignoreIgnore) + ) } + cache.set(packageJson.filePath, retv) + return retv } diff --git a/lib/util/get-package-json.js b/lib/util/get-package-json.js index 92481528..23433754 100644 --- a/lib/util/get-package-json.js +++ b/lib/util/get-package-json.js @@ -16,7 +16,7 @@ const cache = new Cache() * Don't cache the data. * * @param {string} dir - The path to a directory to read. - * @returns {object|null} The read `package.json` data, or null. + * @returns {import('type-fest').JsonObject|null} The read `package.json` data, or null. */ function readPackageJson(dir) { const filePath = path.join(dir, "package.json") @@ -24,7 +24,11 @@ function readPackageJson(dir) { const text = fs.readFileSync(filePath, "utf8") const data = JSON.parse(text) - if (typeof data === "object" && data !== null) { + if ( + data != null && + typeof data === "object" && + Array.isArray(data) === false + ) { data.filePath = filePath return data } @@ -40,10 +44,10 @@ function readPackageJson(dir) { * The data is cached if found, then it's used after. * * @param {string} [startPath] - A file path to lookup. - * @returns {object|null} A found `package.json` data or `null`. + * @returns {import('type-fest').JsonObject|null} A found `package.json` data or `null`. * This object have additional property `filePath`. */ -module.exports = function getPackageJson(startPath = "a.js") { +function getPackageJson(startPath = "a.js") { const startDir = path.dirname(path.resolve(startPath)) let dir = startDir let prevDir = "" @@ -73,3 +77,5 @@ module.exports = function getPackageJson(startPath = "a.js") { cache.set(startDir, null) return null } + +module.exports = { getPackageJson } diff --git a/lib/util/get-resolve-paths.js b/lib/util/get-resolve-paths.js index d848cc64..6d043129 100644 --- a/lib/util/get-resolve-paths.js +++ b/lib/util/get-resolve-paths.js @@ -4,19 +4,19 @@ */ "use strict" -const DEFAULT_VALUE = Object.freeze([]) +/** @type {string[]} */ +const DEFAULT_VALUE = [] /** * Gets `resolvePaths` property from a given option object. * - * @param {object|undefined} option - An option object to get. - * @returns {string[]|null} The `allowModules` value, or `null`. + * @param {{ resolvePaths: unknown[] } | undefined} option - An option object to get. + * @returns {string[] | undefined} The `allowModules` value, or `null`. */ function get(option) { - if (option && option.resolvePaths && Array.isArray(option.resolvePaths)) { + if (Array.isArray(option?.resolvePaths)) { return option.resolvePaths.map(String) } - return null } /** @@ -26,15 +26,14 @@ function get(option) { * 2. This checks `settings.n` | `settings.node` property, then returns it if exists. * 3. This returns `[]`. * - * @param {RuleContext} context - The rule context. + * @param {import('eslint').Rule.RuleContext} context - The rule context. * @returns {string[]} A list of extensions. */ module.exports = function getResolvePaths(context, optionIndex = 0) { return ( - get(context.options && context.options[optionIndex]) || - get( - context.settings && (context.settings.n || context.settings.node) - ) || + get(context.options?.[optionIndex]) ?? + get(context.settings?.n) ?? + get(context.settings?.node) ?? DEFAULT_VALUE ) } diff --git a/lib/util/get-semver-range.js b/lib/util/get-semver-range.js index f9a4acdc..c1518352 100644 --- a/lib/util/get-semver-range.js +++ b/lib/util/get-semver-range.js @@ -10,21 +10,22 @@ const cache = new Map() /** * Get the `semver.Range` object of a given range text. * @param {string} x The text expression for a semver range. - * @returns {Range|null} The range object of a given range text. + * @returns {Range|undefined} The range object of a given range text. * It's null if the `x` is not a valid range text. */ module.exports = function getSemverRange(x) { - const s = String(x) - let ret = cache.get(s) || null - - if (!ret) { - try { - ret = new Range(s) - } catch { - // Ignore parsing error. - } - cache.set(s, ret) + const stringVersion = String(x) + const cached = cache.get(stringVersion) + if (cached != null) { + return cached } - return ret + try { + const output = new Range(stringVersion) + cache.set(stringVersion, output) + return output + } catch { + // Ignore parsing error. + cache.set(stringVersion, null) + } } diff --git a/lib/util/get-try-extensions.js b/lib/util/get-try-extensions.js index 29c16cad..87bc1a00 100644 --- a/lib/util/get-try-extensions.js +++ b/lib/util/get-try-extensions.js @@ -7,14 +7,10 @@ const { getTSConfigForContext } = require("./get-tsconfig") const isTypescript = require("./is-typescript") -const DEFAULT_JS_VALUE = Object.freeze([ - ".js", - ".json", - ".node", - ".mjs", - ".cjs", -]) -const DEFAULT_TS_VALUE = Object.freeze([ +/** @type {string[]} */ +const DEFAULT_JS_VALUE = [".js", ".json", ".node", ".mjs", ".cjs"] +/** @type {string[]} */ +const DEFAULT_TS_VALUE = [ ".js", ".ts", ".mjs", @@ -23,19 +19,18 @@ const DEFAULT_TS_VALUE = Object.freeze([ ".cts", ".json", ".node", -]) +] /** * Gets `tryExtensions` property from a given option object. * - * @param {object|undefined} option - An option object to get. - * @returns {string[]|null} The `tryExtensions` value, or `null`. + * @param {{ tryExtensions: unknown[] } | undefined} option - An option object to get. + * @returns {string[] | undefined} The `tryExtensions` value, or `null`. */ function get(option) { if (Array.isArray(option?.tryExtensions)) { return option.tryExtensions.map(String) } - return null } /** @@ -45,7 +40,7 @@ function get(option) { * 2. This checks `settings.n` | `settings.node` property, then returns it if exists. * 3. This returns `[".js", ".json", ".node", ".mjs", ".cjs"]`. * - * @param {RuleContext} context - The rule context. + * @param {import('eslint').Rule.RuleContext} context - The rule context. * @returns {string[]} A list of extensions. */ module.exports = function getTryExtensions(context, optionIndex = 0) { @@ -58,11 +53,16 @@ module.exports = function getTryExtensions(context, optionIndex = 0) { return configured } - if (isTypescript(context)) { - const tsconfig = getTSConfigForContext(context) - if (tsconfig?.config?.compilerOptions?.allowImportingTsExtensions) { - return DEFAULT_TS_VALUE - } + if (isTypescript(context) === false) { + return DEFAULT_JS_VALUE + } + + const allowImportingTsExtensions = + getTSConfigForContext(context)?.config?.compilerOptions + ?.allowImportingTsExtensions + + if (allowImportingTsExtensions === true) { + return DEFAULT_TS_VALUE } return DEFAULT_JS_VALUE diff --git a/lib/util/get-typescript-extension-map.js b/lib/util/get-typescript-extension-map.js index 5c6a3b1b..b9e8ef19 100644 --- a/lib/util/get-typescript-extension-map.js +++ b/lib/util/get-typescript-extension-map.js @@ -27,17 +27,20 @@ const tsConfigMapping = { } /** - * @typedef {Object} ExtensionMap + * @typedef ExtensionMap * @property {Record} forward Convert from typescript to javascript * @property {Record} backward Convert from javascript to typescript */ /** - * @param {Record} typescriptExtensionMap A forward extension mapping + * @param {[string, string][]} typescriptExtensionMap A forward extension mapping * @returns {ExtensionMap} */ function normalise(typescriptExtensionMap) { + /** @type {Record} */ const forward = {} + + /** @type {Record} */ const backward = {} for (const [typescript, javascript] of typescriptExtensionMap) { @@ -56,26 +59,33 @@ function normalise(typescriptExtensionMap) { * Attempts to get the ExtensionMap from the resolved tsconfig. * * @param {import("get-tsconfig").TsConfigJsonResolved} [tsconfig] - The resolved tsconfig - * @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`. + * @returns {ExtensionMap | null} The `typescriptExtensionMap` value, or `null`. */ function getMappingFromTSConfig(tsconfig) { const jsx = tsconfig?.compilerOptions?.jsx - if ({}.hasOwnProperty.call(tsConfigMapping, jsx)) { + if (jsx != null && {}.hasOwnProperty.call(tsConfigMapping, jsx)) { return tsConfigMapping[jsx] } return null } +/** + * @typedef Options + * @property {[string, string][] | keyof tsConfigMapping} typescriptExtensionMap + * @property {string | null} tsconfigPath + */ + /** * Gets `typescriptExtensionMap` property from a given option object. * - * @param {object|undefined} option - An option object to get. - * @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`. + * @param {Options} [option] - An option object to get. + * @returns {ExtensionMap | null} The `typescriptExtensionMap` value, or `null`. */ function get(option) { if ( + typeof option?.typescriptExtensionMap === "string" && {}.hasOwnProperty.call(tsConfigMapping, option?.typescriptExtensionMap) ) { return tsConfigMapping[option.typescriptExtensionMap] @@ -96,7 +106,7 @@ function get(option) { * Attempts to get the ExtensionMap from the tsconfig of a given file. * * @param {import('eslint').Rule.RuleContext} context - The current file context - * @returns {ExtensionMap} The `typescriptExtensionMap` value, or `null`. + * @returns {ExtensionMap | null} The `typescriptExtensionMap` value, or `null`. */ function getFromTSConfigFromFile(context) { return getMappingFromTSConfig(getTSConfigForContext(context)?.config) diff --git a/lib/util/import-target.js b/lib/util/import-target.js index 8df130c3..93f72c74 100644 --- a/lib/util/import-target.js +++ b/lib/util/import-target.js @@ -5,19 +5,33 @@ "use strict" const { resolve } = require("path") -const isBuiltin = require("is-builtin-module") +const isBuiltinModule = require("is-builtin-module") const resolver = require("enhanced-resolve") const isTypescript = require("./is-typescript") const { getTSConfigForContext } = require("./get-tsconfig.js") const getTypescriptExtensionMap = require("./get-typescript-extension-map") +/** + * @overload + * @param {string[]} input + * @returns {string[]} + */ +/** + * @overload + * @param {string} input + * @returns {string} + */ +/** + * @param {string | string[]} input + * @returns {string | string[]} + */ function removeTrailWildcard(input) { - if (Array.isArray(input)) { - return [...input].map(removeTrailWildcard) + if (typeof input === "string") { + return input.replace(/[/\\*]+$/, "") } - return input.replace(/[/\\*]+$/, "") + return input.map(removeTrailWildcard) } /** @@ -41,20 +55,26 @@ function getTSConfigAliases(context) { } /** - * @typedef {Object} Options + * @typedef Options * @property {string[]} [extensions] * @property {string[]} [paths] * @property {string} basedir */ +/** @typedef { 'unknown' | 'relative' | 'absolute' | 'node' | 'npm' | 'http' } ModuleType */ +/** @typedef { 'import' | 'require' | 'type' } ModuleStyle */ + /** - * @typedef { 'unknown' | 'relative' | 'absolute' | 'node' | 'npm' | 'http' } ModuleType - * @typedef { 'import' | 'require' | 'type' } ModuleStyle + * @param {string} string The string to manipulate + * @param {string} matcher The character to use as a segmenter + * @param {Number} [count=1] How many segments to keep + * @returns {string} */ - function trimAfter(string, matcher, count = 1) { return string.split(matcher).slice(0, count).join(matcher) } +/** @typedef {import('estree').Node & { parent?: Node }} Node */ + /** * Information of an import target. */ @@ -62,7 +82,7 @@ module.exports = class ImportTarget { /** * Initialize this instance. * @param {import('eslint').Rule.RuleContext} context - The context for the import origin. - * @param {import('eslint').Rule.Node} node - The node of a `require()` or a module declaraiton. + * @param {Node} node - The node of a `require()` or a module declaraiton. * @param {string} name - The name of an import target. * @param {Options} options - The options of `enhanced-resolve` module. * @param {'import' | 'require'} moduleType - whether the target was require-ed or imported @@ -70,13 +90,13 @@ module.exports = class ImportTarget { constructor(context, node, name, options, moduleType) { /** * The context for the import origin - * @type {import('eslint').Rule.Node} + * @type {import('eslint').Rule.RuleContext} */ this.context = context /** * The node of a `require()` or a module declaraiton. - * @type {import('eslint').Rule.Node} + * @type {Node} */ this.node = node @@ -107,7 +127,7 @@ module.exports = class ImportTarget { /** * The module name of this import target. * If the target is a relative path then this is `null`. - * @type {string | null} + * @type {string | undefined} */ this.moduleName = this.getModuleName() @@ -132,7 +152,7 @@ module.exports = class ImportTarget { return "absolute" } - if (isBuiltin(this.name)) { + if (isBuiltinModule(this.name)) { return "node" } @@ -153,35 +173,32 @@ module.exports = class ImportTarget { * @returns {ModuleStyle} */ getModuleStyle(fallback) { - /** @type {import('eslint').Rule.Node} */ - let node = { parent: this.node } + let node = this.node do { - node = node.parent + if (node.parent == null) { + break + } // `const {} = require('')` if ( - node.type === "CallExpression" && - node.callee.name === "require" + node.parent.type === "CallExpression" && + node.parent.callee.type === "Identifier" && + node.parent.callee.name === "require" ) { return "require" } - // `import type {} from '';` - if ( - node.type === "ImportDeclaration" && - node.importKind === "type" - ) { - return "type" - } - // `import {} from '';` - if ( - node.type === "ImportDeclaration" && - node.importKind === "value" - ) { - return "import" + if (node.parent.type === "ImportDeclaration") { + // `import type {} from '';` + return "importKind" in node.parent && + node.parent.importKind === "type" + ? "type" + : "import" } + + node = node.parent } while (node.parent) return fallback @@ -189,7 +206,7 @@ module.exports = class ImportTarget { /** * Get the node or npm module name - * @returns {string} + * @returns {string | undefined} */ getModuleName() { if (this.moduleType === "relative") return @@ -211,6 +228,9 @@ module.exports = class ImportTarget { } } + /** + * @returns {string[]} + */ getPaths() { if (Array.isArray(this.options.paths)) { return [...this.options.paths, this.options.basedir] @@ -268,7 +288,8 @@ module.exports = class ImportTarget { for (const directory of this.getPaths()) { try { const baseDir = resolve(cwd, directory) - return requireResolve(baseDir, this.name) + const resolved = requireResolve(baseDir, this.name) + if (typeof resolved === "string") return resolved } catch { continue } diff --git a/lib/util/is-bin-file.js b/lib/util/is-bin-file.js new file mode 100644 index 00000000..f16ef0a3 --- /dev/null +++ b/lib/util/is-bin-file.js @@ -0,0 +1,53 @@ +"use strict" + +const path = require("path") + +/** + * @param {string} filePath + * @param {string} binField + * @returns {boolean} + */ +function simulateNodeResolutionAlgorithm(filePath, binField) { + const possibilities = [filePath] + let newFilePath = filePath.replace(/\.js$/u, "") + possibilities.push(newFilePath) + newFilePath = newFilePath.replace(/[/\\]index$/u, "") + possibilities.push(newFilePath) + return possibilities.includes(binField) +} + +/** + * Checks whether or not a given path is a `bin` file. + * + * @param {string} filePath - A file path to check. + * @param {unknown} binField - A value of the `bin` field of `package.json`. + * @param {string} basedir - A directory path that `package.json` exists. + * @returns {boolean} `true` if the file is a `bin` file. + */ +function isBinFile(filePath, binField, basedir) { + if (!binField) { + return false + } + + if (typeof binField === "string") { + return simulateNodeResolutionAlgorithm( + filePath, + path.resolve(basedir, binField) + ) + } + + if (binField instanceof Object === false) { + return false + } + + for (const value of Object.values(binField)) { + const resolvedPath = path.resolve(basedir, value) + if (simulateNodeResolutionAlgorithm(filePath, resolvedPath)) { + return true + } + } + + return false +} + +module.exports = { isBinFile } diff --git a/lib/util/is-typescript.js b/lib/util/is-typescript.js index fb468d21..80163066 100644 --- a/lib/util/is-typescript.js +++ b/lib/util/is-typescript.js @@ -7,7 +7,7 @@ const typescriptExtensions = [".ts", ".tsx", ".cts", ".mts"] /** * Determine if the context source file is typescript. * - * @param {RuleContext} context - A context + * @param {import('eslint').Rule.RuleContext} context - A context * @returns {boolean} */ module.exports = function isTypescript(context) { diff --git a/lib/util/map-typescript-extension.js b/lib/util/map-typescript-extension.js index b4806033..9233d728 100644 --- a/lib/util/map-typescript-extension.js +++ b/lib/util/map-typescript-extension.js @@ -14,27 +14,43 @@ const getTypescriptExtensionMap = require("./get-typescript-extension-map") * @param {import('eslint').Rule.RuleContext} context * @param {string} filePath The filePath of the import * @param {string} fallbackExtension The non-typescript fallback - * @param {boolean} reverse Execute a reverse path mapping * @returns {string} The file extension to append to the import statement. */ -module.exports = function mapTypescriptExtension( - context, - filePath, - fallbackExtension, - reverse = false -) { - const { forward, backward } = getTypescriptExtensionMap(context) +function convertTsExtensionToJs(context, filePath, fallbackExtension) { + const { forward } = getTypescriptExtensionMap(context) const ext = path.extname(filePath) - if (reverse) { - if (isTypescript(context) && ext in backward) { - return backward[ext] - } - return [fallbackExtension] - } else { - if (isTypescript(context) && ext in forward) { - return forward[ext] - } + + if (isTypescript(context) && ext in forward) { + return forward[ext] } return fallbackExtension } + +/** + * Maps the typescript file extension that should be added in an import statement, + * based on the given file extension of the referenced file OR fallsback to the original given extension. + * + * For example, in typescript, when referencing another typescript from a typescript file, + * a .js extension should be used instead of the original .ts extension of the referenced file. + * + * @param {import('eslint').Rule.RuleContext} context + * @param {string} filePath The filePath of the import + * @param {string} fallbackExtension The non-typescript fallback + * @returns {string[]} The file extension to append to the import statement. + */ +function convertJsExtensionToTs(context, filePath, fallbackExtension) { + const { backward } = getTypescriptExtensionMap(context) + const ext = path.extname(filePath) + + if (isTypescript(context) && Object.hasOwn(backward, ext)) { + return backward[ext] + } + + return [fallbackExtension] +} + +module.exports = { + convertTsExtensionToJs, + convertJsExtensionToTs, +} diff --git a/lib/util/merge-visitors-in-place.js b/lib/util/merge-visitors-in-place.js index eea30da3..3b6457d1 100644 --- a/lib/util/merge-visitors-in-place.js +++ b/lib/util/merge-visitors-in-place.js @@ -7,27 +7,35 @@ /** * Merge two visitors. * This function modifies `visitor1` directly to merge. - * @param {Visitor} visitor1 The visitor which is assigned. - * @param {Visitor} visitor2 The visitor which is assigning. - * @returns {Visitor} `visitor1`. + * @param {import('eslint').Rule.RuleListener} visitor1 The visitor which is assigned. + * @param {import('eslint').Rule.RuleListener} visitor2 The visitor which is assigning. + * @returns {import('eslint').Rule.RuleListener} `visitor1`. */ module.exports = function mergeVisitorsInPlace(visitor1, visitor2) { for (const key of Object.keys(visitor2)) { const handler1 = visitor1[key] const handler2 = visitor2[key] - if (typeof handler1 === "function") { - if (handler1._handlers) { - handler1._handlers.push(handler2) - } else { - const handlers = [handler1, handler2] - visitor1[key] = Object.assign(dispatch.bind(null, handlers), { - _handlers: handlers, - }) - } - } else { + if (typeof handler1 !== "function") { visitor1[key] = handler2 + continue } + + if (typeof handler2 !== "function") { + continue + } + + if ("_handlers" in handler1 && Array.isArray(handler1._handlers)) { + handler1._handlers.push(handler2) + continue + } + + const handlers = [handler1, handler2] + + // @ts-expect-error - This is because its expecting a function that can match all {Rule.RuleListener[string]} functions! + visitor1[key] = Object.assign(dispatch.bind(null, handlers), { + _handlers: handlers, + }) } return visitor1 diff --git a/lib/util/strip-import-path-params.js b/lib/util/strip-import-path-params.js index c1afabdf..3dcfe5f6 100644 --- a/lib/util/strip-import-path-params.js +++ b/lib/util/strip-import-path-params.js @@ -4,7 +4,17 @@ */ "use strict" +/** + * @param {unknown} path + * @returns {string} + */ module.exports = function stripImportPathParams(path) { - const i = path.toString().indexOf("!") - return i === -1 ? path : path.slice(0, i) + const pathString = String(path) + const index = pathString.indexOf("!") + + if (index === -1) { + return pathString + } + + return pathString.slice(0, index) } diff --git a/lib/util/visit-import.js b/lib/util/visit-import.js index 69c41599..4742dcbe 100644 --- a/lib/util/visit-import.js +++ b/lib/util/visit-import.js @@ -5,30 +5,37 @@ "use strict" const path = require("path") -const isCoreModule = require("is-core-module") +const isBuiltinModule = require("is-builtin-module") const getResolvePaths = require("./get-resolve-paths") const getTryExtensions = require("./get-try-extensions") const ImportTarget = require("./import-target") const stripImportPathParams = require("./strip-import-path-params") +/** @typedef {import('@typescript-eslint/typescript-estree').TSESTree.ImportDeclaration} ImportDeclaration */ + +/** + * @typedef VisitImportOptions + * @property {boolean} [includeCore=false] The flag to include core modules. + * @property {number} [optionIndex=0] The index of rule options. + * @property {boolean} [ignoreTypeImport=false] The flag to ignore typescript type imports. + */ + /** * Gets a list of `import`/`export` declaration targets. * * Core modules of Node.js (e.g. `fs`, `http`) are excluded. * - * @param {RuleContext} context - The rule context. - * @param {Object} [options] - The flag to include core modules. - * @param {boolean} [options.includeCore] - The flag to include core modules. - * @param {number} [options.optionIndex] - The index of rule options. - * @param {boolean} [options.ignoreTypeImport] - The flag to ignore typescript type imports. + * @param {import('eslint').Rule.RuleContext} context - The rule context. + * @param {VisitImportOptions} options - The flag to include core modules. * @param {function(ImportTarget[]):void} callback The callback function to get result. - * @returns {ImportTarget[]} A list of found target's information. + * @returns {import('eslint').Rule.RuleListener} A list of found target's information. */ module.exports = function visitImport( context, - { includeCore = false, optionIndex = 0, ignoreTypeImport = false } = {}, + { includeCore = false, optionIndex = 0, ignoreTypeImport = false }, callback ) { + /** @type {import('./import-target.js')[]} */ const targets = [] const basedir = path.dirname( path.resolve(context.filename ?? context.getFilename()) @@ -37,46 +44,53 @@ module.exports = function visitImport( const extensions = getTryExtensions(context, optionIndex) const options = { basedir, paths, extensions } - return { - [[ - "ExportAllDeclaration", - "ExportNamedDeclaration", - "ImportDeclaration", - "ImportExpression", - ]](node) { - const sourceNode = node.source + /** + * @param {( + * | import('estree').ExportAllDeclaration + * | import('estree').ExportNamedDeclaration + * | import('estree').ImportDeclaration + * | import('estree').ImportExpression + * )} node + */ + function addTarget(node) { + if (node.source == null || node.source.type !== "Literal") { + return + } - // skip `import(foo)` - if ( - node.type === "ImportExpression" && - sourceNode && - sourceNode.type !== "Literal" - ) { + const name = stripImportPathParams(node.source?.value) + if (includeCore === true || isBuiltinModule(name) === false) { + targets.push( + new ImportTarget(context, node.source, name, options, "import") + ) + } + } + + return { + ExportAllDeclaration(node) { + addTarget(node) + }, + ExportNamedDeclaration(node) { + addTarget(node) + }, + ImportDeclaration(node) { + if (node.source?.value == null) { return } - - // skip `import type { foo } from 'bar'` (for eslint-typescript) if ( - ignoreTypeImport && - node.type === "ImportDeclaration" && - node.importKind === "type" + ignoreTypeImport === true && + /** @type {ImportDeclaration} */ (node).importKind === "type" ) { return } - const name = sourceNode && stripImportPathParams(sourceNode.value) - // Note: "999" arbitrary to check current/future Node.js version - if (name && (includeCore || !isCoreModule(name, "999"))) { - targets.push( - new ImportTarget( - context, - sourceNode, - name, - options, - "import" - ) - ) + addTarget(node) + }, + ImportExpression(node) { + if (node.source?.type !== "Literal") { + return } + + addTarget(node) }, "Program:exit"() { diff --git a/lib/util/visit-require.js b/lib/util/visit-require.js index 99fb3ce2..394f83a6 100644 --- a/lib/util/visit-require.js +++ b/lib/util/visit-require.js @@ -10,28 +10,33 @@ const { ReferenceTracker, getStringIfConstant, } = require("@eslint-community/eslint-utils") -const isCoreModule = require("is-core-module") +const isBuiltinModule = require("is-builtin-module") const getResolvePaths = require("./get-resolve-paths") const getTryExtensions = require("./get-try-extensions") const ImportTarget = require("./import-target") const stripImportPathParams = require("./strip-import-path-params") +/** + * @typedef VisitRequireOptions + * @property {boolean} [includeCore=false] The flag to include core modules. + */ + /** * Gets a list of `require()` targets. * * Core modules of Node.js (e.g. `fs`, `http`) are excluded. * - * @param {RuleContext} context - The rule context. - * @param {Object} [options] - The flag to include core modules. - * @param {boolean} [options.includeCore] - The flag to include core modules. - * @param {function(ImportTarget[]):void} callback The callback function to get result. - * @returns {Object} The visitor. + * @param {import('eslint').Rule.RuleContext} context - The rule context. + * @param {VisitRequireOptions} options - The flag to include core modules. + * @param {function(ImportTarget[]): void} callback The callback function to get result. + * @returns {import('eslint').Rule.RuleListener} The visitor. */ module.exports = function visitRequire( context, - { includeCore = false } = {}, + { includeCore = false }, callback ) { + /** @type {import('./import-target.js')[]} */ const targets = [] const basedir = path.dirname( path.resolve(context.filename ?? context.getFilename()) @@ -54,11 +59,18 @@ module.exports = function visitRequire( }) for (const { node } of references) { + if (node.type !== "CallExpression") { + continue + } + const targetNode = node.arguments[0] const rawName = getStringIfConstant(targetNode) - const name = rawName && stripImportPathParams(rawName) - // Note: "999" arbitrary to check current/future Node.js version - if (name && (includeCore || !isCoreModule(name, "999"))) { + if (typeof rawName !== "string") { + continue + } + + const name = stripImportPathParams(rawName) + if (includeCore || !isBuiltinModule(name)) { targets.push( new ImportTarget( context, diff --git a/package.json b/package.json index eb7e17b9..056b6f79 100644 --- a/package.json +++ b/package.json @@ -6,29 +6,34 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "main": "lib/index.js", + "types": "types/index.d.ts", "files": [ "lib/", - "configs/" + "configs/", + "types/index.d.ts" ], "peerDependencies": { "eslint": ">=8.23.0" }, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", + "@types/eslint": "^8.56.2", "enhanced-resolve": "^5.15.0", "eslint-plugin-es-x": "^7.5.0", "get-tsconfig": "^4.7.0", "globals": "^14.0.0", "ignore": "^5.2.4", "is-builtin-module": "^3.2.1", - "is-core-module": "^2.12.1", "minimatch": "^9.0.0", "semver": "^7.5.3" }, "devDependencies": { "@eslint/js": "^9.0.0", "@types/eslint": "^8.56.2", + "@types/estree": "^1.0.5", + "@types/node": "^20.11.0", "@typescript-eslint/parser": "^7.0.0", + "@typescript-eslint/typescript-estree": "^6.18.1", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-doc-generator": "^1.6.1", @@ -46,6 +51,7 @@ "punycode": "^2.3.0", "release-it": "^17.0.0", "rimraf": "^5.0.1", + "type-fest": "^4.9.0", "typescript": "^5.1.3" }, "scripts": { @@ -59,13 +65,13 @@ "lint:js": "eslint .", "new": "node scripts/new-rule", "postversion": "git push && git push --tags", + "prepack": "tsc --emitDeclarationOnly", "prepare": "husky", - "pretest": "npm run -s lint", "preversion": "npm test", - "test": "nyc npm run -s test:_mocha", - "test:_mocha": "_mocha --reporter progress --timeout 4000 \"tests/lib/**/*.js\"", + "test": "run-p lint:* test:types test:tests", "test:mocha": "_mocha --reporter progress --timeout 4000", - "test:ci": "nyc npm run -s test:_mocha", + "test:tests": "npm run test:mocha \"tests/lib/**/*.js\"", + "test:types": "tsc --noEmit --emitDeclarationOnly false", "update:eslint-docs": "eslint-doc-generator", "version": "npm run -s build && eslint lib/rules --fix && git add .", "watch": "npm run test:_mocha -- --watch --growl" diff --git a/scripts/rules.js b/scripts/rules.js index 31acdcdb..c0d6144c 100644 --- a/scripts/rules.js +++ b/scripts/rules.js @@ -10,7 +10,7 @@ const rootDir = path.resolve(__dirname, "../lib/rules/") const { pluginName } = require("./utils") /** - * @typedef {Object} RuleInfo + * @typedef RuleInfo * @property {string} filePath The path to the rule definition. * @property {string} id The rule ID. (This includes `n/` prefix.) * @property {string} name The rule name. (This doesn't include `n/` prefix.) @@ -23,7 +23,7 @@ const { pluginName } = require("./utils") */ /** - * @typedef {Object} CategoryInfo + * @typedef CategoryInfo * @property {string} id The category ID. * @property {RuleInfo[]} rules The rules which belong to this category. */ diff --git a/tests/fixtures/file-extension-in-import/ts-allow-extension/tsconfig.json b/tests/fixtures/file-extension-in-import/ts-allow-extension/tsconfig.json index 2035ba8b..32d0a2a9 100644 --- a/tests/fixtures/file-extension-in-import/ts-allow-extension/tsconfig.json +++ b/tests/fixtures/file-extension-in-import/ts-allow-extension/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { "noEmit": true, - "allowImportingTsExtensions": true, - }, + "allowImportingTsExtensions": true + } } diff --git a/tests/fixtures/no-missing/ts-allow-extension/tsconfig.json b/tests/fixtures/no-missing/ts-allow-extension/tsconfig.json index 2035ba8b..32d0a2a9 100644 --- a/tests/fixtures/no-missing/ts-allow-extension/tsconfig.json +++ b/tests/fixtures/no-missing/ts-allow-extension/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { "noEmit": true, - "allowImportingTsExtensions": true, - }, + "allowImportingTsExtensions": true + } } diff --git a/tests/fixtures/no-missing/ts-extends/tsconfig.json b/tests/fixtures/no-missing/ts-extends/tsconfig.json index ad3a83db..383418e6 100644 --- a/tests/fixtures/no-missing/ts-extends/tsconfig.json +++ b/tests/fixtures/no-missing/ts-extends/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": ["./base.tsconfig.json"], + "extends": ["./base.tsconfig.json"] } diff --git a/tests/fixtures/no-missing/ts-paths/tsconfig.json b/tests/fixtures/no-missing/ts-paths/tsconfig.json index b933e44c..f832947b 100644 --- a/tests/fixtures/no-missing/ts-paths/tsconfig.json +++ b/tests/fixtures/no-missing/ts-paths/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "paths": { "@direct": ["./some/where.ts"], - "@wild/*": ["./some/*"], - }, - }, + "@wild/*": ["./some/*"] + } + } } diff --git a/tests/fixtures/no-missing/ts-preserve/tsconfig.json b/tests/fixtures/no-missing/ts-preserve/tsconfig.json index 35e2dff9..94e40481 100644 --- a/tests/fixtures/no-missing/ts-preserve/tsconfig.json +++ b/tests/fixtures/no-missing/ts-preserve/tsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { - "jsx": "preserve", - }, + "jsx": "preserve" + } } diff --git a/tests/fixtures/no-missing/ts-react/tsconfig.json b/tests/fixtures/no-missing/ts-react/tsconfig.json index d35c99c2..b5fbd521 100644 --- a/tests/fixtures/no-missing/ts-react/tsconfig.json +++ b/tests/fixtures/no-missing/ts-react/tsconfig.json @@ -1,5 +1,5 @@ { "compilerOptions": { - "jsx": "react", - }, + "jsx": "react" + } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..24ffaedd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2022", + "moduleResolution": "node16", + + "module": "nodenext", + + "allowJs": true, + "checkJs": true, + + "declaration": true, + "emitDeclarationOnly": true, + "declarationDir": "./types", + + "allowSyntheticDefaultImports": false, + "esModuleInterop": true, + "resolveJsonModule": true, + + "strict": true, + "skipDefaultLibCheck": false, + "skipLibCheck": false + }, + "include": ["./lib/eslint-utils.d.ts"], + "files": ["./lib/index.js"] +}