@taiga-ui/eslint-plugin-experience-next 0.500.0 → 0.502.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.
Files changed (2) hide show
  1. package/index.esm.js +249 -41
  2. package/package.json +1 -1
package/index.esm.js CHANGED
@@ -247667,8 +247667,8 @@ function getTypeAwareRuleContext(context) {
247667
247667
 
247668
247668
  const importGraphCacheByProgram = new WeakMap();
247669
247669
  const defaultExportCacheByProgram = new WeakMap();
247670
+ const fallbackResolutionByProgram = new WeakMap();
247670
247671
  const moduleExportNamesCache = new WeakMap();
247671
- const resolutionStateByProgram = new WeakMap();
247672
247672
  const sourceFileCacheByProgram = new WeakMap();
247673
247673
  const codeFileExtensionRegExp = /\.[cm]?[jt]sx?$/;
247674
247674
  // Angular DI functions resolve tokens at instantiation time, not at module load time,
@@ -247689,8 +247689,38 @@ function createCanonicalFileName() {
247689
247689
  : resolvedFileName.toLowerCase();
247690
247690
  };
247691
247691
  }
247692
- function getResolutionState(program) {
247693
- const cached = resolutionStateByProgram.get(program);
247692
+ function normalizeSlashes(fileName) {
247693
+ return fileName.replaceAll('\\', '/');
247694
+ }
247695
+ function computeRelativeImportPath(fromFile, toFile) {
247696
+ const relative = path.relative(path.dirname(fromFile), toFile);
247697
+ const normalized = normalizeSlashes(relative).replace(codeFileExtensionRegExp, '');
247698
+ return normalized.startsWith('.') ? normalized : `./${normalized}`;
247699
+ }
247700
+ function isProjectCodeFile(sourceFile) {
247701
+ const normalizedFileName = normalizeSlashes(sourceFile.fileName);
247702
+ return (!sourceFile.isDeclarationFile &&
247703
+ codeFileExtensionRegExp.test(normalizedFileName) &&
247704
+ !normalizedFileName.includes('/node_modules/'));
247705
+ }
247706
+ // Returns the module resolution cache that the TypeScript program itself populated during
247707
+ // compilation. Not part of the public ts.Program API but available at runtime on TS 4+.
247708
+ function getProgramResolutionCache(program) {
247709
+ const record = program;
247710
+ const getCache = record['getModuleResolutionCache'];
247711
+ if (typeof getCache !== 'function') {
247712
+ return null;
247713
+ }
247714
+ const cache = getCache.call(program);
247715
+ if (cache === null ||
247716
+ typeof cache !== 'object' ||
247717
+ !('getOrCreateCacheForDirectory' in cache)) {
247718
+ return null;
247719
+ }
247720
+ return cache;
247721
+ }
247722
+ function getFallbackResolution(program) {
247723
+ const cached = fallbackResolutionByProgram.get(program);
247694
247724
  if (cached) {
247695
247725
  return cached;
247696
247726
  }
@@ -247699,23 +247729,22 @@ function getResolutionState(program) {
247699
247729
  compilerHost: ts.createCompilerHost(compilerOptions, true),
247700
247730
  resolutionCache: ts.createModuleResolutionCache(program.getCurrentDirectory(), (fileName) => fileName, compilerOptions),
247701
247731
  };
247702
- resolutionStateByProgram.set(program, state);
247732
+ fallbackResolutionByProgram.set(program, state);
247703
247733
  return state;
247704
247734
  }
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
247735
  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;
247736
+ // Prefer the program's own resolution cache (already populated during compilation)
247737
+ // over running a fresh resolution, which requires file system access per unique
247738
+ // (directory, module) pair and dominates lint time on cold starts.
247739
+ const programCache = getProgramResolutionCache(program);
247740
+ if (programCache) {
247741
+ const fromCache = ts.resolveModuleNameFromCache(moduleSpecifier, containingFile, programCache);
247742
+ if (fromCache !== undefined) {
247743
+ return fromCache.resolvedModule ?? null;
247744
+ }
247745
+ }
247746
+ const { compilerHost, resolutionCache } = getFallbackResolution(program);
247747
+ return (ts.resolveModuleName(moduleSpecifier, containingFile, program.getCompilerOptions(), compilerHost, resolutionCache).resolvedModule ?? null);
247719
247748
  }
247720
247749
  function resolveModuleFileName(program, containingFile, moduleSpecifier) {
247721
247750
  const resolved = resolveModule(program, containingFile, moduleSpecifier);
@@ -247802,7 +247831,11 @@ function buildDependenciesByFileName(program, canonicalFileName) {
247802
247831
  if (!projectFileNames.has(targetFileName)) {
247803
247832
  continue;
247804
247833
  }
247805
- edges.push({ moduleSpecifier, targetFileName });
247834
+ edges.push({
247835
+ isImport: ts.isImportDeclaration(statement),
247836
+ moduleSpecifier,
247837
+ targetFileName,
247838
+ });
247806
247839
  }
247807
247840
  dependenciesByFileName.set(fileName, edges);
247808
247841
  }
@@ -247877,6 +247910,32 @@ function collectSelfCycles(dependenciesByFileName) {
247877
247910
  }
247878
247911
  return selfCycleFileNames;
247879
247912
  }
247913
+ function buildSccHasImportEdge(dependenciesByFileName, componentIdByFileName) {
247914
+ const result = new Map();
247915
+ for (const [fileName, edges] of dependenciesByFileName) {
247916
+ const sourceComponentId = componentIdByFileName.get(fileName);
247917
+ if (sourceComponentId === undefined || result.get(sourceComponentId) === true) {
247918
+ continue;
247919
+ }
247920
+ for (const edge of edges) {
247921
+ if (edge.isImport &&
247922
+ componentIdByFileName.get(edge.targetFileName) === sourceComponentId) {
247923
+ result.set(sourceComponentId, true);
247924
+ break;
247925
+ }
247926
+ }
247927
+ }
247928
+ return result;
247929
+ }
247930
+ function buildBarrelFileNames(dependenciesByFileName) {
247931
+ const result = new Set();
247932
+ for (const [fileName, edges] of dependenciesByFileName) {
247933
+ if (edges.some((edge) => !edge.isImport)) {
247934
+ result.add(fileName);
247935
+ }
247936
+ }
247937
+ return result;
247938
+ }
247880
247939
  function getImportGraph(program) {
247881
247940
  const cached = importGraphCacheByProgram.get(program);
247882
247941
  if (cached) {
@@ -247886,10 +247945,12 @@ function getImportGraph(program) {
247886
247945
  const { dependenciesByFileName, displayFileNameByFileName } = buildDependenciesByFileName(program, canonicalFileName);
247887
247946
  const { componentIdByFileName, componentSizeById } = findStronglyConnectedComponents(dependenciesByFileName);
247888
247947
  const cache = {
247948
+ barrelFileNames: buildBarrelFileNames(dependenciesByFileName),
247889
247949
  componentIdByFileName,
247890
247950
  componentSizeById,
247891
247951
  dependenciesByFileName,
247892
247952
  displayFileNameByFileName,
247953
+ sccHasImportEdgeById: buildSccHasImportEdge(dependenciesByFileName, componentIdByFileName),
247893
247954
  selfCycleFileNames: collectSelfCycles(dependenciesByFileName),
247894
247955
  };
247895
247956
  importGraphCacheByProgram.set(program, cache);
@@ -248103,6 +248164,34 @@ function getExportSpecifierName(name) {
248103
248164
  function hasNamedValueExport(exportNames, exportedName) {
248104
248165
  return exportedName !== 'default' && (exportNames?.has(exportedName) ?? false);
248105
248166
  }
248167
+ // Angular resolves the reference lazily (at instantiation, not at module load time)
248168
+ // in all of these contexts, so a cycle through them is safe at the ES module level.
248169
+ function isInAngularSafeContext(identifier) {
248170
+ // TSESTree types parent as Node (non-optional), but the root node's parent is
248171
+ // null at runtime. Widening to include null/undefined lets while(current) serve
248172
+ // as the exit condition when traversal reaches the top of the tree.
248173
+ let current = identifier.parent;
248174
+ while (current) {
248175
+ // Lazy evaluation: body runs only when the function/arrow is called.
248176
+ if (current.type === dist$3.AST_NODE_TYPES.ArrowFunctionExpression ||
248177
+ current.type === dist$3.AST_NODE_TYPES.FunctionExpression ||
248178
+ current.type === dist$3.AST_NODE_TYPES.FunctionDeclaration) {
248179
+ return true;
248180
+ }
248181
+ // Angular decorator metadata (@Component, @Directive, etc.) is processed
248182
+ // after all modules in the cycle have finished loading.
248183
+ if (current.type === dist$3.AST_NODE_TYPES.Decorator) {
248184
+ return true;
248185
+ }
248186
+ // Non-static class field initializers run at instantiation time, not at
248187
+ // module load time, so they do not create a load-time cycle edge.
248188
+ if (current.type === dist$3.AST_NODE_TYPES.PropertyDefinition && !current.static) {
248189
+ return true;
248190
+ }
248191
+ current = current.parent;
248192
+ }
248193
+ return false;
248194
+ }
248106
248195
  function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248107
248196
  const valueSpecifiers = node.specifiers.filter((specifier) => {
248108
248197
  if (specifier.type === dist$3.AST_NODE_TYPES.ImportDefaultSpecifier ||
@@ -248114,6 +248203,10 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248114
248203
  if (valueSpecifiers.length === 0) {
248115
248204
  return false;
248116
248205
  }
248206
+ // True if at least one reference is a genuine DI call or lives in a lazy/decorator
248207
+ // context. Pure type-only references don't count: an import used only as a type
248208
+ // should use `import type` and is not considered safe from a cycle perspective.
248209
+ let hasSafeRuntimeUsage = false;
248117
248210
  for (const specifier of valueSpecifiers) {
248118
248211
  const [variable] = sourceCode.getDeclaredVariables(specifier);
248119
248212
  if (!variable || variable.references.length === 0) {
@@ -248122,6 +248215,17 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248122
248215
  for (const ref of variable.references) {
248123
248216
  const { identifier } = ref;
248124
248217
  const parent = identifier.parent;
248218
+ // Type-only references (e.g., param: Type<T>) don't create runtime
248219
+ // dependencies. Skip without marking hasSafeRuntimeUsage — an import
248220
+ // that is exclusively used as a type should use `import type` instead.
248221
+ if (parent.type === dist$3.AST_NODE_TYPES.TSTypeReference) {
248222
+ continue;
248223
+ }
248224
+ // References inside lazy/decorator contexts don't create load-time edges.
248225
+ if (isInAngularSafeContext(identifier)) {
248226
+ hasSafeRuntimeUsage = true;
248227
+ continue;
248228
+ }
248125
248229
  const callee = parent.type === dist$3.AST_NODE_TYPES.CallExpression ? parent.callee : null;
248126
248230
  const isDirectCall = callee?.type === dist$3.AST_NODE_TYPES.Identifier &&
248127
248231
  ANGULAR_DI_FIRST_ARG_FUNCTIONS.has(callee.name);
@@ -248136,9 +248240,10 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248136
248240
  parent.arguments[0] !== identifier) {
248137
248241
  return false;
248138
248242
  }
248243
+ hasSafeRuntimeUsage = true;
248139
248244
  }
248140
248245
  }
248141
- return true;
248246
+ return hasSafeRuntimeUsage;
248142
248247
  }
248143
248248
  const rule$K = createRule({
248144
248249
  create(context) {
@@ -248165,18 +248270,111 @@ const rule$K = createRule({
248165
248270
  }
248166
248271
  const graph = getImportGraph(tsProgram);
248167
248272
  const targetFileName = findCyclicTargetFileName(graph, currentFileName, moduleSpecifier);
248168
- if (!targetFileName ||
248169
- (node.type === dist$3.AST_NODE_TYPES.ImportDeclaration &&
248170
- isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode))) {
248273
+ if (!targetFileName) {
248171
248274
  return;
248172
248275
  }
248173
- context.report({
248174
- data: {
248175
- cyclePath: formatCyclePath(graph, currentFileName, targetFileName, context.cwd),
248176
- },
248177
- messageId: 'importCycle',
248178
- node: sourceNode,
248179
- });
248276
+ const cyclePath = formatCyclePath(graph, currentFileName, targetFileName, context.cwd);
248277
+ if (node.type === dist$3.AST_NODE_TYPES.ImportDeclaration) {
248278
+ // Compute the redirect replacement eagerly so we can decide whether
248279
+ // to suppress. If a redirect to the direct source file is possible,
248280
+ // always report (the fix breaks the cycle). Otherwise, suppress when
248281
+ // all usages are Angular-DI-safe (inject, class fields, etc.).
248282
+ const replacement = computeCycleBreakingReplacement(node);
248283
+ if (!replacement &&
248284
+ isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode)) {
248285
+ return;
248286
+ }
248287
+ context.report({
248288
+ data: { cyclePath },
248289
+ fix: replacement
248290
+ ? (fixer) => [fixer.replaceText(node, replacement)]
248291
+ : undefined,
248292
+ messageId: 'importCycle',
248293
+ node: sourceNode,
248294
+ });
248295
+ }
248296
+ else {
248297
+ // For re-export nodes (export * from / export {x} from), suppress the
248298
+ // error when the same SCC already contains an ImportDeclaration edge.
248299
+ // That ImportDeclaration will be reported (and fixed) directly, so
248300
+ // reporting the barrel re-export would create duplicate, unfixable noise.
248301
+ const componentId = graph.componentIdByFileName.get(currentFileName);
248302
+ if (componentId !== undefined &&
248303
+ graph.sccHasImportEdgeById.get(componentId) === true) {
248304
+ return;
248305
+ }
248306
+ context.report({
248307
+ data: { cyclePath },
248308
+ messageId: 'importCycle',
248309
+ node: sourceNode,
248310
+ });
248311
+ }
248312
+ }
248313
+ function computeCycleBreakingReplacement(node) {
248314
+ // Only named imports can be safely redirected to their source file
248315
+ if (node.specifiers.some((s) => s.type !== dist$3.AST_NODE_TYPES.ImportSpecifier)) {
248316
+ return null;
248317
+ }
248318
+ const barrelFileName = resolveModuleFileName(tsProgram, context.filename, node.source.value);
248319
+ const canonicalBarrelFileName = barrelFileName
248320
+ ? canonicalFileName(barrelFileName)
248321
+ : null;
248322
+ // Not a barrel (has no re-export edges) — symbols are defined locally,
248323
+ // so there is no shorter direct-source path to redirect to.
248324
+ if (!canonicalBarrelFileName ||
248325
+ !getImportGraph(tsProgram).barrelFileNames.has(canonicalBarrelFileName)) {
248326
+ return null;
248327
+ }
248328
+ const specifiersBySourceFile = new Map();
248329
+ for (const specifier of node.specifiers) {
248330
+ if (specifier.type !== dist$3.AST_NODE_TYPES.ImportSpecifier) {
248331
+ return null;
248332
+ }
248333
+ const tsSpecifier = esTreeNodeToTSNodeMap.get(specifier);
248334
+ if (!ts.isImportSpecifier(tsSpecifier)) {
248335
+ return null;
248336
+ }
248337
+ const localSymbol = checker.getSymbolAtLocation(tsSpecifier.name);
248338
+ if (!localSymbol) {
248339
+ return null;
248340
+ }
248341
+ const originalSymbol = getAliasedSymbolIfNeeded(checker, localSymbol);
248342
+ const { declarations } = originalSymbol;
248343
+ const firstDeclaration = declarations?.[0];
248344
+ if (!firstDeclaration) {
248345
+ return null;
248346
+ }
248347
+ const sourceFile = firstDeclaration.getSourceFile();
248348
+ if (!isProjectCodeFile(sourceFile)) {
248349
+ return null;
248350
+ }
248351
+ const sourceFilePath = sourceFile.fileName;
248352
+ // Symbol is defined directly in the barrel — no shorter path exists
248353
+ if (canonicalBarrelFileName === canonicalFileName(sourceFilePath)) {
248354
+ return null;
248355
+ }
248356
+ const importedName = tsSpecifier.propertyName?.text ?? tsSpecifier.name.text;
248357
+ const localName = tsSpecifier.name.text;
248358
+ const typePrefix = specifier.importKind === 'type' ? 'type ' : '';
248359
+ const specText = importedName === localName
248360
+ ? `${typePrefix}${importedName}`
248361
+ : `${typePrefix}${importedName} as ${localName}`;
248362
+ const group = specifiersBySourceFile.get(sourceFilePath) ?? [];
248363
+ group.push(specText);
248364
+ specifiersBySourceFile.set(sourceFilePath, group);
248365
+ }
248366
+ if (specifiersBySourceFile.size === 0) {
248367
+ return null;
248368
+ }
248369
+ const semi = sourceCode.getText(node).endsWith(';') ? ';' : '';
248370
+ const quote = node.source.raw[0] ?? "'";
248371
+ const importPrefix = node.importKind === 'type' ? 'import type' : 'import';
248372
+ const newImports = [];
248373
+ for (const [sourceFilePath, names] of specifiersBySourceFile) {
248374
+ const relPath = computeRelativeImportPath(context.filename, sourceFilePath);
248375
+ newImports.push(`${importPrefix} {${names.join(', ')}} from ${quote}${relPath}${quote}${semi}`);
248376
+ }
248377
+ return newImports.join('\n');
248180
248378
  }
248181
248379
  function checkDefaultImport(node) {
248182
248380
  if ((!checkDefaultImports && !checkNamedAsDefault) ||
@@ -250700,6 +250898,19 @@ function isNullableCallType(call, checker, nodeMap) {
250700
250898
  return false;
250701
250899
  }
250702
250900
  }
250901
+ function getTargetNode(call) {
250902
+ const { parent } = call;
250903
+ if (parent.type === dist$3.AST_NODE_TYPES.TSAsExpression) {
250904
+ return parent;
250905
+ }
250906
+ if (parent.type === dist$3.AST_NODE_TYPES.UnaryExpression &&
250907
+ parent.operator === '!' &&
250908
+ parent.parent.type === dist$3.AST_NODE_TYPES.UnaryExpression &&
250909
+ parent.parent.operator === '!') {
250910
+ return parent.parent;
250911
+ }
250912
+ return call;
250913
+ }
250703
250914
  function getCalleeName(node) {
250704
250915
  const { callee } = node;
250705
250916
  if (callee.type === dist$3.AST_NODE_TYPES.MemberExpression &&
@@ -250795,11 +251006,7 @@ const rule$r = createRule({
250795
251006
  fixer.insertTextBefore(parentStatement, `const ${varName} = ${callText};\n\n${indent}`),
250796
251007
  ];
250797
251008
  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));
251009
+ fixes.push(fixer.replaceText(getTargetNode(call), varName));
250803
251010
  }
250804
251011
  return fixes;
250805
251012
  }
@@ -250812,12 +251019,7 @@ const rule$r = createRule({
250812
251019
  const bodyText = sourceCode.getText(arrowBody);
250813
251020
  const bodyStart = arrowBody.range[0];
250814
251021
  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
- })
251022
+ .map(getTargetNode)
250821
251023
  .sort((a, b) => a.range[0] - b.range[0]);
250822
251024
  let replacedBody = '';
250823
251025
  let lastIndex = 0;
@@ -250829,7 +251031,13 @@ const rule$r = createRule({
250829
251031
  }
250830
251032
  replacedBody += bodyText.slice(lastIndex);
250831
251033
  const newBody = `{\n${innerIndent}const ${varName} = ${callText};\n\n${innerIndent}return ${replacedBody};\n${outerIndent}}`;
250832
- return [fixer.replaceText(arrowBody, newBody)];
251034
+ const bodyRangeStart = arrowBody.range[0];
251035
+ const textBeforeBody = sourceCode.text.slice(0, bodyRangeStart);
251036
+ const whitespaceBeforeBody = /\s*$/.exec(textBeforeBody)?.[0] ?? '';
251037
+ const replaceFrom = bodyRangeStart - whitespaceBeforeBody.length;
251038
+ return [
251039
+ fixer.replaceTextRange([replaceFrom, arrowBody.range[1]], ` ${newBody}`),
251040
+ ];
250833
251041
  },
250834
251042
  messageId: 'noRepeatedSignalInConditional',
250835
251043
  node: firstCall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.500.0",
3
+ "version": "0.502.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",