diff --git a/plugins/groovy/groovy-psi/resources/inspectionDescriptions/SingletonConstructor.html b/plugins/groovy/groovy-psi/resources/inspectionDescriptions/SingletonConstructor.html new file mode 100644 index 000000000000..69ed586942b4 --- /dev/null +++ b/plugins/groovy/groovy-psi/resources/inspectionDescriptions/SingletonConstructor.html @@ -0,0 +1,5 @@ + + +Checks that classes annotated by @Singleton do not have constructors unless it is declared non strict. + + \ No newline at end of file diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/fixes/RemoveElementQuickFix.java b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/fixes/RemoveElementQuickFix.java new file mode 100644 index 000000000000..b6de7c11b1e2 --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/fixes/RemoveElementQuickFix.java @@ -0,0 +1,53 @@ +/* + * 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.codeInspection.fixes; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.util.Function; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +public class RemoveElementQuickFix implements LocalQuickFix { + + private final String myName; + private final Function myElementFunction; + + public RemoveElementQuickFix(@NotNull String name, @NotNull Function function) { + myName = name; + myElementFunction = function; + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return myName; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement descriptorElement = descriptor.getPsiElement(); + if (descriptorElement == null) return; + + PsiElement elementToRemove = myElementFunction.fun(descriptorElement); + if (elementToRemove == null) return; + + elementToRemove.delete(); + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/GroovyTransformationsBundle.properties b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/GroovyTransformationsBundle.properties index e69de29bb2d1..ce01aae13af0 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/GroovyTransformationsBundle.properties +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/GroovyTransformationsBundle.properties @@ -0,0 +1,5 @@ +# @Singleton +singleton.constructor.inspection=@Singleton constructors +singleton.constructor.found=@Singleton class should not have constructors +singleton.constructor.remove=Remove constructor +singleton.constructor.makeNonStrict=Make @Singleton non-strict diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/MakeNonStrictQuickFix.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/MakeNonStrictQuickFix.kt new file mode 100644 index 000000000000..c32e1a39259c --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/MakeNonStrictQuickFix.kt @@ -0,0 +1,45 @@ +/* + * 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.singleton + +import com.intellij.codeInsight.AnnotationUtil +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import org.jetbrains.annotations.Nls +import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory +import org.jetbrains.plugins.groovy.transformations.message + +internal class MakeNonStrictQuickFix : LocalQuickFix { + + @Nls + override fun getFamilyName() = message("singleton.constructor.makeNonStrict") + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val annotation = getAnnotation(descriptor.psiElement) ?: return + val existingValue = AnnotationUtil.findDeclaredAttribute(annotation, "strict") + val newValue = GroovyPsiElementFactory.getInstance(project) + .createAnnotationFromText("@A(strict=false)") + .parameterList + .attributes[0] + if (existingValue == null) { + annotation.parameterList.add(newValue) + } + else { + existingValue.replace(newValue) + } + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspection.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspection.kt new file mode 100644 index 000000000000..6b08f7956c11 --- /dev/null +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspection.kt @@ -0,0 +1,49 @@ +/* + * 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.singleton + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import org.jetbrains.plugins.groovy.codeInspection.fixes.RemoveElementQuickFix +import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes +import org.jetbrains.plugins.groovy.lang.psi.impl.booleanValue +import org.jetbrains.plugins.groovy.lang.psi.impl.findDeclaredDetachedValue +import org.jetbrains.plugins.groovy.transformations.message + +class SingletonConstructorInspection : LocalInspectionTool() { + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : PsiElementVisitor() { + + override fun visitElement(element: PsiElement) { + if (element.node.elementType !== GroovyTokenTypes.mIDENT) return + val annotation = getAnnotation(element) ?: return + val strict = annotation.findDeclaredDetachedValue("strict").booleanValue() ?: true + if (!strict) return + + holder.registerProblem( + element, + message("singleton.constructor.found"), + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + RemoveElementQuickFix(message("singleton.constructor.remove")) { e -> e.parent }, + MakeNonStrictQuickFix() + ) + } + } +} diff --git a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/impl.kt b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/impl.kt index 2d68c7d443e8..c38b6cf3279f 100644 --- a/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/impl.kt +++ b/plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/impl.kt @@ -15,7 +15,19 @@ */ package org.jetbrains.plugins.groovy.transformations.singleton +import com.intellij.codeInsight.AnnotationUtil +import com.intellij.psi.PsiElement +import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.annotation.GrAnnotation +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition +import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod import org.jetbrains.plugins.groovy.lang.psi.util.GroovyCommonClassNames internal val singletonFqn = GroovyCommonClassNames.GROOVY_LANG_SINGLETON -internal val singletonOriginInfo = "by @Singleton" \ No newline at end of file +internal val singletonOriginInfo = "by @Singleton" + +internal fun getAnnotation(identifier: PsiElement?): GrAnnotation? { + val parent = identifier?.parent as? GrMethod ?: return null + if (!parent.isConstructor) return null + val clazz = parent.containingClass as? GrTypeDefinition ?: return null + return AnnotationUtil.findAnnotation(clazz, singletonFqn) as? GrAnnotation +} diff --git a/plugins/groovy/src/META-INF/plugin.xml b/plugins/groovy/src/META-INF/plugin.xml index 54cce5af8b63..9ac4d73c13f1 100644 --- a/plugins/groovy/src/META-INF/plugin.xml +++ b/plugins/groovy/src/META-INF/plugin.xml @@ -1148,6 +1148,10 @@ enabledByDefault="true" level="WEAK WARNING" implementationClass="org.jetbrains.plugins.groovy.codeInspection.untypedUnresolvedAccess.GrUnresolvedAccessInspection"/> + diff --git a/plugins/groovy/test/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspectionTest.groovy b/plugins/groovy/test/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspectionTest.groovy new file mode 100644 index 000000000000..1ac32add9080 --- /dev/null +++ b/plugins/groovy/test/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspectionTest.groovy @@ -0,0 +1,117 @@ +/* + * 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.singleton + +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.transformations.GroovyTransformationsBundle + +@CompileStatic +class SingletonConstructorInspectionTest extends LightGroovyTestCase { + + final LightProjectDescriptor projectDescriptor = GroovyLightProjectDescriptor.GROOVY_LATEST + + void setUp() { + super.setUp() + fixture.enableInspections(SingletonConstructorInspection) + } + + void 'test highlighting'() { + fixture.with { + configureByText '_.groovy', '''\ +@Singleton +class A { + A() {} + A(a) {} +} + +@Singleton(strict=true) +class ExplicitStrict { + ExplicitStrict() {} + ExplicitStrict(a) {} +} + +@Singleton(strict = false) +class NonStrict { + NonStrict() {} + NonStrict(a) {} +} +''' + checkHighlighting() + } + } + + void 'test make non strict fix'() { + fixture.with { + configureByText '_.groovy', '''\ +@Singleton +class A { + A() {} +} +''' + def intention = findSingleIntention(GroovyTransformationsBundle.message("singleton.constructor.makeNonStrict")) + assert intention + launchAction(intention) + checkResult '''\ +@Singleton(strict = false) +class A { + A() {} +} +''' + } + } + + void 'test make non strict fix existing'() { + fixture.with { + configureByText '_.groovy', '''\ +@Singleton(strict = true, property = "lol") +class A { + A() {} +} +''' + def intention = findSingleIntention(GroovyTransformationsBundle.message("singleton.constructor.makeNonStrict")) + assert intention + launchAction(intention) + checkResult '''\ +@Singleton(strict = false, property = "lol") +class A { + A() {} +} +''' + } + } + + void 'test remove constructor fix'() { + fixture.with { + configureByText '_.groovy', '''\ +@Singleton +class A { + A() {} +} +''' + def intention = findSingleIntention(GroovyTransformationsBundle.message("singleton.constructor.remove")) + assert intention + launchAction(intention) + checkResult '''\ +@Singleton +class A { +} +''' + } + } +}