Repository navigation
Detect delegating superclass constructors from bytecode instead of a hard-coded list #367
Description
Context
#354 lets a user constructor that starts with super(...) take the state given by the spec of the constructor it calls. When that class has no spec of its own and the spec sits on an ancestor (e.g. MyException extends Exception calling super(msg), with the spec on Throwable(String)), the lookup has to walk through the intermediate binary class. That's only safe if the intermediate constructor passes its arguments unchanged to the superclass constructor.
We can't read the bodies of binary classes, so #354 hard-codes the classes known to do this: DELEGATING_JDK_CLASSES = {Exception, RuntimeException, Error} in AuxStateHandler. Any other intermediate class stops the walk and falls back to the default state. This is sound, but it doesn't scale beyond those classes.
Proposal: check delegation in the bytecode
A constructor that only delegates compiles (with javac) to a fixed instruction shape:
aload_0
<load param 1> ... <load param n> // in order, with the right load instruction per type
invokespecial Super.<init>(<same descriptor>)
return
(ignoring labels, line numbers and frames). If the intermediate constructor matches that shape, keep walking up to the superclass's spec. If it doesn't, stop. For example, Exception(String) matches and gets walked through, while ClassNotFoundException(String) (which runs aload_1; aconst_null; invokespecial Exception.<init>(String, Throwable)) doesn't match and is rejected.
Sketch
- Add
org.ow2.asm:asm(BSD-3-Clause, single small jar). - Read the class bytes with
ClassLoader.getSystemResourceAsStream("java/lang/Exception.class")for JDK classes. Third-party classes need a class loader over the classpath the verifier is given. - A
MethodVisitormatches the shape above.long/doubleparameters take two slots. - Cache the result per constructor.
- Replace the
DELEGATING_JDK_CLASSEScheck inspecifiedConstructorwith this check, and delete the set.
Any constructor that doesn't match falls back to the default state, as it does today, so a false negative is safe.
Tests
MyException extends IOExceptionwith the spec only onThrowableinherits the state (walks throughIOExceptionandException).- A subclass of
ClassNotFoundExceptioncallingsuper(String)does not inheritThrowable(String)'s state. - A non-JDK library hierarchy, if one is available on the test classpath.
Alternatives considered
- Hand-kept list (current): sound, but it only covers the classes listed.
- Specs for every intermediate class: no verifier code, but specs get duplicated down every hierarchy, and a forgotten class silently gets the default state.
@ExternalRefinementsFor(Klass.class)instead of a string: it gives compile-time checking of the class name but doesn't expose constructor bodies, so on its own it doesn't solve this.
Estimate: half a day to a day including tests.
Activity