Repository navigation
Apply external class specs to user subclasses - #354
Conversation
Fixes #353. A typestate call on a subclass of a specified class (e.g. a custom RuntimeException calling initCause) crashed with a Z3 sort mismatch. - RefinedVariable records supertypes transitively, so a subclass is related to the specified class however far up it is. - A constructor whose first statement is super(...) takes the state the super constructor's spec gives, with the super arguments renamed to this constructor's parameters. The lookup walks through Exception, RuntimeException and Error only, whose constructors delegate to Throwable's unchanged; other JDK classes may not (ClassNotFoundException(String) sets a null cause), so they are not assumed to. - RefinementProcessor registers all external specs before checking user classes, so the order of types in the package no longer matters. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| return null; | ||
| } | ||
|
|
||
| private static boolean inheritSuperConstructorState(CtConstructor<?> c, RefinedFunction f, TypeChecker tc) { |
There was a problem hiding this comment.
this is a huge method can we maybe split it? and maybe add some documentation - at least 1 line what is doing
There was a problem hiding this comment.
Split in 2c1cfbc: inheritSuperConstructorState is now about 15 lines and calls superConstructorCall (find the leading super(...)), specifiedConstructor (find the spec, walking through the delegating JDK classes), superParamsToArguments (map super params to the argument names) and renameStates (substitute them into the to-states). Each one has a Javadoc line. The method's own Javadoc had also ended up above the DELEGATING_JDK_CLASSES constant. I moved it back onto the method.
- Log unresolvable supertypes in debug mode (new DebugLog.warn) - Walk supertypes in a single loop - Split inheritSuperConstructorState into documented helpers and fix its misplaced Javadoc Co-Authored-By: Claude Opus 5.5 <[email protected]>
| * The given states with the super constructor's parameters renamed, or null if one of them mentions a parameter | ||
| * whose argument has no name. | ||
| */ | ||
| private static List<ObjectState> renameStates(List<Predicate> toStates, Map<String, String> rename) { |
There was a problem hiding this comment.
these are a lot of new functions in here, do we need them all? could we make them more concise? you can use more of a functional style if they end up more concise
There was a problem hiding this comment.
Cut down in 7aa1bb0: there are now 3 functions instead of 5, and the code is about 30 lines shorter.
superConstructorCallis inlined intoinheritSuperConstructorStateas a pattern-matching guard.superParamsToArgumentsandrenameStatesare merged into the main method. It builds a rename map and anunnamedset, rejects the inheritance if a state mentions an unnamed parameter (anyMatch), then maps each to-state through the substitutions with areduce.argumentNameis small, returns anOptional, and says how a single argument can be named.specifiedConstructortakes theCtExecutableReferencedirectly instead of its three parts.
I kept argumentName and specifiedConstructor separate: the first is the per-argument rule and the second is the JDK delegation walk, and inlining them would make the main method harder to read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#433) Closes #367. ## Problem A JDK exception created directly gets no state from the `Throwable` spec, so a correct `initCause` is reported: ```java RuntimeException e = new RuntimeException("failed"); e.initCause(t); // State Refinement Error: found true but expected noThrowable(e) ``` `new IllegalStateException("failed")` gives the same error. The real bug in this pattern, in scijava-common's `CommandInfo` (fixed in a6fd9cf), was reported, but only because the state was unknown. The actual cause is that `ClassNotFoundException()` already sets a (null) cause: ```java final ClassNotFoundException cnfe = new ClassNotFoundException(); cnfe.initCause(exc); // IllegalStateException: Can't overwrite cause ``` ## Cause A constructor with no spec of its own gets the default state. #354 lets a lookup walk up to an ancestor's spec, but only: - through a hard-coded list (`Exception`, `RuntimeException`, `Error`); - for `super(...)` in user constructors; - when the ancestor constructor has the same parameter types. It also looked only at the first statement. So `super(m); initCause(c);` in a `RuntimeException` subclass inherited `Throwable(String)`'s state, and a second `initCause` was missed. ## Fix - **`ConstructorDelegation`** reads a JDK class with ASM. It accepts a constructor only when its whole body has this shape: ``` aload_0 (load of a parameter | constant [checkcast after null])* invokespecial <direct superclass>.<init> return ``` Any other instruction rejects the constructor: a field write, a method call, `this(...)`, or a branch. For each superclass parameter it records what is passed: one of the constructor's own parameters (any order or subset), or a constant. Int constants passed for a boolean, int, short or byte parameter keep their value. Other constants (`null`, strings, long/float/double, chars) have no known value, in bytecode and in source alike. Results are cached per constructor. - **`AuxStateHandler`** replaces `DELEGATING_JDK_CLASSES` with a walk through delegating constructors: - The arguments are mapped at each step. - Each ancestor's spec is looked up by exact parameter types, so `getFunction`'s fallback to another overload with the same number of parameters is not used. - The spec's parameters are replaced by the values passed: a variable's current instance, or a literal for boolean and int constants. A later assignment to the variable does not change the state. If a `to` state mentions a parameter whose value is not known (for example, a parameter that has no instance yet), the walk is refused. - A spec whose `to` mentions `old(this)` is skipped. That happens when a constructor spec leaves another state set unchanged. Calling such a constructor directly already fails on main with `Ghost 'old' could not be found`; that pre-existing bug is left for a separate issue. - State sets of the constructed class that the ancestor's `to` does not mention keep their default value. For example, `Stack()` runs `Vector()`, and Stack's own state set still starts empty. - **Where it applies:** to `new X(...)` when `X` has no spec of its own (except anonymous classes), and to a user constructor whose body is only `super(...)`, with no instance or field initializers. That is the source-level version of the bytecode rule. Anything else gets the default state, as before. `ClassNotFoundException()` is `aload_0; aconst_null; checkcast Throwable; invokespecial ReflectiveOperationException(Throwable)`, so it reaches `Throwable(Throwable)` and the object is `withThrowable`. Not covered: - **Only JDK classes are read**, through the platform class loader. The verifier runs without the program's classpath, and its own libraries may be different versions from the ones the program uses. Third-party intermediate classes stop the walk. - **The result depends on the JDK the verifier runs on**, which may not be the one the program targets. If that JDK changes a constructor body, or its class files are newer than ASM 9.11 can read, the walk stops without a warning and the object gets the default state, as on main. - **Not walked:** `this(...)` chains inside a binary class, and constructors that do anything besides delegate. - **Parameter refinements:** a walked spec's parameter refinements are not checked at the `new` site. Only its state is applied, and nothing was checked there before. - **State-change hint:** on the delegated path the hint metadata that names the call which set the state is not recorded, so state hints cannot point at that call. - **Overridden methods:** methods a subclass overrides and an ancestor's constructor calls (e.g. `fillInStackTrace`) are not considered, as for any spec. So a subclass of a newly walked class such as `IOException`, whose override sets the cause, is now missed. Main reported it only because the state was unknown. A call to `initCause` inside the override is still reported. - **Type variables:** the ancestor lookup compares exact type names, so a spec parameter written as a type variable does not match the erased bytecode type. The walk then stops and the state falls back to the default. ## Tests - **`classes/throwable_jdk_subclass_correct`:** `new RuntimeException(msg)`, `new IllegalStateException(msg)` and `new Exception()` followed by `initCause`, and an `IOException` subclass calling `super(msg)`. Main reports 3 false errors. - **`classes/throwable_jdk_subclass_error`:** `new ClassNotFoundException()`, `new RuntimeException(msg, cause)` and a `ClassNotFoundException` subclass calling `super(String)`, each followed by `initCause`. It also has `MissingResourceException`, which is not delegating and so gets the default state. `TestDelegatedConstructorStates` checks that the state found on the first three is `withThrowable`; main reports the same lines with an unknown state. - **`classes/throwable_subclass_more_than_super_error`:** `super(m); initCause(c);`, an implicit `super()` followed by `initCause`, and an instance initializer that sets the cause. Main misses the first and third. - **`classes/delegated_constant_arguments_error`:** `LinkedHashSet()` reaches `HashSet(16, .75f, true)`, and `16` and `true` reach the spec as literals. - **`classes/delegated_subclass_sets_error`:** `Stack` keeps its own default state after `Vector()`. - **`classes/delegated_argument_reassigned_error`:** a variable passed to `new` and assigned before or after the call, and a lambda parameter. - **`classes/delegated_spec_keeping_other_sets_correct`:** a delegated call to a constructor spec that mentions `old(this)` gets the default state, without an error. - **`classes/super_swapped_arguments_correct` / `_error`:** `Sub(boolean y, boolean x) { super(y, x); }` for `Base(boolean x, boolean y)`, whose state mentions both parameters. They are replaced at once; replacing one after the other turned both into `x` (also wrong on main). - **`ConstructorDelegationTest`:** unit tests for the bytecode matcher. - **`mvn test`:** 532 tests, 0 failures (rebased on main at 92963a7). ## Real-file results - **scijava-common `CommandInfo.java` (d9dce68)** with the Throwable spec: - Main: `found !#fresh but expected noThrowable(cnfe)`. - With this PR: `found !#fresh && withThrowable(cnfe) but expected noThrowable(cnfe)` at line 304. - The fixed version (`throw new ClassNotFoundException(null, exc)`) passes on both. - **The 53 OSS exercises (74 files, buggy and fixed)**, on main (4a89df6) and with this PR: identical results. ## Dependency `org.ow2.asm:asm` 9.11, licence BSD-3-Clause, a single jar with no transitive dependencies. Spoon 11.5.0 does not bring ASM in. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Fixes #353.
Problem
An external spec (
@ExternalRefinementsFor("java.lang.Throwable")) did not reach user subclasses: callinginitCauseon a customRuntimeExceptionsubclass crashed withSort mismatch at argument #1 for function java.lang.Throwable.state1 … supplied sort is AppException.Example
With
super(message)instead, the sameinitCausepasses.Change
RefinedVariable.addSuperTypeswalks superclasses and interfaces transitively.AuxStateHandler: a constructor whose first statement issuper(...)inherits the to-state of the super constructor's spec, with the super arguments renamed to this constructor's parameters (or literals). If a to-state uses an argument that cannot be named, it falls back to the default state as before. The spec lookup walks up only throughException,RuntimeExceptionandError, whose constructors delegate unchanged toThrowable; other JDK subclasses are not assumed to (e.g.ClassNotFoundException(String)sets a null cause).RefinementProcessor: the first pass is split so all external specs are registered before any user class is checked; before, the result depended on the order of types in the package.Tests
testSuite/classes/throwable_subclass_error: subclass built withsuper(message, cause), theninitCause, gives a State Refinement Error.testSuite/classes/throwable_subclass_correct: subclass built withsuper(message), theninitCause, passes.mvn testpasses.🤖 Generated with Claude Code