junit 5 malformed parameterized sources inspection

This commit is contained in:
Anna.Kozlova
2017-04-21 20:03:27 +02:00
parent 27ad90521f
commit 258d275306
7 changed files with 352 additions and 0 deletions
@@ -2214,4 +2214,5 @@ string.concatenation.introduce.fix.name=Introduce new {1} to update variable ''{
ignored.class.names=Ignore classes (including subclasses)
junit5.platform.runner.display.name=@RunWith(JUnitPlatform.class) without test methods
junit5.valid.parameterized.configuration.display.name=JUnit 5 malformed parameterized sources
@@ -26,4 +26,11 @@ public class JUnitCommonClassNames {
public static final String ORG_JUNIT_TEST = "org.junit.Test";
public static final String ORG_JUNIT_RULE = "org.junit.Rule";
public static final String ORG_JUNIT_CLASS_RULE = "org.junit.ClassRule";
public static final String ORG_JUNIT_JUPITER_PARAMS_PARAMETERIZED_TEST = "org.junit.jupiter.params.ParameterizedTest";
public static final String ORG_JUNIT_JUPITER_PARAMS_PROVIDER_METHOD_SOURCE = "org.junit.jupiter.params.provider.MethodSource";
public static final String ORG_JUNIT_JUPITER_PARAMS_VALUES_SOURCE = "org.junit.jupiter.params.provider.ValueSource";
public static final String ORG_JUNIT_JUPITER_PARAMS_ENUM_SOURCE = "org.junit.jupiter.params.provider.EnumSource";
public static final String ORG_JUNIT_JUPITER_PARAMS_PROVIDER_ARGUMENTS = "org.junit.jupiter.params.provider.Arguments";
public static final String ORG_JUNIT_JUPITER_PARAMS_PROVIDER_CSV_SOURCE = "org.junit.jupiter.params.provider.CsvSource";
public static final String ORG_JUNIT_JUPITER_PARAMS_PROVIDER_CSV_FILE_SOURCE = "org.junit.jupiter.params.provider.CsvFileSource";
}
+5
View File
@@ -77,6 +77,11 @@
<implicitUsageProvider implementation="com.intellij.execution.junit2.inspection.JUnitImplicitUsageProvider"/>
<predefinedMigrationMapProvider implementation="com.intellij.execution.junit2.refactoring.JUnit5Migration"/>
<psi.referenceContributor implementation="com.intellij.execution.junit.codeInsight.references.JUnitReferenceContributor"/>
<localInspection groupPath="Java" language="JAVA" shortName="JUnit5MalformedParameterized" bundle="com.siyeh.InspectionGadgetsBundle"
key="junit5.valid.parameterized.configuration.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.junit.issues" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.execution.junit.codeInsight.JUnit5MalformedParameterizedInspection"/>
</extensions>
<extensionPoints>
@@ -0,0 +1,191 @@
/*
* 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.execution.junit.codeInsight
import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil
import com.intellij.codeInsight.intention.QuickFixFactory
import com.intellij.codeInspection.BaseJavaBatchLocalInspectionTool
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.execution.junit.codeInsight.references.MethodSourceReference
import com.intellij.openapi.projectRoots.JavaSdkVersion
import com.intellij.openapi.projectRoots.JavaVersionService
import com.intellij.psi.*
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiUtil
import com.intellij.util.containers.ContainerUtil
import com.siyeh.InspectionGadgetsBundle
import com.siyeh.ig.junit.JUnitCommonClassNames
import org.jetbrains.annotations.Nls
class JUnit5MalformedParameterizedInspection : BaseJavaBatchLocalInspectionTool() {
@Nls
override fun getDisplayName(): String {
return InspectionGadgetsBundle.message("junit5.valid.parameterized.configuration.display.name")
}
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
val file = holder.file
if (!JavaVersionService.getInstance().isAtLeast(file, JavaSdkVersion.JDK_1_8)) return PsiElementVisitor.EMPTY_VISITOR
if (JavaPsiFacade.getInstance(file.project).findClass(JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PARAMETERIZED_TEST, file.resolveScope) == null) {
return PsiElementVisitor.EMPTY_VISITOR
}
return object : JavaElementVisitor() {
override fun visitMethod(method: PsiMethod) {
val modifierList = method.modifierList
val parameterizedAnnotation = modifierList.findAnnotation(JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PARAMETERIZED_TEST)
if (parameterizedAnnotation != null) {
val methodSource = modifierList.findAnnotation(JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PROVIDER_METHOD_SOURCE)
if (methodSource != null) {
checkMethodSource(method, methodSource)
}
val valuesSource = modifierList.findAnnotation(JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_VALUES_SOURCE)
if (valuesSource != null) {
checkValuesSource(method, valuesSource)
}
val enumSource = modifierList.findAnnotation(JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_ENUM_SOURCE)
if (enumSource != null) {
checkEnumSource(method, enumSource)
}
val csvFileSource = modifierList.findAnnotation(JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PROVIDER_CSV_FILE_SOURCE)
val csvSource = modifierList.findAnnotation(JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PROVIDER_CSV_SOURCE)
val noMultiArgsProvider = methodSource == null && csvFileSource == null && csvSource == null
if (valuesSource == null && enumSource == null && noMultiArgsProvider) {
holder.registerProblem(parameterizedAnnotation, "No sources are provided, the suite would be empty")
}
else if (method.parameterList.parametersCount > 1 && noMultiArgsProvider) {
holder.registerProblem(valuesSource ?: enumSource!!, "Multiple parameters are not supported by this source")
}
}
}
private fun checkEnumSource(method: PsiMethod, enumSource: PsiAnnotation) {
processArrayInAnnotationParameter(enumSource.findAttributeValue("value"), { value ->
if (value is PsiClassObjectAccessExpression) {
checkSourceTypeAndParameterTypeAgree(method, value, value.operand.type)
}
})
}
private fun checkValuesSource(method: PsiMethod, valuesSource: PsiAnnotation) {
val possibleValues = ContainerUtil.immutableMapBuilder<String, PsiType>()
.put("strings", PsiType.getJavaLangString(method.manager, method.resolveScope))
.put("ints", PsiType.INT)
.put("longs", PsiType.LONG)
.put("doubles", PsiType.DOUBLE).build()
for (valueKey in possibleValues.keys) {
processArrayInAnnotationParameter(valuesSource.findDeclaredAttributeValue(valueKey),
{ value -> checkSourceTypeAndParameterTypeAgree(method, value, possibleValues[valueKey]!!) })
}
val attributesNumber = valuesSource.parameterList.attributes.size
if (attributesNumber > 1) {
holder.registerProblem(valuesSource, "Exactly one type of input must be provided")
}
else if (attributesNumber == 0) {
holder.registerProblem(valuesSource, "No value source is defined")
}
}
private fun checkMethodSource(method: PsiMethod, methodSource: PsiAnnotation) {
val annotationMemberValue = methodSource.findDeclaredAttributeValue("names")
processArrayInAnnotationParameter(annotationMemberValue, { attributeValue ->
for (reference in attributeValue.references) {
if (reference is MethodSourceReference) {
val resolve = reference.resolve()
if (resolve !is PsiMethod) {
holder.registerProblem(attributeValue,
"Cannot resolve target method source: \'" + reference.value + "\'")
}
else {
val sourceProvider : PsiMethod = resolve
val providerName = sourceProvider.name
if (!sourceProvider.hasModifierProperty(PsiModifier.STATIC)) {
holder.registerProblem(attributeValue, "Method source \'$providerName\' must be static",
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
QuickFixFactory.getInstance().createModifierListFix(sourceProvider, PsiModifier.STATIC, true, false))
}
else if (sourceProvider.parameterList.parametersCount != 0) {
holder.registerProblem(attributeValue, "Method source \'$providerName\' should have no parameters")
}
else {
val componentType = getComponentType(sourceProvider.returnType, method)
if (componentType == null) {
holder.registerProblem(attributeValue,
"Method source \'$providerName\' must have one of the following return type: Stream<?>, Iterator<?>, Iterable<?> or Object[]")
}
else if (method.parameterList.parametersCount > 1 && !isArgumentsInheritor(componentType)) {
holder.registerProblem(attributeValue, "Multiple parameters have to be wrapped in Arguments")
}
}
}
}
}
})
}
private fun processArrayInAnnotationParameter(attributeValue: PsiAnnotationMemberValue?,
checker: (value : PsiAnnotationMemberValue) -> Unit) {
if (attributeValue is PsiLiteral || attributeValue is PsiClassObjectAccessExpression) {
checker.invoke(attributeValue)
}
else if (attributeValue is PsiArrayInitializerMemberValue) {
for (memberValue in attributeValue.initializers) {
processArrayInAnnotationParameter(memberValue, checker)
}
}
}
private fun checkSourceTypeAndParameterTypeAgree(method: PsiMethod,
attributeValue: PsiAnnotationMemberValue,
componentType: PsiType) {
val parameters = method.parameterList.parameters
if (parameters.size == 1 && !parameters[0].type.isAssignableFrom(componentType) && !isArgumentsInheritor(componentType)) {
holder.registerProblem(attributeValue,
"No implicit conversion found to convert object of type " + componentType.presentableText + " to " + parameters[0].type.presentableText)
}
}
private fun isArgumentsInheritor(componentType: PsiType): Boolean {
return InheritanceUtil.isInheritor(componentType, JUnitCommonClassNames.ORG_JUNIT_JUPITER_PARAMS_PROVIDER_ARGUMENTS)
}
private fun getComponentType(returnType: PsiType?, method: PsiMethod): PsiType? {
val collectionItemType = JavaGenericsUtil.getCollectionItemType(returnType, method.resolveScope)
if (collectionItemType != null) {
return collectionItemType
}
val streamItemType = PsiUtil.substituteTypeParameter(returnType, CommonClassNames.JAVA_UTIL_STREAM_STREAM, 1, false)
if (streamItemType != null) {
return streamItemType
}
return PsiUtil.substituteTypeParameter(returnType, CommonClassNames.JAVA_UTIL_ITERATOR, 1, false)
}
}
}
}
@@ -0,0 +1,19 @@
<html>
<body>
Reports parameterized tests which have malformed sources:
<ul>
<li>
MethodSource has unknown target or method is not static, no-arg
</li>
<li>
ValueSource/EnumSource types are not convertible to method parameters
</li>
<li>
No sources are defined
</li>
</ul>
<!-- tooltip end -->
<p>
<small>New in 2017.2</small>
</body>
</html>
@@ -0,0 +1,63 @@
/*
* 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.execution.junit.codeInsight;
import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.testFramework.LightProjectDescriptor;
import com.siyeh.ig.LightInspectionTestCase;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class JUnit5MalformedParameterizedTest extends LightInspectionTestCase {
@Nullable
@Override
protected InspectionProfileEntry getInspection() {
return new JUnit5MalformedParameterizedInspection();
}
@Override
protected void setUp() throws Exception {
super.setUp();
addEnvironmentClass("package org.junit.jupiter.params;\n" +
"public @interface ParameterizedTest {}");
addEnvironmentClass("package org.junit.jupiter.params.provider;\n" +
"public @interface MethodSource {String[] names();}");
addEnvironmentClass("package org.junit.jupiter.params.provider;\n" +
"public @interface EnumSource { Class<? extends Enum<?>> value();}");
addEnvironmentClass("package org.junit.jupiter.params.provider;\n" +
"public @interface ValueSource {\n" +
"String[] strings() default {};\n" +
"int[] ints() default {};\n" +
"long[] longs() default {};\n" +
"double[] doubles() default {};\n" +
"}\n");
}
public void testMalformedSources() throws Exception {
doTest();
}
@Override
protected String getBasePath() {
return "/plugins/junit/testData/codeInsight/malformedParameterized";
}
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return JAVA_8;
}
}
@@ -0,0 +1,66 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
class ParameterizedTestsDemo {
<warning descr="No sources are provided, the suite would be empty">@ParameterizedTest</warning>
void testWithParamsNoSource(int i) { }
@ParameterizedTest
@MethodSource(names = {<warning descr="Method source 'a' must be static">"a"</warning>,
<warning descr="Method source 'b' should have no parameters">"b"</warning>,
<warning descr="Method source 'c' must have one of the following return type: Stream<?>, Iterator<?>, Iterable<?> or Object[]">"c"</warning>,
"d"})
void testWithParams(Object s) { }
String[] a() {
return new String[] {"a", "b"};
}
static String[] b(int i) {
return new String[] {"a", "b"};
}
static Object c() {
return new String[] {"a", "b"};
}
static Object[] d() {
return new String[] {"a", "b"};
}
@ParameterizedTest
@MethodSource(names = {<warning descr="Multiple parameters have to be wrapped in Arguments">"d"</warning>})
void testWithMultipleParams(Object s, int i) { }
@ParameterizedTest
@EnumSource(<warning descr="No implicit conversion found to convert object of type E to int">E.class</warning>)
void testWithEnumSource(int i) { }
@ParameterizedTest
@EnumSource(E.class)
void testWithEnumSourceCorrect(E e) { }
enum E {
A, B;
}
@ParameterizedTest
@ValueSource(ints = {1})
void testWithValues(int i) { }
@ParameterizedTest
<warning descr="Exactly one type of input must be provided">@ValueSource(ints = {1},
strings = <warning descr="No implicit conversion found to convert object of type String to int">"str"</warning>)</warning>
void testWithMultipleValues(int i) { }
@ParameterizedTest
<warning descr="No value source is defined">@ValueSource()</warning>
void testWithNoValues(int i) { }
@ParameterizedTest
<warning descr="Multiple parameters are not supported by this source">@ValueSource(ints = 1)</warning>
void testWithValuesMultipleParams(int i, int j) { }
}