circle-ir 4.15.0 → 4.16.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.
Files changed (135) hide show
  1. package/configs/sinks/golang.json +4 -4
  2. package/dist/analysis/config-loader.d.ts.map +1 -1
  3. package/dist/analysis/config-loader.js +11 -7
  4. package/dist/analysis/config-loader.js.map +1 -1
  5. package/dist/analysis/constant-caller-gate.d.ts +39 -0
  6. package/dist/analysis/constant-caller-gate.d.ts.map +1 -0
  7. package/dist/analysis/constant-caller-gate.js +298 -0
  8. package/dist/analysis/constant-caller-gate.js.map +1 -0
  9. package/dist/analysis/constant-propagation/evaluator.d.ts.map +1 -1
  10. package/dist/analysis/constant-propagation/evaluator.js +3 -1
  11. package/dist/analysis/constant-propagation/evaluator.js.map +1 -1
  12. package/dist/analysis/constant-propagation/patterns.d.ts +19 -0
  13. package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
  14. package/dist/analysis/constant-propagation/patterns.js +33 -0
  15. package/dist/analysis/constant-propagation/patterns.js.map +1 -1
  16. package/dist/analysis/constant-propagation/propagator.d.ts +59 -0
  17. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  18. package/dist/analysis/constant-propagation/propagator.js +396 -49
  19. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  20. package/dist/analysis/constant-propagation/types.d.ts +9 -0
  21. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  22. package/dist/analysis/findings.d.ts.map +1 -1
  23. package/dist/analysis/findings.js +134 -0
  24. package/dist/analysis/findings.js.map +1 -1
  25. package/dist/analysis/interprocedural.d.ts.map +1 -1
  26. package/dist/analysis/interprocedural.js +98 -3
  27. package/dist/analysis/interprocedural.js.map +1 -1
  28. package/dist/analysis/java-array-element-keys.d.ts +14 -0
  29. package/dist/analysis/java-array-element-keys.d.ts.map +1 -0
  30. package/dist/analysis/java-array-element-keys.js +36 -0
  31. package/dist/analysis/java-array-element-keys.js.map +1 -0
  32. package/dist/analysis/js-crlf-strip.d.ts +26 -0
  33. package/dist/analysis/js-crlf-strip.d.ts.map +1 -0
  34. package/dist/analysis/js-crlf-strip.js +244 -0
  35. package/dist/analysis/js-crlf-strip.js.map +1 -0
  36. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +12 -0
  37. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  38. package/dist/analysis/passes/csharp-sanitizer-credit.js +109 -12
  39. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  40. package/dist/analysis/passes/language-sources-pass.d.ts +38 -3
  41. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  42. package/dist/analysis/passes/language-sources-pass.js +1176 -119
  43. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  44. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  45. package/dist/analysis/passes/param-source-flow-gate-pass.js +53 -1
  46. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  47. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts +6 -0
  48. package/dist/analysis/passes/plaintext-password-storage-pass.d.ts.map +1 -1
  49. package/dist/analysis/passes/plaintext-password-storage-pass.js +160 -0
  50. package/dist/analysis/passes/plaintext-password-storage-pass.js.map +1 -1
  51. package/dist/analysis/passes/scan-secrets-pass.d.ts +1 -1
  52. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  53. package/dist/analysis/passes/scan-secrets-pass.js +7 -1
  54. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  55. package/dist/analysis/passes/sink-filter-pass.d.ts +1 -1
  56. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  57. package/dist/analysis/passes/sink-filter-pass.js +307 -31
  58. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  59. package/dist/analysis/passes/taint-matcher-pass.d.ts.map +1 -1
  60. package/dist/analysis/passes/taint-matcher-pass.js +2 -2
  61. package/dist/analysis/passes/taint-matcher-pass.js.map +1 -1
  62. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  63. package/dist/analysis/passes/taint-propagation-pass.js +1349 -58
  64. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  65. package/dist/analysis/passes/weak-hash-pass.d.ts +6 -0
  66. package/dist/analysis/passes/weak-hash-pass.d.ts.map +1 -1
  67. package/dist/analysis/passes/weak-hash-pass.js +191 -4
  68. package/dist/analysis/passes/weak-hash-pass.js.map +1 -1
  69. package/dist/analysis/passes/weak-random-pass.d.ts +5 -0
  70. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  71. package/dist/analysis/passes/weak-random-pass.js +30 -0
  72. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  73. package/dist/analysis/project-callee-escape-gate.d.ts +40 -0
  74. package/dist/analysis/project-callee-escape-gate.d.ts.map +1 -0
  75. package/dist/analysis/project-callee-escape-gate.js +243 -0
  76. package/dist/analysis/project-callee-escape-gate.js.map +1 -0
  77. package/dist/analysis/rust-literal-guard-rebind.d.ts +34 -0
  78. package/dist/analysis/rust-literal-guard-rebind.d.ts.map +1 -0
  79. package/dist/analysis/rust-literal-guard-rebind.js +197 -0
  80. package/dist/analysis/rust-literal-guard-rebind.js.map +1 -0
  81. package/dist/analysis/rust-literal-guard.d.ts +33 -0
  82. package/dist/analysis/rust-literal-guard.d.ts.map +1 -0
  83. package/dist/analysis/rust-literal-guard.js +422 -0
  84. package/dist/analysis/rust-literal-guard.js.map +1 -0
  85. package/dist/analysis/scalar-derivation.d.ts +8 -0
  86. package/dist/analysis/scalar-derivation.d.ts.map +1 -1
  87. package/dist/analysis/scalar-derivation.js +172 -0
  88. package/dist/analysis/scalar-derivation.js.map +1 -1
  89. package/dist/analysis/taint-matcher.d.ts +2 -2
  90. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  91. package/dist/analysis/taint-matcher.js +531 -31
  92. package/dist/analysis/taint-matcher.js.map +1 -1
  93. package/dist/analysis/taint-propagation.d.ts +26 -1
  94. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  95. package/dist/analysis/taint-propagation.js +234 -13
  96. package/dist/analysis/taint-propagation.js.map +1 -1
  97. package/dist/analyzer.d.ts +16 -2
  98. package/dist/analyzer.d.ts.map +1 -1
  99. package/dist/analyzer.js +13 -3
  100. package/dist/analyzer.js.map +1 -1
  101. package/dist/browser/circle-ir.js +24319 -19934
  102. package/dist/core/circle-ir-core.cjs +4936 -3575
  103. package/dist/core/circle-ir-core.js +4936 -3575
  104. package/dist/core/extractors/calls.d.ts +12 -1
  105. package/dist/core/extractors/calls.d.ts.map +1 -1
  106. package/dist/core/extractors/calls.js +332 -22
  107. package/dist/core/extractors/calls.js.map +1 -1
  108. package/dist/core/extractors/dfg.js +102 -10
  109. package/dist/core/extractors/dfg.js.map +1 -1
  110. package/dist/core/extractors/types.d.ts +5 -1
  111. package/dist/core/extractors/types.d.ts.map +1 -1
  112. package/dist/core/extractors/types.js +62 -0
  113. package/dist/core/extractors/types.js.map +1 -1
  114. package/dist/graph/analysis-pass.d.ts +6 -1
  115. package/dist/graph/analysis-pass.d.ts.map +1 -1
  116. package/dist/graph/analysis-pass.js +2 -1
  117. package/dist/graph/analysis-pass.js.map +1 -1
  118. package/dist/navigation/navigation-index.d.ts +134 -14
  119. package/dist/navigation/navigation-index.d.ts.map +1 -1
  120. package/dist/navigation/navigation-index.js +1158 -104
  121. package/dist/navigation/navigation-index.js.map +1 -1
  122. package/dist/navigation/types.d.ts +59 -2
  123. package/dist/navigation/types.d.ts.map +1 -1
  124. package/dist/resolution/cross-file.d.ts +9 -0
  125. package/dist/resolution/cross-file.d.ts.map +1 -1
  126. package/dist/resolution/cross-file.js +102 -43
  127. package/dist/resolution/cross-file.js.map +1 -1
  128. package/dist/resolution/type-hierarchy.d.ts +3 -0
  129. package/dist/resolution/type-hierarchy.d.ts.map +1 -1
  130. package/dist/resolution/type-hierarchy.js +45 -1
  131. package/dist/resolution/type-hierarchy.js.map +1 -1
  132. package/dist/types/index.d.ts +33 -0
  133. package/dist/types/index.d.ts.map +1 -1
  134. package/dist/types/index.js.map +1 -1
  135. package/package.json +1 -1
@@ -10,5 +10,16 @@ import { type NodeCache } from '../parser.js';
10
10
  * @param cache Optional node cache for performance
11
11
  * @param language Optional language hint ('java' | 'javascript' | 'typescript' | 'python' | 'rust')
12
12
  */
13
- export declare function extractCalls(tree: Tree, cache?: NodeCache, language?: string): CallInfo[];
13
+ /**
14
+ * Options for `extractCalls`. Every field is off by default, and the default
15
+ * output is unchanged: `ir.calls` is read by the SAST passes.
16
+ */
17
+ export interface CallExtractionOptions {
18
+ /**
19
+ * Java only: give each argument its static type and start position
20
+ * (`ArgumentInfo.type` / `.location`), which overload selection needs.
21
+ */
22
+ navigationTypes?: boolean;
23
+ }
24
+ export declare function extractCalls(tree: Tree, cache?: NodeCache, language?: string, options?: CallExtractionOptions): CallInfo[];
14
25
  //# sourceMappingURL=calls.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"calls.d.ts","sourceRoot":"","sources":["../../../src/core/extractors/calls.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAQ,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAgC,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAA2D,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA8EvG;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ,EAAE,CAoDzF"}
1
+ {"version":3,"file":"calls.d.ts","sourceRoot":"","sources":["../../../src/core/extractors/calls.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAQ,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAgC,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAA2D,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AA8EvG;;;;;GAKG;AACH;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,IAAI,EACV,KAAK,CAAC,EAAE,SAAS,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,GAAE,qBAA0B,GAClC,QAAQ,EAAE,CAqDZ"}
@@ -47,13 +47,7 @@ function detectLanguageFromTree(tree, cache) {
47
47
  // Default to Java for backwards compatibility
48
48
  return javaIndicators >= jsIndicators ? 'java' : 'javascript';
49
49
  }
50
- /**
51
- * Extract all method calls from the tree.
52
- * @param tree The parsed AST tree
53
- * @param cache Optional node cache for performance
54
- * @param language Optional language hint ('java' | 'javascript' | 'typescript' | 'python' | 'rust')
55
- */
56
- export function extractCalls(tree, cache, language) {
50
+ export function extractCalls(tree, cache, language, options = {}) {
57
51
  const calls = [];
58
52
  // Use language hint if provided, otherwise detect from tree
59
53
  const detectedLanguage = language ?? detectLanguageFromTree(tree, cache);
@@ -82,13 +76,14 @@ export function extractCalls(tree, cache, language) {
82
76
  const context = buildResolutionContext(tree, cache);
83
77
  // Find all method invocations
84
78
  const invocations = getNodesFromCache(tree.rootNode, 'method_invocation', cache);
79
+ const typed = options.navigationTypes === true;
85
80
  for (const inv of invocations) {
86
- calls.push(extractCallInfo(inv, context));
81
+ calls.push(extractCallInfo(inv, context, typed));
87
82
  }
88
83
  // Find object creation expressions (constructor calls)
89
84
  const objectCreations = getNodesFromCache(tree.rootNode, 'object_creation_expression', cache);
90
85
  for (const creation of objectCreations) {
91
- const callInfo = extractObjectCreation(creation, context);
86
+ const callInfo = extractObjectCreation(creation, context, typed);
92
87
  if (callInfo) {
93
88
  calls.push(callInfo);
94
89
  }
@@ -115,15 +110,16 @@ export function extractCalls(tree, cache, language) {
115
110
  * match only covers for static calls. Type comes from the declaration
116
111
  * (`HttpClient c = …`) or, for `var`, from the `new T(…)` initializer.
117
112
  */
118
- function buildCSharpReceiverTypeMap(tree, cache) {
113
+ function buildCSharpReceiverTypeMap(tree, cache, scope) {
119
114
  const map = new Map();
115
+ const nodesOf = (type) => scope ? scope.descendantsOfType(type).filter((n) => n !== null) : getNodesFromCache(tree.rootNode, type, cache);
120
116
  const simple = (t) => t.replace(/<[^>]*>/g, '').split('.').pop()?.trim() ?? t;
121
117
  // cognium-dev#542 — method/constructor parameters. A receiver is just as often a parameter as a
122
118
  // local — `void Bad(HttpRequest req, HttpResponse resp)` is the canonical
123
119
  // ASP.NET handler shape — and omitting them meant every receiver gate keyed on
124
120
  // a declared type silently failed for parameters. Locals below win on a name
125
121
  // clash, since an inner declaration shadows the parameter.
126
- for (const prm of getNodesFromCache(tree.rootNode, 'parameter', cache)) {
122
+ for (const prm of nodesOf('parameter')) {
127
123
  const nameNode = prm.childForFieldName('name');
128
124
  const typeNode = prm.childForFieldName('type');
129
125
  if (nameNode?.type !== 'identifier' || !typeNode)
@@ -132,7 +128,7 @@ function buildCSharpReceiverTypeMap(tree, cache) {
132
128
  if (t && t !== 'var')
133
129
  map.set(getNodeText(nameNode), simple(t));
134
130
  }
135
- for (const vd of getNodesFromCache(tree.rootNode, 'variable_declaration', cache)) {
131
+ for (const vd of nodesOf('variable_declaration')) {
136
132
  const typeNode = vd.childForFieldName('type');
137
133
  const declaredType = typeNode ? getNodeText(typeNode) : null;
138
134
  for (let i = 0; i < vd.childCount; i++) {
@@ -243,6 +239,25 @@ function csharpCreatedSimpleType(creation) {
243
239
  function extractCSharpCalls(tree, cache) {
244
240
  const calls = [];
245
241
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
242
+ // The file-wide map keeps one type per name, so `ProcessStartInfo proc` in one
243
+ // method hides `Process proc` in another. For a receiver check, look at the
244
+ // declarations of the method that holds the call first, then the file-wide map
245
+ // (fields, members declared outside any method).
246
+ const methodTypeMaps = new Map();
247
+ const csharpReceiverTypeAt = (inv, name) => {
248
+ let n = inv.parent;
249
+ while (n && n.type !== 'method_declaration' && n.type !== 'constructor_declaration' &&
250
+ n.type !== 'local_function_statement' && n.type !== 'accessor_declaration')
251
+ n = n.parent;
252
+ if (!n)
253
+ return typeMap.get(name);
254
+ let scoped = methodTypeMaps.get(n.id);
255
+ if (!scoped) {
256
+ scoped = buildCSharpReceiverTypeMap(tree, cache, n);
257
+ methodTypeMaps.set(n.id, scoped);
258
+ }
259
+ return scoped.get(name) ?? typeMap.get(name);
260
+ };
246
261
  const invocations = getNodesFromCache(tree.rootNode, 'invocation_expression', cache);
247
262
  for (const inv of invocations) {
248
263
  const fn = inv.childForFieldName('function');
@@ -271,6 +286,14 @@ function extractCSharpCalls(tree, cache) {
271
286
  if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
272
287
  args = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args];
273
288
  }
289
+ // cognium-dev#594: `proc.Start()` takes no argument; the executable rides the
290
+ // `Process` object (`proc.StartInfo.FileName = tainted`, seeded in
291
+ // taint-propagation). Surface the receiver as arg[0], as for `Execute*()`
292
+ // above. Gated on the receiver being a declared `Process` so `Stopwatch`,
293
+ // `Timer` and `Thread` `Start()` calls are untouched.
294
+ if (receiver && methodName === 'Start' && args.length === 0 && csharpReceiverTypeAt(inv, receiver) === 'Process') {
295
+ args = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }];
296
+ }
274
297
  // `Response.Headers.Add(name, value)` is the ASP.NET Core response-header
275
298
  // writer, but a bare `Add` is far too common to register as a CRLF sink
276
299
  // (List.Add, Dictionary.Add, …). Normalize the header-dictionary form to the
@@ -288,7 +311,7 @@ function extractCSharpCalls(tree, cache) {
288
311
  calls.push({
289
312
  method_name: methodName,
290
313
  receiver,
291
- receiver_type: receiverTypeFromCreation ?? (receiver ? (typeMap.get(receiver) ?? null) : null),
314
+ receiver_type: receiverTypeFromCreation ?? (receiver ? (csharpReceiverTypeAt(inv, receiver) ?? null) : null),
292
315
  receiver_type_fqn: null,
293
316
  arguments: args,
294
317
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
@@ -1505,7 +1528,7 @@ const JAVA_LANG_TYPES = new Set([
1505
1528
  /**
1506
1529
  * Extract call information from a method_invocation node.
1507
1530
  */
1508
- function extractCallInfo(node, context) {
1531
+ function extractCallInfo(node, context, typed = false) {
1509
1532
  // Get method name
1510
1533
  const nameNode = node.childForFieldName('name');
1511
1534
  const methodName = nameNode ? getNodeText(nameNode) : 'unknown';
@@ -1514,13 +1537,23 @@ function extractCallInfo(node, context) {
1514
1537
  const receiver = objectNode ? getNodeText(objectNode) : null;
1515
1538
  // Get arguments
1516
1539
  const argsNode = node.childForFieldName('arguments');
1517
- const args = argsNode ? extractArguments(argsNode) : [];
1540
+ const args = argsNode ? extractArguments(argsNode, typed) : [];
1518
1541
  // Find enclosing method
1519
1542
  const enclosingMethod = findEnclosingMethod(node);
1520
1543
  // Resolve the call
1521
1544
  const { resolved, resolution } = resolveMethodCall(methodName, receiver, context);
1522
- // Resolve the receiver's declared type and FQN
1523
- const { simpleName, fqn } = resolveReceiverType(receiver, context);
1545
+ // Resolve the receiver's declared type and FQN. For navigation, a plain
1546
+ // variable is looked up where it is used, so a lambda parameter or a name
1547
+ // two methods declare differently gets its own type, or none; the
1548
+ // file-wide table is the fallback for what no enclosing scope declares.
1549
+ let { simpleName, fqn } = resolveReceiverType(receiver, context);
1550
+ if (typed && objectNode?.type === 'identifier') {
1551
+ const scoped = lookupInScope(objectNode, getNodeText(objectNode));
1552
+ if (scoped === null)
1553
+ ({ simpleName, fqn } = { simpleName: null, fqn: null });
1554
+ else if (scoped !== undefined)
1555
+ ({ simpleName, fqn } = resolveFqn(stripGenerics(scoped), context));
1556
+ }
1524
1557
  return {
1525
1558
  method_name: methodName,
1526
1559
  receiver,
@@ -1539,7 +1572,7 @@ function extractCallInfo(node, context) {
1539
1572
  /**
1540
1573
  * Extract call information from an object_creation_expression (new ...).
1541
1574
  */
1542
- function extractObjectCreation(node, context) {
1575
+ function extractObjectCreation(node, context, typed = false) {
1543
1576
  // Get the type being instantiated
1544
1577
  const typeNode = node.childForFieldName('type');
1545
1578
  if (!typeNode)
@@ -1547,7 +1580,7 @@ function extractObjectCreation(node, context) {
1547
1580
  const typeName = getNodeText(typeNode);
1548
1581
  // Get arguments
1549
1582
  const argsNode = node.childForFieldName('arguments');
1550
- const args = argsNode ? extractArguments(argsNode) : [];
1583
+ const args = argsNode ? extractArguments(argsNode, typed) : [];
1551
1584
  // Find enclosing method
1552
1585
  const enclosingMethod = findEnclosingMethod(node);
1553
1586
  // Constructor calls are always resolved (we know the class)
@@ -1578,7 +1611,7 @@ function extractObjectCreation(node, context) {
1578
1611
  /**
1579
1612
  * Extract arguments from an argument_list node.
1580
1613
  */
1581
- function extractArguments(argsNode) {
1614
+ function extractArguments(argsNode, typed = false) {
1582
1615
  const args = [];
1583
1616
  let position = 0;
1584
1617
  for (let i = 0; i < argsNode.childCount; i++) {
@@ -1591,16 +1624,293 @@ function extractArguments(argsNode) {
1591
1624
  }
1592
1625
  const expression = getNodeText(child);
1593
1626
  const { variable, literal } = analyzeArgument(child);
1594
- args.push({
1627
+ const arg = {
1595
1628
  position,
1596
1629
  expression,
1597
1630
  variable,
1598
1631
  literal,
1599
- });
1632
+ };
1633
+ if (typed) {
1634
+ arg.type = javaStaticType(child);
1635
+ arg.location = { line: child.startPosition.row + 1, column: child.startPosition.column };
1636
+ }
1637
+ args.push(arg);
1600
1638
  position++;
1601
1639
  }
1602
1640
  return args;
1603
1641
  }
1642
+ const NUMERIC_RANK = { byte: 1, short: 2, char: 2, int: 3, long: 4, float: 5, double: 6 };
1643
+ const BOOLEAN_OPERATORS = new Set(['==', '!=', '<', '>', '<=', '>=', '&&', '||', 'instanceof']);
1644
+ /**
1645
+ * The static type of a Java expression, as far as the syntax and the
1646
+ * declarations in scope say it — and `null` past that.
1647
+ *
1648
+ * Read for overload selection, so it errs one way only: a type it cannot be
1649
+ * sure of is `null` ("could be anything"), never a guess. A method call's
1650
+ * result is not typed here; the navigation index types it by binding that
1651
+ * call, which needs the whole tree.
1652
+ */
1653
+ function javaStaticType(node) {
1654
+ const text = getNodeText(node);
1655
+ switch (node.type) {
1656
+ case 'string_literal':
1657
+ case 'text_block':
1658
+ return 'String';
1659
+ case 'character_literal':
1660
+ return 'char';
1661
+ case 'decimal_integer_literal':
1662
+ case 'hex_integer_literal':
1663
+ case 'octal_integer_literal':
1664
+ case 'binary_integer_literal':
1665
+ return /[lL]$/.test(text) ? 'long' : 'int';
1666
+ case 'decimal_floating_point_literal':
1667
+ case 'hex_floating_point_literal':
1668
+ return /[fF]$/.test(text) ? 'float' : 'double';
1669
+ case 'true':
1670
+ case 'false':
1671
+ case 'instanceof_expression':
1672
+ return 'boolean';
1673
+ case 'null_literal':
1674
+ return 'null';
1675
+ case 'class_literal':
1676
+ return 'Class';
1677
+ case 'parenthesized_expression': {
1678
+ const inner = node.namedChild(0);
1679
+ return inner ? javaStaticType(inner) : null;
1680
+ }
1681
+ case 'cast_expression': {
1682
+ const t = node.childForFieldName('type');
1683
+ return t ? getNodeText(t) : null;
1684
+ }
1685
+ case 'object_creation_expression': {
1686
+ const t = node.childForFieldName('type');
1687
+ return t ? getNodeText(t) : null;
1688
+ }
1689
+ case 'array_creation_expression': {
1690
+ const t = node.childForFieldName('type');
1691
+ if (!t)
1692
+ return null;
1693
+ const dims = (text.match(/\[/g) ?? []).length - (getNodeText(t).match(/\[/g) ?? []).length;
1694
+ return `${getNodeText(t)}${'[]'.repeat(Math.max(dims, 1))}`;
1695
+ }
1696
+ case 'unary_expression': {
1697
+ const op = node.childForFieldName('operator');
1698
+ const operand = node.childForFieldName('operand');
1699
+ if (op && getNodeText(op) === '!')
1700
+ return 'boolean';
1701
+ const t = operand ? javaStaticType(operand) : null;
1702
+ return t && t in NUMERIC_RANK ? (NUMERIC_RANK[t] < 3 ? 'int' : t) : null;
1703
+ }
1704
+ case 'binary_expression': {
1705
+ const op = node.childForFieldName('operator');
1706
+ const opText = op ? getNodeText(op) : '';
1707
+ if (BOOLEAN_OPERATORS.has(opText))
1708
+ return 'boolean';
1709
+ const left = node.childForFieldName('left');
1710
+ const right = node.childForFieldName('right');
1711
+ const lt = left ? javaStaticType(left) : null;
1712
+ const rt = right ? javaStaticType(right) : null;
1713
+ if (opText === '+' && (lt === 'String' || rt === 'String'))
1714
+ return 'String';
1715
+ // Binary numeric promotion, only when both sides are known primitives.
1716
+ if (lt && rt && lt in NUMERIC_RANK && rt in NUMERIC_RANK) {
1717
+ const wider = NUMERIC_RANK[lt] >= NUMERIC_RANK[rt] ? lt : rt;
1718
+ return NUMERIC_RANK[wider] < 3 ? 'int' : wider;
1719
+ }
1720
+ return null;
1721
+ }
1722
+ case 'ternary_expression': {
1723
+ const a = node.childForFieldName('consequence');
1724
+ const b = node.childForFieldName('alternative');
1725
+ const at = a ? javaStaticType(a) : null;
1726
+ const bt = b ? javaStaticType(b) : null;
1727
+ return at !== null && at === bt ? at : null;
1728
+ }
1729
+ case 'identifier':
1730
+ return declaredTypeInScope(node, text);
1731
+ case 'field_access': {
1732
+ const obj = node.childForFieldName('object');
1733
+ const field = node.childForFieldName('field');
1734
+ if (obj?.type !== 'this' || !field)
1735
+ return null;
1736
+ const body = findAncestor(node, 'class_body') ?? findAncestor(node, 'enum_body_declarations');
1737
+ return body ? fieldTypeIn(body, getNodeText(field)) : null;
1738
+ }
1739
+ default:
1740
+ return null;
1741
+ }
1742
+ }
1743
+ /**
1744
+ * The declared type of the variable `name` as seen from `node`: the nearest
1745
+ * enclosing local, parameter, resource, loop or catch variable, then a field
1746
+ * of an enclosing type. `null` when the nearest declaration has no written
1747
+ * type (a lambda's inferred parameter, `var` without a readable initializer)
1748
+ * or when nothing in the file declares it — an inherited field, say.
1749
+ */
1750
+ function declaredTypeInScope(node, name) {
1751
+ return lookupInScope(node, name) ?? null;
1752
+ }
1753
+ /** `declaredTypeInScope`, telling "declared without a written type" (`null`) from "not declared here" (`undefined`). */
1754
+ function lookupInScope(node, name) {
1755
+ const typeOf = (typeNode, value) => {
1756
+ if (!typeNode)
1757
+ return null;
1758
+ const t = getNodeText(typeNode);
1759
+ if (t !== 'var')
1760
+ return t;
1761
+ return value ? javaStaticType(value) : null;
1762
+ };
1763
+ const fromDeclaration = (decl) => {
1764
+ for (const d of decl.namedChildren) {
1765
+ if (d?.type !== 'variable_declarator')
1766
+ continue;
1767
+ const n = d.childForFieldName('name');
1768
+ if (n && getNodeText(n) === name)
1769
+ return typeOf(decl.childForFieldName('type'), d.childForFieldName('value'));
1770
+ }
1771
+ return undefined;
1772
+ };
1773
+ const fromParameters = (params) => {
1774
+ for (const p of params.namedChildren) {
1775
+ if (!p)
1776
+ continue;
1777
+ if (p.type === 'formal_parameter') {
1778
+ const n = p.childForFieldName('name');
1779
+ if (n && getNodeText(n) === name)
1780
+ return typeOf(p.childForFieldName('type'), null);
1781
+ }
1782
+ else if (p.type === 'spread_parameter') {
1783
+ const d = p.namedChildren.find(c => c?.type === 'variable_declarator');
1784
+ const n = d?.childForFieldName('name');
1785
+ const t = p.namedChildren.find(c => c !== null && /type/.test(c.type));
1786
+ if (n && getNodeText(n) === name)
1787
+ return t ? `${getNodeText(t)}[]` : null;
1788
+ }
1789
+ else if (p.type === 'identifier' && getNodeText(p) === name) {
1790
+ return null; // an inferred lambda parameter
1791
+ }
1792
+ }
1793
+ return undefined;
1794
+ };
1795
+ let child = node;
1796
+ for (let scope = node.parent; scope; child = scope, scope = scope.parent) {
1797
+ let found;
1798
+ switch (scope.type) {
1799
+ case 'block':
1800
+ case 'constructor_body':
1801
+ case 'switch_block_statement_group':
1802
+ for (const stmt of scope.namedChildren) {
1803
+ if (!stmt || stmt.startIndex >= child.startIndex)
1804
+ break;
1805
+ if (stmt.type === 'local_variable_declaration')
1806
+ found = fromDeclaration(stmt) ?? found;
1807
+ }
1808
+ break;
1809
+ case 'for_statement':
1810
+ for (const init of scope.childrenForFieldName('init')) {
1811
+ if (init?.type === 'local_variable_declaration')
1812
+ found = fromDeclaration(init) ?? found;
1813
+ }
1814
+ break;
1815
+ case 'enhanced_for_statement': {
1816
+ const n = scope.childForFieldName('name');
1817
+ if (n && getNodeText(n) === name)
1818
+ found = typeOf(scope.childForFieldName('type'), null);
1819
+ break;
1820
+ }
1821
+ case 'catch_clause': {
1822
+ const p = scope.namedChildren.find(c => c?.type === 'catch_formal_parameter');
1823
+ const n = p?.childForFieldName('name');
1824
+ if (p && n && getNodeText(n) === name) {
1825
+ const ct = p.namedChildren.find(c => c?.type === 'catch_type');
1826
+ // A multi-catch has no single static type that names a class here.
1827
+ found = ct && ct.namedChildCount === 1 ? getNodeText(ct) : null;
1828
+ }
1829
+ break;
1830
+ }
1831
+ case 'try_with_resources_statement': {
1832
+ const spec = scope.childForFieldName('resources');
1833
+ for (const r of spec?.namedChildren ?? []) {
1834
+ const n = r?.childForFieldName('name');
1835
+ if (r && n && getNodeText(n) === name)
1836
+ found = typeOf(r.childForFieldName('type'), r.childForFieldName('value'));
1837
+ }
1838
+ break;
1839
+ }
1840
+ case 'lambda_expression': {
1841
+ const params = scope.childForFieldName('parameters');
1842
+ if (params?.type === 'identifier')
1843
+ found = getNodeText(params) === name ? forEachElementType(scope) : undefined;
1844
+ else if (params?.type === 'inferred_parameters') {
1845
+ found = params.namedChildren.some(c => c && getNodeText(c) === name) ? null : undefined;
1846
+ }
1847
+ else if (params)
1848
+ found = fromParameters(params);
1849
+ break;
1850
+ }
1851
+ case 'method_declaration':
1852
+ case 'constructor_declaration':
1853
+ case 'record_declaration': {
1854
+ const params = scope.childForFieldName('parameters');
1855
+ if (params)
1856
+ found = fromParameters(params);
1857
+ break;
1858
+ }
1859
+ case 'class_body':
1860
+ case 'interface_body':
1861
+ case 'enum_body_declarations': {
1862
+ const t = fieldTypeIn(scope, name);
1863
+ if (t !== null)
1864
+ found = t;
1865
+ break;
1866
+ }
1867
+ default:
1868
+ break;
1869
+ }
1870
+ if (found !== undefined)
1871
+ return found;
1872
+ }
1873
+ return undefined;
1874
+ }
1875
+ const ITERABLE_TYPES = new Set([
1876
+ 'Iterable', 'Collection', 'List', 'Set', 'SortedSet', 'NavigableSet', 'Queue', 'Deque',
1877
+ 'ArrayList', 'LinkedList', 'HashSet', 'LinkedHashSet', 'TreeSet', 'ArrayDeque',
1878
+ ]);
1879
+ /**
1880
+ * The parameter type of `xs.forEach(x -> …)`, where `xs` is declared as a JDK
1881
+ * collection of one element type: `List<Loan> xs` gives `Loan`. Anything
1882
+ * else — another method, a wildcard, a map, an untyped receiver — is `null`.
1883
+ */
1884
+ function forEachElementType(lambda) {
1885
+ const args = lambda.parent;
1886
+ const inv = args?.parent;
1887
+ if (args?.type !== 'argument_list' || inv?.type !== 'method_invocation' || args.namedChildCount !== 1)
1888
+ return null;
1889
+ const name = inv.childForFieldName('name');
1890
+ const obj = inv.childForFieldName('object');
1891
+ if (!name || getNodeText(name) !== 'forEach' || obj?.type !== 'identifier')
1892
+ return null;
1893
+ const declared = declaredTypeInScope(obj, getNodeText(obj));
1894
+ const m = declared ? /^(?:java\.util\.|java\.lang\.)?(\w+)\s*<\s*([\w.]+)\s*>$/.exec(declared.trim()) : null;
1895
+ return m && ITERABLE_TYPES.has(m[1]) ? m[2] : null;
1896
+ }
1897
+ /** The written type of field `name` declared directly in a type body, or `null`. */
1898
+ function fieldTypeIn(body, name) {
1899
+ for (const f of body.namedChildren) {
1900
+ if (f?.type !== 'field_declaration' && f?.type !== 'constant_declaration')
1901
+ continue;
1902
+ for (const d of f.namedChildren) {
1903
+ if (d?.type !== 'variable_declarator')
1904
+ continue;
1905
+ const n = d.childForFieldName('name');
1906
+ if (n && getNodeText(n) === name) {
1907
+ const t = f.childForFieldName('type');
1908
+ return t ? getNodeText(t) : null;
1909
+ }
1910
+ }
1911
+ }
1912
+ return null;
1913
+ }
1604
1914
  /**
1605
1915
  * Analyze an argument to extract variable name or literal value.
1606
1916
  */