-
Notifications
You must be signed in to change notification settings - Fork 725
SONARJAVA-6112 Only one "main" method should be present #5455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rombirli
merged 19 commits into
master
from
rombirli/SONARJAVA-6112-only-one-main-method
Feb 25, 2026
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
79b6f82
Initial commit - documentation, reproducer
rombirli 575f219
Rule implementation
rombirli a17436f
Try to fix : autoscan | 1
rombirli fb430dd
Try to fix : autoscan | 2
rombirli 6c65f75
Better issue message
rombirli d74df81
Support Interfaces, Enums and Records
rombirli 4a8d472
Support implicit classes
rombirli 16f2dc2
Update rule metadata : tag java25
rombirli eab0c45
Try to fix : autoscan | 3
rombirli caefca6
Try to fix : autoscan | 4
rombirli fe158d0
Address Tomasz' comments
rombirli ca0ca64
Extend rule implementation - detect multiple main methods due to over…
rombirli c783f28
Address Tomasz'review - improve error messages logic, verify preceden…
rombirli fefdf71
Address Tomasz'review - use iteration over recursion
rombirli 498e9c4
Try to fix : QG
rombirli 17f922a
Address Tomasz' review - get rid of optional, remove outdated comment
rombirli 9a0ae73
Address Tomasz' review, adding main with args onto main without args …
rombirli 3f53b1a
Try to fix : QG
rombirli f66aebd
Address Tomasz' review 3
rombirli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
its/autoscan/src/test/resources/autoscan/diffs/diff_S8446.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "ruleKey": "S8446", | ||
| "hasTruePositives": true, | ||
| "falseNegatives": 0, | ||
| "falsePositives": 0 | ||
| } |
180 changes: 180 additions & 0 deletions
180
java-checks-test-sources/default/src/main/java/checks/MultipleMainInstancesSample.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| public class MultipleMainInstancesSample { | ||
| public static class 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."); | ||
| } | ||
|
|
||
| void main() { | ||
| 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."); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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() { | ||
| 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() { | ||
|
|
||
| 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() { | ||
| System.out.println("Unreachable entry point in record due to static precedence."); | ||
| } | ||
| } | ||
|
|
||
| public static class CompliantChildPrecedence { | ||
| class Parent { | ||
| void main() { | ||
| System.out.println("Parent instance main method."); | ||
| } | ||
| } | ||
|
|
||
| class Child extends Parent { | ||
| 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."); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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() { | ||
| 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() { // 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."); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 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 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 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."); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // test implicit class | ||
| void main() { // Noncompliant | ||
| System.out.println("Hello World!"); | ||
| } | ||
|
|
||
| static public void main(String[] args) { | ||
| System.out.println("Hello World!"); | ||
| } |
107 changes: 107 additions & 0 deletions
107
java-checks/src/main/java/org/sonar/java/checks/MultipleMainInstancesCheck.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| /* | ||
| * 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.ArrayList; | ||
| 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 implements JavaVersionAwareVisitor { | ||
| @Override | ||
| public List<Tree.Kind> nodesToVisit() { | ||
| return List.of(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.RECORD, Tree.Kind.IMPLICIT_CLASS); | ||
| } | ||
|
|
||
| @Override | ||
| public void visitNode(Tree tree) { | ||
| ClassTree ct = (ClassTree) tree; | ||
| List<MethodTree> 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<MethodTree> superMainMethods = findMainMethodsInSuperclasses(ct); | ||
| 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); | ||
|
|
||
| // override case | ||
| var mainWithHigherPriorityInSuper = superMainMethods.stream().filter(superMainMethod -> | ||
| MethodTreeUtils.compareMainMethodPriority(singleMainMethod, superMainMethod) < 0 | ||
| ).findFirst(); | ||
|
|
||
| // 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)) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| private static String enclosingClassName(MethodTree mainMethod) { | ||
| var enclosingClass = mainMethod.symbol().enclosingClass(); | ||
| return enclosingClass == null ? "unknown" : enclosingClass.name(); | ||
| } | ||
|
|
||
| private List<MethodTree> findMainMethodsInSuperclasses(ClassTree ct) { | ||
| List<MethodTree> mains = new ArrayList<>(); | ||
| var superClass = ct.superClass(); | ||
| while (superClass != null) { | ||
| var superClassTree = superClass.symbolType().symbol().declaration(); | ||
| findMainMethodsInMembers(superClassTree) | ||
| .forEach(mains::add); | ||
| superClass = superClassTree.superClass(); | ||
| } | ||
| return mains; | ||
| } | ||
|
|
||
| private Stream<MethodTree> findMainMethodsInMembers(ClassTree ct) { | ||
| return ct.members().stream() | ||
| .filter(MethodTree.class::isInstance) | ||
| .map(MethodTree.class::cast) | ||
| .filter(this::isMainMethod); | ||
| } | ||
|
|
||
| private boolean isMainMethod(MethodTree tree) { | ||
| return MethodTreeUtils.isMainMethod(tree, context.getJavaVersion()); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isCompatibleWithJavaVersion(JavaVersion version) { | ||
| return version.isJava25Compatible(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
java-checks/src/test/java/org/sonar/java/checks/MultipleMainInstancesCheckTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /* | ||
| * 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()) | ||
| .withJavaVersion(25) | ||
| .verifyIssues(); | ||
| } | ||
|
|
||
| @Test | ||
| void test_java_24() { | ||
| CheckVerifier.newVerifier() | ||
| .onFile(mainCodeSourcesPath("checks/MultipleMainInstancesSample.java")) | ||
| .withCheck(new MultipleMainInstancesCheck()) | ||
| .withJavaVersion(24) | ||
| .verifyNoIssues(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.