@harness-engineering/core 0.21.1 → 0.21.3
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/dist/architecture/matchers.js +383 -332
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-BQUWXBGR.mjs → chunk-4W4FRAA6.mjs} +383 -332
- package/dist/index.d.mts +362 -45
- package/dist/index.d.ts +362 -45
- package/dist/index.js +2052 -1363
- package/dist/index.mjs +1653 -1033
- package/package.json +3 -3
|
@@ -389,6 +389,28 @@ async function detectCircularDepsInFiles(files, parser, graphDependencyData) {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
// src/architecture/collectors/circular-deps.ts
|
|
392
|
+
function makeStubParser() {
|
|
393
|
+
return {
|
|
394
|
+
name: "typescript",
|
|
395
|
+
extensions: [".ts", ".tsx"],
|
|
396
|
+
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
|
|
397
|
+
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
|
|
398
|
+
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
|
|
399
|
+
health: async () => ({ ok: true, value: { available: true } })
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function mapCycleViolations(cycles, rootDir, category) {
|
|
403
|
+
return cycles.map((cycle) => {
|
|
404
|
+
const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
|
|
405
|
+
const firstFile = relativePosix(rootDir, cycle.cycle[0]);
|
|
406
|
+
return {
|
|
407
|
+
id: violationId(firstFile, category, cyclePath),
|
|
408
|
+
file: firstFile,
|
|
409
|
+
detail: `Circular dependency: ${cyclePath}`,
|
|
410
|
+
severity: cycle.severity
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
}
|
|
392
414
|
var CircularDepsCollector = class {
|
|
393
415
|
category = "circular-deps";
|
|
394
416
|
getRules(_config, _rootDir) {
|
|
@@ -404,21 +426,7 @@ var CircularDepsCollector = class {
|
|
|
404
426
|
}
|
|
405
427
|
async collect(_config, rootDir) {
|
|
406
428
|
const files = await findFiles("**/*.ts", rootDir);
|
|
407
|
-
const
|
|
408
|
-
name: "typescript",
|
|
409
|
-
extensions: [".ts", ".tsx"],
|
|
410
|
-
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
|
|
411
|
-
extractImports: () => ({
|
|
412
|
-
ok: false,
|
|
413
|
-
error: { code: "EXTRACT_ERROR", message: "not needed" }
|
|
414
|
-
}),
|
|
415
|
-
extractExports: () => ({
|
|
416
|
-
ok: false,
|
|
417
|
-
error: { code: "EXTRACT_ERROR", message: "not needed" }
|
|
418
|
-
}),
|
|
419
|
-
health: async () => ({ ok: true, value: { available: true } })
|
|
420
|
-
};
|
|
421
|
-
const graphResult = await buildDependencyGraph(files, stubParser);
|
|
429
|
+
const graphResult = await buildDependencyGraph(files, makeStubParser());
|
|
422
430
|
if (!graphResult.ok) {
|
|
423
431
|
return [
|
|
424
432
|
{
|
|
@@ -443,16 +451,7 @@ var CircularDepsCollector = class {
|
|
|
443
451
|
];
|
|
444
452
|
}
|
|
445
453
|
const { cycles, largestCycle } = result.value;
|
|
446
|
-
const violations = cycles.
|
|
447
|
-
const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
|
|
448
|
-
const firstFile = relativePosix(rootDir, cycle.cycle[0]);
|
|
449
|
-
return {
|
|
450
|
-
id: violationId(firstFile, this.category, cyclePath),
|
|
451
|
-
file: firstFile,
|
|
452
|
-
detail: `Circular dependency: ${cyclePath}`,
|
|
453
|
-
severity: cycle.severity
|
|
454
|
-
};
|
|
455
|
-
});
|
|
454
|
+
const violations = mapCycleViolations(cycles, rootDir, this.category);
|
|
456
455
|
return [
|
|
457
456
|
{
|
|
458
457
|
category: this.category,
|
|
@@ -466,6 +465,30 @@ var CircularDepsCollector = class {
|
|
|
466
465
|
};
|
|
467
466
|
|
|
468
467
|
// src/architecture/collectors/layer-violations.ts
|
|
468
|
+
function makeLayerStubParser() {
|
|
469
|
+
return {
|
|
470
|
+
name: "typescript",
|
|
471
|
+
extensions: [".ts", ".tsx"],
|
|
472
|
+
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
473
|
+
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
474
|
+
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
475
|
+
health: async () => ({ ok: true, value: { available: true } })
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function mapLayerViolations(layerViolations, rootDir, category) {
|
|
479
|
+
return layerViolations.map((v) => {
|
|
480
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
481
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
482
|
+
const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
|
|
483
|
+
return {
|
|
484
|
+
id: violationId(relFile, category ?? "", detail),
|
|
485
|
+
file: relFile,
|
|
486
|
+
category,
|
|
487
|
+
detail,
|
|
488
|
+
severity: "error"
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
}
|
|
469
492
|
var LayerViolationCollector = class {
|
|
470
493
|
category = "layer-violations";
|
|
471
494
|
getRules(_config, _rootDir) {
|
|
@@ -480,18 +503,10 @@ var LayerViolationCollector = class {
|
|
|
480
503
|
];
|
|
481
504
|
}
|
|
482
505
|
async collect(_config, rootDir) {
|
|
483
|
-
const stubParser = {
|
|
484
|
-
name: "typescript",
|
|
485
|
-
extensions: [".ts", ".tsx"],
|
|
486
|
-
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
487
|
-
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
488
|
-
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
489
|
-
health: async () => ({ ok: true, value: { available: true } })
|
|
490
|
-
};
|
|
491
506
|
const result = await validateDependencies({
|
|
492
507
|
layers: [],
|
|
493
508
|
rootDir,
|
|
494
|
-
parser:
|
|
509
|
+
parser: makeLayerStubParser(),
|
|
495
510
|
fallbackBehavior: "skip"
|
|
496
511
|
});
|
|
497
512
|
if (!result.ok) {
|
|
@@ -505,29 +520,12 @@ var LayerViolationCollector = class {
|
|
|
505
520
|
}
|
|
506
521
|
];
|
|
507
522
|
}
|
|
508
|
-
const
|
|
509
|
-
(v) => v.reason === "WRONG_LAYER"
|
|
523
|
+
const violations = mapLayerViolations(
|
|
524
|
+
result.value.violations.filter((v) => v.reason === "WRONG_LAYER"),
|
|
525
|
+
rootDir,
|
|
526
|
+
this.category
|
|
510
527
|
);
|
|
511
|
-
|
|
512
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
513
|
-
const relImport = relativePosix(rootDir, v.imports);
|
|
514
|
-
const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
|
|
515
|
-
return {
|
|
516
|
-
id: violationId(relFile, this.category, detail),
|
|
517
|
-
file: relFile,
|
|
518
|
-
category: this.category,
|
|
519
|
-
detail,
|
|
520
|
-
severity: "error"
|
|
521
|
-
};
|
|
522
|
-
});
|
|
523
|
-
return [
|
|
524
|
-
{
|
|
525
|
-
category: this.category,
|
|
526
|
-
scope: "project",
|
|
527
|
-
value: violations.length,
|
|
528
|
-
violations
|
|
529
|
-
}
|
|
530
|
-
];
|
|
528
|
+
return [{ category: this.category, scope: "project", value: violations.length, violations }];
|
|
531
529
|
}
|
|
532
530
|
};
|
|
533
531
|
|
|
@@ -651,39 +649,43 @@ function collectOrphanedBaselineViolations(baseline, visitedCategories) {
|
|
|
651
649
|
}
|
|
652
650
|
return resolved;
|
|
653
651
|
}
|
|
652
|
+
function diffCategory(category, agg, baselineCategory, acc) {
|
|
653
|
+
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
654
|
+
const baselineValue = baselineCategory?.value ?? 0;
|
|
655
|
+
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
656
|
+
acc.newViolations.push(...classified.newViolations);
|
|
657
|
+
acc.preExisting.push(...classified.preExisting);
|
|
658
|
+
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
659
|
+
acc.resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
660
|
+
if (baselineCategory && agg.value > baselineValue) {
|
|
661
|
+
acc.regressions.push({
|
|
662
|
+
category,
|
|
663
|
+
baselineValue,
|
|
664
|
+
currentValue: agg.value,
|
|
665
|
+
delta: agg.value - baselineValue
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
654
669
|
function diff(current, baseline) {
|
|
655
670
|
const aggregated = aggregateByCategory(current);
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
671
|
+
const acc = {
|
|
672
|
+
newViolations: [],
|
|
673
|
+
resolvedViolations: [],
|
|
674
|
+
preExisting: [],
|
|
675
|
+
regressions: []
|
|
676
|
+
};
|
|
660
677
|
const visitedCategories = /* @__PURE__ */ new Set();
|
|
661
678
|
for (const [category, agg] of aggregated) {
|
|
662
679
|
visitedCategories.add(category);
|
|
663
|
-
|
|
664
|
-
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
665
|
-
const baselineValue = baselineCategory?.value ?? 0;
|
|
666
|
-
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
667
|
-
newViolations.push(...classified.newViolations);
|
|
668
|
-
preExisting.push(...classified.preExisting);
|
|
669
|
-
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
670
|
-
resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
671
|
-
if (baselineCategory && agg.value > baselineValue) {
|
|
672
|
-
regressions.push({
|
|
673
|
-
category,
|
|
674
|
-
baselineValue,
|
|
675
|
-
currentValue: agg.value,
|
|
676
|
-
delta: agg.value - baselineValue
|
|
677
|
-
});
|
|
678
|
-
}
|
|
680
|
+
diffCategory(category, agg, baseline.metrics[category], acc);
|
|
679
681
|
}
|
|
680
|
-
resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
682
|
+
acc.resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
681
683
|
return {
|
|
682
|
-
passed: newViolations.length === 0 && regressions.length === 0,
|
|
683
|
-
newViolations,
|
|
684
|
-
resolvedViolations,
|
|
685
|
-
preExisting,
|
|
686
|
-
regressions
|
|
684
|
+
passed: acc.newViolations.length === 0 && acc.regressions.length === 0,
|
|
685
|
+
newViolations: acc.newViolations,
|
|
686
|
+
resolvedViolations: acc.resolvedViolations,
|
|
687
|
+
preExisting: acc.preExisting,
|
|
688
|
+
regressions: acc.regressions
|
|
687
689
|
};
|
|
688
690
|
}
|
|
689
691
|
|
|
@@ -753,26 +755,28 @@ function findFunctionEnd(lines, startIdx) {
|
|
|
753
755
|
}
|
|
754
756
|
return lines.length - 1;
|
|
755
757
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
for (const pattern of
|
|
758
|
+
var DECISION_PATTERNS = [
|
|
759
|
+
/\bif\s*\(/g,
|
|
760
|
+
/\belse\s+if\s*\(/g,
|
|
761
|
+
/\bwhile\s*\(/g,
|
|
762
|
+
/\bfor\s*\(/g,
|
|
763
|
+
/\bcase\s+/g,
|
|
764
|
+
/&&/g,
|
|
765
|
+
/\|\|/g,
|
|
766
|
+
/\?(?!=)/g,
|
|
767
|
+
// Ternary ? but not ?. or ??
|
|
768
|
+
/\bcatch\s*\(/g
|
|
769
|
+
];
|
|
770
|
+
function countDecisionPoints(body) {
|
|
771
|
+
let count = 0;
|
|
772
|
+
for (const pattern of DECISION_PATTERNS) {
|
|
771
773
|
const matches = body.match(pattern);
|
|
772
|
-
if (matches)
|
|
773
|
-
complexity += matches.length;
|
|
774
|
-
}
|
|
774
|
+
if (matches) count += matches.length;
|
|
775
775
|
}
|
|
776
|
+
return count;
|
|
777
|
+
}
|
|
778
|
+
function computeCyclomaticComplexity(body) {
|
|
779
|
+
let complexity = 1 + countDecisionPoints(body);
|
|
776
780
|
const elseIfMatches = body.match(/\belse\s+if\s*\(/g);
|
|
777
781
|
if (elseIfMatches) {
|
|
778
782
|
complexity -= elseIfMatches.length;
|
|
@@ -988,6 +992,42 @@ async function detectComplexityViolations(snapshot, config, graphData) {
|
|
|
988
992
|
}
|
|
989
993
|
|
|
990
994
|
// src/architecture/collectors/complexity.ts
|
|
995
|
+
function buildSnapshot(files, rootDir) {
|
|
996
|
+
return {
|
|
997
|
+
files: files.map((f) => ({
|
|
998
|
+
path: f,
|
|
999
|
+
ast: { type: "Program", body: null, language: "typescript" },
|
|
1000
|
+
imports: [],
|
|
1001
|
+
exports: [],
|
|
1002
|
+
internalSymbols: [],
|
|
1003
|
+
jsDocComments: []
|
|
1004
|
+
})),
|
|
1005
|
+
dependencyGraph: { nodes: [], edges: [] },
|
|
1006
|
+
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
1007
|
+
docs: [],
|
|
1008
|
+
codeReferences: [],
|
|
1009
|
+
entryPoints: [],
|
|
1010
|
+
rootDir,
|
|
1011
|
+
config: { rootDir, analyze: {} },
|
|
1012
|
+
buildTime: 0
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function resolveMaxComplexity(config) {
|
|
1016
|
+
const threshold = config.thresholds.complexity;
|
|
1017
|
+
return typeof threshold === "number" ? threshold : threshold?.max ?? 15;
|
|
1018
|
+
}
|
|
1019
|
+
function mapComplexityViolations(complexityViolations, rootDir, category) {
|
|
1020
|
+
return complexityViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
|
|
1021
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1022
|
+
return {
|
|
1023
|
+
id: violationId(relFile, category ?? "", `${v.metric}:${v.function}`),
|
|
1024
|
+
file: relFile,
|
|
1025
|
+
category,
|
|
1026
|
+
detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
|
|
1027
|
+
severity: v.severity
|
|
1028
|
+
};
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
991
1031
|
var ComplexityCollector = class {
|
|
992
1032
|
category = "complexity";
|
|
993
1033
|
getRules(_config, _rootDir) {
|
|
@@ -1003,32 +1043,11 @@ var ComplexityCollector = class {
|
|
|
1003
1043
|
}
|
|
1004
1044
|
async collect(_config, rootDir) {
|
|
1005
1045
|
const files = await findFiles("**/*.ts", rootDir);
|
|
1006
|
-
const snapshot =
|
|
1007
|
-
|
|
1008
|
-
path: f,
|
|
1009
|
-
ast: { type: "Program", body: null, language: "typescript" },
|
|
1010
|
-
imports: [],
|
|
1011
|
-
exports: [],
|
|
1012
|
-
internalSymbols: [],
|
|
1013
|
-
jsDocComments: []
|
|
1014
|
-
})),
|
|
1015
|
-
dependencyGraph: { nodes: [], edges: [] },
|
|
1016
|
-
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
1017
|
-
docs: [],
|
|
1018
|
-
codeReferences: [],
|
|
1019
|
-
entryPoints: [],
|
|
1020
|
-
rootDir,
|
|
1021
|
-
config: { rootDir, analyze: {} },
|
|
1022
|
-
buildTime: 0
|
|
1023
|
-
};
|
|
1024
|
-
const complexityThreshold = _config.thresholds.complexity;
|
|
1025
|
-
const maxComplexity = typeof complexityThreshold === "number" ? complexityThreshold : complexityThreshold?.max ?? 15;
|
|
1046
|
+
const snapshot = buildSnapshot(files, rootDir);
|
|
1047
|
+
const maxComplexity = resolveMaxComplexity(_config);
|
|
1026
1048
|
const complexityConfig = {
|
|
1027
1049
|
thresholds: {
|
|
1028
|
-
cyclomaticComplexity: {
|
|
1029
|
-
error: maxComplexity,
|
|
1030
|
-
warn: Math.floor(maxComplexity * 0.7)
|
|
1031
|
-
}
|
|
1050
|
+
cyclomaticComplexity: { error: maxComplexity, warn: Math.floor(maxComplexity * 0.7) }
|
|
1032
1051
|
}
|
|
1033
1052
|
};
|
|
1034
1053
|
const result = await detectComplexityViolations(snapshot, complexityConfig);
|
|
@@ -1044,20 +1063,7 @@ var ComplexityCollector = class {
|
|
|
1044
1063
|
];
|
|
1045
1064
|
}
|
|
1046
1065
|
const { violations: complexityViolations, stats } = result.value;
|
|
1047
|
-
const
|
|
1048
|
-
(v) => v.severity === "error" || v.severity === "warning"
|
|
1049
|
-
);
|
|
1050
|
-
const violations = filtered.map((v) => {
|
|
1051
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
1052
|
-
const idDetail = `${v.metric}:${v.function}`;
|
|
1053
|
-
return {
|
|
1054
|
-
id: violationId(relFile, this.category, idDetail),
|
|
1055
|
-
file: relFile,
|
|
1056
|
-
category: this.category,
|
|
1057
|
-
detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
|
|
1058
|
-
severity: v.severity
|
|
1059
|
-
};
|
|
1060
|
-
});
|
|
1066
|
+
const violations = mapComplexityViolations(complexityViolations, rootDir, this.category);
|
|
1061
1067
|
return [
|
|
1062
1068
|
{
|
|
1063
1069
|
category: this.category,
|
|
@@ -1136,8 +1142,8 @@ function resolveImportSource(source, fromFile, snapshot) {
|
|
|
1136
1142
|
}
|
|
1137
1143
|
return void 0;
|
|
1138
1144
|
}
|
|
1139
|
-
function
|
|
1140
|
-
|
|
1145
|
+
function resolveThresholds2(config) {
|
|
1146
|
+
return {
|
|
1141
1147
|
fanOut: { ...DEFAULT_THRESHOLDS2.fanOut, ...config?.thresholds?.fanOut },
|
|
1142
1148
|
fanIn: { ...DEFAULT_THRESHOLDS2.fanIn, ...config?.thresholds?.fanIn },
|
|
1143
1149
|
couplingRatio: { ...DEFAULT_THRESHOLDS2.couplingRatio, ...config?.thresholds?.couplingRatio },
|
|
@@ -1146,53 +1152,70 @@ function checkViolations(metrics, config) {
|
|
|
1146
1152
|
...config?.thresholds?.transitiveDependencyDepth
|
|
1147
1153
|
}
|
|
1148
1154
|
};
|
|
1155
|
+
}
|
|
1156
|
+
function checkFanOut(m, threshold) {
|
|
1157
|
+
if (m.fanOut <= threshold) return null;
|
|
1158
|
+
return {
|
|
1159
|
+
file: m.file,
|
|
1160
|
+
metric: "fanOut",
|
|
1161
|
+
value: m.fanOut,
|
|
1162
|
+
threshold,
|
|
1163
|
+
tier: 2,
|
|
1164
|
+
severity: "warning",
|
|
1165
|
+
message: `File has ${m.fanOut} imports (threshold: ${threshold})`
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function checkFanIn(m, threshold) {
|
|
1169
|
+
if (m.fanIn <= threshold) return null;
|
|
1170
|
+
return {
|
|
1171
|
+
file: m.file,
|
|
1172
|
+
metric: "fanIn",
|
|
1173
|
+
value: m.fanIn,
|
|
1174
|
+
threshold,
|
|
1175
|
+
tier: 3,
|
|
1176
|
+
severity: "info",
|
|
1177
|
+
message: `File is imported by ${m.fanIn} files (threshold: ${threshold})`
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function checkCouplingRatio(m, threshold) {
|
|
1181
|
+
const totalConnections = m.fanIn + m.fanOut;
|
|
1182
|
+
if (totalConnections <= 5 || m.couplingRatio <= threshold) return null;
|
|
1183
|
+
return {
|
|
1184
|
+
file: m.file,
|
|
1185
|
+
metric: "couplingRatio",
|
|
1186
|
+
value: m.couplingRatio,
|
|
1187
|
+
threshold,
|
|
1188
|
+
tier: 2,
|
|
1189
|
+
severity: "warning",
|
|
1190
|
+
message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${threshold})`
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function checkTransitiveDepth(m, threshold) {
|
|
1194
|
+
if (m.transitiveDepth <= threshold) return null;
|
|
1195
|
+
return {
|
|
1196
|
+
file: m.file,
|
|
1197
|
+
metric: "transitiveDependencyDepth",
|
|
1198
|
+
value: m.transitiveDepth,
|
|
1199
|
+
threshold,
|
|
1200
|
+
tier: 3,
|
|
1201
|
+
severity: "info",
|
|
1202
|
+
message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${threshold})`
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function checkMetricViolations(m, thresholds) {
|
|
1206
|
+
const candidates = [
|
|
1207
|
+
checkFanOut(m, thresholds.fanOut.warn),
|
|
1208
|
+
checkFanIn(m, thresholds.fanIn.info),
|
|
1209
|
+
checkCouplingRatio(m, thresholds.couplingRatio.warn),
|
|
1210
|
+
checkTransitiveDepth(m, thresholds.transitiveDependencyDepth.info)
|
|
1211
|
+
];
|
|
1212
|
+
return candidates.filter((v) => v !== null);
|
|
1213
|
+
}
|
|
1214
|
+
function checkViolations(metrics, config) {
|
|
1215
|
+
const thresholds = resolveThresholds2(config);
|
|
1149
1216
|
const violations = [];
|
|
1150
1217
|
for (const m of metrics) {
|
|
1151
|
-
|
|
1152
|
-
violations.push({
|
|
1153
|
-
file: m.file,
|
|
1154
|
-
metric: "fanOut",
|
|
1155
|
-
value: m.fanOut,
|
|
1156
|
-
threshold: thresholds.fanOut.warn,
|
|
1157
|
-
tier: 2,
|
|
1158
|
-
severity: "warning",
|
|
1159
|
-
message: `File has ${m.fanOut} imports (threshold: ${thresholds.fanOut.warn})`
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
if (thresholds.fanIn.info !== void 0 && m.fanIn > thresholds.fanIn.info) {
|
|
1163
|
-
violations.push({
|
|
1164
|
-
file: m.file,
|
|
1165
|
-
metric: "fanIn",
|
|
1166
|
-
value: m.fanIn,
|
|
1167
|
-
threshold: thresholds.fanIn.info,
|
|
1168
|
-
tier: 3,
|
|
1169
|
-
severity: "info",
|
|
1170
|
-
message: `File is imported by ${m.fanIn} files (threshold: ${thresholds.fanIn.info})`
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
const totalConnections = m.fanIn + m.fanOut;
|
|
1174
|
-
if (totalConnections > 5 && thresholds.couplingRatio.warn !== void 0 && m.couplingRatio > thresholds.couplingRatio.warn) {
|
|
1175
|
-
violations.push({
|
|
1176
|
-
file: m.file,
|
|
1177
|
-
metric: "couplingRatio",
|
|
1178
|
-
value: m.couplingRatio,
|
|
1179
|
-
threshold: thresholds.couplingRatio.warn,
|
|
1180
|
-
tier: 2,
|
|
1181
|
-
severity: "warning",
|
|
1182
|
-
message: `Coupling ratio is ${m.couplingRatio.toFixed(2)} (threshold: ${thresholds.couplingRatio.warn})`
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
if (thresholds.transitiveDependencyDepth.info !== void 0 && m.transitiveDepth > thresholds.transitiveDependencyDepth.info) {
|
|
1186
|
-
violations.push({
|
|
1187
|
-
file: m.file,
|
|
1188
|
-
metric: "transitiveDependencyDepth",
|
|
1189
|
-
value: m.transitiveDepth,
|
|
1190
|
-
threshold: thresholds.transitiveDependencyDepth.info,
|
|
1191
|
-
tier: 3,
|
|
1192
|
-
severity: "info",
|
|
1193
|
-
message: `Transitive dependency depth is ${m.transitiveDepth} (threshold: ${thresholds.transitiveDependencyDepth.info})`
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1218
|
+
violations.push(...checkMetricViolations(m, thresholds));
|
|
1196
1219
|
}
|
|
1197
1220
|
return violations;
|
|
1198
1221
|
}
|
|
@@ -1224,6 +1247,38 @@ async function detectCouplingViolations(snapshot, config, graphData) {
|
|
|
1224
1247
|
}
|
|
1225
1248
|
|
|
1226
1249
|
// src/architecture/collectors/coupling.ts
|
|
1250
|
+
function buildCouplingSnapshot(files, rootDir) {
|
|
1251
|
+
return {
|
|
1252
|
+
files: files.map((f) => ({
|
|
1253
|
+
path: f,
|
|
1254
|
+
ast: { type: "Program", body: null, language: "typescript" },
|
|
1255
|
+
imports: [],
|
|
1256
|
+
exports: [],
|
|
1257
|
+
internalSymbols: [],
|
|
1258
|
+
jsDocComments: []
|
|
1259
|
+
})),
|
|
1260
|
+
dependencyGraph: { nodes: [], edges: [] },
|
|
1261
|
+
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
1262
|
+
docs: [],
|
|
1263
|
+
codeReferences: [],
|
|
1264
|
+
entryPoints: [],
|
|
1265
|
+
rootDir,
|
|
1266
|
+
config: { rootDir, analyze: {} },
|
|
1267
|
+
buildTime: 0
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function mapCouplingViolations(couplingViolations, rootDir, category) {
|
|
1271
|
+
return couplingViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
|
|
1272
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1273
|
+
return {
|
|
1274
|
+
id: violationId(relFile, category ?? "", v.metric),
|
|
1275
|
+
file: relFile,
|
|
1276
|
+
category,
|
|
1277
|
+
detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
|
|
1278
|
+
severity: v.severity
|
|
1279
|
+
};
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1227
1282
|
var CouplingCollector = class {
|
|
1228
1283
|
category = "coupling";
|
|
1229
1284
|
getRules(_config, _rootDir) {
|
|
@@ -1239,24 +1294,7 @@ var CouplingCollector = class {
|
|
|
1239
1294
|
}
|
|
1240
1295
|
async collect(_config, rootDir) {
|
|
1241
1296
|
const files = await findFiles("**/*.ts", rootDir);
|
|
1242
|
-
const snapshot =
|
|
1243
|
-
files: files.map((f) => ({
|
|
1244
|
-
path: f,
|
|
1245
|
-
ast: { type: "Program", body: null, language: "typescript" },
|
|
1246
|
-
imports: [],
|
|
1247
|
-
exports: [],
|
|
1248
|
-
internalSymbols: [],
|
|
1249
|
-
jsDocComments: []
|
|
1250
|
-
})),
|
|
1251
|
-
dependencyGraph: { nodes: [], edges: [] },
|
|
1252
|
-
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
1253
|
-
docs: [],
|
|
1254
|
-
codeReferences: [],
|
|
1255
|
-
entryPoints: [],
|
|
1256
|
-
rootDir,
|
|
1257
|
-
config: { rootDir, analyze: {} },
|
|
1258
|
-
buildTime: 0
|
|
1259
|
-
};
|
|
1297
|
+
const snapshot = buildCouplingSnapshot(files, rootDir);
|
|
1260
1298
|
const result = await detectCouplingViolations(snapshot);
|
|
1261
1299
|
if (!result.ok) {
|
|
1262
1300
|
return [
|
|
@@ -1270,20 +1308,7 @@ var CouplingCollector = class {
|
|
|
1270
1308
|
];
|
|
1271
1309
|
}
|
|
1272
1310
|
const { violations: couplingViolations, stats } = result.value;
|
|
1273
|
-
const
|
|
1274
|
-
(v) => v.severity === "error" || v.severity === "warning"
|
|
1275
|
-
);
|
|
1276
|
-
const violations = filtered.map((v) => {
|
|
1277
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
1278
|
-
const idDetail = `${v.metric}`;
|
|
1279
|
-
return {
|
|
1280
|
-
id: violationId(relFile, this.category, idDetail),
|
|
1281
|
-
file: relFile,
|
|
1282
|
-
category: this.category,
|
|
1283
|
-
detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
|
|
1284
|
-
severity: v.severity
|
|
1285
|
-
};
|
|
1286
|
-
});
|
|
1311
|
+
const violations = mapCouplingViolations(couplingViolations, rootDir, this.category);
|
|
1287
1312
|
return [
|
|
1288
1313
|
{
|
|
1289
1314
|
category: this.category,
|
|
@@ -1297,6 +1322,30 @@ var CouplingCollector = class {
|
|
|
1297
1322
|
};
|
|
1298
1323
|
|
|
1299
1324
|
// src/architecture/collectors/forbidden-imports.ts
|
|
1325
|
+
function makeForbiddenStubParser() {
|
|
1326
|
+
return {
|
|
1327
|
+
name: "typescript",
|
|
1328
|
+
extensions: [".ts", ".tsx"],
|
|
1329
|
+
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
1330
|
+
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
1331
|
+
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
1332
|
+
health: async () => ({ ok: true, value: { available: true } })
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function mapForbiddenImportViolations(forbidden, rootDir, category) {
|
|
1336
|
+
return forbidden.map((v) => {
|
|
1337
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1338
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
1339
|
+
const detail = `forbidden import: ${relFile} -> ${relImport}`;
|
|
1340
|
+
return {
|
|
1341
|
+
id: violationId(relFile, category ?? "", detail),
|
|
1342
|
+
file: relFile,
|
|
1343
|
+
category,
|
|
1344
|
+
detail,
|
|
1345
|
+
severity: "error"
|
|
1346
|
+
};
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1300
1349
|
var ForbiddenImportCollector = class {
|
|
1301
1350
|
category = "forbidden-imports";
|
|
1302
1351
|
getRules(_config, _rootDir) {
|
|
@@ -1311,18 +1360,10 @@ var ForbiddenImportCollector = class {
|
|
|
1311
1360
|
];
|
|
1312
1361
|
}
|
|
1313
1362
|
async collect(_config, rootDir) {
|
|
1314
|
-
const stubParser = {
|
|
1315
|
-
name: "typescript",
|
|
1316
|
-
extensions: [".ts", ".tsx"],
|
|
1317
|
-
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "" } }),
|
|
1318
|
-
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
1319
|
-
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "" } }),
|
|
1320
|
-
health: async () => ({ ok: true, value: { available: true } })
|
|
1321
|
-
};
|
|
1322
1363
|
const result = await validateDependencies({
|
|
1323
1364
|
layers: [],
|
|
1324
1365
|
rootDir,
|
|
1325
|
-
parser:
|
|
1366
|
+
parser: makeForbiddenStubParser(),
|
|
1326
1367
|
fallbackBehavior: "skip"
|
|
1327
1368
|
});
|
|
1328
1369
|
if (!result.ok) {
|
|
@@ -1336,91 +1377,94 @@ var ForbiddenImportCollector = class {
|
|
|
1336
1377
|
}
|
|
1337
1378
|
];
|
|
1338
1379
|
}
|
|
1339
|
-
const
|
|
1340
|
-
(v) => v.reason === "FORBIDDEN_IMPORT"
|
|
1380
|
+
const violations = mapForbiddenImportViolations(
|
|
1381
|
+
result.value.violations.filter((v) => v.reason === "FORBIDDEN_IMPORT"),
|
|
1382
|
+
rootDir,
|
|
1383
|
+
this.category
|
|
1341
1384
|
);
|
|
1342
|
-
|
|
1343
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
1344
|
-
const relImport = relativePosix(rootDir, v.imports);
|
|
1345
|
-
const detail = `forbidden import: ${relFile} -> ${relImport}`;
|
|
1346
|
-
return {
|
|
1347
|
-
id: violationId(relFile, this.category, detail),
|
|
1348
|
-
file: relFile,
|
|
1349
|
-
category: this.category,
|
|
1350
|
-
detail,
|
|
1351
|
-
severity: "error"
|
|
1352
|
-
};
|
|
1353
|
-
});
|
|
1354
|
-
return [
|
|
1355
|
-
{
|
|
1356
|
-
category: this.category,
|
|
1357
|
-
scope: "project",
|
|
1358
|
-
value: violations.length,
|
|
1359
|
-
violations
|
|
1360
|
-
}
|
|
1361
|
-
];
|
|
1385
|
+
return [{ category: this.category, scope: "project", value: violations.length, violations }];
|
|
1362
1386
|
}
|
|
1363
1387
|
};
|
|
1364
1388
|
|
|
1365
1389
|
// src/architecture/collectors/module-size.ts
|
|
1366
1390
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
1367
1391
|
import { join as join2 } from "path";
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1392
|
+
function isSkippedEntry(name) {
|
|
1393
|
+
return name.startsWith(".") || name === "node_modules" || name === "dist";
|
|
1394
|
+
}
|
|
1395
|
+
function isTsSourceFile(name) {
|
|
1396
|
+
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
|
|
1397
|
+
if (name.endsWith(".test.ts") || name.endsWith(".test.tsx") || name.endsWith(".spec.ts"))
|
|
1398
|
+
return false;
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1401
|
+
async function countLoc(filePath) {
|
|
1402
|
+
try {
|
|
1403
|
+
const content = await readFile3(filePath, "utf-8");
|
|
1404
|
+
return content.split("\n").filter((line) => line.trim().length > 0).length;
|
|
1405
|
+
} catch {
|
|
1406
|
+
return 0;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async function buildModuleStats(rootDir, dir, tsFiles) {
|
|
1410
|
+
let totalLoc = 0;
|
|
1411
|
+
for (const f of tsFiles) {
|
|
1412
|
+
totalLoc += await countLoc(f);
|
|
1413
|
+
}
|
|
1414
|
+
return {
|
|
1415
|
+
modulePath: relativePosix(rootDir, dir),
|
|
1416
|
+
fileCount: tsFiles.length,
|
|
1417
|
+
totalLoc,
|
|
1418
|
+
files: tsFiles.map((f) => relativePosix(rootDir, f))
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
async function scanDir(rootDir, dir, modules) {
|
|
1422
|
+
let entries;
|
|
1423
|
+
try {
|
|
1424
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1425
|
+
} catch {
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const tsFiles = [];
|
|
1429
|
+
const subdirs = [];
|
|
1430
|
+
for (const entry of entries) {
|
|
1431
|
+
if (isSkippedEntry(entry.name)) continue;
|
|
1432
|
+
const fullPath = join2(dir, entry.name);
|
|
1433
|
+
if (entry.isDirectory()) {
|
|
1434
|
+
subdirs.push(fullPath);
|
|
1435
|
+
continue;
|
|
1405
1436
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1437
|
+
if (entry.isFile() && isTsSourceFile(entry.name)) {
|
|
1438
|
+
tsFiles.push(fullPath);
|
|
1408
1439
|
}
|
|
1409
1440
|
}
|
|
1410
|
-
|
|
1441
|
+
if (tsFiles.length > 0) {
|
|
1442
|
+
modules.push(await buildModuleStats(rootDir, dir, tsFiles));
|
|
1443
|
+
}
|
|
1444
|
+
for (const sub of subdirs) {
|
|
1445
|
+
await scanDir(rootDir, sub, modules);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async function discoverModules(rootDir) {
|
|
1449
|
+
const modules = [];
|
|
1450
|
+
await scanDir(rootDir, rootDir, modules);
|
|
1411
1451
|
return modules;
|
|
1412
1452
|
}
|
|
1453
|
+
function extractThresholds(config) {
|
|
1454
|
+
const thresholds = config.thresholds["module-size"];
|
|
1455
|
+
let maxLoc = Infinity;
|
|
1456
|
+
let maxFiles = Infinity;
|
|
1457
|
+
if (typeof thresholds === "object" && thresholds !== null) {
|
|
1458
|
+
const t = thresholds;
|
|
1459
|
+
if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
|
|
1460
|
+
if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
|
|
1461
|
+
}
|
|
1462
|
+
return { maxLoc, maxFiles };
|
|
1463
|
+
}
|
|
1413
1464
|
var ModuleSizeCollector = class {
|
|
1414
1465
|
category = "module-size";
|
|
1415
1466
|
getRules(config, _rootDir) {
|
|
1416
|
-
const
|
|
1417
|
-
let maxLoc = Infinity;
|
|
1418
|
-
let maxFiles = Infinity;
|
|
1419
|
-
if (typeof thresholds === "object" && thresholds !== null) {
|
|
1420
|
-
const t = thresholds;
|
|
1421
|
-
if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
|
|
1422
|
-
if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
|
|
1423
|
-
}
|
|
1467
|
+
const { maxLoc, maxFiles } = extractThresholds(config);
|
|
1424
1468
|
const rules = [];
|
|
1425
1469
|
if (maxLoc < Infinity) {
|
|
1426
1470
|
const desc = `Module LOC must not exceed ${maxLoc}`;
|
|
@@ -1453,14 +1497,7 @@ var ModuleSizeCollector = class {
|
|
|
1453
1497
|
}
|
|
1454
1498
|
async collect(config, rootDir) {
|
|
1455
1499
|
const modules = await discoverModules(rootDir);
|
|
1456
|
-
const
|
|
1457
|
-
let maxLoc = Infinity;
|
|
1458
|
-
let maxFiles = Infinity;
|
|
1459
|
-
if (typeof thresholds === "object" && thresholds !== null) {
|
|
1460
|
-
const t = thresholds;
|
|
1461
|
-
if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
|
|
1462
|
-
if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
|
|
1463
|
-
}
|
|
1500
|
+
const { maxLoc, maxFiles } = extractThresholds(config);
|
|
1464
1501
|
return modules.map((mod) => {
|
|
1465
1502
|
const violations = [];
|
|
1466
1503
|
if (mod.totalLoc > maxLoc) {
|
|
@@ -1510,27 +1547,33 @@ function extractImportSources(content, filePath) {
|
|
|
1510
1547
|
}
|
|
1511
1548
|
return sources;
|
|
1512
1549
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1550
|
+
function isSkippedEntry2(name) {
|
|
1551
|
+
return name.startsWith(".") || name === "node_modules" || name === "dist";
|
|
1552
|
+
}
|
|
1553
|
+
function isTsSourceFile2(name) {
|
|
1554
|
+
if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
|
|
1555
|
+
return !name.endsWith(".test.ts") && !name.endsWith(".test.tsx") && !name.endsWith(".spec.ts");
|
|
1556
|
+
}
|
|
1557
|
+
async function scanDir2(d, results) {
|
|
1558
|
+
let entries;
|
|
1559
|
+
try {
|
|
1560
|
+
entries = await readdir2(d, { withFileTypes: true });
|
|
1561
|
+
} catch {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
for (const entry of entries) {
|
|
1565
|
+
if (isSkippedEntry2(entry.name)) continue;
|
|
1566
|
+
const fullPath = join3(d, entry.name);
|
|
1567
|
+
if (entry.isDirectory()) {
|
|
1568
|
+
await scanDir2(fullPath, results);
|
|
1569
|
+
} else if (entry.isFile() && isTsSourceFile2(entry.name)) {
|
|
1570
|
+
results.push(fullPath);
|
|
1531
1571
|
}
|
|
1532
1572
|
}
|
|
1533
|
-
|
|
1573
|
+
}
|
|
1574
|
+
async function collectTsFiles(dir) {
|
|
1575
|
+
const results = [];
|
|
1576
|
+
await scanDir2(dir, results);
|
|
1534
1577
|
return results;
|
|
1535
1578
|
}
|
|
1536
1579
|
function computeLongestChain(file, graph, visited, memo) {
|
|
@@ -1561,34 +1604,42 @@ var DepDepthCollector = class {
|
|
|
1561
1604
|
}
|
|
1562
1605
|
];
|
|
1563
1606
|
}
|
|
1564
|
-
async
|
|
1565
|
-
const allFiles = await collectTsFiles(rootDir);
|
|
1607
|
+
async buildImportGraph(allFiles) {
|
|
1566
1608
|
const graph = /* @__PURE__ */ new Map();
|
|
1567
1609
|
const fileSet = new Set(allFiles);
|
|
1568
1610
|
for (const file of allFiles) {
|
|
1569
1611
|
try {
|
|
1570
1612
|
const content = await readFile4(file, "utf-8");
|
|
1571
|
-
|
|
1572
|
-
|
|
1613
|
+
graph.set(
|
|
1614
|
+
file,
|
|
1615
|
+
extractImportSources(content, file).filter((imp) => fileSet.has(imp))
|
|
1616
|
+
);
|
|
1573
1617
|
} catch {
|
|
1574
1618
|
graph.set(file, []);
|
|
1575
1619
|
}
|
|
1576
1620
|
}
|
|
1621
|
+
return graph;
|
|
1622
|
+
}
|
|
1623
|
+
buildModuleMap(allFiles, rootDir) {
|
|
1577
1624
|
const moduleMap = /* @__PURE__ */ new Map();
|
|
1578
1625
|
for (const file of allFiles) {
|
|
1579
1626
|
const relDir = relativePosix(rootDir, dirname3(file));
|
|
1580
1627
|
if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
|
|
1581
1628
|
moduleMap.get(relDir).push(file);
|
|
1582
1629
|
}
|
|
1630
|
+
return moduleMap;
|
|
1631
|
+
}
|
|
1632
|
+
async collect(config, rootDir) {
|
|
1633
|
+
const allFiles = await collectTsFiles(rootDir);
|
|
1634
|
+
const graph = await this.buildImportGraph(allFiles);
|
|
1635
|
+
const moduleMap = this.buildModuleMap(allFiles, rootDir);
|
|
1583
1636
|
const memo = /* @__PURE__ */ new Map();
|
|
1584
1637
|
const threshold = typeof config.thresholds["dependency-depth"] === "number" ? config.thresholds["dependency-depth"] : Infinity;
|
|
1585
1638
|
const results = [];
|
|
1586
1639
|
for (const [modulePath, files] of moduleMap) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
if (depth > longestChain) longestChain = depth;
|
|
1591
|
-
}
|
|
1640
|
+
const longestChain = files.reduce((max, file) => {
|
|
1641
|
+
return Math.max(max, computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo));
|
|
1642
|
+
}, 0);
|
|
1592
1643
|
const violations = [];
|
|
1593
1644
|
if (longestChain > threshold) {
|
|
1594
1645
|
violations.push({
|