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