Skip to content

7.2.x (grails-scaffolding) - Parameterize the @Scaffold controller superclass so scaffolded controller members aren't type-erased#15729

Merged
codeconsole merged 2 commits into
apache:7.2.xfrom
codeconsole:scaffold-controller-generic-fix
Jun 15, 2026
Merged

7.2.x (grails-scaffolding) - Parameterize the @Scaffold controller superclass so scaffolded controller members aren't type-erased#15729
codeconsole merged 2 commits into
apache:7.2.xfrom
codeconsole:scaffold-controller-generic-fix

Conversation

@codeconsole

Copy link
Copy Markdown
Contributor

Summary

Controller-side counterpart of #15717. ScaffoldingControllerInjector set the injected superclass via GrailsASTUtils.nonGeneric(superClassNode, domainClass), so a scaffolded controller's superclass was written raw and every inherited generic member erased to its GormEntity upper bound under static compilation:

@Scaffold(RestfulController<Widget>)
@GrailsCompileStatic
class WidgetController {
    @Override
    protected Widget queryForResource(Serializable id) {
        def widget = super.queryForResource(id)   // before: typed GormEntity, not Widget
        (widget?.owner == currentOwner) ? widget : null
    }
}

fails with:

[Static type checking] - No such property: owner for class: org.grails.datastore.gorm.GormEntity
[Static type checking] - Cannot return value of type org.grails.datastore.gorm.GormEntity for method returning com.example.Widget

The common workaround in real apps is sprinkling @CompileDynamic over every queryForResource/createResource/listAllResources override, giving up static compilation exactly where project-scoping security checks live.

Design

Same pattern #15717 applied to ScaffoldingServiceInjector: take a getPlainNodeReference() copy of the resolved superclass, set its generics to the domain type, mark the class node with setUsingGenerics(true) — injection runs at CANONICALIZATION (after generics resolution), so the generic superclass signature is only emitted when the class node itself reports usesGenerics; otherwise it is written raw — then setSuperClass(parameterizedSuper). Applies to all three forms: @Scaffold(Widget), @Scaffold(RestfulController<Widget>), and custom bases @Scaffold(ApiController<Widget>) (the declared base is preserved, not collapsed to RestfulController).

Tests

New ScaffoldingControllerInjectorSpec mirroring ScaffoldingServiceInjectorSpec:

  • simple form @Scaffold(Widget)genericSuperclass is RestfulController<Widget>, not raw
  • generic form @Scaffold(RestfulController<Widget>) → parameterized
  • custom scaffold base preserved and parameterized as CustomBase<Widget>
  • a @CompileStatic scaffolded controller overriding queryForResource / createResource / listAllResources and narrowing the super.* results to the domain type compiles without casts

All four tests fail against the previous injector and pass with the fix; :grails-scaffolding:test passes in full.

…ed controller members aren't type-erased
@codeconsole codeconsole changed the title Parameterize the @Scaffold controller superclass so scaffolded controller members aren't type-erased 7.2.x (grails-scaffolding) - Parameterize the @Scaffold controller superclass so scaffolded controller members aren't type-erased Jun 11, 2026
@codeconsole

Copy link
Copy Markdown
Contributor Author

Added a small robustness guard (both injectors, since the merged #15717 service-injector code shares the shape): the superclass is only parameterized when the base declares exactly one type parameter. A base with zero or multiple type parameters — e.g. @Scaffold(MultiParamBase<Widget, String>) — previously would have received a malformed single-argument generic signature; it now keeps the prior raw form. Covered by a new spec case in each injector spec (both fail without the guard).

@testlens-app

testlens-app Bot commented Jun 11, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: a32edba
▶️ Tests: 28363 executed
⚪️ Checks: 37/37 completed


Learn more about TestLens at testlens.app.

@jdaugherty jdaugherty 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.

In 8.x, I 100% agree with this change. In 7.2.x, I'm not sure. @matrei would you consider this a breaking change?

@codeconsole

Copy link
Copy Markdown
Contributor Author

@jdaugherty it matches #15717 which is already in 7.2.x

@jdaugherty

Copy link
Copy Markdown
Contributor

I'm good to merge then.

@codeconsole codeconsole merged commit 65edb8c into apache:7.2.x Jun 15, 2026
38 checks passed
@codeconsole

Copy link
Copy Markdown
Contributor Author

@jdaugherty thanks

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants