[groovy] @Singleton: add constructor inspection
authorDaniil Ovchinnikov <daniil.ovchinnikov@jetbrains.com>
Tue, 8 Nov 2016 16:33:23 +0000 (19:33 +0300)
committerDaniil Ovchinnikov <daniil.ovchinnikov@jetbrains.com>
Tue, 8 Nov 2016 16:58:30 +0000 (19:58 +0300)
with ability to remove inappropriate constructor or mark @Singleton as
 non-strict

plugins/groovy/groovy-psi/resources/inspectionDescriptions/SingletonConstructor.html [new file with mode: 0644]
plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/codeInspection/fixes/RemoveElementQuickFix.java [new file with mode: 0644]
plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/GroovyTransformationsBundle.properties
plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/MakeNonStrictQuickFix.kt [new file with mode: 0644]
plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspection.kt [new file with mode: 0644]
plugins/groovy/groovy-psi/src/org/jetbrains/plugins/groovy/transformations/singleton/impl.kt
plugins/groovy/src/META-INF/plugin.xml
plugins/groovy/test/org/jetbrains/plugins/groovy/transformations/singleton/SingletonConstructorInspectionTest.groovy [new file with mode: 0644]

diff --git a/plugins/groovy/groovy-psi/resources/inspectionDescriptions/SingletonConstructor.html b/plugins/groovy/groovy-psi/resources/inspectionDescriptions/SingletonConstructor.html
new file mode 100644 (file)
index 0000000..69ed586
--- /dev/null
@@ -0,0 +1,5 @@
+<html>
+<body>
+Checks that classes annotated by <code>@Singleton</code> do not have constructors unless it is declared non strict.
+</body>
+</html>
\ 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 (file)
index 0000000..b6de7c1
--- /dev/null
@@ -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<PsiElement, PsiElement> myElementFunction;
+
+  public RemoveElementQuickFix(@NotNull String name, @NotNull Function<PsiElement, PsiElement> 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();
+  }
+}
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ce01aae13af0ac1b2d13fe2ee6753ad4e384b46c 100644 (file)
@@ -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 (file)
index 0000000..c32e1a3
--- /dev/null
@@ -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 (file)
index 0000000..6b08f79
--- /dev/null
@@ -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()
+      )
+    }
+  }
+}
index 2d68c7d443e8bcf7a44627e84d926d5da48fab76..c38b6cf3279f1bb5bb94795fe8984dbcb53b4225 100644 (file)
  */
 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
+}
index 54cce5af8b63e8b71f884961fe3214752984aa56..9ac4d73c13f1d9c2831a86710b9cfea0fbd1a2cd 100644 (file)
                      enabledByDefault="true" level="WEAK WARNING"
                      implementationClass="org.jetbrains.plugins.groovy.codeInspection.untypedUnresolvedAccess.GrUnresolvedAccessInspection"/>
 
+    <localInspection language="Groovy" groupPath="Groovy" groupName="Annotations verifying"
+                     bundle="org.jetbrains.plugins.groovy.transformations.GroovyTransformationsBundle"
+                     key="singleton.constructor.inspection" enabledByDefault="true" level="ERROR"
+                     implementationClass="org.jetbrains.plugins.groovy.transformations.singleton.SingletonConstructorInspection"/>
     <localInspection language="Groovy" groupPath="Groovy" shortName="DelegatesTo" displayName="@DelegatesTo inspection"
                      groupName="Annotations verifying" enabledByDefault="true" level="WARNING"
                      implementationClass="org.jetbrains.plugins.groovy.codeInspection.confusing.DelegatesToInspection"/>
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 (file)
index 0000000..1ac32ad
--- /dev/null
@@ -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 {
+    <error descr="@Singleton class should not have constructors">A</error>() {}
+    <error descr="@Singleton class should not have constructors">A</error>(a) {}
+}
+
+@Singleton(strict=true)
+class ExplicitStrict {
+  <error descr="@Singleton class should not have constructors">ExplicitStrict</error>() {}
+  <error descr="@Singleton class should not have constructors">ExplicitStrict</error>(a) {}
+}
+
+@Singleton(strict = false)
+class NonStrict {
+  NonStrict() {}
+  NonStrict(a) {}
+}
+'''
+      checkHighlighting()
+    }
+  }
+
+  void 'test make non strict fix'() {
+    fixture.with {
+      configureByText '_.groovy', '''\
+@Singleton
+class A {
+  <caret>A() {}
+}
+'''
+      def intention = findSingleIntention(GroovyTransformationsBundle.message("singleton.constructor.makeNonStrict"))
+      assert intention
+      launchAction(intention)
+      checkResult '''\
+@Singleton(strict = false)
+class A {
+  <caret>A() {}
+}
+'''
+    }
+  }
+
+  void 'test make non strict fix existing'() {
+    fixture.with {
+      configureByText '_.groovy', '''\
+@Singleton(strict = true, property = "lol")
+class A {
+  <caret>A() {}
+}
+'''
+      def intention = findSingleIntention(GroovyTransformationsBundle.message("singleton.constructor.makeNonStrict"))
+      assert intention
+      launchAction(intention)
+      checkResult '''\
+@Singleton(strict = false, property = "lol")
+class A {
+  <caret>A() {}
+}
+'''
+    }
+  }
+
+  void 'test remove constructor fix'() {
+    fixture.with {
+      configureByText '_.groovy', '''\
+@Singleton
+class A {
+  <caret>A() {}
+}
+'''
+      def intention = findSingleIntention(GroovyTransformationsBundle.message("singleton.constructor.remove"))
+      assert intention
+      launchAction(intention)
+      checkResult '''\
+@Singleton
+class A {
+}
+'''
+    }
+  }
+}