Repository navigation
Field-typed receivers are not inherited: a field declared on a superclass types nothing in a subclass (Java, ObjC, C#) #3151
Description
Summary
The field -> TypeName tables that type a this.<field> / [self.<field> …] receiver are looked up
by the declaring class only, with no walk up the inherits chain. A field declared on a
superclass therefore types nothing in a subclass, and every member call through it is dropped — even
though the inherits edge is sitting in the same graph and the parent's field table in the same run.
Reproduced in Java and Objective-C; the lookup in C# (csharp_field_types) has the same
shape. Not a cross-file data problem: Java fails even when parent and child are in one file.
graphifyy 0.9.51 (v8), Python 3.10, macOS 14.
Reproduction — Java, one file
// src/lib/Greeter.java
package lib;
public class Greeter {
public String greet(String who) {
return "hi " + who;
}
}
// src/lib/Pair.java
package lib;
class PBase {
protected Greeter greeter = new Greeter();
}
public class Pair extends PBase {
void run() {
this.greeter.greet("same-file-inherited");
}
}calls : [] <- expected Pair.run -> Greeter.greet
inherits: [('src_lib_pair_pair', 'src_lib_pair_pbase')] <- the link is right there
Reproduction — Java, own field vs inherited field, same corpus
Direct declares its own field; Sub inherits an identical one from Base. Four files,
src/lib/{Greeter,Base,Sub,Direct}.java:
public class Base { protected Greeter greeter = new Greeter(); }
public class Sub extends Base { void run() { this.greeter.greet("inherited-field"); } }
public class Direct { private Greeter greeter = new Greeter();
void run() { this.greeter.greet("own-field"); } }$ graphify update . --no-cluster
calls:
src_lib_direct_direct_run -> src_lib_greeter_greeter_greet [INFERRED L7]
inherits:
src_lib_sub_sub -> src_lib_base_baseDirect.run resolves, Sub.run does not. One call site out of two, differing only in where the
field was declared.
Reproduction — Objective-C
Same four-class shape (Greeter, Base owning @property Greeter *greeter, Sub : Base messaging
[self.greeter greet], Direct with its own property doing the same):
calls : [('src_direct_direct_run', 'src_greeter_greeter_greet', 'INFERRED')]
inherits: [..., ('src_sub_sub', 'src_base_base')]
Note this must be run through extract() with relative paths — via the CLI both call sites
resolve to nothing for the unrelated reason in #3150.
Root cause
Java / C# — extractors/engine.py:4856-4871, a flat per-class lookup:
java_receiver_types = {
body_id: _java_method_receiver_types(method_node, source,
java_field_types.get(class_nid, {}))
for body_id, (method_node, class_nid) in java_method_scopes.items()
}
csharp_receiver_types = { ... csharp_field_types.get(class_nid, {}) ... }java_field_types is filled at engine.py:3896 keyed by parent_class_nid (the declaring class).
Nothing consults the superclass, so the stamped receiver_type on raw_calls is simply absent and
_resolve_java_member_calls bails at extract.py:3613-3615 (if not type_name: continue).
ObjC — extract.py:3681-3697 merges objc_field_types into field_types_by_class keyed by class
nid, and the receiver lookup uses the enclosing class' entry alone. Same omission, one layer up.
For Java/C# there is a structural wrinkle: the table is consumed inside the per-file extractor, so
a superclass in another file is not even in scope there. The Java 4-file case above therefore has two
causes stacked; the same-file case isolates the walk itself.
Suggested shape
- Do the walk in the cross-file pass, where both the tables and the
inheritsedges exist:- ObjC: at
extract.py:3681, afterfield_types_by_classis merged, fold each class' table with
its ancestors' (child entry wins on conflict). - Java/C#: export the per-class field tables from the extractor the way ObjC already exports
objc_field_types, then fold + stampreceiver_typein_resolve_java_member_calls/
_resolve_csharp_member_callsfor thethis.<field>receivers left untyped.
- ObjC: at
- Precedence: nearest declaration wins (a subclass field shadowing a parent's is the language
semantics in all three). Keep the existing conflict rule for same-level collisions
(extract.py:3689-3697drops rather than guesses). - Guard the walk against cycles and against a missing/out-of-corpus ancestor (stop, don't guess) —
an ancestor whose type is not in the corpus should leave the receiver untyped exactly as today. - The single-definition guard on the resolved type stays untouched; this only widens receiver
typing, not target selection.
Happy to send a PR for the ObjC half (self-contained) and, if the export shape is acceptable, the
Java one.
Impact
Measured on a private 26-repo Objective-C/Swift codebase (0.9.39, built via extract() so #3150 was
not in play): of 44,434 [self.<field> …] message sends, 5,600 (12.6%) have a receiver whose
field is declared on an ancestor class and are dropped for this reason alone. That is one iOS
codebase, not a public-corpus figure, but the ratio is a property of how much inheritance the code
uses rather than anything project-specific.
Note
Two siblings from the same investigation: #3150 (ObjC field-table keys stale after the id remap, so
self.<field> typing is inert via the CLI) and a separate report on the receiver type being discarded
at resolver bail-out, which is what blocks cross-repo call edges after merge-graphs. Each is
independent — this one is the narrowest.
Activity
fix(engine): resolve fields declared on a superclass for receivers in…
chore: bump to 0.9.52
safishamsi commentedon Aug 29, 2026
Fixed in v0.9.52: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.52. Closing as resolved.