mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
fix(java): a record is indexed as a class, with its components' fields and accessors
Neither engine treated record_declaration as a class type, so a Java record
left no node of its own. Its methods and explicit constructor became
package-level functions (p::norm, p::Point), a nested record's methods were
filed under the class around it, compact constructors, static fields and
records nested in a record were dropped, and calls on a record-typed value
resolved to nothing or to a same-named member of another type.
The Java extractor and the kernel now treat record_declaration as a class
and a compact constructor as a method. Each component becomes a private
field ("<Type> <name>") and a public accessor name() with the component's
type as its return type, unless the body declares it; they are synthesized
before Lombok's members, the same way.
An implicit accessor is reached only through what types its receiver: a
typed receiver, a method reference, a qualified call, or a call from inside
the record. The name-only strategies (the receiver-word guesses, exact-name
and fuzzy matching) no longer settle on one, so result.content() on a
library type stops linking to whichever record has a content component.
Records usually sit in sealed hierarchies, and that exposed a resolver gap:
the member-type reader took the head "sealed interface Blocks permits Block"
for a member named Block of type "permits", typed the receiver of
Block.fromKey(...) inside the record as a built-in and dropped the call.
"permits" and "throws" are now among the words that are not a type there.
Validation:
- java-records.test.ts, 5 cases on both engines (10 tests), all red on
main; the sealed-permits and name-only-guess cases are each red without
their resolver line.
- Kernel parity: TortureRecord.java in the torture and CRLF runs; real-repo
sweeps at full parity with deferrals unchanged (jextract 218/218,
spring-modulith 682/682, playframework 801/801, kestra 2997/2997,
spring-ai 2491/2492, Minestom 1859/1894).
- A/B against main d02a88d0, every bucket triaged by sampling (edge sites
added/removed): jextract +207/-1, spring-modulith +375/-1, playframework
+80/-2, Minestom +17,963/-287, spring-ai +8,806/-471, kestra
+10,077/-497; most retargets are a constructor function becoming its
record. The accessor rule drops only name-only results (spring-ai 1,063,
kestra 765, Minestom 408 sites at 0.65-0.8 confidence); no typed call,
method reference or qualified call moves. click and gin byte-identical.
- Cost (process CPU, median of 3 interleaved indexes): Minestom
47.2s -> 50.3s (+6.6%, for 17% more nodes and 23% more edges),
spring-ai 93.5s -> 94.8s (+1.4%).
Original PR #1946 by @danusha2345.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Colby Mchenry
co-authored by
Claude Opus 5.5
parent
a565f215f1
commit
3a93f57ae1
@@ -23,7 +23,11 @@ use tree_sitter::{Node, Parser};
|
||||
const MAX_VALUE_REF_NODES: usize = 20_000;
|
||||
|
||||
fn is_method_type(kind: &str) -> bool {
|
||||
matches!(kind, "method_declaration" | "constructor_declaration")
|
||||
matches!(kind, "method_declaration" | "constructor_declaration" | "compact_constructor_declaration")
|
||||
}
|
||||
/// classTypes (languages/java.ts): a `record` is a class.
|
||||
fn is_class_type(kind: &str) -> bool {
|
||||
matches!(kind, "class_declaration" | "record_declaration")
|
||||
}
|
||||
fn is_interface_type(kind: &str) -> bool {
|
||||
matches!(kind, "interface_declaration" | "annotation_type_declaration")
|
||||
@@ -494,7 +498,7 @@ impl<'t> Walker<'t> {
|
||||
|
||||
self.maybe_capture_fn_refs(node);
|
||||
|
||||
if kind == "class_declaration" {
|
||||
if is_class_type(kind) {
|
||||
self.extract_class(node);
|
||||
skip_children = true;
|
||||
} else if is_method_type(kind) {
|
||||
@@ -562,7 +566,7 @@ impl<'t> Walker<'t> {
|
||||
// Static-member / value-read (`Type.CONST`) — self-gates on field_access.
|
||||
self.extract_static_member_ref(node);
|
||||
|
||||
if kind == "class_declaration" {
|
||||
if is_class_type(kind) {
|
||||
self.extract_class(node);
|
||||
return;
|
||||
}
|
||||
@@ -603,7 +607,11 @@ impl<'t> Walker<'t> {
|
||||
self.visit_node(c);
|
||||
}
|
||||
}
|
||||
// Lombok member synthesis (#912) — class still on the stack.
|
||||
// synthesizeJavaMembers — class still on the stack: record
|
||||
// components first, then Lombok (#912).
|
||||
if node.kind() == "record_declaration" {
|
||||
self.synthesize_record_components(node, row);
|
||||
}
|
||||
self.synthesize_lombok_members(node, row);
|
||||
self.stack.pop();
|
||||
}
|
||||
@@ -1418,6 +1426,82 @@ impl<'t> Walker<'t> {
|
||||
names
|
||||
}
|
||||
|
||||
/// Members already declared directly in a class (exact `classQN::name`
|
||||
/// matches), split into the method and field namespaces.
|
||||
fn declared_member_names(&self, class_row: u32) -> (HashSet<String>, HashSet<String>) {
|
||||
let class_qn = self.nodes_meta[class_row as usize].qualified_name.clone();
|
||||
let mut taken_methods: HashSet<String> = HashSet::new();
|
||||
let mut taken_fields: HashSet<String> = HashSet::new();
|
||||
for m in &self.nodes_meta {
|
||||
if m.qualified_name == format!("{class_qn}::{}", m.name) {
|
||||
match m.kind {
|
||||
"method" | "function" => {
|
||||
taken_methods.insert(m.name.clone());
|
||||
}
|
||||
"field" | "variable" | "constant" | "property" => {
|
||||
taken_fields.insert(m.name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
(taken_methods, taken_fields)
|
||||
}
|
||||
|
||||
/// synthesizeRecordComponents (languages/java.ts): per record component a
|
||||
/// private field and a public accessor, anchored on the component's name.
|
||||
fn synthesize_record_components(&mut self, record_node: Node<'t>, record_row: u32) {
|
||||
let Some(params) = record_node.child_by_field_name("parameters") else { return };
|
||||
let (mut taken_methods, mut taken_fields) = self.declared_member_names(record_row);
|
||||
for i in 0..params.named_child_count() {
|
||||
let Some(param) = params.named_child(i) else { continue };
|
||||
if param.kind() != "formal_parameter" {
|
||||
continue;
|
||||
}
|
||||
let (Some(name_node), Some(type_node)) =
|
||||
(param.child_by_field_name("name"), param.child_by_field_name("type"))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let name = self.text(name_node).trim().to_string();
|
||||
let type_text = self.text(type_node).trim().to_string();
|
||||
if name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !taken_fields.contains(&name) {
|
||||
taken_fields.insert(name.clone());
|
||||
self.create_node(
|
||||
"field",
|
||||
&name,
|
||||
name_node,
|
||||
Extra {
|
||||
visibility: Some(2),
|
||||
is_static: Some(false),
|
||||
signature: Some(format!("{type_text} {name}")),
|
||||
..Extra::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
if !taken_methods.contains(&name) {
|
||||
taken_methods.insert(name.clone());
|
||||
let return_type = self.normalize_java_type(Some(type_node));
|
||||
self.create_node(
|
||||
"method",
|
||||
&name,
|
||||
name_node,
|
||||
Extra {
|
||||
visibility: Some(1),
|
||||
is_static: Some(false),
|
||||
signature: Some(format!("{type_text} {name}()")),
|
||||
docstring: Some("Implicit record component accessor".to_string()),
|
||||
return_type,
|
||||
..Extra::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn synthesize_lombok_members(&mut self, class_node: Node<'t>, class_row: u32) {
|
||||
let class_anns = self.lombok_annotation_names(class_node);
|
||||
let class_getter = class_anns.contains("Getter");
|
||||
@@ -1448,23 +1532,8 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
// Members the source already declares (exact `classQN::name` matches).
|
||||
let class_qn = self.nodes_meta[class_row as usize].qualified_name.clone();
|
||||
let class_name = self.nodes_meta[class_row as usize].name.clone();
|
||||
let mut taken_methods: HashSet<String> = HashSet::new();
|
||||
let mut taken_fields: HashSet<String> = HashSet::new();
|
||||
for m in &self.nodes_meta {
|
||||
if m.qualified_name == format!("{class_qn}::{}", m.name) {
|
||||
match m.kind {
|
||||
"method" | "function" => {
|
||||
taken_methods.insert(m.name.clone());
|
||||
}
|
||||
"field" | "variable" | "constant" | "property" => {
|
||||
taken_fields.insert(m.name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let (mut taken_methods, taken_fields) = self.declared_member_names(class_row);
|
||||
|
||||
let class_name_node = class_node.child_by_field_name("name").unwrap_or(class_node);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user