diff --git a/check_api/src/main/java/com/google/errorprone/matchers/Matchers.java b/check_api/src/main/java/com/google/errorprone/matchers/Matchers.java index 15dffe166aa..59b91e628d6 100644 --- a/check_api/src/main/java/com/google/errorprone/matchers/Matchers.java +++ b/check_api/src/main/java/com/google/errorprone/matchers/Matchers.java @@ -20,6 +20,7 @@ import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Streams.stream; +import static com.google.errorprone.matchers.MethodVisibility.Visibility.PRIVATE; import static com.google.errorprone.matchers.MethodVisibility.Visibility.PUBLIC; import static com.google.errorprone.predicates.TypePredicates.isDescendantOf; import static com.google.errorprone.predicates.TypePredicates.isExactType; @@ -52,6 +53,7 @@ import com.google.errorprone.suppliers.Supplier; import com.google.errorprone.suppliers.Suppliers; import com.google.errorprone.util.ASTHelpers; +import com.google.errorprone.util.SourceVersion; import com.sun.source.tree.AnnotationTree; import com.sun.source.tree.AssertTree; import com.sun.source.tree.AssignmentTree; @@ -1454,14 +1456,35 @@ public static Matcher instanceEqualsInvocation() { return (Matcher) INSTANCE_EQUALS; } + /** + * Matches main method candidates. On Java 25+, matches as defined in + * JLS 12.1.4: + * named {@code main}, returns {@code void}, not {@code private}, and either has a single {@code + * String[]} parameter or no parameters. On earlier versions, matches the traditional + * {@code public static void main(String[])} signature. + */ public static final Matcher MAIN_METHOD = allOf( - methodHasArity(1), - methodHasVisibility(PUBLIC), - hasModifier(STATIC), methodReturns(Suppliers.VOID_TYPE), methodIsNamed("main"), - methodHasParameters(isSameType(Suppliers.arrayOf(STRING_TYPE)))); + anyOf( + // Traditional: public static void main(String[]) + allOf( + methodHasVisibility(PUBLIC), + hasModifier(STATIC), + methodHasParameters(isSameType(Suppliers.arrayOf(STRING_TYPE)))), + // Java 25+: void main() or void main(String[]), not private + allOf( + supportsInstanceMainMethods(), + not(methodHasVisibility(PRIVATE)), + anyOf( + methodHasNoParameters(), + methodHasParameters( + isSameType(Suppliers.arrayOf(STRING_TYPE))))))); + + private static Matcher supportsInstanceMainMethods() { + return (tree, state) -> SourceVersion.supportsInstanceMainMethods(state.context); + } private static final Matcher INSTANCE_HASHCODE = allOf(instanceMethod().anyClass().named("hashCode").withNoParameters(), isSameType(INT_TYPE)); diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/SystemExitOutsideMainTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/SystemExitOutsideMainTest.java index e0cc3eeb52c..ffdd810c2a8 100644 --- a/core/src/test/java/com/google/errorprone/bugpatterns/SystemExitOutsideMainTest.java +++ b/core/src/test/java/com/google/errorprone/bugpatterns/SystemExitOutsideMainTest.java @@ -16,6 +16,8 @@ package com.google.errorprone.bugpatterns; +import static com.google.common.truth.TruthJUnit.assume; + import com.google.errorprone.CompilationTestHelper; import org.junit.Test; import org.junit.runner.RunWith; @@ -46,6 +48,7 @@ void f() { @Test public void systemExitMainLookalikeWithoutParameters() { + assume().that(Runtime.version().feature()).isLessThan(25); helper .addSourceLines( "Test.java", @@ -60,6 +63,22 @@ public static void main() { .doTest(); } + @Test + public void systemExitMainNoArgsNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + public static void main() { + System.exit(0); + } + } + """) + .doTest(); + } + @Test public void systemExitMainLookalikeWithTwoParameters() { helper @@ -78,6 +97,7 @@ public static void main(String[] args, int num) { @Test public void systemExitMainLookalikeWithoutStatic() { + assume().that(Runtime.version().feature()).isLessThan(25); helper .addSourceLines( "Test.java", @@ -92,6 +112,22 @@ public void main(String[] args) { .doTest(); } + @Test + public void systemExitInstanceMainNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + public void main(String[] args) { + System.exit(0); + } + } + """) + .doTest(); + } + @Test public void systemExitMainLookalikeDifferentReturnType() { helper @@ -221,4 +257,124 @@ public static void main(String[] args) { """) .doTest(); } + + @Test + public void systemExitMainLookalikePackagePrivate() { + assume().that(Runtime.version().feature()).isLessThan(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + static void main(String[] args) { + // BUG: Diagnostic contains: SystemExitOutsideMain + System.exit(0); + } + } + """) + .doTest(); + } + + @Test + public void systemExitProtectedMainNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + protected static void main(String[] args) { + System.exit(0); + } + } + """) + .doTest(); + } + + @Test + public void systemExitPackagePrivateMainNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + static void main(String[] args) { + System.exit(0); + } + } + """) + .doTest(); + } + + @Test + public void systemExitInstanceNoArgsMainNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + void main() { + System.exit(0); + } + } + """) + .doTest(); + } + + @Test + public void systemExitInMethodNoArgsMainInClassNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + void main() { + foo(); + } + + static void foo() { + System.exit(0); + } + } + """) + .doTest(); + } + + @Test + public void systemExitInMethodInstanceMainInClassNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + class Test { + void main(String[] args) { + foo(); + } + + static void foo() { + System.exit(0); + } + } + """) + .doTest(); + } + + @Test + public void systemExitImplicitlyDeclaredClassNegative() { + assume().that(Runtime.version().feature()).isAtLeast(25); + helper + .addSourceLines( + "Test.java", + """ + void main() { + System.exit(0); + } + """) + .doTest(); + } + }