Repository navigation
Commit f7f366c
Walk to an ancestor's constructor spec through JDK bytecode delegation (#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 <noreply@anthropic.com>1 parent 92963a7 commit f7f366c
30 files changed
Lines changed: 1037 additions & 52 deletions
File tree
- liquidjava-example/src/main/java/testSuite/classes
- delegated_argument_reassigned_error
- delegated_constant_arguments_error
- delegated_spec_keeping_other_sets_correct
- delegated_subclass_sets_error
- super_swapped_arguments_correct
- super_swapped_arguments_error
- throwable_jdk_subclass_correct
- throwable_jdk_subclass_error
- throwable_subclass_more_than_super_error
- liquidjava-verifier
- src
- main/java/liquidjava
- processor/refinement_checker
- general_checkers
- object_checkers
- rj_language
- test/java/liquidjava
- api/tests
- processor/refinement_checker/object_checkers
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
0 commit comments