Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2879,6 +2879,12 @@
"falseNegatives": 0,
"falsePositives": 0
},
{
"ruleKey": "8446",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
},
{
"ruleKey": "8447",
"hasTruePositives": true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S8446",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
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!");
}
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();
Comment thread
rombirli marked this conversation as resolved.
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
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();
}
}
Loading
Loading