syncstaff-mcp 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/lib/agent-state.js +119 -0
- package/dist/lib/blast.js +462 -0
- package/dist/lib/client-config.js +81 -0
- package/dist/lib/env-compat.js +66 -0
- package/dist/lib/globs.js +0 -0
- package/dist/lib/ids.js +24 -0
- package/dist/lib/index/aliases.js +244 -0
- package/dist/lib/index/call-sites.js +178 -0
- package/dist/lib/index/checker-resolver.js +257 -0
- package/dist/lib/index/context-card.js +140 -0
- package/dist/lib/index/coverage.js +218 -0
- package/dist/lib/index/delivery.js +66 -0
- package/dist/lib/index/discovery.js +90 -0
- package/dist/lib/index/embedding.js +110 -0
- package/dist/lib/index/file-index.js +222 -0
- package/dist/lib/index/fingerprint.js +0 -0
- package/dist/lib/index/git-history.js +136 -0
- package/dist/lib/index/graph.js +234 -0
- package/dist/lib/index/impact.js +174 -0
- package/dist/lib/index/incremental.js +332 -0
- package/dist/lib/index/lexical.js +462 -0
- package/dist/lib/index/order.js +43 -0
- package/dist/lib/index/pages.js +357 -0
- package/dist/lib/index/persistence.js +233 -0
- package/dist/lib/index/pipeline.js +527 -0
- package/dist/lib/index/registry.js +106 -0
- package/dist/lib/index/resolve.js +280 -0
- package/dist/lib/index/semantic.js +381 -0
- package/dist/lib/index/surfaces.js +27 -0
- package/dist/lib/index/symbols.js +426 -0
- package/dist/lib/index/transformers-embedder.js +73 -0
- package/dist/lib/index/typescript-parser.js +532 -0
- package/dist/lib/index/vector-cache.js +176 -0
- package/dist/lib/index/verification.js +58 -0
- package/dist/lib/mcp-compaction.js +241 -0
- package/dist/lib/model-roles.js +206 -0
- package/dist/lib/path-warnings.js +90 -0
- package/dist/lib/protocol.js +95 -0
- package/dist/lib/types.js +69 -0
- package/dist/lib/version.js +21 -0
- package/dist/lib/worktree.js +211 -0
- package/dist/mcp/approval.js +0 -0
- package/dist/mcp/cloud-connector.js +99 -0
- package/dist/mcp/daemon-client.js +156 -0
- package/dist/mcp/daemon-protocol.js +100 -0
- package/dist/mcp/escalation-waiter.js +183 -0
- package/dist/mcp/graph-ops.js +169 -0
- package/dist/mcp/index.js +1151 -0
- package/dist/mcp/login.js +169 -0
- package/dist/mcp/setup.js +90 -0
- package/package.json +42 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2
|
+
/**
|
|
3
|
+
* Bump on any change to extraction rules.
|
|
4
|
+
*
|
|
5
|
+
* This is part of the index fingerprint by design: an index built by a smarter
|
|
6
|
+
* parser is a different index over identical bytes, and forgetting to bump
|
|
7
|
+
* would let the two claim equality.
|
|
8
|
+
*/
|
|
9
|
+
const PARSER_VERSION = "4";
|
|
10
|
+
export function createTypeScriptParser(tsApi) {
|
|
11
|
+
return {
|
|
12
|
+
id: "keel-typescript",
|
|
13
|
+
version: PARSER_VERSION,
|
|
14
|
+
language: "typescript",
|
|
15
|
+
extensions: EXTENSIONS,
|
|
16
|
+
parse: (source, path) => parseSource(tsApi, source, path),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load the backend if `typescript` is installed, or return null.
|
|
21
|
+
*
|
|
22
|
+
* The published adapter must stay installable with `npx` by someone who has
|
|
23
|
+
* never built a native module, and `typescript` is large. Rather than force it
|
|
24
|
+
* on everyone, the caller registers a backend only when one is available —
|
|
25
|
+
* and the registry already models a missing backend as `unsupported`, which is
|
|
26
|
+
* a gap in coverage rather than a failure. So an adapter without typescript
|
|
27
|
+
* still coordinates; it simply reports that it understood no files, honestly,
|
|
28
|
+
* instead of pretending to a graph it does not have.
|
|
29
|
+
*/
|
|
30
|
+
export async function loadTypeScriptParser() {
|
|
31
|
+
try {
|
|
32
|
+
const loaded = (await import("typescript"));
|
|
33
|
+
const api = loaded.default ?? loaded;
|
|
34
|
+
if (typeof api.createSourceFile !== "function")
|
|
35
|
+
return null;
|
|
36
|
+
return createTypeScriptParser(api);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function parseSource(tsApi, source, path) {
|
|
43
|
+
const imports = [];
|
|
44
|
+
const exports = [];
|
|
45
|
+
const definitions = [];
|
|
46
|
+
const calls = [];
|
|
47
|
+
const references = [];
|
|
48
|
+
const diagnostics = [];
|
|
49
|
+
const file = tsApi.createSourceFile(path, source, tsApi.ScriptTarget.Latest,
|
|
50
|
+
/* setParentNodes */ true, scriptKind(tsApi, path));
|
|
51
|
+
const lineOf = (pos) => file.getLineAndCharacterOfPosition(pos).line + 1;
|
|
52
|
+
const columnOf = (pos) => file.getLineAndCharacterOfPosition(pos).character + 1;
|
|
53
|
+
// parseDiagnostics is internal but stable, and it is the only way to learn a
|
|
54
|
+
// syntactic parse recovered from errors. Guarded so a compiler upgrade that
|
|
55
|
+
// renames it degrades to "no diagnostics" rather than throwing.
|
|
56
|
+
const parseErrors = file
|
|
57
|
+
.parseDiagnostics ?? [];
|
|
58
|
+
for (const error of parseErrors.slice(0, 20)) {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
// The compiler's own code, never its message: TS messages routinely
|
|
61
|
+
// quote the offending source.
|
|
62
|
+
code: error.code === undefined ? "syntax" : `TS${error.code}`,
|
|
63
|
+
message: "syntax error recovered by the parser",
|
|
64
|
+
...(error.start === undefined ? {} : { line: lineOf(error.start), column: columnOf(error.start) }),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Names of the functions/classes we are currently inside, outermost first. */
|
|
68
|
+
const scope = [];
|
|
69
|
+
const enclosing = () => (scope.length === 0 ? undefined : scope[scope.length - 1]);
|
|
70
|
+
/** Class names only, so `this` can be attributed without a type checker. */
|
|
71
|
+
const classes = [];
|
|
72
|
+
const enclosingClass = () => classes.length === 0 ? undefined : classes[classes.length - 1];
|
|
73
|
+
const isExported = (node) => {
|
|
74
|
+
const modifiers = node.modifiers;
|
|
75
|
+
return (modifiers ?? []).some((m) => m.kind === tsApi.SyntaxKind.ExportKeyword);
|
|
76
|
+
};
|
|
77
|
+
const addDefinition = (name, kind, node, owner) => {
|
|
78
|
+
const exported = isExported(node);
|
|
79
|
+
definitions.push({
|
|
80
|
+
name,
|
|
81
|
+
kind,
|
|
82
|
+
exported,
|
|
83
|
+
...(owner ? { owner } : {}),
|
|
84
|
+
line: lineOf(node.getStart(file)),
|
|
85
|
+
});
|
|
86
|
+
if (exported) {
|
|
87
|
+
exports.push({ name, kind, confidence: "exact", line: lineOf(node.getStart(file)) });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const addReference = (name, kind, node, extra = {}) => {
|
|
91
|
+
references.push({
|
|
92
|
+
name,
|
|
93
|
+
kind,
|
|
94
|
+
...(extra.receiver ? { receiver: extra.receiver } : {}),
|
|
95
|
+
...(extra.ownerClass ? { ownerClass: extra.ownerClass } : {}),
|
|
96
|
+
...(enclosing() ? { enclosing: enclosing() } : {}),
|
|
97
|
+
line: lineOf(node.getStart(file)),
|
|
98
|
+
confidence: extra.confidence ?? "exact",
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const visit = (node) => {
|
|
102
|
+
// ---- imports -------------------------------------------------------
|
|
103
|
+
if (tsApi.isImportDeclaration(node)) {
|
|
104
|
+
const specifier = literalText(tsApi, node.moduleSpecifier);
|
|
105
|
+
if (specifier !== null) {
|
|
106
|
+
const names = [];
|
|
107
|
+
const aliases = {};
|
|
108
|
+
let confidence = "exact";
|
|
109
|
+
const clause = node.importClause;
|
|
110
|
+
// `import type { X }` and `import { type X }` are compile-time only.
|
|
111
|
+
let typeOnly = clause?.isTypeOnly === true;
|
|
112
|
+
if (clause?.name)
|
|
113
|
+
names.push(clause.name.text);
|
|
114
|
+
const bindings = clause?.namedBindings;
|
|
115
|
+
if (bindings && tsApi.isNamedImports(bindings)) {
|
|
116
|
+
// `import { a as b }` binds `b` locally while the target exports `a`.
|
|
117
|
+
// Recording both is what lets resolution follow the alias instead of
|
|
118
|
+
// failing to find `b` in the target and calling the edge unresolved.
|
|
119
|
+
let allTypeOnly = bindings.elements.length > 0;
|
|
120
|
+
for (const element of bindings.elements) {
|
|
121
|
+
names.push(element.name.text);
|
|
122
|
+
if (element.propertyName)
|
|
123
|
+
aliases[element.name.text] = element.propertyName.text;
|
|
124
|
+
if (!element.isTypeOnly)
|
|
125
|
+
allTypeOnly = false;
|
|
126
|
+
// The line a rename has to touch. Recorded under the LOCAL name,
|
|
127
|
+
// exactly like a call: resolution owns the alias mapping and
|
|
128
|
+
// reports the target's own name for the symbol, so writing the
|
|
129
|
+
// target's name here would instead produce an unresolvable
|
|
130
|
+
// reference to a binding this file does not have.
|
|
131
|
+
addReference(element.name.text, "import", element);
|
|
132
|
+
}
|
|
133
|
+
if (allTypeOnly)
|
|
134
|
+
typeOnly = true;
|
|
135
|
+
}
|
|
136
|
+
else if (bindings && tsApi.isNamespaceImport(bindings)) {
|
|
137
|
+
names.push(bindings.name.text);
|
|
138
|
+
// A namespace binding is one name standing for a whole module; a
|
|
139
|
+
// call through it is knowable but not certain.
|
|
140
|
+
confidence = "probable";
|
|
141
|
+
}
|
|
142
|
+
imports.push({
|
|
143
|
+
specifier,
|
|
144
|
+
names,
|
|
145
|
+
...(Object.keys(aliases).length > 0 ? { aliases } : {}),
|
|
146
|
+
...(typeOnly ? { typeOnly: true } : {}),
|
|
147
|
+
dynamic: false,
|
|
148
|
+
confidence,
|
|
149
|
+
line: lineOf(node.getStart(file)),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// ---- re-exports ----------------------------------------------------
|
|
154
|
+
if (tsApi.isExportDeclaration(node)) {
|
|
155
|
+
const specifier = node.moduleSpecifier ? literalText(tsApi, node.moduleSpecifier) : null;
|
|
156
|
+
if (specifier !== null) {
|
|
157
|
+
const names = [];
|
|
158
|
+
if (node.exportClause && tsApi.isNamedExports(node.exportClause)) {
|
|
159
|
+
for (const element of node.exportClause.elements) {
|
|
160
|
+
names.push(element.name.text);
|
|
161
|
+
addReference(element.name.text, "re-export", element);
|
|
162
|
+
exports.push({
|
|
163
|
+
name: element.name.text,
|
|
164
|
+
kind: "re-export",
|
|
165
|
+
from: specifier,
|
|
166
|
+
// The name is certain; what it ultimately refers to is not.
|
|
167
|
+
confidence: "probable",
|
|
168
|
+
line: lineOf(element.getStart(file)),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// `export * from "./x.js"` — the names cannot be known without
|
|
174
|
+
// reading the target, which this parser will not do. Recorded as a
|
|
175
|
+
// diagnostic so coverage reflects the gap instead of hiding it.
|
|
176
|
+
diagnostics.push({
|
|
177
|
+
code: "star-reexport",
|
|
178
|
+
message: "export * cannot be enumerated without cross-file resolution",
|
|
179
|
+
line: lineOf(node.getStart(file)),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
imports.push({
|
|
183
|
+
specifier,
|
|
184
|
+
names,
|
|
185
|
+
...(node.isTypeOnly ? { typeOnly: true } : {}),
|
|
186
|
+
dynamic: false,
|
|
187
|
+
confidence: "probable",
|
|
188
|
+
line: lineOf(node.getStart(file)),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else if (node.exportClause && tsApi.isNamedExports(node.exportClause)) {
|
|
192
|
+
// `export { a, b }` re-exporting local declarations.
|
|
193
|
+
for (const element of node.exportClause.elements) {
|
|
194
|
+
addReference(element.propertyName?.text ?? element.name.text, "re-export", element);
|
|
195
|
+
exports.push({
|
|
196
|
+
name: element.name.text,
|
|
197
|
+
kind: "binding",
|
|
198
|
+
confidence: "exact",
|
|
199
|
+
line: lineOf(element.getStart(file)),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (tsApi.isExportAssignment(node)) {
|
|
205
|
+
exports.push({
|
|
206
|
+
name: "default",
|
|
207
|
+
kind: node.isExportEquals ? "export=" : "default",
|
|
208
|
+
confidence: "exact",
|
|
209
|
+
line: lineOf(node.getStart(file)),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// ---- declarations ---------------------------------------------------
|
|
213
|
+
if (tsApi.isFunctionDeclaration(node) && node.name) {
|
|
214
|
+
addDefinition(node.name.text, "function", node);
|
|
215
|
+
}
|
|
216
|
+
else if (tsApi.isClassDeclaration(node) && node.name) {
|
|
217
|
+
addDefinition(node.name.text, "class", node);
|
|
218
|
+
}
|
|
219
|
+
else if (tsApi.isInterfaceDeclaration(node)) {
|
|
220
|
+
addDefinition(node.name.text, "interface", node);
|
|
221
|
+
}
|
|
222
|
+
else if (tsApi.isTypeAliasDeclaration(node)) {
|
|
223
|
+
addDefinition(node.name.text, "type", node);
|
|
224
|
+
}
|
|
225
|
+
else if (tsApi.isEnumDeclaration(node)) {
|
|
226
|
+
addDefinition(node.name.text, "enum", node);
|
|
227
|
+
}
|
|
228
|
+
else if (tsApi.isVariableStatement(node)) {
|
|
229
|
+
for (const declaration of node.declarationList.declarations) {
|
|
230
|
+
if (!tsApi.isIdentifier(declaration.name))
|
|
231
|
+
continue;
|
|
232
|
+
const initializer = declaration.initializer;
|
|
233
|
+
const kind = initializer && (tsApi.isArrowFunction(initializer) || tsApi.isFunctionExpression(initializer))
|
|
234
|
+
? "function"
|
|
235
|
+
: "const";
|
|
236
|
+
addDefinition(declaration.name.text, kind, node);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (tsApi.isMethodDeclaration(node) && tsApi.isIdentifier(node.name)) {
|
|
240
|
+
const owner = scope[scope.length - 1];
|
|
241
|
+
definitions.push({
|
|
242
|
+
name: node.name.text,
|
|
243
|
+
kind: "method",
|
|
244
|
+
exported: false,
|
|
245
|
+
...(owner ? { owner } : {}),
|
|
246
|
+
line: lineOf(node.getStart(file)),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// ---- calls -----------------------------------------------------------
|
|
250
|
+
if (tsApi.isCallExpression(node)) {
|
|
251
|
+
const callee = node.expression;
|
|
252
|
+
if (callee.kind === tsApi.SyntaxKind.ImportKeyword) {
|
|
253
|
+
const argument = node.arguments[0];
|
|
254
|
+
const specifier = argument ? literalText(tsApi, argument) : null;
|
|
255
|
+
imports.push({
|
|
256
|
+
// A dynamic import with a literal argument is still statically
|
|
257
|
+
// knowable; one built from a variable is not. Both are marked
|
|
258
|
+
// dynamic so graph.ts records rather than resolves them.
|
|
259
|
+
specifier: specifier ?? "<computed>",
|
|
260
|
+
names: [],
|
|
261
|
+
dynamic: true,
|
|
262
|
+
confidence: specifier === null ? "speculative" : "probable",
|
|
263
|
+
line: lineOf(node.getStart(file)),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else if (tsApi.isIdentifier(callee)) {
|
|
267
|
+
if (callee.text === "require") {
|
|
268
|
+
const argument = node.arguments[0];
|
|
269
|
+
const specifier = argument ? literalText(tsApi, argument) : null;
|
|
270
|
+
if (specifier !== null) {
|
|
271
|
+
imports.push({
|
|
272
|
+
specifier,
|
|
273
|
+
names: [],
|
|
274
|
+
dynamic: false,
|
|
275
|
+
confidence: "probable",
|
|
276
|
+
line: lineOf(node.getStart(file)),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
calls.push({
|
|
282
|
+
callee: callee.text,
|
|
283
|
+
...(enclosing() ? { enclosing: enclosing() } : {}),
|
|
284
|
+
line: lineOf(node.getStart(file)),
|
|
285
|
+
confidence: "exact",
|
|
286
|
+
});
|
|
287
|
+
addReference(callee.text, "call", node);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (tsApi.isPropertyAccessExpression(callee) && tsApi.isIdentifier(callee.name)) {
|
|
291
|
+
const receiver = tsApi.isIdentifier(callee.expression)
|
|
292
|
+
? callee.expression.text
|
|
293
|
+
: callee.expression.kind === tsApi.SyntaxKind.ThisKeyword
|
|
294
|
+
? "this"
|
|
295
|
+
: undefined;
|
|
296
|
+
// `this` needs no type inference: the enclosing class is in the AST.
|
|
297
|
+
const owner = receiver === "this" ? enclosingClass() : undefined;
|
|
298
|
+
calls.push({
|
|
299
|
+
callee: receiver ? `${receiver}.${callee.name.text}` : callee.name.text,
|
|
300
|
+
...(receiver ? { receiver } : {}),
|
|
301
|
+
...(owner ? { ownerClass: owner } : {}),
|
|
302
|
+
...(enclosing() ? { enclosing: enclosing() } : {}),
|
|
303
|
+
line: lineOf(node.getStart(file)),
|
|
304
|
+
// A this-call is exact once the class is known; any other receiver
|
|
305
|
+
// would need the type checker this backend deliberately avoids.
|
|
306
|
+
confidence: owner ? "exact" : "probable",
|
|
307
|
+
});
|
|
308
|
+
addReference(receiver ? `${receiver}.${callee.name.text}` : callee.name.text, "call", node, { ...(receiver ? { receiver } : {}), ...(owner ? { ownerClass: owner } : {}), confidence: owner ? "exact" : "probable" });
|
|
309
|
+
}
|
|
310
|
+
// Anything else — an IIFE, a call on a call — is not attributable to a
|
|
311
|
+
// name, and inventing one would be the whole failure this avoids.
|
|
312
|
+
}
|
|
313
|
+
// ---- construction ------------------------------------------------------
|
|
314
|
+
// `new Store()` is a NewExpression, not a CallExpression, so a backend
|
|
315
|
+
// looking only for calls never saw a single instantiation in this
|
|
316
|
+
// repository. Nothing about it is less of a use than an invocation.
|
|
317
|
+
if (tsApi.isNewExpression(node)) {
|
|
318
|
+
const constructed = node.expression;
|
|
319
|
+
if (tsApi.isIdentifier(constructed)) {
|
|
320
|
+
addReference(constructed.text, "new", node);
|
|
321
|
+
}
|
|
322
|
+
else if (tsApi.isPropertyAccessExpression(constructed) &&
|
|
323
|
+
tsApi.isIdentifier(constructed.name) &&
|
|
324
|
+
tsApi.isIdentifier(constructed.expression)) {
|
|
325
|
+
addReference(`${constructed.expression.text}.${constructed.name.text}`, "new", node, {
|
|
326
|
+
receiver: constructed.expression.text,
|
|
327
|
+
// The receiver could have been reassigned between the import and
|
|
328
|
+
// here, exactly as for a member call.
|
|
329
|
+
confidence: "probable",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// ---- type positions ----------------------------------------------------
|
|
334
|
+
// `x: Config` and `class A extends Base` are compile-time references, and
|
|
335
|
+
// changing the target's shape breaks this file's build. `import type` is
|
|
336
|
+
// already carried as typeOnly on the edge, so the distinction survives
|
|
337
|
+
// downstream; what did not survive was the site itself.
|
|
338
|
+
if (tsApi.isTypeReferenceNode(node)) {
|
|
339
|
+
const named = node.typeName;
|
|
340
|
+
if (tsApi.isIdentifier(named)) {
|
|
341
|
+
addReference(named.text, "type", node);
|
|
342
|
+
}
|
|
343
|
+
else if (tsApi.isQualifiedName(named) && tsApi.isIdentifier(named.left)) {
|
|
344
|
+
addReference(`${named.left.text}.${named.right.text}`, "type", node, {
|
|
345
|
+
receiver: named.left.text,
|
|
346
|
+
confidence: "probable",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (tsApi.isExpressionWithTypeArguments(node) && tsApi.isIdentifier(node.expression)) {
|
|
351
|
+
// The heritage clause of `extends`/`implements`.
|
|
352
|
+
addReference(node.expression.text, "type", node);
|
|
353
|
+
}
|
|
354
|
+
else if (tsApi.isTypeQueryNode(node) && tsApi.isIdentifier(node.exprName)) {
|
|
355
|
+
// `typeof CONFIG` reads the binding to talk about its type.
|
|
356
|
+
addReference(node.exprName.text, "type", node);
|
|
357
|
+
}
|
|
358
|
+
// ---- JSX ---------------------------------------------------------------
|
|
359
|
+
// <Editor /> invokes the component; React calls it. It is not a
|
|
360
|
+
// CallExpression, so a backend looking only for calls sees nothing, which
|
|
361
|
+
// left every component use in this repo's web app invisible. Lowercase
|
|
362
|
+
// names are intrinsic elements (<div>) and reference nothing.
|
|
363
|
+
if (tsApi.isJsxOpeningElement(node) || tsApi.isJsxSelfClosingElement(node)) {
|
|
364
|
+
const tag = node.tagName;
|
|
365
|
+
const name = tsApi.isIdentifier(tag)
|
|
366
|
+
? tag.text
|
|
367
|
+
: tsApi.isPropertyAccessExpression(tag) && tsApi.isIdentifier(tag.name)
|
|
368
|
+
? tag.name.text
|
|
369
|
+
: null;
|
|
370
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
371
|
+
const receiver = tsApi.isPropertyAccessExpression(tag) && tsApi.isIdentifier(tag.expression)
|
|
372
|
+
? tag.expression.text
|
|
373
|
+
: undefined;
|
|
374
|
+
calls.push({
|
|
375
|
+
callee: receiver ? `${receiver}.${name}` : name,
|
|
376
|
+
...(receiver ? { receiver } : {}),
|
|
377
|
+
jsx: true,
|
|
378
|
+
...(enclosing() ? { enclosing: enclosing() } : {}),
|
|
379
|
+
line: lineOf(node.getStart(file)),
|
|
380
|
+
confidence: "exact",
|
|
381
|
+
});
|
|
382
|
+
addReference(receiver ? `${receiver}.${name}` : name, "jsx", node, {
|
|
383
|
+
...(receiver ? { receiver } : {}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// ---- plain reads -------------------------------------------------------
|
|
388
|
+
// The branch that was missing entirely. `KEEL_RELEASE_VERSION` is read in
|
|
389
|
+
// four files and invoked in none, so every extractor above walks straight
|
|
390
|
+
// past it and the graph reports that nothing uses a constant three modules
|
|
391
|
+
// depend on. Everything already handled above is excluded here rather than
|
|
392
|
+
// recorded twice — see isPlainRead.
|
|
393
|
+
if (tsApi.isIdentifier(node) && isPlainRead(tsApi, node)) {
|
|
394
|
+
addReference(node.text, "read", node);
|
|
395
|
+
}
|
|
396
|
+
// ---- descend, tracking scope -----------------------------------------
|
|
397
|
+
const scopeName = scopeNameOf(tsApi, node);
|
|
398
|
+
if (scopeName)
|
|
399
|
+
scope.push(scopeName);
|
|
400
|
+
const className = tsApi.isClassDeclaration(node) || tsApi.isClassExpression(node)
|
|
401
|
+
? node.name?.text ?? "(anonymous class)"
|
|
402
|
+
: null;
|
|
403
|
+
if (className)
|
|
404
|
+
classes.push(className);
|
|
405
|
+
tsApi.forEachChild(node, visit);
|
|
406
|
+
if (className)
|
|
407
|
+
classes.pop();
|
|
408
|
+
if (scopeName)
|
|
409
|
+
scope.pop();
|
|
410
|
+
};
|
|
411
|
+
tsApi.forEachChild(file, visit);
|
|
412
|
+
// `partial`, not `failed`: declarations either side of a syntax error are
|
|
413
|
+
// real, and discarding them would lose most of a file over one bad line.
|
|
414
|
+
// `failed` is reserved for a parse that yielded nothing usable.
|
|
415
|
+
const recovered = imports.length + exports.length + definitions.length + calls.length > 0;
|
|
416
|
+
const status = parseErrors.length === 0 ? "parsed" : recovered ? "partial" : "failed";
|
|
417
|
+
return { status, imports, exports, definitions, calls, references, diagnostics };
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Whether an identifier is a *use* of some binding, rather than the place a
|
|
421
|
+
* binding is introduced or a member name that belongs to someone else's type.
|
|
422
|
+
*
|
|
423
|
+
* Written as an exclusion list because the inclusive version is unbounded and
|
|
424
|
+
* the exclusive one is checkable: every case below is a position where treating
|
|
425
|
+
* the identifier as a reference would produce a claim the parser cannot
|
|
426
|
+
* support. Two categories, and they fail differently:
|
|
427
|
+
*
|
|
428
|
+
* Declaration names (`function parse`, `const parse`, a parameter named
|
|
429
|
+
* `parse`) would make every file that happens to declare the name look like a
|
|
430
|
+
* file that uses it. That is `grep-v1` again, wearing an AST.
|
|
431
|
+
*
|
|
432
|
+
* Member names (`obj.parse`, `A.B` in a type, `{ parse: fn }`) name a
|
|
433
|
+
* property of a value whose type this backend does not infer. Resolving
|
|
434
|
+
* `obj.parse` against a local function called `parse` is a guess, and the
|
|
435
|
+
* member-call path already refuses that same guess deliberately.
|
|
436
|
+
*
|
|
437
|
+
* Everything the walk records under another kind — calls, `new`, type
|
|
438
|
+
* references, JSX tags, import and export specifiers — is excluded here too, so
|
|
439
|
+
* a site is reported exactly once with the most specific kind that fits.
|
|
440
|
+
*/
|
|
441
|
+
function isPlainRead(tsApi, id) {
|
|
442
|
+
const parent = id.parent;
|
|
443
|
+
if (!parent)
|
|
444
|
+
return false;
|
|
445
|
+
// The name side of any declaration: `function f`, `const f`, `class F`,
|
|
446
|
+
// `f(param)`, `{ key: value }`, `interface I`, `<T>`, `enum E { Member }`.
|
|
447
|
+
// Object shorthand is the one exception — in `{ VERSION }` the identifier is
|
|
448
|
+
// both the key and a genuine read of the binding, and shorthand is one of the
|
|
449
|
+
// ordinary ways a constant travels between modules.
|
|
450
|
+
const named = parent.name;
|
|
451
|
+
if (named === id)
|
|
452
|
+
return tsApi.isShorthandPropertyAssignment(parent);
|
|
453
|
+
// `const { a: b } = x` — `a` is a property of x, not a binding in scope.
|
|
454
|
+
if (tsApi.isBindingElement(parent) && parent.propertyName === id)
|
|
455
|
+
return false;
|
|
456
|
+
// `config.timeout` is two halves and only one of them is knowable. The
|
|
457
|
+
// member needs the receiver's type, which this backend does not infer — but
|
|
458
|
+
// the receiver itself is an ordinary binding, and it is frequently the
|
|
459
|
+
// imported constant somebody is actually asking about. Dropping both halves
|
|
460
|
+
// would lose the reference the whole phase exists to find.
|
|
461
|
+
if (tsApi.isPropertyAccessExpression(parent))
|
|
462
|
+
return parent.name !== id;
|
|
463
|
+
if (tsApi.isQualifiedName(parent))
|
|
464
|
+
return parent.right !== id;
|
|
465
|
+
switch (parent.kind) {
|
|
466
|
+
// Recorded above with a more specific kind.
|
|
467
|
+
case tsApi.SyntaxKind.ImportSpecifier:
|
|
468
|
+
case tsApi.SyntaxKind.ExportSpecifier:
|
|
469
|
+
case tsApi.SyntaxKind.ImportClause:
|
|
470
|
+
case tsApi.SyntaxKind.NamespaceImport:
|
|
471
|
+
case tsApi.SyntaxKind.NamespaceExport:
|
|
472
|
+
case tsApi.SyntaxKind.TypeReference:
|
|
473
|
+
case tsApi.SyntaxKind.ExpressionWithTypeArguments:
|
|
474
|
+
case tsApi.SyntaxKind.TypeQuery:
|
|
475
|
+
case tsApi.SyntaxKind.JsxOpeningElement:
|
|
476
|
+
case tsApi.SyntaxKind.JsxClosingElement:
|
|
477
|
+
case tsApi.SyntaxKind.JsxSelfClosingElement:
|
|
478
|
+
return false;
|
|
479
|
+
// Not references to anything: labels, and `import.meta`.
|
|
480
|
+
case tsApi.SyntaxKind.LabeledStatement:
|
|
481
|
+
case tsApi.SyntaxKind.BreakStatement:
|
|
482
|
+
case tsApi.SyntaxKind.ContinueStatement:
|
|
483
|
+
case tsApi.SyntaxKind.MetaProperty:
|
|
484
|
+
return false;
|
|
485
|
+
default:
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
// The callee of `f()` and the constructor of `new F()` are already recorded
|
|
489
|
+
// as "call" and "new". Arguments to either are ordinary reads and fall
|
|
490
|
+
// through, which is the point of testing the position rather than the parent.
|
|
491
|
+
if (tsApi.isCallExpression(parent) && parent.expression === id)
|
|
492
|
+
return false;
|
|
493
|
+
if (tsApi.isNewExpression(parent) && parent.expression === id)
|
|
494
|
+
return false;
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
/** The text of a string literal, or null for anything computed. */
|
|
498
|
+
function literalText(tsApi, node) {
|
|
499
|
+
if (tsApi.isStringLiteral(node))
|
|
500
|
+
return node.text;
|
|
501
|
+
if (tsApi.isNoSubstitutionTemplateLiteral(node))
|
|
502
|
+
return node.text;
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
/** The name a node contributes to the enclosing-scope stack, if any. */
|
|
506
|
+
function scopeNameOf(tsApi, node) {
|
|
507
|
+
if (tsApi.isFunctionDeclaration(node) && node.name)
|
|
508
|
+
return node.name.text;
|
|
509
|
+
if (tsApi.isClassDeclaration(node) && node.name)
|
|
510
|
+
return node.name.text;
|
|
511
|
+
if (tsApi.isMethodDeclaration(node) && tsApi.isIdentifier(node.name))
|
|
512
|
+
return node.name.text;
|
|
513
|
+
if (tsApi.isConstructorDeclaration(node))
|
|
514
|
+
return "constructor";
|
|
515
|
+
if ((tsApi.isArrowFunction(node) || tsApi.isFunctionExpression(node)) &&
|
|
516
|
+
node.parent &&
|
|
517
|
+
tsApi.isVariableDeclaration(node.parent) &&
|
|
518
|
+
tsApi.isIdentifier(node.parent.name)) {
|
|
519
|
+
return node.parent.name.text;
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
function scriptKind(tsApi, path) {
|
|
524
|
+
const lower = path.toLowerCase();
|
|
525
|
+
if (lower.endsWith(".tsx"))
|
|
526
|
+
return tsApi.ScriptKind.TSX;
|
|
527
|
+
if (lower.endsWith(".jsx"))
|
|
528
|
+
return tsApi.ScriptKind.JSX;
|
|
529
|
+
if (lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs"))
|
|
530
|
+
return tsApi.ScriptKind.JS;
|
|
531
|
+
return tsApi.ScriptKind.TS;
|
|
532
|
+
}
|