circle-ir 4.9.29 → 4.11.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 (45) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +23 -0
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  5. package/dist/analysis/passes/language-sources-pass.js +39 -0
  6. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  7. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +53 -0
  8. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -0
  9. package/dist/analysis/passes/param-source-flow-gate-pass.js +144 -0
  10. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -0
  11. package/dist/analysis/passes/scan-secrets-pass.d.ts +3 -3
  12. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  13. package/dist/analysis/passes/scan-secrets-pass.js +31 -3
  14. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  15. package/dist/analyzer.d.ts +22 -0
  16. package/dist/analyzer.d.ts.map +1 -1
  17. package/dist/analyzer.js +10 -1
  18. package/dist/analyzer.js.map +1 -1
  19. package/dist/browser/circle-ir.js +233 -11
  20. package/dist/core/circle-ir-core.cjs +94 -3
  21. package/dist/core/circle-ir-core.js +94 -3
  22. package/dist/core/extractors/types.d.ts +28 -1
  23. package/dist/core/extractors/types.d.ts.map +1 -1
  24. package/dist/core/extractors/types.js +86 -3
  25. package/dist/core/extractors/types.js.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/navigation/index.d.ts +8 -0
  31. package/dist/navigation/index.d.ts.map +1 -0
  32. package/dist/navigation/index.js +6 -0
  33. package/dist/navigation/index.js.map +1 -0
  34. package/dist/navigation/navigation-index.d.ts +208 -0
  35. package/dist/navigation/navigation-index.d.ts.map +1 -0
  36. package/dist/navigation/navigation-index.js +968 -0
  37. package/dist/navigation/navigation-index.js.map +1 -0
  38. package/dist/navigation/types.d.ts +160 -0
  39. package/dist/navigation/types.d.ts.map +1 -0
  40. package/dist/navigation/types.js +17 -0
  41. package/dist/navigation/types.js.map +1 -0
  42. package/dist/types/index.d.ts +19 -0
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/dist/types/index.js.map +1 -1
  45. package/package.json +3 -3
@@ -4458,7 +4458,7 @@ function detectLanguage(tree) {
4458
4458
  if (pythonScore > jsScore && pythonScore > javaScore) return "python";
4459
4459
  return jsScore > javaScore ? "javascript" : "java";
4460
4460
  }
4461
- function extractTypes(tree, cache, language) {
4461
+ function extractTypes(tree, cache, language, options) {
4462
4462
  const effectiveLanguage = language ?? detectLanguage(tree);
4463
4463
  const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
4464
4464
  const isPython = effectiveLanguage === "python";
@@ -4478,7 +4478,7 @@ function extractTypes(tree, cache, language) {
4478
4478
  if (effectiveLanguage === "csharp") {
4479
4479
  return extractCSharpTypes(tree, cache);
4480
4480
  }
4481
- return extractJavaTypes(tree, cache);
4481
+ return extractJavaTypes(tree, cache, options);
4482
4482
  }
4483
4483
  function csharpMemberNodes(body2) {
4484
4484
  if (!body2) return [];
@@ -4619,7 +4619,7 @@ function extractCSharpModifiers(node) {
4619
4619
  }
4620
4620
  return mods;
4621
4621
  }
4622
- function extractJavaTypes(tree, cache) {
4622
+ function extractJavaTypes(tree, cache, options) {
4623
4623
  const types = [];
4624
4624
  const classes = getNodesFromCache(tree.rootNode, "class_declaration", cache);
4625
4625
  for (const cls of classes) {
@@ -4633,8 +4633,76 @@ function extractJavaTypes(tree, cache) {
4633
4633
  for (const enumDecl of enums) {
4634
4634
  types.push(extractEnumInfo(enumDecl));
4635
4635
  }
4636
+ if (!options?.navigationTypes) return types;
4637
+ for (const rec of getNodesFromCache(tree.rootNode, "record_declaration", cache)) {
4638
+ const info2 = extractClassInfo(rec);
4639
+ info2.is_record = true;
4640
+ for (const comp of recordComponents(rec)) {
4641
+ if (!info2.fields.some((f) => f.name === comp.name)) {
4642
+ info2.fields.push({
4643
+ name: comp.name,
4644
+ type: comp.type,
4645
+ modifiers: ["private", "final"],
4646
+ annotations: []
4647
+ });
4648
+ }
4649
+ if (!info2.methods.some((m) => m.name === comp.name)) {
4650
+ info2.methods.push({
4651
+ name: comp.name,
4652
+ return_type: comp.type,
4653
+ parameters: [],
4654
+ annotations: [],
4655
+ modifiers: ["public"],
4656
+ start_line: rec.startPosition.row + 1,
4657
+ end_line: rec.startPosition.row + 1
4658
+ });
4659
+ }
4660
+ }
4661
+ types.push(info2);
4662
+ }
4663
+ const declNodes = [
4664
+ ...classes,
4665
+ ...interfaces,
4666
+ ...enums,
4667
+ ...getNodesFromCache(tree.rootNode, "record_declaration", cache)
4668
+ ];
4669
+ for (const t of types) {
4670
+ const node = declNodes.find(
4671
+ (n) => n.startPosition.row + 1 === t.start_line && getIdentifier(n, "name") === t.name
4672
+ );
4673
+ const path = node ? enclosingTypePath(node) : [];
4674
+ if (path.length > 0) t.enclosing_type = path.join(".");
4675
+ }
4636
4676
  return types;
4637
4677
  }
4678
+ function recordComponents(node) {
4679
+ const out2 = [];
4680
+ const params = node.childForFieldName("parameters");
4681
+ if (!params) return out2;
4682
+ for (let i2 = 0; i2 < params.childCount; i2++) {
4683
+ const c = params.child(i2);
4684
+ if (!c || c.type !== "formal_parameter") continue;
4685
+ const name2 = c.childForFieldName("name");
4686
+ const type = c.childForFieldName("type");
4687
+ if (name2) out2.push({ name: getNodeText(name2), type: type ? getNodeText(type) : null });
4688
+ }
4689
+ return out2;
4690
+ }
4691
+ function enclosingTypePath(node) {
4692
+ const DECLS = /* @__PURE__ */ new Set([
4693
+ "class_declaration",
4694
+ "interface_declaration",
4695
+ "enum_declaration",
4696
+ "record_declaration"
4697
+ ]);
4698
+ const path = [];
4699
+ for (let p = node.parent; p; p = p.parent) {
4700
+ if (!DECLS.has(p.type)) continue;
4701
+ const n = getIdentifier(p, "name");
4702
+ if (n) path.unshift(n);
4703
+ }
4704
+ return path;
4705
+ }
4638
4706
  function extractJavaScriptTypes(tree, cache) {
4639
4707
  const types = [];
4640
4708
  const classes = getNodesFromCache(tree.rootNode, "class_declaration", cache);
@@ -13417,6 +13485,29 @@ var DEFAULT_SINKS = [
13417
13485
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13418
13486
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13419
13487
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13488
+ // cognium-dev#541 — the crates the removed classless rows were silently
13489
+ // covering. #484 replaced a classless `from_str` / `from_slice` with
13490
+ // class-scoped rows, but only for the four formats that already had entries;
13491
+ // every other serde data format lost its sink, and `rmp_serde::from_slice`
13492
+ // on bytes read straight off a `TcpStream` stopped being reported. Each row
13493
+ // below is a serde *data format* entry point — a function whose whole job is
13494
+ // to build a typed value out of untrusted bytes or text.
13495
+ { method: "from_slice", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13496
+ { method: "from_read", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13497
+ { method: "from_read_ref", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13498
+ { method: "from_slice", class: "serde_yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13499
+ { method: "from_slice", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13500
+ { method: "from_reader", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13501
+ { method: "from_bytes", class: "postcard", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13502
+ { method: "from_slice", class: "serde_cbor", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13503
+ { method: "from_reader", class: "serde_cbor", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13504
+ { method: "from_reader", class: "ciborium", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13505
+ { method: "from_str", class: "serde_xml_rs", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13506
+ { method: "from_reader", class: "serde_xml_rs", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13507
+ { method: "from_str", class: "quick_xml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13508
+ { method: "from_reader", class: "quick_xml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13509
+ { method: "from_slice", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13510
+ { method: "deserialize_from", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13420
13511
  // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13421
13512
  // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13422
13513
  // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
@@ -4392,7 +4392,7 @@ function detectLanguage(tree) {
4392
4392
  if (pythonScore > jsScore && pythonScore > javaScore) return "python";
4393
4393
  return jsScore > javaScore ? "javascript" : "java";
4394
4394
  }
4395
- function extractTypes(tree, cache, language) {
4395
+ function extractTypes(tree, cache, language, options) {
4396
4396
  const effectiveLanguage = language ?? detectLanguage(tree);
4397
4397
  const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
4398
4398
  const isPython = effectiveLanguage === "python";
@@ -4412,7 +4412,7 @@ function extractTypes(tree, cache, language) {
4412
4412
  if (effectiveLanguage === "csharp") {
4413
4413
  return extractCSharpTypes(tree, cache);
4414
4414
  }
4415
- return extractJavaTypes(tree, cache);
4415
+ return extractJavaTypes(tree, cache, options);
4416
4416
  }
4417
4417
  function csharpMemberNodes(body2) {
4418
4418
  if (!body2) return [];
@@ -4553,7 +4553,7 @@ function extractCSharpModifiers(node) {
4553
4553
  }
4554
4554
  return mods;
4555
4555
  }
4556
- function extractJavaTypes(tree, cache) {
4556
+ function extractJavaTypes(tree, cache, options) {
4557
4557
  const types = [];
4558
4558
  const classes = getNodesFromCache(tree.rootNode, "class_declaration", cache);
4559
4559
  for (const cls of classes) {
@@ -4567,8 +4567,76 @@ function extractJavaTypes(tree, cache) {
4567
4567
  for (const enumDecl of enums) {
4568
4568
  types.push(extractEnumInfo(enumDecl));
4569
4569
  }
4570
+ if (!options?.navigationTypes) return types;
4571
+ for (const rec of getNodesFromCache(tree.rootNode, "record_declaration", cache)) {
4572
+ const info2 = extractClassInfo(rec);
4573
+ info2.is_record = true;
4574
+ for (const comp of recordComponents(rec)) {
4575
+ if (!info2.fields.some((f) => f.name === comp.name)) {
4576
+ info2.fields.push({
4577
+ name: comp.name,
4578
+ type: comp.type,
4579
+ modifiers: ["private", "final"],
4580
+ annotations: []
4581
+ });
4582
+ }
4583
+ if (!info2.methods.some((m) => m.name === comp.name)) {
4584
+ info2.methods.push({
4585
+ name: comp.name,
4586
+ return_type: comp.type,
4587
+ parameters: [],
4588
+ annotations: [],
4589
+ modifiers: ["public"],
4590
+ start_line: rec.startPosition.row + 1,
4591
+ end_line: rec.startPosition.row + 1
4592
+ });
4593
+ }
4594
+ }
4595
+ types.push(info2);
4596
+ }
4597
+ const declNodes = [
4598
+ ...classes,
4599
+ ...interfaces,
4600
+ ...enums,
4601
+ ...getNodesFromCache(tree.rootNode, "record_declaration", cache)
4602
+ ];
4603
+ for (const t of types) {
4604
+ const node = declNodes.find(
4605
+ (n) => n.startPosition.row + 1 === t.start_line && getIdentifier(n, "name") === t.name
4606
+ );
4607
+ const path = node ? enclosingTypePath(node) : [];
4608
+ if (path.length > 0) t.enclosing_type = path.join(".");
4609
+ }
4570
4610
  return types;
4571
4611
  }
4612
+ function recordComponents(node) {
4613
+ const out2 = [];
4614
+ const params = node.childForFieldName("parameters");
4615
+ if (!params) return out2;
4616
+ for (let i2 = 0; i2 < params.childCount; i2++) {
4617
+ const c = params.child(i2);
4618
+ if (!c || c.type !== "formal_parameter") continue;
4619
+ const name2 = c.childForFieldName("name");
4620
+ const type = c.childForFieldName("type");
4621
+ if (name2) out2.push({ name: getNodeText(name2), type: type ? getNodeText(type) : null });
4622
+ }
4623
+ return out2;
4624
+ }
4625
+ function enclosingTypePath(node) {
4626
+ const DECLS = /* @__PURE__ */ new Set([
4627
+ "class_declaration",
4628
+ "interface_declaration",
4629
+ "enum_declaration",
4630
+ "record_declaration"
4631
+ ]);
4632
+ const path = [];
4633
+ for (let p = node.parent; p; p = p.parent) {
4634
+ if (!DECLS.has(p.type)) continue;
4635
+ const n = getIdentifier(p, "name");
4636
+ if (n) path.unshift(n);
4637
+ }
4638
+ return path;
4639
+ }
4572
4640
  function extractJavaScriptTypes(tree, cache) {
4573
4641
  const types = [];
4574
4642
  const classes = getNodesFromCache(tree.rootNode, "class_declaration", cache);
@@ -13351,6 +13419,29 @@ var DEFAULT_SINKS = [
13351
13419
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13352
13420
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13353
13421
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13422
+ // cognium-dev#541 — the crates the removed classless rows were silently
13423
+ // covering. #484 replaced a classless `from_str` / `from_slice` with
13424
+ // class-scoped rows, but only for the four formats that already had entries;
13425
+ // every other serde data format lost its sink, and `rmp_serde::from_slice`
13426
+ // on bytes read straight off a `TcpStream` stopped being reported. Each row
13427
+ // below is a serde *data format* entry point — a function whose whole job is
13428
+ // to build a typed value out of untrusted bytes or text.
13429
+ { method: "from_slice", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13430
+ { method: "from_read", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13431
+ { method: "from_read_ref", class: "rmp_serde", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13432
+ { method: "from_slice", class: "serde_yaml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13433
+ { method: "from_slice", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13434
+ { method: "from_reader", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13435
+ { method: "from_bytes", class: "postcard", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13436
+ { method: "from_slice", class: "serde_cbor", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13437
+ { method: "from_reader", class: "serde_cbor", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13438
+ { method: "from_reader", class: "ciborium", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13439
+ { method: "from_str", class: "serde_xml_rs", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13440
+ { method: "from_reader", class: "serde_xml_rs", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13441
+ { method: "from_str", class: "quick_xml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13442
+ { method: "from_reader", class: "quick_xml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13443
+ { method: "from_slice", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13444
+ { method: "deserialize_from", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13354
13445
  // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13355
13446
  // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13356
13447
  // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
@@ -4,8 +4,35 @@
4
4
  import type { Tree } from 'web-tree-sitter';
5
5
  import type { TypeInfo, SupportedLanguage } from '../../types/index.js';
6
6
  import { type NodeCache } from '../parser.js';
7
+ /**
8
+ * Extra type information the navigation API needs and no existing consumer
9
+ * expects. Every field it adds is optional and absent unless asked for, so
10
+ * `extractTypes(tree, cache, language)` returns exactly what it always did.
11
+ *
12
+ * Two things are missing from the default Java extraction, both found by
13
+ * auditing the call sites the resolver cannot bind:
14
+ *
15
+ * - **A `record` yields no type at all.** `extractJavaTypes` walks
16
+ * `class_declaration`, `interface_declaration` and `enum_declaration`;
17
+ * `record_declaration` is not among them, so a record is invisible — not
18
+ * in `ir.types`, and its body's calls are not in `ir.calls` either.
19
+ * Records are ordinary Java 17+ code and cost 182 of 455 bindable misses
20
+ * in the 405-file sample, the single largest cause.
21
+ * - **Nesting is lost.** `Outer.Inner` is emitted as a type named `Inner`,
22
+ * so its fully-qualified name collides with a top-level `Inner` and an
23
+ * `import static app.Outer.Inner.make` cannot be matched to it.
24
+ *
25
+ * Both are fixed here rather than in the default path because `ir.types` is
26
+ * read by the SAST passes and by the method-scoping gate: adding a type, or
27
+ * changing one's identity, changes their results and belongs behind the SAST
28
+ * corpora gates, not in a navigation change.
29
+ */
30
+ export interface TypeExtractionOptions {
31
+ /** Emit `record_declaration` types, and set `enclosing_type` on nested types. */
32
+ navigationTypes?: boolean;
33
+ }
7
34
  /**
8
35
  * Extract all type definitions from the tree.
9
36
  */
10
- export declare function extractTypes(tree: Tree, cache?: NodeCache, language?: SupportedLanguage): TypeInfo[];
37
+ export declare function extractTypes(tree: Tree, cache?: NodeCache, language?: SupportedLanguage, options?: TypeExtractionOptions): TypeInfo[];
11
38
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/extractors/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAQ,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAwC,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9G,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAgDzF;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,QAAQ,EAAE,CAsBpG"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/extractors/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAQ,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAwC,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9G,OAAO,EAA6C,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAgDzF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,IAAI,EACV,KAAK,CAAC,EAAE,SAAS,EACjB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,QAAQ,EAAE,CAsBZ"}
@@ -49,7 +49,7 @@ function detectLanguage(tree) {
49
49
  /**
50
50
  * Extract all type definitions from the tree.
51
51
  */
52
- export function extractTypes(tree, cache, language) {
52
+ export function extractTypes(tree, cache, language, options) {
53
53
  const effectiveLanguage = language ?? detectLanguage(tree);
54
54
  const isJavaScript = effectiveLanguage === 'javascript' || effectiveLanguage === 'typescript' || effectiveLanguage === 'tsx';
55
55
  const isPython = effectiveLanguage === 'python';
@@ -69,7 +69,7 @@ export function extractTypes(tree, cache, language) {
69
69
  if (effectiveLanguage === 'csharp') {
70
70
  return extractCSharpTypes(tree, cache);
71
71
  }
72
- return extractJavaTypes(tree, cache);
72
+ return extractJavaTypes(tree, cache, options);
73
73
  }
74
74
  /**
75
75
  * C# type extraction (Phase-0 spike — cognium-dev C#/.NET epic).
@@ -278,7 +278,7 @@ function extractCSharpModifiers(node) {
278
278
  /**
279
279
  * Extract Java types.
280
280
  */
281
- function extractJavaTypes(tree, cache) {
281
+ function extractJavaTypes(tree, cache, options) {
282
282
  const types = [];
283
283
  // Extract classes
284
284
  const classes = getNodesFromCache(tree.rootNode, 'class_declaration', cache);
@@ -295,8 +295,91 @@ function extractJavaTypes(tree, cache) {
295
295
  for (const enumDecl of enums) {
296
296
  types.push(extractEnumInfo(enumDecl));
297
297
  }
298
+ if (!options?.navigationTypes)
299
+ return types;
300
+ // Records, which the default path omits entirely. A record's body holds
301
+ // ordinary method declarations, so `extractClassInfo` reads it unchanged;
302
+ // only the node type and the `is_record` marker differ.
303
+ for (const rec of getNodesFromCache(tree.rootNode, 'record_declaration', cache)) {
304
+ const info = extractClassInfo(rec);
305
+ info.is_record = true;
306
+ // A record's components are its fields and its accessors. Both are
307
+ // declared by the header, not written in the body, so neither
308
+ // `extractFields` nor `extractMethods` can see them.
309
+ for (const comp of recordComponents(rec)) {
310
+ if (!info.fields.some(f => f.name === comp.name)) {
311
+ info.fields.push({
312
+ name: comp.name,
313
+ type: comp.type,
314
+ modifiers: ['private', 'final'],
315
+ annotations: [],
316
+ });
317
+ }
318
+ if (!info.methods.some(m => m.name === comp.name)) {
319
+ info.methods.push({
320
+ name: comp.name,
321
+ return_type: comp.type,
322
+ parameters: [],
323
+ annotations: [],
324
+ modifiers: ['public'],
325
+ start_line: rec.startPosition.row + 1,
326
+ end_line: rec.startPosition.row + 1,
327
+ });
328
+ }
329
+ }
330
+ types.push(info);
331
+ }
332
+ // Nesting, which the default path flattens. Walking every emitted type's
333
+ // ancestors is cheaper than threading the path through five extractors.
334
+ const declNodes = [
335
+ ...classes,
336
+ ...interfaces,
337
+ ...enums,
338
+ ...getNodesFromCache(tree.rootNode, 'record_declaration', cache),
339
+ ];
340
+ for (const t of types) {
341
+ const node = declNodes.find(n => n.startPosition.row + 1 === t.start_line && getIdentifier(n, 'name') === t.name);
342
+ const path = node ? enclosingTypePath(node) : [];
343
+ if (path.length > 0)
344
+ t.enclosing_type = path.join('.');
345
+ }
298
346
  return types;
299
347
  }
348
+ /** The `(String host, int port)` header of a `record_declaration`. */
349
+ function recordComponents(node) {
350
+ const out = [];
351
+ const params = node.childForFieldName('parameters');
352
+ if (!params)
353
+ return out;
354
+ for (let i = 0; i < params.childCount; i++) {
355
+ const c = params.child(i);
356
+ if (!c || c.type !== 'formal_parameter')
357
+ continue;
358
+ const name = c.childForFieldName('name');
359
+ const type = c.childForFieldName('type');
360
+ if (name)
361
+ out.push({ name: getNodeText(name), type: type ? getNodeText(type) : null });
362
+ }
363
+ return out;
364
+ }
365
+ /** The names of the types a declaration sits inside, outermost first. */
366
+ function enclosingTypePath(node) {
367
+ const DECLS = new Set([
368
+ 'class_declaration',
369
+ 'interface_declaration',
370
+ 'enum_declaration',
371
+ 'record_declaration',
372
+ ]);
373
+ const path = [];
374
+ for (let p = node.parent; p; p = p.parent) {
375
+ if (!DECLS.has(p.type))
376
+ continue;
377
+ const n = getIdentifier(p, 'name');
378
+ if (n)
379
+ path.unshift(n);
380
+ }
381
+ return path;
382
+ }
300
383
  /**
301
384
  * Extract JavaScript/TypeScript types.
302
385
  */