diff --git a/.gitignore b/.gitignore index d1b60cfa4d..168c8bc683 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ Desktop.ini .java-version .mvn/.develocity/ + +CLAUDE.md +.claude diff --git a/java-checks-test-sources/default/src/main/java/checks/ConstantsShouldBeStaticFinalCheck.java b/java-checks-test-sources/default/src/main/java/checks/ConstantsShouldBeStaticFinalCheck.java index 7f54facb76..183c11f97b 100644 --- a/java-checks-test-sources/default/src/main/java/checks/ConstantsShouldBeStaticFinalCheck.java +++ b/java-checks-test-sources/default/src/main/java/checks/ConstantsShouldBeStaticFinalCheck.java @@ -151,3 +151,25 @@ class FieldAssignments { private final int bar = foo[2]; // Compliant (array access) } +// Test case from SONARJAVA-5796: @Builder.Default with @SuperBuilder should not trigger +@lombok.experimental.SuperBuilder +class LombokSuperBuilderWithDefault { + @lombok.Builder.Default + private final int foo = 1; // Compliant - Builder.Default requires this pattern + + @lombok.Builder.Default + private final String bar = "test"; // Compliant - Builder.Default requires this pattern +} + +@lombok.Builder +class LombokBuilderWithDefault { + @lombok.Builder.Default + private final int baz = 42; // Compliant - Builder.Default requires this pattern +} + +// Without Builder annotations, should still raise issues +class NotABuilder { + @lombok.Builder.Default + private final int shouldRaise = 1; // Noncompliant {{Make this final field static too.}} +} + diff --git a/java-checks/src/main/java/org/sonar/java/checks/ConstantsShouldBeStaticFinalCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ConstantsShouldBeStaticFinalCheck.java index e23c5dffc1..949f84565d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ConstantsShouldBeStaticFinalCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ConstantsShouldBeStaticFinalCheck.java @@ -52,10 +52,11 @@ public void setContext(JavaFileScannerContext context) { @Override public void visitNode(Tree tree) { nestedClassesLevel++; - for (Tree member : ((ClassTree) tree).members()) { + ClassTree classTree = (ClassTree) tree; + for (Tree member : classTree.members()) { if (member.is(Tree.Kind.VARIABLE)) { VariableTree variableTree = (VariableTree) member; - if (staticNonFinal(variableTree) && hasConstantInitializer(variableTree) && !isObjectInInnerClass(variableTree)) { + if (staticNonFinal(variableTree) && hasConstantInitializer(variableTree) && !isObjectInInnerClass(variableTree) && !isLombokBuilderDefault(variableTree, classTree)) { reportIssue(variableTree.simpleName(), "Make this final field static too."); } } @@ -136,4 +137,28 @@ private static boolean isFinal(VariableTree variableTree) { private static boolean isStatic(VariableTree variableTree) { return ModifiersUtils.hasModifier(variableTree.modifiers(), Modifier.STATIC); } + + /** + * Check if a field is annotated with @Builder.Default in a class annotated with @Builder or @SuperBuilder. + * Lombok's builder pattern requires final fields with initializers when using @Builder.Default. + */ + private static boolean isLombokBuilderDefault(VariableTree variableTree, ClassTree classTree) { + // Check if field has @Builder.Default annotation + boolean hasBuilderDefault = variableTree.modifiers().annotations().stream() + .anyMatch(annotation -> { + var type = annotation.symbolType(); + return type.is("lombok.Builder.Default") || type.is("lombok.Builder$Default"); + }); + + if (!hasBuilderDefault) { + return false; + } + + // Check if class has @Builder or @SuperBuilder annotation + return classTree.modifiers().annotations().stream() + .anyMatch(annotation -> { + var type = annotation.symbolType(); + return type.is("lombok.Builder") || type.is("lombok.experimental.SuperBuilder"); + }); + } }