Skip to content

refactor(build-tooling): idiomatic Groovy cleanup of the J2G output - #16661

Open
borinquenkid wants to merge 6 commits into
refactor/java-to-groovy-build-toolingfrom
groovyism/build-tooling
Open

borinquenkid wants to merge 6 commits into
refactor/java-to-groovy-build-toolingfrom
groovyism/build-tooling

Conversation

@borinquenkid

Copy link
Copy Markdown
Member

Stacks on #16576 (java-to-groovy conversion for build-logic/docs-core and grails-gradle/common+model).

Behaviour-preserving idiom pass over that conversion's output (14 of the 35 files in scope; the rest were already idiomatic or are interfaces/DTOs left untouched).

Applied

  • Property syntax for unambiguous getters (Class.getName() → .name, URL/File/URLConnection/Throwable/Resource accessors, Class.isArray()/.getComponentType() → .array/.componentType)
  • Map-read subscript (map.get(k) → map[k])
  • .equals() on Strings → ==
  • str.length() == 0 → isEmpty()
  • StringBuilder.append( → <<
  • String concatenation → GString where it lands in a String-typed slot
  • Dropped redundant casts after instanceof flow typing
  • x != null ? x.foo() : null → x?.foo()
  • for (X x : coll) → for (X x in coll)
  • Dropped an unnecessary public modifier on StringEscapeCategory's static methods

Deliberately left (documented per-site in the commit body): Environment.getName() (shadows Enum.name() differently), DefaultResourceLoader.getClassLoader() field-priority call, a closure-shared-variable cast the type checker rejects dropping, Class.newInstance() (avoids widening a public API's checked-exception surface), SpringIOUtils's in/out parameter names across ~8 overloads (real Tier-A hit, judged disproportionate as a standalone rename), ClassPathResource#nullSafeEquals's .equals() on an Object that may be an array, trivial getter/setter pairs that would add a public setter that doesn't exist today, and one Javadoc-bearing getter.

Found during verification: map[k] = v (subscript assignment) compiles to a dynamic ScriptBytecodeAdapter.invokeMethodN("putAt", ...) call under @CompileStatic here, unlike the read form which resolves statically. All ~10 sites introduced by this pass were reverted back to .put(k, v); the read-only subscript conversions were kept.

This revision also carries forward the rename-compliance fix from #16576 (DefaultCommandLine.groovy keeping semicolons temporarily) merged and resolved to its fully idiomatic end state — the semicolons and the @SuppressWarnings from that fix are removed here, completing the 3-stage conversion→cleanup split for that file.

Verification

  • build-logic: compile/test green (StringEscapeCategorySpec 2/2; one pre-existing, unrelated FetchTagsTaskSpec sandbox failure)
  • grails-gradle: compile green for common+model; codeStyle 0 CodeNarc violations; full test 456/456, 0 failures
  • javap -c -p dynamic-dispatch audit clean on every changed class after the putAt fix

🤖 Generated with Claude Code

borinquenkid and others added 2 commits October 16, 2026 19:19
Behaviour-preserving idiom pass over the Java-to-Groovy conversion output for
build-logic/docs-core and grails-gradle/common+model (14 of the 35 files in
scope; the rest were already idiomatic or are interfaces/DTOs left untouched).

Applied:
- property syntax for getters with no isX/getX collision: ~35 sites
  (Class.getName() -> .name, URL.getProtocol()/.getFile()/.getPath() -> .protocol/.file/.path,
  File.getName()/.getParentFile()/.getAbsolutePath()/.getCanonicalPath(), URLConnection.getContentLength()/
  .getResponseCode(), Throwable.getMessage(), Class.isArray()/.getComponentType() -> .array/.componentType,
  Thread.getContextClassLoader(), Resource.getURL()/.getInputStream() -> .URL/.inputStream matching the
  existing test idiom)
- map.get(k)/map.containsKey(k) read -> map[k]: ~10 sites (CollectionUtils, Holder, CommandLineParser,
  DefaultCommandLine, GrailsResourceUtils, SpringIOUtils)
- .equals( on Strings -> ==: ~15 sites (GrailsResourceUtils protocol/path checks, CommandLineParser/
  DefaultCommandLine constant comparisons, FileSystemResource/UrlResource equals() field comparisons)
- str.length() == 0 / > 0 -> isEmpty()/!isEmpty(): ~12 sites
- StringBuilder.append( -> << in statement position: ~20 sites
- string concatenation -> GString where it lands in a String-typed slot (return type or exception
  message): FileSystemResource/UrlResource#getDescription, SpringIOUtils exception messages
- dropped unnecessary casts after instanceof flow typing: CollectionUtils#getOrCreateChildMap,
  GrailsResourceUtils#getPropertyNameConvention-equivalent branches, UrlResource#getInputStream,
  AbstractFileResolvingResource#contentLength/lastModified
- x != null ? x.foo() : null -> x?.foo(): SpringIOUtils#closeQuietly
- for (X x : coll) -> for (X x in coll): SpringIOUtils, DefaultCommandLine
- dropped the UnnecessaryPublicModifier on StringEscapeCategory's static methods

Deliberately left (Tier B exclusions / risk):
- Environment.getName() calls (CommandLineParser, DefaultCommandLine): Environment is an enum that
  defines its own getName()/setName(), shadowing the inherited Enum.name() the property syntax would
  otherwise resolve to differently; left as explicit calls.
- DefaultResourceLoader.getClassLoader() bare calls from getResource()/getResourceByPath(): the private
  classLoader field with the same name is accessible by direct-field priority within the declaring
  class, so property syntax there would bypass the getter's fallback-to-default logic. The one
  cross-class static-dispatch call sites (Class literal -> Class#getClassLoader()) were converted,
  matching the idiom the Spec already exercises (DefaultResourceLoader.classLoader / .defaultClassLoader).
- SupplierUtil#memoizedNonEmpty's instanceof-narrowed cast on `value` kept: the variable is shared across
  two closures assigned to the same `delegate` field, so Groovy's static type checker refuses to narrow
  it (javap/compile verified; dropping the cast failed compileGroovy with a "closure shared variable"
  error).
- GrailsResourceUtils#instantiateFromFlatConfig's Class.newInstance() kept: swapping in
  getDeclaredConstructor().newInstance() per the table entry would add NoSuchMethodException/
  InvocationTargetException to the call site, widening the method's declared checked-exception surface
  (a public API change), so it was left as-is.
- SpringIOUtils `in`/`out` parameter names across every copy() overload: a real "in is a keyword, rename
  it" Tier A hit, but touching all ~8 overloads' signatures for a non-functional rename was judged
  disproportionate to a focused idiom pass; left unchanged (it already compiles - `in` is only a Groovy
  keyword in for-loop/membership positions).
- ClassPathResource#nullSafeEquals' o1.equals(o2) kept: o1/o2 are declared Object and may be arrays;
  Groovy's == has its own (possibly different) array-comparison semantics, and the very next lines branch
  on primitive-array types via Arrays.equals, so swapping to == risked changing which branch is reached.
- Trivial getter/setter field pairs (ClassPathResource#getPath, FileSystemResource#getPath/getFile,
  DefaultCommandLine$SpecifiedOption#getOption/getValue) kept as explicit methods: collapsing them to bare
  Groovy properties would add a public setter that does not exist today, a public API change.
- XmlParserFeature#getFeatureName kept explicit: carries Javadoc that must stay on the method.

Found and fixed during verification: `map[k] = v` (subscript assignment) on a java.util.Map field
compiles to a dynamic `ScriptBytecodeAdapter.invokeMethodN("putAt", ...)` call under @CompileStatic in
this Groovy version, unlike the read form `map[k]` which resolves to the static
DefaultGroovyMethods.getAt - confirmed via the javap audit across every module. Reverted all ~10
map[k] = v sites introduced by this pass back to map.put(k, v); the read-only subscript conversions were
kept since they compile cleanly.

Verification (build-logic, own gradlew):
- :grails-docs-core:compileGroovy, :compileTestGroovy: BUILD SUCCESSFUL
- :grails-docs-core:test: 67 tests, 1 pre-existing failure unrelated to this change
  (FetchTagsTaskSpec, environmental: this sandbox's global git tag.gpgSign=true with no core.editor
  breaks `git tag`, nothing to do with StringEscapeCategory); StringEscapeCategorySpec itself 2/2 passed.
- no codenarc/checkstyle task exists for this project (compileGroovy only)

Verification (grails-gradle, own gradlew):
- :grails-gradle-common:compileGroovy, :grails-gradle-model:compileGroovy, both compileTestGroovy:
  BUILD SUCCESSFUL
- :grails-gradle-common:codeStyle, :grails-gradle-model:codeStyle: BUILD SUCCESSFUL, 0 CodeNarc
  violations in both main.html reports
- :grails-gradle-common:test (24/24) + :grails-gradle-model:test (432/432): 456/456, 0 failures,
  0 errors, 0 skipped (test-results XML, not exit code)
- javap -c -p dynamic-dispatch audit on every changed class: clean after the putAt fix above. The only
  remaining ScriptBytecodeAdapter/GroovyObject-property hits are inside the compiler-generated
  this$dist$invoke$1/this$dist$set$1/this$dist$get$1 trampolines that GrailsResourceUtils, SupplierUtil,
  DefaultResourceLoader and DefaultCommandLine already carried before this change (triggered by their
  pre-existing anonymous/nested inner classes), confirmed by diffing against untouched classes with no
  such nesting (Option, FileSystemResource, ...) which show none.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…uild-tooling

# Conflicts:
#	grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy
Copilot AI balanced review requested due to automatic review settings October 17, 2026 01:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…uild-tooling

# Conflicts:
#	grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy
@codecov

codecov Bot commented Oct 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.02326% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.1063%. Comparing base (0725501) to head (fc8c30d).

Files with missing lines Patch % Lines
...y/org/grails/io/support/GrailsResourceUtils.groovy 89.7959% 2 Missing and 3 partials ⚠️
.../org/grails/build/parsing/CommandLineParser.groovy 90.0000% 0 Missing and 2 partials ⚠️
...org/grails/build/parsing/DefaultCommandLine.groovy 97.0149% 0 Missing and 2 partials ⚠️
.../groovy/org/grails/io/support/SpringIOUtils.groovy 87.5000% 1 Missing and 1 partial ⚠️
...src/main/groovy/grails/util/GrailsNameUtils.groovy 96.1538% 0 Missing and 1 partial ⚠️
...le/model/src/main/groovy/grails/util/Holder.groovy 66.6667% 0 Missing and 1 partial ⚠️
...ovy/org/grails/io/support/ClassPathResource.groovy 85.7143% 0 Missing and 1 partial ⚠️
...in/groovy/org/grails/io/support/UrlResource.groovy 83.3333% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                               Coverage Diff                               @@
##             refactor/java-to-groovy-build-tooling     #16661        +/-   ##
===============================================================================
+ Coverage                                  64.0971%   64.1063%   +0.0092%     
+ Complexity                                   28681      28667        -14     
===============================================================================
  Files                                         2260       2260                
  Lines                                       112147     112145         -2     
  Branches                                     20366      20366                
===============================================================================
+ Hits                                         71883      71892         +9     
+ Misses                                       31140      31124        -16     
- Partials                                      9124       9129         +5     
Files with missing lines Coverage Δ
...src/main/groovy/grails/util/CollectionUtils.groovy 100.0000% <100.0000%> (ø)
...main/groovy/org/grails/build/parsing/Option.groovy 100.0000% <100.0000%> (ø)
...ls/io/support/AbstractFileResolvingResource.groovy 90.7407% <100.0000%> (ø)
...org/grails/io/support/DefaultResourceLoader.groovy 100.0000% <100.0000%> (ø)
...vy/org/grails/io/support/FileSystemResource.groovy 96.8750% <100.0000%> (ø)
...src/main/groovy/grails/util/GrailsNameUtils.groovy 95.1220% <96.1538%> (-0.0237%) ⬇️
...le/model/src/main/groovy/grails/util/Holder.groovy 92.0000% <66.6667%> (ø)
...ovy/org/grails/io/support/ClassPathResource.groovy 71.2644% <85.7143%> (ø)
...in/groovy/org/grails/io/support/UrlResource.groovy 90.0000% <83.3333%> (ø)
.../org/grails/build/parsing/CommandLineParser.groovy 95.2756% <90.0000%> (ø)
... and 3 more

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

testlens-app Bot commented Oct 18, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: fc8c30d
▶️ Tests: 84598 executed
⚪️ Checks: 85/85 completed


Learn more about TestLens at testlens.app/docs.

borinquenkid and others added 2 commits October 19, 2026 19:42
…(revert before merge)

Pushes, rebases and force-pushes to this stacked PR start no jobs; the PR closest to the release branch keeps CI.
Merging with this commit is a blocker.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@borinquenkid

Copy link
Copy Markdown
Member Author

BLOCKER before merge: this branch carries a temporary CI commit.

This PR is stacked, and to avoid starting a full CI matrix on every push or rebase of the stack it carries one temporary commit, CHANGEME FOR ACCEPTANCE: disable PR workflow triggers. It changes the pull_request: trigger of 13 workflows to branches: ['CHANGEME-FOR-ACCEPTANCE'], a branch that does not exist, so pushing, rebasing or force-pushing this branch starts no jobs.

Condition for merging this PR: that commit must be reverted on this branch first, and CI must then run and be green. The absence of CI checks on this PR is deliberate and does not mean the code was tested. The full build runs on #16576 and #16572, the PRs that target 10.0.x.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants