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,648 @@
|
|
|
1
|
+
import { childByType, collapseWs, locFromNode, namedChildren, } from "./types.js";
|
|
2
|
+
import { jsCallSyntax, jsFunctionScope, jsModuleScope, jsParameterList, } from "./call-syntax.js";
|
|
3
|
+
/** Module-level scope of the file being extracted, plus an empty class. */
|
|
4
|
+
function moduleEnv(root) {
|
|
5
|
+
return { scope: jsModuleScope(root), className: null };
|
|
6
|
+
}
|
|
7
|
+
function isFnLike(type) {
|
|
8
|
+
return (type === "function_declaration" ||
|
|
9
|
+
type === "function_expression" ||
|
|
10
|
+
type === "arrow_function" ||
|
|
11
|
+
type === "generator_function" ||
|
|
12
|
+
type === "generator_function_declaration" ||
|
|
13
|
+
type === "method_definition");
|
|
14
|
+
}
|
|
15
|
+
/** Nested function/arrow, not a method — skip as a sibling, maybe nest under a call. */
|
|
16
|
+
function isCallback(type) {
|
|
17
|
+
return isFnLike(type) && type !== "method_definition";
|
|
18
|
+
}
|
|
19
|
+
function getParamsLabel(params) {
|
|
20
|
+
if (!params)
|
|
21
|
+
return "()";
|
|
22
|
+
// Single-param arrow: `x => ...` — params node may be the identifier itself
|
|
23
|
+
if (params.type === "identifier")
|
|
24
|
+
return `(${params.text})`;
|
|
25
|
+
if (params.type !== "formal_parameters")
|
|
26
|
+
return "()";
|
|
27
|
+
const parts = [];
|
|
28
|
+
for (const p of namedChildren(params)) {
|
|
29
|
+
if (p.type === "identifier") {
|
|
30
|
+
parts.push(p.text);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (p.type === "rest_pattern" || p.type === "rest_parameter") {
|
|
34
|
+
const id = childByType(p, "identifier");
|
|
35
|
+
parts.push(id ? `...${id.text}` : "...");
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (p.type === "assignment_pattern") {
|
|
39
|
+
const id = childByType(p, "identifier");
|
|
40
|
+
parts.push(id?.text ?? "_");
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (p.type === "object_pattern") {
|
|
44
|
+
parts.push("{}");
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (p.type === "array_pattern") {
|
|
48
|
+
parts.push("[]");
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// TS-style wrappers if JSX/mixed grammars ever surface them
|
|
52
|
+
if (p.type === "required_parameter" || p.type === "optional_parameter") {
|
|
53
|
+
const id = childByType(p, "identifier");
|
|
54
|
+
parts.push(id?.text ?? "_");
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
parts.push("_");
|
|
58
|
+
}
|
|
59
|
+
return parts.length === 0 ? "()" : `(${parts.join(", ")})`;
|
|
60
|
+
}
|
|
61
|
+
function condText(test) {
|
|
62
|
+
if (test.type === "parenthesized_expression") {
|
|
63
|
+
const inner = test.namedChild(0);
|
|
64
|
+
if (inner)
|
|
65
|
+
return collapseWs(inner.text);
|
|
66
|
+
}
|
|
67
|
+
return collapseWs(test.text);
|
|
68
|
+
}
|
|
69
|
+
function branchKey(kind, cond) {
|
|
70
|
+
if (kind === "else")
|
|
71
|
+
return "else";
|
|
72
|
+
return `${kind}:${cond}`;
|
|
73
|
+
}
|
|
74
|
+
function calleeKey(node, className) {
|
|
75
|
+
if (node.type === "identifier")
|
|
76
|
+
return node.text;
|
|
77
|
+
if (node.type === "this")
|
|
78
|
+
return className;
|
|
79
|
+
if (node.type === "member_expression") {
|
|
80
|
+
const object = node.namedChild(0);
|
|
81
|
+
const property = namedChildren(node).find((c) => c.type === "property_identifier" ||
|
|
82
|
+
c.type === "private_property_identifier") ?? null;
|
|
83
|
+
if (!object || !property)
|
|
84
|
+
return null;
|
|
85
|
+
const propName = property.text;
|
|
86
|
+
if (object.type === "this" && className) {
|
|
87
|
+
return `${className}.${propName}`;
|
|
88
|
+
}
|
|
89
|
+
if (object.type === "identifier") {
|
|
90
|
+
return `${object.text}.${propName}`;
|
|
91
|
+
}
|
|
92
|
+
if (className)
|
|
93
|
+
return `${className}.${propName}`;
|
|
94
|
+
return propName;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Treat JSX tags as component "calls". PascalCase identifiers and any
|
|
100
|
+
* member expression (`Foo.Bar`, `motion.div`) count; lowercase tags are HTML.
|
|
101
|
+
*/
|
|
102
|
+
function jsxCalleeKey(node) {
|
|
103
|
+
for (const child of namedChildren(node)) {
|
|
104
|
+
if (child.type === "identifier") {
|
|
105
|
+
const name = child.text;
|
|
106
|
+
return /^[A-Z]/.test(name) ? name : null;
|
|
107
|
+
}
|
|
108
|
+
if (child.type === "member_expression") {
|
|
109
|
+
return calleeKey(child, null);
|
|
110
|
+
}
|
|
111
|
+
if (child.type === "jsx_attribute" ||
|
|
112
|
+
child.type === "jsx_expression" ||
|
|
113
|
+
child.type === "type_arguments") {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function statementsOf(node) {
|
|
120
|
+
if (node.type === "statement_block")
|
|
121
|
+
return namedChildren(node);
|
|
122
|
+
return [node];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Steps of one statement list. `env` carries the lexical scope in effect
|
|
126
|
+
* (module, function, or method), which is what classifies call targets.
|
|
127
|
+
*/
|
|
128
|
+
function collectStatements(file, statements, env) {
|
|
129
|
+
const steps = [];
|
|
130
|
+
const seenCalls = new Set();
|
|
131
|
+
const addCall = (key, node, syntax) => {
|
|
132
|
+
const mark = `${key}:${node.startIndex}`;
|
|
133
|
+
if (seenCalls.has(mark))
|
|
134
|
+
return;
|
|
135
|
+
seenCalls.add(mark);
|
|
136
|
+
const step = { type: "call", key, ...locFromNode(file, node) };
|
|
137
|
+
if (syntax)
|
|
138
|
+
step.syntax = syntax;
|
|
139
|
+
steps.push(step);
|
|
140
|
+
};
|
|
141
|
+
const emitCall = (key, node, nested, syntax) => {
|
|
142
|
+
if (key && nested.length > 0) {
|
|
143
|
+
const step = {
|
|
144
|
+
type: "call",
|
|
145
|
+
key,
|
|
146
|
+
...locFromNode(file, node),
|
|
147
|
+
children: nested,
|
|
148
|
+
};
|
|
149
|
+
if (syntax)
|
|
150
|
+
step.syntax = syntax;
|
|
151
|
+
steps.push(step);
|
|
152
|
+
}
|
|
153
|
+
else if (key) {
|
|
154
|
+
addCall(key, node, syntax);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
steps.push(...nested);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const walkExpr = (node) => {
|
|
161
|
+
const type = node.type;
|
|
162
|
+
if (isCallback(type)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (type === "if_statement") {
|
|
166
|
+
const test = childByType(node, "parenthesized_expression") ??
|
|
167
|
+
namedChildren(node).find((c) => c.type !== "else_clause") ??
|
|
168
|
+
null;
|
|
169
|
+
const kids = namedChildren(node);
|
|
170
|
+
const consequent = kids.find((c) => c.type !== "parenthesized_expression" && c.type !== "else_clause") ?? null;
|
|
171
|
+
const elseClause = childByType(node, "else_clause");
|
|
172
|
+
const cond = test ? condText(test) : "";
|
|
173
|
+
steps.push({
|
|
174
|
+
type: "branch",
|
|
175
|
+
key: branchKey("if", cond),
|
|
176
|
+
label: test ? `if (${condText(test)})` : "if",
|
|
177
|
+
...locFromNode(file, test ?? node),
|
|
178
|
+
children: consequent
|
|
179
|
+
? collectStatements(file, statementsOf(consequent), env)
|
|
180
|
+
: [],
|
|
181
|
+
});
|
|
182
|
+
let current = elseClause;
|
|
183
|
+
while (current) {
|
|
184
|
+
const inner = current.namedChild(0);
|
|
185
|
+
if (!inner)
|
|
186
|
+
break;
|
|
187
|
+
if (inner.type === "if_statement") {
|
|
188
|
+
const elseTest = childByType(inner, "parenthesized_expression") ?? null;
|
|
189
|
+
const elseKids = namedChildren(inner);
|
|
190
|
+
const elseConsequent = elseKids.find((c) => c.type !== "parenthesized_expression" &&
|
|
191
|
+
c.type !== "else_clause") ?? null;
|
|
192
|
+
const elseCond = elseTest ? condText(elseTest) : "";
|
|
193
|
+
steps.push({
|
|
194
|
+
type: "branch",
|
|
195
|
+
key: branchKey("else-if", elseCond),
|
|
196
|
+
label: elseTest ? `else if (${condText(elseTest)})` : "else if",
|
|
197
|
+
...locFromNode(file, elseTest ?? current),
|
|
198
|
+
children: elseConsequent
|
|
199
|
+
? collectStatements(file, statementsOf(elseConsequent), env)
|
|
200
|
+
: [],
|
|
201
|
+
});
|
|
202
|
+
current = childByType(inner, "else_clause");
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
steps.push({
|
|
206
|
+
type: "branch",
|
|
207
|
+
key: branchKey("else", ""),
|
|
208
|
+
label: "else",
|
|
209
|
+
...locFromNode(file, current),
|
|
210
|
+
children: collectStatements(file, statementsOf(inner), env),
|
|
211
|
+
});
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (type === "try_statement") {
|
|
217
|
+
const tryBlock = childByType(node, "statement_block");
|
|
218
|
+
steps.push({
|
|
219
|
+
type: "branch",
|
|
220
|
+
key: "try",
|
|
221
|
+
label: "try",
|
|
222
|
+
...locFromNode(file, node),
|
|
223
|
+
children: tryBlock
|
|
224
|
+
? collectStatements(file, statementsOf(tryBlock), env)
|
|
225
|
+
: [],
|
|
226
|
+
});
|
|
227
|
+
for (const clause of namedChildren(node)) {
|
|
228
|
+
if (clause.type === "catch_clause") {
|
|
229
|
+
const param = childByType(clause, "identifier") ??
|
|
230
|
+
namedChildren(clause).find((c) => c.type !== "statement_block") ??
|
|
231
|
+
null;
|
|
232
|
+
const text = param ? collapseWs(param.text) : "";
|
|
233
|
+
const block = childByType(clause, "statement_block");
|
|
234
|
+
steps.push({
|
|
235
|
+
type: "branch",
|
|
236
|
+
key: text ? `catch:${text}` : "catch",
|
|
237
|
+
label: text ? `catch (${text})` : "catch",
|
|
238
|
+
...locFromNode(file, param ?? clause),
|
|
239
|
+
children: block
|
|
240
|
+
? collectStatements(file, statementsOf(block), env)
|
|
241
|
+
: [],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (clause.type === "finally_clause") {
|
|
245
|
+
const block = childByType(clause, "statement_block");
|
|
246
|
+
steps.push({
|
|
247
|
+
type: "branch",
|
|
248
|
+
key: "finally",
|
|
249
|
+
label: "finally",
|
|
250
|
+
...locFromNode(file, clause),
|
|
251
|
+
children: block
|
|
252
|
+
? collectStatements(file, statementsOf(block), env)
|
|
253
|
+
: [],
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (type === "switch_statement") {
|
|
260
|
+
const body = childByType(node, "switch_body");
|
|
261
|
+
for (const clause of body ? namedChildren(body) : []) {
|
|
262
|
+
if (clause.type === "switch_case") {
|
|
263
|
+
const value = namedChildren(clause).find((c) => c.type !== "statement_block" &&
|
|
264
|
+
c.type !== "break_statement" &&
|
|
265
|
+
c.type !== "expression_statement" &&
|
|
266
|
+
c.type !== "return_statement" &&
|
|
267
|
+
c.type !== "throw_statement") ?? null;
|
|
268
|
+
// Prefer the first non-statement child as the case value
|
|
269
|
+
const kids = namedChildren(clause);
|
|
270
|
+
const caseValue = kids.find((c) => ![
|
|
271
|
+
"expression_statement",
|
|
272
|
+
"break_statement",
|
|
273
|
+
"return_statement",
|
|
274
|
+
"throw_statement",
|
|
275
|
+
"statement_block",
|
|
276
|
+
"lexical_declaration",
|
|
277
|
+
"variable_declaration",
|
|
278
|
+
"empty_statement",
|
|
279
|
+
].includes(c.type)) ?? value;
|
|
280
|
+
const text = caseValue ? collapseWs(caseValue.text) : "";
|
|
281
|
+
const stmts = kids.filter((c) => c.type === "expression_statement" ||
|
|
282
|
+
c.type === "return_statement" ||
|
|
283
|
+
c.type === "throw_statement" ||
|
|
284
|
+
c.type === "statement_block" ||
|
|
285
|
+
c.type === "lexical_declaration" ||
|
|
286
|
+
c.type === "variable_declaration");
|
|
287
|
+
steps.push({
|
|
288
|
+
type: "branch",
|
|
289
|
+
key: text ? `case:${text}` : "case",
|
|
290
|
+
label: text ? `case ${text}` : "case",
|
|
291
|
+
...locFromNode(file, caseValue ?? clause),
|
|
292
|
+
children: collectStatements(file, stmts, env),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (clause.type === "switch_default") {
|
|
296
|
+
const stmts = namedChildren(clause).filter((c) => c.type !== "break_statement");
|
|
297
|
+
steps.push({
|
|
298
|
+
type: "branch",
|
|
299
|
+
key: "default",
|
|
300
|
+
label: "default",
|
|
301
|
+
...locFromNode(file, clause),
|
|
302
|
+
children: collectStatements(file, stmts, env),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (type === "call_expression" || type === "new_expression") {
|
|
309
|
+
const isNew = type === "new_expression";
|
|
310
|
+
const callee = node.namedChild(0);
|
|
311
|
+
const bare = callee ? calleeKey(callee, isNew ? null : env.className) : null;
|
|
312
|
+
const args = childByType(node, "arguments");
|
|
313
|
+
const syntax = callee ? jsCallSyntax(callee, args, env) : undefined;
|
|
314
|
+
// A dynamic target has no key from `calleeKey`; the call is still a call,
|
|
315
|
+
// so the target expression itself becomes its identity.
|
|
316
|
+
const base = bare ?? (syntax ? syntax.callee : null);
|
|
317
|
+
const key = base && isNew && !base.startsWith("new ") ? `new ${base}` : base;
|
|
318
|
+
emitCall(key, node, args ? stepsFromArguments(file, args, env) : [], syntax);
|
|
319
|
+
// `foo(x).bar()` keeps `foo` — the receiver is not an argument.
|
|
320
|
+
if (callee)
|
|
321
|
+
walkExpr(callee);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (type === "jsx_element") {
|
|
325
|
+
const opening = childByType(node, "jsx_opening_element");
|
|
326
|
+
const childNodes = namedChildren(node).filter((c) => c.type !== "jsx_opening_element" && c.type !== "jsx_closing_element");
|
|
327
|
+
const fromAttrs = [];
|
|
328
|
+
if (opening) {
|
|
329
|
+
for (const attr of namedChildren(opening)) {
|
|
330
|
+
if (attr.type === "jsx_attribute" ||
|
|
331
|
+
attr.type === "jsx_expression") {
|
|
332
|
+
fromAttrs.push(...collectStatements(file, [attr], env));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const nested = [
|
|
337
|
+
...fromAttrs,
|
|
338
|
+
...collectStatements(file, childNodes, env),
|
|
339
|
+
];
|
|
340
|
+
emitCall(opening ? jsxCalleeKey(opening) : null, opening ?? node, nested);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (type === "jsx_self_closing_element") {
|
|
344
|
+
const attrNodes = namedChildren(node).filter((c) => c.type === "jsx_attribute" || c.type === "jsx_expression");
|
|
345
|
+
emitCall(jsxCalleeKey(node), node, collectStatements(file, attrNodes, env));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
for (const child of namedChildren(node)) {
|
|
349
|
+
walkExpr(child);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
for (const stmt of statements) {
|
|
353
|
+
walkExpr(stmt);
|
|
354
|
+
}
|
|
355
|
+
return steps;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Calls and callback bodies inside `(...)`, as children of the receiving call.
|
|
359
|
+
* Skip a callback already registered as its own definition (`const x = wrap(fn)`).
|
|
360
|
+
*/
|
|
361
|
+
function stepsFromArguments(file, args, env) {
|
|
362
|
+
const skipCallbacks = args.parent ? hoistsCallback(args.parent) : false;
|
|
363
|
+
const steps = [];
|
|
364
|
+
for (const rawArg of namedChildren(args)) {
|
|
365
|
+
const arg = stripParens(rawArg);
|
|
366
|
+
if (isCallback(arg.type)) {
|
|
367
|
+
if (skipCallbacks)
|
|
368
|
+
continue;
|
|
369
|
+
steps.push(...collectStepsFromBody(file, unwrapCurriedBody(bodyOf(arg)), env));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
steps.push(...collectStatements(file, [arg], env));
|
|
373
|
+
}
|
|
374
|
+
return steps;
|
|
375
|
+
}
|
|
376
|
+
/** True when this call's callback is extracted as a named definition. */
|
|
377
|
+
function hoistsCallback(call) {
|
|
378
|
+
let current = call.parent;
|
|
379
|
+
while (current && current.type === "parenthesized_expression") {
|
|
380
|
+
current = current.parent;
|
|
381
|
+
}
|
|
382
|
+
if (!current)
|
|
383
|
+
return false;
|
|
384
|
+
if (current.type === "variable_declarator" ||
|
|
385
|
+
current.type === "export_statement") {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
if (current.type === "arguments") {
|
|
389
|
+
const outer = current.parent;
|
|
390
|
+
return outer?.type === "call_expression" ? hoistsCallback(outer) : false;
|
|
391
|
+
}
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
function collectStepsFromBody(file, body, env) {
|
|
395
|
+
if (!body)
|
|
396
|
+
return [];
|
|
397
|
+
if (body.type === "statement_block") {
|
|
398
|
+
return collectStatements(file, namedChildren(body), env);
|
|
399
|
+
}
|
|
400
|
+
return collectStatements(file, [body], env);
|
|
401
|
+
}
|
|
402
|
+
function functionFromParts(file, key, label, params, body, exported, start, end, env) {
|
|
403
|
+
const declared = jsParameterList(params);
|
|
404
|
+
const info = {
|
|
405
|
+
key,
|
|
406
|
+
label: `${label}${getParamsLabel(params)}`,
|
|
407
|
+
file,
|
|
408
|
+
steps: collectStepsFromBody(file, body, env),
|
|
409
|
+
exported,
|
|
410
|
+
start,
|
|
411
|
+
end,
|
|
412
|
+
};
|
|
413
|
+
if (declared)
|
|
414
|
+
info.params = declared;
|
|
415
|
+
return info;
|
|
416
|
+
}
|
|
417
|
+
function paramsOf(node) {
|
|
418
|
+
return (childByType(node, "formal_parameters") ??
|
|
419
|
+
// single-param arrow without parens
|
|
420
|
+
(node.type === "arrow_function"
|
|
421
|
+
? (namedChildren(node).find((c) => c.type === "identifier") ?? null)
|
|
422
|
+
: null));
|
|
423
|
+
}
|
|
424
|
+
/** Named children of a function node that are never its runtime body. */
|
|
425
|
+
const NON_BODY = new Set([
|
|
426
|
+
"comment",
|
|
427
|
+
"formal_parameters",
|
|
428
|
+
"identifier",
|
|
429
|
+
"property_identifier",
|
|
430
|
+
"private_property_identifier",
|
|
431
|
+
"decorator",
|
|
432
|
+
]);
|
|
433
|
+
function bodyOf(node) {
|
|
434
|
+
return (childByType(node, "statement_block") ??
|
|
435
|
+
namedChildren(node).find((c) => !NON_BODY.has(c.type)) ??
|
|
436
|
+
null);
|
|
437
|
+
}
|
|
438
|
+
/** Peel `(fn)` so the node underneath can be matched on its own terms. */
|
|
439
|
+
function stripParens(node) {
|
|
440
|
+
let current = node;
|
|
441
|
+
while (current.type === "parenthesized_expression") {
|
|
442
|
+
const inner = current.namedChild(0);
|
|
443
|
+
if (!inner)
|
|
444
|
+
return current;
|
|
445
|
+
current = inner;
|
|
446
|
+
}
|
|
447
|
+
return current;
|
|
448
|
+
}
|
|
449
|
+
/** Peel `(a) => (b) => body` to `body`. A returned factory (`return () => tick()`) stays out. */
|
|
450
|
+
function unwrapCurriedBody(body) {
|
|
451
|
+
let current = body ? stripParens(body) : null;
|
|
452
|
+
while (current &&
|
|
453
|
+
(current.type === "arrow_function" ||
|
|
454
|
+
current.type === "function_expression" ||
|
|
455
|
+
current.type === "generator_function")) {
|
|
456
|
+
const inner = bodyOf(current);
|
|
457
|
+
if (!inner)
|
|
458
|
+
return current;
|
|
459
|
+
current = stripParens(inner);
|
|
460
|
+
}
|
|
461
|
+
return current;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Register definitions declared inside a function body.
|
|
465
|
+
*
|
|
466
|
+
* `visitStatement` only walks top-level statements, so a helper declared inside
|
|
467
|
+
* a body was never indexed at all, and calls to it fell through to whatever
|
|
468
|
+
* top-level function elsewhere in the repo happened to share the bare name.
|
|
469
|
+
* See #19.
|
|
470
|
+
*
|
|
471
|
+
* Helper bodies are still not attributed to the outer caller (contract #5);
|
|
472
|
+
* they become definitions in their own right, marked `local`.
|
|
473
|
+
*/
|
|
474
|
+
function collectLocalDefinitions(file, body, env, functions) {
|
|
475
|
+
if (!body)
|
|
476
|
+
return;
|
|
477
|
+
const walk = (node) => {
|
|
478
|
+
for (const child of namedChildren(node)) {
|
|
479
|
+
if (child.type === "function_declaration" ||
|
|
480
|
+
child.type === "generator_function_declaration") {
|
|
481
|
+
const id = childByType(child, "identifier");
|
|
482
|
+
handleFunctionNode(file, child, id?.text ?? null, false, env, functions, true);
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (child.type === "lexical_declaration" ||
|
|
486
|
+
child.type === "variable_declaration") {
|
|
487
|
+
for (const d of namedChildren(child)) {
|
|
488
|
+
if (d.type !== "variable_declarator")
|
|
489
|
+
continue;
|
|
490
|
+
const id = childByType(d, "identifier");
|
|
491
|
+
const init = childByType(d, "arrow_function") ??
|
|
492
|
+
childByType(d, "function_expression");
|
|
493
|
+
if (id && init) {
|
|
494
|
+
handleFunctionNode(file, init, id.text, false, env, functions, true);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Fall through: `walk` skips the initializer bodies as fn-like below,
|
|
498
|
+
// so a declaration list is never registered twice.
|
|
499
|
+
}
|
|
500
|
+
// Anonymous callbacks are not addressable by name; skip their bodies.
|
|
501
|
+
if (isFnLike(child.type))
|
|
502
|
+
continue;
|
|
503
|
+
walk(child);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
walk(body);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Scope of one function body. The enclosing class identity is inherited, so a
|
|
510
|
+
* local closure still has its method's `this` bindings.
|
|
511
|
+
*/
|
|
512
|
+
function functionEnv(node, outer) {
|
|
513
|
+
return {
|
|
514
|
+
scope: jsFunctionScope(node, outer.scope),
|
|
515
|
+
className: outer.className,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function handleFunctionNode(file, node, name, exported, outer, functions,
|
|
519
|
+
/** Declared inside another body: key stays bare and resolution is file-scoped. */
|
|
520
|
+
local = false) {
|
|
521
|
+
if (!name)
|
|
522
|
+
return;
|
|
523
|
+
const key = outer.className && !local ? `${outer.className}.${name}` : name;
|
|
524
|
+
const body = unwrapCurriedBody(bodyOf(node));
|
|
525
|
+
const env = functionEnv(node, outer);
|
|
526
|
+
const info = functionFromParts(file, key, key, paramsOf(node), body, exported, node.startIndex, node.endIndex, env);
|
|
527
|
+
functions.push(local ? { ...info, local: true } : info);
|
|
528
|
+
collectLocalDefinitions(file, body, env, functions);
|
|
529
|
+
}
|
|
530
|
+
function handleClass(file, node, exported, functions, outer) {
|
|
531
|
+
const nameNode = childByType(node, "identifier");
|
|
532
|
+
const className = nameNode?.text ?? null;
|
|
533
|
+
if (!className)
|
|
534
|
+
return;
|
|
535
|
+
const body = childByType(node, "class_body");
|
|
536
|
+
if (!body)
|
|
537
|
+
return;
|
|
538
|
+
// Members resolve `this.name()`; the class body itself sees the outer scope.
|
|
539
|
+
const classEnv = {
|
|
540
|
+
scope: outer.scope,
|
|
541
|
+
className,
|
|
542
|
+
};
|
|
543
|
+
for (const element of namedChildren(body)) {
|
|
544
|
+
if (element.type === "method_definition") {
|
|
545
|
+
const keyNode = childByType(element, "property_identifier") ??
|
|
546
|
+
childByType(element, "private_property_identifier") ??
|
|
547
|
+
childByType(element, "computed_property_name");
|
|
548
|
+
const methodName = keyNode?.text ?? null;
|
|
549
|
+
if (!methodName)
|
|
550
|
+
continue;
|
|
551
|
+
// Skip computed names
|
|
552
|
+
if (keyNode?.type === "computed_property_name")
|
|
553
|
+
continue;
|
|
554
|
+
const isConstructor = methodName === "constructor";
|
|
555
|
+
const params = childByType(element, "formal_parameters");
|
|
556
|
+
const fnBody = childByType(element, "statement_block");
|
|
557
|
+
const key = isConstructor
|
|
558
|
+
? `${className}.constructor`
|
|
559
|
+
: `${className}.${methodName}`;
|
|
560
|
+
const label = isConstructor ? `new ${className}()` : key;
|
|
561
|
+
const env = functionEnv(element, classEnv);
|
|
562
|
+
functions.push(functionFromParts(file, key, label, params, fnBody, exported, element.startIndex, element.endIndex, env));
|
|
563
|
+
collectLocalDefinitions(file, fnBody, env, functions);
|
|
564
|
+
}
|
|
565
|
+
if (element.type === "field_definition") {
|
|
566
|
+
const keyNode = childByType(element, "property_identifier");
|
|
567
|
+
const value = childByType(element, "arrow_function") ??
|
|
568
|
+
childByType(element, "function_expression");
|
|
569
|
+
if (keyNode && value) {
|
|
570
|
+
handleFunctionNode(file, value, keyNode.text, exported, classEnv, functions);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
function visitStatement(file, node, exported, functions, env) {
|
|
576
|
+
if (node.type === "export_statement") {
|
|
577
|
+
const decl = namedChildren(node).find((c) => c.type !== "export_clause") ?? null;
|
|
578
|
+
if (!decl)
|
|
579
|
+
return;
|
|
580
|
+
const isDefault = node.text.startsWith("export default");
|
|
581
|
+
if (decl.type === "function_declaration" ||
|
|
582
|
+
decl.type === "function_expression" ||
|
|
583
|
+
decl.type === "generator_function_declaration" ||
|
|
584
|
+
decl.type === "generator_function") {
|
|
585
|
+
const id = childByType(decl, "identifier");
|
|
586
|
+
const name = id?.text ?? (isDefault ? "default" : null);
|
|
587
|
+
handleFunctionNode(file, decl, name, true, env, functions);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (decl.type === "arrow_function") {
|
|
591
|
+
handleFunctionNode(file, decl, isDefault ? "default" : null, true, env, functions);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (decl.type === "class_declaration" || decl.type === "class") {
|
|
595
|
+
handleClass(file, decl, true, functions, env);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (decl.type === "lexical_declaration" ||
|
|
599
|
+
decl.type === "variable_declaration") {
|
|
600
|
+
visitStatement(file, decl, true, functions, env);
|
|
601
|
+
}
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (node.type === "function_declaration" ||
|
|
605
|
+
node.type === "generator_function_declaration") {
|
|
606
|
+
const id = childByType(node, "identifier");
|
|
607
|
+
handleFunctionNode(file, node, id?.text ?? null, exported, env, functions);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (node.type === "class_declaration") {
|
|
611
|
+
handleClass(file, node, exported, functions, env);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (node.type === "lexical_declaration" ||
|
|
615
|
+
node.type === "variable_declaration") {
|
|
616
|
+
for (const d of namedChildren(node)) {
|
|
617
|
+
if (d.type !== "variable_declarator")
|
|
618
|
+
continue;
|
|
619
|
+
const id = childByType(d, "identifier");
|
|
620
|
+
const init = childByType(d, "arrow_function") ??
|
|
621
|
+
childByType(d, "function_expression");
|
|
622
|
+
if (id && init) {
|
|
623
|
+
handleFunctionNode(file, init, id.text, exported, env, functions);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function extractFromTree(file, _source, tree) {
|
|
629
|
+
const functions = [];
|
|
630
|
+
const env = moduleEnv(tree.rootNode);
|
|
631
|
+
for (const stmt of namedChildren(tree.rootNode)) {
|
|
632
|
+
visitStatement(file, stmt, false, functions, env);
|
|
633
|
+
}
|
|
634
|
+
return functions;
|
|
635
|
+
}
|
|
636
|
+
export const javascriptExtractor = {
|
|
637
|
+
id: "javascript",
|
|
638
|
+
extensions: [".js", ".mjs", ".cjs"],
|
|
639
|
+
grammarPackage: "tree-sitter-javascript",
|
|
640
|
+
extract: extractFromTree,
|
|
641
|
+
};
|
|
642
|
+
/** JSX (.jsx) — same grammar as JS; tree-sitter-javascript parses both. */
|
|
643
|
+
export const javascriptreactExtractor = {
|
|
644
|
+
id: "javascriptreact",
|
|
645
|
+
extensions: [".jsx"],
|
|
646
|
+
grammarPackage: "tree-sitter-javascript",
|
|
647
|
+
extract: extractFromTree,
|
|
648
|
+
};
|