From 4aebbb90e1d493bb6c16ab5bbe2b4f9b452d269f Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 16:18:57 +0200 Subject: [PATCH 01/17] SONARJAVA-6506 Avoid suggesting records for DTO contracts --- .../RecordInsteadOfClassCheckSample.java | 13 ++ .../RecordInsteadOfClassCheckSample.java | 113 +++++++++++++++ .../checks/RecordInsteadOfClassCheck.java | 132 +++++++++++++++++- .../java/checks/helpers/SpringUtils.java | 17 ++- ...nsteadOfClassCheckPackagePrefixSample.java | 51 +++++++ .../io/micronaut/http/annotation/Get.java | 20 +++ .../checks/RecordInsteadOfClassCheckTest.java | 12 ++ .../context/annotation/Configuration.java | 20 +++ .../elasticsearch/annotations/Document.java | 20 +++ .../data/mongodb/core/mapping/Document.java | 20 +++ .../data/mongodb/repository/Query.java | 21 +++ 11 files changed, 428 insertions(+), 11 deletions(-) create mode 100644 java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java create mode 100644 java-checks/src/test/java/io/micronaut/http/annotation/Get.java create mode 100644 java-checks/src/test/java/org/springframework/context/annotation/Configuration.java create mode 100644 java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java create mode 100644 java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java create mode 100644 java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/RecordInsteadOfClassCheckSample.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/RecordInsteadOfClassCheckSample.java index fda58f989e3..95a213a1d90 100644 --- a/java-checks-test-sources/default/src/main/files/non-compiling/checks/RecordInsteadOfClassCheckSample.java +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/RecordInsteadOfClassCheckSample.java @@ -28,4 +28,17 @@ public int getI() { return i; } } + + @UnknownFrameworkAnnotation + public final class UnknownAnnotatedClass { // Compliant, unknown annotations may represent framework contracts + private final int i; + + public UnknownAnnotatedClass(final int i) { + this.i = i; + } + + public int getI() { + return i; + } + } } diff --git a/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java index fc737f4b6c3..1eaaac4479d 100644 --- a/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java @@ -1,5 +1,17 @@ package checks; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.io.ObjectStreamField; +import java.io.Serializable; import java.util.Optional; public class RecordInsteadOfClassCheckSample { @@ -146,6 +158,10 @@ final class ClassWithPrivateNonFinalField { private int base; } final class ClassWithPublicFinalField { public final int base = 0; } final class ClassWithoutFields { } final class ClassWithoutFinalFields { private int sum; } + Object anonymousClass = new Object() { + private final int sum = 0; + int getSum() { return sum; } + }; abstract class AbstractClass { abstract void foo(); } interface NotAClass { void foo(); } @@ -167,6 +183,103 @@ public Optional bar() { // Not the same type as the field bar. } } + final class SerializableClass implements Serializable { // Compliant, records have different serialization behavior + private final int sum; + + SerializableClass(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + final class ExternalizableClass implements Externalizable { // Compliant, records have different serialization behavior + private final int sum; + + ExternalizableClass(int sum) { this.sum = sum; } + int getSum() { return sum; } + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { } + public void writeExternal(ObjectOutput out) throws IOException { } + } + + final class ClassWithWriteObject { + private final int sum; + + ClassWithWriteObject(int sum) { this.sum = sum; } + int getSum() { return sum; } + private void writeObject(ObjectOutputStream out) throws IOException { } + } + + final class ClassWithReadObject { + private final int sum; + + ClassWithReadObject(int sum) { this.sum = sum; } + int getSum() { return sum; } + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { } + } + + final class ClassWithReadObjectNoData { + private final int sum; + + ClassWithReadObjectNoData(int sum) { this.sum = sum; } + int getSum() { return sum; } + private void readObjectNoData() throws ObjectStreamException { } + } + + final class ClassWithWriteReplace { + private final int sum; + + ClassWithWriteReplace(int sum) { this.sum = sum; } + int getSum() { return sum; } + private Object writeReplace() throws ObjectStreamException { return this; } + } + + final class ClassWithReadResolve { + private final int sum; + + ClassWithReadResolve(int sum) { this.sum = sum; } + int getSum() { return sum; } + private Object readResolve() throws ObjectStreamException { return this; } + } + + final class ClassWithSerialPersistentFields { + private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; + private final int sum; + + ClassWithSerialPersistentFields(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + final class ClassWithJsonAnnotation { // Compliant, framework metadata owns the class shape + private final int sum; + + ClassWithJsonAnnotation(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + final class ClassWithJsonCreatorConstructor { + private final int sum; + + @JsonCreator + ClassWithJsonCreatorConstructor(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + final class ClassWithJsonAnnotatedField { + @JsonProperty("total") + private final int sum; + + ClassWithJsonAnnotatedField(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + final class ClassWithJsonAnnotatedGetter { + private final int sum; + + ClassWithJsonAnnotatedGetter(int sum) { this.sum = sum; } + + @JsonProperty("total") + int getSum() { return sum; } + } + // When the constructor has smaller visibility, it is not possible to create a record with the same behavior. // Order: Public > protected > package private > private diff --git a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java index 7a58866fc6d..05312655f3f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java @@ -22,24 +22,75 @@ import java.util.Set; import java.util.stream.Collectors; import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.SpringUtils; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.ArrayTypeTree; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.ParameterizedTypeTree; import org.sonar.plugins.java.api.tree.PrimitiveTypeTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.TypeTree; import org.sonar.plugins.java.api.tree.VariableTree; +import static org.sonar.java.checks.helpers.AnnotationsHelper.hasUnknownAnnotation; + @Rule(key = "S6206") public class RecordInsteadOfClassCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { + private static final String JAVA_IO_EXTERNALIZABLE = "java.io.Externalizable"; + private static final String JAVA_IO_SERIALIZABLE = "java.io.Serializable"; + + private static final Set JACKSON_ANNOTATION_PACKAGES = Set.of( + "com.fasterxml.jackson.annotation.", + "com.fasterxml.jackson.databind.annotation."); + private static final Set GSON_ANNOTATION_PACKAGES = Set.of("com.google.gson.annotations."); + private static final Set MICRONAUT_ANNOTATION_PACKAGES = Set.of( + "io.micronaut.core.annotation.", + "io.micronaut.data.annotation.", + "io.micronaut.serde.annotation."); + private static final Set JAKARTA_EE_ANNOTATION_PACKAGES = Set.of( + "jakarta.inject.", + "jakarta.persistence.", + "jakarta.xml.bind.annotation."); + private static final Set JAVA_EE_ANNOTATION_PACKAGES = Set.of( + "javax.inject.", + "javax.persistence.", + "javax.xml.bind.annotation."); + private static final Set LOMBOK_ANNOTATION_PACKAGES = Set.of("lombok."); + private static final Set SPRING_ANNOTATION_PACKAGES = Set.of( + SpringUtils.BEANS_FACTORY_ANNOTATION_PACKAGE, + SpringUtils.BOOT_CONTEXT_PROPERTIES_PACKAGE, + SpringUtils.DATA_PACKAGE + "annotation."); + + private static final Map> FRAMEWORK_ANNOTATION_PREFIXES = Map.of( + "Jackson", JACKSON_ANNOTATION_PACKAGES, + "Gson", GSON_ANNOTATION_PACKAGES, + "Micronaut", MICRONAUT_ANNOTATION_PACKAGES, + "Jakarta EE", JAKARTA_EE_ANNOTATION_PACKAGES, + "Java EE", JAVA_EE_ANNOTATION_PACKAGES, + "Lombok", LOMBOK_ANNOTATION_PACKAGES, + "Spring", SPRING_ANNOTATION_PACKAGES); + + private static final MethodMatchers SERIALIZATION_CONTRACT_METHODS = MethodMatchers.or( + methodMatcher("readObject", "java.io.ObjectInputStream"), + methodMatcher("writeObject", "java.io.ObjectOutputStream"), + methodMatcher("readExternal", "java.io.ObjectInput"), + methodMatcher("writeExternal", "java.io.ObjectOutput"), + MethodMatchers.create() + .ofAnyType() + .names("readObjectNoData", "writeReplace", "readResolve") + .addWithoutParametersMatcher() + .build()); + @Override public boolean isCompatibleWithJavaVersion(JavaVersion version) { return version.isJava16Compatible(); @@ -53,6 +104,10 @@ public List nodesToVisit() { @Override public void visitNode(Tree tree) { ClassTree classTree = (ClassTree) tree; + if (classTree.simpleName() == null) { + // Anonymous classes can not be converted to records. + return; + } if (classTree.superClass() != null) { // records can not extends other classes return; @@ -66,27 +121,94 @@ public void visitNode(Tree tree) { // records can not be extended return; } + if (hasSerializationContract(classTree)) { + // records have special serialization behavior, so this refactoring is not behavior-preserving. + return; + } List fields = classFields(classSymbol); if (fields.isEmpty() || !hasOnlyPrivateFinalFields(fields)) { return; } List methods = classMethods(classSymbol); - Map fieldsNameToType = fields.stream().collect(Collectors.toMap(Symbol::name, Symbol::type)); - - if (!hasGetterForEveryField(methods, fieldsNameToType)) { - return; - } List constructors = classConstructors(methods); if (constructors.size() != 1) { return; } Symbol.MethodSymbol constructor = constructors.get(0); + Map fieldsNameToType = fields.stream().collect(Collectors.toMap(Symbol::name, Symbol::type)); + if (hasFrameworkContract(classSymbol, fields, methods, fieldsNameToType, constructor)) { + return; + } + + if (!hasGetterForEveryField(methods, fieldsNameToType)) { + return; + } if (hasParameterForEveryField(constructor, fieldsNameToType.keySet()) && !constructorHasSmallerVisibility(constructor, classSymbol)) { reportIssue(classTree.simpleName(), String.format("Refactor this class declaration to use 'record %s'.", recordName(classTree, constructor))); } } + private static boolean hasSerializationContract(ClassTree classTree) { + Type type = classTree.symbol().type(); + return type.isSubtypeOf(JAVA_IO_SERIALIZABLE) + || type.isSubtypeOf(JAVA_IO_EXTERNALIZABLE) + || classTree.members().stream().anyMatch(RecordInsteadOfClassCheck::isSerializationContractMember); + } + + private static boolean isSerializationContractMember(Tree member) { + if (member.is(Tree.Kind.METHOD)) { + return SERIALIZATION_CONTRACT_METHODS.matches((MethodTree) member); + } + if (member.is(Tree.Kind.VARIABLE)) { + return isSerialPersistentFields(((VariableTree) member).symbol()); + } + return false; + } + + private static boolean isSerialPersistentFields(Symbol field) { + return "serialPersistentFields".equals(field.name()) + && field.isPrivate() + && field.isStatic() + && field.isFinal() + && field.type().is("java.io.ObjectStreamField[]"); + } + + private static MethodMatchers methodMatcher(String methodName, String parameterType) { + return MethodMatchers.create() + .ofAnyType() + .names(methodName) + .addParametersMatcher(parameterType) + .build(); + } + + private static boolean hasFrameworkContract( + Symbol.TypeSymbol classSymbol, + List fields, + List methods, + Map fieldsNameToType, + Symbol.MethodSymbol constructor) { + + return hasFrameworkAnnotation(classSymbol.metadata()) + || hasFrameworkAnnotation(constructor.metadata()) + || fields.stream().anyMatch(field -> hasFrameworkAnnotation(field.metadata())) + || methods.stream() + .filter(method -> isGetter(method, fieldsNameToType)) + .anyMatch(method -> hasFrameworkAnnotation(method.metadata())); + } + + private static boolean hasFrameworkAnnotation(SymbolMetadata metadata) { + return hasUnknownAnnotation(metadata) || metadata.annotations().stream().anyMatch(RecordInsteadOfClassCheck::isFrameworkAnnotation); + } + + private static boolean isFrameworkAnnotation(SymbolMetadata.AnnotationInstance annotation) { + Type annotationType = annotation.symbol().type(); + return !annotationType.isUnknown() + && FRAMEWORK_ANNOTATION_PREFIXES.values().stream() + .flatMap(Set::stream) + .anyMatch(annotationType.fullyQualifiedName()::startsWith); + } + private static boolean constructorHasSmallerVisibility(Symbol.MethodSymbol constructor, Symbol.TypeSymbol classSymbol) { boolean constructorIsPrivate = constructor.isPrivate(); boolean constructorIsPackageVisibility = constructor.isPackageVisibility(); diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java index 4250acd3ede..25c059f32bd 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java @@ -26,19 +26,24 @@ public final class SpringUtils { + public static final String BEANS_FACTORY_ANNOTATION_PACKAGE = "org.springframework.beans.factory.annotation."; + public static final String BOOT_CONTEXT_PROPERTIES_PACKAGE = "org.springframework.boot.context.properties."; + public static final String CONTEXT_ANNOTATION_PACKAGE = "org.springframework.context.annotation."; + public static final String DATA_PACKAGE = "org.springframework.data."; + public static final String SPRING_BOOT_APP_ANNOTATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; public static final String CONTROLLER_ANNOTATION = "org.springframework.stereotype.Controller"; public static final String COMPONENT_ANNOTATION = "org.springframework.stereotype.Component"; public static final String REPOSITORY_ANNOTATION = "org.springframework.stereotype.Repository"; public static final String SERVICE_ANNOTATION = "org.springframework.stereotype.Service"; - public static final String AUTOWIRED_ANNOTATION = "org.springframework.beans.factory.annotation.Autowired"; - public static final String VALUE_ANNOTATION = "org.springframework.beans.factory.annotation.Value"; + public static final String AUTOWIRED_ANNOTATION = BEANS_FACTORY_ANNOTATION_PACKAGE + "Autowired"; + public static final String VALUE_ANNOTATION = BEANS_FACTORY_ANNOTATION_PACKAGE + "Value"; public static final String TRANSACTIONAL_ANNOTATION = "org.springframework.transaction.annotation.Transactional"; - public static final String BEAN_ANNOTATION = "org.springframework.context.annotation.Bean"; - public static final String SCOPE_ANNOTATION = "org.springframework.context.annotation.Scope"; - public static final String CONFIGURATION_ANNOTATION = "org.springframework.context.annotation.Configuration"; + public static final String BEAN_ANNOTATION = CONTEXT_ANNOTATION_PACKAGE + "Bean"; + public static final String SCOPE_ANNOTATION = CONTEXT_ANNOTATION_PACKAGE + "Scope"; + public static final String CONFIGURATION_ANNOTATION = CONTEXT_ANNOTATION_PACKAGE + "Configuration"; public static final String ASYNC_ANNOTATION = "org.springframework.scheduling.annotation.Async"; - public static final String DATA_REPOSITORY_ANNOTATION = "org.springframework.data.repository.Repository"; + public static final String DATA_REPOSITORY_ANNOTATION = DATA_PACKAGE + "repository.Repository"; public static final String REST_CONTROLLER_ANNOTATION = "org.springframework.web.bind.annotation.RestController"; public static final String SPRING_BOOT_TEST_ANNOTATION = "org.springframework.boot.test.context.SpringBootTest"; diff --git a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java new file mode 100644 index 00000000000..6797b802dab --- /dev/null +++ b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java @@ -0,0 +1,51 @@ +package checks; + +import io.micronaut.http.annotation.Get; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.mongodb.repository.Query; + +class RecordInsteadOfClassCheckPackagePrefixSample { + + @Configuration + final class ClassWithSpringConfigurationAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringConfigurationAnnotation(int sum)'.}} + private final int sum; + + ClassWithSpringConfigurationAnnotation(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + final class ClassWithSpringDataQueryGetter { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataQueryGetter(int sum)'.}} + private final int sum; + + ClassWithSpringDataQueryGetter(int sum) { this.sum = sum; } + + @Query("{}") + int getSum() { return sum; } + } + + @org.springframework.data.mongodb.core.mapping.Document + final class ClassWithSpringDataMongoDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataMongoDocumentAnnotation(int sum)'.}} + private final int sum; + + ClassWithSpringDataMongoDocumentAnnotation(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + @Document + final class ClassWithSpringDataElasticsearchDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataElasticsearchDocumentAnnotation(int sum)'.}} + private final int sum; + + ClassWithSpringDataElasticsearchDocumentAnnotation(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + final class ClassWithMicronautHttpGetter { // Noncompliant {{Refactor this class declaration to use 'record ClassWithMicronautHttpGetter(int sum)'.}} + private final int sum; + + ClassWithMicronautHttpGetter(int sum) { this.sum = sum; } + + @Get + int getSum() { return sum; } + } +} diff --git a/java-checks/src/test/java/io/micronaut/http/annotation/Get.java b/java-checks/src/test/java/io/micronaut/http/annotation/Get.java new file mode 100644 index 00000000000..2d04c92b5dd --- /dev/null +++ b/java-checks/src/test/java/io/micronaut/http/annotation/Get.java @@ -0,0 +1,20 @@ +/* + * 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 io.micronaut.http.annotation; + +public @interface Get { +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index e6f356aa5f4..cc80bf32f02 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -16,6 +16,8 @@ */ package org.sonar.java.checks; +import java.io.File; +import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; @@ -33,6 +35,16 @@ void test() { .verifyIssues(); } + @Test + void test_broad_framework_annotations_are_not_skipped() { + CheckVerifier.newVerifier() + .onFile("src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java") + .withCheck(new RecordInsteadOfClassCheck()) + .withClassPath(List.of(new File("target/test-classes"))) + .withJavaVersion(16) + .verifyIssues(); + } + @Test void test_incomplete_semantic() { CheckVerifier.newVerifier() diff --git a/java-checks/src/test/java/org/springframework/context/annotation/Configuration.java b/java-checks/src/test/java/org/springframework/context/annotation/Configuration.java new file mode 100644 index 00000000000..16e42e04f88 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/context/annotation/Configuration.java @@ -0,0 +1,20 @@ +/* + * 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.springframework.context.annotation; + +public @interface Configuration { +} diff --git a/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java b/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java new file mode 100644 index 00000000000..16a56917f37 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java @@ -0,0 +1,20 @@ +/* + * 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.springframework.data.elasticsearch.annotations; + +public @interface Document { +} diff --git a/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java b/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java new file mode 100644 index 00000000000..413a0467871 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java @@ -0,0 +1,20 @@ +/* + * 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.springframework.data.mongodb.core.mapping; + +public @interface Document { +} diff --git a/java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java b/java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java new file mode 100644 index 00000000000..536c08ee984 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java @@ -0,0 +1,21 @@ +/* + * 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.springframework.data.mongodb.repository; + +public @interface Query { + String value() default ""; +} From 4f4d86abd349a0b5e1d568d671aae1148554910d Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 16:31:43 +0200 Subject: [PATCH 02/17] Refactor framework annotation prefix lookup --- .../checks/RecordInsteadOfClassCheck.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java index 05312655f3f..f63d69d808f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.SpringUtils; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; @@ -71,14 +72,16 @@ public class RecordInsteadOfClassCheck extends IssuableSubscriptionVisitor imple SpringUtils.BOOT_CONTEXT_PROPERTIES_PACKAGE, SpringUtils.DATA_PACKAGE + "annotation."); - private static final Map> FRAMEWORK_ANNOTATION_PREFIXES = Map.of( - "Jackson", JACKSON_ANNOTATION_PACKAGES, - "Gson", GSON_ANNOTATION_PACKAGES, - "Micronaut", MICRONAUT_ANNOTATION_PACKAGES, - "Jakarta EE", JAKARTA_EE_ANNOTATION_PACKAGES, - "Java EE", JAVA_EE_ANNOTATION_PACKAGES, - "Lombok", LOMBOK_ANNOTATION_PACKAGES, - "Spring", SPRING_ANNOTATION_PACKAGES); + private static final Set FRAMEWORK_ANNOTATION_PREFIXES = Stream.of( + JACKSON_ANNOTATION_PACKAGES, + GSON_ANNOTATION_PACKAGES, + MICRONAUT_ANNOTATION_PACKAGES, + JAKARTA_EE_ANNOTATION_PACKAGES, + JAVA_EE_ANNOTATION_PACKAGES, + LOMBOK_ANNOTATION_PACKAGES, + SPRING_ANNOTATION_PACKAGES) + .flatMap(Set::stream) + .collect(Collectors.toUnmodifiableSet()); private static final MethodMatchers SERIALIZATION_CONTRACT_METHODS = MethodMatchers.or( methodMatcher("readObject", "java.io.ObjectInputStream"), @@ -204,9 +207,7 @@ private static boolean hasFrameworkAnnotation(SymbolMetadata metadata) { private static boolean isFrameworkAnnotation(SymbolMetadata.AnnotationInstance annotation) { Type annotationType = annotation.symbol().type(); return !annotationType.isUnknown() - && FRAMEWORK_ANNOTATION_PREFIXES.values().stream() - .flatMap(Set::stream) - .anyMatch(annotationType.fullyQualifiedName()::startsWith); + && FRAMEWORK_ANNOTATION_PREFIXES.stream().anyMatch(annotationType.fullyQualifiedName()::startsWith); } private static boolean constructorHasSmallerVisibility(Symbol.MethodSymbol constructor, Symbol.TypeSymbol classSymbol) { From 2ca67ad3a7e0a4bead084986c384e32a84fa0005 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 16:33:26 +0200 Subject: [PATCH 03/17] SONARJAVA-6506 Update scope coverage tests --- ...nsteadOfClassCheckPackagePrefixSample.java | 36 +++++++++++++++++++ .../micronaut/serde/annotation/Serdeable.java | 20 +++++++++++ .../checks/RecordInsteadOfClassCheckTest.java | 2 +- .../beans/factory/annotation/Value.java | 21 +++++++++++ .../properties/ConfigurationProperties.java | 21 +++++++++++ .../springframework/data/annotation/Id.java | 20 +++++++++++ 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java create mode 100644 java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java create mode 100644 java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java create mode 100644 java-checks/src/test/java/org/springframework/data/annotation/Id.java diff --git a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java index 6797b802dab..96756a78ef3 100644 --- a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java +++ b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java @@ -1,12 +1,48 @@ package checks; import io.micronaut.http.annotation.Get; +import io.micronaut.serde.annotation.Serdeable; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.mongodb.repository.Query; class RecordInsteadOfClassCheckPackagePrefixSample { + @Value("${record.sum}") + final class ClassWithSpringValueAnnotation { // Compliant, beans factory annotations are in scope + private final int sum; + + ClassWithSpringValueAnnotation(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + @ConfigurationProperties("record") + final class ClassWithConfigurationPropertiesAnnotation { // Compliant, boot context properties annotations are in scope + private final int sum; + + ClassWithConfigurationPropertiesAnnotation(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + final class ClassWithSpringDataAnnotationField { // Compliant, spring data annotations are in scope + @Id + private final int sum; + + ClassWithSpringDataAnnotationField(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + + @Serdeable + final class ClassWithMicronautSerdeAnnotation { // Compliant, Micronaut serde annotations are in scope + private final int sum; + + ClassWithMicronautSerdeAnnotation(int sum) { this.sum = sum; } + int getSum() { return sum; } + } + @Configuration final class ClassWithSpringConfigurationAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringConfigurationAnnotation(int sum)'.}} private final int sum; diff --git a/java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java b/java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java new file mode 100644 index 00000000000..dccdee6632e --- /dev/null +++ b/java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java @@ -0,0 +1,20 @@ +/* + * 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 io.micronaut.serde.annotation; + +public @interface Serdeable { +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index cc80bf32f02..2a82c686484 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -36,7 +36,7 @@ void test() { } @Test - void test_broad_framework_annotations_are_not_skipped() { + void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() .onFile("src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java") .withCheck(new RecordInsteadOfClassCheck()) diff --git a/java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java b/java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java new file mode 100644 index 00000000000..d273e377756 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java @@ -0,0 +1,21 @@ +/* + * 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.springframework.beans.factory.annotation; + +public @interface Value { + String value(); +} diff --git a/java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java b/java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java new file mode 100644 index 00000000000..ed0b253513b --- /dev/null +++ b/java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java @@ -0,0 +1,21 @@ +/* + * 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.springframework.boot.context.properties; + +public @interface ConfigurationProperties { + String value(); +} diff --git a/java-checks/src/test/java/org/springframework/data/annotation/Id.java b/java-checks/src/test/java/org/springframework/data/annotation/Id.java new file mode 100644 index 00000000000..f54e9579e37 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/data/annotation/Id.java @@ -0,0 +1,20 @@ +/* + * 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.springframework.data.annotation; + +public @interface Id { +} From 67102338a9c61f66f8517857263e12597ad75556 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 16:35:10 +0200 Subject: [PATCH 04/17] SONARJAVA-6506 Cover Spring Data document types --- .../java/org/sonar/java/checks/RecordInsteadOfClassCheck.java | 4 +++- .../main/java/org/sonar/java/checks/helpers/SpringUtils.java | 2 ++ .../checks/RecordInsteadOfClassCheckPackagePrefixSample.java | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java index f63d69d808f..26306cb015d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java @@ -70,7 +70,9 @@ public class RecordInsteadOfClassCheck extends IssuableSubscriptionVisitor imple private static final Set SPRING_ANNOTATION_PACKAGES = Set.of( SpringUtils.BEANS_FACTORY_ANNOTATION_PACKAGE, SpringUtils.BOOT_CONTEXT_PROPERTIES_PACKAGE, - SpringUtils.DATA_PACKAGE + "annotation."); + SpringUtils.DATA_PACKAGE + "annotation.", + SpringUtils.DATA_ELASTICSEARCH_ANNOTATIONS_PACKAGE, + SpringUtils.DATA_MONGODB_CORE_MAPPING_PACKAGE); private static final Set FRAMEWORK_ANNOTATION_PREFIXES = Stream.of( JACKSON_ANNOTATION_PACKAGES, diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java index 25c059f32bd..6f9c06f9966 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java @@ -30,6 +30,8 @@ public final class SpringUtils { public static final String BOOT_CONTEXT_PROPERTIES_PACKAGE = "org.springframework.boot.context.properties."; public static final String CONTEXT_ANNOTATION_PACKAGE = "org.springframework.context.annotation."; public static final String DATA_PACKAGE = "org.springframework.data."; + public static final String DATA_ELASTICSEARCH_ANNOTATIONS_PACKAGE = DATA_PACKAGE + "elasticsearch.annotations."; + public static final String DATA_MONGODB_CORE_MAPPING_PACKAGE = DATA_PACKAGE + "mongodb.core.mapping."; public static final String SPRING_BOOT_APP_ANNOTATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; public static final String CONTROLLER_ANNOTATION = "org.springframework.stereotype.Controller"; diff --git a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java index 96756a78ef3..9d7b3ee2e2a 100644 --- a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java +++ b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java @@ -61,7 +61,7 @@ final class ClassWithSpringDataQueryGetter { // Noncompliant {{Refactor this cla } @org.springframework.data.mongodb.core.mapping.Document - final class ClassWithSpringDataMongoDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataMongoDocumentAnnotation(int sum)'.}} + final class ClassWithSpringDataMongoDocumentAnnotation { // Compliant, MongoDB document annotations are in scope private final int sum; ClassWithSpringDataMongoDocumentAnnotation(int sum) { this.sum = sum; } @@ -69,7 +69,7 @@ final class ClassWithSpringDataMongoDocumentAnnotation { // Noncompliant {{Refac } @Document - final class ClassWithSpringDataElasticsearchDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataElasticsearchDocumentAnnotation(int sum)'.}} + final class ClassWithSpringDataElasticsearchDocumentAnnotation { // Compliant, Elasticsearch document annotations are in scope private final int sum; ClassWithSpringDataElasticsearchDocumentAnnotation(int sum) { this.sum = sum; } From 476334d00e69e1cf8642dc9d6fe681cfcf63ccee Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 16:47:25 +0200 Subject: [PATCH 05/17] SONARJAVA-6506 Remove arbitrary Spring Data prefixes --- .../checks/RecordInsteadOfClassCheck.java | 4 +--- .../java/checks/helpers/SpringUtils.java | 2 -- ...nsteadOfClassCheckPackagePrefixSample.java | 4 ++-- .../checks/RecordInsteadOfClassCheckTest.java | 5 ++++- .../elasticsearch/annotations/Document.java | 20 ------------------- .../data/mongodb/core/mapping/Document.java | 20 ------------------- 6 files changed, 7 insertions(+), 48 deletions(-) delete mode 100644 java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java delete mode 100644 java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java diff --git a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java index 26306cb015d..f63d69d808f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java @@ -70,9 +70,7 @@ public class RecordInsteadOfClassCheck extends IssuableSubscriptionVisitor imple private static final Set SPRING_ANNOTATION_PACKAGES = Set.of( SpringUtils.BEANS_FACTORY_ANNOTATION_PACKAGE, SpringUtils.BOOT_CONTEXT_PROPERTIES_PACKAGE, - SpringUtils.DATA_PACKAGE + "annotation.", - SpringUtils.DATA_ELASTICSEARCH_ANNOTATIONS_PACKAGE, - SpringUtils.DATA_MONGODB_CORE_MAPPING_PACKAGE); + SpringUtils.DATA_PACKAGE + "annotation."); private static final Set FRAMEWORK_ANNOTATION_PREFIXES = Stream.of( JACKSON_ANNOTATION_PACKAGES, diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java index 6f9c06f9966..25c059f32bd 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/SpringUtils.java @@ -30,8 +30,6 @@ public final class SpringUtils { public static final String BOOT_CONTEXT_PROPERTIES_PACKAGE = "org.springframework.boot.context.properties."; public static final String CONTEXT_ANNOTATION_PACKAGE = "org.springframework.context.annotation."; public static final String DATA_PACKAGE = "org.springframework.data."; - public static final String DATA_ELASTICSEARCH_ANNOTATIONS_PACKAGE = DATA_PACKAGE + "elasticsearch.annotations."; - public static final String DATA_MONGODB_CORE_MAPPING_PACKAGE = DATA_PACKAGE + "mongodb.core.mapping."; public static final String SPRING_BOOT_APP_ANNOTATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; public static final String CONTROLLER_ANNOTATION = "org.springframework.stereotype.Controller"; diff --git a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java index 9d7b3ee2e2a..96756a78ef3 100644 --- a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java +++ b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java @@ -61,7 +61,7 @@ final class ClassWithSpringDataQueryGetter { // Noncompliant {{Refactor this cla } @org.springframework.data.mongodb.core.mapping.Document - final class ClassWithSpringDataMongoDocumentAnnotation { // Compliant, MongoDB document annotations are in scope + final class ClassWithSpringDataMongoDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataMongoDocumentAnnotation(int sum)'.}} private final int sum; ClassWithSpringDataMongoDocumentAnnotation(int sum) { this.sum = sum; } @@ -69,7 +69,7 @@ final class ClassWithSpringDataMongoDocumentAnnotation { // Compliant, MongoDB d } @Document - final class ClassWithSpringDataElasticsearchDocumentAnnotation { // Compliant, Elasticsearch document annotations are in scope + final class ClassWithSpringDataElasticsearchDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataElasticsearchDocumentAnnotation(int sum)'.}} private final int sum; ClassWithSpringDataElasticsearchDocumentAnnotation(int sum) { this.sum = sum; } diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index 2a82c686484..3af15f38833 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -40,7 +40,10 @@ void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() .onFile("src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java") .withCheck(new RecordInsteadOfClassCheck()) - .withClassPath(List.of(new File("target/test-classes"))) + .withClassPath(List.of( + new File("target/test-classes"), + new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-mongodb/3.3.5/spring-data-mongodb-3.3.5.jar"), + new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-elasticsearch/3.0.8.RELEASE/spring-data-elasticsearch-3.0.8.RELEASE.jar"))) .withJavaVersion(16) .verifyIssues(); } diff --git a/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java b/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java deleted file mode 100644 index 16a56917f37..00000000000 --- a/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.springframework.data.elasticsearch.annotations; - -public @interface Document { -} diff --git a/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java b/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java deleted file mode 100644 index 413a0467871..00000000000 --- a/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.springframework.data.mongodb.core.mapping; - -public @interface Document { -} From b8eaea8ff20f7f0380a0965f9e308cebfd90031e Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 17:01:09 +0200 Subject: [PATCH 06/17] Restore self-contained framework prefix test --- .../checks/RecordInsteadOfClassCheckTest.java | 7 +------ .../elasticsearch/annotations/Document.java | 20 +++++++++++++++++++ .../data/mongodb/core/mapping/Document.java | 20 +++++++++++++++++++ 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java create mode 100644 java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index 3af15f38833..46597eddd38 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -16,8 +16,6 @@ */ package org.sonar.java.checks; -import java.io.File; -import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; @@ -40,10 +38,7 @@ void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() .onFile("src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java") .withCheck(new RecordInsteadOfClassCheck()) - .withClassPath(List.of( - new File("target/test-classes"), - new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-mongodb/3.3.5/spring-data-mongodb-3.3.5.jar"), - new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-elasticsearch/3.0.8.RELEASE/spring-data-elasticsearch-3.0.8.RELEASE.jar"))) + .withClassPath(java.util.List.of(new java.io.File("target/test-classes"))) .withJavaVersion(16) .verifyIssues(); } diff --git a/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java b/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java new file mode 100644 index 00000000000..16a56917f37 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java @@ -0,0 +1,20 @@ +/* + * 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.springframework.data.elasticsearch.annotations; + +public @interface Document { +} diff --git a/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java b/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java new file mode 100644 index 00000000000..413a0467871 --- /dev/null +++ b/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java @@ -0,0 +1,20 @@ +/* + * 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.springframework.data.mongodb.core.mapping; + +public @interface Document { +} From 270595d0b9de9ef89e03eefaee26d1f475b89ff5 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 17:10:29 +0200 Subject: [PATCH 07/17] SONARJAVA-6506 Handle constructor parameter annotations --- .../main/java/checks/RecordInsteadOfClassCheckSample.java | 7 +++++++ .../org/sonar/java/checks/RecordInsteadOfClassCheck.java | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java index 1eaaac4479d..74f00791df7 100644 --- a/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckSample.java @@ -263,6 +263,13 @@ final class ClassWithJsonCreatorConstructor { int getSum() { return sum; } } + final class ClassWithJsonAnnotatedConstructorParameter { + private final int sum; + + ClassWithJsonAnnotatedConstructorParameter(@JsonProperty("total") int sum) { this.sum = sum; } + int getSum() { return sum; } + } + final class ClassWithJsonAnnotatedField { @JsonProperty("total") private final int sum; diff --git a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java index f63d69d808f..7c9fe35e8b3 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java @@ -194,12 +194,18 @@ private static boolean hasFrameworkContract( return hasFrameworkAnnotation(classSymbol.metadata()) || hasFrameworkAnnotation(constructor.metadata()) + || hasFrameworkAnnotationOnConstructorParameters(constructor) || fields.stream().anyMatch(field -> hasFrameworkAnnotation(field.metadata())) || methods.stream() .filter(method -> isGetter(method, fieldsNameToType)) .anyMatch(method -> hasFrameworkAnnotation(method.metadata())); } + private static boolean hasFrameworkAnnotationOnConstructorParameters(Symbol.MethodSymbol constructor) { + return constructor.declaration().parameters().stream() + .anyMatch(parameter -> hasFrameworkAnnotation(parameter.symbol().metadata())); + } + private static boolean hasFrameworkAnnotation(SymbolMetadata metadata) { return hasUnknownAnnotation(metadata) || metadata.annotations().stream().anyMatch(RecordInsteadOfClassCheck::isFrameworkAnnotation); } From f33b9db7aae9d466f49796a2b449d808034271c8 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 17:36:14 +0200 Subject: [PATCH 08/17] SONARJAVA-6506 Remove Spring Data document test stubs --- .../checks/RecordInsteadOfClassCheckTest.java | 7 ++++++- .../elasticsearch/annotations/Document.java | 20 ------------------- .../data/mongodb/core/mapping/Document.java | 20 ------------------- 3 files changed, 6 insertions(+), 41 deletions(-) delete mode 100644 java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java delete mode 100644 java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index 46597eddd38..3af15f38833 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -16,6 +16,8 @@ */ package org.sonar.java.checks; +import java.io.File; +import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; @@ -38,7 +40,10 @@ void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() .onFile("src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java") .withCheck(new RecordInsteadOfClassCheck()) - .withClassPath(java.util.List.of(new java.io.File("target/test-classes"))) + .withClassPath(List.of( + new File("target/test-classes"), + new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-mongodb/3.3.5/spring-data-mongodb-3.3.5.jar"), + new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-elasticsearch/3.0.8.RELEASE/spring-data-elasticsearch-3.0.8.RELEASE.jar"))) .withJavaVersion(16) .verifyIssues(); } diff --git a/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java b/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java deleted file mode 100644 index 16a56917f37..00000000000 --- a/java-checks/src/test/java/org/springframework/data/elasticsearch/annotations/Document.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.springframework.data.elasticsearch.annotations; - -public @interface Document { -} diff --git a/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java b/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java deleted file mode 100644 index 413a0467871..00000000000 --- a/java-checks/src/test/java/org/springframework/data/mongodb/core/mapping/Document.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.springframework.data.mongodb.core.mapping; - -public @interface Document { -} From 2797558f7f7a38576dcd5bfc8966a99871ab2f79 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Tue, 23 Jun 2026 17:38:21 +0200 Subject: [PATCH 09/17] SONARJAVA-6506 Remove MongoDB test references --- ...nsteadOfClassCheckPackagePrefixSample.java | 27 ------------------- .../checks/RecordInsteadOfClassCheckTest.java | 7 +---- .../data/mongodb/repository/Query.java | 21 --------------- 3 files changed, 1 insertion(+), 54 deletions(-) delete mode 100644 java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java diff --git a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java index 96756a78ef3..001a2a89731 100644 --- a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java +++ b/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java @@ -6,8 +6,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.mongodb.repository.Query; class RecordInsteadOfClassCheckPackagePrefixSample { @@ -51,31 +49,6 @@ final class ClassWithSpringConfigurationAnnotation { // Noncompliant {{Refactor int getSum() { return sum; } } - final class ClassWithSpringDataQueryGetter { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataQueryGetter(int sum)'.}} - private final int sum; - - ClassWithSpringDataQueryGetter(int sum) { this.sum = sum; } - - @Query("{}") - int getSum() { return sum; } - } - - @org.springframework.data.mongodb.core.mapping.Document - final class ClassWithSpringDataMongoDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataMongoDocumentAnnotation(int sum)'.}} - private final int sum; - - ClassWithSpringDataMongoDocumentAnnotation(int sum) { this.sum = sum; } - int getSum() { return sum; } - } - - @Document - final class ClassWithSpringDataElasticsearchDocumentAnnotation { // Noncompliant {{Refactor this class declaration to use 'record ClassWithSpringDataElasticsearchDocumentAnnotation(int sum)'.}} - private final int sum; - - ClassWithSpringDataElasticsearchDocumentAnnotation(int sum) { this.sum = sum; } - int getSum() { return sum; } - } - final class ClassWithMicronautHttpGetter { // Noncompliant {{Refactor this class declaration to use 'record ClassWithMicronautHttpGetter(int sum)'.}} private final int sum; diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index 3af15f38833..46597eddd38 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -16,8 +16,6 @@ */ package org.sonar.java.checks; -import java.io.File; -import java.util.List; import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; @@ -40,10 +38,7 @@ void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() .onFile("src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java") .withCheck(new RecordInsteadOfClassCheck()) - .withClassPath(List.of( - new File("target/test-classes"), - new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-mongodb/3.3.5/spring-data-mongodb-3.3.5.jar"), - new File(System.getProperty("user.home") + "/.m2/repository/org/springframework/data/spring-data-elasticsearch/3.0.8.RELEASE/spring-data-elasticsearch-3.0.8.RELEASE.jar"))) + .withClassPath(java.util.List.of(new java.io.File("target/test-classes"))) .withJavaVersion(16) .verifyIssues(); } diff --git a/java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java b/java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java deleted file mode 100644 index 536c08ee984..00000000000 --- a/java-checks/src/test/java/org/springframework/data/mongodb/repository/Query.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.springframework.data.mongodb.repository; - -public @interface Query { - String value() default ""; -} From 2c91b517da35a22d171733a9f291871570c0a1cb Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 13:44:26 +0200 Subject: [PATCH 10/17] SONARJAVA-6506 Migrate framework prefix test sample to test-sources Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../checks/RecordInsteadOfClassCheckPackagePrefixSample.java | 0 .../org/sonar/java/checks/RecordInsteadOfClassCheckTest.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {java-checks/src/test/files => java-checks-test-sources/default/src/main/java}/checks/RecordInsteadOfClassCheckPackagePrefixSample.java (100%) diff --git a/java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java b/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckPackagePrefixSample.java similarity index 100% rename from java-checks/src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java rename to java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckPackagePrefixSample.java diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index 46597eddd38..ecad0bbe6c9 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -36,7 +36,7 @@ void test() { @Test void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() - .onFile("src/test/files/checks/RecordInsteadOfClassCheckPackagePrefixSample.java") + .onFile(mainCodeSourcesPath("checks/RecordInsteadOfClassCheckPackagePrefixSample.java")) .withCheck(new RecordInsteadOfClassCheck()) .withClassPath(java.util.List.of(new java.io.File("target/test-classes"))) .withJavaVersion(16) From 25c306616aca437be6474704f230361e7ddcef8d Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 13:56:32 +0200 Subject: [PATCH 11/17] SONARJAVA-6506 Fix DTO contract follow-ups --- ...nsteadOfClassCheckPackagePrefixSample.java | 10 ++++----- .../io/micronaut/http/annotation/Get.java | 20 ------------------ .../micronaut/serde/annotation/Serdeable.java | 20 ------------------ .../checks/RecordInsteadOfClassCheckTest.java | 5 +++-- .../beans/factory/annotation/Value.java | 21 ------------------- .../properties/ConfigurationProperties.java | 21 ------------------- .../context/annotation/Configuration.java | 20 ------------------ .../springframework/data/annotation/Id.java | 20 ------------------ .../org/sonar/l10n/java/rules/java/S6206.html | 7 ++++++- 9 files changed, 14 insertions(+), 130 deletions(-) delete mode 100644 java-checks/src/test/java/io/micronaut/http/annotation/Get.java delete mode 100644 java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java delete mode 100644 java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java delete mode 100644 java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java delete mode 100644 java-checks/src/test/java/org/springframework/context/annotation/Configuration.java delete mode 100644 java-checks/src/test/java/org/springframework/data/annotation/Id.java diff --git a/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckPackagePrefixSample.java b/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckPackagePrefixSample.java index 001a2a89731..9b465e259d7 100644 --- a/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckPackagePrefixSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/RecordInsteadOfClassCheckPackagePrefixSample.java @@ -1,7 +1,7 @@ package checks; import io.micronaut.http.annotation.Get; -import io.micronaut.serde.annotation.Serdeable; +import io.micronaut.core.annotation.Introspected; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @@ -9,8 +9,8 @@ class RecordInsteadOfClassCheckPackagePrefixSample { - @Value("${record.sum}") final class ClassWithSpringValueAnnotation { // Compliant, beans factory annotations are in scope + @Value("${record.sum}") private final int sum; ClassWithSpringValueAnnotation(int sum) { this.sum = sum; } @@ -33,11 +33,11 @@ final class ClassWithSpringDataAnnotationField { // Compliant, spring data annot int getSum() { return sum; } } - @Serdeable - final class ClassWithMicronautSerdeAnnotation { // Compliant, Micronaut serde annotations are in scope + @Introspected + final class ClassWithMicronautCoreAnnotation { // Compliant, Micronaut core annotations are in scope private final int sum; - ClassWithMicronautSerdeAnnotation(int sum) { this.sum = sum; } + ClassWithMicronautCoreAnnotation(int sum) { this.sum = sum; } int getSum() { return sum; } } diff --git a/java-checks/src/test/java/io/micronaut/http/annotation/Get.java b/java-checks/src/test/java/io/micronaut/http/annotation/Get.java deleted file mode 100644 index 2d04c92b5dd..00000000000 --- a/java-checks/src/test/java/io/micronaut/http/annotation/Get.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 io.micronaut.http.annotation; - -public @interface Get { -} diff --git a/java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java b/java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java deleted file mode 100644 index dccdee6632e..00000000000 --- a/java-checks/src/test/java/io/micronaut/serde/annotation/Serdeable.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 io.micronaut.serde.annotation; - -public @interface Serdeable { -} diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index ecad0bbe6c9..84eafdb5684 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -19,8 +19,9 @@ import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; -import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; +import static org.sonar.java.test.classpath.TestClasspathUtils.DEFAULT_MODULE; class RecordInsteadOfClassCheckTest { @@ -38,7 +39,7 @@ void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() .onFile(mainCodeSourcesPath("checks/RecordInsteadOfClassCheckPackagePrefixSample.java")) .withCheck(new RecordInsteadOfClassCheck()) - .withClassPath(java.util.List.of(new java.io.File("target/test-classes"))) + .withClassPath(DEFAULT_MODULE.getClassPath()) .withJavaVersion(16) .verifyIssues(); } diff --git a/java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java b/java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java deleted file mode 100644 index d273e377756..00000000000 --- a/java-checks/src/test/java/org/springframework/beans/factory/annotation/Value.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.springframework.beans.factory.annotation; - -public @interface Value { - String value(); -} diff --git a/java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java b/java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java deleted file mode 100644 index ed0b253513b..00000000000 --- a/java-checks/src/test/java/org/springframework/boot/context/properties/ConfigurationProperties.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.springframework.boot.context.properties; - -public @interface ConfigurationProperties { - String value(); -} diff --git a/java-checks/src/test/java/org/springframework/context/annotation/Configuration.java b/java-checks/src/test/java/org/springframework/context/annotation/Configuration.java deleted file mode 100644 index 16e42e04f88..00000000000 --- a/java-checks/src/test/java/org/springframework/context/annotation/Configuration.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.springframework.context.annotation; - -public @interface Configuration { -} diff --git a/java-checks/src/test/java/org/springframework/data/annotation/Id.java b/java-checks/src/test/java/org/springframework/data/annotation/Id.java deleted file mode 100644 index f54e9579e37..00000000000 --- a/java-checks/src/test/java/org/springframework/data/annotation/Id.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.springframework.data.annotation; - -public @interface Id { -} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html index 913e9a5096b..d75b23e8e5a 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html @@ -9,6 +9,12 @@

Why is this an issue?

  • has only one constructor with a parameter for all fields
  • has getters for all fields
  • +

    The rule does not report classes for which converting to a record could change an external contract. This includes classes participating in Java +serialization, such as classes implementing Serializable or Externalizable, and classes that declare serialization hooks +such as readObject, writeObject, or serialPersistentFields.

    +

    The rule also ignores classes, constructors, constructor parameters, fields, and getters annotated with known framework annotations. This covers +annotations from Jackson, Gson, Micronaut, Jakarta EE, Java EE, Lombok, and selected Spring packages for dependency injection, configuration +properties, and data mapping.

    Noncompliant code example

     final class Person { // Noncompliant
    @@ -42,4 +48,3 @@ 

    Resources

    - From bc4e83c658812f841490fdaac92cf26ca3cac632 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 14:05:39 +0200 Subject: [PATCH 12/17] SONARJAVA-6506 Update autoscan expectation --- .../src/test/resources/autoscan/diffs/diff_S6206.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S6206.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S6206.json index 25bc174d6f4..22b55d6f0cf 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S6206.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S6206.json @@ -1,6 +1,6 @@ { "ruleKey": "S6206", "hasTruePositives": true, - "falseNegatives": 0, - "falsePositives": 0 -} \ No newline at end of file + "falseNegatives": 2, + "falsePositives": 1 +} From 8fd59fd6e9a76609931421566b941146ea1465c9 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 14:17:45 +0200 Subject: [PATCH 13/17] SONARJAVA-6506 Update autoscan FP count --- its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java index ad179fb3db2..fde5893c34a 100644 --- a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java +++ b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java @@ -198,7 +198,7 @@ public void javaCheckTestSources() throws Exception { SoftAssertions softly = new SoftAssertions(); softly.assertThat(newDiffs).containsExactlyInAnyOrderElementsOf(knownDiffs.values()); softly.assertThat(newTotal).isEqualTo(knownTotal); - softly.assertThat(rulesCausingFPs).hasSize(10); + softly.assertThat(rulesCausingFPs).hasSize(11); softly.assertThat(rulesNotReporting).hasSize(19); /** From 5c8f3ee144fa13ae5dabc2c643637da3174e4318 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 14:35:36 +0200 Subject: [PATCH 14/17] SONARJAVA-6506 Remove redundant Externalizable check --- .../java/org/sonar/java/checks/RecordInsteadOfClassCheck.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java index 7c9fe35e8b3..25562e36c88 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java @@ -47,7 +47,6 @@ @Rule(key = "S6206") public class RecordInsteadOfClassCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { - private static final String JAVA_IO_EXTERNALIZABLE = "java.io.Externalizable"; private static final String JAVA_IO_SERIALIZABLE = "java.io.Serializable"; private static final Set JACKSON_ANNOTATION_PACKAGES = Set.of( @@ -155,7 +154,6 @@ public void visitNode(Tree tree) { private static boolean hasSerializationContract(ClassTree classTree) { Type type = classTree.symbol().type(); return type.isSubtypeOf(JAVA_IO_SERIALIZABLE) - || type.isSubtypeOf(JAVA_IO_EXTERNALIZABLE) || classTree.members().stream().anyMatch(RecordInsteadOfClassCheck::isSerializationContractMember); } From 0fbccb94cf5f5397fa8ad6478fa1d27a91a24eb1 Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 15:08:04 +0200 Subject: [PATCH 15/17] SONARJAVA-6506 Adjust serialization contract formatting --- .../org/sonar/java/checks/RecordInsteadOfClassCheck.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java index 25562e36c88..9cefd9a6aed 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java @@ -153,8 +153,9 @@ public void visitNode(Tree tree) { private static boolean hasSerializationContract(ClassTree classTree) { Type type = classTree.symbol().type(); - return type.isSubtypeOf(JAVA_IO_SERIALIZABLE) - || classTree.members().stream().anyMatch(RecordInsteadOfClassCheck::isSerializationContractMember); + return type.isSubtypeOf(JAVA_IO_SERIALIZABLE) || classTree.members() + .stream() + .anyMatch(RecordInsteadOfClassCheck::isSerializationContractMember); } private static boolean isSerializationContractMember(Tree member) { From 06c1d007147b09baf635178d6b2f56769b2c890f Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 16:21:20 +0200 Subject: [PATCH 16/17] SONARJAVA-6506 Update S6206 rule documentation --- .../org/sonar/l10n/java/rules/java/S6206.html | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html index d75b23e8e5a..9b2a70025cd 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6206.html @@ -9,12 +9,6 @@

    Why is this an issue?

  • has only one constructor with a parameter for all fields
  • has getters for all fields
  • -

    The rule does not report classes for which converting to a record could change an external contract. This includes classes participating in Java -serialization, such as classes implementing Serializable or Externalizable, and classes that declare serialization hooks -such as readObject, writeObject, or serialPersistentFields.

    -

    The rule also ignores classes, constructors, constructor parameters, fields, and getters annotated with known framework annotations. This covers -annotations from Jackson, Gson, Micronaut, Jakarta EE, Java EE, Lombok, and selected Spring packages for dependency injection, configuration -properties, and data mapping.

    Noncompliant code example

     final class Person { // Noncompliant
    @@ -44,6 +38,44 @@ 

    Compliant solution

     record Person(String name, int age) { }
     
    +

    Exceptions

    +

    No issue is raised when changing the class to a record could change its contract. This includes classes involved in Java serialization, such as +classes implementing Serializable or Externalizable, classes declaring serialization methods like writeObject, +readObject, readObjectNoData, writeReplace, or readResolve, and classes declaring +serialPersistentFields.

    +
    +final class Person implements java.io.Serializable { // Compliant
    +  private final String name;
    +
    +  Person(String name) {
    +    this.name = name;
    +  }
    +
    +  String getName() {
    +    return name;
    +  }
    +}
    +
    +

    No issue is raised when the class, constructor, constructor parameters, fields, or getters are annotated with framework metadata that defines the +class shape or binding contract. This includes metadata used by serialization, persistence, dependency-injection, or data-binding frameworks such as +Jackson, Gson, Jakarta EE, Java EE, Lombok, Micronaut, and Spring.

    +
    +import com.fasterxml.jackson.annotation.JsonCreator;
    +import com.fasterxml.jackson.annotation.JsonProperty;
    +
    +final class PersonDto { // Compliant
    +  private final String name;
    +
    +  @JsonCreator
    +  PersonDto(@JsonProperty("full_name") String name) {
    +    this.name = name;
    +  }
    +
    +  String getName() {
    +    return name;
    +  }
    +}
    +

    Resources

    • Records specification
    • From 6f8a94cdbd3cf7f36b811a9113142582859edccb Mon Sep 17 00:00:00 2001 From: Francois Mora Date: Wed, 24 Jun 2026 16:32:25 +0200 Subject: [PATCH 17/17] Remove redundant test classpath override --- .../org/sonar/java/checks/RecordInsteadOfClassCheckTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java index 84eafdb5684..04447b48f20 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java @@ -21,7 +21,6 @@ import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; -import static org.sonar.java.test.classpath.TestClasspathUtils.DEFAULT_MODULE; class RecordInsteadOfClassCheckTest { @@ -39,7 +38,6 @@ void test_framework_annotation_prefix_scope() { CheckVerifier.newVerifier() .onFile(mainCodeSourcesPath("checks/RecordInsteadOfClassCheckPackagePrefixSample.java")) .withCheck(new RecordInsteadOfClassCheck()) - .withClassPath(DEFAULT_MODULE.getClassPath()) .withJavaVersion(16) .verifyIssues(); }