diff --git a/src/main/java/groovy/lang/MetaClassImpl.java b/src/main/java/groovy/lang/MetaClassImpl.java index 3afaf40f471..f4950846605 100644 --- a/src/main/java/groovy/lang/MetaClassImpl.java +++ b/src/main/java/groovy/lang/MetaClassImpl.java @@ -18,6 +18,7 @@ */ package groovy.lang; +import groovy.transform.Internal; import org.apache.groovy.internal.util.UncheckedThrow; import org.apache.groovy.util.BeanUtils; import org.apache.groovy.util.SystemUtil; @@ -2253,13 +2254,15 @@ public List getProperties() { // simply return the values of the metaproperty map as a List List ret = new ArrayList<>(propertyMap.size()); for (MetaProperty mp : propertyMap.values()) { - if (mp instanceof CachedField) { - if (mp.isSynthetic() + if (mp instanceof CachedField cf) { + if (cf.isSynthetic() + || cf.isAnnotationPresent(Internal.class) // GROOVY-5169, GROOVY-9081, GROOVY-9103, GROOVY-10438, GROOVY-10555, et al. - || (!permissivePropertyAccess && !checkAccessible(getClass(), ((CachedField) mp).getDeclaringClass(), mp.getModifiers(), false))) { + || (!permissivePropertyAccess && !checkAccessible(getClass(), cf.getDeclaringClass(), cf.getModifiers(), false))) { continue; } } else if (mp instanceof MetaBeanProperty mbp) { + if (isMarkedInternal(mbp)) continue; // filter out extrinsic properties (DGM, ...) boolean getter = true, setter = true; MetaMethod getterMetaMethod = mbp.getGetter(); @@ -2291,6 +2294,22 @@ private static boolean canAccessLegally(final MetaMethod method) { || ((CachedMethod) method).canAccessLegally(MetaClassImpl.class); } + private static boolean isMarkedInternal(final MetaBeanProperty mbp) { + CachedField field = mbp.getField(); + if (field != null && field.isAnnotationPresent(Internal.class)) { + return true; + } + MetaMethod getter = mbp.getGetter(); + if (getter instanceof CachedMethod cm && cm.getAnnotation(Internal.class) != null) { + return true; + } + MetaMethod setter = mbp.getSetter(); + if (setter instanceof CachedMethod cm && cm.getAnnotation(Internal.class) != null) { + return true; + } + return false; + } + /** * return null if nothing valid has been found, a MetaMethod (for getter always the case if not null) or * a LinkedList<MetaMethod> if there are multiple setter diff --git a/src/main/java/org/apache/groovy/ast/tools/AnnotatedNodeUtils.java b/src/main/java/org/apache/groovy/ast/tools/AnnotatedNodeUtils.java index 2bbd0092a6e..7e6bf7a721b 100644 --- a/src/main/java/org/apache/groovy/ast/tools/AnnotatedNodeUtils.java +++ b/src/main/java/org/apache/groovy/ast/tools/AnnotatedNodeUtils.java @@ -19,10 +19,14 @@ package org.apache.groovy.ast.tools; import groovy.transform.Generated; +import groovy.transform.Internal; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; import org.codehaus.groovy.ast.ClassHelper; import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.PropertyNode; import java.util.List; @@ -31,6 +35,7 @@ */ public class AnnotatedNodeUtils { private static final ClassNode GENERATED_TYPE = ClassHelper.make(Generated.class); + private static final ClassNode INTERNAL_TYPE = ClassHelper.make(Internal.class); private AnnotatedNodeUtils() { } @@ -54,4 +59,46 @@ public static boolean hasAnnotation(final AnnotatedNode node, final ClassNode an public static boolean isGenerated(final AnnotatedNode node) { return hasAnnotation(node, GENERATED_TYPE); } + + /** + * Marks a node with the {@link Internal @Internal} annotation. + * + * @since 6.0.0 + */ + public static T markAsInternal(final T nodeToMark) { + if (!isInternal(nodeToMark)) { + nodeToMark.addAnnotation(new AnnotationNode(INTERNAL_TYPE)); + } + return nodeToMark; + } + + /** + * Checks whether a node is annotated with {@link Internal @Internal}. + * + * @since 6.0.0 + */ + public static boolean isInternal(final AnnotatedNode node) { + return hasAnnotation(node, INTERNAL_TYPE); + } + + /** + * Checks whether an AST node is deemed internal, either by name convention + * (contains {@code $}) or by being annotated with {@link Internal @Internal}. + * + * @since 6.0.0 + */ + public static boolean deemedInternal(final AnnotatedNode node) { + if (isInternal(node)) return true; + String name; + if (node instanceof FieldNode fn) { + name = fn.getName(); + } else if (node instanceof PropertyNode pn) { + name = pn.getName(); + } else if (node instanceof MethodNode mn) { + name = mn.getName(); + } else { + return false; + } + return name.contains("$"); + } } diff --git a/src/main/java/org/codehaus/groovy/reflection/CachedField.java b/src/main/java/org/codehaus/groovy/reflection/CachedField.java index d8ee24da4c1..0e9d270b30b 100644 --- a/src/main/java/org/codehaus/groovy/reflection/CachedField.java +++ b/src/main/java/org/codehaus/groovy/reflection/CachedField.java @@ -50,6 +50,16 @@ public Class getDeclaringClass() { return field.getDeclaringClass(); } + /** + * Checks whether the underlying field has the specified annotation. + * Unlike {@link #getCachedField()}, this does not trigger accessibility changes. + * + * @since 6.0.0 + */ + public boolean isAnnotationPresent(Class annotationType) { + return field.isAnnotationPresent(annotationType); + } + /** * {@inheritDoc} */ diff --git a/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java index 3f0732020a2..d21e002ec64 100644 --- a/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/AbstractASTTransformation.java @@ -252,10 +252,26 @@ public static List tokenize(String rawExcludes) { return rawExcludes == null ? new ArrayList<>() : StringGroovyMethods.tokenize(rawExcludes, ", "); } + /** + * @see org.apache.groovy.ast.tools.AnnotatedNodeUtils#markAsInternal(AnnotatedNode) + * @since 6.0.0 + */ + public static void markAsInternal(AnnotatedNode node) { + AnnotatedNodeUtils.markAsInternal(node); + } + public static boolean deemedInternalName(String name) { return name.contains("$"); } + /** + * @see org.apache.groovy.ast.tools.AnnotatedNodeUtils#deemedInternal(AnnotatedNode) + * @since 6.0.0 + */ + public static boolean deemedInternal(AnnotatedNode node) { + return AnnotatedNodeUtils.deemedInternal(node); + } + public static boolean shouldSkipUndefinedAware(String name, List excludes, List includes) { return shouldSkipUndefinedAware(name, excludes, includes, false); } @@ -266,6 +282,18 @@ public static boolean shouldSkipUndefinedAware(String name, List exclude (includes != null && !includes.contains(name)); } + /** + * Variant that checks both the name and {@link groovy.transform.Internal @Internal} annotation. + * + * @since 6.0.0 + */ + public static boolean shouldSkipUndefinedAware(AnnotatedNode node, List excludes, List includes, boolean allNames) { + String name = nodeName(node); + return (excludes != null && excludes.contains(name)) || + (!allNames && deemedInternal(node)) || + (includes != null && !includes.contains(name)); + } + public static boolean shouldSkip(String name, List excludes, List includes) { return shouldSkip(name, excludes, includes, false); } @@ -276,6 +304,25 @@ public static boolean shouldSkip(String name, List excludes, List excludes, List includes, boolean allNames) { + String name = nodeName(node); + return (excludes != null && excludes.contains(name)) || + (!allNames && deemedInternal(node)) || + (includes != null && !includes.isEmpty() && !includes.contains(name)); + } + + private static String nodeName(AnnotatedNode node) { + if (node instanceof FieldNode fn) return fn.getName(); + if (node instanceof PropertyNode pn) return pn.getName(); + if (node instanceof MethodNode mn) return mn.getName(); + return ""; + } + public static boolean shouldSkipOnDescriptorUndefinedAware(boolean checkReturn, Map genericsSpec, MethodNode mNode, List excludeTypes, List includeTypes) { String descriptor = mNode.getTypeDescriptor(); diff --git a/src/main/java/org/codehaus/groovy/transform/BuilderASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/BuilderASTTransformation.java index d5337d643be..03799ec8854 100644 --- a/src/main/java/org/codehaus/groovy/transform/BuilderASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/BuilderASTTransformation.java @@ -122,7 +122,7 @@ protected static List getPropertyInfoFromClassNode(ClassNode cNode protected static List getPropertyInfoFromClassNode(ClassNode cNode, List includes, List excludes, boolean allNames) { List props = new ArrayList<>(); for (FieldNode fNode : getInstancePropertyFields(cNode)) { - if (shouldSkip(fNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkip(fNode, excludes, includes, allNames)) continue; props.add(new PropertyInfo(fNode.getName(), fNode.getType())); } return props; @@ -223,12 +223,12 @@ protected List getPropertyInfoFromClassNode(BuilderASTTransformati List props = new ArrayList<>(); List seen = new ArrayList<>(); for (PropertyNode pNode : BeanUtils.getAllProperties(cNode, false, false, allProperties)) { - if (shouldSkip(pNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkip(pNode, excludes, includes, allNames)) continue; props.add(new PropertyInfo(pNode.getName(), pNode.getType())); seen.add(pNode.getName()); } for (FieldNode fNode : getFields(transform, anno, cNode)) { - if (seen.contains(fNode.getName()) || shouldSkip(fNode.getName(), excludes, includes, allNames)) continue; + if (seen.contains(fNode.getName()) || shouldSkip(fNode, excludes, includes, allNames)) continue; props.add(new PropertyInfo(fNode.getName(), fNode.getType())); } return props; diff --git a/src/main/java/org/codehaus/groovy/transform/DelegateASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/DelegateASTTransformation.java index 758b74c22fa..bfdbf209932 100644 --- a/src/main/java/org/codehaus/groovy/transform/DelegateASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/DelegateASTTransformation.java @@ -269,7 +269,7 @@ private static Collection filterMethods(final Collection methods.removeIf(candidate -> { if (!candidate.isPublic() || candidate.isStatic() || (candidate.getModifiers () & ACC_SYNTHETIC) != 0) return true; - if (shouldSkip(candidate.getName(), delegate.excludes, delegate.includes, allNames)) return true; + if (shouldSkip(candidate, delegate.excludes, delegate.includes, allNames)) return true; if (!includeDeprecated && !candidate.getAnnotations(DEPRECATED_TYPE).isEmpty()) return true; @@ -320,7 +320,7 @@ private static void addSetterIfNeeded(final DelegateDescription delegate, final String setterName = getSetterName(name); if (!prop.isFinal() && delegate.owner.getSetterMethod(setterName) == null && delegate.owner.getProperty(name) == null - && !shouldSkipPropertyMethod(name, setterName, delegate.excludes, delegate.includes, allNames)) { + && !shouldSkipPropertyMethod(prop, setterName, delegate.excludes, delegate.includes, allNames)) { addGeneratedMethod( delegate.owner, setterName, @@ -352,7 +352,7 @@ private static void addGetterIfNeeded(final DelegateDescription delegate, final extractAccessorInfo(delegate.owner, name, ownerWillHaveGetAccessor, ownerWillHaveIsAccessor); if (willHaveGetAccessor && !ownerWillHaveGetAccessor.get() - && !shouldSkipPropertyMethod(name, getterName, delegate.excludes, delegate.includes, allNames)) { + && !shouldSkipPropertyMethod(prop, getterName, delegate.excludes, delegate.includes, allNames)) { addGeneratedMethod( delegate.owner, getterName, @@ -365,7 +365,7 @@ private static void addGetterIfNeeded(final DelegateDescription delegate, final } if (willHaveIsAccessor && !ownerWillHaveIsAccessor.get() - && !shouldSkipPropertyMethod(name, getterName, delegate.excludes, delegate.includes, allNames)) { + && !shouldSkipPropertyMethod(prop, isserName, delegate.excludes, delegate.includes, allNames)) { addGeneratedMethod( delegate.owner, isserName, @@ -386,8 +386,9 @@ private static void extractAccessorInfo(final ClassNode owner, final String name willHaveIsAccessor.set(hasIsAccessor || (prop != null && !hasGetAccessor && isPrimitiveBoolean(prop.getOriginType()))); } - private static boolean shouldSkipPropertyMethod(final String propertyName, final String methodName, final List excludes, final List includes, final boolean allNames) { - return ((!allNames && deemedInternalName(propertyName)) + private static boolean shouldSkipPropertyMethod(final PropertyNode prop, final String methodName, final List excludes, final List includes, final boolean allNames) { + String propertyName = prop.getName(); + return ((!allNames && deemedInternal(prop)) || excludes != null && (excludes.contains(propertyName) || excludes.contains(methodName)) || (includes != null && !includes.isEmpty() && !includes.contains(propertyName) && !includes.contains(methodName))); } diff --git a/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java index b521d9cfa6a..adb0221b850 100644 --- a/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/EqualsAndHashCodeASTTransformation.java @@ -88,6 +88,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafe; import static org.codehaus.groovy.ast.tools.GenericsUtils.nonGeneric; +import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsInternal; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC; @@ -178,6 +179,7 @@ public static void createHashCode(ClassNode cNode, boolean cacheResult, boolean // TODO use pList and fList if (cacheResult) { final FieldNode hashField = cNode.addField("$hash$code", ACC_PRIVATE | ACC_SYNTHETIC, ClassHelper.int_TYPE, null); + markAsInternal(hashField); final Expression hash = varX(hashField); body.addStatement(ifS( isZeroX(hash), @@ -217,7 +219,7 @@ private static Statement calculateHashStatementsDefault(ClassNode cNode, Express body.addStatement(declS(result, callX(HASHUTIL_TYPE, "initHash"))); for (PropertyNode pNode : pList) { - if (shouldSkipUndefinedAware(pNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(pNode, excludes, includes, allNames)) continue; // _result = HashCodeHelper.updateHash(_result, getProperty()) // plus self-reference checking Expression prop = useGetter ? getterThisX(cNode, pNode) : propX(varX("this"), pNode.getName()); final Expression current = callX(HASHUTIL_TYPE, UPDATE_HASH, args(result, prop)); @@ -227,7 +229,7 @@ private static Statement calculateHashStatementsDefault(ClassNode cNode, Express } for (FieldNode fNode : fList) { - if (shouldSkipUndefinedAware(fNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(fNode, excludes, includes, allNames)) continue; // _result = HashCodeHelper.updateHash(_result, field) // plus self-reference checking final Expression fieldExpr = varX(fNode); final Expression current = callX(HASHUTIL_TYPE, UPDATE_HASH, args(result, fieldExpr)); @@ -259,7 +261,7 @@ private static Statement calculateHashStatementsPOJO(ClassNode cNode, Expression final BlockStatement body = new BlockStatement(); final ArgumentListExpression args = new ArgumentListExpression(); for (PropertyNode pNode : pList) { - if (shouldSkipUndefinedAware(pNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(pNode, excludes, includes, allNames)) continue; if (useGetter) { args.addExpression(getterThisX(cNode, pNode)); } else { @@ -267,7 +269,7 @@ private static Statement calculateHashStatementsPOJO(ClassNode cNode, Expression } } for (FieldNode fNode : fList) { - if (shouldSkipUndefinedAware(fNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(fNode, excludes, includes, allNames)) continue; args.addExpression(varX(fNode)); } if (callSuper) { @@ -357,7 +359,7 @@ public static void createEquals(ClassNode cNode, boolean includeFields, boolean final Set names = new HashSet<>(); final List pList = getAllProperties(names, cNode, true, includeFields, allProperties, false, false, false); for (PropertyNode pNode : pList) { - if (shouldSkipUndefinedAware(pNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(pNode, excludes, includes, allNames)) continue; boolean canBeSelf = StaticTypeCheckingSupport.implementsInterfaceOrIsSubclassOf( pNode.getOriginType(), cNode ); @@ -385,7 +387,7 @@ public static void createEquals(ClassNode cNode, boolean includeFields, boolean fList.addAll(getInstanceNonPropertyFields(cNode)); } for (FieldNode fNode : fList) { - if (shouldSkipUndefinedAware(fNode.getName(), excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(fNode, excludes, includes, allNames)) continue; Expression fieldsEqual = pojo ? callX(OBJECTS_TYPE, EQUALS, args(varX(fNode), propX(otherTyped, fNode.getName()))) : hasEqualFieldX(fNode, otherTyped); diff --git a/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java index ceb6a11b2b9..53502b75f4b 100644 --- a/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/ImmutableASTTransformation.java @@ -24,6 +24,7 @@ import groovy.transform.CompilationUnitAware; import groovy.transform.ImmutableBase; import groovy.transform.options.PropertyHandler; +import org.apache.groovy.ast.tools.AnnotatedNodeUtils; import org.apache.groovy.ast.tools.ImmutablePropertyUtils; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotatedNode; @@ -185,8 +186,7 @@ static boolean isSpecialNamedArgCase(final List list, final boolea private static void ensureNotPublic(final AbstractASTTransformation xform, final String cNode, final FieldNode fNode) { String fName = fNode.getName(); - // TODO: Do we need to lock down things like: $ownClass? - if (fNode.isPublic() && !fName.contains("$") && !(fNode.isStatic() && fNode.isFinal())) { + if (fNode.isPublic() && !AnnotatedNodeUtils.deemedInternal(fNode) && !(fNode.isStatic() && fNode.isFinal())) { xform.addError("Public field '" + fName + "' not allowed for " + MY_TYPE_NAME + " class '" + cNode + "'.", fNode); } } diff --git a/src/main/java/org/codehaus/groovy/transform/LazyASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/LazyASTTransformation.java index acaf9671015..6d8e354c8e0 100644 --- a/src/main/java/org/codehaus/groovy/transform/LazyASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/LazyASTTransformation.java @@ -18,6 +18,7 @@ */ package org.codehaus.groovy.transform; +import org.apache.groovy.ast.tools.AnnotatedNodeUtils; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.AnnotatedNode; import org.codehaus.groovy.ast.AnnotationNode; @@ -95,6 +96,7 @@ static void visitField(ErrorCollecting xform, AnnotationNode node, FieldNode fie String backingFieldName = "$" + fieldNode.getName(); fieldNode.rename(backingFieldName); fieldNode.setModifiers(ACC_PRIVATE | ACC_SYNTHETIC | (fieldNode.getModifiers() & (~(ACC_PUBLIC | ACC_PROTECTED)))); + AnnotatedNodeUtils.markAsInternal(fieldNode); PropertyNode pNode = fieldNode.getDeclaringClass().getProperty(backingFieldName); if (pNode != null) { fieldNode.getDeclaringClass().getProperties().remove(pNode); diff --git a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java index c07bdf27aa5..23a51241d05 100644 --- a/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/MapConstructorASTTransformation.java @@ -214,7 +214,7 @@ protected SourceUnit getSourceUnit() { private static void createInitializers(final AbstractASTTransformation xform, final AnnotationNode aNode, final ClassNode cNode, final PropertyHandler handler, final boolean allNames, final List excludes, final List includes, final List list, final Parameter map, final BlockStatement block) { for (PropertyNode pNode : list) { String name = pNode.getName(); - if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(pNode, excludes, includes, allNames)) continue; Statement propInit = handler.createPropInit(xform, aNode, cNode, pNode, map); if (propInit != null) { block.addStatement(propInit); diff --git a/src/main/java/org/codehaus/groovy/transform/ReadWriteLockASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/ReadWriteLockASTTransformation.java index 4449670c61e..3e3f7494755 100644 --- a/src/main/java/org/codehaus/groovy/transform/ReadWriteLockASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/ReadWriteLockASTTransformation.java @@ -36,6 +36,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import static org.codehaus.groovy.ast.ClassHelper.make; +import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsInternal; import static org.codehaus.groovy.ast.tools.GeneralUtils.block; import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; @@ -118,6 +119,7 @@ private FieldNode determineLock(String value, ClassNode targetClass, boolean isS if (field == null) { int visibility = ACC_PRIVATE | ACC_STATIC | ACC_FINAL; field = targetClass.addField(DEFAULT_STATIC_LOCKNAME, visibility, LOCK_TYPE, createLockObject()); + markAsInternal(field); } else if (!field.isStatic()) { addError("Error during " + myTypeName + " processing: " + DEFAULT_STATIC_LOCKNAME + " field must be static", field); return null; @@ -128,6 +130,7 @@ private FieldNode determineLock(String value, ClassNode targetClass, boolean isS if (field == null) { int visibility = ACC_PRIVATE | ACC_FINAL; field = targetClass.addField(DEFAULT_INSTANCE_LOCKNAME, visibility, LOCK_TYPE, createLockObject()); + markAsInternal(field); } else if (field.isStatic()) { addError("Error during " + myTypeName + " processing: " + DEFAULT_INSTANCE_LOCKNAME + " field must not be static", field); return null; diff --git a/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java index cff3909f6e9..5a5139c8cca 100644 --- a/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/ToStringASTTransformation.java @@ -244,7 +244,7 @@ private static Expression calculateToStringStatements(ClassNode cNode, boolean i for (PropertyNode pNode : list) { String name = pNode.getName(); - if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(pNode, excludes, includes, allNames)) continue; FieldNode fNode = pNode.getField(); if (!cNode.hasProperty(name) && fNode.getDeclaringClass() != null) { // it's really just a field diff --git a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java index b493adc5ae0..3d56d902b72 100644 --- a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java +++ b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java @@ -225,7 +225,7 @@ private static void createConstructor(final AbstractASTTransformation xform, fin for (PropertyNode pNode : superList) { String name = pNode.getName(); FieldNode fNode = pNode.getField(); - if (!shouldSkipUndefinedAware(name, excludes, includes, allNames)) { + if (!shouldSkipUndefinedAware(pNode, excludes, includes, allNames)) { params.add(createParam(fNode, name, defaultsMode, xform, makeImmutable)); if (callSuper) { superParams.add(varX(name)); @@ -245,7 +245,7 @@ private static void createConstructor(final AbstractASTTransformation xform, fin } for (PropertyNode pNode : list) { String name = pNode.getName(); - if (shouldSkipUndefinedAware(name, excludes, includes, allNames)) continue; + if (shouldSkipUndefinedAware(pNode, excludes, includes, allNames)) continue; Statement propInit = handler.createPropInit(xform, anno, cNode, pNode, null); if (propInit != null) { body.addStatement(propInit); diff --git a/src/test/groovy/groovy/lang/MetaClassInternalPropertyTest.groovy b/src/test/groovy/groovy/lang/MetaClassInternalPropertyTest.groovy new file mode 100644 index 00000000000..009feb283cf --- /dev/null +++ b/src/test/groovy/groovy/lang/MetaClassInternalPropertyTest.groovy @@ -0,0 +1,103 @@ +/* + * 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.lang + +import groovy.json.JsonOutput +import groovy.transform.Internal +import groovy.transform.ToString +import groovy.transform.EqualsAndHashCode +import org.junit.jupiter.api.Test + +class MetaClassInternalPropertyTest { + + static class WithInternalField { + String name + @Internal String secret + } + + static class WithInternalGetter { + String name + private String hidden = 'secret' + + @Internal + String getHidden() { hidden } + } + + @Test + void internalFieldExcludedFromProperties() { + def props = new WithInternalField(name: 'test', secret: 'x').metaClass.properties*.name + assert 'name' in props + assert !('secret' in props) + } + + @Test + void internalGetterExcludedFromProperties() { + def props = new WithInternalGetter(name: 'test').metaClass.properties*.name + assert 'name' in props + assert !('hidden' in props) + } + + @Test + void internalFieldExcludedFromJsonOutput() { + def json = JsonOutput.toJson(new WithInternalField(name: 'test', secret: 'x')) + assert json.contains('"name"') + assert !json.contains('"secret"') + } + + @ToString + static class WithInternalForToString { + String name + @Internal String secret + } + + @Test + void internalFieldExcludedFromToString() { + def obj = new WithInternalForToString(name: 'test', secret: 'x') + def str = obj.toString() + assert str.contains('test') + assert !str.contains('x') + } + + @EqualsAndHashCode + static class WithInternalForEquals { + String name + @Internal String tag + } + + @Test + void internalFieldExcludedFromEqualsAndHashCode() { + def a = new WithInternalForEquals(name: 'test', tag: 'one') + def b = new WithInternalForEquals(name: 'test', tag: 'two') + // tag is internal so should not affect equality + assert a == b + assert a.hashCode() == b.hashCode() + } + + @Test + void regularPropertiesStillIncluded() { + def obj = new WithInternalField(name: 'test', secret: 'x') + // direct access still works + assert obj.name == 'test' + assert obj.secret == 'x' + // only metaClass.properties filters it + def propNames = obj.metaClass.properties*.name + assert 'name' in propNames + assert 'class' in propNames + } +}