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,722 @@
|
|
|
1
|
+
import { childByType, collapseWs, locFromNode, namedChildren, sameNode, } from "./types.js";
|
|
2
|
+
import { jsCallSyntax, jsFunctionScope, jsModuleScope, jsParameterList, } from "./call-syntax.js";
|
|
3
|
+
import { extractDispatchContext } from "./typescript-dispatch.js";
|
|
4
|
+
import { extractTypeContracts } from "./typescript-contracts.js";
|
|
5
|
+
/** Nothing declared: no class owner, no field type to reach a receiver through. */
|
|
6
|
+
const NO_RECEIVERS = new Map();
|
|
7
|
+
/** A `new` target names a class, not a member of the enclosing class. */
|
|
8
|
+
const NO_CLASS = { className: null, receivers: NO_RECEIVERS };
|
|
9
|
+
/** Module-level scope of the file being extracted, plus an empty class. */
|
|
10
|
+
function moduleEnv(root) {
|
|
11
|
+
return { scope: jsModuleScope(root), className: null, receivers: NO_RECEIVERS };
|
|
12
|
+
}
|
|
13
|
+
function isFnLike(type) {
|
|
14
|
+
return (type === "function_declaration" ||
|
|
15
|
+
type === "function_expression" ||
|
|
16
|
+
type === "arrow_function" ||
|
|
17
|
+
type === "generator_function" ||
|
|
18
|
+
type === "generator_function_declaration" ||
|
|
19
|
+
type === "method_definition");
|
|
20
|
+
}
|
|
21
|
+
/** Nested function/arrow, not a method — skip as a sibling, maybe nest under a call. */
|
|
22
|
+
function isCallback(type) {
|
|
23
|
+
return isFnLike(type) && type !== "method_definition";
|
|
24
|
+
}
|
|
25
|
+
function getParamsLabel(params) {
|
|
26
|
+
if (!params || params.type !== "formal_parameters")
|
|
27
|
+
return "()";
|
|
28
|
+
const parts = [];
|
|
29
|
+
for (const p of namedChildren(params)) {
|
|
30
|
+
if (p.type === "required_parameter" || p.type === "optional_parameter") {
|
|
31
|
+
const rest = childByType(p, "rest_pattern");
|
|
32
|
+
if (rest) {
|
|
33
|
+
const id = childByType(rest, "identifier");
|
|
34
|
+
parts.push(id ? `...${id.text}` : "...");
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const id = childByType(p, "identifier");
|
|
38
|
+
if (id) {
|
|
39
|
+
parts.push(id.text);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (childByType(p, "object_pattern")) {
|
|
43
|
+
parts.push("{}");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (childByType(p, "array_pattern")) {
|
|
47
|
+
parts.push("[]");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
parts.push("_");
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (p.type === "rest_parameter" || p.type === "rest_pattern") {
|
|
54
|
+
const id = childByType(p, "identifier");
|
|
55
|
+
parts.push(id ? `...${id.text}` : "...");
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
parts.push("_");
|
|
59
|
+
}
|
|
60
|
+
return parts.length === 0 ? "()" : `(${parts.join(", ")})`;
|
|
61
|
+
}
|
|
62
|
+
function condText(test) {
|
|
63
|
+
if (test.type === "parenthesized_expression") {
|
|
64
|
+
const inner = test.namedChild(0);
|
|
65
|
+
if (inner)
|
|
66
|
+
return collapseWs(inner.text);
|
|
67
|
+
}
|
|
68
|
+
return collapseWs(test.text);
|
|
69
|
+
}
|
|
70
|
+
function branchKey(kind, cond) {
|
|
71
|
+
if (kind === "else")
|
|
72
|
+
return "else";
|
|
73
|
+
return `${kind}:${cond}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Declared class type of a receiver reached through `this.<field>`: the field
|
|
77
|
+
* or constructor parameter property it names, and nothing else. A receiver the
|
|
78
|
+
* scope cannot type resolves to nothing rather than to the enclosing class.
|
|
79
|
+
*/
|
|
80
|
+
function thisReceiverType(object, receivers) {
|
|
81
|
+
if (object.type !== "member_expression")
|
|
82
|
+
return null;
|
|
83
|
+
const root = object.namedChild(0);
|
|
84
|
+
const property = namedChildren(object).find((c) => c.type === "property_identifier" ||
|
|
85
|
+
c.type === "private_property_identifier") ?? null;
|
|
86
|
+
if (root?.type !== "this" || !property)
|
|
87
|
+
return null;
|
|
88
|
+
return receivers.get(property.text) ?? null;
|
|
89
|
+
}
|
|
90
|
+
function calleeKey(node, ctx) {
|
|
91
|
+
if (node.type === "identifier")
|
|
92
|
+
return node.text;
|
|
93
|
+
if (node.type === "this")
|
|
94
|
+
return ctx.className;
|
|
95
|
+
if (node.type === "member_expression") {
|
|
96
|
+
const object = node.namedChild(0);
|
|
97
|
+
const property = namedChildren(node).find((c) => c.type === "property_identifier" ||
|
|
98
|
+
c.type === "private_property_identifier") ?? null;
|
|
99
|
+
if (!object || !property)
|
|
100
|
+
return null;
|
|
101
|
+
const propName = property.text;
|
|
102
|
+
if (object.type === "this" && ctx.className) {
|
|
103
|
+
return `${ctx.className}.${propName}`;
|
|
104
|
+
}
|
|
105
|
+
if (object.type === "identifier") {
|
|
106
|
+
return `${object.text}.${propName}`;
|
|
107
|
+
}
|
|
108
|
+
// `this.<field>.<method>()`: key by the receiver's declared class, never by
|
|
109
|
+
// the class the call was written in. An untyped receiver keeps its own text
|
|
110
|
+
// as the key, so it cannot borrow a same-named method of the wrong owner.
|
|
111
|
+
const receiver = thisReceiverType(object, ctx.receivers);
|
|
112
|
+
if (receiver)
|
|
113
|
+
return `${receiver}.${propName}`;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Treat JSX tags as component "calls". PascalCase identifiers and any
|
|
120
|
+
* member expression (`Foo.Bar`, `motion.div`) count; lowercase tags are HTML.
|
|
121
|
+
*/
|
|
122
|
+
function jsxCalleeKey(node, ctx) {
|
|
123
|
+
for (const child of namedChildren(node)) {
|
|
124
|
+
if (child.type === "identifier") {
|
|
125
|
+
const name = child.text;
|
|
126
|
+
return /^[A-Z]/.test(name) ? name : null;
|
|
127
|
+
}
|
|
128
|
+
if (child.type === "member_expression") {
|
|
129
|
+
return calleeKey(child, ctx);
|
|
130
|
+
}
|
|
131
|
+
if (child.type === "jsx_attribute" ||
|
|
132
|
+
child.type === "jsx_expression" ||
|
|
133
|
+
child.type === "type_arguments") {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function statementsOf(node) {
|
|
140
|
+
if (node.type === "statement_block")
|
|
141
|
+
return namedChildren(node);
|
|
142
|
+
return [node];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Steps of one statement list. `env` carries the lexical scope in effect
|
|
146
|
+
* (module, function, or method), which is what classifies call targets.
|
|
147
|
+
*/
|
|
148
|
+
function collectStatements(file, statements, env) {
|
|
149
|
+
const steps = [];
|
|
150
|
+
const seenCalls = new Set();
|
|
151
|
+
const addCall = (key, node, syntax) => {
|
|
152
|
+
const mark = `${key}:${node.startIndex}`;
|
|
153
|
+
if (seenCalls.has(mark))
|
|
154
|
+
return;
|
|
155
|
+
seenCalls.add(mark);
|
|
156
|
+
const step = { type: "call", key, ...locFromNode(file, node) };
|
|
157
|
+
if (syntax)
|
|
158
|
+
step.syntax = syntax;
|
|
159
|
+
steps.push(step);
|
|
160
|
+
};
|
|
161
|
+
const emitCall = (key, node, nested, syntax) => {
|
|
162
|
+
if (key && nested.length > 0) {
|
|
163
|
+
const step = {
|
|
164
|
+
type: "call",
|
|
165
|
+
key,
|
|
166
|
+
...locFromNode(file, node),
|
|
167
|
+
children: nested,
|
|
168
|
+
};
|
|
169
|
+
if (syntax)
|
|
170
|
+
step.syntax = syntax;
|
|
171
|
+
steps.push(step);
|
|
172
|
+
}
|
|
173
|
+
else if (key) {
|
|
174
|
+
addCall(key, node, syntax);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
steps.push(...nested);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const walkExpr = (node) => {
|
|
181
|
+
const type = node.type;
|
|
182
|
+
if (isCallback(type)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (type === "if_statement") {
|
|
186
|
+
const test = childByType(node, "parenthesized_expression") ??
|
|
187
|
+
namedChildren(node).find((c) => c.type !== "else_clause") ??
|
|
188
|
+
null;
|
|
189
|
+
const kids = namedChildren(node);
|
|
190
|
+
const consequent = kids.find((c) => c.type !== "parenthesized_expression" && c.type !== "else_clause") ?? null;
|
|
191
|
+
const elseClause = childByType(node, "else_clause");
|
|
192
|
+
const cond = test ? condText(test) : "";
|
|
193
|
+
steps.push({
|
|
194
|
+
type: "branch",
|
|
195
|
+
key: branchKey("if", cond),
|
|
196
|
+
label: test ? `if (${condText(test)})` : "if",
|
|
197
|
+
...locFromNode(file, test ?? node),
|
|
198
|
+
children: consequent
|
|
199
|
+
? collectStatements(file, statementsOf(consequent), env)
|
|
200
|
+
: [],
|
|
201
|
+
});
|
|
202
|
+
let current = elseClause;
|
|
203
|
+
while (current) {
|
|
204
|
+
const inner = current.namedChild(0);
|
|
205
|
+
if (!inner)
|
|
206
|
+
break;
|
|
207
|
+
if (inner.type === "if_statement") {
|
|
208
|
+
const elseTest = childByType(inner, "parenthesized_expression") ?? null;
|
|
209
|
+
const elseKids = namedChildren(inner);
|
|
210
|
+
const elseConsequent = elseKids.find((c) => c.type !== "parenthesized_expression" &&
|
|
211
|
+
c.type !== "else_clause") ?? null;
|
|
212
|
+
const elseCond = elseTest ? condText(elseTest) : "";
|
|
213
|
+
steps.push({
|
|
214
|
+
type: "branch",
|
|
215
|
+
key: branchKey("else-if", elseCond),
|
|
216
|
+
label: elseTest ? `else if (${condText(elseTest)})` : "else if",
|
|
217
|
+
...locFromNode(file, elseTest ?? current),
|
|
218
|
+
children: elseConsequent
|
|
219
|
+
? collectStatements(file, statementsOf(elseConsequent), env)
|
|
220
|
+
: [],
|
|
221
|
+
});
|
|
222
|
+
current = childByType(inner, "else_clause");
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
steps.push({
|
|
226
|
+
type: "branch",
|
|
227
|
+
key: branchKey("else", ""),
|
|
228
|
+
label: "else",
|
|
229
|
+
...locFromNode(file, current),
|
|
230
|
+
children: collectStatements(file, statementsOf(inner), env),
|
|
231
|
+
});
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (type === "call_expression" || type === "new_expression") {
|
|
237
|
+
const isNew = type === "new_expression";
|
|
238
|
+
const callee = node.namedChild(0);
|
|
239
|
+
const bare = callee ? calleeKey(callee, isNew ? NO_CLASS : env) : null;
|
|
240
|
+
const args = childByType(node, "arguments");
|
|
241
|
+
const syntax = callee ? jsCallSyntax(callee, args, env) : undefined;
|
|
242
|
+
// A dynamic target has no key from `calleeKey`; the call is still a call,
|
|
243
|
+
// so the target expression itself becomes its identity.
|
|
244
|
+
const base = bare ?? (syntax ? syntax.callee : null);
|
|
245
|
+
const key = base && isNew && !base.startsWith("new ") ? `new ${base}` : base;
|
|
246
|
+
emitCall(key, node, args ? stepsFromArguments(file, args, env) : [], syntax);
|
|
247
|
+
// `foo(x).bar()` keeps `foo` — the receiver is not an argument.
|
|
248
|
+
if (callee)
|
|
249
|
+
walkExpr(callee);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (type === "jsx_element") {
|
|
253
|
+
const opening = childByType(node, "jsx_opening_element");
|
|
254
|
+
const childNodes = namedChildren(node).filter((c) => c.type !== "jsx_opening_element" && c.type !== "jsx_closing_element");
|
|
255
|
+
const fromAttrs = [];
|
|
256
|
+
if (opening) {
|
|
257
|
+
for (const attr of namedChildren(opening)) {
|
|
258
|
+
if (attr.type === "jsx_attribute" ||
|
|
259
|
+
attr.type === "jsx_expression") {
|
|
260
|
+
fromAttrs.push(...collectStatements(file, [attr], env));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const nested = [
|
|
265
|
+
...fromAttrs,
|
|
266
|
+
...collectStatements(file, childNodes, env),
|
|
267
|
+
];
|
|
268
|
+
emitCall(opening ? jsxCalleeKey(opening, env) : null, opening ?? node, nested);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (type === "jsx_self_closing_element") {
|
|
272
|
+
const attrNodes = namedChildren(node).filter((c) => c.type === "jsx_attribute" || c.type === "jsx_expression");
|
|
273
|
+
emitCall(jsxCalleeKey(node, env), node, collectStatements(file, attrNodes, env));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
for (const child of namedChildren(node)) {
|
|
277
|
+
walkExpr(child);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
for (const stmt of statements) {
|
|
281
|
+
walkExpr(stmt);
|
|
282
|
+
}
|
|
283
|
+
return steps;
|
|
284
|
+
}
|
|
285
|
+
/** Named children of a function node that are never its runtime body. */
|
|
286
|
+
const NON_BODY = new Set([
|
|
287
|
+
"comment",
|
|
288
|
+
"formal_parameters",
|
|
289
|
+
"type_parameters",
|
|
290
|
+
"type_annotation",
|
|
291
|
+
"identifier",
|
|
292
|
+
"accessibility_modifier",
|
|
293
|
+
"async",
|
|
294
|
+
"readonly",
|
|
295
|
+
]);
|
|
296
|
+
function bodyOf(node) {
|
|
297
|
+
return (childByType(node, "statement_block") ??
|
|
298
|
+
namedChildren(node).find((c) => !NON_BODY.has(c.type)) ??
|
|
299
|
+
null);
|
|
300
|
+
}
|
|
301
|
+
/** Peel `(a) => (b) => body` to `body`. A returned factory (`return () => tick()`) stays out. */
|
|
302
|
+
function unwrapCurriedBody(body) {
|
|
303
|
+
let current = body ? stripTypeWrappers(body) : null;
|
|
304
|
+
while (current &&
|
|
305
|
+
(current.type === "arrow_function" ||
|
|
306
|
+
current.type === "function_expression" ||
|
|
307
|
+
current.type === "generator_function")) {
|
|
308
|
+
const inner = bodyOf(current);
|
|
309
|
+
if (!inner)
|
|
310
|
+
return current;
|
|
311
|
+
current = stripTypeWrappers(inner);
|
|
312
|
+
}
|
|
313
|
+
return current;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Calls and callback bodies inside `(...)`, as children of the receiving call.
|
|
317
|
+
* Skip a callback already registered as its own definition (`const x = wrap(fn)`).
|
|
318
|
+
*/
|
|
319
|
+
function stepsFromArguments(file, args, env) {
|
|
320
|
+
const skipCallbacks = args.parent ? hoistsCallback(args.parent) : false;
|
|
321
|
+
const steps = [];
|
|
322
|
+
for (const rawArg of namedChildren(args)) {
|
|
323
|
+
const arg = stripTypeWrappers(rawArg);
|
|
324
|
+
if (isCallback(arg.type)) {
|
|
325
|
+
if (skipCallbacks)
|
|
326
|
+
continue;
|
|
327
|
+
steps.push(...collectStepsFromBody(file, unwrapCurriedBody(bodyOf(arg)), env));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
steps.push(...collectStatements(file, [arg], env));
|
|
331
|
+
}
|
|
332
|
+
return steps;
|
|
333
|
+
}
|
|
334
|
+
/** True when this call's callback is extracted as a named definition. */
|
|
335
|
+
function hoistsCallback(call) {
|
|
336
|
+
let current = call.parent;
|
|
337
|
+
while (current && TRANSPARENT_EXPRESSIONS.has(current.type)) {
|
|
338
|
+
current = current.parent;
|
|
339
|
+
}
|
|
340
|
+
if (!current)
|
|
341
|
+
return false;
|
|
342
|
+
if (current.type === "variable_declarator" ||
|
|
343
|
+
current.type === "export_statement") {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
if (current.type === "arguments") {
|
|
347
|
+
const outer = current.parent;
|
|
348
|
+
return outer?.type === "call_expression" ? hoistsCallback(outer) : false;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
function collectStepsFromBody(file, body, env) {
|
|
353
|
+
if (!body)
|
|
354
|
+
return [];
|
|
355
|
+
if (body.type === "statement_block") {
|
|
356
|
+
return collectStatements(file, namedChildren(body), env);
|
|
357
|
+
}
|
|
358
|
+
return collectStatements(file, [body], env);
|
|
359
|
+
}
|
|
360
|
+
function functionFromParts(file, key, label, params, body, exported, start, end, env) {
|
|
361
|
+
const declared = jsParameterList(params);
|
|
362
|
+
const info = {
|
|
363
|
+
key,
|
|
364
|
+
label: `${label}${getParamsLabel(params)}`,
|
|
365
|
+
file,
|
|
366
|
+
steps: collectStepsFromBody(file, body, env),
|
|
367
|
+
exported,
|
|
368
|
+
start,
|
|
369
|
+
end,
|
|
370
|
+
};
|
|
371
|
+
if (declared)
|
|
372
|
+
info.params = declared;
|
|
373
|
+
return info;
|
|
374
|
+
}
|
|
375
|
+
const TRANSPARENT_EXPRESSIONS = new Set([
|
|
376
|
+
"parenthesized_expression",
|
|
377
|
+
"as_expression",
|
|
378
|
+
"satisfies_expression",
|
|
379
|
+
"non_null_expression",
|
|
380
|
+
"type_assertion",
|
|
381
|
+
]);
|
|
382
|
+
/**
|
|
383
|
+
* Peel the wrappers TypeScript allows around a value — `(fn)`, `fn as Handler`,
|
|
384
|
+
* `fn satisfies Handler`, `<Handler>fn`, `fn!` — so the node underneath can be
|
|
385
|
+
* matched on its own terms. In every one of these the value is the first named
|
|
386
|
+
* child, except `<T>fn`, where the leading `type_arguments` comes first.
|
|
387
|
+
*/
|
|
388
|
+
function stripTypeWrappers(node) {
|
|
389
|
+
let current = node;
|
|
390
|
+
while (TRANSPARENT_EXPRESSIONS.has(current.type)) {
|
|
391
|
+
const inner = namedChildren(current).find((c) => c.type !== "type_arguments");
|
|
392
|
+
if (!inner)
|
|
393
|
+
return current;
|
|
394
|
+
current = inner;
|
|
395
|
+
}
|
|
396
|
+
return current;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* `export default defineEventHandler(async (event) => {...})` — the module's
|
|
400
|
+
* payload is a function argument to a wrapper call, not a declaration. Extract
|
|
401
|
+
* it, keyed by its own name where it has one and by `fallbackName` otherwise:
|
|
402
|
+
* the declared variable for `const handler = wrapper(...)`, the file stem for a
|
|
403
|
+
* default export, so routes in different files do not collide.
|
|
404
|
+
*
|
|
405
|
+
* Returns whether a function was found, so a nested call that holds none —
|
|
406
|
+
* `createHandler(makeOptions(), async () => {...})` — does not stop the scan
|
|
407
|
+
* before the sibling argument that does.
|
|
408
|
+
*/
|
|
409
|
+
function unwrapWrappedFunction(file, call, fallbackName, exported, functions, outer, local = false) {
|
|
410
|
+
const args = namedChildren(call).find((c) => c.type === "arguments");
|
|
411
|
+
if (!args)
|
|
412
|
+
return false;
|
|
413
|
+
for (const rawArg of namedChildren(args)) {
|
|
414
|
+
const arg = stripTypeWrappers(rawArg);
|
|
415
|
+
// Wrappers compose: `memo(forwardRef(function Input() {...}))`.
|
|
416
|
+
if (arg.type === "call_expression") {
|
|
417
|
+
if (unwrapWrappedFunction(file, arg, fallbackName, exported, functions, outer, local)) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
// Generators count too: `Effect.gen(function* () {...})` is this shape.
|
|
423
|
+
if (!isCallback(arg.type))
|
|
424
|
+
continue;
|
|
425
|
+
const named = childByType(arg, "identifier");
|
|
426
|
+
handleFunctionNode(file, arg, named?.text ?? fallbackName, exported, outer, functions, local);
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
/** File path minus extension — the key for an anonymous default export. */
|
|
432
|
+
function fileStem(file) {
|
|
433
|
+
return file.replace(/\.[^./]+$/, "");
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Extract a function from a variable declarator's initialiser: a direct
|
|
437
|
+
* arrow/function expression, or a function argument to a wrapper call.
|
|
438
|
+
*/
|
|
439
|
+
function extractDeclaratorFunction(file, d, exported, functions, outer, local = false) {
|
|
440
|
+
const id = childByType(d, "identifier");
|
|
441
|
+
if (!id)
|
|
442
|
+
return;
|
|
443
|
+
// The initialiser is the declarator's value child — everything but the
|
|
444
|
+
// name and any type annotation — with its type wrappers peeled off.
|
|
445
|
+
const value = namedChildren(d).find((c) => !sameNode(c, id) && c.type !== "type_annotation");
|
|
446
|
+
const init = value ? stripTypeWrappers(value) : null;
|
|
447
|
+
if (!init)
|
|
448
|
+
return;
|
|
449
|
+
if (init.type === "arrow_function" || init.type === "function_expression") {
|
|
450
|
+
handleFunctionNode(file, init, id.text, exported, outer, functions, local);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// `const handler = defineEventHandler(async (event) => {...})` — the
|
|
454
|
+
// initialiser is the wrapper call, so the function is one level in.
|
|
455
|
+
if (init.type === "call_expression") {
|
|
456
|
+
unwrapWrappedFunction(file, init, id.text, exported, functions, outer, local);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Register definitions declared inside a function body.
|
|
461
|
+
*
|
|
462
|
+
* `visitStatement` only walks top-level statements, so a helper declared inside
|
|
463
|
+
* a body was never indexed at all, and calls to it fell through to whatever
|
|
464
|
+
* top-level function elsewhere in the repo happened to share the bare name.
|
|
465
|
+
* See #19.
|
|
466
|
+
*
|
|
467
|
+
* Helper bodies are still not attributed to the outer caller (contract #5);
|
|
468
|
+
* they become definitions in their own right, marked `local`.
|
|
469
|
+
*/
|
|
470
|
+
function collectLocalDefinitions(file, body, env, functions) {
|
|
471
|
+
if (!body)
|
|
472
|
+
return;
|
|
473
|
+
const walk = (node) => {
|
|
474
|
+
for (const child of namedChildren(node)) {
|
|
475
|
+
if (child.type === "function_declaration" ||
|
|
476
|
+
child.type === "generator_function_declaration") {
|
|
477
|
+
const id = childByType(child, "identifier");
|
|
478
|
+
handleFunctionNode(file, child, id?.text ?? null, false, env, functions, true);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (child.type === "lexical_declaration" ||
|
|
482
|
+
child.type === "variable_declaration") {
|
|
483
|
+
for (const d of namedChildren(child)) {
|
|
484
|
+
if (d.type !== "variable_declarator")
|
|
485
|
+
continue;
|
|
486
|
+
extractDeclaratorFunction(file, d, false, functions, env, true);
|
|
487
|
+
}
|
|
488
|
+
// Fall through: `walk` skips the initializer bodies as fn-like below,
|
|
489
|
+
// so a declaration list is never registered twice.
|
|
490
|
+
}
|
|
491
|
+
// Anonymous callbacks are not addressable by name; skip their bodies.
|
|
492
|
+
if (isFnLike(child.type))
|
|
493
|
+
continue;
|
|
494
|
+
walk(child);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
walk(body);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Scope of one function body. The enclosing class identity is inherited, so a
|
|
501
|
+
* local closure still has its method's `this` bindings — including the declared
|
|
502
|
+
* class of each field a receiver is reached through.
|
|
503
|
+
*/
|
|
504
|
+
function functionEnv(node, outer) {
|
|
505
|
+
return {
|
|
506
|
+
scope: jsFunctionScope(node, outer.scope),
|
|
507
|
+
className: outer.className,
|
|
508
|
+
receivers: outer.receivers,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function handleFunctionNode(file, node, name, exported, outer, functions,
|
|
512
|
+
/** Declared inside another body: key stays bare and resolution is file-scoped. */
|
|
513
|
+
local = false) {
|
|
514
|
+
if (!name)
|
|
515
|
+
return;
|
|
516
|
+
const key = outer.className && !local ? `${outer.className}.${name}` : name;
|
|
517
|
+
const params = childByType(node, "formal_parameters");
|
|
518
|
+
const body = unwrapCurriedBody(bodyOf(node));
|
|
519
|
+
const env = functionEnv(node, outer);
|
|
520
|
+
const info = functionFromParts(file, key, key, params, body, exported, node.startIndex, node.endIndex, env);
|
|
521
|
+
functions.push(local ? { ...info, local: true } : info);
|
|
522
|
+
collectLocalDefinitions(file, body, env, functions);
|
|
523
|
+
}
|
|
524
|
+
/** The one class an annotation names, or nothing when the type is not bare. */
|
|
525
|
+
function bareClassType(annotation) {
|
|
526
|
+
if (annotation?.type !== "type_annotation")
|
|
527
|
+
return null;
|
|
528
|
+
const type = annotation.namedChild(0);
|
|
529
|
+
return type?.type === "type_identifier" ? type.text : null;
|
|
530
|
+
}
|
|
531
|
+
/** `static` is an unnamed token on the member, not a named child. */
|
|
532
|
+
function isStaticMember(node) {
|
|
533
|
+
return node.children.some((child) => child.type === "static");
|
|
534
|
+
}
|
|
535
|
+
/** Only a visibility modifier or `readonly` makes a parameter a property. */
|
|
536
|
+
function isParameterProperty(param) {
|
|
537
|
+
if (param.type !== "required_parameter" &&
|
|
538
|
+
param.type !== "optional_parameter") {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
if (childByType(param, "accessibility_modifier"))
|
|
542
|
+
return true;
|
|
543
|
+
return param.children.some((child) => child.type === "readonly");
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Class types a `this.<name>` receiver can have without guessing: a field or a
|
|
547
|
+
* constructor parameter property annotated with one bare class name.
|
|
548
|
+
*
|
|
549
|
+
* A generic, union, qualified, or inferred type names no single owner, so the
|
|
550
|
+
* receiver stays unknown and a call through it keeps its own text instead of
|
|
551
|
+
* borrowing a same-named member of whichever class it was written in.
|
|
552
|
+
*/
|
|
553
|
+
function classReceivers(body) {
|
|
554
|
+
const receivers = new Map();
|
|
555
|
+
const add = (name, annotation) => {
|
|
556
|
+
const type = bareClassType(annotation);
|
|
557
|
+
if (name && type)
|
|
558
|
+
receivers.set(name.text, type);
|
|
559
|
+
};
|
|
560
|
+
for (const member of namedChildren(body)) {
|
|
561
|
+
// A static member is not reachable through an instance `this`.
|
|
562
|
+
if (isStaticMember(member))
|
|
563
|
+
continue;
|
|
564
|
+
if (member.type === "public_field_definition") {
|
|
565
|
+
add(member.childForFieldName("name"), member.childForFieldName("type"));
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (member.type !== "method_definition")
|
|
569
|
+
continue;
|
|
570
|
+
if (childByType(member, "property_identifier")?.text !== "constructor") {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const params = childByType(member, "formal_parameters");
|
|
574
|
+
for (const param of params ? namedChildren(params) : []) {
|
|
575
|
+
if (!isParameterProperty(param))
|
|
576
|
+
continue;
|
|
577
|
+
add(param.childForFieldName("pattern"), param.childForFieldName("type"));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return receivers;
|
|
581
|
+
}
|
|
582
|
+
function handleClass(file, node, exported, functions, outer) {
|
|
583
|
+
const nameNode = childByType(node, "type_identifier") ?? childByType(node, "identifier");
|
|
584
|
+
const className = nameNode?.text ?? null;
|
|
585
|
+
if (!className)
|
|
586
|
+
return;
|
|
587
|
+
const body = childByType(node, "class_body");
|
|
588
|
+
if (!body)
|
|
589
|
+
return;
|
|
590
|
+
// Members resolve `this.name()`; the class body itself sees the outer scope.
|
|
591
|
+
const classEnv = {
|
|
592
|
+
scope: outer.scope,
|
|
593
|
+
className,
|
|
594
|
+
receivers: classReceivers(body),
|
|
595
|
+
};
|
|
596
|
+
// In a static member `this` is the class itself, not an instance, so its
|
|
597
|
+
// instance fields are not the receivers of `this.<field>` there.
|
|
598
|
+
const staticEnv = { ...classEnv, receivers: NO_RECEIVERS };
|
|
599
|
+
let decoratorStart;
|
|
600
|
+
for (const element of namedChildren(body)) {
|
|
601
|
+
if (element.type === "decorator") {
|
|
602
|
+
decoratorStart ??= element.startIndex;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (element.type === "comment")
|
|
606
|
+
continue;
|
|
607
|
+
const definitionStart = decoratorStart ?? element.startIndex;
|
|
608
|
+
decoratorStart = undefined;
|
|
609
|
+
if (element.type === "method_definition") {
|
|
610
|
+
const keyNode = childByType(element, "property_identifier") ??
|
|
611
|
+
childByType(element, "private_property_identifier") ??
|
|
612
|
+
childByType(element, "computed_property_name");
|
|
613
|
+
const methodName = keyNode?.text ?? null;
|
|
614
|
+
const isConstructor = methodName === "constructor";
|
|
615
|
+
if (!methodName)
|
|
616
|
+
continue;
|
|
617
|
+
const accessibility = childByType(element, "accessibility_modifier");
|
|
618
|
+
const methodExported = exported || accessibility?.text === "public";
|
|
619
|
+
const params = childByType(element, "formal_parameters");
|
|
620
|
+
const fnBody = childByType(element, "statement_block");
|
|
621
|
+
const key = isConstructor
|
|
622
|
+
? `${className}.constructor`
|
|
623
|
+
: `${className}.${methodName}`;
|
|
624
|
+
const label = isConstructor ? `new ${className}()` : key;
|
|
625
|
+
const env = functionEnv(element, isStaticMember(element) ? staticEnv : classEnv);
|
|
626
|
+
functions.push(functionFromParts(file, key, label, params, fnBody, methodExported, definitionStart, element.endIndex, env));
|
|
627
|
+
collectLocalDefinitions(file, fnBody, env, functions);
|
|
628
|
+
}
|
|
629
|
+
if (element.type === "public_field_definition") {
|
|
630
|
+
const keyNode = childByType(element, "property_identifier");
|
|
631
|
+
const value = childByType(element, "arrow_function") ??
|
|
632
|
+
childByType(element, "function_expression");
|
|
633
|
+
if (keyNode && value) {
|
|
634
|
+
handleFunctionNode(file, value, keyNode.text, exported, isStaticMember(element) ? staticEnv : classEnv, functions);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function visitStatement(file, node, exported, functions, env) {
|
|
640
|
+
if (node.type === "export_statement") {
|
|
641
|
+
// Decorators can precede the declaration inside an export statement.
|
|
642
|
+
// Select grammar fields, not the first named child (which may be metadata).
|
|
643
|
+
const found = node.childForFieldName("declaration") ??
|
|
644
|
+
node.childForFieldName("value");
|
|
645
|
+
if (!found)
|
|
646
|
+
return;
|
|
647
|
+
const decl = stripTypeWrappers(found);
|
|
648
|
+
const isDefault = node.children.some((child) => child.type === "default");
|
|
649
|
+
if (decl.type === "function_declaration" ||
|
|
650
|
+
decl.type === "function_expression" ||
|
|
651
|
+
decl.type === "generator_function_declaration" ||
|
|
652
|
+
decl.type === "generator_function") {
|
|
653
|
+
const id = childByType(decl, "identifier");
|
|
654
|
+
const name = id?.text ?? (isDefault ? "default" : null);
|
|
655
|
+
handleFunctionNode(file, decl, name, true, env, functions);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (decl.type === "arrow_function") {
|
|
659
|
+
handleFunctionNode(file, decl, isDefault ? "default" : null, true, env, functions);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (decl.type === "class_declaration" ||
|
|
663
|
+
decl.type === "abstract_class_declaration" ||
|
|
664
|
+
decl.type === "class") {
|
|
665
|
+
handleClass(file, decl, true, functions, env);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (decl.type === "call_expression") {
|
|
669
|
+
unwrapWrappedFunction(file, decl, isDefault ? fileStem(file) : null, true, functions, env);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (decl.type === "lexical_declaration" ||
|
|
673
|
+
decl.type === "variable_declaration") {
|
|
674
|
+
visitStatement(file, decl, true, functions, env);
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (node.type === "function_declaration" ||
|
|
679
|
+
node.type === "generator_function_declaration") {
|
|
680
|
+
const id = childByType(node, "identifier");
|
|
681
|
+
handleFunctionNode(file, node, id?.text ?? null, exported, env, functions);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (node.type === "class_declaration" ||
|
|
685
|
+
node.type === "abstract_class_declaration") {
|
|
686
|
+
handleClass(file, node, exported, functions, env);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (node.type === "lexical_declaration" ||
|
|
690
|
+
node.type === "variable_declaration") {
|
|
691
|
+
for (const d of namedChildren(node)) {
|
|
692
|
+
if (d.type !== "variable_declarator")
|
|
693
|
+
continue;
|
|
694
|
+
extractDeclaratorFunction(file, d, exported, functions, env);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function extractFromTree(file, _source, tree) {
|
|
699
|
+
const functions = [];
|
|
700
|
+
const env = moduleEnv(tree.rootNode);
|
|
701
|
+
for (const stmt of namedChildren(tree.rootNode)) {
|
|
702
|
+
visitStatement(file, stmt, false, functions, env);
|
|
703
|
+
}
|
|
704
|
+
extractTypeContracts(file, tree, functions);
|
|
705
|
+
extractDispatchContext(file, tree, functions);
|
|
706
|
+
return functions;
|
|
707
|
+
}
|
|
708
|
+
export const typescriptExtractor = {
|
|
709
|
+
id: "typescript",
|
|
710
|
+
extensions: [".ts", ".mts", ".cts"],
|
|
711
|
+
grammarPackage: "tree-sitter-typescript",
|
|
712
|
+
grammarExport: "typescript",
|
|
713
|
+
extract: extractFromTree,
|
|
714
|
+
};
|
|
715
|
+
/** TSX (.tsx) — separate tree-sitter-typescript grammar export from plain TS. */
|
|
716
|
+
export const typescriptreactExtractor = {
|
|
717
|
+
id: "typescriptreact",
|
|
718
|
+
extensions: [".tsx"],
|
|
719
|
+
grammarPackage: "tree-sitter-typescript",
|
|
720
|
+
grammarExport: "tsx",
|
|
721
|
+
extract: extractFromTree,
|
|
722
|
+
};
|