ThreadingFixCore.java
/*******************************************************************************
* Copyright (c) 2025 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer - initial API and implementation
*******************************************************************************/
package org.sandbox.jdt.internal.corext.fix;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.triggerpattern.api.Match;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.TriggerPatternEngine;
/**
* Fix core for threading anti-patterns using TriggerPattern hints.
*
* <p>Inspired by NetBeans' Tiny.java threading hints, this class detects and fixes
* common threading mistakes such as calling {@code Thread.run()} directly instead
* of {@code Thread.start()}.</p>
*
* @since 1.2.5
* @see <a href="https://github.com/apache/netbeans/blob/master/java/java.hints/src/org/netbeans/modules/java/hints/threading/Tiny.java">NetBeans Tiny.java</a>
*/
public class ThreadingFixCore {
private static final TriggerPatternEngine ENGINE = new TriggerPatternEngine();
/**
* Finds threading anti-pattern operations in the compilation unit.
*
* @param compilationUnit the compilation unit to search
* @param operations the set to add found operations to
*/
public static void findOperations(CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperation> operations) {
// Pattern 1: $thread.run() -> $thread.start()
// Calling Thread.run() directly executes on the current thread instead of starting a new one
Pattern threadRunPattern = Pattern.of("$thread.run()", PatternKind.EXPRESSION); //$NON-NLS-1$
List<Match> threadRunMatches = ENGINE.findMatches(compilationUnit, threadRunPattern);
for (Match match : threadRunMatches) {
if (isThreadType(match)) {
operations.add(new ThreadRunToStartOperation(match));
}
}
}
/**
* Checks whether the matched {@code $thread.run()} call is on a {@code java.lang.Thread}
* receiver, to avoid rewriting unrelated {@code run()} calls (e.g., {@code Runnable.run()}).
*/
private static boolean isThreadType(Match match) {
ASTNode matchedNode = match.getMatchedNode();
if (!(matchedNode instanceof MethodInvocation)) {
return false;
}
MethodInvocation invocation = (MethodInvocation) matchedNode;
// Preferred: check the declaring class of the resolved method binding
IMethodBinding methodBinding = invocation.resolveMethodBinding();
if (methodBinding != null) {
ITypeBinding declaringClass = methodBinding.getDeclaringClass();
if (declaringClass != null) {
return isThreadOrSubtype(declaringClass);
}
}
// Fallback: check the receiver expression type
Expression receiver = invocation.getExpression();
if (receiver != null) {
ITypeBinding receiverType = receiver.resolveTypeBinding();
if (receiverType != null) {
return isThreadOrSubtype(receiverType);
}
}
// Skip when bindings are unavailable to avoid incorrect rewrites
return false;
}
private static boolean isThreadOrSubtype(ITypeBinding type) {
ITypeBinding current = type;
while (current != null) {
if ("java.lang.Thread".equals(current.getQualifiedName())) { //$NON-NLS-1$
return true;
}
current = current.getSuperclass();
}
return false;
}
/**
* Rewrite operation for Thread.run() → Thread.start() transformation.
*
* <p>Replaces direct calls to {@code Thread.run()} with {@code Thread.start()},
* because calling {@code run()} directly does not start a new thread.</p>
*/
private static class ThreadRunToStartOperation extends CompilationUnitRewriteOperation {
private final Match match;
public ThreadRunToStartOperation(Match match) {
this.match = match;
}
@Override
public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
TextEditGroup group = createTextEditGroup("Replace Thread.run() with Thread.start()", cuRewrite); //$NON-NLS-1$
ASTNode matchedNode = match.getMatchedNode();
if (!(matchedNode instanceof MethodInvocation)) {
return;
}
MethodInvocation originalInvocation = (MethodInvocation) matchedNode;
// Get the receiver expression ($thread)
ASTNode threadNode = match.getBinding("$thread"); //$NON-NLS-1$
// Create the replacement: $thread.start()
MethodInvocation startInvocation = ast.newMethodInvocation();
if (threadNode instanceof Expression) {
startInvocation.setExpression((Expression) ASTNode.copySubtree(ast, (Expression) threadNode));
}
startInvocation.setName(ast.newSimpleName("start")); //$NON-NLS-1$
rewrite.replace(originalInvocation, startInvocation, group);
}
}
}