@taiga-ui/eslint-plugin-experience-next 0.503.0 → 0.504.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 import default, namespace, cycle, duplicate, named-as-default, and self-import 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, self-import, and path 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,7 +38,7 @@ 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<"duplicateImport" | "importCycle" | "missingDefaultExport" | "namedAsDefault" | "namedAsDefaultMember" | "selfImport" | "unknownNamespaceMember", [({
41
+ 'import-integrity': import("@typescript-eslint/utils/ts-eslint").RuleModule<"duplicateImport" | "importCycle" | "missingDefaultExport" | "namedAsDefault" | "namedAsDefaultMember" | "selfImport" | "unknownNamespaceMember" | "uselessPathSegments", [({
42
42
  checkCycles?: boolean;
43
43
  checkDefaultImports?: boolean;
44
44
  checkDuplicateImports?: boolean;
@@ -46,6 +46,7 @@ declare const plugin: {
46
46
  checkNamedAsDefaultMembers?: boolean;
47
47
  checkNamespaceMembers?: boolean;
48
48
  checkSelfImports?: boolean;
49
+ checkUselessPathSegments?: boolean;
49
50
  ignoreExternalDefaultImports?: boolean;
50
51
  } | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
51
52
  name: string;
package/index.esm.js CHANGED
@@ -890,7 +890,7 @@ var recommended = defineConfig([
890
890
  'import/no-named-as-default-member': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
891
891
  'import/no-self-import': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
892
892
  'import/no-unresolved': 'off',
893
- 'import/no-useless-path-segments': ['error', { noUselessIndex: true }],
893
+ 'import/no-useless-path-segments': 'off', // Covered by @taiga-ui/experience-next/import-integrity.
894
894
  'import/no-webpack-loader-syntax': 'error',
895
895
  'lines-around-comment': [
896
896
  'error',
@@ -247754,9 +247754,37 @@ function resolveModuleFileName(program, containingFile, moduleSpecifier) {
247754
247754
  }
247755
247755
  return resolved.resolvedFileName;
247756
247756
  }
247757
- function getModuleSpecifierPath(moduleSpecifier) {
247757
+ function splitModuleSpecifierQuery(moduleSpecifier) {
247758
247758
  const queryIndex = moduleSpecifier.indexOf('?');
247759
- return queryIndex === -1 ? moduleSpecifier : moduleSpecifier.slice(0, queryIndex);
247759
+ return queryIndex === -1
247760
+ ? { path: moduleSpecifier, query: '' }
247761
+ : {
247762
+ path: moduleSpecifier.slice(0, queryIndex),
247763
+ query: moduleSpecifier.slice(queryIndex),
247764
+ };
247765
+ }
247766
+ function getModuleSpecifierPath(moduleSpecifier) {
247767
+ return splitModuleSpecifierQuery(moduleSpecifier).path;
247768
+ }
247769
+ function toRelativeImportPath(relativePath) {
247770
+ const stripped = relativePath.replaceAll(/\/$/g, '');
247771
+ return /^\.{1,2}$|^\.{1,2}\//.test(stripped) ? stripped : `./${stripped}`;
247772
+ }
247773
+ function normalizeImportPath(moduleSpecifierPath) {
247774
+ return toRelativeImportPath(path.posix.normalize(moduleSpecifierPath));
247775
+ }
247776
+ function countRelativeParents(pathSegments) {
247777
+ return pathSegments.filter((segment) => segment === '..').length;
247778
+ }
247779
+ function isIndexModulePath(moduleSpecifierPath) {
247780
+ return /(?:^|\/)index(?:\.[cm]?[jt]sx?)?$/.test(moduleSpecifierPath);
247781
+ }
247782
+ function quoteModuleSpecifier(source, moduleSpecifier) {
247783
+ const quote = source.raw.startsWith('"') ? '"' : "'";
247784
+ const escaped = moduleSpecifier
247785
+ .replaceAll('\\', '\\\\')
247786
+ .replaceAll(quote, `\\${quote}`);
247787
+ return `${quote}${escaped}${quote}`;
247760
247788
  }
247761
247789
  function resolveModuleKey(program, containingFile, moduleSpecifier, canonicalFileName) {
247762
247790
  const moduleSpecifierPath = getModuleSpecifierPath(moduleSpecifier);
@@ -247860,149 +247888,14 @@ function getRuntimeModuleSpecifier(statement) {
247860
247888
  }
247861
247889
  return null;
247862
247890
  }
247863
- function buildDependenciesByFileName(program, canonicalFileName) {
247864
- const sourceFiles = program.getSourceFiles().filter(isProjectCodeFile);
247865
- const projectFileNames = new Set(sourceFiles.map((sourceFile) => canonicalFileName(sourceFile.fileName)));
247866
- const dependenciesByFileName = new Map();
247867
- const displayFileNameByFileName = new Map();
247868
- for (const sourceFile of sourceFiles) {
247869
- const fileName = canonicalFileName(sourceFile.fileName);
247870
- const edges = [];
247871
- displayFileNameByFileName.set(fileName, sourceFile.fileName);
247872
- for (const statement of sourceFile.statements) {
247873
- const moduleSpecifier = getRuntimeModuleSpecifier(statement);
247874
- if (!moduleSpecifier) {
247875
- continue;
247876
- }
247877
- const resolvedFileName = resolveModuleFileName(program, sourceFile.fileName, moduleSpecifier);
247878
- if (!resolvedFileName) {
247879
- continue;
247880
- }
247881
- const targetFileName = canonicalFileName(resolvedFileName);
247882
- if (!projectFileNames.has(targetFileName)) {
247883
- continue;
247884
- }
247885
- edges.push({
247886
- isImport: ts.isImportDeclaration(statement),
247887
- moduleSpecifier,
247888
- targetFileName,
247889
- });
247890
- }
247891
- dependenciesByFileName.set(fileName, edges);
247892
- }
247893
- return { dependenciesByFileName, displayFileNameByFileName };
247894
- }
247895
- function findStronglyConnectedComponents(dependenciesByFileName) {
247896
- let nextComponentId = 0;
247897
- let nextIndex = 0;
247898
- const componentIdByFileName = new Map();
247899
- const componentSizeById = new Map();
247900
- const nodeStateByFileName = new Map();
247901
- const stack = [];
247902
- function visit(fileName) {
247903
- const state = {
247904
- index: nextIndex,
247905
- lowLink: nextIndex,
247906
- onStack: true,
247907
- };
247908
- nextIndex += 1;
247909
- nodeStateByFileName.set(fileName, state);
247910
- stack.push(fileName);
247911
- for (const edge of dependenciesByFileName.get(fileName) ?? []) {
247912
- const targetState = nodeStateByFileName.get(edge.targetFileName);
247913
- if (!targetState) {
247914
- visit(edge.targetFileName);
247915
- const visitedTargetState = nodeStateByFileName.get(edge.targetFileName);
247916
- if (visitedTargetState) {
247917
- state.lowLink = Math.min(state.lowLink, visitedTargetState.lowLink);
247918
- }
247919
- continue;
247920
- }
247921
- if (targetState.onStack) {
247922
- state.lowLink = Math.min(state.lowLink, targetState.index);
247923
- }
247924
- }
247925
- if (state.lowLink !== state.index) {
247926
- return;
247927
- }
247928
- const componentId = nextComponentId;
247929
- let componentSize = 0;
247930
- let shouldPop = stack.length > 0;
247931
- nextComponentId += 1;
247932
- while (shouldPop) {
247933
- const memberFileName = stack.pop();
247934
- if (!memberFileName) {
247935
- shouldPop = false;
247936
- continue;
247937
- }
247938
- const memberState = nodeStateByFileName.get(memberFileName);
247939
- if (memberState) {
247940
- memberState.onStack = false;
247941
- }
247942
- componentSize += 1;
247943
- componentIdByFileName.set(memberFileName, componentId);
247944
- shouldPop = stack.length > 0 && memberFileName !== fileName;
247945
- }
247946
- componentSizeById.set(componentId, componentSize);
247947
- }
247948
- for (const fileName of dependenciesByFileName.keys()) {
247949
- if (!nodeStateByFileName.has(fileName)) {
247950
- visit(fileName);
247951
- }
247952
- }
247953
- return { componentIdByFileName, componentSizeById };
247954
- }
247955
- function collectSelfCycles(dependenciesByFileName) {
247956
- const selfCycleFileNames = new Set();
247957
- for (const [fileName, edges] of dependenciesByFileName) {
247958
- if (edges.some((edge) => edge.targetFileName === fileName)) {
247959
- selfCycleFileNames.add(fileName);
247960
- }
247961
- }
247962
- return selfCycleFileNames;
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
- }
247990
247891
  function getImportGraph(program) {
247991
247892
  const cached = importGraphCacheByProgram.get(program);
247992
247893
  if (cached) {
247993
247894
  return cached;
247994
247895
  }
247995
- const canonicalFileName = createCanonicalFileName();
247996
- const { dependenciesByFileName, displayFileNameByFileName } = buildDependenciesByFileName(program, canonicalFileName);
247997
- const { componentIdByFileName, componentSizeById } = findStronglyConnectedComponents(dependenciesByFileName);
247998
247896
  const cache = {
247999
- barrelFileNames: buildBarrelFileNames(dependenciesByFileName),
248000
- componentIdByFileName,
248001
- componentSizeById,
248002
- dependenciesByFileName,
248003
- displayFileNameByFileName,
248004
- sccHasImportEdgeById: buildSccHasImportEdge(dependenciesByFileName, componentIdByFileName),
248005
- selfCycleFileNames: collectSelfCycles(dependenciesByFileName),
247897
+ dependenciesByFileName: new Map(),
247898
+ displayFileNameByFileName: new Map(),
248006
247899
  };
248007
247900
  importGraphCacheByProgram.set(program, cache);
248008
247901
  return cache;
@@ -248092,76 +247985,136 @@ function getImportOrReExportModuleSpecifier(node) {
248092
247985
  }
248093
247986
  return typeof node.source.value === 'string' ? node.source.value : null;
248094
247987
  }
248095
- function findCyclicTargetFileName(graph, currentFileName, moduleSpecifier) {
248096
- const currentComponentId = graph.componentIdByFileName.get(currentFileName);
248097
- if (currentComponentId === undefined) {
248098
- return null;
247988
+ function getProjectSourceFile(program, fileName) {
247989
+ const sourceFile = getSourceFileByFileName(program, fileName);
247990
+ return sourceFile && isProjectCodeFile(sourceFile) ? sourceFile : null;
247991
+ }
247992
+ function getDependenciesForFile(program, graph, fileName, canonicalFileName) {
247993
+ const cached = graph.dependenciesByFileName.get(fileName);
247994
+ if (cached) {
247995
+ return cached;
248099
247996
  }
248100
- const currentComponentSize = graph.componentSizeById.get(currentComponentId) ?? 0;
248101
- for (const edge of graph.dependenciesByFileName.get(currentFileName) ?? []) {
248102
- if (edge.moduleSpecifier !== moduleSpecifier) {
247997
+ const sourceFile = getProjectSourceFile(program, fileName);
247998
+ const edges = [];
247999
+ graph.dependenciesByFileName.set(fileName, edges);
248000
+ if (!sourceFile) {
248001
+ return edges;
248002
+ }
248003
+ graph.displayFileNameByFileName.set(fileName, sourceFile.fileName);
248004
+ for (const statement of sourceFile.statements) {
248005
+ const moduleSpecifier = getRuntimeModuleSpecifier(statement);
248006
+ if (!moduleSpecifier) {
248103
248007
  continue;
248104
248008
  }
248105
- const isSelfCycle = edge.targetFileName === currentFileName &&
248106
- graph.selfCycleFileNames.has(currentFileName);
248107
- const isSameComponentCycle = currentComponentSize > 1 &&
248108
- graph.componentIdByFileName.get(edge.targetFileName) === currentComponentId;
248109
- if (isSelfCycle || isSameComponentCycle) {
248110
- return edge.targetFileName;
248009
+ const resolvedFileName = resolveModuleFileName(program, sourceFile.fileName, moduleSpecifier);
248010
+ if (!resolvedFileName) {
248011
+ continue;
248012
+ }
248013
+ const targetSourceFile = getProjectSourceFile(program, resolvedFileName);
248014
+ if (!targetSourceFile) {
248015
+ continue;
248111
248016
  }
248017
+ const targetFileName = canonicalFileName(targetSourceFile.fileName);
248018
+ graph.displayFileNameByFileName.set(targetFileName, targetSourceFile.fileName);
248019
+ edges.push({
248020
+ isImport: ts.isImportDeclaration(statement),
248021
+ moduleSpecifier,
248022
+ targetFileName,
248023
+ });
248112
248024
  }
248113
- return null;
248025
+ return edges;
248026
+ }
248027
+ function fileHasReExportEdges(program, graph, fileName, canonicalFileName) {
248028
+ return getDependenciesForFile(program, graph, fileName, canonicalFileName).some((edge) => !edge.isImport);
248029
+ }
248030
+ function getCycleStateKey(fileName, hasImportEdge) {
248031
+ return `${hasImportEdge ? '1' : '0'}:${fileName}`;
248032
+ }
248033
+ function reconstructSearchPath(stateKey, previousStateKeyByStateKey, fileNameByStateKey) {
248034
+ const pathSegments = [];
248035
+ let currentStateKey = stateKey;
248036
+ while (currentStateKey !== null) {
248037
+ const fileName = fileNameByStateKey.get(currentStateKey);
248038
+ if (!fileName) {
248039
+ return [];
248040
+ }
248041
+ pathSegments.push(fileName);
248042
+ const previousStateKey = previousStateKeyByStateKey.get(currentStateKey);
248043
+ if (previousStateKey === undefined) {
248044
+ return [];
248045
+ }
248046
+ currentStateKey = previousStateKey;
248047
+ }
248048
+ return pathSegments.reverse();
248114
248049
  }
248115
- function findPathWithinComponent(graph, startFileName, endFileName, componentId) {
248116
- const queue = [startFileName];
248117
- const previousFileName = new Map([[startFileName, null]]);
248118
- for (let index = 0; index < queue.length && !previousFileName.has(endFileName); index += 1) {
248119
- const currentFileName = queue[index];
248120
- if (!currentFileName) {
248050
+ function findCyclePathFromEdge(program, graph, currentFileName, edge, canonicalFileName) {
248051
+ const queue = [{ fileName: edge.targetFileName, hasImportEdge: edge.isImport }];
248052
+ const startStateKey = getCycleStateKey(edge.targetFileName, edge.isImport);
248053
+ const previousStateKeyByStateKey = new Map([
248054
+ [startStateKey, null],
248055
+ ]);
248056
+ const fileNameByStateKey = new Map([
248057
+ [startStateKey, edge.targetFileName],
248058
+ ]);
248059
+ let firstResult = null;
248060
+ for (const state of queue) {
248061
+ const stateKey = getCycleStateKey(state.fileName, state.hasImportEdge);
248062
+ if (state.fileName === currentFileName) {
248063
+ const path = reconstructSearchPath(stateKey, previousStateKeyByStateKey, fileNameByStateKey);
248064
+ if (path.length === 0) {
248065
+ continue;
248066
+ }
248067
+ const result = {
248068
+ cyclePath: [currentFileName, ...path],
248069
+ hasImportEdge: state.hasImportEdge,
248070
+ targetFileName: edge.targetFileName,
248071
+ };
248072
+ if (result.hasImportEdge) {
248073
+ return result;
248074
+ }
248075
+ firstResult ??= result;
248121
248076
  continue;
248122
248077
  }
248123
- for (const edge of graph.dependenciesByFileName.get(currentFileName) ?? []) {
248124
- if (graph.componentIdByFileName.get(edge.targetFileName) !== componentId ||
248125
- previousFileName.has(edge.targetFileName)) {
248078
+ for (const nextEdge of getDependenciesForFile(program, graph, state.fileName, canonicalFileName)) {
248079
+ const nextState = {
248080
+ fileName: nextEdge.targetFileName,
248081
+ hasImportEdge: state.hasImportEdge || nextEdge.isImport,
248082
+ };
248083
+ const nextStateKey = getCycleStateKey(nextState.fileName, nextState.hasImportEdge);
248084
+ if (previousStateKeyByStateKey.has(nextStateKey)) {
248126
248085
  continue;
248127
248086
  }
248128
- previousFileName.set(edge.targetFileName, currentFileName);
248129
- queue.push(edge.targetFileName);
248087
+ previousStateKeyByStateKey.set(nextStateKey, stateKey);
248088
+ fileNameByStateKey.set(nextStateKey, nextState.fileName);
248089
+ queue.push(nextState);
248130
248090
  }
248131
248091
  }
248132
- if (!previousFileName.has(endFileName)) {
248133
- return [startFileName, endFileName];
248134
- }
248135
- const pathSegments = [];
248136
- let currentFileName = endFileName;
248137
- while (currentFileName !== null) {
248138
- pathSegments.push(currentFileName);
248139
- const nextFileName = previousFileName.get(currentFileName);
248140
- if (nextFileName === undefined) {
248141
- return [startFileName, endFileName];
248092
+ return firstResult;
248093
+ }
248094
+ function findCyclePathForSpecifier(program, graph, currentFileName, moduleSpecifier, canonicalFileName) {
248095
+ let firstResult = null;
248096
+ for (const edge of getDependenciesForFile(program, graph, currentFileName, canonicalFileName)) {
248097
+ if (edge.moduleSpecifier !== moduleSpecifier) {
248098
+ continue;
248099
+ }
248100
+ const result = findCyclePathFromEdge(program, graph, currentFileName, edge, canonicalFileName);
248101
+ if (!result) {
248102
+ continue;
248103
+ }
248104
+ if (result.hasImportEdge) {
248105
+ return result;
248142
248106
  }
248143
- currentFileName = nextFileName;
248107
+ firstResult ??= result;
248144
248108
  }
248145
- return pathSegments.reverse();
248109
+ return firstResult;
248146
248110
  }
248147
248111
  function formatFileName(graph, fileName, cwd) {
248148
248112
  const displayFileName = graph.displayFileNameByFileName.get(fileName) ?? fileName;
248149
248113
  const relativeFileName = normalizeSlashes(path.relative(cwd, displayFileName));
248150
248114
  return relativeFileName || normalizeSlashes(path.basename(displayFileName));
248151
248115
  }
248152
- function formatCyclePath(graph, currentFileName, targetFileName, cwd) {
248153
- const componentId = graph.componentIdByFileName.get(currentFileName);
248154
- if (componentId === undefined || currentFileName === targetFileName) {
248155
- return [currentFileName, targetFileName]
248156
- .map((fileName) => formatFileName(graph, fileName, cwd))
248157
- .join(' -> ');
248158
- }
248159
- return [
248160
- currentFileName,
248161
- ...findPathWithinComponent(graph, targetFileName, currentFileName, componentId),
248162
- ]
248163
- .map((fileName) => formatFileName(graph, fileName, cwd))
248164
- .join(' -> ');
248116
+ function formatCyclePath(graph, cyclePath, cwd) {
248117
+ return cyclePath.map((fileName) => formatFileName(graph, fileName, cwd)).join(' -> ');
248165
248118
  }
248166
248119
  function getAliasedSymbolIfNeeded(checker, symbol) {
248167
248120
  return (symbol.flags & ts.SymbolFlags.Alias) === 0
@@ -248306,6 +248259,7 @@ const rule$K = createRule({
248306
248259
  const checkNamedAsDefaultMembers = context.options[0]?.checkNamedAsDefaultMembers ?? true;
248307
248260
  const checkNamespaceMembers = context.options[0]?.checkNamespaceMembers ?? true;
248308
248261
  const checkSelfImports = context.options[0]?.checkSelfImports ?? true;
248262
+ const shouldCheckUselessPathSegments = context.options[0]?.checkUselessPathSegments ?? true;
248309
248263
  const ignoreExternalDefaultImports = context.options[0]?.ignoreExternalDefaultImports ?? true;
248310
248264
  const canonicalFileName = createCanonicalFileName();
248311
248265
  const currentFileName = canonicalFileName(context.filename);
@@ -248435,10 +248389,82 @@ const rule$K = createRule({
248435
248389
  node,
248436
248390
  });
248437
248391
  }
248438
- function checkDeclarationSelfImport(node) {
248439
- if (!node.source || typeof node.source.value !== 'string') {
248392
+ function getNormalizedPathReplacement(moduleSpecifierPath, canonicalResolvedFileName) {
248393
+ const normalizedPath = normalizeImportPath(moduleSpecifierPath);
248394
+ if (normalizedPath === moduleSpecifierPath) {
248395
+ return null;
248396
+ }
248397
+ const normalizedResolved = resolveModule(tsProgram, context.filename, normalizedPath);
248398
+ return normalizedResolved &&
248399
+ canonicalFileName(normalizedResolved.resolvedFileName) ===
248400
+ canonicalResolvedFileName
248401
+ ? normalizedPath
248402
+ : null;
248403
+ }
248404
+ function getIndexPathReplacement(moduleSpecifierPath, canonicalResolvedFileName) {
248405
+ if (!isIndexModulePath(moduleSpecifierPath)) {
248406
+ return null;
248407
+ }
248408
+ const parentDirectory = path.posix.dirname(moduleSpecifierPath);
248409
+ if (parentDirectory === '.' || parentDirectory === '..') {
248410
+ return parentDirectory;
248411
+ }
248412
+ const parentResolved = resolveModule(tsProgram, context.filename, parentDirectory);
248413
+ return parentResolved &&
248414
+ canonicalFileName(parentResolved.resolvedFileName) !==
248415
+ canonicalResolvedFileName
248416
+ ? `${parentDirectory}/`
248417
+ : parentDirectory;
248418
+ }
248419
+ function getExcessParentPathReplacement(moduleSpecifierPath, resolvedFileName) {
248420
+ if (moduleSpecifierPath.startsWith('./')) {
248421
+ return null;
248422
+ }
248423
+ const expectedPath = computeRelativeImportPath(context.filename, resolvedFileName);
248424
+ const importSegments = moduleSpecifierPath.replace(/^\.\//, '').split('/');
248425
+ const importParentCount = countRelativeParents(importSegments);
248426
+ const expectedParentCount = countRelativeParents(expectedPath.split('/'));
248427
+ if (importParentCount <= expectedParentCount ||
248428
+ expectedPath === moduleSpecifierPath) {
248429
+ return null;
248430
+ }
248431
+ return expectedPath;
248432
+ }
248433
+ function getUselessPathSegmentsReplacement(moduleSpecifierPath) {
248434
+ const resolved = resolveModule(tsProgram, context.filename, moduleSpecifierPath);
248435
+ if (!resolved) {
248436
+ return null;
248437
+ }
248438
+ const canonicalResolvedFileName = canonicalFileName(resolved.resolvedFileName);
248439
+ return (getNormalizedPathReplacement(moduleSpecifierPath, canonicalResolvedFileName) ??
248440
+ getIndexPathReplacement(moduleSpecifierPath, canonicalResolvedFileName) ??
248441
+ getExcessParentPathReplacement(moduleSpecifierPath, resolved.resolvedFileName));
248442
+ }
248443
+ function checkUselessPathSegments(source) {
248444
+ if (!shouldCheckUselessPathSegments || !source.value.startsWith('.')) {
248445
+ return;
248446
+ }
248447
+ const parts = splitModuleSpecifierQuery(source.value);
248448
+ const proposedPath = getUselessPathSegmentsReplacement(parts.path);
248449
+ if (!proposedPath) {
248450
+ return;
248451
+ }
248452
+ const proposedModuleSpecifier = `${proposedPath}${parts.query}`;
248453
+ context.report({
248454
+ data: {
248455
+ moduleSpecifier: source.value,
248456
+ proposedPath: proposedModuleSpecifier,
248457
+ },
248458
+ fix: (fixer) => fixer.replaceText(source, quoteModuleSpecifier(source, proposedModuleSpecifier)),
248459
+ messageId: 'uselessPathSegments',
248460
+ node: source,
248461
+ });
248462
+ }
248463
+ function checkDeclarationModuleSpecifier(node) {
248464
+ if (!isStringLiteral(node.source)) {
248440
248465
  return;
248441
248466
  }
248467
+ checkUselessPathSegments(node.source);
248442
248468
  checkSelfImport(node.source.value, node.source);
248443
248469
  }
248444
248470
  function checkRequireSelfImport(node) {
@@ -248454,12 +248480,12 @@ const rule$K = createRule({
248454
248480
  }
248455
248481
  checkSelfImport(moduleSpecifier.value, moduleSpecifier);
248456
248482
  }
248457
- function checkDynamicSelfImport(node) {
248483
+ function checkDynamicImport(node) {
248458
248484
  const { source } = node;
248459
- if (source.type !== dist$3.AST_NODE_TYPES.Literal ||
248460
- typeof source.value !== 'string') {
248485
+ if (!isStringLiteral(source)) {
248461
248486
  return;
248462
248487
  }
248488
+ checkUselessPathSegments(source);
248463
248489
  checkSelfImport(source.value, source);
248464
248490
  }
248465
248491
  function checkImportCycle(node) {
@@ -248475,11 +248501,11 @@ const rule$K = createRule({
248475
248501
  return;
248476
248502
  }
248477
248503
  const graph = getImportGraph(tsProgram);
248478
- const targetFileName = findCyclicTargetFileName(graph, currentFileName, moduleSpecifier);
248479
- if (!targetFileName) {
248504
+ const cycle = findCyclePathForSpecifier(tsProgram, graph, currentFileName, moduleSpecifier, canonicalFileName);
248505
+ if (!cycle) {
248480
248506
  return;
248481
248507
  }
248482
- const cyclePath = formatCyclePath(graph, currentFileName, targetFileName, context.cwd);
248508
+ const cyclePath = formatCyclePath(graph, cycle.cyclePath, context.cwd);
248483
248509
  if (node.type === dist$3.AST_NODE_TYPES.ImportDeclaration) {
248484
248510
  // Compute the redirect replacement eagerly so we can decide whether
248485
248511
  // to suppress. If a redirect to the direct source file is possible,
@@ -248501,12 +248527,10 @@ const rule$K = createRule({
248501
248527
  }
248502
248528
  else {
248503
248529
  // For re-export nodes (export * from / export {x} from), suppress the
248504
- // error when the same SCC already contains an ImportDeclaration edge.
248530
+ // error when the reachable cycle contains an ImportDeclaration edge.
248505
248531
  // That ImportDeclaration will be reported (and fixed) directly, so
248506
248532
  // 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) {
248533
+ if (cycle.hasImportEdge) {
248510
248534
  return;
248511
248535
  }
248512
248536
  context.report({
@@ -248528,7 +248552,7 @@ const rule$K = createRule({
248528
248552
  // Not a barrel (has no re-export edges) — symbols are defined locally,
248529
248553
  // so there is no shorter direct-source path to redirect to.
248530
248554
  if (!canonicalBarrelFileName ||
248531
- !getImportGraph(tsProgram).barrelFileNames.has(canonicalBarrelFileName)) {
248555
+ !fileHasReExportEdges(tsProgram, getImportGraph(tsProgram), canonicalBarrelFileName, canonicalFileName)) {
248532
248556
  return null;
248533
248557
  }
248534
248558
  const specifiersBySourceFile = new Map();
@@ -248794,21 +248818,21 @@ const rule$K = createRule({
248794
248818
  CallExpression: checkRequireSelfImport,
248795
248819
  ExportAllDeclaration(node) {
248796
248820
  checkImportCycle(node);
248797
- checkDeclarationSelfImport(node);
248821
+ checkDeclarationModuleSpecifier(node);
248798
248822
  },
248799
248823
  ExportNamedDeclaration(node) {
248800
248824
  checkImportCycle(node);
248801
- checkDeclarationSelfImport(node);
248825
+ checkDeclarationModuleSpecifier(node);
248802
248826
  checkNamedAsDefaultExport(node);
248803
248827
  },
248804
248828
  ImportDeclaration(node) {
248805
248829
  checkImportCycle(node);
248806
- checkDeclarationSelfImport(node);
248830
+ checkDeclarationModuleSpecifier(node);
248807
248831
  collectDuplicateImport(node);
248808
248832
  checkDefaultImport(node);
248809
248833
  collectNamespaceImport(node);
248810
248834
  },
248811
- ImportExpression: checkDynamicSelfImport,
248835
+ ImportExpression: checkDynamicImport,
248812
248836
  MemberExpression(node) {
248813
248837
  checkNamedAsDefaultMemberExpression(node);
248814
248838
  checkNamespaceMember(node);
@@ -248819,7 +248843,7 @@ const rule$K = createRule({
248819
248843
  },
248820
248844
  meta: {
248821
248845
  docs: {
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',
248846
+ 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, import/no-self-import, and import/no-useless-path-segments checks',
248823
248847
  },
248824
248848
  fixable: 'code',
248825
248849
  messages: {
@@ -248830,6 +248854,7 @@ const rule$K = createRule({
248830
248854
  namedAsDefaultMember: 'Default import "{{defaultName}}" from "{{moduleSpecifier}}" also has a named export "{{memberName}}". Use a named import instead.',
248831
248855
  selfImport: 'Module imports itself.',
248832
248856
  unknownNamespaceMember: 'Namespace import "{{namespaceName}}" from "{{moduleSpecifier}}" has no exported member "{{memberName}}".',
248857
+ uselessPathSegments: 'Useless path segments for "{{moduleSpecifier}}", should be "{{proposedPath}}".',
248833
248858
  },
248834
248859
  schema: [
248835
248860
  {
@@ -248863,6 +248888,10 @@ const rule$K = createRule({
248863
248888
  description: 'Report imports, re-exports, dynamic imports, and require() calls that resolve to the current file. Defaults to true.',
248864
248889
  type: 'boolean',
248865
248890
  },
248891
+ checkUselessPathSegments: {
248892
+ description: 'Report relative imports, re-exports, and dynamic imports with unnecessary path segments or /index suffixes. Defaults to true.',
248893
+ type: 'boolean',
248894
+ },
248866
248895
  ignoreExternalDefaultImports: {
248867
248896
  description: 'Skip default import checks for modules resolved from external libraries. Defaults to true.',
248868
248897
  type: 'boolean',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.503.0",
3
+ "version": "0.504.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,5 +1,5 @@
1
1
  import { type TSESLint } from '@typescript-eslint/utils';
2
- type MessageId = 'duplicateImport' | 'importCycle' | 'missingDefaultExport' | 'namedAsDefault' | 'namedAsDefaultMember' | 'selfImport' | 'unknownNamespaceMember';
2
+ type MessageId = 'duplicateImport' | 'importCycle' | 'missingDefaultExport' | 'namedAsDefault' | 'namedAsDefaultMember' | 'selfImport' | 'unknownNamespaceMember' | 'uselessPathSegments';
3
3
  type Options = [
4
4
  {
5
5
  checkCycles?: boolean;
@@ -9,6 +9,7 @@ type Options = [
9
9
  checkNamedAsDefaultMembers?: boolean;
10
10
  checkNamespaceMembers?: boolean;
11
11
  checkSelfImports?: boolean;
12
+ checkUselessPathSegments?: boolean;
12
13
  ignoreExternalDefaultImports?: boolean;
13
14
  }?
14
15
  ];