Skip to content

SONARJAVA-6506 Avoid suggesting records for DTO contracts#5695

Merged
francois-mora-sonarsource merged 17 commits into
masterfrom
fix/SONARJSAVA-6506-public-dto
Jun 24, 2026
Merged

SONARJAVA-6506 Avoid suggesting records for DTO contracts#5695
francois-mora-sonarsource merged 17 commits into
masterfrom
fix/SONARJSAVA-6506-public-dto

Conversation

@francois-mora-sonarsource

@francois-mora-sonarsource francois-mora-sonarsource commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • prevent S6206 from suggesting records for classes that participate in Java serialization contracts
  • skip the rule when class, constructor, fields, or getter methods carry known framework annotations or unresolved annotations
  • narrow the Spring-related prefix handling and add regression coverage to avoid over-skipping unrelated annotations

Root cause

S6206 only checked structural record eligibility. That missed cases where converting a class to a record would change runtime behavior because serialization hooks or framework metadata define part of the class contract.

Validation

  • mvn -pl java-checks -am -Dtest=org.sonar.java.checks.RecordInsteadOfClassCheckTest -DfailIfNoTests=false -Dsurefire.failIfNoSpecifiedTests=false test
  • git diff --check

RSPEC

https://github.com/SonarSource/rspec/pull/7177

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-6506

Comment thread java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java Outdated
@francois-mora-sonarsource francois-mora-sonarsource marked this pull request as ready for review June 23, 2026 15:28

@asya-vorobeva asya-vorobeva left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense also to update RSpec desc for this rule and shortly list exceptions.

@asya-vorobeva asya-vorobeva left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally LGTM, one little comment.

@sonarqube-next

Copy link
Copy Markdown

@francois-mora-sonarsource francois-mora-sonarsource merged commit 996572c into master Jun 24, 2026
15 checks passed
@francois-mora-sonarsource francois-mora-sonarsource deleted the fix/SONARJSAVA-6506-public-dto branch June 24, 2026 14:50
@gitar-bot

gitar-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 resolved / 4 findings

Prevents S6206 from suggesting record conversion for classes with serialization or framework contracts. Cleaned up redundant Externalizable checks and resolved dependency issues in test configuration.

✅ 4 resolved
Quality: FRAMEWORK_ANNOTATION_PREFIXES map keys are unused and stream is rebuilt per check

📄 java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java:74-81 📄 java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java:204-210
FRAMEWORK_ANNOTATION_PREFIXES is declared as a Map<String, Set<String>> keyed by framework label ("Jackson", "Gson", ...), but the labels are never read anywhere — isFrameworkAnnotation only consumes FRAMEWORK_ANNOTATION_PREFIXES.values(). The map structure therefore adds no value and is slightly misleading (it implies the labels are surfaced, e.g. in the issue message, which they are not).

Additionally, isFrameworkAnnotation re-flattens all value sets into a fresh stream on every annotation it inspects (class + constructor + each field + each getter). Pre-computing a single flattened Set<String> once as a static field avoids this repeated work and simplifies the lookup.

This is non-functional; behavior is correct. Suggestion: collapse the prefixes into one static Set<String> FRAMEWORK_ANNOTATION_PREFIXES and match against it directly.

Bug: Test depends on undeclared jars hardcoded from local ~/.m2 repo

📄 java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java:43-46
test_framework_annotation_prefix_scope builds its classpath from hardcoded paths into the local Maven repository:

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")

This is fragile and will break outside the author's machine:

  1. spring-data-mongodb and spring-data-elasticsearch are NOT declared as dependencies in java-checks/pom.xml (nor the parent). Maven therefore never resolves/downloads them, so the jars are not guaranteed to exist in ~/.m2/repository on CI or other developers' machines. When the file is missing, the type org.springframework.data.mongodb.core.mapping.Document / ...elasticsearch.annotations.Document resolves to unknown semantics and the // Noncompliant expectations may not be raised (or raised differently), causing a CI failure.
  2. The Maven local repository location is not always ~/.m2/repository — it can be relocated via settings.xml <localRepository> or the M2_REPO/maven.repo.local settings. The repo's own TestClasspathUtils.findMavenLocalRepository honors M2_REPO; this hardcoded path does not.
  3. The versions (3.3.5, 3.0.8.RELEASE) are pinned inline and will silently rot.

Note this commit also deleted the local stub Document.java test fixtures (org/springframework/data/.../Document.java) that previously provided these types, so the test now wholly relies on these external jars being present.

Suggested fix: declare the two artifacts as test scope dependencies in java-checks/pom.xml and resolve them through the established TestClasspathUtils / generated target/test-classpath.txt mechanism (as other checks tests do) instead of hardcoding absolute paths and versions.

Bug: Test hardcodes local ~/.m2 jar paths instead of resolved classpath

📄 java-checks/src/test/java/org/sonar/java/checks/RecordInsteadOfClassCheckTest.java:43-46
test_framework_annotation_prefix_scope builds its classpath by pointing File instances at version-pinned artifacts under System.getProperty("user.home") + "/.m2/repository/...". This is fragile and non-portable:

  • It assumes the local Maven repository lives at ~/.m2/repository, which is not guaranteed (custom -Dmaven.repo.local, CI cache layout, Gradle, etc.).
  • It pins exact versions (spring-data-mongodb-3.3.5, spring-data-elasticsearch-3.0.8.RELEASE). When these dependencies are upgraded the test silently breaks because the file no longer exists, so the jar is omitted, annotations no longer resolve, and the test may produce false pass/fail results rather than a clear failure.
  • If the artifacts have not been downloaded yet, the files simply don't exist and the classpath entries are silently ignored.

This is the same local-jar dependency that was previously flagged on this test; this commit re-introduces it after removing the Spring Data stub classes.

The repo already has the correct mechanism: org.sonar.java.test.classpath.TestClasspathUtils exposes DEFAULT_MODULE (mapped to java-checks-test-sources/default), whose pom already declares spring-data-mongodb:3.3.5 and spring-data-elasticsearch:3.0.8.RELEASE as provided dependencies. Use DEFAULT_MODULE.getClassPath() so the classpath is resolved from the build rather than hardcoded paths. Other tests (e.g. MissingPathVariableAnnotationCheckTest, SpringComposedRequestMappingCheckTest) follow this pattern.

Quality: Redundant Externalizable subtype check in hasSerializationContract

📄 java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java:155-160 📄 java-checks/src/main/java/org/sonar/java/checks/RecordInsteadOfClassCheck.java:50
hasSerializationContract checks type.isSubtypeOf(JAVA_IO_SERIALIZABLE) and then type.isSubtypeOf(JAVA_IO_EXTERNALIZABLE). Since java.io.Externalizable extends java.io.Serializable, any class implementing Externalizable is already a subtype of Serializable, so the second condition is unreachable/redundant and can never independently change the result. This is harmless but slightly misleading. Consider dropping the JAVA_IO_EXTERNALIZABLE constant and the second isSubtypeOf call (the ExternalizableClass test case is still covered by the Serializable check), or keep it only if you want explicit documentation of intent via a comment.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants