From fbeb1d97888d83cff1712202c28fb4776206facd Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Thu, 5 Feb 2026 14:36:22 +0100 Subject: [PATCH 1/2] Implement S3051 checking the signature of the main method. --- .../checks/mainSignature/CompactSource.java | 7 ++ .../java/checks/mainSignature/MainJava21.java | 9 ++ .../checks/mainSignature/NonInstantiable.java | 15 ++++ .../checks/mainSignature/PrivateMain.java | 9 ++ .../java/checks/mainSignature/Sample.java | 66 ++++++++++++++ .../java/checks/mainSignature/Varargs.java | 10 +++ .../main/java/checks/mainSignature/main.java | 10 +++ .../java/checks/MainMethodSignatureCheck.java | 51 +++++++++++ .../checks/MainMethodSignatureCheckTest.java | 89 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S3051.html | 35 ++++++++ .../org/sonar/l10n/java/rules/java/S3051.json | 24 +++++ .../java/rules/java/Sonar_way_profile.json | 1 + 12 files changed, 326 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/mainSignature/CompactSource.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/mainSignature/MainJava21.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/mainSignature/NonInstantiable.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/mainSignature/PrivateMain.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/mainSignature/Sample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/mainSignature/Varargs.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/mainSignature/main.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/MainMethodSignatureCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/MainMethodSignatureCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.json diff --git a/java-checks-test-sources/default/src/main/java/checks/mainSignature/CompactSource.java b/java-checks-test-sources/default/src/main/java/checks/mainSignature/CompactSource.java new file mode 100644 index 0000000000..dc9ff87b3e --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/mainSignature/CompactSource.java @@ -0,0 +1,7 @@ +void main() { + IO.println("compact source"); +} + +int main(int i) { // Noncompliant + return i * 2; +} diff --git a/java-checks-test-sources/default/src/main/java/checks/mainSignature/MainJava21.java b/java-checks-test-sources/default/src/main/java/checks/mainSignature/MainJava21.java new file mode 100644 index 0000000000..7d7761ec31 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/mainSignature/MainJava21.java @@ -0,0 +1,9 @@ +package checks.mainSignature; + +public class MainJava21 { + public static void main(String[] args) { // Compliant + } + + void main() { // Noncompliant + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/mainSignature/NonInstantiable.java b/java-checks-test-sources/default/src/main/java/checks/mainSignature/NonInstantiable.java new file mode 100644 index 0000000000..dfadec32a6 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/mainSignature/NonInstantiable.java @@ -0,0 +1,15 @@ +package checks.mainSignature; + +interface NonInstantiable { + int multiply(int a, int b); + + default void main() { // Compliant + System.out.println("default"); + } + + int main(int a); // Noncompliant + + static void main(String[] arg) { // Compliant + System.out.println("yep, inside an interface"); + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/mainSignature/PrivateMain.java b/java-checks-test-sources/default/src/main/java/checks/mainSignature/PrivateMain.java new file mode 100644 index 0000000000..8947298745 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/mainSignature/PrivateMain.java @@ -0,0 +1,9 @@ +package checks.mainSignature; + +public class PrivateMain { + private static void main(String[] args) { // Noncompliant + } + + private void main() { // Noncompliant + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/mainSignature/Sample.java b/java-checks-test-sources/default/src/main/java/checks/mainSignature/Sample.java new file mode 100644 index 0000000000..a284d522e9 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/mainSignature/Sample.java @@ -0,0 +1,66 @@ +package checks.mainSignature; + +public class Sample { + public static class Traditional { + public static void main(String[] args) { // Compliant + System.out.println("traditional"); + } + } + + public static class NoArg { + public static void main() { // Compliant + System.out.println("no arg"); + } + } + + public static class Instance { + void main(String[] args) { // Compliant + System.out.println("instance"); + } + } + + public static class Wrong { + public static void main(String[] args) { + System.out.println(new Wrong().main(42)); + } + + int main(int x) { // Noncompliant +// ^^^^ + return x * x; + } + + String main(String x, String y) { // Noncompliant +// ^^^^ + return ""; + } + } + + public static class WrongReturn { + public static int main(String[] args) { // Noncompliant +// ^^^^ + return 1; + } + } + + @interface MyAnnotation { + String main() default ""; // Noncompliant +// ^^^^ + } + + enum MyEnum { + A, B; + + void main(int x) { // Noncompliant +// ^^^^ + } + } + + record MyRecord(int value) { + void main(int x) { // Noncompliant +// ^^^^ + } + + static void main(String[] args) { + } + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/mainSignature/Varargs.java b/java-checks-test-sources/default/src/main/java/checks/mainSignature/Varargs.java new file mode 100644 index 0000000000..2a8e360935 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/mainSignature/Varargs.java @@ -0,0 +1,10 @@ +package checks.mainSignature; + +public class Varargs { + void main(String... args) { + } + + void main(int i, String ... args) { // Noncompliant + + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/mainSignature/main.java b/java-checks-test-sources/default/src/main/java/checks/mainSignature/main.java new file mode 100644 index 0000000000..abbff780fb --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/mainSignature/main.java @@ -0,0 +1,10 @@ +package checks.mainSignature; + +// This one is really weird. Doesn't really matter what we report, +// as long as nothing crashes. + +public class main { + public main() {} + + public main(String[] args) {} +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/MainMethodSignatureCheck.java b/java-checks/src/main/java/org/sonar/java/checks/MainMethodSignatureCheck.java new file mode 100644 index 0000000000..458f23b918 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/MainMethodSignatureCheck.java @@ -0,0 +1,51 @@ +/* + * 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.java.checks.helpers.MethodTreeUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.Tree; + + +/** + * Checks that the "main" method has the correct signature for a program entry point. + * + *

Note, that even with a correct signature, the "main" method may not be valid entry point. + * For example, it may be declared in an abstract class or an interface. + */ +@Rule(key = "S3051") +public class MainMethodSignatureCheck extends IssuableSubscriptionVisitor { + + private static final String MESSAGE = "\"main\" method should only be used for the program entry point and should have appropriate signature."; + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.METHOD); + } + + @Override + public void visitNode(Tree tree) { + MethodTree methodTree = (MethodTree) tree; + if ("main".equals(methodTree.simpleName().name()) + && !MethodTreeUtils.isMainMethod(methodTree, context.getJavaVersion())) { + reportIssue(methodTree.simpleName(), MESSAGE); + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/MainMethodSignatureCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/MainMethodSignatureCheckTest.java new file mode 100644 index 0000000000..51b0fd7791 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/MainMethodSignatureCheckTest.java @@ -0,0 +1,89 @@ +/* + * 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 MainMethodSignatureCheckTest { + + private static final MainMethodSignatureCheck CHECK = new MainMethodSignatureCheck(); + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/mainSignature/Sample.java")) + .withCheck(CHECK) + .withJavaVersion(25) + .verifyIssues(); + } + + @Test + void test_java21() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/mainSignature/MainJava21.java")) + .withCheck(CHECK) + .withJavaVersion(21) + .verifyIssues(); + } + + @Test + void nonInstantiable() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/mainSignature/NonInstantiable.java")) + .withCheck(CHECK) + .withJavaVersion(25) + .verifyIssues(); + } + + @Test + void privateMain() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/mainSignature/PrivateMain.java")) + .withCheck(CHECK) + .verifyIssues(); + } + + @Test + void compactSource() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/mainSignature/CompactSource.java")) + .withCheck(CHECK) + .withJavaVersion(25) + .verifyIssues(); + } + + @Test + void varargs() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/mainSignature/Varargs.java")) + .withCheck(CHECK) + .withJavaVersion(25) + .verifyIssues(); + } + + @Test + void constructor() { + // The check will not run, because it does not visit constructor nodes. + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/mainSignature/main.java")) + .withCheck(CHECK) + .verifyNoIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.html new file mode 100644 index 0000000000..ae497aa6ae --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.html @@ -0,0 +1,35 @@ +

Why is this an issue?

+

"A rose by any other name would smell as sweet," but main by any other name would not. Just because a method has the name "main", that +doesn’t make it the entry point to an application.

+

In Java versions prior to Java 25, the main method must have a specific signature to serve as the application entry point: it must be public +static void and accept a single String [] as an argument.

+

Starting with Java 25, the requirements for main methods have been relaxed. The main method can now be:

+ +

Using main for anything other than an application entry point can be confusing to developers unfamiliar with Java, as the method name +strongly suggests it is the starting point of execution. To avoid this confusion, methods with different purposes should be given other names.

+

Noncompliant code example

+
+public void main(String arg) {  // Noncompliant
+  // ...
+}
+
+

Compliant solution

+
+void main(String [] args) { // Compliant Java 25 instance main
+  run(args[0]);
+}
+
+public void run(String arg) { // Renamed
+  // ...
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.json new file mode 100644 index 0000000000..bbb091b287 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3051.json @@ -0,0 +1,24 @@ +{ + "title": "\"main\" should have the right signature", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "15min" + }, + "tags": [ + "convention", + "confusing" + ], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-3051", + "sqKey": "S3051", + "scope": "Main", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "LOW" + }, + "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 2661f07bb4..a32f46b3c7 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 @@ -252,6 +252,7 @@ "S3039", "S3042", "S3046", + "S3051", "S3063", "S3064", "S3066", From ac99ee13bbe0ddb137cf44ea83d5c6e5c0e0d30f Mon Sep 17 00:00:00 2001 From: Tomasz Tylenda Date: Wed, 11 Feb 2026 14:02:11 +0100 Subject: [PATCH 2/2] Autoscan diff --- .../src/test/resources/autoscan/diffs/diff_S3051.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S3051.json diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S3051.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S3051.json new file mode 100644 index 0000000000..73d50768c0 --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S3051.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S3051", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +}