diff --git a/.github/release-please/manifest.json b/.github/release-please/manifest.json index 8448bf45..3b4eedf3 100644 --- a/.github/release-please/manifest.json +++ b/.github/release-please/manifest.json @@ -1 +1 @@ -{".":"17.15.1"} +{".":"17.16.0"} diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f40897..72b145d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [17.16.0](https://github.com/eslint-community/eslint-plugin-n/compare/v17.15.1...v17.16.0) (2025-02-23) + + +### ๐ŸŒŸ Features + +* add support for ignoring sync methods from certain locations ([#392](https://github.com/eslint-community/eslint-plugin-n/issues/392)) ([5544f20](https://github.com/eslint-community/eslint-plugin-n/commit/5544f20f113e59d6789a249dc24df73fdc354fa1)) + + +### ๐Ÿฉน Fixes + +* False-positive `no-extraneous-import` when using the `tsconfig > paths` alias import ([#408](https://github.com/eslint-community/eslint-plugin-n/issues/408)) ([f486492](https://github.com/eslint-community/eslint-plugin-n/commit/f48649239392f647fe8426f66fc9b4ea6a9ba45e)) + + +### ๐Ÿงน Chores + +* eslint v8 compat ([#397](https://github.com/eslint-community/eslint-plugin-n/issues/397)) ([86a5242](https://github.com/eslint-community/eslint-plugin-n/commit/86a524250dcc7c32225f2880ec66767a06c6258d)) +* improve `prefer-node-protocol`'s performance ([#406](https://github.com/eslint-community/eslint-plugin-n/issues/406)) ([4efe60f](https://github.com/eslint-community/eslint-plugin-n/commit/4efe60f37c71ce3d71932714207bac780332cf3d)) + ## [17.15.1](https://github.com/eslint-community/eslint-plugin-n/compare/v17.15.0...v17.15.1) (2024-12-20) diff --git a/docs/rules/no-sync.md b/docs/rules/no-sync.md index dc400974..38dea638 100644 --- a/docs/rules/no-sync.md +++ b/docs/rules/no-sync.md @@ -61,6 +61,7 @@ fs.readFileSync(somePath).toString(); #### ignores You can `ignore` specific function names using this option. +Additionally, if you are using TypeScript you can optionally specify where the function is declared. Examples of **incorrect** code for this rule with the `{ ignores: ['readFileSync'] }` option: @@ -78,6 +79,62 @@ Examples of **correct** code for this rule with the `{ ignores: ['readFileSync'] fs.readFileSync(somePath); ``` +##### Advanced (TypeScript only) + +You can provide a list of specifiers to ignore. Specifiers are typed as follows: + +```ts +type Specifier = +| string +| { + from: "file"; + path?: string; + name?: string[]; + } +| { + from: "package"; + package?: string; + name?: string[]; + } +| { + from: "lib"; + name?: string[]; + } +``` + +###### From a file + +Examples of **correct** code for this rule with the ignore file specifier: + +```js +/*eslint n/no-sync: ["error", { ignores: [{ from: 'file', path: './foo.ts' }]}] */ + +import { fooSync } from "./foo" +fooSync() +``` + +###### From a package + +Examples of **correct** code for this rule with the ignore package specifier: + +```js +/*eslint n/no-sync: ["error", { ignores: [{ from: 'package', package: 'effect' }]}] */ + +import { Effect } from "effect" +const value = Effect.runSync(Effect.succeed(42)) +``` + +###### From the TypeScript library + +Examples of **correct** code for this rule with the ignore lib specifier: + +```js +/*eslint n/no-sync: ["error", { ignores: [{ from: 'lib' }]}] */ + +const stylesheet = new CSSStyleSheet() +stylesheet.replaceSync("body { font-size: 1.4em; } p { color: red; }") +``` + ## ๐Ÿ”Ž Implementation - [Rule source](../../lib/rules/no-sync.js) diff --git a/lib/rules/callback-return.js b/lib/rules/callback-return.js index 5ae1c65a..2c9e3819 100644 --- a/lib/rules/callback-return.js +++ b/lib/rules/callback-return.js @@ -3,6 +3,7 @@ * See LICENSE file in root directory for full license. */ "use strict" +const { getSourceCode } = require("../util/eslint-compat") /** @type {import('eslint').Rule.RuleModule} */ module.exports = { @@ -27,7 +28,7 @@ module.exports = { create(context) { const callbacks = context.options[0] || ["callback", "cb", "next"] - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 + const sourceCode = getSourceCode(context) /** * Find the closest parent matching a list of types. diff --git a/lib/rules/exports-style.js b/lib/rules/exports-style.js index 49809db9..5a71b573 100644 --- a/lib/rules/exports-style.js +++ b/lib/rules/exports-style.js @@ -5,6 +5,7 @@ "use strict" const { hasParentNode } = require("../util/has-parent-node.js") +const { getSourceCode, getScope } = require("../util/eslint-compat") /*istanbul ignore next */ /** @@ -302,7 +303,7 @@ module.exports = { const batchAssignAllowed = Boolean( context.options[1] != null && context.options[1].allowBatchAssign ) - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 + const sourceCode = getSourceCode(context) /** * Gets the location info of reports. @@ -426,8 +427,8 @@ module.exports = { } return { - "Program:exit"(node) { - const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 + "Program:exit"() { + const scope = getScope(context) switch (mode) { case "module.exports": diff --git a/lib/rules/global-require.js b/lib/rules/global-require.js index 988ddac7..ed4a7758 100644 --- a/lib/rules/global-require.js +++ b/lib/rules/global-require.js @@ -4,6 +4,8 @@ */ "use strict" +const { getScope, getAncestors } = require("../util/eslint-compat") + const ACCEPTABLE_PARENTS = [ "AssignmentExpression", "VariableDeclarator", @@ -59,26 +61,18 @@ module.exports = { }, create(context) { - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 - return { CallExpression(node) { - const currentScope = - sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 + const currentScope = getScope(context, node) if ( node.callee.type === "Identifier" && node.callee.name === "require" && !isShadowed(currentScope, node.callee) ) { - const isGoodRequire = ( - sourceCode.getAncestors?.(node) ?? - context.getAncestors() - ) // TODO: remove context.getAncestors() when dropping support for ESLint < v9 - .every( - parent => - ACCEPTABLE_PARENTS.indexOf(parent.type) > -1 - ) + const isGoodRequire = getAncestors(context, node).every( + parent => ACCEPTABLE_PARENTS.indexOf(parent.type) > -1 + ) if (!isGoodRequire) { context.report({ node, messageId: "unexpected" }) diff --git a/lib/rules/handle-callback-err.js b/lib/rules/handle-callback-err.js index 313659ed..6f7127a5 100644 --- a/lib/rules/handle-callback-err.js +++ b/lib/rules/handle-callback-err.js @@ -4,6 +4,8 @@ */ "use strict" +const { getScope } = require("../util/eslint-compat") + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -25,7 +27,6 @@ module.exports = { }, create(context) { - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 const errorArgument = context.options[0] || "err" /** @@ -71,7 +72,7 @@ module.exports = { * @returns {void} */ function checkForError(node) { - const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 + const scope = getScope(context, node) const parameters = getParameters(scope) const firstParameter = parameters[0] diff --git a/lib/rules/hashbang.js b/lib/rules/hashbang.js index 3b9a0fa1..786287dc 100644 --- a/lib/rules/hashbang.js +++ b/lib/rules/hashbang.js @@ -11,6 +11,7 @@ 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 { getSourceCode } = require("../util/eslint-compat") const ENV_SHEBANG = "#!/usr/bin/env" const NODE_SHEBANG = `${ENV_SHEBANG} {{executableName}}\n` @@ -119,7 +120,7 @@ module.exports = { }, }, create(context) { - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 + const sourceCode = getSourceCode(context) const filePath = context.filename ?? context.getFilename() if (filePath === "") { return {} diff --git a/lib/rules/no-deprecated-api.js b/lib/rules/no-deprecated-api.js index 53a5d78e..a875c8d2 100644 --- a/lib/rules/no-deprecated-api.js +++ b/lib/rules/no-deprecated-api.js @@ -15,6 +15,7 @@ const getConfiguredNodeVersion = require("../util/get-configured-node-version") const getSemverRange = require("../util/get-semver-range") const extendTrackmapWithNodePrefix = require("../util/extend-trackmap-with-node-prefix") const unprefixNodeColon = require("../util/unprefix-node-colon") +const { getScope } = require("../util/eslint-compat") /** @typedef {import('../unsupported-features/types.js').DeprecatedInfo} DeprecatedInfo */ /** @@ -820,10 +821,9 @@ module.exports = { }) } - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 return { - "Program:exit"(node) { - const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 + "Program:exit"() { + const scope = getScope(context) const tracker = new ReferenceTracker(scope, { mode: "legacy", diff --git a/lib/rules/no-exports-assign.js b/lib/rules/no-exports-assign.js index fcfa9453..6811f704 100644 --- a/lib/rules/no-exports-assign.js +++ b/lib/rules/no-exports-assign.js @@ -5,6 +5,7 @@ "use strict" const { findVariable } = require("@eslint-community/eslint-utils") +const { getScope } = require("../util/eslint-compat") /** * @param {import('estree').Node} node @@ -61,11 +62,9 @@ module.exports = { type: "problem", }, create(context) { - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 - return { AssignmentExpression(node) { - const scope = sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 + const scope = getScope(context) if ( !isExports(node.left, scope) || diff --git a/lib/rules/no-path-concat.js b/lib/rules/no-path-concat.js index da90aa68..0fb08561 100644 --- a/lib/rules/no-path-concat.js +++ b/lib/rules/no-path-concat.js @@ -11,6 +11,7 @@ const { getStringIfConstant, } = require("@eslint-community/eslint-utils") const { hasParentNode } = require("../util/has-parent-node.js") +const { getSourceCode } = require("../util/eslint-compat") /** * Get the first char of the specified template element. @@ -195,7 +196,7 @@ module.exports = { create(context) { return { "Program:exit"(programNode) { - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 + const sourceCode = getSourceCode(context) const globalScope = sourceCode.getScope?.(programNode) ?? context.getScope() const tracker = new ReferenceTracker(globalScope) diff --git a/lib/rules/no-sync.js b/lib/rules/no-sync.js index fb0de8e6..6e9529c8 100644 --- a/lib/rules/no-sync.js +++ b/lib/rules/no-sync.js @@ -4,6 +4,14 @@ */ "use strict" +const typeMatchesSpecifier = + /** @type {import('ts-declaration-location').default} */ ( + /** @type {unknown} */ (require("ts-declaration-location")) + ) +const getTypeOfNode = require("../util/get-type-of-node") +const getParserServices = require("../util/get-parser-services") +const getFullTypeName = require("../util/get-full-type-name") + const selectors = [ // fs.readFileSync() // readFileSync.call(null, 'path') @@ -32,7 +40,56 @@ module.exports = { }, ignores: { type: "array", - items: { type: "string" }, + items: { + oneOf: [ + { type: "string" }, + { + type: "object", + properties: { + from: { const: "file" }, + path: { + type: "string", + }, + name: { + type: "array", + items: { + type: "string", + }, + }, + }, + additionalProperties: false, + }, + { + type: "object", + properties: { + from: { const: "lib" }, + name: { + type: "array", + items: { + type: "string", + }, + }, + }, + additionalProperties: false, + }, + { + type: "object", + properties: { + from: { const: "package" }, + package: { + type: "string", + }, + name: { + type: "array", + items: { + type: "string", + }, + }, + }, + additionalProperties: false, + }, + ], + }, default: [], }, }, @@ -57,15 +114,64 @@ module.exports = { * @returns {void} */ [selector.join(",")](node) { - if (ignores.includes(node.name)) { - return + const parserServices = getParserServices(context) + + /** + * @type {import('typescript').Type | undefined | null} + */ + let type = undefined + + /** + * @type {string | undefined | null} + */ + let fullName = undefined + + for (const ignore of ignores) { + if (typeof ignore === "string") { + if (ignore === node.name) { + return + } + + continue + } + + if ( + parserServices === null || + parserServices.program === null + ) { + throw new Error( + 'TypeScript parser services not available. Rule "n/no-sync" is configured to use "ignores" option with a non-string value. This requires TypeScript parser services to be available.' + ) + } + + type = + type === undefined + ? getTypeOfNode(node, parserServices) + : type + + fullName = + fullName === undefined + ? getFullTypeName(type) + : fullName + + if ( + typeMatchesSpecifier( + parserServices.program, + ignore, + type + ) && + (ignore.name === undefined || + ignore.name.includes(fullName ?? node.name)) + ) { + return + } } context.report({ node: node.parent, messageId: "noSync", data: { - propertyName: node.name, + propertyName: fullName ?? node.name, }, }) }, diff --git a/lib/rules/no-unsupported-features/es-syntax.js b/lib/rules/no-unsupported-features/es-syntax.js index b5987cfb..bc994e1e 100644 --- a/lib/rules/no-unsupported-features/es-syntax.js +++ b/lib/rules/no-unsupported-features/es-syntax.js @@ -10,6 +10,7 @@ const rangeSubset = require("semver/ranges/subset") const getConfiguredNodeVersion = require("../../util/get-configured-node-version") const getSemverRange = require("../../util/get-semver-range") const mergeVisitorsInPlace = require("../../util/merge-visitors-in-place") +const { getScope } = require("../../util/eslint-compat") /** @type {Record} */ const features = require("./es-syntax.json") @@ -113,8 +114,7 @@ function normalizeScope(initialScope, 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 + const scope = getScope(context) return normalizeScope(scope, node).isStrict } diff --git a/lib/rules/prefer-node-protocol.js b/lib/rules/prefer-node-protocol.js index fa1a8c73..d8da92d8 100644 --- a/lib/rules/prefer-node-protocol.js +++ b/lib/rules/prefer-node-protocol.js @@ -4,12 +4,13 @@ */ "use strict" +const { getStringIfConstant } = require("@eslint-community/eslint-utils") + const { Range } = require("semver") const getConfiguredNodeVersion = require("../util/get-configured-node-version") -const visitImport = require("../util/visit-import") -const visitRequire = require("../util/visit-require") -const mergeVisitorsInPlace = require("../util/merge-visitors-in-place") +const stripImportPathParams = require("../util/strip-import-path-params") + const { NodeBuiltinModules, } = require("../unsupported-features/node-builtins.js") @@ -27,6 +28,104 @@ const messageId = "preferNodeProtocol" const supportedRangeForEsm = new Range("^12.20.0 || >= 14.13.1") const supportedRangeForCjs = new Range("^14.18.0 || >= 16.0.0") +/** + * @param {import('estree').Node} [node] + * @returns {node is import('estree').Literal} + */ +function isStringLiteral(node) { + return node?.type === "Literal" && typeof node.type === "string" +} + +/** + * @param {import('eslint').Rule.RuleContext} context + * @param {import('../util/import-target.js').ModuleStyle} moduleStyle + * @returns {boolean} + */ +function isEnablingThisRule(context, moduleStyle) { + const version = getConfiguredNodeVersion(context) + + // Only check Node.js version because this rule is meaningless if configured Node.js version doesn't match semver range. + if (!version.intersects(supportedRangeForEsm)) { + return false + } + + // Only check when using `require` + if ( + moduleStyle === "require" && + !version.intersects(supportedRangeForCjs) + ) { + return false + } + + return true +} + +/** + * @param {import('estree').Node} node + * @returns {boolean} + **/ +function isValidRequireArgument(node) { + const rawName = getStringIfConstant(node) + if (typeof rawName !== "string") { + return false + } + + const name = stripImportPathParams(rawName) + if (!isBuiltin(name)) { + return false + } + + return true +} + +/** + * @param {import('estree').Node | null | undefined} node + * @param {import('eslint').Rule.RuleContext} context + * @param {import('../util/import-target.js').ModuleStyle} moduleStyle + */ +function validate(node, context, moduleStyle) { + if (node == null) { + return + } + + if (!isEnablingThisRule(context, moduleStyle)) { + return + } + + if (!isStringLiteral(node)) { + return + } + + if (moduleStyle === "require" && !isValidRequireArgument(node)) { + return + } + + if ( + !("value" in node) || + typeof node.value !== "string" || + node.value.startsWith("node:") || + !isBuiltin(node.value) || + !isBuiltin(`node:${node.value}`) + ) { + return + } + + context.report({ + node, + messageId, + data: { + moduleName: node.value, + }, + fix(fixer) { + const firstCharacterIndex = (node?.range?.[0] ?? 0) + 1 + return fixer.replaceTextRange( + [firstCharacterIndex, firstCharacterIndex], + "node:" + ) + }, + }) +} + /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { @@ -52,139 +151,36 @@ 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 - } - - if (node.optional) { - return false - } - - if (node.arguments.length !== argumentsLength) { - return false - } - - if ( - node.callee.type !== "Identifier" || - node.callee.name !== name - ) { - return false - } - - 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]) - ) - } - - /** - * @param {import('eslint').Rule.RuleContext} context - * @param {import('../util/import-target.js').ModuleStyle} moduleStyle - * @returns {boolean} - */ - function isEnablingThisRule(context, moduleStyle) { - const version = getConfiguredNodeVersion(context) - - // Only check Node.js version because this rule is meaningless if configured Node.js version doesn't match semver range. - if (!version.intersects(supportedRangeForEsm)) { - return false - } - - // Only check when using `require` - if ( - moduleStyle === "require" && - !version.intersects(supportedRangeForCjs) - ) { - return false - } - - return true - } + return { + CallExpression(node) { + if (node.type !== "CallExpression") { + return + } + + if ( + node.optional || + node.arguments.length !== 1 || + node.callee.type !== "Identifier" || + node.callee.name !== "require" + ) { + return + } + + return validate(node.arguments[0], context, "require") + }, - /** @type {import('../util/import-target.js')[]} */ - const targets = [] - return [ - visitImport(context, { includeCore: true }, importTargets => { - targets.push(...importTargets) - }), - visitRequire(context, { includeCore: true }, requireTargets => { - targets.push( - ...requireTargets.filter(target => - isStaticRequire(target.node.parent) - ) - ) - }), - { - "Program:exit"() { - for (const { node, moduleStyle } of targets) { - if (!isEnablingThisRule(context, moduleStyle)) { - continue - } - - if (node.type === "TemplateLiteral") { - continue - } - - if ( - !("value" in node) || - typeof node.value !== "string" || - node.value.startsWith("node:") || - !isBuiltin(node.value) || - !isBuiltin(`node:${node.value}`) - ) { - continue - } - - context.report({ - node, - messageId, - data: { - moduleName: node.value, - }, - fix(fixer) { - const firstCharacterIndex = - (node?.range?.[0] ?? 0) + 1 - return fixer.replaceTextRange( - [firstCharacterIndex, firstCharacterIndex], - "node:" - ) - }, - }) - } - }, + ExportAllDeclaration(node) { + return validate(node.source, context, "import") }, - ].reduce( - (mergedVisitor, thisVisitor) => - mergeVisitorsInPlace(mergedVisitor, thisVisitor), - {} - ) + ExportNamedDeclaration(node) { + return validate(node.source, context, "import") + }, + ImportDeclaration(node) { + return validate(node.source, context, "import") + }, + ImportExpression(node) { + return validate(node.source, context, "import") + }, + } }, } diff --git a/lib/rules/prefer-promises/dns.js b/lib/rules/prefer-promises/dns.js index 9b151720..abdf937b 100644 --- a/lib/rules/prefer-promises/dns.js +++ b/lib/rules/prefer-promises/dns.js @@ -9,6 +9,7 @@ const { CONSTRUCT, ReferenceTracker, } = require("@eslint-community/eslint-utils") +const { getScope } = require("../../util/eslint-compat") /** @type {import('@eslint-community/eslint-utils').TraceMap} */ const dns = { @@ -57,9 +58,8 @@ module.exports = { create(context) { return { - "Program:exit"(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 + "Program:exit"() { + const scope = getScope(context) const tracker = new ReferenceTracker(scope, { mode: "legacy" }) const references = [ ...tracker.iterateCjsReferences(traceMap), diff --git a/lib/rules/prefer-promises/fs.js b/lib/rules/prefer-promises/fs.js index 0a005470..e9e135b1 100644 --- a/lib/rules/prefer-promises/fs.js +++ b/lib/rules/prefer-promises/fs.js @@ -5,6 +5,7 @@ "use strict" const { CALL, ReferenceTracker } = require("@eslint-community/eslint-utils") +const { getScope } = require("../../util/eslint-compat") /** @type {import('@eslint-community/eslint-utils').TraceMap} */ const traceMap = { @@ -55,9 +56,8 @@ module.exports = { create(context) { return { - "Program:exit"(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 + "Program:exit"() { + const scope = getScope(context) const tracker = new ReferenceTracker(scope, { mode: "legacy" }) const references = [ ...tracker.iterateCjsReferences(traceMap), diff --git a/lib/util/check-prefer-global.js b/lib/util/check-prefer-global.js index ca685de8..fa058469 100644 --- a/lib/util/check-prefer-global.js +++ b/lib/util/check-prefer-global.js @@ -5,7 +5,7 @@ "use strict" const { ReferenceTracker } = require("@eslint-community/eslint-utils") - +const { getScope } = require("../util/eslint-compat") /** * @typedef TraceMap * @property {import('@eslint-community/eslint-utils').TraceMap} globals @@ -36,9 +36,7 @@ class Verifier { */ verifyToPreferGlobals() { 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 scope = getScope(context) const tracker = new ReferenceTracker(scope, { mode: "legacy", }) @@ -57,9 +55,7 @@ class Verifier { */ verifyToPreferModules() { 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 scope = getScope(context) const tracker = new ReferenceTracker(scope) for (const { node } of tracker.iterateGlobalReferences( diff --git a/lib/util/check-unsupported-builtins.js b/lib/util/check-unsupported-builtins.js index 0412fd03..8096edb5 100644 --- a/lib/util/check-unsupported-builtins.js +++ b/lib/util/check-unsupported-builtins.js @@ -10,6 +10,7 @@ const getConfiguredNodeVersion = require("./get-configured-node-version") const getSemverRange = require("./get-semver-range") const unprefixNodeColon = require("./unprefix-node-colon") const semverRangeSubset = require("semver/ranges/subset") +const { getScope } = require("../util/eslint-compat") /** * Parses the options. @@ -86,8 +87,7 @@ module.exports.checkUnsupportedBuiltins = function checkUnsupportedBuiltins( 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 scope = getScope(context) const tracker = new ReferenceTracker(scope, { mode: "legacy" }) const references = [ ...tracker.iterateCjsReferences(traceMap.modules ?? {}), diff --git a/lib/util/eslint-compat.js b/lib/util/eslint-compat.js new file mode 100644 index 00000000..cbba37a4 --- /dev/null +++ b/lib/util/eslint-compat.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Utilities for eslint compatibility. + * @see https://eslint.org/docs/latest/use/migrate-to-9.0.0#removed-context-methods + * @author aladdin-add + */ +"use strict" + +/** @import { Rule } from 'eslint' */ +/** @typedef {import('estree').Node} Node */ + +exports.getSourceCode = function (/** @type Rule.RuleContext */ context) { + return context.sourceCode || context.getSourceCode() +} + +exports.getScope = function ( + /** @type {Rule.RuleContext} */ context, + /** @type {Node} */ node +) { + const sourceCode = exports.getSourceCode(context) + return sourceCode.getScope?.(node || sourceCode.ast) || context.getScope() +} + +exports.getAncestors = function ( + /** @type {Rule.RuleContext} */ context, + /** @type {Node} */ node +) { + const sourceCode = exports.getSourceCode(context) + return sourceCode.getAncestors?.(node) || context.getAncestors() +} + +exports.getCwd = function (/** @type {Rule.RuleContext} */ context) { + return context.cwd || context.getCwd() +} + +exports.getPhysicalFilename = function ( + /** @type {Rule.RuleContext} */ context +) { + return context.physicalFilename || context.getPhysicalFilename?.() +} + +exports.getFilename = function (/** @type {Rule.RuleContext} */ context) { + return context.filename || context.getFilename?.() +} diff --git a/lib/util/get-full-type-name.js b/lib/util/get-full-type-name.js new file mode 100644 index 00000000..9e5ee7bf --- /dev/null +++ b/lib/util/get-full-type-name.js @@ -0,0 +1,47 @@ +"use strict" + +const ts = (() => { + try { + // eslint-disable-next-line n/no-unpublished-require + return require("typescript") + } catch { + return null + } +})() + +/** + * @param {import('typescript').Type | null} type + * @returns {string | null} + */ +module.exports = function getFullTypeName(type) { + if (ts === null || type === null) { + return null + } + + /** + * @type {string[]} + */ + let nameParts = [] + let currentSymbol = type.getSymbol() + while (currentSymbol !== undefined) { + if ( + currentSymbol.valueDeclaration?.kind === ts.SyntaxKind.SourceFile || + currentSymbol.valueDeclaration?.kind === + ts.SyntaxKind.ModuleDeclaration + ) { + break + } + + nameParts.unshift(currentSymbol.getName()) + currentSymbol = + /** @type {import('typescript').Symbol & {parent: import('typescript').Symbol | undefined}} */ ( + currentSymbol + ).parent + } + + if (nameParts.length === 0) { + return null + } + + return nameParts.join(".") +} diff --git a/lib/util/get-parser-services.js b/lib/util/get-parser-services.js new file mode 100644 index 00000000..1d0be646 --- /dev/null +++ b/lib/util/get-parser-services.js @@ -0,0 +1,24 @@ +"use strict" + +const { + getParserServices: getParserServicesFromTsEslint, +} = require("@typescript-eslint/utils/eslint-utils") + +/** + * Get the TypeScript parser services. + * If TypeScript isn't present, returns `null`. + * + * @param {import('eslint').Rule.RuleContext} context - rule context + * @returns {import('@typescript-eslint/parser').ParserServices | null} + */ +module.exports = function getParserServices(context) { + // Not using tseslint parser? + if ( + context.sourceCode.parserServices?.esTreeNodeToTSNodeMap == null || + context.sourceCode.parserServices.tsNodeToESTreeNodeMap == null + ) { + return null + } + + return getParserServicesFromTsEslint(/** @type {any} */ (context), true) +} diff --git a/lib/util/get-tsconfig.js b/lib/util/get-tsconfig.js index 8bb6b5f2..802470e6 100644 --- a/lib/util/get-tsconfig.js +++ b/lib/util/get-tsconfig.js @@ -1,6 +1,7 @@ "use strict" const { getTsconfig, parseTsconfig } = require("get-tsconfig") +const { getPhysicalFilename, getFilename } = require("./eslint-compat") const fsCache = new Map() /** @@ -30,12 +31,7 @@ function getTSConfigForFile(filename) { * @returns {import("get-tsconfig").TsConfigResult | null} */ function getTSConfigForContext(context) { - // TODO: remove context.get(PhysicalFilename|Filename) when dropping eslint < v10 - const filename = - context.physicalFilename ?? - context.getPhysicalFilename?.() ?? - context.filename ?? - context.getFilename?.() + const filename = getPhysicalFilename(context) ?? getFilename(context) return getTSConfigForFile(filename) } diff --git a/lib/util/get-type-of-node.js b/lib/util/get-type-of-node.js new file mode 100644 index 00000000..ca5050f0 --- /dev/null +++ b/lib/util/get-type-of-node.js @@ -0,0 +1,21 @@ +"use strict" + +/** + * Get the type of a node. + * If TypeScript isn't present, returns `null`. + * + * @param {import('estree').Node} node - A node + * @param {import('@typescript-eslint/parser').ParserServices} parserServices - A parserServices + * @returns {import('typescript').Type | null} + */ +module.exports = function getTypeOfNode(node, parserServices) { + const { esTreeNodeToTSNodeMap, program } = parserServices + if (program === null) { + return null + } + const tsNode = esTreeNodeToTSNodeMap.get(/** @type {any} */ (node)) + const checker = program.getTypeChecker() + const nodeType = checker.getTypeAtLocation(tsNode) + const constrained = checker.getBaseConstraintOfType(nodeType) + return constrained ?? nodeType +} diff --git a/lib/util/import-target.js b/lib/util/import-target.js index bf89b693..c12a0e6b 100644 --- a/lib/util/import-target.js +++ b/lib/util/import-target.js @@ -176,6 +176,20 @@ module.exports = class ImportTarget { return "node" } + // This check should be done before the RegExp that checks npm packages, + // because `tsconfig.json` aliases may start with `~`, `@` and this will + // cause imports using aliases to be treated as npm-module imports + // https://github.com/eslint-community/eslint-plugin-n/issues/379 + if (isTypescript(this.context)) { + const aliases = getTSConfigAliases(this.context) + if ( + Array.isArray(aliases) && + aliases.some(alias => this.name.startsWith(alias.name)) + ) { + return "relative" + } + } + if (/^(@[\w~-][\w.~-]*\/)?[\w~-][\w.~-]*/.test(this.name)) { return "npm" } diff --git a/lib/util/is-typescript.js b/lib/util/is-typescript.js index 80163066..3c7065fe 100644 --- a/lib/util/is-typescript.js +++ b/lib/util/is-typescript.js @@ -1,6 +1,7 @@ "use strict" const path = require("path") +const { getPhysicalFilename, getFilename } = require("./eslint-compat") const typescriptExtensions = [".ts", ".tsx", ".cts", ".mts"] @@ -12,10 +13,7 @@ const typescriptExtensions = [".ts", ".tsx", ".cts", ".mts"] */ module.exports = function isTypescript(context) { const sourceFileExt = path.extname( - context.physicalFilename ?? - context.getPhysicalFilename?.() ?? - context.filename ?? - context.getFilename?.() + getPhysicalFilename(context) ?? getFilename(context) ) return typescriptExtensions.includes(sourceFileExt) } diff --git a/lib/util/visit-import.js b/lib/util/visit-import.js index aee0a194..3b76072f 100644 --- a/lib/util/visit-import.js +++ b/lib/util/visit-import.js @@ -11,7 +11,7 @@ const getResolverConfig = require("./get-resolver-config") const getTryExtensions = require("./get-try-extensions") const ImportTarget = require("./import-target") const stripImportPathParams = require("./strip-import-path-params") - +const { getFilename } = require("./eslint-compat") /** @typedef {import('@typescript-eslint/typescript-estree').TSESTree.ImportDeclaration} ImportDeclaration */ /** @@ -38,9 +38,7 @@ module.exports = function visitImport( ) { /** @type {import('./import-target.js')[]} */ const targets = [] - const basedir = path.dirname( - path.resolve(context.filename ?? context.getFilename()) - ) + const basedir = path.dirname(path.resolve(getFilename(context))) const paths = getResolvePaths(context, optionIndex) const resolverConfig = getResolverConfig(context, optionIndex) const extensions = getTryExtensions(context, optionIndex) diff --git a/lib/util/visit-require.js b/lib/util/visit-require.js index ba4026f4..e5711885 100644 --- a/lib/util/visit-require.js +++ b/lib/util/visit-require.js @@ -16,6 +16,7 @@ const getResolverConfig = require("./get-resolver-config") const getTryExtensions = require("./get-try-extensions") const ImportTarget = require("./import-target") const stripImportPathParams = require("./strip-import-path-params") +const { getScope, getFilename } = require("../util/eslint-compat") /** * @typedef VisitRequireOptions @@ -39,20 +40,15 @@ module.exports = function visitRequire( ) { /** @type {import('./import-target.js')[]} */ const targets = [] - const basedir = path.dirname( - path.resolve(context.filename ?? context.getFilename()) - ) + const basedir = path.dirname(path.resolve(getFilename(context))) const paths = getResolvePaths(context) const resolverConfig = getResolverConfig(context) const extensions = getTryExtensions(context) const options = { basedir, paths, extensions, resolverConfig } return { - "Program:exit"(node) { - const sourceCode = context.sourceCode ?? context.getSourceCode() // TODO: just use context.sourceCode when dropping eslint < v9 - const tracker = new ReferenceTracker( - sourceCode.getScope?.(node) ?? context.getScope() //TODO: remove context.getScope() when dropping support for ESLint < v9 - ) + "Program:exit"() { + const tracker = new ReferenceTracker(getScope(context)) const references = tracker.iterateGlobalReferences({ require: { [CALL]: true, diff --git a/package.json b/package.json index 6d9ecfbf..ac96e186 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-n", - "version": "17.15.1", + "version": "17.16.0", "description": "Additional ESLint's rules for Node.js", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17,21 +17,23 @@ }, "dependencies": { "@eslint-community/eslint-utils": "^4.4.1", + "@typescript-eslint/utils": "^8.21.0", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", "globals": "^15.11.0", "ignore": "^5.3.2", "minimatch": "^9.0.5", - "semver": "^7.6.3" + "semver": "^7.6.3", + "ts-declaration-location": "^1.0.5" }, "devDependencies": { "@eslint/js": "^9.14.0", "@types/eslint": "^9.6.1", "@types/estree": "^1.0.6", "@types/node": "^20.17.5", - "@typescript-eslint/parser": "^8.12.2", - "@typescript-eslint/typescript-estree": "^8.12.2", + "@typescript-eslint/parser": "^8.21.0", + "@typescript-eslint/typescript-estree": "^8.21.0", "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", "eslint-doc-generator": "^1.7.1", @@ -51,7 +53,7 @@ "rimraf": "^5.0.10", "ts-ignore-import": "^4.0.1", "type-fest": "^4.26.1", - "typescript": "~5.6" + "typescript": "^5.7" }, "scripts": { "build": "node scripts/update", diff --git a/tests/fixtures/no-extraneous/tsconfig-paths/index.ts b/tests/fixtures/no-extraneous/tsconfig-paths/index.ts new file mode 100644 index 00000000..182a8503 --- /dev/null +++ b/tests/fixtures/no-extraneous/tsconfig-paths/index.ts @@ -0,0 +1 @@ +// File needs to exists diff --git a/tests/fixtures/no-extraneous/tsconfig-paths/src/configurations/foo.ts b/tests/fixtures/no-extraneous/tsconfig-paths/src/configurations/foo.ts new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/tests/fixtures/no-extraneous/tsconfig-paths/src/configurations/foo.ts @@ -0,0 +1 @@ +export default {}; diff --git a/tests/fixtures/no-extraneous/tsconfig-paths/tsconfig.json b/tests/fixtures/no-extraneous/tsconfig-paths/tsconfig.json new file mode 100644 index 00000000..355777b5 --- /dev/null +++ b/tests/fixtures/no-extraneous/tsconfig-paths/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "paths": { + "~configurations/*": ["./src/configurations/*"], + "@configurations/*": ["./src/configurations/*"], + "#configurations/*": ["./src/configurations/*"] + } + } +} diff --git a/tests/fixtures/no-sync/base/file.ts b/tests/fixtures/no-sync/base/file.ts new file mode 100644 index 00000000..182a8503 --- /dev/null +++ b/tests/fixtures/no-sync/base/file.ts @@ -0,0 +1 @@ +// File needs to exists diff --git a/tests/fixtures/no-sync/base/tsconfig.json b/tests/fixtures/no-sync/base/tsconfig.json new file mode 100644 index 00000000..5b217cb9 --- /dev/null +++ b/tests/fixtures/no-sync/base/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true + }, + "include": ["**/*"] +} diff --git a/tests/fixtures/no-sync/file.ts b/tests/fixtures/no-sync/file.ts new file mode 100644 index 00000000..182a8503 --- /dev/null +++ b/tests/fixtures/no-sync/file.ts @@ -0,0 +1 @@ +// File needs to exists diff --git a/tests/fixtures/no-sync/ignore-package/file.ts b/tests/fixtures/no-sync/ignore-package/file.ts new file mode 100644 index 00000000..182a8503 --- /dev/null +++ b/tests/fixtures/no-sync/ignore-package/file.ts @@ -0,0 +1 @@ +// File needs to exists diff --git a/tests/fixtures/no-sync/ignore-package/node_modules/aaa/index.d.ts b/tests/fixtures/no-sync/ignore-package/node_modules/aaa/index.d.ts new file mode 100644 index 00000000..38f18aef --- /dev/null +++ b/tests/fixtures/no-sync/ignore-package/node_modules/aaa/index.d.ts @@ -0,0 +1 @@ +export function fooSync(): void; diff --git a/tests/fixtures/no-sync/ignore-package/node_modules/aaa/index.js b/tests/fixtures/no-sync/ignore-package/node_modules/aaa/index.js new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/no-sync/ignore-package/node_modules/aaa/package.json b/tests/fixtures/no-sync/ignore-package/node_modules/aaa/package.json new file mode 100644 index 00000000..39437d0a --- /dev/null +++ b/tests/fixtures/no-sync/ignore-package/node_modules/aaa/package.json @@ -0,0 +1,6 @@ +{ + "name": "aaa", + "version": "0.0.0", + "main": "./index.js", + "types": "./index.d.ts" +} diff --git a/tests/fixtures/no-sync/ignore-package/package.json b/tests/fixtures/no-sync/ignore-package/package.json new file mode 100644 index 00000000..5c6c2690 --- /dev/null +++ b/tests/fixtures/no-sync/ignore-package/package.json @@ -0,0 +1,7 @@ +{ + "name": "test", + "version": "0.0.0", + "dependencies": { + "aaa": "0.0.0" + } +} diff --git a/tests/fixtures/no-sync/ignore-package/tsconfig.json b/tests/fixtures/no-sync/ignore-package/tsconfig.json new file mode 100644 index 00000000..5b217cb9 --- /dev/null +++ b/tests/fixtures/no-sync/ignore-package/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true + }, + "include": ["**/*"] +} diff --git a/tests/fixtures/no-sync/tsconfig.json b/tests/fixtures/no-sync/tsconfig.json new file mode 100644 index 00000000..5b217cb9 --- /dev/null +++ b/tests/fixtures/no-sync/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true + }, + "include": ["**/*"] +} diff --git a/tests/lib/rules/no-extraneous-import.js b/tests/lib/rules/no-extraneous-import.js index 1b180e90..54ac2303 100644 --- a/tests/lib/rules/no-extraneous-import.js +++ b/tests/lib/rules/no-extraneous-import.js @@ -33,6 +33,11 @@ function fixture(name) { const ruleTester = new RuleTester({ languageOptions: { sourceType: "module" }, + settings: { + n: { + tryExtensions: [".ts"], + }, + }, }) ruleTester.run("no-extraneous-import", rule, { valid: [ @@ -78,6 +83,21 @@ ruleTester.run("no-extraneous-import", rule, { filename: fixture("dependencies/a.js"), code: "import ccc from 'ccc'", }, + + // imports using `tsconfig.json > compilerOptions > paths` setting + // https://github.com/eslint-community/eslint-plugin-n/issues/379 + { + filename: fixture("tsconfig-paths/index.ts"), + code: "import foo from '@configurations/foo'", + }, + { + filename: fixture("tsconfig-paths/index.ts"), + code: "import foo from '~configurations/foo'", + }, + { + filename: fixture("tsconfig-paths/index.ts"), + code: "import foo from '#configurations/foo'", + }, ], invalid: [ { diff --git a/tests/lib/rules/no-sync.js b/tests/lib/rules/no-sync.js index 0c7141ee..9fb8c33d 100644 --- a/tests/lib/rules/no-sync.js +++ b/tests/lib/rules/no-sync.js @@ -4,7 +4,7 @@ */ "use strict" -const RuleTester = require("#test-helpers").RuleTester +const { RuleTester, TsRuleTester } = require("#test-helpers") const rule = require("../../../lib/rules/no-sync") new RuleTester().run("no-sync", rule, { @@ -149,3 +149,173 @@ new RuleTester().run("no-sync", rule, { }, ], }) + +new (TsRuleTester("no-sync/base").run)("no-sync", rule, { + valid: [ + { + code: ` +declare function fooSync(): void; +fooSync(); +`, + options: [ + { + ignores: [ + { + from: "file", + }, + ], + }, + ], + }, + { + code: ` +declare function fooSync(): void; +fooSync(); +`, + options: [ + { + ignores: [ + { + from: "file", + name: ["fooSync"], + }, + ], + }, + ], + }, + { + code: ` +const stylesheet = new CSSStyleSheet(); +stylesheet.replaceSync("body { font-size: 1.4em; } p { color: red; }"); +`, + options: [ + { + ignores: [ + { + from: "lib", + name: ["CSSStyleSheet.replaceSync"], + }, + ], + }, + ], + }, + ], + invalid: [ + { + code: ` +declare function fooSync(): void; +fooSync(); +`, + options: [ + { + ignores: [ + { + from: "file", + path: "**/bar.ts", + }, + ], + }, + ], + errors: [ + { + messageId: "noSync", + data: { propertyName: "fooSync" }, + type: "CallExpression", + }, + ], + }, + { + code: ` +declare function fooSync(): void; +fooSync(); +`, + options: [ + { + ignores: [ + { + from: "file", + name: ["barSync"], + }, + ], + }, + ], + errors: [ + { + messageId: "noSync", + data: { propertyName: "fooSync" }, + type: "CallExpression", + }, + ], + }, + { + code: ` +const stylesheet = new CSSStyleSheet(); +stylesheet.replaceSync("body { font-size: 1.4em; } p { color: red; }"); +`, + options: [ + { + ignores: [ + { + from: "file", + name: ["CSSStyleSheet.replaceSync"], + }, + ], + }, + ], + errors: [ + { + messageId: "noSync", + data: { propertyName: "CSSStyleSheet.replaceSync" }, + type: "MemberExpression", + }, + ], + }, + ], +}) + +new (TsRuleTester("no-sync/ignore-package").run)("no-sync", rule, { + valid: [ + { + code: ` +import { fooSync } from "aaa"; +fooSync(); +`, + options: [ + { + ignores: [ + { + from: "package", + package: "aaa", + name: ["fooSync"], + }, + ], + }, + ], + }, + ], + invalid: [ + { + code: ` +import { fooSync } from "aaa"; +fooSync(); +`, + options: [ + { + ignores: [ + { + from: "file", + name: ["fooSync"], + }, + ], + }, + ], + errors: [ + { + messageId: "noSync", + data: { propertyName: "fooSync" }, + type: "CallExpression", + }, + ], + }, + ], +}) diff --git a/tests/test-helpers.js b/tests/test-helpers.js index bff30126..69ce7abd 100644 --- a/tests/test-helpers.js +++ b/tests/test-helpers.js @@ -3,12 +3,15 @@ * @author ๅ”ฏ็„ถ */ "use strict" + +const path = require("path") const eslintVersion = require("eslint/package.json").version const { RuleTester } = require("eslint") const { FlatRuleTester } = require("eslint/use-at-your-own-risk") const globals = require("globals") const semverSatisfies = require("semver/functions/satisfies") const os = require("os") +const typescriptParser = require("@typescript-eslint/parser") // greater than or equal to ESLint v9 exports.gteEslintV9 = semverSatisfies(eslintVersion, ">=9", { @@ -33,6 +36,26 @@ const defaultConfig = { globals: { ...globals.es2015, ...globals.node }, }, } + +/** + * @param {string} fixturePath - Path to the fixture directory relative to the fixtures directory + * @returns + */ +function getTsConfig(fixturePath) { + return { + languageOptions: { + parser: typescriptParser, + parserOptions: { + tsconfigRootDir: path.join(__dirname, "fixtures", fixturePath), + projectService: { + // Ensure we're not using the default project + maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING: 0, + }, + }, + }, + } +} + exports.RuleTester = function (config = defaultConfig) { if (config.languageOptions.env?.node === false) config.languageOptions.globals = config.languageOptions.globals || {} @@ -55,6 +78,31 @@ exports.RuleTester = function (config = defaultConfig) { return ruleTester } +/** + * @param {string | import('eslint').Linter.Config} configOrFixturePath + * @returns + */ +exports.TsRuleTester = function (configOrFixturePath) { + const config = + typeof configOrFixturePath === "object" + ? configOrFixturePath + : getTsConfig(configOrFixturePath) + + const ruleTester = exports.RuleTester.call(this, config) + const $run = ruleTester.run.bind(ruleTester) + ruleTester.run = function (name, rule, tests) { + tests.valid = tests.valid.map(setTsFilename) + tests.invalid = tests.invalid.map(setTsFilename) + + $run(name, rule, tests) + } + return ruleTester +} +Object.setPrototypeOf( + exports.TsRuleTester.prototype, + exports.RuleTester.prototype +) + // support skip in tests function shouldRun(item) { if (typeof item === "string") return true @@ -63,3 +111,15 @@ function shouldRun(item) { delete item.skip return skip === void 0 || skip === false } + +function setTsFilename(item) { + if (typeof item === "string") { + return { + code: item, + filename: "file.ts", + } + } + + item.filename ??= "file.ts" + return item +}