From d439596a2f08e7e227a6b21c23f602b54ddddeec Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Fri, 21 Aug 2026 17:38:31 +0200 Subject: [PATCH 1/8] SONARJAVA-6827: Implement S9357 Anonymous classes on functional interfaces should be lambdas Detects anonymous inner classes implementing functional interfaces (single abstract method) that can be replaced with lambda expressions. Mirrors the detection logic of S1604 but applies to all code (main and test scope). Co-Authored-By: Claude Opus 4.6 --- ...ClassOnFunctionalInterfaceCheckSample.java | 310 ++++++++++++++++++ ...alInterfaceCheckSampleWithoutSemantic.java | 310 ++++++++++++++++++ ...nymousClassOnFunctionalInterfaceCheck.java | 188 +++++++++++ ...usClassOnFunctionalInterfaceCheckTest.java | 44 +++ .../org/sonar/l10n/java/rules/java/S9357.html | 72 ++++ .../org/sonar/l10n/java/rules/java/S9357.json | 25 ++ .../main/resources/profiles/Sonar_way/S9357 | 0 7 files changed, 949 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9357 diff --git a/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSample.java new file mode 100644 index 00000000000..67b1e226b17 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSample.java @@ -0,0 +1,310 @@ +package checks; + +class AnonymousClassOnFunctionalInterfaceCheckSample { + + enum Foo { + FOO { + @Override + public String method() { + return "foo"; + } + }, + BAR { + @Override + public String method() { + return "bar"; + } + }; + + public String method() { + return ""; + } + } + + interface Handler { + String handle(); + } + + interface MyInterface { + enum InnerEnum { + A, B, C; + } + } + + void toto() { + new MyInterface() {}; // Compliant + + new Handler(){ // Noncompliant {{Make this anonymous inner class a lambda}} + @Override + public String handle() { + return "handled"; + } + }.handle(); + + new Handler(){ + private String myMethod(){ + return "plop"; + } + + @Override + public String handle() { + return myMethod(); + } + }.handle(); + + new Handler(){ + @Override + public String handle() { + return this.toString(); + } + }.handle(); + + new Handler(){ // Noncompliant {{Make this anonymous inner class a lambda}} + @Override + public String handle() { + class C{ + String meth(){ + return ""; + } + String fun(){ + return this.meth(); + } + } + return new C().fun(); + } + }; + + new Handler(){ // Compliant + int myVar; + + @Override + public String handle() { + return ""; + } + }; + + new Handler(){ // Noncompliant {{Make this anonymous inner class a lambda}} + @Override + public String handle() { + return ""; + }; // this empty statement should not be counted! + }; + + new + Handler // Noncompliant {{Make this anonymous inner class a lambda}} + (){ + @Override + public String handle() { + return AnonymousClassOnFunctionalInterfaceCheckSample.this.toString(); + } + }; + + new Handler(){ // Compliant, annotation prevents transform to a lambda + @Override + @SuppressWarnings("something") + public String handle() { + return "handled"; + } + }.handle(); + + new Handler(){ // Compliant, annotation prevents transform to a lambda + @SuppressWarnings("something") + @Override + public String handle() { + return "handled"; + } + }.handle(); + } + + String toStr(){ + return ""; + } + + abstract static class AbstractClass { + public abstract void foo(); + + static void bar() { + AbstractClass ac1 = new AbstractClass() { // Compliant: not a SAM + @Override + public void foo() { + } + }; + } + } + + interface MyHandler extends Handler{} + + public abstract static class Main { + + public abstract void myMethod(); + + public static void main(String[] args) { + Main main = new Main() { + @Override + public void myMethod() { + } + }; + main.myMethod(); + Object o1 = new Object() { + @Override + public String toString(){ + return null; + } + }; + Object o12 = new MyHandler() { // Noncompliant + @Override + public String handle() { + return null; + } + }; + } + } +} + +class SamWithExceptionS9357 { + + class MyCheckedException extends Exception {} + interface I { + void apply(String s) throws MyCheckedException; + } + void foo(I i) { + foo(new I() { // Compliant: cannot refactor as lambda because of checked exception + @Override + public void apply(String s) throws MyCheckedException { + } + }); + } +} + +abstract class WithinLambdaS9357 { + + @FunctionalInterface + interface Action { + T run(); + } + + abstract T doSomething(Action action); + + private void bar(WithinLambdaS9357 a) { + a.doSomething( + (Action) () -> { + new Thread( + new Runnable() { // Noncompliant + @Override + public void run() { + } + }); + return null; + }); + } +} + +interface ABS9357 { + default void foo() { + } + + default void bar() { + } + + static void main() { + ABS9357 a = new ABS9357() { // Compliant + @Override + public void foo() { + } + }; + } +} + +interface BAS9357 { + default void foo() { + } + + void bar(); + + static void main() { + BAS9357 a = new BAS9357() { // Noncompliant + @Override + public void bar() { + } + }; + } +} + +class AlphaS9357 { + + interface Lvl1 { + void foo(); + } + + interface Lvl2 extends Lvl1 { + @Override + void foo(); + } + + Lvl2 level = new Lvl2() { // Noncompliant + @Override + public void foo() { + } + }; + Lvl2 level2 = () -> {}; +} + +class ThisInstanceTestS9357 { + + interface WithDefault { + default String defaultMethod() { return "defaultMethod"; } + String funcMethod(); + } + void testDefault() { + WithDefault f = new WithDefault() { // Compliant, invoke a default method + @Override + public String funcMethod() { + return defaultMethod(); + } + }; + } + + interface Math { + int powerOfTwo(int n); + } + + void testRecursion() { + Math f = new Math() { // Compliant, recursion + @Override + public int powerOfTwo(int n) { + return n == 0 ? 1 : 2 * powerOfTwo(n -1); + } + }; + } + + int globalPowerOfTwo(int n) { + return n == 0 ? 1 : 2 * globalPowerOfTwo(n -1); + } + + void testNotThisInstanceMethod() { + Math f = new Math() { // Noncompliant + @Override + public int powerOfTwo(int n) { + return globalPowerOfTwo(n); + } + }; + } +} + +abstract class GenericTypeS9357 { + + void foo(GenericTypeS9357 something) { + bar(something, new MyComparable() { // Compliant - compare is a generic method + @Override + public > int compare(T obj1, T obj2) { + return 0; + } + }); + } + + abstract > void bar(GenericTypeS9357 object, MyComparable comp); + + interface MyComparable { + > int compare(T obj1, T obj2); + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java b/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java new file mode 100644 index 00000000000..3a9756ac425 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java @@ -0,0 +1,310 @@ +package checks; + +class AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic { + + enum Foo { + FOO { + @Override + public String method() { + return "foo"; + } + }, + BAR { + @Override + public String method() { + return "bar"; + } + }; + + public String method() { + return ""; + } + } + + interface Handler { + String handle(); + } + + interface MyInterface { + enum InnerEnum { + A, B, C; + } + } + + void toto() { + new MyInterface() {}; // Compliant + + new Handler(){ // Noncompliant + @Override + public String handle() { + return "handled"; + } + }.handle(); + + new Handler(){ + private String myMethod(){ + return "plop"; + } + + @Override + public String handle() { + return myMethod(); + } + }.handle(); + + new Handler(){ + @Override + public String handle() { + return this.toString(); + } + }.handle(); + + new Handler(){ // Noncompliant + @Override + public String handle() { + class C{ + String meth(){ + return ""; + } + String fun(){ + return this.meth(); + } + } + return new C().fun(); + } + }; + + new Handler(){ // Compliant + int myVar; + + @Override + public String handle() { + return ""; + } + }; + + new Handler(){ // Noncompliant + @Override + public String handle() { + return ""; + }; // this empty statement should not be counted! + }; + + new + Handler // Noncompliant + (){ + @Override + public String handle() { + return AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.this.toString(); + } + }; + + new Handler(){ // Compliant, annotation prevents transform to a lambda + @Override + @SuppressWarnings("something") + public String handle() { + return "handled"; + } + }.handle(); + + new Handler(){ // Compliant, annotation prevents transform to a lambda + @SuppressWarnings("something") + @Override + public String handle() { + return "handled"; + } + }.handle(); + } + + String toStr(){ + return ""; + } + + abstract static class AbstractClass { + public abstract void foo(); + + static void bar() { + AbstractClass ac1 = new AbstractClass() { // Compliant: not a SAM + @Override + public void foo() { + } + }; + } + } + + interface MyHandler extends Handler{} + + public abstract static class Main { + + public abstract void myMethod(); + + public static void main(String[] args) { + Main main = new Main() { + @Override + public void myMethod() { + } + }; + main.myMethod(); + Object o1 = new Object() { + @Override + public String toString(){ + return null; + } + }; + Object o12 = new MyHandler() { // Noncompliant + @Override + public String handle() { + return null; + } + }; + } + } +} + +class SamWithExceptionS9357WS { + + class MyCheckedException extends Exception {} + interface I { + void apply(String s) throws MyCheckedException; + } + void foo(I i) { + foo(new I() { // Compliant: checked exception + @Override + public void apply(String s) throws MyCheckedException { + } + }); + } +} + +abstract class WithinLambdaS9357WS { + + @FunctionalInterface + interface Action { + T run(); + } + + abstract T doSomething(Action action); + + private void bar(WithinLambdaS9357WS a) { + a.doSomething( + (Action) () -> { + new Thread( + new Runnable() { // Noncompliant + @Override + public void run() { + } + }); + return null; + }); + } +} + +interface ABS9357WS { + default void foo() { + } + + default void bar() { + } + + static void main() { + ABS9357WS a = new ABS9357WS() { // Compliant + @Override + public void foo() { + } + }; + } +} + +interface BAS9357WS { + default void foo() { + } + + void bar(); + + static void main() { + BAS9357WS a = new BAS9357WS() { // Noncompliant + @Override + public void bar() { + } + }; + } +} + +class AlphaS9357WS { + + interface Lvl1 { + void foo(); + } + + interface Lvl2 extends Lvl1 { + @Override + void foo(); + } + + Lvl2 level = new Lvl2() { // Noncompliant + @Override + public void foo() { + } + }; + Lvl2 level2 = () -> {}; +} + +class ThisInstanceTestS9357WS { + + interface WithDefault { + default String defaultMethod() { return "defaultMethod"; } + String funcMethod(); + } + void testDefault() { + WithDefault f = new WithDefault() { // Compliant, invoke a default method + @Override + public String funcMethod() { + return defaultMethod(); + } + }; + } + + interface Math { + int powerOfTwo(int n); + } + + void testRecursion() { + Math f = new Math() { // Compliant, recursion + @Override + public int powerOfTwo(int n) { + return n == 0 ? 1 : 2 * powerOfTwo(n -1); + } + }; + } + + int globalPowerOfTwo(int n) { + return n == 0 ? 1 : 2 * globalPowerOfTwo(n -1); + } + + void testNotThisInstanceMethod() { + Math f = new Math() { // Noncompliant + @Override + public int powerOfTwo(int n) { + return globalPowerOfTwo(n); + } + }; + } +} + +abstract class GenericTypeS9357WS { + + void foo(GenericTypeS9357WS something) { + bar(something, new MyComparable() { // Compliant - compare is a generic method + @Override + public > int compare(T obj1, T obj2) { + return 0; + } + }); + } + + abstract > void bar(GenericTypeS9357WS object, MyComparable comp); + + interface MyComparable { + > int compare(T obj1, T obj2); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java new file mode 100644 index 00000000000..91d35f99197 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java @@ -0,0 +1,188 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.JavaFileScanner; +import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.EnumConstantTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.NewClassTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeTree; + +@Rule(key = "S9357") +public class AnonymousClassOnFunctionalInterfaceCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { + + private static final String JAVA_LANG_OBJECT = "java.lang.Object"; + private JavaFileScannerContext context; + private final Set enumConstants = new HashSet<>(); + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava8Compatible(); + } + + @Override + public void scanFile(JavaFileScannerContext context) { + this.context = context; + enumConstants.clear(); + scan(context.getTree()); + } + + @Override + public void visitEnumConstant(EnumConstantTree tree) { + enumConstants.add(tree.simpleName()); + super.visitEnumConstant(tree); + enumConstants.remove(tree.simpleName()); + } + + @Override + public void visitNewClass(NewClassTree tree) { + super.visitNewClass(tree); + ClassTree classBody = tree.classBody(); + if (classBody != null) { + TypeTree identifier = tree.identifier(); + if (!useThisInstance(classBody) && !enumConstants.contains(identifier) && isSAM(classBody)) { + context.reportIssue(this, identifier, "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); + } + } + } + + private static boolean isSAM(ClassTree classBody) { + if (hasOnlyOneMethod(classBody.members())) { + Symbol.TypeSymbol symbol = classBody.symbol(); + return symbol.interfaces().size() == 1 + && symbol.superClass().is(JAVA_LANG_OBJECT) + && hasSingleAbstractMethodInHierarchy(symbol.superTypes()); + } + return false; + } + + private static boolean hasSingleAbstractMethodInHierarchy(Set superTypes) { + return superTypes.stream() + .filter(type -> !type.is(JAVA_LANG_OBJECT)) + .map(Type::symbol) + .flatMap(superType -> superType.memberSymbols().stream().filter(Symbol::isMethodSymbol).filter(Symbol::isAbstract)) + .map(Symbol.MethodSymbol.class::cast) + .filter(symbol -> !isObjectMethod(symbol)) + .filter(symbol -> !symbol.isParametrizedMethod()) + .map(AnonymousClassOnFunctionalInterfaceCheck::overriddenSymbolIfAny) + .collect(Collectors.toSet()) + .size() == 1; + } + + private static Symbol.MethodSymbol overriddenSymbolIfAny(MethodSymbol symbol) { + return symbol.overriddenSymbols().stream() + .findFirst() + .orElse(symbol); + } + + private static boolean isObjectMethod(Symbol.MethodSymbol methodSymbol) { + return methodSymbol.overriddenSymbols().stream() + .map(Symbol::owner) + .map(Symbol::type) + .anyMatch(t -> t.is(JAVA_LANG_OBJECT)); + } + + private static boolean hasOnlyOneMethod(List members) { + MethodTree methodTree = null; + for (Tree tree : members) { + if (!tree.is(Tree.Kind.EMPTY_STATEMENT, Tree.Kind.METHOD)) { + return false; + } + if (tree.is(Tree.Kind.METHOD)) { + if (methodTree != null) { + return false; + } + methodTree = (MethodTree) tree; + } + } + return methodTree != null && canRefactorMethod(methodTree); + } + + private static boolean canRefactorMethod(MethodTree methodTree) { + return methodTree.throwsClauses().isEmpty() + && methodTree.symbol().metadata().annotations().stream() + .allMatch(annotation -> annotation.symbol().type().is("java.lang.Override")); + } + + private static boolean useThisInstance(ClassTree body) { + UsesThisInstanceVisitor visitor = new UsesThisInstanceVisitor(body.symbol().type()); + body.accept(visitor); + return visitor.usesThisInstance; + } + + private static class UsesThisInstanceVisitor extends BaseTreeVisitor { + private final Type instanceType; + boolean usesThisInstance = false; + boolean visitedClassTree = false; + + public UsesThisInstanceVisitor(Type instanceType) { + this.instanceType = instanceType; + } + + @Override + public void visitClass(ClassTree tree) { + if (!visitedClassTree) { + visitedClassTree = true; + super.visitClass(tree); + } + } + + @Override + public void visitNewClass(NewClassTree tree) { + // ignore anonymous classes + } + + @Override + public void visitMemberSelectExpression(MemberSelectExpressionTree tree) { + scan(tree.expression()); + } + + @Override + public void visitMethodInvocation(MethodInvocationTree tree) { + if (tree.methodSelect().is(Tree.Kind.IDENTIFIER)) { + Symbol symbol = ((IdentifierTree) tree.methodSelect()).symbol(); + usesThisInstance |= symbol.isMethodSymbol() && + !symbol.isStatic() && + instanceType.isSubtypeOf(symbol.owner().type()); + } + super.visitMethodInvocation(tree); + } + + @Override + public void visitIdentifier(IdentifierTree tree) { + usesThisInstance |= "this".equals(tree.name()); + } + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java new file mode 100644 index 00000000000..c442f243eac --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class AnonymousClassOnFunctionalInterfaceCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AnonymousClassOnFunctionalInterfaceCheckSample.java")) + .withCheck(new AnonymousClassOnFunctionalInterfaceCheck()) + .withJavaVersion(8) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java")) + .withCheck(new AnonymousClassOnFunctionalInterfaceCheck()) + .withJavaVersion(8) + .withoutSemantic() + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.html new file mode 100644 index 00000000000..b070e3d3ba4 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.html @@ -0,0 +1,72 @@ +

This rule raises an issue when you create an anonymous inner class that implements a functional interface (an interface with exactly one abstract +method). In Java, these anonymous inner classes should be replaced with lambda expressions, which provide a cleaner and more concise syntax for the +same functionality.

+

Why is this an issue?

+

A single-method interface is a type that defines exactly one abstract behavior contract. Common examples include interfaces for tasks, +computations, comparisons, and various transformation operations.

+

Before modern versions of the language introduced simplified syntax, the only way to implement a single-method interface was through verbose inline +type implementations. This approach requires significant boilerplate code that obscures the actual logic:

+
+taskVariable = new InterfaceImplementation() {
+    @Override
+    public void methodName() {
+        processData();
+    }
+};
+
+

Modern language versions introduced inline function syntax specifically to simplify this pattern. An inline function literal is a concise way to +represent a single-method interface using an expression. The same functionality can be written as:

+
+taskVariable = () -> processData();
+
+

Using inline function syntax instead of verbose type implementations offers several benefits:

+
    +
  • Reduced boilerplate: Inline functions eliminate the need to repeat the interface name, method name, and method override + annotations.
  • +
  • Improved readability: The code focuses on what the method does rather than the mechanics of creating an inline + implementation.
  • +
  • Better maintenance: Less code means fewer opportunities for errors and easier refactoring.
  • +
  • Modern style: Inline function syntax is the idiomatic way to work with single-method interfaces in modern versions of the + language.
  • +
+

The conversion from verbose inline implementation to concise function syntax is straightforward because the compiler can infer the interface type +from the context. This type inference is what makes inline functions so concise.

+

In Java, these single-method interfaces are called functional interfaces. Common examples include Runnable, +Callable, Comparator, and the many interfaces in the java.util.function package. Lambda expressions were +introduced in Java 8 as the standard syntax for implementing functional interfaces concisely.

+

When you use lambda expressions instead of anonymous inner classes:

+
    +
  • Your code becomes more readable and easier to maintain
  • +
  • You reduce boilerplate code significantly
  • +
  • You make your intent clearer to other developers
  • +
  • You follow modern Java conventions and best practices
  • +
+

What is the potential impact?

+

Using older verbose syntax for defining inline behavior instead of modern concise functional syntax makes the code more verbose and harder to read. +While this doesn’t affect the runtime behavior or security of the application, it impacts code maintainability. Developers spend more time reading and +understanding unnecessarily complex code, which slows down development and increases the likelihood of introducing errors during modifications.

+

How to fix it

+

Replace the anonymous inner class with a lambda expression. The lambda syntax uses parameters in parentheses, an arrow , and the +method body. For single-expression bodies, you can omit the braces and return keyword.

+

Code examples

+

Noncompliant code example

+
+Runnable task = new Runnable() {
+    @Override
+    public void run() {
+        System.out.println("Processing...");
+    }
+}; // Noncompliant
+
+

Compliant solution

+
+Runnable task = () -> System.out.println("Processing...");
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json new file mode 100644 index 00000000000..51d00e67bbc --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json @@ -0,0 +1,25 @@ +{ + "title": "Anonymous classes on functional interfaces should be lambdas", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "lambda", + "java8", + "convention" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9357", + "sqKey": "S9357", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CLEAR" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9357 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9357 new file mode 100644 index 00000000000..e69de29bb2d From ed59ed29a95ce84228fe19d63c93e14cd30b862c Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 24 Aug 2026 10:07:12 +0200 Subject: [PATCH 2/8] SONARJAVA-6827: Refactor S9357 to eliminate duplication with S1604 Extract shared detection logic into AbstractAnonymousClassToLambdaCheck base class used by both AnonymousClassOnFunctionalInterfaceCheck (S9357) and AnonymousClassShouldBeLambdaCheck (S1604). Change S9357 scope from "All" to "Tests" to avoid double-reporting on main code where S1604 already applies. Move S9357 test samples to test source path accordingly. Co-Authored-By: Claude Opus 4.6 --- ...ClassOnFunctionalInterfaceCheckSample.java | 2 +- ...alInterfaceCheckSampleWithoutSemantic.java | 2 +- .../AbstractAnonymousClassToLambdaCheck.java | 197 ++++++++++++++++++ ...nymousClassOnFunctionalInterfaceCheck.java | 167 +-------------- .../AnonymousClassShouldBeLambdaCheck.java | 180 +--------------- ...usClassOnFunctionalInterfaceCheckTest.java | 6 +- .../org/sonar/l10n/java/rules/java/S9357.json | 2 +- 7 files changed, 205 insertions(+), 351 deletions(-) rename java-checks-test-sources/default/src/{main/java/checks => test/java/checks/tests}/AnonymousClassOnFunctionalInterfaceCheckSample.java (99%) rename java-checks-test-sources/default/src/{main/java/checks => test/java/checks/tests}/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java (99%) create mode 100644 java-checks/src/main/java/org/sonar/java/checks/AbstractAnonymousClassToLambdaCheck.java diff --git a/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSample.java b/java-checks-test-sources/default/src/test/java/checks/tests/AnonymousClassOnFunctionalInterfaceCheckSample.java similarity index 99% rename from java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSample.java rename to java-checks-test-sources/default/src/test/java/checks/tests/AnonymousClassOnFunctionalInterfaceCheckSample.java index 67b1e226b17..274f73e1eb8 100644 --- a/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSample.java +++ b/java-checks-test-sources/default/src/test/java/checks/tests/AnonymousClassOnFunctionalInterfaceCheckSample.java @@ -1,4 +1,4 @@ -package checks; +package checks.tests; class AnonymousClassOnFunctionalInterfaceCheckSample { diff --git a/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java b/java-checks-test-sources/default/src/test/java/checks/tests/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java similarity index 99% rename from java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java rename to java-checks-test-sources/default/src/test/java/checks/tests/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java index 3a9756ac425..441b561eb73 100644 --- a/java-checks-test-sources/default/src/main/java/checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java +++ b/java-checks-test-sources/default/src/test/java/checks/tests/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java @@ -1,4 +1,4 @@ -package checks; +package checks.tests; class AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic { diff --git a/java-checks/src/main/java/org/sonar/java/checks/AbstractAnonymousClassToLambdaCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AbstractAnonymousClassToLambdaCheck.java new file mode 100644 index 00000000000..0573000cdcc --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/AbstractAnonymousClassToLambdaCheck.java @@ -0,0 +1,197 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.plugins.java.api.JavaFileScanner; +import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.EnumConstantTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.NewClassTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeTree; + +public abstract class AbstractAnonymousClassToLambdaCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { + + private static final String JAVA_LANG_OBJECT = "java.lang.Object"; + private JavaFileScannerContext context; + private final Set enumConstants = new HashSet<>(); + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava8Compatible(); + } + + @Override + public void scanFile(JavaFileScannerContext context) { + this.context = context; + enumConstants.clear(); + scan(context.getTree()); + } + + @Override + public void visitEnumConstant(EnumConstantTree tree) { + enumConstants.add(tree.simpleName()); + super.visitEnumConstant(tree); + enumConstants.remove(tree.simpleName()); + } + + @Override + public void visitNewClass(NewClassTree tree) { + super.visitNewClass(tree); + ClassTree classBody = tree.classBody(); + if (classBody != null) { + TypeTree identifier = tree.identifier(); + if (!useThisInstance(classBody) && !enumConstants.contains(identifier) && isSAM(classBody)) { + context.reportIssue(this, identifier, "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); + } + } + } + + private static boolean isSAM(ClassTree classBody) { + if (hasOnlyOneMethod(classBody.members())) { + // When overriding only one method of a functional interface, it can only be the single abstract method + // and not one of the default methods. No need to check that the method signature matches. + Symbol.TypeSymbol symbol = classBody.symbol(); + // should be anonymous class of interface and not abstract class + return symbol.interfaces().size() == 1 + && symbol.superClass().is(JAVA_LANG_OBJECT) + && hasSingleAbstractMethodInHierarchy(symbol.superTypes()); + } + return false; + } + + private static boolean hasSingleAbstractMethodInHierarchy(Set superTypes) { + return superTypes.stream() + .filter(type -> !type.is(JAVA_LANG_OBJECT)) + .map(Type::symbol) + // collect all the methods declared in hierarchy + .flatMap(superType -> superType.memberSymbols().stream().filter(Symbol::isMethodSymbol).filter(Symbol::isAbstract)) + .map(Symbol.MethodSymbol.class::cast) + // remove objects methods redefined in interfaces + .filter(symbol -> !isObjectMethod(symbol)) + // remove generic methods, which can not be written as lambda (JLS-11 §15.27) + .filter(symbol -> !symbol.isParametrizedMethod()) + // always take same symbol if method is redeclared over and over in hierarchy + .map(AbstractAnonymousClassToLambdaCheck::overriddenSymbolIfAny) + .collect(Collectors.toSet()) + .size() == 1; + } + + private static Symbol.MethodSymbol overriddenSymbolIfAny(MethodSymbol symbol) { + return symbol.overriddenSymbols().stream() + .findFirst() + .orElse(symbol); + } + + private static boolean isObjectMethod(Symbol.MethodSymbol methodSymbol) { + return methodSymbol.overriddenSymbols().stream() + .map(Symbol::owner) + .map(Symbol::type) + .anyMatch(t -> t.is(JAVA_LANG_OBJECT)); + } + + private static boolean hasOnlyOneMethod(List members) { + MethodTree methodTree = null; + for (Tree tree : members) { + if (!tree.is(Tree.Kind.EMPTY_STATEMENT, Tree.Kind.METHOD)) { + return false; + } + if (tree.is(Tree.Kind.METHOD)) { + if (methodTree != null) { + return false; + } + methodTree = (MethodTree) tree; + } + } + return methodTree != null && canRefactorMethod(methodTree); + } + + private static boolean canRefactorMethod(MethodTree methodTree) { + // if overridden method declares to throw an exception, refactoring to a lambda might prove tricky + // if it is annotated with something else than @Override, it is not possible to refactor the code + return methodTree.throwsClauses().isEmpty() + && methodTree.symbol().metadata().annotations().stream() + .allMatch(annotation -> annotation.symbol().type().is("java.lang.Override")); + } + + private static boolean useThisInstance(ClassTree body) { + UsesThisInstanceVisitor visitor = new UsesThisInstanceVisitor(body.symbol().type()); + body.accept(visitor); + return visitor.usesThisInstance; + } + + private static class UsesThisInstanceVisitor extends BaseTreeVisitor { + private final Type instanceType; + boolean usesThisInstance = false; + boolean visitedClassTree = false; + + public UsesThisInstanceVisitor(Type instanceType) { + this.instanceType = instanceType; + } + + @Override + public void visitClass(ClassTree tree) { + // visit the class body but ignore inner classes + if (!visitedClassTree) { + visitedClassTree = true; + super.visitClass(tree); + } + } + + @Override + public void visitNewClass(NewClassTree tree) { + // ignore anonymous classes + } + + @Override + public void visitMemberSelectExpression(MemberSelectExpressionTree tree) { + scan(tree.expression()); + // ignore identifier, because if it is this, it is a qualified this. + } + + @Override + public void visitMethodInvocation(MethodInvocationTree tree) { + if (tree.methodSelect().is(Tree.Kind.IDENTIFIER)) { + Symbol symbol = ((IdentifierTree) tree.methodSelect()).symbol(); + usesThisInstance |= symbol.isMethodSymbol() && + !symbol.isStatic() && + instanceType.isSubtypeOf(symbol.owner().type()); + } + super.visitMethodInvocation(tree); + } + + @Override + public void visitIdentifier(IdentifierTree tree) { + usesThisInstance |= "this".equals(tree.name()); + } + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java index 91d35f99197..c7377de90a9 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java @@ -16,173 +16,8 @@ */ package org.sonar.java.checks; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import org.sonar.check.Rule; -import org.sonar.plugins.java.api.JavaFileScanner; -import org.sonar.plugins.java.api.JavaFileScannerContext; -import org.sonar.plugins.java.api.JavaVersion; -import org.sonar.plugins.java.api.JavaVersionAwareVisitor; -import org.sonar.plugins.java.api.semantic.Symbol; -import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol; -import org.sonar.plugins.java.api.semantic.Type; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; -import org.sonar.plugins.java.api.tree.ClassTree; -import org.sonar.plugins.java.api.tree.EnumConstantTree; -import org.sonar.plugins.java.api.tree.IdentifierTree; -import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; -import org.sonar.plugins.java.api.tree.MethodInvocationTree; -import org.sonar.plugins.java.api.tree.MethodTree; -import org.sonar.plugins.java.api.tree.NewClassTree; -import org.sonar.plugins.java.api.tree.Tree; -import org.sonar.plugins.java.api.tree.TypeTree; @Rule(key = "S9357") -public class AnonymousClassOnFunctionalInterfaceCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { - - private static final String JAVA_LANG_OBJECT = "java.lang.Object"; - private JavaFileScannerContext context; - private final Set enumConstants = new HashSet<>(); - - @Override - public boolean isCompatibleWithJavaVersion(JavaVersion version) { - return version.isJava8Compatible(); - } - - @Override - public void scanFile(JavaFileScannerContext context) { - this.context = context; - enumConstants.clear(); - scan(context.getTree()); - } - - @Override - public void visitEnumConstant(EnumConstantTree tree) { - enumConstants.add(tree.simpleName()); - super.visitEnumConstant(tree); - enumConstants.remove(tree.simpleName()); - } - - @Override - public void visitNewClass(NewClassTree tree) { - super.visitNewClass(tree); - ClassTree classBody = tree.classBody(); - if (classBody != null) { - TypeTree identifier = tree.identifier(); - if (!useThisInstance(classBody) && !enumConstants.contains(identifier) && isSAM(classBody)) { - context.reportIssue(this, identifier, "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); - } - } - } - - private static boolean isSAM(ClassTree classBody) { - if (hasOnlyOneMethod(classBody.members())) { - Symbol.TypeSymbol symbol = classBody.symbol(); - return symbol.interfaces().size() == 1 - && symbol.superClass().is(JAVA_LANG_OBJECT) - && hasSingleAbstractMethodInHierarchy(symbol.superTypes()); - } - return false; - } - - private static boolean hasSingleAbstractMethodInHierarchy(Set superTypes) { - return superTypes.stream() - .filter(type -> !type.is(JAVA_LANG_OBJECT)) - .map(Type::symbol) - .flatMap(superType -> superType.memberSymbols().stream().filter(Symbol::isMethodSymbol).filter(Symbol::isAbstract)) - .map(Symbol.MethodSymbol.class::cast) - .filter(symbol -> !isObjectMethod(symbol)) - .filter(symbol -> !symbol.isParametrizedMethod()) - .map(AnonymousClassOnFunctionalInterfaceCheck::overriddenSymbolIfAny) - .collect(Collectors.toSet()) - .size() == 1; - } - - private static Symbol.MethodSymbol overriddenSymbolIfAny(MethodSymbol symbol) { - return symbol.overriddenSymbols().stream() - .findFirst() - .orElse(symbol); - } - - private static boolean isObjectMethod(Symbol.MethodSymbol methodSymbol) { - return methodSymbol.overriddenSymbols().stream() - .map(Symbol::owner) - .map(Symbol::type) - .anyMatch(t -> t.is(JAVA_LANG_OBJECT)); - } - - private static boolean hasOnlyOneMethod(List members) { - MethodTree methodTree = null; - for (Tree tree : members) { - if (!tree.is(Tree.Kind.EMPTY_STATEMENT, Tree.Kind.METHOD)) { - return false; - } - if (tree.is(Tree.Kind.METHOD)) { - if (methodTree != null) { - return false; - } - methodTree = (MethodTree) tree; - } - } - return methodTree != null && canRefactorMethod(methodTree); - } - - private static boolean canRefactorMethod(MethodTree methodTree) { - return methodTree.throwsClauses().isEmpty() - && methodTree.symbol().metadata().annotations().stream() - .allMatch(annotation -> annotation.symbol().type().is("java.lang.Override")); - } - - private static boolean useThisInstance(ClassTree body) { - UsesThisInstanceVisitor visitor = new UsesThisInstanceVisitor(body.symbol().type()); - body.accept(visitor); - return visitor.usesThisInstance; - } - - private static class UsesThisInstanceVisitor extends BaseTreeVisitor { - private final Type instanceType; - boolean usesThisInstance = false; - boolean visitedClassTree = false; - - public UsesThisInstanceVisitor(Type instanceType) { - this.instanceType = instanceType; - } - - @Override - public void visitClass(ClassTree tree) { - if (!visitedClassTree) { - visitedClassTree = true; - super.visitClass(tree); - } - } - - @Override - public void visitNewClass(NewClassTree tree) { - // ignore anonymous classes - } - - @Override - public void visitMemberSelectExpression(MemberSelectExpressionTree tree) { - scan(tree.expression()); - } - - @Override - public void visitMethodInvocation(MethodInvocationTree tree) { - if (tree.methodSelect().is(Tree.Kind.IDENTIFIER)) { - Symbol symbol = ((IdentifierTree) tree.methodSelect()).symbol(); - usesThisInstance |= symbol.isMethodSymbol() && - !symbol.isStatic() && - instanceType.isSubtypeOf(symbol.owner().type()); - } - super.visitMethodInvocation(tree); - } - - @Override - public void visitIdentifier(IdentifierTree tree) { - usesThisInstance |= "this".equals(tree.name()); - } - } - +public class AnonymousClassOnFunctionalInterfaceCheck extends AbstractAnonymousClassToLambdaCheck { } diff --git a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java index 3881452ba3d..1c4f9d53019 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java @@ -16,186 +16,8 @@ */ package org.sonar.java.checks; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; import org.sonar.check.Rule; -import org.sonar.plugins.java.api.JavaFileScanner; -import org.sonar.plugins.java.api.JavaFileScannerContext; -import org.sonar.plugins.java.api.JavaVersion; -import org.sonar.plugins.java.api.JavaVersionAwareVisitor; -import org.sonar.plugins.java.api.semantic.Symbol; -import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol; -import org.sonar.plugins.java.api.semantic.Type; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; -import org.sonar.plugins.java.api.tree.ClassTree; -import org.sonar.plugins.java.api.tree.EnumConstantTree; -import org.sonar.plugins.java.api.tree.IdentifierTree; -import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; -import org.sonar.plugins.java.api.tree.MethodInvocationTree; -import org.sonar.plugins.java.api.tree.MethodTree; -import org.sonar.plugins.java.api.tree.NewClassTree; -import org.sonar.plugins.java.api.tree.Tree; -import org.sonar.plugins.java.api.tree.TypeTree; @Rule(key = "S1604") -public class AnonymousClassShouldBeLambdaCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { - - private static final String JAVA_LANG_OBJECT = "java.lang.Object"; - private JavaFileScannerContext context; - private final Set enumConstants = new HashSet<>(); - - @Override - public boolean isCompatibleWithJavaVersion(JavaVersion version) { - return version.isJava8Compatible(); - } - - @Override - public void scanFile(JavaFileScannerContext context) { - this.context = context; - enumConstants.clear(); - scan(context.getTree()); - } - - @Override - public void visitEnumConstant(EnumConstantTree tree) { - enumConstants.add(tree.simpleName()); - super.visitEnumConstant(tree); - enumConstants.remove(tree.simpleName()); - } - - @Override - public void visitNewClass(NewClassTree tree) { - super.visitNewClass(tree); - ClassTree classBody = tree.classBody(); - if (classBody != null) { - TypeTree identifier = tree.identifier(); - if (!useThisInstance(classBody) && !enumConstants.contains(identifier) && isSAM(classBody)) { - context.reportIssue(this, identifier, "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); - } - } - } - - private static boolean isSAM(ClassTree classBody) { - if (hasOnlyOneMethod(classBody.members())) { - // When overriding only one method of a functional interface, it can only be the single abstract method - // and not one of the default methods. No need to check that the method signature matches. - Symbol.TypeSymbol symbol = classBody.symbol(); - // should be anonymous class of interface and not abstract class - return symbol.interfaces().size() == 1 - && symbol.superClass().is(JAVA_LANG_OBJECT) - && hasSingleAbstractMethodInHierarchy(symbol.superTypes()); - } - return false; - } - - private static boolean hasSingleAbstractMethodInHierarchy(Set superTypes) { - return superTypes.stream() - .filter(type -> !type.is(JAVA_LANG_OBJECT)) - .map(Type::symbol) - // collect all the methods declared in hierarchy - .flatMap(superType -> superType.memberSymbols().stream().filter(Symbol::isMethodSymbol).filter(Symbol::isAbstract)) - .map(Symbol.MethodSymbol.class::cast) - // remove objects methods redefined in interfaces - .filter(symbol -> !isObjectMethod(symbol)) - // remove generic methods, which can not be written as lambda (JLS-11 §15.27) - .filter(symbol -> !symbol.isParametrizedMethod()) - // always take same symbol if method is redeclared over and over in hierarchy - .map(AnonymousClassShouldBeLambdaCheck::overriddenSymbolIfAny) - .collect(Collectors.toSet()) - .size() == 1; - } - - private static Symbol.MethodSymbol overriddenSymbolIfAny(MethodSymbol symbol) { - return symbol.overriddenSymbols().stream() - .findFirst() - .orElse(symbol); - } - - private static boolean isObjectMethod(Symbol.MethodSymbol methodSymbol) { - return methodSymbol.overriddenSymbols().stream() - .map(Symbol::owner) - .map(Symbol::type) - .anyMatch(t -> t.is(JAVA_LANG_OBJECT)); - } - - private static boolean hasOnlyOneMethod(List members) { - MethodTree methodTree = null; - for (Tree tree : members) { - if (!tree.is(Tree.Kind.EMPTY_STATEMENT, Tree.Kind.METHOD)) { - return false; - } - if (tree.is(Tree.Kind.METHOD)) { - if (methodTree != null) { - return false; - } - methodTree = (MethodTree) tree; - - } - } - - return methodTree != null && canRefactorMethod(methodTree); - } - - private static boolean canRefactorMethod(MethodTree methodTree) { - // if overridden method declares to throw an exception, refactoring to a lambda might prove tricky - // if it is annotated with something else than @Override, it is not possible to refactor the code - return methodTree.throwsClauses().isEmpty() - && methodTree.symbol().metadata().annotations().stream() - .allMatch(annotation -> annotation.symbol().type().is("java.lang.Override")); - } - - private static boolean useThisInstance(ClassTree body) { - UsesThisInstanceVisitor visitor = new UsesThisInstanceVisitor(body.symbol().type()); - body.accept(visitor); - return visitor.usesThisInstance; - } - - private static class UsesThisInstanceVisitor extends BaseTreeVisitor { - private final Type instanceType; - boolean usesThisInstance = false; - boolean visitedClassTree = false; - - public UsesThisInstanceVisitor(Type instanceType) { - this.instanceType = instanceType; - } - - @Override - public void visitClass(ClassTree tree) { - // visit the class body but ignore inner classes - if (!visitedClassTree) { - visitedClassTree = true; - super.visitClass(tree); - } - } - - @Override - public void visitNewClass(NewClassTree tree) { - // ignore anonymous classes - } - - @Override - public void visitMemberSelectExpression(MemberSelectExpressionTree tree) { - scan(tree.expression()); - // ignore identifier, because if it is this, it is a qualified this. - } - - @Override - public void visitMethodInvocation(MethodInvocationTree tree) { - if (tree.methodSelect().is(Tree.Kind.IDENTIFIER)) { - Symbol symbol = ((IdentifierTree) tree.methodSelect()).symbol(); - usesThisInstance |= symbol.isMethodSymbol() && - !symbol.isStatic() && - instanceType.isSubtypeOf(symbol.owner().type()); - } - super.visitMethodInvocation(tree); - } - - @Override - public void visitIdentifier(IdentifierTree tree) { - usesThisInstance |= "this".equals(tree.name()); - } - } - +public class AnonymousClassShouldBeLambdaCheck extends AbstractAnonymousClassToLambdaCheck { } diff --git a/java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java index c442f243eac..f575c3a9bd1 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheckTest.java @@ -19,14 +19,14 @@ import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; -import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +import static org.sonar.java.checks.verifier.TestUtils.testCodeSourcesPath; class AnonymousClassOnFunctionalInterfaceCheckTest { @Test void test() { CheckVerifier.newVerifier() - .onFile(mainCodeSourcesPath("checks/AnonymousClassOnFunctionalInterfaceCheckSample.java")) + .onFile(testCodeSourcesPath("checks/tests/AnonymousClassOnFunctionalInterfaceCheckSample.java")) .withCheck(new AnonymousClassOnFunctionalInterfaceCheck()) .withJavaVersion(8) .verifyIssues(); @@ -35,7 +35,7 @@ void test() { @Test void test_without_semantic() { CheckVerifier.newVerifier() - .onFile(mainCodeSourcesPath("checks/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java")) + .onFile(testCodeSourcesPath("checks/tests/AnonymousClassOnFunctionalInterfaceCheckSampleWithoutSemantic.java")) .withCheck(new AnonymousClassOnFunctionalInterfaceCheck()) .withJavaVersion(8) .withoutSemantic() diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json index 51d00e67bbc..06f2bcd0e81 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json @@ -14,7 +14,7 @@ "defaultSeverity": "Major", "ruleSpecification": "RSPEC-9357", "sqKey": "S9357", - "scope": "All", + "scope": "Tests", "quickfix": "unknown", "code": { "impacts": { From 22af25b5ac41c21225d63a15614cbd52d3c036ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:24:44 +0100 Subject: [PATCH 3/8] Update ruling results for PR #5995 (#5996) Co-authored-by: github-actions[bot] --- .../commons-beanutils/java-S9357.json | 5 ++ .../resources/eclipse-jetty/java-S9357.json | 47 +++++++++++ .../resources/sonar-server/java-S9357.json | 79 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 its/ruling/src/test/resources/commons-beanutils/java-S9357.json create mode 100644 its/ruling/src/test/resources/eclipse-jetty/java-S9357.json create mode 100644 its/ruling/src/test/resources/sonar-server/java-S9357.json diff --git a/its/ruling/src/test/resources/commons-beanutils/java-S9357.json b/its/ruling/src/test/resources/commons-beanutils/java-S9357.json new file mode 100644 index 00000000000..8dec2c78df4 --- /dev/null +++ b/its/ruling/src/test/resources/commons-beanutils/java-S9357.json @@ -0,0 +1,5 @@ +{ +"commons-beanutils:commons-beanutils:src/test/java/org/apache/commons/beanutils2/bugs/Jira509TestCase.java": [ +60 +] +} diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9357.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9357.json new file mode 100644 index 00000000000..a234f34288b --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9357.json @@ -0,0 +1,47 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java": [ +150, +203, +238, +264, +295, +331, +526, +562, +707, +764, +1600, +1702, +1748, +1998 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/ssl/SSLEngineTest.java": [ +285 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/IteratingCallbackTest.java": [ +219, +293, +301 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/SharedBlockingCallbackTest.java": [ +77, +139, +177 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/thread/SchedulerTest.java": [ +85, +107, +123, +144, +165, +188, +202 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/thread/SweeperTest.java": [ +77, +113 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/test/java/org/eclipse/jetty/util/thread/strategy/ExecutionStrategyTest.java": [ +188 +] +} diff --git a/its/ruling/src/test/resources/sonar-server/java-S9357.json b/its/ruling/src/test/resources/sonar-server/java-S9357.json new file mode 100644 index 00000000000..ab90c0f4e03 --- /dev/null +++ b/its/ruling/src/test/resources/sonar-server/java-S9357.json @@ -0,0 +1,79 @@ +{ +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/container/TaskContainerImplTest.java": [ +64 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/component/CrawlerDepthLimitTest.java": [ +46, +52 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulatorTest.java": [ +184 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/measure/MapBasedRawMeasureRepositoryTest.java": [ +86, +210 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/measure/MeasureRepositoryImplTest.java": [ +267 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/measure/MeasureRepositoryRule.java": [ +65 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/measure/MeasureTest.java": [ +98 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/source/SourceHashRepositoryImplTest.java": [ +83 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/step/PurgeDatastoresStepTest.java": [ +93, +111 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportComputationStepsTest.java": [ +42, +63 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/duplication/ws/DuplicationsParserTest.java": [ +235 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/es/EsTester.java": [ +168 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java": [ +259 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/notification/NotificationDaemonTest.java": [ +252 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java": [ +55 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/qualityprofile/ws/CompareActionMediumTest.java": [ +54 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/rule/RegisterRulesMediumTest.java": [ +118, +135, +149, +183, +205, +230, +265, +290, +320, +356, +380, +403 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/rule/RegisterRulesTest.java": [ +252, +268, +287, +299, +322, +334 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/util/AbstractStoppableExecutorServiceTest.java": [ +44 +] +} From 2bc57e07afc93b0b7cf21fc51a0eb39eba2a7b19 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 24 Aug 2026 10:25:05 +0200 Subject: [PATCH 4/8] SONARJAVA-6827: Add missing 'tests' tag to S9357 metadata Rules with scope "Tests" must include the "tests" tag to pass GeneratedCheckListTest validation. Co-Authored-By: Claude Opus 4.6 --- .../main/resources/org/sonar/l10n/java/rules/java/S9357.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json index 06f2bcd0e81..f22599703fa 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9357.json @@ -9,7 +9,8 @@ "tags": [ "lambda", "java8", - "convention" + "convention", + "tests" ], "defaultSeverity": "Major", "ruleSpecification": "RSPEC-9357", From 9e5e323a23e026e2629b2d44b1de9b5f7bd057d3 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 24 Aug 2026 10:48:35 +0200 Subject: [PATCH 5/8] SONARJAVA-6827: Add AbstractAnonymousClassToLambdaCheck to BLACK_LIST The abstract base class extracted in the refactoring commit is counted by the file-scanning logic but excluded from the generated check list (no @Rule annotation), causing a count mismatch in GeneratedCheckListTest. Co-Authored-By: Claude Opus 4.6 --- .../java/org/sonar/plugins/java/GeneratedCheckListTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java index 7e982e7672a..2eb939ac1db 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java @@ -50,7 +50,8 @@ class GeneratedCheckListTest { "AbstractXPathBasedCheck.java", "AbstractWebXmlXPathBasedCheck.java", "AbstractRedosCheck.java", - "AbstractRegexCheck.java"); + "AbstractRegexCheck.java", + "AbstractAnonymousClassToLambdaCheck.java"); /** * Enforces that each check declared in list. From 901e3bf013f27f015770131a2ca56a1dcc4d0ab1 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 24 Aug 2026 13:03:39 +0200 Subject: [PATCH 6/8] SONARJAVA-6827: Replace abstract class with utility class to avoid coupling Replace AbstractAnonymousClassToLambdaCheck with AnonymousClassToLambdaUtils utility class. Both AnonymousClassShouldBeLambdaCheck (S1604) and AnonymousClassOnFunctionalInterfaceCheck (S9357) are now standalone checks that delegate shared logic to the utility class. Co-Authored-By: Claude Opus 4.6 --- ...nymousClassOnFunctionalInterfaceCheck.java | 46 ++++++++++++++- .../AnonymousClassShouldBeLambdaCheck.java | 46 ++++++++++++++- .../AnonymousClassToLambdaUtils.java} | 57 +++---------------- .../plugins/java/GeneratedCheckListTest.java | 3 +- 4 files changed, 98 insertions(+), 54 deletions(-) rename java-checks/src/main/java/org/sonar/java/checks/{AbstractAnonymousClassToLambdaCheck.java => helpers/AnonymousClassToLambdaUtils.java} (65%) diff --git a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java index c7377de90a9..6ade879e5ff 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java @@ -16,8 +16,52 @@ */ package org.sonar.java.checks; +import java.util.HashSet; +import java.util.Set; import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.AnonymousClassToLambdaUtils; +import org.sonar.plugins.java.api.JavaFileScanner; +import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.EnumConstantTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.NewClassTree; @Rule(key = "S9357") -public class AnonymousClassOnFunctionalInterfaceCheck extends AbstractAnonymousClassToLambdaCheck { +public class AnonymousClassOnFunctionalInterfaceCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { + + private JavaFileScannerContext context; + private final Set enumConstants = new HashSet<>(); + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava8Compatible(); + } + + @Override + public void scanFile(JavaFileScannerContext context) { + this.context = context; + enumConstants.clear(); + scan(context.getTree()); + } + + @Override + public void visitEnumConstant(EnumConstantTree tree) { + enumConstants.add(tree.simpleName()); + super.visitEnumConstant(tree); + enumConstants.remove(tree.simpleName()); + } + + @Override + public void visitNewClass(NewClassTree tree) { + super.visitNewClass(tree); + ClassTree classBody = tree.classBody(); + if (classBody != null && AnonymousClassToLambdaUtils.canBeConvertedToLambda(classBody, enumConstants)) { + context.reportIssue(this, tree.identifier(), "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); + } + } + } diff --git a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java index 1c4f9d53019..5a11e5b7d3d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java @@ -16,8 +16,52 @@ */ package org.sonar.java.checks; +import java.util.HashSet; +import java.util.Set; import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.AnonymousClassToLambdaUtils; +import org.sonar.plugins.java.api.JavaFileScanner; +import org.sonar.plugins.java.api.JavaFileScannerContext; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.EnumConstantTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.NewClassTree; @Rule(key = "S1604") -public class AnonymousClassShouldBeLambdaCheck extends AbstractAnonymousClassToLambdaCheck { +public class AnonymousClassShouldBeLambdaCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { + + private JavaFileScannerContext context; + private final Set enumConstants = new HashSet<>(); + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava8Compatible(); + } + + @Override + public void scanFile(JavaFileScannerContext context) { + this.context = context; + enumConstants.clear(); + scan(context.getTree()); + } + + @Override + public void visitEnumConstant(EnumConstantTree tree) { + enumConstants.add(tree.simpleName()); + super.visitEnumConstant(tree); + enumConstants.remove(tree.simpleName()); + } + + @Override + public void visitNewClass(NewClassTree tree) { + super.visitNewClass(tree); + ClassTree classBody = tree.classBody(); + if (classBody != null && AnonymousClassToLambdaUtils.canBeConvertedToLambda(classBody, enumConstants)) { + context.reportIssue(this, tree.identifier(), "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); + } + } + } diff --git a/java-checks/src/main/java/org/sonar/java/checks/AbstractAnonymousClassToLambdaCheck.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/AnonymousClassToLambdaUtils.java similarity index 65% rename from java-checks/src/main/java/org/sonar/java/checks/AbstractAnonymousClassToLambdaCheck.java rename to java-checks/src/main/java/org/sonar/java/checks/helpers/AnonymousClassToLambdaUtils.java index 0573000cdcc..188612a7b56 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AbstractAnonymousClassToLambdaCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/AnonymousClassToLambdaUtils.java @@ -14,73 +14,38 @@ * You should have received a copy of the Sonar Source-Available License * along with this program; if not, see https://sonarsource.com/license/ssal/ */ -package org.sonar.java.checks; +package org.sonar.java.checks.helpers; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import org.sonar.plugins.java.api.JavaFileScanner; -import org.sonar.plugins.java.api.JavaFileScannerContext; -import org.sonar.plugins.java.api.JavaVersion; -import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; -import org.sonar.plugins.java.api.tree.EnumConstantTree; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.NewClassTree; import org.sonar.plugins.java.api.tree.Tree; -import org.sonar.plugins.java.api.tree.TypeTree; -public abstract class AbstractAnonymousClassToLambdaCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { +public final class AnonymousClassToLambdaUtils { private static final String JAVA_LANG_OBJECT = "java.lang.Object"; - private JavaFileScannerContext context; - private final Set enumConstants = new HashSet<>(); - @Override - public boolean isCompatibleWithJavaVersion(JavaVersion version) { - return version.isJava8Compatible(); + private AnonymousClassToLambdaUtils() { } - @Override - public void scanFile(JavaFileScannerContext context) { - this.context = context; - enumConstants.clear(); - scan(context.getTree()); - } - - @Override - public void visitEnumConstant(EnumConstantTree tree) { - enumConstants.add(tree.simpleName()); - super.visitEnumConstant(tree); - enumConstants.remove(tree.simpleName()); - } - - @Override - public void visitNewClass(NewClassTree tree) { - super.visitNewClass(tree); - ClassTree classBody = tree.classBody(); - if (classBody != null) { - TypeTree identifier = tree.identifier(); - if (!useThisInstance(classBody) && !enumConstants.contains(identifier) && isSAM(classBody)) { - context.reportIssue(this, identifier, "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); - } - } + public static boolean canBeConvertedToLambda(ClassTree classBody, Set enumConstants) { + var identifier = ((NewClassTree) classBody.parent()).identifier(); + return !useThisInstance(classBody) && !enumConstants.contains(identifier) && isSAM(classBody); } private static boolean isSAM(ClassTree classBody) { if (hasOnlyOneMethod(classBody.members())) { - // When overriding only one method of a functional interface, it can only be the single abstract method - // and not one of the default methods. No need to check that the method signature matches. Symbol.TypeSymbol symbol = classBody.symbol(); - // should be anonymous class of interface and not abstract class return symbol.interfaces().size() == 1 && symbol.superClass().is(JAVA_LANG_OBJECT) && hasSingleAbstractMethodInHierarchy(symbol.superTypes()); @@ -92,15 +57,11 @@ private static boolean hasSingleAbstractMethodInHierarchy(Set superTypes) return superTypes.stream() .filter(type -> !type.is(JAVA_LANG_OBJECT)) .map(Type::symbol) - // collect all the methods declared in hierarchy .flatMap(superType -> superType.memberSymbols().stream().filter(Symbol::isMethodSymbol).filter(Symbol::isAbstract)) .map(Symbol.MethodSymbol.class::cast) - // remove objects methods redefined in interfaces .filter(symbol -> !isObjectMethod(symbol)) - // remove generic methods, which can not be written as lambda (JLS-11 §15.27) .filter(symbol -> !symbol.isParametrizedMethod()) - // always take same symbol if method is redeclared over and over in hierarchy - .map(AbstractAnonymousClassToLambdaCheck::overriddenSymbolIfAny) + .map(AnonymousClassToLambdaUtils::overriddenSymbolIfAny) .collect(Collectors.toSet()) .size() == 1; } @@ -135,8 +96,6 @@ private static boolean hasOnlyOneMethod(List members) { } private static boolean canRefactorMethod(MethodTree methodTree) { - // if overridden method declares to throw an exception, refactoring to a lambda might prove tricky - // if it is annotated with something else than @Override, it is not possible to refactor the code return methodTree.throwsClauses().isEmpty() && methodTree.symbol().metadata().annotations().stream() .allMatch(annotation -> annotation.symbol().type().is("java.lang.Override")); @@ -159,7 +118,6 @@ public UsesThisInstanceVisitor(Type instanceType) { @Override public void visitClass(ClassTree tree) { - // visit the class body but ignore inner classes if (!visitedClassTree) { visitedClassTree = true; super.visitClass(tree); @@ -174,7 +132,6 @@ public void visitNewClass(NewClassTree tree) { @Override public void visitMemberSelectExpression(MemberSelectExpressionTree tree) { scan(tree.expression()); - // ignore identifier, because if it is this, it is a qualified this. } @Override diff --git a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java index 2eb939ac1db..7e982e7672a 100644 --- a/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java +++ b/sonar-java-plugin/src/test/java/org/sonar/plugins/java/GeneratedCheckListTest.java @@ -50,8 +50,7 @@ class GeneratedCheckListTest { "AbstractXPathBasedCheck.java", "AbstractWebXmlXPathBasedCheck.java", "AbstractRedosCheck.java", - "AbstractRegexCheck.java", - "AbstractAnonymousClassToLambdaCheck.java"); + "AbstractRegexCheck.java"); /** * Enforces that each check declared in list. From 2a0cb2131e89c12e626744002a5e57b34133d38a Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 24 Aug 2026 13:35:10 +0200 Subject: [PATCH 7/8] SONARJAVA-6827: Replace abstract class with utility class to avoid coupling Move the shared visitor logic from AnonymousClassShouldBeLambdaCheck and AnonymousClassOnFunctionalInterfaceCheck into a static method in AnonymousClassToLambdaUtils. Both checks are now thin wrappers that delegate to the utility, eliminating code duplication without introducing an abstract class coupling. Co-Authored-By: Claude Opus 4.6 --- ...nymousClassOnFunctionalInterfaceCheck.java | 32 ++--------------- .../AnonymousClassShouldBeLambdaCheck.java | 32 ++--------------- .../helpers/AnonymousClassToLambdaUtils.java | 36 +++++++++++++++++++ 3 files changed, 40 insertions(+), 60 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java index 6ade879e5ff..bbb6c9222f8 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassOnFunctionalInterfaceCheck.java @@ -16,25 +16,15 @@ */ package org.sonar.java.checks; -import java.util.HashSet; -import java.util.Set; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.AnonymousClassToLambdaUtils; import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; -import org.sonar.plugins.java.api.tree.ClassTree; -import org.sonar.plugins.java.api.tree.EnumConstantTree; -import org.sonar.plugins.java.api.tree.IdentifierTree; -import org.sonar.plugins.java.api.tree.NewClassTree; @Rule(key = "S9357") -public class AnonymousClassOnFunctionalInterfaceCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { - - private JavaFileScannerContext context; - private final Set enumConstants = new HashSet<>(); +public class AnonymousClassOnFunctionalInterfaceCheck implements JavaFileScanner, JavaVersionAwareVisitor { @Override public boolean isCompatibleWithJavaVersion(JavaVersion version) { @@ -43,25 +33,7 @@ public boolean isCompatibleWithJavaVersion(JavaVersion version) { @Override public void scanFile(JavaFileScannerContext context) { - this.context = context; - enumConstants.clear(); - scan(context.getTree()); - } - - @Override - public void visitEnumConstant(EnumConstantTree tree) { - enumConstants.add(tree.simpleName()); - super.visitEnumConstant(tree); - enumConstants.remove(tree.simpleName()); - } - - @Override - public void visitNewClass(NewClassTree tree) { - super.visitNewClass(tree); - ClassTree classBody = tree.classBody(); - if (classBody != null && AnonymousClassToLambdaUtils.canBeConvertedToLambda(classBody, enumConstants)) { - context.reportIssue(this, tree.identifier(), "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); - } + AnonymousClassToLambdaUtils.scanForAnonymousClassConvertibleToLambda(this, context); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java index 5a11e5b7d3d..17d09285295 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/AnonymousClassShouldBeLambdaCheck.java @@ -16,25 +16,15 @@ */ package org.sonar.java.checks; -import java.util.HashSet; -import java.util.Set; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.AnonymousClassToLambdaUtils; import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; -import org.sonar.plugins.java.api.tree.BaseTreeVisitor; -import org.sonar.plugins.java.api.tree.ClassTree; -import org.sonar.plugins.java.api.tree.EnumConstantTree; -import org.sonar.plugins.java.api.tree.IdentifierTree; -import org.sonar.plugins.java.api.tree.NewClassTree; @Rule(key = "S1604") -public class AnonymousClassShouldBeLambdaCheck extends BaseTreeVisitor implements JavaFileScanner, JavaVersionAwareVisitor { - - private JavaFileScannerContext context; - private final Set enumConstants = new HashSet<>(); +public class AnonymousClassShouldBeLambdaCheck implements JavaFileScanner, JavaVersionAwareVisitor { @Override public boolean isCompatibleWithJavaVersion(JavaVersion version) { @@ -43,25 +33,7 @@ public boolean isCompatibleWithJavaVersion(JavaVersion version) { @Override public void scanFile(JavaFileScannerContext context) { - this.context = context; - enumConstants.clear(); - scan(context.getTree()); - } - - @Override - public void visitEnumConstant(EnumConstantTree tree) { - enumConstants.add(tree.simpleName()); - super.visitEnumConstant(tree); - enumConstants.remove(tree.simpleName()); - } - - @Override - public void visitNewClass(NewClassTree tree) { - super.visitNewClass(tree); - ClassTree classBody = tree.classBody(); - if (classBody != null && AnonymousClassToLambdaUtils.canBeConvertedToLambda(classBody, enumConstants)) { - context.reportIssue(this, tree.identifier(), "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); - } + AnonymousClassToLambdaUtils.scanForAnonymousClassConvertibleToLambda(this, context); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/AnonymousClassToLambdaUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/AnonymousClassToLambdaUtils.java index 188612a7b56..2e656c5f912 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/AnonymousClassToLambdaUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/AnonymousClassToLambdaUtils.java @@ -16,14 +16,18 @@ */ package org.sonar.java.checks.helpers; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import org.sonar.plugins.java.api.JavaFileScanner; +import org.sonar.plugins.java.api.JavaFileScannerContext; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Symbol.MethodSymbol; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.EnumConstantTree; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; @@ -38,6 +42,11 @@ public final class AnonymousClassToLambdaUtils { private AnonymousClassToLambdaUtils() { } + public static void scanForAnonymousClassConvertibleToLambda(JavaFileScanner check, JavaFileScannerContext context) { + var visitor = new AnonymousClassToLambdaVisitor(check, context); + context.getTree().accept(visitor); + } + public static boolean canBeConvertedToLambda(ClassTree classBody, Set enumConstants) { var identifier = ((NewClassTree) classBody.parent()).identifier(); return !useThisInstance(classBody) && !enumConstants.contains(identifier) && isSAM(classBody); @@ -107,6 +116,33 @@ private static boolean useThisInstance(ClassTree body) { return visitor.usesThisInstance; } + private static class AnonymousClassToLambdaVisitor extends BaseTreeVisitor { + private final JavaFileScanner check; + private final JavaFileScannerContext context; + private final Set enumConstants = new HashSet<>(); + + AnonymousClassToLambdaVisitor(JavaFileScanner check, JavaFileScannerContext context) { + this.check = check; + this.context = context; + } + + @Override + public void visitEnumConstant(EnumConstantTree tree) { + enumConstants.add(tree.simpleName()); + super.visitEnumConstant(tree); + enumConstants.remove(tree.simpleName()); + } + + @Override + public void visitNewClass(NewClassTree tree) { + super.visitNewClass(tree); + ClassTree classBody = tree.classBody(); + if (classBody != null && canBeConvertedToLambda(classBody, enumConstants)) { + context.reportIssue(check, tree.identifier(), "Make this anonymous inner class a lambda" + context.getJavaVersion().java8CompatibilityMessage()); + } + } + } + private static class UsesThisInstanceVisitor extends BaseTreeVisitor { private final Type instanceType; boolean usesThisInstance = false; From 88776eceba9cbd7a73765200a92a26278916d64c Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Tue, 25 Aug 2026 11:18:15 +0200 Subject: [PATCH 8/8] SONARJAVA-6827: Exclude S9357 from AutoScan AutoScan lacks sonar.java.source, so unspecified version is treated as Java 8-compatible. S9357 may recommend lambdas in Java 7 test code where lambdas are unsupported. This mirrors the existing S1604 exclusion. Co-Authored-By: Claude Opus 4.6 --- check-list/src/main/java/org/sonar/java/CheckListGenerator.java | 1 + 1 file changed, 1 insertion(+) diff --git a/check-list/src/main/java/org/sonar/java/CheckListGenerator.java b/check-list/src/main/java/org/sonar/java/CheckListGenerator.java index 4c79d3d28c3..63d53b364a3 100644 --- a/check-list/src/main/java/org/sonar/java/CheckListGenerator.java +++ b/check-list/src/main/java/org/sonar/java/CheckListGenerator.java @@ -180,6 +180,7 @@ public final class ${className} { CallToDeprecatedMethodCheck.class, // Rules relying on correct setup of java version AbstractClassNoFieldShouldBeInterfaceCheck.class, + AnonymousClassOnFunctionalInterfaceCheck.class, AnonymousClassShouldBeLambdaCheck.class, CombineCatchCheck.class, DateAndTimesCheck.class,