diff --git a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json index 5f59a1871f2..74dd603242b 100644 --- a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json +++ b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json @@ -740,7 +740,7 @@ { "ruleKey": "S2055", "hasTruePositives": true, - "falseNegatives": 0, + "falseNegatives": 1, "falsePositives": 0 }, { diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2055.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2055.json index 828622f7496..5e06815f981 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2055.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2055.json @@ -1,6 +1,6 @@ { "ruleKey": "S2055", "hasTruePositives": true, - "falseNegatives": 0, + "falseNegatives": 1, "falsePositives": 0 -} \ No newline at end of file +} diff --git a/java-checks-test-sources/default/src/main/java/checks/serialization/SerializableSuperConstructorCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/serialization/SerializableSuperConstructorCheckSample.java index 0a08d107b20..bfb19980cce 100644 --- a/java-checks-test-sources/default/src/main/java/checks/serialization/SerializableSuperConstructorCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/serialization/SerializableSuperConstructorCheckSample.java @@ -2,6 +2,8 @@ import java.io.ObjectStreamException; import java.io.Serializable; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; class NonSerializableWithoutConstructor {} @@ -46,3 +48,34 @@ private S2055_Az() {} class S2055_Bz2 extends S2055_Az implements Serializable { S2055_Bz2(String arg1) { super(arg1); } } + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +class NonSerializableWithLombokPrivateNoArgsConstructor { + int field; + + public NonSerializableWithLombokPrivateNoArgsConstructor(int field) { + this.field = field; + } +} + +@NoArgsConstructor +class NonSerializableWithLombokNoArgsConstructor { + int field; + + public NonSerializableWithLombokNoArgsConstructor(int field) { + this.field = field; + } +} + +class S2055_LombokPrivate extends NonSerializableWithLombokPrivateNoArgsConstructor implements Serializable { // Noncompliant {{Add a no-arg constructor to "NonSerializableWithLombokPrivateNoArgsConstructor".}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + S2055_LombokPrivate(int field) { + super(field); + } +} + +class S2055_LombokPublic extends NonSerializableWithLombokNoArgsConstructor implements Serializable { // Compliant + S2055_LombokPublic(int field) { + super(field); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/serialization/SerializableSuperConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/serialization/SerializableSuperConstructorCheck.java index 793c94e2511..ab1215ee60f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/serialization/SerializableSuperConstructorCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/serialization/SerializableSuperConstructorCheck.java @@ -21,9 +21,11 @@ import java.util.List; import javax.annotation.Nullable; import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.AnnotationsHelper; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; 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.ClassTree; import org.sonar.plugins.java.api.tree.Tree; @@ -31,6 +33,8 @@ @Rule(key = "S2055") public class SerializableSuperConstructorCheck extends IssuableSubscriptionVisitor { + private static final String LOMBOK_NO_ARGS_CONSTRUCTOR_ANNOTATION = "lombok.NoArgsConstructor"; + private static final MethodMatchers WRITE_REPLACE = MethodMatchers.create() .ofAnyType() .names("writeReplace") @@ -60,7 +64,8 @@ private static boolean isNotSerializableMissingNoArgConstructor(@Nullable Type s return superclass != null && !superclass.isUnknown() && !isSerializable(superclass) - && !hasNonPrivateNoArgConstructor(superclass); + && !hasNonPrivateNoArgConstructor(superclass) + && !hasCompliantGeneratedNoArgConstructor(superclass); } private static boolean isSerializable(Type type) { @@ -80,6 +85,27 @@ private static boolean hasNonPrivateNoArgConstructor(Type type) { return constructors.isEmpty(); } + private static boolean hasCompliantGeneratedNoArgConstructor(Type type) { + return type.symbol() + .metadata() + .annotations() + .stream() + .anyMatch(annotation -> isLombokNoArgConstructorGenerator(annotation.symbol().type()) && !hasPrivateAccess(annotation)); + } + + private static boolean isLombokNoArgConstructorGenerator(Type symbolType) { + if (symbolType.isUnknown()) { + return AnnotationsHelper.annotationTypeIdentifier(LOMBOK_NO_ARGS_CONSTRUCTOR_ANNOTATION).equals(symbolType.name()); + } + return LOMBOK_NO_ARGS_CONSTRUCTOR_ANNOTATION.equals(symbolType.fullyQualifiedName()); + } + + private static boolean hasPrivateAccess(SymbolMetadata.AnnotationInstance annotation) { + return annotation.values() + .stream() + .anyMatch(v -> "access".equals(v.name()) && "PRIVATE".equals(((Symbol) v.value()).name())); + } + private static boolean implementsSerializableMethods(Symbol.TypeSymbol classSymbol) { return classSymbol.memberSymbols().stream().anyMatch(WRITE_REPLACE::matches); }