From 928fc59e4b3a0e5ffe9af0da1c6deb2f50b2aeb5 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 14:36:23 +0100 Subject: [PATCH 01/21] Add empty check + test / reproducer --- .../PresuperLogicShoudntBloatConstructor.java | 64 +++++++++++++++++++ .../PresuperLogicBloatsConstructorCheck.java | 42 ++++++++++++ ...esuperLogicBloatsConstructorCheckTest.java | 44 +++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java new file mode 100644 index 00000000000..20c9d3f2aec --- /dev/null +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java @@ -0,0 +1,64 @@ +package org.sonar.java.checks; + +public class PresuperLogicShoudntBloatConstructor { + public static class File { + public File(String path) { + // Simulate file initialization logic + } + } + + public static class NonCompliantSecureFile { + public NonCompliantSecureFile(String path) { + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("Path cannot be empty"); + } + if (path.contains("..")) { + throw new IllegalArgumentException("Relative path traversal is forbidden"); + } + if (path.startsWith("/root") || path.startsWith("/etc")) { + throw new SecurityException("Access to system directories is restricted"); + } + if (path.length() > 255) { + throw new IllegalArgumentException("Path exceeds maximum length"); + } + if (!path.matches("^[a-zA-Z0-9/._-]+$")) { + throw new IllegalArgumentException("Path contains illegal characters"); + } + String sanitizedPath = path.trim().replace("//", "/"); + if (sanitizedPath.endsWith("/")) { + sanitizedPath = sanitizedPath.substring(0, sanitizedPath.length() - 1); + } + super(sanitizedPath); // Noncompliant + } + } + + public static class CompliantSecureFile extends File { + public CompliantSecureFile(String path) { + // Compliant: Logic is encapsulated in static helpers + validatePathSecurity(path); + validatePathFormat(path); + String sanitizedPath = normalizePath(path); + super(sanitizedPath); + } + + private static void validatePathSecurity(String path) { + if (path == null || path.contains("..")) { + throw new IllegalArgumentException("Invalid or dangerous path sequence"); + } + if (path.startsWith("/root") || path.startsWith("/etc")) { + throw new SecurityException("Access to system directories is restricted"); + } + } + + private static void validatePathFormat(String path) { + if (path.length() > 255 || !path.matches("^[a-zA-Z0-9/._-]+$")) { + throw new IllegalArgumentException("Path format or length is invalid"); + } + } + + private static String normalizePath(String path) { + String cleaned = path.trim().replace("//", "/"); + return cleaned.endsWith("/") ? cleaned.substring(0, cleaned.length() - 1) : cleaned; + } + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java new file mode 100644 index 00000000000..62d0c493887 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java @@ -0,0 +1,42 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S8444") +public class PresuperLogicBloatsConstructorCheck extends IssuableSubscriptionVisitor { + + @Override + public List nodesToVisit() { + return List.of(); + } +} + diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java new file mode 100644 index 00000000000..a2caff4a054 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; + +class PresuperLogicBloatsConstructorCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructor.java")) + .withCheck(new PresuperLogicBloatsConstructorCheck()) + .verifyIssues(); + } + + @Test + void test_no_semantic() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructor.java")) + .withCheck(new JacksonDeserializationCheck()) + .withoutSemantic() + .verifyNoIssues(); + } + +} From 429a78ae7535bb9c589ac60e40c79ded26733922 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 14:54:36 +0100 Subject: [PATCH 02/21] Refactoring : extract flexible constructor logic to abstract class FlexibleConstructorCheck # Conflicts: # java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java --- ...lexibleConstructorBodyValidationCheck.java | 67 +++--------------- .../java/checks/FlexibleConstructorCheck.java | 70 +++++++++++++++++++ 2 files changed, 79 insertions(+), 58 deletions(-) create mode 100644 java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java diff --git a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java index bdcfee0fa17..b00bef07467 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java @@ -16,21 +16,15 @@ */ package org.sonar.java.checks; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.sonar.check.Rule; -import org.sonar.java.model.ExpressionUtils; -import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; -import org.sonar.plugins.java.api.JavaVersion; -import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; -import org.sonar.plugins.java.api.tree.BlockTree; import org.sonar.plugins.java.api.tree.ExpressionStatementTree; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.IdentifierTree; @@ -40,9 +34,10 @@ import org.sonar.plugins.java.api.tree.StatementTree; import org.sonar.plugins.java.api.tree.ThrowStatementTree; import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.VariableTree; @Rule(key = "S8433") -public class FlexibleConstructorBodyValidationCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { +public class FlexibleConstructorBodyValidationCheck extends FlexibleConstructorCheck { private static final MethodMatchers VALIDATION_METHODS = MethodMatchers.or( MethodMatchers.create() @@ -68,42 +63,18 @@ public class FlexibleConstructorBodyValidationCheck extends IssuableSubscription ); @Override - public List nodesToVisit() { - return Collections.singletonList(Tree.Kind.CONSTRUCTOR); - } - - @Override - public boolean isCompatibleWithJavaVersion(JavaVersion version) { - return version.isJava25Compatible(); - } - - @Override - public void visitNode(Tree tree) { - MethodTree constructor = (MethodTree) tree; - BlockTree body = constructor.block(); - - if (body == null || body.body().isEmpty()) { - return; - } - - // Find the super() or this() call - int constructorCallIndex = findConstructorCallIndex(body); - - // Get statements after the constructor call - List statements = body.body(); - if (constructorCallIndex == statements.size() - 1 + void validateConstructor(MethodTree constructor, List body, int constructorCallIndex) { + if (constructorCallIndex == body.size() - 1 || (constructorCallIndex == -1 && hasNoExplicitSuperClass(constructor))) { // No statements after constructor call or no superclass and no constructor call return; } - // Collect constructor parameters for analysis - Set parameters = new HashSet<>(); - constructor.parameters().forEach(param -> parameters.add(param.symbol())); + Set parameters = constructor.parameters().stream().map(VariableTree::symbol).collect(Collectors.toSet()); // Analyze statements after the constructor call for movable validation - for (int i = constructorCallIndex + 1; i < statements.size(); i++) { - StatementTree statement = statements.get(i); + for (int i = constructorCallIndex + 1; i < body.size(); i++) { + StatementTree statement = body.get(i); if (isValidationStatement(statement) && canBeMovedToPrologue(statement, parameters)) { reportIssue(statement, "Move this validation logic before the super() or this() call."); @@ -111,26 +82,6 @@ public void visitNode(Tree tree) { } } - /** - * Find the index of an explicit super() or this() call in the constructor body. - * - * @param body the constructor body to search - * @return the index of the explicit super() or this() call, or -1 if no explicit call is found (implicit super()) - */ - private static int findConstructorCallIndex(BlockTree body) { - List statements = body.body(); - for (int i = 0; i < statements.size(); i++) { - if (statements.get(i) instanceof ExpressionStatementTree expressionStatementTree - && expressionStatementTree.expression() instanceof MethodInvocationTree methodInvocationTree - && methodInvocationTree.methodSelect() instanceof IdentifierTree identifierTree - && ExpressionUtils.isThisOrSuper(identifierTree.name())){ - return i; - } - } - // No explicit super() or this() call - return -1; - } - private static boolean hasNoExplicitSuperClass(MethodTree constructor) { Type superClass = constructor.symbol().enclosingClass().superClass(); return (superClass == null || superClass.is("java.lang.Object")); @@ -202,7 +153,7 @@ public void visitIdentifier(IdentifierTree tree) { Symbol symbol = tree.symbol(); // Allow parameters, local variables and static fields / methods - if (symbol.isLocalVariable() || symbol.isStatic()|| parameters.contains(symbol)) { + if (symbol.isLocalVariable() || symbol.isStatic() || parameters.contains(symbol)) { return; } diff --git a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java new file mode 100644 index 00000000000..20f9a81c013 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java @@ -0,0 +1,70 @@ +package org.sonar.java.checks; + +import java.util.Collections; +import java.util.List; +import org.sonar.java.model.ExpressionUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; +import org.sonar.plugins.java.api.tree.BlockTree; +import org.sonar.plugins.java.api.tree.ExpressionStatementTree; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.StatementTree; +import org.sonar.plugins.java.api.tree.Tree; + +public abstract class FlexibleConstructorCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { + + /** + * Validate the constructor body, providing the constructor method tree, the list of statements in the constructor body, and the index of any explicit super() or this() call (or -1 if no explicit call is found). + * @param constructor the constructor method tree being validated + * @param body the list of statements in the constructor body + * @param constructorCallIndex the index of any explicit super() or this() call in the body, or -1 if no explicit call is found (implicit super()) + */ + abstract void validateConstructor(MethodTree constructor, List body, int constructorCallIndex); + + @Override + public final List nodesToVisit() { + return Collections.singletonList(Tree.Kind.CONSTRUCTOR); + } + + @Override + public final boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava25Compatible(); + } + + @Override + public final void visitNode(Tree tree) { + MethodTree constructor = (MethodTree) tree; + BlockTree block = constructor.block(); + if (block == null || block.body().isEmpty()) { + // No body or empty body, nothing to validate + return; + } + List body = block.body(); + + // Find the super() or this() call + int constructorCallIndex = findConstructorCallIndex(body); + validateConstructor(constructor, body, constructorCallIndex); + } + + /** + * Find the index of an explicit super() or this() call in the constructor body. + * + * @param body the constructor body to search + * @return the index of the explicit super() or this() call, or -1 if no explicit call is found (implicit super()) + */ + private static int findConstructorCallIndex(List body) { + for (int i = 0; i < body.size(); i++) { + if (body.get(i) instanceof ExpressionStatementTree expressionStatementTree + && expressionStatementTree.expression() instanceof MethodInvocationTree methodInvocationTree + && methodInvocationTree.methodSelect() instanceof IdentifierTree identifierTree + && ExpressionUtils.isThisOrSuper(identifierTree.name())) { + return i; + } + } + // No explicit super() or this() call + return -1; + } +} From ededae2eb13750627fdf4ce2db07d8226e272141 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 15:54:55 +0100 Subject: [PATCH 03/21] Initial rule implementation --- .../PresuperLogicShoudntBloatConstructor.java | 5 ++-- .../PresuperLogicBloatsConstructorCheck.java | 27 +++++++++---------- ...esuperLogicBloatsConstructorCheckTest.java | 3 +-- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java index 20c9d3f2aec..9a9756aaacc 100644 --- a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java @@ -9,7 +9,8 @@ public File(String path) { public static class NonCompliantSecureFile { public NonCompliantSecureFile(String path) { - if (path == null || path.isBlank()) { + if (path == null || path.isBlank()) { // Noncompliant {{Excessive logic in this "pre-construction" phase makes the code harder to read and maintain.}} + // ^[el=+19;ec=7] throw new IllegalArgumentException("Path cannot be empty"); } if (path.contains("..")) { @@ -28,7 +29,7 @@ public NonCompliantSecureFile(String path) { if (sanitizedPath.endsWith("/")) { sanitizedPath = sanitizedPath.substring(0, sanitizedPath.length() - 1); } - super(sanitizedPath); // Noncompliant + super(sanitizedPath); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java index 62d0c493887..f974132ed5f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java @@ -16,27 +16,24 @@ */ package org.sonar.java.checks; -import java.util.Arrays; import java.util.List; -import java.util.Optional; import org.sonar.check.Rule; -import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; -import org.sonar.plugins.java.api.semantic.MethodMatchers; -import org.sonar.plugins.java.api.semantic.Type; -import org.sonar.plugins.java.api.tree.AnnotationTree; -import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; -import org.sonar.plugins.java.api.tree.ExpressionTree; -import org.sonar.plugins.java.api.tree.IdentifierTree; -import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; -import org.sonar.plugins.java.api.tree.MethodInvocationTree; -import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.StatementTree; @Rule(key = "S8444") -public class PresuperLogicBloatsConstructorCheck extends IssuableSubscriptionVisitor { +public class PresuperLogicBloatsConstructorCheck extends FlexibleConstructorCheck { + private static final int MAX_STATEMENTS_BEFORE_CONSTRUCTOR_CALL = 3; @Override - public List nodesToVisit() { - return List.of(); + void validateConstructor(MethodTree constructor, List body, int constructorCallIndex) { + if (constructorCallIndex > MAX_STATEMENTS_BEFORE_CONSTRUCTOR_CALL) { + reportIssue( + body.get(0), + body.get(constructorCallIndex - 1), + "Excessive logic in this \"pre-construction\" phase makes the code harder to read and maintain." + ); + } } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java index a2caff4a054..6bdb6623d9f 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -19,7 +19,6 @@ import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; -import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; class PresuperLogicBloatsConstructorCheckTest { @@ -29,6 +28,7 @@ void test() { CheckVerifier.newVerifier() .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructor.java")) .withCheck(new PresuperLogicBloatsConstructorCheck()) + .withJavaVersion(25) .verifyIssues(); } @@ -40,5 +40,4 @@ void test_no_semantic() { .withoutSemantic() .verifyNoIssues(); } - } From 25d7baca6f1d5db425f91c7d1c0c4f855da4f95f Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 15:56:37 +0100 Subject: [PATCH 04/21] Remove useless test_no_semantic --- .../PresuperLogicBloatsConstructorCheckTest.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java index 6bdb6623d9f..8c70e6d1915 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -22,7 +22,6 @@ import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; class PresuperLogicBloatsConstructorCheckTest { - @Test void test() { CheckVerifier.newVerifier() @@ -31,13 +30,4 @@ void test() { .withJavaVersion(25) .verifyIssues(); } - - @Test - void test_no_semantic() { - CheckVerifier.newVerifier() - .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructor.java")) - .withCheck(new JacksonDeserializationCheck()) - .withoutSemantic() - .verifyNoIssues(); - } } From 6638b6850e767fa3a1b1c75edcb2f0f63efcf441 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 15:57:27 +0100 Subject: [PATCH 05/21] mvn license:format --- .../java/checks/FlexibleConstructorCheck.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java index 20f9a81c013..02ae863fb0d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java @@ -1,3 +1,19 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ package org.sonar.java.checks; import java.util.Collections; From 2638ea26fa0edf7ce21f3cba1ec98b9f1a4d88fc Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 16:22:37 +0100 Subject: [PATCH 06/21] Added nested statements support --- .../PresuperLogicShoudntBloatConstructor.java | 30 ++++++++++++++++++- .../PresuperLogicBloatsConstructorCheck.java | 9 +++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java index 9a9756aaacc..e354cac473f 100644 --- a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java @@ -7,7 +7,7 @@ public File(String path) { } } - public static class NonCompliantSecureFile { + public static class NonCompliantSecureFile extends File { public NonCompliantSecureFile(String path) { if (path == null || path.isBlank()) { // Noncompliant {{Excessive logic in this "pre-construction" phase makes the code harder to read and maintain.}} // ^[el=+19;ec=7] @@ -62,4 +62,32 @@ private static String normalizePath(String path) { return cleaned.endsWith("/") ? cleaned.substring(0, cleaned.length() - 1) : cleaned; } } + + public static class NonCompliantSecureFile2 extends File { + public NonCompliantSecureFile(String path) { + if (true) { // Noncompliant {{Excessive logic in this "pre-construction" phase makes the code harder to read and maintain.}} + // ^[el=+21;ec=7] + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("Path cannot be empty"); + } + if (path.contains("..")) { + throw new IllegalArgumentException("Relative path traversal is forbidden"); + } + if (path.startsWith("/root") || path.startsWith("/etc")) { + throw new SecurityException("Access to system directories is restricted"); + } + if (path.length() > 255) { + throw new IllegalArgumentException("Path exceeds maximum length"); + } + if (!path.matches("^[a-zA-Z0-9/._-]+$")) { + throw new IllegalArgumentException("Path contains illegal characters"); + } + String sanitizedPath = path.trim().replace("//", "/"); + if (sanitizedPath.endsWith("/")) { + sanitizedPath = sanitizedPath.substring(0, sanitizedPath.length() - 1); + } + } + super(sanitizedPath); + } + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java index f974132ed5f..60134b3bcb8 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java @@ -18,6 +18,7 @@ import java.util.List; import org.sonar.check.Rule; +import org.sonar.java.ast.visitors.StatementVisitor; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.StatementTree; @@ -27,7 +28,13 @@ public class PresuperLogicBloatsConstructorCheck extends FlexibleConstructorChec @Override void validateConstructor(MethodTree constructor, List body, int constructorCallIndex) { - if (constructorCallIndex > MAX_STATEMENTS_BEFORE_CONSTRUCTOR_CALL) { + if (constructorCallIndex < 0) { + // No constructor call, nothing to check + return; + } + StatementVisitor statementVisitor = new StatementVisitor(); + int statementsBeforeConstructorCall = body.stream().limit(constructorCallIndex).map(statementVisitor::numberOfStatements).reduce(0, Integer::sum); + if (statementsBeforeConstructorCall > MAX_STATEMENTS_BEFORE_CONSTRUCTOR_CALL) { reportIssue( body.get(0), body.get(constructorCallIndex - 1), From 15eafd6b000a157c5c7af45c5cd75714c954f27d Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 16:34:42 +0100 Subject: [PATCH 07/21] Add edge cases tests --- .../PresuperLogicShoudntBloatConstructor.java | 106 ++++++++++++++---- 1 file changed, 85 insertions(+), 21 deletions(-) diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java index e354cac473f..77f5aee4fcd 100644 --- a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java @@ -33,6 +33,34 @@ public NonCompliantSecureFile(String path) { } } + public static class NonCompliantSecureFileNestedStatements extends File { + public NonCompliantSecureFile(String path) { + if (true) { // Noncompliant {{Excessive logic in this "pre-construction" phase makes the code harder to read and maintain.}} + // ^[el=+21;ec=7] + if (path == null || path.isBlank()) { + throw new IllegalArgumentException("Path cannot be empty"); + } + if (path.contains("..")) { + throw new IllegalArgumentException("Relative path traversal is forbidden"); + } + if (path.startsWith("/root") || path.startsWith("/etc")) { + throw new SecurityException("Access to system directories is restricted"); + } + if (path.length() > 255) { + throw new IllegalArgumentException("Path exceeds maximum length"); + } + if (!path.matches("^[a-zA-Z0-9/._-]+$")) { + throw new IllegalArgumentException("Path contains illegal characters"); + } + String sanitizedPath = path.trim().replace("//", "/"); + if (sanitizedPath.endsWith("/")) { + sanitizedPath = sanitizedPath.substring(0, sanitizedPath.length() - 1); + } + } + super(sanitizedPath); + } + } + public static class CompliantSecureFile extends File { public CompliantSecureFile(String path) { // Compliant: Logic is encapsulated in static helpers @@ -63,31 +91,67 @@ private static String normalizePath(String path) { } } - public static class NonCompliantSecureFile2 extends File { - public NonCompliantSecureFile(String path) { - if (true) { // Noncompliant {{Excessive logic in this "pre-construction" phase makes the code harder to read and maintain.}} - // ^[el=+21;ec=7] - if (path == null || path.isBlank()) { - throw new IllegalArgumentException("Path cannot be empty"); - } - if (path.contains("..")) { - throw new IllegalArgumentException("Relative path traversal is forbidden"); - } - if (path.startsWith("/root") || path.startsWith("/etc")) { - throw new SecurityException("Access to system directories is restricted"); - } - if (path.length() > 255) { - throw new IllegalArgumentException("Path exceeds maximum length"); - } - if (!path.matches("^[a-zA-Z0-9/._-]+$")) { - throw new IllegalArgumentException("Path contains illegal characters"); + public static class EdgeCaseSecureFile extends File { + public EdgeCaseSecureFile(String path) { + // Compliant: There are 3 statements before super() : if, throw, var declaration + if (path.length() > 255 || !path.matches("^[a-zA-Z0-9/._-]+$")) { + throw new IllegalArgumentException("Path format or length is invalid"); + } + String sanitizedPath = normalizePath(path); + super(sanitizedPath); + } + + public EdgeCaseSecureFile(String path, boolean b) { + // Non-compliant: There are 4 statements before super() : if, throw, var declaration, method call + validatePathSecurity(path); // Noncompliant {{Excessive logic in this "pre-construction" phase makes the code harder to read and maintain.}} + // ^[el=+5;ec=49] + if (path.length() > 255 || !path.matches("^[a-zA-Z0-9/._-]+$")) { + throw new IllegalArgumentException("Path format or length is invalid"); + } + String sanitizedPath = normalizePath(path); + super(sanitizedPath); + } + + public EdgeCaseSecureFile(String path, int i) { + // Compliant: There are 3 statements before super() : if, if, try-catch block + if (true) { + if (true) { + try {} + catch (Exception e) {} + finally {} } - String sanitizedPath = path.trim().replace("//", "/"); - if (sanitizedPath.endsWith("/")) { - sanitizedPath = sanitizedPath.substring(0, sanitizedPath.length() - 1); + } + super(sanitizedPath); + } + + + public EdgeCaseSecureFile(String path, float f) { + // Compliant: There are 4 statements before super() : if, if, try-catch block, if + if (true) { // Noncompliant {{Excessive logic in this "pre-construction" phase makes the code harder to read and maintain.}} + // ^[el=+9;ec=7] + if (true) { + try { + if (true) {} + } + catch (Exception e) {} + finally {} } } super(sanitizedPath); } + + private static void validatePathSecurity(String path) { + if (path == null || path.contains("..")) { + throw new IllegalArgumentException("Invalid or dangerous path sequence"); + } + if (path.startsWith("/root") || path.startsWith("/etc")) { + throw new SecurityException("Access to system directories is restricted"); + } + } + + private static String normalizePath(String path) { + String cleaned = path.trim().replace("//", "/"); + return cleaned.endsWith("/") ? cleaned.substring(0, cleaned.length() - 1) : cleaned; + } } } From 7aca6bdc73067a06690a76645ac4b0e517182c5d Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 16:37:04 +0100 Subject: [PATCH 08/21] Remove redundant helper functions from the sample --- .../PresuperLogicShoudntBloatConstructor.java | 47 +++++++------------ 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java index 77f5aee4fcd..fe0e9995342 100644 --- a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java @@ -69,26 +69,6 @@ public CompliantSecureFile(String path) { String sanitizedPath = normalizePath(path); super(sanitizedPath); } - - private static void validatePathSecurity(String path) { - if (path == null || path.contains("..")) { - throw new IllegalArgumentException("Invalid or dangerous path sequence"); - } - if (path.startsWith("/root") || path.startsWith("/etc")) { - throw new SecurityException("Access to system directories is restricted"); - } - } - - private static void validatePathFormat(String path) { - if (path.length() > 255 || !path.matches("^[a-zA-Z0-9/._-]+$")) { - throw new IllegalArgumentException("Path format or length is invalid"); - } - } - - private static String normalizePath(String path) { - String cleaned = path.trim().replace("//", "/"); - return cleaned.endsWith("/") ? cleaned.substring(0, cleaned.length() - 1) : cleaned; - } } public static class EdgeCaseSecureFile extends File { @@ -139,19 +119,26 @@ public EdgeCaseSecureFile(String path, float f) { } super(sanitizedPath); } + } - private static void validatePathSecurity(String path) { - if (path == null || path.contains("..")) { - throw new IllegalArgumentException("Invalid or dangerous path sequence"); - } - if (path.startsWith("/root") || path.startsWith("/etc")) { - throw new SecurityException("Access to system directories is restricted"); - } + + private static void validatePathSecurity(String path) { + if (path == null || path.contains("..")) { + throw new IllegalArgumentException("Invalid or dangerous path sequence"); + } + if (path.startsWith("/root") || path.startsWith("/etc")) { + throw new SecurityException("Access to system directories is restricted"); } + } - private static String normalizePath(String path) { - String cleaned = path.trim().replace("//", "/"); - return cleaned.endsWith("/") ? cleaned.substring(0, cleaned.length() - 1) : cleaned; + private static void validatePathFormat(String path) { + if (path.length() > 255 || !path.matches("^[a-zA-Z0-9/._-]+$")) { + throw new IllegalArgumentException("Path format or length is invalid"); } } + + private static String normalizePath(String path) { + String cleaned = path.trim().replace("//", "/"); + return cleaned.endsWith("/") ? cleaned.substring(0, cleaned.length() - 1) : cleaned; + } } From a6570128f3725249a98eb9b899f1058a6bfb7150 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 16:37:55 +0100 Subject: [PATCH 09/21] Rename PresuperLogicShoudntBloatConstructor -> PresuperLogicShoudntBloatConstructorSample --- ...tor.java => PresuperLogicShoudntBloatConstructorSample.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename java-checks-test-sources/default/src/main/files/non-compiling/checks/{PresuperLogicShoudntBloatConstructor.java => PresuperLogicShoudntBloatConstructorSample.java} (98%) diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructorSample.java similarity index 98% rename from java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java rename to java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructorSample.java index fe0e9995342..ad7bdefff88 100644 --- a/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructor.java +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/PresuperLogicShoudntBloatConstructorSample.java @@ -1,6 +1,6 @@ package org.sonar.java.checks; -public class PresuperLogicShoudntBloatConstructor { +public class PresuperLogicShoudntBloatConstructorSample { public static class File { public File(String path) { // Simulate file initialization logic From 1b6b789d496bfd98f8f497209d5c05dbbe230773 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 16:38:38 +0100 Subject: [PATCH 10/21] Continue : Rename PresuperLogicShoudntBloatConstructor -> PresuperLogicShoudntBloatConstructorSample - fix tests --- .../java/checks/PresuperLogicBloatsConstructorCheckTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java index 8c70e6d1915..5cfe3f85d13 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -25,7 +25,7 @@ class PresuperLogicBloatsConstructorCheckTest { @Test void test() { CheckVerifier.newVerifier() - .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructor.java")) + .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructorSample.java")) .withCheck(new PresuperLogicBloatsConstructorCheck()) .withJavaVersion(25) .verifyIssues(); From cbdec08ab4e2a5420e699ce32f5901d33b338def Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Tue, 10 Feb 2026 16:40:29 +0100 Subject: [PATCH 11/21] Add rule documentation --- .../org/sonar/l10n/java/rules/java/S8444.html | 77 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S8444.json | 23 ++++++ .../java/rules/java/Sonar_way_profile.json | 1 + 3 files changed, 101 insertions(+) create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html new file mode 100644 index 00000000000..64ad4bacb2c --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html @@ -0,0 +1,77 @@ +

While Java 25+ allows statements to appear before the super(…​) or this(…​) call in a constructor, this area should be +reserved for simple validation, transformation, or preparation of arguments. Excessive logic in this "pre-construction" phase makes the code harder to +read and maintain.

+

Why is this an issue?

+

The ability to place code before super() is intended for basic tasks like parameter validation or simple transformations. Using this +space for complex logic obscures the primary purpose of the constructor, increases maintenance risk by introducing complex control flow before object +initialization, and violates the separation of concerns.

+

How to fix it

+

Refactor complex pre-construction logic into private static helper methods or static factory methods. Keep statements before super() +limited to simple validations and direct parameter transformations.

+

Code examples

+
+public class SecureFile extends File {
+    public SecureFile(String path) {
+        // Noncompliant: Validation and path normalization logic is too verbose
+        if (path == null || path.isBlank()) {
+            throw new IllegalArgumentException("Path cannot be empty");
+        }
+        if (path.contains("..")) {
+            throw new IllegalArgumentException("Relative path traversal is forbidden");
+        }
+        if (path.startsWith("/root") || path.startsWith("/etc")) {
+            throw new SecurityException("Access to system directories is restricted");
+        }
+        if (path.length() > 255) {
+            throw new IllegalArgumentException("Path exceeds maximum length");
+        }
+        if (!path.matches("^[a-zA-Z0-9/._-]+$")) {
+            throw new IllegalArgumentException("Path contains illegal characters");
+        }
+
+        String sanitizedPath = path.trim().replace("//", "/");
+        if (sanitizedPath.endsWith("/")) {
+            sanitizedPath = sanitizedPath.substring(0, sanitizedPath.length() - 1);
+        }
+
+        super(sanitizedPath);
+    }
+}
+
+

Compliant solution

+
+public class SecureFile extends File {
+    public SecureFile(String path) {
+        // Compliant: Logic is encapsulated in static helpers
+        validatePathSecurity(path);
+        validatePathFormat(path);
+        String sanitizedPath = normalizePath(path);
+        super(sanitizedPath);
+    }
+
+    private static void validatePathSecurity(String path) {
+        if (path == null || path.contains("..")) {
+            throw new IllegalArgumentException("Invalid or dangerous path sequence");
+        }
+        if (path.startsWith("/root") || path.startsWith("/etc")) {
+            throw new SecurityException("Access to system directories is restricted");
+        }
+    }
+
+    private static void validatePathFormat(String path) {
+        if (path.length() > 255 || !path.matches("^[a-zA-Z0-9/._-]+$")) {
+            throw new IllegalArgumentException("Path format or length is invalid");
+        }
+    }
+
+    private static String normalizePath(String path) {
+        String cleaned = path.trim().replace("//", "/");
+        return cleaned.endsWith("/") ? cleaned.substring(0, cleaned.length() - 1) : cleaned;
+    }
+}
+
+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json new file mode 100644 index 00000000000..e868d79a8c4 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json @@ -0,0 +1,23 @@ +{ + "title": "Validation and data preparation logic before super() should not bloat constructor", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "java25" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-8444", + "sqKey": "S8444", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index d5a11a963cf..5cf5febfe27 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -520,5 +520,6 @@ "S8346", "S8432", "S8433" + "S8444" ] } From e735e4bc36acb3ed06a74adb5ecab8864919196d Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 11 Feb 2026 10:18:35 +0100 Subject: [PATCH 12/21] Add test to verify that there is no issue on jdk24 --- .../checks/PresuperLogicBloatsConstructorCheckTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java index 5cfe3f85d13..e94e6e5b8fb 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -30,4 +30,13 @@ void test() { .withJavaVersion(25) .verifyIssues(); } + + @Test + void test_java_24() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructorSample.java")) + .withCheck(new PresuperLogicBloatsConstructorCheck()) + .withJavaVersion(24) + .verifyNoIssues(); + } } From 65cd056a4fc370ad5c15cbbcf9d92e18b0f8e5c1 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 11 Feb 2026 10:24:40 +0100 Subject: [PATCH 13/21] Make statementsThreshold parametrizable --- .../checks/PresuperLogicBloatsConstructorCheck.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java index 60134b3bcb8..c6b7c98a52e 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java @@ -18,13 +18,20 @@ import java.util.List; import org.sonar.check.Rule; +import org.sonar.check.RuleProperty; import org.sonar.java.ast.visitors.StatementVisitor; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.StatementTree; @Rule(key = "S8444") public class PresuperLogicBloatsConstructorCheck extends FlexibleConstructorCheck { - private static final int MAX_STATEMENTS_BEFORE_CONSTRUCTOR_CALL = 3; + + private static final int DEFAULT_STATEMENTS_THRESHOLD = 3; + @RuleProperty( + key = "statementsThreshold", + description = "The issue message", + defaultValue = "" + DEFAULT_STATEMENTS_THRESHOLD) + public int statementsThreshold = DEFAULT_STATEMENTS_THRESHOLD; @Override void validateConstructor(MethodTree constructor, List body, int constructorCallIndex) { @@ -34,7 +41,7 @@ void validateConstructor(MethodTree constructor, List body, int c } StatementVisitor statementVisitor = new StatementVisitor(); int statementsBeforeConstructorCall = body.stream().limit(constructorCallIndex).map(statementVisitor::numberOfStatements).reduce(0, Integer::sum); - if (statementsBeforeConstructorCall > MAX_STATEMENTS_BEFORE_CONSTRUCTOR_CALL) { + if (statementsBeforeConstructorCall > statementsThreshold) { reportIssue( body.get(0), body.get(constructorCallIndex - 1), From 1ee4dd5e437e74861ae36d249a369634371087f2 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 11 Feb 2026 10:28:14 +0100 Subject: [PATCH 14/21] Set DEFAULT_STATEMENTS_THRESHOLD to 5, fix tests, fix RuleProperty description --- .../java/checks/PresuperLogicBloatsConstructorCheck.java | 4 ++-- .../java/checks/PresuperLogicBloatsConstructorCheckTest.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java index c6b7c98a52e..275f8d8ef3f 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java @@ -26,10 +26,10 @@ @Rule(key = "S8444") public class PresuperLogicBloatsConstructorCheck extends FlexibleConstructorCheck { - private static final int DEFAULT_STATEMENTS_THRESHOLD = 3; + private static final int DEFAULT_STATEMENTS_THRESHOLD = 5; @RuleProperty( key = "statementsThreshold", - description = "The issue message", + description = "Maximum number of statements allowed before the constructor call.", defaultValue = "" + DEFAULT_STATEMENTS_THRESHOLD) public int statementsThreshold = DEFAULT_STATEMENTS_THRESHOLD; diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java index e94e6e5b8fb..9562cca4a30 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -24,9 +24,11 @@ class PresuperLogicBloatsConstructorCheckTest { @Test void test() { + var check = new PresuperLogicBloatsConstructorCheck(); + check.statementsThreshold = 3; CheckVerifier.newVerifier() .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructorSample.java")) - .withCheck(new PresuperLogicBloatsConstructorCheck()) + .withCheck(check) .withJavaVersion(25) .verifyIssues(); } From ecf3b83769efd01f36b66e9c8d6c576d00a5ac57 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 11 Feb 2026 10:30:19 +0100 Subject: [PATCH 15/21] Fix tests --- .../PresuperLogicBloatsConstructorCheckTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java index 9562cca4a30..a28f646248a 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -16,16 +16,23 @@ */ package org.sonar.java.checks; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.sonar.java.checks.verifier.CheckVerifier; import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; class PresuperLogicBloatsConstructorCheckTest { + private static final PresuperLogicBloatsConstructorCheck check = new PresuperLogicBloatsConstructorCheck(); + + @BeforeAll + static void setUp() { + check.statementsThreshold = 3; + } + @Test void test() { - var check = new PresuperLogicBloatsConstructorCheck(); - check.statementsThreshold = 3; + CheckVerifier.newVerifier() .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructorSample.java")) .withCheck(check) From 678d04e2dce36b5839955730d0d7f586a072a66a Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 11 Feb 2026 10:30:39 +0100 Subject: [PATCH 16/21] Fix tests --- .../java/checks/PresuperLogicBloatsConstructorCheckTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java index a28f646248a..8931867e421 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheckTest.java @@ -44,7 +44,7 @@ void test() { void test_java_24() { CheckVerifier.newVerifier() .onFile(nonCompilingTestSourcesPath("checks/PresuperLogicShoudntBloatConstructorSample.java")) - .withCheck(new PresuperLogicBloatsConstructorCheck()) + .withCheck(check) .withJavaVersion(24) .verifyNoIssues(); } From 493d4c1484f526a9df58e0c90a4825e0a17a62ac Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 11 Feb 2026 10:51:50 +0100 Subject: [PATCH 17/21] Update rule metadata --- .../main/resources/org/sonar/l10n/java/rules/java/S8444.html | 5 +++++ .../main/resources/org/sonar/l10n/java/rules/java/S8444.json | 2 +- sonarpedia.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html index 64ad4bacb2c..279550c2a9e 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.html @@ -8,6 +8,11 @@

Why is this an issue?

How to fix it

Refactor complex pre-construction logic into private static helper methods or static factory methods. Keep statements before super() limited to simple validations and direct parameter transformations.

+

Parameters

statementsThreshold +
+5
+
+

Maximum number of statements allowed before the constructor call.

Code examples

 public class SecureFile extends File {
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json
index e868d79a8c4..c336600dadf 100644
--- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8444.json
@@ -1,5 +1,5 @@
 {
-  "title": "Validation and data preparation logic before super() should not bloat constructor",
+  "title": "Excessive logic before super() should not bloat constructor",
   "type": "CODE_SMELL",
   "status": "ready",
   "remediation": {
diff --git a/sonarpedia.json b/sonarpedia.json
index daa120100b9..323b874aace 100644
--- a/sonarpedia.json
+++ b/sonarpedia.json
@@ -8,4 +8,4 @@
     "no-language-in-filenames": true,
     "preserve-filenames": false
   }
-}
\ No newline at end of file
+}

From b0c000c634385035bb23f0be84ec4cfe272c5421 Mon Sep 17 00:00:00 2001
From: "romain.birling" 
Date: Wed, 11 Feb 2026 17:01:00 +0100
Subject: [PATCH 18/21] fix Sonar_way_profile.json

---
 .../org/sonar/l10n/java/rules/java/Sonar_way_profile.json       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json
index 5cf5febfe27..c6ee6637bd6 100644
--- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json
@@ -519,7 +519,7 @@
     "S7629",
     "S8346",
     "S8432",
-    "S8433"
+    "S8433",
     "S8444"
   ]
 }

From e428faa23198f03e8c816c0a27f1cc954f1abdfe Mon Sep 17 00:00:00 2001
From: "romain.birling" 
Date: Thu, 12 Feb 2026 07:54:04 +0100
Subject: [PATCH 19/21] Try to fix : GeneratedCheckListTest

---
 .../java/checks/FlexibleConstructorBodyValidationCheck.java     | 2 +-
 ...bleConstructorCheck.java => FlexibleConstructorVisitor.java} | 2 +-
 .../sonar/java/checks/PresuperLogicBloatsConstructorCheck.java  | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)
 rename java-checks/src/main/java/org/sonar/java/checks/{FlexibleConstructorCheck.java => FlexibleConstructorVisitor.java} (96%)

diff --git a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java
index b00bef07467..caf7e2ec485 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorBodyValidationCheck.java
@@ -37,7 +37,7 @@
 import org.sonar.plugins.java.api.tree.VariableTree;
 
 @Rule(key = "S8433")
-public class FlexibleConstructorBodyValidationCheck extends FlexibleConstructorCheck {
+public class FlexibleConstructorBodyValidationCheck extends FlexibleConstructorVisitor {
 
   private static final MethodMatchers VALIDATION_METHODS = MethodMatchers.or(
     MethodMatchers.create()
diff --git a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorVisitor.java
similarity index 96%
rename from java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java
rename to java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorVisitor.java
index 02ae863fb0d..ff10bff98a1 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorVisitor.java
@@ -30,7 +30,7 @@
 import org.sonar.plugins.java.api.tree.StatementTree;
 import org.sonar.plugins.java.api.tree.Tree;
 
-public abstract class FlexibleConstructorCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {
+public abstract class FlexibleConstructorVisitor extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {
 
   /**
    * Validate the constructor body, providing the constructor method tree, the list of statements in the constructor body, and the index of any explicit super() or this() call (or -1 if no explicit call is found).
diff --git a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java
index 275f8d8ef3f..b39d9514768 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/PresuperLogicBloatsConstructorCheck.java
@@ -24,7 +24,7 @@
 import org.sonar.plugins.java.api.tree.StatementTree;
 
 @Rule(key = "S8444")
-public class PresuperLogicBloatsConstructorCheck extends FlexibleConstructorCheck {
+public class PresuperLogicBloatsConstructorCheck extends FlexibleConstructorVisitor {
 
   private static final int DEFAULT_STATEMENTS_THRESHOLD = 5;
   @RuleProperty(

From a2fb2ed4e1cea7a0583f2420522da33636ead64d Mon Sep 17 00:00:00 2001
From: "romain.birling" 
Date: Thu, 12 Feb 2026 08:14:11 +0100
Subject: [PATCH 20/21] Try to fix : Autoscan, QG

---
 .../src/test/resources/autoscan/diffs/diff_S8444.json       | 6 ++++++
 .../org/sonar/java/checks/FlexibleConstructorVisitor.java   | 3 ++-
 2 files changed, 8 insertions(+), 1 deletion(-)
 create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S8444.json

diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8444.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8444.json
new file mode 100644
index 00000000000..5c4d1d40100
--- /dev/null
+++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8444.json
@@ -0,0 +1,6 @@
+{
+  "ruleKey": "S8444",
+  "hasTruePositives": false,
+  "falseNegatives": 0,
+  "falsePositives": 0
+}
\ No newline at end of file
diff --git a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorVisitor.java b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorVisitor.java
index ff10bff98a1..b2a7eefe1cc 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorVisitor.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/FlexibleConstructorVisitor.java
@@ -33,7 +33,8 @@
 public abstract class FlexibleConstructorVisitor extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {
 
   /**
-   * Validate the constructor body, providing the constructor method tree, the list of statements in the constructor body, and the index of any explicit super() or this() call (or -1 if no explicit call is found).
+   * Validate the constructor body, providing the constructor method tree, the list of statements in the constructor body, and the index of any explicit super() or this() call
+   * (or -1 if no explicit call is found).
    * @param constructor the constructor method tree being validated
    * @param body the list of statements in the constructor body
    * @param constructorCallIndex the index of any explicit super() or this() call in the body, or -1 if no explicit call is found (implicit super())

From 70f41a4979de849077d0b9f870a018dbfe0f3b33 Mon Sep 17 00:00:00 2001
From: "romain.birling" 
Date: Thu, 12 Feb 2026 08:32:37 +0100
Subject: [PATCH 21/21] Try to fix : Autoscan | 2

---
 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 a17dd65453f..6e25b8a73c0 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
@@ -199,7 +199,7 @@ public void javaCheckTestSources() throws Exception {
     softly.assertThat(newDiffs).containsExactlyInAnyOrderElementsOf(knownDiffs.values());
     softly.assertThat(newTotal).isEqualTo(knownTotal);
     softly.assertThat(rulesCausingFPs).hasSize(10);
-    softly.assertThat(rulesNotReporting).hasSize(15);
+    softly.assertThat(rulesNotReporting).hasSize(16);
 
     /**
      * 4. Check total number of differences (FPs + FNs)