Skip to content

Commit e65c0da

Browse files
Implement S3051 checking the signature of the main method.
1 parent 0410788 commit e65c0da

10 files changed

Lines changed: 328 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void main() {
2+
IO.println("compact source");
3+
}
4+
5+
int main(int i) { // Noncompliant
6+
return i * 2;
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package checks.mainSignature;
2+
3+
interface NonInstantiable {
4+
int multiply(int a, int b);
5+
6+
default void main() { // Compliant
7+
System.out.println("default");
8+
}
9+
10+
int main(int a); // Noncompliant
11+
12+
static void main(String[] arg) { // Compliant
13+
System.out.println("yep, inside an interface");
14+
}
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package checks.mainSignature;
2+
3+
public class PrivateMain {
4+
private static void main(String[] args) { // Noncompliant
5+
}
6+
7+
private void main() { // Noncompliant
8+
}
9+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package checks.mainSignature;
2+
3+
public class Sample {
4+
public static class Traditional {
5+
public static void main(String[] args) { // Compliant
6+
System.out.println("traditional");
7+
}
8+
}
9+
10+
public static class NoArg {
11+
public static void main() { // Compliant
12+
System.out.println("no arg");
13+
}
14+
}
15+
16+
public static class Instance {
17+
void main(String[] args) { // Compliant
18+
System.out.println("instance");
19+
}
20+
}
21+
22+
public static class Wrong {
23+
public static void main(String[] args) {
24+
System.out.println(new Wrong().main(42));
25+
}
26+
27+
int main(int x) { // Noncompliant
28+
return x * x;
29+
}
30+
31+
String main(String x, String y) { // Noncompliant
32+
return "";
33+
}
34+
}
35+
36+
public static class WrongReturn {
37+
public static int main(String[] args) { // Noncompliant
38+
return 1;
39+
}
40+
}
41+
42+
@interface MyAnnotation {
43+
String main() default ""; // Noncompliant
44+
}
45+
46+
enum MyEnum {
47+
A, B;
48+
49+
void main(int x) { // Noncompliant
50+
}
51+
}
52+
53+
record MyRecord(int value) {
54+
void main(int x) { // Noncompliant
55+
}
56+
57+
static void main(String[] args) {
58+
}
59+
}
60+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package checks.mainSignature;
2+
3+
public class Varargs {
4+
void main(String... args) {
5+
}
6+
7+
void main(int i, String ... args) { // Noncompliant
8+
9+
}
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package checks.mainSignature;
2+
3+
// This one is really weird. Doesn't really matter what we report,
4+
// as long as nothing crashes.
5+
6+
public class main {
7+
public main() {}
8+
9+
public main(String[] args) {}
10+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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.Collections;
20+
import java.util.List;
21+
import org.sonar.check.Rule;
22+
import org.sonar.java.model.ModifiersUtils;
23+
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
24+
import org.sonar.plugins.java.api.tree.*;
25+
26+
/**
27+
* Checks that the "main" method has the correct signature for a program entry point.
28+
*
29+
* <p>Note, that even with a correct signature, the "main" method may not be valid entry point.
30+
* For example, it may be declared in an abstract class or an interface.
31+
*/
32+
@Rule(key = "S3051")
33+
public class MainMethodSignatureCheck extends IssuableSubscriptionVisitor {
34+
35+
private static final String MESSAGE = "\"main\" method should only be used for the program entry point and should have appropriate signature.";
36+
37+
@Override
38+
public List<Tree.Kind> nodesToVisit() {
39+
return List.of(Tree.Kind.METHOD);
40+
}
41+
42+
@Override
43+
public void visitNode(Tree tree) {
44+
MethodTree methodTree = (MethodTree) tree;
45+
if (!"main".equals(methodTree.simpleName().name())) {
46+
return;
47+
}
48+
if (!isValidMainSignature(methodTree)) {
49+
reportIssue(methodTree.simpleName(), MESSAGE);
50+
}
51+
}
52+
53+
/**
54+
* Checks if the signature is a valid "main" method signature in Java 25.
55+
*
56+
* @return true, false, or null if it cannot be determined
57+
*/
58+
private static Boolean isValidMainSignature(MethodTree methodTree) {
59+
// main cannot be private
60+
if (ModifiersUtils.hasModifier(methodTree.modifiers(), Modifier.PRIVATE)) {
61+
return false;
62+
}
63+
64+
// return type must be void
65+
TypeTree returnType = methodTree.returnType();
66+
// null check just to avoid NPE
67+
if (returnType == null || !returnType.symbolType().isVoid()) {
68+
return false;
69+
}
70+
71+
List<VariableTree> parameters = methodTree.parameters();
72+
// no arguments or single String[] argument
73+
return parameters.isEmpty() ||
74+
(parameters.size() == 1
75+
&& parameters.get(0).type() instanceof ArrayTypeTree att
76+
&& "String".equals(att.type().symbolType().name()));
77+
}
78+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 MainMethodSignatureCheckTest {
25+
26+
private static final MainMethodSignatureCheck CHECK = new MainMethodSignatureCheck();
27+
28+
@Test
29+
void test() {
30+
CheckVerifier.newVerifier()
31+
.onFile(mainCodeSourcesPath("checks/mainSignature/Sample.java"))
32+
.withCheck(CHECK)
33+
.verifyIssues();
34+
}
35+
36+
@Test
37+
void nonInstantiable() {
38+
CheckVerifier.newVerifier()
39+
.onFile(mainCodeSourcesPath("checks/mainSignature/NonInstantiable.java"))
40+
.withCheck(CHECK)
41+
.verifyIssues();
42+
}
43+
44+
@Test
45+
void privateMain() {
46+
CheckVerifier.newVerifier()
47+
.onFile(mainCodeSourcesPath("checks/mainSignature/PrivateMain.java"))
48+
.withCheck(CHECK)
49+
.verifyIssues();
50+
}
51+
52+
@Test
53+
void compactSource() {
54+
CheckVerifier.newVerifier()
55+
.onFile(mainCodeSourcesPath("checks/mainSignature/CompactSource.java"))
56+
.withCheck(CHECK)
57+
.verifyIssues();
58+
}
59+
60+
@Test
61+
void varargs() {
62+
CheckVerifier.newVerifier()
63+
.onFile(mainCodeSourcesPath("checks/mainSignature/Varargs.java"))
64+
.withCheck(CHECK)
65+
.verifyIssues();
66+
}
67+
68+
@Test
69+
void constructor() {
70+
// The check will not run, because it does not visit constructor nodes.
71+
CheckVerifier.newVerifier()
72+
.onFile(mainCodeSourcesPath("checks/mainSignature/main.java"))
73+
.withCheck(CHECK)
74+
.verifyNoIssues();
75+
}
76+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>A method named <code>main</code> with an invalid signature for a program entry point is confusing. It suggests the method is intended to be an entry point, but the JVM will not recognize it as such.</p>
3+
<p>Valid signatures for a program entry point are:</p>
4+
<ul>
5+
<li><code>public static void main(String[] args)</code> - traditional signature</li>
6+
<li><code>static void main(String[] args)</code> - static main without public modifier (Java 21+)</li>
7+
<li><code>void main(String[] args)</code> - instance main method (Java 21+)</li>
8+
<li><code>void main()</code> - main method without parameters (Java 21+)</li>
9+
<li><code>static void main()</code> - static main without parameters (Java 21+)</li>
10+
</ul>
11+
<h3>Noncompliant code example</h3>
12+
<pre>
13+
class App {
14+
// Wrong return type
15+
int main(String[] args) { // Noncompliant
16+
return 0;
17+
}
18+
19+
// Wrong parameter type
20+
void main(int[] args) { // Noncompliant
21+
}
22+
23+
// Too many parameters
24+
void main(String[] args, int extra) { // Noncompliant
25+
}
26+
}
27+
</pre>
28+
<h3>Compliant solution</h3>
29+
<pre>
30+
class App {
31+
public static void main(String[] args) { // Compliant
32+
}
33+
34+
// Or using Java 21+ flexible main methods:
35+
void main() { // Compliant
36+
}
37+
}
38+
</pre>
39+
<h3>Exceptions</h3>
40+
<p>Methods named <code>main</code> in interfaces, abstract classes, and annotations are ignored, as these cannot be used as program entry points.</p>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "\"main\" method should have the correct signature",
3+
"type": "CODE_SMELL",
4+
"code": {
5+
"impacts": {
6+
"MAINTAINABILITY": "LOW"
7+
},
8+
"attribute": "CLEAR"
9+
},
10+
"status": "ready",
11+
"remediation": {
12+
"func": "Constant/Issue",
13+
"constantCost": "5min"
14+
},
15+
"tags": [
16+
"confusing"
17+
],
18+
"defaultSeverity": "Minor",
19+
"ruleSpecification": "RSPEC-3051",
20+
"sqKey": "S3051",
21+
"scope": "Main",
22+
"quickfix": "unknown"
23+
}

0 commit comments

Comments
 (0)