Skip to content

Detect inherited AT and AW members, plus cross-file AT conflicts - #17

Merged
GhostTypes merged 6 commits into
mainfrom
claude/at-inherited-members-and-cross-file
Aug 6, 2026
Merged

GhostTypes merged 6 commits into
mainfrom
claude/at-inherited-members-and-cross-file

Conversation

@GhostTypes

@GhostTypes GhostTypes commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Finishes the access transformer work from #12. The bytecode switch in #16 fixed the record false positives and made these remaining quirks solvable. With this, AT support covers everything the issue raised, and the same inherited-member handling applies to Fabric access wideners.

Inherited members

An AT only transforms the class it names. Point one at a member the class merely inherits and it's silently inert — the build passes, the game loads, the member stays inaccessible. The validator previously made this worse by suggesting a typo fix for a member that exists perfectly well one class up.

It now resolves the full ancestor closure and reports the class that actually declares the member, with a corrected directive you can paste over the broken line:

public net.minecraft.world.entity.LivingEntity getType()Lnet/minecraft/world/entity/EntityType;
  ERROR: Method 'getType' is not declared in net.minecraft.world.entity.LivingEntity — it is
         inherited from net.minecraft.world.entity.Entity. An access transformer only transforms
         the class it names, so this directive has no effect.
  Use: public net.minecraft.world.entity.Entity getType()Lnet/minecraft/world/entity/EntityType;
  • The walk is transitive over superName + interfaces and runs in rounds, since a parent is only discoverable from its child's class file.
  • Ancestors outside the JAR (java.lang.*, library types) are skipped, not reported missing.
  • The check runs before the descriptor-mismatch branch, so a class declaring tick(I)V when the AT asks for tick()V is reported as an inheritance problem rather than a bad descriptor.
  • Constructors are excluded — they're never inherited, so pointing at a parent's <init> would be wrong advice.

Access wideners

Fabric AWs have identical semantics. AccessWidenerClassVisitor looks up EntryTriple(className, name, descriptor) against the class it's visiting through a plain HashMap.getOrDefault, with no hierarchy resolution. An AW naming a subclass for an inherited member widens nothing.

The hierarchy walk lives in src/utils/bytecode-hierarchy.ts and both validators share it, so they can't drift on a rule they both have. Only the corrected-directive rendering differs, since the grammars do.

Access widener class-name notation

The AW parser normalized class names to dots (parts[2].replace(/\//g, '.')), so every directive and suggestion came out dotted. Access wideners are slash-only — Fabric's reader errors with "Class-names must be specified as a/b/C, not a.b.C" — meaning a pasted suggestion would break the build.

The parser now preserves the token as written. Lookups already went through toInternalName(), which normalizes either form.

Cross-file conflicts

Mods routinely ship more than one AT and the loader applies them together, so a conflict spanning two files fails the Forge build exactly like one inside a single file. validate_access_transformer takes an optional extraFiles.

Siblings aren't validated against bytecode — validate each on its own call for that. They join the union only for the checks where a second file changes the answer:

  • Duplicate and conflict detection, naming both files and both lines.
  • The record canonical-constructor note, suppressed when a sibling widens the ctor, since it genuinely is widened at runtime.

Findings are restricted to those involving the file being validated, so checking a.cfg never re-reports an argument that exists purely between b.cfg and c.cfg. A path that doesn't resolve comes back as extraFilesNotFound rather than being dropped, which would read as "no conflicts found".

CLI array flags

extraFiles is the first array param anyone would realistically type by hand, and the CLI only took JSON (--extraFiles '["a.cfg","b.cfg"]'), which is miserable to quote through cmd.exe or PowerShell. Now --extraFiles a.cfg,b.cfg and repeated --extraFiles a.cfg --extraFiles b.cfg both work. JSON still parses; non-array flags keep last-wins.

Verified against real Minecraft bytecode

  • rlnt's 19-entry Summoning Rituals AT validates clean on 1.21.1 mojmap: 0 errors, 3 informational record-ctor notes.
  • Positive controls fire on real hierarchies: AnyOfCondition terms → CompositeLootItemCondition, LivingEntity getType() → Entity on both the AT and AW side. Locally-declared members stay clean.

Tests

575 passing.

  • access-transformer-inheritance.test.ts (18): superclass chain, multi-level walks, interface default methods, overrides, descriptor-mismatch precedence, unresolvable parents, cyclic hierarchies, constructor exclusion, and the cross-file conflict/duplicate/restriction/record-suppression matrix. Two run against the real committed stub JAR.
  • 6 AW inheritance tests.
  • 5 tool-level tests for validate_access_widener output, covering the rendered payload rather than the envelope shape — directive, message and suggestion as they reach the caller.
  • Ancestor-closure coverage in the bytecode index tests, extraFiles coverage at the tool layer, and CLI array-flag tests.

@GhostTypes

Copy link
Copy Markdown
Contributor Author

Follow-up: access wideners now get the same inherited-member detection, so the two validators no longer disagree about a rule they both have.

I verified the semantics against Fabric's own implementation rather than assuming they matched. AccessWidenerClassVisitor looks a member up as EntryTriple(className, name, descriptor) for the class it is currently visiting, and AccessWidener resolves that through a plain HashMap with no superclass fallback:

accessWidener.getMethodAccess(new EntryTriple(className, name, descriptor))
...
Access getMethodAccess(EntryTriple entryTriple) {
    return methodAccess.getOrDefault(entryTriple, MethodAccess.DEFAULT);

So an AW entry naming a subclass for an inherited member widens nothing — identical to ATs, and identically silent.

The hierarchy walk therefore moved to src/utils/bytecode-hierarchy.ts (ancestorsOf, findDeclaringAncestor) and both validators share one implementation; only the corrected-directive rendering differs, since the grammars differ. Verified against real cached MC 1.21.11 yarn bytecode:

> accessible method net/minecraft/entity/LivingEntity getType ()Lnet/minecraft/entity/EntityType;
  ERROR: Method 'getType' is not declared in net.minecraft.entity.LivingEntity — it is inherited
         from net.minecraft.entity.Entity. An access widener only widens the class it names, so
         this entry has no effect.
  Use: accessible method net/minecraft/entity/Entity getType ()Lnet/minecraft/entity/EntityType;

> accessible method net/minecraft/entity/LivingEntity tickMovement ()V
  valid=true

One detail worth noting: the AW parser dot-normalizes class names, so the message renders the declarer in whichever notation the entry used (no net/mc/Foo vs net.mc.Foo in one sentence) while the pasteable Use: line keeps AW's on-disk slash form.

570 tests passing.

@GhostTypes GhostTypes self-assigned this Jul 28, 2026
@GhostTypes GhostTypes added the enhancement New feature or request label Jul 28, 2026
@GhostTypes GhostTypes changed the title Detect inherited AT members and cross-file conflicts Detect inherited AT and AW members, plus cross-file AT conflicts Jul 28, 2026
@GhostTypes
GhostTypes merged commit 1acf732 into main Aug 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants