fix(go): a struct links the types its fields are declared with (#2655)

A Go struct referenced nothing through its fields: etcd's `type
PutResponse struct { Header *ResponseHeader }` or gin's `type Context
struct { handlers HandlersChain }` had no `references` edge to those
types, so impact on `ResponseHeader` or `HandlersChain` missed every
struct holding one, and their users. Main had 0 such edges on gin, etcd,
prometheus and kubernetes; only embedded fields were read (`extends`).

TS extractor and Rust kernel, mirrored:
- goStructFieldTypeNames / struct_field_type_names walk each named
  field's type and push a `references` ref per type name from the
  struct, on the name. Fields mint no nodes: impact never climbs
  `contains` upward (#536), so a field node would stop impact short of
  the struct; Dart attributes field types to the class too (#2327).
- #2441's alias walk is factored into collectGoTypeNames /
  collect_type_names + goDeclaredNames / declared_names, so struct
  fields skip what aliases skip: predeclared types and, written bare,
  the declaration's own type parameters and its own name (no self-edge,
  no failed row for `next *node`).
- An embedded field stays `extends`; an embedded generic's type
  arguments are references (client-go's
  `listers.ResourceIndexer[*v1.Pod]`: 651 such fields in kubernetes).

Resolver: gateTargetKind's unknown-qualifier rule (#2417) now covers
every Go type declaration (GO_TYPE_KINDS). Without it, a field
`op clientv3.Op` under an unaliased go.etcd.io/etcd/client/v3 import
bound to the same file's own `Op`.

Explore: a `references` edge whose ends are both type declarations no
longer makes either end connected to an entry. With the new edges alone
the #1500 CG-10 gate failed: payroll's 8-line `RunResult` (holding a
`[]Payslip`) became connected and displaced the 51-line
runPayrollCycleAll body. The rule alone changes no rendered symbol on
gin, etcd, prometheus, kubernetes, bloc, bulletproof or refine-examples.

Validation (main de3eb71d, site-keyed A/B, every added edge judged
against real package clauses): gin +41, etcd +1006, prometheus +1637,
kubernetes +20203, all correct; 0 dropped/retargeted/metadata-only; no
node changes or self-loops; kernel == wasm. Index time within noise,
DB +0.4-1.4%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 09:35:01 +00:00
committed by GitHub
parent c7b50382af
commit a944ec3579
10 changed files with 833 additions and 51 deletions
+70 -24
View File
@@ -581,6 +581,13 @@ impl<'t> Walker<'t> {
// Struct embedding → extends (field_declaration without a
// field_identifier), reached via the inheritance recursion.
self.extract_inheritance(type_child, row);
// The types its fields are declared with are the struct's
// references: Go fields mint no nodes.
let references = edge_kind_index("references").unwrap();
for ty in self.struct_field_type_names(node) {
let text = self.text(ty).to_string();
self.push_ref_at(row, &text, references, ty);
}
let body = type_child.child_by_field_name("body").unwrap_or(type_child);
for i in 0..body.named_child_count() {
if let Some(c) = body.named_child(i) {
@@ -897,6 +904,27 @@ impl<'t> Walker<'t> {
}
}
/// goDeclaredNames (languages/go.ts): what a type name written bare
/// inside a type declaration can mean without naming another type — the
/// declaration's own type parameters, and its own name, which in a
/// recursive type is the declaration itself.
fn declared_names(&self, decl: Node<'t>) -> HashSet<&'t str> {
let mut names = HashSet::new();
if let Some(name) = decl.child_by_field_name("name") {
names.insert(self.text(name));
}
if let Some(list) = decl.child_by_field_name("type_parameters") {
for param in (0..list.named_child_count()).filter_map(|i| list.named_child(i)) {
for c in (0..param.named_child_count()).filter_map(|j| param.named_child(j)) {
if c.kind() == "identifier" {
names.insert(self.text(c));
}
}
}
}
names
}
/// goAliasTypeNames (languages/go.ts): the name nodes of the types an
/// alias's or a defined type's `type` field names, in source order, but
/// its own type parameters, the predeclared types and its own name written
@@ -905,36 +933,54 @@ impl<'t> Walker<'t> {
/// around an error, never reaches the kernel (its file defers to wasm).
fn alias_type_names(&self, node: Node<'t>) -> Vec<Node<'t>> {
let mut names = Vec::new();
let Some(ty) = node.child_by_field_name("type") else { return names };
let own = node.child_by_field_name("name").map(|n| self.text(n));
let mut params: HashSet<&str> = HashSet::new();
if let Some(list) = node.child_by_field_name("type_parameters") {
for decl in (0..list.named_child_count()).filter_map(|i| list.named_child(i)) {
for c in (0..decl.named_child_count()).filter_map(|j| decl.named_child(j)) {
if c.kind() == "identifier" {
params.insert(self.text(c));
}
}
}
if let Some(ty) = node.child_by_field_name("type") {
self.collect_type_names(ty, &self.declared_names(node), false, &mut names);
}
self.collect_alias_type_names(ty, &params, own, false, &mut names);
names
}
/// `qualified`: the name of a `pkg.Name`, which is that package's
/// whatever this one declares.
fn collect_alias_type_names(
&self,
node: Node<'t>,
params: &HashSet<&str>,
own: Option<&str>,
qualified: bool,
out: &mut Vec<Node<'t>>,
) {
/// goStructFieldTypeNames (languages/go.ts): the name nodes of the types a
/// struct's fields are declared with, in source order, skipped as in
/// alias_type_names. An embedded field's type is a supertype
/// (embedded_type_name), but the types it is instantiated with are not
/// (`Pod` in client-go's `listers.ResourceIndexer[*v1.Pod]`).
fn struct_field_type_names(&self, node: Node<'t>) -> Vec<Node<'t>> {
let mut names = Vec::new();
let fields = node.child_by_field_name("type").and_then(|ty| {
(0..ty.named_child_count())
.filter_map(|i| ty.named_child(i))
.find(|c| c.kind() == "field_declaration_list")
});
let Some(fields) = fields else { return names };
let declared = self.declared_names(node);
for field in (0..fields.named_child_count()).filter_map(|i| fields.named_child(i)) {
if field.kind() != "field_declaration" {
continue;
}
let mut ty = field.child_by_field_name("type");
// Embedded: no field name (extract_inheritance's test).
let embedded = !(0..field.named_child_count())
.filter_map(|j| field.named_child(j))
.any(|c| c.kind() == "field_identifier");
if embedded {
ty = ty
.filter(|t| t.kind() == "generic_type")
.and_then(|t| t.child_by_field_name("type_arguments"));
}
if let Some(ty) = ty {
self.collect_type_names(ty, &declared, false, &mut names);
}
}
names
}
/// collectGoTypeNames (languages/go.ts). `qualified`: the name of a
/// `pkg.Name`, which is that package's whatever this one declares.
fn collect_type_names(&self, node: Node<'t>, declared: &HashSet<&str>, qualified: bool, out: &mut Vec<Node<'t>>) {
stack_guard!();
if node.kind() == "type_identifier" {
let text = self.text(node);
if !is_go_predeclared_type(text) && (qualified || (!params.contains(text) && own != Some(text))) {
if !is_go_predeclared_type(text) && (qualified || !declared.contains(text)) {
out.push(node);
}
return;
@@ -942,7 +988,7 @@ impl<'t> Walker<'t> {
let qualified = node.kind() == "qualified_type";
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.collect_alias_type_names(c, params, own, qualified, out);
self.collect_type_names(c, declared, qualified, out);
}
}
}