This is an automated email from the ASF dual-hosted git repository.
daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 121c8605f7 GROOVY-12030: Graduate PropertyHandler from incubating to
stable
121c8605f7 is described below
commit 121c8605f7994fcefb4f13f21b5a4a46b95ee84a
Author: Paul King <[email protected]>
AuthorDate: Sat May 23 09:45:44 2026 +1000
GROOVY-12030: Graduate PropertyHandler from incubating to stable
---
.../transform/options/DefaultPropertyHandler.java | 4 +-
.../groovy/transform/options/PropertyHandler.java | 45 +++++--
.../transform/options/PropertyHandlerTest.groovy | 133 +++++++++++++++++++++
3 files changed, 168 insertions(+), 14 deletions(-)
diff --git a/src/main/java/groovy/transform/options/DefaultPropertyHandler.java
b/src/main/java/groovy/transform/options/DefaultPropertyHandler.java
index 433d6159e9..6d60861d68 100644
--- a/src/main/java/groovy/transform/options/DefaultPropertyHandler.java
+++ b/src/main/java/groovy/transform/options/DefaultPropertyHandler.java
@@ -62,9 +62,7 @@ public class DefaultPropertyHandler extends PropertyHandler {
*/
@Override
public boolean validateAttributes(final AbstractASTTransformation xform,
final AnnotationNode anno) {
- boolean success = true;
- //success |= isValidAttribute(xform, anno, "");
- return success;
+ return true;
}
/**
diff --git a/src/main/java/groovy/transform/options/PropertyHandler.java
b/src/main/java/groovy/transform/options/PropertyHandler.java
index 6fe7d4362f..b50e3a98be 100644
--- a/src/main/java/groovy/transform/options/PropertyHandler.java
+++ b/src/main/java/groovy/transform/options/PropertyHandler.java
@@ -20,7 +20,6 @@ package groovy.transform.options;
import groovy.lang.GroovyClassLoader;
import groovy.transform.PropertyOptions;
-import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
@@ -37,10 +36,21 @@ import static
org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
/**
* Used to provide custom property handling when getting, setting or
initializing properties.
+ * <p>
+ * Subclasses are plugged in via the {@code propertyHandler} attribute on the
+ * {@link PropertyOptions} annotation. Implementations must declare a public
no-argument
+ * constructor; the handler is instantiated via {@link #createPropertyHandler}
using
+ * reflection at compile time.
+ * <p>
+ * During each transform, methods are invoked in a fixed order:
+ * {@link #validateAttributes} first; if it returns {@code true}, then
+ * {@link #validateProperties}; if that also returns {@code true}, then
+ * {@link #createPropInit}, {@link #createPropGetter}, and {@link
#createPropSetter}
+ * are called per property as required by the host transform. Returning {@code
false}
+ * from a validation method short-circuits processing.
*
* @since 2.5.0
*/
-@Incubating
public abstract class PropertyHandler {
private static final Class<? extends Annotation> PROPERTY_OPTIONS_CLASS =
PropertyOptions.class;
@@ -72,15 +82,18 @@ public abstract class PropertyHandler {
}
/**
- * Create a statement that will initialize the property including any
defensive copying. Null if no statement should be added.
+ * Create a statement that will initialize the property including any
defensive copying.
+ * Return {@code null} to indicate that no initialization statement should
be emitted
+ * for this property; this is distinct from returning an empty block and
skips the
+ * corresponding entry in the generated constructor body altogether.
*
* @param xform the transform being processed
- * @param anno the '@ImmutableBase' annotation node
+ * @param anno the annotation node
* @param cNode the classnode containing the property
* @param pNode the property node to initialize
- * @param namedArgMap an "args" Map if the property value should come from
a named arg map or null if not
+ * @param namedArgsMap an "args" Map if the property value should come
from a named arg map or null if not
*/
- public abstract Statement createPropInit(AbstractASTTransformation xform,
AnnotationNode anno, ClassNode cNode, PropertyNode pNode, Parameter
namedArgMap);
+ public abstract Statement createPropInit(AbstractASTTransformation xform,
AnnotationNode anno, ClassNode cNode, PropertyNode pNode, Parameter
namedArgsMap);
/**
* Create the getter block used when reading the property including any
defensive copying.
@@ -101,15 +114,20 @@ public abstract class PropertyHandler {
}
/**
- * Checks whether the named annotation attribute is unsupported for this
handler.
+ * Helper for use from {@link #validateAttributes}: confirms that the
named attribute is
+ * <em>absent</em> from the host annotation. Despite the positive name,
this returns
+ * {@code false} <em>and</em> records a compile error when the attribute
is set, so
+ * subclasses can chain calls in the style {@code return
isValidAttribute(xform, anno, "useSuper");}
+ * to reject attributes that the host transform supports but this handler
does not.
*
* @param xform the active transform
* @param anno the annotation being processed
- * @param memberName the attribute name to validate
- * @return {@code true} if the attribute is valid for this handler
+ * @param memberName the attribute name expected to be absent
+ * @return {@code true} if the attribute is absent (i.e. valid for this
handler);
+ * {@code false} if present (a compile error is added as a side
effect)
*/
protected boolean isValidAttribute(final AbstractASTTransformation xform,
final AnnotationNode anno, final String memberName) {
- if (xform.getMemberValue(anno, memberName) != null) {
+ if (anno.getMember(memberName) != null) {
xform.addError("Error during " + xform.getAnnotationName() + "
processing: Annotation attribute '" + memberName +
"' not supported for property handler " +
getClass().getSimpleName(), anno);
return false;
@@ -118,7 +136,12 @@ public abstract class PropertyHandler {
}
/**
- * Creates the property handler configured for the supplied class.
+ * Creates the property handler configured for the supplied class. If the
class carries
+ * a {@link PropertyOptions} annotation, the {@code propertyHandler}
attribute is read
+ * and the named handler class is instantiated via its public no-argument
constructor;
+ * otherwise a {@link DefaultPropertyHandler} is returned. A compile error
is recorded
+ * (and {@code null} returned) if the handler cannot be loaded, lacks a
no-arg
+ * constructor, or is not a {@code PropertyHandler} subtype.
*
* @param xform the active transform
* @param loader the class loader used to instantiate custom handlers
diff --git
a/src/test/groovy/groovy/transform/options/PropertyHandlerTest.groovy
b/src/test/groovy/groovy/transform/options/PropertyHandlerTest.groovy
new file mode 100644
index 0000000000..eeda442da0
--- /dev/null
+++ b/src/test/groovy/groovy/transform/options/PropertyHandlerTest.groovy
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.transform.options
+
+import org.codehaus.groovy.ast.AnnotationNode
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.Parameter
+import org.codehaus.groovy.ast.PropertyNode
+import org.codehaus.groovy.ast.stmt.Statement
+import org.codehaus.groovy.transform.AbstractASTTransformation
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+import static org.codehaus.groovy.ast.tools.GeneralUtils.assignS
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
+import static org.codehaus.groovy.ast.tools.GeneralUtils.varX
+
+/**
+ * Locks in the public contract of {@link PropertyHandler}: that user-supplied
+ * handlers wired in via {@code @PropertyOptions(propertyHandler = …)} are
+ * loaded, validated and consulted at compile time.
+ */
+final class PropertyHandlerTest {
+
+ @Test
+ void testCustomHandlerControlsPropertyInitialisation() {
+ assertScript '''
+ import groovy.transform.PropertyOptions
+ import groovy.transform.TupleConstructor
+ import
groovy.transform.options.PropertyHandlerTest.PrefixingPropertyHandler
+
+ @PropertyOptions(propertyHandler = PrefixingPropertyHandler)
+ @TupleConstructor
+ class Foo {
+ String name
+ }
+
+ assert new Foo('hello').name == 'X_hello'
+ '''
+ }
+
+ @Test
+ void testHandlerValidateAttributesCanRejectAnAttribute() {
+ // Exercises a list-valued attribute (`includes = [...]`)
+ def err = shouldFail '''
+ import groovy.transform.PropertyOptions
+ import groovy.transform.TupleConstructor
+ import
groovy.transform.options.PropertyHandlerTest.RejectsIncludesPropertyHandler
+
+ @PropertyOptions(propertyHandler = RejectsIncludesPropertyHandler)
+ @TupleConstructor(includes = ['name'])
+ class Bar {
+ String name
+ String tag
+ }
+ '''
+ assert err.message.contains("Annotation attribute 'includes' not
supported for property handler RejectsIncludesPropertyHandler")
+ }
+
+ @Test
+ void testNoPropertyOptionsFallsBackToDefaultPropertyHandler() {
+ assertScript '''
+ import groovy.transform.TupleConstructor
+
+ @TupleConstructor
+ class Baz {
+ String name
+ }
+
+ // unchanged value confirms DefaultPropertyHandler was used, not
PrefixingPropertyHandler
+ assert new Baz('plain').name == 'plain'
+ '''
+ }
+
+ /**
+ * Custom handler that rewrites every property initialisation to prefix the
+ * incoming constructor argument with {@code 'X_'}, demonstrating that
+ * {@link PropertyHandler#createPropInit} fully controls the emitted code.
+ */
+ static class PrefixingPropertyHandler extends PropertyHandler {
+ @Override
+ boolean validateAttributes(AbstractASTTransformation xform,
AnnotationNode anno) {
+ true
+ }
+
+ @Override
+ Statement createPropInit(AbstractASTTransformation xform,
AnnotationNode anno,
+ ClassNode cNode, PropertyNode pNode,
Parameter namedArgsMap) {
+ def name = pNode.name
+ // Generated: this.<name> = 'X_' + <name>
+ return assignS(propX(varX('this'), name), plusX(constX('X_'),
varX(name)))
+ }
+ }
+
+ /**
+ * Custom handler that demonstrates rejecting a host-transform attribute
via
+ * {@link PropertyHandler#isValidAttribute}; combining this handler with
the
+ * {@code includes} attribute on {@code @TupleConstructor} produces a
compile
+ * error rather than silently ignoring the attribute. The list-valued shape
+ * also exercises that detection is not restricted to scalar constant
attributes.
+ */
+ static class RejectsIncludesPropertyHandler extends PropertyHandler {
+ @Override
+ boolean validateAttributes(AbstractASTTransformation xform,
AnnotationNode anno) {
+ isValidAttribute(xform, anno, 'includes')
+ }
+
+ @Override
+ Statement createPropInit(AbstractASTTransformation xform,
AnnotationNode anno,
+ ClassNode cNode, PropertyNode pNode,
Parameter namedArgsMap) {
+ null // never reached: validateAttributes returns false above
+ }
+ }
+}