diff --git a/plugins/groovy/groovy-psi/resources/standardDsls/vetoableTransform.gdsl b/plugins/groovy/groovy-psi/resources/standardDsls/vetoableTransform.gdsl deleted file mode 100644 index f3aad6c48b2c..000000000000 --- a/plugins/groovy/groovy-psi/resources/standardDsls/vetoableTransform.gdsl +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2000-2014 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 standardDsls - -def vetoableMethods = { - method name: "addVetoableChangeListener", - type: "void", - params: [propertyName: "java.lang.String", - listener: "java.beans.VetoableChangeListener"], - doc: """ - Add a VetoableChangeListener for a specific property. The listener will be invoked only when a call on - fireVetoableChange names that specific property. The same listener object may be added more than once. - For each property, the listener will be invoked the number of times it was added for that property. If - propertyName or listener is null, no exception is thrown and no action is taken. - @param propertyName The name of the property to listen on. - @param listener The VetoableChangeListener to be added""" - - method name: "addVetoableChangeListener", - type: "void", - params: [listener: "java.beans.VetoableChangeListener"], - doc: """ - Add a VetoableListener to the listener list. The listener is registered for all properties. The same listener - object may beadded more than once, and will be called as many times as it is added. If listener is null, no - exception is thrown and no action is taken. - @param listener The VetoableChangeListener to be added""" - - method name: "removeVetoableChangeListener", - type: "void", - params: [propertyName: "java.lang.String", - listener: "java.beans.VetoableChangeListener"], - doc: """ - Remove a VetoableChangeListener for a specific property. If listener was added more than once to the same event - source for the specified property, it will be notified one less time after being removed. If propertyName is null, - no exception is thrown and no action is taken. If listener is null, or was never added for the specified property, - no exception is thrown and no action is taken. - @param propertyName The name of the property that was listened on. - @param listener The VetoableChangeListener to be removed""" - - method name: "removeVetoableChangeListener", - type: "void", - params: [listener: "java.beans.VetoableChangeListener"], - doc: """ - Remove a VetoableChangeListener from the listener list. This removes a VetoableChangeListener that was registered - for all properties. If listener was added more than once to the same event source, it will be notified one less - time after being removed. If listener is null, or was never added, no exception is thrown and no action is taken. - @param listener The VetoableChangeListener to be removed""" - - method name: "fireVetoableChange", - type: "void", - params: [propertyName: "java.lang.String", oldValue: "java.lang.Object", newValue: "java.lang.Object"], - doc: """ - Report a vetoable property update to any registered listeners. If anyone vetos the change, then fire a new event - reverting everyone to the old value and then rethrow the PropertyVetoException. - No event is fired if old and new are equal and non-null. - @param propertyName The programmatic name of the property that is about to change.. - @param oldValue The old value of the property. - @param newValue The new value of the property. """ - - method name: "getVetoableChangeListeners", - type: "java.beans.VetoableChangeListener[]", - doc: """ - Returns the list of VetoableChangeListeners. If named vetoable change listeners were added, then VetoableChangeListenerProxy - wrappers will returned. - @return List of VetoableChangeListeners and VetoableChangeListenerProxys if named property change listeners were added.""" - - method name: "getVetoableChangeListeners", - type: "java.beans.VetoableChangeListener[]", - params: [propertyName: "java.lang.String"], - doc: """ - Returns an array of all the listeners which have been associated with the named property. - @param propertyName The name of the property being listened to - @return all the VetoableChangeListeners associated with the named property. If no such listeners have been added, or if - propertyName is null, an empty array is returned.""" -} - -contributor(ctype:hasField(hasAnnotation("groovy.beans.Vetoable"))) { - if (psiClass?.fields?.find {it.hasAnnotation("groovy.beans.Vetoable")}) { - Closure c = vetoableMethods.clone() - c.delegate = delegate - c.resolveStrategy = Closure.DELEGATE_FIRST - c() - } -} \ No newline at end of file diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/GroovyCoreEnvironment.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/GroovyCoreEnvironment.java index f27757140308..e42aab8eb594 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/GroovyCoreEnvironment.java +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/lang/GroovyCoreEnvironment.java @@ -94,7 +94,7 @@ import org.jetbrains.plugins.groovy.lang.psi.typeEnhancers.*; import org.jetbrains.plugins.groovy.lang.psi.util.GroovyConstantExpressionEvaluator; import org.jetbrains.plugins.groovy.lang.resolve.*; import org.jetbrains.plugins.groovy.lang.resolve.ast.*; -import org.jetbrains.plugins.groovy.transformations.impl.BindableTransformationSupport; +import org.jetbrains.plugins.groovy.transformations.impl.*; import org.jetbrains.plugins.groovy.lang.resolve.ast.builder.strategy.DefaultBuilderStrategySupport; import org.jetbrains.plugins.groovy.lang.resolve.ast.builder.strategy.ExternalBuilderStrategySupport; import org.jetbrains.plugins.groovy.lang.resolve.ast.builder.strategy.InitializerBuilderStrategySupport; @@ -108,10 +108,6 @@ import org.jetbrains.plugins.groovy.structure.GroovyStructureViewFactory; import org.jetbrains.plugins.groovy.swingBuilder.SwingBuilderNamedArgumentProvider; import org.jetbrains.plugins.groovy.swingBuilder.SwingBuilderNonCodeMemberContributor; import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport; -import org.jetbrains.plugins.groovy.transformations.impl.CategoryTransformationSupport; -import org.jetbrains.plugins.groovy.transformations.impl.DefaultTransformationSupport; -import org.jetbrains.plugins.groovy.transformations.impl.DelegateTransformationSupport; -import org.jetbrains.plugins.groovy.transformations.impl.TraitTransformationSupport; /** * Upsource @@ -156,6 +152,7 @@ public class GroovyCoreEnvironment { appEnvironment.addExtension(AstTransformationSupport.EP_NAME, new ExternalBuilderStrategySupport()); appEnvironment.addExtension(AstTransformationSupport.EP_NAME, new InitializerBuilderStrategySupport()); appEnvironment.addExtension(AstTransformationSupport.EP_NAME, new BindableTransformationSupport()); + appEnvironment.addExtension(AstTransformationSupport.EP_NAME, new VetoableTransformationSupport()); appEnvironment.addExtension(AstTransformationSupport.EP_NAME, new LoggingContributor()); appEnvironment.addExtension(AstTransformationSupport.EP_NAME, new CategoryTransformationSupport()); appEnvironment.addExtension(AstTransformationSupport.EP_NAME, new DelegateTransformationSupport()); diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/impl/VetoableTransformationSupport.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/impl/VetoableTransformationSupport.kt new file mode 100644 index 000000000000..735df2066dff --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/impl/VetoableTransformationSupport.kt @@ -0,0 +1,105 @@ +/* + * 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.transformations.impl + +import com.intellij.codeInsight.AnnotationUtil +import com.intellij.psi.CommonClassNames +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiType +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition +import org.jetbrains.plugins.groovy.lang.psi.impl.statements.expressions.TypesUtil +import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder +import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil +import org.jetbrains.plugins.groovy.transformations.AstTransformationSupport +import org.jetbrains.plugins.groovy.transformations.TransformationContext + +private val VETOABLE_FQN = "groovy.beans.Vetoable" +private val VCL_FQN = "java.beans.VetoableChangeListener" +private val VCS_FQN = "java.beans.VetoableChangeSupport" +val ORIGIN_INFO = "via @Vetoable" + +class VetoableTransformationSupport : AstTransformationSupport { + + override fun applyTransformation(context: TransformationContext) { + val clazz = context.codeClass + if (!isApplicable(clazz)) return + + val methods = mutableListOf() + + methods += context.memberBuilder.method ("addVetoableChangeListener") { + returnType = PsiType.VOID + addParameter("propertyName", CommonClassNames.JAVA_LANG_STRING) + addParameter("listener", VCL_FQN) + } + + methods += context.memberBuilder.method("addVetoableChangeListener") { + returnType = PsiType.VOID + addParameter("listener", VCL_FQN) + } + + methods += context.memberBuilder.method("removeVetoableChangeListener") { + returnType = PsiType.VOID + addParameter("propertyName", CommonClassNames.JAVA_LANG_STRING) + addParameter("listener", VCL_FQN) + } + + methods += context.memberBuilder.method("removeVetoableChangeListener") { + returnType = PsiType.VOID + addParameter("listener", VCL_FQN) + } + + methods += context.memberBuilder.method("fireVetoableChange") { + returnType = PsiType.VOID + addParameter("propertyName", CommonClassNames.JAVA_LANG_STRING) + addParameter("oldValue", CommonClassNames.JAVA_LANG_OBJECT) + addParameter("newValue", CommonClassNames.JAVA_LANG_OBJECT) + } + + val vclArrayType = PsiArrayType(TypesUtil.createType(VCL_FQN, context.codeClass)) + + methods += context.memberBuilder.method("getVetoableChangeListeners") { + returnType = vclArrayType + } + + methods += context.memberBuilder.method("getVetoableChangeListeners") { + returnType = vclArrayType + addParameter("propertyName", CommonClassNames.JAVA_LANG_STRING) + } + + val docDelegate = context.psiFacade.findClass(VCS_FQN, context.resolveScope) + for (method in methods) { + method.addModifier(PsiModifier.PUBLIC) + method.originInfo = ORIGIN_INFO + method.putUserData(ResolveUtil.DOCUMENTATION_DELEGATE, docDelegate?.findMethodBySignature(method, false)) + } + + context.addMethods(methods) + } + + private fun isApplicable(clazz: GrTypeDefinition): Boolean { + val annotation = AnnotationUtil.findAnnotation(clazz, true, VETOABLE_FQN) + if (annotation != null) return true + + for (method in clazz.codeFields) { + if (AnnotationUtil.findAnnotation(method, true, VETOABLE_FQN) != null) { + return true + } + } + + return false + } +} \ 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 42b2d57d8b03..75056aeb3f5b 100644 --- a/plugins/groovy/src/META-INF/plugin.xml +++ b/plugins/groovy/src/META-INF/plugin.xml @@ -125,6 +125,7 @@ + diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/transformations/GrVetoableSupportTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/transformations/GrVetoableSupportTest.groovy new file mode 100644 index 000000000000..a1ee38af2c94 --- /dev/null +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/transformations/GrVetoableSupportTest.groovy @@ -0,0 +1,121 @@ +/* + * 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.transformations + +import com.intellij.openapi.roots.LanguageLevelProjectExtension +import com.intellij.pom.java.LanguageLevel +import com.intellij.psi.PsiCall +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiExpressionStatement +import com.intellij.psi.PsiJavaFile +import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture +import groovy.transform.CompileStatic +import org.jetbrains.plugins.groovy.SetupRule +import org.jetbrains.plugins.groovy.codeInspection.assignment.GroovyAssignabilityCheckInspection +import org.jetbrains.plugins.groovy.codeInspection.untypedUnresolvedAccess.GrUnresolvedAccessInspection +import org.jetbrains.plugins.groovy.lang.psi.GroovyFile +import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrCall +import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightMethodBuilder +import org.jetbrains.plugins.groovy.lang.resolve.ResolveUtil +import org.jetbrains.plugins.groovy.transformations.impl.VetoableTransformationSupportKt +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@CompileStatic +@RunWith(Parameterized) +class GrVetoableSupportTest { + + public @Rule SetupRule setupRule = new SetupRule() + + JavaCodeInsightTestFixture getFixture() { + setupRule.testCase.fixture + } + + @Parameterized.Parameters(name = "{0}") + public static Collection data() { + [ + '@Vetoable on class' : '@groovy.beans.Vetoable class Person {}', + '@Vetoable on class @CompileStatic': '@groovy.transform.CompileStatic @groovy.beans.Vetoable class Person {}', + '@Vetoable on field' : 'class Person { @groovy.beans.Vetoable foo }', + '@Vetoable on field @CompileStatic': '@groovy.transform.CompileStatic class Person { @groovy.beans.Vetoable foo }' + ].collect { k, v -> + [k, v] as Object[] + } + } + + @Parameterized.Parameter + public String name + + @Parameterized.Parameter(1) + public String clazzText + + private PsiClass clazz + + @Before + void addPerson() { + fixture.addFileToProject('Person.groovy', clazzText) + clazz = fixture.findClass('Person') + assert clazz + } + + @Test + void 'test groovy'() { + fixture.enableInspections(GrUnresolvedAccessInspection, GroovyAssignabilityCheckInspection) + [ + "new Person().addVetoableChangeListener {}", + "new Person().addVetoableChangeListener('') {}", + "new Person().removeVetoableChangeListener {}", + "new Person().removeVetoableChangeListener('') {}", + "new Person().fireVetoableChange('', null, null)", + "new Person().getVetoableChangeListeners()", + "new Person().getVetoableChangeListeners('')" + ].each { + def file = fixture.configureByText('test.groovy', it) as GroovyFile + def call = file.statements.last() as GrCall + def method = call.resolveMethod() as GrLightMethodBuilder + assert method + assert method.containingClass == clazz + assert method.originInfo == VetoableTransformationSupportKt.ORIGIN_INFO + assert method.getUserData(ResolveUtil.DOCUMENTATION_DELEGATE) + fixture.checkHighlighting() + } + } + + @Test + void 'test java'() { + LanguageLevelProjectExtension.getInstance(fixture.project).languageLevel = LanguageLevel.HIGHEST + [ + 'new Person().addVetoableChangeListener(e -> {});', + 'new Person().addVetoableChangeListener("", e -> {});', + 'new Person().removeVetoableChangeListener(e -> {});', + 'new Person().removeVetoableChangeListener("", e -> {});', + 'new Person().fireVetoableChange("", null, null);', + 'new Person().getVetoableChangeListeners();', + 'new Person().getVetoableChangeListeners("");' + ].each { + def file = fixture.configureByText('Main.java', "class Main { void bar() {$it} }") as PsiJavaFile + def call = (file.classes[0].methods[0].body.statements[0] as PsiExpressionStatement).expression as PsiCall + def method = call.resolveMethod() as GrLightMethodBuilder + assert method + assert method.containingClass == clazz + assert method.originInfo == VetoableTransformationSupportKt.ORIGIN_INFO + fixture.checkHighlighting() + } + } +}