Skip to content

A getter returning a map's mutable element: wrongly @UnmodifiableView, and modifying the element does not reach the owner #100

Closed

Description

@bnaudts

Two findings from one fernflower shape, StructContext.instanceOf: it takes a class from its map through a getter and calls a lazily-loading method on it.

class Ctx {
    static class C {
        private Object pool;
        Object getPool() { if (pool == null) pool = new Object(); return pool; }   // modifying
    }
    private final Map<String, C> classes = new HashMap<>();
    C getClass(String n) { return classes.get(n); }
    boolean direct(String n)         { C c = classes.get(n); return c.getPool() != null; }
    boolean viaGetter(String n)      { C c = getClass(n); return c.getPool() != null; }
    boolean viaGetterChain(String n) { return getClass(n).getPool() != null; }
}

1. A getter handing out a mutable element is @UnmodifiableView

getClass (and list.get(0) the same way) got unmodifiableViewMethod = true. TypeModIndyAnalyzerImpl.unmodifiableValue falls back to the callee's own verdict, and Map.get's IMMUTABLE_METHOD is immutable-HC: its result is a type parameter, the map's hidden content. Here the concrete element type C is mutable, so the result is no view. The IDE shows the wrong annotation.

Fixed on ws/maddi-bug: when the callee's formal result is a type parameter, the concrete type decides (at least immutable-HC: a view; otherwise not). Gate NOVIEWOFTYPEPARAMETER. Test TestUnmodifiableView.element (red with the gate).

2. Modifying an element handed out by a getter does not reach the map's owner (open)

method fixpoint MODREACH
direct (classes.get(n), then modified) modifying (deep) modifying (deep)
viaGetter (getClass(n) into a local, then modified) non-modifying modifying (deep)
viaGetterChain (getClass(n).getPool()) non-modifying non-modifying

The element is in this.classes' hidden content, so modifying it modifies this, deep-only, as direct shows. Through the getter the modification is lost: unsound. It is the element (∈) counterpart of #80 and #98, which handled a getter returning a field (≺). Not affected by fix 1.

How it was found

The rust-llvm oracle (maddi#167) on fernflower: StructContext::instance_of(&self) borrowed cl.get_pool() mutably through a shared borrow (E0596).

🤖 Generated with Claude Code

Activity

  1. added
    engine:linkThe link engine and shared-variable reconstruction
    on Nov 7, 2026
  2. bnaudts commented on Nov 13, 2026

    @bnaudts
    Author

    Both halves fixed on devel: 4477b1b (modifying an element a getter hands out modifies the owner, deep-only; /Users/bnaudts/git/jfocus-devops/scripts/boxlock nice -n 10 gate NOELEMENTOWNER; TestModificationOfElementFromGetter with this issue's getClass(n).getPool() shape, direct, via getter, via chain, via first(), and a read-only control; TestCallResultElementOfReceiver in the link module), and a5eed0e (a callee whose result is a type parameter is no view of a mutable element; /Users/bnaudts/git/jfocus-devops/scripts/boxlock nice -n 10 gate NOVIEWOFTYPEPARAMETER; TestUnmodifiableView.element). All pass on devel (2026-11-13).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    engine:linkThe link engine and shared-variable reconstruction

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions