[groovy] reimplement @Vetoable support as transformation

This commit is contained in:
Daniil Ovchinnikov
2016-05-20 19:19:40 +03:00
parent 03b4a0976f
commit 0938bb4346
5 changed files with 229 additions and 101 deletions
@@ -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()
}
}
@@ -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());
@@ -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<GrLightMethodBuilder>()
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
}
}
+1
View File
@@ -125,6 +125,7 @@
<astTransformationSupport implementation="org.jetbrains.plugins.groovy.lang.resolve.ast.builder.strategy.ExternalBuilderStrategySupport"/>
<astTransformationSupport implementation="org.jetbrains.plugins.groovy.lang.resolve.ast.builder.strategy.InitializerBuilderStrategySupport"/>
<astTransformationSupport implementation="org.jetbrains.plugins.groovy.transformations.impl.BindableTransformationSupport"/>
<astTransformationSupport implementation="org.jetbrains.plugins.groovy.transformations.impl.VetoableTransformationSupport"/>
<astTransformationSupport implementation="org.jetbrains.plugins.groovy.lang.resolve.ast.LoggingContributor"/>
<astTransformationSupport implementation="org.jetbrains.plugins.groovy.transformations.impl.CategoryTransformationSupport"/>
@@ -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<Object[]> 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()
}
}
}