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,391 @@
|
|
|
1
|
+
import { heuristicCallSyntax } from "./call-syntax.js";
|
|
2
|
+
import { childByType, declaredParamsOf, collapseWs, locFromNode, namedChildren, } from "./types.js";
|
|
3
|
+
function getParamsLabel(params) {
|
|
4
|
+
if (!params || params.type !== "method_parameters")
|
|
5
|
+
return "()";
|
|
6
|
+
const names = [];
|
|
7
|
+
for (const p of namedChildren(params)) {
|
|
8
|
+
if (p.type === "identifier" || p.type === "optional_parameter") {
|
|
9
|
+
const id = p.type === "identifier" ? p : childByType(p, "identifier");
|
|
10
|
+
names.push(id?.text ?? "_");
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (p.type === "splat_parameter" ||
|
|
14
|
+
p.type === "hash_splat_parameter" ||
|
|
15
|
+
p.type === "block_parameter") {
|
|
16
|
+
const id = childByType(p, "identifier");
|
|
17
|
+
const prefix = p.type === "hash_splat_parameter"
|
|
18
|
+
? "**"
|
|
19
|
+
: p.type === "block_parameter"
|
|
20
|
+
? "&"
|
|
21
|
+
: "*";
|
|
22
|
+
names.push(id ? `${prefix}${id.text}` : prefix);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (p.type === "keyword_parameter") {
|
|
26
|
+
const id = childByType(p, "identifier");
|
|
27
|
+
names.push(id ? `${id.text}:` : "_:");
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
names.push("_");
|
|
31
|
+
}
|
|
32
|
+
return names.length === 0 ? "()" : `(${names.join(", ")})`;
|
|
33
|
+
}
|
|
34
|
+
function callKey(node, className) {
|
|
35
|
+
// call → [receiver,] identifier/constant, [argument_list|!]
|
|
36
|
+
const kids = namedChildren(node).filter((c) => c.type !== "argument_list" &&
|
|
37
|
+
c.type !== "block" &&
|
|
38
|
+
c.type !== "do_block");
|
|
39
|
+
if (kids.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
if (kids.length === 1) {
|
|
42
|
+
const only = kids[0];
|
|
43
|
+
if (only.type === "identifier") {
|
|
44
|
+
return className ? `${className}.${only.text}` : only.text;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const method = kids[kids.length - 1];
|
|
49
|
+
const receiver = kids[0];
|
|
50
|
+
if (method.type !== "identifier")
|
|
51
|
+
return null;
|
|
52
|
+
const prop = method.text;
|
|
53
|
+
if (receiver.type === "self" && className) {
|
|
54
|
+
return `${className}.${prop}`;
|
|
55
|
+
}
|
|
56
|
+
if (receiver.type === "constant") {
|
|
57
|
+
if (prop === "new")
|
|
58
|
+
return `new ${receiver.text}`;
|
|
59
|
+
return `${receiver.text}.${prop}`;
|
|
60
|
+
}
|
|
61
|
+
if (receiver.type === "identifier") {
|
|
62
|
+
return `${receiver.text}.${prop}`;
|
|
63
|
+
}
|
|
64
|
+
if (className)
|
|
65
|
+
return `${className}.${prop}`;
|
|
66
|
+
return prop;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Bare identifier used as a call (Ruby paren-less style).
|
|
70
|
+
* Only treat as call when it appears in statement/expression call position.
|
|
71
|
+
*/
|
|
72
|
+
function bareCallKey(node, className) {
|
|
73
|
+
if (node.type !== "identifier")
|
|
74
|
+
return null;
|
|
75
|
+
const name = node.text;
|
|
76
|
+
// Skip assignment targets handled elsewhere; keywords-ish
|
|
77
|
+
if (name === "self" || name === "super") {
|
|
78
|
+
return className && name === "super" ? className : null;
|
|
79
|
+
}
|
|
80
|
+
return className ? `${className}.${name}` : name;
|
|
81
|
+
}
|
|
82
|
+
function collectBody(file, body, className) {
|
|
83
|
+
if (!body)
|
|
84
|
+
return [];
|
|
85
|
+
if (body.type === "body_statement" || body.type === "then" || body.type === "else") {
|
|
86
|
+
return collectStatements(file, namedChildren(body), className);
|
|
87
|
+
}
|
|
88
|
+
return collectStatements(file, [body], className);
|
|
89
|
+
}
|
|
90
|
+
function collectStatements(file, statements, className) {
|
|
91
|
+
const steps = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
const addCall = (key, node) => {
|
|
94
|
+
const mark = `${key}:${node.startIndex}`;
|
|
95
|
+
if (seen.has(mark))
|
|
96
|
+
return;
|
|
97
|
+
seen.add(mark);
|
|
98
|
+
const step = { type: "call", key, ...locFromNode(file, node) };
|
|
99
|
+
step.syntax = heuristicCallSyntax(node, key);
|
|
100
|
+
steps.push(step);
|
|
101
|
+
};
|
|
102
|
+
const walk = (node, asStatement) => {
|
|
103
|
+
if (node.type === "method" ||
|
|
104
|
+
node.type === "singleton_method" ||
|
|
105
|
+
node.type === "class" ||
|
|
106
|
+
node.type === "module" ||
|
|
107
|
+
node.type === "lambda" ||
|
|
108
|
+
node.type === "block" ||
|
|
109
|
+
node.type === "do_block") {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (node.type === "if" || node.type === "unless") {
|
|
113
|
+
const kids = namedChildren(node);
|
|
114
|
+
const cond = kids.find((c) => c.type !== "then" &&
|
|
115
|
+
c.type !== "else" &&
|
|
116
|
+
c.type !== "elsif") ?? null;
|
|
117
|
+
const thenNode = childByType(node, "then");
|
|
118
|
+
const condText = cond ? collapseWs(cond.text) : "";
|
|
119
|
+
const kind = node.type; // if | unless
|
|
120
|
+
steps.push({
|
|
121
|
+
type: "branch",
|
|
122
|
+
key: condText ? `${kind}:${condText}` : kind,
|
|
123
|
+
label: condText ? `${kind} ${condText}` : kind,
|
|
124
|
+
...locFromNode(file, cond ?? node),
|
|
125
|
+
children: collectBody(file, thenNode, className),
|
|
126
|
+
});
|
|
127
|
+
for (const clause of kids) {
|
|
128
|
+
if (clause.type === "elsif") {
|
|
129
|
+
const elsifKids = namedChildren(clause);
|
|
130
|
+
const elsifCond = elsifKids.find((c) => c.type !== "then" && c.type !== "else") ??
|
|
131
|
+
null;
|
|
132
|
+
const text = elsifCond ? collapseWs(elsifCond.text) : "";
|
|
133
|
+
steps.push({
|
|
134
|
+
type: "branch",
|
|
135
|
+
key: text ? `else-if:${text}` : "else-if",
|
|
136
|
+
label: text ? `elsif ${text}` : "elsif",
|
|
137
|
+
...locFromNode(file, elsifCond ?? clause),
|
|
138
|
+
children: collectBody(file, childByType(clause, "then"), className),
|
|
139
|
+
});
|
|
140
|
+
// nested else under elsif
|
|
141
|
+
const nestedElse = childByType(clause, "else");
|
|
142
|
+
if (nestedElse) {
|
|
143
|
+
steps.push({
|
|
144
|
+
type: "branch",
|
|
145
|
+
key: "else",
|
|
146
|
+
label: "else",
|
|
147
|
+
...locFromNode(file, nestedElse),
|
|
148
|
+
children: collectBody(file, nestedElse, className),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (clause.type === "else") {
|
|
153
|
+
steps.push({
|
|
154
|
+
type: "branch",
|
|
155
|
+
key: "else",
|
|
156
|
+
label: "else",
|
|
157
|
+
...locFromNode(file, clause),
|
|
158
|
+
children: collectBody(file, clause, className),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (node.type === "begin") {
|
|
165
|
+
// Body statements are direct children before rescue/ensure/else
|
|
166
|
+
const bodyStmts = namedChildren(node).filter((c) => c.type !== "rescue" &&
|
|
167
|
+
c.type !== "ensure" &&
|
|
168
|
+
c.type !== "else");
|
|
169
|
+
steps.push({
|
|
170
|
+
type: "branch",
|
|
171
|
+
key: "try",
|
|
172
|
+
label: "begin",
|
|
173
|
+
...locFromNode(file, node),
|
|
174
|
+
children: collectStatements(file, bodyStmts, className),
|
|
175
|
+
});
|
|
176
|
+
for (const clause of namedChildren(node)) {
|
|
177
|
+
if (clause.type === "rescue") {
|
|
178
|
+
const exceptions = childByType(clause, "exceptions");
|
|
179
|
+
const text = exceptions ? collapseWs(exceptions.text) : "";
|
|
180
|
+
const thenNode = childByType(clause, "then");
|
|
181
|
+
steps.push({
|
|
182
|
+
type: "branch",
|
|
183
|
+
key: text ? `rescue:${text}` : "rescue",
|
|
184
|
+
label: text ? `rescue ${text}` : "rescue",
|
|
185
|
+
...locFromNode(file, exceptions ?? clause),
|
|
186
|
+
children: thenNode
|
|
187
|
+
? collectBody(file, thenNode, className)
|
|
188
|
+
: collectStatements(file, namedChildren(clause).filter((c) => c.type !== "exceptions" && c.type !== "exception_variable"), className),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (clause.type === "else") {
|
|
192
|
+
steps.push({
|
|
193
|
+
type: "branch",
|
|
194
|
+
key: "else",
|
|
195
|
+
label: "else",
|
|
196
|
+
...locFromNode(file, clause),
|
|
197
|
+
children: collectBody(file, clause, className),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (clause.type === "ensure") {
|
|
201
|
+
steps.push({
|
|
202
|
+
type: "branch",
|
|
203
|
+
key: "ensure",
|
|
204
|
+
label: "ensure",
|
|
205
|
+
...locFromNode(file, clause),
|
|
206
|
+
children: collectStatements(file, namedChildren(clause), className),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (node.type === "case") {
|
|
213
|
+
for (const clause of namedChildren(node)) {
|
|
214
|
+
if (clause.type === "when") {
|
|
215
|
+
const pattern = childByType(clause, "pattern") ??
|
|
216
|
+
namedChildren(clause).find((c) => c.type !== "then") ??
|
|
217
|
+
null;
|
|
218
|
+
const text = pattern ? collapseWs(pattern.text) : "";
|
|
219
|
+
steps.push({
|
|
220
|
+
type: "branch",
|
|
221
|
+
key: text ? `when:${text}` : "when",
|
|
222
|
+
label: text ? `when ${text}` : "when",
|
|
223
|
+
...locFromNode(file, pattern ?? clause),
|
|
224
|
+
children: collectBody(file, childByType(clause, "then"), className),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (clause.type === "else") {
|
|
228
|
+
steps.push({
|
|
229
|
+
type: "branch",
|
|
230
|
+
key: "else",
|
|
231
|
+
label: "else",
|
|
232
|
+
...locFromNode(file, clause),
|
|
233
|
+
children: collectBody(file, clause, className),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (node.type === "call") {
|
|
240
|
+
// Ruby models `obj.attr` as `call` even without args. Count those only
|
|
241
|
+
// at statement level; always count calls that pass an argument_list.
|
|
242
|
+
const hasArgs = childByType(node, "argument_list") !== null;
|
|
243
|
+
const hasBlock = childByType(node, "block") !== null ||
|
|
244
|
+
childByType(node, "do_block") !== null;
|
|
245
|
+
const kids = namedChildren(node).filter((c) => c.type !== "argument_list" &&
|
|
246
|
+
c.type !== "block" &&
|
|
247
|
+
c.type !== "do_block");
|
|
248
|
+
const isReceiverCall = kids.length >= 2;
|
|
249
|
+
// `lambda { ... }` / `proc { ... }` are closures, not real callees
|
|
250
|
+
const bareName = kids.length === 1 && kids[0].type === "identifier"
|
|
251
|
+
? kids[0].text
|
|
252
|
+
: null;
|
|
253
|
+
const isClosureCtor = hasBlock && (bareName === "lambda" || bareName === "proc");
|
|
254
|
+
if (!isClosureCtor &&
|
|
255
|
+
(asStatement || hasArgs || !isReceiverCall)) {
|
|
256
|
+
const key = callKey(node, className);
|
|
257
|
+
if (key)
|
|
258
|
+
addCall(key, node);
|
|
259
|
+
}
|
|
260
|
+
// Nested invocations inside arguments (not bare attr reads)
|
|
261
|
+
const args = childByType(node, "argument_list");
|
|
262
|
+
if (args) {
|
|
263
|
+
for (const child of namedChildren(args))
|
|
264
|
+
walk(child, false);
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (node.type === "assignment") {
|
|
269
|
+
// RHS may be bare call identifier
|
|
270
|
+
const kids = namedChildren(node);
|
|
271
|
+
const rhs = kids[1] ?? null;
|
|
272
|
+
if (rhs) {
|
|
273
|
+
if (rhs.type === "identifier") {
|
|
274
|
+
// Assignment RHS is a free call, never a receiver method.
|
|
275
|
+
const key = bareCallKey(rhs, null);
|
|
276
|
+
if (key)
|
|
277
|
+
addCall(key, rhs);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
walk(rhs, false);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Statement-level bare identifier → method call
|
|
286
|
+
if (node.type === "identifier" && asStatement) {
|
|
287
|
+
const key = bareCallKey(node, className);
|
|
288
|
+
if (key)
|
|
289
|
+
addCall(key, node);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
for (const child of namedChildren(node)) {
|
|
293
|
+
walk(child, false);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
for (const stmt of statements)
|
|
297
|
+
walk(stmt, true);
|
|
298
|
+
return steps;
|
|
299
|
+
}
|
|
300
|
+
function handleMethod(file, node, className, functions) {
|
|
301
|
+
const name = childByType(node, "identifier")?.text ?? null;
|
|
302
|
+
if (!name)
|
|
303
|
+
return;
|
|
304
|
+
const params = childByType(node, "method_parameters");
|
|
305
|
+
const body = childByType(node, "body_statement");
|
|
306
|
+
const isInit = className !== null && name === "initialize";
|
|
307
|
+
const key = className
|
|
308
|
+
? isInit
|
|
309
|
+
? `${className}.initialize`
|
|
310
|
+
: `${className}.${name}`
|
|
311
|
+
: name;
|
|
312
|
+
const labelBase = isInit ? `${className}` : key;
|
|
313
|
+
const info = {
|
|
314
|
+
key,
|
|
315
|
+
label: `${labelBase}${getParamsLabel(params)}`,
|
|
316
|
+
params: declaredParamsOf(node),
|
|
317
|
+
file,
|
|
318
|
+
steps: collectBody(file, body, className),
|
|
319
|
+
exported: !name.startsWith("_"),
|
|
320
|
+
start: node.startIndex,
|
|
321
|
+
end: node.endIndex,
|
|
322
|
+
};
|
|
323
|
+
functions.push(info);
|
|
324
|
+
if (isInit && className) {
|
|
325
|
+
functions.push({
|
|
326
|
+
...info,
|
|
327
|
+
key: `new ${className}`,
|
|
328
|
+
label: `${className}${getParamsLabel(params)}`,
|
|
329
|
+
params: declaredParamsOf(node),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function handleSingletonMethod(file, node, className, functions) {
|
|
334
|
+
// def self.bar / def Foo.bar
|
|
335
|
+
const name = childByType(node, "identifier")?.text ?? null;
|
|
336
|
+
if (!name)
|
|
337
|
+
return;
|
|
338
|
+
const params = childByType(node, "method_parameters");
|
|
339
|
+
const body = childByType(node, "body_statement");
|
|
340
|
+
const key = className ? `${className}.${name}` : name;
|
|
341
|
+
functions.push({
|
|
342
|
+
key,
|
|
343
|
+
label: `${key}${getParamsLabel(params)}`,
|
|
344
|
+
params: declaredParamsOf(node),
|
|
345
|
+
file,
|
|
346
|
+
steps: collectBody(file, body, className),
|
|
347
|
+
exported: !name.startsWith("_"),
|
|
348
|
+
start: node.startIndex,
|
|
349
|
+
end: node.endIndex,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function handleClass(file, node, functions) {
|
|
353
|
+
const className = childByType(node, "constant")?.text ?? null;
|
|
354
|
+
if (!className)
|
|
355
|
+
return;
|
|
356
|
+
const body = childByType(node, "body_statement");
|
|
357
|
+
if (!body)
|
|
358
|
+
return;
|
|
359
|
+
for (const stmt of namedChildren(body)) {
|
|
360
|
+
if (stmt.type === "method") {
|
|
361
|
+
handleMethod(file, stmt, className, functions);
|
|
362
|
+
}
|
|
363
|
+
else if (stmt.type === "singleton_method") {
|
|
364
|
+
handleSingletonMethod(file, stmt, className, functions);
|
|
365
|
+
}
|
|
366
|
+
else if (stmt.type === "class") {
|
|
367
|
+
handleClass(file, stmt, functions);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function extractFromTree(file, _source, tree) {
|
|
372
|
+
const functions = [];
|
|
373
|
+
for (const stmt of namedChildren(tree.rootNode)) {
|
|
374
|
+
if (stmt.type === "method") {
|
|
375
|
+
handleMethod(file, stmt, null, functions);
|
|
376
|
+
}
|
|
377
|
+
else if (stmt.type === "singleton_method") {
|
|
378
|
+
handleSingletonMethod(file, stmt, null, functions);
|
|
379
|
+
}
|
|
380
|
+
else if (stmt.type === "class") {
|
|
381
|
+
handleClass(file, stmt, functions);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return functions;
|
|
385
|
+
}
|
|
386
|
+
export const rubyExtractor = {
|
|
387
|
+
id: "ruby",
|
|
388
|
+
extensions: [".rb"],
|
|
389
|
+
grammarPackage: "tree-sitter-ruby",
|
|
390
|
+
extract: extractFromTree,
|
|
391
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { heuristicCallSyntax } from "./call-syntax.js";
|
|
2
|
+
import { childByType, declaredParamsOf, collapseWs, locFromNode, namedChildren, sameNode, } from "./types.js";
|
|
3
|
+
function isPublic(node) {
|
|
4
|
+
return childByType(node, "visibility_modifier") !== null;
|
|
5
|
+
}
|
|
6
|
+
function getParamsLabel(params) {
|
|
7
|
+
if (!params || params.type !== "parameters")
|
|
8
|
+
return "()";
|
|
9
|
+
const names = [];
|
|
10
|
+
for (const p of namedChildren(params)) {
|
|
11
|
+
if (p.type === "self_parameter") {
|
|
12
|
+
names.push("self");
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (p.type === "parameter") {
|
|
16
|
+
const ident = namedChildren(p).find((c) => c.type === "identifier") ?? null;
|
|
17
|
+
names.push(ident?.text ?? "_");
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (p.type === "variadic_parameter") {
|
|
21
|
+
names.push("...");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return names.length === 0 ? "()" : `(${names.join(", ")})`;
|
|
25
|
+
}
|
|
26
|
+
function calleeKey(node, typeName) {
|
|
27
|
+
if (node.type === "identifier")
|
|
28
|
+
return node.text;
|
|
29
|
+
if (node.type === "field_expression") {
|
|
30
|
+
const object = node.namedChild(0);
|
|
31
|
+
const field = childByType(node, "field_identifier");
|
|
32
|
+
if (!object || !field)
|
|
33
|
+
return null;
|
|
34
|
+
const prop = field.text;
|
|
35
|
+
if (object.type === "self" && typeName) {
|
|
36
|
+
return `${typeName}.${prop}`;
|
|
37
|
+
}
|
|
38
|
+
if (object.type === "identifier") {
|
|
39
|
+
const objName = object.text;
|
|
40
|
+
if (typeName &&
|
|
41
|
+
objName[0] &&
|
|
42
|
+
objName[0] === objName[0].toLowerCase()) {
|
|
43
|
+
return `${typeName}.${prop}`;
|
|
44
|
+
}
|
|
45
|
+
return `${objName}.${prop}`;
|
|
46
|
+
}
|
|
47
|
+
if (typeName)
|
|
48
|
+
return `${typeName}.${prop}`;
|
|
49
|
+
return prop;
|
|
50
|
+
}
|
|
51
|
+
if (node.type === "scoped_identifier") {
|
|
52
|
+
// Foo::new / Foo::bar — last two identifier-like parts
|
|
53
|
+
const parts = namedChildren(node).filter((c) => c.type === "identifier" ||
|
|
54
|
+
c.type === "type_identifier" ||
|
|
55
|
+
c.type === "scoped_identifier");
|
|
56
|
+
// Flatten: take text of last child as name, preceding type_identifier/identifier
|
|
57
|
+
const nameNode = parts.at(-1);
|
|
58
|
+
if (!nameNode)
|
|
59
|
+
return null;
|
|
60
|
+
const name = nameNode.text;
|
|
61
|
+
// Prefer a type_identifier sibling before the name
|
|
62
|
+
const typeNode = namedChildren(node).find((c) => c.type === "type_identifier") ??
|
|
63
|
+
namedChildren(node).find((c) => c.type === "identifier" && !sameNode(c, nameNode)) ??
|
|
64
|
+
null;
|
|
65
|
+
if (typeNode) {
|
|
66
|
+
if (name === "new")
|
|
67
|
+
return `new ${typeNode.text}`;
|
|
68
|
+
return `${typeNode.text}.${name}`;
|
|
69
|
+
}
|
|
70
|
+
return name;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function statementsOf(node) {
|
|
75
|
+
if (node.type === "block")
|
|
76
|
+
return namedChildren(node);
|
|
77
|
+
return [node];
|
|
78
|
+
}
|
|
79
|
+
function collectStatements(file, statements, typeName) {
|
|
80
|
+
const steps = [];
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const addCall = (key, node) => {
|
|
83
|
+
const mark = `${key}:${node.startIndex}`;
|
|
84
|
+
if (seen.has(mark))
|
|
85
|
+
return;
|
|
86
|
+
seen.add(mark);
|
|
87
|
+
const step = { type: "call", key, ...locFromNode(file, node) };
|
|
88
|
+
step.syntax = heuristicCallSyntax(node, key);
|
|
89
|
+
steps.push(step);
|
|
90
|
+
};
|
|
91
|
+
const pushIfChain = (node, asElseIf) => {
|
|
92
|
+
const kids = namedChildren(node);
|
|
93
|
+
const cond = kids.find((c) => c.type !== "block" && c.type !== "else_clause") ?? null;
|
|
94
|
+
const thenBlock = kids.find((c) => c.type === "block") ?? null;
|
|
95
|
+
const elseClause = childByType(node, "else_clause");
|
|
96
|
+
const condText = cond ? collapseWs(cond.text) : "";
|
|
97
|
+
const kind = asElseIf ? "else-if" : "if";
|
|
98
|
+
const labelKind = asElseIf ? "else if" : "if";
|
|
99
|
+
steps.push({
|
|
100
|
+
type: "branch",
|
|
101
|
+
key: condText ? `${kind}:${condText}` : kind,
|
|
102
|
+
label: condText ? `${labelKind} ${condText}` : labelKind,
|
|
103
|
+
...locFromNode(file, cond ?? node),
|
|
104
|
+
children: thenBlock
|
|
105
|
+
? collectStatements(file, statementsOf(thenBlock), typeName)
|
|
106
|
+
: [],
|
|
107
|
+
});
|
|
108
|
+
if (!elseClause)
|
|
109
|
+
return;
|
|
110
|
+
const elseInner = elseClause.namedChild(0);
|
|
111
|
+
if (!elseInner)
|
|
112
|
+
return;
|
|
113
|
+
if (elseInner.type === "if_expression") {
|
|
114
|
+
pushIfChain(elseInner, true);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
steps.push({
|
|
118
|
+
type: "branch",
|
|
119
|
+
key: "else",
|
|
120
|
+
label: "else",
|
|
121
|
+
...locFromNode(file, elseClause),
|
|
122
|
+
children: collectStatements(file, statementsOf(elseInner), typeName),
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
const walk = (node) => {
|
|
126
|
+
if (node.type === "function_item" ||
|
|
127
|
+
node.type === "closure_expression" ||
|
|
128
|
+
node.type === "impl_item") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (node.type === "if_expression") {
|
|
132
|
+
pushIfChain(node, false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (node.type === "match_expression") {
|
|
136
|
+
const block = childByType(node, "match_block");
|
|
137
|
+
for (const arm of block ? namedChildren(block) : []) {
|
|
138
|
+
if (arm.type !== "match_arm")
|
|
139
|
+
continue;
|
|
140
|
+
const pattern = childByType(arm, "match_pattern") ??
|
|
141
|
+
namedChildren(arm).find((c) => c.type !== "block" &&
|
|
142
|
+
c.type !== "call_expression" &&
|
|
143
|
+
c.type !== "identifier" &&
|
|
144
|
+
c.type !== "if_expression" &&
|
|
145
|
+
c.type !== "match_expression" &&
|
|
146
|
+
c.type !== "closure_expression") ??
|
|
147
|
+
null;
|
|
148
|
+
// Body is the last named child that isn't the pattern / attribute
|
|
149
|
+
const kids = namedChildren(arm).filter((c) => c.type !== "attribute_item");
|
|
150
|
+
const body = kids.length > 1 ? kids[kids.length - 1] : null;
|
|
151
|
+
const text = pattern ? collapseWs(pattern.text) : "";
|
|
152
|
+
steps.push({
|
|
153
|
+
type: "branch",
|
|
154
|
+
key: text ? `case:${text}` : "case",
|
|
155
|
+
label: text ? `case ${text}` : "case",
|
|
156
|
+
...locFromNode(file, pattern ?? arm),
|
|
157
|
+
children: body
|
|
158
|
+
? collectStatements(file, statementsOf(body), typeName)
|
|
159
|
+
: [],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (node.type === "call_expression") {
|
|
165
|
+
const callee = node.namedChild(0);
|
|
166
|
+
if (callee) {
|
|
167
|
+
const key = calleeKey(callee, typeName);
|
|
168
|
+
if (key)
|
|
169
|
+
addCall(key, node);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const child of namedChildren(node))
|
|
173
|
+
walk(child);
|
|
174
|
+
};
|
|
175
|
+
for (const stmt of statements)
|
|
176
|
+
walk(stmt);
|
|
177
|
+
return steps;
|
|
178
|
+
}
|
|
179
|
+
function handleFunctionItem(file, node, typeName, functions) {
|
|
180
|
+
const name = childByType(node, "identifier")?.text ?? null;
|
|
181
|
+
if (!name)
|
|
182
|
+
return;
|
|
183
|
+
const params = childByType(node, "parameters");
|
|
184
|
+
const body = childByType(node, "block");
|
|
185
|
+
const isNew = typeName !== null && name === "new";
|
|
186
|
+
const key = typeName
|
|
187
|
+
? isNew
|
|
188
|
+
? `${typeName}.new`
|
|
189
|
+
: `${typeName}.${name}`
|
|
190
|
+
: name;
|
|
191
|
+
const labelBase = isNew ? `new ${typeName}` : key;
|
|
192
|
+
const exported = isPublic(node) || !name.startsWith("_");
|
|
193
|
+
const info = {
|
|
194
|
+
key,
|
|
195
|
+
label: `${labelBase}${getParamsLabel(params)}`,
|
|
196
|
+
params: declaredParamsOf(node),
|
|
197
|
+
file,
|
|
198
|
+
steps: body ? collectStatements(file, statementsOf(body), typeName) : [],
|
|
199
|
+
exported,
|
|
200
|
+
start: node.startIndex,
|
|
201
|
+
end: node.endIndex,
|
|
202
|
+
};
|
|
203
|
+
functions.push(info);
|
|
204
|
+
if (isNew && typeName) {
|
|
205
|
+
functions.push({
|
|
206
|
+
...info,
|
|
207
|
+
key: `new ${typeName}`,
|
|
208
|
+
label: `new ${typeName}${getParamsLabel(params)}`,
|
|
209
|
+
params: declaredParamsOf(node),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function handleImpl(file, node, functions) {
|
|
214
|
+
const typeIds = namedChildren(node).filter((c) => c.type === "type_identifier");
|
|
215
|
+
// `impl Foo` → one type_identifier; `impl Trait for Foo` → last is the type
|
|
216
|
+
const typeName = typeIds.at(-1)?.text ?? null;
|
|
217
|
+
if (!typeName)
|
|
218
|
+
return;
|
|
219
|
+
const list = childByType(node, "declaration_list");
|
|
220
|
+
if (!list)
|
|
221
|
+
return;
|
|
222
|
+
for (const item of namedChildren(list)) {
|
|
223
|
+
if (item.type === "function_item") {
|
|
224
|
+
handleFunctionItem(file, item, typeName, functions);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function handleModule(file, node, functions) {
|
|
229
|
+
const list = childByType(node, "declaration_list");
|
|
230
|
+
if (!list)
|
|
231
|
+
return;
|
|
232
|
+
for (const item of namedChildren(list)) {
|
|
233
|
+
visit(file, item, functions);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function visit(file, node, functions) {
|
|
237
|
+
if (node.type === "function_item") {
|
|
238
|
+
handleFunctionItem(file, node, null, functions);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (node.type === "impl_item") {
|
|
242
|
+
handleImpl(file, node, functions);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (node.type === "mod_item") {
|
|
246
|
+
handleModule(file, node, functions);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function extractFromTree(file, _source, tree) {
|
|
250
|
+
const functions = [];
|
|
251
|
+
for (const stmt of namedChildren(tree.rootNode)) {
|
|
252
|
+
visit(file, stmt, functions);
|
|
253
|
+
}
|
|
254
|
+
return functions;
|
|
255
|
+
}
|
|
256
|
+
export const rustExtractor = {
|
|
257
|
+
id: "rust",
|
|
258
|
+
extensions: [".rs"],
|
|
259
|
+
grammarPackage: "tree-sitter-rust",
|
|
260
|
+
extract: extractFromTree,
|
|
261
|
+
};
|