@harness-engineering/core 0.26.4 → 0.27.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/analyzer-VY6DJVOU.mjs +8 -0
- package/dist/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/architecture/matchers.mjs +4 -1
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-BTUDWWB4.mjs +1729 -0
- package/dist/chunk-FYTJQ2ZY.mjs +1171 -0
- package/dist/chunk-IIEDD47I.mjs +125 -0
- package/dist/{chunk-JIOBXIVB.mjs → chunk-MUWJHO2S.mjs} +600 -1886
- package/dist/chunk-XKLVJIPL.mjs +261 -0
- package/dist/index.d.mts +71 -30
- package/dist/index.d.ts +71 -30
- package/dist/index.js +7327 -6794
- package/dist/index.mjs +1053 -2795
- package/dist/timeline-manager-FPYKJRHR.mjs +8 -0
- package/package.json +2 -2
- package/dist/{matchers-DSibUtbV.d.mts → matchers-OE6gO1nh.d.mts} +6 -6
- package/dist/{matchers-DSibUtbV.d.ts → matchers-OE6gO1nh.d.ts} +6 -6
|
@@ -1,131 +1,3 @@
|
|
|
1
|
-
var __defProp = Object.defineProperty;
|
|
2
|
-
var __export = (target, all) => {
|
|
3
|
-
for (var name in all)
|
|
4
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
// src/architecture/types.ts
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
var ArchMetricCategorySchema = z.enum([
|
|
10
|
-
"circular-deps",
|
|
11
|
-
"layer-violations",
|
|
12
|
-
"complexity",
|
|
13
|
-
"coupling",
|
|
14
|
-
"forbidden-imports",
|
|
15
|
-
"module-size",
|
|
16
|
-
"dependency-depth"
|
|
17
|
-
]);
|
|
18
|
-
var ViolationSchema = z.object({
|
|
19
|
-
id: z.string(),
|
|
20
|
-
// stable hash: sha256(relativePath + ':' + category + ':' + normalizedDetail)
|
|
21
|
-
file: z.string(),
|
|
22
|
-
// relative to project root
|
|
23
|
-
category: ArchMetricCategorySchema.optional(),
|
|
24
|
-
// context for baseline reporting
|
|
25
|
-
detail: z.string(),
|
|
26
|
-
// human-readable description
|
|
27
|
-
severity: z.enum(["error", "warning"])
|
|
28
|
-
});
|
|
29
|
-
var MetricResultSchema = z.object({
|
|
30
|
-
category: ArchMetricCategorySchema,
|
|
31
|
-
scope: z.string(),
|
|
32
|
-
// e.g., 'project', 'src/services', 'src/api/routes.ts'
|
|
33
|
-
value: z.number(),
|
|
34
|
-
// numeric metric (violation count, complexity score, etc.)
|
|
35
|
-
violations: z.array(ViolationSchema),
|
|
36
|
-
metadata: z.record(z.unknown()).optional()
|
|
37
|
-
});
|
|
38
|
-
var CategoryBaselineSchema = z.object({
|
|
39
|
-
value: z.number(),
|
|
40
|
-
// aggregate metric value at baseline time
|
|
41
|
-
violationIds: z.array(z.string())
|
|
42
|
-
// stable IDs of known violations (the allowlist)
|
|
43
|
-
});
|
|
44
|
-
var ArchBaselineSchema = z.object({
|
|
45
|
-
version: z.literal(1),
|
|
46
|
-
updatedAt: z.string().datetime(),
|
|
47
|
-
// ISO 8601
|
|
48
|
-
updatedFrom: z.string(),
|
|
49
|
-
// commit hash
|
|
50
|
-
metrics: z.record(ArchMetricCategorySchema, CategoryBaselineSchema)
|
|
51
|
-
});
|
|
52
|
-
var CategoryRegressionSchema = z.object({
|
|
53
|
-
category: ArchMetricCategorySchema,
|
|
54
|
-
baselineValue: z.number(),
|
|
55
|
-
currentValue: z.number(),
|
|
56
|
-
delta: z.number()
|
|
57
|
-
});
|
|
58
|
-
var ArchDiffResultSchema = z.object({
|
|
59
|
-
passed: z.boolean(),
|
|
60
|
-
newViolations: z.array(ViolationSchema),
|
|
61
|
-
// in current but not in baseline -> FAIL
|
|
62
|
-
resolvedViolations: z.array(z.string()),
|
|
63
|
-
// in baseline but not in current -> celebrate
|
|
64
|
-
preExisting: z.array(z.string()),
|
|
65
|
-
// in both -> allowed, tracked
|
|
66
|
-
regressions: z.array(CategoryRegressionSchema)
|
|
67
|
-
// aggregate value exceeded baseline
|
|
68
|
-
});
|
|
69
|
-
var ThresholdConfigSchema = z.record(
|
|
70
|
-
ArchMetricCategorySchema,
|
|
71
|
-
z.union([z.number(), z.record(z.string(), z.number())])
|
|
72
|
-
);
|
|
73
|
-
var ArchConfigSchema = z.object({
|
|
74
|
-
enabled: z.boolean().default(true),
|
|
75
|
-
baselinePath: z.string().default(".harness/arch/baselines.json"),
|
|
76
|
-
thresholds: ThresholdConfigSchema.default({}),
|
|
77
|
-
modules: z.record(z.string(), ThresholdConfigSchema).default({})
|
|
78
|
-
});
|
|
79
|
-
var ConstraintRuleSchema = z.object({
|
|
80
|
-
id: z.string(),
|
|
81
|
-
// stable hash: sha256(category + ':' + scope + ':' + description)
|
|
82
|
-
category: ArchMetricCategorySchema,
|
|
83
|
-
description: z.string(),
|
|
84
|
-
// e.g., "Layer 'services' must not import from 'ui'"
|
|
85
|
-
scope: z.string(),
|
|
86
|
-
// e.g., 'src/services/', 'project'
|
|
87
|
-
targets: z.array(z.string()).optional()
|
|
88
|
-
// forward-compat for governs edges
|
|
89
|
-
});
|
|
90
|
-
var ViolationSnapshotSchema = z.object({
|
|
91
|
-
timestamp: z.string().datetime(),
|
|
92
|
-
violations: z.array(ViolationSchema)
|
|
93
|
-
});
|
|
94
|
-
var ViolationHistorySchema = z.object({
|
|
95
|
-
version: z.literal(1),
|
|
96
|
-
snapshots: z.array(ViolationSnapshotSchema)
|
|
97
|
-
});
|
|
98
|
-
var EmergenceConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
99
|
-
var EmergentConstraintSuggestionSchema = z.object({
|
|
100
|
-
suggestedRule: ConstraintRuleSchema,
|
|
101
|
-
confidence: EmergenceConfidenceSchema,
|
|
102
|
-
occurrences: z.number(),
|
|
103
|
-
uniqueFiles: z.number(),
|
|
104
|
-
pattern: z.string(),
|
|
105
|
-
sampleViolations: z.array(ViolationSchema),
|
|
106
|
-
rationale: z.string()
|
|
107
|
-
});
|
|
108
|
-
var EmergenceResultSchema = z.object({
|
|
109
|
-
suggestions: z.array(EmergentConstraintSuggestionSchema),
|
|
110
|
-
totalViolationsAnalyzed: z.number(),
|
|
111
|
-
windowWeeks: z.number(),
|
|
112
|
-
minOccurrences: z.number()
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// src/architecture/collectors/hash.ts
|
|
116
|
-
import { createHash } from "crypto";
|
|
117
|
-
function violationId(relativePath, category, normalizedDetail) {
|
|
118
|
-
const input = `${relativePath}:${category}:${normalizedDetail}`;
|
|
119
|
-
return createHash("sha256").update(input).digest("hex");
|
|
120
|
-
}
|
|
121
|
-
function constraintRuleId(category, scope, description) {
|
|
122
|
-
const input = `${category}:${scope}:${description}`;
|
|
123
|
-
return createHash("sha256").update(input).digest("hex");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// src/shared/result.ts
|
|
127
|
-
import { Ok, Err, isOk, isErr } from "@harness-engineering/types";
|
|
128
|
-
|
|
129
1
|
// src/shared/errors.ts
|
|
130
2
|
function createError(code, message, details = {}, suggestions = []) {
|
|
131
3
|
return { code, message, details, suggestions };
|
|
@@ -134,26 +6,17 @@ function createEntropyError(code, message, details = {}, suggestions = []) {
|
|
|
134
6
|
return { code, message, details, suggestions };
|
|
135
7
|
}
|
|
136
8
|
|
|
137
|
-
// src/
|
|
138
|
-
import {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
allowedDependencies
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function resolveFileToLayer(file, layers) {
|
|
147
|
-
for (const layer of layers) {
|
|
148
|
-
for (const pattern of layer.patterns) {
|
|
149
|
-
if (minimatch(file, pattern)) {
|
|
150
|
-
return layer;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return void 0;
|
|
9
|
+
// src/shared/result.ts
|
|
10
|
+
import { Ok, Err, isOk, isErr } from "@harness-engineering/types";
|
|
11
|
+
|
|
12
|
+
// src/shared/parsers/base.ts
|
|
13
|
+
function createParseError(code, message, details = {}, suggestions = []) {
|
|
14
|
+
return { code, message, details, suggestions };
|
|
155
15
|
}
|
|
156
16
|
|
|
17
|
+
// src/shared/parsers/typescript.ts
|
|
18
|
+
import { parse } from "@typescript-eslint/typescript-estree";
|
|
19
|
+
|
|
157
20
|
// src/shared/fs-utils.ts
|
|
158
21
|
import { access, constants, readFile } from "fs";
|
|
159
22
|
import { promisify } from "util";
|
|
@@ -194,485 +57,110 @@ function relativePosix(from, to) {
|
|
|
194
57
|
return relative(from, to).replaceAll("\\", "/");
|
|
195
58
|
}
|
|
196
59
|
|
|
197
|
-
// src/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
python: [".py"],
|
|
203
|
-
go: [".go"],
|
|
204
|
-
rust: [".rs"],
|
|
205
|
-
java: [".java"]
|
|
206
|
-
};
|
|
207
|
-
var JS_EXT_FALLBACKS = {
|
|
208
|
-
".js": [".ts", ".tsx", ".jsx"],
|
|
209
|
-
".jsx": [".tsx"],
|
|
210
|
-
".mjs": [".mts"],
|
|
211
|
-
".cjs": [".cts"]
|
|
212
|
-
};
|
|
213
|
-
function detectLangFromExt(ext) {
|
|
214
|
-
for (const [lang, exts] of Object.entries(EXTENSION_BY_LANG)) {
|
|
215
|
-
if (exts.includes(ext)) return lang;
|
|
60
|
+
// src/shared/parsers/typescript.ts
|
|
61
|
+
function walk(node, visitor) {
|
|
62
|
+
if (!node || typeof node !== "object") return;
|
|
63
|
+
if ("type" in node) {
|
|
64
|
+
visitor(node);
|
|
216
65
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
case "javascript":
|
|
224
|
-
return [".js", ".jsx", ".mjs", ".cjs"];
|
|
225
|
-
case "python":
|
|
226
|
-
return [".py"];
|
|
227
|
-
case "go":
|
|
228
|
-
return [".go"];
|
|
229
|
-
case "rust":
|
|
230
|
-
return [".rs"];
|
|
231
|
-
case "java":
|
|
232
|
-
return [".java"];
|
|
233
|
-
default:
|
|
234
|
-
return [".ts", ".tsx", ".js", ".jsx"];
|
|
66
|
+
for (const value of Object.values(node)) {
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
value.forEach((v) => walk(v, visitor));
|
|
69
|
+
} else {
|
|
70
|
+
walk(value, visitor);
|
|
71
|
+
}
|
|
235
72
|
}
|
|
236
73
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
74
|
+
function makeLocation(node) {
|
|
75
|
+
return {
|
|
76
|
+
file: "",
|
|
77
|
+
line: node.loc?.start.line ?? 0,
|
|
78
|
+
column: node.loc?.start.column ?? 0
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function processImportSpecifiers(importDecl, imp) {
|
|
82
|
+
for (const spec of importDecl.specifiers) {
|
|
83
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
84
|
+
imp.default = spec.local.name;
|
|
85
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
86
|
+
imp.namespace = spec.local.name;
|
|
87
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
88
|
+
imp.specifiers.push(spec.local.name);
|
|
89
|
+
if (spec.importKind === "type") {
|
|
90
|
+
imp.kind = "type";
|
|
91
|
+
}
|
|
255
92
|
}
|
|
256
93
|
}
|
|
257
|
-
const hasKnownExt = Object.values(EXTENSION_BY_LANG).flat().some((e) => resolved.endsWith(e));
|
|
258
|
-
if (hasKnownExt) {
|
|
259
|
-
return resolved.replace(/\\/g, "/");
|
|
260
|
-
}
|
|
261
|
-
const extensions = getExtensionsForLang(fromLang);
|
|
262
|
-
return (resolved + extensions[0]).replace(/\\/g, "/");
|
|
263
94
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
return "static";
|
|
95
|
+
function getExportedName(exported) {
|
|
96
|
+
return exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
267
97
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
98
|
+
function processReExportSpecifiers(exportDecl, exports) {
|
|
99
|
+
for (const spec of exportDecl.specifiers) {
|
|
100
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
101
|
+
exports.push({
|
|
102
|
+
name: getExportedName(spec.exported),
|
|
103
|
+
type: "named",
|
|
104
|
+
location: makeLocation(exportDecl),
|
|
105
|
+
isReExport: true,
|
|
106
|
+
source: exportDecl.source.value
|
|
273
107
|
});
|
|
274
108
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (!importsResult.ok) {
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
for (const imp of importsResult.value) {
|
|
291
|
-
const resolvedPath = await resolveImportPath(imp.source, file, "");
|
|
292
|
-
if (resolvedPath) {
|
|
293
|
-
edges.push({
|
|
294
|
-
from: normalizedFile,
|
|
295
|
-
to: resolvedPath,
|
|
296
|
-
importType: getImportType(imp),
|
|
297
|
-
line: imp.location.line
|
|
109
|
+
}
|
|
110
|
+
function processExportDeclaration(exportDecl, exports) {
|
|
111
|
+
const decl = exportDecl.declaration;
|
|
112
|
+
if (!decl) return;
|
|
113
|
+
if (decl.type === "VariableDeclaration") {
|
|
114
|
+
for (const declarator of decl.declarations) {
|
|
115
|
+
if (declarator.id.type === "Identifier") {
|
|
116
|
+
exports.push({
|
|
117
|
+
name: declarator.id.name,
|
|
118
|
+
type: "named",
|
|
119
|
+
location: makeLocation(decl),
|
|
120
|
+
isReExport: false
|
|
298
121
|
});
|
|
299
122
|
}
|
|
300
123
|
}
|
|
124
|
+
} else if ((decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id) {
|
|
125
|
+
exports.push({
|
|
126
|
+
name: decl.id.name,
|
|
127
|
+
type: "named",
|
|
128
|
+
location: makeLocation(decl),
|
|
129
|
+
isReExport: false
|
|
130
|
+
});
|
|
301
131
|
}
|
|
302
|
-
return Ok({ nodes, edges });
|
|
303
132
|
}
|
|
304
|
-
function
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (!fromLayer.allowedDependencies.includes(toLayer.name)) {
|
|
314
|
-
violations.push({
|
|
315
|
-
file: edge.from,
|
|
316
|
-
imports: edge.to,
|
|
317
|
-
fromLayer: fromLayer.name,
|
|
318
|
-
toLayer: toLayer.name,
|
|
319
|
-
reason: "WRONG_LAYER",
|
|
320
|
-
line: edge.line,
|
|
321
|
-
suggestion: `Move the dependency to an allowed layer (${fromLayer.allowedDependencies.join(", ") || "none"}) or update layer rules`
|
|
322
|
-
});
|
|
323
|
-
}
|
|
133
|
+
function processExportListSpecifiers(exportDecl, exports) {
|
|
134
|
+
for (const spec of exportDecl.specifiers) {
|
|
135
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
136
|
+
exports.push({
|
|
137
|
+
name: getExportedName(spec.exported),
|
|
138
|
+
type: "named",
|
|
139
|
+
location: makeLocation(exportDecl),
|
|
140
|
+
isReExport: false
|
|
141
|
+
});
|
|
324
142
|
}
|
|
325
|
-
return violations;
|
|
326
143
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
144
|
+
var TypeScriptParser = class {
|
|
145
|
+
name = "typescript";
|
|
146
|
+
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
147
|
+
async parseFile(path) {
|
|
148
|
+
const contentResult = await readFileContent(path);
|
|
149
|
+
if (!contentResult.ok) {
|
|
150
|
+
return Err(
|
|
151
|
+
createParseError("NOT_FOUND", `File not found: ${path}`, { path }, [
|
|
152
|
+
"Check that the file exists",
|
|
153
|
+
"Verify the path is correct"
|
|
154
|
+
])
|
|
155
|
+
);
|
|
333
156
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const healthResult = await parser.health();
|
|
342
|
-
if (!healthResult.ok || !healthResult.value.available) {
|
|
343
|
-
if (fallbackBehavior === "skip") {
|
|
344
|
-
return Ok({
|
|
345
|
-
valid: true,
|
|
346
|
-
violations: [],
|
|
347
|
-
graph: { nodes: [], edges: [] },
|
|
348
|
-
skipped: true,
|
|
349
|
-
reason: "Parser unavailable"
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
if (fallbackBehavior === "warn") {
|
|
353
|
-
console.warn(`Parser ${parser.name} unavailable, skipping validation`);
|
|
354
|
-
return Ok({
|
|
355
|
-
valid: true,
|
|
356
|
-
violations: [],
|
|
357
|
-
graph: { nodes: [], edges: [] },
|
|
358
|
-
skipped: true,
|
|
359
|
-
reason: "Parser unavailable"
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
return Err(
|
|
363
|
-
createError(
|
|
364
|
-
"PARSER_UNAVAILABLE",
|
|
365
|
-
`Parser ${parser.name} is not available`,
|
|
366
|
-
{ parser: parser.name },
|
|
367
|
-
["Install required runtime", "Use different parser", 'Set fallbackBehavior: "skip"']
|
|
368
|
-
)
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
const allFiles = [];
|
|
372
|
-
for (const layer of layers) {
|
|
373
|
-
for (const pattern of layer.patterns) {
|
|
374
|
-
const files = await findFiles(pattern, rootDir);
|
|
375
|
-
allFiles.push(...files);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
const uniqueFiles = [...new Set(allFiles)];
|
|
379
|
-
const graphResult = await buildDependencyGraph(uniqueFiles, parser);
|
|
380
|
-
if (!graphResult.ok) {
|
|
381
|
-
return Err(graphResult.error);
|
|
382
|
-
}
|
|
383
|
-
const violations = checkLayerViolations(graphResult.value, layers, rootDir);
|
|
384
|
-
return Ok({
|
|
385
|
-
valid: violations.length === 0,
|
|
386
|
-
violations,
|
|
387
|
-
graph: graphResult.value
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// src/constraints/circular-deps.ts
|
|
392
|
-
function buildAdjacencyList(graph) {
|
|
393
|
-
const adjacency = /* @__PURE__ */ new Map();
|
|
394
|
-
const nodeSet = new Set(graph.nodes);
|
|
395
|
-
for (const node of graph.nodes) {
|
|
396
|
-
adjacency.set(node, []);
|
|
397
|
-
}
|
|
398
|
-
for (const edge of graph.edges) {
|
|
399
|
-
const neighbors = adjacency.get(edge.from);
|
|
400
|
-
if (neighbors && nodeSet.has(edge.to)) {
|
|
401
|
-
neighbors.push(edge.to);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return adjacency;
|
|
405
|
-
}
|
|
406
|
-
function isCyclicSCC(scc, adjacency) {
|
|
407
|
-
if (scc.length > 1) return true;
|
|
408
|
-
if (scc.length === 1) {
|
|
409
|
-
const selfNode = scc[0];
|
|
410
|
-
const selfNeighbors = adjacency.get(selfNode) ?? [];
|
|
411
|
-
return selfNeighbors.includes(selfNode);
|
|
412
|
-
}
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
function processNeighbors(node, neighbors, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
416
|
-
for (const neighbor of neighbors) {
|
|
417
|
-
const neighborData = nodeMap.get(neighbor);
|
|
418
|
-
if (!neighborData) {
|
|
419
|
-
strongConnectImpl(neighbor, nodeMap, stack, adjacency, sccs, indexRef);
|
|
420
|
-
const nodeData = nodeMap.get(node);
|
|
421
|
-
const updatedNeighborData = nodeMap.get(neighbor);
|
|
422
|
-
nodeData.lowlink = Math.min(nodeData.lowlink, updatedNeighborData.lowlink);
|
|
423
|
-
} else if (neighborData.onStack) {
|
|
424
|
-
const nodeData = nodeMap.get(node);
|
|
425
|
-
nodeData.lowlink = Math.min(nodeData.lowlink, neighborData.index);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
function strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
430
|
-
nodeMap.set(node, { index: indexRef.value, lowlink: indexRef.value, onStack: true });
|
|
431
|
-
indexRef.value++;
|
|
432
|
-
stack.push(node);
|
|
433
|
-
processNeighbors(node, adjacency.get(node) ?? [], nodeMap, stack, adjacency, sccs, indexRef);
|
|
434
|
-
const nodeData = nodeMap.get(node);
|
|
435
|
-
if (nodeData.lowlink === nodeData.index) {
|
|
436
|
-
const scc = [];
|
|
437
|
-
let w;
|
|
438
|
-
do {
|
|
439
|
-
w = stack.pop();
|
|
440
|
-
nodeMap.get(w).onStack = false;
|
|
441
|
-
scc.push(w);
|
|
442
|
-
} while (w !== node);
|
|
443
|
-
if (isCyclicSCC(scc, adjacency)) {
|
|
444
|
-
sccs.push(scc);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
function tarjanSCC(graph) {
|
|
449
|
-
const nodeMap = /* @__PURE__ */ new Map();
|
|
450
|
-
const stack = [];
|
|
451
|
-
const sccs = [];
|
|
452
|
-
const indexRef = { value: 0 };
|
|
453
|
-
const adjacency = buildAdjacencyList(graph);
|
|
454
|
-
for (const node of graph.nodes) {
|
|
455
|
-
if (!nodeMap.has(node)) {
|
|
456
|
-
strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return sccs;
|
|
460
|
-
}
|
|
461
|
-
function detectCircularDeps(graph) {
|
|
462
|
-
const sccs = tarjanSCC(graph);
|
|
463
|
-
const cycles = sccs.map((scc) => {
|
|
464
|
-
const reversed = scc.reverse();
|
|
465
|
-
const firstNode = reversed[reversed.length - 1];
|
|
466
|
-
const cycle = [...reversed, firstNode];
|
|
467
|
-
return {
|
|
468
|
-
cycle,
|
|
469
|
-
severity: "error",
|
|
470
|
-
size: scc.length
|
|
471
|
-
};
|
|
472
|
-
});
|
|
473
|
-
const largestCycle = cycles.reduce((max, c) => Math.max(max, c.size), 0);
|
|
474
|
-
return Ok({
|
|
475
|
-
hasCycles: cycles.length > 0,
|
|
476
|
-
cycles,
|
|
477
|
-
largestCycle
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
async function detectCircularDepsInFiles(files, parser, graphDependencyData) {
|
|
481
|
-
const graphResult = await buildDependencyGraph(files, parser, graphDependencyData);
|
|
482
|
-
if (!graphResult.ok) {
|
|
483
|
-
return graphResult;
|
|
484
|
-
}
|
|
485
|
-
return detectCircularDeps(graphResult.value);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// src/architecture/collectors/circular-deps.ts
|
|
489
|
-
function makeStubParser() {
|
|
490
|
-
return {
|
|
491
|
-
name: "typescript",
|
|
492
|
-
extensions: [".ts", ".tsx"],
|
|
493
|
-
parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
|
|
494
|
-
extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
|
|
495
|
-
extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
|
|
496
|
-
health: async () => ({ ok: true, value: { available: true } })
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
function mapCycleViolations(cycles, rootDir, category) {
|
|
500
|
-
return cycles.map((cycle) => {
|
|
501
|
-
const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
|
|
502
|
-
const firstFile = relativePosix(rootDir, cycle.cycle[0]);
|
|
503
|
-
return {
|
|
504
|
-
id: violationId(firstFile, category, cyclePath),
|
|
505
|
-
file: firstFile,
|
|
506
|
-
detail: `Circular dependency: ${cyclePath}`,
|
|
507
|
-
severity: cycle.severity
|
|
508
|
-
};
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
var CircularDepsCollector = class {
|
|
512
|
-
category = "circular-deps";
|
|
513
|
-
getRules(_config, _rootDir) {
|
|
514
|
-
const description = "No circular dependencies allowed";
|
|
515
|
-
return [
|
|
516
|
-
{
|
|
517
|
-
id: constraintRuleId(this.category, "project", description),
|
|
518
|
-
category: this.category,
|
|
519
|
-
description,
|
|
520
|
-
scope: "project"
|
|
521
|
-
}
|
|
522
|
-
];
|
|
523
|
-
}
|
|
524
|
-
async collect(_config, rootDir) {
|
|
525
|
-
const files = await findFiles("**/*.ts", rootDir);
|
|
526
|
-
const graphResult = await buildDependencyGraph(files, makeStubParser());
|
|
527
|
-
if (!graphResult.ok) {
|
|
528
|
-
return [
|
|
529
|
-
{
|
|
530
|
-
category: this.category,
|
|
531
|
-
scope: "project",
|
|
532
|
-
value: 0,
|
|
533
|
-
violations: [],
|
|
534
|
-
metadata: { error: "Failed to build dependency graph" }
|
|
535
|
-
}
|
|
536
|
-
];
|
|
537
|
-
}
|
|
538
|
-
const result = detectCircularDeps(graphResult.value);
|
|
539
|
-
if (!result.ok) {
|
|
540
|
-
return [
|
|
541
|
-
{
|
|
542
|
-
category: this.category,
|
|
543
|
-
scope: "project",
|
|
544
|
-
value: 0,
|
|
545
|
-
violations: [],
|
|
546
|
-
metadata: { error: "Failed to detect circular deps" }
|
|
547
|
-
}
|
|
548
|
-
];
|
|
549
|
-
}
|
|
550
|
-
const { cycles, largestCycle } = result.value;
|
|
551
|
-
const violations = mapCycleViolations(cycles, rootDir, this.category);
|
|
552
|
-
return [
|
|
553
|
-
{
|
|
554
|
-
category: this.category,
|
|
555
|
-
scope: "project",
|
|
556
|
-
value: cycles.length,
|
|
557
|
-
violations,
|
|
558
|
-
metadata: { largestCycle, cycleCount: cycles.length }
|
|
559
|
-
}
|
|
560
|
-
];
|
|
561
|
-
}
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// src/shared/parsers/typescript.ts
|
|
565
|
-
import { parse } from "@typescript-eslint/typescript-estree";
|
|
566
|
-
|
|
567
|
-
// src/shared/parsers/base.ts
|
|
568
|
-
function createParseError(code, message, details = {}, suggestions = []) {
|
|
569
|
-
return { code, message, details, suggestions };
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// src/shared/parsers/typescript.ts
|
|
573
|
-
function walk(node, visitor) {
|
|
574
|
-
if (!node || typeof node !== "object") return;
|
|
575
|
-
if ("type" in node) {
|
|
576
|
-
visitor(node);
|
|
577
|
-
}
|
|
578
|
-
for (const value of Object.values(node)) {
|
|
579
|
-
if (Array.isArray(value)) {
|
|
580
|
-
value.forEach((v) => walk(v, visitor));
|
|
581
|
-
} else {
|
|
582
|
-
walk(value, visitor);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
function makeLocation(node) {
|
|
587
|
-
return {
|
|
588
|
-
file: "",
|
|
589
|
-
line: node.loc?.start.line ?? 0,
|
|
590
|
-
column: node.loc?.start.column ?? 0
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
function processImportSpecifiers(importDecl, imp) {
|
|
594
|
-
for (const spec of importDecl.specifiers) {
|
|
595
|
-
if (spec.type === "ImportDefaultSpecifier") {
|
|
596
|
-
imp.default = spec.local.name;
|
|
597
|
-
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
598
|
-
imp.namespace = spec.local.name;
|
|
599
|
-
} else if (spec.type === "ImportSpecifier") {
|
|
600
|
-
imp.specifiers.push(spec.local.name);
|
|
601
|
-
if (spec.importKind === "type") {
|
|
602
|
-
imp.kind = "type";
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
function getExportedName(exported) {
|
|
608
|
-
return exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
609
|
-
}
|
|
610
|
-
function processReExportSpecifiers(exportDecl, exports) {
|
|
611
|
-
for (const spec of exportDecl.specifiers) {
|
|
612
|
-
if (spec.type !== "ExportSpecifier") continue;
|
|
613
|
-
exports.push({
|
|
614
|
-
name: getExportedName(spec.exported),
|
|
615
|
-
type: "named",
|
|
616
|
-
location: makeLocation(exportDecl),
|
|
617
|
-
isReExport: true,
|
|
618
|
-
source: exportDecl.source.value
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
function processExportDeclaration(exportDecl, exports) {
|
|
623
|
-
const decl = exportDecl.declaration;
|
|
624
|
-
if (!decl) return;
|
|
625
|
-
if (decl.type === "VariableDeclaration") {
|
|
626
|
-
for (const declarator of decl.declarations) {
|
|
627
|
-
if (declarator.id.type === "Identifier") {
|
|
628
|
-
exports.push({
|
|
629
|
-
name: declarator.id.name,
|
|
630
|
-
type: "named",
|
|
631
|
-
location: makeLocation(decl),
|
|
632
|
-
isReExport: false
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
} else if ((decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id) {
|
|
637
|
-
exports.push({
|
|
638
|
-
name: decl.id.name,
|
|
639
|
-
type: "named",
|
|
640
|
-
location: makeLocation(decl),
|
|
641
|
-
isReExport: false
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
function processExportListSpecifiers(exportDecl, exports) {
|
|
646
|
-
for (const spec of exportDecl.specifiers) {
|
|
647
|
-
if (spec.type !== "ExportSpecifier") continue;
|
|
648
|
-
exports.push({
|
|
649
|
-
name: getExportedName(spec.exported),
|
|
650
|
-
type: "named",
|
|
651
|
-
location: makeLocation(exportDecl),
|
|
652
|
-
isReExport: false
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
var TypeScriptParser = class {
|
|
657
|
-
name = "typescript";
|
|
658
|
-
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
659
|
-
async parseFile(path) {
|
|
660
|
-
const contentResult = await readFileContent(path);
|
|
661
|
-
if (!contentResult.ok) {
|
|
662
|
-
return Err(
|
|
663
|
-
createParseError("NOT_FOUND", `File not found: ${path}`, { path }, [
|
|
664
|
-
"Check that the file exists",
|
|
665
|
-
"Verify the path is correct"
|
|
666
|
-
])
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
try {
|
|
670
|
-
const ast = parse(contentResult.value, {
|
|
671
|
-
loc: true,
|
|
672
|
-
range: true,
|
|
673
|
-
jsx: path.endsWith(".tsx"),
|
|
674
|
-
errorOnUnknownASTType: false
|
|
675
|
-
});
|
|
157
|
+
try {
|
|
158
|
+
const ast = parse(contentResult.value, {
|
|
159
|
+
loc: true,
|
|
160
|
+
range: true,
|
|
161
|
+
jsx: path.endsWith(".tsx"),
|
|
162
|
+
errorOnUnknownASTType: false
|
|
163
|
+
});
|
|
676
164
|
return Ok({
|
|
677
165
|
type: "Program",
|
|
678
166
|
body: ast,
|
|
@@ -760,9 +248,6 @@ var TypeScriptParser = class {
|
|
|
760
248
|
}
|
|
761
249
|
};
|
|
762
250
|
|
|
763
|
-
// src/code-nav/parser.ts
|
|
764
|
-
import Parser from "web-tree-sitter";
|
|
765
|
-
|
|
766
251
|
// src/code-nav/types.ts
|
|
767
252
|
var EXTENSION_MAP = {
|
|
768
253
|
".ts": "typescript",
|
|
@@ -784,6 +269,7 @@ function detectLanguage(filePath) {
|
|
|
784
269
|
}
|
|
785
270
|
|
|
786
271
|
// src/code-nav/parser.ts
|
|
272
|
+
import Parser from "web-tree-sitter";
|
|
787
273
|
var parserCache = /* @__PURE__ */ new Map();
|
|
788
274
|
var initialized = false;
|
|
789
275
|
var GRAMMAR_MAP = {
|
|
@@ -1083,604 +569,218 @@ function formatOutline(outline) {
|
|
|
1083
569
|
return lines.join("\n");
|
|
1084
570
|
}
|
|
1085
571
|
|
|
1086
|
-
// src/
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (sym.name === name) return { line: sym.line, endLine: sym.endLine };
|
|
1090
|
-
if (sym.children) {
|
|
1091
|
-
const found = findSymbolByName(sym.children, name);
|
|
1092
|
-
if (found) return found;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
function makeLocation2(node) {
|
|
572
|
+
// src/constraints/layers.ts
|
|
573
|
+
import { minimatch } from "minimatch";
|
|
574
|
+
function defineLayer(name, patterns, allowedDependencies) {
|
|
1098
575
|
return {
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
576
|
+
name,
|
|
577
|
+
patterns,
|
|
578
|
+
allowedDependencies
|
|
1102
579
|
};
|
|
1103
580
|
}
|
|
1104
|
-
function
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return { source, specifiers: [], location: makeLocation2(node), kind: "value" };
|
|
1111
|
-
}
|
|
1112
|
-
function extractPythonImport(child) {
|
|
1113
|
-
const name = child.childForFieldName("name");
|
|
1114
|
-
if (!name) return null;
|
|
1115
|
-
return makeImport(child, name.text);
|
|
1116
|
-
}
|
|
1117
|
-
function extractPythonFromImport(child) {
|
|
1118
|
-
const moduleName = child.childForFieldName("module_name");
|
|
1119
|
-
const specifiers = [];
|
|
1120
|
-
for (const c of child.children) {
|
|
1121
|
-
if (c.type === "dotted_name" && c !== moduleName) specifiers.push(c.text);
|
|
1122
|
-
if (c.type === "aliased_import") {
|
|
1123
|
-
const n = c.childForFieldName("name");
|
|
1124
|
-
if (n) specifiers.push(n.text);
|
|
581
|
+
function resolveFileToLayer(file, layers) {
|
|
582
|
+
for (const layer of layers) {
|
|
583
|
+
for (const pattern of layer.patterns) {
|
|
584
|
+
if (minimatch(file, pattern)) {
|
|
585
|
+
return layer;
|
|
586
|
+
}
|
|
1125
587
|
}
|
|
1126
588
|
}
|
|
1127
|
-
return
|
|
1128
|
-
source: moduleName?.text ?? "",
|
|
1129
|
-
specifiers,
|
|
1130
|
-
location: makeLocation2(child),
|
|
1131
|
-
kind: "value"
|
|
1132
|
-
};
|
|
589
|
+
return void 0;
|
|
1133
590
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
return null;
|
|
1145
|
-
}
|
|
1146
|
-
var pythonStrategy = {
|
|
1147
|
-
extractImports(root) {
|
|
1148
|
-
const imports = [];
|
|
1149
|
-
for (const child of root.children) {
|
|
1150
|
-
if (child.type === "import_statement") {
|
|
1151
|
-
const imp = extractPythonImport(child);
|
|
1152
|
-
if (imp) imports.push(imp);
|
|
1153
|
-
} else if (child.type === "import_from_statement") {
|
|
1154
|
-
imports.push(extractPythonFromImport(child));
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
return imports;
|
|
1158
|
-
},
|
|
1159
|
-
extractExports(root) {
|
|
1160
|
-
const exports = [];
|
|
1161
|
-
for (const child of root.children) {
|
|
1162
|
-
const exp = extractPythonExport(child);
|
|
1163
|
-
if (exp) exports.push(exp);
|
|
1164
|
-
}
|
|
1165
|
-
return exports;
|
|
1166
|
-
}
|
|
591
|
+
|
|
592
|
+
// src/constraints/dependencies.ts
|
|
593
|
+
import { dirname, resolve, extname } from "path";
|
|
594
|
+
var EXTENSION_BY_LANG = {
|
|
595
|
+
typescript: [".ts", ".tsx"],
|
|
596
|
+
javascript: [".js", ".jsx", ".mjs", ".cjs"],
|
|
597
|
+
python: [".py"],
|
|
598
|
+
go: [".go"],
|
|
599
|
+
rust: [".rs"],
|
|
600
|
+
java: [".java"]
|
|
1167
601
|
};
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
if (
|
|
1177
|
-
}
|
|
1178
|
-
const specList = child.children.find((c) => c.type === "import_spec_list");
|
|
1179
|
-
if (specList) {
|
|
1180
|
-
for (const spec of specList.children.filter((c) => c.type === "import_spec")) {
|
|
1181
|
-
const source = extractGoImportPath(spec);
|
|
1182
|
-
if (source) imports.push(makeImport(spec, source));
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
return imports;
|
|
1186
|
-
}
|
|
1187
|
-
function isGoExported(name) {
|
|
1188
|
-
return /^[A-Z]/.test(name);
|
|
1189
|
-
}
|
|
1190
|
-
function extractGoExport(child) {
|
|
1191
|
-
if (child.type === "function_declaration" || child.type === "method_declaration") {
|
|
1192
|
-
const name = child.childForFieldName("name");
|
|
1193
|
-
if (name && isGoExported(name.text)) return makeNamedExport(child);
|
|
1194
|
-
}
|
|
1195
|
-
if (child.type === "type_declaration") {
|
|
1196
|
-
const typeSpec = child.children.find((c) => c.type === "type_spec");
|
|
1197
|
-
if (typeSpec) {
|
|
1198
|
-
const name = typeSpec.childForFieldName("name");
|
|
1199
|
-
if (name && isGoExported(name.text)) {
|
|
1200
|
-
return { name: name.text, type: "named", location: makeLocation2(child), isReExport: false };
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
602
|
+
var JS_EXT_FALLBACKS = {
|
|
603
|
+
".js": [".ts", ".tsx", ".jsx"],
|
|
604
|
+
".jsx": [".tsx"],
|
|
605
|
+
".mjs": [".mts"],
|
|
606
|
+
".cjs": [".cts"]
|
|
607
|
+
};
|
|
608
|
+
function detectLangFromExt(ext) {
|
|
609
|
+
for (const [lang, exts] of Object.entries(EXTENSION_BY_LANG)) {
|
|
610
|
+
if (exts.includes(ext)) return lang;
|
|
1203
611
|
}
|
|
1204
612
|
return null;
|
|
1205
613
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
};
|
|
1223
|
-
var RUST_USE_ARG_TYPES = /* @__PURE__ */ new Set([
|
|
1224
|
-
"scoped_identifier",
|
|
1225
|
-
"use_wildcard",
|
|
1226
|
-
"scoped_use_list",
|
|
1227
|
-
"identifier"
|
|
1228
|
-
]);
|
|
1229
|
-
var RUST_PUB_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
1230
|
-
"function_item",
|
|
1231
|
-
"struct_item",
|
|
1232
|
-
"enum_item",
|
|
1233
|
-
"trait_item",
|
|
1234
|
-
"type_item",
|
|
1235
|
-
"const_item",
|
|
1236
|
-
"static_item"
|
|
1237
|
-
]);
|
|
1238
|
-
var rustStrategy = {
|
|
1239
|
-
extractImports(root) {
|
|
1240
|
-
const imports = [];
|
|
1241
|
-
for (const child of root.children) {
|
|
1242
|
-
if (child.type !== "use_declaration") continue;
|
|
1243
|
-
const arg = child.childForFieldName("argument") ?? child.children.find((c) => RUST_USE_ARG_TYPES.has(c.type));
|
|
1244
|
-
if (arg) imports.push(makeImport(child, arg.text));
|
|
1245
|
-
}
|
|
1246
|
-
return imports;
|
|
1247
|
-
},
|
|
1248
|
-
extractExports(root, source) {
|
|
1249
|
-
const exports = [];
|
|
1250
|
-
const lines = source.split("\n");
|
|
1251
|
-
for (const child of root.children) {
|
|
1252
|
-
const line = lines[child.startPosition.row] ?? "";
|
|
1253
|
-
if (!/^\s*pub\b/.test(line)) continue;
|
|
1254
|
-
if (RUST_PUB_ITEM_TYPES.has(child.type)) {
|
|
1255
|
-
const exp = makeNamedExport(child);
|
|
1256
|
-
if (exp) exports.push(exp);
|
|
1257
|
-
} else if (child.type === "mod_item") {
|
|
1258
|
-
const name = child.childForFieldName("name");
|
|
1259
|
-
if (name) {
|
|
1260
|
-
exports.push({
|
|
1261
|
-
name: name.text,
|
|
1262
|
-
type: "namespace",
|
|
1263
|
-
location: makeLocation2(child),
|
|
1264
|
-
isReExport: false
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
return exports;
|
|
1270
|
-
}
|
|
1271
|
-
};
|
|
1272
|
-
var JAVA_IMPORT_TYPES = /* @__PURE__ */ new Set(["scoped_identifier", "scoped_absolute_identifier"]);
|
|
1273
|
-
var JAVA_EXPORT_TYPES = /* @__PURE__ */ new Set([
|
|
1274
|
-
"class_declaration",
|
|
1275
|
-
"interface_declaration",
|
|
1276
|
-
"enum_declaration",
|
|
1277
|
-
"record_declaration"
|
|
1278
|
-
]);
|
|
1279
|
-
var javaStrategy = {
|
|
1280
|
-
extractImports(root) {
|
|
1281
|
-
const imports = [];
|
|
1282
|
-
for (const child of root.children) {
|
|
1283
|
-
if (child.type !== "import_declaration") continue;
|
|
1284
|
-
const scoped = child.children.find((c) => JAVA_IMPORT_TYPES.has(c.type));
|
|
1285
|
-
if (scoped) imports.push(makeImport(child, scoped.text));
|
|
1286
|
-
}
|
|
1287
|
-
return imports;
|
|
1288
|
-
},
|
|
1289
|
-
extractExports(root, source) {
|
|
1290
|
-
const exports = [];
|
|
1291
|
-
const lines = source.split("\n");
|
|
1292
|
-
for (const child of root.children) {
|
|
1293
|
-
if (!JAVA_EXPORT_TYPES.has(child.type)) continue;
|
|
1294
|
-
const line = lines[child.startPosition.row] ?? "";
|
|
1295
|
-
if (!/\bpublic\b/.test(line)) continue;
|
|
1296
|
-
const exp = makeNamedExport(child);
|
|
1297
|
-
if (exp) exports.push(exp);
|
|
1298
|
-
}
|
|
1299
|
-
return exports;
|
|
614
|
+
function getExtensionsForLang(lang) {
|
|
615
|
+
switch (lang) {
|
|
616
|
+
case "typescript":
|
|
617
|
+
return [".ts", ".tsx"];
|
|
618
|
+
case "javascript":
|
|
619
|
+
return [".js", ".jsx", ".mjs", ".cjs"];
|
|
620
|
+
case "python":
|
|
621
|
+
return [".py"];
|
|
622
|
+
case "go":
|
|
623
|
+
return [".go"];
|
|
624
|
+
case "rust":
|
|
625
|
+
return [".rs"];
|
|
626
|
+
case "java":
|
|
627
|
+
return [".java"];
|
|
628
|
+
default:
|
|
629
|
+
return [".ts", ".tsx", ".js", ".jsx"];
|
|
1300
630
|
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
rust: rustStrategy,
|
|
1306
|
-
java: javaStrategy
|
|
1307
|
-
};
|
|
1308
|
-
var TreeSitterParser = class {
|
|
1309
|
-
name;
|
|
1310
|
-
extensions;
|
|
1311
|
-
lang;
|
|
1312
|
-
strategy;
|
|
1313
|
-
constructor(lang, extensions, strategy) {
|
|
1314
|
-
this.name = lang;
|
|
1315
|
-
this.lang = lang;
|
|
1316
|
-
this.extensions = extensions;
|
|
1317
|
-
this.strategy = strategy;
|
|
631
|
+
}
|
|
632
|
+
async function resolveImportPath(importSource, fromFile, _rootDir) {
|
|
633
|
+
if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
|
|
634
|
+
return null;
|
|
1318
635
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
const parser = await getParser(this.lang);
|
|
1330
|
-
const tree = parser.parse(contentResult.value);
|
|
1331
|
-
return Ok({
|
|
1332
|
-
type: "Program",
|
|
1333
|
-
body: { tree, source: contentResult.value },
|
|
1334
|
-
language: this.lang
|
|
1335
|
-
});
|
|
1336
|
-
} catch (e) {
|
|
1337
|
-
const error = e;
|
|
1338
|
-
return Err(
|
|
1339
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${path}: ${error.message}`, { path }, [
|
|
1340
|
-
"Check for syntax errors in the file"
|
|
1341
|
-
])
|
|
1342
|
-
);
|
|
636
|
+
const fromDir = dirname(fromFile);
|
|
637
|
+
const resolved = resolve(fromDir, importSource);
|
|
638
|
+
const sourceExt = extname(resolved);
|
|
639
|
+
const fromLang = detectLangFromExt(extname(fromFile));
|
|
640
|
+
const fallbacks = JS_EXT_FALLBACKS[sourceExt];
|
|
641
|
+
if (fallbacks) {
|
|
642
|
+
const base = resolved.slice(0, -sourceExt.length);
|
|
643
|
+
for (const ext of fallbacks) {
|
|
644
|
+
const candidate = base + ext;
|
|
645
|
+
if (await fileExists(candidate)) return candidate.replace(/\\/g, "/");
|
|
1343
646
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
return Ok(this.strategy.extractImports(tree.rootNode, source));
|
|
1348
|
-
}
|
|
1349
|
-
extractExports(ast) {
|
|
1350
|
-
const { tree, source } = ast.body;
|
|
1351
|
-
return Ok(this.strategy.extractExports(tree.rootNode, source));
|
|
1352
|
-
}
|
|
1353
|
-
async health() {
|
|
1354
|
-
try {
|
|
1355
|
-
await getParser(this.lang);
|
|
1356
|
-
return Ok({ available: true, message: `tree-sitter ${this.lang} grammar loaded` });
|
|
1357
|
-
} catch {
|
|
1358
|
-
return Ok({ available: false, message: `tree-sitter ${this.lang} grammar not available` });
|
|
647
|
+
for (const indexExt of [".ts", ".tsx", ".jsx"]) {
|
|
648
|
+
const indexPath = resolve(base, "index" + indexExt);
|
|
649
|
+
if (await fileExists(indexPath)) return indexPath.replace(/\\/g, "/");
|
|
1359
650
|
}
|
|
1360
651
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
return
|
|
1364
|
-
}
|
|
1365
|
-
unfold(filePath, ast, symbolName) {
|
|
1366
|
-
const outlineResult = this.outline(filePath, ast);
|
|
1367
|
-
if (outlineResult.error || !outlineResult.symbols.length) return null;
|
|
1368
|
-
const { source } = ast.body;
|
|
1369
|
-
const match = findSymbolByName(outlineResult.symbols, symbolName);
|
|
1370
|
-
if (!match) return null;
|
|
1371
|
-
const lines = source.split("\n");
|
|
1372
|
-
const content = lines.slice(match.line - 1, match.endLine).join("\n");
|
|
1373
|
-
return {
|
|
1374
|
-
file: filePath,
|
|
1375
|
-
symbolName,
|
|
1376
|
-
startLine: match.line,
|
|
1377
|
-
endLine: match.endLine,
|
|
1378
|
-
content,
|
|
1379
|
-
language: this.lang,
|
|
1380
|
-
fallback: false
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
};
|
|
1384
|
-
function createTreeSitterParser(lang) {
|
|
1385
|
-
const strategy = STRATEGIES[lang];
|
|
1386
|
-
if (!strategy) return null;
|
|
1387
|
-
const extensionMap = {
|
|
1388
|
-
python: [".py"],
|
|
1389
|
-
go: [".go"],
|
|
1390
|
-
rust: [".rs"],
|
|
1391
|
-
java: [".java"]
|
|
1392
|
-
};
|
|
1393
|
-
const extensions = extensionMap[lang];
|
|
1394
|
-
if (!extensions) return null;
|
|
1395
|
-
return new TreeSitterParser(lang, extensions, strategy);
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// src/shared/parsers/registry.ts
|
|
1399
|
-
var ParserRegistry = class {
|
|
1400
|
-
parsers = /* @__PURE__ */ new Map();
|
|
1401
|
-
register(parser) {
|
|
1402
|
-
this.parsers.set(parser.name, parser);
|
|
1403
|
-
}
|
|
1404
|
-
getByLanguage(lang) {
|
|
1405
|
-
return this.parsers.get(lang) ?? null;
|
|
1406
|
-
}
|
|
1407
|
-
getForFile(filePath) {
|
|
1408
|
-
const lang = detectLanguage(filePath);
|
|
1409
|
-
if (!lang) return null;
|
|
1410
|
-
return this.getByLanguage(lang);
|
|
1411
|
-
}
|
|
1412
|
-
getSupportedExtensions() {
|
|
1413
|
-
return Object.keys(EXTENSION_MAP);
|
|
1414
|
-
}
|
|
1415
|
-
getSupportedLanguages() {
|
|
1416
|
-
return Array.from(this.parsers.keys());
|
|
1417
|
-
}
|
|
1418
|
-
isSupportedExtension(ext) {
|
|
1419
|
-
return ext in EXTENSION_MAP;
|
|
1420
|
-
}
|
|
1421
|
-
};
|
|
1422
|
-
var defaultRegistry = null;
|
|
1423
|
-
function getDefaultRegistry() {
|
|
1424
|
-
if (defaultRegistry) return defaultRegistry;
|
|
1425
|
-
const registry = new ParserRegistry();
|
|
1426
|
-
const tsParser = new TypeScriptParser();
|
|
1427
|
-
registry.register(tsParser);
|
|
1428
|
-
registry.register({
|
|
1429
|
-
name: "javascript",
|
|
1430
|
-
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
1431
|
-
parseFile: tsParser.parseFile.bind(tsParser),
|
|
1432
|
-
extractImports: tsParser.extractImports.bind(tsParser),
|
|
1433
|
-
extractExports: tsParser.extractExports.bind(tsParser),
|
|
1434
|
-
health: tsParser.health.bind(tsParser)
|
|
1435
|
-
});
|
|
1436
|
-
const treeSitterLanguages = ["python", "go", "rust", "java"];
|
|
1437
|
-
for (const lang of treeSitterLanguages) {
|
|
1438
|
-
const parser = createTreeSitterParser(lang);
|
|
1439
|
-
if (parser) {
|
|
1440
|
-
registry.register(parser);
|
|
1441
|
-
}
|
|
652
|
+
const hasKnownExt = Object.values(EXTENSION_BY_LANG).flat().some((e) => resolved.endsWith(e));
|
|
653
|
+
if (hasKnownExt) {
|
|
654
|
+
return resolved.replace(/\\/g, "/");
|
|
1442
655
|
}
|
|
1443
|
-
|
|
1444
|
-
return
|
|
656
|
+
const extensions = getExtensionsForLang(fromLang);
|
|
657
|
+
return (resolved + extensions[0]).replace(/\\/g, "/");
|
|
1445
658
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return layerViolations.map((v) => {
|
|
1450
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
1451
|
-
const relImport = relativePosix(rootDir, v.imports);
|
|
1452
|
-
const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
|
|
1453
|
-
return {
|
|
1454
|
-
id: violationId(relFile, category ?? "", detail),
|
|
1455
|
-
file: relFile,
|
|
1456
|
-
category,
|
|
1457
|
-
detail,
|
|
1458
|
-
severity: "error"
|
|
1459
|
-
};
|
|
1460
|
-
});
|
|
659
|
+
function getImportType(imp) {
|
|
660
|
+
if (imp.kind === "type") return "type-only";
|
|
661
|
+
return "static";
|
|
1461
662
|
}
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
{
|
|
1468
|
-
id: constraintRuleId(this.category, "project", description),
|
|
1469
|
-
category: this.category,
|
|
1470
|
-
description,
|
|
1471
|
-
scope: "project"
|
|
1472
|
-
}
|
|
1473
|
-
];
|
|
1474
|
-
}
|
|
1475
|
-
async collect(_config, rootDir) {
|
|
1476
|
-
const registry = getDefaultRegistry();
|
|
1477
|
-
const parser = registry.getByLanguage("typescript") ?? registry.getByLanguage("javascript");
|
|
1478
|
-
const result = await validateDependencies({
|
|
1479
|
-
layers: [],
|
|
1480
|
-
rootDir,
|
|
1481
|
-
parser,
|
|
1482
|
-
fallbackBehavior: "skip"
|
|
663
|
+
async function buildDependencyGraph(files, parser, graphDependencyData) {
|
|
664
|
+
if (graphDependencyData) {
|
|
665
|
+
return Ok({
|
|
666
|
+
nodes: graphDependencyData.nodes,
|
|
667
|
+
edges: graphDependencyData.edges
|
|
1483
668
|
});
|
|
1484
|
-
if (!result.ok) {
|
|
1485
|
-
return [
|
|
1486
|
-
{
|
|
1487
|
-
category: this.category,
|
|
1488
|
-
scope: "project",
|
|
1489
|
-
value: 0,
|
|
1490
|
-
violations: [],
|
|
1491
|
-
metadata: { error: "Failed to validate dependencies" }
|
|
1492
|
-
}
|
|
1493
|
-
];
|
|
1494
|
-
}
|
|
1495
|
-
const violations = mapLayerViolations(
|
|
1496
|
-
result.value.violations.filter((v) => v.reason === "WRONG_LAYER"),
|
|
1497
|
-
rootDir,
|
|
1498
|
-
this.category
|
|
1499
|
-
);
|
|
1500
|
-
return [{ category: this.category, scope: "project", value: violations.length, violations }];
|
|
1501
669
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
}
|
|
1513
|
-
/**
|
|
1514
|
-
* Snapshot the current metric results into an ArchBaseline.
|
|
1515
|
-
* Aggregates multiple MetricResults for the same category by summing values
|
|
1516
|
-
* and concatenating violation IDs.
|
|
1517
|
-
*/
|
|
1518
|
-
capture(results, commitHash) {
|
|
1519
|
-
const metrics = {};
|
|
1520
|
-
for (const result of results) {
|
|
1521
|
-
const existing = metrics[result.category];
|
|
1522
|
-
if (existing) {
|
|
1523
|
-
existing.value += result.value;
|
|
1524
|
-
existing.violationIds.push(...result.violations.map((v) => v.id));
|
|
1525
|
-
} else {
|
|
1526
|
-
metrics[result.category] = {
|
|
1527
|
-
value: result.value,
|
|
1528
|
-
violationIds: result.violations.map((v) => v.id)
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
for (const baseline of Object.values(metrics)) {
|
|
1533
|
-
baseline.violationIds = [...new Set(baseline.violationIds)];
|
|
670
|
+
const isLookup = "getForFile" in parser;
|
|
671
|
+
const nodes = files.map((f) => f.replace(/\\/g, "/"));
|
|
672
|
+
const edges = [];
|
|
673
|
+
for (const file of files) {
|
|
674
|
+
const normalizedFile = file.replace(/\\/g, "/");
|
|
675
|
+
const fileParser = isLookup ? parser.getForFile(file) : parser;
|
|
676
|
+
if (!fileParser) continue;
|
|
677
|
+
const parseResult = await fileParser.parseFile(file);
|
|
678
|
+
if (!parseResult.ok) {
|
|
679
|
+
continue;
|
|
1534
680
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
updatedFrom: commitHash,
|
|
1539
|
-
metrics
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
/**
|
|
1543
|
-
* Load the baselines file from disk.
|
|
1544
|
-
* Returns null if the file does not exist, contains invalid JSON,
|
|
1545
|
-
* or fails ArchBaselineSchema validation.
|
|
1546
|
-
*/
|
|
1547
|
-
load() {
|
|
1548
|
-
if (!existsSync(this.baselinesPath)) {
|
|
1549
|
-
console.error(`Baseline file not found at: ${this.baselinesPath}`);
|
|
1550
|
-
return null;
|
|
681
|
+
const importsResult = fileParser.extractImports(parseResult.value);
|
|
682
|
+
if (!importsResult.ok) {
|
|
683
|
+
continue;
|
|
1551
684
|
}
|
|
1552
|
-
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
);
|
|
1561
|
-
return null;
|
|
685
|
+
for (const imp of importsResult.value) {
|
|
686
|
+
const resolvedPath = await resolveImportPath(imp.source, file, "");
|
|
687
|
+
if (resolvedPath) {
|
|
688
|
+
edges.push({
|
|
689
|
+
from: normalizedFile,
|
|
690
|
+
to: resolvedPath,
|
|
691
|
+
importType: getImportType(imp),
|
|
692
|
+
line: imp.location.line
|
|
693
|
+
});
|
|
1562
694
|
}
|
|
1563
|
-
return parsed.data;
|
|
1564
|
-
} catch (error) {
|
|
1565
|
-
console.error(`Error loading baseline from ${this.baselinesPath}:`, error);
|
|
1566
|
-
return null;
|
|
1567
695
|
}
|
|
1568
696
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
697
|
+
return Ok({ nodes, edges });
|
|
698
|
+
}
|
|
699
|
+
function checkLayerViolations(graph, layers, rootDir) {
|
|
700
|
+
const violations = [];
|
|
701
|
+
for (const edge of graph.edges) {
|
|
702
|
+
const fromRelative = relativePosix(rootDir, edge.from);
|
|
703
|
+
const toRelative = relativePosix(rootDir, edge.to);
|
|
704
|
+
const fromLayer = resolveFileToLayer(fromRelative, layers);
|
|
705
|
+
const toLayer = resolveFileToLayer(toRelative, layers);
|
|
706
|
+
if (!fromLayer || !toLayer) continue;
|
|
707
|
+
if (fromLayer.name === toLayer.name) continue;
|
|
708
|
+
if (!fromLayer.allowedDependencies.includes(toLayer.name)) {
|
|
709
|
+
violations.push({
|
|
710
|
+
file: edge.from,
|
|
711
|
+
imports: edge.to,
|
|
712
|
+
fromLayer: fromLayer.name,
|
|
713
|
+
toLayer: toLayer.name,
|
|
714
|
+
reason: "WRONG_LAYER",
|
|
715
|
+
line: edge.line,
|
|
716
|
+
suggestion: `Move the dependency to an allowed layer (${fromLayer.allowedDependencies.join(", ") || "none"}) or update layer rules`
|
|
717
|
+
});
|
|
1584
718
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
const dir = dirname2(this.baselinesPath);
|
|
1595
|
-
if (!existsSync(dir)) {
|
|
1596
|
-
mkdirSync(dir, { recursive: true });
|
|
719
|
+
}
|
|
720
|
+
return violations;
|
|
721
|
+
}
|
|
722
|
+
async function validateDependencies(config) {
|
|
723
|
+
const { layers, rootDir, parser, fallbackBehavior = "error", graphDependencyData } = config;
|
|
724
|
+
if (graphDependencyData) {
|
|
725
|
+
const graphResult2 = await buildDependencyGraph([], parser, graphDependencyData);
|
|
726
|
+
if (!graphResult2.ok) {
|
|
727
|
+
return Err(graphResult2.error);
|
|
1597
728
|
}
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
729
|
+
const violations2 = checkLayerViolations(graphResult2.value, layers, rootDir);
|
|
730
|
+
return Ok({
|
|
731
|
+
valid: violations2.length === 0,
|
|
732
|
+
violations: violations2,
|
|
733
|
+
graph: graphResult2.value
|
|
734
|
+
});
|
|
1601
735
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
existing.violations.push(...result.violations);
|
|
1612
|
-
} else {
|
|
1613
|
-
map.set(result.category, {
|
|
1614
|
-
value: result.value,
|
|
1615
|
-
violations: [...result.violations]
|
|
736
|
+
const healthResult = await parser.health();
|
|
737
|
+
if (!healthResult.ok || !healthResult.value.available) {
|
|
738
|
+
if (fallbackBehavior === "skip") {
|
|
739
|
+
return Ok({
|
|
740
|
+
valid: true,
|
|
741
|
+
violations: [],
|
|
742
|
+
graph: { nodes: [], edges: [] },
|
|
743
|
+
skipped: true,
|
|
744
|
+
reason: "Parser unavailable"
|
|
1616
745
|
});
|
|
1617
746
|
}
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
} else {
|
|
1628
|
-
newViolations.push(violation);
|
|
747
|
+
if (fallbackBehavior === "warn") {
|
|
748
|
+
console.warn(`Parser ${parser.name} unavailable, skipping validation`);
|
|
749
|
+
return Ok({
|
|
750
|
+
valid: true,
|
|
751
|
+
violations: [],
|
|
752
|
+
graph: { nodes: [], edges: [] },
|
|
753
|
+
skipped: true,
|
|
754
|
+
reason: "Parser unavailable"
|
|
755
|
+
});
|
|
1629
756
|
}
|
|
757
|
+
return Err(
|
|
758
|
+
createError(
|
|
759
|
+
"PARSER_UNAVAILABLE",
|
|
760
|
+
`Parser ${parser.name} is not available`,
|
|
761
|
+
{ parser: parser.name },
|
|
762
|
+
["Install required runtime", "Use different parser", 'Set fallbackBehavior: "skip"']
|
|
763
|
+
)
|
|
764
|
+
);
|
|
1630
765
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1637
|
-
function collectOrphanedBaselineViolations(baseline, visitedCategories) {
|
|
1638
|
-
const resolved = [];
|
|
1639
|
-
for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
|
|
1640
|
-
if (!visitedCategories.has(category) && baselineCategory) {
|
|
1641
|
-
resolved.push(...baselineCategory.violationIds);
|
|
766
|
+
const allFiles = [];
|
|
767
|
+
for (const layer of layers) {
|
|
768
|
+
for (const pattern of layer.patterns) {
|
|
769
|
+
const files = await findFiles(pattern, rootDir);
|
|
770
|
+
allFiles.push(...files);
|
|
1642
771
|
}
|
|
1643
772
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
const baselineValue = baselineCategory?.value ?? 0;
|
|
1649
|
-
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
1650
|
-
acc.newViolations.push(...classified.newViolations);
|
|
1651
|
-
acc.preExisting.push(...classified.preExisting);
|
|
1652
|
-
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
1653
|
-
acc.resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
1654
|
-
if (baselineCategory && agg.value > baselineValue) {
|
|
1655
|
-
acc.regressions.push({
|
|
1656
|
-
category,
|
|
1657
|
-
baselineValue,
|
|
1658
|
-
currentValue: agg.value,
|
|
1659
|
-
delta: agg.value - baselineValue
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
function diff(current, baseline) {
|
|
1664
|
-
const aggregated = aggregateByCategory(current);
|
|
1665
|
-
const acc = {
|
|
1666
|
-
newViolations: [],
|
|
1667
|
-
resolvedViolations: [],
|
|
1668
|
-
preExisting: [],
|
|
1669
|
-
regressions: []
|
|
1670
|
-
};
|
|
1671
|
-
const visitedCategories = /* @__PURE__ */ new Set();
|
|
1672
|
-
for (const [category, agg] of aggregated) {
|
|
1673
|
-
visitedCategories.add(category);
|
|
1674
|
-
diffCategory(category, agg, baseline.metrics[category], acc);
|
|
773
|
+
const uniqueFiles = [...new Set(allFiles)];
|
|
774
|
+
const graphResult = await buildDependencyGraph(uniqueFiles, parser);
|
|
775
|
+
if (!graphResult.ok) {
|
|
776
|
+
return Err(graphResult.error);
|
|
1675
777
|
}
|
|
1676
|
-
|
|
1677
|
-
return {
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
regressions: acc.regressions
|
|
1683
|
-
};
|
|
778
|
+
const violations = checkLayerViolations(graphResult.value, layers, rootDir);
|
|
779
|
+
return Ok({
|
|
780
|
+
valid: violations.length === 0,
|
|
781
|
+
violations,
|
|
782
|
+
graph: graphResult.value
|
|
783
|
+
});
|
|
1684
784
|
}
|
|
1685
785
|
|
|
1686
786
|
// src/entropy/detectors/complexity.ts
|
|
@@ -1985,94 +1085,6 @@ async function detectComplexityViolations(snapshot, config, graphData) {
|
|
|
1985
1085
|
});
|
|
1986
1086
|
}
|
|
1987
1087
|
|
|
1988
|
-
// src/architecture/collectors/complexity.ts
|
|
1989
|
-
function buildSnapshot(files, rootDir) {
|
|
1990
|
-
return {
|
|
1991
|
-
files: files.map((f) => ({
|
|
1992
|
-
path: f,
|
|
1993
|
-
ast: { type: "Program", body: null, language: "typescript" },
|
|
1994
|
-
imports: [],
|
|
1995
|
-
exports: [],
|
|
1996
|
-
internalSymbols: [],
|
|
1997
|
-
jsDocComments: []
|
|
1998
|
-
})),
|
|
1999
|
-
dependencyGraph: { nodes: [], edges: [] },
|
|
2000
|
-
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
2001
|
-
docs: [],
|
|
2002
|
-
codeReferences: [],
|
|
2003
|
-
entryPoints: [],
|
|
2004
|
-
rootDir,
|
|
2005
|
-
config: { rootDir, analyze: {} },
|
|
2006
|
-
buildTime: 0
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
|
-
function resolveMaxComplexity(config) {
|
|
2010
|
-
const threshold = config.thresholds.complexity;
|
|
2011
|
-
return typeof threshold === "number" ? threshold : threshold?.max ?? 15;
|
|
2012
|
-
}
|
|
2013
|
-
function mapComplexityViolations(complexityViolations, rootDir, category) {
|
|
2014
|
-
return complexityViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
|
|
2015
|
-
const relFile = relativePosix(rootDir, v.file);
|
|
2016
|
-
return {
|
|
2017
|
-
id: violationId(relFile, category ?? "", `${v.metric}:${v.function}`),
|
|
2018
|
-
file: relFile,
|
|
2019
|
-
category,
|
|
2020
|
-
detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
|
|
2021
|
-
severity: v.severity
|
|
2022
|
-
};
|
|
2023
|
-
});
|
|
2024
|
-
}
|
|
2025
|
-
var ComplexityCollector = class {
|
|
2026
|
-
category = "complexity";
|
|
2027
|
-
getRules(_config, _rootDir) {
|
|
2028
|
-
const description = "Cyclomatic complexity must stay within thresholds";
|
|
2029
|
-
return [
|
|
2030
|
-
{
|
|
2031
|
-
id: constraintRuleId(this.category, "project", description),
|
|
2032
|
-
category: this.category,
|
|
2033
|
-
description,
|
|
2034
|
-
scope: "project"
|
|
2035
|
-
}
|
|
2036
|
-
];
|
|
2037
|
-
}
|
|
2038
|
-
async collect(_config, rootDir) {
|
|
2039
|
-
const files = await findFiles("**/*.ts", rootDir);
|
|
2040
|
-
const snapshot = buildSnapshot(files, rootDir);
|
|
2041
|
-
const maxComplexity = resolveMaxComplexity(_config);
|
|
2042
|
-
const complexityConfig = {
|
|
2043
|
-
thresholds: {
|
|
2044
|
-
cyclomaticComplexity: { error: maxComplexity, warn: Math.floor(maxComplexity * 0.7) }
|
|
2045
|
-
}
|
|
2046
|
-
};
|
|
2047
|
-
const result = await detectComplexityViolations(snapshot, complexityConfig);
|
|
2048
|
-
if (!result.ok) {
|
|
2049
|
-
return [
|
|
2050
|
-
{
|
|
2051
|
-
category: this.category,
|
|
2052
|
-
scope: "project",
|
|
2053
|
-
value: 0,
|
|
2054
|
-
violations: [],
|
|
2055
|
-
metadata: { error: "Failed to detect complexity violations" }
|
|
2056
|
-
}
|
|
2057
|
-
];
|
|
2058
|
-
}
|
|
2059
|
-
const { violations: complexityViolations, stats } = result.value;
|
|
2060
|
-
const violations = mapComplexityViolations(complexityViolations, rootDir, this.category);
|
|
2061
|
-
return [
|
|
2062
|
-
{
|
|
2063
|
-
category: this.category,
|
|
2064
|
-
scope: "project",
|
|
2065
|
-
value: violations.length,
|
|
2066
|
-
violations,
|
|
2067
|
-
metadata: {
|
|
2068
|
-
filesAnalyzed: stats.filesAnalyzed,
|
|
2069
|
-
functionsAnalyzed: stats.functionsAnalyzed
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
];
|
|
2073
|
-
}
|
|
2074
|
-
};
|
|
2075
|
-
|
|
2076
1088
|
// src/entropy/detectors/coupling.ts
|
|
2077
1089
|
var DEFAULT_THRESHOLDS2 = {
|
|
2078
1090
|
fanOut: { warn: 15 },
|
|
@@ -2240,633 +1252,368 @@ async function detectCouplingViolations(snapshot, config, graphData) {
|
|
|
2240
1252
|
});
|
|
2241
1253
|
}
|
|
2242
1254
|
|
|
2243
|
-
// src/
|
|
2244
|
-
function
|
|
1255
|
+
// src/shared/parsers/tree-sitter.ts
|
|
1256
|
+
function findSymbolByName(symbols, name) {
|
|
1257
|
+
for (const sym of symbols) {
|
|
1258
|
+
if (sym.name === name) return { line: sym.line, endLine: sym.endLine };
|
|
1259
|
+
if (sym.children) {
|
|
1260
|
+
const found = findSymbolByName(sym.children, name);
|
|
1261
|
+
if (found) return found;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
function makeLocation2(node) {
|
|
2245
1267
|
return {
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
imports: [],
|
|
2250
|
-
exports: [],
|
|
2251
|
-
internalSymbols: [],
|
|
2252
|
-
jsDocComments: []
|
|
2253
|
-
})),
|
|
2254
|
-
dependencyGraph: { nodes: [], edges: [] },
|
|
2255
|
-
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
2256
|
-
docs: [],
|
|
2257
|
-
codeReferences: [],
|
|
2258
|
-
entryPoints: [],
|
|
2259
|
-
rootDir,
|
|
2260
|
-
config: { rootDir, analyze: {} },
|
|
2261
|
-
buildTime: 0
|
|
1268
|
+
file: "",
|
|
1269
|
+
line: node.startPosition.row + 1,
|
|
1270
|
+
column: node.startPosition.column
|
|
2262
1271
|
};
|
|
2263
1272
|
}
|
|
2264
|
-
function
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
id: violationId(relFile, category ?? "", v.metric),
|
|
2269
|
-
file: relFile,
|
|
2270
|
-
category,
|
|
2271
|
-
detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
|
|
2272
|
-
severity: v.severity
|
|
2273
|
-
};
|
|
2274
|
-
});
|
|
1273
|
+
function makeNamedExport(node) {
|
|
1274
|
+
const name = node.childForFieldName("name");
|
|
1275
|
+
if (!name) return null;
|
|
1276
|
+
return { name: name.text, type: "named", location: makeLocation2(node), isReExport: false };
|
|
2275
1277
|
}
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
1278
|
+
function makeImport(node, source) {
|
|
1279
|
+
return { source, specifiers: [], location: makeLocation2(node), kind: "value" };
|
|
1280
|
+
}
|
|
1281
|
+
function extractPythonImport(child) {
|
|
1282
|
+
const name = child.childForFieldName("name");
|
|
1283
|
+
if (!name) return null;
|
|
1284
|
+
return makeImport(child, name.text);
|
|
1285
|
+
}
|
|
1286
|
+
function extractPythonFromImport(child) {
|
|
1287
|
+
const moduleName = child.childForFieldName("module_name");
|
|
1288
|
+
const specifiers = [];
|
|
1289
|
+
for (const c of child.children) {
|
|
1290
|
+
if (c.type === "dotted_name" && c !== moduleName) specifiers.push(c.text);
|
|
1291
|
+
if (c.type === "aliased_import") {
|
|
1292
|
+
const n = c.childForFieldName("name");
|
|
1293
|
+
if (n) specifiers.push(n.text);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return {
|
|
1297
|
+
source: moduleName?.text ?? "",
|
|
1298
|
+
specifiers,
|
|
1299
|
+
location: makeLocation2(child),
|
|
1300
|
+
kind: "value"
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function extractPythonExport(child) {
|
|
1304
|
+
if (child.type === "function_definition" || child.type === "class_definition") {
|
|
1305
|
+
return makeNamedExport(child);
|
|
1306
|
+
}
|
|
1307
|
+
if (child.type === "assignment") {
|
|
1308
|
+
const left = child.childForFieldName("left") ?? child.children[0];
|
|
1309
|
+
if (left && !left.text.startsWith("_")) {
|
|
1310
|
+
return { name: left.text, type: "named", location: makeLocation2(child), isReExport: false };
|
|
2303
1311
|
}
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
1312
|
+
}
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
var pythonStrategy = {
|
|
1316
|
+
extractImports(root) {
|
|
1317
|
+
const imports = [];
|
|
1318
|
+
for (const child of root.children) {
|
|
1319
|
+
if (child.type === "import_statement") {
|
|
1320
|
+
const imp = extractPythonImport(child);
|
|
1321
|
+
if (imp) imports.push(imp);
|
|
1322
|
+
} else if (child.type === "import_from_statement") {
|
|
1323
|
+
imports.push(extractPythonFromImport(child));
|
|
2313
1324
|
}
|
|
2314
|
-
|
|
1325
|
+
}
|
|
1326
|
+
return imports;
|
|
1327
|
+
},
|
|
1328
|
+
extractExports(root) {
|
|
1329
|
+
const exports = [];
|
|
1330
|
+
for (const child of root.children) {
|
|
1331
|
+
const exp = extractPythonExport(child);
|
|
1332
|
+
if (exp) exports.push(exp);
|
|
1333
|
+
}
|
|
1334
|
+
return exports;
|
|
2315
1335
|
}
|
|
2316
1336
|
};
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
1337
|
+
function extractGoImportPath(spec) {
|
|
1338
|
+
const pathNode = spec.childForFieldName("path") ?? spec.children.find((c) => c.type === "interpreted_string_literal");
|
|
1339
|
+
return pathNode ? pathNode.text.replace(/"/g, "") : null;
|
|
1340
|
+
}
|
|
1341
|
+
function extractGoImportsFromDecl(child) {
|
|
1342
|
+
const imports = [];
|
|
1343
|
+
for (const spec of child.children.filter((c) => c.type === "import_spec")) {
|
|
1344
|
+
const source = extractGoImportPath(spec);
|
|
1345
|
+
if (source) imports.push(makeImport(child, source));
|
|
1346
|
+
}
|
|
1347
|
+
const specList = child.children.find((c) => c.type === "import_spec_list");
|
|
1348
|
+
if (specList) {
|
|
1349
|
+
for (const spec of specList.children.filter((c) => c.type === "import_spec")) {
|
|
1350
|
+
const source = extractGoImportPath(spec);
|
|
1351
|
+
if (source) imports.push(makeImport(spec, source));
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return imports;
|
|
1355
|
+
}
|
|
1356
|
+
function isGoExported(name) {
|
|
1357
|
+
return /^[A-Z]/.test(name);
|
|
2332
1358
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
1359
|
+
function extractGoExport(child) {
|
|
1360
|
+
if (child.type === "function_declaration" || child.type === "method_declaration") {
|
|
1361
|
+
const name = child.childForFieldName("name");
|
|
1362
|
+
if (name && isGoExported(name.text)) return makeNamedExport(child);
|
|
1363
|
+
}
|
|
1364
|
+
if (child.type === "type_declaration") {
|
|
1365
|
+
const typeSpec = child.children.find((c) => c.type === "type_spec");
|
|
1366
|
+
if (typeSpec) {
|
|
1367
|
+
const name = typeSpec.childForFieldName("name");
|
|
1368
|
+
if (name && isGoExported(name.text)) {
|
|
1369
|
+
return { name: name.text, type: "named", location: makeLocation2(child), isReExport: false };
|
|
2343
1370
|
}
|
|
2344
|
-
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
var goStrategy = {
|
|
1376
|
+
extractImports(root) {
|
|
1377
|
+
const imports = [];
|
|
1378
|
+
for (const child of root.children) {
|
|
1379
|
+
if (child.type === "import_declaration") imports.push(...extractGoImportsFromDecl(child));
|
|
1380
|
+
}
|
|
1381
|
+
return imports;
|
|
1382
|
+
},
|
|
1383
|
+
extractExports(root) {
|
|
1384
|
+
const exports = [];
|
|
1385
|
+
for (const child of root.children) {
|
|
1386
|
+
const exp = extractGoExport(child);
|
|
1387
|
+
if (exp) exports.push(exp);
|
|
1388
|
+
}
|
|
1389
|
+
return exports;
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
var RUST_USE_ARG_TYPES = /* @__PURE__ */ new Set([
|
|
1393
|
+
"scoped_identifier",
|
|
1394
|
+
"use_wildcard",
|
|
1395
|
+
"scoped_use_list",
|
|
1396
|
+
"identifier"
|
|
1397
|
+
]);
|
|
1398
|
+
var RUST_PUB_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
1399
|
+
"function_item",
|
|
1400
|
+
"struct_item",
|
|
1401
|
+
"enum_item",
|
|
1402
|
+
"trait_item",
|
|
1403
|
+
"type_item",
|
|
1404
|
+
"const_item",
|
|
1405
|
+
"static_item"
|
|
1406
|
+
]);
|
|
1407
|
+
var rustStrategy = {
|
|
1408
|
+
extractImports(root) {
|
|
1409
|
+
const imports = [];
|
|
1410
|
+
for (const child of root.children) {
|
|
1411
|
+
if (child.type !== "use_declaration") continue;
|
|
1412
|
+
const arg = child.childForFieldName("argument") ?? child.children.find((c) => RUST_USE_ARG_TYPES.has(c.type));
|
|
1413
|
+
if (arg) imports.push(makeImport(child, arg.text));
|
|
1414
|
+
}
|
|
1415
|
+
return imports;
|
|
1416
|
+
},
|
|
1417
|
+
extractExports(root, source) {
|
|
1418
|
+
const exports = [];
|
|
1419
|
+
const lines = source.split("\n");
|
|
1420
|
+
for (const child of root.children) {
|
|
1421
|
+
const line = lines[child.startPosition.row] ?? "";
|
|
1422
|
+
if (!/^\s*pub\b/.test(line)) continue;
|
|
1423
|
+
if (RUST_PUB_ITEM_TYPES.has(child.type)) {
|
|
1424
|
+
const exp = makeNamedExport(child);
|
|
1425
|
+
if (exp) exports.push(exp);
|
|
1426
|
+
} else if (child.type === "mod_item") {
|
|
1427
|
+
const name = child.childForFieldName("name");
|
|
1428
|
+
if (name) {
|
|
1429
|
+
exports.push({
|
|
1430
|
+
name: name.text,
|
|
1431
|
+
type: "namespace",
|
|
1432
|
+
location: makeLocation2(child),
|
|
1433
|
+
isReExport: false
|
|
1434
|
+
});
|
|
2363
1435
|
}
|
|
2364
|
-
|
|
1436
|
+
}
|
|
2365
1437
|
}
|
|
2366
|
-
|
|
2367
|
-
result.value.violations.filter((v) => v.reason === "FORBIDDEN_IMPORT"),
|
|
2368
|
-
rootDir,
|
|
2369
|
-
this.category
|
|
2370
|
-
);
|
|
2371
|
-
return [{ category: this.category, scope: "project", value: violations.length, violations }];
|
|
1438
|
+
return exports;
|
|
2372
1439
|
}
|
|
2373
1440
|
};
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
async function countLoc(filePath) {
|
|
2389
|
-
try {
|
|
2390
|
-
const content = await readFile3(filePath, "utf-8");
|
|
2391
|
-
return content.split("\n").filter((line) => line.trim().length > 0).length;
|
|
2392
|
-
} catch {
|
|
2393
|
-
return 0;
|
|
2394
|
-
}
|
|
2395
|
-
}
|
|
2396
|
-
async function buildModuleStats(rootDir, dir, tsFiles) {
|
|
2397
|
-
let totalLoc = 0;
|
|
2398
|
-
for (const f of tsFiles) {
|
|
2399
|
-
totalLoc += await countLoc(f);
|
|
2400
|
-
}
|
|
2401
|
-
return {
|
|
2402
|
-
modulePath: relativePosix(rootDir, dir),
|
|
2403
|
-
fileCount: tsFiles.length,
|
|
2404
|
-
totalLoc,
|
|
2405
|
-
files: tsFiles.map((f) => relativePosix(rootDir, f))
|
|
2406
|
-
};
|
|
2407
|
-
}
|
|
2408
|
-
async function scanDir(rootDir, dir, modules) {
|
|
2409
|
-
let entries;
|
|
2410
|
-
try {
|
|
2411
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
2412
|
-
} catch {
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
const tsFiles = [];
|
|
2416
|
-
const subdirs = [];
|
|
2417
|
-
for (const entry of entries) {
|
|
2418
|
-
if (isSkippedEntry(entry.name)) continue;
|
|
2419
|
-
const fullPath = join2(dir, entry.name);
|
|
2420
|
-
if (entry.isDirectory()) {
|
|
2421
|
-
subdirs.push(fullPath);
|
|
2422
|
-
continue;
|
|
1441
|
+
var JAVA_IMPORT_TYPES = /* @__PURE__ */ new Set(["scoped_identifier", "scoped_absolute_identifier"]);
|
|
1442
|
+
var JAVA_EXPORT_TYPES = /* @__PURE__ */ new Set([
|
|
1443
|
+
"class_declaration",
|
|
1444
|
+
"interface_declaration",
|
|
1445
|
+
"enum_declaration",
|
|
1446
|
+
"record_declaration"
|
|
1447
|
+
]);
|
|
1448
|
+
var javaStrategy = {
|
|
1449
|
+
extractImports(root) {
|
|
1450
|
+
const imports = [];
|
|
1451
|
+
for (const child of root.children) {
|
|
1452
|
+
if (child.type !== "import_declaration") continue;
|
|
1453
|
+
const scoped = child.children.find((c) => JAVA_IMPORT_TYPES.has(c.type));
|
|
1454
|
+
if (scoped) imports.push(makeImport(child, scoped.text));
|
|
2423
1455
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
1456
|
+
return imports;
|
|
1457
|
+
},
|
|
1458
|
+
extractExports(root, source) {
|
|
1459
|
+
const exports = [];
|
|
1460
|
+
const lines = source.split("\n");
|
|
1461
|
+
for (const child of root.children) {
|
|
1462
|
+
if (!JAVA_EXPORT_TYPES.has(child.type)) continue;
|
|
1463
|
+
const line = lines[child.startPosition.row] ?? "";
|
|
1464
|
+
if (!/\bpublic\b/.test(line)) continue;
|
|
1465
|
+
const exp = makeNamedExport(child);
|
|
1466
|
+
if (exp) exports.push(exp);
|
|
2426
1467
|
}
|
|
1468
|
+
return exports;
|
|
2427
1469
|
}
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
const { maxLoc, maxFiles } = extractThresholds(config);
|
|
2455
|
-
const rules = [];
|
|
2456
|
-
if (maxLoc < Infinity) {
|
|
2457
|
-
const desc = `Module LOC must not exceed ${maxLoc}`;
|
|
2458
|
-
rules.push({
|
|
2459
|
-
id: constraintRuleId(this.category, "project", desc),
|
|
2460
|
-
category: this.category,
|
|
2461
|
-
description: desc,
|
|
2462
|
-
scope: "project"
|
|
2463
|
-
});
|
|
2464
|
-
}
|
|
2465
|
-
if (maxFiles < Infinity) {
|
|
2466
|
-
const desc = `Module file count must not exceed ${maxFiles}`;
|
|
2467
|
-
rules.push({
|
|
2468
|
-
id: constraintRuleId(this.category, "project", desc),
|
|
2469
|
-
category: this.category,
|
|
2470
|
-
description: desc,
|
|
2471
|
-
scope: "project"
|
|
2472
|
-
});
|
|
1470
|
+
};
|
|
1471
|
+
var STRATEGIES = {
|
|
1472
|
+
python: pythonStrategy,
|
|
1473
|
+
go: goStrategy,
|
|
1474
|
+
rust: rustStrategy,
|
|
1475
|
+
java: javaStrategy
|
|
1476
|
+
};
|
|
1477
|
+
var TreeSitterParser = class {
|
|
1478
|
+
name;
|
|
1479
|
+
extensions;
|
|
1480
|
+
lang;
|
|
1481
|
+
strategy;
|
|
1482
|
+
constructor(lang, extensions, strategy) {
|
|
1483
|
+
this.name = lang;
|
|
1484
|
+
this.lang = lang;
|
|
1485
|
+
this.extensions = extensions;
|
|
1486
|
+
this.strategy = strategy;
|
|
1487
|
+
}
|
|
1488
|
+
async parseFile(path) {
|
|
1489
|
+
const contentResult = await readFileContent(path);
|
|
1490
|
+
if (!contentResult.ok) {
|
|
1491
|
+
return Err(
|
|
1492
|
+
createParseError("NOT_FOUND", `File not found: ${path}`, { path }, [
|
|
1493
|
+
"Check that the file exists"
|
|
1494
|
+
])
|
|
1495
|
+
);
|
|
2473
1496
|
}
|
|
2474
|
-
|
|
2475
|
-
const
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
1497
|
+
try {
|
|
1498
|
+
const parser = await getParser(this.lang);
|
|
1499
|
+
const tree = parser.parse(contentResult.value);
|
|
1500
|
+
return Ok({
|
|
1501
|
+
type: "Program",
|
|
1502
|
+
body: { tree, source: contentResult.value },
|
|
1503
|
+
language: this.lang
|
|
2481
1504
|
});
|
|
1505
|
+
} catch (e) {
|
|
1506
|
+
const error = e;
|
|
1507
|
+
return Err(
|
|
1508
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path}: ${error.message}`, { path }, [
|
|
1509
|
+
"Check for syntax errors in the file"
|
|
1510
|
+
])
|
|
1511
|
+
);
|
|
2482
1512
|
}
|
|
2483
|
-
return rules;
|
|
2484
|
-
}
|
|
2485
|
-
async collect(config, rootDir) {
|
|
2486
|
-
const modules = await discoverModules(rootDir);
|
|
2487
|
-
const { maxLoc, maxFiles } = extractThresholds(config);
|
|
2488
|
-
return modules.map((mod) => {
|
|
2489
|
-
const violations = [];
|
|
2490
|
-
if (mod.totalLoc > maxLoc) {
|
|
2491
|
-
violations.push({
|
|
2492
|
-
id: violationId(mod.modulePath, this.category, "totalLoc-exceeded"),
|
|
2493
|
-
file: mod.modulePath,
|
|
2494
|
-
detail: `Module has ${mod.totalLoc} lines of code (threshold: ${maxLoc})`,
|
|
2495
|
-
severity: "warning"
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2498
|
-
if (mod.fileCount > maxFiles) {
|
|
2499
|
-
violations.push({
|
|
2500
|
-
id: violationId(mod.modulePath, this.category, "fileCount-exceeded"),
|
|
2501
|
-
file: mod.modulePath,
|
|
2502
|
-
detail: `Module has ${mod.fileCount} files (threshold: ${maxFiles})`,
|
|
2503
|
-
severity: "warning"
|
|
2504
|
-
});
|
|
2505
|
-
}
|
|
2506
|
-
return {
|
|
2507
|
-
category: this.category,
|
|
2508
|
-
scope: mod.modulePath,
|
|
2509
|
-
value: mod.totalLoc,
|
|
2510
|
-
violations,
|
|
2511
|
-
metadata: { fileCount: mod.fileCount, totalLoc: mod.totalLoc }
|
|
2512
|
-
};
|
|
2513
|
-
});
|
|
2514
1513
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
|
|
2519
|
-
import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
|
|
2520
|
-
import { DEFAULT_SKIP_DIRS as DEFAULT_SKIP_DIRS2 } from "@harness-engineering/graph";
|
|
2521
|
-
function extractImportSources(content, filePath) {
|
|
2522
|
-
const importRegex = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
|
|
2523
|
-
const dynamicRegex = /import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
2524
|
-
const sources = [];
|
|
2525
|
-
const dir = dirname3(filePath);
|
|
2526
|
-
for (const regex of [importRegex, dynamicRegex]) {
|
|
2527
|
-
let match;
|
|
2528
|
-
while ((match = regex.exec(content)) !== null) {
|
|
2529
|
-
let resolved = resolve2(dir, match[1]);
|
|
2530
|
-
if (!resolved.endsWith(".ts") && !resolved.endsWith(".tsx")) {
|
|
2531
|
-
resolved += ".ts";
|
|
2532
|
-
}
|
|
2533
|
-
sources.push(resolved);
|
|
2534
|
-
}
|
|
1514
|
+
extractImports(ast) {
|
|
1515
|
+
const { tree, source } = ast.body;
|
|
1516
|
+
return Ok(this.strategy.extractImports(tree.rootNode, source));
|
|
2535
1517
|
}
|
|
2536
|
-
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
let entries;
|
|
2547
|
-
try {
|
|
2548
|
-
entries = await readdir2(d, { withFileTypes: true });
|
|
2549
|
-
} catch {
|
|
2550
|
-
return;
|
|
2551
|
-
}
|
|
2552
|
-
for (const entry of entries) {
|
|
2553
|
-
if (isSkippedEntry2(entry.name)) continue;
|
|
2554
|
-
const fullPath = join3(d, entry.name);
|
|
2555
|
-
if (entry.isDirectory()) {
|
|
2556
|
-
await scanDir2(fullPath, results);
|
|
2557
|
-
} else if (entry.isFile() && isTsSourceFile2(entry.name)) {
|
|
2558
|
-
results.push(fullPath);
|
|
1518
|
+
extractExports(ast) {
|
|
1519
|
+
const { tree, source } = ast.body;
|
|
1520
|
+
return Ok(this.strategy.extractExports(tree.rootNode, source));
|
|
1521
|
+
}
|
|
1522
|
+
async health() {
|
|
1523
|
+
try {
|
|
1524
|
+
await getParser(this.lang);
|
|
1525
|
+
return Ok({ available: true, message: `tree-sitter ${this.lang} grammar loaded` });
|
|
1526
|
+
} catch {
|
|
1527
|
+
return Ok({ available: false, message: `tree-sitter ${this.lang} grammar not available` });
|
|
2559
1528
|
}
|
|
2560
1529
|
}
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
await scanDir2(dir, results);
|
|
2565
|
-
return results;
|
|
2566
|
-
}
|
|
2567
|
-
function computeLongestChain(file, graph, visited, memo) {
|
|
2568
|
-
if (memo.has(file)) return memo.get(file);
|
|
2569
|
-
if (visited.has(file)) return 0;
|
|
2570
|
-
visited.add(file);
|
|
2571
|
-
const deps = graph.get(file) || [];
|
|
2572
|
-
let maxDepth = 0;
|
|
2573
|
-
for (const dep of deps) {
|
|
2574
|
-
const depth = 1 + computeLongestChain(dep, graph, visited, memo);
|
|
2575
|
-
if (depth > maxDepth) maxDepth = depth;
|
|
1530
|
+
outline(filePath, ast) {
|
|
1531
|
+
const { tree, source } = ast.body;
|
|
1532
|
+
return extractOutlineFromTree(tree.rootNode, this.lang, source, filePath);
|
|
2576
1533
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
const
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
}
|
|
2595
|
-
async buildImportGraph(allFiles) {
|
|
2596
|
-
const graph = /* @__PURE__ */ new Map();
|
|
2597
|
-
const fileSet = new Set(allFiles);
|
|
2598
|
-
for (const file of allFiles) {
|
|
2599
|
-
try {
|
|
2600
|
-
const content = await readFile4(file, "utf-8");
|
|
2601
|
-
graph.set(
|
|
2602
|
-
file,
|
|
2603
|
-
extractImportSources(content, file).filter((imp) => fileSet.has(imp))
|
|
2604
|
-
);
|
|
2605
|
-
} catch {
|
|
2606
|
-
graph.set(file, []);
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
return graph;
|
|
2610
|
-
}
|
|
2611
|
-
buildModuleMap(allFiles, rootDir) {
|
|
2612
|
-
const moduleMap = /* @__PURE__ */ new Map();
|
|
2613
|
-
for (const file of allFiles) {
|
|
2614
|
-
const relDir = relativePosix(rootDir, dirname3(file));
|
|
2615
|
-
if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
|
|
2616
|
-
moduleMap.get(relDir).push(file);
|
|
2617
|
-
}
|
|
2618
|
-
return moduleMap;
|
|
2619
|
-
}
|
|
2620
|
-
async collect(config, rootDir) {
|
|
2621
|
-
const allFiles = await collectTsFiles(rootDir);
|
|
2622
|
-
const graph = await this.buildImportGraph(allFiles);
|
|
2623
|
-
const moduleMap = this.buildModuleMap(allFiles, rootDir);
|
|
2624
|
-
const memo = /* @__PURE__ */ new Map();
|
|
2625
|
-
const threshold = typeof config.thresholds["dependency-depth"] === "number" ? config.thresholds["dependency-depth"] : Infinity;
|
|
2626
|
-
const results = [];
|
|
2627
|
-
for (const [modulePath, files] of moduleMap) {
|
|
2628
|
-
const longestChain = files.reduce((max, file) => {
|
|
2629
|
-
return Math.max(max, computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo));
|
|
2630
|
-
}, 0);
|
|
2631
|
-
const violations = [];
|
|
2632
|
-
if (longestChain > threshold) {
|
|
2633
|
-
violations.push({
|
|
2634
|
-
id: violationId(modulePath, this.category, "depth-exceeded"),
|
|
2635
|
-
file: modulePath,
|
|
2636
|
-
detail: `Import chain depth is ${longestChain} (threshold: ${threshold})`,
|
|
2637
|
-
severity: "warning"
|
|
2638
|
-
});
|
|
2639
|
-
}
|
|
2640
|
-
results.push({
|
|
2641
|
-
category: this.category,
|
|
2642
|
-
scope: modulePath,
|
|
2643
|
-
value: longestChain,
|
|
2644
|
-
violations,
|
|
2645
|
-
metadata: { longestChain }
|
|
2646
|
-
});
|
|
2647
|
-
}
|
|
2648
|
-
return results;
|
|
1534
|
+
unfold(filePath, ast, symbolName) {
|
|
1535
|
+
const outlineResult = this.outline(filePath, ast);
|
|
1536
|
+
if (outlineResult.error || !outlineResult.symbols.length) return null;
|
|
1537
|
+
const { source } = ast.body;
|
|
1538
|
+
const match = findSymbolByName(outlineResult.symbols, symbolName);
|
|
1539
|
+
if (!match) return null;
|
|
1540
|
+
const lines = source.split("\n");
|
|
1541
|
+
const content = lines.slice(match.line - 1, match.endLine).join("\n");
|
|
1542
|
+
return {
|
|
1543
|
+
file: filePath,
|
|
1544
|
+
symbolName,
|
|
1545
|
+
startLine: match.line,
|
|
1546
|
+
endLine: match.endLine,
|
|
1547
|
+
content,
|
|
1548
|
+
language: this.lang,
|
|
1549
|
+
fallback: false
|
|
1550
|
+
};
|
|
2649
1551
|
}
|
|
2650
1552
|
};
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
new ModuleSizeCollector(),
|
|
2660
|
-
new DepDepthCollector()
|
|
2661
|
-
];
|
|
2662
|
-
async function runAll(config, rootDir, collectors = defaultCollectors) {
|
|
2663
|
-
const results = await Promise.allSettled(collectors.map((c) => c.collect(config, rootDir)));
|
|
2664
|
-
const allResults = [];
|
|
2665
|
-
for (let i = 0; i < results.length; i++) {
|
|
2666
|
-
const result = results[i];
|
|
2667
|
-
if (result.status === "fulfilled") {
|
|
2668
|
-
allResults.push(...result.value);
|
|
2669
|
-
} else {
|
|
2670
|
-
allResults.push({
|
|
2671
|
-
category: collectors[i].category,
|
|
2672
|
-
scope: "project",
|
|
2673
|
-
value: 0,
|
|
2674
|
-
violations: [],
|
|
2675
|
-
metadata: { error: String(result.reason) }
|
|
2676
|
-
});
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
return allResults;
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
// src/architecture/matchers.ts
|
|
2683
|
-
function architecture(options) {
|
|
2684
|
-
return {
|
|
2685
|
-
kind: "arch-handle",
|
|
2686
|
-
scope: "project",
|
|
2687
|
-
rootDir: options?.rootDir ?? process.cwd(),
|
|
2688
|
-
config: options?.config
|
|
2689
|
-
};
|
|
2690
|
-
}
|
|
2691
|
-
function archModule(modulePath, options) {
|
|
2692
|
-
return {
|
|
2693
|
-
kind: "arch-handle",
|
|
2694
|
-
scope: modulePath,
|
|
2695
|
-
rootDir: options?.rootDir ?? process.cwd(),
|
|
2696
|
-
config: options?.config
|
|
1553
|
+
function createTreeSitterParser(lang) {
|
|
1554
|
+
const strategy = STRATEGIES[lang];
|
|
1555
|
+
if (!strategy) return null;
|
|
1556
|
+
const extensionMap = {
|
|
1557
|
+
python: [".py"],
|
|
1558
|
+
go: [".go"],
|
|
1559
|
+
rust: [".rs"],
|
|
1560
|
+
java: [".java"]
|
|
2697
1561
|
};
|
|
1562
|
+
const extensions = extensionMap[lang];
|
|
1563
|
+
if (!extensions) return null;
|
|
1564
|
+
return new TreeSitterParser(lang, extensions, strategy);
|
|
2698
1565
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
1566
|
+
|
|
1567
|
+
// src/shared/parsers/registry.ts
|
|
1568
|
+
var ParserRegistry = class {
|
|
1569
|
+
parsers = /* @__PURE__ */ new Map();
|
|
1570
|
+
register(parser) {
|
|
1571
|
+
this.parsers.set(parser.name, parser);
|
|
2705
1572
|
}
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
}
|
|
2709
|
-
function formatViolationList(violations, limit = 10) {
|
|
2710
|
-
const lines = violations.slice(0, limit).map((v) => ` - ${v.file}: ${v.detail}`);
|
|
2711
|
-
if (violations.length > limit) {
|
|
2712
|
-
lines.push(` ... and ${violations.length - limit} more`);
|
|
1573
|
+
getByLanguage(lang) {
|
|
1574
|
+
return this.parsers.get(lang) ?? null;
|
|
2713
1575
|
}
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
const violations = results.flatMap((r) => r.violations);
|
|
2719
|
-
const pass = violations.length === 0;
|
|
2720
|
-
return {
|
|
2721
|
-
pass,
|
|
2722
|
-
message: () => pass ? "Expected circular dependencies but found none" : `Found ${violations.length} circular dependenc${violations.length === 1 ? "y" : "ies"}:
|
|
2723
|
-
${formatViolationList(violations)}`
|
|
2724
|
-
};
|
|
2725
|
-
}
|
|
2726
|
-
async function toHaveNoLayerViolations(received) {
|
|
2727
|
-
const results = await collectCategory(received, new LayerViolationCollector());
|
|
2728
|
-
const violations = results.flatMap((r) => r.violations);
|
|
2729
|
-
const pass = violations.length === 0;
|
|
2730
|
-
return {
|
|
2731
|
-
pass,
|
|
2732
|
-
message: () => pass ? "Expected layer violations but found none" : `Found ${violations.length} layer violation${violations.length === 1 ? "" : "s"}:
|
|
2733
|
-
${formatViolationList(violations)}`
|
|
2734
|
-
};
|
|
2735
|
-
}
|
|
2736
|
-
async function toMatchBaseline(received, options) {
|
|
2737
|
-
let diffResult;
|
|
2738
|
-
if ("_mockDiff" in received && received._mockDiff) {
|
|
2739
|
-
diffResult = received._mockDiff;
|
|
2740
|
-
} else {
|
|
2741
|
-
const config = resolveConfig(received);
|
|
2742
|
-
const results = await runAll(config, received.rootDir);
|
|
2743
|
-
const manager = new ArchBaselineManager(received.rootDir, config.baselinePath);
|
|
2744
|
-
const baseline = manager.load();
|
|
2745
|
-
if (!baseline) {
|
|
2746
|
-
return {
|
|
2747
|
-
pass: false,
|
|
2748
|
-
message: () => "No baseline found. Run `harness check-arch --update-baseline` to create one."
|
|
2749
|
-
};
|
|
2750
|
-
}
|
|
2751
|
-
diffResult = diff(results, baseline);
|
|
1576
|
+
getForFile(filePath) {
|
|
1577
|
+
const lang = detectLanguage(filePath);
|
|
1578
|
+
if (!lang) return null;
|
|
1579
|
+
return this.getByLanguage(lang);
|
|
2752
1580
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
1581
|
+
getSupportedExtensions() {
|
|
1582
|
+
return Object.keys(EXTENSION_MAP);
|
|
1583
|
+
}
|
|
1584
|
+
getSupportedLanguages() {
|
|
1585
|
+
return Array.from(this.parsers.keys());
|
|
1586
|
+
}
|
|
1587
|
+
isSupportedExtension(ext) {
|
|
1588
|
+
return ext in EXTENSION_MAP;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
var defaultRegistry = null;
|
|
1592
|
+
function getDefaultRegistry() {
|
|
1593
|
+
if (defaultRegistry) return defaultRegistry;
|
|
1594
|
+
const registry = new ParserRegistry();
|
|
1595
|
+
const tsParser = new TypeScriptParser();
|
|
1596
|
+
registry.register(tsParser);
|
|
1597
|
+
registry.register({
|
|
1598
|
+
name: "javascript",
|
|
1599
|
+
extensions: [".js", ".jsx", ".mjs", ".cjs"],
|
|
1600
|
+
parseFile: tsParser.parseFile.bind(tsParser),
|
|
1601
|
+
extractImports: tsParser.extractImports.bind(tsParser),
|
|
1602
|
+
extractExports: tsParser.extractExports.bind(tsParser),
|
|
1603
|
+
health: tsParser.health.bind(tsParser)
|
|
1604
|
+
});
|
|
1605
|
+
const treeSitterLanguages = ["python", "go", "rust", "java"];
|
|
1606
|
+
for (const lang of treeSitterLanguages) {
|
|
1607
|
+
const parser = createTreeSitterParser(lang);
|
|
1608
|
+
if (parser) {
|
|
1609
|
+
registry.register(parser);
|
|
2778
1610
|
}
|
|
2779
|
-
};
|
|
2780
|
-
}
|
|
2781
|
-
function filterByScope(results, scope) {
|
|
2782
|
-
return results.filter(
|
|
2783
|
-
(r) => r.scope === scope || r.scope.startsWith(scope + "/") || r.scope === "project"
|
|
2784
|
-
);
|
|
2785
|
-
}
|
|
2786
|
-
async function toHaveMaxComplexity(received, maxComplexity) {
|
|
2787
|
-
const results = await collectCategory(received, new ComplexityCollector());
|
|
2788
|
-
const scoped = filterByScope(results, received.scope);
|
|
2789
|
-
const violations = scoped.flatMap((r) => r.violations);
|
|
2790
|
-
const totalValue = scoped.reduce((sum, r) => sum + r.value, 0);
|
|
2791
|
-
const pass = totalValue <= maxComplexity && violations.length === 0;
|
|
2792
|
-
return {
|
|
2793
|
-
pass,
|
|
2794
|
-
message: () => pass ? `Expected complexity to exceed ${maxComplexity} but it was within limits` : `Module '${received.scope}' has complexity violations (${violations.length} violation${violations.length === 1 ? "" : "s"}):
|
|
2795
|
-
${formatViolationList(violations)}`
|
|
2796
|
-
};
|
|
2797
|
-
}
|
|
2798
|
-
async function toHaveMaxCoupling(received, limits) {
|
|
2799
|
-
const config = resolveConfig(received);
|
|
2800
|
-
if (limits.fanIn !== void 0 || limits.fanOut !== void 0) {
|
|
2801
|
-
config.thresholds.coupling = {
|
|
2802
|
-
...typeof config.thresholds.coupling === "object" ? config.thresholds.coupling : {},
|
|
2803
|
-
...limits.fanIn !== void 0 ? { maxFanIn: limits.fanIn } : {},
|
|
2804
|
-
...limits.fanOut !== void 0 ? { maxFanOut: limits.fanOut } : {}
|
|
2805
|
-
};
|
|
2806
1611
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
const scoped = filterByScope(results, received.scope);
|
|
2810
|
-
const violations = scoped.flatMap((r) => r.violations);
|
|
2811
|
-
const pass = violations.length === 0;
|
|
2812
|
-
return {
|
|
2813
|
-
pass,
|
|
2814
|
-
message: () => pass ? `Expected coupling violations in '${received.scope}' but found none` : `Module '${received.scope}' has ${violations.length} coupling violation${violations.length === 1 ? "" : "s"} (fanIn limit: ${limits.fanIn ?? "none"}, fanOut limit: ${limits.fanOut ?? "none"}):
|
|
2815
|
-
${formatViolationList(violations)}`
|
|
2816
|
-
};
|
|
2817
|
-
}
|
|
2818
|
-
async function toHaveMaxFileCount(received, maxFiles) {
|
|
2819
|
-
const results = await collectCategory(received, new ModuleSizeCollector());
|
|
2820
|
-
const scoped = filterByScope(results, received.scope);
|
|
2821
|
-
const fileCount = scoped.reduce((max, r) => {
|
|
2822
|
-
const meta = r.metadata;
|
|
2823
|
-
const fc = typeof meta?.fileCount === "number" ? meta.fileCount : 0;
|
|
2824
|
-
return fc > max ? fc : max;
|
|
2825
|
-
}, 0);
|
|
2826
|
-
const pass = fileCount <= maxFiles;
|
|
2827
|
-
return {
|
|
2828
|
-
pass,
|
|
2829
|
-
message: () => pass ? `Expected file count in '${received.scope}' to exceed ${maxFiles} but it was ${fileCount}` : `Module '${received.scope}' has ${fileCount} files (limit: ${maxFiles})`
|
|
2830
|
-
};
|
|
2831
|
-
}
|
|
2832
|
-
async function toNotDependOn(received, forbiddenModule) {
|
|
2833
|
-
const results = await collectCategory(received, new ForbiddenImportCollector());
|
|
2834
|
-
const allViolations = results.flatMap((r) => r.violations);
|
|
2835
|
-
const scopePrefix = received.scope.replace(/\/+$/, "");
|
|
2836
|
-
const forbiddenPrefix = forbiddenModule.replace(/\/+$/, "");
|
|
2837
|
-
const relevantViolations = allViolations.filter(
|
|
2838
|
-
(v) => (v.file === scopePrefix || v.file.startsWith(scopePrefix + "/")) && (v.detail.includes(forbiddenPrefix + "/") || v.detail.endsWith(forbiddenPrefix))
|
|
2839
|
-
);
|
|
2840
|
-
const pass = relevantViolations.length === 0;
|
|
2841
|
-
return {
|
|
2842
|
-
pass,
|
|
2843
|
-
message: () => pass ? `Expected '${received.scope}' to depend on '${forbiddenModule}' but no such imports found` : `Module '${received.scope}' depends on '${forbiddenModule}' (${relevantViolations.length} import${relevantViolations.length === 1 ? "" : "s"}):
|
|
2844
|
-
${formatViolationList(relevantViolations)}`
|
|
2845
|
-
};
|
|
2846
|
-
}
|
|
2847
|
-
async function toHaveMaxDepDepth(received, maxDepth) {
|
|
2848
|
-
const results = await collectCategory(received, new DepDepthCollector());
|
|
2849
|
-
const scoped = filterByScope(results, received.scope);
|
|
2850
|
-
const maxActual = scoped.reduce((max, r) => r.value > max ? r.value : max, 0);
|
|
2851
|
-
const pass = maxActual <= maxDepth;
|
|
2852
|
-
return {
|
|
2853
|
-
pass,
|
|
2854
|
-
message: () => pass ? `Expected dependency depth in '${received.scope}' to exceed ${maxDepth} but it was ${maxActual}` : `Module '${received.scope}' has dependency depth ${maxActual} (limit: ${maxDepth})`
|
|
2855
|
-
};
|
|
1612
|
+
defaultRegistry = registry;
|
|
1613
|
+
return registry;
|
|
2856
1614
|
}
|
|
2857
|
-
var archMatchers = {
|
|
2858
|
-
toHaveNoCircularDeps,
|
|
2859
|
-
toHaveNoLayerViolations,
|
|
2860
|
-
toMatchBaseline,
|
|
2861
|
-
toHaveMaxComplexity,
|
|
2862
|
-
toHaveMaxCoupling,
|
|
2863
|
-
toHaveMaxFileCount,
|
|
2864
|
-
toNotDependOn,
|
|
2865
|
-
toHaveMaxDepDepth
|
|
2866
|
-
};
|
|
2867
1615
|
|
|
2868
1616
|
export {
|
|
2869
|
-
__export,
|
|
2870
1617
|
createError,
|
|
2871
1618
|
createEntropyError,
|
|
2872
1619
|
Ok,
|
|
@@ -2889,39 +1636,6 @@ export {
|
|
|
2889
1636
|
resolveFileToLayer,
|
|
2890
1637
|
buildDependencyGraph,
|
|
2891
1638
|
validateDependencies,
|
|
2892
|
-
detectCircularDeps,
|
|
2893
|
-
detectCircularDepsInFiles,
|
|
2894
1639
|
detectComplexityViolations,
|
|
2895
|
-
detectCouplingViolations
|
|
2896
|
-
ArchMetricCategorySchema,
|
|
2897
|
-
ViolationSchema,
|
|
2898
|
-
MetricResultSchema,
|
|
2899
|
-
CategoryBaselineSchema,
|
|
2900
|
-
ArchBaselineSchema,
|
|
2901
|
-
CategoryRegressionSchema,
|
|
2902
|
-
ArchDiffResultSchema,
|
|
2903
|
-
ThresholdConfigSchema,
|
|
2904
|
-
ArchConfigSchema,
|
|
2905
|
-
ConstraintRuleSchema,
|
|
2906
|
-
ViolationSnapshotSchema,
|
|
2907
|
-
ViolationHistorySchema,
|
|
2908
|
-
EmergenceConfidenceSchema,
|
|
2909
|
-
EmergentConstraintSuggestionSchema,
|
|
2910
|
-
EmergenceResultSchema,
|
|
2911
|
-
violationId,
|
|
2912
|
-
constraintRuleId,
|
|
2913
|
-
CircularDepsCollector,
|
|
2914
|
-
LayerViolationCollector,
|
|
2915
|
-
ComplexityCollector,
|
|
2916
|
-
CouplingCollector,
|
|
2917
|
-
ForbiddenImportCollector,
|
|
2918
|
-
ModuleSizeCollector,
|
|
2919
|
-
DepDepthCollector,
|
|
2920
|
-
defaultCollectors,
|
|
2921
|
-
runAll,
|
|
2922
|
-
ArchBaselineManager,
|
|
2923
|
-
diff,
|
|
2924
|
-
architecture,
|
|
2925
|
-
archModule,
|
|
2926
|
-
archMatchers
|
|
1640
|
+
detectCouplingViolations
|
|
2927
1641
|
};
|