You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by em...@apache.org on 2023/06/08 16:18:21 UTC

[groovy] branch master updated: GROOVY-11088: stubgen: support `@PackageScope` for classes and fields

This is an automated email from the ASF dual-hosted git repository.

emilles 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 9e23c5f21f GROOVY-11088: stubgen: support `@PackageScope` for classes and fields
9e23c5f21f is described below

commit 9e23c5f21fb293707ae4d1d779d6606dc4b1be4c
Author: Eric Milles <er...@thomsonreuters.com>
AuthorDate: Thu Jun 8 10:31:08 2023 -0500

    GROOVY-11088: stubgen: support `@PackageScope` for classes and fields
---
 .../groovy/tools/javac/JavaStubGenerator.java      | 274 +++++++++++++--------
 .../groovy/tools/stubgenerator/Groovy11088.groovy  |  47 ++++
 .../tools/stubgenerator/Groovy11088pt2.groovy      |  50 ++++
 .../tools/stubgenerator/Groovy11088pt3.groovy      |  50 ++++
 4 files changed, 314 insertions(+), 107 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java b/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java
index a28cb9e58a..2db373365c 100644
--- a/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java
+++ b/src/main/java/org/codehaus/groovy/tools/javac/JavaStubGenerator.java
@@ -18,11 +18,14 @@
  */
 package org.codehaus.groovy.tools.javac;
 
+import groovy.transform.PackageScope;
+import groovy.transform.PackageScopeTarget;
 import org.apache.groovy.ast.tools.ExpressionUtils;
 import org.apache.groovy.io.StringBuilderWriter;
 import org.codehaus.groovy.ast.AnnotatedNode;
 import org.codehaus.groovy.ast.AnnotationNode;
 import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.CodeVisitorSupport;
 import org.codehaus.groovy.ast.ConstructorNode;
 import org.codehaus.groovy.ast.DynamicVariable;
 import org.codehaus.groovy.ast.FieldNode;
@@ -102,6 +105,7 @@ import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
 import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
 import static org.codehaus.groovy.ast.ClassHelper.isStaticConstantInitializerType;
 import static org.codehaus.groovy.ast.ClassHelper.isStringType;
+import static org.codehaus.groovy.ast.ClassHelper.makeCached;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.defaultValueX;
 import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpec;
 import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecRecurse;
@@ -112,10 +116,11 @@ import static org.codehaus.groovy.ast.tools.WideningCategories.isLongCategory;
 public class JavaStubGenerator {
 
     private final String encoding;
-    private final boolean requireSuperResolved;
     private final File outputPath;
+    private final boolean requireSuperResolved;
     private final List<ConstructorNode> constructors = new ArrayList<>();
     private final Map<String, MethodNode> propertyMethods = new LinkedHashMap<>();
+    private final static ClassNode PACKAGE_SCOPE_TYPE = makeCached(PackageScope.class);
 
     private ModuleNode currentModule;
 
@@ -205,11 +210,11 @@ public class JavaStubGenerator {
         return writer.toString();
     }
 
-    private static Iterable<ClassNode> findTraits(ClassNode node) {
+    private static Iterable<ClassNode> findTraits(ClassNode classNode) {
         Set<ClassNode> traits = new LinkedHashSet<>();
 
         LinkedList<ClassNode> todo = new LinkedList<>();
-        Collections.addAll(todo, node.getInterfaces());
+        Collections.addAll(todo, classNode.getInterfaces());
         while (!todo.isEmpty()) {
             ClassNode next = todo.removeLast();
             if (Traits.isTrait(next)) traits.add(next);
@@ -328,13 +333,17 @@ public class JavaStubGenerator {
                 classNode.getDeclaredConstructors().clear();
             }
 
-            boolean isInterface = isInterfaceOrTrait(classNode);
             boolean isEnum = classNode.isEnum();
+            boolean isInterface = !isEnum && isInterfaceOrTrait(classNode);
             boolean isAnnotationDefinition = classNode.isAnnotationDefinition();
             printAnnotations(out, classNode);
-            printModifiers(out, classNode.getModifiers()
-                    & ~(isInterface ? Opcodes.ACC_ABSTRACT : 0)
-                    & ~(isEnum ? Opcodes.ACC_FINAL | Opcodes.ACC_ABSTRACT : 0));
+
+            int flags = classNode.getModifiers();
+            if (isEnum) flags &= ~Opcodes.ACC_FINAL;
+            if (isEnum || isInterface) flags &= ~Opcodes.ACC_ABSTRACT;
+            if (classNode.isSyntheticPublic() && hasPackageScopeXform(classNode,
+                        PackageScopeTarget.CLASS)) flags &= ~Opcodes.ACC_PUBLIC;
+            printModifiers(out, flags);
 
             if (isInterface) {
                 if (isAnnotationDefinition) {
@@ -377,7 +386,7 @@ public class JavaStubGenerator {
             }
             out.println(" {");
 
-            printFields(out, classNode);
+            printFields(out, classNode, isInterface);
             printMethods(out, classNode, isEnum);
 
             for (Iterator<InnerClassNode> inner = classNode.getInnerClasses(); inner.hasNext(); ) {
@@ -394,99 +403,31 @@ public class JavaStubGenerator {
         }
     }
 
-    private void printMethods(PrintWriter out, ClassNode classNode, boolean isEnum) {
-        if (!isEnum) printConstructors(out, classNode);
-
-        List<MethodNode> methods = new ArrayList<>(propertyMethods.values());
-        methods.addAll(classNode.getMethods());
-        for (MethodNode method : methods) {
-            if (isEnum && method.isSynthetic()) {
-                // skip values() method and valueOf(String)
-                String name = method.getName();
-                Parameter[] params = method.getParameters();
-                if (params.length == 0 && name.equals("values")) continue;
-                if (params.length == 1
-                        && name.equals("valueOf")
-                        && isStringType(params[0].getType())) {
-                    continue;
-                }
-            }
-            printMethod(out, classNode, method);
-        }
-
-        // print the methods from traits
-        for (ClassNode trait : findTraits(classNode)) {
-            Map<String, ClassNode> generics = trait.isUsingGenerics() ? createGenericsSpec(trait) : null;
-            List<MethodNode> traitMethods = trait.getMethods();
-            for (MethodNode traitOrigMethod : traitMethods) {
-                // GROOVY-9606: replace method return type and parameter type placeholder with resolved type from trait generics
-                MethodNode traitMethod = correctToGenericsSpec(generics, traitOrigMethod);
-                MethodNode existingMethod = classNode.getMethod(traitMethod.getName(), traitMethod.getParameters());
-                if (existingMethod != null) continue;
-                for (MethodNode propertyMethod : propertyMethods.values()) {
-                    if (propertyMethod.getName().equals(traitMethod.getName())) {
-                        boolean sameParams = sameParameterTypes(propertyMethod, traitMethod);
-                        if (sameParams) {
-                            existingMethod = propertyMethod;
-                            break;
-                        }
-                    }
-                }
-                if (existingMethod == null && isConcreteTraitMethod(trait, traitMethod)) {
-                    printMethod(out, classNode, traitMethod);
-                }
-            }
-        }
-    }
-
-    private static boolean isConcreteTraitMethod(final ClassNode trait, final MethodNode traitMethod) {
-        if (!(trait.redirect() instanceof DecompiledClassNode)) {
-            return !traitMethod.isAbstract();
-        }
-        boolean isSynthetic = (traitMethod.getModifiers() & Opcodes.ACC_SYNTHETIC) != 0;
-        if (!isSynthetic && !traitMethod.getName().contains("$")) {
-            for (MethodNode helperMethod : Traits.findHelper(trait).getMethods(traitMethod.getName())) {
-                Parameter[] params = helperMethod.getParameters();
-                params = Arrays.copyOfRange(params, 1, params.length);
-                if (sameParameterTypes(params, traitMethod.getParameters())) return true;
-            }
-        }
-        return false;
-    }
-
-    private static boolean sameParameterTypes(final MethodNode firstMethod, final MethodNode secondMethod) {
-        return sameParameterTypes(firstMethod.getParameters(), secondMethod.getParameters());
-    }
-
-    private static boolean sameParameterTypes(final Parameter[] firstParams, final Parameter[] secondParams) {
-        return org.codehaus.groovy.ast.tools.ParameterUtils.parametersEqual(firstParams, secondParams);
-    }
-
-    private void printConstructors(final PrintWriter out, final ClassNode classNode) {
-        List<ConstructorNode> constructors = new ArrayList<>(this.constructors);
-        constructors.addAll(classNode.getDeclaredConstructors());
-        for (ConstructorNode constructor : constructors) {
-            printConstructor(out, classNode, constructor);
-        }
-    }
-
-    private void printFields(final PrintWriter out, final ClassNode classNode) {
+    private void printFields(PrintWriter out, ClassNode classNode, boolean ifaceOrTrait) {
         List<FieldNode> fields = classNode.getFields();
         if (!fields.isEmpty()) {
-            List<FieldNode> enumFields = new LinkedList<>();
-            List<FieldNode> normalFields = new LinkedList<>();
+            List<FieldNode> enumFields = new ArrayList<>();
+            List<FieldNode> normalFields = new ArrayList<>();
             for (FieldNode field : fields) {
-                if (field.isEnum()) {
+                int flags = field.getModifiers();
+                if (hasPackageScopeXform(field, PackageScopeTarget.FIELDS)){
+                    flags &= ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC);
+                    List<AnnotationNode> annotations = field.getAnnotations();
+                    field = new FieldNode(field.getName(), flags, field.getType(), field.getOwner(), field.getInitialExpression());
+                    field.setDeclaringClass(classNode);
+                    field.addAnnotations(annotations);
+                }
+
+                if ((flags & Opcodes.ACC_ENUM) != 0) {
                     enumFields.add(field);
-                } else if (!field.isPrivate() && (field.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) {
+                } else if ((flags & (Opcodes.ACC_PRIVATE | Opcodes.ACC_SYNTHETIC)) == 0) {
                     normalFields.add(field);
                 }
             }
-            boolean interfaceOrTrait = isInterfaceOrTrait(classNode);
 
             printEnumFields(out, enumFields);
             for (FieldNode normalField : normalFields) {
-                printField(out, normalField, interfaceOrTrait);
+                printField(out, normalField, ifaceOrTrait);
             }
         }
     }
@@ -504,9 +445,9 @@ public class JavaStubGenerator {
         out.println(';');
     }
 
-    private void printField(final PrintWriter out, final FieldNode field, final boolean fromFaceOrTrait) {
+    private void printField(final PrintWriter out, final FieldNode field, final boolean ifaceOrTrait) {
         printAnnotations(out, field);
-        if (!fromFaceOrTrait) {
+        if (!ifaceOrTrait) {
             printModifiers(out, field.getModifiers());
         }
         ClassNode type = field.getType();
@@ -514,7 +455,7 @@ public class JavaStubGenerator {
         out.print(' ');
         out.print(field.getName());
 
-        if (fromFaceOrTrait || field.isFinal()) {
+        if (ifaceOrTrait || field.isFinal()) {
             out.print(" = ");
             if (field.isStatic()) {
                 Expression value = ExpressionUtils.transformInlineConstants(field.getInitialValueExpression(), type);
@@ -556,6 +497,82 @@ public class JavaStubGenerator {
         out.println(';');
     }
 
+    private void printMethods(final PrintWriter out, final ClassNode classNode, final boolean isEnum) {
+        if (!isEnum) printConstructors(out, classNode);
+
+        List<MethodNode> methods = new ArrayList<>(propertyMethods.values());
+        methods.addAll(classNode.getMethods());
+        for (MethodNode method : methods) {
+            if (isEnum && method.isSynthetic()) {
+                // skip values() method and valueOf(String)
+                String name = method.getName();
+                Parameter[] params = method.getParameters();
+                if (params.length == 0 && name.equals("values")) continue;
+                if (params.length == 1
+                        && name.equals("valueOf")
+                        && isStringType(params[0].getType())) {
+                    continue;
+                }
+            }
+            printMethod(out, classNode, method);
+        }
+
+        // print the methods from traits
+        for (ClassNode trait : findTraits(classNode)) {
+            Map<String, ClassNode> generics = trait.isUsingGenerics() ? createGenericsSpec(trait) : null;
+            List<MethodNode> traitMethods = trait.getMethods();
+            for (MethodNode traitOrigMethod : traitMethods) {
+                // GROOVY-9606: replace method return type and parameter type placeholder with resolved type from trait generics
+                MethodNode traitMethod = correctToGenericsSpec(generics, traitOrigMethod);
+                MethodNode existingMethod = classNode.getMethod(traitMethod.getName(), traitMethod.getParameters());
+                if (existingMethod != null) continue;
+                for (MethodNode propertyMethod : propertyMethods.values()) {
+                    if (propertyMethod.getName().equals(traitMethod.getName())) {
+                        boolean sameParams = sameParameterTypes(propertyMethod, traitMethod);
+                        if (sameParams) {
+                            existingMethod = propertyMethod;
+                            break;
+                        }
+                    }
+                }
+                if (existingMethod == null && isConcreteTraitMethod(trait, traitMethod)) {
+                    printMethod(out, classNode, traitMethod);
+                }
+            }
+        }
+    }
+
+    private static boolean isConcreteTraitMethod(final ClassNode trait, final MethodNode traitMethod) {
+        if (!(trait.redirect() instanceof DecompiledClassNode)) {
+            return !traitMethod.isAbstract();
+        }
+        boolean isSynthetic = (traitMethod.getModifiers() & Opcodes.ACC_SYNTHETIC) != 0;
+        if (!isSynthetic && !traitMethod.getName().contains("$")) {
+            for (MethodNode helperMethod : Traits.findHelper(trait).getMethods(traitMethod.getName())) {
+                Parameter[] params = helperMethod.getParameters();
+                params = Arrays.copyOfRange(params, 1, params.length);
+                if (sameParameterTypes(params, traitMethod.getParameters())) return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean sameParameterTypes(final MethodNode firstMethod, final MethodNode secondMethod) {
+        return sameParameterTypes(firstMethod.getParameters(), secondMethod.getParameters());
+    }
+
+    private static boolean sameParameterTypes(final Parameter[] firstParams, final Parameter[] secondParams) {
+        return org.codehaus.groovy.ast.tools.ParameterUtils.parametersEqual(firstParams, secondParams);
+    }
+
+    private void printConstructors(final PrintWriter out, final ClassNode classNode) {
+        List<ConstructorNode> constructors = new ArrayList<>(this.constructors);
+        constructors.addAll(classNode.getDeclaredConstructors());
+        for (ConstructorNode constructor : constructors) {
+            printConstructor(out, classNode, constructor);
+        }
+    }
+
     private void printConstructor(PrintWriter out, ClassNode clazz, ConstructorNode constructorNode) {
         printAnnotations(out, constructorNode);
         // printModifiers(out, constructorNode.getModifiers());
@@ -962,7 +979,8 @@ public class JavaStubGenerator {
 
     private void printAnnotations(final PrintWriter out, final AnnotatedNode annotated) {
         for (AnnotationNode annotation : annotated.getAnnotations()) {
-            printAnnotation(out, annotation);
+            if (!annotation.getClassNode().equals(PACKAGE_SCOPE_TYPE))
+                printAnnotation(out, annotation);
         }
     }
 
@@ -1060,21 +1078,11 @@ public class JavaStubGenerator {
         out.println();
     }
 
-    public void clean() {
-        Stream<JavaFileObject> javaFileObjectStream =
-                javaStubCompilationUnitSet.size() < 2
-                        ? javaStubCompilationUnitSet.stream()
-                        : javaStubCompilationUnitSet.parallelStream();
-
-        javaFileObjectStream.forEach(FileObject::delete);
-        javaStubCompilationUnitSet.clear();
-    }
-
-    private File createJavaStubFile(String path) {
+    private File createJavaStubFile(final String path) {
         return new File(outputPath, path + ".java");
     }
 
-    private static String escapeSpecialChars(String value) {
+    private static String escapeSpecialChars(final String value) {
         return FormatHelper.escapeBackslashes(value).replace("\"", "\\\"");
     }
 
@@ -1082,9 +1090,61 @@ public class JavaStubGenerator {
         return cn.isInterface() || Traits.isTrait(cn);
     }
 
+    private boolean hasPackageScopeXform(final AnnotatedNode node, final PackageScopeTarget type) {
+        boolean member = (!(node instanceof ClassNode) && type != PackageScopeTarget.CLASS);
+        for (AnnotationNode anno : node.getAnnotations()) {
+            if (anno.getClassNode().equals(PACKAGE_SCOPE_TYPE)) {
+                Expression expr = anno.getMember("value");
+                if (expr == null) {
+                    // if empty @PackageScope, node type and target type must be in alignment
+                    return member || (node instanceof ClassNode && type == PackageScopeTarget.CLASS);
+                }
+
+                final boolean[] val = new boolean[1];
+                expr.visit(new CodeVisitorSupport() {
+                    @Override
+                    public void visitPropertyExpression(final PropertyExpression property) {
+                        if (property.getObjectExpression().getText().equals("groovy.transform.PackageScopeTarget")
+                                && property.getPropertyAsString().equals(type.name())) {
+                            val[0] = true;
+                        }
+                    }
+                    @Override
+                    public void visitVariableExpression(final VariableExpression variable) {
+                        if (variable.getName().equals(type.name())) {
+                            ImportNode imp = currentModule.getStaticImports().get(type.name());
+                            if (imp != null && imp.getType().getName().equals("groovy.transform.PackageScopeTarget")) {
+                                val[0] = true;
+                            } else if (imp == null && currentModule.getStaticStarImports().get("groovy.transform.PackageScopeTarget") != null) {
+                                val[0] = true;
+                            }
+                        }
+                    }
+                });
+                return val[0];
+            }
+        }
+        if (member) { // check for @PackageScope(XXX) on class
+            return hasPackageScopeXform(node.getDeclaringClass(), type);
+        }
+        return false;
+    }
+
+    //--------------------------------------------------------------------------
+
     private final Set<JavaFileObject> javaStubCompilationUnitSet = new HashSet<>();
 
     public Set<JavaFileObject> getJavaStubCompilationUnitSet() {
         return javaStubCompilationUnitSet;
     }
+
+    public void clean() {
+        Stream<JavaFileObject> javaFileObjectStream =
+                javaStubCompilationUnitSet.size() < 2
+                        ? javaStubCompilationUnitSet.stream()
+                        : javaStubCompilationUnitSet.parallelStream();
+
+        javaFileObjectStream.forEach(FileObject::delete);
+        javaStubCompilationUnitSet.clear();
+    }
 }
diff --git a/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088.groovy b/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088.groovy
new file mode 100644
index 0000000000..12dc06618c
--- /dev/null
+++ b/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088.groovy
@@ -0,0 +1,47 @@
+/*
+ *  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 org.codehaus.groovy.tools.stubgenerator
+
+final class Groovy11088 extends StringSourcesStubTestCase {
+
+    @Override
+    Map<String, String> provideSources() {
+        [
+            'C.groovy': '''import groovy.transform.*
+                class C {
+                    @Deprecated @PackageScope
+                    final static String VALUE = "x"
+                }
+            ''',
+            'Main.java': '''
+                public class Main {
+                    public static void main(String[] args) {
+                        System.err.println(C.VALUE);
+                    }
+                }
+            ''',
+        ]
+    }
+
+    @Override
+    void verifyStubs() {
+        String stub = stubJavaSourceFor('C')
+        assert stub.contains('@java.lang.Deprecated() static final java.lang.String VALUE = "x";')
+    }
+}
diff --git a/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088pt2.groovy b/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088pt2.groovy
new file mode 100644
index 0000000000..cbcb5deb74
--- /dev/null
+++ b/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088pt2.groovy
@@ -0,0 +1,50 @@
+/*
+ *  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 org.codehaus.groovy.tools.stubgenerator
+
+final class Groovy11088pt2 extends StringSourcesStubTestCase {
+
+    @Override
+    Map<String, String> provideSources() {
+        [
+            'C.groovy': '''import groovy.transform.*
+                @PackageScope([
+                    PackageScopeTarget.FIELDS,
+                    PackageScopeTarget.METHODS
+                ])
+                class C {
+                    @Deprecated static String VALUE = "x"
+                }
+            ''',
+            'Main.java': '''
+                public class Main {
+                    public static void main(String[] args) {
+                        System.err.println(C.VALUE);
+                    }
+                }
+            ''',
+        ]
+    }
+
+    @Override
+    void verifyStubs() {
+        String stub = stubJavaSourceFor('C')
+        assert stub.contains('@java.lang.Deprecated() static java.lang.String VALUE;')
+    }
+}
diff --git a/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088pt3.groovy b/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088pt3.groovy
new file mode 100644
index 0000000000..62a6a27bf5
--- /dev/null
+++ b/src/test/org/codehaus/groovy/tools/stubgenerator/Groovy11088pt3.groovy
@@ -0,0 +1,50 @@
+/*
+ *  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 org.codehaus.groovy.tools.stubgenerator
+
+final class Groovy11088pt3 extends StringSourcesStubTestCase {
+
+    @Override
+    Map<String, String> provideSources() {
+        [
+            'C.groovy': '''import groovy.transform.*
+                @Deprecated @PackageScope
+                class C {
+                    @Deprecated @PackageScope
+                    class D {
+                    }
+                }
+            ''',
+            'Main.java': '''
+                public class Main {
+                    public static void main(String[] args) {
+                        new C();
+                    }
+                }
+            ''',
+        ]
+    }
+
+    @Override
+    void verifyStubs() {
+        String stub = stubJavaSourceFor('C')
+        assert stub.contains('@java.lang.Deprecated() class C')
+        assert stub.contains('@java.lang.Deprecated() class D')
+    }
+}