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,997 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-side call metadata shared by every extractor: declared parameter lists,
|
|
3
|
+
* the arguments as written, lexical scopes for target classification, and the
|
|
4
|
+
* binder that pairs arguments with parameter slots.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here invents a binding. A parameter slot is only named when the AST
|
|
7
|
+
* says so, and every lossy step records a reason from {@link CALL_REASONS}
|
|
8
|
+
* (spread, destructuring, unknown parameter list) instead of guessing.
|
|
9
|
+
*/
|
|
10
|
+
import { CALL_REASONS, MAX_ARGUMENT_CHARS, MAX_CALL_ARGUMENTS, } from "../types.js";
|
|
11
|
+
import { childByType, namedChildren, sameNode } from "./types.js";
|
|
12
|
+
/* ------------------------------------------------------------------- text */
|
|
13
|
+
/**
|
|
14
|
+
* Source text as written, cut to {@link MAX_ARGUMENT_CHARS}. The kept bytes are
|
|
15
|
+
* untouched: no collapsing, trimming, or re-quoting, so a truncated expression
|
|
16
|
+
* is still a byte-exact prefix of the source.
|
|
17
|
+
*/
|
|
18
|
+
export function cappedText(raw) {
|
|
19
|
+
if (raw.length <= MAX_ARGUMENT_CHARS)
|
|
20
|
+
return { expression: raw };
|
|
21
|
+
return {
|
|
22
|
+
expression: raw.slice(0, MAX_ARGUMENT_CHARS),
|
|
23
|
+
truncated: true,
|
|
24
|
+
originalLength: raw.length,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Verbatim parameter list, parenthesised when the grammar omits the parens. */
|
|
28
|
+
export function parameterText(params) {
|
|
29
|
+
const raw = params?.text.trim() ?? "";
|
|
30
|
+
if (raw === "")
|
|
31
|
+
return "()";
|
|
32
|
+
return raw.startsWith("(") ? raw : `(${raw})`;
|
|
33
|
+
}
|
|
34
|
+
/** Parameters known by text only: no slots, so binding stays unknown. */
|
|
35
|
+
export function textOnlyParams(text) {
|
|
36
|
+
return { text };
|
|
37
|
+
}
|
|
38
|
+
/* --------------------------------------------------- declared parameters */
|
|
39
|
+
/** Declared parameters of a JS/TS function, arrow, or method node. */
|
|
40
|
+
export function jsParameterList(params) {
|
|
41
|
+
if (!params)
|
|
42
|
+
return undefined;
|
|
43
|
+
const declaration = {
|
|
44
|
+
text: parameterText(params), start: params.startIndex, end: params.endIndex,
|
|
45
|
+
};
|
|
46
|
+
if (params.hasError)
|
|
47
|
+
return declaration;
|
|
48
|
+
if (params.type === "identifier") {
|
|
49
|
+
declaration.slots = [{ form: "positional", name: params.text, hasDefault: false }];
|
|
50
|
+
return declaration;
|
|
51
|
+
}
|
|
52
|
+
if (params.type !== "formal_parameters")
|
|
53
|
+
return declaration;
|
|
54
|
+
const slots = [];
|
|
55
|
+
for (const parameter of namedChildren(params)) {
|
|
56
|
+
if (parameter.type === "comment")
|
|
57
|
+
continue;
|
|
58
|
+
let part = parameter.childForFieldName("pattern") ?? parameter;
|
|
59
|
+
const hasDefault = parameter.childForFieldName("value") !== null || part.type === "assignment_pattern";
|
|
60
|
+
if (part.type === "assignment_pattern")
|
|
61
|
+
part = part.childForFieldName("left") ?? part;
|
|
62
|
+
// A written TypeScript `this` parameter is not a runtime argument slot.
|
|
63
|
+
if (part.type === "this" || part.text === "this")
|
|
64
|
+
continue;
|
|
65
|
+
switch (part.type) {
|
|
66
|
+
case "identifier":
|
|
67
|
+
case "type_identifier":
|
|
68
|
+
slots.push({ form: "positional", name: part.text, hasDefault });
|
|
69
|
+
break;
|
|
70
|
+
case "rest_pattern":
|
|
71
|
+
slots.push({ form: "rest", name: childByType(part, "identifier")?.text ?? null });
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
slots.push({ form: "pattern", hasDefault });
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
declaration.slots = slots;
|
|
79
|
+
return declaration;
|
|
80
|
+
}
|
|
81
|
+
/** Declared parameters of a Python function or lambda node. */
|
|
82
|
+
export function pythonParameterList(params) {
|
|
83
|
+
if (!params)
|
|
84
|
+
return undefined;
|
|
85
|
+
const text = parameterText(params);
|
|
86
|
+
const provenance = { start: params.startIndex, end: params.endIndex };
|
|
87
|
+
if (params.hasError)
|
|
88
|
+
return { text, ...provenance };
|
|
89
|
+
const slots = [];
|
|
90
|
+
let keywordOnly = false;
|
|
91
|
+
for (const parameter of namedChildren(params)) {
|
|
92
|
+
switch (parameter.type) {
|
|
93
|
+
case "comment":
|
|
94
|
+
continue;
|
|
95
|
+
case "positional_separator":
|
|
96
|
+
for (const slot of slots) {
|
|
97
|
+
if (slot.form === "positional")
|
|
98
|
+
slot.positionalOnly = true;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
case "keyword_separator":
|
|
102
|
+
keywordOnly = true;
|
|
103
|
+
continue;
|
|
104
|
+
case "list_splat_pattern": {
|
|
105
|
+
const name = childByType(parameter, "identifier");
|
|
106
|
+
slots.push({ form: "rest", name: name?.text ?? null });
|
|
107
|
+
keywordOnly = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
case "dictionary_splat_pattern": {
|
|
111
|
+
const name = childByType(parameter, "identifier");
|
|
112
|
+
slots.push({ form: "keyword-rest", name: name?.text ?? null });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
case "identifier":
|
|
116
|
+
slots.push({
|
|
117
|
+
form: keywordOnly ? "keyword" : "positional",
|
|
118
|
+
name: parameter.text,
|
|
119
|
+
hasDefault: false,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
case "default_parameter": {
|
|
123
|
+
const name = parameter.childForFieldName("name");
|
|
124
|
+
if (name) {
|
|
125
|
+
slots.push({
|
|
126
|
+
form: keywordOnly ? "keyword" : "positional",
|
|
127
|
+
name: name.text,
|
|
128
|
+
hasDefault: true,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
case "typed_parameter":
|
|
134
|
+
case "typed_default_parameter": {
|
|
135
|
+
const name = parameter.childForFieldName("name") ??
|
|
136
|
+
namedChildren(parameter).find((child) => child.type === "identifier" ||
|
|
137
|
+
child.type === "list_splat_pattern" ||
|
|
138
|
+
child.type === "dictionary_splat_pattern") ??
|
|
139
|
+
null;
|
|
140
|
+
const hasDefault = parameter.childForFieldName("value") !== null ||
|
|
141
|
+
parameter.type === "typed_default_parameter";
|
|
142
|
+
if (name?.type === "list_splat_pattern") {
|
|
143
|
+
slots.push({
|
|
144
|
+
form: "rest",
|
|
145
|
+
name: childByType(name, "identifier")?.text ?? null,
|
|
146
|
+
});
|
|
147
|
+
keywordOnly = true;
|
|
148
|
+
}
|
|
149
|
+
else if (name?.type === "dictionary_splat_pattern") {
|
|
150
|
+
slots.push({
|
|
151
|
+
form: "keyword-rest",
|
|
152
|
+
name: childByType(name, "identifier")?.text ?? null,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
else if (name) {
|
|
156
|
+
slots.push({
|
|
157
|
+
form: keywordOnly ? "keyword" : "positional",
|
|
158
|
+
name: name.text,
|
|
159
|
+
hasDefault,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
slots.push({ form: "pattern", hasDefault });
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
default:
|
|
168
|
+
// Nested Python parameter patterns have no single bindable name.
|
|
169
|
+
slots.push({ form: "pattern", hasDefault: false });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { text, slots, ...provenance };
|
|
174
|
+
}
|
|
175
|
+
function argument(position, role, raw, keyword) {
|
|
176
|
+
const text = cappedText(raw);
|
|
177
|
+
const entry = {
|
|
178
|
+
position,
|
|
179
|
+
role,
|
|
180
|
+
expression: text.expression,
|
|
181
|
+
};
|
|
182
|
+
if (keyword !== undefined)
|
|
183
|
+
entry.keyword = keyword;
|
|
184
|
+
if (text.truncated) {
|
|
185
|
+
entry.truncated = true;
|
|
186
|
+
entry.originalLength = text.originalLength;
|
|
187
|
+
}
|
|
188
|
+
return entry;
|
|
189
|
+
}
|
|
190
|
+
function readWithinLimit(nodes, build) {
|
|
191
|
+
const argumentsOnly = nodes.filter(node => node.type !== "comment");
|
|
192
|
+
const args = [];
|
|
193
|
+
for (let index = 0; index < Math.min(argumentsOnly.length, MAX_CALL_ARGUMENTS); index++) {
|
|
194
|
+
args.push(build(argumentsOnly[index], index + 1));
|
|
195
|
+
}
|
|
196
|
+
return { args, omitted: Math.max(0, argumentsOnly.length - MAX_CALL_ARGUMENTS) };
|
|
197
|
+
}
|
|
198
|
+
/** Arguments of a JS/TS call: positional expressions and spread elements. */
|
|
199
|
+
function readJsArguments(args) {
|
|
200
|
+
if (!args || args.hasError)
|
|
201
|
+
return undefined;
|
|
202
|
+
return readWithinLimit(namedChildren(args), (node, position) => argument(position, node.type === "spread_element" ? "star" : "positional", node.text));
|
|
203
|
+
}
|
|
204
|
+
/** Arguments of a Python call: positional, keyword, `*arg`, and `**arg`. */
|
|
205
|
+
function readPythonArguments(args) {
|
|
206
|
+
if (!args || args.hasError)
|
|
207
|
+
return undefined;
|
|
208
|
+
return readWithinLimit(namedChildren(args), (node, position) => {
|
|
209
|
+
if (node.type === "keyword_argument") {
|
|
210
|
+
const keyword = node.childForFieldName("name");
|
|
211
|
+
return argument(position, "keyword", node.text, keyword?.text ?? undefined);
|
|
212
|
+
}
|
|
213
|
+
if (node.type === "list_splat")
|
|
214
|
+
return argument(position, "star", node.text);
|
|
215
|
+
if (node.type === "dictionary_splat") {
|
|
216
|
+
return argument(position, "star-star", node.text);
|
|
217
|
+
}
|
|
218
|
+
return argument(position, "positional", node.text);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Arguments of a call in a grammar where every named child of the argument
|
|
223
|
+
* container is one written argument. Roles are positional: those grammars do
|
|
224
|
+
* not declare keyword arguments, so nothing is read as named.
|
|
225
|
+
*/
|
|
226
|
+
function readPositionalArguments(args) {
|
|
227
|
+
if (!args || args.hasError)
|
|
228
|
+
return undefined;
|
|
229
|
+
return readWithinLimit(namedChildren(args), (node, position) => argument(position, node.type === "spread_element" ? "star" : "positional", node.text));
|
|
230
|
+
}
|
|
231
|
+
/** Argument-list containers of the grammars that declare one. */
|
|
232
|
+
const ARGUMENT_CONTAINERS = {
|
|
233
|
+
arguments: true,
|
|
234
|
+
argument_list: true,
|
|
235
|
+
value_arguments: true,
|
|
236
|
+
call_arguments: true,
|
|
237
|
+
arguments_list: true,
|
|
238
|
+
};
|
|
239
|
+
const ARGUMENT_CONTAINER_TYPES = Object.keys(ARGUMENT_CONTAINERS);
|
|
240
|
+
/**
|
|
241
|
+
* Syntax for a call in a grammar without scope analysis: the target is the
|
|
242
|
+
* qualified name the extractor recognised, so its association with a definition
|
|
243
|
+
* stays a heuristic (`candidate`) and arguments are never matched to parameters.
|
|
244
|
+
* The arguments themselves are still read as written, because they come from
|
|
245
|
+
* the call node and are not inferred.
|
|
246
|
+
*/
|
|
247
|
+
export function heuristicCallSyntax(callNode, callee) {
|
|
248
|
+
const args = readPositionalArguments(argumentContainer(callNode, ARGUMENT_CONTAINER_TYPES));
|
|
249
|
+
return callee === "" ? dynamicSyntax(callee, args) : candidateSyntax(callee, args);
|
|
250
|
+
}
|
|
251
|
+
/** Nodes that only wrap a call's suffix, e.g. Kotlin's `callee(arg)` suffix. */
|
|
252
|
+
const CALL_SUFFIXES = {
|
|
253
|
+
call_suffix: true,
|
|
254
|
+
arguments_suffix: true,
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* First named child whose type is one of `types` (a call's argument list).
|
|
258
|
+
* A grammar that wraps the list in a call suffix is unwrapped only there, never
|
|
259
|
+
* through another call node — `callee()()` must not pick up the inner list.
|
|
260
|
+
*/
|
|
261
|
+
function argumentContainer(node, types) {
|
|
262
|
+
const children = namedChildren(node);
|
|
263
|
+
const direct = children.find((child) => types.includes(child.type));
|
|
264
|
+
if (direct)
|
|
265
|
+
return direct;
|
|
266
|
+
const suffix = children.find((child) => Object.hasOwn(CALL_SUFFIXES, child.type));
|
|
267
|
+
if (!suffix)
|
|
268
|
+
return null;
|
|
269
|
+
return namedChildren(suffix).find((child) => types.includes(child.type)) ?? null;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Pair written arguments with declared slots. A slot is filled at most once and
|
|
273
|
+
* only by a name the AST actually declares; anything else leaves the argument
|
|
274
|
+
* unbound and marks the result partial with the reason why.
|
|
275
|
+
*/
|
|
276
|
+
export function bindArguments(params, args) {
|
|
277
|
+
const slots = params.slots ?? [];
|
|
278
|
+
const bound = args.map(() => null);
|
|
279
|
+
const filled = new Set();
|
|
280
|
+
const reasons = [];
|
|
281
|
+
let partial = false;
|
|
282
|
+
let cursor = 0;
|
|
283
|
+
let spreadSeen = false;
|
|
284
|
+
const named = args.some((arg) => arg.role === "keyword" || arg.role === "star-star");
|
|
285
|
+
const bindPositional = (index) => {
|
|
286
|
+
if (spreadSeen) {
|
|
287
|
+
// A spread of unknown length makes every later position unreliable.
|
|
288
|
+
partial = true;
|
|
289
|
+
reasons.push(CALL_REASONS.spreadArgument);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
while (cursor < slots.length) {
|
|
293
|
+
const slot = slots[cursor];
|
|
294
|
+
if (slot.form === "keyword" || slot.form === "keyword-rest") {
|
|
295
|
+
// Positional arguments cannot reach a keyword-only parameter.
|
|
296
|
+
cursor = slots.length;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
cursor++;
|
|
300
|
+
if (slot.form === "positional") {
|
|
301
|
+
bound[index] = slot.name;
|
|
302
|
+
filled.add(cursor - 1);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (slot.form === "rest") {
|
|
306
|
+
cursor--;
|
|
307
|
+
filled.add(cursor);
|
|
308
|
+
if (slot.name)
|
|
309
|
+
bound[index] = slot.name;
|
|
310
|
+
reasons.push(CALL_REASONS.restBinding);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Destructuring or unnamed slot: it takes the argument, names nothing.
|
|
314
|
+
filled.add(cursor - 1);
|
|
315
|
+
partial = true;
|
|
316
|
+
reasons.push(CALL_REASONS.destructuredBinding);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
partial = true;
|
|
320
|
+
reasons.push(CALL_REASONS.extraArguments);
|
|
321
|
+
};
|
|
322
|
+
for (let index = 0; index < args.length; index++) {
|
|
323
|
+
const arg = args[index];
|
|
324
|
+
if (arg.role === "keyword") {
|
|
325
|
+
const match = slots.findIndex((slot) => (slot.form === "keyword" || (slot.form === "positional" && !slot.positionalOnly)) &&
|
|
326
|
+
slot.name === arg.keyword);
|
|
327
|
+
if (match >= 0) {
|
|
328
|
+
const slot = slots[match];
|
|
329
|
+
if (slot.form === "positional" || slot.form === "keyword") {
|
|
330
|
+
bound[index] = slot.name;
|
|
331
|
+
if (filled.has(match)) {
|
|
332
|
+
partial = true;
|
|
333
|
+
reasons.push(CALL_REASONS.duplicateBinding);
|
|
334
|
+
}
|
|
335
|
+
filled.add(match);
|
|
336
|
+
reasons.push(CALL_REASONS.keywordBinding);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const rest = slots.findIndex((slot) => slot.form === "keyword-rest");
|
|
341
|
+
if (rest >= 0) {
|
|
342
|
+
const slot = slots[rest];
|
|
343
|
+
if (slot.form === "keyword-rest") {
|
|
344
|
+
if (slot.name)
|
|
345
|
+
bound[index] = slot.name;
|
|
346
|
+
filled.add(rest);
|
|
347
|
+
partial = true;
|
|
348
|
+
reasons.push(CALL_REASONS.restBinding);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
partial = true;
|
|
353
|
+
reasons.push(CALL_REASONS.extraArguments);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (arg.role === "star") {
|
|
357
|
+
spreadSeen = true;
|
|
358
|
+
partial = true;
|
|
359
|
+
reasons.push(CALL_REASONS.spreadArgument);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (arg.role === "star-star") {
|
|
363
|
+
// Unknown keys may bind declared parameters or the keyword-rest slot.
|
|
364
|
+
// Do not claim the whole dictionary maps to one of them.
|
|
365
|
+
partial = true;
|
|
366
|
+
reasons.push(CALL_REASONS.spreadArgument);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
bindPositional(index);
|
|
370
|
+
}
|
|
371
|
+
const result = {
|
|
372
|
+
mapping: partial ? "partial" : named ? "named" : "positional",
|
|
373
|
+
bound,
|
|
374
|
+
reasons: [...new Set(reasons)],
|
|
375
|
+
partial,
|
|
376
|
+
};
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* The public contract for one call site: the target expression as written, the
|
|
381
|
+
* callee's declared list, and the arguments with their bindings.
|
|
382
|
+
*
|
|
383
|
+
* A candidate target keeps `mapping: "unknown"`: the association is a name
|
|
384
|
+
* heuristic, so its bindings are never assumed.
|
|
385
|
+
*/
|
|
386
|
+
export function callContextFromSyntax(syntax, params, resolution) {
|
|
387
|
+
const reasons = new Set(syntax.reasons);
|
|
388
|
+
let target = syntax.target;
|
|
389
|
+
if (target === "lexical" && !resolution.lexical) {
|
|
390
|
+
target = "candidate";
|
|
391
|
+
reasons.add(CALL_REASONS.targetAmbiguous);
|
|
392
|
+
}
|
|
393
|
+
if (target === "candidate" && !resolution.resolved) {
|
|
394
|
+
target = "unresolved";
|
|
395
|
+
reasons.add(CALL_REASONS.targetNotIndexed);
|
|
396
|
+
}
|
|
397
|
+
const declared = params ?? syntax.params;
|
|
398
|
+
const context = {
|
|
399
|
+
callee: syntax.callee,
|
|
400
|
+
target,
|
|
401
|
+
mapping: "unknown",
|
|
402
|
+
completeness: "unavailable",
|
|
403
|
+
reasons: [...reasons],
|
|
404
|
+
};
|
|
405
|
+
if (declared)
|
|
406
|
+
context.parameters = declared.text;
|
|
407
|
+
if (syntax.omittedArguments)
|
|
408
|
+
context.omittedArguments = syntax.omittedArguments;
|
|
409
|
+
const args = syntax.arguments;
|
|
410
|
+
if (args === undefined) {
|
|
411
|
+
reasons.add(CALL_REASONS.argumentsUnavailable);
|
|
412
|
+
context.reasons = [...reasons];
|
|
413
|
+
return context;
|
|
414
|
+
}
|
|
415
|
+
context.arguments = args.map((arg) => {
|
|
416
|
+
const entry = {
|
|
417
|
+
position: arg.position,
|
|
418
|
+
expression: arg.expression,
|
|
419
|
+
};
|
|
420
|
+
if (arg.truncated) {
|
|
421
|
+
entry.truncated = true;
|
|
422
|
+
entry.originalLength = arg.originalLength;
|
|
423
|
+
}
|
|
424
|
+
return entry;
|
|
425
|
+
});
|
|
426
|
+
if (args.some((arg) => arg.truncated)) {
|
|
427
|
+
reasons.add(CALL_REASONS.expressionTruncated);
|
|
428
|
+
}
|
|
429
|
+
if (target === "lexical" && declared?.slots) {
|
|
430
|
+
const bound = bindArguments(declared, args);
|
|
431
|
+
context.mapping = bound.mapping;
|
|
432
|
+
for (const reason of bound.reasons)
|
|
433
|
+
reasons.add(reason);
|
|
434
|
+
for (let index = 0; index < bound.bound.length; index++) {
|
|
435
|
+
const name = bound.bound[index];
|
|
436
|
+
if (name)
|
|
437
|
+
context.arguments[index].parameter = name;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
reasons.add(CALL_REASONS.bindingUnavailable);
|
|
442
|
+
}
|
|
443
|
+
const incomplete = context.mapping === "unknown" ||
|
|
444
|
+
(args.some((arg) => arg.truncated) ?? false) ||
|
|
445
|
+
reasons.has(CALL_REASONS.argumentsTruncated) ||
|
|
446
|
+
reasons.has(CALL_REASONS.spreadArgument) ||
|
|
447
|
+
reasons.has(CALL_REASONS.destructuredBinding) ||
|
|
448
|
+
reasons.has(CALL_REASONS.extraArguments) ||
|
|
449
|
+
reasons.has(CALL_REASONS.duplicateBinding) ||
|
|
450
|
+
reasons.has(CALL_REASONS.bindingUnavailable);
|
|
451
|
+
context.reasons = [...reasons];
|
|
452
|
+
context.completeness = incomplete ? "partial" : "complete";
|
|
453
|
+
return context;
|
|
454
|
+
}
|
|
455
|
+
/** Syntax for a target the extractor cannot associate with any declaration. */
|
|
456
|
+
export function dynamicSyntax(callee, args, reasons = [CALL_REASONS.dynamicCallee]) {
|
|
457
|
+
return syntaxFromParts(callee, "unresolved", args, reasons);
|
|
458
|
+
}
|
|
459
|
+
/** Syntax for a target whose binding is a name/member heuristic. */
|
|
460
|
+
export function candidateSyntax(callee, args, reasons = []) {
|
|
461
|
+
return syntaxFromParts(callee, "candidate", args, reasons);
|
|
462
|
+
}
|
|
463
|
+
/** Syntax for a target a declaration in this file actually binds. */
|
|
464
|
+
export function lexicalSyntax(callee, params, args, reasons = []) {
|
|
465
|
+
const syntax = syntaxFromParts(callee, "lexical", args, reasons);
|
|
466
|
+
if (params)
|
|
467
|
+
syntax.params = params;
|
|
468
|
+
return syntax;
|
|
469
|
+
}
|
|
470
|
+
function syntaxFromParts(callee, target, args, reasons) {
|
|
471
|
+
const text = cappedText(callee);
|
|
472
|
+
const syntax = {
|
|
473
|
+
callee: text.expression,
|
|
474
|
+
target,
|
|
475
|
+
reasons: [...new Set(reasons)],
|
|
476
|
+
};
|
|
477
|
+
if (text.truncated)
|
|
478
|
+
syntax.reasons.push(CALL_REASONS.expressionTruncated);
|
|
479
|
+
if (args !== undefined) {
|
|
480
|
+
syntax.arguments = args.args;
|
|
481
|
+
if (args.omitted) {
|
|
482
|
+
syntax.omittedArguments = args.omitted;
|
|
483
|
+
syntax.reasons.push(CALL_REASONS.argumentsTruncated);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return syntax;
|
|
487
|
+
}
|
|
488
|
+
export function newScope(parent = null) {
|
|
489
|
+
return { bindings: new Map(), parent, writes: parent?.writes ?? new Set() };
|
|
490
|
+
}
|
|
491
|
+
export function scopeLookup(scope, name) {
|
|
492
|
+
for (let current = scope; current; current = current.parent) {
|
|
493
|
+
const binding = current.bindings.get(name);
|
|
494
|
+
if (binding)
|
|
495
|
+
return binding;
|
|
496
|
+
}
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
function declare(scope, name, binding) {
|
|
500
|
+
// Repeated declarations or assignments make this association ambiguous.
|
|
501
|
+
scope.bindings.set(name, scope.bindings.has(name) ? { callable: false, kind: "local" } : binding);
|
|
502
|
+
}
|
|
503
|
+
/** A possibly reassigned name cannot prove one callable signature. */
|
|
504
|
+
function collectWrites(root, writes) {
|
|
505
|
+
const record = (node) => {
|
|
506
|
+
if (!node)
|
|
507
|
+
return;
|
|
508
|
+
if (node.type === "identifier" || node.type === "shorthand_property_identifier_pattern") {
|
|
509
|
+
writes.add(node.text);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
for (const child of namedChildren(node))
|
|
513
|
+
record(child);
|
|
514
|
+
};
|
|
515
|
+
const visit = (node) => {
|
|
516
|
+
switch (node.type) {
|
|
517
|
+
case "assignment_expression":
|
|
518
|
+
case "augmented_assignment_expression":
|
|
519
|
+
case "assignment":
|
|
520
|
+
case "augmented_assignment":
|
|
521
|
+
case "named_expression":
|
|
522
|
+
record(node.childForFieldName("left") ?? node.childForFieldName("name"));
|
|
523
|
+
break;
|
|
524
|
+
case "update_expression":
|
|
525
|
+
record(node.childForFieldName("argument"));
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
for (let index = 0; index < node.namedChildCount; index++) {
|
|
529
|
+
const child = node.namedChild(index);
|
|
530
|
+
if (child)
|
|
531
|
+
visit(child);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
visit(root);
|
|
535
|
+
}
|
|
536
|
+
/** Binding patterns shadow names even when no single parameter name exists. */
|
|
537
|
+
function shadowPattern(node, scope, kind) {
|
|
538
|
+
if (!node)
|
|
539
|
+
return;
|
|
540
|
+
if (node.type === "identifier" || node.type === "shorthand_property_identifier_pattern") {
|
|
541
|
+
declare(scope, node.text, { callable: false, kind });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
for (const child of namedChildren(node))
|
|
545
|
+
shadowPattern(child, scope, kind);
|
|
546
|
+
}
|
|
547
|
+
/* ------------------------------------------------------ JavaScript scopes */
|
|
548
|
+
const JS_FUNCTION_NODES = {
|
|
549
|
+
function_declaration: true,
|
|
550
|
+
generator_function_declaration: true,
|
|
551
|
+
function_expression: true,
|
|
552
|
+
generator_function: true,
|
|
553
|
+
arrow_function: true,
|
|
554
|
+
method_definition: true,
|
|
555
|
+
};
|
|
556
|
+
function declaredName(node) {
|
|
557
|
+
const name = node.childForFieldName("name") ??
|
|
558
|
+
childByType(node, "identifier") ??
|
|
559
|
+
childByType(node, "type_identifier") ??
|
|
560
|
+
childByType(node, "property_identifier");
|
|
561
|
+
return name?.text ?? null;
|
|
562
|
+
}
|
|
563
|
+
function jsFunctionValueParams(value) {
|
|
564
|
+
if (!value || !Object.hasOwn(JS_FUNCTION_NODES, value.type))
|
|
565
|
+
return undefined;
|
|
566
|
+
if (value.type === "method_definition") {
|
|
567
|
+
return jsParameterList(childByType(value, "formal_parameters"));
|
|
568
|
+
}
|
|
569
|
+
return jsParameterList(childByType(value, "formal_parameters") ?? value.childForFieldName("parameter"));
|
|
570
|
+
}
|
|
571
|
+
/** Body declarations of one JS function/arrow: `const`, `function`, `class`. */
|
|
572
|
+
function jsBodyBindings(body, scope) {
|
|
573
|
+
if (!body)
|
|
574
|
+
return;
|
|
575
|
+
const walk = (node) => {
|
|
576
|
+
for (const child of namedChildren(node)) {
|
|
577
|
+
if (Object.hasOwn(JS_FUNCTION_NODES, child.type)) {
|
|
578
|
+
const name = declaredName(child);
|
|
579
|
+
if (name &&
|
|
580
|
+
(child.type === "function_declaration" ||
|
|
581
|
+
child.type === "generator_function_declaration")) {
|
|
582
|
+
declare(scope, name, {
|
|
583
|
+
callable: sameNode(node, body),
|
|
584
|
+
params: jsFunctionValueParams(child),
|
|
585
|
+
kind: "function",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
continue; // never attribute a nested body's locals to this scope
|
|
589
|
+
}
|
|
590
|
+
if (child.type === "assignment_expression" || child.type === "augmented_assignment_expression") {
|
|
591
|
+
shadowPattern(child.childForFieldName("left"), scope, "local");
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (child.type === "catch_clause" || child.type === "for_in_statement") {
|
|
595
|
+
shadowPattern(child.childForFieldName("parameter") ?? child.childForFieldName("left"), scope, "local");
|
|
596
|
+
}
|
|
597
|
+
if (child.type === "lexical_declaration" ||
|
|
598
|
+
child.type === "variable_declaration") {
|
|
599
|
+
for (const declarator of namedChildren(child)) {
|
|
600
|
+
if (declarator.type !== "variable_declarator")
|
|
601
|
+
continue;
|
|
602
|
+
if (sameNode(node, body))
|
|
603
|
+
declareDeclarator(declarator, scope);
|
|
604
|
+
else
|
|
605
|
+
shadowPattern(declarator.childForFieldName("name"), scope, "local");
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (child.type === "class_declaration" || child.type === "class") {
|
|
610
|
+
const name = declaredName(child);
|
|
611
|
+
if (name)
|
|
612
|
+
declare(scope, name, { callable: true, kind: "class" });
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
walk(child);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
walk(body);
|
|
619
|
+
}
|
|
620
|
+
/** Params and body declarations of one JS function/arrow/method node. */
|
|
621
|
+
export function jsFunctionScope(node, parent) {
|
|
622
|
+
const scope = newScope(parent);
|
|
623
|
+
const paramsNode = childByType(node, "formal_parameters") ?? node.childForFieldName("parameter");
|
|
624
|
+
shadowPattern(paramsNode, scope, "param");
|
|
625
|
+
jsBodyBindings(childByType(node, "statement_block"), scope);
|
|
626
|
+
return scope;
|
|
627
|
+
}
|
|
628
|
+
function importBindings(clause, scope) {
|
|
629
|
+
if (!clause)
|
|
630
|
+
return;
|
|
631
|
+
const add = (name) => {
|
|
632
|
+
if (name)
|
|
633
|
+
declare(scope, name, { callable: true, kind: "import" });
|
|
634
|
+
};
|
|
635
|
+
for (const child of namedChildren(clause)) {
|
|
636
|
+
if (child.type === "identifier") {
|
|
637
|
+
add(child.text);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (child.type === "namespace_import") {
|
|
641
|
+
add(childByType(child, "identifier")?.text ?? null);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (child.type === "named_imports") {
|
|
645
|
+
for (const specifier of namedChildren(child)) {
|
|
646
|
+
if (specifier.type !== "import_specifier")
|
|
647
|
+
continue;
|
|
648
|
+
const alias = specifier.childForFieldName("alias");
|
|
649
|
+
const name = specifier.childForFieldName("name");
|
|
650
|
+
add(alias?.text ?? name?.text ?? null);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* What one variable declaration binds: a callable when its value is a
|
|
657
|
+
* function/arrow, a class name, otherwise a plain local that shadows the name.
|
|
658
|
+
*/
|
|
659
|
+
function declareDeclarator(declarator, scope) {
|
|
660
|
+
const pattern = declarator.childForFieldName("name");
|
|
661
|
+
if (pattern?.type !== "identifier") {
|
|
662
|
+
shadowPattern(pattern, scope, "local");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const name = pattern.text;
|
|
666
|
+
const value = declarator.childForFieldName("value") ??
|
|
667
|
+
namedChildren(declarator).at(-1) ??
|
|
668
|
+
null;
|
|
669
|
+
if (value && Object.hasOwn(JS_FUNCTION_NODES, value.type)) {
|
|
670
|
+
declare(scope, name, {
|
|
671
|
+
callable: true,
|
|
672
|
+
params: jsFunctionValueParams(value),
|
|
673
|
+
kind: "function",
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
else if (value?.type === "class") {
|
|
677
|
+
declare(scope, name, { callable: true, kind: "class" });
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
declare(scope, name, { callable: false, kind: "local" });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Module bindings of a JS/TS file: declarations and imports. Each statement is
|
|
685
|
+
* classified on its own, and an `export` is classified as what it declares.
|
|
686
|
+
*/
|
|
687
|
+
export function jsModuleScope(root) {
|
|
688
|
+
const scope = newScope();
|
|
689
|
+
collectWrites(root, scope.writes);
|
|
690
|
+
const declareStatement = (node) => {
|
|
691
|
+
switch (node.type) {
|
|
692
|
+
case "import_statement":
|
|
693
|
+
importBindings(childByType(node, "import_clause"), scope);
|
|
694
|
+
return;
|
|
695
|
+
case "export_statement": {
|
|
696
|
+
const declaration = node.childForFieldName("declaration");
|
|
697
|
+
if (declaration)
|
|
698
|
+
declareStatement(declaration);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
case "function_declaration":
|
|
702
|
+
case "generator_function_declaration": {
|
|
703
|
+
const name = declaredName(node);
|
|
704
|
+
if (name) {
|
|
705
|
+
declare(scope, name, {
|
|
706
|
+
callable: true,
|
|
707
|
+
params: jsFunctionValueParams(node),
|
|
708
|
+
kind: "function",
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
case "class_declaration":
|
|
714
|
+
case "class": {
|
|
715
|
+
const name = declaredName(node);
|
|
716
|
+
if (name)
|
|
717
|
+
declare(scope, name, { callable: true, kind: "class" });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
case "lexical_declaration":
|
|
721
|
+
case "variable_declaration": {
|
|
722
|
+
for (const declarator of namedChildren(node)) {
|
|
723
|
+
if (declarator.type !== "variable_declarator")
|
|
724
|
+
continue;
|
|
725
|
+
declareDeclarator(declarator, scope);
|
|
726
|
+
}
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
case "expression_statement": {
|
|
730
|
+
const assignment = node.namedChild(0);
|
|
731
|
+
if (assignment?.type === "assignment_expression" || assignment?.type === "augmented_assignment_expression") {
|
|
732
|
+
shadowPattern(assignment.childForFieldName("left"), scope, "local");
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
default:
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
for (const child of namedChildren(root))
|
|
741
|
+
declareStatement(child);
|
|
742
|
+
return scope;
|
|
743
|
+
}
|
|
744
|
+
function shadowReasons(binding) {
|
|
745
|
+
if (!binding || binding.callable)
|
|
746
|
+
return [];
|
|
747
|
+
return [
|
|
748
|
+
binding.kind === "param"
|
|
749
|
+
? CALL_REASONS.parameterShadow
|
|
750
|
+
: CALL_REASONS.localShadow,
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
function stripJsParens(node) {
|
|
754
|
+
let current = node;
|
|
755
|
+
while (current.type === "parenthesized_expression") {
|
|
756
|
+
const inner = current.namedChild(0);
|
|
757
|
+
if (!inner)
|
|
758
|
+
return current;
|
|
759
|
+
current = inner;
|
|
760
|
+
}
|
|
761
|
+
return current;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Classify plain, unambiguous declarations lexically. Imports and member
|
|
765
|
+
* dispatch remain candidates because this extractor does not resolve them.
|
|
766
|
+
*/
|
|
767
|
+
export function classifyJsCallee(node, env) {
|
|
768
|
+
const callee = node.text;
|
|
769
|
+
const targetNode = stripJsParens(node);
|
|
770
|
+
if (targetNode.type === "identifier") {
|
|
771
|
+
const binding = scopeLookup(env.scope, targetNode.text);
|
|
772
|
+
if (env.scope.writes.has(targetNode.text))
|
|
773
|
+
return { callee, target: "candidate", reasons: [CALL_REASONS.localShadow] };
|
|
774
|
+
if (binding?.callable && binding.kind === "function" && binding.params) {
|
|
775
|
+
return { callee, target: "lexical", params: binding.params, reasons: [] };
|
|
776
|
+
}
|
|
777
|
+
return { callee, target: "candidate", reasons: shadowReasons(binding) };
|
|
778
|
+
}
|
|
779
|
+
if (targetNode.type === "member_expression") {
|
|
780
|
+
return { callee, target: "candidate", reasons: [] };
|
|
781
|
+
}
|
|
782
|
+
return { callee, target: "unresolved", reasons: [CALL_REASONS.dynamicCallee] };
|
|
783
|
+
}
|
|
784
|
+
/* ------------------------------------------------------ JavaScript syntax */
|
|
785
|
+
/** Classified target plus arguments of one JS/TS call site. */
|
|
786
|
+
export function jsCallSyntax(calleeNode, argsNode, env) {
|
|
787
|
+
if (!calleeNode)
|
|
788
|
+
return undefined;
|
|
789
|
+
const target = classifyJsCallee(calleeNode, env);
|
|
790
|
+
const args = readJsArguments(argsNode);
|
|
791
|
+
return target.target === "lexical"
|
|
792
|
+
? lexicalSyntax(target.callee, target.params, args, target.reasons)
|
|
793
|
+
: target.target === "candidate"
|
|
794
|
+
? candidateSyntax(target.callee, args, target.reasons)
|
|
795
|
+
: dynamicSyntax(target.callee, args, target.reasons);
|
|
796
|
+
}
|
|
797
|
+
/* ---------------------------------------------------------- Python scopes */
|
|
798
|
+
function pythonParamsOf(node) {
|
|
799
|
+
const params = node.childForFieldName("parameters") ??
|
|
800
|
+
childByType(node, "parameters") ??
|
|
801
|
+
childByType(node, "lambda_parameters");
|
|
802
|
+
return pythonParameterList(params);
|
|
803
|
+
}
|
|
804
|
+
function pythonImportNames(node) {
|
|
805
|
+
const names = [];
|
|
806
|
+
for (const child of namedChildren(node)) {
|
|
807
|
+
if (child.type === "aliased_import") {
|
|
808
|
+
const alias = child.childForFieldName("alias");
|
|
809
|
+
if (alias)
|
|
810
|
+
names.push(alias.text);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (child.type === "dotted_name") {
|
|
814
|
+
if (node.type === "import_from_statement" && sameNode(child, node.childForFieldName("module_name"))) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
names.push(namedChildren(child)[0]?.text ?? child.text);
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return names;
|
|
822
|
+
}
|
|
823
|
+
/** Names a Python body assigns, plus defs and lambdas it declares. */
|
|
824
|
+
function pythonBodyBindings(body, scope) {
|
|
825
|
+
if (!body)
|
|
826
|
+
return;
|
|
827
|
+
const assign = (target) => {
|
|
828
|
+
if (!target)
|
|
829
|
+
return;
|
|
830
|
+
if (target.type === "identifier") {
|
|
831
|
+
declare(scope, target.text, { callable: false, kind: "local" });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
for (const child of namedChildren(target))
|
|
835
|
+
assign(child);
|
|
836
|
+
};
|
|
837
|
+
const walk = (node) => {
|
|
838
|
+
for (const child of namedChildren(node)) {
|
|
839
|
+
switch (child.type) {
|
|
840
|
+
case "decorated_definition": {
|
|
841
|
+
const declaration = childByType(child, "function_definition");
|
|
842
|
+
const name = declaration?.childForFieldName("name");
|
|
843
|
+
if (name)
|
|
844
|
+
declare(scope, name.text, { callable: false, kind: "local" });
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
case "function_definition":
|
|
848
|
+
case "lambda": {
|
|
849
|
+
const name = childByType(child, "identifier")?.text;
|
|
850
|
+
if (name && child.type === "function_definition") {
|
|
851
|
+
declare(scope, name, {
|
|
852
|
+
callable: true,
|
|
853
|
+
params: pythonParamsOf(child),
|
|
854
|
+
kind: "function",
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
continue; // nested bodies have their own scope
|
|
858
|
+
}
|
|
859
|
+
case "class_definition": {
|
|
860
|
+
const name = childByType(child, "identifier")?.text;
|
|
861
|
+
if (name)
|
|
862
|
+
declare(scope, name, { callable: true, kind: "class" });
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
case "assignment":
|
|
866
|
+
assign(child.childForFieldName("left"));
|
|
867
|
+
continue;
|
|
868
|
+
case "augmented_assignment":
|
|
869
|
+
case "named_expression":
|
|
870
|
+
assign(child.childForFieldName("left"));
|
|
871
|
+
continue;
|
|
872
|
+
case "for_statement":
|
|
873
|
+
assign(child.childForFieldName("left"));
|
|
874
|
+
break;
|
|
875
|
+
case "with_item":
|
|
876
|
+
case "except_clause": {
|
|
877
|
+
const value = child.childForFieldName("value");
|
|
878
|
+
if (value?.type === "as_pattern") {
|
|
879
|
+
assign(value.childForFieldName("alias"));
|
|
880
|
+
}
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
default:
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
walk(child);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
walk(body);
|
|
890
|
+
}
|
|
891
|
+
/** Params and body assignments of one Python function or lambda node. */
|
|
892
|
+
export function pythonFunctionScope(node, parent) {
|
|
893
|
+
const scope = newScope(parent);
|
|
894
|
+
for (const slot of pythonParamsOf(node)?.slots ?? []) {
|
|
895
|
+
if (slot.form === "positional" || slot.form === "keyword") {
|
|
896
|
+
declare(scope, slot.name, { callable: false, kind: "param" });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
pythonBodyBindings(childByType(node, "block"), scope);
|
|
900
|
+
return scope;
|
|
901
|
+
}
|
|
902
|
+
/** Module bindings of a Python file: defs, classes, imports, assignments. */
|
|
903
|
+
export function pythonModuleScope(root) {
|
|
904
|
+
const scope = newScope();
|
|
905
|
+
collectWrites(root, scope.writes);
|
|
906
|
+
for (const child of namedChildren(root)) {
|
|
907
|
+
visitPythonModuleChild(child, scope);
|
|
908
|
+
}
|
|
909
|
+
return scope;
|
|
910
|
+
}
|
|
911
|
+
function visitPythonModuleChild(node, scope) {
|
|
912
|
+
switch (node.type) {
|
|
913
|
+
case "decorated_definition": {
|
|
914
|
+
const inner = childByType(node, "function_definition") ??
|
|
915
|
+
childByType(node, "class_definition");
|
|
916
|
+
const name = inner?.childForFieldName("name");
|
|
917
|
+
if (name)
|
|
918
|
+
declare(scope, name.text, { callable: false, kind: "local" });
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
case "function_definition": {
|
|
922
|
+
const name = childByType(node, "identifier")?.text;
|
|
923
|
+
if (name) {
|
|
924
|
+
declare(scope, name, {
|
|
925
|
+
callable: true,
|
|
926
|
+
params: pythonParamsOf(node),
|
|
927
|
+
kind: "function",
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
case "class_definition": {
|
|
933
|
+
const name = childByType(node, "identifier")?.text;
|
|
934
|
+
if (name)
|
|
935
|
+
declare(scope, name, { callable: true, kind: "class" });
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
case "import_statement":
|
|
939
|
+
case "import_from_statement": {
|
|
940
|
+
for (const name of pythonImportNames(node)) {
|
|
941
|
+
declare(scope, name, { callable: true, kind: "import" });
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
case "expression_statement": {
|
|
946
|
+
const assignment = childByType(node, "assignment");
|
|
947
|
+
if (!assignment)
|
|
948
|
+
return;
|
|
949
|
+
const name = childByType(assignment, "identifier")?.text;
|
|
950
|
+
if (!name)
|
|
951
|
+
return;
|
|
952
|
+
const value = assignment.childForFieldName("right");
|
|
953
|
+
if (value?.type === "lambda") {
|
|
954
|
+
declare(scope, name, {
|
|
955
|
+
callable: true,
|
|
956
|
+
params: pythonParamsOf(value),
|
|
957
|
+
kind: "function",
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
declare(scope, name, { callable: false, kind: "local" });
|
|
962
|
+
}
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
default:
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/** Python imports, decorators, and member dispatch do not establish a target. */
|
|
970
|
+
export function classifyPythonCallee(node, env) {
|
|
971
|
+
const callee = node.text;
|
|
972
|
+
const targetNode = stripJsParens(node);
|
|
973
|
+
if (targetNode.type === "identifier") {
|
|
974
|
+
const binding = scopeLookup(env.scope, targetNode.text);
|
|
975
|
+
if (env.scope.writes.has(targetNode.text))
|
|
976
|
+
return { callee, target: "candidate", reasons: [CALL_REASONS.localShadow] };
|
|
977
|
+
if (binding?.callable && binding.kind === "function" && binding.params) {
|
|
978
|
+
return { callee, target: "lexical", params: binding.params, reasons: [] };
|
|
979
|
+
}
|
|
980
|
+
return { callee, target: "candidate", reasons: shadowReasons(binding) };
|
|
981
|
+
}
|
|
982
|
+
if (targetNode.type === "attribute")
|
|
983
|
+
return { callee, target: "candidate", reasons: [] };
|
|
984
|
+
return { callee, target: "unresolved", reasons: [CALL_REASONS.dynamicCallee] };
|
|
985
|
+
}
|
|
986
|
+
/** Classified target plus arguments of one Python call site. */
|
|
987
|
+
export function pythonCallSyntax(calleeNode, argsNode, env) {
|
|
988
|
+
if (!calleeNode)
|
|
989
|
+
return undefined;
|
|
990
|
+
const target = classifyPythonCallee(calleeNode, env);
|
|
991
|
+
const args = readPythonArguments(argsNode);
|
|
992
|
+
return target.target === "lexical"
|
|
993
|
+
? lexicalSyntax(target.callee, target.params, args, target.reasons)
|
|
994
|
+
: target.target === "candidate"
|
|
995
|
+
? candidateSyntax(target.callee, args, target.reasons)
|
|
996
|
+
: dynamicSyntax(target.callee, args, target.reasons);
|
|
997
|
+
}
|