diff --git a/plugins/groovy/resources/standardDsls/categoryTransform.gdsl b/plugins/groovy/resources/standardDsls/categoryTransform.gdsl index 24df1224b240..d7279e4a6449 100644 --- a/plugins/groovy/resources/standardDsls/categoryTransform.gdsl +++ b/plugins/groovy/resources/standardDsls/categoryTransform.gdsl @@ -20,12 +20,3 @@ contributor(ctype:hasAnnotation("groovy.lang.Category")) { } } -contributor(ctype:hasAnnotation("groovy.lang.Mixin")) { - def annotations = psiClass?.getAnnotations("groovy.lang.Mixin") - for (a in annotations) { - def refs = a.findAttributeValue("value") - for (c in refs?.asList()) { - delegatesTo c?.bind() - } - } -} \ No newline at end of file diff --git a/plugins/groovy/src/META-INF/plugin.xml b/plugins/groovy/src/META-INF/plugin.xml index 40bec478ac61..9cde000e8359 100644 --- a/plugins/groovy/src/META-INF/plugin.xml +++ b/plugins/groovy/src/META-INF/plugin.xml @@ -72,6 +72,7 @@ + diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyCommonClassNames.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyCommonClassNames.java index 82c33a84cf7b..4d603346469c 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyCommonClassNames.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/psi/util/GroovyCommonClassNames.java @@ -46,7 +46,8 @@ public final class GroovyCommonClassNames { @NonNls public static final String GROOVY_TRANSFORM_AUTO_EXTERNALIZE = "groovy.transform.AutoExternalize"; @NonNls public static final String GROOVY_TRANSFORM_AUTO_CLONE = "groovy.transform.AutoClone"; @NonNls public static final String GROOVY_LANG_CATEGORY = "groovy.lang.Category"; - public static final String GROOVY_LANG_USE = "groovy.lang.Use"; + @NonNls public static final String GROOVY_LANG_USE = "groovy.lang.Use"; + @NonNls public static final String GROOVY_LANG_MIXIN = "groovy.lang.Mixin"; private GroovyCommonClassNames() { diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/GdkMethodDslProvider.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/GdkMethodDslProvider.java index 2d9a834d199c..65030b728a6d 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/GdkMethodDslProvider.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/GdkMethodDslProvider.java @@ -75,9 +75,9 @@ public class GdkMethodDslProvider implements GdslMembersProvider { } public static GdkMethodHolder retrieveMethodMap(final Project project, - final GlobalSearchScope scope, - final boolean isStatic, - @NotNull final PsiClass categoryClass) { + final GlobalSearchScope scope, + final boolean isStatic, + @NotNull final PsiClass categoryClass) { return CachedValuesManager.getManager(project) .getCachedValue(categoryClass, METHOD_KEY, new CachedValueProvider() { @Override @@ -86,7 +86,8 @@ public class GdkMethodDslProvider implements GdslMembersProvider { final ProjectRootManager rootManager = ProjectRootManager.getInstance(project); final VirtualFile vfile = categoryClass.getContainingFile().getVirtualFile(); - if (vfile != null && (rootManager.getFileIndex().isInLibraryClasses(vfile) || rootManager.getFileIndex().isInLibrarySource(vfile))) { + if (vfile != null && + (rootManager.getFileIndex().isInLibraryClasses(vfile) || rootManager.getFileIndex().isInLibrarySource(vfile))) { return Result.create(result, rootManager); } @@ -94,7 +95,7 @@ public class GdkMethodDslProvider implements GdslMembersProvider { } }, false); } - + private static class GdkMethodHolder { private final MultiMap originalMethodsByName; private final NotNullLazyValue> originalMethodByType; @@ -110,7 +111,7 @@ public class GdkMethodDslProvider implements GdslMembersProvider { for (PsiMethod m : categoryClass.getMethods()) { final PsiParameter[] params = m.getParameterList().getParameters(); if (params.length == 0) continue; - + byName.putValue(m.getName(), m); } this.originalMethodsByName = byName; @@ -121,7 +122,6 @@ public class GdkMethodDslProvider implements GdslMembersProvider { MultiMap map = new MultiMap(); for (PsiMethod method : originalMethodsByName.values()) { map.putValue(getCategoryTargetType(method).getCanonicalText(), method); - } return map; } @@ -142,7 +142,7 @@ public class GdkMethodDslProvider implements GdslMembersProvider { if (name != null) { for (PsiMethod method : originalMethodsByName.get(name)) { if (getCategoryTargetType(method).isAssignableFrom(psiType)) { - if (!processor.execute(new GrGdkMethodImpl(method, myStatic), state)) { + if (!processor.execute(GrGdkMethodImpl.createGdkMethod(method, myStatic), state)) { return false; } } @@ -153,14 +153,13 @@ public class GdkMethodDslProvider implements GdslMembersProvider { for (String superType : ResolveUtil.getAllSuperTypes(psiType, descriptor.getProject()).keySet()) { for (PsiMethod method : originalMethodByType.getValue().get(superType)) { - if (!processor.execute(new GrGdkMethodImpl(method, myStatic), state)) { + if (!processor.execute(GrGdkMethodImpl.createGdkMethod(method, myStatic), state)) { return false; } } } return true; - } } } diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/MixinMemberContributor.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/MixinMemberContributor.java new file mode 100644 index 000000000000..2e1d8fd17c47 --- /dev/null +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/MixinMemberContributor.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2011 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.*; +import com.intellij.psi.scope.DelegatingScopeProcessor; +import com.intellij.psi.scope.PsiScopeProcessor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement; +import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation; +import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotationArrayInitializer; +import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotationMemberValue; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; +import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.TypesUtil; +import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrGdkMethodImpl; +import org.jetbrains.plugins.groovy.lang.psi.util.GroovyCommonClassNames; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Max Medvedev + */ +public class MixinMemberContributor extends NonCodeMembersContributor { + @Override + public void processDynamicElements(@NotNull final PsiType qualifierType, + PsiScopeProcessor processor, + GroovyPsiElement place, + ResolveState state) { + if (!(qualifierType instanceof PsiClassType)) return; + if (isInAnnotation(place)) return; + final PsiClassType.ClassResolveResult resolveResult = ((PsiClassType)qualifierType).resolveGenerics(); + final PsiClass aClass = resolveResult.getElement(); + if (aClass == null) return; + + final PsiModifierList modifierList = aClass.getModifierList(); + if (modifierList == null) return; + + final PsiAnnotation annotation = modifierList.findAnnotation(GroovyCommonClassNames.GROOVY_LANG_MIXIN); + if (annotation == null) return; + + final PsiAnnotationMemberValue value = annotation.findAttributeValue("value"); + + List mixins = new ArrayList(); + if (value instanceof GrAnnotationArrayInitializer) { + final GrAnnotationMemberValue[] initializers = ((GrAnnotationArrayInitializer)value).getInitializers(); + for (GrAnnotationMemberValue initializer : initializers) { + addMixin(initializer, mixins); + } + } + else if (value instanceof GrExpression) { + addMixin((GrExpression)value, mixins); + } + + for (PsiClass mixin : mixins) { + if (!mixin.processDeclarations(new DelegatingScopeProcessor(processor) { + @Override + public boolean execute(PsiElement element, ResolveState state) { + if (isCategoryMethod(element, qualifierType)) { + return super.execute(GrGdkMethodImpl.createGdkMethod((PsiMethod)element, false), state); + } + else { + return super.execute(element, state); + } + } + }, state, null, place)) { + return; + } + } + } + + private static boolean isCategoryMethod(PsiElement element, PsiType qualifierType) { + if (!(element instanceof PsiMethod)) return false; + if (!((PsiMethod)element).hasModifierProperty(PsiModifier.STATIC)) return false; + + final PsiParameter[] parameters = ((PsiMethod)element).getParameterList().getParameters(); + if (parameters.length == 0) return false; + + final PsiParameter selfParam = parameters[0]; + final PsiType selfType = selfParam.getType(); + + return TypesUtil.isAssignable(selfType, qualifierType, element.getManager(), element.getResolveScope()); + } + + private static boolean isInAnnotation(GroovyPsiElement place) { + return place.getParent() instanceof GrAnnotation || place.getParent() instanceof GrAnnotationArrayInitializer; + } + + private static void addMixin(GrAnnotationMemberValue value, List mixins) { + if (value instanceof GrReferenceExpression) { + final PsiElement resolved = ((GrReferenceExpression)value).resolve(); + if (resolved instanceof PsiClass) { + mixins.add((PsiClass)resolved); + } + } + } +} diff --git a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/ResolveUtil.java b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/ResolveUtil.java index 4f7fe69dc602..6c2dc9d2e79b 100644 --- a/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/ResolveUtil.java +++ b/plugins/groovy/src/org/jetbrains/plugins/groovy/lang/resolve/ResolveUtil.java @@ -42,6 +42,7 @@ import org.jetbrains.plugins.groovy.lang.psi.api.statements.*; import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrNamedArgument; import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock; +import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrCodeBlock; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall; import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression; @@ -75,12 +76,21 @@ public class ResolveUtil { private ResolveUtil() { } + /** + * + * @param place - place to start tree walk up + * @param processor + * @param processNonCodeMethods. this parameter tells us if we need non code members. But non code members are started to process only after we walk up any code block or script + * @return + */ public static boolean treeWalkUp(@NotNull GroovyPsiElement place, PsiScopeProcessor processor, boolean processNonCodeMethods) { PsiElement lastParent = null; PsiElement run = place; final Project project = place.getProject(); PsiElementFactory factory = JavaPsiFacade.getElementFactory(project); + + boolean doProcessNonCodeMembers = false; while (run != null) { if (!run.processDeclarations(processor, ResolveState.initial(), lastParent, place)) return false; @@ -89,18 +99,24 @@ public class ResolveUtil { if (superClass != null && !superClass.processDeclarations(processor, ResolveState.initial(), null, place)) return false; } if (processNonCodeMethods) { - if (run instanceof GrTypeDefinition) { - if (!processNonCodeMembers(factory.createType(((GrTypeDefinition)run)), processor, place)) return false; + if (!doProcessNonCodeMembers) { + if (run instanceof GrCodeBlock) doProcessNonCodeMembers = true; + else if (run instanceof GrStatement && run.getContext() instanceof GroovyFile) doProcessNonCodeMembers = true; } - else if ((run instanceof GroovyFileBase) && ((GroovyFileBase)run).isScript()) { - final PsiClass psiClass = ((GroovyFileBase)run).getScriptClass(); - if (psiClass != null) { - if (!processNonCodeMembers(factory.createType(psiClass), processor, place)) return false; + if (doProcessNonCodeMembers) { + if (run instanceof GrTypeDefinition) { + if (!processNonCodeMembers(factory.createType(((GrTypeDefinition)run)), processor, place)) return false; + } + else if ((run instanceof GroovyFileBase) && ((GroovyFileBase)run).isScript()) { + final PsiClass psiClass = ((GroovyFileBase)run).getScriptClass(); + if (psiClass != null) { + if (!processNonCodeMembers(factory.createType(psiClass), processor, place)) return false; + } + } + else if (run instanceof GrClosableBlock) { + if (!GdkMethodUtil.categoryIteration((GrClosableBlock)run, processor)) return false; + if (!GdkMethodUtil.withIteration((GrClosableBlock)run, processor, place)) return false; } - } - else if (run instanceof GrClosableBlock) { - if (!GdkMethodUtil.categoryIteration((GrClosableBlock)run, processor)) return false; - if (!GdkMethodUtil.withIteration((GrClosableBlock)run, processor, place)) return false; } } lastParent = run; diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/resolve/ResolveMethodTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/resolve/ResolveMethodTest.groovy index f9c7d123dfbf..f6c9cdc369e2 100644 --- a/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/resolve/ResolveMethodTest.groovy +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/lang/resolve/ResolveMethodTest.groovy @@ -757,4 +757,43 @@ class Foo { assertInstanceOf(resolved, GrReflectedMethod) assertTrue(resolved.modifierList.hasModifierProperty(PsiModifier.STATIC)) } + + void testMixinAndCategory() { + def ref = configureByText(""" +@Category(B) +class A { + def foo() {print getName()} +} + +@Mixin(A) +class B { + def getName('B'); +} + +print new B().foo() +""") + + def resolved = ref.resolve() + assertInstanceOf(resolved, GrGdkMethod) + assertInstanceOf(resolved.staticMethod, GrReflectedMethod) + } + + void testOnlyMixin() { + def ref = configureByText(""" +class A { + def foo() {print getName()} +} + +@Mixin(A) +class B { + def getName('B'); +} + +print new B().foo() +""") + + def resolved = ref.resolve() + assertInstanceOf(resolved, GrMethod) + assertTrue(resolved.isPhysical()) + } }