You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/06/28 09:16:09 UTC
(groovy) 02/02: minor refactor: trait examples to align with GEP-22
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 20208b0a0bf94d2f71981c2eb8cb41575eb66e12
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Jun 28 19:15:58 2026 +1000
minor refactor: trait examples to align with GEP-22
---
src/spec/doc/_traits.adoc | 74 ++++++++++++++++++++++------
src/spec/test/TraitsSpecificationTest.groovy | 51 +++++++++++++++++++
2 files changed, 110 insertions(+), 15 deletions(-)
diff --git a/src/spec/doc/_traits.adoc b/src/spec/doc/_traits.adoc
index 01edb90fd6..66e6553563 100644
--- a/src/spec/doc/_traits.adoc
+++ b/src/spec/doc/_traits.adoc
@@ -651,7 +651,7 @@ runtime mixins, not the @Mixin annotation which is deprecated in favour of trait
First of all, methods defined in a trait are visible in bytecode:
-* internally, the trait is represented as an interface (without default or static methods) and several helper classes
+* internally, the trait is represented as an interface (without default methods; public static methods are exposed as JVM-native interface statics) and several helper classes
* this means that an object implementing a trait effectively implements an _interface_
* those methods are visible from Java
* they are compatible with type checking and static compilation
@@ -676,22 +676,23 @@ A will continue responding to A even if mixed in.
== Static methods, properties and fields
-WARNING: The following instructions are subject to caution. Static member support is work in progress and still experimental. The
-information below is valid for {groovyVersion} only.
+NOTE: Static member support in traits is *incubating*: the behaviour described here is settled for {groovyVersion}
+but may still be refined. GEP-22 gives the full specification.
-It is possible to define static methods in a trait, but it comes with numerous limitations:
+A trait may declare static methods, properties and fields:
-* Traits with static methods cannot be compiled statically or type checked. All static methods,
-properties and field are accessed dynamically (it's a limitation from the JVM).
-* Static methods do not appear within the generated interfaces for each trait.
-* The trait is interpreted as a _template_ for the implementing class, which means that each
-implementing class will get its own static methods, properties and fields. So a static member
-declared on a trait doesn't belong to the `Trait`, but to its implementing class.
-* You should typically not mix static and instance methods of the same signature. The normal
-rules for applying traits apply (including multiple inheritance conflict resolution). If the
-method chosen is static but some implemented trait has an instance variant, a compilation error
-will occur. If the method chosen is the instance variant, the static variant will be ignored
-(the behavior is similar to static methods in Java interfaces for this case).
+* The trait is interpreted as a _template_ for the implementing class: each implementing class gets its *own copy*
+of the trait's static members. A static member declared on a trait belongs to the implementing class, not to a
+single copy shared across all implementers.
+* Public static methods are _declarer-bound_ by default and are promoted onto the generated trait interface as
+JVM-native static methods, so both `Trait.m(...)` and `Impl.m(...)` resolve. A private static method stays internal
+to the trait and is not promoted.
+* Static methods may be declared in, and called from, `@CompileStatic` and `@TypeChecked` code; they are not
+restricted to dynamic Groovy.
+* You should typically not mix static and instance methods of the same signature. The normal trait method-selection
+rules apply (including multiple inheritance conflict resolution): if the selected method is static but some
+implemented trait has an instance variant, a compilation error occurs; if the instance variant is selected, the
+static variant is ignored (as with static methods in Java interfaces for this case).
Let's start with a simple example:
@@ -724,6 +725,49 @@ include::../test/TraitsSpecificationTest.groovy[tags=staticfield_distinct,indent
<4> the static field `CALLED` on `Bar` is updated
<5> but the static field `CALLED` on `Baz` is not, because it is *distinct*
+=== Overridable static defaults with `@Virtual`
+
+By default a trait's static method is _declarer-bound_: when other trait code calls it, it always resolves to the
+trait's own copy, even if an implementing class declares a static method of the same signature. That is the right
+choice for an internal helper or a constant that the trait owns outright.
+
+Sometimes, though, a trait wants to publish a static *default that an implementing class can override*, and have
+the trait's own logic honour that override. This is the template-method pattern applied to a static member — the
+same override-visibility that trait _instance_ methods already have. Annotate the method with
+`@groovy.transform.Virtual` to opt into per-implementer override dispatch:
+
+[source,groovy]
+----
+include::../test/TraitsSpecificationTest.groovy[tags=virtual_default,indent=0]
+----
+<1> the overridable default, marked `@Virtual`
+<2> trait logic that consults the setting
+<3> an unqualified call, so it dispatches through the implementing class
+<4> uses the trait's default
+<5> overrides the default with its own `static`
+<6> the default is seen for `StrictEntity`
+<7> the override is seen by the trait's own `describe()` for `LenientEntity`
+<8> and externally via `Impl.m()`
+
+Reach for `@Virtual` when the overridable thing is genuinely a property of the _type_ rather than of an instance —
+a policy or configuration default that answers a question about the class as a whole (Grails uses this for hooks
+such as `Validateable.defaultNullable()`), that may be consulted where no instance exists yet (class
+initialisation, static factory methods, building metadata), and that external callers can read statically as
+`Impl.setting()`. For overridable static _data_ rather than behaviour, use a `@Virtual` accessor
+(`@Virtual static getSetting()`) rather than a static field, since a bare static-field read from trait code is not
+override-visible (see <<_inheritance_of_state_gotchas,Inheritance of state gotchas>>).
+
+A `@Virtual` static is deliberately *not* exposed on the trait interface (interface statics are declarer-bound by
+JVM rule, which is incompatible with override dispatch). It therefore has no qualified `Trait.m(...)` form: read a
+concrete class's value with `Impl.m(...)`, or use `T.super.m(...)` from within trait code for the trait's own
+default. A qualified `Trait.m(...)` call to a `@Virtual` static is rejected at compile time:
+
+[source,groovy]
+----
+include::../test/TraitsSpecificationTest.groovy[tags=virtual_qualified,indent=0]
+----
+<1> rejected: a `@Virtual` static has no qualified `Trait.m(...)` form
+
== Inheritance of state gotchas
We have seen that traits are stateful. It is possible for a trait to define fields or properties, but when a class implements a trait, it gets those fields/properties on
diff --git a/src/spec/test/TraitsSpecificationTest.groovy b/src/spec/test/TraitsSpecificationTest.groovy
index e7333cac4f..7b0f91c7df 100644
--- a/src/spec/test/TraitsSpecificationTest.groovy
+++ b/src/spec/test/TraitsSpecificationTest.groovy
@@ -544,6 +544,57 @@ assert !Baz.TestHelper__CALLED // <5>
'''
}
+ @Test
+ void testVirtualStaticDefault() {
+ assertScript '''// tag::virtual_default[]
+import groovy.transform.CompileStatic
+import groovy.transform.Virtual
+
+@CompileStatic
+trait Configurable {
+ @Virtual static boolean nullableByDefault() { false } // <1>
+ static String describe() { // <2>
+ "nullable=${nullableByDefault()}" // <3>
+ }
+}
+
+@CompileStatic
+class StrictEntity implements Configurable {} // <4>
+
+@CompileStatic
+class LenientEntity implements Configurable {
+ static boolean nullableByDefault() { true } // <5>
+}
+
+assert StrictEntity.describe() == 'nullable=false' // <6>
+assert LenientEntity.describe() == 'nullable=true' // <7>
+assert LenientEntity.nullableByDefault() == true // <8>
+// end::virtual_default[]
+'''
+ }
+
+ @Test
+ void testVirtualQualifiedRejected() {
+ def err = shouldFail '''
+// tag::virtual_qualified[]
+import groovy.transform.CompileStatic
+import groovy.transform.Virtual
+
+@CompileStatic
+trait Configurable {
+ @Virtual static boolean nullableByDefault() { false }
+}
+
+@CompileStatic
+class Entity implements Configurable {
+ static check() { Configurable.nullableByDefault() } // <1>
+}
+Entity.check()
+// end::virtual_qualified[]
+'''
+ assert err.message.contains('@Virtual trait static method')
+ }
+
@Test
void testPrePostfixIsDisallowed() {
def message = shouldFail '''