From 5d1aee613fbe351a628bf3206fc04d6df565324b Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Thu, 28 May 2026 11:48:42 +0200 Subject: [PATCH 1/2] Handle array arguments in @MethodSource. --- .../tests/UnusedPrivateMethodSampleTest.java | 73 +++++++++++++++++++ .../unused/UnusedPrivateMethodCheck.java | 46 +++++++++--- .../unused/UnusedPrivateMethodCheckTest.java | 8 ++ 3 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 java-checks-test-sources/default/src/test/java/checks/tests/UnusedPrivateMethodSampleTest.java diff --git a/java-checks-test-sources/default/src/test/java/checks/tests/UnusedPrivateMethodSampleTest.java b/java-checks-test-sources/default/src/test/java/checks/tests/UnusedPrivateMethodSampleTest.java new file mode 100644 index 00000000000..5b8a66803e3 --- /dev/null +++ b/java-checks-test-sources/default/src/test/java/checks/tests/UnusedPrivateMethodSampleTest.java @@ -0,0 +1,73 @@ +package checks.tests; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.fail; + +class UnusedPrivateMethodSampleTest { + private static Stream provideData() { + return Stream.of(1, 2, 3); + } + + private static Stream provideDataValue() { + return Stream.of(1, 2, 3); + } + + // False positive: JUnit 5 supports fully qualified method names, but we do not. + private static Stream provideDataFQ() { // Noncompliant + return Stream.of(1, 2, 3); + } + + private static Stream provideDataArray1() { + return Stream.of(1, 2, 3); + } + + private static Stream provideDataArray2() { + return Stream.of(4, 5, 6); + } + + private static Stream provideDataArray1Value() { + return Stream.of(1, 2, 3); + } + + private static Stream provideDataArray2Value() { + return Stream.of(4, 5, 6); + } + + private static Stream notUsed() { // Noncompliant + return Stream.of(7, 8, 9); + } + + @ParameterizedTest + @MethodSource("provideData") + void test(int num) { + fail(); + } + + @ParameterizedTest + @MethodSource(value = "provideDataValue") + void testValue(int num) { + fail(); + } + + @ParameterizedTest + @MethodSource("checks.tests.UnusedPrivateMethodSampleTest#provideDataFQ") + void testFQ(int num) { + fail(); + } + + @ParameterizedTest + @MethodSource({"provideDataArray1", "provideDataArray2"}) + void testArray(int num) { + fail(); + } + + @ParameterizedTest + @MethodSource(value = {"provideDataArray1Value", "provideDataArray2Value"}) + void testArrayValue(int num) { + fail(); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java b/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java index 636d8d20769..94a2fa19fa5 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java @@ -37,12 +37,14 @@ import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.LiteralTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.MethodReferenceTree; import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.NewArrayTree; import org.sonar.plugins.java.api.tree.NewClassTree; import org.sonar.plugins.java.api.tree.ParameterizedTypeTree; import org.sonar.plugins.java.api.tree.Tree; @@ -119,6 +121,14 @@ public List getUnusedResolvedPrivateMethods() { return unusedPrivateMethods.stream().filter(it -> !unresolvedMethodNames.contains(it.simpleName().name())).toList(); } + /** + * JUnit's {@code @MethodSource} annotation is attached to a test and can be used in two ways: + *
    + *
  • Without arguments, it indicates that the method with the same name as the test provides arguments.
  • + *
  • If one or more arguments are provided, then they explicitly indicate which methods will be called.
  • + *
+ * Here we handle the first case. The second case is handled by the more general {@link MethodsUsedInAnnotationsFilter}. + */ private static List getMethodSourcesNames(ClassTree tree) { return tree.members().stream() .filter(it -> it instanceof MethodTree mt && isAnnotatedWithMethodSource(mt)) @@ -273,26 +283,44 @@ private static boolean isNameIndicatingMethod(String name) { return name.toLowerCase(Locale.getDefault()).contains("method"); } - private void removeMethodName(LiteralTree literal) { - filteredNames.remove(removeQuotes(literal.value())); + private void removeMethodName(String quotedName) { + filteredNames.remove(removeQuotes(quotedName)); } private static String removeQuotes(String withQuotes) { return withQuotes.substring(1, withQuotes.length() - 1); } + private void removeMethodName(LiteralTree literal) { + removeMethodName(literal.value()); + } + + private void removeMethodNames(NewArrayTree newArrayTree) { + for (ExpressionTree initializer : newArrayTree.initializers()) { + if (initializer.is(Tree.Kind.STRING_LITERAL)) { + removeMethodName((LiteralTree) initializer); + } + } + } + + private void removeMethodName(ExpressionTree arg) { + if (arg.is(Tree.Kind.STRING_LITERAL)) { + removeMethodName((LiteralTree) arg); + } else if(arg.is(Tree.Kind.NEW_ARRAY)) { + removeMethodNames((NewArrayTree) arg); + } + } + @Override public void visitAnnotation(AnnotationTree annotationTree) { var isMethodAnnotation = isNameIndicatingMethod(annotationTree.annotationType().symbolType().name()); for (var arg : annotationTree.arguments()) { - if (arg.is(Tree.Kind.STRING_LITERAL)) { - if (isMethodAnnotation) { - removeMethodName((LiteralTree) arg); + if (arg instanceof AssignmentExpressionTree asgn) { + if (isMethodAnnotation || isNameIndicatingMethod(((IdentifierTree) asgn.variable()).name())) { + removeMethodName(asgn.expression()); } - } else if (arg instanceof AssignmentExpressionTree asgn && asgn.expression().is(Tree.Kind.STRING_LITERAL) && ( - isMethodAnnotation || isNameIndicatingMethod(((IdentifierTree) asgn.variable()).name()) - )) { - removeMethodName((LiteralTree) asgn.expression()); + } else if (isMethodAnnotation) { + removeMethodName(arg); } } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheckTest.java index 5c147b5bd8a..fcb8a7a4c3e 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheckTest.java @@ -30,6 +30,14 @@ void test() { .verifyIssues(); } + @Test + void test_methodSource() { + CheckVerifier.newVerifier() + .onFile(TestUtils.testCodeSourcesPath("checks/tests/UnusedPrivateMethodSampleTest.java")) + .withCheck(new UnusedPrivateMethodCheck()) + .verifyIssues(); + } + @Test void test_non_compiling() { CheckVerifier.newVerifier() From 289f27d9595dd4b915bcba6b650079803453b758 Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Fri, 29 May 2026 16:33:07 +0200 Subject: [PATCH 2/2] Comment on assignment in annotation. --- .../org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java b/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java index 94a2fa19fa5..97b194b5954 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedPrivateMethodCheck.java @@ -316,10 +316,12 @@ public void visitAnnotation(AnnotationTree annotationTree) { var isMethodAnnotation = isNameIndicatingMethod(annotationTree.annotationType().symbolType().name()); for (var arg : annotationTree.arguments()) { if (arg instanceof AssignmentExpressionTree asgn) { + // Handle syntax with assignment, for example, @MethodSource(value = "methodName"). if (isMethodAnnotation || isNameIndicatingMethod(((IdentifierTree) asgn.variable()).name())) { removeMethodName(asgn.expression()); } } else if (isMethodAnnotation) { + // No assignment, for example: @MethodSource("methodName"). removeMethodName(arg); } }