From 258d27530610e780e7e94cd2ceba5513c9e2a28d Mon Sep 17 00:00:00 2001 From: "Anna.Kozlova" Date: Fri, 21 Apr 2017 18:55:51 +0200 Subject: [PATCH] junit 5 malformed parameterized sources inspection --- .../siyeh/InspectionGadgetsBundle.properties | 1 + .../siyeh/ig/junit/JUnitCommonClassNames.java | 7 + plugins/junit/src/META-INF/plugin.xml | 5 + .../JUnit5MalformedParameterizedInspection.kt | 191 ++++++++++++++++++ ...JUnit5ValidParameterizedConfiguration.html | 19 ++ .../JUnit5MalformedParameterizedTest.java | 63 ++++++ .../MalformedSources.java | 66 ++++++ 7 files changed, 352 insertions(+) create mode 100644 plugins/junit/src/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedInspection.kt create mode 100644 plugins/junit/src/inspectionDescriptions/JUnit5ValidParameterizedConfiguration.html create mode 100644 plugins/junit/test/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedTest.java create mode 100644 plugins/junit/testData/codeInsight/malformedParameterized/MalformedSources.java diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/InspectionGadgetsBundle.properties b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/InspectionGadgetsBundle.properties index afb434276b33..2a60652ce99a 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/InspectionGadgetsBundle.properties +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/InspectionGadgetsBundle.properties @@ -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 diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java index fc10305facdf..1486fb052ba9 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/junit/JUnitCommonClassNames.java @@ -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"; } diff --git a/plugins/junit/src/META-INF/plugin.xml b/plugins/junit/src/META-INF/plugin.xml index ac2f6e1dc580..e617724bfac1 100644 --- a/plugins/junit/src/META-INF/plugin.xml +++ b/plugins/junit/src/META-INF/plugin.xml @@ -77,6 +77,11 @@ + + diff --git a/plugins/junit/src/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedInspection.kt b/plugins/junit/src/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedInspection.kt new file mode 100644 index 000000000000..2e2001393fdc --- /dev/null +++ b/plugins/junit/src/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedInspection.kt @@ -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() + .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) + } + } + } +} diff --git a/plugins/junit/src/inspectionDescriptions/JUnit5ValidParameterizedConfiguration.html b/plugins/junit/src/inspectionDescriptions/JUnit5ValidParameterizedConfiguration.html new file mode 100644 index 000000000000..b5b0fe35a498 --- /dev/null +++ b/plugins/junit/src/inspectionDescriptions/JUnit5ValidParameterizedConfiguration.html @@ -0,0 +1,19 @@ + + +Reports parameterized tests which have malformed sources: +
    +
  • + MethodSource has unknown target or method is not static, no-arg +
  • +
  • + ValueSource/EnumSource types are not convertible to method parameters +
  • +
  • + No sources are defined +
  • +
+ +

+ New in 2017.2 + + \ No newline at end of file diff --git a/plugins/junit/test/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedTest.java b/plugins/junit/test/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedTest.java new file mode 100644 index 000000000000..5eea8ed9d9fb --- /dev/null +++ b/plugins/junit/test/com/intellij/execution/junit/codeInsight/JUnit5MalformedParameterizedTest.java @@ -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> 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; + } +} diff --git a/plugins/junit/testData/codeInsight/malformedParameterized/MalformedSources.java b/plugins/junit/testData/codeInsight/malformedParameterized/MalformedSources.java new file mode 100644 index 000000000000..a6b95922511b --- /dev/null +++ b/plugins/junit/testData/codeInsight/malformedParameterized/MalformedSources.java @@ -0,0 +1,66 @@ + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.*; + +class ParameterizedTestsDemo { + + @ParameterizedTest + void testWithParamsNoSource(int i) { } + + @ParameterizedTest + @MethodSource(names = {"a", + "b", + "c", + "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 = {"d"}) + void testWithMultipleParams(Object s, int i) { } + + @ParameterizedTest + @EnumSource(E.class) + 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 + @ValueSource(ints = {1}, + strings = "str") + void testWithMultipleValues(int i) { } + + @ParameterizedTest + @ValueSource() + void testWithNoValues(int i) { } + + @ParameterizedTest + @ValueSource(ints = 1) + void testWithValuesMultipleParams(int i, int j) { } + +}