eslint-plugin-import-next 2.3.7 → 2.3.8

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [2.3.6] - 2026-05-03
2
2
 
3
+ ## 2.3.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [#252](https://github.com/ofri-peretz/eslint/pull/252) [`d67e395`](https://github.com/ofri-peretz/eslint/commit/d67e3953c2748ad36e6aebe0f24b1d04e518b4d0) Thanks [@ofri-peretz](https://github.com/ofri-peretz)! - Fix Codecov badge showing "unknown" — switch from flag to component URL format
8
+
3
9
  ## 2.3.7
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  <a href="https://www.npmjs.com/package/eslint-plugin-import-next" target="_blank"><img src="https://img.shields.io/npm/v/eslint-plugin-import-next.svg" alt="NPM Version" /></a>
11
11
  <a href="https://www.npmjs.com/package/eslint-plugin-import-next" target="_blank"><img src="https://img.shields.io/npm/dm/eslint-plugin-import-next.svg" alt="NPM Downloads" /></a>
12
12
  <a href="https://opensource.org/licenses/MIT" target="_blank"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="Package License" /></a>
13
- <a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=import-next" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=import-next" alt="Codecov" /></a>
13
+ <a href="https://app.codecov.io/gh/ofri-peretz/eslint/components?components%5B0%5D=eslint-plugin-import-next" target="_blank"><img src="https://codecov.io/gh/ofri-peretz/eslint/graph/badge.svg?component=eslint-plugin-import-next" alt="Codecov" /></a>
14
14
  <a href="https://github.com/ofri-peretz/eslint" target="_blank"><img src="https://img.shields.io/badge/Since-Dec_2025-blue?logo=rocket&logoColor=white" alt="Since Dec 2025" /></a>
15
15
  </p>
16
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-import-next",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
4
4
  "description": "Drop-in replacement for eslint-plugin-import. 100x faster no-cycle detection, AI-optimized fixes, zero config migration.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -146,7 +146,7 @@ exports.rules = {
146
146
  exports.plugin = {
147
147
  meta: {
148
148
  name: 'eslint-plugin-import-next',
149
- version: '2.3.7',
149
+ version: '2.3.8',
150
150
  },
151
151
  rules: exports.rules,
152
152
  };
@@ -187,32 +187,27 @@ exports.enforceDependencyDirection = (0, eslint_devkit_2.createRule)({
187
187
  }
188
188
  // For relative imports, check the import path string directly for layer patterns
189
189
  // This works for test cases where paths like '../infrastructure/repository' contain the layer
190
+ // The early return above guarantees importPath starts with '.' here.
190
191
  let targetLayer = null;
191
- if (importPath.startsWith('.')) {
192
- // Check if import path contains layer patterns
193
- // e.g., '../infrastructure/repository' -> extract 'infrastructure'
194
- const segments = importPath.split(/[/\\]/);
195
- for (const segment of segments) {
196
- if (layerPatterns.includes(segment.toLowerCase())) {
197
- targetLayer = segment.toLowerCase();
198
- break;
199
- }
192
+ // Check if import path contains layer patterns
193
+ // e.g., '../infrastructure/repository' -> extract 'infrastructure'
194
+ const segments = importPath.split(/[/\\]/);
195
+ for (const segment of segments) {
196
+ if (layerPatterns.includes(segment.toLowerCase())) {
197
+ targetLayer = segment.toLowerCase();
198
+ break;
200
199
  }
201
- // Fallback: try regex matching
202
- if (!targetLayer) {
203
- for (const pattern of layerPatterns) {
204
- const regex = new RegExp(`[/\\\\]${pattern}[/\\\\]`, 'i');
205
- if (regex.test(importPath)) {
206
- targetLayer = pattern;
207
- break;
208
- }
200
+ }
201
+ // Fallback: try regex matching
202
+ if (!targetLayer) {
203
+ for (const pattern of layerPatterns) {
204
+ const regex = new RegExp(`[/\\\\]${pattern}[/\\\\]`, 'i');
205
+ if (regex.test(importPath)) {
206
+ targetLayer = pattern;
207
+ break;
209
208
  }
210
209
  }
211
210
  }
212
- else {
213
- // For absolute or external imports, try to extract layer
214
- targetLayer = extractLayer(importPath, layerPatterns);
215
- }
216
211
  const targetOrder = getLayerOrder(targetLayer, layers);
217
212
  // Skip if layers are unknown
218
213
  if (sourceOrder < 0 || targetOrder < 0) {
@@ -229,8 +224,10 @@ exports.enforceDependencyDirection = (0, eslint_devkit_2.createRule)({
229
224
  node,
230
225
  messageId: 'dependencyDirectionViolation',
231
226
  data: {
232
- sourceLayer: sourceLayer || 'unknown',
233
- targetLayer: targetLayer || 'unknown',
227
+ // Both are guaranteed non-null here: sourceOrder/targetOrder >= 0
228
+ // implies the layers were resolved (see the guard above).
229
+ sourceLayer: sourceLayer,
230
+ targetLayer: targetLayer,
234
231
  },
235
232
  suggest: [
236
233
  {
@@ -170,23 +170,18 @@ exports.enforceImportOrder = (0, eslint_devkit_1.createRule)({
170
170
  return 'external';
171
171
  }
172
172
  function getGroupRank(importType) {
173
- const index = options.groups?.indexOf(importType) ?? -1;
173
+ const index = options.groups.indexOf(importType);
174
174
  return index !== -1 ? index : 999;
175
175
  }
176
176
  function getExtendedRange(node) {
177
177
  let start = node.range[0];
178
178
  let end = node.range[1];
179
179
  const commentsBefore = sourceCode.getCommentsBefore(node);
180
- // Check if comments belong to this import (e.g. on previous lines, not detached)
180
+ // getCommentsBefore only returns comments strictly between the previous
181
+ // non-comment token and this node, so they always belong to this import —
182
+ // include them all in the range.
181
183
  if (commentsBefore.length > 0) {
182
- const lastComment = commentsBefore[commentsBefore.length - 1];
183
- const tokenBefore = sourceCode.getTokenBefore(node);
184
- // If comment is between previous token and this node
185
- if (!tokenBefore || tokenBefore.range[1] <= lastComment.range[0]) {
186
- // Simple heuristic: include all preceding comments that are contiguous with the import
187
- // This is tricky, but we'll take all commentsBefore for now to be safe
188
- start = commentsBefore[0].range[0];
189
- }
184
+ start = commentsBefore[0].range[0];
190
185
  }
191
186
  // Include semicolon if present
192
187
  const tokenAfter = sourceCode.getTokenAfter(node);
@@ -170,10 +170,10 @@ exports.enforceTeamBoundaries = (0, eslint_devkit_1.createRule)({
170
170
  if (!sourceTeam) {
171
171
  return {};
172
172
  }
173
+ // Narrowed binding: the early return above guarantees sourceTeam is non-null,
174
+ // and closures cannot rely on that narrowing without re-checking.
175
+ const owningTeam = sourceTeam;
173
176
  function checkImport(node) {
174
- // Guard for TypeScript - sourceTeam is checked before this function is registered
175
- if (!sourceTeam)
176
- return;
177
177
  let importPath = null;
178
178
  if (node.type === eslint_devkit_1.AST_NODE_TYPES.ImportDeclaration) {
179
179
  importPath = typeof node.source.value === 'string' ? node.source.value : null;
@@ -207,11 +207,11 @@ exports.enforceTeamBoundaries = (0, eslint_devkit_1.createRule)({
207
207
  return;
208
208
  }
209
209
  // Same team - always allowed
210
- if (targetTeam.team === sourceTeam.team) {
210
+ if (targetTeam.team === owningTeam.team) {
211
211
  return;
212
212
  }
213
213
  // Check if target team is in allowed dependencies
214
- const allowedDeps = sourceTeam.allowedDependencies || [];
214
+ const allowedDeps = owningTeam.allowedDependencies || [];
215
215
  if (allowedDeps.includes(targetTeam.team)) {
216
216
  return;
217
217
  }
@@ -220,7 +220,7 @@ exports.enforceTeamBoundaries = (0, eslint_devkit_1.createRule)({
220
220
  node,
221
221
  messageId: 'unauthorizedTeamImport',
222
222
  data: {
223
- sourceTeam: sourceTeam.team,
223
+ sourceTeam: owningTeam.team,
224
224
  targetTeam: targetTeam.team,
225
225
  importPath,
226
226
  },
@@ -66,12 +66,10 @@ exports.named = (0, eslint_devkit_1.createRule)({
66
66
  const moduleSymbol = checker?.getSymbolAtLocation?.(moduleNode);
67
67
  if (!moduleSymbol)
68
68
  return;
69
- if (moduleSymbol) {
70
- context.report({
71
- node: node.imported,
72
- messageId: 'named',
73
- });
74
- }
69
+ context.report({
70
+ node: node.imported,
71
+ messageId: 'named',
72
+ });
75
73
  }
76
74
  },
77
75
  };
@@ -63,7 +63,6 @@ exports.noAmd = (0, eslint_devkit_1.createRule)({
63
63
  function shouldAllow() {
64
64
  // Check if the call is in an allowed pattern
65
65
  return allow.some((pattern) => {
66
- /* v8 ignore next -- defensive check, filename always provided by ESLint */
67
66
  if (!filename)
68
67
  return false;
69
68
  // Support glob patterns like **/legacy/**
@@ -27,6 +27,14 @@ export interface Options {
27
27
  }>;
28
28
  }
29
29
  type RuleOptions = [Options?];
30
+ /**
31
+ * Check if import violates domain boundaries
32
+ */
33
+ export declare function violatesDomainBoundary(sourceFile: string, importPath: string, options: Options): {
34
+ violates: boolean;
35
+ sourceDomain: string | null;
36
+ targetDomain: string | null;
37
+ };
30
38
  export declare const noCrossDomainImports: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
31
39
  name: string;
32
40
  };
@@ -6,6 +6,7 @@
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.noCrossDomainImports = void 0;
9
+ exports.violatesDomainBoundary = violatesDomainBoundary;
9
10
  const eslint_devkit_1 = require("@interlace/eslint-devkit");
10
11
  const eslint_devkit_2 = require("@interlace/eslint-devkit");
11
12
  const eslint_devkit_3 = require("@interlace/eslint-devkit");
@@ -51,8 +52,9 @@ function violatesDomainBoundary(sourceFile, importPath, options) {
51
52
  const sourceDomain = extractDomain(sourceFile, domainPatterns);
52
53
  // For relative imports, we need to resolve the path to get the actual target file
53
54
  // In tests, we can extract from the import path string directly
55
+ // The early return above guarantees importPath starts with '.' here.
54
56
  let targetDomain = null;
55
- if (importPath.startsWith('.')) {
57
+ {
56
58
  // Check if import path contains domain patterns
57
59
  // e.g., '../../domains/order/service' -> extract 'order'
58
60
  // Try multiple patterns to find the domain
@@ -94,10 +96,6 @@ function violatesDomainBoundary(sourceFile, importPath, options) {
94
96
  }
95
97
  }
96
98
  }
97
- else {
98
- // For absolute or external imports, try to extract domain
99
- targetDomain = extractDomain(importPath, domainPatterns);
100
- }
101
99
  // If no domain detected, allow
102
100
  if (!sourceDomain || !targetDomain) {
103
101
  return { violates: false, sourceDomain, targetDomain };
@@ -339,11 +339,6 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
339
339
  */
340
340
  function generateMessageData(cycle, strategy, sourceImport) {
341
341
  switch (strategy) {
342
- case 'module-split':
343
- return {
344
- messageId: 'moduleSplit',
345
- data: generateModuleSplitMessage(cycle),
346
- };
347
342
  case 'direct-import':
348
343
  return {
349
344
  messageId: 'directImport',
@@ -360,6 +355,7 @@ exports.noCycle = (0, eslint_devkit_1.createRule)({
360
355
  data: generateDependencyInjectionMessage(cycle),
361
356
  };
362
357
  default:
358
+ // 'module-split' (and the impossible fall-through) both split.
363
359
  return {
364
360
  messageId: 'moduleSplit',
365
361
  data: generateModuleSplitMessage(cycle),
@@ -159,21 +159,20 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
159
159
  // Check for @deprecated decorator in TypeScript
160
160
  if ('decorators' in node &&
161
161
  Array.isArray(node.decorators)) {
162
+ // The Array.isArray guard above proves decorators is an array.
162
163
  const decorators = node.decorators;
163
- if (decorators) {
164
- for (const decorator of decorators) {
165
- const dec = decorator;
166
- if ((dec.expression.type === 'CallExpression' &&
167
- dec.expression.callee.type === 'Identifier' &&
168
- dec.expression.callee.name === 'deprecated') ||
169
- (dec.expression.type === 'Identifier' &&
170
- dec.expression.name === 'deprecated')) {
171
- deprecatedItems.set(name, {
172
- location: node,
173
- reason: 'Marked with @deprecated decorator',
174
- });
175
- break;
176
- }
164
+ for (const decorator of decorators) {
165
+ const dec = decorator;
166
+ if ((dec.expression.type === 'CallExpression' &&
167
+ dec.expression.callee.type === 'Identifier' &&
168
+ dec.expression.callee.name === 'deprecated') ||
169
+ (dec.expression.type === 'Identifier' &&
170
+ dec.expression.name === 'deprecated')) {
171
+ deprecatedItems.set(name, {
172
+ location: node,
173
+ reason: 'Marked with @deprecated decorator',
174
+ });
175
+ break;
177
176
  }
178
177
  }
179
178
  }
@@ -184,6 +183,7 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
184
183
  const deprecationInfo = deprecatedItems.get(name);
185
184
  if (!deprecationInfo)
186
185
  return;
186
+ const { replacement } = deprecationInfo;
187
187
  context.report({
188
188
  node,
189
189
  messageId: 'deprecatedUsage',
@@ -197,17 +197,16 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
197
197
  ? `Replace with ${deprecationInfo.replacement}`
198
198
  : 'Find modern replacement in documentation',
199
199
  },
200
- suggest: deprecationInfo.replacement
200
+ suggest: replacement
201
201
  ? [
202
202
  {
203
203
  messageId: 'deprecatedUsage',
204
204
  fix(fixer) {
205
- return fixer.replaceText(node, deprecationInfo.replacement ||
206
- 'Check documentation for replacement');
205
+ return fixer.replaceText(node, replacement);
207
206
  },
208
207
  data: {
209
- replacement: deprecationInfo.replacement,
210
- suggestion: `Replace with ${deprecationInfo.replacement}`,
208
+ replacement,
209
+ suggestion: `Replace with ${replacement}`,
211
210
  },
212
211
  },
213
212
  ]
@@ -287,8 +286,9 @@ exports.noDeprecated = (0, eslint_devkit_1.createRule)({
287
286
  },
288
287
  // Check for usage of deprecated items
289
288
  MemberExpression(node) {
289
+ // No has() pre-check: reportDeprecatedUsage bails out when the
290
+ // object name has no recorded deprecation info.
290
291
  if (node.object.type === 'Identifier' &&
291
- deprecatedItems.has(node.object.name) &&
292
292
  node.property.type === 'Identifier') {
293
293
  reportDeprecatedUsage(node, node.object.name, `${node.object.name}.${node.property.name}`);
294
294
  }
@@ -35,13 +35,12 @@ exports.noDuplicates = (0, eslint_devkit_1.createRule)({
35
35
  return {
36
36
  ImportDeclaration(node) {
37
37
  const source = node.source.value;
38
- if (!imports.has(source)) {
39
- imports.set(source, []);
40
- }
41
- const sourceNodes = imports.get(source);
42
- if (sourceNodes) {
43
- sourceNodes.push(node);
38
+ let sourceNodes = imports.get(source);
39
+ if (!sourceNodes) {
40
+ sourceNodes = [];
41
+ imports.set(source, sourceNodes);
44
42
  }
43
+ sourceNodes.push(node);
45
44
  },
46
45
  'Program:exit'() {
47
46
  for (const nodes of imports.values()) {
@@ -56,8 +56,7 @@ exports.noNamedAsDefaultMember = (0, eslint_devkit_1.createRule)({
56
56
  },
57
57
  MemberExpression(node) {
58
58
  // Check if accessing property of default import
59
- if (node.object.type !== eslint_devkit_1.AST_NODE_TYPES.Identifier ||
60
- !defaultImports.has(node.object.name)) {
59
+ if (node.object.type !== eslint_devkit_1.AST_NODE_TYPES.Identifier) {
61
60
  return;
62
61
  }
63
62
  // Skip computed properties like obj[prop]
@@ -93,21 +93,15 @@ exports.noNamedExport = (0, eslint_devkit_1.createRule)({
93
93
  if (pattern.includes('*')) {
94
94
  const regexStr = '^' + pattern
95
95
  .replace(/[.+?^${}()|[\]\\]/g, (match) => {
96
- if (match === '*')
97
- return '*';
96
+ // The char class above never matches '*', so no special-casing needed.
98
97
  return '\\' + match;
99
98
  })
100
99
  .replace(/\*\*/g, '.*')
101
100
  .replace(/\*/g, '[^/]*') + '$';
102
- try {
103
- const regex = new RegExp(regexStr);
104
- const result = regex.test(filename);
105
- // DEBUG LOGIC if needed, but relying on report below
106
- return result;
107
- }
108
- catch {
109
- return false;
110
- }
101
+ // regexStr escapes every regex metacharacter before translating
102
+ // glob tokens, so new RegExp() can never throw here.
103
+ const regex = new RegExp(regexStr);
104
+ return regex.test(filename);
111
105
  }
112
106
  return filename.includes(pattern);
113
107
  });
@@ -119,19 +113,15 @@ exports.noNamedExport = (0, eslint_devkit_1.createRule)({
119
113
  if (pattern.includes('*')) {
120
114
  const regexStr = '^' + pattern
121
115
  .replace(/[.+?^${}()|[\]\\]/g, (match) => {
122
- if (match === '*')
123
- return '*';
116
+ // The char class above never matches '*', so no special-casing needed.
124
117
  return '\\' + match;
125
118
  })
126
119
  .replace(/\*\*/g, '.*')
127
120
  .replace(/\*/g, '[^/]*') + '$';
128
- try {
129
- const regex = new RegExp(regexStr);
130
- return regex.test(filename);
131
- }
132
- catch {
133
- return false;
134
- }
121
+ // regexStr escapes every regex metacharacter before translating
122
+ // glob tokens, so new RegExp() can never throw here.
123
+ const regex = new RegExp(regexStr);
124
+ return regex.test(filename);
135
125
  }
136
126
  return filename.includes(pattern);
137
127
  });
@@ -101,10 +101,10 @@ exports.noRelativePackages = (0, eslint_devkit_1.createRule)({
101
101
  },
102
102
  fix(fixer) {
103
103
  // Calculate the subpath within the package
104
+ // subPath is never '' or '.': the package root is found by walking
105
+ // UP from the resolved path's parent, so it is a strict ancestor.
104
106
  const subPath = path.relative(importPackageRoot, resolvedPath);
105
- const newImport = subPath && subPath !== '.'
106
- ? `${packageName}/${subPath.replace(/\.[^/.]+$/, '')}`
107
- : packageName;
107
+ const newImport = `${packageName}/${subPath.replace(/\.[^/.]+$/, '')}`;
108
108
  return fixer.replaceText(source, `'${newImport}'`);
109
109
  },
110
110
  });