ucn 5.4.1 → 5.5.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/.claude/skills/ucn/SKILL.md +58 -236
- package/.claude/skills/ucn/references/commands.md +144 -68
- package/.claude/skills/ucn/references/resolution-rules.md +171 -0
- package/.claude/skills/ucn/references/trust-contract.md +27 -33
- package/README.md +18 -12
- package/cli/index.js +42 -16
- package/core/accessors.js +118 -3
- package/core/account.js +30 -0
- package/core/analysis.js +508 -264
- package/core/ast-analysis.js +76 -117
- package/core/async-producers.js +1729 -0
- package/core/bridge.js +1877 -268
- package/core/brief.js +6 -0
- package/core/build-worker.js +78 -14
- package/core/cache.js +522 -31
- package/core/callers.js +12053 -1047
- package/core/check.js +30 -38
- package/core/class-identity.js +1028 -0
- package/core/command-contracts.js +6 -6
- package/core/configuration-items.js +86 -0
- package/core/contract-membership.js +2262 -0
- package/core/cpp-scope.js +532 -0
- package/core/deadcode.js +171 -49
- package/core/decorator-binding.js +260 -0
- package/core/discovery.js +281 -164
- package/core/errors.js +52 -0
- package/core/execute.js +21 -9
- package/core/external-macros.js +326 -0
- package/core/graph-build.js +277 -8
- package/core/graph.js +35 -2
- package/core/ignore-rules.js +472 -0
- package/core/imports.js +504 -42
- package/core/index-ir.js +94 -11
- package/core/inprocess-clients.js +636 -0
- package/core/ir.js +87 -8
- package/core/macro-expansion.js +1416 -0
- package/core/output/analysis-ext.js +6 -6
- package/core/output/analysis.js +18 -2
- package/core/output/brief.js +3 -0
- package/core/output/doctor.js +4 -0
- package/core/output/endpoints.js +15 -6
- package/core/output/find.js +11 -2
- package/core/output/lines.js +8 -2
- package/core/output/refactoring.js +121 -14
- package/core/output/reporting.js +31 -3
- package/core/output/shared.js +14 -0
- package/core/output-budget.js +316 -122
- package/core/parallel-build.js +124 -47
- package/core/parser.js +3 -2
- package/core/project.js +457 -60
- package/core/provenance-facts.js +156 -12
- package/core/provenance-overload.js +30 -3
- package/core/provenance.js +104 -30
- package/core/pytest-fixtures.js +434 -0
- package/core/python-fixture-flow.js +17 -3
- package/core/python-modules.js +173 -0
- package/core/reflection.js +877 -0
- package/core/registry.js +1 -1
- package/core/reporting.js +234 -52
- package/core/request-helpers.js +561 -0
- package/core/route-graph.js +1922 -0
- package/core/route-lists.js +2414 -0
- package/core/rust-macro-expansion.js +2113 -0
- package/core/rust-modules.js +185 -0
- package/core/search.js +121 -29
- package/core/shared.js +19 -1
- package/core/test-name-bindings.js +256 -0
- package/core/type-denotation.js +1161 -0
- package/core/type-references.js +3229 -0
- package/core/verify.js +3360 -672
- package/languages/adapter.js +31 -5
- package/languages/c-family.js +4061 -212
- package/languages/c-macro-invocations.js +380 -0
- package/languages/c-preprocessor.js +254 -0
- package/languages/csharp.js +1406 -49
- package/languages/go.js +1243 -70
- package/languages/index.js +728 -6
- package/languages/java.js +473 -61
- package/languages/javascript.js +1270 -197
- package/languages/lexical-scope.js +1280 -0
- package/languages/python.js +1557 -72
- package/languages/rust-macro-rules.js +1020 -0
- package/languages/rust-value-flow.js +416 -0
- package/languages/rust.js +2237 -115
- package/languages/utils.js +298 -4
- package/mcp/server.js +25 -11
- package/package.json +1 -1
package/languages/go.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { ReceiverTypeMap, typeOrigin } = require('./type-evidence');
|
|
9
|
+
const { referenceScope, scopeFields } = require('./lexical-scope');
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
const {
|
|
@@ -17,6 +18,9 @@ const {
|
|
|
17
18
|
extractGoDocstring,
|
|
18
19
|
visitNameNodes,
|
|
19
20
|
sameNode,
|
|
21
|
+
parseErrorRegions,
|
|
22
|
+
cachedNodeRange,
|
|
23
|
+
goPathPackageName,
|
|
20
24
|
} = require('./utils');
|
|
21
25
|
const { PARSE_OPTIONS, safeParse } = require('./index');
|
|
22
26
|
|
|
@@ -197,8 +201,135 @@ function _processFunction(node, functions, processedRanges, lines) {
|
|
|
197
201
|
* Process a node for type/class extraction (single-pass helper)
|
|
198
202
|
* Returns true if node was matched, false otherwise
|
|
199
203
|
*/
|
|
204
|
+
const goBindingFactsByTree = new WeakMap();
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Declarations and assignments of each name inside one top-level function
|
|
208
|
+
* or method, nested func literals included (fix #381). A one-hop alias needs
|
|
209
|
+
* exactly one declaration and no assignment of the name.
|
|
210
|
+
*/
|
|
211
|
+
function goBindingFacts(fnNode) {
|
|
212
|
+
let byId = goBindingFactsByTree.get(fnNode.tree);
|
|
213
|
+
if (!byId) { byId = new Map(); goBindingFactsByTree.set(fnNode.tree, byId); }
|
|
214
|
+
let facts = byId.get(fnNode.id);
|
|
215
|
+
if (facts) return facts;
|
|
216
|
+
facts = { declared: new Map(), assigned: new Set() };
|
|
217
|
+
const declare = node => {
|
|
218
|
+
if (node?.type === 'identifier') {
|
|
219
|
+
facts.declared.set(node.text, (facts.declared.get(node.text) || 0) + 1);
|
|
220
|
+
} else if (node?.type === 'expression_list') {
|
|
221
|
+
for (const child of node.namedChildren) declare(child);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const record = (child) => {
|
|
225
|
+
if (child.type === 'parameter_declaration' || child.type === 'variadic_parameter_declaration' ||
|
|
226
|
+
child.type === 'var_spec' || child.type === 'const_spec') {
|
|
227
|
+
for (const name of child.childrenForFieldName('name')) declare(name);
|
|
228
|
+
} else if (child.type === 'short_var_declaration' || child.type === 'range_clause' ||
|
|
229
|
+
child.type === 'receive_statement') {
|
|
230
|
+
if (child.type !== 'receive_statement' || child.text.includes(':=')) {
|
|
231
|
+
declare(child.childForFieldName('left'));
|
|
232
|
+
}
|
|
233
|
+
} else if (child.type === 'type_switch_statement') {
|
|
234
|
+
declare(child.childForFieldName('alias'));
|
|
235
|
+
} else if (child.type === 'assignment_statement') {
|
|
236
|
+
const left = child.childForFieldName('left');
|
|
237
|
+
for (const target of left?.type === 'expression_list' ? left.namedChildren : [left]) {
|
|
238
|
+
if (target?.type === 'identifier') facts.assigned.add(target.text);
|
|
239
|
+
}
|
|
240
|
+
} else if (child.type === 'inc_statement' || child.type === 'dec_statement') {
|
|
241
|
+
const target = child.namedChild(0);
|
|
242
|
+
if (target?.type === 'identifier') facts.assigned.add(target.text);
|
|
243
|
+
} else if (child.type === 'unary_expression' && child.child(0)?.type === '&') {
|
|
244
|
+
// `&c` may be written through; not a stable alias.
|
|
245
|
+
const target = child.namedChild(0);
|
|
246
|
+
if (target?.type === 'identifier') facts.assigned.add(target.text);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
// Every named descendant, from the flat node list when one is cached
|
|
250
|
+
// (fix #388); the facts are counts and sets, so visit order is free.
|
|
251
|
+
const range = cachedNodeRange(fnNode);
|
|
252
|
+
if (range) {
|
|
253
|
+
const { nodes, subtreeEnds, index } = range;
|
|
254
|
+
for (let i = index + 1, end = subtreeEnds[index]; i < end; i++) record(nodes[i]);
|
|
255
|
+
} else {
|
|
256
|
+
const walk = (node) => {
|
|
257
|
+
for (const child of node.namedChildren) {
|
|
258
|
+
record(child);
|
|
259
|
+
walk(child);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
walk(fnNode);
|
|
263
|
+
}
|
|
264
|
+
byId.set(fnNode.id, facts);
|
|
265
|
+
return facts;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function goOuterFunction(node) {
|
|
269
|
+
let outer = null;
|
|
270
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
271
|
+
if (parent.type === 'function_declaration' || parent.type === 'method_declaration') outer = parent;
|
|
272
|
+
}
|
|
273
|
+
return outer;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The selector `c` stands for after `c := s.config` (fix #381): one
|
|
278
|
+
* declaration of `c` in the function, never assigned, the root never
|
|
279
|
+
* reassigned, and the use inside the declaring block after the declaration.
|
|
280
|
+
*/
|
|
281
|
+
function goFieldAliasOf(identNode) {
|
|
282
|
+
const fnNode = goOuterFunction(identNode);
|
|
283
|
+
if (!fnNode) return null;
|
|
284
|
+
const name = identNode.text;
|
|
285
|
+
const facts = goBindingFacts(fnNode);
|
|
286
|
+
if (facts.declared.get(name) !== 1 || facts.assigned.has(name)) return null;
|
|
287
|
+
// The declaring statement is the nearest preceding short declaration in
|
|
288
|
+
// an enclosing block.
|
|
289
|
+
for (let block = identNode.parent; block && block.id !== fnNode.id; block = block.parent) {
|
|
290
|
+
if (block.type !== 'block' && block.type !== 'statement_list') continue;
|
|
291
|
+
for (const stmt of block.namedChildren) {
|
|
292
|
+
if (stmt.startIndex >= identNode.startIndex) break;
|
|
293
|
+
if (stmt.type !== 'short_var_declaration') continue;
|
|
294
|
+
const left = stmt.childForFieldName('left');
|
|
295
|
+
const right = stmt.childForFieldName('right');
|
|
296
|
+
const lefts = left?.type === 'expression_list' ? left.namedChildren : [left];
|
|
297
|
+
const rights = right?.type === 'expression_list' ? right.namedChildren : [right];
|
|
298
|
+
const index = lefts.findIndex(n => n?.type === 'identifier' && n.text === name);
|
|
299
|
+
if (index < 0) continue;
|
|
300
|
+
if (lefts.length !== rights.length) return null;
|
|
301
|
+
const value = rights[index];
|
|
302
|
+
if (value?.type !== 'selector_expression') return null;
|
|
303
|
+
const root = value.childForFieldName('operand');
|
|
304
|
+
if (root?.type !== 'identifier' || root.text === name ||
|
|
305
|
+
facts.assigned.has(root.text) || (facts.declared.get(root.text) || 0) > 1) return null;
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* A type declared inside a function body is in scope from its declaration to
|
|
314
|
+
* the end of the enclosing block (fix #378): `type testCase struct{...}` in one
|
|
315
|
+
* test function never names the package-level testCase used elsewhere.
|
|
316
|
+
*/
|
|
317
|
+
function goLocalTypeScope(node) {
|
|
318
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
319
|
+
if (parent.type === 'source_file') return null;
|
|
320
|
+
if (parent.type === 'block') {
|
|
321
|
+
return {
|
|
322
|
+
lexicalScopeStartLine: node.startPosition.row + 1,
|
|
323
|
+
lexicalScopeEndLine: parent.endPosition.row + 1,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
200
330
|
function _processClass(node, types, processedRanges, lines) {
|
|
201
331
|
if (node.type !== 'type_declaration') return false;
|
|
332
|
+
const localScope = goLocalTypeScope(node);
|
|
202
333
|
|
|
203
334
|
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
204
335
|
if (processedRanges.has(rangeKey)) return false;
|
|
@@ -238,16 +369,21 @@ function _processClass(node, types, processedRanges, lines) {
|
|
|
238
369
|
// which must distinguish an open external method set.
|
|
239
370
|
.map(m => m.name);
|
|
240
371
|
|
|
372
|
+
// A spec of a grouped `type ( ... )` declaration names its
|
|
373
|
+
// type on its own line (fix #386).
|
|
374
|
+
const nameLine = nameNode.startPosition.row + 1;
|
|
241
375
|
types.push({
|
|
242
376
|
name,
|
|
243
377
|
startLine,
|
|
244
378
|
endLine,
|
|
379
|
+
...(nameLine !== startLine && { nameLine }),
|
|
245
380
|
type: typeKind,
|
|
246
381
|
members,
|
|
247
382
|
modifiers: isExported ? ['export'] : [],
|
|
248
383
|
...(docstring && { docstring }),
|
|
249
384
|
...(typeParams && { generics: typeParams }),
|
|
250
|
-
...(embeddedBases.length > 0 && { extends: embeddedBases.join(', ') })
|
|
385
|
+
...(embeddedBases.length > 0 && { extends: embeddedBases.join(', ') }),
|
|
386
|
+
...localScope,
|
|
251
387
|
});
|
|
252
388
|
}
|
|
253
389
|
} else if (spec.type === 'type_alias') {
|
|
@@ -258,18 +394,31 @@ function _processClass(node, types, processedRanges, lines) {
|
|
|
258
394
|
const nameNode = spec.childForFieldName('name');
|
|
259
395
|
const typeNode = spec.childForFieldName('type');
|
|
260
396
|
if (nameNode && typeNode) {
|
|
261
|
-
|
|
262
|
-
|
|
397
|
+
// `type IntFlag = FlagBase[int, ...]` instantiates a generic
|
|
398
|
+
// base: the alias is that instantiation, whose method set is
|
|
399
|
+
// the base's (fix #378).
|
|
400
|
+
const aliased = typeNode.type === 'generic_type'
|
|
401
|
+
? typeNode.childForFieldName('type') : typeNode;
|
|
402
|
+
const aliasOf = aliased?.type === 'type_identifier' ? aliased.text
|
|
403
|
+
: aliased?.type === 'qualified_type' ? aliased.childForFieldName('name')?.text
|
|
263
404
|
: null;
|
|
405
|
+
// `type X = pkg.Base` is pkg's Base, never a same-named
|
|
406
|
+
// type of another package (fix #378).
|
|
407
|
+
const aliasQualifier = aliased?.type === 'qualified_type'
|
|
408
|
+
? aliased.childForFieldName('package')?.text : null;
|
|
264
409
|
const { startLine, endLine } = nodeToLocation(node, lines);
|
|
410
|
+
const nameLine = nameNode.startPosition.row + 1;
|
|
265
411
|
types.push({
|
|
266
412
|
name: nameNode.text,
|
|
267
413
|
startLine,
|
|
268
414
|
endLine,
|
|
415
|
+
...(nameLine !== startLine && { nameLine }),
|
|
269
416
|
type: 'type',
|
|
270
417
|
members: [],
|
|
271
418
|
modifiers: /^[A-Z]/.test(nameNode.text) ? ['export'] : [],
|
|
272
419
|
...(aliasOf && { aliasOf }),
|
|
420
|
+
...(aliasOf && aliasQualifier && { aliasQualifier }),
|
|
421
|
+
...localScope,
|
|
273
422
|
});
|
|
274
423
|
}
|
|
275
424
|
}
|
|
@@ -599,6 +748,687 @@ function findStateObjects(code, parser) {
|
|
|
599
748
|
return objects;
|
|
600
749
|
}
|
|
601
750
|
|
|
751
|
+
/**
|
|
752
|
+
* Interface-conversion evidence (fix #360): every site where a value of a
|
|
753
|
+
* named CONCRETE type flows into a slot whose declared type is spelled
|
|
754
|
+
* differently. In compiling Go the slot type is then an interface the
|
|
755
|
+
* concrete type satisfies (or an alias of it), so the record is compiler
|
|
756
|
+
* evidence that renaming a method of the concrete type can break
|
|
757
|
+
* satisfaction of that interface - including interfaces outside the project
|
|
758
|
+
* (`return &pseudoRoot{}` as `os.FileInfo`, `var _ IRouter = (*Engine)(nil)`).
|
|
759
|
+
*
|
|
760
|
+
* Concrete values: `T{}`, `&T{}`, `(*T)(nil)`, `new(T)`, identifiers bound in
|
|
761
|
+
* the same function from those shapes or declared with a named type
|
|
762
|
+
* (parameters, receivers, `var x T`), and one-hop field selectors on such
|
|
763
|
+
* identifiers (resolved at query time from the declared field type). Slots:
|
|
764
|
+
* function results by position, typed `var` specs, assignments to typed
|
|
765
|
+
* locals, `[]I{...}` / `map[K]I{...}` / `[N]I{...}` elements, `append` onto a
|
|
766
|
+
* typed slice local, keyed struct-literal fields and call arguments (the last
|
|
767
|
+
* two are resolved against the project definitions at query time). Only the
|
|
768
|
+
* spelled shapes are recorded; nothing is inferred from names.
|
|
769
|
+
*/
|
|
770
|
+
const GO_BUILTIN_TYPE_NAMES = new Set([
|
|
771
|
+
'bool', 'byte', 'complex64', 'complex128', 'error', 'float32', 'float64',
|
|
772
|
+
'int', 'int8', 'int16', 'int32', 'int64', 'rune', 'string', 'uint',
|
|
773
|
+
'uint8', 'uint16', 'uint32', 'uint64', 'uintptr', 'any',
|
|
774
|
+
]);
|
|
775
|
+
|
|
776
|
+
const CONVERSION_FUNCTIONS = ['function_declaration', 'method_declaration', 'func_literal'];
|
|
777
|
+
const BODY_SHAPES = new Set([...CONVERSION_FUNCTIONS,
|
|
778
|
+
'short_var_declaration', 'assignment_statement', 'var_spec',
|
|
779
|
+
'return_statement', 'composite_literal', 'call_expression']);
|
|
780
|
+
|
|
781
|
+
function extractTypeConversions(tree) {
|
|
782
|
+
const out = [];
|
|
783
|
+
// Qualifiers of standard-library imports (no '.' in the first path
|
|
784
|
+
// element): their types are never project types, so their values are
|
|
785
|
+
// no evidence about project interfaces (fix #384).
|
|
786
|
+
const stdQualifiers = new Set();
|
|
787
|
+
// Package qualifiers the file imports (fix #384): a call through one may
|
|
788
|
+
// reach a function outside the project.
|
|
789
|
+
const importQualifiers = new Set();
|
|
790
|
+
// Import declarations are top-level: read them without a tree walk.
|
|
791
|
+
const importSpecs = [];
|
|
792
|
+
for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
|
|
793
|
+
const declaration = tree.rootNode.namedChild(i);
|
|
794
|
+
if (declaration.type !== 'import_declaration') continue;
|
|
795
|
+
for (const child of declaration.namedChildren) {
|
|
796
|
+
if (child.type === 'import_spec') importSpecs.push(child);
|
|
797
|
+
else if (child.type === 'import_spec_list') {
|
|
798
|
+
for (const spec of child.namedChildren) if (spec.type === 'import_spec') importSpecs.push(spec);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
for (const spec of importSpecs) {
|
|
803
|
+
const importPath = (spec.childForFieldName('path')?.text || '').replace(/^["`]|["`]$/g, '');
|
|
804
|
+
if (!importPath) continue;
|
|
805
|
+
const alias = spec.childForFieldName('name')?.text;
|
|
806
|
+
const segments = importPath.split('/');
|
|
807
|
+
// `example.com/mod/v2` imports package `mod`; `gopkg.in/yaml.v3` `yaml`.
|
|
808
|
+
const conventional = goPathPackageName(importPath);
|
|
809
|
+
const qualifier = alias || conventional;
|
|
810
|
+
if (!qualifier || qualifier === '_' || qualifier === '.') continue;
|
|
811
|
+
importQualifiers.add(qualifier);
|
|
812
|
+
if (!segments[0].includes('.')) stdQualifiers.add(qualifier);
|
|
813
|
+
}
|
|
814
|
+
const stdTyped = (type) => {
|
|
815
|
+
const dot = String(type || '').indexOf('.');
|
|
816
|
+
return dot > 0 && stdQualifiers.has(type.slice(0, dot));
|
|
817
|
+
};
|
|
818
|
+
const typeText = (node) => node ? nodeTextWithoutComments(node).replace(/\s+/g, '') : null;
|
|
819
|
+
const namedTypeOf = (node) => {
|
|
820
|
+
if (!node) return null;
|
|
821
|
+
if (node.type === 'pointer_type') return namedTypeOf(node.namedChild(0));
|
|
822
|
+
if (node.type === 'type_identifier' || node.type === 'qualified_type') return typeText(node);
|
|
823
|
+
if (node.type === 'generic_type') return namedTypeOf(node.childForFieldName('type'));
|
|
824
|
+
return null;
|
|
825
|
+
};
|
|
826
|
+
// `(*T)(nil)` parses as parenthesized(unary * identifier|selector).
|
|
827
|
+
const derefTypeOf = (node) => {
|
|
828
|
+
if (!node || node.type !== 'unary_expression') return null;
|
|
829
|
+
const operand = node.childForFieldName('operand');
|
|
830
|
+
const op = node.child(0);
|
|
831
|
+
if (!op || op.type !== '*' || !operand) return null;
|
|
832
|
+
if (operand.type === 'identifier') return operand.text;
|
|
833
|
+
if (operand.type === 'selector_expression' &&
|
|
834
|
+
operand.childForFieldName('operand')?.type === 'identifier') {
|
|
835
|
+
return `${operand.childForFieldName('operand').text}.${operand.childForFieldName('field').text}`;
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
};
|
|
839
|
+
const constructedTypeOf = (expr) => {
|
|
840
|
+
if (!expr) return null;
|
|
841
|
+
if (expr.type === 'parenthesized_expression' && expr.namedChildCount === 1) {
|
|
842
|
+
return constructedTypeOf(expr.namedChild(0));
|
|
843
|
+
}
|
|
844
|
+
if (expr.type === 'composite_literal') return namedTypeOf(expr.childForFieldName('type'));
|
|
845
|
+
if (expr.type === 'unary_expression' && expr.child(0)?.type === '&') {
|
|
846
|
+
return constructedTypeOf(expr.childForFieldName('operand'));
|
|
847
|
+
}
|
|
848
|
+
if (expr.type === 'call_expression') {
|
|
849
|
+
const fn = expr.childForFieldName('function');
|
|
850
|
+
const args = expr.childForFieldName('arguments');
|
|
851
|
+
if (fn?.type === 'parenthesized_expression' && fn.namedChildCount === 1) {
|
|
852
|
+
return derefTypeOf(fn.namedChild(0));
|
|
853
|
+
}
|
|
854
|
+
if (fn?.type === 'identifier' && fn.text === 'new' && args?.namedChildCount === 1) {
|
|
855
|
+
return namedTypeOf(args.namedChild(0));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
};
|
|
860
|
+
const funcTypes = new Set(['function_declaration', 'method_declaration', 'func_literal']);
|
|
861
|
+
const paramBindings = (list, locals, declared) => {
|
|
862
|
+
if (!list) return;
|
|
863
|
+
for (let i = 0; i < list.namedChildCount; i++) {
|
|
864
|
+
const decl = list.namedChild(i);
|
|
865
|
+
if (decl.type !== 'parameter_declaration' &&
|
|
866
|
+
decl.type !== 'variadic_parameter_declaration') continue;
|
|
867
|
+
const type = decl.childForFieldName('type');
|
|
868
|
+
const named = decl.type === 'parameter_declaration' ? namedTypeOf(type) : null;
|
|
869
|
+
for (let j = 0; j < decl.namedChildCount; j++) {
|
|
870
|
+
const child = decl.namedChild(j);
|
|
871
|
+
if (child.type !== 'identifier' || child.text === '_') continue;
|
|
872
|
+
if (named) locals.set(child.text, { type: named });
|
|
873
|
+
else locals.delete(child.text);
|
|
874
|
+
if (type) declared.set(child.text, typeText(type));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
const resultSlots = (fn) => {
|
|
879
|
+
const result = fn.childForFieldName('result');
|
|
880
|
+
if (!result) return [];
|
|
881
|
+
if (result.type !== 'parameter_list') return [typeText(result)];
|
|
882
|
+
const slots = [];
|
|
883
|
+
for (let i = 0; i < result.namedChildCount; i++) {
|
|
884
|
+
const decl = result.namedChild(i);
|
|
885
|
+
if (decl.type !== 'parameter_declaration') continue;
|
|
886
|
+
const type = typeText(decl.childForFieldName('type'));
|
|
887
|
+
let names = 0;
|
|
888
|
+
for (let j = 0; j < decl.namedChildCount; j++) {
|
|
889
|
+
if (decl.namedChild(j).type === 'identifier') names++;
|
|
890
|
+
}
|
|
891
|
+
for (let k = 0; k < Math.max(1, names); k++) slots.push(type);
|
|
892
|
+
}
|
|
893
|
+
return slots;
|
|
894
|
+
};
|
|
895
|
+
const elementSlotOf = (typeNode) => {
|
|
896
|
+
if (!typeNode) return null;
|
|
897
|
+
if (typeNode.type === 'slice_type' || typeNode.type === 'array_type') {
|
|
898
|
+
return typeText(typeNode.childForFieldName('element'));
|
|
899
|
+
}
|
|
900
|
+
if (typeNode.type === 'map_type') return typeText(typeNode.childForFieldName('value'));
|
|
901
|
+
return null;
|
|
902
|
+
};
|
|
903
|
+
// The callee a call expression names, for query-time result typing.
|
|
904
|
+
const callShapeOf = (expr, locals) => {
|
|
905
|
+
if (expr?.type !== 'call_expression') return null;
|
|
906
|
+
const fnNode = expr.childForFieldName('function');
|
|
907
|
+
if (fnNode?.type === 'identifier') return { callee: fnNode.text };
|
|
908
|
+
if (fnNode?.type === 'selector_expression') {
|
|
909
|
+
const operand = fnNode.childForFieldName('operand');
|
|
910
|
+
const field = fnNode.childForFieldName('field');
|
|
911
|
+
if (field && operand?.type === 'identifier') {
|
|
912
|
+
const bound = locals.get(operand.text);
|
|
913
|
+
return {
|
|
914
|
+
callee: field.text,
|
|
915
|
+
receiver: operand.text,
|
|
916
|
+
...(bound?.type && { receiverType: bound.type }),
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
};
|
|
922
|
+
const valueOf = (value, locals) => {
|
|
923
|
+
const constructed = constructedTypeOf(value);
|
|
924
|
+
if (constructed) return { concrete: constructed, constructed: true };
|
|
925
|
+
// Value forms that keep a named type in this model (fix #384):
|
|
926
|
+
// parentheses, `&x` and `*p` of a local, and a type assertion
|
|
927
|
+
// `x.(T)` / `x.(*T)`, whose value has the asserted type.
|
|
928
|
+
let expr = value;
|
|
929
|
+
for (;;) {
|
|
930
|
+
if (expr?.type === 'parenthesized_expression' && expr.namedChildCount === 1) {
|
|
931
|
+
expr = expr.namedChild(0);
|
|
932
|
+
} else if (expr?.type === 'unary_expression' &&
|
|
933
|
+
(expr.child(0)?.type === '&' || expr.child(0)?.type === '*') &&
|
|
934
|
+
expr.childForFieldName('operand')?.type === 'identifier') {
|
|
935
|
+
expr = expr.childForFieldName('operand');
|
|
936
|
+
} else {
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (expr?.type === 'type_assertion_expression') {
|
|
941
|
+
const asserted = namedTypeOf(expr.childForFieldName('type'));
|
|
942
|
+
return asserted ? { concrete: asserted, asserted: true } : null;
|
|
943
|
+
}
|
|
944
|
+
if (expr?.type === 'identifier') {
|
|
945
|
+
const bound = locals.get(expr.text);
|
|
946
|
+
if (bound?.type) return { concrete: bound.type, constructed: !!bound.constructed };
|
|
947
|
+
if (bound?.call) return { concreteCall: bound.call };
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
if (expr?.type === 'selector_expression') {
|
|
951
|
+
const operand = expr.childForFieldName('operand');
|
|
952
|
+
const field = expr.childForFieldName('field');
|
|
953
|
+
const bound = operand?.type === 'identifier' ? locals.get(operand.text) : null;
|
|
954
|
+
if (bound?.type && field) {
|
|
955
|
+
return { concreteField: { owner: bound.type, field: field.text } };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
// A call's single declared result (`return mem.NewFileHandle(f)`),
|
|
959
|
+
// resolved against the callee's definition at query time.
|
|
960
|
+
const call = callShapeOf(expr, locals);
|
|
961
|
+
if (call) return { concreteCall: call };
|
|
962
|
+
return null;
|
|
963
|
+
};
|
|
964
|
+
const emit = (expr, value, slot) => {
|
|
965
|
+
if (!value) return;
|
|
966
|
+
const bare = (t) => String(t || '').replace(/^\*/, '');
|
|
967
|
+
if (slot.slot && value.concrete && bare(slot.slot) === bare(value.concrete)) return;
|
|
968
|
+
// Derived values (call results, field reads) are only evidence when
|
|
969
|
+
// the slot is a named non-builtin type an interface could be:
|
|
970
|
+
// `return f()` into `error`/`string`/`*T` slots is not a conversion
|
|
971
|
+
// UCN needs, and recording every one would bloat the index.
|
|
972
|
+
if ((value.concreteCall || value.concreteField) && slot.slot &&
|
|
973
|
+
(!/^[A-Za-z_]\w*(\.[A-Za-z_]\w*)?$/.test(slot.slot) ||
|
|
974
|
+
GO_BUILTIN_TYPE_NAMES.has(slot.slot))) return;
|
|
975
|
+
out.push({
|
|
976
|
+
line: expr.startPosition.row + 1,
|
|
977
|
+
column: expr.startPosition.column,
|
|
978
|
+
...(value.concrete && { concrete: value.concrete }),
|
|
979
|
+
...(value.concreteField && { concreteField: value.concreteField }),
|
|
980
|
+
...(value.concreteCall && { concreteCall: value.concreteCall }),
|
|
981
|
+
...slot,
|
|
982
|
+
});
|
|
983
|
+
};
|
|
984
|
+
// The walk acts only on these shapes and passes through every other node
|
|
985
|
+
// (fix #365: a recursive named-child walk over every node of every file
|
|
986
|
+
// dominated Go indexing). One native query yields them in document
|
|
987
|
+
// order; a frame stack replays the recursive walk's scoping exactly:
|
|
988
|
+
// function scopes copy the enclosing locals at their position, an
|
|
989
|
+
// assignment visits only its right-hand values and binds its targets
|
|
990
|
+
// after them, a function-level `var` spec visits only its values and
|
|
991
|
+
// binds (and emits) after them, and a function without a body, its
|
|
992
|
+
// parameter list, and every assignment target are never visited.
|
|
993
|
+
const shapes = tree.rootNode.descendantsOfType([...BODY_SHAPES]);
|
|
994
|
+
const frames = []; // { end, fn?, locals, declared, results, allowed?, post? }
|
|
995
|
+
const fnFrame = () => {
|
|
996
|
+
for (let k = frames.length - 1; k >= 0; k--) if (frames[k].fn) return frames[k];
|
|
997
|
+
return null;
|
|
998
|
+
};
|
|
999
|
+
const openFunction = (fn, outer) => {
|
|
1000
|
+
const locals = new Map(outer ? outer.locals : undefined);
|
|
1001
|
+
const declared = new Map(outer ? outer.declared : undefined);
|
|
1002
|
+
paramBindings(fn.childForFieldName('receiver'), locals, declared);
|
|
1003
|
+
paramBindings(fn.childForFieldName('parameters'), locals, declared);
|
|
1004
|
+
const results = resultSlots(fn);
|
|
1005
|
+
const body = fn.childForFieldName('body');
|
|
1006
|
+
if (!body) return false;
|
|
1007
|
+
frames.push({ fn: true, end: fn.endIndex, locals, declared, results,
|
|
1008
|
+
allowed: [[body.startIndex, body.endIndex]] });
|
|
1009
|
+
return true;
|
|
1010
|
+
};
|
|
1011
|
+
const assignmentPost = (node, lhs, rhs, locals, declared) => () => {
|
|
1012
|
+
if (lhs.length === rhs.length) {
|
|
1013
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
1014
|
+
if (lhs[i].type !== 'identifier' || lhs[i].text === '_') continue;
|
|
1015
|
+
if (node.type === 'assignment_statement' && declared.has(lhs[i].text)) continue;
|
|
1016
|
+
const constructed = constructedTypeOf(rhs[i]);
|
|
1017
|
+
const call = constructed ? null : callShapeOf(rhs[i], locals);
|
|
1018
|
+
if (constructed) locals.set(lhs[i].text, { type: constructed, constructed: true });
|
|
1019
|
+
else if (call) locals.set(lhs[i].text, { call: { ...call, index: 0 } });
|
|
1020
|
+
else locals.delete(lhs[i].text);
|
|
1021
|
+
if (node.type === 'short_var_declaration') declared.delete(lhs[i].text);
|
|
1022
|
+
}
|
|
1023
|
+
} else {
|
|
1024
|
+
// `f, err := os.Create(name)`: each target is one result
|
|
1025
|
+
// position of the call.
|
|
1026
|
+
const call = rhs.length === 1 ? callShapeOf(rhs[0], locals) : null;
|
|
1027
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
1028
|
+
const target = lhs[i];
|
|
1029
|
+
if (target.type !== 'identifier') continue;
|
|
1030
|
+
if (node.type === 'assignment_statement' && declared.has(target.text)) continue;
|
|
1031
|
+
if (call && target.text !== '_') locals.set(target.text, { call: { ...call, index: i } });
|
|
1032
|
+
else locals.delete(target.text);
|
|
1033
|
+
if (node.type === 'short_var_declaration') declared.delete(target.text);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
const varSpecPost = (node, type, names, values, locals, declared) => () => {
|
|
1038
|
+
for (let i = 0; i < names.length; i++) {
|
|
1039
|
+
if (type) {
|
|
1040
|
+
if (values[i]) emit(values[i], valueOf(values[i], locals), { slot: typeText(type), kind: 'var' });
|
|
1041
|
+
const named = namedTypeOf(type);
|
|
1042
|
+
if (names[i].text !== '_') {
|
|
1043
|
+
declared.set(names[i].text, typeText(type));
|
|
1044
|
+
if (named) locals.set(names[i].text, { type: named });
|
|
1045
|
+
else locals.delete(names[i].text);
|
|
1046
|
+
}
|
|
1047
|
+
} else if (names[i].text !== '_') {
|
|
1048
|
+
const constructed = constructedTypeOf(values[i]);
|
|
1049
|
+
if (constructed) locals.set(names[i].text, { type: constructed, constructed: true });
|
|
1050
|
+
else locals.delete(names[i].text);
|
|
1051
|
+
declared.delete(names[i].text);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
const closeFramesBefore = (offset) => {
|
|
1056
|
+
while (frames.length > 0 && frames[frames.length - 1].end <= offset) {
|
|
1057
|
+
const frame = frames.pop();
|
|
1058
|
+
if (frame.post) frame.post();
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
const actInFunction = (node, scope) => {
|
|
1062
|
+
const { locals, declared, results } = scope;
|
|
1063
|
+
if (node.type === 'short_var_declaration' || node.type === 'assignment_statement') {
|
|
1064
|
+
const left = node.childForFieldName('left');
|
|
1065
|
+
const right = node.childForFieldName('right');
|
|
1066
|
+
const lhs = left ? left.namedChildren : [];
|
|
1067
|
+
const rhs = right ? right.namedChildren : [];
|
|
1068
|
+
if (node.type === 'assignment_statement' && lhs.length === rhs.length) {
|
|
1069
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
1070
|
+
const target = lhs[i];
|
|
1071
|
+
if (target.type === 'selector_expression') {
|
|
1072
|
+
// `srv.Handler = h`: the field of the operand's
|
|
1073
|
+
// declared named type (fix #384).
|
|
1074
|
+
const operand = target.childForFieldName('operand');
|
|
1075
|
+
const field = target.childForFieldName('field');
|
|
1076
|
+
const ownerType = operand?.type === 'identifier'
|
|
1077
|
+
? declared.get(operand.text) : null;
|
|
1078
|
+
const value = field && ownerType &&
|
|
1079
|
+
/^\*?[A-Za-z_]\w*(\.[A-Za-z_]\w*)?$/.test(ownerType)
|
|
1080
|
+
? valueOf(rhs[i], locals) : null;
|
|
1081
|
+
if (value?.concrete) {
|
|
1082
|
+
emit(rhs[i], value, {
|
|
1083
|
+
fieldSlot: { owner: ownerType.replace(/^\*/, ''), field: field.text },
|
|
1084
|
+
kind: 'field',
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (target.type !== 'identifier') continue;
|
|
1090
|
+
const slotType = declared.get(target.text);
|
|
1091
|
+
if (slotType) emit(rhs[i], valueOf(rhs[i], locals), { slot: slotType, kind: 'assign' });
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
frames.push({ end: node.endIndex, allowed: rhs.map(child => [child.startIndex, child.endIndex]),
|
|
1095
|
+
post: assignmentPost(node, lhs, rhs, locals, declared) });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (node.type === 'var_spec') {
|
|
1099
|
+
const type = node.childForFieldName('type');
|
|
1100
|
+
const value = node.childForFieldName('value');
|
|
1101
|
+
const names = [];
|
|
1102
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
1103
|
+
const child = node.namedChild(i);
|
|
1104
|
+
if (child.type === 'identifier') names.push(child);
|
|
1105
|
+
}
|
|
1106
|
+
const values = value ? value.namedChildren : [];
|
|
1107
|
+
frames.push({ end: node.endIndex, allowed: values.map(child => [child.startIndex, child.endIndex]),
|
|
1108
|
+
post: varSpecPost(node, type, names, values, locals, declared) });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (node.type === 'return_statement') {
|
|
1112
|
+
const list = node.namedChildCount > 0 ? node.namedChild(0) : null;
|
|
1113
|
+
const values = list?.type === 'expression_list' ? list.namedChildren : (list ? [list] : []);
|
|
1114
|
+
if (values.length === results.length) {
|
|
1115
|
+
for (let i = 0; i < values.length; i++) {
|
|
1116
|
+
if (results[i]) emit(values[i], valueOf(values[i], locals), { slot: results[i], kind: 'return' });
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (node.type === 'composite_literal') {
|
|
1122
|
+
const typeNode = node.childForFieldName('type');
|
|
1123
|
+
const element = elementSlotOf(typeNode);
|
|
1124
|
+
const structType = element ? null : namedTypeOf(typeNode);
|
|
1125
|
+
const bodyNode = node.childForFieldName('body');
|
|
1126
|
+
for (let i = 0; bodyNode && i < bodyNode.namedChildCount; i++) {
|
|
1127
|
+
const item = bodyNode.namedChild(i);
|
|
1128
|
+
let valueNode = item;
|
|
1129
|
+
let key = null;
|
|
1130
|
+
if (item.type === 'keyed_element') {
|
|
1131
|
+
key = item.namedChild(0)?.namedChild(0) || null;
|
|
1132
|
+
valueNode = item.namedChild(item.namedChildCount - 1);
|
|
1133
|
+
}
|
|
1134
|
+
if (valueNode?.type === 'literal_element') valueNode = valueNode.namedChild(0);
|
|
1135
|
+
if (!valueNode) continue;
|
|
1136
|
+
if (element) {
|
|
1137
|
+
emit(valueNode, valueOf(valueNode, locals), { slot: element, kind: 'element' });
|
|
1138
|
+
} else if (structType && key?.type === 'identifier') {
|
|
1139
|
+
emit(valueNode, valueOf(valueNode, locals), {
|
|
1140
|
+
fieldSlot: { owner: structType, field: key.text }, kind: 'field',
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (node.type === 'call_expression') {
|
|
1147
|
+
const fnNode = node.childForFieldName('function');
|
|
1148
|
+
const args = node.childForFieldName('arguments');
|
|
1149
|
+
const argList = args ? args.namedChildren : [];
|
|
1150
|
+
if (fnNode?.type === 'identifier' && fnNode.text === 'append' && argList.length >= 2 &&
|
|
1151
|
+
argList[0].type === 'identifier') {
|
|
1152
|
+
const sliceType = declared.get(argList[0].text) || '';
|
|
1153
|
+
const element = sliceType.startsWith('[]') ? sliceType.slice(2) : null;
|
|
1154
|
+
if (element) {
|
|
1155
|
+
for (const arg of argList.slice(1)) {
|
|
1156
|
+
emit(arg, valueOf(arg, locals), { slot: element, kind: 'element' });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
} else if (fnNode && (fnNode.type === 'identifier' || fnNode.type === 'selector_expression')) {
|
|
1160
|
+
let callee;
|
|
1161
|
+
let receiver = null;
|
|
1162
|
+
let receiverType = null;
|
|
1163
|
+
// A callee that may live outside the project: a function
|
|
1164
|
+
// qualified by an imported package (fix #384).
|
|
1165
|
+
let mayBeOutside = false;
|
|
1166
|
+
if (fnNode.type === 'identifier') {
|
|
1167
|
+
callee = fnNode.text;
|
|
1168
|
+
} else {
|
|
1169
|
+
callee = fnNode.childForFieldName('field')?.text || null;
|
|
1170
|
+
const operand = fnNode.childForFieldName('operand');
|
|
1171
|
+
if (operand?.type === 'identifier') {
|
|
1172
|
+
receiver = operand.text;
|
|
1173
|
+
receiverType = locals.get(operand.text)?.type || null;
|
|
1174
|
+
mayBeOutside = importQualifiers.has(operand.text) &&
|
|
1175
|
+
!declared.has(operand.text) && !locals.has(operand.text);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
for (let i = 0; callee && i < argList.length; i++) {
|
|
1179
|
+
const value = valueOf(argList[i], locals);
|
|
1180
|
+
// Arguments: construction-shaped or asserted values; a
|
|
1181
|
+
// typed local only toward a callee that may live outside
|
|
1182
|
+
// the project (fix #384: a concrete value passed to an
|
|
1183
|
+
// out-of-project parameter may be converted to an
|
|
1184
|
+
// interface UCN cannot see). A local passed along to a
|
|
1185
|
+
// project callee is usually already the parameter's own
|
|
1186
|
+
// (interface) type.
|
|
1187
|
+
if (!value?.concrete || GO_BUILTIN_TYPE_NAMES.has(value.concrete)) continue;
|
|
1188
|
+
if (!value.constructed && !value.asserted &&
|
|
1189
|
+
(!mayBeOutside || stdTyped(value.concrete))) continue;
|
|
1190
|
+
emit(argList[i], value, {
|
|
1191
|
+
argSlot: {
|
|
1192
|
+
callee,
|
|
1193
|
+
argIndex: i,
|
|
1194
|
+
...(receiver && { receiver }),
|
|
1195
|
+
...(receiverType && { receiverType }),
|
|
1196
|
+
},
|
|
1197
|
+
kind: 'argument',
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
const actAtTop = (node) => {
|
|
1204
|
+
if (node.type !== 'var_spec') return;
|
|
1205
|
+
const type = node.childForFieldName('type');
|
|
1206
|
+
const value = node.childForFieldName('value');
|
|
1207
|
+
if (type && value) {
|
|
1208
|
+
for (const child of value.namedChildren) {
|
|
1209
|
+
const constructed = constructedTypeOf(child);
|
|
1210
|
+
if (constructed) {
|
|
1211
|
+
emit(child, { concrete: constructed, constructed: true },
|
|
1212
|
+
{ slot: typeText(type), kind: 'var' });
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
const starts = shapes.map(node => node.startIndex);
|
|
1218
|
+
for (let k = 0; k < shapes.length; k++) {
|
|
1219
|
+
const node = shapes[k];
|
|
1220
|
+
const start = starts[k];
|
|
1221
|
+
closeFramesBefore(start);
|
|
1222
|
+
const inner = frames[frames.length - 1];
|
|
1223
|
+
let end = -1;
|
|
1224
|
+
const skipSubtree = () => {
|
|
1225
|
+
if (end < 0) end = node.endIndex;
|
|
1226
|
+
while (k + 1 < shapes.length && starts[k + 1] < end) k++;
|
|
1227
|
+
};
|
|
1228
|
+
if (inner && inner.allowed) {
|
|
1229
|
+
end = node.endIndex;
|
|
1230
|
+
if (!inner.allowed.some(([a, b]) => start >= a && end <= b)) { skipSubtree(); continue; }
|
|
1231
|
+
}
|
|
1232
|
+
const scope = fnFrame();
|
|
1233
|
+
if (funcTypes.has(node.type)) {
|
|
1234
|
+
if (!openFunction(node, scope)) skipSubtree();
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (scope) actInFunction(node, scope);
|
|
1238
|
+
else actAtTop(node);
|
|
1239
|
+
}
|
|
1240
|
+
closeFramesBefore(Infinity);
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Extract the base type name from a type node (strips pointer, qualified, etc.)
|
|
1245
|
+
function goTypeNameOf(typeNode) {
|
|
1246
|
+
if (!typeNode) return null;
|
|
1247
|
+
if (typeNode.type === 'type_identifier') return typeNode.text;
|
|
1248
|
+
if (typeNode.type === 'pointer_type') {
|
|
1249
|
+
// *Framework -> Framework
|
|
1250
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
1251
|
+
const r = goTypeNameOf(typeNode.namedChild(i));
|
|
1252
|
+
if (r) return r;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (typeNode.type === 'qualified_type') {
|
|
1256
|
+
// pkg.Type -> Type
|
|
1257
|
+
const tn = typeNode.childForFieldName('name');
|
|
1258
|
+
if (tn) return tn.text;
|
|
1259
|
+
}
|
|
1260
|
+
// Box[T] / pkg.Box[T] -> Box: an instantiation has the generic
|
|
1261
|
+
// type's method set (fix #378).
|
|
1262
|
+
if (typeNode.type === 'generic_type') {
|
|
1263
|
+
return goTypeNameOf(typeNode.childForFieldName('type'));
|
|
1264
|
+
}
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Preserve the package qualifier separately from the bare type name.
|
|
1269
|
+
// `http.Handler` and a project-local `Handler` are different identities;
|
|
1270
|
+
// dropping `http.` lets an unrelated same-named project type steal method
|
|
1271
|
+
// dispatch. Pointer wrappers do not change that provenance.
|
|
1272
|
+
function goTypeQualifierOf(typeNode) {
|
|
1273
|
+
if (!typeNode) return null;
|
|
1274
|
+
if (typeNode.type === 'pointer_type') {
|
|
1275
|
+
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
1276
|
+
const q = goTypeQualifierOf(typeNode.namedChild(i));
|
|
1277
|
+
if (q) return q;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (typeNode.type === 'qualified_type') {
|
|
1281
|
+
const pkg = typeNode.childForFieldName('package') || typeNode.namedChild(0);
|
|
1282
|
+
return pkg?.text || null;
|
|
1283
|
+
}
|
|
1284
|
+
if (typeNode.type === 'generic_type') {
|
|
1285
|
+
return goTypeQualifierOf(typeNode.childForFieldName('type'));
|
|
1286
|
+
}
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const GO_CONTAINER_TYPE_NODES = new Set(['slice_type', 'map_type', 'array_type']);
|
|
1291
|
+
|
|
1292
|
+
/** The var_spec nodes of a top-level `var` declaration (single or grouped). */
|
|
1293
|
+
function goVarSpecs(declaration) {
|
|
1294
|
+
const specs = [];
|
|
1295
|
+
for (const child of declaration.namedChildren) {
|
|
1296
|
+
if (child.type === 'var_spec') specs.push(child);
|
|
1297
|
+
else if (child.type === 'var_spec_list') {
|
|
1298
|
+
for (const spec of child.namedChildren) if (spec.type === 'var_spec') specs.push(spec);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return specs;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Package-level variables of a file (fix #399): every name a top-level
|
|
1306
|
+
* `var` declares, with the static type its declaration states (`var x T`,
|
|
1307
|
+
* `var x = T{..}`, `var x = &T{..}`) and the AST witness. A variable
|
|
1308
|
+
* initialized by a call is typed at query time from the producer's declared
|
|
1309
|
+
* result, in this file (its flow map holds the module-scope assignment).
|
|
1310
|
+
* Package scope is the directory: other files of the package reach these
|
|
1311
|
+
* names unqualified, importers through the package qualifier. Constants
|
|
1312
|
+
* are recorded too (`constant: true`, typed only by a written type): a
|
|
1313
|
+
* name the package declares is never an import's name (fix #400).
|
|
1314
|
+
*/
|
|
1315
|
+
function extractPackageVars(rootNode) {
|
|
1316
|
+
const out = [];
|
|
1317
|
+
for (const declaration of rootNode.namedChildren) {
|
|
1318
|
+
if (declaration.type === 'const_declaration') {
|
|
1319
|
+
for (const spec of declaration.namedChildren) {
|
|
1320
|
+
if (spec.type !== 'const_spec') continue;
|
|
1321
|
+
const declaredType = spec.childForFieldName('type');
|
|
1322
|
+
const typeName = declaredType && !GO_CONTAINER_TYPE_NODES.has(declaredType.type)
|
|
1323
|
+
? goTypeNameOf(declaredType) : null;
|
|
1324
|
+
const qualifier = typeName ? goTypeQualifierOf(declaredType) : null;
|
|
1325
|
+
for (const id of spec.namedChildren) {
|
|
1326
|
+
if (id.type !== 'identifier' || id.text === '_') continue;
|
|
1327
|
+
out.push({
|
|
1328
|
+
name: id.text,
|
|
1329
|
+
line: id.startPosition.row + 1,
|
|
1330
|
+
constant: true,
|
|
1331
|
+
...(typeName && { type: typeName, origin: typeOrigin('annotation', declaredType) }),
|
|
1332
|
+
...(qualifier && { qualifier }),
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (declaration.type !== 'var_declaration') continue;
|
|
1339
|
+
for (const spec of goVarSpecs(declaration)) {
|
|
1340
|
+
const declaredType = spec.childForFieldName('type');
|
|
1341
|
+
let typeName = null;
|
|
1342
|
+
let qualifier = null;
|
|
1343
|
+
let origin = null;
|
|
1344
|
+
let valueFrom = null;
|
|
1345
|
+
if (declaredType && !GO_CONTAINER_TYPE_NODES.has(declaredType.type)) {
|
|
1346
|
+
typeName = goTypeNameOf(declaredType);
|
|
1347
|
+
qualifier = typeName ? goTypeQualifierOf(declaredType) : null;
|
|
1348
|
+
origin = typeName ? typeOrigin('annotation', declaredType) : null;
|
|
1349
|
+
} else if (!declaredType) {
|
|
1350
|
+
const valueNode = spec.childForFieldName('value');
|
|
1351
|
+
let init = valueNode?.type === 'expression_list' && valueNode.namedChildCount === 1
|
|
1352
|
+
? valueNode.namedChild(0) : null;
|
|
1353
|
+
if (init?.type === 'unary_expression') init = init.namedChildCount > 0 ? init.namedChild(0) : null;
|
|
1354
|
+
if (init?.type === 'composite_literal') {
|
|
1355
|
+
const tn = init.childForFieldName('type');
|
|
1356
|
+
typeName = goTypeNameOf(tn);
|
|
1357
|
+
qualifier = typeName ? goTypeQualifierOf(tn) : null;
|
|
1358
|
+
origin = typeName ? typeOrigin('constructor', init) : null;
|
|
1359
|
+
} else if (init?.type === 'selector_expression' && valueNode.namedChild(0)?.type === 'selector_expression') {
|
|
1360
|
+
// `var json = jsoniter.ConfigCompatibleWithStandardLibrary`
|
|
1361
|
+
// (fix #400): the value another package declares; its
|
|
1362
|
+
// type is that declaration's, decided at query time.
|
|
1363
|
+
const operand = init.childForFieldName('operand');
|
|
1364
|
+
const field = init.childForFieldName('field');
|
|
1365
|
+
if (operand?.type === 'identifier' && field) valueFrom = { qualifier: operand.text, name: field.text };
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
for (const id of spec.namedChildren) {
|
|
1369
|
+
if (id.type !== 'identifier' || id.text === '_') continue;
|
|
1370
|
+
out.push({
|
|
1371
|
+
name: id.text,
|
|
1372
|
+
line: id.startPosition.row + 1,
|
|
1373
|
+
...(typeName && { type: typeName, origin }),
|
|
1374
|
+
...(qualifier && { qualifier }),
|
|
1375
|
+
...(valueFrom && { valueFrom }),
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return out;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* The file's build constraint (fix #399): the `//go:build` expression (or
|
|
1385
|
+
* legacy `// +build` lines) before the package clause, whitespace-normalized;
|
|
1386
|
+
* null for a file every build compiles (file-name GOOS/GOARCH suffixes are
|
|
1387
|
+
* judged from the path at query time).
|
|
1388
|
+
*/
|
|
1389
|
+
function goBuildConstraint(rootNode) {
|
|
1390
|
+
const legacy = [];
|
|
1391
|
+
for (const node of rootNode.namedChildren) {
|
|
1392
|
+
if (node.type === 'package_clause') break;
|
|
1393
|
+
if (node.type !== 'comment') continue;
|
|
1394
|
+
const text = node.text.trim();
|
|
1395
|
+
const modern = /^\/\/go:build\s+(.+)$/.exec(text);
|
|
1396
|
+
if (modern) return modern[1].replace(/\s+/g, ' ').trim();
|
|
1397
|
+
const old = /^\/\/\s*\+build\s+(.+)$/.exec(text);
|
|
1398
|
+
if (old) legacy.push(old[1].replace(/\s+/g, ' ').trim());
|
|
1399
|
+
}
|
|
1400
|
+
return legacy.length > 0 ? legacy.join(' && ') : null;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** Names the file declares at package level: vars, consts, types, funcs
|
|
1404
|
+
* (only vars when `varsOnly`). */
|
|
1405
|
+
function goFileLevelNames(rootNode, varsOnly = false) {
|
|
1406
|
+
const names = new Set();
|
|
1407
|
+
for (const declaration of rootNode.namedChildren) {
|
|
1408
|
+
if (declaration.type === 'var_declaration') {
|
|
1409
|
+
for (const spec of goVarSpecs(declaration)) {
|
|
1410
|
+
for (const id of spec.namedChildren) if (id.type === 'identifier') names.add(id.text);
|
|
1411
|
+
}
|
|
1412
|
+
} else if (varsOnly) {
|
|
1413
|
+
continue;
|
|
1414
|
+
} else if (declaration.type === 'const_declaration') {
|
|
1415
|
+
for (const spec of declaration.namedChildren) {
|
|
1416
|
+
if (spec.type !== 'const_spec') continue;
|
|
1417
|
+
for (const id of spec.namedChildren) if (id.type === 'identifier') names.add(id.text);
|
|
1418
|
+
}
|
|
1419
|
+
} else if (declaration.type === 'type_declaration') {
|
|
1420
|
+
for (const spec of declaration.namedChildren) {
|
|
1421
|
+
const nameNode = spec.childForFieldName?.('name');
|
|
1422
|
+
if (nameNode) names.add(nameNode.text);
|
|
1423
|
+
}
|
|
1424
|
+
} else if (declaration.type === 'function_declaration') {
|
|
1425
|
+
const nameNode = declaration.childForFieldName('name');
|
|
1426
|
+
if (nameNode) names.add(nameNode.text);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return names;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
602
1432
|
/**
|
|
603
1433
|
* Parse a Go file completely
|
|
604
1434
|
*/
|
|
@@ -622,13 +1452,31 @@ function parse(code, parser) {
|
|
|
622
1452
|
classes.sort((a, b) => a.startLine - b.startLine);
|
|
623
1453
|
stateObjects.sort((a, b) => a.startLine - b.startLine);
|
|
624
1454
|
|
|
1455
|
+
// The package clause (fix #383): an external test package
|
|
1456
|
+
// (`package x_test`) shares its directory with package x but not its
|
|
1457
|
+
// scope.
|
|
1458
|
+
let packageName = null;
|
|
1459
|
+
for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
|
|
1460
|
+
const child = tree.rootNode.namedChild(i);
|
|
1461
|
+
if (child.type !== 'package_clause') continue;
|
|
1462
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
1463
|
+
const id = child.namedChild(j);
|
|
1464
|
+
if (id.type === 'package_identifier' || id.type === 'identifier') { packageName = id.text; break; }
|
|
1465
|
+
}
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
625
1469
|
return {
|
|
626
1470
|
language: 'go',
|
|
627
1471
|
totalLines: lines.length,
|
|
628
1472
|
functions,
|
|
629
1473
|
classes,
|
|
630
1474
|
stateObjects,
|
|
631
|
-
...(
|
|
1475
|
+
...(packageName && { packageName }),
|
|
1476
|
+
packageVars: extractPackageVars(tree.rootNode),
|
|
1477
|
+
...(goBuildConstraint(tree.rootNode) && { buildConstraint: goBuildConstraint(tree.rootNode) }),
|
|
1478
|
+
...(tree.rootNode.hasError && { parseRecovery: true, parseErrorRegions: parseErrorRegions(tree.rootNode) }),
|
|
1479
|
+
typeConversions: extractTypeConversions(tree),
|
|
632
1480
|
imports: [],
|
|
633
1481
|
exports: []
|
|
634
1482
|
};
|
|
@@ -663,6 +1511,22 @@ const GO_BUILTINS = new Set([
|
|
|
663
1511
|
* own LHS position, single-value semantics
|
|
664
1512
|
* Identifier targets only; blank (`_`) targets return undefined.
|
|
665
1513
|
*/
|
|
1514
|
+
// Value positions of an instantiated generic function value (fix #378).
|
|
1515
|
+
const GENERIC_VALUE_PARENTS = new Set(['argument_list', 'literal_element', 'expression_list']);
|
|
1516
|
+
|
|
1517
|
+
// An expression_list holding VALUES: the right side of `=` / `:=`, a var
|
|
1518
|
+
// spec's values, or a return list (never an assignment's left side).
|
|
1519
|
+
function isValueExpressionList(list) {
|
|
1520
|
+
const owner = list.parent;
|
|
1521
|
+
if (!owner) return false;
|
|
1522
|
+
if (owner.type === 'return_statement') return true;
|
|
1523
|
+
if (owner.type === 'var_spec') return sameNode(owner.childForFieldName('value'), list);
|
|
1524
|
+
if (owner.type === 'short_var_declaration' || owner.type === 'assignment_statement') {
|
|
1525
|
+
return sameNode(owner.childForFieldName('right'), list);
|
|
1526
|
+
}
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
666
1530
|
function goAssignmentTargetOf(callNode) {
|
|
667
1531
|
let n = callNode;
|
|
668
1532
|
let p = n.parent;
|
|
@@ -750,6 +1614,114 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
750
1614
|
}
|
|
751
1615
|
return null;
|
|
752
1616
|
};
|
|
1617
|
+
// fix #383: the arguments of a route registration (`r.GET("/x", h, mw)`)
|
|
1618
|
+
// after its path: each argument's name, '<anonymous>' for a function
|
|
1619
|
+
// literal, the callee's name for a call, null for any other expression.
|
|
1620
|
+
// The router decides which one is the handler (gin: the last; echo: the
|
|
1621
|
+
// first, middleware follow it).
|
|
1622
|
+
const GO_ROUTE_VERBS = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE|Any|Handle|HandleFunc|Get|Post|Put|Delete|Patch|Head|Options|Connect|Trace|All|Match|Add)$/;
|
|
1623
|
+
const goArgName = (arg) => {
|
|
1624
|
+
if (arg.type === 'identifier') return arg.text;
|
|
1625
|
+
if (arg.type === 'selector_expression') return arg.childForFieldName('field')?.text || null;
|
|
1626
|
+
if (arg.type === 'func_literal') return '<anonymous>';
|
|
1627
|
+
if (arg.type === 'call_expression') {
|
|
1628
|
+
const fn = arg.childForFieldName('function');
|
|
1629
|
+
if (fn?.type === 'identifier') return fn.text;
|
|
1630
|
+
if (fn?.type === 'selector_expression') return fn.childForFieldName('field')?.text || null;
|
|
1631
|
+
}
|
|
1632
|
+
return null;
|
|
1633
|
+
};
|
|
1634
|
+
const getHandlerArgs = (callNode, skip = 1) => {
|
|
1635
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
1636
|
+
if (!argsNode) return null;
|
|
1637
|
+
const out = [];
|
|
1638
|
+
let seen = 0;
|
|
1639
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1640
|
+
const arg = argsNode.namedChild(i);
|
|
1641
|
+
if (arg.type.endsWith('comment')) continue;
|
|
1642
|
+
if (seen++ < skip) continue;
|
|
1643
|
+
out.push(goArgName(arg));
|
|
1644
|
+
}
|
|
1645
|
+
return out.length > 0 ? out : null;
|
|
1646
|
+
};
|
|
1647
|
+
// An HTTP method expression: "GET" or http.MethodGet.
|
|
1648
|
+
const goHttpMethodOf = (node) => {
|
|
1649
|
+
if (!node) return null;
|
|
1650
|
+
const lit = _extractStringArg(node);
|
|
1651
|
+
if (lit && !lit.interp && /^[A-Z]+$/.test(lit.value || '')) return lit.value;
|
|
1652
|
+
if (node.type === 'selector_expression') {
|
|
1653
|
+
const field = node.childForFieldName('field')?.text || '';
|
|
1654
|
+
if (/^Method[A-Z][a-z]+$/.test(field)) return field.slice('Method'.length).toUpperCase();
|
|
1655
|
+
}
|
|
1656
|
+
return null;
|
|
1657
|
+
};
|
|
1658
|
+
// fix #383: a registration that takes the method first
|
|
1659
|
+
// (`r.Handle(http.MethodGet, "/x", h)`, `e.Add("GET", "/x", h)`,
|
|
1660
|
+
// `e.Match([]string{"GET", "POST"}, "/x", h)`): its methods and path.
|
|
1661
|
+
const getMethodFirstRoute = (callNode) => {
|
|
1662
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
1663
|
+
if (!argsNode) return null;
|
|
1664
|
+
const args = [];
|
|
1665
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1666
|
+
const arg = argsNode.namedChild(i);
|
|
1667
|
+
if (!arg.type.endsWith('comment')) args.push(arg);
|
|
1668
|
+
}
|
|
1669
|
+
if (args.length < 2) return null;
|
|
1670
|
+
let methods = null;
|
|
1671
|
+
const single = goHttpMethodOf(args[0]);
|
|
1672
|
+
if (single) methods = [single];
|
|
1673
|
+
else if (args[0].type === 'composite_literal') {
|
|
1674
|
+
const body = args[0].childForFieldName('body');
|
|
1675
|
+
const items = [];
|
|
1676
|
+
for (let i = 0; body && i < body.namedChildCount; i++) {
|
|
1677
|
+
const element = body.namedChild(i);
|
|
1678
|
+
const value = element.type === 'literal_element' || element.type === 'element'
|
|
1679
|
+
? element.namedChild(0) : element;
|
|
1680
|
+
const method = goHttpMethodOf(value);
|
|
1681
|
+
if (!method) return null;
|
|
1682
|
+
items.push(method);
|
|
1683
|
+
}
|
|
1684
|
+
if (items.length > 0) methods = items;
|
|
1685
|
+
}
|
|
1686
|
+
if (!methods) return null;
|
|
1687
|
+
const pathLit = _extractStringArg(args[1]);
|
|
1688
|
+
if (!pathLit || !pathLit.value) return null;
|
|
1689
|
+
return { methods, path: pathLit.value, ...(pathLit.interp && { interp: true }) };
|
|
1690
|
+
};
|
|
1691
|
+
// fix #383: `http.NewRequest(method, url, body)` and
|
|
1692
|
+
// `NewRequestWithContext(ctx, method, url, body)`: the request's method
|
|
1693
|
+
// (a string or an `http.MethodX` constant) and URL are not the first
|
|
1694
|
+
// argument.
|
|
1695
|
+
const getRequestTarget = (callNode, name) => {
|
|
1696
|
+
const argsNode = callNode.childForFieldName('arguments');
|
|
1697
|
+
if (!argsNode) return null;
|
|
1698
|
+
const args = [];
|
|
1699
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
1700
|
+
const arg = argsNode.namedChild(i);
|
|
1701
|
+
if (!arg.type.endsWith('comment')) args.push(arg);
|
|
1702
|
+
}
|
|
1703
|
+
const offset = name === 'NewRequestWithContext' ? 1 : 0;
|
|
1704
|
+
const methodNode = args[offset];
|
|
1705
|
+
const urlNode = args[offset + 1];
|
|
1706
|
+
if (!methodNode || !urlNode) return null;
|
|
1707
|
+
let method = null;
|
|
1708
|
+
const m = _extractStringArg(methodNode);
|
|
1709
|
+
if (m && !m.interp && m.value) method = m.value.toUpperCase();
|
|
1710
|
+
else if (methodNode.type === 'selector_expression') {
|
|
1711
|
+
const field = methodNode.childForFieldName('field')?.text || '';
|
|
1712
|
+
if (/^Method[A-Z][a-z]+$/.test(field)) method = field.slice('Method'.length).toUpperCase();
|
|
1713
|
+
}
|
|
1714
|
+
let url = null;
|
|
1715
|
+
if (urlNode.type === 'call_expression') {
|
|
1716
|
+
const inner = urlNode.childForFieldName('function');
|
|
1717
|
+
if (inner?.type === 'selector_expression' && inner.childForFieldName('operand')?.text === 'fmt' &&
|
|
1718
|
+
inner.childForFieldName('field')?.text === 'Sprintf') url = _extractSprintfPrefix(urlNode);
|
|
1719
|
+
} else {
|
|
1720
|
+
url = _extractStringArg(urlNode);
|
|
1721
|
+
}
|
|
1722
|
+
if (!url || !url.value) return null;
|
|
1723
|
+
return { url: url.value, ...(url.interp && { interp: true }), ...(method && { method }) };
|
|
1724
|
+
};
|
|
753
1725
|
// Skip common non-function identifiers when detecting callback arguments
|
|
754
1726
|
const GO_SKIP_IDENTS = new Set(['nil', 'true', 'false', 'err', 'ctx', 'context', 'iota']);
|
|
755
1727
|
// Track local closures per function scope. The declared result belongs
|
|
@@ -797,43 +1769,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
797
1769
|
return ['function_declaration', 'method_declaration', 'func_literal'].includes(node.type);
|
|
798
1770
|
};
|
|
799
1771
|
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
if (!typeNode) return null;
|
|
803
|
-
if (typeNode.type === 'type_identifier') return typeNode.text;
|
|
804
|
-
if (typeNode.type === 'pointer_type') {
|
|
805
|
-
// *Framework -> Framework
|
|
806
|
-
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
807
|
-
const r = extractTypeName(typeNode.namedChild(i));
|
|
808
|
-
if (r) return r;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
if (typeNode.type === 'qualified_type') {
|
|
812
|
-
// pkg.Type -> Type
|
|
813
|
-
const tn = typeNode.childForFieldName('name');
|
|
814
|
-
if (tn) return tn.text;
|
|
815
|
-
}
|
|
816
|
-
return null;
|
|
817
|
-
};
|
|
818
|
-
|
|
819
|
-
// Preserve the package qualifier separately from the bare type name.
|
|
820
|
-
// `http.Handler` and a project-local `Handler` are different identities;
|
|
821
|
-
// dropping `http.` lets an unrelated same-named project type steal method
|
|
822
|
-
// dispatch. Pointer wrappers do not change that provenance.
|
|
823
|
-
const extractTypeQualifier = (typeNode) => {
|
|
824
|
-
if (!typeNode) return null;
|
|
825
|
-
if (typeNode.type === 'pointer_type') {
|
|
826
|
-
for (let i = 0; i < typeNode.namedChildCount; i++) {
|
|
827
|
-
const q = extractTypeQualifier(typeNode.namedChild(i));
|
|
828
|
-
if (q) return q;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (typeNode.type === 'qualified_type') {
|
|
832
|
-
const pkg = typeNode.childForFieldName('package') || typeNode.namedChild(0);
|
|
833
|
-
return pkg?.text || null;
|
|
834
|
-
}
|
|
835
|
-
return null;
|
|
836
|
-
};
|
|
1772
|
+
const extractTypeName = goTypeNameOf;
|
|
1773
|
+
const extractTypeQualifier = goTypeQualifierOf;
|
|
837
1774
|
|
|
838
1775
|
// Build type map from function/method parameters and receiver.
|
|
839
1776
|
// Also returns funcParamNames: parameter names with function types (func(...) ...)
|
|
@@ -930,7 +1867,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
930
1867
|
const allScopes = functionStack.map(scope => scope.startLine);
|
|
931
1868
|
let scopeChain = [allScopes[allScopes.length - 1]];
|
|
932
1869
|
if (refNode && name) {
|
|
933
|
-
const bindingScope =
|
|
1870
|
+
const bindingScope = bindingScopeOf(refNode, name);
|
|
934
1871
|
const bindingIndex = allScopes.lastIndexOf(bindingScope);
|
|
935
1872
|
if (bindingIndex >= 0) scopeChain = allScopes.slice(bindingIndex);
|
|
936
1873
|
}
|
|
@@ -962,7 +1899,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
962
1899
|
// Look up variable type from scope chain
|
|
963
1900
|
const getReceiverType = (varName, refNode, evidence = false) => {
|
|
964
1901
|
const bindingScope = refNode
|
|
965
|
-
?
|
|
1902
|
+
? bindingScopeOf(refNode, varName) : null;
|
|
966
1903
|
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
967
1904
|
const scopeStart = functionStack[i].startLine;
|
|
968
1905
|
const typeMap = scopeTypes.get(scopeStart);
|
|
@@ -974,7 +1911,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
974
1911
|
};
|
|
975
1912
|
const getReceiverTypeQualifier = (varName, refNode) => {
|
|
976
1913
|
const bindingScope = refNode
|
|
977
|
-
?
|
|
1914
|
+
? bindingScopeOf(refNode, varName) : null;
|
|
978
1915
|
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
979
1916
|
const scopeStart = functionStack[i].startLine;
|
|
980
1917
|
const qualifiers = scopeTypeQualifiers.get(scopeStart);
|
|
@@ -986,7 +1923,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
986
1923
|
};
|
|
987
1924
|
const getIndexedSource = (varName, refNode) => {
|
|
988
1925
|
const bindingScope = refNode
|
|
989
|
-
?
|
|
1926
|
+
? bindingScopeOf(refNode, varName) : null;
|
|
990
1927
|
for (let i = functionStack.length - 1; i >= 0; i--) {
|
|
991
1928
|
const scopeStart = functionStack[i].startLine;
|
|
992
1929
|
const sources = scopeIndexedSources.get(scopeStart);
|
|
@@ -1240,8 +2177,52 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1240
2177
|
}
|
|
1241
2178
|
return null;
|
|
1242
2179
|
};
|
|
1243
|
-
|
|
1244
|
-
|
|
2180
|
+
// Names the file binds locally anywhere (a superset of every binding
|
|
2181
|
+
// form lexicalBindingScopeStart recognizes), from one native query: a
|
|
2182
|
+
// package qualifier no local ever spells cannot be shadowed, so the
|
|
2183
|
+
// per-reference upward scope scan is skipped (fix #365).
|
|
2184
|
+
let localBindingNames = null;
|
|
2185
|
+
const locallyBound = (name) => {
|
|
2186
|
+
if (!localBindingNames) {
|
|
2187
|
+
localBindingNames = new Set();
|
|
2188
|
+
const collect = (node) => {
|
|
2189
|
+
for (const child of node.namedChildren) {
|
|
2190
|
+
if (child.type === 'identifier') localBindingNames.add(child.text);
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
for (const node of tree.rootNode.descendantsOfType([
|
|
2194
|
+
'short_var_declaration', 'range_clause', 'type_switch_statement',
|
|
2195
|
+
'var_spec', 'parameter_declaration', 'variadic_parameter_declaration',
|
|
2196
|
+
])) {
|
|
2197
|
+
collect(node);
|
|
2198
|
+
for (const child of node.namedChildren) {
|
|
2199
|
+
if (child.type === 'expression_list') collect(child);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return localBindingNames.has(name);
|
|
2204
|
+
};
|
|
2205
|
+
// One climb per reference: receiver typing and the package-scope test
|
|
2206
|
+
// ask the same question of the same node (fix #399).
|
|
2207
|
+
const bindingScopes = new Map();
|
|
2208
|
+
const bindingScopeOf = (refNode, name) => {
|
|
2209
|
+
if (!locallyBound(name)) return null;
|
|
2210
|
+
const key = `${refNode.startIndex}\0${name}`;
|
|
2211
|
+
if (bindingScopes.has(key)) return bindingScopes.get(key);
|
|
2212
|
+
const scope = lexicalBindingScopeStart(refNode, name);
|
|
2213
|
+
bindingScopes.set(key, scope);
|
|
2214
|
+
return scope;
|
|
2215
|
+
};
|
|
2216
|
+
const isShadowedByLocal = (refNode, name) => bindingScopeOf(refNode, name) != null;
|
|
2217
|
+
let fileLevelNameSet = null;
|
|
2218
|
+
const fileLevelNames = () => (fileLevelNameSet || (fileLevelNameSet = goFileLevelNames(tree.rootNode)));
|
|
2219
|
+
let fileVarNameSet = null;
|
|
2220
|
+
const fileVarNames = () => (fileVarNameSet || (fileVarNameSet = goFileLevelNames(tree.rootNode, true)));
|
|
2221
|
+
// An identifier operand naming an import that no local binding shadows
|
|
2222
|
+
// is a package qualifier: `pkg.Name` then denotes a package-level
|
|
2223
|
+
// symbol (function, type, var or const), never a member of a value.
|
|
2224
|
+
const isPackageQualifier = (operandNode, receiver) => !!(receiver &&
|
|
2225
|
+
importAliases.has(receiver) && !isShadowedByLocal(operandNode, receiver));
|
|
1245
2226
|
|
|
1246
2227
|
// Go package declarations are in scope throughout the file, including
|
|
1247
2228
|
// function bodies preceding the declaration. Locals still enter in order.
|
|
@@ -1592,6 +2573,67 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1592
2573
|
}
|
|
1593
2574
|
}
|
|
1594
2575
|
|
|
2576
|
+
// Explicit generic instantiation calls (fix #378): tree-sitter-go
|
|
2577
|
+
// parses `f[T](x)` and `pkg.F[T](x)` (one or more type arguments on
|
|
2578
|
+
// a callee it cannot tell from a type) as a type_conversion_expression
|
|
2579
|
+
// over a generic_type; only a multi-argument `f[A, B](x)` with a bare
|
|
2580
|
+
// callee comes out as a call_expression. Both are calls of the named
|
|
2581
|
+
// function (or conversions to the named generic type, recorded like
|
|
2582
|
+
// `T(x)`). The same syntax indexes a local slice or map of funcs
|
|
2583
|
+
// (`handlers[i](x)`) or a field (`s.m[k](x)`): a locally bound or
|
|
2584
|
+
// function-typed-parameter base, or a qualifier that is not an
|
|
2585
|
+
// import, is an index expression and emits nothing.
|
|
2586
|
+
if (node.type === 'type_conversion_expression') {
|
|
2587
|
+
const typeNode = node.childForFieldName('type');
|
|
2588
|
+
const base = typeNode?.type === 'generic_type'
|
|
2589
|
+
? typeNode.childForFieldName('type') : null;
|
|
2590
|
+
let callName = null;
|
|
2591
|
+
let pkgReceiver;
|
|
2592
|
+
let nameNode = null;
|
|
2593
|
+
if (base?.type === 'type_identifier') {
|
|
2594
|
+
const name = base.text;
|
|
2595
|
+
if (!GO_BUILTINS.has(name) && !getLocalClosure(name) &&
|
|
2596
|
+
!isFuncTypedParam(name) && !isShadowedByLocal(base, name)) {
|
|
2597
|
+
callName = name;
|
|
2598
|
+
nameNode = base;
|
|
2599
|
+
}
|
|
2600
|
+
} else if (base?.type === 'qualified_type') {
|
|
2601
|
+
const pkgNode = base.childForFieldName('package');
|
|
2602
|
+
const fieldNode = base.childForFieldName('name');
|
|
2603
|
+
if (pkgNode && fieldNode && importAliases.has(pkgNode.text) &&
|
|
2604
|
+
!isShadowedByLocal(pkgNode, pkgNode.text)) {
|
|
2605
|
+
callName = fieldNode.text;
|
|
2606
|
+
pkgReceiver = pkgNode.text;
|
|
2607
|
+
nameNode = fieldNode;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
if (callName) {
|
|
2611
|
+
const operand = node.childForFieldName('operand');
|
|
2612
|
+
const assigned = goAssignmentTargetOf(node);
|
|
2613
|
+
calls.push({
|
|
2614
|
+
name: callName,
|
|
2615
|
+
line: nameNode.startPosition.row + 1,
|
|
2616
|
+
column: nameNode.startPosition.column,
|
|
2617
|
+
callStart: node.startIndex,
|
|
2618
|
+
callEnd: node.endIndex,
|
|
2619
|
+
isMethod: false,
|
|
2620
|
+
...(pkgReceiver && { receiver: pkgReceiver }),
|
|
2621
|
+
argCount: operand ? 1 : 0,
|
|
2622
|
+
...(assigned && { assignedTo: assigned.assignedTo }),
|
|
2623
|
+
...(assigned?.assignedTuple && { assignedTuple: true }),
|
|
2624
|
+
...(assigned?.assignedTupleIndex != null && {
|
|
2625
|
+
assignedTupleIndex: assigned.assignedTupleIndex,
|
|
2626
|
+
}),
|
|
2627
|
+
...(assigned?.assignedTupleTargets && {
|
|
2628
|
+
assignedTupleTargets: assigned.assignedTupleTargets,
|
|
2629
|
+
}),
|
|
2630
|
+
...(assigned?.assignedTupleRest && { assignedTupleRest: assigned.assignedTupleRest }),
|
|
2631
|
+
enclosingFunction: getCurrentEnclosingFunction(),
|
|
2632
|
+
uncertain: false,
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
1595
2637
|
// Handle function calls: foo(), pkg.Foo(), obj.Method()
|
|
1596
2638
|
if (node.type === 'call_expression') {
|
|
1597
2639
|
const funcNode = node.childForFieldName('function');
|
|
@@ -1691,13 +2733,25 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1691
2733
|
} else if (funcNode.type === 'selector_expression') {
|
|
1692
2734
|
// Method or package call: obj.Method() or pkg.Func()
|
|
1693
2735
|
const fieldNode = funcNode.childForFieldName('field');
|
|
1694
|
-
|
|
2736
|
+
let operandNode = funcNode.childForFieldName('operand');
|
|
2737
|
+
// fix #381: `c.Load()` after `c := s.config` receives like
|
|
2738
|
+
// `s.config.Load()`.
|
|
2739
|
+
if (operandNode?.type === 'identifier' && !importAliases.has(operandNode.text) &&
|
|
2740
|
+
!getReceiverType(operandNode.text, operandNode)) {
|
|
2741
|
+
const aliased = goFieldAliasOf(operandNode);
|
|
2742
|
+
if (aliased) operandNode = aliased;
|
|
2743
|
+
}
|
|
1695
2744
|
|
|
1696
2745
|
if (fieldNode) {
|
|
1697
2746
|
const receiver = operandNode?.type === 'identifier' ? operandNode.text : undefined;
|
|
1698
2747
|
// Distinguish pkg.Func() (package-qualified) from obj.Method()
|
|
1699
2748
|
// If receiver is a known import alias, this is a package call, not a method call
|
|
1700
|
-
|
|
2749
|
+
// A local binding shadows a same-named import inside its
|
|
2750
|
+
// scope (`fs := NewMemMapFs(); fs.Chown(...)` in a file
|
|
2751
|
+
// importing "io/fs"): that call is a METHOD call on the
|
|
2752
|
+
// local, never the package function (fix #360).
|
|
2753
|
+
const isPkgCall = receiver && importAliases.has(receiver) &&
|
|
2754
|
+
!isShadowedByLocal(operandNode, receiver);
|
|
1701
2755
|
let receiverTypeEvidence;
|
|
1702
2756
|
let receiverType = (!isPkgCall && receiver)
|
|
1703
2757
|
? getReceiverType(receiver, operandNode) : undefined;
|
|
@@ -1705,6 +2759,15 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1705
2759
|
? getReceiverTypeQualifier(receiver, operandNode) : undefined;
|
|
1706
2760
|
const receiverIndexedSource = !isPkgCall && receiver && !receiverType
|
|
1707
2761
|
? getIndexedSource(receiver, operandNode) : undefined;
|
|
2762
|
+
// A receiver no scope of this file binds names a
|
|
2763
|
+
// package-level declaration of another file of the
|
|
2764
|
+
// package (fix #399): typed at query time. So is an
|
|
2765
|
+
// untyped package-level variable of this file, whose
|
|
2766
|
+
// value may come from another package (fix #400).
|
|
2767
|
+
const receiverPackageScope = !!(receiver && !isPkgCall && !receiverType &&
|
|
2768
|
+
!receiverIndexedSource &&
|
|
2769
|
+
(!fileLevelNames().has(receiver) || fileVarNames().has(receiver)) &&
|
|
2770
|
+
!isShadowedByLocal(operandNode, receiver));
|
|
1708
2771
|
// Composite-literal receiver (fix #298):
|
|
1709
2772
|
// (&Kit{...}).Run(...) — compiler-true type, never guessed.
|
|
1710
2773
|
if (!receiver && !receiverType) {
|
|
@@ -1728,7 +2791,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1728
2791
|
receiverRoot = rootNode.text;
|
|
1729
2792
|
receiverRootNode = rootNode;
|
|
1730
2793
|
receiverFieldName = fldNode.text;
|
|
1731
|
-
if (importAliases.has(rootNode.text)
|
|
2794
|
+
if (importAliases.has(rootNode.text) &&
|
|
2795
|
+
!isShadowedByLocal(rootNode, rootNode.text)) {
|
|
1732
2796
|
// Package-owned value receiver:
|
|
1733
2797
|
// io.Discard.Write(...). The package qualifier
|
|
1734
2798
|
// is identity evidence even though the value's
|
|
@@ -1786,7 +2850,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1786
2850
|
// Selector records report the FIELD's own line
|
|
1787
2851
|
// (fix #223 name-node convention)
|
|
1788
2852
|
receiverCallLine = pf.startPosition.row + 1;
|
|
1789
|
-
if (po?.type === 'identifier' && importAliases.has(po.text)
|
|
2853
|
+
if (po?.type === 'identifier' && importAliases.has(po.text) &&
|
|
2854
|
+
!isShadowedByLocal(po, po.text)) {
|
|
1790
2855
|
receiverCallReceiver = po.text;
|
|
1791
2856
|
} else {
|
|
1792
2857
|
receiverCallIsMethod = true;
|
|
@@ -1795,6 +2860,12 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1795
2860
|
}
|
|
1796
2861
|
}
|
|
1797
2862
|
const firstArg = getFirstStringArg(node);
|
|
2863
|
+
const methodFirstRoute = !firstArg && GO_ROUTE_VERBS.test(fieldNode.text) && argCount >= 2
|
|
2864
|
+
? getMethodFirstRoute(node) : null;
|
|
2865
|
+
const handlerArgs = (firstArg || methodFirstRoute) && GO_ROUTE_VERBS.test(fieldNode.text) && argCount >= 2
|
|
2866
|
+
? getHandlerArgs(node, methodFirstRoute ? 2 : 1) : null;
|
|
2867
|
+
const requestTarget = fieldNode.text === 'NewRequest' || fieldNode.text === 'NewRequestWithContext'
|
|
2868
|
+
? getRequestTarget(node, fieldNode.text) : null;
|
|
1798
2869
|
const receiverBindingNode = receiver ? operandNode : receiverRootNode;
|
|
1799
2870
|
const receiverBindingName = receiver || receiverRoot;
|
|
1800
2871
|
const methodEnclosingFunction = !isPkgCall &&
|
|
@@ -1829,6 +2900,7 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1829
2900
|
}),
|
|
1830
2901
|
...(receiverFieldName && { receiverRoot, receiverField: receiverFieldName }),
|
|
1831
2902
|
...(receiverRootIsModule && { receiverRootIsModule: true }),
|
|
2903
|
+
...(receiverPackageScope && { receiverPackageScope: true }),
|
|
1832
2904
|
...(receiverFieldName && receiverRootType && { receiverRootType }),
|
|
1833
2905
|
...(receiverFieldName && receiverRootTypeQualifier && { receiverRootTypeQualifier }),
|
|
1834
2906
|
...(receiverFieldName && receiverRootType && receiverRootTypeGuessed && { receiverRootTypeGuessed: true }),
|
|
@@ -1855,7 +2927,10 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1855
2927
|
...(assigned?.assignedTupleRest && { assignedTupleRest: assigned.assignedTupleRest }),
|
|
1856
2928
|
enclosingFunction: methodEnclosingFunction,
|
|
1857
2929
|
uncertain,
|
|
1858
|
-
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
|
|
2930
|
+
...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp }),
|
|
2931
|
+
...(handlerArgs && { handlerArgs }),
|
|
2932
|
+
...(methodFirstRoute && { methodFirstRoute }),
|
|
2933
|
+
...(requestTarget && { requestTarget })
|
|
1859
2934
|
});
|
|
1860
2935
|
}
|
|
1861
2936
|
}
|
|
@@ -1877,7 +2952,10 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1877
2952
|
// `Outer{ field: Inner{...} }`. Both the outer and inner are real
|
|
1878
2953
|
// constructors, so we DO emit each, but we must not emit the same
|
|
1879
2954
|
// node twice. Tree-sitter visits each node once, so this is fine.
|
|
1880
|
-
const
|
|
2955
|
+
const literalType = node.childForFieldName('type');
|
|
2956
|
+
// Box[int]{...} constructs the generic type Box (fix #378).
|
|
2957
|
+
const typeNode = literalType?.type === 'generic_type'
|
|
2958
|
+
? literalType.childForFieldName('type') : literalType;
|
|
1881
2959
|
if (typeNode) {
|
|
1882
2960
|
let typeName = null;
|
|
1883
2961
|
let typeQualifier = null;
|
|
@@ -1920,6 +2998,9 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1920
2998
|
const operandNode = node.childForFieldName('operand');
|
|
1921
2999
|
if (fieldNode && operandNode) {
|
|
1922
3000
|
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
3001
|
+
// `pkg.Func` as a value is a package-qualified function
|
|
3002
|
+
// reference, never a method value (fix #378).
|
|
3003
|
+
const pkgValue = isPackageQualifier(operandNode, receiver);
|
|
1923
3004
|
let receiverTypeEvidence;
|
|
1924
3005
|
let receiverType = receiver
|
|
1925
3006
|
? getReceiverType(receiver, operandNode) : undefined;
|
|
@@ -1942,7 +3023,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1942
3023
|
name: fieldNode.text,
|
|
1943
3024
|
line: fieldNode.startPosition.row + 1,
|
|
1944
3025
|
column: fieldNode.startPosition.column,
|
|
1945
|
-
isMethod:
|
|
3026
|
+
isMethod: !pkgValue,
|
|
3027
|
+
...(pkgValue && { isFunctionReference: true }),
|
|
1946
3028
|
receiver,
|
|
1947
3029
|
...(receiverType && { receiverType, ...(receiverTypeEvidence ? { receiverTypeSource: 'constructor', receiverTypeEvidence } : getReceiverType(receiver, node, true) || { receiverTypeSource: 'unknown' }) }),
|
|
1948
3030
|
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
@@ -1955,6 +3037,51 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1955
3037
|
}
|
|
1956
3038
|
}
|
|
1957
3039
|
|
|
3040
|
+
// Instantiated generic function VALUES (fix #378): `id[int]` /
|
|
3041
|
+
// `pkg.F[int]` in a value position (argument, right-hand side,
|
|
3042
|
+
// return value, literal element) reference the generic function.
|
|
3043
|
+
// The same syntax indexes a slice or map: a locally bound operand is
|
|
3044
|
+
// an index, and a package-level non-function never matches a
|
|
3045
|
+
// function pin (Go forbids a same-package var and func of one name).
|
|
3046
|
+
if (node.type === 'index_expression' && GENERIC_VALUE_PARENTS.has(node.parent?.type) &&
|
|
3047
|
+
(node.parent.type !== 'expression_list' || isValueExpressionList(node.parent))) {
|
|
3048
|
+
const operandNode = node.childForFieldName('operand');
|
|
3049
|
+
if (operandNode?.type === 'identifier') {
|
|
3050
|
+
const name = operandNode.text;
|
|
3051
|
+
if (!GO_SKIP_IDENTS.has(name) && !GO_BUILTINS.has(name) &&
|
|
3052
|
+
!importAliases.has(name) && !isShadowedByLocal(operandNode, name) &&
|
|
3053
|
+
!isFuncTypedParam(name) && !getLocalClosure(name)) {
|
|
3054
|
+
calls.push({
|
|
3055
|
+
name,
|
|
3056
|
+
line: operandNode.startPosition.row + 1,
|
|
3057
|
+
column: operandNode.startPosition.column,
|
|
3058
|
+
isMethod: false,
|
|
3059
|
+
isFunctionReference: true,
|
|
3060
|
+
isPotentialCallback: true,
|
|
3061
|
+
enclosingFunction: getCurrentEnclosingFunction(),
|
|
3062
|
+
uncertain: false,
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
} else if (operandNode?.type === 'selector_expression') {
|
|
3066
|
+
const qualifier = operandNode.childForFieldName('operand');
|
|
3067
|
+
const fieldNode = operandNode.childForFieldName('field');
|
|
3068
|
+
const receiver = qualifier?.type === 'identifier' ? qualifier.text : undefined;
|
|
3069
|
+
if (fieldNode && isPackageQualifier(qualifier, receiver)) {
|
|
3070
|
+
calls.push({
|
|
3071
|
+
name: fieldNode.text,
|
|
3072
|
+
line: fieldNode.startPosition.row + 1,
|
|
3073
|
+
column: fieldNode.startPosition.column,
|
|
3074
|
+
isMethod: false,
|
|
3075
|
+
receiver,
|
|
3076
|
+
isFunctionReference: true,
|
|
3077
|
+
isPotentialCallback: true,
|
|
3078
|
+
enclosingFunction: getCurrentEnclosingFunction(),
|
|
3079
|
+
uncertain: false,
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
|
|
1958
3085
|
// Detect plain identifier function references passed as arguments:
|
|
1959
3086
|
// e.g., r.GET("/users", listUsers) — listUsers is a plain identifier in argument_list
|
|
1960
3087
|
if (node.type === 'identifier' && node.parent?.type === 'argument_list') {
|
|
@@ -1980,11 +3107,14 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1980
3107
|
// Pattern 2: sched.SchedulePod = schedulePod (field = function reference)
|
|
1981
3108
|
// Pattern 3: var handler = processEvent (short variable = function reference)
|
|
1982
3109
|
// The RHS is a function reference (not a call — no parentheses)
|
|
1983
|
-
|
|
3110
|
+
// `var handler = processEvent` / `var f = pkg.F` values too (fix #378).
|
|
3111
|
+
if (node.type === 'assignment_statement' || node.type === 'short_var_declaration' ||
|
|
3112
|
+
node.type === 'var_spec') {
|
|
1984
3113
|
// Skip blank identifier assignments: _ = x (used to suppress unused warnings)
|
|
1985
|
-
const
|
|
3114
|
+
const isVarSpec = node.type === 'var_spec';
|
|
3115
|
+
const left = node.childForFieldName(isVarSpec ? 'name' : 'left');
|
|
1986
3116
|
const isBlankAssign = left && left.text.trim() === '_';
|
|
1987
|
-
const right = isBlankAssign ? null : node.childForFieldName('right');
|
|
3117
|
+
const right = isBlankAssign ? null : node.childForFieldName(isVarSpec ? 'value' : 'right');
|
|
1988
3118
|
if (right) {
|
|
1989
3119
|
// Walk through the expression list (could be multiple assignments)
|
|
1990
3120
|
const rhsNodes = right.type === 'expression_list' ? right.namedChildren : [right];
|
|
@@ -1995,6 +3125,9 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
1995
3125
|
const operandNode = rhs.childForFieldName('operand');
|
|
1996
3126
|
if (fieldNode && operandNode) {
|
|
1997
3127
|
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
3128
|
+
// `pkg.Func` as a value is a package-qualified function
|
|
3129
|
+
// reference, never a method value (fix #378).
|
|
3130
|
+
const pkgValue = isPackageQualifier(operandNode, receiver);
|
|
1998
3131
|
let receiverTypeEvidence;
|
|
1999
3132
|
let receiverType = receiver
|
|
2000
3133
|
? getReceiverType(receiver, operandNode) : undefined;
|
|
@@ -2014,7 +3147,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
2014
3147
|
name: fieldNode.text,
|
|
2015
3148
|
line: fieldNode.startPosition.row + 1,
|
|
2016
3149
|
column: fieldNode.startPosition.column,
|
|
2017
|
-
isMethod:
|
|
3150
|
+
isMethod: !pkgValue,
|
|
3151
|
+
...(pkgValue && { isFunctionReference: true }),
|
|
2018
3152
|
receiver,
|
|
2019
3153
|
...(receiverType && { receiverType, ...(receiverTypeEvidence ? { receiverTypeSource: 'constructor', receiverTypeEvidence } : getReceiverType(receiver, node, true) || { receiverTypeSource: 'unknown' }) }),
|
|
2020
3154
|
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
@@ -2064,6 +3198,9 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
2064
3198
|
const operandNode = val.childForFieldName('operand');
|
|
2065
3199
|
if (!fieldNode || !operandNode) continue;
|
|
2066
3200
|
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
3201
|
+
// `pkg.Func` as a value is a package-qualified function
|
|
3202
|
+
// reference, never a method value (fix #378).
|
|
3203
|
+
const pkgValue = isPackageQualifier(operandNode, receiver);
|
|
2067
3204
|
let receiverTypeEvidence;
|
|
2068
3205
|
let receiverType = receiver
|
|
2069
3206
|
? getReceiverType(receiver, operandNode) : undefined;
|
|
@@ -2082,7 +3219,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
2082
3219
|
// #223 name-node convention: the field's own line.
|
|
2083
3220
|
line: fieldNode.startPosition.row + 1,
|
|
2084
3221
|
column: fieldNode.startPosition.column,
|
|
2085
|
-
isMethod:
|
|
3222
|
+
isMethod: !pkgValue,
|
|
3223
|
+
...(pkgValue && { isFunctionReference: true }),
|
|
2086
3224
|
receiver,
|
|
2087
3225
|
...(receiverType && { receiverType, ...(receiverTypeEvidence ? { receiverTypeSource: 'constructor', receiverTypeEvidence } : getReceiverType(receiver, node, true) || { receiverTypeSource: 'unknown' }) }),
|
|
2088
3226
|
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
@@ -2154,6 +3292,9 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
2154
3292
|
const operandNode = valueNode.childForFieldName('operand');
|
|
2155
3293
|
if (fieldNode && operandNode) {
|
|
2156
3294
|
const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
|
|
3295
|
+
// `pkg.Func` as a value is a package-qualified function
|
|
3296
|
+
// reference, never a method value (fix #378).
|
|
3297
|
+
const pkgValue = isPackageQualifier(operandNode, receiver);
|
|
2157
3298
|
let receiverTypeEvidence;
|
|
2158
3299
|
let receiverType = receiver
|
|
2159
3300
|
? getReceiverType(receiver, operandNode) : undefined;
|
|
@@ -2173,7 +3314,8 @@ function findCallsInCode(code, parser, options = {}) {
|
|
|
2173
3314
|
name: fieldNode.text,
|
|
2174
3315
|
line: fieldNode.startPosition.row + 1,
|
|
2175
3316
|
column: fieldNode.startPosition.column,
|
|
2176
|
-
isMethod:
|
|
3317
|
+
isMethod: !pkgValue,
|
|
3318
|
+
...(pkgValue && { isFunctionReference: true }),
|
|
2177
3319
|
receiver,
|
|
2178
3320
|
...(receiverType && { receiverType, ...(receiverTypeEvidence ? { receiverTypeSource: 'constructor', receiverTypeEvidence } : getReceiverType(receiver, node, true) || { receiverTypeSource: 'unknown' }) }),
|
|
2179
3321
|
...(receiverTypeQualifier && { receiverTypeQualifier }),
|
|
@@ -2264,19 +3406,16 @@ function findImportsInCode(code, parser) {
|
|
|
2264
3406
|
if (modulePath) {
|
|
2265
3407
|
// Package name is last segment of path, skipping Go version suffixes (v2, v3, etc.)
|
|
2266
3408
|
let pkgName = alias;
|
|
2267
|
-
if (!pkgName)
|
|
2268
|
-
const parts = modulePath.split('/');
|
|
2269
|
-
// Go convention: if last segment matches /^v\d+$/, use the previous segment
|
|
2270
|
-
// e.g., k8s.io/klog/v2 → klog, github.com/foo/bar/v3 → bar
|
|
2271
|
-
const last = parts[parts.length - 1];
|
|
2272
|
-
pkgName = (/^v\d+$/.test(last) && parts.length > 1) ? parts[parts.length - 2] : last;
|
|
2273
|
-
}
|
|
3409
|
+
if (!pkgName) pkgName = goPathPackageName(modulePath);
|
|
2274
3410
|
imports.push({
|
|
2275
3411
|
module: modulePath,
|
|
2276
3412
|
names: [pkgName],
|
|
2277
3413
|
type: importType,
|
|
2278
3414
|
dynamic,
|
|
2279
|
-
line
|
|
3415
|
+
line,
|
|
3416
|
+
// No written name: the local name is the imported package's
|
|
3417
|
+
// own package clause, which the path only suggests (fix #400).
|
|
3418
|
+
...(!alias && { implicitName: true }),
|
|
2280
3419
|
});
|
|
2281
3420
|
}
|
|
2282
3421
|
}
|
|
@@ -2393,9 +3532,35 @@ function findExportsInCode(code, parser) {
|
|
|
2393
3532
|
* @param {object} [tree] - Pre-parsed tree (per-operation cache); parsed here when absent
|
|
2394
3533
|
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
2395
3534
|
*/
|
|
2396
|
-
|
|
3535
|
+
/**
|
|
3536
|
+
* The callee name node of an explicit generic instantiation call:
|
|
3537
|
+
* `f[T](x)` (type_identifier under generic_type) or `pkg.F[T](x)` (the name
|
|
3538
|
+
* of a qualified_type under generic_type), the generic_type being the `type`
|
|
3539
|
+
* of a type_conversion_expression (fix #378), or of a composite literal
|
|
3540
|
+
* (`Box[int]{...}`, a construction like `Box{...}`).
|
|
3541
|
+
*/
|
|
3542
|
+
function isGenericInstantiationCallee(node) {
|
|
3543
|
+
let base = node.parent;
|
|
3544
|
+
if (base?.type === 'qualified_type') {
|
|
3545
|
+
if (!sameNode(base.childForFieldName('name'), node)) return false;
|
|
3546
|
+
} else {
|
|
3547
|
+
base = node;
|
|
3548
|
+
}
|
|
3549
|
+
const generic = base.parent;
|
|
3550
|
+
if (generic?.type !== 'generic_type' ||
|
|
3551
|
+
!sameNode(generic.childForFieldName('type'), base)) return false;
|
|
3552
|
+
const conversion = generic.parent;
|
|
3553
|
+
// `Box[int]{...}` constructs like `Box{...}`.
|
|
3554
|
+
return (conversion?.type === 'type_conversion_expression' ||
|
|
3555
|
+
conversion?.type === 'composite_literal') &&
|
|
3556
|
+
sameNode(conversion.childForFieldName('type'), generic);
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
function findUsagesInCode(code, name, parser, tree, options = {}) {
|
|
2397
3560
|
tree = tree || parseTree(parser, code);
|
|
2398
3561
|
const usages = [];
|
|
3562
|
+
// Lexical scope verdicts (fix #392) only for refactoring internals.
|
|
3563
|
+
const scopeMemo = options.lexicalScopes ? new Map() : null;
|
|
2399
3564
|
|
|
2400
3565
|
visitNameNodes(tree, code, name, (node) => {
|
|
2401
3566
|
// Look for identifier, field_identifier (method names in selector expressions),
|
|
@@ -2462,6 +3627,11 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
2462
3627
|
sameNode(parent.childForFieldName('name'), node)) {
|
|
2463
3628
|
usageType = 'definition';
|
|
2464
3629
|
}
|
|
3630
|
+
// Generic instantiation call: `f[T](x)` / `pkg.F[T](x)` parse as a
|
|
3631
|
+
// type_conversion_expression over generic_type (fix #378).
|
|
3632
|
+
else if (isGenericInstantiationCallee(node)) {
|
|
3633
|
+
usageType = 'call';
|
|
3634
|
+
}
|
|
2465
3635
|
// Composite literal: Type{} — type_identifier is the type of composite_literal
|
|
2466
3636
|
else if (parent.type === 'composite_literal' &&
|
|
2467
3637
|
sameNode(parent.childForFieldName('type'), node)) {
|
|
@@ -2484,7 +3654,10 @@ function findUsagesInCode(code, name, parser, tree) {
|
|
|
2484
3654
|
}
|
|
2485
3655
|
}
|
|
2486
3656
|
|
|
2487
|
-
|
|
3657
|
+
// Where a bare reference resolves (fix #392).
|
|
3658
|
+
const scope = scopeMemo && usageType === 'reference'
|
|
3659
|
+
? scopeFields(referenceScope(node, 'go', scopeMemo)) : null;
|
|
3660
|
+
usages.push({ line, column, usageType, ...scope });
|
|
2488
3661
|
return true;
|
|
2489
3662
|
});
|
|
2490
3663
|
|