matrei commented on PR #16292:
URL: https://github.com/apache/grails-core/pull/16292#issuecomment-5665462955
# Review Findings (round 6)
Head `c8756534f2` (*Decide the static-compilation mode per body, not per
class*), one commit since the previous round, base `8.0.x`, merge-base clean.
It resolves the round-5 finding and both of its non-blocking notes:
`staticsInReach(MethodNode, boolean)` reads a method's own `@CompileStatic`
through the same `isTypeCheckingSkipped` the class level uses, the walk moved
into `walkBody` so each body carries its own reachable set, the `endsWith`
choice is explained in place, and the Javadoc and guide now say "or in a method
it marks `@CompileStatic` itself". All four new spec rows pass and I
re-measured each against the runtime with the check bypassed: the
`@CompileDynamic` method on a static host really fails, and the
`@CompileStatic` method on a dynamic host really returns `hello`. No blocking
findings. One residual gap in the same family, measured on both host kinds, and
it is one level deeper than the shape this commit fixed.
## New Findings
### [P3] A nested anonymous class takes the class-level answer even when its
enclosing method is `@CompileDynamic`
**File:**
`grails-beans-dsl/src/main/java/org/grails/compiler/beans/GrailsBeansASTTransformation.java:1582-1584,1608-1610`
The per-body decision stops at the first anonymous class. An anonymous class
created *inside* a method body is queued on `nested` and then re-entered with
`classOwn` as `enclosingReachable`, and its own `classStaticsInReach` is
recomputed from `isStaticallyCompiled(inner.getOuterClass())`, which walks the
outer *class* chain and never sees the method the class was written in.
Groovy's static compiler visits an anonymous class from the constructor-call
site inside the enclosing method's visit, so a `@CompileDynamic` method takes
every anonymous class in its body with it. Measured on the pushed head, then
with the sibling and group reach checks bypassed:
| Host | Anonymous method | Nested anonymous class calls | Check says | With
check bypassed |
|---|---|---|---|---|
| `@CompileStatic` | `@CompileDynamic String greet() { new G() { String
greet() { helper() } }.greet() }` | `helper()` | compiles clean |
`NoSuchFieldError: Class PgGGrailsPlugin$1 does not have member field
'PgGGrailsPlugin this$0'` |
| `@CompileStatic`, in a `group('extras')` | same | `helper()` | compiles
clean | same `NoSuchFieldError` on `PgLGrailsPlugin$1` |
| `@CompileStatic` | plain `String greet() { ... }` | `helper()` | compiles
clean | `greet()` returns `hello` |
| dynamic | `@CompileStatic String greet() { ... }` | `helper()` |
static-member message | `NoSuchFieldError` |
| dynamic | plain | `helper()` | static-member message | `NoSuchFieldError` |
The first two rows are the runtime failure the check exists to catch.
`javap` on the compiled classes shows why: under the `@CompileDynamic` method
the nested class's `greet()` is `invokedynamic ... invoke` with a
`methodMissing` that reads `this$0`, while under the plain method it is
`invokestatic PgHGrailsPlugin.helper`. The other three rows agree with the
runtime, including the fourth, so a method-level `@CompileStatic` on a dynamic
host does *not* extend to an anonymous class created inside it. The rule that
matches every measured row is: a nested anonymous class reaches the enclosing
statics only when the class chain says so **and** every enclosing body says so.
Two things have to change together for the nested class to get that answer,
because `helper` currently reaches it by both routes:
1. Pass the body's own set, not `classOwn`, as `enclosingReachable` when
re-entering for a class nested in that body. With the class-level
`ownWithStatics` passed down, `helper` is already in reach before the nested
class's own answer is consulted.
2. Let the caller override the nested class's `classStaticsInReach`.
`nested` could carry the body's answer next to each call (a small record, or
two parallel lists), and `reportOutwardReferences` could take a `Boolean` that
the two top-level callers pass as `null` (compute from the chain, as now) and
the recursive call passes as `classStaticsInReach && bodyStaticsInReach`.
Field initializers and object-initializer statements of the nested class
keep the class answer, as they do today. The `@CompileDynamic` method of a
`@CompileStatic` host is already the shape someone reaches for when a body will
not type-check, and an anonymous callback inside it is not exotic. Two spec
rows would pin it: the sibling and group shapes in the first two rows above,
expecting the static-member message; the third row could join `"the
static-compilation mode is read per body, so #shape reaches the enclosing
statics"` as a control.
## Notes (not blocking)
-
`grails-beans-dsl/src/main/java/grails/compiler/beans/GrailsBeans.java:228` is
a 58-column line inside a paragraph that wraps at 98-101; a reflow of that
sentence would keep the block even. Cosmetic.
## Verified Correct
- **Every spelling of the per-method rule agrees with the runtime.** Beyond
the four rows the spec pins, I measured `@GrailsCompileStatic` on the method of
a dynamic host (compiles, `hello`), `@CompileStatic(TypeCheckingMode.PASS)` on
the method of a dynamic host (compiles, `hello`),
`@CompileStatic(TypeCheckingMode.SKIP)` on the method of a static host
(static-member message; bypassed, `NoSuchFieldError`), `@TypeChecked` on the
method of a dynamic host (static-member message; bypassed, `NoSuchFieldError`,
since `@TypeChecked` leaves the bytecode dynamic and `staticsInReach` rightly
ignores it), and a `@CompileStatic(TypeCheckingMode.SKIP)` host with a
`@CompileStatic` method (compiles, `hello`, so the method's own annotation wins
over a skipped class in the compiler and in the check alike).
- **The group path follows the per-body rule too.** A dynamic host with a
`@CompileStatic` method inside `group('extras')` compiles and returns `hello`;
a `@CompileStatic` host with a `@CompileDynamic` method inside the group is
rejected with the static-member sentence, and with the check bypassed fails
with `NoSuchFieldError`. The spec pins only the sibling path, so a group row
for each direction would keep the two from drifting, but nothing drifts today.
- **`walkBody` is a faithful extraction.** The visitor body is byte-for-byte
the previous one; `nested` is still collected across every body and re-entered
after the walk, and `visited` still guards re-entry, so the traversal order and
the `answersAnything` short-circuit are unchanged.
- **`staticsInReach(MethodNode, boolean)`** reads `annotations.get(0)` on a
non-repeatable annotation, so there is exactly one; the fallback to
`classAnswer` is the pre-commit behaviour for unannotated methods and
field/initializer bodies, which the `"an unannotated method of a static host"`
and `"an unannotated method of a dynamic host"` rows pin in both directions.
- **Docs.** The Javadoc and the guide NOTE state the same condition the code
applies, and the endsWith comment at `:1806-1808` says what round 5 asked for.
## Verification
- Read the full diff of `c8756534f2` (4 files, +114/-26) and re-read
`reportOutwardReferences`, `staticsInReach`, `walkBody`, `isStaticallyCompiled`
and `isTypeCheckingSkipped` in the current file.
- Ran `./gradlew :grails-beans-dsl:check --continue` on the branch as
pushed: 397 tests, 0 failures, 0 errors, 0 skipped; module Checkstyle reports 0
violations across 5 files.
- Wrote a throwaway spec with twelve shapes (the eleven listed above plus
the group nested row) that prints, for each, whether the check rejects and, if
not, what `greet()` does at runtime, and ran it twice: on the pushed head, and
with an early `return` at the top of `rejectAnonymousClassReachingOutward`.
Compiled the three nested sibling shapes to a scratch directory and read
`greet()` with `javap -p -c`. Results are the tables and bullets above. The
patch was reverted and the spec deleted; `git status` shows no tracked changes.
## What I Did Not Run
- `./gradlew clean aggregateViolations :grails-test-report:check --continue`
from the root (CLAUDE.md rule #12), so CodeNarc, PMD and SpotBugs on the new
code are unverified here.
- `./gradlew :grails-core:test` - nothing outside `grails-beans-dsl` and
`grails-doc` changed in this round.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]