ucn 5.1.1 → 5.2.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/languages/go.js CHANGED
@@ -227,6 +227,11 @@ function _processClass(node, types, processedRanges, lines) {
227
227
 
228
228
  const embeddedBases = members
229
229
  .filter(m => m.embedded)
230
+ // The inheritance graph is keyed by project type names.
231
+ // Keep the historical bare name here so promoted methods
232
+ // remain connected; the field symbol separately retains
233
+ // the qualified authored type (`io.Reader`) for consumers
234
+ // which must distinguish an open external method set.
230
235
  .map(m => m.name);
231
236
 
232
237
  types.push({
@@ -641,9 +646,11 @@ const GO_BUILTINS = new Set([
641
646
  /**
642
647
  * Variable receiving this call's result (fix #207 return-type flow):
643
648
  * bb := balancer.Get(n) → { assignedTo: 'bb' }
644
- * x, err := pkg.Make() → { assignedTo: 'x', assignedTuple: true }
645
- * (tuple unpack — the flow map pairs the first
646
- * return element with the first variable)
649
+ * x, err := pkg.Make() → { assignedTo: 'x', assignedTuple: true,
650
+ * assignedTupleTargets: [{name:'x', index:0},
651
+ * {name:'err', index:1}] }
652
+ * (tuple unpack — the flow map pairs every named
653
+ * target with its declared return position)
647
654
  * y = q() → { assignedTo: 'y' } (plain `=` only — `+=` etc.
648
655
  * don't bind the call's type to the variable)
649
656
  * a, b := g(), h() → parallel assignment: each call pairs with its
@@ -679,19 +686,24 @@ function goAssignmentTargetOf(callNode) {
679
686
  return target?.type === 'identifier' && target.text !== '_'
680
687
  ? { assignedTo: target.text } : undefined;
681
688
  }
682
- const target = names[0];
683
- if (target?.type !== 'identifier' || target.text === '_') return undefined;
684
689
  if (names.length > 1) {
685
- // All LHS names (fix #220): an EXTERNAL producer decides every tuple
686
- // element's type, not just the first — `tmpFile, err := os.CreateTemp`
687
- // marks err external-flow too. The TYPED flow keeps pairing only
688
- // element 0 with the producer's return tuple (#207).
689
- const rest = names.slice(1)
690
- .filter(t => t.type === 'identifier' && t.text !== '_')
691
- .map(t => t.text);
692
- return { assignedTo: target.text, assignedTuple: true,
693
- ...(rest.length > 0 && { assignedTupleRest: rest }) };
690
+ const targets = names
691
+ .map((item, index) => ({ item, index }))
692
+ .filter(({ item }) => item.type === 'identifier' && item.text !== '_')
693
+ .map(({ item, index }) => ({ name: item.text, index }));
694
+ if (targets.length === 0) return undefined;
695
+ const [target, ...restTargets] = targets;
696
+ const rest = restTargets.map(item => item.name);
697
+ return {
698
+ assignedTo: target.name,
699
+ assignedTuple: true,
700
+ assignedTupleIndex: target.index,
701
+ assignedTupleTargets: targets,
702
+ ...(rest.length > 0 && { assignedTupleRest: rest }),
703
+ };
694
704
  }
705
+ const target = names[0];
706
+ if (target?.type !== 'identifier' || target.text === '_') return undefined;
695
707
  return { assignedTo: target.text };
696
708
  }
697
709
 
@@ -727,7 +739,10 @@ function findCallsInCode(code, parser, options = {}) {
727
739
  };
728
740
  // Skip common non-function identifiers when detecting callback arguments
729
741
  const GO_SKIP_IDENTS = new Set(['nil', 'true', 'false', 'err', 'ctx', 'context', 'iota']);
730
- // Track local closure names per function scope (scopeStartLine -> Set<name>)
742
+ // Track local closures per function scope. The declared result belongs
743
+ // to the lexical value (not to a package-level function with the same
744
+ // name), but it is still compiler-grade assignment-flow evidence.
745
+ // scopeStartLine -> Map<name, { returnType?: string }>
731
746
  const closureScopes = new Map();
732
747
  // Track variable -> type mappings per function scope (scopeStartLine -> Map<varName, typeName>)
733
748
  const scopeTypes = new Map();
@@ -741,6 +756,12 @@ function findCallsInCode(code, parser, options = {}) {
741
756
  const packageTypeQualifiers = new Map();
742
757
  // Names whose scope type is a New*-prefix GUESS (fix #266) — per scope
743
758
  const scopeGuesses = new Map();
759
+ // Container-typed variables (slice/map/array declared TYPE TEXT) for
760
+ // range-element typing (fix #300, mux-measured): `for _, route := range
761
+ // r.routes` types route from the field's declared []*Route. Element
762
+ // types are compiler-true — Go range yields the container's element.
763
+ const scopeContainerTypes = new Map(); // scopeStartLine -> Map<name, typeText>
764
+ const packageContainerTypes = new Map();
744
765
  // Track function-typed parameter names per scope (scopeStartLine -> Set<name>)
745
766
  const funcParamScopes = new Map();
746
767
 
@@ -804,6 +825,7 @@ function findCallsInCode(code, parser, options = {}) {
804
825
  const typeMap = new Map();
805
826
  const typeQualifierMap = new Map();
806
827
  const funcParamNames = new Set();
828
+ const containerMap = new Map();
807
829
 
808
830
  // Method receiver: func (f *Framework) Method()
809
831
  if (node.type === 'method_declaration') {
@@ -844,6 +866,10 @@ function findCallsInCode(code, parser, options = {}) {
844
866
  if (nameNodes.length === 0) continue;
845
867
  if (typeNode.type === 'function_type') {
846
868
  for (const nn of nameNodes) funcParamNames.add(nn.text);
869
+ } else if (CONTAINER_TYPE_NODES.has(typeNode.type)) {
870
+ // Container-typed parameter (fix #300): the range
871
+ // VALUE var over it gets the element type.
872
+ for (const nn of nameNodes) containerMap.set(nn.text, typeNode.text);
847
873
  } else {
848
874
  const typeName = extractTypeName(typeNode);
849
875
  if (typeName) {
@@ -858,7 +884,7 @@ function findCallsInCode(code, parser, options = {}) {
858
884
  }
859
885
  }
860
886
 
861
- return { typeMap, typeQualifierMap, funcParamNames };
887
+ return { typeMap, typeQualifierMap, funcParamNames, containerMap };
862
888
  };
863
889
 
864
890
  // Helper to extract function name from a function node
@@ -884,13 +910,13 @@ function findCallsInCode(code, parser, options = {}) {
884
910
  : null;
885
911
  };
886
912
 
887
- // Check if current scope has a local closure with the given name
888
- const isLocalClosure = (name) => {
913
+ // Resolve a local closure through the lexical function-scope chain.
914
+ const getLocalClosure = (name) => {
889
915
  for (let i = functionStack.length - 1; i >= 0; i--) {
890
916
  const scope = closureScopes.get(functionStack[i].startLine);
891
- if (scope?.has(name)) return true;
917
+ if (scope?.has(name)) return scope.get(name);
892
918
  }
893
- return false;
919
+ return null;
894
920
  };
895
921
 
896
922
  // Check if name is a function-typed parameter (e.g., match func(Object) bool)
@@ -920,6 +946,37 @@ function findCallsInCode(code, parser, options = {}) {
920
946
  return refNode && isShadowedByLocal(refNode, varName)
921
947
  ? undefined : packageTypeQualifiers.get(varName);
922
948
  };
949
+ // Compiler-true receiver type from a composite-literal receiver
950
+ // expression (fix #298, websocket-measured — the #220(7) typing-sources
951
+ // family): `(&Kit{...}).Run`, `(&net.Dialer{}).DialContext(...)`,
952
+ // `Kit{}.Walk(...)`. Unwraps parens and the & — the literal's TYPE is the
953
+ // receiver's dynamic type. Anonymous types (slice/map/array/struct
954
+ // literals) return null: they never receive project methods.
955
+ const literalReceiverInfo = (operandNode) => {
956
+ let n = operandNode;
957
+ if (n?.type === 'parenthesized_expression') {
958
+ n = n.namedChildCount > 0 ? n.namedChild(0) : null;
959
+ }
960
+ if (n?.type === 'unary_expression') {
961
+ n = n.namedChildCount > 0 ? n.namedChild(0) : null;
962
+ }
963
+ if (!n || n.type !== 'composite_literal') return null;
964
+ let typeNode = n.childForFieldName('type');
965
+ if (typeNode?.type === 'generic_type') {
966
+ typeNode = typeNode.namedChildCount > 0 ? typeNode.namedChild(0) : null;
967
+ }
968
+ if (typeNode?.type === 'qualified_type') {
969
+ const pkg = typeNode.childForFieldName('package')?.text;
970
+ const name = typeNode.childForFieldName('name')?.text;
971
+ return name
972
+ ? { receiverType: name, ...(pkg && { receiverTypeQualifier: pkg }) }
973
+ : null;
974
+ }
975
+ if (typeNode?.type === 'type_identifier') {
976
+ return { receiverType: typeNode.text };
977
+ }
978
+ return null;
979
+ };
923
980
  // Is the FIRST scope-chain hit for this variable a New*-prefix GUESS
924
981
  // (fix #266, viper-measured)? `registry := NewCodecRegistry()` types
925
982
  // registry as 'CodecRegistry' by NAME CONVENTION — the actual return
@@ -935,6 +992,66 @@ function findCallsInCode(code, parser, options = {}) {
935
992
  }
936
993
  return false;
937
994
  };
995
+ // Range-element typing helpers (fix #300). Container type TEXT for a
996
+ // variable declared as a slice/map/array; element extraction from that
997
+ // text; and a lazily built same-file struct→fields map so a selector
998
+ // iterable (`range r.routes`) resolves through the receiver's struct.
999
+ const lookupContainerType = (varName) => {
1000
+ for (let i = functionStack.length - 1; i >= 0; i--) {
1001
+ const m = scopeContainerTypes.get(functionStack[i].startLine);
1002
+ if (m?.has(varName)) return m.get(varName);
1003
+ }
1004
+ return packageContainerTypes.get(varName);
1005
+ };
1006
+ const CONTAINER_TYPE_NODES = new Set(['slice_type', 'map_type', 'array_type']);
1007
+ const containerElementType = (typeText) => {
1008
+ if (!typeText) return null;
1009
+ const t = String(typeText).trim();
1010
+ let m = t.match(/^\[\d*\]\s*(.+)$/s); // []T and [N]T
1011
+ if (!m) m = t.match(/^map\[[^\]]+\]\s*(.+)$/s); // map[K]V → V
1012
+ if (!m) return null;
1013
+ const el = m[1].trim().replace(/^\*/, '').trim();
1014
+ const qm = el.match(/^([A-Za-z_]\w*)\.([A-Za-z_]\w*)$/);
1015
+ if (qm) return { type: qm[2], qualifier: qm[1] };
1016
+ return /^[A-Za-z_]\w*$/.test(el) ? { type: el, qualifier: null } : null;
1017
+ };
1018
+ let structFieldsCache = null;
1019
+ const getStructFields = () => {
1020
+ if (structFieldsCache) return structFieldsCache;
1021
+ structFieldsCache = new Map();
1022
+ const stack = [tree.rootNode];
1023
+ while (stack.length > 0) {
1024
+ const n = stack.pop();
1025
+ if (n.type === 'type_declaration') {
1026
+ for (let i = 0; i < n.namedChildCount; i++) {
1027
+ const spec = n.namedChild(i);
1028
+ if (spec.type !== 'type_spec') continue;
1029
+ const nameNode = spec.childForFieldName('name');
1030
+ const typeNode = spec.childForFieldName('type');
1031
+ if (!nameNode || typeNode?.type !== 'struct_type') continue;
1032
+ const fields = new Map();
1033
+ for (let j = 0; j < typeNode.namedChildCount; j++) {
1034
+ const list = typeNode.namedChild(j);
1035
+ if (list.type !== 'field_declaration_list') continue;
1036
+ for (let k = 0; k < list.namedChildCount; k++) {
1037
+ const fd = list.namedChild(k);
1038
+ if (fd.type !== 'field_declaration') continue;
1039
+ const ftype = fd.childForFieldName('type');
1040
+ if (!ftype) continue;
1041
+ for (let x = 0; x < fd.namedChildCount; x++) {
1042
+ const fn = fd.namedChild(x);
1043
+ if (fn.type === 'field_identifier') fields.set(fn.text, ftype.text);
1044
+ }
1045
+ }
1046
+ }
1047
+ structFieldsCache.set(nameNode.text, fields);
1048
+ }
1049
+ continue;
1050
+ }
1051
+ for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
1052
+ }
1053
+ return structFieldsCache;
1054
+ };
938
1055
 
939
1056
  // fix #203 (Go): is a bare-identifier function REFERENCE shadowed by an
940
1057
  // enclosing func-literal/function parameter, method receiver, range/init
@@ -1043,14 +1160,61 @@ function findCallsInCode(code, parser, options = {}) {
1043
1160
  endLine: node.endPosition.row + 1
1044
1161
  };
1045
1162
  functionStack.push(entry);
1046
- const { typeMap, typeQualifierMap, funcParamNames } = buildScopeTypeMap(node);
1163
+ const { typeMap, typeQualifierMap, funcParamNames, containerMap } = buildScopeTypeMap(node);
1047
1164
  scopeTypes.set(entry.startLine, typeMap);
1048
1165
  scopeTypeQualifiers.set(entry.startLine, typeQualifierMap);
1166
+ scopeContainerTypes.set(entry.startLine, containerMap || new Map());
1049
1167
  if (funcParamNames.size > 0) {
1050
1168
  funcParamScopes.set(entry.startLine, funcParamNames);
1051
1169
  }
1052
1170
  }
1053
1171
 
1172
+ // Range-element typing (fix #300, mux-measured): `for _, route :=
1173
+ // range r.routes` — the VALUE variable's compile-time type is the
1174
+ // container's element type. Sources: a selector iterable whose root
1175
+ // is a typed var and whose field's declaring struct lives in this
1176
+ // file (declared field text `[]*Route`), or an identifier iterable
1177
+ // declared/parameterized with a container type. Only the two-var
1178
+ // form's SECOND variable is typed (single-var range yields the
1179
+ // index/key); untypeable shapes stay untyped — never guessed.
1180
+ if (node.type === 'range_clause' && functionStack.length > 0) {
1181
+ const left = node.childForFieldName('left');
1182
+ const right = node.childForFieldName('right');
1183
+ const vars = left && left.type === 'expression_list'
1184
+ ? Array.from({ length: left.namedChildCount }, (_, i) => left.namedChild(i))
1185
+ : [];
1186
+ const valueVar = vars.length === 2 && vars[1].type === 'identifier' &&
1187
+ vars[1].text !== '_' ? vars[1].text : null;
1188
+ if (valueVar && right) {
1189
+ let containerText = null;
1190
+ if (right.type === 'selector_expression') {
1191
+ const operand = right.childForFieldName('operand');
1192
+ const fieldN = right.childForFieldName('field');
1193
+ if (operand?.type === 'identifier' && fieldN &&
1194
+ !isGuessedType(operand.text)) {
1195
+ const rootType = getReceiverType(operand.text, operand);
1196
+ if (rootType) {
1197
+ containerText = getStructFields().get(rootType)?.get(fieldN.text) || null;
1198
+ }
1199
+ }
1200
+ } else if (right.type === 'identifier') {
1201
+ containerText = lookupContainerType(right.text);
1202
+ }
1203
+ const el = containerElementType(containerText);
1204
+ if (el) {
1205
+ const scopeKey = functionStack[functionStack.length - 1].startLine;
1206
+ const typeMap = scopeTypes.get(scopeKey);
1207
+ if (typeMap) {
1208
+ typeMap.set(valueVar, el.type);
1209
+ const qualifiers = scopeTypeQualifiers.get(scopeKey);
1210
+ if (el.qualifier) qualifiers?.set(valueVar, el.qualifier);
1211
+ else qualifiers?.delete(valueVar);
1212
+ scopeGuesses.get(scopeKey)?.delete(valueVar);
1213
+ }
1214
+ }
1215
+ }
1216
+ }
1217
+
1054
1218
  // Track local variable types from composite literals and typed assignments
1055
1219
  // e.g., s := &Status{...} → s has type Status
1056
1220
  // registry := Registry{...} → registry has type Registry
@@ -1076,6 +1240,13 @@ function findCallsInCode(code, parser, options = {}) {
1076
1240
  // &Type{...} or Type{...}
1077
1241
  if (val.type === 'composite_literal') {
1078
1242
  const typeNode = val.childForFieldName('type');
1243
+ if (typeNode && CONTAINER_TYPE_NODES.has(typeNode.type)) {
1244
+ // xs := []*Route{...} — container literal
1245
+ // feeds range-element typing (fix #300).
1246
+ scopeContainerTypes.get(scopeKey)
1247
+ ?.set(names[vi], typeNode.text);
1248
+ continue;
1249
+ }
1079
1250
  typeName = extractTypeName(typeNode);
1080
1251
  typeQualifier = extractTypeQualifier(typeNode);
1081
1252
  } else if (val.type === 'unary_expression' && val.childCount > 0) {
@@ -1130,6 +1301,24 @@ function findCallsInCode(code, parser, options = {}) {
1130
1301
  }
1131
1302
  }
1132
1303
  }
1304
+ } else if (val.type === 'type_assertion_expression') {
1305
+ // d, ok := dialer.(proxy.ContextDialer) — the
1306
+ // asserted type IS d's static type (fix #298,
1307
+ // websocket-measured: `return d.DialContext, nil`
1308
+ // through the assertion). Compiler-true.
1309
+ const typeNode = val.childForFieldName('type');
1310
+ typeName = extractTypeName(typeNode);
1311
+ typeQualifier = extractTypeQualifier(typeNode);
1312
+ } else if (val.type === 'identifier') {
1313
+ // Var-copy: d := cstDialer — Go is statically
1314
+ // typed, so the copy shares the source variable's
1315
+ // type (fix #298; guess-ness propagates).
1316
+ typeName = getReceiverType(val.text, val) || null;
1317
+ if (typeName) {
1318
+ typeQualifier =
1319
+ getReceiverTypeQualifier(val.text, val) || null;
1320
+ typeGuessed = isGuessedType(val.text);
1321
+ }
1133
1322
  }
1134
1323
  if (typeName) {
1135
1324
  typeMap.set(names[vi], typeName);
@@ -1161,11 +1350,46 @@ function findCallsInCode(code, parser, options = {}) {
1161
1350
  const varQualifierMap = scopeKey == null
1162
1351
  ? packageTypeQualifiers : scopeTypeQualifiers.get(scopeKey);
1163
1352
  if (varTypeMap) {
1353
+ const varContainerMap = scopeKey == null
1354
+ ? packageContainerTypes : scopeContainerTypes.get(scopeKey);
1164
1355
  const recordSpec = (spec) => {
1165
1356
  if (spec.type !== 'var_spec') return;
1166
- const typeName = extractTypeName(spec.childForFieldName('type'));
1167
- if (!typeName) return;
1168
- const qualifier = extractTypeQualifier(spec.childForFieldName('type'));
1357
+ const declaredType = spec.childForFieldName('type');
1358
+ // Container-typed declaration (fix #300): `var routes
1359
+ // []*Route` feeds range-element typing.
1360
+ if (declaredType && CONTAINER_TYPE_NODES.has(declaredType.type) &&
1361
+ varContainerMap) {
1362
+ for (let j = 0; j < spec.namedChildCount; j++) {
1363
+ const id = spec.namedChild(j);
1364
+ if (id.type === 'identifier') {
1365
+ varContainerMap.set(id.text, declaredType.text);
1366
+ }
1367
+ }
1368
+ return;
1369
+ }
1370
+ let typeName = extractTypeName(spec.childForFieldName('type'));
1371
+ let qualifier = typeName
1372
+ ? extractTypeQualifier(spec.childForFieldName('type')) : null;
1373
+ if (!typeName) {
1374
+ // var cstDialer = Dialer{...} — the initializer's
1375
+ // composite-literal type IS the variable's type
1376
+ // (fix #298, websocket-measured: `d := cstDialer`
1377
+ // copies then call DialContext). Single-value specs
1378
+ // only; multi-value pairing stays untyped.
1379
+ const valueNode = spec.childForFieldName('value');
1380
+ let init = valueNode?.type === 'expression_list' &&
1381
+ valueNode.namedChildCount === 1
1382
+ ? valueNode.namedChild(0) : null;
1383
+ if (init?.type === 'unary_expression') {
1384
+ init = init.namedChildCount > 0 ? init.namedChild(0) : null;
1385
+ }
1386
+ if (init?.type === 'composite_literal') {
1387
+ const tn = init.childForFieldName('type');
1388
+ typeName = extractTypeName(tn);
1389
+ qualifier = extractTypeQualifier(tn);
1390
+ }
1391
+ if (!typeName) return;
1392
+ }
1169
1393
  for (let j = 0; j < spec.namedChildCount; j++) {
1170
1394
  const id = spec.namedChild(j);
1171
1395
  if (id.type === 'identifier') {
@@ -1188,52 +1412,66 @@ function findCallsInCode(code, parser, options = {}) {
1188
1412
  }
1189
1413
  }
1190
1414
 
1191
- // Track local closures: atoi := func(...) { ... } or var handler = func(...) { ... }
1415
+ // Track local closures: atoi := func(...) { ... } or
1416
+ // var handler = func(...) { ... }. Pair each lexical name with the
1417
+ // literal's declared result so an invocation can type its assignment
1418
+ // without pretending it calls a same-named package symbol.
1192
1419
  if (node.type === 'short_var_declaration' || node.type === 'var_declaration') {
1193
- // Check if a subtree contains a func_literal
1194
- const hasFunc = (n) => {
1420
+ const firstFuncLiteral = (n) => {
1195
1421
  if (!n) return false;
1196
- if (n.type === 'func_literal') return true;
1422
+ if (n.type === 'func_literal') return n;
1197
1423
  for (let i = 0; i < n.childCount; i++) {
1198
- if (hasFunc(n.child(i))) return true;
1424
+ const found = firstFuncLiteral(n.child(i));
1425
+ if (found) return found;
1199
1426
  }
1200
- return false;
1427
+ return null;
1201
1428
  };
1202
- let names = [];
1203
- if (node.type === 'short_var_declaration') {
1204
- // short_var_declaration checks the whole RHS
1205
- if (hasFunc(node)) {
1206
- const left = node.childForFieldName('left');
1207
- if (left) {
1208
- names = left.type === 'expression_list'
1209
- ? Array.from({ length: left.namedChildCount }, (_, i) => left.namedChild(i))
1210
- .filter(n => n.type === 'identifier').map(n => n.text)
1211
- : left.type === 'identifier' ? [left.text] : [];
1212
- }
1429
+ const entries = [];
1430
+ const pairNamesWithValues = (left, right) => {
1431
+ if (!left || !right) return;
1432
+ const names = left.type === 'expression_list'
1433
+ ? left.namedChildren.filter(child => child.type === 'identifier')
1434
+ : left.type === 'identifier' ? [left] : [];
1435
+ const values = right.type === 'expression_list'
1436
+ ? right.namedChildren : [right];
1437
+ for (let i = 0; i < names.length; i++) {
1438
+ const literal = firstFuncLiteral(values[i] ||
1439
+ (values.length === 1 ? values[0] : null));
1440
+ if (literal) entries.push({
1441
+ name: names[i].text,
1442
+ returnType: extractReturnType(literal),
1443
+ });
1213
1444
  }
1445
+ };
1446
+ if (node.type === 'short_var_declaration') {
1447
+ pairNamesWithValues(
1448
+ node.childForFieldName('left'),
1449
+ node.childForFieldName('right'));
1214
1450
  } else {
1215
- // var_declaration: check per-spec so only names with func_literal values are tracked
1451
+ // var_declaration: check per-spec so only names paired with
1452
+ // func_literal values are tracked.
1216
1453
  // Handle both: var x = func(){} (var_declaration > var_spec)
1217
1454
  // and: var (\n x = func(){} \n) (var_declaration > var_spec_list > var_spec)
1218
- const collectClosureNames = (parent) => {
1455
+ const collectClosureEntries = (parent) => {
1219
1456
  for (let i = 0; i < parent.namedChildCount; i++) {
1220
1457
  const child = parent.namedChild(i);
1221
- if (child.type === 'var_spec' && hasFunc(child)) {
1222
- const nameNode = child.childForFieldName('name');
1223
- if (nameNode && nameNode.type === 'identifier') {
1224
- names.push(nameNode.text);
1225
- }
1458
+ if (child.type === 'var_spec') {
1459
+ pairNamesWithValues(
1460
+ child.childForFieldName('name'),
1461
+ child.childForFieldName('value'));
1226
1462
  } else if (child.type === 'var_spec_list') {
1227
- collectClosureNames(child);
1463
+ collectClosureEntries(child);
1228
1464
  }
1229
1465
  }
1230
1466
  };
1231
- collectClosureNames(node);
1467
+ collectClosureEntries(node);
1232
1468
  }
1233
- if (names.length > 0 && functionStack.length > 0) {
1469
+ if (entries.length > 0 && functionStack.length > 0) {
1234
1470
  const scopeKey = functionStack[functionStack.length - 1].startLine;
1235
- if (!closureScopes.has(scopeKey)) closureScopes.set(scopeKey, new Set());
1236
- for (const n of names) closureScopes.get(scopeKey).add(n);
1471
+ if (!closureScopes.has(scopeKey)) closureScopes.set(scopeKey, new Map());
1472
+ for (const entry of entries) {
1473
+ closureScopes.get(scopeKey).set(entry.name, entry);
1474
+ }
1237
1475
  }
1238
1476
  }
1239
1477
 
@@ -1269,8 +1507,42 @@ function findCallsInCode(code, parser, options = {}) {
1269
1507
  const callName = funcNode.text;
1270
1508
  // Skip Go built-in function calls
1271
1509
  if (GO_BUILTINS.has(callName)) return true;
1272
- // Skip calls to local closures (they shadow package-level functions)
1273
- if (isLocalClosure(callName)) return true;
1510
+ // Local closures shadow package-level functions. Keep an
1511
+ // internal-only call record when their declared result can
1512
+ // type an assignment; the sentinel name prevents semantic
1513
+ // caller matching against a package symbol of the same name.
1514
+ const localClosure = getLocalClosure(callName);
1515
+ if (localClosure) {
1516
+ if (assigned && localClosure.returnType) {
1517
+ calls.push({
1518
+ name: '<local-closure>',
1519
+ localCallName: callName,
1520
+ localValueCall: true,
1521
+ returnTypeHint: localClosure.returnType,
1522
+ line: node.startPosition.row + 1,
1523
+ column: funcNode.startPosition.column,
1524
+ callStart: node.startIndex,
1525
+ callEnd: node.endIndex,
1526
+ isMethod: false,
1527
+ argCount,
1528
+ ...(argSpread && { argSpread: true }),
1529
+ assignedTo: assigned.assignedTo,
1530
+ ...(assigned.assignedTuple && { assignedTuple: true }),
1531
+ ...(assigned.assignedTupleIndex != null && {
1532
+ assignedTupleIndex: assigned.assignedTupleIndex,
1533
+ }),
1534
+ ...(assigned.assignedTupleTargets && {
1535
+ assignedTupleTargets: assigned.assignedTupleTargets,
1536
+ }),
1537
+ ...(assigned.assignedTupleRest && {
1538
+ assignedTupleRest: assigned.assignedTupleRest,
1539
+ }),
1540
+ enclosingFunction,
1541
+ uncertain: false,
1542
+ });
1543
+ }
1544
+ return true;
1545
+ }
1274
1546
  // Skip calls to function-typed parameters (e.g., match func(Object) bool)
1275
1547
  // These are local parameter invocations, not calls to global functions
1276
1548
  if (isFuncTypedParam(callName)) return true;
@@ -1280,12 +1552,21 @@ function findCallsInCode(code, parser, options = {}) {
1280
1552
  calls.push({
1281
1553
  name: callName,
1282
1554
  line: node.startPosition.row + 1,
1555
+ column: funcNode.startPosition.column,
1556
+ callStart: node.startIndex,
1557
+ callEnd: node.endIndex,
1283
1558
  isMethod: false,
1284
1559
  argCount,
1285
1560
  ...(argSpread && { argSpread: true }),
1286
1561
  ...(assigned && { assignedTo: assigned.assignedTo }),
1287
1562
  ...(assigned?.assignedTuple && { assignedTuple: true }),
1288
- ...(assigned?.assignedTupleRest && { assignedTupleRest: assigned.assignedTupleRest }),
1563
+ ...(assigned?.assignedTupleIndex != null && {
1564
+ assignedTupleIndex: assigned.assignedTupleIndex,
1565
+ }),
1566
+ ...(assigned?.assignedTupleTargets && {
1567
+ assignedTupleTargets: assigned.assignedTupleTargets,
1568
+ }),
1569
+ ...(assigned?.assignedTupleRest && { assignedTupleRest: assigned.assignedTupleRest }),
1289
1570
  enclosingFunction,
1290
1571
  uncertain,
1291
1572
  ...(firstArg && { firstStringArg: firstArg.value, firstStringArgInterp: firstArg.interp })
@@ -1300,10 +1581,19 @@ function findCallsInCode(code, parser, options = {}) {
1300
1581
  // Distinguish pkg.Func() (package-qualified) from obj.Method()
1301
1582
  // If receiver is a known import alias, this is a package call, not a method call
1302
1583
  const isPkgCall = receiver && importAliases.has(receiver);
1303
- const receiverType = (!isPkgCall && receiver)
1584
+ let receiverType = (!isPkgCall && receiver)
1304
1585
  ? getReceiverType(receiver, operandNode) : undefined;
1305
- const receiverTypeQualifier = receiverType
1586
+ let receiverTypeQualifier = receiverType
1306
1587
  ? getReceiverTypeQualifier(receiver, operandNode) : undefined;
1588
+ // Composite-literal receiver (fix #298):
1589
+ // (&Kit{...}).Run(...) — compiler-true type, never guessed.
1590
+ if (!receiver && !receiverType) {
1591
+ const lit = literalReceiverInfo(operandNode);
1592
+ if (lit) {
1593
+ receiverType = lit.receiverType;
1594
+ receiverTypeQualifier = lit.receiverTypeQualifier;
1595
+ }
1596
+ }
1307
1597
  // fix #202: one-hop declared-field receivers — h.inner.Run().
1308
1598
  // receiverRoot/Field/RootType let findCallers hop to the
1309
1599
  // field's declared struct-field type cross-file.
@@ -1338,14 +1628,34 @@ function findCallsInCode(code, parser, options = {}) {
1338
1628
  // declared return (*pflag.FlagSet → external → routed).
1339
1629
  // Package-qualified producers (os.CreateTemp().Name())
1340
1630
  // carry the qualifier for strict import-package resolution.
1341
- let receiverCall, receiverCallIsMethod, receiverCallReceiver, receiverCallLine;
1631
+ let receiverCall, receiverCallIsMethod, receiverCallReceiver,
1632
+ receiverCallLine, receiverCallStart, receiverCallEnd,
1633
+ receiverCallResultType, receiverCallResultTypeQualifier;
1342
1634
  if (!receiver && !receiverFieldName && operandNode?.type === 'call_expression') {
1343
1635
  const prodFunc = operandNode.childForFieldName('function');
1636
+ receiverCallStart = operandNode.startIndex;
1637
+ receiverCallEnd = operandNode.endIndex;
1344
1638
  if (prodFunc?.type === 'identifier') {
1345
1639
  receiverCall = prodFunc.text;
1346
1640
  // Producer link (fix #258): plain-call records carry
1347
1641
  // the call node's start line
1348
1642
  receiverCallLine = operandNode.startPosition.row + 1;
1643
+ // Go's builtin new has an exact static result:
1644
+ // `new(Route)` is `*Route`. The builtin itself is
1645
+ // intentionally absent from the semantic call
1646
+ // index, so carry the result type on its consumer.
1647
+ if (receiverCall === 'new') {
1648
+ const prodArgs = operandNode.childForFieldName('arguments');
1649
+ const typeArg = prodArgs?.namedChild(0);
1650
+ if (typeArg) {
1651
+ const authored = typeArg.text.replace(/^\*+/, '').trim();
1652
+ const parts = authored.split('.');
1653
+ receiverCallResultType = parts.pop();
1654
+ if (parts.length > 0) {
1655
+ receiverCallResultTypeQualifier = parts.join('.');
1656
+ }
1657
+ }
1658
+ }
1349
1659
  } else if (prodFunc?.type === 'selector_expression') {
1350
1660
  const pf = prodFunc.childForFieldName('field');
1351
1661
  const po = prodFunc.childForFieldName('operand');
@@ -1372,6 +1682,9 @@ function findCallsInCode(code, parser, options = {}) {
1372
1682
  // set and the oracles key by the name's line; Go was
1373
1683
  // the only parser still using the call node's start.
1374
1684
  line: fieldNode.startPosition.row + 1,
1685
+ column: fieldNode.startPosition.column,
1686
+ callStart: node.startIndex,
1687
+ callEnd: node.endIndex,
1375
1688
  isMethod: !isPkgCall,
1376
1689
  receiver,
1377
1690
  ...(receiverType && { receiverType }),
@@ -1386,10 +1699,22 @@ function findCallsInCode(code, parser, options = {}) {
1386
1699
  ...(receiverCallIsMethod && { receiverCallIsMethod: true }),
1387
1700
  ...(receiverCallReceiver && { receiverCallReceiver }),
1388
1701
  ...(receiverCallLine && { receiverCallLine }),
1702
+ ...(receiverCallStart != null && { receiverCallStart }),
1703
+ ...(receiverCallEnd != null && { receiverCallEnd }),
1704
+ ...(receiverCallResultType && { receiverCallResultType }),
1705
+ ...(receiverCallResultTypeQualifier && {
1706
+ receiverCallResultTypeQualifier,
1707
+ }),
1389
1708
  argCount,
1390
1709
  ...(argSpread && { argSpread: true }),
1391
1710
  ...(assigned && { assignedTo: assigned.assignedTo }),
1392
1711
  ...(assigned?.assignedTuple && { assignedTuple: true }),
1712
+ ...(assigned?.assignedTupleIndex != null && {
1713
+ assignedTupleIndex: assigned.assignedTupleIndex,
1714
+ }),
1715
+ ...(assigned?.assignedTupleTargets && {
1716
+ assignedTupleTargets: assigned.assignedTupleTargets,
1717
+ }),
1393
1718
  ...(assigned?.assignedTupleRest && { assignedTupleRest: assigned.assignedTupleRest }),
1394
1719
  enclosingFunction,
1395
1720
  uncertain,
@@ -1437,6 +1762,7 @@ function findCallsInCode(code, parser, options = {}) {
1437
1762
  calls.push({
1438
1763
  name: typeName,
1439
1764
  line: node.startPosition.row + 1,
1765
+ column: (typeNode.childForFieldName?.('name') || typeNode).startPosition.column,
1440
1766
  isMethod: false,
1441
1767
  isConstructor: true,
1442
1768
  ...(typeQualifier && { receiver: typeQualifier }),
@@ -1457,19 +1783,29 @@ function findCallsInCode(code, parser, options = {}) {
1457
1783
  const operandNode = node.childForFieldName('operand');
1458
1784
  if (fieldNode && operandNode) {
1459
1785
  const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
1460
- const receiverType = receiver
1786
+ let receiverType = receiver
1461
1787
  ? getReceiverType(receiver, operandNode) : undefined;
1462
- const receiverTypeQualifier = receiverType
1788
+ let receiverTypeQualifier = receiverType
1463
1789
  ? getReceiverTypeQualifier(receiver, operandNode) : undefined;
1790
+ // Composite-literal receiver (fix #298): the literal's
1791
+ // type is compiler-true, never guessed.
1792
+ if (!receiver && !receiverType) {
1793
+ const lit = literalReceiverInfo(operandNode);
1794
+ if (lit) {
1795
+ receiverType = lit.receiverType;
1796
+ receiverTypeQualifier = lit.receiverTypeQualifier;
1797
+ }
1798
+ }
1464
1799
  const enclosingFunction = getCurrentEnclosingFunction();
1465
1800
  calls.push({
1466
1801
  name: fieldNode.text,
1467
- line: node.startPosition.row + 1,
1802
+ line: fieldNode.startPosition.row + 1,
1803
+ column: fieldNode.startPosition.column,
1468
1804
  isMethod: true,
1469
1805
  receiver,
1470
1806
  ...(receiverType && { receiverType }),
1471
1807
  ...(receiverTypeQualifier && { receiverTypeQualifier }),
1472
- ...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
1808
+ ...(receiverType && receiver && isGuessedType(receiver) && { receiverTypeGuessed: true }),
1473
1809
  enclosingFunction,
1474
1810
  isPotentialCallback: true,
1475
1811
  uncertain: false
@@ -1487,6 +1823,7 @@ function findCallsInCode(code, parser, options = {}) {
1487
1823
  calls.push({
1488
1824
  name,
1489
1825
  line: node.startPosition.row + 1,
1826
+ column: node.startPosition.column,
1490
1827
  isMethod: false,
1491
1828
  isFunctionReference: true,
1492
1829
  isPotentialCallback: true,
@@ -1517,19 +1854,28 @@ function findCallsInCode(code, parser, options = {}) {
1517
1854
  const operandNode = rhs.childForFieldName('operand');
1518
1855
  if (fieldNode && operandNode) {
1519
1856
  const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
1520
- const receiverType = receiver
1857
+ let receiverType = receiver
1521
1858
  ? getReceiverType(receiver, operandNode) : undefined;
1522
- const receiverTypeQualifier = receiverType
1859
+ let receiverTypeQualifier = receiverType
1523
1860
  ? getReceiverTypeQualifier(receiver, operandNode) : undefined;
1861
+ // Composite-literal receiver (fix #298).
1862
+ if (!receiver && !receiverType) {
1863
+ const lit = literalReceiverInfo(operandNode);
1864
+ if (lit) {
1865
+ receiverType = lit.receiverType;
1866
+ receiverTypeQualifier = lit.receiverTypeQualifier;
1867
+ }
1868
+ }
1524
1869
  const enclosingFunction = getCurrentEnclosingFunction();
1525
1870
  calls.push({
1526
1871
  name: fieldNode.text,
1527
- line: rhs.startPosition.row + 1,
1872
+ line: fieldNode.startPosition.row + 1,
1873
+ column: fieldNode.startPosition.column,
1528
1874
  isMethod: true,
1529
1875
  receiver,
1530
1876
  ...(receiverType && { receiverType }),
1531
1877
  ...(receiverTypeQualifier && { receiverTypeQualifier }),
1532
- ...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
1878
+ ...(receiverType && receiver && isGuessedType(receiver) && { receiverTypeGuessed: true }),
1533
1879
  enclosingFunction,
1534
1880
  isPotentialCallback: true,
1535
1881
  uncertain: false
@@ -1544,6 +1890,7 @@ function findCallsInCode(code, parser, options = {}) {
1544
1890
  calls.push({
1545
1891
  name,
1546
1892
  line: rhs.startPosition.row + 1,
1893
+ column: rhs.startPosition.column,
1547
1894
  isMethod: false,
1548
1895
  isFunctionReference: true,
1549
1896
  isPotentialCallback: true,
@@ -1557,6 +1904,51 @@ function findCallsInCode(code, parser, options = {}) {
1557
1904
  }
1558
1905
  }
1559
1906
 
1907
+ // Method values in RETURN position (fix #298, websocket-measured):
1908
+ // `return (&httpProxyDialer{...}).DialContext, nil` is the
1909
+ // compile-breaking reference a rename sweep must see — it was
1910
+ // invisible (no record at all). Selector values only; plain
1911
+ // identifier returns (`return err`) stay unemitted — every returned
1912
+ // variable would join the calls cache (classified-deferred).
1913
+ if (node.type === 'return_statement') {
1914
+ const exprList = node.namedChildCount > 0 ? node.namedChild(0) : null;
1915
+ const values = exprList?.type === 'expression_list'
1916
+ ? exprList.namedChildren
1917
+ : (exprList ? [exprList] : []);
1918
+ for (const val of values) {
1919
+ if (val.type !== 'selector_expression') continue;
1920
+ const fieldNode = val.childForFieldName('field');
1921
+ const operandNode = val.childForFieldName('operand');
1922
+ if (!fieldNode || !operandNode) continue;
1923
+ const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
1924
+ let receiverType = receiver
1925
+ ? getReceiverType(receiver, operandNode) : undefined;
1926
+ let receiverTypeQualifier = receiverType
1927
+ ? getReceiverTypeQualifier(receiver, operandNode) : undefined;
1928
+ if (!receiver && !receiverType) {
1929
+ const lit = literalReceiverInfo(operandNode);
1930
+ if (lit) {
1931
+ receiverType = lit.receiverType;
1932
+ receiverTypeQualifier = lit.receiverTypeQualifier;
1933
+ }
1934
+ }
1935
+ calls.push({
1936
+ name: fieldNode.text,
1937
+ // #223 name-node convention: the field's own line.
1938
+ line: fieldNode.startPosition.row + 1,
1939
+ column: fieldNode.startPosition.column,
1940
+ isMethod: true,
1941
+ receiver,
1942
+ ...(receiverType && { receiverType }),
1943
+ ...(receiverTypeQualifier && { receiverTypeQualifier }),
1944
+ ...(receiverType && receiver && isGuessedType(receiver) && { receiverTypeGuessed: true }),
1945
+ enclosingFunction: getCurrentEnclosingFunction(),
1946
+ isPotentialCallback: true,
1947
+ uncertain: false
1948
+ });
1949
+ }
1950
+ }
1951
+
1560
1952
  // Detect function references in composite literal fields:
1561
1953
  // ResourceEventHandlerFuncs{AddFunc: addNodeToCache, UpdateFunc: updateNode}
1562
1954
  // keyed_element → literal_element(key) ":" literal_element(value)
@@ -1600,6 +1992,7 @@ function findCallsInCode(code, parser, options = {}) {
1600
1992
  calls.push({
1601
1993
  name,
1602
1994
  line: valueNode.startPosition.row + 1,
1995
+ column: valueNode.startPosition.column,
1603
1996
  isMethod: false,
1604
1997
  isFunctionReference: true,
1605
1998
  isPotentialCallback: true,
@@ -1616,19 +2009,28 @@ function findCallsInCode(code, parser, options = {}) {
1616
2009
  const operandNode = valueNode.childForFieldName('operand');
1617
2010
  if (fieldNode && operandNode) {
1618
2011
  const receiver = operandNode.type === 'identifier' ? operandNode.text : undefined;
1619
- const receiverType = receiver
2012
+ let receiverType = receiver
1620
2013
  ? getReceiverType(receiver, operandNode) : undefined;
1621
- const receiverTypeQualifier = receiverType
2014
+ let receiverTypeQualifier = receiverType
1622
2015
  ? getReceiverTypeQualifier(receiver, operandNode) : undefined;
2016
+ // Composite-literal receiver (fix #298).
2017
+ if (!receiver && !receiverType) {
2018
+ const lit = literalReceiverInfo(operandNode);
2019
+ if (lit) {
2020
+ receiverType = lit.receiverType;
2021
+ receiverTypeQualifier = lit.receiverTypeQualifier;
2022
+ }
2023
+ }
1623
2024
  const enclosingFunction = getCurrentEnclosingFunction();
1624
2025
  calls.push({
1625
2026
  name: fieldNode.text,
1626
- line: valueNode.startPosition.row + 1,
2027
+ line: fieldNode.startPosition.row + 1,
2028
+ column: fieldNode.startPosition.column,
1627
2029
  isMethod: true,
1628
2030
  receiver,
1629
2031
  ...(receiverType && { receiverType }),
1630
2032
  ...(receiverTypeQualifier && { receiverTypeQualifier }),
1631
- ...(receiverType && isGuessedType(receiver) && { receiverTypeGuessed: true }),
2033
+ ...(receiverType && receiver && isGuessedType(receiver) && { receiverTypeGuessed: true }),
1632
2034
  enclosingFunction,
1633
2035
  isPotentialCallback: true,
1634
2036
  uncertain: false,