sift-light 1.0.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 +0 -0
- package/CONTRIBUTING.md +19 -0
- package/LICENSE +661 -0
- package/README.md +177 -0
- package/README.zh-CN.md +177 -0
- package/SECURITY.md +25 -0
- package/package.json +142 -0
- package/plugins/sift-light/.claude-plugin/plugin.json +10 -0
- package/plugins/sift-light/.codex-plugin/plugin.json +20 -0
- package/plugins/sift-light/.mcp.json +24 -0
- package/plugins/sift-light/LICENSE +661 -0
- package/plugins/sift-light/concept-worker.mjs +567 -0
- package/plugins/sift-light/hooks/THIRD_PARTY_NOTICES.txt +70 -0
- package/plugins/sift-light/hooks/hooks.json +16 -0
- package/plugins/sift-light/hooks/search-policy.mjs +4113 -0
- package/plugins/sift-light/hooks/tree-sitter-bash.wasm +0 -0
- package/plugins/sift-light/hooks/tree-sitter-powershell.wasm +0 -0
- package/plugins/sift-light/hooks/tree-sitter.wasm +0 -0
- package/plugins/sift-light/kimi.plugin.json +36 -0
- package/plugins/sift-light/mcp-server.mjs +1 -0
- package/plugins/sift-light/omp-extension.mjs +20914 -0
- package/plugins/sift-light/package.json +11 -0
- package/plugins/sift-light/syntax-worker.mjs +280 -0
- package/plugins/sift-light/syntax-worker.toml +2 -0
- package/src/analysis-evidence.ts +43 -0
- package/src/analysis-limits.ts +27 -0
- package/src/analysis-store.ts +559 -0
- package/src/analysis-term-pages.ts +73 -0
- package/src/analysis-types.ts +162 -0
- package/src/audit-search.ts +391 -0
- package/src/capped-lines.ts +112 -0
- package/src/concept-embedding-cache.ts +186 -0
- package/src/concept-inference.ts +218 -0
- package/src/concept-model.ts +96 -0
- package/src/concept-search.ts +577 -0
- package/src/concept-setup.ts +75 -0
- package/src/concept-source-generation.ts +268 -0
- package/src/concept-worker.mjs +567 -0
- package/src/concept-worker.ts +210 -0
- package/src/config-reader.ts +283 -0
- package/src/config.ts +37 -0
- package/src/context-budget.ts +26 -0
- package/src/discovery-errors.ts +2 -0
- package/src/errors.ts +95 -0
- package/src/evidence-candidates.ts +344 -0
- package/src/evidence-ranking.ts +48 -0
- package/src/evidence-service.ts +1047 -0
- package/src/evidence-validation.ts +466 -0
- package/src/evidence-validity.ts +33 -0
- package/src/excerpt.ts +53 -0
- package/src/file-discovery.ts +150 -0
- package/src/file-metadata-filter.ts +46 -0
- package/src/format.ts +407 -0
- package/src/git-diff.ts +195 -0
- package/src/git-process.ts +189 -0
- package/src/git-repository.ts +367 -0
- package/src/git-source.ts +385 -0
- package/src/historical-paths.ts +205 -0
- package/src/hybrid-search.ts +340 -0
- package/src/import-model.ts +611 -0
- package/src/import-navigation.ts +100 -0
- package/src/import-resolution.ts +240 -0
- package/src/index.ts +97 -0
- package/src/inspect-batch.ts +265 -0
- package/src/inspect.ts +271 -0
- package/src/language-capabilities.ts +157 -0
- package/src/language-capability-definitions.ts +204 -0
- package/src/literal-search.ts +25 -0
- package/src/mcp-cli.ts +21 -0
- package/src/mcp-model-output.ts +120 -0
- package/src/mcp-output.ts +9 -0
- package/src/mcp-semantic-judge.ts +39 -0
- package/src/mcp-server.mjs +14064 -0
- package/src/mcp-server.ts +157 -0
- package/src/mcp-stdio.ts +114 -0
- package/src/mcp.ts +631 -0
- package/src/model-error.ts +120 -0
- package/src/multi-term-search.ts +122 -0
- package/src/omp-index.ts +277 -0
- package/src/omp-marketplace-entry.ts +7 -0
- package/src/operation-lifecycle.ts +413 -0
- package/src/operation-output.ts +100 -0
- package/src/owned-parallel.ts +22 -0
- package/src/owned-process.ts +102 -0
- package/src/owned-task-queue.ts +38 -0
- package/src/package-version.ts +3 -0
- package/src/path-policy.ts +203 -0
- package/src/pi-search-policy.ts +33 -0
- package/src/prompt-guidelines.ts +54 -0
- package/src/python-outline.ts +231 -0
- package/src/record-value.ts +3 -0
- package/src/redaction.ts +95 -0
- package/src/request-contract-catalog.ts +215 -0
- package/src/request-contract-error.ts +96 -0
- package/src/request-contract.ts +481 -0
- package/src/request.ts +132 -0
- package/src/result-statistics.ts +178 -0
- package/src/rg.ts +574 -0
- package/src/ripgrep-diagnostics.ts +122 -0
- package/src/ripgrep-executable.ts +43 -0
- package/src/runtime.ts +53 -0
- package/src/scan-revisions.ts +148 -0
- package/src/script-runtime.ts +11 -0
- package/src/search-policy-commands.ts +170 -0
- package/src/search-policy-hook.ts +61 -0
- package/src/search-policy-recovery.ts +388 -0
- package/src/search-policy-shell.ts +280 -0
- package/src/search-policy.ts +102 -0
- package/src/search-retention.ts +77 -0
- package/src/semantic-judge-batches.ts +135 -0
- package/src/semantic-judge.ts +519 -0
- package/src/service.ts +945 -0
- package/src/session-summary.ts +100 -0
- package/src/snapshot-store.ts +203 -0
- package/src/source-access.ts +288 -0
- package/src/source-continuations.ts +168 -0
- package/src/source-document.ts +277 -0
- package/src/source-inspection.ts +731 -0
- package/src/source-pages.ts +112 -0
- package/src/source.ts +286 -0
- package/src/structural-search.ts +125 -0
- package/src/structure.ts +259 -0
- package/src/summary-previews.ts +96 -0
- package/src/summary.ts +79 -0
- package/src/syntax-facts.ts +464 -0
- package/src/syntax-fields.ts +161 -0
- package/src/syntax-search.ts +361 -0
- package/src/syntax-tree.ts +30 -0
- package/src/syntax-types.ts +72 -0
- package/src/syntax-worker.mjs +280 -0
- package/src/syntax-worker.toml +2 -0
- package/src/syntax-worker.ts +143 -0
- package/src/syntax.ts +259 -0
- package/src/test-navigation-facts.ts +261 -0
- package/src/test-navigation.ts +543 -0
- package/src/tool-schema.ts +321 -0
- package/src/tui/dashboard.ts +107 -0
- package/src/tui/layout.ts +291 -0
- package/src/tui/palette.ts +73 -0
- package/src/tui/presentation.ts +240 -0
- package/src/tui/renderers.ts +106 -0
- package/src/types.ts +368 -0
- package/src/validation-output.ts +148 -0
- package/src/validation-types.ts +48 -0
- package/src/workspace-files.ts +142 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { MAX_ANALYSIS_RESULTS, MAX_ANALYSIS_STORAGE_BYTES } from "./analysis-limits.js";
|
|
3
|
+
import { SiftLightError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
NavigationContext,
|
|
6
|
+
moduleRange,
|
|
7
|
+
navigationError,
|
|
8
|
+
nodeLine,
|
|
9
|
+
nodeText,
|
|
10
|
+
type ImportBinding,
|
|
11
|
+
type ModuleFacts,
|
|
12
|
+
type NavigationHost,
|
|
13
|
+
type NavigationInput,
|
|
14
|
+
type NavigationItem,
|
|
15
|
+
type NavigationResult,
|
|
16
|
+
} from "./import-model.js";
|
|
17
|
+
import {
|
|
18
|
+
importStatementExcerpt,
|
|
19
|
+
resolveStaticModule,
|
|
20
|
+
traceImport,
|
|
21
|
+
tracePaths,
|
|
22
|
+
type ImportTrace,
|
|
23
|
+
} from "./import-resolution.js";
|
|
24
|
+
import { TestBindings, collectTestCases, type TestCaseFact } from "./test-navigation-facts.js";
|
|
25
|
+
import type { SyntaxSymbol } from "./syntax-types.js";
|
|
26
|
+
import { syntaxField } from "./syntax-tree.js";
|
|
27
|
+
|
|
28
|
+
type Association = "direct" | "indirect" | "weak";
|
|
29
|
+
interface Relation {
|
|
30
|
+
association: Association;
|
|
31
|
+
binding?: ImportBinding;
|
|
32
|
+
trace?: ImportTrace;
|
|
33
|
+
reason: string;
|
|
34
|
+
targetBinding: boolean;
|
|
35
|
+
paths: string[];
|
|
36
|
+
}
|
|
37
|
+
interface UseEvidence {
|
|
38
|
+
path: string;
|
|
39
|
+
line: number;
|
|
40
|
+
range: { start: number; end: number };
|
|
41
|
+
binding: string;
|
|
42
|
+
excerpt: string;
|
|
43
|
+
excerptTruncated: boolean;
|
|
44
|
+
}
|
|
45
|
+
const SOURCE_EXTENSION = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
|
|
46
|
+
const TEST_FILENAME =
|
|
47
|
+
/(?:^|\/)(?:__tests__|tests?)(?:\/|$)|(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]s|[jt]sx)$/i;
|
|
48
|
+
export const TEST_DISCOVERY_PATTERN = String.raw`\b(?:describe|it|test)\s*\(|\b(?:from\s*|require\s*\(\s*)["'](?:node:test|bun:test|vitest|@jest/globals)["']`;
|
|
49
|
+
|
|
50
|
+
export function isLikelyTestPath(path: string): boolean {
|
|
51
|
+
return TEST_FILENAME.test(path);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TestNavigationOptions {
|
|
55
|
+
entryPaths?: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function basenameStem(path: string): string {
|
|
59
|
+
return posix
|
|
60
|
+
.basename(path)
|
|
61
|
+
.replace(SOURCE_EXTENSION, "")
|
|
62
|
+
.replace(/\.(?:test|spec)$/i, "");
|
|
63
|
+
}
|
|
64
|
+
interface TestTargetSymbol extends SyntaxSymbol {
|
|
65
|
+
directBinding?: { start: number; end: number };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function targetSymbol(facts: ModuleFacts, input: NavigationInput): TestTargetSymbol | undefined {
|
|
69
|
+
if (input.line === undefined && input.symbol === undefined) return undefined;
|
|
70
|
+
const symbols = facts.syntax.symbols
|
|
71
|
+
.filter((symbol) => {
|
|
72
|
+
if (!symbol.hasBody) return false;
|
|
73
|
+
if (input.symbol !== undefined && symbol.name !== input.symbol) return false;
|
|
74
|
+
const start = facts.document.lineAt(facts.document.toByteOffset(symbol.start));
|
|
75
|
+
const end = facts.document.lineAt(
|
|
76
|
+
Math.max(
|
|
77
|
+
facts.document.toByteOffset(symbol.start),
|
|
78
|
+
facts.document.toByteOffset(symbol.end) - 1,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
return input.line === undefined || (start <= input.line && input.line <= end);
|
|
82
|
+
})
|
|
83
|
+
.toSorted((a, b) => a.end - a.start - (b.end - b.start));
|
|
84
|
+
if (symbols.length === 0)
|
|
85
|
+
throw new SiftLightError(
|
|
86
|
+
"Test navigation target does not identify an implemented function/method",
|
|
87
|
+
);
|
|
88
|
+
if (input.line === undefined && symbols.length > 1)
|
|
89
|
+
throw new SiftLightError("Test navigation symbol is ambiguous; include its source line");
|
|
90
|
+
const symbol = symbols[0];
|
|
91
|
+
if (!symbol) throw new Error("Missing selected test target symbol");
|
|
92
|
+
let carrier = symbol.node;
|
|
93
|
+
let parent = facts.syntax.nodes[carrier]?.parent;
|
|
94
|
+
while (
|
|
95
|
+
parent !== null &&
|
|
96
|
+
parent !== undefined &&
|
|
97
|
+
[
|
|
98
|
+
"parenthesized_expression",
|
|
99
|
+
"as_expression",
|
|
100
|
+
"satisfies_expression",
|
|
101
|
+
"type_assertion",
|
|
102
|
+
"non_null_expression",
|
|
103
|
+
].includes(facts.syntax.nodes[parent]?.kind ?? "")
|
|
104
|
+
) {
|
|
105
|
+
carrier = parent;
|
|
106
|
+
parent = facts.syntax.nodes[carrier]?.parent;
|
|
107
|
+
}
|
|
108
|
+
const directBinding =
|
|
109
|
+
parent !== null &&
|
|
110
|
+
parent !== undefined &&
|
|
111
|
+
facts.syntax.nodes[parent]?.kind === "variable_declarator" &&
|
|
112
|
+
syntaxField(facts.syntax, parent, "value") === carrier
|
|
113
|
+
? moduleRange(facts, parent)
|
|
114
|
+
: undefined;
|
|
115
|
+
return { ...symbol, ...(directBinding ? { directBinding } : {}) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function traceTargetsSymbol(
|
|
119
|
+
context: NavigationContext,
|
|
120
|
+
trace: ImportTrace,
|
|
121
|
+
target: ModuleFacts,
|
|
122
|
+
symbol: TestTargetSymbol | undefined,
|
|
123
|
+
): boolean {
|
|
124
|
+
if (!symbol) return trace.status === "resolved" || trace.status === "module";
|
|
125
|
+
const destination = trace.destination;
|
|
126
|
+
if (
|
|
127
|
+
trace.status !== "resolved" ||
|
|
128
|
+
!destination ||
|
|
129
|
+
context.normalizePath(destination.source.path) !== context.normalizePath(target.document.path)
|
|
130
|
+
)
|
|
131
|
+
return false;
|
|
132
|
+
const start = target.document.toByteOffset(symbol.start);
|
|
133
|
+
const end = target.document.toByteOffset(symbol.end);
|
|
134
|
+
return (
|
|
135
|
+
(destination.range.start === start && destination.range.end === end) ||
|
|
136
|
+
(destination.kind === "variable_declarator" &&
|
|
137
|
+
symbol.directBinding !== undefined &&
|
|
138
|
+
destination.range.start === symbol.directBinding.start &&
|
|
139
|
+
destination.range.end === symbol.directBinding.end)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function relations(
|
|
144
|
+
context: NavigationContext,
|
|
145
|
+
test: ModuleFacts,
|
|
146
|
+
target: ModuleFacts,
|
|
147
|
+
symbol: TestTargetSymbol | undefined,
|
|
148
|
+
): Promise<Relation[]> {
|
|
149
|
+
const output: Relation[] = [];
|
|
150
|
+
for (const binding of test.imports) {
|
|
151
|
+
if (!binding.source?.startsWith(".")) continue;
|
|
152
|
+
// oxlint-disable-next-line no-await-in-loop -- resolution and tracing share one source/read budget.
|
|
153
|
+
const resolved = await resolveStaticModule(context, test.document.path, binding.source);
|
|
154
|
+
const targetPath = context.normalizePath(target.document.path);
|
|
155
|
+
const direct = resolved.path === targetPath;
|
|
156
|
+
// oxlint-disable-next-line no-await-in-loop -- dependencies are parsed serially within the bounded request.
|
|
157
|
+
const trace = await traceImport(context, test, binding);
|
|
158
|
+
if (trace.reason === "structural-read-budget-exhausted") {
|
|
159
|
+
context.reasons.add(trace.reason);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
const paths = tracePaths(trace).map((path) => context.normalizePath(path));
|
|
163
|
+
const indirect = !direct && paths.includes(targetPath) && trace.status !== "unresolved";
|
|
164
|
+
if (!direct && !indirect) {
|
|
165
|
+
if (
|
|
166
|
+
trace.reason &&
|
|
167
|
+
[
|
|
168
|
+
"hop-budget-exhausted",
|
|
169
|
+
"file-budget-exhausted",
|
|
170
|
+
"byte-budget-exhausted",
|
|
171
|
+
"source-changed",
|
|
172
|
+
"syntax-timeout",
|
|
173
|
+
"syntax-limit",
|
|
174
|
+
].includes(trace.reason)
|
|
175
|
+
)
|
|
176
|
+
context.reasons.add(`${test.document.path}: ${trace.reason}`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
output.push({
|
|
180
|
+
association: direct ? "direct" : "indirect",
|
|
181
|
+
binding,
|
|
182
|
+
trace,
|
|
183
|
+
reason: direct ? "static-import-target-module" : "static-import-re-export-path-to-target",
|
|
184
|
+
targetBinding: !binding.typeOnly && traceTargetsSymbol(context, trace, target, symbol),
|
|
185
|
+
paths: [...new Set([context.normalizePath(test.document.path), targetPath, ...paths])],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (output.length) return output;
|
|
189
|
+
const stem = basenameStem(target.document.path);
|
|
190
|
+
const nameSimilar = basenameStem(test.document.path) === stem;
|
|
191
|
+
const textSimilar = symbol
|
|
192
|
+
? test.document.text.includes(symbol.name)
|
|
193
|
+
: stem.length > 0 && test.document.text.includes(stem);
|
|
194
|
+
if (nameSimilar || textSimilar)
|
|
195
|
+
output.push({
|
|
196
|
+
association: "weak",
|
|
197
|
+
reason: nameSimilar ? "filename-similarity-only" : "source-text-similarity-only",
|
|
198
|
+
targetBinding: false,
|
|
199
|
+
paths: [
|
|
200
|
+
context.normalizePath(test.document.path),
|
|
201
|
+
context.normalizePath(target.document.path),
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
return output;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function usesInCases(
|
|
208
|
+
facts: ModuleFacts,
|
|
209
|
+
bindings: TestBindings,
|
|
210
|
+
cases: TestCaseFact[],
|
|
211
|
+
related: Relation[],
|
|
212
|
+
): Map<number, UseEvidence[]> {
|
|
213
|
+
const names = new Set(
|
|
214
|
+
related
|
|
215
|
+
.filter((relation) => relation.targetBinding && relation.binding?.local)
|
|
216
|
+
.map((relation) => relation.binding?.local),
|
|
217
|
+
);
|
|
218
|
+
const uses = new Map<number, UseEvidence[]>();
|
|
219
|
+
const callbacks = new Map(
|
|
220
|
+
cases.flatMap((test) => (test.callback === undefined ? [] : [[test.callback, test] as const])),
|
|
221
|
+
);
|
|
222
|
+
const owners: (TestCaseFact | undefined)[] = [];
|
|
223
|
+
for (let id = 0; id < facts.syntax.nodes.length; id++) {
|
|
224
|
+
const node = facts.syntax.nodes[id];
|
|
225
|
+
if (!node) continue;
|
|
226
|
+
const test = callbacks.get(id) ?? (node.parent === null ? undefined : owners[node.parent]);
|
|
227
|
+
owners[id] = test;
|
|
228
|
+
const callback = test?.callback === undefined ? undefined : facts.syntax.nodes[test.callback];
|
|
229
|
+
if (!test || !callback) continue;
|
|
230
|
+
const name = nodeText(facts, id);
|
|
231
|
+
if (!name || !names.has(name) || !bindings.isReference(id) || bindings.shadowed(name, id))
|
|
232
|
+
continue;
|
|
233
|
+
let excerptNode = id;
|
|
234
|
+
let parent = node.parent;
|
|
235
|
+
while (parent !== null) {
|
|
236
|
+
const value = facts.syntax.nodes[parent];
|
|
237
|
+
if (!value || value.start < callback.start || value.end > callback.end) break;
|
|
238
|
+
excerptNode = parent;
|
|
239
|
+
if (
|
|
240
|
+
value.kind === "expression_statement" ||
|
|
241
|
+
value.kind === "return_statement" ||
|
|
242
|
+
value.kind === "variable_declarator"
|
|
243
|
+
)
|
|
244
|
+
break;
|
|
245
|
+
parent = value.parent;
|
|
246
|
+
}
|
|
247
|
+
const text = nodeText(facts, excerptNode) ?? name;
|
|
248
|
+
const evidence = uses.get(test.node) ?? [];
|
|
249
|
+
evidence.push({
|
|
250
|
+
path: facts.document.path,
|
|
251
|
+
line: nodeLine(facts, id),
|
|
252
|
+
range: moduleRange(facts, id),
|
|
253
|
+
binding: name,
|
|
254
|
+
excerpt: text.length > 500 ? `${text.slice(0, 500)}…` : text,
|
|
255
|
+
excerptTruncated: text.length > 500,
|
|
256
|
+
});
|
|
257
|
+
uses.set(test.node, evidence);
|
|
258
|
+
}
|
|
259
|
+
return uses;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function relationDetails(facts: ModuleFacts, relation: Relation): Record<string, unknown> {
|
|
263
|
+
return {
|
|
264
|
+
association: relation.association,
|
|
265
|
+
reason: relation.reason,
|
|
266
|
+
...(relation.binding
|
|
267
|
+
? {
|
|
268
|
+
imported: relation.binding.imported,
|
|
269
|
+
local: relation.binding.local,
|
|
270
|
+
typeOnly: relation.binding.typeOnly,
|
|
271
|
+
importLine: nodeLine(facts, relation.binding.statement),
|
|
272
|
+
importRange: moduleRange(facts, relation.binding.statement),
|
|
273
|
+
importExcerpt: importStatementExcerpt(facts, relation.binding.statement),
|
|
274
|
+
}
|
|
275
|
+
: {}),
|
|
276
|
+
...(relation.trace
|
|
277
|
+
? {
|
|
278
|
+
chain: relation.trace.chain,
|
|
279
|
+
importStatus: relation.trace.status,
|
|
280
|
+
importReason: relation.trace.reason,
|
|
281
|
+
}
|
|
282
|
+
: {}),
|
|
283
|
+
targetBindingProven: relation.targetBinding,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Candidates and static uses only: no tests run, no coverage or passing assertion claims. */
|
|
288
|
+
export async function findRelatedTests(
|
|
289
|
+
host: NavigationHost,
|
|
290
|
+
input: NavigationInput,
|
|
291
|
+
options: TestNavigationOptions = {},
|
|
292
|
+
): Promise<NavigationResult> {
|
|
293
|
+
const started = performance.now();
|
|
294
|
+
if (input.line !== undefined && (!Number.isSafeInteger(input.line) || input.line < 1))
|
|
295
|
+
throw new SiftLightError("Test target line must be a positive integer");
|
|
296
|
+
if (input.symbol !== undefined && input.symbol.trim().length === 0)
|
|
297
|
+
throw new SiftLightError("Test target symbol must be nonempty");
|
|
298
|
+
const context = new NavigationContext(host);
|
|
299
|
+
let target;
|
|
300
|
+
try {
|
|
301
|
+
target = await context.module(input.path, true);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const reason = navigationError(error);
|
|
304
|
+
if (!reason) throw error;
|
|
305
|
+
context.reasons.add(reason);
|
|
306
|
+
return context.result([]);
|
|
307
|
+
}
|
|
308
|
+
let symbol: TestTargetSymbol | undefined;
|
|
309
|
+
try {
|
|
310
|
+
symbol = targetSymbol(target, input);
|
|
311
|
+
} finally {
|
|
312
|
+
context.release(target);
|
|
313
|
+
}
|
|
314
|
+
const allFiles = [...(await context.files())];
|
|
315
|
+
const selectedEntries = options.entryPaths
|
|
316
|
+
? new Set(options.entryPaths.map((path) => context.normalizePath(path)))
|
|
317
|
+
: undefined;
|
|
318
|
+
const targetPath = context.normalizePath(target.document.path);
|
|
319
|
+
const eligibleEntries = allFiles.filter(
|
|
320
|
+
(path) => path !== targetPath && SOURCE_EXTENSION.test(path),
|
|
321
|
+
);
|
|
322
|
+
const files = allFiles
|
|
323
|
+
.filter(
|
|
324
|
+
(path) =>
|
|
325
|
+
path !== targetPath &&
|
|
326
|
+
SOURCE_EXTENSION.test(path) &&
|
|
327
|
+
(!selectedEntries || selectedEntries.has(path)),
|
|
328
|
+
)
|
|
329
|
+
.toSorted(
|
|
330
|
+
(a, b) => Number(TEST_FILENAME.test(b)) - Number(TEST_FILENAME.test(a)) || a.localeCompare(b),
|
|
331
|
+
);
|
|
332
|
+
const items: NavigationItem[] = [];
|
|
333
|
+
const affected: string[][] = [];
|
|
334
|
+
let serializedBytes = 0;
|
|
335
|
+
const append = (item: NavigationItem, dependencies: string[]): boolean => {
|
|
336
|
+
const bytes = Buffer.byteLength(JSON.stringify(item));
|
|
337
|
+
if (
|
|
338
|
+
serializedBytes + bytes > MAX_ANALYSIS_STORAGE_BYTES ||
|
|
339
|
+
items.length >= MAX_ANALYSIS_RESULTS
|
|
340
|
+
) {
|
|
341
|
+
context.reasons.add(
|
|
342
|
+
serializedBytes + bytes > MAX_ANALYSIS_STORAGE_BYTES
|
|
343
|
+
? "serialized-result-budget-exhausted"
|
|
344
|
+
: "result-item-budget-exhausted",
|
|
345
|
+
);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
serializedBytes += bytes;
|
|
349
|
+
items.push(item);
|
|
350
|
+
affected.push(dependencies);
|
|
351
|
+
return true;
|
|
352
|
+
};
|
|
353
|
+
for (const path of files) {
|
|
354
|
+
context.checkAbort();
|
|
355
|
+
let facts;
|
|
356
|
+
try {
|
|
357
|
+
// oxlint-disable-next-line no-await-in-loop -- the configured file and 32 MiB limits apply before each file.
|
|
358
|
+
facts = await context.module(path, true);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const reason = navigationError(error);
|
|
361
|
+
if (!reason) throw error;
|
|
362
|
+
context.reasons.add(`${path}: ${reason}`);
|
|
363
|
+
if (
|
|
364
|
+
reason === "file-budget-exhausted" ||
|
|
365
|
+
reason === "byte-budget-exhausted" ||
|
|
366
|
+
reason === "structural-read-budget-exhausted"
|
|
367
|
+
)
|
|
368
|
+
break;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const bindings = new TestBindings(facts);
|
|
373
|
+
const cases = collectTestCases(facts, bindings);
|
|
374
|
+
const filename = TEST_FILENAME.test(path);
|
|
375
|
+
const frameworkImport = facts.imports.some((binding) =>
|
|
376
|
+
["node:test", "bun:test", "vitest", "@jest/globals"].includes(binding.source ?? ""),
|
|
377
|
+
);
|
|
378
|
+
if (!filename && !frameworkImport && cases.length === 0) continue;
|
|
379
|
+
// oxlint-disable-next-line no-await-in-loop -- relation traversal consumes the same request context.
|
|
380
|
+
const related = await relations(context, facts, target, symbol);
|
|
381
|
+
if (context.reasons.has("structural-read-budget-exhausted")) break;
|
|
382
|
+
if (!related.length) continue;
|
|
383
|
+
const association: Association = related.some((relation) => relation.association === "direct")
|
|
384
|
+
? "direct"
|
|
385
|
+
: related.some((relation) => relation.association === "indirect")
|
|
386
|
+
? "indirect"
|
|
387
|
+
: "weak";
|
|
388
|
+
const dependencies = [...new Set(related.flatMap((relation) => relation.paths))];
|
|
389
|
+
const usesByCase = usesInCases(facts, bindings, cases, related);
|
|
390
|
+
const relationIndices: number[] = [];
|
|
391
|
+
for (const relation of related) {
|
|
392
|
+
const node = relation.binding?.statement ?? 0;
|
|
393
|
+
const item: NavigationItem = {
|
|
394
|
+
path,
|
|
395
|
+
line: nodeLine(facts, node),
|
|
396
|
+
label: `${relation.association} related test module: ${relation.reason}`,
|
|
397
|
+
source: facts.document.reference,
|
|
398
|
+
range: moduleRange(facts, node),
|
|
399
|
+
...(relation.binding
|
|
400
|
+
? { excerpt: importStatementExcerpt(facts, relation.binding.statement) }
|
|
401
|
+
: {}),
|
|
402
|
+
details: {
|
|
403
|
+
kind: "test-relation",
|
|
404
|
+
target: target.document.reference,
|
|
405
|
+
...relationDetails(facts, relation),
|
|
406
|
+
execution: "not-run",
|
|
407
|
+
assertionCoverage: "not-evaluated",
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
if (!append(item, dependencies)) break;
|
|
411
|
+
relationIndices.push(items.length);
|
|
412
|
+
}
|
|
413
|
+
if (
|
|
414
|
+
context.reasons.has("result-item-budget-exhausted") ||
|
|
415
|
+
context.reasons.has("serialized-result-budget-exhausted")
|
|
416
|
+
)
|
|
417
|
+
break;
|
|
418
|
+
const selections: (TestCaseFact | undefined)[] = cases.length ? cases : [undefined];
|
|
419
|
+
for (const test of selections) {
|
|
420
|
+
if (items.length >= MAX_ANALYSIS_RESULTS) {
|
|
421
|
+
context.reasons.add("result-item-budget-exhausted");
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
const uses = test ? (usesByCase.get(test.node) ?? []) : [];
|
|
425
|
+
const notes = [
|
|
426
|
+
...(test?.notes ?? ["no-statically-readable-test-case"]),
|
|
427
|
+
...(uses.length === 0 ? ["no-target-binding-use-in-case"] : []),
|
|
428
|
+
];
|
|
429
|
+
const node =
|
|
430
|
+
test?.node ?? related.find((relation) => relation.binding)?.binding?.statement ?? 0;
|
|
431
|
+
const range = moduleRange(facts, node);
|
|
432
|
+
const testName = test?.name;
|
|
433
|
+
const label = `${association} related test candidate: ${testName ?? (test ? "<dynamic or unavailable name>" : path)}`;
|
|
434
|
+
const caseId = JSON.stringify([path, range.start]);
|
|
435
|
+
const item: NavigationItem = {
|
|
436
|
+
path,
|
|
437
|
+
line: nodeLine(facts, node),
|
|
438
|
+
label,
|
|
439
|
+
source: facts.document.reference,
|
|
440
|
+
range,
|
|
441
|
+
...(uses[0] ? { excerpt: uses[0].excerpt } : {}),
|
|
442
|
+
details: {
|
|
443
|
+
kind: "test-case",
|
|
444
|
+
caseId,
|
|
445
|
+
association,
|
|
446
|
+
status: test?.status ?? "syntax-candidate",
|
|
447
|
+
target: target.document.reference,
|
|
448
|
+
...(symbol
|
|
449
|
+
? { targetSymbol: { name: symbol.name, range: moduleRange(target, symbol.node) } }
|
|
450
|
+
: {}),
|
|
451
|
+
...(test
|
|
452
|
+
? {
|
|
453
|
+
test: {
|
|
454
|
+
...(testName !== undefined ? { name: testName } : {}),
|
|
455
|
+
framework: test.framework,
|
|
456
|
+
kind: test.testKind,
|
|
457
|
+
modifiers: test.modifiers,
|
|
458
|
+
range,
|
|
459
|
+
},
|
|
460
|
+
}
|
|
461
|
+
: {}),
|
|
462
|
+
relationItems: {
|
|
463
|
+
first: relationIndices[0],
|
|
464
|
+
last: relationIndices.at(-1),
|
|
465
|
+
count: relationIndices.length,
|
|
466
|
+
},
|
|
467
|
+
useCount: uses.length,
|
|
468
|
+
notes,
|
|
469
|
+
assertionCoverage: "not-evaluated",
|
|
470
|
+
execution: "not-run",
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
if (!append(item, dependencies)) break;
|
|
474
|
+
const caseIndex = items.length;
|
|
475
|
+
for (const use of uses) {
|
|
476
|
+
const evidence: NavigationItem = {
|
|
477
|
+
path,
|
|
478
|
+
line: use.line,
|
|
479
|
+
label: `Static binding use in test candidate: ${testName ?? "<dynamic or unavailable name>"}`,
|
|
480
|
+
source: facts.document.reference,
|
|
481
|
+
range: use.range,
|
|
482
|
+
excerpt: use.excerpt,
|
|
483
|
+
details: {
|
|
484
|
+
kind: "test-use",
|
|
485
|
+
caseId,
|
|
486
|
+
caseIndex,
|
|
487
|
+
association,
|
|
488
|
+
target: target.document.reference,
|
|
489
|
+
binding: use.binding,
|
|
490
|
+
excerptTruncated: use.excerptTruncated,
|
|
491
|
+
execution: "not-run",
|
|
492
|
+
assertionCoverage: "not-evaluated",
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
if (!append(evidence, dependencies)) break;
|
|
496
|
+
}
|
|
497
|
+
if (
|
|
498
|
+
context.reasons.has("result-item-budget-exhausted") ||
|
|
499
|
+
context.reasons.has("serialized-result-budget-exhausted")
|
|
500
|
+
)
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
if (
|
|
504
|
+
context.reasons.has("result-item-budget-exhausted") ||
|
|
505
|
+
context.reasons.has("serialized-result-budget-exhausted")
|
|
506
|
+
)
|
|
507
|
+
break;
|
|
508
|
+
} finally {
|
|
509
|
+
context.release(facts);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const invalid = await context.verify();
|
|
513
|
+
for (let index = 0; index < items.length; index++) {
|
|
514
|
+
const item = items[index];
|
|
515
|
+
const reason = affected[index]
|
|
516
|
+
?.map((path) => invalid.get(path))
|
|
517
|
+
.find((value) => value !== undefined);
|
|
518
|
+
if (!item || reason === undefined) continue;
|
|
519
|
+
item.label = `Related test candidate invalidated: ${reason}`;
|
|
520
|
+
item.details = {
|
|
521
|
+
...item.details,
|
|
522
|
+
status: "invalidated",
|
|
523
|
+
reason,
|
|
524
|
+
association: "unresolved",
|
|
525
|
+
uses: [],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
...context.result(items),
|
|
530
|
+
counts: {
|
|
531
|
+
candidateFiles: new Set(items.map((item) => item.path)).size,
|
|
532
|
+
testCases: items.filter((item) => item.details.kind === "test-case").length,
|
|
533
|
+
useSites: items.filter((item) => item.details.kind === "test-use").length,
|
|
534
|
+
moduleRelations: items.filter((item) => item.details.kind === "test-relation").length,
|
|
535
|
+
},
|
|
536
|
+
stats: {
|
|
537
|
+
filesParsed: context.modules.size,
|
|
538
|
+
filesSkipped: Math.max(0, eligibleEntries.length - files.length),
|
|
539
|
+
parseMs: Math.round(performance.now() - started),
|
|
540
|
+
budgetExhausted: [...context.reasons].some((reason) => reason.includes("budget-exhausted")),
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|