Skip to content

Parameterize the @Scaffold service superclass so scaffolded service methods aren't type-erased#15717

Open
codeconsole wants to merge 2 commits into
apache:7.2.xfrom
codeconsole:scaffold-generic-fix
Open

Parameterize the @Scaffold service superclass so scaffolded service methods aren't type-erased#15717
codeconsole wants to merge 2 commits into
apache:7.2.xfrom
codeconsole:scaffold-generic-fix

Conversation

@codeconsole
Copy link
Copy Markdown
Contributor

Problem

A scaffolded service declared with a parameterized base, e.g. @Scaffold(GormMongoService<Book>), is given the raw type GormService/GormMongoService as its superclass — the <Book> type argument is dropped. So every inherited generic method (get():T, list():List<T>, save(T):T, …) resolves to the GormEntity upper bound rather than the concrete domain type.

Under @GrailsCompileStatic/@CompileStatic this forces a cast at every call site:

Book b         = bookService.get(id)      // Cannot assign value of type GormEntity to variable of type Book
List<Book> all = bookService.list(params) // List<GormEntity>

Root cause

In ScaffoldingServiceInjector.performInjectionOnAnnotatedClass:

superClassNode = valueClassNode.getPlainNodeReference()                    // strips the <Book> generic
...
classNode.setSuperClass(GrailsASTUtils.nonGeneric(superClassNode, domainClass))

getPlainNodeReference() removes the generics, so the following nonGeneric(raw, domain) is a no-op: replaceGenericsPlaceholders early-returns the plain node because the type is no longer using generics. The domain type is known but never applied to the superclass.

Fix

Set the superclass to a parameterized GormService<Domain>:

ClassNode parameterizedSuper = superClassNode.getPlainNodeReference()
parameterizedSuper.setGenericsTypes(
    [new GenericsType(GrailsASTUtils.nonGeneric(domainClass))] as GenericsType[])
classNode.setSuperClass(parameterizedSuper)

Impact

Inherited scaffold-service methods now resolve to the domain type with no casts under static compilation:

Book b         = bookService.get(id)      // ✅ Book
List<Book> all = bookService.list(params) // ✅ List<Book>

Generics are compile-time only, so there is no runtime behavior change. Verified against a real app's @Scaffold(GormMongoService<T>) services — get()/list() compile to the domain type under @GrailsCompileStatic with zero casts (previously every such call required a cast or @CompileDynamic).

…thods aren't type-erased

ScaffoldingServiceInjector set a RAW superclass (extends GormMongoService), because it built the node via valueClassNode.getPlainNodeReference() then nonGeneric(raw, domain) — which no-ops since the type already has no generics. Result: inherited get():T/list():List<T>/save(T):T erased to the GormEntity upper bound, forcing casts/@CompileDynamic at every statically-compiled call site. Set the superclass to a parameterized GormMongoService<Domain> instead.
Copy link
Copy Markdown
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

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

Let's add a test that makes use of this.

…le form + add tests

The previous change parameterized the @scaffold superclass node, but the generic
signature only survived for the generic form (@scaffold(GormService<Widget>)),
where the source usage already flagged the class as using generics. The simple
form (@scaffold(Widget)) still compiled to a raw superclass, so inherited
get():T/list():List<T>/save(T):T erased to GormEntity under static compilation.

Injection runs at CANONICALIZATION (after generics resolution), so the class-level
generic signature is only written at generation time when the class node reports
usesGenerics(). Set classNode.setUsingGenerics(true) so the parameterized
superclass is emitted for every form, while preserving custom bases
(e.g. GormMongoService<Schedule>).

Add ScaffoldingServiceInjectorSpec covering the simple form, the generic form,
a custom scaffold base, and static-compilation resolution of get/list/save.
@testlens-app
Copy link
Copy Markdown

testlens-app Bot commented Jun 5, 2026

✅ All tests passed ✅

🏷️ Commit: 5b905eb
▶️ Tests: 45588 executed
⚪️ Checks: 37/37 completed


Learn more about TestLens at testlens.app.

@codeconsole
Copy link
Copy Markdown
Contributor Author

Let's add a test that makes use of this.

@jdaugherty done

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.

3 participants