Skip to content

fix: @CompileStatic on @Service abstract class with injected @Service properties fails to compile#15396

Open
jamesfredley wants to merge 4 commits intoapache:7.0.xfrom
jamesfredley:fix/compile-static-service-injection
Open

fix: @CompileStatic on @Service abstract class with injected @Service properties fails to compile#15396
jamesfredley wants to merge 4 commits intoapache:7.0.xfrom
jamesfredley:fix/compile-static-service-injection

Conversation

@jamesfredley
Copy link
Contributor

@jamesfredley jamesfredley commented Feb 17, 2026

Summary

@CompileStatic (or @GrailsCompileStatic) on a @Service abstract class causes a compilation failure when the class has properties whose types are annotated with @Service. The error is:

BUG! exception in phase 'instruction selection' in source unit '...'
Unexpected return statement at -1:-1 return authorDataService

This forces users to omit @CompileStatic on any @Service abstract class that injects other Data Services, losing static compilation benefits (compile-time type checking, better performance).

Bug Description

Given two Data Services where one injects the other:

@Service(Author)
interface AuthorDataService {
    Author get(Serializable id)
    Author save(Author author)
}

@CompileStatic          // <-- causes compilation failure
@Service(Book)
@Transactional
abstract class BookService implements BookDataService {
    AuthorDataService authorDataService  // <-- triggers the bug

    Book createBookWithAuthorCheck(String title, Serializable authorId) {
        Author author = authorDataService.get(authorId)
        // ...
    }
}

Compilation fails with:

BUG! exception in phase 'instruction selection' in source unit '...BookService.groovy'
Unexpected return statement at -1:-1 return authorDataService

Without @CompileStatic, the code compiles and runs correctly.

Root Cause

ServiceTransformation (SEMANTIC_ANALYSIS phase) iterates over the abstract class's properties and, for each property whose type has @Service, sets a lazy getter block on the abstract class's PropertyNode:

pn.setGetterBlock(
    block(
        ifS(equalsNullX(fieldVar),
            assignX(fieldVar, callX(varX('datastore'), 'getService', classX(propertyType)))
        ),
        returnS(fieldVar)
    )
)

This causes two problems under @CompileStatic:

  1. varX('datastore') is unresolvable - the datastore field only exists on the generated $BookServiceImplementation class, not on the abstract BookService class where the getter block is being set.

  2. ReturnStatement in property getter block - StaticTypeCheckingVisitor.visitProperty() does not expect ReturnStatement nodes inside property getter blocks, causing the "Unexpected return statement" error during instruction selection.

Under dynamic Groovy (no @CompileStatic), neither issue surfaces because variable references resolve at runtime and the static type checker is not invoked.

Fix

Removed the lazy getter block from the abstract class's PropertyNode entirely. The getter block was redundant because the generated setDatastore() method on the impl class already eagerly populates all @Service-typed properties during initialization:

// Generated on $BookServiceImplementation.setDatastore():
this.authorDataService = datastore.getService(AuthorDataService)

This eager initialization runs when the Spring context wires the Datastore bean, so by the time any user code accesses the property, it's already populated. The lazy fallback was unnecessary.

Changes: 1 file modified - ServiceTransformation.groovy (8 insertions, 9 deletions). Replaced the pn.setGetterBlock(...) block with a comment explaining why it was removed.

Test Coverage

Unit Tests - CompileStaticServiceInjectionSpec (6 tests)

  1. @CompileStatic + single @service property compiles - abstract class with @CompileStatic and one @Service-typed property compiles without error
  2. Dynamic mode still works - same class without @CompileStatic still compiles (regression check)
  3. No @service properties - no datastore infrastructure - abstract class without @Service-typed properties doesn't get datastore field (regression check)
  4. @CompileStatic + multiple @service properties - abstract class with two @Service-typed properties compiles without error
  5. @CompileStatic + custom methods with complex return statements - ensures return statements in custom methods are not confused with property getter returns
  6. Impl has correct setDatastore/getDatastore/datastore field - verifies the generated implementation class has the datastore infrastructure and setDatastore eagerly populates service properties

Functional Tests - GormDataServicesSpec (7 new tests in grails-test-examples/gorm)

Added AuthorDataService (interface @Service(Author)) and CompileStaticBookService (@CompileStatic abstract @Service(Book) injecting AuthorDataService) to the gorm functional test app, with integration tests:

  1. @CompileStatic service is autowired - service bean is available in Spring context
  2. @CompileStatic service basic CRUD operations - save, get, findByTitle work through static compilation
  3. Injected AuthorDataService is available - the @Service-typed property is wired correctly
  4. Cross-service method returns book with author details - custom method calls injected service, returns combined data
  5. Cross-service method with no author - handles book without author association
  6. Cross-service method with non-existent book - returns null for missing book
  7. getCounts returns both book and author counts - cross-service count aggregation works

All tests pass. All 30 existing ServiceTransformSpec tests pass (no regressions).

Example Application

https://github.com/jamesfredley/grails-compile-static-service-bug

A minimal Grails 7 app demonstrating the bug. BookService is a @Service(Book) abstract class with an AuthorDataService property. @CompileStatic is commented out as a workaround.

To reproduce the bug:

  1. Uncomment @CompileStatic in grails-app/services/com/example/BookService.groovy
  2. Run ./gradlew compileGroovy
  3. Observe: BUG! exception in phase 'instruction selection' ... Unexpected return statement at -1:-1 return authorDataService

To run with workaround:

  1. ./gradlew bootRun
  2. Visit http://localhost:8080/bugDemo/index
  3. Returns: {"bug_present": true, "workaround_applied": true, ...}

Environment Information

  • Grails 7.0.7
  • GORM 7.0.7
  • Spring Boot 3.5.10
  • Groovy 4.0.30
  • JDK 17+

Version

7.0.7

…stract classes

ServiceTransformation generated lazy getters on abstract class PropertyNodes
that referenced varX('datastore') — a field only present on the generated
$ServiceImplementation class. Under @CompileStatic, this caused:
1. Unresolvable variable reference (datastore field not in abstract class scope)
2. StaticTypeCheckingVisitor 'Unexpected return statement' in property getter blocks

Fix: Remove lazy getter block from abstract class PropertyNodes entirely. The
generated setDatastore() method on the impl class already eagerly populates all
@Service-typed properties during initialization, making the lazy fallback redundant.

Adds 6 Spock tests covering:
- @CompileStatic + single/multiple @Service-typed properties
- @CompileStatic + custom methods with complex return statements
- Regression: dynamic mode still works
- Regression: no datastore infrastructure when no @Service-typed properties
- Impl class has correct setDatastore/getDatastore/datastore field
Copy link
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.

We should add a functional test for this scenario too.

@jamesfredley jamesfredley marked this pull request as draft February 17, 2026 19:58
@jamesfredley jamesfredley marked this pull request as ready for review February 18, 2026 21:42
Copilot AI review requested due to automatic review settings February 18, 2026 21:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request fixes a compilation bug where @CompileStatic (or @GrailsCompileStatic) on a @Service abstract class causes compilation failure when the class has properties whose types are annotated with @Service. The root cause was that ServiceTransformation was setting a lazy getter block on the abstract class's PropertyNode that referenced a datastore field only present on the generated implementation class, causing "Unexpected return statement" errors under @CompileStatic.

Changes:

  • Removed lazy getter block generation from abstract class PropertyNode in ServiceTransformation
  • Added comprehensive test suite covering @CompileStatic service injection scenarios
  • Added explanatory comment documenting why lazy getter block is not set

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ServiceTransformation.groovy Removed lazy getter block that referenced non-existent 'datastore' field; added comment explaining the fix relies on eager initialization in setDatastore()
CompileStaticServiceInjectionSpec.groovy New comprehensive test suite with 6 tests covering @CompileStatic + service injection, dynamic mode regression, multiple services, and datastore infrastructure verification

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…ervice property

Add AuthorDataService interface, CompileStaticBookService abstract class
with @CompileStatic and AuthorDataService injection, and 7 integration
tests in GormDataServicesSpec that exercise cross-service method calls
under static compilation.

Assisted-by: OpenCode <opencode@opencode.ai>
@jamesfredley jamesfredley dismissed jdaugherty’s stale review February 18, 2026 21:53

Functional tests added to existing test app

Fix inaccurate comment in ServiceTransformation that said 'lazy getter
methods are generated on the impl class' when the code actually uses
eager assignment in setDatastore(). Rename test from 'setDatastore
eagerly populates service properties' to 'impl has datastore
infrastructure' since it only verifies structural presence, not
runtime behavior.

Assisted-by: OpenCode <opencode@opencode.ai>
Remove assignX, equalsNullX, and ifS static imports from
ServiceTransformation that became unused after the lazy getter
code was removed from @service abstract classes.

Assisted-by: Claude Code <Claude@Claude.ai>
@jamesfredley jamesfredley moved this to In Progress in Apache Grails Feb 18, 2026
@jamesfredley jamesfredley added this to the grails:7.0.8 milestone Feb 18, 2026
@jamesfredley jamesfredley requested a review from matrei February 18, 2026 23:36
jamesfredley added a commit to jamesfredley/grails-core that referenced this pull request Feb 19, 2026
…iTenant routing, and CRUD connection fixes

Add documentation reflecting the post-fix state after PRs apache#15393,
apache#15395, and apache#15396 are merged:

- Add @CompileStatic + injected @service property example (PR apache#15396)
- Add Multi-Tenancy with explicit datasource section (PR apache#15393)
- List all CRUD methods that respect connection routing (PR apache#15395)
- Soften IMPORTANT boxes to NOTE with authoritative tone

Assisted-by: Claude Code <Claude@Claude.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants

Comments