Ben Sherman created GROOVY-12166:
------------------------------------
Summary: STC: `return field instanceof Subtype` narrows the
field's type class-wide, causing `ClassCastException` -- Groovy 5 regression
Key: GROOVY-12166
URL: https://issues.apache.org/jira/browse/GROOVY-12166
Project: Groovy
Issue Type: Bug
Components: Static compilation
Affects Versions: 5.0.7
Environment: Ubuntu, JDK 17
Reporter: Ben Sherman
## Summary
Under Groovy 5, when a `@CompileStatic` class contains a method whose body is
`return someField instanceof SomeSubtype`, the static type checker narrows the
**declared type of the field itself** to `SomeSubtype` for the rest of the
class.
The static compiler then emits a spurious `checkcast SomeSubtype` before
*other*,
unrelated calls to that field elsewhere in the class. At runtime those methods
throw `ClassCastException` whenever the field holds a different subtype of its
declared type.
The field's declared type is a common base class; the `instanceof` is only a
runtime test and must not change the field's static type outside its own
expression. This worked correctly in Groovy 4.
## Affected / unaffected versions
| Groovy version | Result |
| --- | --- |
| 4.0.29 | OK — prints `base` |
| 4.0.31 | OK — prints `base` |
| 5.0.7 | **BUG** — `ClassCastException` |
This is a **regression** introduced somewhere between 4.0.x and 5.0.7.
## Minimal reproducible example
`Bug.groovy`:
```groovy
import groovy.transform.CompileStatic
abstract class Base {
String describe() { 'base' }
abstract void unrelated()
}
class Alpha extends Base { void unrelated() {} }
class Beta extends Base { void unrelated() {} }
@CompileStatic
class Holder {
private Base field
Holder(Base f) { this.field = f }
// (1) `instanceof` applied directly to the FIELD, with an explicit
`return`.
// This is what poisons the field's inferred type.
boolean isAlpha() { return field instanceof Alpha }
// (2) An UNRELATED method that merely calls a Base method on the field.
// Under Groovy 5 the static compiler wrongly narrows `field` to `Alpha`
// here and emits `checkcast Alpha` before the call.
String use() { field.describe() }
}
// `field` holds a Beta and is never an Alpha
def h = new Holder(new Beta())
assert h.isAlpha() == false
println h.use() // Groovy 4 -> "base" | Groovy 5 -> ClassCastException
```
### How to run
```bash
# run with a given Groovy version
groovy Bug.groovy
```
### Expected output
```
base
```
### Actual output (Groovy 5.0.7)
```
Exception in thread "main" java.lang.ClassCastException: class Beta cannot be
cast to class Alpha (Beta and Alpha are in unnamed module of loader 'app')
at Holder.use(Bug.groovy)
at Bug.run(Bug.groovy)
```
## Bytecode evidence
Disassembly of `Holder.use()` (`javap -p -c Holder`), which contains **no**
`instanceof` and only calls `Base.describe()`:
**Groovy 4.0.29 / 4.0.31 (correct):**
```
1: getfield // Field field:LBase;
4: invokevirtual // Method Base.describe:()Ljava/lang/String;
```
**Groovy 5.0.7 (incorrect — bogus cast injected):**
```
1: getfield // Field field:LBase;
4: checkcast // class Alpha <-- injected; field is declared `Base`
7: invokevirtual // Method Alpha.describe:()Ljava/lang/String;
```
Note that `Alpha` does not even override `describe()` — the method is inherited
from `Base` — so narrowing the receiver to `Alpha` is both unnecessary and
wrong.
## Trigger conditions
>From bisecting the example, all of the following are required to reproduce:
1. The enclosing class is `@CompileStatic`.
2. A field (declared with a base/abstract type) is tested with `instanceof` a
subtype **directly on the field reference** (not via a local copy).
3. That `instanceof` expression is returned with an **explicit `return`**
keyword: `return field instanceof Sub`. Using Groovy's implicit return
(`field instanceof Sub` as the last expression) does **not** trigger it.
4. Another method in the same class calls a method on the same field.
Only the `return` on the `instanceof` method matters; the second method may use
either explicit or implicit return.
## Workaround
Copy the field into a local variable before the `instanceof` check so the
narrowing applies to the local, not the field:
```groovy
boolean isAlpha() {
final f = field
return f instanceof Alpha
}
```
## Real-world impact
Found while upgrading [Nextflow](https://github.com/nextflow-io/nextflow) to
Groovy 5.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)