From 1aa21507085a82a9305ce7e753aa4c08a0b86d46 Mon Sep 17 00:00:00 2001 From: scagood <2230835+scagood@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:21:06 +0000 Subject: [PATCH] feat: Types for lib/rules --- lib/eslint-utils.d.ts | 4 + lib/rules/exports-style.js | 122 ++++++++++++------ lib/rules/file-extension-in-import.js | 5 +- lib/rules/global-require.js | 27 ++-- lib/rules/handle-callback-err.js | 6 +- lib/rules/hashbang.js | 51 +------- lib/rules/no-callback-literal.js | 16 +-- lib/rules/no-deprecated-api.js | 33 +++-- lib/rules/no-exports-assign.js | 10 ++ lib/rules/no-hide-core-modules.js | 7 +- lib/rules/no-mixed-requires.js | 28 +++- lib/rules/no-path-concat.js | 35 +++-- lib/rules/no-process-env.js | 22 ++-- lib/rules/no-process-exit.js | 12 +- lib/rules/no-sync.js | 5 + lib/rules/no-unpublished-bin.js | 21 +-- .../no-unsupported-features/es-syntax.js | 51 ++++++-- .../no-unsupported-features/es-syntax.json | 10 +- lib/rules/prefer-node-protocol.js | 2 +- lib/util/check-prefer-global.js | 2 +- lib/util/enumerate-property-names.js | 9 +- lib/util/extend-trackmap-with-node-prefix.js | 5 +- lib/util/is-bin-file.js | 53 ++++++++ 23 files changed, 328 insertions(+), 208 deletions(-) create mode 100644 lib/util/is-bin-file.js diff --git a/lib/eslint-utils.d.ts b/lib/eslint-utils.d.ts index 5c41a16f..0db31dba 100644 --- a/lib/eslint-utils.d.ts +++ b/lib/eslint-utils.d.ts @@ -1,3 +1,7 @@ +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'; diff --git a/lib/rules/exports-style.js b/lib/rules/exports-style.js index f142c695..6a157151 100644 --- a/lib/rules/exports-style.js +++ b/lib/rules/exports-style.js @@ -4,26 +4,30 @@ */ "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 {import('eslint').Rule.Node} node - The node to get. + * @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?.type) { case "Property": case "MethodDefinition": - prop = /** @type {import('estree').Property} */ (node).key + prop = node.key break case "MemberExpression": - prop = /** @type {import('estree').MemberExpression} */ (node) - .property + prop = node.property break // no default @@ -31,22 +35,21 @@ function getStaticPropertyName(node) { switch (prop?.type) { case "Literal": - return String(/** @type {import('estree').Literal} */ (prop).value) + return String(prop.value) case "TemplateLiteral": - if ( - /** @type {import('estree').TemplateLiteral} */ (prop) - .expressions.length === 0 && - /** @type {import('estree').TemplateLiteral} */ (prop).quasis - .length === 1 - ) { - return /** @type {import('estree').TemplateLiteral} */ (prop) - .quasis[0].value.cooked + if (prop.expressions.length === 0 && prop.quasis.length === 1) { + return prop.quasis[0].value.cooked } break case "Identifier": - if (!node.computed) { + if ( + !( + /** @type {import('estree').MemberExpression} */ (node) + .computed + ) + ) { return prop.name } break @@ -60,12 +63,13 @@ function getStaticPropertyName(node) { /** * Checks whether the given node is assignee or not. * - * @param {import('eslint').Rule.Node} 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 ) } @@ -75,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 {import('eslint').Rule.Node} leafNode - The node to get. - * @returns {import('eslint').Rule.Node|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 @@ -95,7 +99,7 @@ function getTopAssignment(leafNode) { } // Find the top. - while (node.parent.type === "AssignmentExpression") { + while (node.parent?.type === "AssignmentExpression") { node = node.parent } @@ -105,20 +109,18 @@ function getTopAssignment(leafNode) { /** * Gets top assignment nodes of the given node list. * - * @param {import('eslint').Rule.Node[]} nodes - The node list to get. - * @returns {import('eslint').Rule.Node[]} Gotten top assignment nodes. + * @param {Node[]} nodes - The node list to get. + * @returns {Node[]} Gotten top assignment nodes. */ function createAssignmentList(nodes) { - return /** @type {import('eslint').Rule.Node[]} */ ( - nodes.map(getTopAssignment).filter(Boolean) - ) + return /** @type {Node[]} */ (nodes.map(getTopAssignment).filter(Boolean)) } /** * Gets the reference of `module.exports` from the given scope. * * @param {import('eslint').Scope.Scope} scope - The scope to get. - * @returns {import('eslint').Rule.Node[]} Gotten MemberExpression node list. + * @returns {Node[]} Gotten MemberExpression node list. */ function getModuleExportsNodes(scope) { const variable = scope.set.get("module") @@ -126,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" ) } @@ -149,6 +155,11 @@ function getExportsNodes(scope) { 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: @@ -162,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}` @@ -172,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: @@ -190,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") @@ -280,14 +307,16 @@ module.exports = { * module.exports = foo * ^^^^^^^^^^^^^^^^ * - * @param {import('eslint').Rule.Node} 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, } } @@ -306,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 } @@ -340,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) @@ -366,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 ac20aa83..01236a32 100644 --- a/lib/rules/file-extension-in-import.js +++ b/lib/rules/file-extension-in-import.js @@ -70,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 } diff --git a/lib/rules/global-require.js b/lib/rules/global-require.js index 7f0244b3..78151e1e 100644 --- a/lib/rules/global-require.js +++ b/lib/rules/global-require.js @@ -17,34 +17,28 @@ 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} */ @@ -73,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 a8d05fe6..313659ed 100644 --- a/lib/rules/handle-callback-err.js +++ b/lib/rules/handle-callback-err.js @@ -55,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( @@ -67,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 daa36699..34a8a9df 100644 --- a/lib/rules/hashbang.js +++ b/lib/rules/hashbang.js @@ -10,62 +10,13 @@ const matcher = require("ignore").default const getConvertPath = require("../util/get-convert-path") 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 -/** - * @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) { - for (const [value] of Object.values(binField)) { - if (typeof value !== "string") { - continue - } - - const resolvedPath = path.resolve(basedir, value) - if (simulateNodeResolutionAlgorithm(filePath, resolvedPath)) { - return true - } - } - } - - return false -} - /** * Gets the shebang line (includes a line ending) from a given code. * diff --git a/lib/rules/no-callback-literal.js b/lib/rules/no-callback-literal.js index 1b311d44..150271e4 100644 --- a/lib/rules/no-callback-literal.js +++ b/lib/rules/no-callback-literal.js @@ -4,6 +4,8 @@ */ "use strict" +const callbackNames = ["callback", "cb"] + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -23,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, @@ -51,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 65d491bb..38fb2650 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 {Object} DeprecatedInfo + * @property {string} since + * @property {string|{ name: string, supported: string }[]|null} replacedBy + */ +/** + * @typedef {Object} 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 ") @@ -675,8 +691,7 @@ function toName(type, path) { /** * Parses the options. * @param {import('eslint').Rule.RuleContext} context The rule context. - * @returns {{version:Range,ignoredGlobalItems:Set,ignoredModuleItems:Set}} Parsed - * value. + * @returns {ParsedOptions} Parsed options */ function parseOptions(context) { const raw = context.options[0] || {} @@ -739,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 4be08c6c..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 diff --git a/lib/rules/no-hide-core-modules.js b/lib/rules/no-hide-core-modules.js index 5f6f159a..e213faa1 100644 --- a/lib/rules/no-hide-core-modules.js +++ b/lib/rules/no-hide-core-modules.js @@ -14,6 +14,7 @@ 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", @@ -90,6 +91,7 @@ module.exports = { const filePath = path.resolve(filename) const dirPath = path.dirname(filePath) const packageJson = getPackageJson(filePath) + /** @type {Set} */ const deps = new Set([ ...Object.keys(packageJson?.dependencies ?? {}), ...Object.keys(packageJson?.devDependencies ?? {}), @@ -102,6 +104,7 @@ module.exports = { const ignoreIndirectDependencies = Boolean( options.ignoreIndirectDependencies ) + /** @type {import('../util/import-target.js')[]} */ const targets = [] return [ @@ -134,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 diff --git a/lib/rules/no-mixed-requires.js b/lib/rules/no-mixed-requires.js index a2827e98..662d519e 100644 --- a/lib/rules/no-mixed-requires.js +++ b/lib/rules/no-mixed-requires.js @@ -127,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) { @@ -163,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) { @@ -171,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);" @@ -199,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) { @@ -220,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-path-concat.js b/lib/rules/no-path-concat.js index 96f11ae4..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 ( @@ -180,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 @@ -204,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 d7057fa9..daec0def 100644 --- a/lib/rules/no-process-env.js +++ b/lib/rules/no-process-env.js @@ -8,6 +8,13 @@ // Rule Definition //------------------------------------------------------------------------------ +const querySelector = [ + `MemberExpression`, + `[computed!=true]`, + `[object.name="process"]`, + `[property.name="env"]`, +] + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -26,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 b39ec84c..c295aec2 100644 --- a/lib/rules/no-process-exit.js +++ b/lib/rules/no-process-exit.js @@ -4,6 +4,13 @@ */ "use strict" +const querySelector = [ + `CallExpression > `, + `MemberExpression.callee`, + `[object.name="process"]`, + `[property.name="exit"]`, +] + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -22,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-sync.js b/lib/rules/no-sync.js index 78c49635..f32f0fed 100644 --- a/lib/rules/no-sync.js +++ b/lib/rules/no-sync.js @@ -55,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 a42c9d72..56da0181 100644 --- a/lib/rules/no-unpublished-bin.js +++ b/lib/rules/no-unpublished-bin.js @@ -8,26 +8,7 @@ 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 { isBinFile } = require("../util/is-bin-file") /** @type {import('eslint').Rule.RuleModule} */ module.exports = { diff --git a/lib/rules/no-unsupported-features/es-syntax.js b/lib/rules/no-unsupported-features/es-syntax.js index 5052311d..a2950599 100644 --- a/lib/rules/no-unsupported-features/es-syntax.js +++ b/lib/rules/no-unsupported-features/es-syntax.js @@ -18,7 +18,7 @@ const ignoreKeys = new Set() /** * @typedef {Object} ESSyntax - * @property {string[]} aliases + * @property {string[]} [aliases] * @property {string | null} supported * @property {string} [strictMode] * @property {string} [deprecated] @@ -29,6 +29,7 @@ const ignoreKeys = new Set() * @property {string} feature * @property {string[]} ignoreNames * @property {import("semver").Range} supported + * @property {import("semver").Range} [strictMode] * @property {boolean} deprecated */ @@ -57,8 +58,12 @@ 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), } }) @@ -66,9 +71,10 @@ const ruleMap = Object.entries(features).map(([ruleId, meta]) => { /** * Parses the options. * @param {import('eslint').Rule.RuleContext} context The rule context. - * @returns {{version:Range,ignores:Set}} Parsed value. + * @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 @@ -101,7 +112,7 @@ function isStrict(context, node) { /** * Define the visitor object as merging the rules of eslint-plugin-es-x. * @param {import('eslint').Rule.RuleContext} context The rule context. - * @param {{version:Range,ignores:Set}} options The options. + * @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,18 +161,20 @@ 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, {}) } 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/prefer-node-protocol.js b/lib/rules/prefer-node-protocol.js index b4588916..85fba1cf 100644 --- a/lib/rules/prefer-node-protocol.js +++ b/lib/rules/prefer-node-protocol.js @@ -101,7 +101,7 @@ module.exports = { /** * @param {import('eslint').Rule.RuleContext} context * @param {import('../util/import-target.js').ModuleStyle} moduleStyle - * @returns {Boolean} + * @returns {boolean} */ function isEnablingThisRule(context, moduleStyle) { const version = getConfiguredNodeVersion(context) diff --git a/lib/util/check-prefer-global.js b/lib/util/check-prefer-global.js index a2b06fd0..944d0354 100644 --- a/lib/util/check-prefer-global.js +++ b/lib/util/check-prefer-global.js @@ -72,7 +72,7 @@ class Verifier { /** * @param {import('eslint').Rule.RuleContext} context - * @param {TraceMap} trackMap [description] + * @param {TraceMap} trackMap * @returns {void} */ module.exports = function checkForPreferGlobal(context, trackMap) { diff --git a/lib/util/enumerate-property-names.js b/lib/util/enumerate-property-names.js index 87776fb5..5b18df6f 100644 --- a/lib/util/enumerate-property-names.js +++ b/lib/util/enumerate-property-names.js @@ -7,9 +7,16 @@ const { CALL, CONSTRUCT, READ } = require("@eslint-community/eslint-utils") const unprefixNodeColon = require("./unprefix-node-colon") +/** + * @typedef {Object} DeprecatedInfo + * @property {string} since + * @property {string|{ name: string, supported: string }[]|null} replacedBy + */ +/** @typedef {import('@eslint-community/eslint-utils').TraceMap} DeprecatedInfoTraceMap */ + /** * Enumerate property names of a given object recursively. - * @param {import('../unsupported-features/types.js').SupportVersionTree} trackMap The map for APIs to enumerate. + * @param {import('../unsupported-features/types.js').SupportVersionTree | DeprecatedInfoTraceMap} trackMap 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) * @returns {IterableIterator} The property names of the map. diff --git a/lib/util/extend-trackmap-with-node-prefix.js b/lib/util/extend-trackmap-with-node-prefix.js index 4b9599c8..4ca10d19 100644 --- a/lib/util/extend-trackmap-with-node-prefix.js +++ b/lib/util/extend-trackmap-with-node-prefix.js @@ -4,8 +4,9 @@ const isBuiltinModule = require("is-builtin-module") /** * Extend trackMap.modules with `node:` prefixed modules - * @param {import('@eslint-community/eslint-utils').TraceMap} modules Like `{assert: foo}` - * @returns {import('@eslint-community/eslint-utils').TraceMap} Like `{assert: foo}, "node:assert": foo}` + * @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 extendTraceMapWithNodePrefix(modules) { const ret = { 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 }