@fraction12/deepclean 0.1.0-alpha.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/args.d.ts +9 -0
- package/dist/args.js +105 -0
- package/dist/args.js.map +1 -0
- package/dist/candidates.d.ts +6 -0
- package/dist/candidates.js +319 -0
- package/dist/candidates.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +521 -0
- package/dist/cli.js.map +1 -0
- package/dist/clusters.d.ts +7 -0
- package/dist/clusters.js +399 -0
- package/dist/clusters.js.map +1 -0
- package/dist/defaults.d.ts +5 -0
- package/dist/defaults.js +130 -0
- package/dist/defaults.js.map +1 -0
- package/dist/discovery.d.ts +10 -0
- package/dist/discovery.js +48 -0
- package/dist/discovery.js.map +1 -0
- package/dist/evidence.d.ts +16 -0
- package/dist/evidence.js +853 -0
- package/dist/evidence.js.map +1 -0
- package/dist/ids.d.ts +4 -0
- package/dist/ids.js +16 -0
- package/dist/ids.js.map +1 -0
- package/dist/json.d.ts +3 -0
- package/dist/json.js +12 -0
- package/dist/json.js.map +1 -0
- package/dist/plans.d.ts +3 -0
- package/dist/plans.js +171 -0
- package/dist/plans.js.map +1 -0
- package/dist/reporting.d.ts +13 -0
- package/dist/reporting.js +227 -0
- package/dist/reporting.js.map +1 -0
- package/dist/reviewers.d.ts +22 -0
- package/dist/reviewers.js +461 -0
- package/dist/reviewers.js.map +1 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.js +211 -0
- package/dist/state.js.map +1 -0
- package/dist/synthesis.d.ts +17 -0
- package/dist/synthesis.js +396 -0
- package/dist/synthesis.js.map +1 -0
- package/dist/types.d.ts +389 -0
- package/dist/types.js +236 -0
- package/dist/types.js.map +1 -0
- package/dist/verification.d.ts +11 -0
- package/dist/verification.js +111 -0
- package/dist/verification.js.map +1 -0
- package/docs/privacy-and-trust.md +33 -0
- package/docs/public-readiness.md +19 -0
- package/docs/reviewer-references.md +33 -0
- package/docs/troubleshooting.md +80 -0
- package/package.json +55 -0
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
import { isTestPath, normalizePath } from "./discovery.js";
|
|
8
|
+
import { stableId } from "./ids.js";
|
|
9
|
+
import { schemaVersion } from "./types.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
export const evidenceAdapters = {
|
|
12
|
+
"file-metrics": fileMetricsAdapter,
|
|
13
|
+
"line-window-duplication": duplicationAdapter,
|
|
14
|
+
"jscpd": jscpdAdapter,
|
|
15
|
+
"semgrep": semgrepAdapter,
|
|
16
|
+
"sarif-ingest": sarifIngestAdapter,
|
|
17
|
+
"code-graph": codeGraphAdapter,
|
|
18
|
+
"import-graph": importGraphAdapter,
|
|
19
|
+
"typescript-structure": typescriptStructureAdapter,
|
|
20
|
+
"git-history": gitHistoryAdapter,
|
|
21
|
+
"test-discovery": testDiscoveryAdapter,
|
|
22
|
+
};
|
|
23
|
+
export async function runEvidenceAdapters(enabledAdapters, context) {
|
|
24
|
+
const evidence = [];
|
|
25
|
+
const diagnostics = [];
|
|
26
|
+
for (const adapterName of enabledAdapters) {
|
|
27
|
+
const adapter = evidenceAdapters[adapterName];
|
|
28
|
+
if (!adapter) {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
level: "warning",
|
|
31
|
+
code: "adapter_unknown",
|
|
32
|
+
message: `Unknown evidence adapter: ${adapterName}`,
|
|
33
|
+
adapter: adapterName,
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const result = await adapter(context);
|
|
39
|
+
evidence.push(...result.evidence);
|
|
40
|
+
diagnostics.push(...result.diagnostics);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
level: "error",
|
|
45
|
+
code: "adapter_failed",
|
|
46
|
+
message: error instanceof Error ? error.message : String(error),
|
|
47
|
+
adapter: adapterName,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { evidence, diagnostics };
|
|
52
|
+
}
|
|
53
|
+
async function jscpdAdapter(context) {
|
|
54
|
+
const settings = context.config.externalAnalyzers.jscpd;
|
|
55
|
+
if (!settings.enabled) {
|
|
56
|
+
return { evidence: [], diagnostics: [] };
|
|
57
|
+
}
|
|
58
|
+
const outputDir = await mkdtemp(path.join(os.tmpdir(), "deepclean-jscpd-"));
|
|
59
|
+
try {
|
|
60
|
+
await execFileAsync(settings.command, [
|
|
61
|
+
"--silent",
|
|
62
|
+
"--min-tokens",
|
|
63
|
+
String(settings.minTokens),
|
|
64
|
+
"--reporters",
|
|
65
|
+
"json",
|
|
66
|
+
"--output",
|
|
67
|
+
outputDir,
|
|
68
|
+
context.root,
|
|
69
|
+
], { maxBuffer: 1024 * 1024 * 20 });
|
|
70
|
+
const report = await readFirstJsonReport(outputDir);
|
|
71
|
+
const duplicates = Array.isArray(report["duplicates"]) ? report["duplicates"] : [];
|
|
72
|
+
const sourcePathSet = new Set(context.files.map((file) => file.path));
|
|
73
|
+
const evidence = [];
|
|
74
|
+
for (const duplicate of duplicates.slice(0, settings.maxFindings)) {
|
|
75
|
+
if (!isObject(duplicate)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const files = duplicateFiles(duplicate, context.root).filter((file) => sourcePathSet.has(file.path));
|
|
79
|
+
const uniquePaths = new Set(files.map((file) => file.path));
|
|
80
|
+
if (uniquePaths.size < 2) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const lines = typeof duplicate["lines"] === "number" ? duplicate["lines"] : undefined;
|
|
84
|
+
evidence.push(makeEvidence(context, {
|
|
85
|
+
id: stableId("ev", `jscpd:${[...uniquePaths].join("|")}:${lines ?? ""}`),
|
|
86
|
+
adapter: "jscpd",
|
|
87
|
+
kind: "external-duplicate",
|
|
88
|
+
title: `jscpd duplicate across ${uniquePaths.size} files`,
|
|
89
|
+
summary: `jscpd reported a duplicate block across ${uniquePaths.size} files${lines ? ` spanning about ${lines} lines` : ""}.`,
|
|
90
|
+
files,
|
|
91
|
+
data: compactObject(duplicate, ["fragment"]),
|
|
92
|
+
confidence: uniquePaths.size >= 3 ? "high" : "medium",
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
return { evidence, diagnostics: [] };
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
evidence: [],
|
|
100
|
+
diagnostics: [{
|
|
101
|
+
level: "info",
|
|
102
|
+
code: "jscpd_unavailable",
|
|
103
|
+
message: `jscpd adapter skipped: ${error instanceof Error ? error.message : String(error)}`,
|
|
104
|
+
adapter: "jscpd",
|
|
105
|
+
}],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function semgrepAdapter(context) {
|
|
113
|
+
const settings = context.config.externalAnalyzers.semgrep;
|
|
114
|
+
if (!settings.enabled) {
|
|
115
|
+
return { evidence: [], diagnostics: [] };
|
|
116
|
+
}
|
|
117
|
+
const outputDir = await mkdtemp(path.join(os.tmpdir(), "deepclean-semgrep-"));
|
|
118
|
+
const outputPath = path.join(outputDir, "semgrep.sarif");
|
|
119
|
+
try {
|
|
120
|
+
await execFileAsync(settings.command, [
|
|
121
|
+
"scan",
|
|
122
|
+
"--config",
|
|
123
|
+
settings.config,
|
|
124
|
+
"--sarif",
|
|
125
|
+
"--output",
|
|
126
|
+
outputPath,
|
|
127
|
+
context.root,
|
|
128
|
+
], {
|
|
129
|
+
maxBuffer: 1024 * 1024 * 20,
|
|
130
|
+
timeout: settings.timeoutMs,
|
|
131
|
+
});
|
|
132
|
+
const raw = await readFile(outputPath, "utf8");
|
|
133
|
+
const parsed = JSON.parse(raw);
|
|
134
|
+
return {
|
|
135
|
+
evidence: evidenceFromSarif(context, parsed, `semgrep:${settings.config}`).slice(0, settings.maxFindings),
|
|
136
|
+
diagnostics: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return {
|
|
141
|
+
evidence: [],
|
|
142
|
+
diagnostics: [{
|
|
143
|
+
level: "info",
|
|
144
|
+
code: "semgrep_unavailable",
|
|
145
|
+
message: `Semgrep adapter skipped: ${error instanceof Error ? error.message : String(error)}`,
|
|
146
|
+
adapter: "semgrep",
|
|
147
|
+
}],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function sarifIngestAdapter(context) {
|
|
155
|
+
const evidence = [];
|
|
156
|
+
for (const sarifPath of context.config.externalAnalyzers.sarifPaths) {
|
|
157
|
+
const absolutePath = path.resolve(context.root, sarifPath);
|
|
158
|
+
let raw;
|
|
159
|
+
try {
|
|
160
|
+
raw = await readFile(absolutePath, "utf8");
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const parsed = JSON.parse(raw);
|
|
166
|
+
evidence.push(...evidenceFromSarif(context, parsed, sarifPath));
|
|
167
|
+
}
|
|
168
|
+
return { evidence: evidence.slice(0, 80), diagnostics: [] };
|
|
169
|
+
}
|
|
170
|
+
function evidenceFromSarif(context, parsed, sarifPath) {
|
|
171
|
+
const evidence = [];
|
|
172
|
+
if (!isObject(parsed) || !Array.isArray(parsed["runs"])) {
|
|
173
|
+
return evidence;
|
|
174
|
+
}
|
|
175
|
+
for (const run of parsed["runs"]) {
|
|
176
|
+
if (!isObject(run) || !Array.isArray(run["results"])) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const toolName = sarifToolName(run);
|
|
180
|
+
for (const result of run["results"].slice(0, 50)) {
|
|
181
|
+
if (!isObject(result)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const files = sarifFiles(result);
|
|
185
|
+
if (files.length === 0) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const title = sarifTitle(result);
|
|
189
|
+
evidence.push(makeEvidence(context, {
|
|
190
|
+
id: stableId("ev", `sarif:${sarifPath}:${title}:${files.map((file) => `${file.path}:${file.startLine ?? ""}`).join("|")}`),
|
|
191
|
+
adapter: sarifPath.startsWith("semgrep:") ? "semgrep" : "sarif-ingest",
|
|
192
|
+
kind: "sarif-finding",
|
|
193
|
+
title,
|
|
194
|
+
summary: `${toolName} reported ${title}.`,
|
|
195
|
+
files,
|
|
196
|
+
data: {
|
|
197
|
+
sarifPath,
|
|
198
|
+
toolName,
|
|
199
|
+
ruleId: typeof result["ruleId"] === "string" ? result["ruleId"] : undefined,
|
|
200
|
+
level: typeof result["level"] === "string" ? result["level"] : undefined,
|
|
201
|
+
},
|
|
202
|
+
confidence: sarifConfidence(result),
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return evidence;
|
|
207
|
+
}
|
|
208
|
+
function makeEvidence(context, values) {
|
|
209
|
+
return {
|
|
210
|
+
schemaVersion,
|
|
211
|
+
recordType: "evidence",
|
|
212
|
+
runId: context.runId,
|
|
213
|
+
createdAt: context.createdAt,
|
|
214
|
+
...values,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async function fileMetricsAdapter(context) {
|
|
218
|
+
const evidence = [];
|
|
219
|
+
for (const file of context.files) {
|
|
220
|
+
const nonBlank = file.lines.filter((line) => line.trim().length > 0).length;
|
|
221
|
+
if (nonBlank < 220) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
evidence.push(makeEvidence(context, {
|
|
225
|
+
id: stableId("ev", `file-metrics:${file.path}:${nonBlank}`),
|
|
226
|
+
adapter: "file-metrics",
|
|
227
|
+
kind: "large-file",
|
|
228
|
+
title: `Large source file: ${file.path}`,
|
|
229
|
+
summary: `${file.path} has ${nonBlank} non-blank lines, which is a useful maintainability hotspot for review.`,
|
|
230
|
+
files: [{ path: file.path, startLine: 1, endLine: file.lines.length }],
|
|
231
|
+
data: { nonBlankLines: nonBlank, totalLines: file.lines.length },
|
|
232
|
+
confidence: nonBlank >= 500 ? "high" : "medium",
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
return { evidence, diagnostics: [] };
|
|
236
|
+
}
|
|
237
|
+
async function duplicationAdapter(context) {
|
|
238
|
+
const windows = new Map();
|
|
239
|
+
const windowSize = 6;
|
|
240
|
+
for (const file of context.files.filter((item) => !isTestPath(item.path))) {
|
|
241
|
+
const normalized = file.lines.map(normalizeCodeLine);
|
|
242
|
+
for (let index = 0; index <= normalized.length - windowSize; index += 1) {
|
|
243
|
+
const slice = normalized.slice(index, index + windowSize);
|
|
244
|
+
if (slice.filter(Boolean).length < windowSize) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (looksLikeLiteralList(slice)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (slice.filter((line) => /[=({.]|return|if |for |while /.test(line)).length < 3) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const key = slice.join("\n");
|
|
254
|
+
const existing = windows.get(key) ?? [];
|
|
255
|
+
existing.push({
|
|
256
|
+
file,
|
|
257
|
+
startLine: index + 1,
|
|
258
|
+
text: file.lines.slice(index, index + windowSize).join("\n"),
|
|
259
|
+
});
|
|
260
|
+
windows.set(key, existing);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const evidence = [];
|
|
264
|
+
for (const [key, matches] of windows.entries()) {
|
|
265
|
+
const uniqueFiles = new Set(matches.map((match) => match.file.path));
|
|
266
|
+
if (uniqueFiles.size < 2) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const selected = firstMatchPerFile(matches).slice(0, 5);
|
|
270
|
+
evidence.push(makeEvidence(context, {
|
|
271
|
+
id: stableId("ev", `duplication:${key}`),
|
|
272
|
+
adapter: "line-window-duplication",
|
|
273
|
+
kind: "duplicate-cluster",
|
|
274
|
+
title: `Repeated code block across ${uniqueFiles.size} files`,
|
|
275
|
+
summary: `A repeated ${windowSize}-line normalized code block appears in ${uniqueFiles.size} files.`,
|
|
276
|
+
files: selected.map((match) => ({
|
|
277
|
+
path: match.file.path,
|
|
278
|
+
startLine: match.startLine,
|
|
279
|
+
endLine: match.startLine + windowSize - 1,
|
|
280
|
+
})),
|
|
281
|
+
data: {
|
|
282
|
+
occurrences: matches.length,
|
|
283
|
+
uniqueFiles: [...uniqueFiles],
|
|
284
|
+
sample: selected[0]?.text ?? "",
|
|
285
|
+
},
|
|
286
|
+
confidence: uniqueFiles.size >= 3 ? "high" : "medium",
|
|
287
|
+
}));
|
|
288
|
+
if (evidence.length >= 25) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { evidence, diagnostics: [] };
|
|
293
|
+
}
|
|
294
|
+
async function codeGraphAdapter(context) {
|
|
295
|
+
const graph = buildLocalImportGraph(context.files.filter((file) => !isTestPath(file.path)));
|
|
296
|
+
const nodes = [...graph.nodes.entries()].map(([filePath, node]) => ({
|
|
297
|
+
path: filePath,
|
|
298
|
+
directory: moduleDirectory(filePath),
|
|
299
|
+
topLevel: topLevel(filePath),
|
|
300
|
+
incoming: node.importedBy.size,
|
|
301
|
+
outgoing: node.imports.size,
|
|
302
|
+
}));
|
|
303
|
+
const hotspots = [...nodes]
|
|
304
|
+
.sort((a, b) => (b.incoming + b.outgoing) - (a.incoming + a.outgoing))
|
|
305
|
+
.slice(0, 40);
|
|
306
|
+
const directories = summarizeDirectories(nodes, graph.edges);
|
|
307
|
+
const files = hotspots.slice(0, 12).map((node) => ({ path: node.path }));
|
|
308
|
+
return {
|
|
309
|
+
evidence: [makeEvidence(context, {
|
|
310
|
+
id: stableId("ev", `code-graph:${graph.nodes.size}:${graph.edges.length}:${hotspots.map((node) => node.path).join("|")}`),
|
|
311
|
+
adapter: "code-graph",
|
|
312
|
+
kind: "code-graph-summary",
|
|
313
|
+
title: "Local import graph summary",
|
|
314
|
+
summary: `The local source graph contains ${graph.nodes.size} files and ${graph.edges.length} local import edges across ${directories.length} directories.`,
|
|
315
|
+
files,
|
|
316
|
+
data: {
|
|
317
|
+
nodeCount: graph.nodes.size,
|
|
318
|
+
edgeCount: graph.edges.length,
|
|
319
|
+
hotspots,
|
|
320
|
+
directories,
|
|
321
|
+
edges: graph.edges.slice(0, 800),
|
|
322
|
+
nodes: nodes.slice(0, 300),
|
|
323
|
+
},
|
|
324
|
+
confidence: graph.edges.length > 0 ? "high" : "low",
|
|
325
|
+
})],
|
|
326
|
+
diagnostics: [],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async function importGraphAdapter(context) {
|
|
330
|
+
const graph = buildLocalImportGraph(context.files.filter((file) => !isTestPath(file.path)));
|
|
331
|
+
const evidence = [];
|
|
332
|
+
for (const [filePath, node] of graph.nodes.entries()) {
|
|
333
|
+
const incoming = node.importedBy.size;
|
|
334
|
+
const outgoing = node.imports.size;
|
|
335
|
+
if (incoming < 4 && outgoing < 8) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
evidence.push(makeEvidence(context, {
|
|
339
|
+
id: stableId("ev", `import-graph:${filePath}:${incoming}:${outgoing}`),
|
|
340
|
+
adapter: "import-graph",
|
|
341
|
+
kind: "dependency-hotspot",
|
|
342
|
+
title: `Dependency hotspot: ${filePath}`,
|
|
343
|
+
summary: `${filePath} has ${incoming} incoming and ${outgoing} outgoing local dependencies.`,
|
|
344
|
+
files: [{ path: filePath }],
|
|
345
|
+
data: {
|
|
346
|
+
incoming,
|
|
347
|
+
outgoing,
|
|
348
|
+
imports: [...node.imports],
|
|
349
|
+
importedBy: [...node.importedBy],
|
|
350
|
+
},
|
|
351
|
+
confidence: incoming >= 8 || outgoing >= 12 ? "high" : "medium",
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
return { evidence, diagnostics: [] };
|
|
355
|
+
}
|
|
356
|
+
async function typescriptStructureAdapter(context) {
|
|
357
|
+
const evidence = [];
|
|
358
|
+
for (const file of context.files.filter(isTsLikeFile)) {
|
|
359
|
+
const shallowWrappers = [];
|
|
360
|
+
const sourceFile = ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true, file.extension.includes("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
361
|
+
visitNode(sourceFile, (node) => {
|
|
362
|
+
if (!isFunctionLikeWithBody(node)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
|
|
366
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.end);
|
|
367
|
+
const span = end.line - start.line + 1;
|
|
368
|
+
const name = functionName(node) ?? "anonymous function";
|
|
369
|
+
if (span >= 70) {
|
|
370
|
+
evidence.push(makeEvidence(context, {
|
|
371
|
+
id: stableId("ev", `ts-structure:function:${file.path}:${start.line}:${name}`),
|
|
372
|
+
adapter: "typescript-structure",
|
|
373
|
+
kind: "large-function",
|
|
374
|
+
title: `Large function: ${name}`,
|
|
375
|
+
summary: `${name} spans ${span} lines in ${file.path}.`,
|
|
376
|
+
files: [{
|
|
377
|
+
path: file.path,
|
|
378
|
+
startLine: start.line + 1,
|
|
379
|
+
endLine: end.line + 1,
|
|
380
|
+
}],
|
|
381
|
+
data: { name, lines: span },
|
|
382
|
+
confidence: span >= 120 ? "high" : "medium",
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
const body = node.body;
|
|
386
|
+
if (body && ts.isBlock(body) && body.statements.length === 1 && span <= 12) {
|
|
387
|
+
const onlyStatement = body.statements[0];
|
|
388
|
+
if (onlyStatement && ts.isReturnStatement(onlyStatement) && onlyStatement.expression) {
|
|
389
|
+
shallowWrappers.push({
|
|
390
|
+
name,
|
|
391
|
+
startLine: start.line + 1,
|
|
392
|
+
endLine: end.line + 1,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
if (shallowWrappers.length >= 5) {
|
|
398
|
+
evidence.push(makeEvidence(context, {
|
|
399
|
+
id: stableId("ev", `ts-structure:wrapper-cluster:${file.path}:${shallowWrappers.length}`),
|
|
400
|
+
adapter: "typescript-structure",
|
|
401
|
+
kind: "shallow-wrapper-cluster",
|
|
402
|
+
title: `Shallow wrapper cluster: ${file.path}`,
|
|
403
|
+
summary: `${file.path} contains ${shallowWrappers.length} tiny wrappers that only return another expression.`,
|
|
404
|
+
files: shallowWrappers.slice(0, 8).map((wrapper) => ({
|
|
405
|
+
path: file.path,
|
|
406
|
+
startLine: wrapper.startLine,
|
|
407
|
+
endLine: wrapper.endLine,
|
|
408
|
+
})),
|
|
409
|
+
data: { wrappers: shallowWrappers },
|
|
410
|
+
confidence: "medium",
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { evidence: evidence.slice(0, 80), diagnostics: [] };
|
|
415
|
+
}
|
|
416
|
+
async function gitHistoryAdapter(context) {
|
|
417
|
+
try {
|
|
418
|
+
const { stdout } = await execFileAsync("git", ["-C", context.root, "log", "--since=90 days ago", "--numstat", "--format=format:--commit--"], { maxBuffer: 1024 * 1024 * 10 });
|
|
419
|
+
const stats = new Map();
|
|
420
|
+
const seenInCommit = new Set();
|
|
421
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
422
|
+
if (line === "--commit--") {
|
|
423
|
+
for (const filePath of seenInCommit) {
|
|
424
|
+
const current = stats.get(filePath) ?? { commits: 0, changedLines: 0 };
|
|
425
|
+
current.commits += 1;
|
|
426
|
+
stats.set(filePath, current);
|
|
427
|
+
}
|
|
428
|
+
seenInCommit.clear();
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const parts = line.split("\t");
|
|
432
|
+
if (parts.length < 3) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const [addedRaw, removedRaw, filePathRaw] = parts;
|
|
436
|
+
if (!addedRaw || !removedRaw || !filePathRaw) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const filePath = normalizePath(filePathRaw);
|
|
440
|
+
if (!context.files.some((file) => file.path === filePath)) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const added = Number.parseInt(addedRaw, 10) || 0;
|
|
444
|
+
const removed = Number.parseInt(removedRaw, 10) || 0;
|
|
445
|
+
const current = stats.get(filePath) ?? { commits: 0, changedLines: 0 };
|
|
446
|
+
current.changedLines += added + removed;
|
|
447
|
+
stats.set(filePath, current);
|
|
448
|
+
seenInCommit.add(filePath);
|
|
449
|
+
}
|
|
450
|
+
const evidence = [];
|
|
451
|
+
for (const [filePath, stat] of stats.entries()) {
|
|
452
|
+
if (stat.commits < 4 && stat.changedLines < 180) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
evidence.push(makeEvidence(context, {
|
|
456
|
+
id: stableId("ev", `git-history:${filePath}:${stat.commits}:${stat.changedLines}`),
|
|
457
|
+
adapter: "git-history",
|
|
458
|
+
kind: "churn-hotspot",
|
|
459
|
+
title: `High churn file: ${filePath}`,
|
|
460
|
+
summary: `${filePath} changed in ${stat.commits} commits with ${stat.changedLines} changed lines over the last 90 days.`,
|
|
461
|
+
files: [{ path: filePath }],
|
|
462
|
+
data: stat,
|
|
463
|
+
confidence: stat.commits >= 8 || stat.changedLines >= 400 ? "high" : "medium",
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
return { evidence, diagnostics: [] };
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
const message = error instanceof Error ? error.message : "Git history is unavailable";
|
|
470
|
+
const hasNoCommits = message.includes("does not have any commits yet");
|
|
471
|
+
return {
|
|
472
|
+
evidence: [],
|
|
473
|
+
diagnostics: [{
|
|
474
|
+
level: hasNoCommits ? "info" : "warning",
|
|
475
|
+
code: "git_history_unavailable",
|
|
476
|
+
message: hasNoCommits
|
|
477
|
+
? "Git history adapter skipped because this repository has no commits yet."
|
|
478
|
+
: message,
|
|
479
|
+
adapter: "git-history",
|
|
480
|
+
}],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function testDiscoveryAdapter(context) {
|
|
485
|
+
const testFiles = context.files.filter((file) => isTestPath(file.path));
|
|
486
|
+
const sourceFiles = context.files.filter((file) => !isTestPath(file.path));
|
|
487
|
+
const testStems = new Set(testFiles.map((file) => stem(file.path)));
|
|
488
|
+
const evidence = [];
|
|
489
|
+
for (const file of sourceFiles) {
|
|
490
|
+
const nonBlank = file.lines.filter((line) => line.trim().length > 0).length;
|
|
491
|
+
if (nonBlank < 90) {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (testStems.has(stem(file.path))) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
evidence.push(makeEvidence(context, {
|
|
498
|
+
id: stableId("ev", `test-discovery:${file.path}:${nonBlank}`),
|
|
499
|
+
adapter: "test-discovery",
|
|
500
|
+
kind: "test-gap",
|
|
501
|
+
title: `No nearby test discovered: ${file.path}`,
|
|
502
|
+
summary: `${file.path} has ${nonBlank} non-blank lines and no nearby test file discovered by naming convention.`,
|
|
503
|
+
files: [{ path: file.path }],
|
|
504
|
+
data: { nonBlankLines: nonBlank, discoveredTestFiles: testFiles.length },
|
|
505
|
+
confidence: nonBlank >= 220 ? "medium" : "low",
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
return { evidence, diagnostics: [] };
|
|
509
|
+
}
|
|
510
|
+
function normalizeCodeLine(line) {
|
|
511
|
+
return line
|
|
512
|
+
.trim()
|
|
513
|
+
.replace(/\s+/g, " ")
|
|
514
|
+
.replace(/["'`][^"'`]*["'`]/g, "<string>")
|
|
515
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, "<number>");
|
|
516
|
+
}
|
|
517
|
+
function looksLikeLiteralList(lines) {
|
|
518
|
+
return lines.every((line) => /^["'`<][^=({]*["'`>]?,?$/.test(line.trim()));
|
|
519
|
+
}
|
|
520
|
+
function firstMatchPerFile(matches) {
|
|
521
|
+
const selected = new Map();
|
|
522
|
+
for (const match of matches) {
|
|
523
|
+
if (!selected.has(match.file.path)) {
|
|
524
|
+
selected.set(match.file.path, match);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return [...selected.values()];
|
|
528
|
+
}
|
|
529
|
+
async function readFirstJsonReport(dir) {
|
|
530
|
+
const files = await readdir(dir, { recursive: true });
|
|
531
|
+
for (const file of files) {
|
|
532
|
+
if (typeof file !== "string" || !file.endsWith(".json")) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const raw = await readFile(path.join(dir, file), "utf8");
|
|
536
|
+
const parsed = JSON.parse(raw);
|
|
537
|
+
if (isObject(parsed)) {
|
|
538
|
+
return parsed;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return {};
|
|
542
|
+
}
|
|
543
|
+
function duplicateFiles(value, root) {
|
|
544
|
+
const result = [];
|
|
545
|
+
for (const key of ["firstFile", "secondFile"]) {
|
|
546
|
+
const file = value[key];
|
|
547
|
+
if (!isObject(file) || typeof file["name"] !== "string") {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const reference = {
|
|
551
|
+
path: normalizePath(path.relative(root, file["name"])),
|
|
552
|
+
};
|
|
553
|
+
if (typeof file["start"] === "number") {
|
|
554
|
+
reference.startLine = file["start"];
|
|
555
|
+
}
|
|
556
|
+
if (typeof file["end"] === "number") {
|
|
557
|
+
reference.endLine = file["end"];
|
|
558
|
+
}
|
|
559
|
+
result.push(reference);
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
function sarifToolName(run) {
|
|
564
|
+
const tool = run["tool"];
|
|
565
|
+
if (!isObject(tool)) {
|
|
566
|
+
return "SARIF tool";
|
|
567
|
+
}
|
|
568
|
+
const driver = tool["driver"];
|
|
569
|
+
if (!isObject(driver)) {
|
|
570
|
+
return "SARIF tool";
|
|
571
|
+
}
|
|
572
|
+
return typeof driver["name"] === "string" ? driver["name"] : "SARIF tool";
|
|
573
|
+
}
|
|
574
|
+
function sarifTitle(result) {
|
|
575
|
+
const message = result["message"];
|
|
576
|
+
if (isObject(message) && typeof message["text"] === "string") {
|
|
577
|
+
return message["text"].slice(0, 160);
|
|
578
|
+
}
|
|
579
|
+
if (typeof result["ruleId"] === "string") {
|
|
580
|
+
return result["ruleId"];
|
|
581
|
+
}
|
|
582
|
+
return "External analyzer finding";
|
|
583
|
+
}
|
|
584
|
+
function sarifFiles(result) {
|
|
585
|
+
const locations = Array.isArray(result["locations"]) ? result["locations"] : [];
|
|
586
|
+
const files = [];
|
|
587
|
+
for (const location of locations.slice(0, 5)) {
|
|
588
|
+
if (!isObject(location)) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const physical = location["physicalLocation"];
|
|
592
|
+
if (!isObject(physical)) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const artifact = physical["artifactLocation"];
|
|
596
|
+
if (!isObject(artifact) || typeof artifact["uri"] !== "string") {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const region = physical["region"];
|
|
600
|
+
const reference = {
|
|
601
|
+
path: normalizePath(decodeURIComponent(artifact["uri"])),
|
|
602
|
+
};
|
|
603
|
+
if (isObject(region) && typeof region["startLine"] === "number") {
|
|
604
|
+
reference.startLine = region["startLine"];
|
|
605
|
+
}
|
|
606
|
+
if (isObject(region) && typeof region["endLine"] === "number") {
|
|
607
|
+
reference.endLine = region["endLine"];
|
|
608
|
+
}
|
|
609
|
+
files.push(reference);
|
|
610
|
+
}
|
|
611
|
+
return files;
|
|
612
|
+
}
|
|
613
|
+
function sarifConfidence(result) {
|
|
614
|
+
const level = result["level"];
|
|
615
|
+
if (level === "error") {
|
|
616
|
+
return "high";
|
|
617
|
+
}
|
|
618
|
+
if (level === "warning") {
|
|
619
|
+
return "medium";
|
|
620
|
+
}
|
|
621
|
+
return "low";
|
|
622
|
+
}
|
|
623
|
+
function compactObject(value, omitKeys) {
|
|
624
|
+
const omitted = new Set(omitKeys);
|
|
625
|
+
const result = {};
|
|
626
|
+
for (const [key, item] of Object.entries(value)) {
|
|
627
|
+
if (!omitted.has(key)) {
|
|
628
|
+
result[key] = item;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
function unique(values) {
|
|
634
|
+
return [...new Set(values)];
|
|
635
|
+
}
|
|
636
|
+
function isObject(value) {
|
|
637
|
+
return typeof value === "object" && value !== null;
|
|
638
|
+
}
|
|
639
|
+
function buildLocalImportGraph(files) {
|
|
640
|
+
const sourcePaths = new Set(files.map((file) => file.path));
|
|
641
|
+
const nodes = new Map();
|
|
642
|
+
const edges = [];
|
|
643
|
+
for (const file of files) {
|
|
644
|
+
nodes.set(file.path, { imports: new Set(), importedBy: new Set() });
|
|
645
|
+
}
|
|
646
|
+
for (const file of files) {
|
|
647
|
+
const node = nodes.get(file.path);
|
|
648
|
+
if (!node) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const imports = collectImports(file)
|
|
652
|
+
.map((specifier) => resolveImportPath(file.path, specifier, sourcePaths))
|
|
653
|
+
.filter((value) => Boolean(value));
|
|
654
|
+
for (const imported of new Set(imports)) {
|
|
655
|
+
node.imports.add(imported);
|
|
656
|
+
const target = nodes.get(imported);
|
|
657
|
+
if (target) {
|
|
658
|
+
target.importedBy.add(file.path);
|
|
659
|
+
}
|
|
660
|
+
edges.push({ from: file.path, to: imported });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
edges.sort((a, b) => `${a.from}:${a.to}`.localeCompare(`${b.from}:${b.to}`));
|
|
664
|
+
return { nodes, edges };
|
|
665
|
+
}
|
|
666
|
+
function summarizeDirectories(nodes, edges) {
|
|
667
|
+
const summaries = new Map();
|
|
668
|
+
for (const node of nodes) {
|
|
669
|
+
const current = summaries.get(node.directory) ?? {
|
|
670
|
+
path: node.directory,
|
|
671
|
+
fileCount: 0,
|
|
672
|
+
internalEdges: 0,
|
|
673
|
+
incomingEdges: 0,
|
|
674
|
+
outgoingEdges: 0,
|
|
675
|
+
};
|
|
676
|
+
current.fileCount += 1;
|
|
677
|
+
summaries.set(node.directory, current);
|
|
678
|
+
}
|
|
679
|
+
for (const edge of edges) {
|
|
680
|
+
const fromDir = moduleDirectory(edge.from);
|
|
681
|
+
const toDir = moduleDirectory(edge.to);
|
|
682
|
+
const fromSummary = summaries.get(fromDir);
|
|
683
|
+
const toSummary = summaries.get(toDir);
|
|
684
|
+
if (fromDir === toDir) {
|
|
685
|
+
if (fromSummary) {
|
|
686
|
+
fromSummary.internalEdges += 1;
|
|
687
|
+
}
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (fromSummary) {
|
|
691
|
+
fromSummary.outgoingEdges += 1;
|
|
692
|
+
}
|
|
693
|
+
if (toSummary) {
|
|
694
|
+
toSummary.incomingEdges += 1;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return [...summaries.values()]
|
|
698
|
+
.sort((a, b) => ((b.fileCount + b.internalEdges + b.incomingEdges + b.outgoingEdges)
|
|
699
|
+
- (a.fileCount + a.internalEdges + a.incomingEdges + a.outgoingEdges)))
|
|
700
|
+
.slice(0, 40);
|
|
701
|
+
}
|
|
702
|
+
function moduleDirectory(filePath) {
|
|
703
|
+
const parts = filePath.split("/");
|
|
704
|
+
if (parts.length <= 1) {
|
|
705
|
+
return ".";
|
|
706
|
+
}
|
|
707
|
+
if (parts.includes("src")) {
|
|
708
|
+
const srcIndex = parts.indexOf("src");
|
|
709
|
+
return parts.slice(0, Math.min(parts.length - 1, srcIndex + 3)).join("/");
|
|
710
|
+
}
|
|
711
|
+
return parts.slice(0, Math.min(parts.length - 1, 2)).join("/");
|
|
712
|
+
}
|
|
713
|
+
function topLevel(filePath) {
|
|
714
|
+
return filePath.split("/")[0] ?? ".";
|
|
715
|
+
}
|
|
716
|
+
function collectImports(file) {
|
|
717
|
+
if (file.extension === ".py") {
|
|
718
|
+
return collectPythonImports(file);
|
|
719
|
+
}
|
|
720
|
+
const sourceFile = ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true, file.extension.includes("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
721
|
+
const imports = [];
|
|
722
|
+
visitNode(sourceFile, (node) => {
|
|
723
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
724
|
+
imports.push(node.moduleSpecifier.text);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
728
|
+
imports.push(node.moduleSpecifier.text);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (ts.isCallExpression(node) && node.arguments.length === 1) {
|
|
732
|
+
const argument = node.arguments[0];
|
|
733
|
+
if (!argument || !ts.isStringLiteral(argument)) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
737
|
+
imports.push(argument.text);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === "require") {
|
|
741
|
+
imports.push(argument.text);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
return imports;
|
|
746
|
+
}
|
|
747
|
+
function resolveImportPath(fromPath, specifier, sourcePaths) {
|
|
748
|
+
const base = specifier.startsWith(".")
|
|
749
|
+
? normalizePath(path.posix.normalize(path.posix.join(path.posix.dirname(fromPath), specifier)))
|
|
750
|
+
: specifier.startsWith("@/")
|
|
751
|
+
? aliasImportBase(fromPath, specifier)
|
|
752
|
+
: normalizePath(specifier.replace(/\./g, "/"));
|
|
753
|
+
const candidates = importPathCandidates(base);
|
|
754
|
+
return candidates.find((candidate) => sourcePaths.has(candidate));
|
|
755
|
+
}
|
|
756
|
+
function importPathCandidates(base) {
|
|
757
|
+
const extension = path.posix.extname(base);
|
|
758
|
+
const withoutExtension = extension ? base.slice(0, -extension.length) : base;
|
|
759
|
+
const emittedJsSourceCandidates = [".js", ".jsx", ".mjs", ".cjs"].includes(extension)
|
|
760
|
+
? [
|
|
761
|
+
`${withoutExtension}.ts`,
|
|
762
|
+
`${withoutExtension}.tsx`,
|
|
763
|
+
`${withoutExtension}.mts`,
|
|
764
|
+
`${withoutExtension}.cts`,
|
|
765
|
+
`${withoutExtension}.js`,
|
|
766
|
+
`${withoutExtension}.jsx`,
|
|
767
|
+
`${withoutExtension}.mjs`,
|
|
768
|
+
`${withoutExtension}.cjs`,
|
|
769
|
+
]
|
|
770
|
+
: [];
|
|
771
|
+
return unique([
|
|
772
|
+
base,
|
|
773
|
+
...emittedJsSourceCandidates,
|
|
774
|
+
`${base}.ts`,
|
|
775
|
+
`${base}.tsx`,
|
|
776
|
+
`${base}.mts`,
|
|
777
|
+
`${base}.cts`,
|
|
778
|
+
`${base}.js`,
|
|
779
|
+
`${base}.jsx`,
|
|
780
|
+
`${base}.mjs`,
|
|
781
|
+
`${base}.cjs`,
|
|
782
|
+
`${base}.py`,
|
|
783
|
+
`${base}/index.ts`,
|
|
784
|
+
`${base}/index.tsx`,
|
|
785
|
+
`${base}/index.mts`,
|
|
786
|
+
`${base}/index.cts`,
|
|
787
|
+
`${base}/index.js`,
|
|
788
|
+
`${base}/index.jsx`,
|
|
789
|
+
`${base}/index.mjs`,
|
|
790
|
+
`${base}/index.cjs`,
|
|
791
|
+
`${base}/__init__.py`,
|
|
792
|
+
]);
|
|
793
|
+
}
|
|
794
|
+
function aliasImportBase(fromPath, specifier) {
|
|
795
|
+
const srcMarker = "/src/";
|
|
796
|
+
const srcIndex = fromPath.indexOf(srcMarker);
|
|
797
|
+
if (srcIndex >= 0) {
|
|
798
|
+
return normalizePath(`${fromPath.slice(0, srcIndex)}${srcMarker}${specifier.slice(2)}`);
|
|
799
|
+
}
|
|
800
|
+
return normalizePath(`src/${specifier.slice(2)}`);
|
|
801
|
+
}
|
|
802
|
+
function collectPythonImports(file) {
|
|
803
|
+
const imports = [];
|
|
804
|
+
for (const line of file.lines) {
|
|
805
|
+
const fromMatch = line.match(/^\s*from\s+([.\w]+)\s+import\s+/);
|
|
806
|
+
if (fromMatch?.[1]) {
|
|
807
|
+
imports.push(fromMatch[1]);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const importMatch = line.match(/^\s*import\s+([.\w]+)/);
|
|
811
|
+
if (importMatch?.[1]) {
|
|
812
|
+
imports.push(importMatch[1]);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return imports;
|
|
816
|
+
}
|
|
817
|
+
function isTsLikeFile(file) {
|
|
818
|
+
return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"].includes(file.extension);
|
|
819
|
+
}
|
|
820
|
+
function visitNode(node, visitor) {
|
|
821
|
+
visitor(node);
|
|
822
|
+
node.forEachChild((child) => visitNode(child, visitor));
|
|
823
|
+
}
|
|
824
|
+
function isFunctionLikeWithBody(node) {
|
|
825
|
+
return (ts.isFunctionDeclaration(node)
|
|
826
|
+
|| ts.isMethodDeclaration(node)
|
|
827
|
+
|| ts.isFunctionExpression(node)
|
|
828
|
+
|| ts.isArrowFunction(node)) && Boolean(node.body);
|
|
829
|
+
}
|
|
830
|
+
function functionName(node) {
|
|
831
|
+
if ("name" in node && node.name && ts.isIdentifier(node.name)) {
|
|
832
|
+
return node.name.text;
|
|
833
|
+
}
|
|
834
|
+
const parent = node.parent;
|
|
835
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
836
|
+
return parent.name.text;
|
|
837
|
+
}
|
|
838
|
+
if (ts.isPropertyAssignment(parent) && ts.isIdentifier(parent.name)) {
|
|
839
|
+
return parent.name.text;
|
|
840
|
+
}
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
function stem(filePath) {
|
|
844
|
+
return filePath
|
|
845
|
+
.replace(/(^|\/)(__tests__|test|tests|spec)\//g, "$1")
|
|
846
|
+
.replace(/\.(test|spec)\.[cm]?[jt]sx?$/, "")
|
|
847
|
+
.replace(/(^|\/)test_([^/]+)\.py$/, "$1$2")
|
|
848
|
+
.replace(/_(test|tests)\.py$/, "")
|
|
849
|
+
.replace(/\.py$/, "")
|
|
850
|
+
.replace(/\.[cm]?[jt]sx?$/, "")
|
|
851
|
+
.replace(/\/index$/, "");
|
|
852
|
+
}
|
|
853
|
+
//# sourceMappingURL=evidence.js.map
|