[jvm] IDEA-231329 Convert @TestOnly inspection to UAST

GitOrigin-RevId: 15230423d944e7b3a236f401bdc251dabb7d5586
This commit is contained in:
Bart
2021-03-23 10:35:38 +00:00
committed by intellij-monorepo-bot
parent df1a5cd71c
commit b3a6e7ebd2
53 changed files with 394 additions and 878 deletions
@@ -317,10 +317,6 @@ inspection.suspicious.collections.method.calls.problem.descriptor1=Suspicious ca
inspection.suspicious.collections.method.calls.problem.descriptor=''{0}'' may not contain {2} of type ''{1}''
inspection.suspicious.getter.setter.field.option=Only warn when field matching getter/setter name is present
inspection.suspicious.integer.div.assignment.option=Report suspicious but possibly exact divisions
inspection.test.only.problems.display.name=Test-only class or method call in production code
inspection.test.only.problems.test.only.class.reference=Test-only class is referenced in production code
inspection.test.only.problems.test.only.field.reference=Test-only field is referenced in production code
inspection.test.only.problems.test.only.method.call=Test-only method is called in production code
inspection.unary.plus.unary.binary.option=Only report in confusing binary or unary expression context
inspection.unnecessary.super.qualifier.option=Ignore clarification 'super' qualifier
inspection.use.compare.method.fix.family.name=Replace with single comparison method
@@ -390,7 +386,6 @@ unused.library.problem.descriptor=Unused library ''{0}''
unused.library.roots.problem.descriptor=Unused roots {0} from library ''{1}''
var.can.be.replaced.with.explicit.type='var' can be replaced with explicit type
vararg.method.call.with.50.poly.arguments=Vararg method call with 50+ poly arguments may cause compilation and analysis slowdown
visible.for.testing.makes.little.sense.on.test.only.code=@VisibleForTesting makes little sense on @TestOnly code
inspection.inconsistent.language.level.display.name=Inconsistent language level settings
inspection.weaker.access.display.name=Declaration access can be weaker
inspection.can.be.final.display.name=Declaration can have final modifier
@@ -166,9 +166,6 @@
key="unnecessary.module.dependency.display.name" groupBundle="messages.InspectionsBundle" groupKey="group.names.declaration.redundancy" enabledByDefault="true"
level="WARNING"
implementationClass="com.intellij.codeInspection.unnecessaryModuleDependency.UnnecessaryModuleDependencyInspection"/>
<localInspection groupPath="Java" language="JAVA" shortName="TestOnlyProblems" bundle="messages.JavaAnalysisBundle" key="inspection.test.only.problems.display.name"
groupBundle="messages.InspectionsBundle" groupKey="inspection.general.tools.group.name" enabledByDefault="false" level="WARNING"
implementationClass="com.intellij.codeInspection.testOnly.TestOnlyInspection" />
<localInspection language="XML" shortName="DeprecatedClassUsageInspection" groupKey="group.names.xml" groupBundle="messages.InspectionsBundle"
enabledByDefault="true" level="WARNING" implementationClass="com.intellij.codeInspection.xml.DeprecatedClassUsageInspection"
key="inspection.deprecated.class.usage.inspection.display.name" bundle="messages.JavaAnalysisBundle"/>
@@ -1,222 +0,0 @@
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection.testOnly;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.TestFrameworks;
import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.codeInspection.RemoveAnnotationQuickFix;
import com.intellij.java.analysis.JavaAnalysisBundle;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.light.LightModifierList;
import com.intellij.psi.impl.source.resolve.JavaResolveUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import static com.intellij.codeInsight.AnnotationUtil.CHECK_EXTERNAL;
public class TestOnlyInspection extends AbstractBaseJavaLocalInspectionTool {
@Override
@NotNull
public String getShortName() {
return "TestOnlyProblems";
}
@Override
@NotNull
public String getGroupDisplayName() {
return getGeneralGroupName();
}
@Override
@NotNull
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder h, boolean isOnTheFly) {
return new JavaElementVisitor() {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression expression) {
validate(expression.getMethodExpression(), expression.resolveMethod(), h);
}
@Override
public void visitNewExpression(PsiNewExpression expression) {
PsiJavaCodeReferenceElement reference = expression.getClassOrAnonymousClassReference();
if (reference != null && validate(reference, expression.resolveMethod(), h)) {
validate(reference, ObjectUtils.tryCast(reference.resolve(), PsiMember.class), h);
}
}
@Override
public void visitMethodReferenceExpression(PsiMethodReferenceExpression expression) {
PsiElement resolve = expression.resolve();
if (resolve instanceof PsiMethod) {
validate(expression, (PsiMethod)resolve, h);
}
}
@Override
public void visitReferenceExpression(PsiReferenceExpression reference) {
PsiElement resolve = reference.resolve();
if (resolve instanceof PsiField) {
validate(reference, (PsiField)resolve, h);
}
}
@Override
public void visitReferenceElement(PsiJavaCodeReferenceElement reference) {
if (reference.getParent() instanceof PsiNewExpression
|| reference.getParent() instanceof PsiAnonymousClass
|| PsiTreeUtil.getParentOfType(reference, PsiImportStatementBase.class) != null) {
return;
}
PsiElement resolve = reference.resolve();
if (resolve instanceof PsiClass) validate(reference, (PsiClass)resolve, h);
}
@Override
public void visitElement(@NotNull PsiElement element) {
if (element instanceof PsiMember) {
PsiAnnotation vft = findVisibleForTestingAnnotation((PsiMember)element);
if (vft != null && isDirectlyTestOnly((PsiMember)element)) {
PsiElement toHighlight = null;
if (element instanceof PsiNameIdentifierOwner) {
toHighlight = ((PsiNameIdentifierOwner)element).getNameIdentifier();
}
if (toHighlight == null) {
toHighlight = element;
}
h.registerProblem(toHighlight, JavaAnalysisBundle.message("visible.for.testing.makes.little.sense.on.test.only.code"), new RemoveAnnotationQuickFix(vft, (PsiModifierListOwner)element));
}
}
super.visitElement(element);
}
};
}
private static boolean validate(@NotNull PsiElement place, @Nullable PsiMember member, ProblemsHolder h) {
if (member == null) {
return true;
}
PsiAnnotation vft = findVisibleForTestingAnnotation(member);
if (vft == null && !isAnnotatedAsTestOnly(member)) {
return true;
}
if (isInsideTestOnlyMethod(place) || isInsideTestOnlyField(place) || isInsideTestOnlyClass(place) || isInsideTestClass(place)
|| isUnderTestSources(place)) {
return true;
}
if (vft != null) {
String modifier = getAccessModifierWithoutTesting(vft);
if (modifier == null) {
modifier = getNextLowerAccessLevel(member);
}
LightModifierList modList = new LightModifierList(member.getManager(), JavaLanguage.INSTANCE, modifier);
if (JavaResolveUtil.isAccessible(member, member.getContainingClass(), modList, place, null, null)) {
return true;
}
}
reportProblem(place, member, h);
return false;
}
private static final List<String> ourModifiersDescending =
Arrays.asList(PsiModifier.PUBLIC, PsiModifier.PROTECTED, PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE);
private static String getNextLowerAccessLevel(@NotNull PsiMember member) {
int methodModifier = ContainerUtil.indexOf(ourModifiersDescending, member::hasModifierProperty);
int minModifier = ourModifiersDescending.size() - 1;
if (member instanceof PsiMethod) {
for (PsiMethod superMethod : ((PsiMethod)member).findSuperMethods()) {
minModifier = Math.min(minModifier, ContainerUtil.indexOf(ourModifiersDescending, superMethod::hasModifierProperty));
}
}
return ourModifiersDescending.get(Math.min(minModifier, methodModifier + 1));
}
@Nullable
private static String getAccessModifierWithoutTesting(PsiAnnotation anno) {
PsiAnnotationMemberValue ref = anno.findAttributeValue("visibility");
if (ref instanceof PsiReferenceExpression) {
PsiElement target = ((PsiReferenceExpression)ref).resolve();
if (target instanceof PsiEnumConstant) {
String name = ((PsiEnumConstant)target).getName();
return "PRIVATE".equals(name) ? PsiModifier.PRIVATE : "PROTECTED".equals(name) ? PsiModifier.PROTECTED : PsiModifier.PACKAGE_LOCAL;
}
}
return null;
}
@Nullable
private static PsiAnnotation findVisibleForTestingAnnotation(@NotNull PsiMember member) {
return AnnotationUtil.findAnnotation(member,
"com.google.common.annotations.VisibleForTesting",
"com.android.annotations.VisibleForTesting",
"org.jetbrains.annotations.VisibleForTesting");
}
private static boolean isInsideTestOnlyMethod(PsiElement e) {
return isAnnotatedAsTestOnly(getTopLevelParentOfType(e, PsiMethod.class));
}
private static boolean isInsideTestOnlyField(PsiElement e) {
return isAnnotatedAsTestOnly(getTopLevelParentOfType(e, PsiField.class));
}
private static boolean isInsideTestOnlyClass(@NotNull PsiElement e) {
return isAnnotatedAsTestOnly(getTopLevelParentOfType(e, PsiClass.class));
}
private static boolean isAnnotatedAsTestOnly(@Nullable PsiMember m) {
if (m == null) return false;
return isDirectlyTestOnly(m) || isAnnotatedAsTestOnly(m.getContainingClass());
}
private static boolean isDirectlyTestOnly(@NotNull PsiMember m) {
return AnnotationUtil.isAnnotated(m, AnnotationUtil.TEST_ONLY, CHECK_EXTERNAL);
}
private static boolean isInsideTestClass(PsiElement e) {
PsiClass c = getTopLevelParentOfType(e, PsiClass.class);
return c != null && TestFrameworks.getInstance().isTestClass(c);
}
private static <T extends PsiElement> T getTopLevelParentOfType(PsiElement e, Class<T> c) {
T parent = PsiTreeUtil.getParentOfType(e, c);
if (parent == null) return null;
do {
T next = PsiTreeUtil.getParentOfType(parent, c);
if (next == null) return parent;
parent = next;
}
while (true);
}
private static boolean isUnderTestSources(PsiElement e) {
ProjectRootManager rm = ProjectRootManager.getInstance(e.getProject());
VirtualFile f = e.getContainingFile().getVirtualFile();
return f != null && rm.getFileIndex().isInTestSourceContent(f);
}
private static void reportProblem(PsiElement e, PsiMember target, ProblemsHolder h) {
String message = JavaAnalysisBundle.message(target instanceof PsiClass
? "inspection.test.only.problems.test.only.class.reference"
: target instanceof PsiField ? "inspection.test.only.problems.test.only.field.reference"
: "inspection.test.only.problems.test.only.method.call");
h.registerProblem(e, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING);
}
}
@@ -1,14 +0,0 @@
<html>
<body>
Reports usages of <b>@TestOnly</b> - or <b>@VisibleForTesting</b>
annotated methods and classes in production code.
<br> The problems are not reported if such method or class is referenced from
<ul>
<li>code under 'Test Sources' folder
<li>a test-class (JUnit/TestNG)
<li>another <b>@TestOnly</b>-annotated method
</ul>
</body>
</html>
@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
<problem>
<file>B.java</file>
<line>10</line>
<package>&lt;default&gt;</package>
<description>Test-only class is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>10</line>
<package>&lt;default&gt;</package>
<description>Test-only class is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>11</line>
<package>&lt;default&gt;</package>
<description>Test-only field is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>13</line>
<package>&lt;default&gt;</package>
<description>Test-only class is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>13</line>
<package>&lt;default&gt;</package>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>14</line>
<package>&lt;default&gt;</package>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>15</line>
<package>&lt;default&gt;</package>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>16</line>
<package>&lt;default&gt;</package>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>18</line>
<package>&lt;default&gt;</package>
<description>Test-only field is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>19</line>
<package>&lt;default&gt;</package>
<description>Test-only field is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>20</line>
<package>&lt;default&gt;</package>
<description>Test-only field is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>22</line>
<package>&lt;default&gt;</package>
<description>Test-only class is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>22</line>
<package>&lt;default&gt;</package>
<description>Test-only method is called in production code</description>
</problem>
</problems>
@@ -1,24 +0,0 @@
import packageA.A;
import java.util.ArrayList;
import static packageA.A.staticImportedField;
import static packageA.A.staticImportedMethod;
public class B {
public void foo() {
Iterable<A> iterable = new ArrayList<A>();
System.out.println(iterable.iterator().next().field);
A a = new A();
a.method();
A.staticMethod();
staticImportedMethod();
System.out.println(a.field);
System.out.println(A.staticField);
System.out.println(staticImportedField);
A b = new A() { };
}
}
@@ -1,14 +0,0 @@
package packageA;
@org.jetbrains.annotations.TestOnly
public class A {
public String field;
public static String staticField;
public static String staticImportedField;
public A() {}
public void method() {}
public static void staticMethod() {}
public static void staticImportedMethod() {}
}
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
<problem>
<file>B.java</file>
<line>5</line>
<method>
<name>A()</name>
<display_name>A()</display_name>
<package>&lt;default&gt;</package>
<class>
<name>A</name>
<display_name>A</display_name>
</class>
</method>
<description>Test-only method is called in production code</description>
</problem>
</problems>
@@ -1,6 +0,0 @@
public class A {
@org.jetbrains.annotations.TestOnly
public A() {}
public A(String ignore) {}
}
@@ -1,8 +0,0 @@
import org.jetbrains.annotations.TestOnly;
public class B {
public void foo() {
new A();
new A("ignore");
}
}
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
<problem>
<file>A.java</file>
<line>14</line>
<module>testInsideField</module>
<package>&lt;default&gt;</package>
<entry_point TYPE="method" FQNAME="A$2 void run()"/>
<description>Test-only field is referenced in production code</description>
</problem>
</problems>
@@ -1,17 +0,0 @@
public class A {
@org.jetbrains.annotations.TestOnly
public static final String MY_FIELD = "VALUE";
@org.jetbrains.annotations.TestOnly
public static final Runnable MY_TEST_ONLY_RUNNABLE = new Runnable() {
public void run() {
System.out.println(MY_FIELD);
}
};
public static final Runnable MY_PRODUCTION_RUNNABLE = new Runnable() {
public void run() {
System.out.println(MY_FIELD);
}
};
}
@@ -1,10 +0,0 @@
public class C {
@org.junit.Test
public void foo() {
new Runnable() {
public void run() {
System.out.println(A.MY_FIELD);
}
};
}
}
@@ -1,15 +0,0 @@
public class D {
public static final Runnable MY_RUNNABLE = new Runnable() {
public void run() {
System.out.println(A.MY_FIELD);
}
};
public void test() {
new Runnable() {
public void run() {
System.out.println(A.MY_FIELD);
}
};
}
}
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
<problem>
<file>A.java</file>
<line>17</line>
<method>
<name>void production()</name>
<display_name>production()</display_name>
<package>&lt;default&gt;</package>
<class>
<name>A</name>
<display_name>A</display_name>
</class>
</method>
<description>Test-only method is called in production code</description>
</problem>
</problems>
@@ -1,21 +0,0 @@
public class A {
@org.jetbrains.annotations.TestOnly
public void foo() {}
@org.jetbrains.annotations.TestOnly
public void test() {
new Runnable() {
public void run() {
foo();
}
};
}
public void production() {
new Runnable() {
public void run() {
foo();
}
};
}
}
@@ -1,10 +0,0 @@
public class C {
@org.junit.Test
public void foo() {
new Runnable() {
public void run() {
new A().foo();
}
};
}
}
@@ -1,11 +0,0 @@
import org.jetbrains.annotations.TestOnly;
public class D {
public void test() {
new Runnable() {
public void run() {
new A().foo();
}
};
}
}
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
</problems>
@@ -1,8 +0,0 @@
import org.jetbrains.annotations.TestOnly;
import java.util.List;
@TestOnly
public class Bar {
static abstract class Foo implements List<Bar> {
}
}
@@ -1,19 +0,0 @@
import org.jetbrains.annotations.TestOnly;
@TestOnly
public class SeveralGenericTypes implements BiFunction<SeveralGenericTypes, Integer, SeveralGenericTypes> {
private final boolean isLeftBorder;
private final int offset;
private final String text;
public SeveralGenericTypes(boolean isLeftBorder, int offset, String text) {
this.isLeftBorder = isLeftBorder;
this.offset = offset;
this.text = text;
}
@Override
public SeveralGenericTypes apply(SeveralGenericTypes first, Integer second) {
return first;
}
}
@@ -1,19 +0,0 @@
import org.jetbrains.annotations.TestOnly;
@TestOnly
public class SingleGenericType implements Comparable<SingleGenericType> {
private final boolean isLeftBorder;
private final int offset;
private final String text;
public SingleGenericType(boolean isLeftBorder, int offset, String text) {
this.isLeftBorder = isLeftBorder;
this.offset = offset;
this.text = text;
}
@Override
public int compareTo(@NotNull SingleGenericType o) {
return offset < o.offset ? 1 : -1;
}
}
@@ -1,8 +0,0 @@
import org.jetbrains.annotations.TestOnly;
@TestOnly
public class Bar {
static {
java.lang.Class<Bar> clazz = Bar.class;
}
}
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
<problem>
<file>A.java</file>
<line>6</line>
<method>
<name>void production()</name>
<display_name>production()</display_name>
<package>&lt;default&gt;</package>
<class>
<name>A</name>
<display_name>A</display_name>
</class>
</method>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>3</line>
<method>
<name>void production()</name>
<display_name>production()</display_name>
<package>&lt;default&gt;</package>
<class>
<name>B</name>
<display_name>B</display_name>
</class>
</method>
<description>Test-only method is called in production code</description>
</problem>
</problems>
@@ -1,8 +0,0 @@
public class A {
@org.jetbrains.annotations.TestOnly
public void test() {}
public void production() {
test();
}
}
@@ -1,10 +0,0 @@
public class B {
public void production() {
new A().test();
}
@org.jetbrains.annotations.TestOnly
public void test() {
new A().test();
}
}
@@ -1,6 +0,0 @@
public class C {
@org.junit.Test
public void foo() {
new A().test();
}
}
@@ -1,7 +0,0 @@
import org.jetbrains.annotations.TestOnly;
public class D {
public void foo() {
new A().test();
}
}
@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems/>
@@ -1,7 +0,0 @@
import org.jetbrains.annotations.TestOnly;
public class A {
public void foo() {
unresolved();
}
}
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<problems>
<problem>
<file>B.java</file>
<line>4</line>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>6</line>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>7</line>
<description>Test-only class is referenced in production code</description>
</problem>
<problem>
<file>B.java</file>
<line>8</line>
<description>Test-only class is referenced in production code</description>
</problem>
<problem>
<file>AGrandChild.java</file>
<line>6</line>
<description>Test-only method is called in production code</description>
</problem>
<problem>
<file>A.java</file>
<line>39</line>
<description>@VisibleForTesting makes little sense on @TestOnly code</description>
<highlighted_element>bothAnnotationsAreAnError</highlighted_element>
</problem>
</problems>
@@ -1,14 +0,0 @@
package com.android.annotations;
public @interface VisibleForTesting {
enum Visibility {
/** The element should be considered protected. */
PROTECTED,
/** The element should be considered package-private. */
PACKAGE,
/** The element should be considered private. */
PRIVATE
}
Visibility visibility() default Visibility.PRIVATE;
}
@@ -1,5 +0,0 @@
package com.google.common.annotations;
public @interface VisibleForTesting {
}
@@ -1,40 +0,0 @@
package pkg1;
public class A {
void publicMethod() {
invisibleMethod(1);
visibleMethod(1);
}
@com.android.annotations.VisibleForTesting
void invisibleMethod(int a) {
}
@com.android.annotations.VisibleForTesting(visibility=com.android.annotations.VisibleForTesting.Visibility.PROTECTED)
void visibleMethod(int a) {
relaxedToPackageLevel(a);
}
@com.google.common.annotations.VisibleForTesting
void relaxedToPackageLevel(int a) {
}
protected void aProtectedMethod() {}
@com.google.common.annotations.VisibleForTesting
static class FooException extends RuntimeException {
FooException(String message) {
super(message);
}
}
public static void usingExceptionPrivately(String[] args) {
A.FooException exception =
new A.FooException("");
}
@org.jetbrains.annotations.TestOnly
@com.google.common.annotations.VisibleForTesting
void bothAnnotationsAreAnError() {}
}
@@ -1,11 +0,0 @@
package pkg1;
public class AChild extends A {
@com.google.common.annotations.VisibleForTesting
protected void aProtectedMethod() {
super.aProtectedMethod();
}
@com.google.common.annotations.VisibleForTesting
protected void childProtectedMethod() {
}
}
@@ -1,11 +0,0 @@
package pkg1;
public class B {
void publicMethod() {
new A().invisibleMethod(2);
new A().visibleMethod(2);
new A().relaxedToPackageLevel(2);
A.FooException exception =
new A.FooException("");
new AChild().aProtectedMethod();
}
}
@@ -1,11 +0,0 @@
@com.google.common.annotations.VisibleForTesting
public class RelaxedForTesting {
RelaxedForTesting() {
}
}
class Client {
{
new RelaxedForTesting();
}
}
@@ -1,9 +0,0 @@
package pkg2;
public class AGrandChild extends pkg1.AChild {
void foo() {
aProtectedMethod();
childProtectedMethod();
}
}
@@ -1,9 +0,0 @@
package pkg1;
public class Test {
void publicMethod() {
new A().invisibleMethod(3);
new A().visibleMethod(3);
new A().relaxedToPackageLevel(3);
new AChild().aProtectedMethod();
}
}
@@ -1,91 +0,0 @@
/*
* Copyright 2000-2017 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 com.intellij.java.codeInspection;
import com.intellij.analysis.AnalysisScope;
import com.intellij.codeInspection.ex.LocalInspectionToolWrapper;
import com.intellij.codeInspection.testOnly.TestOnlyInspection;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.project.IntelliJProjectConfiguration;
import com.intellij.testFramework.JavaInspectionTestCase;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
import com.intellij.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
public class TestOnlyInspectionTest extends JavaInspectionTestCase {
private final static DefaultLightProjectDescriptor ourProjectDescriptor = new DefaultLightProjectDescriptor() {
@Override
public void configureModule(@NotNull Module module, @NotNull ModifiableRootModel model, @NotNull ContentEntry contentEntry) {
super.configureModule(module, model, contentEntry);
contentEntry.addSourceFolder(contentEntry.getUrl() + "/test", true);
IntelliJProjectConfiguration.LibraryRoots junit4Library = IntelliJProjectConfiguration.getProjectLibrary("JUnit4");
PsiTestUtil.addLibrary(model, "JUnit4", "", ArrayUtil.toStringArray(junit4Library.getClassesPaths()));
}
};
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return ourProjectDescriptor;
}
@NotNull
@Override
protected AnalysisScope createAnalysisScope(VirtualFile sourceDir) {
return new AnalysisScope(getModule());
}
public void testSimple() {
doTest();
}
public void testInsideInner() {
doTest();
}
public void testConstructor() {
doTest();
}
public void testVisibleForTesting() { doTest(); }
public void testUnresolved() {
doTest(); // shouldn't throw
}
public void testClass() {
doTest();
}
public void testInsideField() {
doTest();
}
public void testInsideTestOnlyClass() {
doTest();
}
private void doTest() {
TestOnlyInspection i = new TestOnlyInspection();
doTest("testOnly/" + getTestName(true), new LocalInspectionToolWrapper(i));
}
}
@@ -44,6 +44,10 @@
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupKey="jvm.inspections.group.name" key="jvm.inspections.junit.rule.display.name"
implementationClass="com.intellij.codeInspection.JUnitRuleInspection"/>
<localInspection language="UAST" enabledByDefault="false" level="WARNING" shortName="TestOnlyProblems"
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupKey="jvm.inspections.group.name" key="jvm.inspections.testonly.display.name"
implementationClass="com.intellij.codeInspection.TestOnlyInspection"/>
<notificationGroup id="UAST" displayType="BALLOON"/>
</extensions>
<extensions defaultExtensionNs="com.intellij.codeInsight">
@@ -0,0 +1,12 @@
<html>
<body>
Reports usages of <code>@TestOnly</code> and <code>@VisibleForTesting</code> annotated methods and classes in production code.
<br>The problems are not reported if such method or class is referenced from:
<ul>
<li>code under 'Test Sources' folder
<li>a test-class (JUnit/TestNG)
<li>another <code>@TestOnly</code>-annotated method
</ul>
</body>
</html>
@@ -54,6 +54,12 @@ jvm.inspections.junit.rule.display.name=Malformed @Rule/@ClassRule field
jvm.inspections.junit.rule.problem.descriptor=Fields annotated with ''@{0}'' should be {1}
jvm.inspections.junit.rule.type.problem.descriptor=Field type should be subtype of 'org.junit.rules.TestRule'
jvm.inspections.testonly.display.name=Test-only usage in production code
jvm.inspections.testonly.class.reference=Test-only class is referenced in production code
jvm.inspections.testonly.field.reference=Test-only field is referenced in production code
jvm.inspections.testonly.method.call=Test-only method is called in production code
jvm.inspections.testonly.visiblefortesting=@VisibleForTesting makes little sense on @TestOnly code
jvm.inspections.string.touppercase.tolowercase.without.locale.description=<code>String.{0}()</code> called without specifying a Locale using internationalized strings #loc
can.t.build.uast.tree.for.file=Can't build UAST tree for file
title.uast=UAST
@@ -0,0 +1,170 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInspection
import com.intellij.analysis.JvmAnalysisBundle
import com.intellij.codeInsight.AnnotationUtil
import com.intellij.codeInsight.TestFrameworks
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.psi.*
import com.intellij.psi.impl.light.LightModifierList
import com.intellij.psi.impl.source.resolve.JavaResolveUtil
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.uast.UastVisitorAdapter
import com.intellij.util.containers.ContainerUtil
import org.jetbrains.uast.*
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
import kotlin.math.min
class TestOnlyInspection : AbstractBaseUastLocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor =
UastVisitorAdapter(TestOnlyVisitor(holder), true)
class TestOnlyVisitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
val parent = node.uastParent
if (parent is UQualifiedReferenceExpression) {
val parentRecResolved = parent.receiver.tryResolve()
if (parentRecResolved is PsiPackage || parentRecResolved is PsiVariable || parentRecResolved is PsiClass) return true
}
val method = node.resolve() ?: return true
val sourcePsi = node.sourcePsi ?: return true
return validate(sourcePsi, method, holder)
}
override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression): Boolean {
if (node.uastParent is UQualifiedReferenceExpression) return true
val resolve = node.resolveToUElement()
val resolvedJava = resolve?.javaPsi ?: return true
if (resolvedJava !is PsiMember) return true
val sourcePsi = node.sourcePsi ?: return true
return when (resolve) {
is UField -> validate(sourcePsi, resolvedJava, holder)
is UMethod -> if (node.javaPsi !is PsiReferenceExpression) validate(sourcePsi, resolvedJava, holder) else true
else -> true
}
}
override fun visitCallableReferenceExpression(node: UCallableReferenceExpression): Boolean {
val resolve = node.resolveToUElement()
val resolvedJava = resolve?.javaPsi ?: return true
if (resolvedJava !is PsiMember) return true
val sourcePsi = node.sourcePsi ?: return true
return validate(sourcePsi, resolvedJava, holder)
}
override fun visitElement(node: UElement): Boolean {
val javaPsi = node.javaPsi ?: return true
if (javaPsi is PsiMember && node is UDeclaration) {
val vft = findVisibleForTestingAnnotation(javaPsi)
if (vft != null && isDirectlyTestOnly(javaPsi)) {
val toHighlight = node.uastAnchor.sourcePsiElement ?: return true
holder.registerProblem(
toHighlight,
JvmAnalysisBundle.message("jvm.inspections.testonly.visiblefortesting"),
RemoveAnnotationQuickFix(vft, javaPsi as PsiModifierListOwner)
)
return true
}
}
return true
}
private fun validate(place: PsiElement, member: PsiMember, holder: ProblemsHolder): Boolean {
val vft = findVisibleForTestingAnnotation(member)
if (vft == null && !isAnnotatedAsTestOnly(member)) return true
if (isInsideTestOnlyMethod(place) || isInsideTestOnlyField(place) || isInsideTestOnlyClass(place) || isInsideTestClass(place) ||
isUnderTestSources(place)
) return false
if (vft != null) {
var modifier = getAccessModifierWithoutTesting(vft)
if (modifier == null) modifier = getNextLowerAccessLevel(member)
val modList = LightModifierList(member.manager, JavaLanguage.INSTANCE, modifier)
if (JavaResolveUtil.isAccessible(member, member.containingClass, modList, place, null, null)) return true
}
reportProblem(place, member, holder)
return false
}
private fun getNextLowerAccessLevel(member: PsiMember): String {
val methodModifier = ContainerUtil.indexOf(modifierPriority) { name: String -> member.hasModifierProperty(name) }
var minModifier = modifierPriority.size - 1
if (member is PsiMethod) {
for (superMethod in member.findSuperMethods()) {
minModifier = min(minModifier, ContainerUtil.indexOf(modifierPriority) { name: String -> superMethod.hasModifierProperty(name) })
}
}
return modifierPriority[min(minModifier, methodModifier + 1)]
}
private fun getAccessModifierWithoutTesting(anno: PsiAnnotation): String? {
val ref = anno.findAttributeValue("visibility")
if (ref is PsiReferenceExpression) {
val target = ref.resolve()
if (target is PsiEnumConstant) {
return when (target.name) {
"PRIVATE" -> PsiModifier.PRIVATE
"PROTECTED" -> PsiModifier.PROTECTED
else -> PsiModifier.PACKAGE_LOCAL
}
}
}
return null
}
private fun findVisibleForTestingAnnotation(member: PsiMember) = AnnotationUtil.findAnnotation(member, visibleForTestingAnnotations)
private fun isInsideTestOnlyMethod(elem: PsiElement) = isAnnotatedAsTestOnly(getTopLevelParentOfType(elem, PsiMethod::class.java))
private fun isInsideTestOnlyField(elem: PsiElement) = isAnnotatedAsTestOnly(getTopLevelParentOfType(elem, PsiField::class.java))
private fun isInsideTestOnlyClass(elem: PsiElement) = isAnnotatedAsTestOnly(getTopLevelParentOfType(elem, PsiClass::class.java))
private fun isAnnotatedAsTestOnly(member: PsiMember?): Boolean =
if (member == null) false else isDirectlyTestOnly(member) || isAnnotatedAsTestOnly(member.containingClass)
private fun isDirectlyTestOnly(member: PsiMember) =
AnnotationUtil.isAnnotated(member, AnnotationUtil.TEST_ONLY, AnnotationUtil.CHECK_EXTERNAL)
private fun isInsideTestClass(elem: PsiElement): Boolean {
val parent = getTopLevelParentOfType(elem, PsiClass::class.java)
return parent != null && TestFrameworks.getInstance().isTestClass(parent)
}
private fun <T : PsiElement?> getTopLevelParentOfType(e: PsiElement, c: Class<T>): T? {
var parent = PsiTreeUtil.getParentOfType(e, c) ?: return null
do {
val next = PsiTreeUtil.getParentOfType(parent, c) ?: return parent
parent = next
}
while (true)
}
private fun isUnderTestSources(elem: PsiElement): Boolean {
val rootManger = ProjectRootManager.getInstance(elem.project)
val file = elem.containingFile.virtualFile
return file != null && rootManger.fileIndex.isInTestSourceContent(file)
}
private fun reportProblem(elem: PsiElement, target: PsiMember, holder: ProblemsHolder) {
val message = JvmAnalysisBundle.message(
when {
target is PsiMethod && target.isConstructor -> "jvm.inspections.testonly.class.reference"
target is PsiField -> "jvm.inspections.testonly.field.reference"
else -> "jvm.inspections.testonly.method.call"
}
)
holder.registerProblem(elem, message, ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
}
}
companion object {
private val visibleForTestingAnnotations = listOf(
"com.google.common.annotations.VisibleForTesting",
"com.android.annotations.VisibleForTesting",
"org.jetbrains.annotations.VisibleForTesting"
)
private val modifierPriority = listOf(PsiModifier.PUBLIC, PsiModifier.PROTECTED, PsiModifier.PACKAGE_LOCAL, PsiModifier.PRIVATE)
}
}
@@ -0,0 +1,34 @@
package test;
import org.jetbrains.annotations.TestOnly;
import org.jetbrains.annotations.VisibleForTesting;
import java.util.function.Function;
public class TestOnlyTest {
@TestOnly
TestOnlyTest() { }
@TestOnly
static String someString(String someStr) { return someStr + "Foo"; }
@TestOnly
@VisibleForTesting
static String <warning descr="@VisibleForTesting makes little sense on @TestOnly code">doubleAnn</warning>() { return "Foo"; }
static class Bar {
@TestOnly
int aField = 0;
@TestOnly
void aMethod() { }
}
public static void main(String[] args) {
TestOnlyTest foo = <warning descr="Test-only class is referenced in production code">new TestOnlyTest()</warning>;
Bar bar = new Bar();
int aField = <warning descr="Test-only field is referenced in production code">bar.aField</warning>;
<warning descr="Test-only method is called in production code">bar.aMethod()</warning>;
Function<String, String> methodRef = <warning descr="Test-only method is called in production code">TestOnlyTest::someString</warning>;
}
}
@@ -0,0 +1,16 @@
package test;
import org.jetbrains.annotations.VisibleForTesting;
import test.VisibleForTestingTestApi;
public class VisibleForTestingTest {
@VisibleForTesting
static int fooBar = 0;
public static void main(String[] args) {
System.out.println(fooBar);
System.out.println(<warning descr="Test-only field is referenced in production code">VisibleForTestingTestApi.foo</warning>);
<warning descr="Test-only method is called in production code">VisibleForTestingTestApi.bar()</warning>;
}
}
@@ -0,0 +1,12 @@
package test;
import org.jetbrains.annotations.VisibleForTesting;
public class VisibleForTestingTestApi {
@VisibleForTesting
static int foo = x;
@VisibleForTesting
static void bar() { }
}
@@ -0,0 +1,16 @@
package com.intellij.codeInspection.tests.java
import com.intellij.codeInspection.tests.TestOnlyInspectionTestBase
import com.intellij.jvm.analysis.JvmAnalysisTestsUtil
class JavaTestOnlyInspectionTest : TestOnlyInspectionTestBase() {
override fun getTestDataPath() = JvmAnalysisTestsUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH + "/codeInspection/testonly"
fun `test @TestOnly in production code`() {
myFixture.testHighlighting("TestOnlyTest.java")
}
fun `test @VisibleForTesting in production code`() {
myFixture.testHighlighting("VisibleForTestingTest.java", "VisibleForTestingTestApi.java")
}
}
@@ -0,0 +1,28 @@
package test
import org.jetbrains.annotations.TestOnly
import org.jetbrains.annotations.VisibleForTesting
class TestOnlyTest @TestOnly constructor() {
val nonTestField = 0
var aField = 0
@TestOnly get() = field
@TestOnly
fun aMethod(x: Int): Int = x
@TestOnly
@VisibleForTesting
fun <warning descr = "@VisibleForTesting makes little sense on @TestOnly code" > aStringMethod < / warning >(): String = "Foo"
}
fun main() {
val foo1 = <warning descr = "Test-only class is referenced in production code">TestOnlyTest()</warning>
val foo2 = <warning descr = "Test-only class is referenced in production code">test.TestOnlyTest()</warning>
val foo3 = <warning descr = "Test-only class is referenced in production code">TestOnlyTest()</warning>.nonTestField
val bar = <warning descr = "Test-only method is called in production code">foo1.aField</warning>
<warning descr ="Test-only method is called in production code">foo1.aMethod(bar)</warning>
<warning descr ="Test-only method is called in production code">TestOnlyTest::aMethod</warning>.invoke(foo2, foo3)
<warning descr ="Test-only method is called in production code">test.TestOnlyTest::aMethod</warning>.invoke(foo2, foo3)
}
@@ -0,0 +1,15 @@
package test
import org.jetbrains.annotations.VisibleForTesting
import testapi.VisibleForTestingTestApi
object VisibleForTestingTest {
val foobar = 0
@VisibleForTesting get() = field
fun main() {
foobar
<warning descr ="Test-only method is called in production code">VisibleForTestingTestApi.foo</warning>
<warning descr ="Test-only method is called in production code">VisibleForTestingTestApi.bar()</warning>
}
}
@@ -0,0 +1,12 @@
package testapi
import org.jetbrains.annotations.VisibleForTesting
object VisibleForTestingTestApi {
var foo = 0
@VisibleForTesting get() = field
@VisibleForTesting
fun bar() {
}
}
@@ -0,0 +1,16 @@
package com.intellij.codeInspection
import com.intellij.codeInspection.tests.TestOnlyInspectionTestBase
import com.intellij.jvm.analysis.JvmAnalysisKtTestsUtil
class KotlinTestOnlyInspectionTest : TestOnlyInspectionTestBase() {
override fun getBasePath() = JvmAnalysisKtTestsUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH + "/codeInspection/testonly"
fun `test @TestOnly in production code`() {
myFixture.testHighlighting("TestOnlyTest.kt")
}
fun `test @VisibleForTesting in production code`() {
myFixture.testHighlighting("VisibleForTestingTest.kt", "VisibleForTestingTestApi.kt")
}
}
@@ -0,0 +1,53 @@
package com.intellij.codeInspection.tests
import com.intellij.codeInspection.TestOnlyInspection
import com.intellij.pom.java.LanguageLevel
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.builders.JavaModuleFixtureBuilder
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase
abstract class TestOnlyInspectionTestBase : JavaCodeInsightFixtureTestCase() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(inspection)
myFixture.addClass("""
package org.jetbrains.annotations;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.TYPE})
public @interface VisibleForTesting { }
""".trimIndent())
myFixture.addClass("""
package org.jetbrains.annotations;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.TYPE})
public @interface TestOnly { }
""".trimIndent())
}
override fun tuneFixture(moduleBuilder: JavaModuleFixtureBuilder<*>) {
moduleBuilder.addJdk(IdeaTestUtil.getMockJdk18Path().path)
moduleBuilder.setLanguageLevel(LanguageLevel.JDK_1_8)
}
override fun tearDown() {
try {
myFixture.disableInspections(inspection)
}
finally {
super.tearDown()
}
}
companion object {
private val inspection = TestOnlyInspection()
}
}