mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[jvm] IDEA-231329 Convert @TestOnly inspection to UAST
GitOrigin-RevId: 15230423d944e7b3a236f401bdc251dabb7d5586
This commit is contained in:
committed by
intellij-monorepo-bot
parent
df1a5cd71c
commit
b3a6e7ebd2
@@ -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"/>
|
||||
|
||||
-222
@@ -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><default></package>
|
||||
<description>Test-only class is referenced in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>10</line>
|
||||
<package><default></package>
|
||||
<description>Test-only class is referenced in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>11</line>
|
||||
<package><default></package>
|
||||
<description>Test-only field is referenced in production code</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>13</line>
|
||||
<package><default></package>
|
||||
<description>Test-only class is referenced in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>13</line>
|
||||
<package><default></package>
|
||||
<description>Test-only method is called in production code</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>14</line>
|
||||
<package><default></package>
|
||||
<description>Test-only method is called in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>15</line>
|
||||
<package><default></package>
|
||||
<description>Test-only method is called in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>16</line>
|
||||
<package><default></package>
|
||||
<description>Test-only method is called in production code</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>18</line>
|
||||
<package><default></package>
|
||||
<description>Test-only field is referenced in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>19</line>
|
||||
<package><default></package>
|
||||
<description>Test-only field is referenced in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>20</line>
|
||||
<package><default></package>
|
||||
<description>Test-only field is referenced in production code</description>
|
||||
</problem>
|
||||
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>22</line>
|
||||
<package><default></package>
|
||||
<description>Test-only class is referenced in production code</description>
|
||||
</problem>
|
||||
<problem>
|
||||
<file>B.java</file>
|
||||
<line>22</line>
|
||||
<package><default></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><default></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><default></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><default></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>
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
import java.util.List;
|
||||
|
||||
@TestOnly
|
||||
public class Bar {
|
||||
static abstract class Foo implements List<Bar> {
|
||||
}
|
||||
}
|
||||
-19
@@ -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;
|
||||
}
|
||||
}
|
||||
-19
@@ -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;
|
||||
}
|
||||
}
|
||||
-8
@@ -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><default></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><default></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>
|
||||
-14
@@ -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;
|
||||
}
|
||||
-5
@@ -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();
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
@com.google.common.annotations.VisibleForTesting
|
||||
public class RelaxedForTesting {
|
||||
RelaxedForTesting() {
|
||||
}
|
||||
}
|
||||
|
||||
class Client {
|
||||
{
|
||||
new RelaxedForTesting();
|
||||
}
|
||||
}
|
||||
-9
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package test;
|
||||
|
||||
import org.jetbrains.annotations.VisibleForTesting;
|
||||
|
||||
public class VisibleForTestingTestApi {
|
||||
@VisibleForTesting
|
||||
static int foo = x;
|
||||
|
||||
@VisibleForTesting
|
||||
static void bar() { }
|
||||
}
|
||||
|
||||
+16
@@ -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)
|
||||
}
|
||||
+15
@@ -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>
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package testapi
|
||||
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
|
||||
object VisibleForTestingTestApi {
|
||||
var foo = 0
|
||||
@VisibleForTesting get() = field
|
||||
|
||||
@VisibleForTesting
|
||||
fun bar() {
|
||||
}
|
||||
}
|
||||
+16
@@ -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")
|
||||
}
|
||||
}
|
||||
+53
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user