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 2022/10/01 17:25:29 UTC

[groovy] branch master updated: GROOVY-10770: STC: extension composition

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 168ac8f269 GROOVY-10770: STC: extension composition
168ac8f269 is described below

commit 168ac8f269620f3d2bef85dbabd364823f8f2dec
Author: Eric Milles <er...@thomsonreuters.com>
AuthorDate: Sat Oct 1 11:46:52 2022 -0500

    GROOVY-10770: STC: extension composition
---
 build.gradle                                       |   3 +-
 .../org/codehaus/groovy/ast/AnnotatedNode.java     |   6 +-
 .../stc/AbstractTypeCheckingExtension.java         |  39 +++---
 .../stc/DefaultTypeCheckingExtension.java          |  20 +--
 .../stc/GroovyTypeCheckingExtensionSupport.java    |  37 +++--
 .../transform/stc/StaticTypeCheckingVisitor.java   |  74 ++++++++--
 src/test/groovy/bugs/Groovy10770.groovy            | 152 +++++++++++++++++++++
 7 files changed, 275 insertions(+), 56 deletions(-)

diff --git a/build.gradle b/build.gradle
index 85643bd41c..6480853a76 100644
--- a/build.gradle
+++ b/build.gradle
@@ -68,7 +68,8 @@ groovyCore {
     bridgedClasses 'org.codehaus.groovy.runtime.DefaultGroovyMethods',
             'org.codehaus.groovy.runtime.StringGroovyMethods',
             'org.codehaus.groovy.classgen.Verifier',
-            'org.codehaus.groovy.ast.tools.GeneralUtils'
+            'org.codehaus.groovy.ast.tools.GeneralUtils',
+            'org.codehaus.groovy.transform.stc.DefaultTypeCheckingExtension'
 
     excludeFromJavadoc '**/GroovyRecognizer.java', '*.g4' // generated file
 }
diff --git a/src/main/java/org/codehaus/groovy/ast/AnnotatedNode.java b/src/main/java/org/codehaus/groovy/ast/AnnotatedNode.java
index ed3d4bdc5c..2077185edf 100644
--- a/src/main/java/org/codehaus/groovy/ast/AnnotatedNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/AnnotatedNode.java
@@ -38,13 +38,13 @@ public class AnnotatedNode extends ASTNode implements GroovydocHolder<AnnotatedN
     }
 
     public List<AnnotationNode> getAnnotations(final ClassNode type) {
-        List<AnnotationNode> ret = new ArrayList<>(annotations.size());
+        List<AnnotationNode> annotations = new ArrayList<>();
         for (AnnotationNode node : getAnnotations()) {
             if (type.equals(node.getClassNode())) {
-                ret.add(node);
+                annotations.add(node);
             }
         }
-        return ret;
+        return annotations;
     }
 
     public AnnotationNode addAnnotation(final ClassNode type) {
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/AbstractTypeCheckingExtension.java b/src/main/java/org/codehaus/groovy/transform/stc/AbstractTypeCheckingExtension.java
index b0d4af6f65..abcfb6ed22 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/AbstractTypeCheckingExtension.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/AbstractTypeCheckingExtension.java
@@ -19,6 +19,9 @@
 package org.codehaus.groovy.transform.stc;
 
 import groovy.lang.Closure;
+import groovy.lang.DelegatesTo;
+import groovy.transform.stc.ClosureParams;
+import groovy.transform.stc.SimpleType;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.AnnotatedNode;
 import org.codehaus.groovy.ast.ClassHelper;
@@ -36,6 +39,7 @@ import org.codehaus.groovy.ast.expr.PropertyExpression;
 import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.EmptyStatement;
 import org.codehaus.groovy.classgen.asm.InvocationWriter;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 import org.objectweb.asm.Opcodes;
 
 import java.util.Collections;
@@ -45,7 +49,6 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.logging.Logger;
 
 /**
  * <p>Custom type checking extensions may extend this method in order to benefit from a lot
@@ -57,19 +60,20 @@ import java.util.logging.Logger;
  * @since 2.3.0
  */
 public class AbstractTypeCheckingExtension extends TypeCheckingExtension {
-    private static final Logger LOG = Logger.getLogger(GroovyTypeCheckingExtensionSupport.class.getName());
+
     protected final TypeCheckingContext context;
+    /** @see {@link #log(String)} */ protected boolean debug;
+    /** @see {@link #setHandled(boolean)} */ protected boolean handled;
     private final Set<MethodNode> generatedMethods = new LinkedHashSet<>();
     private final LinkedList<TypeCheckingScope> scopeData = new LinkedList<>();
-    // this boolean is used through setHandled(boolean)
-    protected boolean handled = false;
-    protected boolean debug = false;
 
     public AbstractTypeCheckingExtension(final StaticTypeCheckingVisitor typeCheckingVisitor) {
         super(typeCheckingVisitor);
         this.context = typeCheckingVisitor.typeCheckingContext;
     }
 
+    //--------------------------------------------------------------------------
+
     public void setHandled(final boolean handled) {
         this.handled = handled;
     }
@@ -246,12 +250,9 @@ public class AbstractTypeCheckingExtension extends TypeCheckingExtension {
         return argTypeMatches(argumentTypes, index, clazz);
     }
 
-    @SuppressWarnings("unchecked")
-    public <R> R withTypeChecker(Closure<R> code) {
-        Closure<R> clone = (Closure<R>) code.clone();
-        clone.setDelegate(typeCheckingVisitor);
-        clone.setResolveStrategy(Closure.DELEGATE_FIRST);
-        return clone.call();
+    public <R> R withTypeChecker(@DelegatesTo(value = StaticTypeCheckingVisitor.class, strategy = Closure.DELEGATE_FIRST)
+            @ClosureParams(value = SimpleType.class, options = "org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor") final Closure<R> code) {
+        return DefaultGroovyMethods.with(typeCheckingVisitor, code);
     }
 
     /**
@@ -283,7 +284,7 @@ public class AbstractTypeCheckingExtension extends TypeCheckingExtension {
         }
         setHandled(true);
         if (debug) {
-            LOG.info("Turning " + call.getText() + " into a dynamic method call returning " + StaticTypeCheckingSupport.prettyPrintType(returnType));
+            log("Turning " + call.getText() + " into a dynamic method call returning " + StaticTypeCheckingSupport.prettyPrintType(returnType));
         }
         return new MethodNode(call.getMethodAsString(), 0, returnType, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE);
     }
@@ -309,7 +310,7 @@ public class AbstractTypeCheckingExtension extends TypeCheckingExtension {
         storeType(pexp, returnType);
         setHandled(true);
         if (debug) {
-            LOG.info("Turning '" + pexp.getText() + "' into a dynamic property access of type " + StaticTypeCheckingSupport.prettyPrintType(returnType));
+            log("Turning '" + pexp.getText() + "' into a dynamic property access of type " + StaticTypeCheckingSupport.prettyPrintType(returnType));
         }
     }
 
@@ -334,12 +335,13 @@ public class AbstractTypeCheckingExtension extends TypeCheckingExtension {
         storeType(vexp, returnType);
         setHandled(true);
         if (debug) {
-            LOG.info("Turning '" + vexp.getText() + "' into a dynamic variable access of type " + StaticTypeCheckingSupport.prettyPrintType(returnType));
+            log("Turning '" + vexp.getText() + "' into a dynamic variable access of type " + StaticTypeCheckingSupport.prettyPrintType(returnType));
         }
     }
 
-    public void log(String message) {
-        LOG.info(message);
+    public void log(final String message) {
+        java.util.logging.Logger logger = java.util.logging.Logger.getLogger(GroovyTypeCheckingExtensionSupport.class.getName());
+        logger.info(message);
     }
 
     public BinaryExpression getEnclosingBinaryExpression() {
@@ -434,7 +436,9 @@ public class AbstractTypeCheckingExtension extends TypeCheckingExtension {
         context.pushTemporaryTypeInfo();
     }
 
-    private static class TypeCheckingScope extends LinkedHashMap<String, Object> {
+    //--------------------------------------------------------------------------
+
+    public static class TypeCheckingScope extends LinkedHashMap<String, Object> {
         private static final long serialVersionUID = 7607331333917615144L;
         private final AbstractTypeCheckingExtension.TypeCheckingScope parent;
 
@@ -445,6 +449,5 @@ public class AbstractTypeCheckingExtension extends TypeCheckingExtension {
         public AbstractTypeCheckingExtension.TypeCheckingScope getParent() {
             return parent;
         }
-
     }
 }
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/DefaultTypeCheckingExtension.java b/src/main/java/org/codehaus/groovy/transform/stc/DefaultTypeCheckingExtension.java
index 4cdaf1f40c..f5a7426c91 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/DefaultTypeCheckingExtension.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/DefaultTypeCheckingExtension.java
@@ -29,7 +29,6 @@ import org.codehaus.groovy.ast.expr.PropertyExpression;
 import org.codehaus.groovy.ast.expr.VariableExpression;
 import org.codehaus.groovy.ast.stmt.ReturnStatement;
 
-import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -47,17 +46,23 @@ import java.util.List;
  * @since 2.1.0
  */
 public class DefaultTypeCheckingExtension extends TypeCheckingExtension {
+
     protected final List<TypeCheckingExtension> handlers = new LinkedList<TypeCheckingExtension>();
 
     public DefaultTypeCheckingExtension(final StaticTypeCheckingVisitor typeCheckingVisitor) {
         super(typeCheckingVisitor);
     }
 
-    public void addHandler(TypeCheckingExtension handler) {
+    @Deprecated
+    public void addHandler$$bridge(final TypeCheckingExtension handler) {
         handlers.add(handler);
     }
 
-    public void removeHandler(TypeCheckingExtension handler) {
+    public boolean addHandler(final TypeCheckingExtension handler) {
+        return !handlers.contains(handler) ? handlers.add(handler) : false;
+    }
+
+    public void removeHandler(final TypeCheckingExtension handler) {
         handlers.remove(handler);
     }
 
@@ -181,17 +186,16 @@ public class DefaultTypeCheckingExtension extends TypeCheckingExtension {
 
     @Override
     public void setup() {
-        ArrayList<TypeCheckingExtension> copy = new ArrayList<TypeCheckingExtension>(handlers);
-        // we're using a copy here because new extensions can be added during the "setup" phase
-        for (TypeCheckingExtension handler : copy) {
+        // We're using a copy here because new extensions can be added during the setup phase!
+        for (TypeCheckingExtension handler : handlers.toArray(new TypeCheckingExtension[0])) {
             handler.setup();
         }
     }
 
     @Override
     public void finish() {
-        for (TypeCheckingExtension handler : handlers) {
+        for (TypeCheckingExtension handler : handlers.toArray(new TypeCheckingExtension[0])) {
             handler.finish();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
index 5a72984917..eb31312bfa 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/GroovyTypeCheckingExtensionSupport.java
@@ -52,6 +52,7 @@ import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * Base class for type checking extensions written in Groovy. Compared to its superclass, {@link TypeCheckingExtension},
@@ -86,6 +87,8 @@ public class GroovyTypeCheckingExtensionSupport extends AbstractTypeCheckingExte
 
     private final CompilationUnit compilationUnit;
 
+    private TypeCheckingExtension delegateExtension;
+
     /** Closures executed in event-based methods. */
     private final Map<String, List<Closure>> eventHandlers = new HashMap<>();
 
@@ -104,6 +107,21 @@ public class GroovyTypeCheckingExtensionSupport extends AbstractTypeCheckingExte
         this.compilationUnit = compilationUnit;
     }
 
+    @Override
+    public boolean equals(Object that) {
+        if (that == this) return true;
+        if (that == null || that.getClass() != this.getClass()) return false;
+        GroovyTypeCheckingExtensionSupport support = (GroovyTypeCheckingExtensionSupport) that;
+        return Objects.equals(scriptPath,support.scriptPath) && Objects.equals(compilationUnit,support.compilationUnit);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(scriptPath, compilationUnit);
+    }
+
+    //--------------------------------------------------------------------------
+
     public void setDebug(final boolean debug) {
         this.debug = debug;
     }
@@ -133,9 +151,9 @@ public class GroovyTypeCheckingExtensionSupport extends AbstractTypeCheckingExte
                 // since 2.4, we can also register precompiled type checking extensions which are not scripts
                 try {
                     Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(StaticTypeCheckingVisitor.class);
-                    TypeCheckingExtension extension = (TypeCheckingExtension) declaredConstructor.newInstance(typeCheckingVisitor);
-                    typeCheckingVisitor.addTypeCheckingExtension(extension);
-                    extension.setup();
+                    delegateExtension = (TypeCheckingExtension) declaredConstructor.newInstance(typeCheckingVisitor);
+                    typeCheckingVisitor.addTypeCheckingExtension(delegateExtension);
+                    delegateExtension.setup();
                     return;
                 } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
                     addLoadingError(config);
@@ -202,6 +220,11 @@ public class GroovyTypeCheckingExtensionSupport extends AbstractTypeCheckingExte
 
     @Override
     public void finish() {
+        if (delegateExtension != null) { typeCheckingVisitor.extension.removeHandler(delegateExtension);
+            delegateExtension.finish();
+            return;
+        }
+
         List<Closure> list = eventHandlers.get("finish");
         if (list != null) {
             for (Closure closure : list) {
@@ -392,14 +415,6 @@ public class GroovyTypeCheckingExtensionSupport extends AbstractTypeCheckingExte
         return methodList;
     }
 
-    // -------------------------------------
-    // delegate to the type checking context
-    // -------------------------------------
-
-    // --------------------------------------------
-    // end of delegate to the type checking context
-    // --------------------------------------------
-
     public abstract static class TypeCheckingDSL extends Script {
         private GroovyTypeCheckingExtensionSupport extension;
 
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
index cfe5ab5315..b60c50f5f4 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -138,6 +138,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.BiPredicate;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
@@ -395,6 +396,10 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
         this.extension.addHandler(new TraitTypeCheckingExtension(this));
     }
 
+    public void setCompilationUnit(final CompilationUnit compilationUnit) {
+        typeCheckingContext.setCompilationUnit(compilationUnit);
+    }
+
     @Override
     protected SourceUnit getSourceUnit() {
         return typeCheckingContext.getSource();
@@ -404,6 +409,17 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
         extension.setup();
     }
 
+    /**
+     * Returns array of type checking annotations. Subclasses may override this
+     * method in order to provide additional types which must be looked up when
+     * checking if a method or a class node should be skipped.
+     * <p>
+     * The default implementation returns {@link TypeChecked}.
+     */
+    protected ClassNode[] getTypeCheckingAnnotations() {
+        return TYPECHECKING_ANNOTATIONS;
+    }
+
     /**
      * Returns the current type checking context. The context is used internally by the type
      * checker during type checking to store various state data.
@@ -418,10 +434,49 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
         this.extension.addHandler(extension);
     }
 
-    public void setCompilationUnit(final CompilationUnit compilationUnit) {
-        typeCheckingContext.setCompilationUnit(compilationUnit);
+    private List<TypeCheckingExtension> getTypeCheckingExtensions(final AnnotatedNode classOrMethod) {
+        List<Expression> ex = new ArrayList<>();
+        for (ClassNode type : getTypeCheckingAnnotations()) {
+            for (AnnotationNode anno : classOrMethod.getAnnotations(type)) {
+                Expression extensions = anno.getMember("extensions");
+                if (extensions instanceof ConstantExpression) {
+                    ex.add(extensions);
+                } else if (extensions instanceof ListExpression) {
+                    ex.addAll(((ListExpression) extensions).getExpressions());
+                }
+            }
+        }
+        if (ex.isEmpty()) return Collections.emptyList();
+        return ex.stream().filter(e -> e instanceof ConstantExpression).map(pathOrType ->
+            new GroovyTypeCheckingExtensionSupport(this, pathOrType.getText(), typeCheckingContext.getCompilationUnit())
+        ).collect(Collectors.toList());
+    }
+
+    private <Node extends AnnotatedNode> void doWithTypeCheckingExtensions(final Node classOrMethod, final Consumer<Node> visitor) {
+        List<TypeCheckingExtension> extensions = getTypeCheckingExtensions(classOrMethod);
+        if (extensions.isEmpty()) {
+            visitor.accept(classOrMethod);
+        } else { // GROOVY-10770: extension composition
+            List<TypeCheckingExtension> added = new ArrayList<>();
+            for (TypeCheckingExtension ext : extensions) {
+                if (extension.addHandler(ext)) {
+                    added.add(ext);
+                    ext.setup();
+                }
+            }
+            try {
+                visitor.accept(classOrMethod);
+            } finally {
+                for (TypeCheckingExtension ext : added) {
+                    extension.removeHandler(ext);
+                    ext.finish();
+                }
+            }
+        }
     }
 
+    //--------------------------------------------------------------------------
+
     @Override
     public void visitClass(final ClassNode node) {
         if (shouldSkipClassNode(node)) return;
@@ -436,7 +491,7 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
             Set<MethodNode> oldSet = typeCheckingContext.alreadyVisitedMethods;
             typeCheckingContext.alreadyVisitedMethods = new LinkedHashSet<>();
 
-            super.visitClass(node);
+            doWithTypeCheckingExtensions(node, super::visitClass);
             node.getInnerClasses().forEachRemaining(this::visitClass);
 
             typeCheckingContext.alreadyVisitedMethods = oldSet;
@@ -455,17 +510,6 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
         extension.afterVisitClass(node);
     }
 
-    /**
-     * Returns array of type checking annotations. Subclasses may override this
-     * method in order to provide additional types which must be looked up when
-     * checking if a method or a class node should be skipped.
-     * <p>
-     * The default implementation returns {@link TypeChecked}.
-     */
-    protected ClassNode[] getTypeCheckingAnnotations() {
-        return TYPECHECKING_ANNOTATIONS;
-    }
-
     protected boolean shouldSkipClassNode(final ClassNode node) {
         return Boolean.TRUE.equals(node.getNodeMetaData(StaticTypeCheckingVisitor.class)) || isSkipMode(node);
     }
@@ -2595,7 +2639,7 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
             new ReturnAdder(returnStmt -> applyTargetType(returnType, returnStmt.getExpression())).visitMethod(node);
         }
         readClosureParameterAnnotation(node); // GROOVY-6603
-        super.visitConstructorOrMethod(node, isConstructor);
+        doWithTypeCheckingExtensions(node, it -> super.visitConstructorOrMethod(it, isConstructor));
         if (node.hasDefaultValue()) {
             visitDefaultParameterArguments(node.getParameters());
         }
diff --git a/src/test/groovy/bugs/Groovy10770.groovy b/src/test/groovy/bugs/Groovy10770.groovy
new file mode 100644
index 0000000000..cfbba430ad
--- /dev/null
+++ b/src/test/groovy/bugs/Groovy10770.groovy
@@ -0,0 +1,152 @@
+/*
+ *  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.bugs
+
+import groovy.test.NotYetImplemented
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+final class Groovy10770 {
+
+    private final GroovyShell shell = GroovyShell.withConfig {
+        imports {
+            star 'groovy.transform'
+        }
+    }
+
+    @Test
+    void testCheckError1() {
+        def err = shouldFail shell, '''
+            @TypeChecked
+            class C {
+                class D {
+                    def foo() {
+                        bar.baz
+                    }
+                }
+            }
+        '''
+        assert err =~ /The variable .bar. is undeclared/
+    }
+
+    @Test
+    void testCheckError2() {
+        def err = shouldFail shell, '''
+            @TypeChecked(extensions='groovy/transform/stc/UndefinedVariableTestExtension.groovy')
+            class C {
+                class D {
+                    def foo() {
+                        bar.baz
+                    }
+                }
+            }
+        '''
+        assert err =~ /No such property: baz for class: java.lang.String/
+    }
+
+    @Test
+    void testExtensions1() {
+        assertScript shell, '''
+            @TypeChecked(extensions='groovy/transform/stc/UndefinedVariableTestExtension.groovy')
+            class C {
+                @TypeChecked(extensions='groovy/transform/stc/UnresolvedPropertyTestExtension.groovy')
+                class D {
+                    def foo() {
+                        bar.baz
+                    }
+                }
+            }
+            def c = new C()
+            def d = new C.D(c)
+        '''
+    }
+
+    @Test
+    void testExtensions2() {
+        def err = shouldFail shell, '''
+            @CompileStatic(extensions='groovy/transform/stc/UndefinedVariableTestExtension.groovy')
+            class C {
+                @CompileStatic(extensions='groovy/transform/stc/UnresolvedPropertyTestExtension.groovy')
+                class D {
+                    def foo() {
+                        bar.baz
+                    }
+                }
+            }
+            def c = new C()
+            def d = new C.D(c)
+        '''
+        assert err =~ /Access to java.lang.String#baz is forbidden/ // passes STC but fails during compilation
+    }
+
+    @Test
+    void testExtensions3() {
+        def err = shouldFail shell, '''
+            @CompileStatic(extensions='groovy/transform/stc/UndefinedVariableTestExtension.groovy')
+            class C {
+                @TypeChecked(extensions='groovy/transform/stc/UnresolvedPropertyTestExtension.groovy')
+                class D {
+                    def foo() {
+                        bar.baz
+                    }
+                }
+            }
+            def c = new C()
+            def d = new C.D(c)
+        '''
+        assert err =~ /Access to java.lang.String#baz is forbidden/ // passes STC but fails during compilation
+    }
+
+    @Test @NotYetImplemented
+    void testExtensions4() {
+        def err = shouldFail shell, '''
+            @TypeChecked(extensions='groovy/transform/stc/UndefinedVariableTestExtension.groovy')
+            class C {
+                @CompileStatic(extensions='groovy/transform/stc/UnresolvedPropertyTestExtension.groovy')
+                class D {
+                    def foo() {
+                        bar.baz
+                    }
+                }
+            }
+            def c = new C()
+            def d = new C.D(c)
+        '''
+        assert err =~ /Access to java.lang.String#baz is forbidden/ // passes STC but fails during compilation
+    }
+
+    @Test // annotate method
+    void testExtensions5() {
+        assertScript shell, '''
+            @TypeChecked(extensions='groovy/transform/stc/UndefinedVariableTestExtension.groovy')
+            class C {
+                class D {
+                    @TypeChecked(extensions='groovy/transform/stc/UnresolvedPropertyTestExtension.groovy')
+                    def foo() {
+                        bar.baz
+                    }
+                }
+            }
+            def c = new C()
+            def d = new C.D(c)
+        '''
+    }
+}