@harness-engineering/core 0.13.0 → 0.14.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/dist/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/architecture/matchers.js +267 -223
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-ZHGBWFYD.mjs → chunk-BQUWXBGR.mjs} +262 -217
- package/dist/index.d.mts +177 -107
- package/dist/index.d.ts +177 -107
- package/dist/index.js +1905 -1451
- package/dist/index.mjs +1608 -1208
- package/dist/{matchers-Dj1t5vpg.d.mts → matchers-D20x48U9.d.mts} +46 -46
- package/dist/{matchers-Dj1t5vpg.d.ts → matchers-D20x48U9.d.ts} +46 -46
- package/package.json +3 -3
|
@@ -82,14 +82,10 @@ var ConstraintRuleSchema = z.object({
|
|
|
82
82
|
// forward-compat for governs edges
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
// src/architecture/collectors/circular-deps.ts
|
|
86
|
-
import { relative as relative2 } from "path";
|
|
87
|
-
|
|
88
85
|
// src/architecture/collectors/hash.ts
|
|
89
86
|
import { createHash } from "crypto";
|
|
90
87
|
function violationId(relativePath, category, normalizedDetail) {
|
|
91
|
-
const
|
|
92
|
-
const input = `${path}:${category}:${normalizedDetail}`;
|
|
88
|
+
const input = `${relativePath}:${category}:${normalizedDetail}`;
|
|
93
89
|
return createHash("sha256").update(input).digest("hex");
|
|
94
90
|
}
|
|
95
91
|
function constraintRuleId(category, scope, description) {
|
|
@@ -131,6 +127,7 @@ function resolveFileToLayer(file, layers) {
|
|
|
131
127
|
// src/shared/fs-utils.ts
|
|
132
128
|
import { access, constants, readFile } from "fs";
|
|
133
129
|
import { promisify } from "util";
|
|
130
|
+
import { relative } from "path";
|
|
134
131
|
import { glob } from "glob";
|
|
135
132
|
var accessAsync = promisify(access);
|
|
136
133
|
var readFileAsync = promisify(readFile);
|
|
@@ -153,9 +150,12 @@ async function readFileContent(path) {
|
|
|
153
150
|
async function findFiles(pattern, cwd = process.cwd()) {
|
|
154
151
|
return glob(pattern, { cwd, absolute: true });
|
|
155
152
|
}
|
|
153
|
+
function relativePosix(from, to) {
|
|
154
|
+
return relative(from, to).replaceAll("\\", "/");
|
|
155
|
+
}
|
|
156
156
|
|
|
157
157
|
// src/constraints/dependencies.ts
|
|
158
|
-
import { dirname, resolve
|
|
158
|
+
import { dirname, resolve } from "path";
|
|
159
159
|
function resolveImportPath(importSource, fromFile, _rootDir) {
|
|
160
160
|
if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
|
|
161
161
|
return null;
|
|
@@ -207,8 +207,8 @@ async function buildDependencyGraph(files, parser, graphDependencyData) {
|
|
|
207
207
|
function checkLayerViolations(graph, layers, rootDir) {
|
|
208
208
|
const violations = [];
|
|
209
209
|
for (const edge of graph.edges) {
|
|
210
|
-
const fromRelative =
|
|
211
|
-
const toRelative =
|
|
210
|
+
const fromRelative = relativePosix(rootDir, edge.from);
|
|
211
|
+
const toRelative = relativePosix(rootDir, edge.to);
|
|
212
212
|
const fromLayer = resolveFileToLayer(fromRelative, layers);
|
|
213
213
|
const toLayer = resolveFileToLayer(toRelative, layers);
|
|
214
214
|
if (!fromLayer || !toLayer) continue;
|
|
@@ -292,65 +292,71 @@ async function validateDependencies(config) {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
// src/constraints/circular-deps.ts
|
|
295
|
-
function
|
|
296
|
-
const nodeMap = /* @__PURE__ */ new Map();
|
|
297
|
-
const stack = [];
|
|
298
|
-
const sccs = [];
|
|
299
|
-
let index = 0;
|
|
295
|
+
function buildAdjacencyList(graph) {
|
|
300
296
|
const adjacency = /* @__PURE__ */ new Map();
|
|
297
|
+
const nodeSet = new Set(graph.nodes);
|
|
301
298
|
for (const node of graph.nodes) {
|
|
302
299
|
adjacency.set(node, []);
|
|
303
300
|
}
|
|
304
301
|
for (const edge of graph.edges) {
|
|
305
302
|
const neighbors = adjacency.get(edge.from);
|
|
306
|
-
if (neighbors &&
|
|
303
|
+
if (neighbors && nodeSet.has(edge.to)) {
|
|
307
304
|
neighbors.push(edge.to);
|
|
308
305
|
}
|
|
309
306
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
307
|
+
return adjacency;
|
|
308
|
+
}
|
|
309
|
+
function isCyclicSCC(scc, adjacency) {
|
|
310
|
+
if (scc.length > 1) return true;
|
|
311
|
+
if (scc.length === 1) {
|
|
312
|
+
const selfNode = scc[0];
|
|
313
|
+
const selfNeighbors = adjacency.get(selfNode) ?? [];
|
|
314
|
+
return selfNeighbors.includes(selfNode);
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
function processNeighbors(node, neighbors, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
319
|
+
for (const neighbor of neighbors) {
|
|
320
|
+
const neighborData = nodeMap.get(neighbor);
|
|
321
|
+
if (!neighborData) {
|
|
322
|
+
strongConnectImpl(neighbor, nodeMap, stack, adjacency, sccs, indexRef);
|
|
323
|
+
const nodeData = nodeMap.get(node);
|
|
324
|
+
const updatedNeighborData = nodeMap.get(neighbor);
|
|
325
|
+
nodeData.lowlink = Math.min(nodeData.lowlink, updatedNeighborData.lowlink);
|
|
326
|
+
} else if (neighborData.onStack) {
|
|
327
|
+
const nodeData = nodeMap.get(node);
|
|
328
|
+
nodeData.lowlink = Math.min(nodeData.lowlink, neighborData.index);
|
|
330
329
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
333
|
+
nodeMap.set(node, { index: indexRef.value, lowlink: indexRef.value, onStack: true });
|
|
334
|
+
indexRef.value++;
|
|
335
|
+
stack.push(node);
|
|
336
|
+
processNeighbors(node, adjacency.get(node) ?? [], nodeMap, stack, adjacency, sccs, indexRef);
|
|
337
|
+
const nodeData = nodeMap.get(node);
|
|
338
|
+
if (nodeData.lowlink === nodeData.index) {
|
|
339
|
+
const scc = [];
|
|
340
|
+
let w;
|
|
341
|
+
do {
|
|
342
|
+
w = stack.pop();
|
|
343
|
+
nodeMap.get(w).onStack = false;
|
|
344
|
+
scc.push(w);
|
|
345
|
+
} while (w !== node);
|
|
346
|
+
if (isCyclicSCC(scc, adjacency)) {
|
|
347
|
+
sccs.push(scc);
|
|
349
348
|
}
|
|
350
349
|
}
|
|
350
|
+
}
|
|
351
|
+
function tarjanSCC(graph) {
|
|
352
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
353
|
+
const stack = [];
|
|
354
|
+
const sccs = [];
|
|
355
|
+
const indexRef = { value: 0 };
|
|
356
|
+
const adjacency = buildAdjacencyList(graph);
|
|
351
357
|
for (const node of graph.nodes) {
|
|
352
358
|
if (!nodeMap.has(node)) {
|
|
353
|
-
|
|
359
|
+
strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef);
|
|
354
360
|
}
|
|
355
361
|
}
|
|
356
362
|
return sccs;
|
|
@@ -438,8 +444,8 @@ var CircularDepsCollector = class {
|
|
|
438
444
|
}
|
|
439
445
|
const { cycles, largestCycle } = result.value;
|
|
440
446
|
const violations = cycles.map((cycle) => {
|
|
441
|
-
const cyclePath = cycle.cycle.map((f) =>
|
|
442
|
-
const firstFile =
|
|
447
|
+
const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
|
|
448
|
+
const firstFile = relativePosix(rootDir, cycle.cycle[0]);
|
|
443
449
|
return {
|
|
444
450
|
id: violationId(firstFile, this.category, cyclePath),
|
|
445
451
|
file: firstFile,
|
|
@@ -460,7 +466,6 @@ var CircularDepsCollector = class {
|
|
|
460
466
|
};
|
|
461
467
|
|
|
462
468
|
// src/architecture/collectors/layer-violations.ts
|
|
463
|
-
import { relative as relative3 } from "path";
|
|
464
469
|
var LayerViolationCollector = class {
|
|
465
470
|
category = "layer-violations";
|
|
466
471
|
getRules(_config, _rootDir) {
|
|
@@ -504,8 +509,8 @@ var LayerViolationCollector = class {
|
|
|
504
509
|
(v) => v.reason === "WRONG_LAYER"
|
|
505
510
|
);
|
|
506
511
|
const violations = layerViolations.map((v) => {
|
|
507
|
-
const relFile =
|
|
508
|
-
const relImport =
|
|
512
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
513
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
509
514
|
const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
|
|
510
515
|
return {
|
|
511
516
|
id: violationId(relFile, this.category, detail),
|
|
@@ -621,6 +626,31 @@ function aggregateByCategory(results) {
|
|
|
621
626
|
}
|
|
622
627
|
return map;
|
|
623
628
|
}
|
|
629
|
+
function classifyViolations(violations, baselineViolationIds) {
|
|
630
|
+
const newViolations = [];
|
|
631
|
+
const preExisting = [];
|
|
632
|
+
for (const violation of violations) {
|
|
633
|
+
if (baselineViolationIds.has(violation.id)) {
|
|
634
|
+
preExisting.push(violation.id);
|
|
635
|
+
} else {
|
|
636
|
+
newViolations.push(violation);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return { newViolations, preExisting };
|
|
640
|
+
}
|
|
641
|
+
function findResolvedViolations(baselineCategory, currentViolationIds) {
|
|
642
|
+
if (!baselineCategory) return [];
|
|
643
|
+
return baselineCategory.violationIds.filter((id) => !currentViolationIds.has(id));
|
|
644
|
+
}
|
|
645
|
+
function collectOrphanedBaselineViolations(baseline, visitedCategories) {
|
|
646
|
+
const resolved = [];
|
|
647
|
+
for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
|
|
648
|
+
if (!visitedCategories.has(category) && baselineCategory) {
|
|
649
|
+
resolved.push(...baselineCategory.violationIds);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return resolved;
|
|
653
|
+
}
|
|
624
654
|
function diff(current, baseline) {
|
|
625
655
|
const aggregated = aggregateByCategory(current);
|
|
626
656
|
const newViolations = [];
|
|
@@ -633,21 +663,11 @@ function diff(current, baseline) {
|
|
|
633
663
|
const baselineCategory = baseline.metrics[category];
|
|
634
664
|
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
635
665
|
const baselineValue = baselineCategory?.value ?? 0;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
} else {
|
|
640
|
-
newViolations.push(violation);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
666
|
+
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
667
|
+
newViolations.push(...classified.newViolations);
|
|
668
|
+
preExisting.push(...classified.preExisting);
|
|
643
669
|
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
644
|
-
|
|
645
|
-
for (const id of baselineCategory.violationIds) {
|
|
646
|
-
if (!currentViolationIds.has(id)) {
|
|
647
|
-
resolvedViolations.push(id);
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
}
|
|
670
|
+
resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
651
671
|
if (baselineCategory && agg.value > baselineValue) {
|
|
652
672
|
regressions.push({
|
|
653
673
|
category,
|
|
@@ -657,16 +677,9 @@ function diff(current, baseline) {
|
|
|
657
677
|
});
|
|
658
678
|
}
|
|
659
679
|
}
|
|
660
|
-
|
|
661
|
-
if (!visitedCategories.has(category) && baselineCategory) {
|
|
662
|
-
for (const id of baselineCategory.violationIds) {
|
|
663
|
-
resolvedViolations.push(id);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
const passed = newViolations.length === 0 && regressions.length === 0;
|
|
680
|
+
resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
668
681
|
return {
|
|
669
|
-
passed,
|
|
682
|
+
passed: newViolations.length === 0 && regressions.length === 0,
|
|
670
683
|
newViolations,
|
|
671
684
|
resolvedViolations,
|
|
672
685
|
preExisting,
|
|
@@ -674,9 +687,6 @@ function diff(current, baseline) {
|
|
|
674
687
|
};
|
|
675
688
|
}
|
|
676
689
|
|
|
677
|
-
// src/architecture/collectors/complexity.ts
|
|
678
|
-
import { relative as relative4 } from "path";
|
|
679
|
-
|
|
680
690
|
// src/entropy/detectors/complexity.ts
|
|
681
691
|
import { readFile as readFile2 } from "fs/promises";
|
|
682
692
|
var DEFAULT_THRESHOLDS = {
|
|
@@ -687,22 +697,22 @@ var DEFAULT_THRESHOLDS = {
|
|
|
687
697
|
fileLength: { info: 300 },
|
|
688
698
|
hotspotPercentile: { error: 95 }
|
|
689
699
|
};
|
|
700
|
+
var FUNCTION_PATTERNS = [
|
|
701
|
+
// function declarations: function name(params) {
|
|
702
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
703
|
+
// method declarations: name(params) {
|
|
704
|
+
/^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
705
|
+
// arrow functions assigned to const/let/var: const name = (params) =>
|
|
706
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
707
|
+
// arrow functions assigned to const/let/var with single param: const name = param =>
|
|
708
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
|
|
709
|
+
];
|
|
690
710
|
function extractFunctions(content) {
|
|
691
711
|
const functions = [];
|
|
692
712
|
const lines = content.split("\n");
|
|
693
|
-
const patterns = [
|
|
694
|
-
// function declarations: function name(params) {
|
|
695
|
-
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
696
|
-
// method declarations: name(params) {
|
|
697
|
-
/^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
698
|
-
// arrow functions assigned to const/let/var: const name = (params) =>
|
|
699
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
700
|
-
// arrow functions assigned to const/let/var with single param: const name = param =>
|
|
701
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
|
|
702
|
-
];
|
|
703
713
|
for (let i = 0; i < lines.length; i++) {
|
|
704
714
|
const line = lines[i];
|
|
705
|
-
for (const pattern of
|
|
715
|
+
for (const pattern of FUNCTION_PATTERNS) {
|
|
706
716
|
const match = line.match(pattern);
|
|
707
717
|
if (match) {
|
|
708
718
|
const name = match[1] ?? "anonymous";
|
|
@@ -791,26 +801,155 @@ function computeNestingDepth(body) {
|
|
|
791
801
|
}
|
|
792
802
|
return maxDepth;
|
|
793
803
|
}
|
|
794
|
-
|
|
795
|
-
const
|
|
796
|
-
|
|
804
|
+
function resolveThresholds(config) {
|
|
805
|
+
const userThresholds = config?.thresholds;
|
|
806
|
+
if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
|
|
807
|
+
return {
|
|
797
808
|
cyclomaticComplexity: {
|
|
798
|
-
|
|
799
|
-
|
|
809
|
+
...DEFAULT_THRESHOLDS.cyclomaticComplexity,
|
|
810
|
+
...stripUndefined(userThresholds.cyclomaticComplexity)
|
|
800
811
|
},
|
|
801
812
|
nestingDepth: {
|
|
802
|
-
|
|
813
|
+
...DEFAULT_THRESHOLDS.nestingDepth,
|
|
814
|
+
...stripUndefined(userThresholds.nestingDepth)
|
|
803
815
|
},
|
|
804
816
|
functionLength: {
|
|
805
|
-
|
|
817
|
+
...DEFAULT_THRESHOLDS.functionLength,
|
|
818
|
+
...stripUndefined(userThresholds.functionLength)
|
|
806
819
|
},
|
|
807
820
|
parameterCount: {
|
|
808
|
-
|
|
821
|
+
...DEFAULT_THRESHOLDS.parameterCount,
|
|
822
|
+
...stripUndefined(userThresholds.parameterCount)
|
|
809
823
|
},
|
|
810
|
-
fileLength: {
|
|
811
|
-
|
|
812
|
-
|
|
824
|
+
fileLength: { ...DEFAULT_THRESHOLDS.fileLength, ...stripUndefined(userThresholds.fileLength) }
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function stripUndefined(obj) {
|
|
828
|
+
if (!obj) return {};
|
|
829
|
+
const result = {};
|
|
830
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
831
|
+
if (val !== void 0) result[key] = val;
|
|
832
|
+
}
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
function checkFileLengthViolation(filePath, lineCount, threshold) {
|
|
836
|
+
if (lineCount <= threshold) return null;
|
|
837
|
+
return {
|
|
838
|
+
file: filePath,
|
|
839
|
+
function: "<file>",
|
|
840
|
+
line: 1,
|
|
841
|
+
metric: "fileLength",
|
|
842
|
+
value: lineCount,
|
|
843
|
+
threshold,
|
|
844
|
+
tier: 3,
|
|
845
|
+
severity: "info",
|
|
846
|
+
message: `File has ${lineCount} lines (threshold: ${threshold})`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function checkCyclomaticComplexity(filePath, fn, thresholds) {
|
|
850
|
+
const complexity = computeCyclomaticComplexity(fn.body);
|
|
851
|
+
if (complexity > thresholds.error) {
|
|
852
|
+
return {
|
|
853
|
+
file: filePath,
|
|
854
|
+
function: fn.name,
|
|
855
|
+
line: fn.line,
|
|
856
|
+
metric: "cyclomaticComplexity",
|
|
857
|
+
value: complexity,
|
|
858
|
+
threshold: thresholds.error,
|
|
859
|
+
tier: 1,
|
|
860
|
+
severity: "error",
|
|
861
|
+
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.error})`
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
if (complexity > thresholds.warn) {
|
|
865
|
+
return {
|
|
866
|
+
file: filePath,
|
|
867
|
+
function: fn.name,
|
|
868
|
+
line: fn.line,
|
|
869
|
+
metric: "cyclomaticComplexity",
|
|
870
|
+
value: complexity,
|
|
871
|
+
threshold: thresholds.warn,
|
|
872
|
+
tier: 2,
|
|
873
|
+
severity: "warning",
|
|
874
|
+
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.warn})`
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
function checkNestingDepth(filePath, fn, threshold) {
|
|
880
|
+
const depth = computeNestingDepth(fn.body);
|
|
881
|
+
if (depth <= threshold) return null;
|
|
882
|
+
return {
|
|
883
|
+
file: filePath,
|
|
884
|
+
function: fn.name,
|
|
885
|
+
line: fn.line,
|
|
886
|
+
metric: "nestingDepth",
|
|
887
|
+
value: depth,
|
|
888
|
+
threshold,
|
|
889
|
+
tier: 2,
|
|
890
|
+
severity: "warning",
|
|
891
|
+
message: `Function "${fn.name}" has nesting depth of ${depth} (threshold: ${threshold})`
|
|
813
892
|
};
|
|
893
|
+
}
|
|
894
|
+
function checkFunctionLength(filePath, fn, threshold) {
|
|
895
|
+
const fnLength = fn.endLine - fn.startLine + 1;
|
|
896
|
+
if (fnLength <= threshold) return null;
|
|
897
|
+
return {
|
|
898
|
+
file: filePath,
|
|
899
|
+
function: fn.name,
|
|
900
|
+
line: fn.line,
|
|
901
|
+
metric: "functionLength",
|
|
902
|
+
value: fnLength,
|
|
903
|
+
threshold,
|
|
904
|
+
tier: 2,
|
|
905
|
+
severity: "warning",
|
|
906
|
+
message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${threshold})`
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function checkParameterCount(filePath, fn, threshold) {
|
|
910
|
+
if (fn.params <= threshold) return null;
|
|
911
|
+
return {
|
|
912
|
+
file: filePath,
|
|
913
|
+
function: fn.name,
|
|
914
|
+
line: fn.line,
|
|
915
|
+
metric: "parameterCount",
|
|
916
|
+
value: fn.params,
|
|
917
|
+
threshold,
|
|
918
|
+
tier: 2,
|
|
919
|
+
severity: "warning",
|
|
920
|
+
message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${threshold})`
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
function checkHotspot(filePath, fn, graphData) {
|
|
924
|
+
const hotspot = graphData.hotspots.find((h) => h.file === filePath && h.function === fn.name);
|
|
925
|
+
if (!hotspot || hotspot.hotspotScore <= graphData.percentile95Score) return null;
|
|
926
|
+
return {
|
|
927
|
+
file: filePath,
|
|
928
|
+
function: fn.name,
|
|
929
|
+
line: fn.line,
|
|
930
|
+
metric: "hotspotScore",
|
|
931
|
+
value: hotspot.hotspotScore,
|
|
932
|
+
threshold: graphData.percentile95Score,
|
|
933
|
+
tier: 1,
|
|
934
|
+
severity: "error",
|
|
935
|
+
message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function collectFunctionViolations(filePath, fn, thresholds, graphData) {
|
|
939
|
+
const checks = [
|
|
940
|
+
checkCyclomaticComplexity(filePath, fn, thresholds.cyclomaticComplexity),
|
|
941
|
+
checkNestingDepth(filePath, fn, thresholds.nestingDepth.warn),
|
|
942
|
+
checkFunctionLength(filePath, fn, thresholds.functionLength.warn),
|
|
943
|
+
checkParameterCount(filePath, fn, thresholds.parameterCount.warn)
|
|
944
|
+
];
|
|
945
|
+
if (graphData) {
|
|
946
|
+
checks.push(checkHotspot(filePath, fn, graphData));
|
|
947
|
+
}
|
|
948
|
+
return checks.filter((v) => v !== null);
|
|
949
|
+
}
|
|
950
|
+
async function detectComplexityViolations(snapshot, config, graphData) {
|
|
951
|
+
const violations = [];
|
|
952
|
+
const thresholds = resolveThresholds(config);
|
|
814
953
|
let totalFunctions = 0;
|
|
815
954
|
for (const file of snapshot.files) {
|
|
816
955
|
let content;
|
|
@@ -820,107 +959,16 @@ async function detectComplexityViolations(snapshot, config, graphData) {
|
|
|
820
959
|
continue;
|
|
821
960
|
}
|
|
822
961
|
const lines = content.split("\n");
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
value: lines.length,
|
|
830
|
-
threshold: thresholds.fileLength.info,
|
|
831
|
-
tier: 3,
|
|
832
|
-
severity: "info",
|
|
833
|
-
message: `File has ${lines.length} lines (threshold: ${thresholds.fileLength.info})`
|
|
834
|
-
});
|
|
835
|
-
}
|
|
962
|
+
const fileLenViolation = checkFileLengthViolation(
|
|
963
|
+
file.path,
|
|
964
|
+
lines.length,
|
|
965
|
+
thresholds.fileLength.info
|
|
966
|
+
);
|
|
967
|
+
if (fileLenViolation) violations.push(fileLenViolation);
|
|
836
968
|
const functions = extractFunctions(content);
|
|
837
969
|
totalFunctions += functions.length;
|
|
838
970
|
for (const fn of functions) {
|
|
839
|
-
|
|
840
|
-
if (complexity > thresholds.cyclomaticComplexity.error) {
|
|
841
|
-
violations.push({
|
|
842
|
-
file: file.path,
|
|
843
|
-
function: fn.name,
|
|
844
|
-
line: fn.line,
|
|
845
|
-
metric: "cyclomaticComplexity",
|
|
846
|
-
value: complexity,
|
|
847
|
-
threshold: thresholds.cyclomaticComplexity.error,
|
|
848
|
-
tier: 1,
|
|
849
|
-
severity: "error",
|
|
850
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.cyclomaticComplexity.error})`
|
|
851
|
-
});
|
|
852
|
-
} else if (complexity > thresholds.cyclomaticComplexity.warn) {
|
|
853
|
-
violations.push({
|
|
854
|
-
file: file.path,
|
|
855
|
-
function: fn.name,
|
|
856
|
-
line: fn.line,
|
|
857
|
-
metric: "cyclomaticComplexity",
|
|
858
|
-
value: complexity,
|
|
859
|
-
threshold: thresholds.cyclomaticComplexity.warn,
|
|
860
|
-
tier: 2,
|
|
861
|
-
severity: "warning",
|
|
862
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.cyclomaticComplexity.warn})`
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
const nestingDepth = computeNestingDepth(fn.body);
|
|
866
|
-
if (nestingDepth > thresholds.nestingDepth.warn) {
|
|
867
|
-
violations.push({
|
|
868
|
-
file: file.path,
|
|
869
|
-
function: fn.name,
|
|
870
|
-
line: fn.line,
|
|
871
|
-
metric: "nestingDepth",
|
|
872
|
-
value: nestingDepth,
|
|
873
|
-
threshold: thresholds.nestingDepth.warn,
|
|
874
|
-
tier: 2,
|
|
875
|
-
severity: "warning",
|
|
876
|
-
message: `Function "${fn.name}" has nesting depth of ${nestingDepth} (threshold: ${thresholds.nestingDepth.warn})`
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
const fnLength = fn.endLine - fn.startLine + 1;
|
|
880
|
-
if (fnLength > thresholds.functionLength.warn) {
|
|
881
|
-
violations.push({
|
|
882
|
-
file: file.path,
|
|
883
|
-
function: fn.name,
|
|
884
|
-
line: fn.line,
|
|
885
|
-
metric: "functionLength",
|
|
886
|
-
value: fnLength,
|
|
887
|
-
threshold: thresholds.functionLength.warn,
|
|
888
|
-
tier: 2,
|
|
889
|
-
severity: "warning",
|
|
890
|
-
message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${thresholds.functionLength.warn})`
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
if (fn.params > thresholds.parameterCount.warn) {
|
|
894
|
-
violations.push({
|
|
895
|
-
file: file.path,
|
|
896
|
-
function: fn.name,
|
|
897
|
-
line: fn.line,
|
|
898
|
-
metric: "parameterCount",
|
|
899
|
-
value: fn.params,
|
|
900
|
-
threshold: thresholds.parameterCount.warn,
|
|
901
|
-
tier: 2,
|
|
902
|
-
severity: "warning",
|
|
903
|
-
message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${thresholds.parameterCount.warn})`
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
if (graphData) {
|
|
907
|
-
const hotspot = graphData.hotspots.find(
|
|
908
|
-
(h) => h.file === file.path && h.function === fn.name
|
|
909
|
-
);
|
|
910
|
-
if (hotspot && hotspot.hotspotScore > graphData.percentile95Score) {
|
|
911
|
-
violations.push({
|
|
912
|
-
file: file.path,
|
|
913
|
-
function: fn.name,
|
|
914
|
-
line: fn.line,
|
|
915
|
-
metric: "hotspotScore",
|
|
916
|
-
value: hotspot.hotspotScore,
|
|
917
|
-
threshold: graphData.percentile95Score,
|
|
918
|
-
tier: 1,
|
|
919
|
-
severity: "error",
|
|
920
|
-
message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
}
|
|
971
|
+
violations.push(...collectFunctionViolations(file.path, fn, thresholds, graphData));
|
|
924
972
|
}
|
|
925
973
|
}
|
|
926
974
|
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
@@ -1000,7 +1048,7 @@ var ComplexityCollector = class {
|
|
|
1000
1048
|
(v) => v.severity === "error" || v.severity === "warning"
|
|
1001
1049
|
);
|
|
1002
1050
|
const violations = filtered.map((v) => {
|
|
1003
|
-
const relFile =
|
|
1051
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1004
1052
|
const idDetail = `${v.metric}:${v.function}`;
|
|
1005
1053
|
return {
|
|
1006
1054
|
id: violationId(relFile, this.category, idDetail),
|
|
@@ -1025,9 +1073,6 @@ var ComplexityCollector = class {
|
|
|
1025
1073
|
}
|
|
1026
1074
|
};
|
|
1027
1075
|
|
|
1028
|
-
// src/architecture/collectors/coupling.ts
|
|
1029
|
-
import { relative as relative5 } from "path";
|
|
1030
|
-
|
|
1031
1076
|
// src/entropy/detectors/coupling.ts
|
|
1032
1077
|
var DEFAULT_THRESHOLDS2 = {
|
|
1033
1078
|
fanOut: { warn: 15 },
|
|
@@ -1229,7 +1274,7 @@ var CouplingCollector = class {
|
|
|
1229
1274
|
(v) => v.severity === "error" || v.severity === "warning"
|
|
1230
1275
|
);
|
|
1231
1276
|
const violations = filtered.map((v) => {
|
|
1232
|
-
const relFile =
|
|
1277
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1233
1278
|
const idDetail = `${v.metric}`;
|
|
1234
1279
|
return {
|
|
1235
1280
|
id: violationId(relFile, this.category, idDetail),
|
|
@@ -1252,7 +1297,6 @@ var CouplingCollector = class {
|
|
|
1252
1297
|
};
|
|
1253
1298
|
|
|
1254
1299
|
// src/architecture/collectors/forbidden-imports.ts
|
|
1255
|
-
import { relative as relative6 } from "path";
|
|
1256
1300
|
var ForbiddenImportCollector = class {
|
|
1257
1301
|
category = "forbidden-imports";
|
|
1258
1302
|
getRules(_config, _rootDir) {
|
|
@@ -1296,8 +1340,8 @@ var ForbiddenImportCollector = class {
|
|
|
1296
1340
|
(v) => v.reason === "FORBIDDEN_IMPORT"
|
|
1297
1341
|
);
|
|
1298
1342
|
const violations = forbidden.map((v) => {
|
|
1299
|
-
const relFile =
|
|
1300
|
-
const relImport =
|
|
1343
|
+
const relFile = relativePosix(rootDir, v.file);
|
|
1344
|
+
const relImport = relativePosix(rootDir, v.imports);
|
|
1301
1345
|
const detail = `forbidden import: ${relFile} -> ${relImport}`;
|
|
1302
1346
|
return {
|
|
1303
1347
|
id: violationId(relFile, this.category, detail),
|
|
@@ -1320,7 +1364,7 @@ var ForbiddenImportCollector = class {
|
|
|
1320
1364
|
|
|
1321
1365
|
// src/architecture/collectors/module-size.ts
|
|
1322
1366
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
1323
|
-
import { join as join2
|
|
1367
|
+
import { join as join2 } from "path";
|
|
1324
1368
|
async function discoverModules(rootDir) {
|
|
1325
1369
|
const modules = [];
|
|
1326
1370
|
async function scanDir(dir) {
|
|
@@ -1353,10 +1397,10 @@ async function discoverModules(rootDir) {
|
|
|
1353
1397
|
}
|
|
1354
1398
|
}
|
|
1355
1399
|
modules.push({
|
|
1356
|
-
modulePath:
|
|
1400
|
+
modulePath: relativePosix(rootDir, dir),
|
|
1357
1401
|
fileCount: tsFiles.length,
|
|
1358
1402
|
totalLoc,
|
|
1359
|
-
files: tsFiles.map((f) =>
|
|
1403
|
+
files: tsFiles.map((f) => relativePosix(rootDir, f))
|
|
1360
1404
|
});
|
|
1361
1405
|
}
|
|
1362
1406
|
for (const sub of subdirs) {
|
|
@@ -1448,7 +1492,7 @@ var ModuleSizeCollector = class {
|
|
|
1448
1492
|
|
|
1449
1493
|
// src/architecture/collectors/dep-depth.ts
|
|
1450
1494
|
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
1451
|
-
import { join as join3,
|
|
1495
|
+
import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
|
|
1452
1496
|
function extractImportSources(content, filePath) {
|
|
1453
1497
|
const importRegex = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
|
|
1454
1498
|
const dynamicRegex = /import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
@@ -1532,7 +1576,7 @@ var DepDepthCollector = class {
|
|
|
1532
1576
|
}
|
|
1533
1577
|
const moduleMap = /* @__PURE__ */ new Map();
|
|
1534
1578
|
for (const file of allFiles) {
|
|
1535
|
-
const relDir =
|
|
1579
|
+
const relDir = relativePosix(rootDir, dirname3(file));
|
|
1536
1580
|
if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
|
|
1537
1581
|
moduleMap.get(relDir).push(file);
|
|
1538
1582
|
}
|
|
@@ -1790,6 +1834,7 @@ export {
|
|
|
1790
1834
|
fileExists,
|
|
1791
1835
|
readFileContent,
|
|
1792
1836
|
findFiles,
|
|
1837
|
+
relativePosix,
|
|
1793
1838
|
defineLayer,
|
|
1794
1839
|
resolveFileToLayer,
|
|
1795
1840
|
buildDependencyGraph,
|