From 79b6f82b7225f5fd4d652436180f3865734c4f66 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 11:25:23 +0100 Subject: [PATCH 01/19] Initial commit - documentation, reproducer # Conflicts: # sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json # Conflicts: # sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json # Conflicts: # sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json --- .../checks/MultipleMainInstancesSample.java | 29 ++++++++++++ .../checks/MultipleMainInstancesCheck.java | 30 ++++++++++++ .../MultipleMainInstancesCheckTest.java | 33 +++++++++++++ .../org/sonar/l10n/java/rules/java/S8446.html | 46 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S8446.json | 21 +++++++++ .../java/rules/java/Sonar_way_profile.json | 1 + 6 files changed, 160 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java new file mode 100644 index 00000000000..423a3f7ed93 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -0,0 +1,29 @@ +package checks; + +public class MultipleMainInstancesSample { + public static class NonCompliant { + public static void main(String[] args) { // Noncompliant + System.out.println("Static main detected; shadowing instance main."); + } + + void main() { // Noncompliant + System.out.println("Unreachable entry point due to static precedence."); + } + } + + public static class Compliant { + public static class LegacyApplication { + // Compliant: Standard static entry point + public static void main(String[] args) { // Compliant + System.out.println("Running standard static entry point."); + } + } + + static class Application { + // Compliant: Instance main method in a separate class + void main() { // Compliant + System.out.println("Running modern instance main entry point."); + } + } + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java new file mode 100644 index 00000000000..d6d600390fb --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -0,0 +1,30 @@ +/* + * 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.List; +import org.sonar.check.Rule; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S8446") +public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor { + @Override + public List nodesToVisit() { + return List.of(); + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java new file mode 100644 index 00000000000..af50695b3a9 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java @@ -0,0 +1,33 @@ +/* + * 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; + +class MultipleMainInstancesCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/MultipleMainInstancesSample.java")) + .withCheck(new MultipleMainInstancesCheck()) + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.html new file mode 100644 index 00000000000..400ef879d8e --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.html @@ -0,0 +1,46 @@ +

Starting with Java 25 (JEP 512), multiple main method signatures are allowed within a single class. While the JVM follows a strict +selection algorithm to determine the entry point, having more than one main method can lead to significant confusion.

+

Why is this an issue?

+

Having multiple main methods reduces code clarity and maintainability. Readers may be unaware of the specific JVM selection rules—such +as the preference for static over instance methods or methods with parameters over those without—and might incorrectly assume a different +method will execute. This ambiguity forces developers to spend unnecessary time deciphering which method actually serves as the program’s entry +point.

+

How to fix it

+

Keep only one main method per class. If you require multiple entry points for different execution scenarios, create separate classes +or rename the secondary methods to reflect their actual purpose.

+

Code examples

+

Noncompliant code example

+
+public class Application {
+    // Noncompliant: Multiple main methods are confusing and lead to shadowing
+    public static void main(String[] args) {
+        System.out.println("Static main detected; shadowing instance main.");
+    }
+
+    void main() {
+        System.out.println("Unreachable entry point due to static precedence.");
+    }
+}
+
+

Compliant solution

+
+public class LegacyApplication {
+    // Compliant: Standard static entry point
+    public static void main(String[] args) {
+        System.out.println("Running standard static entry point.");
+    }
+}
+
+class Application {
+    // Compliant: Instance main method in a separate class
+    void main() {
+        System.out.println("Running modern instance main entry point.");
+    }
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json new file mode 100644 index 00000000000..4a7800e3262 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json @@ -0,0 +1,21 @@ +{ + "title": "Only one \"main\" method should be present", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-8446", + "sqKey": "S8446", + "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 6945b8dab69..4feda171218 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 @@ -523,6 +523,7 @@ "S8433", "S8444", "S8445", + "S8446", "S8447", "S8450" ] From 575f219f69f700c6fddb630880f5995ccce04bbc Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 11:39:47 +0100 Subject: [PATCH 02/19] Rule implementation --- .../checks/MultipleMainInstancesSample.java | 2 + .../checks/MultipleMainInstancesCheck.java | 38 ++++++++++++++++++- .../MultipleMainInstancesCheckTest.java | 10 +++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index 423a3f7ed93..ae033f7980b 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -3,10 +3,12 @@ public class MultipleMainInstancesSample { public static class NonCompliant { public static void main(String[] args) { // Noncompliant +// ^[el=+3;ec=5] 0 System.out.println("Static main detected; shadowing instance main."); } void main() { // Noncompliant +// ^[el=+3;ec=5] 0 System.out.println("Unreachable entry point due to static precedence."); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index d6d600390fb..85191ceeb22 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -17,14 +17,48 @@ package org.sonar.java.checks; import java.util.List; +import java.util.stream.Stream; import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.MethodTreeUtils; 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.ClassTree; +import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S8446") -public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor { +public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { @Override public List nodesToVisit() { - return List.of(); + return List.of(Tree.Kind.CLASS); + } + + @Override + public void visitNode(Tree tree) { + if (tree instanceof ClassTree ct) { + List mainMethods = ct.members().stream().flatMap(member -> { + if (member instanceof MethodTree mt && isMainMethod(mt)) { + return Stream.of(mt); + } else { + return Stream.empty(); + } + } + ).toList(); + if (mainMethods.size() > 1) { + mainMethods.forEach(mt -> + reportIssue(mt, "Only one main method should be defined in a class.") + ); + } + } + } + + private boolean isMainMethod(MethodTree tree) { + return MethodTreeUtils.isMainMethod(tree, context.getJavaVersion()); + } + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava25Compatible(); } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java index af50695b3a9..226617fdc55 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java @@ -28,6 +28,16 @@ void test() { CheckVerifier.newVerifier() .onFile(mainCodeSourcesPath("checks/MultipleMainInstancesSample.java")) .withCheck(new MultipleMainInstancesCheck()) + .withJavaVersion(25) .verifyIssues(); } + + @Test + void test_java_24() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/MultipleMainInstancesSample.java")) + .withCheck(new MultipleMainInstancesCheck()) + .withJavaVersion(24) + .verifyNoIssues(); + } } From a17436fe10af94e74fb142c5b243282660903d99 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 11:44:01 +0100 Subject: [PATCH 03/19] Try to fix : autoscan | 1 --- .../src/test/resources/autoscan/diffs/diff_S8446.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json new file mode 100644 index 00000000000..ec5c4e2fa2c --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8446", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} From fb430dd99929e65eea1efa92756b4e6dd91aab23 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 11:46:10 +0100 Subject: [PATCH 04/19] Try to fix : autoscan | 2 --- its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json index ec5c4e2fa2c..44f478aa1d8 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json @@ -1,6 +1,6 @@ { "ruleKey": "S8446", - "hasTruePositives": true, + "hasTruePositives": false, "falseNegatives": 0, "falsePositives": 0 } From 6c65f75ff6e6a6858a64c63d86544f75969ffe43 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 11:47:05 +0100 Subject: [PATCH 05/19] Better issue message --- .../java/org/sonar/java/checks/MultipleMainInstancesCheck.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index 85191ceeb22..5a84d76cb89 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -47,7 +47,7 @@ public void visitNode(Tree tree) { ).toList(); if (mainMethods.size() > 1) { mainMethods.forEach(mt -> - reportIssue(mt, "Only one main method should be defined in a class.") + reportIssue(mt, "At most one main method should be defined in a class.") ); } } From d74df8181a69ca0e0d5b6377d3c7ca31a7656dd3 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 12:57:26 +0100 Subject: [PATCH 06/19] Support Interfaces, Enums and Records --- .../checks/MultipleMainInstancesSample.java | 32 +++++++++++++++++++ .../checks/MultipleMainInstancesCheck.java | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index ae033f7980b..c3e316c5097 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -28,4 +28,36 @@ void main() { // Compliant } } } + + public static enum NonCompliantEnum { + INSTANCE; + + public static void main(String[] args) { // Noncompliant + System.out.println("Static main in enum detected; shadowing instance main."); + } + + void main() { // Noncompliant + System.out.println("Unreachable entry point in enum due to static precedence."); + } + } + + public static interface NonCompliantInterface { + static void main(String[] args) { // Noncompliant + System.out.println("Static main in interface detected; shadowing instance main."); + } + + default void main() { // Noncompliant + System.out.println("Unreachable entry point in interface due to static precedence."); + } + } + + public static record NonCompliantRecord(String data) { + public static void main(String[] args) { // Noncompliant + System.out.println("Static main in record detected; shadowing instance main."); + } + + void main() { // Noncompliant + System.out.println("Unreachable entry point in record due to static precedence."); + } + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index 5a84d76cb89..bc497680f23 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -31,7 +31,7 @@ public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { @Override public List nodesToVisit() { - return List.of(Tree.Kind.CLASS); + return List.of(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.RECORD); } @Override From 4a8d472a14c92f545ad98e1b4c041e87b83e6c52 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 14:16:22 +0100 Subject: [PATCH 07/19] Support implicit classes --- .../main/java/checks/MultipleMainInstancesSample.java | 11 +++++++++-- .../sonar/java/checks/MultipleMainInstancesCheck.java | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index c3e316c5097..776be3a5b74 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -1,5 +1,3 @@ -package checks; - public class MultipleMainInstancesSample { public static class NonCompliant { public static void main(String[] args) { // Noncompliant @@ -61,3 +59,12 @@ void main() { // Noncompliant } } } + +// test implicit class +void main() { // Noncompliant + System.out.println("Hello World!"); +} + +static public void main(String[] args) { // Noncompliant + System.out.println("Hello World!"); +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index bc497680f23..1c580a428f4 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -31,7 +31,7 @@ public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { @Override public List nodesToVisit() { - return List.of(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.RECORD); + return List.of(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.RECORD, Tree.Kind.IMPLICIT_CLASS); } @Override From 16f2dc21d080c93e6c7139da97739e3a0abdfb61 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 14:48:48 +0100 Subject: [PATCH 08/19] Update rule metadata : tag java25 --- .../main/resources/org/sonar/l10n/java/rules/java/S8446.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json index 4a7800e3262..d6ac4dff41c 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8446.json @@ -6,7 +6,9 @@ "func": "Constant\/Issue", "constantCost": "5min" }, - "tags": [], + "tags": [ + "java25" + ], "defaultSeverity": "Major", "ruleSpecification": "RSPEC-8446", "sqKey": "S8446", From eab0c458e3244f0394e875d2b7f68d5da82de67b Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 12 Feb 2026 16:11:42 +0100 Subject: [PATCH 09/19] Try to fix : autoscan | 3 --- .../src/test/resources/autoscan/autoscan-diff-by-rules.json | 6 ++++++ 1 file changed, 6 insertions(+) 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 13e4ae799fd..22381df1808 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 @@ -2879,6 +2879,12 @@ "falseNegatives": 0, "falsePositives": 0 }, + { + "ruleKey": "8446", + "hasTruePositives": false, + "falseNegatives": 0, + "falsePositives": 0 + }, { "ruleKey": "8447", "hasTruePositives": true, From caefca65258e8a57e8bfb3b742ce2b783767f3ff Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Fri, 13 Feb 2026 08:03:51 +0100 Subject: [PATCH 10/19] Try to fix : autoscan | 4 --- .../src/test/resources/autoscan/autoscan-diff-by-rules.json | 2 +- its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 22381df1808..861986b624a 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 @@ -2881,7 +2881,7 @@ }, { "ruleKey": "8446", - "hasTruePositives": false, + "hasTruePositives": true, "falseNegatives": 0, "falsePositives": 0 }, diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json index 44f478aa1d8..ec5c4e2fa2c 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json @@ -1,6 +1,6 @@ { "ruleKey": "S8446", - "hasTruePositives": false, + "hasTruePositives": true, "falseNegatives": 0, "falsePositives": 0 } From fe158d063f0431b64ced29c8549bef915b75b5cd Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Fri, 13 Feb 2026 15:30:22 +0100 Subject: [PATCH 11/19] Address Tomasz' comments --- .../checks/MultipleMainInstancesSample.java | 16 ++++++------ .../checks/MultipleMainInstancesCheck.java | 25 ++++++++----------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index 776be3a5b74..f6b04a95edf 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -1,12 +1,11 @@ public class MultipleMainInstancesSample { public static class NonCompliant { public static void main(String[] args) { // Noncompliant -// ^[el=+3;ec=5] 0 +// ^^^^ System.out.println("Static main detected; shadowing instance main."); } - void main() { // Noncompliant -// ^[el=+3;ec=5] 0 + void main() { System.out.println("Unreachable entry point due to static precedence."); } } @@ -34,7 +33,8 @@ public static void main(String[] args) { // Noncompliant System.out.println("Static main in enum detected; shadowing instance main."); } - void main() { // Noncompliant + void main() { + System.out.println("Unreachable entry point in enum due to static precedence."); } } @@ -44,7 +44,8 @@ static void main(String[] args) { // Noncompliant System.out.println("Static main in interface detected; shadowing instance main."); } - default void main() { // Noncompliant + default void main() { + System.out.println("Unreachable entry point in interface due to static precedence."); } } @@ -54,7 +55,8 @@ public static void main(String[] args) { // Noncompliant System.out.println("Static main in record detected; shadowing instance main."); } - void main() { // Noncompliant + void main() { + System.out.println("Unreachable entry point in record due to static precedence."); } } @@ -65,6 +67,6 @@ void main() { // Noncompliant System.out.println("Hello World!"); } -static public void main(String[] args) { // Noncompliant +static public void main(String[] args) { System.out.println("Hello World!"); } diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index 1c580a428f4..ca48ff3845c 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -17,7 +17,6 @@ package org.sonar.java.checks; import java.util.List; -import java.util.stream.Stream; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.MethodTreeUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; @@ -36,20 +35,16 @@ public List nodesToVisit() { @Override public void visitNode(Tree tree) { - if (tree instanceof ClassTree ct) { - List mainMethods = ct.members().stream().flatMap(member -> { - if (member instanceof MethodTree mt && isMainMethod(mt)) { - return Stream.of(mt); - } else { - return Stream.empty(); - } - } - ).toList(); - if (mainMethods.size() > 1) { - mainMethods.forEach(mt -> - reportIssue(mt, "At most one main method should be defined in a class.") - ); - } + ClassTree ct = (ClassTree) tree; + List mainMethods = ct.members().stream() + .filter(MethodTree.class::isInstance) + .map(MethodTree.class::cast) + .filter(this::isMainMethod) + .toList(); + if (mainMethods.size() > 1) { + var firstMainMethod = mainMethods.get(0); + var firstMainMethodToken = firstMainMethod.simpleName(); + reportIssue(firstMainMethodToken, "At most one main method should be defined in a class."); } } From ca0ca6462165fc47303d01d199567301375e64c5 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Mon, 16 Feb 2026 11:08:06 +0100 Subject: [PATCH 12/19] Extend rule implementation - detect multiple main methods due to overrides --- .../checks/MultipleMainInstancesSample.java | 99 ++++++++++++++++++- .../checks/MultipleMainInstancesCheck.java | 46 +++++++-- 2 files changed, 135 insertions(+), 10 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index f6b04a95edf..a752ebc6483 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -1,6 +1,6 @@ public class MultipleMainInstancesSample { public static class NonCompliant { - public static void main(String[] args) { // Noncompliant + public static void main(String[] args) { // Noncompliant {{At most one main method should be defined in a class.}} // ^^^^ System.out.println("Static main detected; shadowing instance main."); } @@ -56,10 +56,105 @@ public static void main(String[] args) { // Noncompliant } void main() { - System.out.println("Unreachable entry point in record due to static precedence."); } } + + public static class NonCompliantWithOverloads { + class Parent { + void main() { + System.out.println("Parent instance main method."); + } + } + + class Child extends Parent { + public static void main(String[] args) { // Noncompliant {{Main method should not be defined in a class if a main method is already defined in a superclass.}} + System.out.println("Static main in child class detected; shadowing instance main."); + } + } + } + + public static class CompliantWithOverloads { + class Parent { + void main() { + System.out.println("Parent instance main method."); + } + } + + class Child extends Parent { + @Override + void main() { // Compliant: This is an instance method that overrides the parent main + System.out.println("Child instance main method overriding parent main."); + } + } + } + + public static class TwoMainsInParentOneOverriddenInChild { + class Parent { + void main() { // Noncompliant {{At most one main method should be defined in a class.}} + System.out.println("Parent void main method."); + } + + void main(String[] args) { + System.out.println("Parent String args main method."); + } + } + + class Child extends Parent { + @Override + void main() { // Compliant: This is an instance method that overrides the parent main + System.out.println("Child instance main method overriding parent main."); + } + } + } + + public static class TwoMainsInParentTwoOverriddenInChild { + class Parent { + void main() { // Noncompliant {{At most one main method should be defined in a class.}} + System.out.println("Parent void main method."); + } + + void main(String[] args) { + System.out.println("Parent String args main method."); + } + } + + class Child extends Parent { + @Override + void main() { // Noncompliant {{At most one main method should be defined in a class.}} + System.out.println("Child instance main method overriding parent main."); + } + + @Override + void main(String[] args) { + System.out.println("Child String args main method overriding parent main."); + } + } + } + + public static class ChainedOverrides { + class GrandParent { + void main() { + System.out.println("Parent void main method."); + } + } + + class Parent extends GrandParent { + } + + class NonCompliantChild extends Parent { + void main(String[] args) { // Noncompliant {{Main method should not be defined in a class if a main method is already defined in a superclass.}} + System.out.println("Child main method detected; shadowing grandparent main."); + } + } + + class CompliantChild extends Parent { + @Override + void main() { // Compliant: This is an instance method that overrides the parent main + System.out.println("Child instance main method overriding grandparent main."); + } + } + } } // test implicit class diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index ca48ff3845c..02e8b15045a 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -17,6 +17,8 @@ package org.sonar.java.checks; import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.MethodTreeUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; @@ -36,16 +38,44 @@ public List nodesToVisit() { @Override public void visitNode(Tree tree) { ClassTree ct = (ClassTree) tree; - List mainMethods = ct.members().stream() - .filter(MethodTree.class::isInstance) - .map(MethodTree.class::cast) - .filter(this::isMainMethod) - .toList(); - if (mainMethods.size() > 1) { - var firstMainMethod = mainMethods.get(0); + List membersMainMethods = findMainMethodsInMembers(ct).toList(); + List superMainMethods = findMainMethodsInSuperclasses(ct).toList(); + boolean hasMembersMainMethod = !membersMainMethods.isEmpty(); + boolean hasMultipleMainMethods = membersMainMethods.size() + superMainMethods.size() > 1; + boolean hasLegitSingleMainOverride = + membersMainMethods.size() == 1 + && Optional.ofNullable(membersMainMethods.get(0).isOverriding()).orElse(false); + + if (hasMembersMainMethod && hasMultipleMainMethods && !hasLegitSingleMainOverride) { + var firstMainMethod = membersMainMethods.get(0); var firstMainMethodToken = firstMainMethod.simpleName(); - reportIssue(firstMainMethodToken, "At most one main method should be defined in a class."); + var errorMessage = membersMainMethods.size() > 1 ? + "At most one main method should be defined in a class." : + "Main method should not be defined in a class if a main method is already defined in a superclass."; + reportIssue(firstMainMethodToken, errorMessage); + } + } + + private Stream findMainMethodsInSuperclasses(ClassTree ct) { + var superClass = ct.superClass(); + if (superClass == null) { + return Stream.empty(); } + var superClassTree = superClass.symbolType().symbol().declaration(); + if (superClassTree == null) { + return Stream.empty(); + } + return Stream.concat( + findMainMethodsInMembers(superClassTree), + findMainMethodsInSuperclasses(superClassTree) + ); + } + + private Stream findMainMethodsInMembers(ClassTree ct) { + return ct.members().stream() + .filter(MethodTree.class::isInstance) + .map(MethodTree.class::cast) + .filter(this::isMainMethod); } private boolean isMainMethod(MethodTree tree) { From c783f28913d704eb0be677a207a1259e23612f9c Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 18 Feb 2026 11:39:22 +0100 Subject: [PATCH 13/19] Address Tomasz'review - improve error messages logic, verify precedence (priority) child/super --- .../checks/MultipleMainInstancesSample.java | 23 ++++++-- .../checks/MultipleMainInstancesCheck.java | 57 ++++++++++++++----- .../java/checks/helpers/MethodTreeUtils.java | 20 +++++++ 3 files changed, 82 insertions(+), 18 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index a752ebc6483..293cb2e7250 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -34,7 +34,6 @@ public static void main(String[] args) { // Noncompliant } void main() { - System.out.println("Unreachable entry point in enum due to static precedence."); } } @@ -60,7 +59,7 @@ void main() { } } - public static class NonCompliantWithOverloads { + public static class NonCompliantChildPrecedence { class Parent { void main() { System.out.println("Parent instance main method."); @@ -68,12 +67,26 @@ void main() { } class Child extends Parent { - public static void main(String[] args) { // Noncompliant {{Main method should not be defined in a class if a main method is already defined in a superclass.}} + void main(String[] args) { // Noncompliant {{Override main from Parent to avoid introducing multiple main methods.}} System.out.println("Static main in child class detected; shadowing instance main."); } } } + public static class NonCompliantParentPrecedence { + class Parent { + void main(String[] args) { + System.out.println("Parent instance main method."); + } + } + + class Child extends Parent { + void main() { // Noncompliant {{This 'main' method will not be the entry point because another inherited 'main' from Parent takes precedence.}} + System.out.println("Parent main method takes precedence over child main; this method will not be the entry point."); + } + } + } + public static class CompliantWithOverloads { class Parent { void main() { @@ -102,7 +115,7 @@ void main(String[] args) { class Child extends Parent { @Override - void main() { // Compliant: This is an instance method that overrides the parent main + void main() { // Noncompliant {{This 'main' method will not be the entry point because another inherited 'main' from Parent takes precedence.}} System.out.println("Child instance main method overriding parent main."); } } @@ -143,7 +156,7 @@ class Parent extends GrandParent { } class NonCompliantChild extends Parent { - void main(String[] args) { // Noncompliant {{Main method should not be defined in a class if a main method is already defined in a superclass.}} + void main(String[] args) { // Noncompliant {{Override main from GrandParent to avoid introducing multiple main methods.}} System.out.println("Child main method detected; shadowing grandparent main."); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index 02e8b15045a..ad56edbaa8a 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -28,6 +28,8 @@ import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; +//https://github.com/SonarSource/sonar-java/pull/5455 + @Rule(key = "S8446") public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { @Override @@ -39,21 +41,50 @@ public List nodesToVisit() { public void visitNode(Tree tree) { ClassTree ct = (ClassTree) tree; List membersMainMethods = findMainMethodsInMembers(ct).toList(); + if (membersMainMethods.isEmpty()) { + return; + } + if (membersMainMethods.size() > 1) { + reportIssue(membersMainMethods.get(0).simpleName(), "At most one main method should be defined in a class."); + return; + } List superMainMethods = findMainMethodsInSuperclasses(ct).toList(); - boolean hasMembersMainMethod = !membersMainMethods.isEmpty(); - boolean hasMultipleMainMethods = membersMainMethods.size() + superMainMethods.size() > 1; - boolean hasLegitSingleMainOverride = - membersMainMethods.size() == 1 - && Optional.ofNullable(membersMainMethods.get(0).isOverriding()).orElse(false); - - if (hasMembersMainMethod && hasMultipleMainMethods && !hasLegitSingleMainOverride) { - var firstMainMethod = membersMainMethods.get(0); - var firstMainMethodToken = firstMainMethod.simpleName(); - var errorMessage = membersMainMethods.size() > 1 ? - "At most one main method should be defined in a class." : - "Main method should not be defined in a class if a main method is already defined in a superclass."; - reportIssue(firstMainMethodToken, errorMessage); + if (superMainMethods.isEmpty()) { + return; } + + // at this point : 1 main method in members and at least 1 main method in superclasses + var singleMainMethod = membersMainMethods.get(0); + boolean isOverriding = Optional.ofNullable(singleMainMethod.isOverriding()).orElse(false); + + // override case + var mainWithHigherPriorityInSuper = superMainMethods.stream().filter(superMainMethod -> + MethodTreeUtils.compareMainMethodPriority(singleMainMethod, superMainMethod) < 0 + ).findFirst(); + mainWithHigherPriorityInSuper.ifPresentOrElse( + // there is a main method in superclasses with higher priority than the one in members, so the one in members will not be the entry point + superMainMethod -> reportIssue( + singleMainMethod.simpleName(), + "This 'main' method will not be the entry point because another inherited 'main' from %s takes precedence." + .formatted(enclosingClassName(superMainMethod)) + ), + // there is no main method in superclasses with higher priority than the one in members, so the one in members will be the entry point, but if it is not overriding, it is a problem as it introduces multiple main methods + () -> { + if (!isOverriding) { + var superMainMethod = superMainMethods.get(0); + reportIssue( + singleMainMethod, + "Override main from %s to avoid introducing multiple main methods." + .formatted(enclosingClassName(superMainMethod)) + ); + } + } + ); + } + + private final String enclosingClassName(MethodTree mainMethod) { + var enclosingClass = mainMethod.symbol().enclosingClass(); + return enclosingClass == null ? "unknown" : enclosingClass.name(); } private Stream findMainMethodsInSuperclasses(ClassTree ct) { diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java index ad6d6f2aadf..954f1032ea5 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java @@ -61,6 +61,26 @@ private static boolean isMainMethodTraditional(MethodTree m) { return isPublic(m) && isStatic(m) && isNamed(m, "main") && returnsPrimitive(m, "void") && hasStringArrayParameter(m); } + /** + * Compares the priority of two main methods. In Java 25 and above, a main method with a String[] parameter has higher priority than a main method without parameters. In earlier versions of Java, only a main method with a String[] parameter is considered valid. + * @param compared: first main method to compare + * @param reference: second main method to compare + * @return 1 if m1 has higher priority, -1 if m2 has higher priority, 0 if both have the same priority + */ + public static int compareMainMethodPriority(MethodTree compared, MethodTree reference) { + boolean comparedHasStringArrayParameter = hasStringArrayParameter(compared); + boolean referenceHasStringArrayParameter = hasStringArrayParameter(reference); + if (comparedHasStringArrayParameter && !referenceHasStringArrayParameter) { + return 1; + } + else if (!comparedHasStringArrayParameter && referenceHasStringArrayParameter) { + return -1; + } + else { + return 0; + } + } + private static boolean hasStringArrayParameter(MethodTree m) { return m.parameters().size() == 1 && isParameterStringArray(m); } From fefdf71417b685a0db50acbf3ac9ee061a0d71c0 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 18 Feb 2026 11:42:10 +0100 Subject: [PATCH 14/19] Address Tomasz'review - use iteration over recursion --- .../checks/MultipleMainInstancesCheck.java | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index ad56edbaa8a..b9f60aca6ae 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -16,6 +16,7 @@ */ package org.sonar.java.checks; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Stream; @@ -48,7 +49,7 @@ public void visitNode(Tree tree) { reportIssue(membersMainMethods.get(0).simpleName(), "At most one main method should be defined in a class."); return; } - List superMainMethods = findMainMethodsInSuperclasses(ct).toList(); + List superMainMethods = findMainMethodsInSuperclasses(ct); if (superMainMethods.isEmpty()) { return; } @@ -64,11 +65,12 @@ public void visitNode(Tree tree) { mainWithHigherPriorityInSuper.ifPresentOrElse( // there is a main method in superclasses with higher priority than the one in members, so the one in members will not be the entry point superMainMethod -> reportIssue( - singleMainMethod.simpleName(), - "This 'main' method will not be the entry point because another inherited 'main' from %s takes precedence." - .formatted(enclosingClassName(superMainMethod)) - ), - // there is no main method in superclasses with higher priority than the one in members, so the one in members will be the entry point, but if it is not overriding, it is a problem as it introduces multiple main methods + singleMainMethod.simpleName(), + "This 'main' method will not be the entry point because another inherited 'main' from %s takes precedence." + .formatted(enclosingClassName(superMainMethod)) + ), + // there is no main method in superclasses with higher priority than the one in members, so the one in members will be the entry point, but if it is not overriding, it is + // a problem as it introduces multiple main methods () -> { if (!isOverriding) { var superMainMethod = superMainMethods.get(0); @@ -82,24 +84,21 @@ public void visitNode(Tree tree) { ); } - private final String enclosingClassName(MethodTree mainMethod) { + private String enclosingClassName(MethodTree mainMethod) { var enclosingClass = mainMethod.symbol().enclosingClass(); return enclosingClass == null ? "unknown" : enclosingClass.name(); } - private Stream findMainMethodsInSuperclasses(ClassTree ct) { + private List findMainMethodsInSuperclasses(ClassTree ct) { + List mains = new ArrayList<>(); var superClass = ct.superClass(); - if (superClass == null) { - return Stream.empty(); + while (superClass != null) { + var superClassTree = superClass.symbolType().symbol().declaration(); + findMainMethodsInMembers(superClassTree) + .forEach(mains::add); + superClass = superClassTree.superClass(); } - var superClassTree = superClass.symbolType().symbol().declaration(); - if (superClassTree == null) { - return Stream.empty(); - } - return Stream.concat( - findMainMethodsInMembers(superClassTree), - findMainMethodsInSuperclasses(superClassTree) - ); + return mains; } private Stream findMainMethodsInMembers(ClassTree ct) { From 498e9c44853ee5c81b60a448a7e2ee2c944a7523 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Thu, 19 Feb 2026 07:54:03 +0100 Subject: [PATCH 15/19] Try to fix : QG --- .../sonar/java/checks/MultipleMainInstancesCheck.java | 2 +- .../org/sonar/java/checks/helpers/MethodTreeUtils.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index b9f60aca6ae..7b68d7985fc 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -84,7 +84,7 @@ public void visitNode(Tree tree) { ); } - private String enclosingClassName(MethodTree mainMethod) { + private static String enclosingClassName(MethodTree mainMethod) { var enclosingClass = mainMethod.symbol().enclosingClass(); return enclosingClass == null ? "unknown" : enclosingClass.name(); } diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java index 954f1032ea5..50c9e682241 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java @@ -62,7 +62,9 @@ private static boolean isMainMethodTraditional(MethodTree m) { } /** - * Compares the priority of two main methods. In Java 25 and above, a main method with a String[] parameter has higher priority than a main method without parameters. In earlier versions of Java, only a main method with a String[] parameter is considered valid. + * Compares the priority of two main methods. + * In Java 25 and above, a main method with a String[] parameter has higher priority than a main method without parameters. + * In earlier versions of Java, only a main method with a String[] parameter is considered valid. * @param compared: first main method to compare * @param reference: second main method to compare * @return 1 if m1 has higher priority, -1 if m2 has higher priority, 0 if both have the same priority @@ -72,11 +74,9 @@ public static int compareMainMethodPriority(MethodTree compared, MethodTree refe boolean referenceHasStringArrayParameter = hasStringArrayParameter(reference); if (comparedHasStringArrayParameter && !referenceHasStringArrayParameter) { return 1; - } - else if (!comparedHasStringArrayParameter && referenceHasStringArrayParameter) { + } else if (!comparedHasStringArrayParameter && referenceHasStringArrayParameter) { return -1; - } - else { + } else { return 0; } } From 17f922a94b1c2058cdbd7b64ad33489924e9091b Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Mon, 23 Feb 2026 16:41:00 +0100 Subject: [PATCH 16/19] Address Tomasz' review - get rid of optional, remove outdated comment --- .../org/sonar/java/checks/MultipleMainInstancesCheck.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index 7b68d7985fc..89be51c0652 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.stream.Stream; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.MethodTreeUtils; @@ -29,8 +28,6 @@ import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Tree; -//https://github.com/SonarSource/sonar-java/pull/5455 - @Rule(key = "S8446") public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor { @Override @@ -56,7 +53,7 @@ public void visitNode(Tree tree) { // at this point : 1 main method in members and at least 1 main method in superclasses var singleMainMethod = membersMainMethods.get(0); - boolean isOverriding = Optional.ofNullable(singleMainMethod.isOverriding()).orElse(false); + boolean isOverriding = Boolean.TRUE.equals(singleMainMethod.isOverriding()); // override case var mainWithHigherPriorityInSuper = superMainMethods.stream().filter(superMainMethod -> From 9a0ae7345deacd5ec1d4fc76125965c5bb4d31d8 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 25 Feb 2026 16:33:19 +0100 Subject: [PATCH 17/19] Address Tomasz' review, adding main with args onto main without args doesnt raise issue --- .../checks/MultipleMainInstancesSample.java | 4 ++-- .../checks/MultipleMainInstancesCheck.java | 20 ++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index 293cb2e7250..81199d31507 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -67,7 +67,7 @@ void main() { } class Child extends Parent { - void main(String[] args) { // Noncompliant {{Override main from Parent to avoid introducing multiple main methods.}} + void main(String[] args) { // Compliant, args may be used for other purposes, and it will shadow the parent main method System.out.println("Static main in child class detected; shadowing instance main."); } } @@ -156,7 +156,7 @@ class Parent extends GrandParent { } class NonCompliantChild extends Parent { - void main(String[] args) { // Noncompliant {{Override main from GrandParent to avoid introducing multiple main methods.}} + void main(String[] args) { // Compliant, args may be used for other purposes, and it will shadow the grandparent main method System.out.println("Child main method detected; shadowing grandparent main."); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index 89be51c0652..96df6961284 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -59,25 +59,17 @@ public void visitNode(Tree tree) { var mainWithHigherPriorityInSuper = superMainMethods.stream().filter(superMainMethod -> MethodTreeUtils.compareMainMethodPriority(singleMainMethod, superMainMethod) < 0 ).findFirst(); - mainWithHigherPriorityInSuper.ifPresentOrElse( + + // if mainWithHigherPriorityInSuper.isEmpty() it means that + // Parent main method has no args and class main method has args that may + // be used for other purposes, and it will shadow the parent main method - do not report an issue in this case + mainWithHigherPriorityInSuper.ifPresent( // there is a main method in superclasses with higher priority than the one in members, so the one in members will not be the entry point superMainMethod -> reportIssue( singleMainMethod.simpleName(), "This 'main' method will not be the entry point because another inherited 'main' from %s takes precedence." .formatted(enclosingClassName(superMainMethod)) - ), - // there is no main method in superclasses with higher priority than the one in members, so the one in members will be the entry point, but if it is not overriding, it is - // a problem as it introduces multiple main methods - () -> { - if (!isOverriding) { - var superMainMethod = superMainMethods.get(0); - reportIssue( - singleMainMethod, - "Override main from %s to avoid introducing multiple main methods." - .formatted(enclosingClassName(superMainMethod)) - ); - } - } + ) ); } From 3f53b1ac43648fe62c22a6f6843ac13ea6850af3 Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 25 Feb 2026 16:49:08 +0100 Subject: [PATCH 18/19] Try to fix : QG --- .../java/org/sonar/java/checks/MultipleMainInstancesCheck.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java index 96df6961284..65021449c95 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java @@ -53,7 +53,6 @@ public void visitNode(Tree tree) { // at this point : 1 main method in members and at least 1 main method in superclasses var singleMainMethod = membersMainMethods.get(0); - boolean isOverriding = Boolean.TRUE.equals(singleMainMethod.isOverriding()); // override case var mainWithHigherPriorityInSuper = superMainMethods.stream().filter(superMainMethod -> From f66aebd96fceeca35b90dcc8157879edb4567fcc Mon Sep 17 00:00:00 2001 From: "romain.birling" Date: Wed, 25 Feb 2026 16:56:44 +0100 Subject: [PATCH 19/19] Address Tomasz' review 3 --- .../src/main/java/checks/MultipleMainInstancesSample.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java index 81199d31507..7a8f9f06782 100644 --- a/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java @@ -59,7 +59,7 @@ void main() { } } - public static class NonCompliantChildPrecedence { + public static class CompliantChildPrecedence { class Parent { void main() { System.out.println("Parent instance main method."); @@ -155,13 +155,13 @@ void main() { class Parent extends GrandParent { } - class NonCompliantChild extends Parent { + class CompliantChildPriority extends Parent { void main(String[] args) { // Compliant, args may be used for other purposes, and it will shadow the grandparent main method System.out.println("Child main method detected; shadowing grandparent main."); } } - class CompliantChild extends Parent { + class CompliantChildOverride extends Parent { @Override void main() { // Compliant: This is an instance method that overrides the parent main System.out.println("Child instance main method overriding grandparent main.");