diffninja 0.1.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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/calltree.d.ts +47 -0
- package/dist/calltree.js +296 -0
- package/dist/cli.d.ts +57 -0
- package/dist/cli.js +340 -0
- package/dist/diff.d.ts +7 -0
- package/dist/diff.js +114 -0
- package/dist/extract.d.ts +26 -0
- package/dist/extract.js +152 -0
- package/dist/git.d.ts +40 -0
- package/dist/git.js +288 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/infer.d.ts +21 -0
- package/dist/infer.js +189 -0
- package/dist/languages/bash.d.ts +2 -0
- package/dist/languages/bash.js +208 -0
- package/dist/languages/c.d.ts +2 -0
- package/dist/languages/c.js +218 -0
- package/dist/languages/call-syntax.d.ts +125 -0
- package/dist/languages/call-syntax.js +997 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +321 -0
- package/dist/languages/csharp.d.ts +2 -0
- package/dist/languages/csharp.js +324 -0
- package/dist/languages/elixir.d.ts +2 -0
- package/dist/languages/elixir.js +331 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +299 -0
- package/dist/languages/grammars.d.ts +50 -0
- package/dist/languages/grammars.js +351 -0
- package/dist/languages/haskell.d.ts +2 -0
- package/dist/languages/haskell.js +250 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +351 -0
- package/dist/languages/javascript.d.ts +4 -0
- package/dist/languages/javascript.js +648 -0
- package/dist/languages/kotlin.d.ts +2 -0
- package/dist/languages/kotlin.js +368 -0
- package/dist/languages/lua.d.ts +2 -0
- package/dist/languages/lua.js +212 -0
- package/dist/languages/ocaml.d.ts +2 -0
- package/dist/languages/ocaml.js +291 -0
- package/dist/languages/perl.d.ts +2 -0
- package/dist/languages/perl.js +418 -0
- package/dist/languages/php.d.ts +2 -0
- package/dist/languages/php.js +397 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +376 -0
- package/dist/languages/registry.d.ts +7 -0
- package/dist/languages/registry.js +69 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +391 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +261 -0
- package/dist/languages/scala.d.ts +2 -0
- package/dist/languages/scala.js +307 -0
- package/dist/languages/solidity.d.ts +2 -0
- package/dist/languages/solidity.js +240 -0
- package/dist/languages/swift.d.ts +2 -0
- package/dist/languages/swift.js +268 -0
- package/dist/languages/types.d.ts +36 -0
- package/dist/languages/types.js +74 -0
- package/dist/languages/typescript-contracts.d.ts +57 -0
- package/dist/languages/typescript-contracts.js +528 -0
- package/dist/languages/typescript-dispatch.d.ts +68 -0
- package/dist/languages/typescript-dispatch.js +710 -0
- package/dist/languages/typescript.d.ts +4 -0
- package/dist/languages/typescript.js +722 -0
- package/dist/languages/zig.d.ts +2 -0
- package/dist/languages/zig.js +243 -0
- package/dist/loc.d.ts +17 -0
- package/dist/loc.js +34 -0
- package/dist/reach.d.ts +17 -0
- package/dist/reach.js +65 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +83 -0
- package/dist/review/brand.d.ts +8 -0
- package/dist/review/brand.js +25 -0
- package/dist/review/call-context.d.ts +27 -0
- package/dist/review/call-context.js +446 -0
- package/dist/review/call-flow-html.d.ts +32 -0
- package/dist/review/call-flow-html.js +1870 -0
- package/dist/review/call-flow-nav.d.ts +151 -0
- package/dist/review/call-flow-nav.js +317 -0
- package/dist/review/call-flow.d.ts +47 -0
- package/dist/review/call-flow.js +229 -0
- package/dist/review/change-facts.d.ts +69 -0
- package/dist/review/change-facts.js +729 -0
- package/dist/review/cli.d.ts +2 -0
- package/dist/review/cli.js +50 -0
- package/dist/review/connected-analysis.d.ts +100 -0
- package/dist/review/connected-analysis.js +163 -0
- package/dist/review/connected-html.d.ts +17 -0
- package/dist/review/connected-html.js +2853 -0
- package/dist/review/connected.d.ts +23 -0
- package/dist/review/connected.js +141 -0
- package/dist/review/escape-html.d.ts +2 -0
- package/dist/review/escape-html.js +9 -0
- package/dist/review/evidence-html.d.ts +21 -0
- package/dist/review/evidence-html.js +521 -0
- package/dist/review/evidence-syntax.d.ts +132 -0
- package/dist/review/evidence-syntax.js +478 -0
- package/dist/review/evidence-types.d.ts +62 -0
- package/dist/review/evidence-types.js +1 -0
- package/dist/review/evidence.d.ts +31 -0
- package/dist/review/evidence.js +1603 -0
- package/dist/review/file-role.d.ts +9 -0
- package/dist/review/file-role.js +29 -0
- package/dist/review/github.d.ts +204 -0
- package/dist/review/github.js +1245 -0
- package/dist/review/history.d.ts +101 -0
- package/dist/review/history.js +412 -0
- package/dist/review/html.d.ts +34 -0
- package/dist/review/html.js +1104 -0
- package/dist/review/input.d.ts +10 -0
- package/dist/review/input.js +113 -0
- package/dist/review/intent.d.ts +4 -0
- package/dist/review/intent.js +75 -0
- package/dist/review/mcp-cli.d.ts +2 -0
- package/dist/review/mcp-cli.js +25 -0
- package/dist/review/mcp.d.ts +12 -0
- package/dist/review/mcp.js +414 -0
- package/dist/review/module-resolution.d.ts +2 -0
- package/dist/review/module-resolution.js +86 -0
- package/dist/review/palette.d.ts +7 -0
- package/dist/review/palette.js +104 -0
- package/dist/review/pipeline.d.ts +77 -0
- package/dist/review/pipeline.js +227 -0
- package/dist/review/pr-input.d.ts +19 -0
- package/dist/review/pr-input.js +130 -0
- package/dist/review/questions.d.ts +201 -0
- package/dist/review/questions.js +174 -0
- package/dist/review/reference-check.d.ts +7 -0
- package/dist/review/reference-check.js +733 -0
- package/dist/review/report-pages.d.ts +109 -0
- package/dist/review/report-pages.js +328 -0
- package/dist/review/service.d.ts +23 -0
- package/dist/review/service.js +198 -0
- package/dist/review/setup.d.ts +112 -0
- package/dist/review/setup.js +549 -0
- package/dist/review/source.d.ts +26 -0
- package/dist/review/source.js +276 -0
- package/dist/review/toml.d.ts +38 -0
- package/dist/review/toml.js +565 -0
- package/dist/review/types.d.ts +179 -0
- package/dist/review/types.js +1 -0
- package/dist/run.d.ts +49 -0
- package/dist/run.js +311 -0
- package/dist/types.d.ts +366 -0
- package/dist/types.js +83 -0
- package/package.json +88 -0
- package/scripts/ensure-native-grammar.mjs +188 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { sameNode } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Line and original spelling of each reference entry. The entry objects live
|
|
4
|
+
* inside `review.references`, so the extractor's cache spreads (which copy the
|
|
5
|
+
* `review` object but keep its arrays and entries) preserve the lookup.
|
|
6
|
+
*/
|
|
7
|
+
const SITE_INFO = new WeakMap();
|
|
8
|
+
/** Declaration nodes whose `name` field names the type instead of using it. */
|
|
9
|
+
const NAME_FIELDS = {
|
|
10
|
+
interface_declaration: true,
|
|
11
|
+
type_alias_declaration: true,
|
|
12
|
+
class_declaration: true,
|
|
13
|
+
abstract_class_declaration: true,
|
|
14
|
+
class: true,
|
|
15
|
+
type_parameter: true,
|
|
16
|
+
mapped_type_clause: true,
|
|
17
|
+
};
|
|
18
|
+
/* ------------------------------------------------------------------ text */
|
|
19
|
+
function stringValue(literal) {
|
|
20
|
+
const fragment = literal.namedChildren.find((c) => c.type === "string_fragment");
|
|
21
|
+
return fragment ? fragment.text : literal.text.replace(/^['"]|['"]$/g, "");
|
|
22
|
+
}
|
|
23
|
+
/* --------------------------------------------------------------- imports */
|
|
24
|
+
/** Module binding of an `import` clause: default names, namespaces, aliases. */
|
|
25
|
+
function readImportClause(clause, module, into) {
|
|
26
|
+
for (const child of clause.namedChildren) {
|
|
27
|
+
// A default import binds the module's default under a local name; the
|
|
28
|
+
// original name is not written here, so only the module is recorded.
|
|
29
|
+
if (child.type === "identifier") {
|
|
30
|
+
into.set(child.text, { module });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (child.type === "namespace_import") {
|
|
34
|
+
const local = child.namedChildren.find((c) => c.type === "identifier");
|
|
35
|
+
if (local)
|
|
36
|
+
into.set(local.text, { module });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (child.type !== "named_imports")
|
|
40
|
+
continue;
|
|
41
|
+
for (const specifier of child.namedChildren) {
|
|
42
|
+
if (specifier.type !== "import_specifier")
|
|
43
|
+
continue;
|
|
44
|
+
const original = specifier.childForFieldName("name");
|
|
45
|
+
const alias = specifier.childForFieldName("alias");
|
|
46
|
+
const local = alias ?? original;
|
|
47
|
+
if (!local)
|
|
48
|
+
continue;
|
|
49
|
+
into.set(local.text, original && original.text !== local.text
|
|
50
|
+
? { module, imported: original.text }
|
|
51
|
+
: { module });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Every named import of the file, keyed by the name the file binds. */
|
|
56
|
+
function fileImports(root) {
|
|
57
|
+
const imports = new Map();
|
|
58
|
+
for (const statement of root.namedChildren) {
|
|
59
|
+
if (statement.type !== "import_statement")
|
|
60
|
+
continue;
|
|
61
|
+
const clause = statement.namedChildren.find((c) => c.type === "import_clause");
|
|
62
|
+
if (!clause)
|
|
63
|
+
continue; // `import "./side-effect"` brings no binding
|
|
64
|
+
const source = statement.childForFieldName("source");
|
|
65
|
+
if (!source)
|
|
66
|
+
continue;
|
|
67
|
+
const module = stringValue(source);
|
|
68
|
+
if (module === "")
|
|
69
|
+
continue;
|
|
70
|
+
readImportClause(clause, module, imports);
|
|
71
|
+
}
|
|
72
|
+
return imports;
|
|
73
|
+
}
|
|
74
|
+
/* ------------------------------------------------------------ references */
|
|
75
|
+
/** Type parameter names declared by a node, which shadow outer types. */
|
|
76
|
+
function declaredTypeParameters(node) {
|
|
77
|
+
const params = node.childForFieldName("type_parameters") ??
|
|
78
|
+
node.namedChildren.find((c) => c.type === "type_parameters");
|
|
79
|
+
if (!params)
|
|
80
|
+
return [];
|
|
81
|
+
const names = [];
|
|
82
|
+
for (const parameter of params.namedChildren) {
|
|
83
|
+
if (parameter.type !== "type_parameter")
|
|
84
|
+
continue;
|
|
85
|
+
const name = parameter.childForFieldName("name");
|
|
86
|
+
if (name)
|
|
87
|
+
names.push(name.text);
|
|
88
|
+
}
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
/** `infer U` names of a conditional type, in scope for its whole subtree. */
|
|
92
|
+
function inferNames(node) {
|
|
93
|
+
const names = [];
|
|
94
|
+
const walk = (current) => {
|
|
95
|
+
if (current.type === "infer_type" || current.type === "infer_type_parameter") {
|
|
96
|
+
const name = current.namedChildren.find((c) => c.type === "type_identifier");
|
|
97
|
+
if (name)
|
|
98
|
+
names.push(name.text);
|
|
99
|
+
}
|
|
100
|
+
for (const child of current.namedChildren)
|
|
101
|
+
walk(child);
|
|
102
|
+
};
|
|
103
|
+
walk(node);
|
|
104
|
+
return names;
|
|
105
|
+
}
|
|
106
|
+
/** Mapped-type keys (`{ [K in ...]: Foo[K] }`) of an object type. */
|
|
107
|
+
function mappedKeyNames(node) {
|
|
108
|
+
const names = [];
|
|
109
|
+
const walk = (current) => {
|
|
110
|
+
if (current.type === "mapped_type_clause") {
|
|
111
|
+
const name = current.childForFieldName("name");
|
|
112
|
+
if (name)
|
|
113
|
+
names.push(name.text);
|
|
114
|
+
}
|
|
115
|
+
for (const child of current.namedChildren)
|
|
116
|
+
walk(child);
|
|
117
|
+
};
|
|
118
|
+
walk(node);
|
|
119
|
+
return names;
|
|
120
|
+
}
|
|
121
|
+
/** True when a `type_identifier` declares a name rather than references one. */
|
|
122
|
+
function namesDeclaration(node) {
|
|
123
|
+
const parent = node.parent;
|
|
124
|
+
if (!parent)
|
|
125
|
+
return false;
|
|
126
|
+
return Object.hasOwn(NAME_FIELDS, parent.type) && parent.childForFieldName("name")?.id === node.id;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The leftmost module segment and whole spelling of a qualified type name:
|
|
130
|
+
* `NS.Ext` -> `NS`, `A.B.C` -> `A`. Null for a plain `type_identifier`.
|
|
131
|
+
*/
|
|
132
|
+
function qualifiedTypeRoot(node) {
|
|
133
|
+
const parent = node.parent;
|
|
134
|
+
if (!parent || parent.type !== "nested_type_identifier")
|
|
135
|
+
return null;
|
|
136
|
+
if (!sameNode(parent.childForFieldName("name"), node))
|
|
137
|
+
return null;
|
|
138
|
+
let module = parent.childForFieldName("module");
|
|
139
|
+
while (module &&
|
|
140
|
+
(module.type === "nested_type_identifier" || module.type === "nested_identifier")) {
|
|
141
|
+
module =
|
|
142
|
+
module.childForFieldName("module") ??
|
|
143
|
+
module.childForFieldName("object") ??
|
|
144
|
+
module.namedChildren[0] ??
|
|
145
|
+
null;
|
|
146
|
+
}
|
|
147
|
+
return module ? { root: module, written: parent.text } : null;
|
|
148
|
+
}
|
|
149
|
+
/** Value name named by `typeof X` / `typeof a.b` in a type position. */
|
|
150
|
+
function typeQueryName(node) {
|
|
151
|
+
const operand = node.namedChildren[0];
|
|
152
|
+
if (!operand)
|
|
153
|
+
return null;
|
|
154
|
+
if (operand.type === "identifier")
|
|
155
|
+
return operand.text;
|
|
156
|
+
if (operand.type === "member_expression") {
|
|
157
|
+
const property = operand.childForFieldName("property");
|
|
158
|
+
return property?.type === "property_identifier" ? property.text : null;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/** `import("mod").Name` — a reference whose module is written in the type. */
|
|
163
|
+
function importTypeReference(node) {
|
|
164
|
+
if (node.type !== "member_expression")
|
|
165
|
+
return null;
|
|
166
|
+
const object = node.childForFieldName("object");
|
|
167
|
+
if (!object || object.type !== "call_expression")
|
|
168
|
+
return null;
|
|
169
|
+
const head = object.childForFieldName("function");
|
|
170
|
+
if (!head || head.type !== "import")
|
|
171
|
+
return null;
|
|
172
|
+
const args = object.childForFieldName("arguments");
|
|
173
|
+
const literal = args
|
|
174
|
+
? args.namedChildren.find((c) => c.type === "string")
|
|
175
|
+
: undefined;
|
|
176
|
+
const property = node.childForFieldName("property");
|
|
177
|
+
if (!literal || property?.type !== "property_identifier")
|
|
178
|
+
return null;
|
|
179
|
+
const module = stringValue(literal);
|
|
180
|
+
if (module === "")
|
|
181
|
+
return null;
|
|
182
|
+
return { written: node.text, name: property.text, module };
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Every type name written in the tree, with import provenance and position.
|
|
186
|
+
* A generic's own parameters, an `infer` variable, and a mapped-type key are
|
|
187
|
+
* shadowed, not recorded, so `function f<T>(x: T)` never references a `T`
|
|
188
|
+
* declared elsewhere.
|
|
189
|
+
*/
|
|
190
|
+
function collectSites(root, imports) {
|
|
191
|
+
const sites = [];
|
|
192
|
+
const emit = (node, written, name, explicitModule) => {
|
|
193
|
+
const binding = explicitModule !== undefined
|
|
194
|
+
? { module: explicitModule }
|
|
195
|
+
: imports.get(name);
|
|
196
|
+
const site = {
|
|
197
|
+
name,
|
|
198
|
+
written,
|
|
199
|
+
offset: node.startIndex,
|
|
200
|
+
line: node.startPosition.row + 1,
|
|
201
|
+
};
|
|
202
|
+
if (binding) {
|
|
203
|
+
site.module = binding.module;
|
|
204
|
+
if (binding.imported !== undefined)
|
|
205
|
+
site.imported = binding.imported;
|
|
206
|
+
}
|
|
207
|
+
sites.push(site);
|
|
208
|
+
};
|
|
209
|
+
const walk = (node, shadows) => {
|
|
210
|
+
const added = [];
|
|
211
|
+
const shadow = (names) => {
|
|
212
|
+
for (const name of names) {
|
|
213
|
+
if (shadows.has(name))
|
|
214
|
+
continue;
|
|
215
|
+
shadows.add(name);
|
|
216
|
+
added.push(name);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
shadow(declaredTypeParameters(node));
|
|
220
|
+
if (node.type === "conditional_type")
|
|
221
|
+
shadow(inferNames(node));
|
|
222
|
+
if (node.type === "object_type")
|
|
223
|
+
shadow(mappedKeyNames(node));
|
|
224
|
+
if (node.type === "type_identifier") {
|
|
225
|
+
if (!namesDeclaration(node) && !shadows.has(node.text)) {
|
|
226
|
+
const qualified = qualifiedTypeRoot(node);
|
|
227
|
+
if (qualified) {
|
|
228
|
+
const namespace = imports.get(qualified.root.text);
|
|
229
|
+
emit(node, qualified.written, node.text, namespace && namespace.imported === undefined
|
|
230
|
+
? namespace.module
|
|
231
|
+
: undefined);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
emit(node, node.text, node.text);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else if (node.type === "type_query") {
|
|
239
|
+
const name = typeQueryName(node);
|
|
240
|
+
if (name && !shadows.has(name))
|
|
241
|
+
emit(node, name, name);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const reference = importTypeReference(node);
|
|
245
|
+
if (reference) {
|
|
246
|
+
emit(node, reference.written, reference.name, reference.module);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const child of node.namedChildren)
|
|
250
|
+
walk(child, shadows);
|
|
251
|
+
for (const name of added)
|
|
252
|
+
shadows.delete(name);
|
|
253
|
+
};
|
|
254
|
+
walk(root, new Set());
|
|
255
|
+
return sites;
|
|
256
|
+
}
|
|
257
|
+
/* --------------------------------------------------------- declarations */
|
|
258
|
+
/** Peek through `declare` (and an `export` wrapper) to the declaration. */
|
|
259
|
+
function unwrapDeclaration(node) {
|
|
260
|
+
if (node.type !== "ambient_declaration")
|
|
261
|
+
return node;
|
|
262
|
+
return node.namedChildren[0] ?? node;
|
|
263
|
+
}
|
|
264
|
+
/** Module-level interfaces, type aliases, and enums, export status included. */
|
|
265
|
+
function topLevelDeclarations(root) {
|
|
266
|
+
const found = [];
|
|
267
|
+
for (const statement of root.namedChildren) {
|
|
268
|
+
let declaration = statement;
|
|
269
|
+
let exported = false;
|
|
270
|
+
if (statement.type === "export_statement") {
|
|
271
|
+
const declared = statement.childForFieldName("declaration");
|
|
272
|
+
if (!declared)
|
|
273
|
+
continue;
|
|
274
|
+
declaration = declared;
|
|
275
|
+
exported = true;
|
|
276
|
+
}
|
|
277
|
+
declaration = unwrapDeclaration(declaration);
|
|
278
|
+
const kind = declaration.type === "interface_declaration" ? "interface"
|
|
279
|
+
: declaration.type === "type_alias_declaration" ? "type"
|
|
280
|
+
: declaration.type === "enum_declaration" ? "enum" : undefined;
|
|
281
|
+
// The span keeps `export` / `declare`, so the reader sees the statement.
|
|
282
|
+
if (kind)
|
|
283
|
+
found.push({ declaration, span: statement, kind, exported });
|
|
284
|
+
}
|
|
285
|
+
return found;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Contract node for one declaration: the whole source span, so a reader sees
|
|
289
|
+
* every field (`errors` included) and the statement's modifiers, and no steps,
|
|
290
|
+
* because a declaration is never called.
|
|
291
|
+
*/
|
|
292
|
+
function contractFromNode(file, declaration, span, kind, exported) {
|
|
293
|
+
const nameNode = declaration.childForFieldName("name") ??
|
|
294
|
+
declaration.namedChildren.find((c) => c.type === "type_identifier" || c.type === "identifier");
|
|
295
|
+
const name = nameNode?.text;
|
|
296
|
+
if (!name)
|
|
297
|
+
return null;
|
|
298
|
+
const line = span.startPosition.row + 1;
|
|
299
|
+
const endLine = span.endPosition.row + 1;
|
|
300
|
+
const info = {
|
|
301
|
+
review: { kind },
|
|
302
|
+
key: name,
|
|
303
|
+
label: `${kind} ${name}`,
|
|
304
|
+
file,
|
|
305
|
+
steps: [],
|
|
306
|
+
exported,
|
|
307
|
+
start: span.startIndex,
|
|
308
|
+
end: span.endIndex,
|
|
309
|
+
line,
|
|
310
|
+
};
|
|
311
|
+
if (endLine > line)
|
|
312
|
+
info.endLine = endLine;
|
|
313
|
+
return info;
|
|
314
|
+
}
|
|
315
|
+
/** The smallest-spanning definition whose source range contains an offset. */
|
|
316
|
+
function innermostOwner(owners, offset) {
|
|
317
|
+
let best = null;
|
|
318
|
+
let bestSpan = Infinity;
|
|
319
|
+
for (const owner of owners) {
|
|
320
|
+
if (offset < owner.start || offset >= owner.end)
|
|
321
|
+
continue;
|
|
322
|
+
const span = owner.end - owner.start;
|
|
323
|
+
if (span < bestSpan || (span === bestSpan && owner.start > (best?.start ?? -1))) {
|
|
324
|
+
best = owner;
|
|
325
|
+
bestSpan = span;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return best;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Record the type contracts of one file: append a non-callable definition per
|
|
332
|
+
* interface / type alias / enum, then attach each written reference to the
|
|
333
|
+
* innermost definition (callable or contract) whose span contains it. A
|
|
334
|
+
* reference in module-level code that belongs to no definition is left out
|
|
335
|
+
* rather than credited to a sibling.
|
|
336
|
+
*/
|
|
337
|
+
export function extractTypeContracts(file, tree, functions) {
|
|
338
|
+
const root = tree.rootNode;
|
|
339
|
+
const imports = fileImports(root);
|
|
340
|
+
const sites = collectSites(root, imports);
|
|
341
|
+
const contracts = topLevelDeclarations(root)
|
|
342
|
+
.map(({ declaration, span, kind, exported }) => contractFromNode(file, declaration, span, kind, exported))
|
|
343
|
+
.filter((info) => info !== null);
|
|
344
|
+
const owners = [...functions, ...contracts];
|
|
345
|
+
const attributed = new Map();
|
|
346
|
+
for (const site of sites) {
|
|
347
|
+
const owner = innermostOwner(owners, site.offset);
|
|
348
|
+
if (!owner)
|
|
349
|
+
continue;
|
|
350
|
+
let entries = attributed.get(owner);
|
|
351
|
+
if (!entries)
|
|
352
|
+
attributed.set(owner, (entries = new Map()));
|
|
353
|
+
const key = `${site.name}\0${site.module ?? ""}\0${site.imported ?? ""}\0${site.line}`;
|
|
354
|
+
if (entries.has(key))
|
|
355
|
+
continue;
|
|
356
|
+
const entry = { name: site.name };
|
|
357
|
+
if (site.module !== undefined)
|
|
358
|
+
entry.module = site.module;
|
|
359
|
+
if (site.imported !== undefined)
|
|
360
|
+
entry.imported = site.imported;
|
|
361
|
+
entries.set(key, entry);
|
|
362
|
+
SITE_INFO.set(entry, { line: site.line, written: site.written });
|
|
363
|
+
}
|
|
364
|
+
for (const [owner, entries] of attributed) {
|
|
365
|
+
// Reuse the existing review object: a sibling helper's WeakMap keyed by it
|
|
366
|
+
// (or its arrays) must keep working, and no field of theirs is displaced.
|
|
367
|
+
const references = [...entries.values()];
|
|
368
|
+
if (owner.review)
|
|
369
|
+
owner.review.references = references;
|
|
370
|
+
else
|
|
371
|
+
owner.review = { references };
|
|
372
|
+
}
|
|
373
|
+
for (const contract of contracts)
|
|
374
|
+
functions.push(contract);
|
|
375
|
+
}
|
|
376
|
+
const BASIS_TEXT = {
|
|
377
|
+
"relative-import": "relative import path",
|
|
378
|
+
"module-path": "module path suffix",
|
|
379
|
+
"same-file": "same-file declaration",
|
|
380
|
+
unique: "unique snapshot declaration",
|
|
381
|
+
};
|
|
382
|
+
/** Cross-platform join for git-style POSIX paths. */
|
|
383
|
+
function joinPath(directory, specifier) {
|
|
384
|
+
const parts = [];
|
|
385
|
+
for (const part of `${directory}/${specifier}`.split("/")) {
|
|
386
|
+
if (part === "" || part === ".")
|
|
387
|
+
continue;
|
|
388
|
+
if (part === "..") {
|
|
389
|
+
parts.pop();
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
parts.push(part);
|
|
393
|
+
}
|
|
394
|
+
return parts.join("/");
|
|
395
|
+
}
|
|
396
|
+
const MODULE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".d.ts"];
|
|
397
|
+
const TS_EXTENSION = /\.(d\.ts|ts|tsx|mts|cts)$/;
|
|
398
|
+
/**
|
|
399
|
+
* Candidate snapshot paths for `./x` / `../x`: the extension the specifier may
|
|
400
|
+
* omit (or the `.js` ESM spelling), and the directory index.
|
|
401
|
+
*/
|
|
402
|
+
function relativeCandidates(ownerFile, specifier) {
|
|
403
|
+
const slash = ownerFile.lastIndexOf("/");
|
|
404
|
+
const directory = slash === -1 ? "" : ownerFile.slice(0, slash);
|
|
405
|
+
const base = joinPath(directory, specifier);
|
|
406
|
+
if (/\.(ts|tsx|mts|cts)$/.test(base) || base.endsWith(".d.ts"))
|
|
407
|
+
return [base];
|
|
408
|
+
if (/\.(js|jsx|mjs|cjs)$/.test(base)) {
|
|
409
|
+
const stem = base.replace(/\.(js|jsx|mjs|cjs)$/, "");
|
|
410
|
+
return MODULE_EXTENSIONS.map((ext) => `${stem}${ext}`);
|
|
411
|
+
}
|
|
412
|
+
const candidates = MODULE_EXTENSIONS.map((ext) => `${base}${ext}`);
|
|
413
|
+
for (const ext of MODULE_EXTENSIONS)
|
|
414
|
+
candidates.push(`${base}/index${ext}`);
|
|
415
|
+
return candidates;
|
|
416
|
+
}
|
|
417
|
+
/** Shortest path tail a bare or aliased specifier names, or null for a package. */
|
|
418
|
+
function moduleTail(specifier) {
|
|
419
|
+
const slash = specifier.indexOf("/");
|
|
420
|
+
if (slash === -1)
|
|
421
|
+
return null;
|
|
422
|
+
const tail = specifier.slice(slash + 1).replace(/#.*$/, "");
|
|
423
|
+
return tail === "" ? null : tail;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Resolve one reference against the snapshot. A relative specifier must name a
|
|
427
|
+
* snapshot file (exact binding); a bare or aliased specifier binds only when
|
|
428
|
+
* exactly one snapshot file's path ends with its tail; a name written without a
|
|
429
|
+
* module binds to the file's own declaration. Only a name no module explains
|
|
430
|
+
* may fall back to a globally unique declaration.
|
|
431
|
+
*/
|
|
432
|
+
export function resolveTypeContracts(functions) {
|
|
433
|
+
const byName = new Map();
|
|
434
|
+
const byFile = new Map();
|
|
435
|
+
for (const info of functions) {
|
|
436
|
+
if (!info.review?.kind)
|
|
437
|
+
continue;
|
|
438
|
+
const named = byName.get(info.key);
|
|
439
|
+
if (named)
|
|
440
|
+
named.push(info);
|
|
441
|
+
else
|
|
442
|
+
byName.set(info.key, [info]);
|
|
443
|
+
let file = byFile.get(info.file);
|
|
444
|
+
if (!file)
|
|
445
|
+
byFile.set(info.file, (file = new Map()));
|
|
446
|
+
const inFile = file.get(info.key);
|
|
447
|
+
if (inFile)
|
|
448
|
+
inFile.push(info);
|
|
449
|
+
else
|
|
450
|
+
file.set(info.key, [info]);
|
|
451
|
+
}
|
|
452
|
+
if (byName.size === 0)
|
|
453
|
+
return [];
|
|
454
|
+
const files = [...byFile.keys()];
|
|
455
|
+
const resolve = (file, ref) => {
|
|
456
|
+
const name = ref.imported ?? ref.name;
|
|
457
|
+
if (ref.module !== undefined) {
|
|
458
|
+
if (ref.module.startsWith("./") || ref.module.startsWith("../")) {
|
|
459
|
+
for (const candidate of relativeCandidates(file, ref.module)) {
|
|
460
|
+
const target = byFile.get(candidate)?.get(name)?.[0];
|
|
461
|
+
if (target)
|
|
462
|
+
return { target, basis: "relative-import" };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
const tail = moduleTail(ref.module);
|
|
467
|
+
if (tail) {
|
|
468
|
+
const matches = new Set();
|
|
469
|
+
for (const snapshotFile of files) {
|
|
470
|
+
const stem = snapshotFile.replace(TS_EXTENSION, "");
|
|
471
|
+
if (stem !== tail &&
|
|
472
|
+
!stem.endsWith(`/${tail}`) &&
|
|
473
|
+
!stem.endsWith(`/${tail}/index`)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const target = byFile.get(snapshotFile)?.get(name)?.[0];
|
|
477
|
+
if (target)
|
|
478
|
+
matches.add(target);
|
|
479
|
+
}
|
|
480
|
+
if (matches.size === 1) {
|
|
481
|
+
return { target: [...matches][0], basis: "module-path" };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const target = byFile.get(file)?.get(name)?.[0];
|
|
489
|
+
if (target)
|
|
490
|
+
return { target, basis: "same-file" };
|
|
491
|
+
}
|
|
492
|
+
// Only an unimported name may use a unique snapshot candidate.
|
|
493
|
+
const unique = byName.get(name);
|
|
494
|
+
if (unique?.length === 1)
|
|
495
|
+
return { target: unique[0], basis: "unique" };
|
|
496
|
+
return null;
|
|
497
|
+
};
|
|
498
|
+
const edges = [];
|
|
499
|
+
for (const owner of functions) {
|
|
500
|
+
const references = owner.review?.references;
|
|
501
|
+
if (!references)
|
|
502
|
+
continue;
|
|
503
|
+
for (const ref of references) {
|
|
504
|
+
const binding = resolve(owner.file, ref);
|
|
505
|
+
// A declaration naming itself adds nothing beside its own node.
|
|
506
|
+
if (!binding || binding.target === owner)
|
|
507
|
+
continue;
|
|
508
|
+
const site = SITE_INFO.get(ref);
|
|
509
|
+
const line = site?.line ?? owner.line ?? 1;
|
|
510
|
+
const written = site?.written ?? ref.name;
|
|
511
|
+
const origin = ref.module
|
|
512
|
+
? ref.imported
|
|
513
|
+
? ` (${ref.imported} as ${ref.name} from "${ref.module}")`
|
|
514
|
+
: ` (from "${ref.module}")`
|
|
515
|
+
: "";
|
|
516
|
+
edges.push({
|
|
517
|
+
owner,
|
|
518
|
+
target: binding.target,
|
|
519
|
+
kind: "contract",
|
|
520
|
+
line,
|
|
521
|
+
evidence: `syntactic type reference ${written}${origin} in ${owner.key}, ` +
|
|
522
|
+
`not a call; bound to ${binding.target.review?.kind ?? "type"} ` +
|
|
523
|
+
`${binding.target.key} by ${BASIS_TEXT[binding.basis]}`,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return edges;
|
|
528
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript dispatch review context.
|
|
3
|
+
*
|
|
4
|
+
* Two wiring styles move work across a module boundary without a call the
|
|
5
|
+
* syntax can follow, so a diff hunk alone never shows the other end:
|
|
6
|
+
*
|
|
7
|
+
* - an event emitter (`x.emit(key, …)` / `x.emitAsync(key, …)`) and the handler
|
|
8
|
+
* registered for that key (`@OnEvent(...)`, `@OnTypedEvent(...)`);
|
|
9
|
+
* - a queue producer (`this.<injected>.add(key, …)`) and the consumer handling
|
|
10
|
+
* that key (`@Processor(channel)`, then `switch (job.name) { case key }`,
|
|
11
|
+
* `if (job.name === key)`, or `@Process(key)`).
|
|
12
|
+
*
|
|
13
|
+
* Extraction records each site as `review.dispatches` on the owning
|
|
14
|
+
* `FunctionInfo`; resolution pairs a producer with its consumer and returns one
|
|
15
|
+
* candidate edge per pair. Nothing here claims runtime delivery. A key match is
|
|
16
|
+
* a static syntax relation, only ever made between keys whose value resolved to
|
|
17
|
+
* the same literal or between the same written member path, and every edge
|
|
18
|
+
* states how it was matched.
|
|
19
|
+
*
|
|
20
|
+
* Unsupported, deliberately:
|
|
21
|
+
* - imperative registrations (`emitter.on(key, handler)`, `queue.process(...)`)
|
|
22
|
+
* — the registration site and the handler are different owners and the
|
|
23
|
+
* review contract has no field for a handler reference;
|
|
24
|
+
* - computed or template keys with substitutions, `obj["add"]`, and keys read
|
|
25
|
+
* from parameters or instance state (recorded as nothing rather than guessed);
|
|
26
|
+
* - queues reached through a receiver other than an `@InjectQueue` field, so a
|
|
27
|
+
* `Set.add` / `classList.add` call can never be read as a job dispatch;
|
|
28
|
+
* - constant values that are not plain string literals (escapes are not
|
|
29
|
+
* decoded, computed enum initialisers are not evaluated).
|
|
30
|
+
*/
|
|
31
|
+
import type Parser from "tree-sitter";
|
|
32
|
+
import type { FunctionInfo } from "../types.js";
|
|
33
|
+
type Tree = Parser.Tree;
|
|
34
|
+
/** One dispatch site, appended to the owning function's `review.dispatches`. */
|
|
35
|
+
export interface DispatchRecord {
|
|
36
|
+
kind: "event" | "queue";
|
|
37
|
+
direction: "emit" | "handle";
|
|
38
|
+
/** Provenance-tagged static key; see the tag constants below. */
|
|
39
|
+
key: string;
|
|
40
|
+
/** Provenance-tagged queue channel. Absent when the channel is not static. */
|
|
41
|
+
channel?: string;
|
|
42
|
+
/** 1-based line of the emit call, registration decorator or switch case. */
|
|
43
|
+
line: number;
|
|
44
|
+
/** Source-derived description of the site, quoted in the resolved edge. */
|
|
45
|
+
evidence: string;
|
|
46
|
+
}
|
|
47
|
+
/** One candidate context edge between a dispatch site and its counterpart. */
|
|
48
|
+
export interface DispatchEdge {
|
|
49
|
+
owner: FunctionInfo;
|
|
50
|
+
target: FunctionInfo;
|
|
51
|
+
evidence: string;
|
|
52
|
+
kind: "event" | "queue";
|
|
53
|
+
line: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Record this file's dispatch sites on their owning functions. Declarations are
|
|
57
|
+
* never added; only `review.dispatches` is appended, next to whatever the
|
|
58
|
+
* contract extractor already wrote there.
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractDispatchContext(file: string, tree: Tree, functions: FunctionInfo[]): void;
|
|
61
|
+
/**
|
|
62
|
+
* Pair every event/queue producer with the handlers registered for its key.
|
|
63
|
+
* Queue edges additionally require both sides to name the same channel, so two
|
|
64
|
+
* queues that reuse a job name never cross. One edge per (kind, owner, target,
|
|
65
|
+
* key): repeated sites in one function collapse onto the earliest line.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveDispatchContext(functions: readonly FunctionInfo[]): DispatchEdge[];
|
|
68
|
+
export {};
|