@taiga-ui/eslint-plugin-experience-next 0.501.0 → 0.503.0

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/README.md CHANGED
@@ -80,7 +80,7 @@ from third-party plugins. The exact severities and file globs live in
80
80
  | [flat-exports](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/flat-exports.md) | Spread nested arrays when exporting Angular entity collections | | 🔧 | |
81
81
  | [host-attributes-sort](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/host-attributes-sort.md) | Sort Angular host metadata attributes using configurable attribute groups | ✅ | 🔧 | |
82
82
  | [html-logical-properties](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/html-logical-properties.md) | Enforce logical CSS properties over directional ones in Angular template style bindings | ✅ | 🔧 | |
83
- | [import-integrity](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/import-integrity.md) | Fast default import, namespace export, named-as-default, and import cycle checks | ✅ | 🔧 | |
83
+ | [import-integrity](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/import-integrity.md) | Fast import default, namespace, cycle, duplicate, named-as-default, and self-import checks | ✅ | 🔧 | |
84
84
  | [injection-token-description](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/injection-token-description.md) | Require `InjectionToken` descriptions to include the token name | ✅ | 🔧 | |
85
85
  | [no-commonjs-import-patterns](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-commonjs-import-patterns.md) | Disallow legacy CommonJS interop import patterns | ✅ | | |
86
86
  | [no-deep-imports](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-deep-imports.md) | Disables deep imports of Taiga UI packages | ✅ | 🔧 | |
package/index.d.ts CHANGED
@@ -38,11 +38,14 @@ declare const plugin: {
38
38
  'html-logical-properties': import("eslint").Rule.RuleModule & {
39
39
  name: string;
40
40
  };
41
- 'import-integrity': import("@typescript-eslint/utils/ts-eslint").RuleModule<"importCycle" | "missingDefaultExport" | "namedAsDefault" | "unknownNamespaceMember", [({
41
+ 'import-integrity': import("@typescript-eslint/utils/ts-eslint").RuleModule<"duplicateImport" | "importCycle" | "missingDefaultExport" | "namedAsDefault" | "namedAsDefaultMember" | "selfImport" | "unknownNamespaceMember", [({
42
42
  checkCycles?: boolean;
43
43
  checkDefaultImports?: boolean;
44
+ checkDuplicateImports?: boolean;
44
45
  checkNamedAsDefault?: boolean;
46
+ checkNamedAsDefaultMembers?: boolean;
45
47
  checkNamespaceMembers?: boolean;
48
+ checkSelfImports?: boolean;
46
49
  ignoreExternalDefaultImports?: boolean;
47
50
  } | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
48
51
  name: string;
package/index.esm.js CHANGED
@@ -867,7 +867,7 @@ var recommended = defineConfig([
867
867
  'error',
868
868
  {
869
869
  methods: 'above',
870
- printWidth: 120,
870
+ printWidth: 90,
871
871
  properties: 'above',
872
872
  },
873
873
  ],
@@ -875,19 +875,20 @@ var recommended = defineConfig([
875
875
  'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
876
876
  'guard-for-in': 'error',
877
877
  'import/consistent-type-specifier-style': ['error', 'prefer-inline'],
878
- 'import/default': 'off',
878
+ 'import/default': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
879
879
  'import/enforce-node-protocol-usage': ['error', 'always'],
880
880
  'import/export': 'off',
881
881
  'import/first': 'error',
882
- 'import/namespace': 'off',
882
+ 'import/namespace': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
883
883
  'import/newline-after-import': ['error', { count: 1 }],
884
884
  'import/no-absolute-path': 'error',
885
- 'import/no-cycle': 'off',
886
- 'import/no-duplicates': ['error', { 'prefer-inline': true }],
885
+ 'import/no-cycle': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
886
+ 'import/no-duplicates': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
887
887
  'import/no-extraneous-dependencies': 'off',
888
888
  'import/no-mutable-exports': 'error',
889
- 'import/no-named-as-default': 'off',
890
- 'import/no-self-import': 'error',
889
+ 'import/no-named-as-default': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
890
+ 'import/no-named-as-default-member': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
891
+ 'import/no-self-import': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
891
892
  'import/no-unresolved': 'off',
892
893
  'import/no-useless-path-segments': ['error', { noUselessIndex: true }],
893
894
  'import/no-webpack-loader-syntax': 'error',
@@ -247667,8 +247668,8 @@ function getTypeAwareRuleContext(context) {
247667
247668
 
247668
247669
  const importGraphCacheByProgram = new WeakMap();
247669
247670
  const defaultExportCacheByProgram = new WeakMap();
247671
+ const fallbackResolutionByProgram = new WeakMap();
247670
247672
  const moduleExportNamesCache = new WeakMap();
247671
- const resolutionStateByProgram = new WeakMap();
247672
247673
  const sourceFileCacheByProgram = new WeakMap();
247673
247674
  const codeFileExtensionRegExp = /\.[cm]?[jt]sx?$/;
247674
247675
  // Angular DI functions resolve tokens at instantiation time, not at module load time,
@@ -247689,8 +247690,38 @@ function createCanonicalFileName() {
247689
247690
  : resolvedFileName.toLowerCase();
247690
247691
  };
247691
247692
  }
247692
- function getResolutionState(program) {
247693
- const cached = resolutionStateByProgram.get(program);
247693
+ function normalizeSlashes(fileName) {
247694
+ return fileName.replaceAll('\\', '/');
247695
+ }
247696
+ function computeRelativeImportPath(fromFile, toFile) {
247697
+ const relative = path.relative(path.dirname(fromFile), toFile);
247698
+ const normalized = normalizeSlashes(relative).replace(codeFileExtensionRegExp, '');
247699
+ return normalized.startsWith('.') ? normalized : `./${normalized}`;
247700
+ }
247701
+ function isProjectCodeFile(sourceFile) {
247702
+ const normalizedFileName = normalizeSlashes(sourceFile.fileName);
247703
+ return (!sourceFile.isDeclarationFile &&
247704
+ codeFileExtensionRegExp.test(normalizedFileName) &&
247705
+ !normalizedFileName.includes('/node_modules/'));
247706
+ }
247707
+ // Returns the module resolution cache that the TypeScript program itself populated during
247708
+ // compilation. Not part of the public ts.Program API but available at runtime on TS 4+.
247709
+ function getProgramResolutionCache(program) {
247710
+ const record = program;
247711
+ const getCache = record['getModuleResolutionCache'];
247712
+ if (typeof getCache !== 'function') {
247713
+ return null;
247714
+ }
247715
+ const cache = getCache.call(program);
247716
+ if (cache === null ||
247717
+ typeof cache !== 'object' ||
247718
+ !('getOrCreateCacheForDirectory' in cache)) {
247719
+ return null;
247720
+ }
247721
+ return cache;
247722
+ }
247723
+ function getFallbackResolution(program) {
247724
+ const cached = fallbackResolutionByProgram.get(program);
247694
247725
  if (cached) {
247695
247726
  return cached;
247696
247727
  }
@@ -247699,23 +247730,22 @@ function getResolutionState(program) {
247699
247730
  compilerHost: ts.createCompilerHost(compilerOptions, true),
247700
247731
  resolutionCache: ts.createModuleResolutionCache(program.getCurrentDirectory(), (fileName) => fileName, compilerOptions),
247701
247732
  };
247702
- resolutionStateByProgram.set(program, state);
247733
+ fallbackResolutionByProgram.set(program, state);
247703
247734
  return state;
247704
247735
  }
247705
- function normalizeSlashes(fileName) {
247706
- return fileName.replaceAll('\\', '/');
247707
- }
247708
- function isProjectCodeFile(sourceFile) {
247709
- const normalizedFileName = normalizeSlashes(sourceFile.fileName);
247710
- return (!sourceFile.isDeclarationFile &&
247711
- codeFileExtensionRegExp.test(normalizedFileName) &&
247712
- !normalizedFileName.includes('/node_modules/'));
247713
- }
247714
247736
  function resolveModule(program, containingFile, moduleSpecifier) {
247715
- const compilerOptions = program.getCompilerOptions();
247716
- const { compilerHost, resolutionCache } = getResolutionState(program);
247717
- const resolved = ts.resolveModuleName(moduleSpecifier, containingFile, compilerOptions, compilerHost, resolutionCache).resolvedModule ?? null;
247718
- return resolved;
247737
+ // Prefer the program's own resolution cache (already populated during compilation)
247738
+ // over running a fresh resolution, which requires file system access per unique
247739
+ // (directory, module) pair and dominates lint time on cold starts.
247740
+ const programCache = getProgramResolutionCache(program);
247741
+ if (programCache) {
247742
+ const fromCache = ts.resolveModuleNameFromCache(moduleSpecifier, containingFile, programCache);
247743
+ if (fromCache !== undefined) {
247744
+ return fromCache.resolvedModule ?? null;
247745
+ }
247746
+ }
247747
+ const { compilerHost, resolutionCache } = getFallbackResolution(program);
247748
+ return (ts.resolveModuleName(moduleSpecifier, containingFile, program.getCompilerOptions(), compilerHost, resolutionCache).resolvedModule ?? null);
247719
247749
  }
247720
247750
  function resolveModuleFileName(program, containingFile, moduleSpecifier) {
247721
247751
  const resolved = resolveModule(program, containingFile, moduleSpecifier);
@@ -247724,6 +247754,56 @@ function resolveModuleFileName(program, containingFile, moduleSpecifier) {
247724
247754
  }
247725
247755
  return resolved.resolvedFileName;
247726
247756
  }
247757
+ function getModuleSpecifierPath(moduleSpecifier) {
247758
+ const queryIndex = moduleSpecifier.indexOf('?');
247759
+ return queryIndex === -1 ? moduleSpecifier : moduleSpecifier.slice(0, queryIndex);
247760
+ }
247761
+ function resolveModuleKey(program, containingFile, moduleSpecifier, canonicalFileName) {
247762
+ const moduleSpecifierPath = getModuleSpecifierPath(moduleSpecifier);
247763
+ const resolved = resolveModule(program, containingFile, moduleSpecifierPath);
247764
+ if (!resolved) {
247765
+ return moduleSpecifier;
247766
+ }
247767
+ const queryIndex = moduleSpecifier.indexOf('?');
247768
+ const query = queryIndex === -1 ? '' : moduleSpecifier.slice(queryIndex);
247769
+ return `${canonicalFileName(resolved.resolvedFileName)}${query}`;
247770
+ }
247771
+ function getDefaultImportName(node) {
247772
+ const defaultImport = node.specifiers.find((specifier) => specifier.type === dist$3.AST_NODE_TYPES.ImportDefaultSpecifier);
247773
+ return defaultImport?.local.name ?? null;
247774
+ }
247775
+ function hasNamespaceImport(node) {
247776
+ return node.specifiers.some((specifier) => specifier.type === dist$3.AST_NODE_TYPES.ImportNamespaceSpecifier);
247777
+ }
247778
+ function hasTypeOnlyDefaultImport(node) {
247779
+ return node.importKind === 'type' && getDefaultImportName(node) !== null;
247780
+ }
247781
+ function hasImportAttributes(node) {
247782
+ const record = node;
247783
+ const attributes = record['attributes'];
247784
+ const assertions = record['assertions'];
247785
+ return ((Array.isArray(attributes) && attributes.length > 0) ||
247786
+ (Array.isArray(assertions) && assertions.length > 0));
247787
+ }
247788
+ function hasProblematicImportComments(node, sourceCode) {
247789
+ const text = sourceCode.getText(node);
247790
+ return (text.includes('/*') ||
247791
+ text.includes('//') ||
247792
+ sourceCode
247793
+ .getCommentsBefore(node)
247794
+ .some((comment) => comment.loc.end.line >= node.loc.start.line - 1) ||
247795
+ sourceCode
247796
+ .getCommentsAfter(node)
247797
+ .some((comment) => comment.loc.start.line === node.loc.end.line));
247798
+ }
247799
+ function getNamedSpecifierText(node, specifier, sourceCode) {
247800
+ const text = sourceCode.getText(specifier);
247801
+ const isTypeOnly = node.importKind === 'type' || specifier.importKind === 'type';
247802
+ return isTypeOnly && !text.trimStart().startsWith('type ') ? `type ${text}` : text;
247803
+ }
247804
+ function getNamedSpecifierKey(text) {
247805
+ return text.trim().replace(/^type\s+/, '');
247806
+ }
247727
247807
  function importDeclarationHasRuntimeEdge(node) {
247728
247808
  const importClause = node.importClause;
247729
247809
  if (!importClause) {
@@ -247802,7 +247882,11 @@ function buildDependenciesByFileName(program, canonicalFileName) {
247802
247882
  if (!projectFileNames.has(targetFileName)) {
247803
247883
  continue;
247804
247884
  }
247805
- edges.push({ moduleSpecifier, targetFileName });
247885
+ edges.push({
247886
+ isImport: ts.isImportDeclaration(statement),
247887
+ moduleSpecifier,
247888
+ targetFileName,
247889
+ });
247806
247890
  }
247807
247891
  dependenciesByFileName.set(fileName, edges);
247808
247892
  }
@@ -247877,6 +247961,32 @@ function collectSelfCycles(dependenciesByFileName) {
247877
247961
  }
247878
247962
  return selfCycleFileNames;
247879
247963
  }
247964
+ function buildSccHasImportEdge(dependenciesByFileName, componentIdByFileName) {
247965
+ const result = new Map();
247966
+ for (const [fileName, edges] of dependenciesByFileName) {
247967
+ const sourceComponentId = componentIdByFileName.get(fileName);
247968
+ if (sourceComponentId === undefined || result.get(sourceComponentId) === true) {
247969
+ continue;
247970
+ }
247971
+ for (const edge of edges) {
247972
+ if (edge.isImport &&
247973
+ componentIdByFileName.get(edge.targetFileName) === sourceComponentId) {
247974
+ result.set(sourceComponentId, true);
247975
+ break;
247976
+ }
247977
+ }
247978
+ }
247979
+ return result;
247980
+ }
247981
+ function buildBarrelFileNames(dependenciesByFileName) {
247982
+ const result = new Set();
247983
+ for (const [fileName, edges] of dependenciesByFileName) {
247984
+ if (edges.some((edge) => !edge.isImport)) {
247985
+ result.add(fileName);
247986
+ }
247987
+ }
247988
+ return result;
247989
+ }
247880
247990
  function getImportGraph(program) {
247881
247991
  const cached = importGraphCacheByProgram.get(program);
247882
247992
  if (cached) {
@@ -247886,10 +247996,12 @@ function getImportGraph(program) {
247886
247996
  const { dependenciesByFileName, displayFileNameByFileName } = buildDependenciesByFileName(program, canonicalFileName);
247887
247997
  const { componentIdByFileName, componentSizeById } = findStronglyConnectedComponents(dependenciesByFileName);
247888
247998
  const cache = {
247999
+ barrelFileNames: buildBarrelFileNames(dependenciesByFileName),
247889
248000
  componentIdByFileName,
247890
248001
  componentSizeById,
247891
248002
  dependenciesByFileName,
247892
248003
  displayFileNameByFileName,
248004
+ sccHasImportEdgeById: buildSccHasImportEdge(dependenciesByFileName, componentIdByFileName),
247893
248005
  selfCycleFileNames: collectSelfCycles(dependenciesByFileName),
247894
248006
  };
247895
248007
  importGraphCacheByProgram.set(program, cache);
@@ -248103,6 +248215,34 @@ function getExportSpecifierName(name) {
248103
248215
  function hasNamedValueExport(exportNames, exportedName) {
248104
248216
  return exportedName !== 'default' && (exportNames?.has(exportedName) ?? false);
248105
248217
  }
248218
+ // Angular resolves the reference lazily (at instantiation, not at module load time)
248219
+ // in all of these contexts, so a cycle through them is safe at the ES module level.
248220
+ function isInAngularSafeContext(identifier) {
248221
+ // TSESTree types parent as Node (non-optional), but the root node's parent is
248222
+ // null at runtime. Widening to include null/undefined lets while(current) serve
248223
+ // as the exit condition when traversal reaches the top of the tree.
248224
+ let current = identifier.parent;
248225
+ while (current) {
248226
+ // Lazy evaluation: body runs only when the function/arrow is called.
248227
+ if (current.type === dist$3.AST_NODE_TYPES.ArrowFunctionExpression ||
248228
+ current.type === dist$3.AST_NODE_TYPES.FunctionExpression ||
248229
+ current.type === dist$3.AST_NODE_TYPES.FunctionDeclaration) {
248230
+ return true;
248231
+ }
248232
+ // Angular decorator metadata (@Component, @Directive, etc.) is processed
248233
+ // after all modules in the cycle have finished loading.
248234
+ if (current.type === dist$3.AST_NODE_TYPES.Decorator) {
248235
+ return true;
248236
+ }
248237
+ // Non-static class field initializers run at instantiation time, not at
248238
+ // module load time, so they do not create a load-time cycle edge.
248239
+ if (current.type === dist$3.AST_NODE_TYPES.PropertyDefinition && !current.static) {
248240
+ return true;
248241
+ }
248242
+ current = current.parent;
248243
+ }
248244
+ return false;
248245
+ }
248106
248246
  function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248107
248247
  const valueSpecifiers = node.specifiers.filter((specifier) => {
248108
248248
  if (specifier.type === dist$3.AST_NODE_TYPES.ImportDefaultSpecifier ||
@@ -248114,6 +248254,10 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248114
248254
  if (valueSpecifiers.length === 0) {
248115
248255
  return false;
248116
248256
  }
248257
+ // True if at least one reference is a genuine DI call or lives in a lazy/decorator
248258
+ // context. Pure type-only references don't count: an import used only as a type
248259
+ // should use `import type` and is not considered safe from a cycle perspective.
248260
+ let hasSafeRuntimeUsage = false;
248117
248261
  for (const specifier of valueSpecifiers) {
248118
248262
  const [variable] = sourceCode.getDeclaredVariables(specifier);
248119
248263
  if (!variable || variable.references.length === 0) {
@@ -248122,6 +248266,17 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248122
248266
  for (const ref of variable.references) {
248123
248267
  const { identifier } = ref;
248124
248268
  const parent = identifier.parent;
248269
+ // Type-only references (e.g., param: Type<T>) don't create runtime
248270
+ // dependencies. Skip without marking hasSafeRuntimeUsage — an import
248271
+ // that is exclusively used as a type should use `import type` instead.
248272
+ if (parent.type === dist$3.AST_NODE_TYPES.TSTypeReference) {
248273
+ continue;
248274
+ }
248275
+ // References inside lazy/decorator contexts don't create load-time edges.
248276
+ if (isInAngularSafeContext(identifier)) {
248277
+ hasSafeRuntimeUsage = true;
248278
+ continue;
248279
+ }
248125
248280
  const callee = parent.type === dist$3.AST_NODE_TYPES.CallExpression ? parent.callee : null;
248126
248281
  const isDirectCall = callee?.type === dist$3.AST_NODE_TYPES.Identifier &&
248127
248282
  ANGULAR_DI_FIRST_ARG_FUNCTIONS.has(callee.name);
@@ -248136,21 +248291,177 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248136
248291
  parent.arguments[0] !== identifier) {
248137
248292
  return false;
248138
248293
  }
248294
+ hasSafeRuntimeUsage = true;
248139
248295
  }
248140
248296
  }
248141
- return true;
248297
+ return hasSafeRuntimeUsage;
248142
248298
  }
248143
248299
  const rule$K = createRule({
248144
248300
  create(context) {
248145
248301
  const { checker, esTreeNodeToTSNodeMap, sourceCode, tsProgram } = getTypeAwareRuleContext(context);
248146
248302
  const checkCycles = context.options[0]?.checkCycles ?? true;
248147
248303
  const checkDefaultImports = context.options[0]?.checkDefaultImports ?? true;
248304
+ const checkDuplicateImports = context.options[0]?.checkDuplicateImports ?? true;
248148
248305
  const checkNamedAsDefault = context.options[0]?.checkNamedAsDefault ?? true;
248306
+ const checkNamedAsDefaultMembers = context.options[0]?.checkNamedAsDefaultMembers ?? true;
248149
248307
  const checkNamespaceMembers = context.options[0]?.checkNamespaceMembers ?? true;
248308
+ const checkSelfImports = context.options[0]?.checkSelfImports ?? true;
248150
248309
  const ignoreExternalDefaultImports = context.options[0]?.ignoreExternalDefaultImports ?? true;
248151
248310
  const canonicalFileName = createCanonicalFileName();
248152
248311
  const currentFileName = canonicalFileName(context.filename);
248312
+ const defaultImports = new Map();
248313
+ const duplicateImportMaps = {
248314
+ importsByModule: new Map(),
248315
+ namespaceImportsByModule: new Map(),
248316
+ };
248153
248317
  const namespaceImports = new Map();
248318
+ function getDuplicateImportMap(node) {
248319
+ return hasNamespaceImport(node)
248320
+ ? duplicateImportMaps.namespaceImportsByModule
248321
+ : duplicateImportMaps.importsByModule;
248322
+ }
248323
+ function collectDuplicateImport(node) {
248324
+ if (!checkDuplicateImports) {
248325
+ return;
248326
+ }
248327
+ const moduleKey = resolveModuleKey(tsProgram, context.filename, node.source.value, canonicalFileName);
248328
+ const importsByModule = getDuplicateImportMap(node);
248329
+ const imports = importsByModule.get(moduleKey) ?? [];
248330
+ imports.push(node);
248331
+ importsByModule.set(moduleKey, imports);
248332
+ }
248333
+ function buildImportRemovalFix(fixer, node) {
248334
+ const [start, end] = node.range;
248335
+ const lineStart = sourceCode.text.lastIndexOf('\n', start - 1) + 1;
248336
+ const removeStart = /^\s*$/.test(sourceCode.text.slice(lineStart, start))
248337
+ ? lineStart
248338
+ : start;
248339
+ const removeEnd = sourceCode.text[end] === '\n' ? end + 1 : end;
248340
+ return [fixer.removeRange([removeStart, removeEnd])];
248341
+ }
248342
+ function buildDuplicateImportFix(fixer, first, rest) {
248343
+ const nodes = [first, ...rest];
248344
+ if (nodes.some((node) => hasNamespaceImport(node) ||
248345
+ hasTypeOnlyDefaultImport(node) ||
248346
+ hasImportAttributes(node) ||
248347
+ hasProblematicImportComments(node, sourceCode))) {
248348
+ return null;
248349
+ }
248350
+ const defaultNames = new Set(nodes.flatMap((node) => {
248351
+ const name = getDefaultImportName(node);
248352
+ return name ? [name] : [];
248353
+ }));
248354
+ if (defaultNames.size > 1) {
248355
+ return null;
248356
+ }
248357
+ const namedSpecifiersByKey = new Map();
248358
+ for (const node of nodes) {
248359
+ for (const specifier of node.specifiers) {
248360
+ if (specifier.type !== dist$3.AST_NODE_TYPES.ImportSpecifier) {
248361
+ continue;
248362
+ }
248363
+ const text = getNamedSpecifierText(node, specifier, sourceCode).trim();
248364
+ const key = getNamedSpecifierKey(text);
248365
+ const existing = namedSpecifiersByKey.get(key);
248366
+ const isTypeOnly = text.startsWith('type ');
248367
+ if (!existing || (existing.startsWith('type ') && !isTypeOnly)) {
248368
+ namedSpecifiersByKey.set(key, text);
248369
+ }
248370
+ }
248371
+ }
248372
+ const namedSpecifiers = [...namedSpecifiersByKey.values()];
248373
+ const [defaultName = null] = defaultNames;
248374
+ const bindings = [
248375
+ ...(defaultName ? [defaultName] : []),
248376
+ ...(namedSpecifiers.length > 0
248377
+ ? [`{${namedSpecifiers.join(', ')}}`]
248378
+ : []),
248379
+ ];
248380
+ const sourceText = sourceCode.getText(first.source);
248381
+ const semi = sourceCode.getText(first).endsWith(';') ? ';' : '';
248382
+ const replacement = bindings.length === 0
248383
+ ? `import ${sourceText}${semi}`
248384
+ : `import ${bindings.join(', ')} from ${sourceText}${semi}`;
248385
+ return [
248386
+ fixer.replaceText(first, replacement),
248387
+ ...rest.flatMap((node) => buildImportRemovalFix(fixer, node)),
248388
+ ];
248389
+ }
248390
+ function reportDuplicateImports(importsByModule) {
248391
+ for (const nodes of importsByModule.values()) {
248392
+ if (nodes.length < 2) {
248393
+ continue;
248394
+ }
248395
+ const [first, ...rest] = nodes;
248396
+ if (!first) {
248397
+ continue;
248398
+ }
248399
+ const moduleSpecifier = first.source.value;
248400
+ context.report({
248401
+ data: { moduleSpecifier },
248402
+ fix: (fixer) => buildDuplicateImportFix(fixer, first, rest),
248403
+ messageId: 'duplicateImport',
248404
+ node: first.source,
248405
+ });
248406
+ for (const node of rest) {
248407
+ context.report({
248408
+ data: { moduleSpecifier },
248409
+ messageId: 'duplicateImport',
248410
+ node: node.source,
248411
+ });
248412
+ }
248413
+ }
248414
+ }
248415
+ function reportAllDuplicateImports() {
248416
+ if (!checkDuplicateImports) {
248417
+ return;
248418
+ }
248419
+ reportDuplicateImports(duplicateImportMaps.importsByModule);
248420
+ reportDuplicateImports(duplicateImportMaps.namespaceImportsByModule);
248421
+ }
248422
+ function checkSelfImport(moduleSpecifier, node) {
248423
+ if (!checkSelfImports ||
248424
+ context.filename === '<text>' ||
248425
+ moduleSpecifier.includes('?')) {
248426
+ return;
248427
+ }
248428
+ const resolved = resolveModule(tsProgram, context.filename, getModuleSpecifierPath(moduleSpecifier));
248429
+ if (!resolved ||
248430
+ canonicalFileName(resolved.resolvedFileName) !== currentFileName) {
248431
+ return;
248432
+ }
248433
+ context.report({
248434
+ messageId: 'selfImport',
248435
+ node,
248436
+ });
248437
+ }
248438
+ function checkDeclarationSelfImport(node) {
248439
+ if (!node.source || typeof node.source.value !== 'string') {
248440
+ return;
248441
+ }
248442
+ checkSelfImport(node.source.value, node.source);
248443
+ }
248444
+ function checkRequireSelfImport(node) {
248445
+ if (node.callee.type !== dist$3.AST_NODE_TYPES.Identifier ||
248446
+ node.callee.name !== 'require' ||
248447
+ node.arguments.length !== 1) {
248448
+ return;
248449
+ }
248450
+ const [moduleSpecifier] = node.arguments;
248451
+ if (moduleSpecifier?.type !== dist$3.AST_NODE_TYPES.Literal ||
248452
+ typeof moduleSpecifier.value !== 'string') {
248453
+ return;
248454
+ }
248455
+ checkSelfImport(moduleSpecifier.value, moduleSpecifier);
248456
+ }
248457
+ function checkDynamicSelfImport(node) {
248458
+ const { source } = node;
248459
+ if (source.type !== dist$3.AST_NODE_TYPES.Literal ||
248460
+ typeof source.value !== 'string') {
248461
+ return;
248462
+ }
248463
+ checkSelfImport(source.value, source);
248464
+ }
248154
248465
  function checkImportCycle(node) {
248155
248466
  if (!checkCycles) {
248156
248467
  return;
@@ -248165,21 +248476,116 @@ const rule$K = createRule({
248165
248476
  }
248166
248477
  const graph = getImportGraph(tsProgram);
248167
248478
  const targetFileName = findCyclicTargetFileName(graph, currentFileName, moduleSpecifier);
248168
- if (!targetFileName ||
248169
- (node.type === dist$3.AST_NODE_TYPES.ImportDeclaration &&
248170
- isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode))) {
248479
+ if (!targetFileName) {
248171
248480
  return;
248172
248481
  }
248173
- context.report({
248174
- data: {
248175
- cyclePath: formatCyclePath(graph, currentFileName, targetFileName, context.cwd),
248176
- },
248177
- messageId: 'importCycle',
248178
- node: sourceNode,
248179
- });
248482
+ const cyclePath = formatCyclePath(graph, currentFileName, targetFileName, context.cwd);
248483
+ if (node.type === dist$3.AST_NODE_TYPES.ImportDeclaration) {
248484
+ // Compute the redirect replacement eagerly so we can decide whether
248485
+ // to suppress. If a redirect to the direct source file is possible,
248486
+ // always report (the fix breaks the cycle). Otherwise, suppress when
248487
+ // all usages are Angular-DI-safe (inject, class fields, etc.).
248488
+ const replacement = computeCycleBreakingReplacement(node);
248489
+ if (!replacement &&
248490
+ isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode)) {
248491
+ return;
248492
+ }
248493
+ context.report({
248494
+ data: { cyclePath },
248495
+ fix: replacement
248496
+ ? (fixer) => [fixer.replaceText(node, replacement)]
248497
+ : undefined,
248498
+ messageId: 'importCycle',
248499
+ node: sourceNode,
248500
+ });
248501
+ }
248502
+ else {
248503
+ // For re-export nodes (export * from / export {x} from), suppress the
248504
+ // error when the same SCC already contains an ImportDeclaration edge.
248505
+ // That ImportDeclaration will be reported (and fixed) directly, so
248506
+ // reporting the barrel re-export would create duplicate, unfixable noise.
248507
+ const componentId = graph.componentIdByFileName.get(currentFileName);
248508
+ if (componentId !== undefined &&
248509
+ graph.sccHasImportEdgeById.get(componentId) === true) {
248510
+ return;
248511
+ }
248512
+ context.report({
248513
+ data: { cyclePath },
248514
+ messageId: 'importCycle',
248515
+ node: sourceNode,
248516
+ });
248517
+ }
248518
+ }
248519
+ function computeCycleBreakingReplacement(node) {
248520
+ // Only named imports can be safely redirected to their source file
248521
+ if (node.specifiers.some((s) => s.type !== dist$3.AST_NODE_TYPES.ImportSpecifier)) {
248522
+ return null;
248523
+ }
248524
+ const barrelFileName = resolveModuleFileName(tsProgram, context.filename, node.source.value);
248525
+ const canonicalBarrelFileName = barrelFileName
248526
+ ? canonicalFileName(barrelFileName)
248527
+ : null;
248528
+ // Not a barrel (has no re-export edges) — symbols are defined locally,
248529
+ // so there is no shorter direct-source path to redirect to.
248530
+ if (!canonicalBarrelFileName ||
248531
+ !getImportGraph(tsProgram).barrelFileNames.has(canonicalBarrelFileName)) {
248532
+ return null;
248533
+ }
248534
+ const specifiersBySourceFile = new Map();
248535
+ for (const specifier of node.specifiers) {
248536
+ if (specifier.type !== dist$3.AST_NODE_TYPES.ImportSpecifier) {
248537
+ return null;
248538
+ }
248539
+ const tsSpecifier = esTreeNodeToTSNodeMap.get(specifier);
248540
+ if (!ts.isImportSpecifier(tsSpecifier)) {
248541
+ return null;
248542
+ }
248543
+ const localSymbol = checker.getSymbolAtLocation(tsSpecifier.name);
248544
+ if (!localSymbol) {
248545
+ return null;
248546
+ }
248547
+ const originalSymbol = getAliasedSymbolIfNeeded(checker, localSymbol);
248548
+ const { declarations } = originalSymbol;
248549
+ const firstDeclaration = declarations?.[0];
248550
+ if (!firstDeclaration) {
248551
+ return null;
248552
+ }
248553
+ const sourceFile = firstDeclaration.getSourceFile();
248554
+ if (!isProjectCodeFile(sourceFile)) {
248555
+ return null;
248556
+ }
248557
+ const sourceFilePath = sourceFile.fileName;
248558
+ // Symbol is defined directly in the barrel — no shorter path exists
248559
+ if (canonicalBarrelFileName === canonicalFileName(sourceFilePath)) {
248560
+ return null;
248561
+ }
248562
+ const importedName = tsSpecifier.propertyName?.text ?? tsSpecifier.name.text;
248563
+ const localName = tsSpecifier.name.text;
248564
+ const typePrefix = specifier.importKind === 'type' ? 'type ' : '';
248565
+ const specText = importedName === localName
248566
+ ? `${typePrefix}${importedName}`
248567
+ : `${typePrefix}${importedName} as ${localName}`;
248568
+ const group = specifiersBySourceFile.get(sourceFilePath) ?? [];
248569
+ group.push(specText);
248570
+ specifiersBySourceFile.set(sourceFilePath, group);
248571
+ }
248572
+ if (specifiersBySourceFile.size === 0) {
248573
+ return null;
248574
+ }
248575
+ const semi = sourceCode.getText(node).endsWith(';') ? ';' : '';
248576
+ const quote = node.source.raw[0] ?? "'";
248577
+ const importPrefix = node.importKind === 'type' ? 'import type' : 'import';
248578
+ const newImports = [];
248579
+ for (const [sourceFilePath, names] of specifiersBySourceFile) {
248580
+ const relPath = computeRelativeImportPath(context.filename, sourceFilePath);
248581
+ newImports.push(`${importPrefix} {${names.join(', ')}} from ${quote}${relPath}${quote}${semi}`);
248582
+ }
248583
+ return newImports.join('\n');
248180
248584
  }
248181
248585
  function checkDefaultImport(node) {
248182
- if ((!checkDefaultImports && !checkNamedAsDefault) ||
248586
+ if ((!checkDefaultImports &&
248587
+ !checkNamedAsDefault &&
248588
+ !checkNamedAsDefaultMembers) ||
248183
248589
  node.importKind === 'type') {
248184
248590
  return;
248185
248591
  }
@@ -248206,6 +248612,17 @@ const rule$K = createRule({
248206
248612
  return;
248207
248613
  }
248208
248614
  const exportNames = getNamespaceImportExportNames(checker, esTreeNodeToTSNodeMap, node);
248615
+ if (checkNamedAsDefaultMembers && exportNames) {
248616
+ const [variable] = sourceCode.getDeclaredVariables(defaultImport);
248617
+ if (variable) {
248618
+ defaultImports.set(defaultImport.local.name, {
248619
+ exportNames,
248620
+ moduleSpecifier,
248621
+ node: defaultImport,
248622
+ variable,
248623
+ });
248624
+ }
248625
+ }
248209
248626
  if (!checkNamedAsDefault ||
248210
248627
  !hasNamedValueExport(exportNames, defaultImport.local.name)) {
248211
248628
  return;
@@ -248216,6 +248633,49 @@ const rule$K = createRule({
248216
248633
  node: defaultImport,
248217
248634
  });
248218
248635
  }
248636
+ function reportNamedAsDefaultMember(defaultImportIdentifier, memberName, reportNode) {
248637
+ if (!checkNamedAsDefaultMembers || !memberName || memberName === 'default') {
248638
+ return;
248639
+ }
248640
+ const usage = defaultImports.get(defaultImportIdentifier.name);
248641
+ if (!usage ||
248642
+ !hasNamedValueExport(usage.exportNames, memberName) ||
248643
+ getResolvedVariable(sourceCode, defaultImportIdentifier) !==
248644
+ usage.variable) {
248645
+ return;
248646
+ }
248647
+ context.report({
248648
+ data: {
248649
+ defaultName: usage.node.local.name,
248650
+ memberName,
248651
+ moduleSpecifier: usage.moduleSpecifier,
248652
+ },
248653
+ messageId: 'namedAsDefaultMember',
248654
+ node: reportNode,
248655
+ });
248656
+ }
248657
+ function checkNamedAsDefaultMemberExpression(node) {
248658
+ if (!checkNamedAsDefaultMembers ||
248659
+ defaultImports.size === 0 ||
248660
+ node.object.type !== dist$3.AST_NODE_TYPES.Identifier) {
248661
+ return;
248662
+ }
248663
+ reportNamedAsDefaultMember(node.object, getMemberExpressionPropertyName(node), node.property);
248664
+ }
248665
+ function checkNamedAsDefaultMemberDestructuring(node) {
248666
+ if (!checkNamedAsDefaultMembers ||
248667
+ defaultImports.size === 0 ||
248668
+ node.id.type !== dist$3.AST_NODE_TYPES.ObjectPattern ||
248669
+ node.init?.type !== dist$3.AST_NODE_TYPES.Identifier) {
248670
+ return;
248671
+ }
248672
+ for (const property of node.id.properties) {
248673
+ if (property.type !== dist$3.AST_NODE_TYPES.Property) {
248674
+ continue;
248675
+ }
248676
+ reportNamedAsDefaultMember(node.init, getObjectPropertyName(property), property.key);
248677
+ }
248678
+ }
248219
248679
  function checkNamedAsDefaultExport(node) {
248220
248680
  if (!checkNamedAsDefault ||
248221
248681
  node.exportKind === 'type' ||
@@ -248331,28 +248791,44 @@ const rule$K = createRule({
248331
248791
  });
248332
248792
  }
248333
248793
  return {
248334
- ExportAllDeclaration: checkImportCycle,
248794
+ CallExpression: checkRequireSelfImport,
248795
+ ExportAllDeclaration(node) {
248796
+ checkImportCycle(node);
248797
+ checkDeclarationSelfImport(node);
248798
+ },
248335
248799
  ExportNamedDeclaration(node) {
248336
248800
  checkImportCycle(node);
248801
+ checkDeclarationSelfImport(node);
248337
248802
  checkNamedAsDefaultExport(node);
248338
248803
  },
248339
248804
  ImportDeclaration(node) {
248340
248805
  checkImportCycle(node);
248806
+ checkDeclarationSelfImport(node);
248807
+ collectDuplicateImport(node);
248341
248808
  checkDefaultImport(node);
248342
248809
  collectNamespaceImport(node);
248343
248810
  },
248344
- MemberExpression: checkNamespaceMember,
248811
+ ImportExpression: checkDynamicSelfImport,
248812
+ MemberExpression(node) {
248813
+ checkNamedAsDefaultMemberExpression(node);
248814
+ checkNamespaceMember(node);
248815
+ },
248816
+ 'Program:exit': reportAllDuplicateImports,
248817
+ VariableDeclarator: checkNamedAsDefaultMemberDestructuring,
248345
248818
  };
248346
248819
  },
248347
248820
  meta: {
248348
248821
  docs: {
248349
- description: 'Fast replacement for import/default, import/namespace, import/no-cycle, and import/no-named-as-default checks',
248822
+ description: 'Fast replacement for import/default, import/namespace, import/no-cycle, import/no-duplicates, import/no-named-as-default, import/no-named-as-default-member, and import/no-self-import checks',
248350
248823
  },
248351
248824
  fixable: 'code',
248352
248825
  messages: {
248826
+ duplicateImport: '"{{moduleSpecifier}}" imported multiple times.',
248353
248827
  importCycle: 'Import cycle detected: {{cyclePath}}.',
248354
248828
  missingDefaultExport: 'No default export found in "{{moduleSpecifier}}".',
248355
248829
  namedAsDefault: 'Using exported name "{{name}}" as identifier for default export.',
248830
+ namedAsDefaultMember: 'Default import "{{defaultName}}" from "{{moduleSpecifier}}" also has a named export "{{memberName}}". Use a named import instead.',
248831
+ selfImport: 'Module imports itself.',
248356
248832
  unknownNamespaceMember: 'Namespace import "{{namespaceName}}" from "{{moduleSpecifier}}" has no exported member "{{memberName}}".',
248357
248833
  },
248358
248834
  schema: [
@@ -248367,14 +248843,26 @@ const rule$K = createRule({
248367
248843
  description: 'Report default imports from modules without a default export. Defaults to true.',
248368
248844
  type: 'boolean',
248369
248845
  },
248846
+ checkDuplicateImports: {
248847
+ description: 'Report repeated import declarations for the same resolved module. Defaults to true.',
248848
+ type: 'boolean',
248849
+ },
248370
248850
  checkNamedAsDefault: {
248371
248851
  description: 'Report default imports and default re-exports named after a named export from the same module. Defaults to true.',
248372
248852
  type: 'boolean',
248373
248853
  },
248854
+ checkNamedAsDefaultMembers: {
248855
+ description: 'Report property access or destructuring of default imports when the property name is a named export from the same module. Defaults to true.',
248856
+ type: 'boolean',
248857
+ },
248374
248858
  checkNamespaceMembers: {
248375
248859
  description: 'Report static namespace import member accesses that are not exported by the imported module. Defaults to true.',
248376
248860
  type: 'boolean',
248377
248861
  },
248862
+ checkSelfImports: {
248863
+ description: 'Report imports, re-exports, dynamic imports, and require() calls that resolve to the current file. Defaults to true.',
248864
+ type: 'boolean',
248865
+ },
248378
248866
  ignoreExternalDefaultImports: {
248379
248867
  description: 'Skip default import checks for modules resolved from external libraries. Defaults to true.',
248380
248868
  type: 'boolean',
@@ -250700,6 +251188,19 @@ function isNullableCallType(call, checker, nodeMap) {
250700
251188
  return false;
250701
251189
  }
250702
251190
  }
251191
+ function getTargetNode(call) {
251192
+ const { parent } = call;
251193
+ if (parent.type === dist$3.AST_NODE_TYPES.TSAsExpression) {
251194
+ return parent;
251195
+ }
251196
+ if (parent.type === dist$3.AST_NODE_TYPES.UnaryExpression &&
251197
+ parent.operator === '!' &&
251198
+ parent.parent.type === dist$3.AST_NODE_TYPES.UnaryExpression &&
251199
+ parent.parent.operator === '!') {
251200
+ return parent.parent;
251201
+ }
251202
+ return call;
251203
+ }
250703
251204
  function getCalleeName(node) {
250704
251205
  const { callee } = node;
250705
251206
  if (callee.type === dist$3.AST_NODE_TYPES.MemberExpression &&
@@ -250795,11 +251296,7 @@ const rule$r = createRule({
250795
251296
  fixer.insertTextBefore(parentStatement, `const ${varName} = ${callText};\n\n${indent}`),
250796
251297
  ];
250797
251298
  for (const call of calls) {
250798
- const { parent } = call;
250799
- const target = parent.type === dist$3.AST_NODE_TYPES.TSAsExpression
250800
- ? parent
250801
- : call;
250802
- fixes.push(fixer.replaceText(target, varName));
251299
+ fixes.push(fixer.replaceText(getTargetNode(call), varName));
250803
251300
  }
250804
251301
  return fixes;
250805
251302
  }
@@ -250812,12 +251309,7 @@ const rule$r = createRule({
250812
251309
  const bodyText = sourceCode.getText(arrowBody);
250813
251310
  const bodyStart = arrowBody.range[0];
250814
251311
  const targets = calls
250815
- .map((call) => {
250816
- const { parent } = call;
250817
- return parent.type === dist$3.AST_NODE_TYPES.TSAsExpression
250818
- ? parent
250819
- : call;
250820
- })
251312
+ .map(getTargetNode)
250821
251313
  .sort((a, b) => a.range[0] - b.range[0]);
250822
251314
  let replacedBody = '';
250823
251315
  let lastIndex = 0;
@@ -250829,7 +251321,13 @@ const rule$r = createRule({
250829
251321
  }
250830
251322
  replacedBody += bodyText.slice(lastIndex);
250831
251323
  const newBody = `{\n${innerIndent}const ${varName} = ${callText};\n\n${innerIndent}return ${replacedBody};\n${outerIndent}}`;
250832
- return [fixer.replaceText(arrowBody, newBody)];
251324
+ const bodyRangeStart = arrowBody.range[0];
251325
+ const textBeforeBody = sourceCode.text.slice(0, bodyRangeStart);
251326
+ const whitespaceBeforeBody = /\s*$/.exec(textBeforeBody)?.[0] ?? '';
251327
+ const replaceFrom = bodyRangeStart - whitespaceBeforeBody.length;
251328
+ return [
251329
+ fixer.replaceTextRange([replaceFrom, arrowBody.range[1]], ` ${newBody}`),
251330
+ ];
250833
251331
  },
250834
251332
  messageId: 'noRepeatedSignalInConditional',
250835
251333
  node: firstCall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.501.0",
3
+ "version": "0.503.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,11 +1,14 @@
1
1
  import { type TSESLint } from '@typescript-eslint/utils';
2
- type MessageId = 'importCycle' | 'missingDefaultExport' | 'namedAsDefault' | 'unknownNamespaceMember';
2
+ type MessageId = 'duplicateImport' | 'importCycle' | 'missingDefaultExport' | 'namedAsDefault' | 'namedAsDefaultMember' | 'selfImport' | 'unknownNamespaceMember';
3
3
  type Options = [
4
4
  {
5
5
  checkCycles?: boolean;
6
6
  checkDefaultImports?: boolean;
7
+ checkDuplicateImports?: boolean;
7
8
  checkNamedAsDefault?: boolean;
9
+ checkNamedAsDefaultMembers?: boolean;
8
10
  checkNamespaceMembers?: boolean;
11
+ checkSelfImports?: boolean;
9
12
  ignoreExternalDefaultImports?: boolean;
10
13
  }?
11
14
  ];