Skip to content

Commit 872967d

Browse files
authored
SONARJAVA-6112 Only one "main" method should be present (#5455)
1 parent c09d19b commit 872967d

9 files changed

Lines changed: 432 additions & 0 deletions

File tree

its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2879,6 +2879,12 @@
28792879
"falseNegatives": 0,
28802880
"falsePositives": 0
28812881
},
2882+
{
2883+
"ruleKey": "8446",
2884+
"hasTruePositives": true,
2885+
"falseNegatives": 0,
2886+
"falsePositives": 0
2887+
},
28822888
{
28832889
"ruleKey": "8447",
28842890
"hasTruePositives": false,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S8446",
3+
"hasTruePositives": true,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
public class MultipleMainInstancesSample {
2+
public static class NonCompliant {
3+
public static void main(String[] args) { // Noncompliant {{At most one main method should be defined in a class.}}
4+
// ^^^^
5+
System.out.println("Static main detected; shadowing instance main.");
6+
}
7+
8+
void main() {
9+
System.out.println("Unreachable entry point due to static precedence.");
10+
}
11+
}
12+
13+
public static class Compliant {
14+
public static class LegacyApplication {
15+
// Compliant: Standard static entry point
16+
public static void main(String[] args) { // Compliant
17+
System.out.println("Running standard static entry point.");
18+
}
19+
}
20+
21+
static class Application {
22+
// Compliant: Instance main method in a separate class
23+
void main() { // Compliant
24+
System.out.println("Running modern instance main entry point.");
25+
}
26+
}
27+
}
28+
29+
public static enum NonCompliantEnum {
30+
INSTANCE;
31+
32+
public static void main(String[] args) { // Noncompliant
33+
System.out.println("Static main in enum detected; shadowing instance main.");
34+
}
35+
36+
void main() {
37+
System.out.println("Unreachable entry point in enum due to static precedence.");
38+
}
39+
}
40+
41+
public static interface NonCompliantInterface {
42+
static void main(String[] args) { // Noncompliant
43+
System.out.println("Static main in interface detected; shadowing instance main.");
44+
}
45+
46+
default void main() {
47+
48+
System.out.println("Unreachable entry point in interface due to static precedence.");
49+
}
50+
}
51+
52+
public static record NonCompliantRecord(String data) {
53+
public static void main(String[] args) { // Noncompliant
54+
System.out.println("Static main in record detected; shadowing instance main.");
55+
}
56+
57+
void main() {
58+
System.out.println("Unreachable entry point in record due to static precedence.");
59+
}
60+
}
61+
62+
public static class CompliantChildPrecedence {
63+
class Parent {
64+
void main() {
65+
System.out.println("Parent instance main method.");
66+
}
67+
}
68+
69+
class Child extends Parent {
70+
void main(String[] args) { // Compliant, args may be used for other purposes, and it will shadow the parent main method
71+
System.out.println("Static main in child class detected; shadowing instance main.");
72+
}
73+
}
74+
}
75+
76+
public static class NonCompliantParentPrecedence {
77+
class Parent {
78+
void main(String[] args) {
79+
System.out.println("Parent instance main method.");
80+
}
81+
}
82+
83+
class Child extends Parent {
84+
void main() { // Noncompliant {{This 'main' method will not be the entry point because another inherited 'main' from Parent takes precedence.}}
85+
System.out.println("Parent main method takes precedence over child main; this method will not be the entry point.");
86+
}
87+
}
88+
}
89+
90+
public static class CompliantWithOverloads {
91+
class Parent {
92+
void main() {
93+
System.out.println("Parent instance main method.");
94+
}
95+
}
96+
97+
class Child extends Parent {
98+
@Override
99+
void main() { // Compliant: This is an instance method that overrides the parent main
100+
System.out.println("Child instance main method overriding parent main.");
101+
}
102+
}
103+
}
104+
105+
public static class TwoMainsInParentOneOverriddenInChild {
106+
class Parent {
107+
void main() { // Noncompliant {{At most one main method should be defined in a class.}}
108+
System.out.println("Parent void main method.");
109+
}
110+
111+
void main(String[] args) {
112+
System.out.println("Parent String args main method.");
113+
}
114+
}
115+
116+
class Child extends Parent {
117+
@Override
118+
void main() { // Noncompliant {{This 'main' method will not be the entry point because another inherited 'main' from Parent takes precedence.}}
119+
System.out.println("Child instance main method overriding parent main.");
120+
}
121+
}
122+
}
123+
124+
public static class TwoMainsInParentTwoOverriddenInChild {
125+
class Parent {
126+
void main() { // Noncompliant {{At most one main method should be defined in a class.}}
127+
System.out.println("Parent void main method.");
128+
}
129+
130+
void main(String[] args) {
131+
System.out.println("Parent String args main method.");
132+
}
133+
}
134+
135+
class Child extends Parent {
136+
@Override
137+
void main() { // Noncompliant {{At most one main method should be defined in a class.}}
138+
System.out.println("Child instance main method overriding parent main.");
139+
}
140+
141+
@Override
142+
void main(String[] args) {
143+
System.out.println("Child String args main method overriding parent main.");
144+
}
145+
}
146+
}
147+
148+
public static class ChainedOverrides {
149+
class GrandParent {
150+
void main() {
151+
System.out.println("Parent void main method.");
152+
}
153+
}
154+
155+
class Parent extends GrandParent {
156+
}
157+
158+
class CompliantChildPriority extends Parent {
159+
void main(String[] args) { // Compliant, args may be used for other purposes, and it will shadow the grandparent main method
160+
System.out.println("Child main method detected; shadowing grandparent main.");
161+
}
162+
}
163+
164+
class CompliantChildOverride extends Parent {
165+
@Override
166+
void main() { // Compliant: This is an instance method that overrides the parent main
167+
System.out.println("Child instance main method overriding grandparent main.");
168+
}
169+
}
170+
}
171+
}
172+
173+
// test implicit class
174+
void main() { // Noncompliant
175+
System.out.println("Hello World!");
176+
}
177+
178+
static public void main(String[] args) {
179+
System.out.println("Hello World!");
180+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.stream.Stream;
22+
import org.sonar.check.Rule;
23+
import org.sonar.java.checks.helpers.MethodTreeUtils;
24+
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
25+
import org.sonar.plugins.java.api.JavaVersion;
26+
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
27+
import org.sonar.plugins.java.api.tree.ClassTree;
28+
import org.sonar.plugins.java.api.tree.MethodTree;
29+
import org.sonar.plugins.java.api.tree.Tree;
30+
31+
@Rule(key = "S8446")
32+
public class MultipleMainInstancesCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {
33+
@Override
34+
public List<Tree.Kind> nodesToVisit() {
35+
return List.of(Tree.Kind.CLASS, Tree.Kind.INTERFACE, Tree.Kind.ENUM, Tree.Kind.RECORD, Tree.Kind.IMPLICIT_CLASS);
36+
}
37+
38+
@Override
39+
public void visitNode(Tree tree) {
40+
ClassTree ct = (ClassTree) tree;
41+
List<MethodTree> membersMainMethods = findMainMethodsInMembers(ct).toList();
42+
if (membersMainMethods.isEmpty()) {
43+
return;
44+
}
45+
if (membersMainMethods.size() > 1) {
46+
reportIssue(membersMainMethods.get(0).simpleName(), "At most one main method should be defined in a class.");
47+
return;
48+
}
49+
List<MethodTree> superMainMethods = findMainMethodsInSuperclasses(ct);
50+
if (superMainMethods.isEmpty()) {
51+
return;
52+
}
53+
54+
// at this point : 1 main method in members and at least 1 main method in superclasses
55+
var singleMainMethod = membersMainMethods.get(0);
56+
57+
// override case
58+
var mainWithHigherPriorityInSuper = superMainMethods.stream().filter(superMainMethod ->
59+
MethodTreeUtils.compareMainMethodPriority(singleMainMethod, superMainMethod) < 0
60+
).findFirst();
61+
62+
// if mainWithHigherPriorityInSuper.isEmpty() it means that
63+
// Parent main method has no args and class main method has args that may
64+
// be used for other purposes, and it will shadow the parent main method - do not report an issue in this case
65+
mainWithHigherPriorityInSuper.ifPresent(
66+
// 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
67+
superMainMethod -> reportIssue(
68+
singleMainMethod.simpleName(),
69+
"This 'main' method will not be the entry point because another inherited 'main' from %s takes precedence."
70+
.formatted(enclosingClassName(superMainMethod))
71+
)
72+
);
73+
}
74+
75+
private static String enclosingClassName(MethodTree mainMethod) {
76+
var enclosingClass = mainMethod.symbol().enclosingClass();
77+
return enclosingClass == null ? "unknown" : enclosingClass.name();
78+
}
79+
80+
private List<MethodTree> findMainMethodsInSuperclasses(ClassTree ct) {
81+
List<MethodTree> mains = new ArrayList<>();
82+
var superClass = ct.superClass();
83+
while (superClass != null) {
84+
var superClassTree = superClass.symbolType().symbol().declaration();
85+
findMainMethodsInMembers(superClassTree)
86+
.forEach(mains::add);
87+
superClass = superClassTree.superClass();
88+
}
89+
return mains;
90+
}
91+
92+
private Stream<MethodTree> findMainMethodsInMembers(ClassTree ct) {
93+
return ct.members().stream()
94+
.filter(MethodTree.class::isInstance)
95+
.map(MethodTree.class::cast)
96+
.filter(this::isMainMethod);
97+
}
98+
99+
private boolean isMainMethod(MethodTree tree) {
100+
return MethodTreeUtils.isMainMethod(tree, context.getJavaVersion());
101+
}
102+
103+
@Override
104+
public boolean isCompatibleWithJavaVersion(JavaVersion version) {
105+
return version.isJava25Compatible();
106+
}
107+
}

java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ private static boolean isMainMethodTraditional(MethodTree m) {
6161
return isPublic(m) && isStatic(m) && isNamed(m, "main") && returnsPrimitive(m, "void") && hasStringArrayParameter(m);
6262
}
6363

64+
/**
65+
* Compares the priority of two main methods.
66+
* In Java 25 and above, a main method with a String[] parameter has higher priority than a main method without parameters.
67+
* In earlier versions of Java, only a main method with a String[] parameter is considered valid.
68+
* @param compared: first main method to compare
69+
* @param reference: second main method to compare
70+
* @return 1 if m1 has higher priority, -1 if m2 has higher priority, 0 if both have the same priority
71+
*/
72+
public static int compareMainMethodPriority(MethodTree compared, MethodTree reference) {
73+
boolean comparedHasStringArrayParameter = hasStringArrayParameter(compared);
74+
boolean referenceHasStringArrayParameter = hasStringArrayParameter(reference);
75+
if (comparedHasStringArrayParameter && !referenceHasStringArrayParameter) {
76+
return 1;
77+
} else if (!comparedHasStringArrayParameter && referenceHasStringArrayParameter) {
78+
return -1;
79+
} else {
80+
return 0;
81+
}
82+
}
83+
6484
private static boolean hasStringArrayParameter(MethodTree m) {
6585
return m.parameters().size() == 1 && isParameterStringArray(m);
6686
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.java.checks.verifier.CheckVerifier;
21+
22+
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
23+
24+
class MultipleMainInstancesCheckTest {
25+
26+
@Test
27+
void test() {
28+
CheckVerifier.newVerifier()
29+
.onFile(mainCodeSourcesPath("checks/MultipleMainInstancesSample.java"))
30+
.withCheck(new MultipleMainInstancesCheck())
31+
.withJavaVersion(25)
32+
.verifyIssues();
33+
}
34+
35+
@Test
36+
void test_java_24() {
37+
CheckVerifier.newVerifier()
38+
.onFile(mainCodeSourcesPath("checks/MultipleMainInstancesSample.java"))
39+
.withCheck(new MultipleMainInstancesCheck())
40+
.withJavaVersion(24)
41+
.verifyNoIssues();
42+
}
43+
}

0 commit comments

Comments
 (0)