diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties index 1ce2a6d362b3..57427fb96099 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/GroovyBundle.properties @@ -384,3 +384,4 @@ anonymous.classes.cannot.be.created.from.traits=Anonymous classes cannot be crea trait.method.cannot.be.protected=Trait methods are not allowed to be protected traits.are.not.supported.in.groovy.0=Traits are not supported in Groovy {0} non.static.classes.not.allowed=Non-static inner classes are not allowed in traits +selfType.class.does.not.inherit=@SelfType: Class ''{0}'' does not inherit ''{1}'' diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java index ea45c939091d..0df22141c435 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/annotator/GroovyAnnotator.java @@ -416,6 +416,18 @@ public class GroovyAnnotator extends GroovyElementVisitor { checkAnnotationCollector(myHolder, typeDefinition); checkSameNameMethodsWithDifferentAccessModifiers(myHolder, typeDefinition.getCodeMethods()); + checkInheritorOfSelfTypes(myHolder, typeDefinition); + } + + private static void checkInheritorOfSelfTypes(AnnotationHolder holder, GrTypeDefinition definition) { + if (!(definition instanceof GrClassDefinition)) return; + List selfTypeClasses = GrTraitUtil.getSelfTypeClasses(definition); + for (PsiClass selfClass : selfTypeClasses) { + if (InheritanceUtil.isInheritorOrSelf(definition, selfClass, true)) continue; + String message = GroovyBundle.message("selfType.class.does.not.inherit", definition.getQualifiedName(), selfClass.getQualifiedName()); + holder.createErrorAnnotation(GrHighlightUtil.getClassHeaderTextRange(definition), message); + break; + } } private static void checkSameNameMethodsWithDifferentAccessModifiers(AnnotationHolder holder, GrMethod[] methods) { diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/typedef/GrTraitTypeDefinitionImpl.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/typedef/GrTraitTypeDefinitionImpl.java index 589dd04fd788..c37d0f2a05fd 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/typedef/GrTraitTypeDefinitionImpl.java +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/impl/statements/typedef/GrTraitTypeDefinitionImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2014 JetBrains s.r.o. + * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,19 @@ package org.jetbrains.plugins.groovy.lang.psi.impl.statements.typedef; import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.ResolveState; +import com.intellij.psi.scope.PsiScopeProcessor; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.groovy.lang.parser.GroovyElementTypes; import org.jetbrains.plugins.groovy.lang.psi.GroovyElementVisitor; import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTraitTypeDefinition; import org.jetbrains.plugins.groovy.lang.psi.stubs.GrTypeDefinitionStub; +import org.jetbrains.plugins.groovy.lang.psi.util.GrTraitUtil; + +import java.util.List; /** * Created by Max Medvedev on 09/04/14 @@ -54,4 +62,17 @@ public class GrTraitTypeDefinitionImpl extends GrTypeDefinitionImpl implements G public void accept(GroovyElementVisitor visitor) { visitor.visitTraitDefinition(this); } + + @Override + public boolean processDeclarations(@NotNull PsiScopeProcessor processor, + @NotNull ResolveState state, + @Nullable PsiElement lastParent, + @NotNull PsiElement place) { + if (!super.processDeclarations(processor, state, lastParent, place)) return false; + List classes = GrTraitUtil.getSelfTypeClasses(this); + for (PsiClass clazz : classes) { + if (!clazz.processDeclarations(processor, state, lastParent, place)) return false; + } + return true; + } } diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/util/GrTraitUtil.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/util/GrTraitUtil.java index a48d28f6ce24..4068bfb0a48b 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/util/GrTraitUtil.java +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/psi/util/GrTraitUtil.java @@ -27,6 +27,7 @@ import com.intellij.psi.impl.java.stubs.impl.PsiJavaFileStubImpl; import com.intellij.psi.stubs.StubElement; import com.intellij.psi.util.CachedValueProvider; import com.intellij.psi.util.CachedValuesManager; +import com.intellij.psi.util.PsiModificationTracker; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.FileBasedIndex; import org.jetbrains.annotations.Contract; @@ -34,15 +35,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField; import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition; +import org.jetbrains.plugins.groovy.lang.psi.impl.GrAnnotationUtil; import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightField; import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder; import org.jetbrains.plugins.groovy.lang.resolve.GroovyTraitFieldsFileIndex; import org.jetbrains.plugins.groovy.lang.resolve.GroovyTraitFieldsFileIndex.TraitFieldDescriptor; import org.jetbrains.plugins.groovy.lang.resolve.GroovyTraitMethodsFileIndex; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.intellij.psi.PsiModifier.ABSTRACT; import static org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifierFlags.*; @@ -71,6 +71,29 @@ public class GrTraitUtil { return method.getModifierList().hasExplicitModifier(ABSTRACT) || isInterface(method.getContainingClass()); } + public static List getSelfTypeClasses(@NotNull PsiClass trait) { + return CachedValuesManager.getCachedValue(trait, () -> { + List result = ContainerUtil.newArrayList(); + Queue queue = new ArrayDeque<>(); + Set visited = ContainerUtil.newHashSet(); + queue.offer(trait); + while (!queue.isEmpty()) { + PsiClass clazz = queue.poll(); + if (!visited.add(clazz)) continue; + ContainerUtil.addAll(queue, clazz.getSupers()); + if (isTrait(clazz)) { + PsiAnnotation annotation = AnnotationUtil.findAnnotation(clazz, "groovy.transform.SelfType"); + if (annotation != null) { + result.addAll( + GrAnnotationUtil.getClassArrayValue(annotation, "value") + ); + } + } + } + return CachedValueProvider.Result.create(result, PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT); + }); + } + @NotNull public static String getTraitFieldPrefix(@NotNull PsiClass aClass) { String qname = aClass.getQualifiedName(); diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/resolve/SelfTypeSupportTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/resolve/SelfTypeSupportTest.groovy new file mode 100644 index 000000000000..a5840b473143 --- /dev/null +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/resolve/SelfTypeSupportTest.groovy @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed 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.jetbrains.plugins.groovy.lang.resolve + +import com.intellij.psi.PsiElement +import com.intellij.testFramework.LightProjectDescriptor +import groovy.transform.CompileStatic +import org.jetbrains.plugins.groovy.GroovyLightProjectDescriptor +import org.jetbrains.plugins.groovy.LightGroovyTestCase +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod + +@CompileStatic +class SelfTypeSupportTest extends LightGroovyTestCase { + + LightProjectDescriptor projectDescriptor = GroovyLightProjectDescriptor.GROOVY_LATEST + String defaultImports = '''\ +import groovy.transform.SelfType +''' + + void 'test resolve from within trait'() { + def resolved = resolveByText('''\ +interface I { def foo() } +@SelfType(I) +trait T { + def bar() { + foo() + } +} +''') + assert (resolved as GrMethod).containingClass.name == 'I' + } + + void 'test resolve outside trait'() { + def resolved = resolveByText('''\ +interface I { def foo() } +@SelfType(I) +trait T {} +def bar(T t) { + t.foo() +} +''') + assert (resolved as GrMethod).containingClass.name == 'I' + } + + void 'test resolve inside trait extending trait'() { + def resolved = resolveByText('''\ +interface I { def foo() } +@SelfType(I) +trait T {} +trait T2 extends T { + def bar() { + foo() + } +} +''') + assert (resolved as GrMethod).containingClass.name == 'I' + } + + void 'test do not count @SelfType on interfaces in hierarchy'() { + assert resolveByText('''\ +interface I { def foo() } +@SelfType(I) +interface II {} +trait T implements II { + def bar() { + foo() + } +} +''') == null + } + + void 'test highlighting'() { + testHighlighting '''\ +interface I {} +@SelfType(I) +trait T {} +class A implements T {} + +trait T2 extends T {} +class B implements T2 {} +''' + } + + PsiElement resolveByText(String text) { + fixture.configureByText '_.groovy', "$defaultImports$text" + file.findReferenceAt(editor.caretModel.offset).resolve() + } + + void testHighlighting(String text) { + fixture.configureByText '_.groovy', "$defaultImports$text" + fixture.checkHighlighting() + } +}