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
@@ -0,0 +1,91 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.staticanalysis;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.search.UsesType;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.TypeUtils;
import org.openrewrite.marker.SearchResult;

import java.time.Duration;
import java.util.Set;

import static java.util.Collections.singleton;

@Value
@EqualsAndHashCode(callSuper = false)
public class FindThreadGroupUsages extends Recipe {

private static final String THREAD_GROUP = "java.lang.ThreadGroup";
private static final MethodMatcher THREAD_GET_THREAD_GROUP =
new MethodMatcher("java.lang.Thread getThreadGroup()");

String displayName = "Find `ThreadGroup` usages";

String description = "Marks uses of `java.lang.ThreadGroup`. `ThreadGroup` was originally " +
"intended to help with thread management but its API has serious design flaws " +
"(most methods are either deprecated or unsafe) and it has been superseded by " +
"`java.util.concurrent.ExecutorService`. Sites flagged include `new ThreadGroup(...)` " +
"constructor calls, calls to `Thread.getThreadGroup()`, and method invocations on " +
"`ThreadGroup` receivers.";

Set<String> tags = singleton("RSPEC-S3014");

Duration estimatedEffortPerOccurrence = Duration.ofMinutes(30);

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.or(
new UsesType<>(THREAD_GROUP, false),
new UsesMethod<>(THREAD_GET_THREAD_GROUP)),
new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
J.NewClass n = super.visitNewClass(newClass, ctx);
if (TypeUtils.isOfClassType(n.getType(), THREAD_GROUP)) {
return SearchResult.found(n,
"`ThreadGroup` is superseded by `java.util.concurrent.ExecutorService`.");
}
return n;
}

@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
if (THREAD_GET_THREAD_GROUP.matches(m)) {
return SearchResult.found(m,
"`Thread.getThreadGroup()` exposes the discouraged `ThreadGroup` API.");
}
if (m.getSelect() != null &&
TypeUtils.isOfClassType(m.getSelect().getType(), THREAD_GROUP)) {
return SearchResult.found(m,
"Method call on a `ThreadGroup` receiver; use an `ExecutorService` instead.");
}
return m;
}
});
}
}
1 change: 1 addition & 0 deletions src/main/resources/META-INF/rewrite/recipes.csv
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanaly
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.FindMissingJavadocOnPublicMethods,Find public methods missing Javadoc,"Locates `public` method declarations that are not documented with a Javadoc comment, marks them with a search result, and records them in a data table.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,"[{""name"":""org.openrewrite.staticanalysis.table.MissingJavadocOnPublicMethods"",""displayName"":""Public methods missing Javadoc"",""instanceName"":""Public methods missing Javadoc"",""description"":""Public method declarations that are not documented with a Javadoc comment."",""columns"":[{""name"":""sourcePath"",""type"":""String"",""displayName"":""Source path"",""description"":""The path to the source file containing the undocumented method.""},{""name"":""className"",""type"":""String"",""displayName"":""Class"",""description"":""The fully qualified name of the class declaring the method.""},{""name"":""methodName"",""type"":""String"",""displayName"":""Method name"",""description"":""The name of the undocumented method.""}]}]"
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.FindNewExceptionWithoutCause,Find new exceptions thrown without the caught exception,"Finds `catch` blocks that throw a newly created exception without referencing the caught exception, which discards the original exception's stack trace and message. Data flow (taint) tracking is used to establish whether the caught exception—or any value derived from it—reaches the thrown exception, so indirect references through local variables and string concatenation are not falsely reported. This mirrors PMD's `PreserveStackTrace` rule.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,"[{""name"":""org.openrewrite.staticanalysis.table.ExceptionsWithoutCause"",""displayName"":""Exceptions thrown without the caught cause"",""instanceName"":""Exceptions thrown without the caught cause"",""description"":""New exceptions thrown from a `catch` block that do not reference the caught exception."",""columns"":[{""name"":""sourcePath"",""type"":""String"",""displayName"":""Source path"",""description"":""The path to the source file containing the offending `throw`.""},{""name"":""caughtType"",""type"":""String"",""displayName"":""Caught exception type"",""description"":""The declared type of the exception caught by the enclosing `catch` clause.""},{""name"":""thrownType"",""type"":""String"",""displayName"":""Thrown exception type"",""description"":""The type of the new exception thrown without referencing the caught exception.""}]}]"
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.FindSystemAndRuntimeExitCalls,Find JVM exit calls,"Marks calls to `System.exit(int)`, `Runtime.exit(int)`, and `Runtime.halt(int)`. Terminating the JVM from library or application code is rarely correct: it bypasses the normal shutdown flow, prevents `finally` blocks from running in other threads, and can leave file, socket, and database resources in an inconsistent state. `Runtime.halt` is particularly dangerous because it also skips shutdown hooks.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.FindThreadGroupUsages,Find `ThreadGroup` usages,"Marks uses of `java.lang.ThreadGroup`. `ThreadGroup` was originally intended to help with thread management but its API has serious design flaws (most methods are either deprecated or unsafe) and it has been superseded by `java.util.concurrent.ExecutorService`. Sites flagged include `new ThreadGroup(...)` constructor calls, calls to `Thread.getThreadGroup()`, and method invocations on `ThreadGroup` receivers.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.FindWaitWithMultipleLocksHeld,Find `Object.wait()` calls made while holding multiple monitors,"Finds zero-argument `Object.wait()` invocations whose enclosing method holds two or more monitors — either through nested `synchronized (...)` blocks, or a `synchronized` method combined with a nested `synchronized` block. `wait()` releases only the monitor of its receiver, so other held monitors continue to block their waiters and can deadlock. Timed waits (`wait(long)`, `wait(long, int)`) are intentionally excluded — sonar-java's S3046 does the same, since timed waits are self-releasing and less likely to cause the failure mode.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.FixStringFormatExpressions,Fix `String#format` and `String#formatted` expressions,"Fix `String#format` and `String#formatted` expressions by replacing `\n` newline characters with `%n` and removing any unused arguments. Note this recipe is scoped to only transform format expressions which do not specify the argument index. Using `%n` ensures the correct platform-specific line separator, and removing unused arguments eliminates dead code that may mask a mismatch between the format string and its parameters.",1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
maven,org.openrewrite.recipe:rewrite-static-analysis,org.openrewrite.staticanalysis.ForLoopControlVariablePostfixOperators,`for` loop counters should use postfix operators,Replace `for` loop control variables using pre-increment (`++i`) or pre-decrement (`--i`) operators with their post-increment (`i++`) or post-decrement (`i++`) notation equivalents.,1,,Static analysis and remediation,,Remediations for issues identified by SAST tools.,,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.staticanalysis;

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

class FindThreadGroupUsagesTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new FindThreadGroupUsages());
}

@DocumentExample
@Test
void flagsThreadGroupConstruction() {
rewriteRun(
//language=java
java(
"""
class A {
void go() {
ThreadGroup group = new ThreadGroup("workers");
Thread t = new Thread(group, () -> {});
t.start();
}
}
""",
"""
class A {
void go() {
ThreadGroup group = /*~~(`ThreadGroup` is superseded by `java.util.concurrent.ExecutorService`.)~~>*/new ThreadGroup("workers");
Thread t = new Thread(group, () -> {});
t.start();
}
}
"""
)
);
}

@Test
void flagsThreadGetThreadGroupCall() {
rewriteRun(
//language=java
java(
"""
class A {
ThreadGroup current() {
return Thread.currentThread().getThreadGroup();
}
}
""",
"""
class A {
ThreadGroup current() {
return /*~~(`Thread.getThreadGroup()` exposes the discouraged `ThreadGroup` API.)~~>*/Thread.currentThread().getThreadGroup();
}
}
"""
)
);
}

@Test
void flagsMethodCallsOnThreadGroupReceiver() {
rewriteRun(
//language=java
java(
"""
class A {
int count(ThreadGroup group) {
return group.activeCount();
}
}
""",
"""
class A {
int count(ThreadGroup group) {
return /*~~(Method call on a `ThreadGroup` receiver; use an `ExecutorService` instead.)~~>*/group.activeCount();
}
}
"""
)
);
}

@Test
void doesNotFlagUnrelatedThreadApi() {
rewriteRun(
//language=java
java(
"""
class A {
void go() {
Thread t = new Thread(() -> {});
t.setName("worker");
t.start();
t.interrupt();
}
}
"""
)
);
}

@Test
void doesNotFlagUserClassNamedThreadGroup() {
rewriteRun(
//language=java
java(
"""
package com.example;

public class ThreadGroup {
public int count() { return 0; }
}
"""
),
//language=java
java(
"""
package com.example;

class A {
int use() {
ThreadGroup tg = new ThreadGroup();
return tg.count();
}
}
"""
)
);
}
}
Loading