eslint-plugin-import-next 2.3.3 → 2.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +80 -70
  3. package/package.json +15 -3
  4. package/src/index.d.ts +192 -0
  5. package/src/index.js +13 -1
  6. package/src/oxlint.d.ts +20 -0
  7. package/src/oxlint.js +21 -0
  8. package/src/rules/consistent-type-specifier-style.js +5 -1
  9. package/src/rules/default.js +1 -0
  10. package/src/rules/dynamic-import-chunkname.js +4 -1
  11. package/src/rules/enforce-dependency-direction.js +2 -1
  12. package/src/rules/enforce-import-order.js +4 -2
  13. package/src/rules/enforce-team-boundaries.js +27 -17
  14. package/src/rules/export.js +3 -0
  15. package/src/rules/exports-last.js +3 -0
  16. package/src/rules/extensions.js +1 -0
  17. package/src/rules/first.js +2 -0
  18. package/src/rules/group-exports.js +3 -0
  19. package/src/rules/max-dependencies.js +2 -1
  20. package/src/rules/named.js +1 -0
  21. package/src/rules/namespace.js +1 -0
  22. package/src/rules/newline-after-import.js +1 -0
  23. package/src/rules/no-absolute-path.js +3 -0
  24. package/src/rules/no-amd.js +2 -1
  25. package/src/rules/no-anonymous-default-export.js +2 -0
  26. package/src/rules/no-barrel-file.js +2 -1
  27. package/src/rules/no-barrel-import.js +1 -0
  28. package/src/rules/no-commonjs.js +2 -1
  29. package/src/rules/no-cross-domain-imports.js +2 -1
  30. package/src/rules/no-cycle.js +94 -73
  31. package/src/rules/no-default-export.js +2 -1
  32. package/src/rules/no-deprecated.js +2 -1
  33. package/src/rules/no-duplicates.js +1 -0
  34. package/src/rules/no-dynamic-require.js +3 -0
  35. package/src/rules/no-empty-named-blocks.js +4 -1
  36. package/src/rules/no-extraneous-dependencies.js +4 -1
  37. package/src/rules/no-full-package-import.js +1 -0
  38. package/src/rules/no-import-module-exports.js +3 -0
  39. package/src/rules/no-internal-modules.js +1 -0
  40. package/src/rules/no-legacy-imports.js +1 -0
  41. package/src/rules/no-mutable-exports.js +2 -1
  42. package/src/rules/no-named-as-default-member.js +3 -0
  43. package/src/rules/no-named-as-default.js +3 -0
  44. package/src/rules/no-named-default.js +3 -0
  45. package/src/rules/no-named-export.js +2 -1
  46. package/src/rules/no-namespace.js +12 -5
  47. package/src/rules/no-nodejs-modules.js +3 -1
  48. package/src/rules/no-relative-packages.js +4 -1
  49. package/src/rules/no-relative-parent-imports.js +1 -0
  50. package/src/rules/no-restricted-paths.js +1 -0
  51. package/src/rules/no-self-import.js +2 -1
  52. package/src/rules/no-unassigned-import.js +1 -0
  53. package/src/rules/no-unresolved.js +3 -2
  54. package/src/rules/no-unused-modules.js +1 -0
  55. package/src/rules/no-useless-path-segments.js +3 -0
  56. package/src/rules/prefer-default-export.js +2 -1
  57. package/src/rules/prefer-direct-import.js +1 -0
  58. package/src/rules/prefer-modern-api.js +1 -0
  59. package/src/rules/prefer-node-protocol.js +2 -0
  60. package/src/rules/prefer-tree-shakeable-imports.js +1 -0
  61. package/src/rules/require-import-approval.js +1 -0
  62. package/src/rules/unambiguous.js +3 -0
@@ -12,7 +12,10 @@ exports.dynamicImportChunkname = (0, eslint_devkit_1.createRule)({
12
12
  meta: {
13
13
  type: 'suggestion',
14
14
  docs: {
15
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/dynamic-import-chunkname.md',
15
16
  description: 'Enforce a leading comment with the webpackChunkName for dynamic imports',
17
+ cwe: 'CWE-1078',
18
+ cvss: 2.5,
16
19
  },
17
20
  hasSuggestions: true,
18
21
  messages: {
@@ -62,7 +65,7 @@ exports.dynamicImportChunkname = (0, eslint_devkit_1.createRule)({
62
65
  const options = context.options[0] || {};
63
66
  const { allowEmpty = false, webpackChunknameFormat = '[a-zA-Z0-9-_/.]+' } = options;
64
67
  const chunkNamePattern = new RegExp(`^${webpackChunknameFormat}$`);
65
- const sourceCode = context.getSourceCode();
68
+ const sourceCode = context.sourceCode;
66
69
  function checkChunkName(node) {
67
70
  // Get leading comments
68
71
  const comments = sourceCode.getCommentsBefore(node.source);
@@ -48,6 +48,7 @@ exports.enforceDependencyDirection = (0, eslint_devkit_2.createRule)({
48
48
  meta: {
49
49
  type: 'problem',
50
50
  docs: {
51
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/enforce-dependency-direction.md',
51
52
  description: 'Ensures dependencies flow in the correct architectural direction',
52
53
  },
53
54
  messages: {
@@ -129,7 +130,7 @@ exports.enforceDependencyDirection = (0, eslint_devkit_2.createRule)({
129
130
  },
130
131
  ],
131
132
  create(context, [options = {}]) {
132
- const filename = context.getFilename();
133
+ const filename = context.filename;
133
134
  const { layers: providedLayers, layerPatterns: providedLayerPatterns, allowSameLayer = true, } = options || {};
134
135
  // Use provided layers or defaults
135
136
  const layers = providedLayers || [
@@ -30,6 +30,7 @@ exports.enforceImportOrder = (0, eslint_devkit_1.createRule)({
30
30
  meta: {
31
31
  type: 'suggestion',
32
32
  docs: {
33
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/enforce-import-order.md',
33
34
  description: 'Enforces a specific order for import statements',
34
35
  },
35
36
  fixable: 'code',
@@ -121,7 +122,7 @@ exports.enforceImportOrder = (0, eslint_devkit_1.createRule)({
121
122
  defaultOptions.alphabetize.caseInsensitive,
122
123
  },
123
124
  };
124
- const sourceCode = context.getSourceCode();
125
+ const sourceCode = context.sourceCode;
125
126
  const imports = [];
126
127
  function getImportType(node) {
127
128
  const source = node.source.value;
@@ -237,7 +238,8 @@ exports.enforceImportOrder = (0, eslint_devkit_1.createRule)({
237
238
  }
238
239
  }
239
240
  const originalImports = [...imports];
240
- const sortedImports = [...imports].sort((a, b) => {
241
+ // oxlint-disable-next-line unicorn/no-array-sort
242
+ const sortedImports = Array.from(imports).sort((a, b) => {
241
243
  const typeA = getImportType(a);
242
244
  const typeB = getImportType(b);
243
245
  const rankA = getGroupRank(typeA);
@@ -7,18 +7,35 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.enforceTeamBoundaries = void 0;
9
9
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
+ /**
11
+ * Translate a user-supplied glob (`foo/**\/*.ts`) into a RegExp.
12
+ *
13
+ * Important: regex metacharacters in the input must be escaped BEFORE the
14
+ * glob tokens are translated. Without escaping, a pattern like `lib\foo` is
15
+ * interpreted as `\f` (form feed) and `lib.foo` matches `libXfoo` rather
16
+ * than the literal dot the user wrote. CodeQL flags the un-escaped backslash
17
+ * specifically (`js/incomplete-sanitization`).
18
+ *
19
+ * Algorithm: escape all regex metas (including `\`), then unescape and
20
+ * translate just the glob tokens `**` and `*` back to their regex form.
21
+ */
22
+ function globToRegex(pattern) {
23
+ // Escape regex metacharacters INCLUDING `*`, so the subsequent replaces
24
+ // can find the now-escaped `\*\*` / `\*` and translate them back to their
25
+ // glob meanings. Without escaping `*`, the `**` in `src/platform/**`
26
+ // reaches `new RegExp()` raw and throws "Nothing to repeat".
27
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\*]/g, '\\$&');
28
+ return new RegExp(escaped
29
+ .replace(/\\\*\\\*/g, '.*')
30
+ .replace(/\\\*/g, '[^/]*'));
31
+ }
10
32
  /**
11
33
  * Determine which team owns this file based on path patterns
12
34
  */
13
35
  function getFileTeam(filePath, teams) {
14
36
  for (const team of teams) {
15
37
  for (const pattern of team.paths) {
16
- // Support glob patterns (simplified)
17
- const regex = new RegExp(pattern
18
- .replace(/\*\*/g, '.*')
19
- .replace(/\*/g, '[^/]*')
20
- .replace(/\//g, '\\/'));
21
- if (regex.test(filePath)) {
38
+ if (globToRegex(pattern).test(filePath)) {
22
39
  return team;
23
40
  }
24
41
  }
@@ -31,11 +48,7 @@ function getFileTeam(filePath, teams) {
31
48
  function getImportTeam(importPath, teams) {
32
49
  for (const team of teams) {
33
50
  for (const pattern of team.paths) {
34
- const regex = new RegExp(pattern
35
- .replace(/\*\*/g, '.*')
36
- .replace(/\*/g, '[^/]*')
37
- .replace(/\//g, '\\/'));
38
- if (regex.test(importPath)) {
51
+ if (globToRegex(pattern).test(importPath)) {
39
52
  return team;
40
53
  }
41
54
  }
@@ -77,11 +90,7 @@ function isExternalImport(importPath) {
77
90
  */
78
91
  function isSharedPath(importPath, sharedPaths) {
79
92
  for (const shared of sharedPaths) {
80
- const regex = new RegExp(shared
81
- .replace(/\*\*/g, '.*')
82
- .replace(/\*/g, '[^/]*')
83
- .replace(/\//g, '\\/'));
84
- if (regex.test(importPath)) {
93
+ if (globToRegex(shared).test(importPath)) {
85
94
  return true;
86
95
  }
87
96
  }
@@ -92,6 +101,7 @@ exports.enforceTeamBoundaries = (0, eslint_devkit_1.createRule)({
92
101
  meta: {
93
102
  type: 'problem',
94
103
  docs: {
104
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/enforce-team-boundaries.md',
95
105
  description: 'Prevent unauthorized cross-team imports in large codebases',
96
106
  },
97
107
  messages: {
@@ -154,7 +164,7 @@ exports.enforceTeamBoundaries = (0, eslint_devkit_1.createRule)({
154
164
  create(context, [options]) {
155
165
  const { teams, sharedPaths = [], allowExternalPackages = true } = options;
156
166
  // Get the file's team ownership
157
- const filename = context.filename || context.getFilename();
167
+ const filename = context.filename;
158
168
  const sourceTeam = getFileTeam(filename, teams);
159
169
  // If file doesn't belong to any team, skip checking
160
170
  if (!sourceTeam) {
@@ -12,7 +12,10 @@ exports.exportRule = (0, eslint_devkit_1.createRule)({
12
12
  meta: {
13
13
  type: 'problem',
14
14
  docs: {
15
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/export.md',
15
16
  description: 'Forbid any invalid exports, i.e. re-export of the same name',
17
+ cwe: 'CWE-694',
18
+ cvss: 7.5,
16
19
  },
17
20
  messages: {
18
21
  duplicateExport: (0, eslint_devkit_1.formatLLMMessage)({
@@ -12,7 +12,10 @@ exports.exportsLast = (0, eslint_devkit_1.createRule)({
12
12
  meta: {
13
13
  type: 'suggestion',
14
14
  docs: {
15
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/exports-last.md',
15
16
  description: 'Ensure all exports appear after other statements',
17
+ cwe: 'CWE-1078',
18
+ cvss: 2.5,
16
19
  },
17
20
  messages: {
18
21
  exportNotLast: (0, eslint_devkit_1.formatLLMMessage)({
@@ -15,6 +15,7 @@ exports.extensions = (0, eslint_devkit_1.createRule)({
15
15
  meta: {
16
16
  type: 'suggestion',
17
17
  docs: {
18
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/extensions.md',
18
19
  description: 'Ensure consistent use of file extensions in imports',
19
20
  },
20
21
  fixable: 'code',
@@ -13,6 +13,7 @@ exports.first = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'suggestion',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/first.md',
16
17
  description: 'Ensure all imports appear before other statements',
17
18
  },
18
19
  fixable: 'code',
@@ -48,6 +49,7 @@ exports.first = (0, eslint_devkit_1.createRule)({
48
49
  }
49
50
  },
50
51
  };
52
+ // oxlint-disable-next-line consistent-function-scoping
51
53
  function isDirective(node) {
52
54
  return (node.type === 'ExpressionStatement' &&
53
55
  node.expression.type === 'Literal' &&
@@ -12,7 +12,10 @@ exports.groupExports = (0, eslint_devkit_1.createRule)({
12
12
  meta: {
13
13
  type: 'suggestion',
14
14
  docs: {
15
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/group-exports.md',
15
16
  description: 'Prefer named exports to be grouped together in a single export declaration',
17
+ cwe: 'CWE-1078',
18
+ cvss: 2.5,
16
19
  },
17
20
  messages: {
18
21
  groupExports: (0, eslint_devkit_1.formatLLMMessage)({
@@ -13,6 +13,7 @@ exports.maxDependencies = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'suggestion',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/max-dependencies.md',
16
17
  description: 'Enforce the maximum number of dependencies a module can have',
17
18
  },
18
19
  hasSuggestions: false,
@@ -87,7 +88,7 @@ exports.maxDependencies = (0, eslint_devkit_1.createRule)({
87
88
  create(context) {
88
89
  const [options] = context.options;
89
90
  const { max = 10, ignoreTypeImports = true, ignoreImports = [], ignoreFiles = [], } = options || {};
90
- const filename = context.getFilename();
91
+ const filename = context.filename;
91
92
  if (!filename) {
92
93
  return {};
93
94
  }
@@ -15,6 +15,7 @@ exports.named = (0, eslint_devkit_1.createRule)({
15
15
  meta: {
16
16
  type: 'problem',
17
17
  docs: {
18
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/named.md',
18
19
  description: 'Ensure named imports correspond to a named export in the remote file',
19
20
  },
20
21
  fixable: 'code',
@@ -15,6 +15,7 @@ exports.namespace = (0, eslint_devkit_1.createRule)({
15
15
  meta: {
16
16
  type: 'problem',
17
17
  docs: {
18
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/namespace.md',
18
19
  description: 'Ensure imported namespaces contain dereferenced properties as they are dereferenced',
19
20
  },
20
21
  messages: {
@@ -13,6 +13,7 @@ exports.newlineAfterImport = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'layout',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/newline-after-import.md',
16
17
  description: 'Enforce a newline after import statements',
17
18
  },
18
19
  fixable: 'whitespace',
@@ -14,7 +14,10 @@ exports.noAbsolutePath = (0, eslint_devkit_1.createRule)({
14
14
  meta: {
15
15
  type: 'suggestion',
16
16
  docs: {
17
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-absolute-path.md',
17
18
  description: 'Forbid import of modules using absolute paths',
19
+ cwe: 'CWE-426',
20
+ cvss: 5,
18
21
  },
19
22
  fixable: 'code',
20
23
  messages: {
@@ -13,6 +13,7 @@ exports.noAmd = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'problem',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-amd.md',
16
17
  description: 'Prevents AMD require/define calls',
17
18
  },
18
19
  hasSuggestions: false,
@@ -58,7 +59,7 @@ exports.noAmd = (0, eslint_devkit_1.createRule)({
58
59
  const { allow = [],
59
60
  // suggestES6 = true // Reserved for future use when hasSuggestions is enabled
60
61
  } = options || {};
61
- const filename = context.getFilename();
62
+ const filename = context.filename;
62
63
  function shouldAllow() {
63
64
  // Check if the call is in an allowed pattern
64
65
  return allow.some((pattern) => {
@@ -13,6 +13,7 @@ exports.noAnonymousDefaultExport = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'suggestion',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-anonymous-default-export.md',
16
17
  description: 'Forbid anonymous values as default exports',
17
18
  },
18
19
  messages: {
@@ -114,6 +115,7 @@ exports.noAnonymousDefaultExport = (0, eslint_devkit_1.createRule)({
114
115
  return true;
115
116
  }
116
117
  }
118
+ // oxlint-disable-next-line consistent-function-scoping
117
119
  function getSuggestionText(node) {
118
120
  const declaration = node.declaration;
119
121
  switch (declaration.type) {
@@ -74,6 +74,7 @@ exports.noBarrelFile = (0, eslint_devkit_1.createRule)({
74
74
  meta: {
75
75
  type: 'problem',
76
76
  docs: {
77
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-barrel-file.md',
77
78
  description: 'Disallow barrel files that harm build performance and tree-shaking efficiency',
78
79
  },
79
80
  hasSuggestions: true,
@@ -148,7 +149,7 @@ exports.noBarrelFile = (0, eslint_devkit_1.createRule)({
148
149
  ],
149
150
  create(context, [options = {}]) {
150
151
  const { threshold = 3, allowedPaths = [], barrelPatterns = DEFAULT_BARREL_PATTERNS, allowWithLocalExports = false, reexportRatio = 0.8, } = options || {};
151
- const filename = context.filename || context.getFilename();
152
+ const filename = context.filename;
152
153
  // Check if file matches barrel patterns
153
154
  if (!matchesPattern(filename, barrelPatterns)) {
154
155
  return {};
@@ -45,6 +45,7 @@ exports.noBarrelImport = (0, eslint_devkit_1.createRule)({
45
45
  meta: {
46
46
  type: 'suggestion',
47
47
  docs: {
48
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-barrel-import.md',
48
49
  description: 'Disallow imports from barrel files to improve build performance and tree-shaking',
49
50
  },
50
51
  fixable: 'code',
@@ -13,6 +13,7 @@ exports.noCommonjs = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'problem',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-commonjs.md',
16
17
  description: 'Prevents CommonJS require/module.exports',
17
18
  },
18
19
  hasSuggestions: true,
@@ -98,7 +99,7 @@ exports.noCommonjs = (0, eslint_devkit_1.createRule)({
98
99
  create(context) {
99
100
  const [options] = context.options;
100
101
  const { allowRequire = false, allowModuleExports = false, allowExports = false, allowInFiles = [], suggestES6 = true, } = options || {};
101
- const filename = context.getFilename();
102
+ const filename = context.filename;
102
103
  function shouldAllow() {
103
104
  if (!filename) {
104
105
  return false;
@@ -130,6 +130,7 @@ exports.noCrossDomainImports = (0, eslint_devkit_2.createRule)({
130
130
  meta: {
131
131
  type: 'problem',
132
132
  docs: {
133
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-cross-domain-imports.md',
133
134
  description: 'Prevents imports across domain/feature boundaries',
134
135
  },
135
136
  messages: {
@@ -221,7 +222,7 @@ exports.noCrossDomainImports = (0, eslint_devkit_2.createRule)({
221
222
  },
222
223
  ],
223
224
  create(context, [options = {}]) {
224
- const filename = context.getFilename();
225
+ const filename = context.filename;
225
226
  /**
226
227
  * Check import declarations
227
228
  */
@@ -57,7 +57,11 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
57
57
  meta: {
58
58
  type: 'problem',
59
59
  docs: {
60
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-cycle.md',
60
61
  description: 'Detect circular dependencies that cause bundle memory bloat and initialization issues',
62
+ cwe: 'CWE-407',
63
+ cvss: 9.5,
64
+ confidence: 'high',
61
65
  },
62
66
  messages: {
63
67
  // 🎯 Token optimization: 45% reduction (~70→38 tokens per message) for architecture clarity
@@ -104,8 +108,8 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
104
108
  properties: {
105
109
  maxDepth: {
106
110
  type: 'number',
107
- default: 5,
108
- description: 'Maximum depth to traverse when detecting cycles (performance optimization)',
111
+ default: Number.MAX_SAFE_INTEGER,
112
+ description: 'Maximum depth to traverse when detecting cycles. Default: unlimited (matches eslint-plugin-import and oxlint). Lower values are a performance escape hatch — but with our nonCyclicFiles cache, traversal cost is amortized, and a low cap silently misses cycles deeper than the limit. Set to a finite number only on huge graphs where the bench latency hurts you.',
109
113
  },
110
114
  ignorePatterns: {
111
115
  type: 'array',
@@ -168,7 +172,12 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
168
172
  },
169
173
  defaultOptions: [
170
174
  {
171
- maxDepth: 10,
175
+ // Unlimited: matches eslint-plugin-import (Infinity) and oxlint
176
+ // (u32::MAX). The earlier default of 10 silently missed any cycle
177
+ // deeper than 10 hops — verified on next.js's webpack-config.ts
178
+ // (~12 hops). The nonCyclicFiles cache + the depth-limit-truncation
179
+ // fix in eslint-devkit's dependency-analysis make unlimited safe.
180
+ maxDepth: Number.MAX_SAFE_INTEGER,
172
181
  ignorePatterns: [],
173
182
  barrelExports: ['index.ts', 'index.tsx', 'index.js', 'index.jsx'],
174
183
  reportAllCycles: true,
@@ -180,7 +189,7 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
180
189
  ],
181
190
  create(context) {
182
191
  const options = context.options[0] || {};
183
- const maxDepth = options.maxDepth ?? 10;
192
+ const maxDepth = options.maxDepth ?? Number.MAX_SAFE_INTEGER;
184
193
  const ignorePatterns = options.ignorePatterns ?? [
185
194
  '**/*.test.ts',
186
195
  '**/*.test.tsx',
@@ -201,13 +210,15 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
201
210
  const moduleNamingConvention = options.moduleNamingConvention ?? 'semantic';
202
211
  const coreModuleSuffix = options.coreModuleSuffix ?? 'core';
203
212
  const extendedModuleSuffix = options.extendedModuleSuffix ?? 'extended';
204
- const filename = context.getFilename();
205
- const workspaceRoot = context.getCwd();
213
+ const filename = context.filename;
214
+ const workspaceRoot = context.cwd;
206
215
  // Get resolver settings from ESLint settings (compatible with eslint-plugin-import)
207
- // eslint-plugin-import uses settings['import/resolver']
216
+ // eslint-plugin-import uses settings['import/resolver']; allow either the
217
+ // upstream key or our drop-in `import-next/resolver` form for users
218
+ // migrating off eslint-plugin-import.
208
219
  const settings = context.settings;
209
220
  const resolverSettings = settings?.['import/resolver'] ||
210
- settings?.['eslint-plugin-llm-optimized/resolver'];
221
+ settings?.['import-next/resolver'];
211
222
  // Skip ignored files early
212
223
  if ((0, eslint_devkit_3.shouldIgnoreFile)(filename, ignorePatterns, sharedCache.compiledPatterns)) {
213
224
  return {};
@@ -257,9 +268,9 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
257
268
  * Generate DIRECT IMPORT message
258
269
  */
259
270
  function generateDirectImportMessage(cycle, sourceImport) {
260
- const currentFile = (0, eslint_devkit_3.getBasename)(context.getFilename());
271
+ const currentFile = (0, eslint_devkit_3.getBasename)(context.filename);
261
272
  const targetFile = cycle[1];
262
- const relativeImport = (0, eslint_devkit_3.getRelativeImportPath)(context.getFilename(), targetFile);
273
+ const relativeImport = (0, eslint_devkit_3.getRelativeImportPath)(context.filename, targetFile);
263
274
  return {
264
275
  cycle: (0, eslint_devkit_3.formatCycleDisplay)(cycle, workspaceRoot),
265
276
  currentFile,
@@ -333,83 +344,93 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
333
344
  };
334
345
  }
335
346
  }
336
- // Store found cycles to report on specific imports
337
- let detectedCycles = [];
338
347
  return {
339
- Program() {
340
- // Clear per-file state
341
- detectedCycles = [];
342
- // Find ALL circular dependencies starting from current file
343
- const cycles = (0, eslint_devkit_4.findAllCircularDependencies)(filename, {
344
- maxDepth,
345
- reportAllCycles,
348
+ ImportDeclaration(node) {
349
+ const importSource = node.source.value;
350
+ // Resolve the import target to an absolute path
351
+ const resolved = (0, eslint_devkit_4.resolveImportPath)(importSource, {
352
+ fromFile: filename,
346
353
  workspaceRoot,
347
354
  barrelExports,
348
355
  cache: sharedCache,
349
356
  resolverSettings,
350
357
  });
351
- if (cycles.length > 0) {
352
- for (const cycle of cycles) {
353
- // Extract the minimal cycle (the actual loop)
354
- const minimalCycle = (0, eslint_devkit_4.getMinimalCycle)(cycle);
355
- // Only report if current file is part of the minimal cycle
356
- // (not just a file that imports into the cycle)
357
- if (!minimalCycle.includes(filename)) {
358
- continue;
359
- }
360
- const cycleStart = minimalCycle.indexOf(filename);
361
- if (cycleStart === -1)
362
- continue;
363
- const relevantCycle = [
364
- ...minimalCycle.slice(cycleStart),
365
- ...minimalCycle.slice(0, cycleStart),
366
- ];
367
- const cycleHash = (0, eslint_devkit_4.getCycleHash)(relevantCycle);
368
- // Skip if we've already reported this cycle (shared across all files)
369
- if (sharedCache.reportedCycles.has(cycleHash)) {
370
- continue;
371
- }
372
- sharedCache.reportedCycles.add(cycleHash);
373
- // Select the appropriate fix strategy
374
- const strategy = selectFixStrategy(relevantCycle, fixStrategy);
375
- // Find the target of the first import that causes the cycle
376
- const cycleTarget = relevantCycle[1] || relevantCycle[0]; // The next file in the cycle
377
- detectedCycles.push({
378
- cycleTarget,
379
- strategy,
380
- relevantCycle,
381
- cycleHash,
382
- });
383
- }
358
+ if (!resolved)
359
+ return;
360
+ // =====================================================================
361
+ // PER-IMPORT CYCLE DETECTION (Phase 3 optimization)
362
+ //
363
+ // Instead of computing all cycles upfront in Program() via full-graph
364
+ // BFS + Tarjan SCC (which reads ALL ~5K reachable files), we check
365
+ // each import individually with a targeted DFS.
366
+ //
367
+ // Performance advantage:
368
+ // - Only reads files along the actual DFS path (not all reachable)
369
+ // - nonCyclicFiles cache provides O(1) rejection after first visit
370
+ // - getFileImports cache reuses parsed results across files
371
+ // =====================================================================
372
+ // Fast path: if the target file is known to be non-cyclic, skip
373
+ if (sharedCache.nonCyclicFiles.has(resolved)) {
374
+ return;
384
375
  }
385
- },
386
- ImportDeclaration(node) {
387
- // Check if this import is part of any detected circular dependency
388
- const importSource = node.source.value;
389
- const resolved = (0, eslint_devkit_4.resolveImportPath)(importSource, {
390
- fromFile: filename,
376
+ // Check if following this import leads back to the current file
377
+ const cycles = (0, eslint_devkit_4.detectCycleFromImport)(filename, resolved, {
378
+ maxDepth,
379
+ reportAllCycles,
391
380
  workspaceRoot,
392
381
  barrelExports,
393
382
  cache: sharedCache,
394
383
  resolverSettings,
395
384
  });
396
- if (!resolved)
385
+ if (cycles.length === 0)
397
386
  return;
398
- for (const cycle of detectedCycles) {
399
- if (resolved === cycle.cycleTarget) {
400
- // This is an import that causes a circular dependency
401
- const sourceImport = {
402
- path: resolved,
403
- source: importSource,
404
- };
405
- // Generate the appropriate message based on strategy
406
- const { messageId, data } = generateMessageData(cycle.relevantCycle, cycle.strategy, sourceImport);
407
- context.report({
408
- node,
409
- messageId,
410
- data,
411
- });
387
+ for (const cycle of cycles) {
388
+ // Extract the minimal cycle (the actual loop)
389
+ const minimalCycle = (0, eslint_devkit_4.getMinimalCycle)(cycle);
390
+ // Only report if current file is part of the minimal cycle
391
+ if (!minimalCycle.includes(filename)) {
392
+ continue;
412
393
  }
394
+ const cycleStart = minimalCycle.indexOf(filename);
395
+ if (cycleStart === -1)
396
+ continue;
397
+ const relevantCycle = [
398
+ ...minimalCycle.slice(cycleStart),
399
+ ...minimalCycle.slice(0, cycleStart),
400
+ ];
401
+ // Dedupe by (file, cycle) instead of cycle alone. Without the
402
+ // file in the key, the cycle [A, B] reports only on whichever
403
+ // of A or B is linted first; the other gets silently skipped.
404
+ // Per-file keying matches oxlint's behavior — every file in a
405
+ // cycle gets its own diagnostic. The user can navigate from any
406
+ // entry point in the cycle and see the report there.
407
+ const cycleHash = `${filename}::${(0, eslint_devkit_4.getCycleHash)(relevantCycle)}`;
408
+ // Skip only if THIS file already reported THIS cycle (e.g. the
409
+ // same import statement appearing twice in the same file).
410
+ // (Earlier we experimented with a `pendingCycleReports` fan-out
411
+ // that emits on every cycle member regardless of which one's
412
+ // DFS discovered it — closes the with-router.tsx presentational
413
+ // gap but caused a 10× slowdown and 23× finding-count explosion
414
+ // because each cycle member iterates every other member of every
415
+ // cycle it touches. The cycle IS still reported on its other
416
+ // end; the one-finding loss isn't worth the perf hit.)
417
+ if (sharedCache.reportedCycles.has(cycleHash)) {
418
+ continue;
419
+ }
420
+ sharedCache.reportedCycles.add(cycleHash);
421
+ // Select the appropriate fix strategy
422
+ const strategy = selectFixStrategy(relevantCycle, fixStrategy);
423
+ // Generate the appropriate message based on strategy
424
+ const sourceImport = {
425
+ path: resolved,
426
+ source: importSource,
427
+ };
428
+ const { messageId, data } = generateMessageData(relevantCycle, strategy, sourceImport);
429
+ context.report({
430
+ node,
431
+ messageId,
432
+ data,
433
+ });
413
434
  }
414
435
  },
415
436
  };
@@ -13,6 +13,7 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'problem',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-default-export.md',
16
17
  description: 'Prevents default exports',
17
18
  },
18
19
  fixable: 'code',
@@ -80,7 +81,7 @@ exports.noDefaultExport = (0, eslint_devkit_1.createRule)({
80
81
  create(context) {
81
82
  const [options] = context.options;
82
83
  const { allowInFiles = [], allowPatterns = [], suggestNamed = true, } = options || {};
83
- const filename = context.getFilename();
84
+ const filename = context.filename;
84
85
  function shouldAllow() {
85
86
  if (!filename) {
86
87
  return false;
@@ -13,6 +13,7 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
13
13
  meta: {
14
14
  type: 'problem',
15
15
  docs: {
16
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-import-next/docs/rules/no-deprecated.md',
16
17
  description: 'Forbid imported names marked with @deprecated documentation tag',
17
18
  },
18
19
  hasSuggestions: true,
@@ -83,7 +84,7 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
83
84
  create(context) {
84
85
  const [options] = context.options;
85
86
  const { allow = [], deprecationMarkers = ['@deprecated'], checkJSDoc = true, checkDecorators = true, } = options || {};
86
- const filename = context.getFilename();
87
+ const filename = context.filename;
87
88
  if (!filename) {
88
89
  return {};
89
90
  }