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
+}