Skip to content

A copied list of shared mutable elements is INDEPENDENT; callers that modify the elements stay @NotModified #112

Closed

Description

@bnaudts

fernflower (test-oss/fernflower, main sources; maddi 8a38863c8, maddi-mod 6ed8f49f), Statement:

private final EnumMap<EdgeType, List<StatEdge>> mapSuccEdges = new EnumMap<>(EdgeType.class);

private List<StatEdge> getEdges(EdgeType type, EdgeDirection direction) {
  EnumMap<EdgeType, List<StatEdge>> map = direction == EdgeDirection.BACKWARD ? mapPredEdges : mapSuccEdges;
  List<StatEdge> res;
  if (/* one type */) {
    res = map.get(type);
    res = res == null ? new ArrayList<>() : new ArrayList<>(res);   // a new list, the same StatEdge objects
  } else {
    res = new ArrayList<>();
    for (...) { List<StatEdge> lst = map.get(edgetype); if (lst != null) res.addAll(lst); }
  }
  return res;
}
public List<StatEdge> getAllSuccessorEdges() { return getEdges(EdgeType.ALL, EdgeDirection.FORWARD); }

and LabelHelper:

private static void resetAllEdges(Statement stat) {
  for (Statement st : stat.getStats()) resetAllEdges(st);
  for (StatEdge edge : stat.getAllSuccessorEdges()) {
    edge.explicit = true;
    edge.labeled = true;
  }
}

Results: getEdges, getAllSuccessorEdges and getSuccessorEdges have independentMethod: 2 (INDEPENDENT), and
resetAllEdges has stat as unmodifiedParameter: 1 (and structurally unmodified).

The returned list is new, but its elements are the statement's own StatEdges, and StatEdge is mutable
(labeled, explicit and others are plain fields). So the result shares hidden content with the receiver: at most
INDEPENDENT_HC, not INDEPENDENT. resetAllEdges writes those edges, so stat should at least not be deeply
@NotModified. The same shape (a defensive copy of a list of mutable objects, handed out and modified by the
caller) recurs in fernflower's label and edge helpers. LabelHelper.setRetEdgesUnlabeled (through
getAllPredecessorEdges), SwitchHelper.prepareForRules and SwitchPatternHelper.normalizeCaseLabels (through
getSuccessorEdges) write StatEdge.labeled/explicit on edges reached this way while their statement parameter is
reported unmodified. LabelHelper.processStatementLabel does the same through getLabelEdges(), a different getter
that I have not checked.

🤖 Generated with Claude Code

Activity

  1. bnaudts commented on Nov 12, 2026

    @bnaudts
    Author

    Another instance of the same shape, possibly a larger one: Exprent.getAllExprents() / getAllExprents(boolean) return a new list of the expression's own (mutable) subexpressions, and have independentMethod: 2 (INDEPENDENT). A caller that modifies those subexpressions keeps its Exprent parameter @NotModified: StackVarsProcessor.setExprentVersionsToNull(Exprent) calls setVersion(0) on every VarExprent in exprent.getAllExprents(true), and exprent is reported unmodified. In fernflower 18 parameters or receivers reported unmodified are written through VarExprent.setVersion / setVarType / setLVTEntry or ConstExprent.setConstType. I have checked only setExprentVersionsToNull to go through getAllExprents; the others may reach their expressions another way.

    🤖 Generated with Claude Code

  2. added 5 commits that reference this issue on Nov 13, 2026
  3. bnaudts commented on Nov 13, 2026

    @bnaudts
    Author

    Fixed on devel (merge 00b2a40): 774640a (a copy of a map value's mutable elements is dependent, and modifying them modifies the map's owner), c695f7a (≥ leaves a variable grouped with the container to its pass set), 2e0dca8. The parameter half was split off as #119 and decided there. Gate NOOBJECTGRAPHINDEP. Gate green on devel after the merge (laser1, 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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions