[
https://issues.apache.org/jira/browse/GROOVY-12375?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18112976#comment-18112976
]
ASF GitHub Bot commented on GROOVY-12375:
-----------------------------------------
daniellansun commented on PR #2898:
URL: https://github.com/apache/groovy/pull/2898#issuecomment-5590815514
## Overview
`Class.forName(name, false, ShortTypeHandling.class.getClassLoader())`
already looks like the right shape. I would not wrap it in a helper, and I
would not turn it into a general-purpose loader with an initialize flag.
The one-argument `Class.forName(name)` is “caller’s class loader,
`initialize=true`”. The caller is `ShortTypeHandling`, so passing
`ShortTypeHandling.class.getClassLoader()` and only flipping `initialize` to
`false` is easy to follow and keeps the old loader behaviour.
`ClassLoader.loadClass` would drop array names such as `"[Ljava.lang.String;"`,
which seems a step backwards. Switching to the thread context class loader
would change *which* classes resolve — a different question, and probably not
one to mix with “do not run `<clinit>`” in the same commit.
None of the touched files crosses a thousand lines on this change
(`ShortTypeHandling.java` ~135, the test ~266, `THREAT_MODEL.md` ~842), and the
production path does not grow extra branches. I do not see a structural split
that would earn its keep.
The three notes below are mostly about how the new contract is recorded and
how the regression is written, not about that one production line.
---
## 1. The “no initialization” claim in the threat model may need a 6.0.0
qualifier
`THREAT_MODEL.md` says it applies to **3.0.x, 4.0.x, 5.0.x, and 6.0.x
alpha**. This change lands on `6.0.0-SNAPSHOT`. On 3/4/5, `'name' as Class`
still runs static initializers.
Section 11a is the “known non-finding” table used by scanners and triage. A
reader on 4.0 or 5.0 who sees “coercion does not run static initializers” might
downgrade a finding that is still true on those lines.
The same document already dates new mitigations (“from 6.0.0” for JSON
`maxNestingDepth` and XML `maxElementDepth`). This row could follow that
pattern:
- On every covered version, the disposition can stay `KNOWN-NON-FINDING` /
downstream boundary — even when initialization still runs, feeding untrusted
data to `as Class` remains an application choice.
- The “resolved without initialization” part could be marked **as of 6.0.0
(GROOVY-12375)**.
That would keep the 6.0 story intact without implying the mitigation already
exists on 3/4/5.
---
## 2. This is a user-visible language change; a 6.0 compatibility note might
help
`castToClass` is `@GroovyABI(since = "2.3.0")` and is how `'name' as Class`
is implemented (`InvocationWriter` calls it directly; the dynamic path goes
through `DefaultTypeTransformation` / `asType`). The old javadoc said the name
was loaded via `Class.forName`, and `Class.forName` is widely understood to
initialize. The likely casualty is the JDBC `Class.forName(driver)` pattern:
`'com.mysql.jdbc.Driver' as Class` will no longer register the driver.
6.0 is a major release, so the change is reasonable. `COMPATIBILITY.md`
still treats “behavioural changes users may have come to rely on” as breaking,
even when `japicmp` cannot see them. The method javadoc is helpful for Java
callers; for the language-level `as Class` operator, a line in the 6.0
compatibility notes or release notes might be safer, along the lines of:
> String-to-`Class` coercion resolves the type; it does not run `<clinit>`.
If you still need that side effect, call `Class.forName(name)` (or
`Class.forName(name, true, loader)`).
The javadoc on the method is already clear. I would not add API or a switch
just to preserve the old side effect.
---
## 3. The regression belongs on the existing test class, and the two-type
split is sound; a few small tidy-ups
Splitting Flag and Probe is necessary: reading a static field on
`StaticInitProbe` would initialize it and hollow out the test. I would keep
that.
Three smaller points:
**Missing `// GROOVY-12375`.** `CONTRIBUTING.md` asks for that comment on
the line immediately above a method added to an existing test class, so `git
grep GROOVY-12375` can find it. Right now the key appears only in the commit
message. The sibling `DefaultTypeTransformationTest` already uses `//
GROOVY-9916`, `// GROOVY-10028`, and so on.
**Top-level Groovy types are public by default.** `StaticInitProbe` and
`StaticInitProbeFlag` become public types on the test classpath, with one-shot
mutable static state. The same package already nests helpers
(`DefaultTypeTransformationTest` uses nested `static class`). Folding Flag and
Probe into `ShortTypeHandlingTest` as static nested types would keep the
package a little quieter.
**Hard-coded FQCN.** Once nested, the binary name becomes
`ShortTypeHandlingTest$StaticInitProbe`. That is what `Class.forName` wants;
`StaticInitProbe.name` tracks the type if it moves, and is a bit more durable
than a string literal.
A possible sketch:
```groovy
// GROOVY-12375
@Test
void testCastToClassDoesNotInitialize() {
def probe = castToClass(StaticInitProbe.name)
assert probe == StaticInitProbe
assert !StaticInitProbeFlag.initialized: 'coercion must not run the
static initializer'
StaticInitProbe.touch()
assert StaticInitProbeFlag.initialized: 'static initializer should
run on first use'
}
static class StaticInitProbeFlag {
static boolean initialized
}
static class StaticInitProbe {
static {
StaticInitProbeFlag.initialized = true
}
static void touch() {}
}
```
Testing `castToClass` directly is enough: that is the method the compiler
emits for `as Class`. I would not add a `GroovyClassLoader` fixture, or a
second assertion on the language operator, unless someone particularly wants
the extra documentation.
The static flag is still one-shot (a retry in the same JVM would fail). That
seems acceptable for this suite; I would not add reset or locking around a
one-way `<clinit>`.
---
## What I would leave as-is
- No shared “load without initializing” helper. Core already uses
three-argument `forName` in place (`IOGroovyMethods.resolveClass`,
`JacksonHelper`); one more call site does not seem to justify a new abstraction.
- I would not switch to the TCCL in this commit. That changes which classes
can be resolved, which is a different problem from skipping `<clinit>`.
- I would not add an `initialize` parameter or mode flag to `castToClass`.
Its job is coercion to `Class`, not a re-export of the full `Class.forName` API.
- File size and control flow look fine. The production implementation
already looks like the shape I would keep.
---
## Summary
The Java change is the one-liner I would have hoped to see. Before merge, it
might be worth:
1. Qualifying the §11a row (as of 6.0.0 / GROOVY-12375);
2. Marking the regression with `// GROOVY-12375` and nesting the probe types.
The 6.0 compatibility note can live in the same commit or wait for the
release-notes pass; it probably should not live only in the method javadoc.
Offered with thanks for the work — happy to be corrected if I have misread
any of this.
> ShortTypeHandling: castToClass should avoid class init
> ------------------------------------------------------
>
> Key: GROOVY-12375
> URL: https://issues.apache.org/jira/browse/GROOVY-12375
> Project: Groovy
> Issue Type: Improvement
> Reporter: Paul King
> Priority: Major
>
--
This message was sent by Atlassian Jira
(v8.20.10#820010)