HintFileFixCore.java
/*******************************************************************************
* Copyright (c) 2026 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.triggerpattern.cleanup;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.ScopeAnalyzer;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.refactoring.structure.ImportRemover;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.internal.corext.util.TypeWideningAnalyzer;
import org.sandbox.jdt.triggerpattern.api.BatchTransformationProcessor;
import org.sandbox.jdt.triggerpattern.api.BatchTransformationProcessor.TransformationResult;
import org.sandbox.jdt.triggerpattern.api.EmbeddedJavaBlock;
import org.sandbox.jdt.triggerpattern.api.HintFile;
import org.sandbox.jdt.triggerpattern.api.ImportDirective;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.TransformationRule;
import org.sandbox.jdt.triggerpattern.eclipse.HintFinding;
import org.sandbox.jdt.triggerpattern.internal.EmbeddedFixExecutor;
import org.sandbox.jdt.triggerpattern.internal.EmbeddedGuardRegistrar;
import org.sandbox.jdt.triggerpattern.internal.EmbeddedJavaCompiler;
import org.sandbox.jdt.triggerpattern.internal.EmbeddedJavaCompiler.CompilationResult;
import org.sandbox.jdt.triggerpattern.internal.GuardRegistry;
import org.sandbox.jdt.triggerpattern.internal.HintFileParser;
import org.sandbox.jdt.triggerpattern.internal.HintFileRegistry;
import org.sandbox.jdt.triggerpattern.internal.JavaSourceMask;
/**
* Fix core that creates rewrite operations from {@code .sandbox-hint} files.
*
* <p>This bridges the gap between the {@code .sandbox-hint} DSL and the Eclipse
* CleanUp framework by using {@link BatchTransformationProcessor} to find matches
* and creating {@link CompilationUnitRewriteOperation} instances for each match
* that has a replacement.</p>
*
* @since 1.3.5
*/
public class HintFileFixCore {
/**
* Finds all hint-file-based cleanup operations for the given compilation unit.
*
* <p>Loads all registered {@code .sandbox-hint} files from the
* {@link HintFileRegistry}, processes them with {@link BatchTransformationProcessor},
* and creates rewrite operations for each match with a replacement.
* Hint-only results (rules without replacement) are silently discarded.
* Use the four-parameter overload to collect hint-only findings.</p>
*
* @param compilationUnit the compilation unit to search
* @param operations the set to add found operations to
* @param enabledBundles set of enabled bundled hint file IDs; project-level
* hint files (with {@code "project:"} prefix) are always included
* regardless of this parameter
*/
public static void findOperations(CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperation> operations,
Set<String> enabledBundles) {
findOperations(compilationUnit, operations, enabledBundles, null);
}
/**
* Finds hint-file-based cleanup operations for the given compilation unit,
* collecting hint-only results into the provided findings list.
*
* <p>Rules with a replacement produce rewrite operations. Rules without a
* replacement (hint-only) produce {@link HintFinding} entries that can be
* reported as problem markers.</p>
*
* @param compilationUnit the compilation unit to search
* @param operations the set to add found rewrite operations to
* @param enabledBundles set of enabled bundled hint file IDs
* @param findings list to collect hint-only findings into; may be {@code null}
* to discard hint-only results (backward compatible)
*/
public static void findOperations(CompilationUnit compilationUnit,
Set<CompilationUnitRewriteOperation> operations,
Set<String> enabledBundles,
List<HintFinding> findings) {
// Ensure built-in guard functions (sourceVersionGE, etc.) are registered
GuardRegistry.getInstance();
HintFileRegistry registry = HintFileRegistry.getInstance();
// Ensure bundled libraries are loaded
registry.loadBundledLibraries(HintFileFixCore.class.getClassLoader());
// Load extension-point contributed hint files (encoding, junit5, etc.)
registry.loadFromExtensions();
// Load project-level .sandbox-hint files if available
if (compilationUnit.getJavaElement() != null
&& compilationUnit.getJavaElement().getJavaProject() != null) {
org.eclipse.core.resources.IProject project = compilationUnit.getJavaElement()
.getJavaProject().getProject();
registry.loadProjectHintFiles(project);
}
for (Map.Entry<String, HintFile> entry : registry.getAllHintFiles().entrySet()) {
String hintFileId = entry.getKey();
HintFile hintFile = entry.getValue();
// Filter by enabled bundles (project-level files are always included)
if (enabledBundles != null && !hintFileId.startsWith("project:") //$NON-NLS-1$
&& !enabledBundles.contains(hintFileId)) {
continue;
}
// Register guard and fix functions from <? ?> blocks
registerEmbeddedFunctions(hintFile, hintFileId);
List<TransformationRule> resolvedRules = registry.resolveIncludes(hintFile);
BatchTransformationProcessor processor = new BatchTransformationProcessor(hintFile, resolvedRules);
List<TransformationResult> results = processor.process(compilationUnit);
for (TransformationResult result : results) {
if (result.hasReplacement()) {
if (isNonWidenableDeclaration(result, compilationUnit)) {
continue;
}
addRewriteOperation(compilationUnit, result, operations);
} else if (findings != null) {
findings.add(HintFinding.fromTransformationResult(result, compilationUnit));
}
}
}
}
/**
* Finds hint-file-based cleanup operations for a specific bundle,
* tracking processed nodes to avoid duplicate processing with imperative helpers.
*
* <p>This method loads a single hint file bundle by ID from the
* {@link HintFileRegistry} and creates rewrite operations for matches.
* Matched nodes are added to {@code nodesprocessed} so that imperative
* helpers can skip them.</p>
*
* @param compilationUnit the compilation unit to search
* @param bundleId the hint file bundle ID to load (e.g., {@code "encoding"})
* @param operations the set to add found operations to
* @param nodesprocessed set of already-processed AST nodes; matched nodes
* are added to this set to prevent double-processing
* @since 1.3.7
*/
public static void findOperationsForBundle(CompilationUnit compilationUnit,
String bundleId, Set<CompilationUnitRewriteOperation> operations,
Set<ASTNode> nodesprocessed) {
findOperationsForBundle(compilationUnit, bundleId, operations, nodesprocessed, null);
}
/**
* Finds hint-file-based cleanup operations for a specific bundle,
* tracking processed nodes and passing compiler options for guard evaluation.
*
* <p>This overload accepts compiler options that are passed to guard
* evaluation. This enables mode-dependent DSL rules via the
* {@code sandbox.cleanup.mode} option.</p>
*
* @param compilationUnit the compilation unit to search
* @param bundleId the hint file bundle ID to load (e.g., {@code "encoding"})
* @param operations the set to add found operations to
* @param nodesprocessed set of already-processed AST nodes; matched nodes
* are added to this set to prevent double-processing
* @param compilerOptions compiler options for guard context; may contain
* {@code sandbox.cleanup.mode} for mode-dependent rules (may be {@code null})
* @since 1.3.8
*/
public static void findOperationsForBundle(CompilationUnit compilationUnit,
String bundleId, Set<CompilationUnitRewriteOperation> operations,
Set<ASTNode> nodesprocessed, Map<String, String> compilerOptions) {
// Ensure built-in guard functions (sourceVersionGE, etc.) are registered
GuardRegistry.getInstance();
HintFileRegistry registry = HintFileRegistry.getInstance();
// Ensure bundled libraries are loaded
registry.loadBundledLibraries(HintFileFixCore.class.getClassLoader());
// Load extension-point contributed hint files (encoding, junit5, etc.)
registry.loadFromExtensions();
// Load project-level .sandbox-hint files if available
if (compilationUnit.getJavaElement() != null
&& compilationUnit.getJavaElement().getJavaProject() != null) {
org.eclipse.core.resources.IProject project = compilationUnit.getJavaElement()
.getJavaProject().getProject();
registry.loadProjectHintFiles(project);
}
HintFile hintFile = registry.getHintFile(bundleId);
if (hintFile == null) {
return;
}
// Register guard and fix functions from <? ?> blocks
registerEmbeddedFunctions(hintFile, bundleId);
List<TransformationRule> resolvedRules = registry.resolveIncludes(hintFile);
BatchTransformationProcessor processor = new BatchTransformationProcessor(hintFile, resolvedRules);
List<TransformationResult> results = processor.process(compilationUnit, compilerOptions);
for (TransformationResult result : results) {
if (result.hasReplacement()) {
if (isNonWidenableDeclaration(result, compilationUnit)) {
continue;
}
ASTNode matchedNode = result.match().getMatchedNode();
if (addRewriteOperation(compilationUnit, result, operations) && matchedNode != null) {
nodesprocessed.add(matchedNode);
}
}
}
}
/**
* Loads a hint file from a string and finds operations.
*
* @param compilationUnit the compilation unit to search
* @param hintFileContent the hint file content
* @param operations the set to add found operations to
*/
public static void findOperationsFromContent(CompilationUnit compilationUnit,
String hintFileContent, Set<CompilationUnitRewriteOperation> operations) {
// Ensure built-in guard functions (sourceVersionGE, etc.) are registered
GuardRegistry.getInstance();
try {
HintFileParser parser = new HintFileParser();
HintFile hintFile = parser.parse(hintFileContent);
findOperationsFromHint(compilationUnit, hintFile, operations);
} catch (HintFileParser.HintParseException e) {
// Skip invalid hint files
}
}
/** Uses an already parsed draft and its explicit binding policy in the ordinary backend. */
public static void findOperationsFromHint(CompilationUnit compilationUnit,
HintFile hintFile, Set<CompilationUnitRewriteOperation> operations) {
GuardRegistry.getInstance();
for (TransformationResult result : new BatchTransformationProcessor(hintFile).process(compilationUnit)) {
if (result.hasReplacement() && !isNonWidenableDeclaration(result, compilationUnit)) {
addRewriteOperation(compilationUnit, result, operations);
}
}
}
/** Declines unsupported or recovered text before constructing an empty cleanup fix. */
private static boolean addRewriteOperation(CompilationUnit root, TransformationResult result,
Set<CompilationUnitRewriteOperation> operations) {
ASTNode node= result.match().getMatchedNode();
boolean expression= node instanceof Expression;
boolean statement= node instanceof Statement
&& !(node instanceof VariableDeclarationStatement && result.replacement().contains("$widestType")); //$NON-NLS-1$
Map<String, String> options= root.getJavaElement() == null
? org.sandbox.jdt.triggerpattern.api.SemanticRewritePlanContext.currentCompilerOptions()
: root.getJavaElement().getJavaProject().getOptions(true);
if ((expression || statement)
&& (HintFileRewriteOperation.hasCommentInRange(root, result.match().getOffset(), result.match().getLength())
|| expression && HintFileRewriteOperation.parseExpression(result.replacement(), options) == null
|| statement && HintFileRewriteOperation.parseBody(result.replacement(), options) == null)) {
return false;
}
Map<String, String> annotationTypes= new java.util.LinkedHashMap<>();
if (node instanceof org.eclipse.jdt.core.dom.MethodDeclaration method) {
var replacement= HintFileRewriteOperation.parseReplacementAsMethodDeclaration(result.replacement(), options);
if (replacement == null) {
return false;
}
for (Object modifier : replacement.modifiers()) {
if (modifier instanceof Annotation annotation) {
// Method rewrites currently render marker annotations only.
if (!(annotation instanceof MarkerAnnotation)) {
return false;
}
String name= annotation.getTypeName().getFullyQualifiedName();
String type= HintFileRewriteOperation.resolveAnnotationType(method, name);
if (type == null) {
return false;
}
annotationTypes.put(name, type);
}
}
}
operations.add(new HintFileRewriteOperation(result, annotationTypes));
return true;
}
/**
* Registers guard and fix functions from embedded Java blocks in a hint file.
*
* <p>Compiles each non-blank {@code <? ?>} block and registers its guard
* methods in the {@link GuardRegistry} and fix methods in the
* {@link EmbeddedFixExecutor}.</p>
*
* @param hintFile the hint file containing embedded Java blocks
* @param hintFileId the hint file ID for tracking
* @since 1.5.0
*/
private static void registerEmbeddedFunctions(HintFile hintFile, String hintFileId) {
// Unregister previously registered guards/fixes for this hint file
// to avoid stale entries when a hint file is reloaded after editing
EmbeddedGuardRegistrar.unregisterGuards(hintFileId);
EmbeddedFixExecutor.unregisterFixes(hintFileId);
for (EmbeddedJavaBlock block : hintFile.getEmbeddedJavaBlocks()) {
if (block.getSource().isBlank()) {
continue;
}
CompilationResult compResult = EmbeddedJavaCompiler.compile(block, hintFileId);
if (!compResult.hasErrors()) {
EmbeddedGuardRegistrar.registerGuards(compResult, hintFileId);
EmbeddedFixExecutor.registerFixes(compResult, hintFileId);
}
}
}
/**
* Checks if a transformation result is a DECLARATION pattern with {@code $widestType}
* that cannot actually be widened. When widening IS possible, the widest type FQN
* is stored in the match's extra data for later retrieval by
* {@code handleDeclarationRewrite}.
*
* <p>This filtering ensures that rewrite operations are only created when a real
* type change will be produced — avoiding entering {@code rewriteAST} without a
* change to apply.</p>
*
* @param result the transformation result to check
* @param compilationUnit the compilation unit for type widening analysis
* @return {@code true} if this is a $widestType declaration that cannot be widened
* (should be skipped); {@code false} otherwise (proceed normally)
* @since 1.3.12
*/
private static boolean isNonWidenableDeclaration(TransformationResult result,
CompilationUnit compilationUnit) {
String replacement = result.replacement();
if (replacement == null || !replacement.contains("$widestType")) { //$NON-NLS-1$
return false; // Not a $widestType rule — proceed normally
}
ASTNode matchedNode = result.match().getMatchedNode();
if (!(matchedNode instanceof VariableDeclarationStatement declStmt)) {
return false; // Not a declaration — proceed normally
}
if (declStmt.fragments().size() != 1) {
return true; // Multi-fragment — cannot widen
}
VariableDeclarationFragment fragment =
(VariableDeclarationFragment) declStmt.fragments().get(0);
IVariableBinding varBinding = fragment.resolveBinding();
if (varBinding == null) {
return true; // No binding — cannot analyze
}
Map<String, TypeWideningAnalyzer.TypeWideningResult> analysisResults =
TypeWideningAnalyzer.analyzeCompilationUnit(compilationUnit);
TypeWideningAnalyzer.TypeWideningResult wideningResult =
analysisResults.get(varBinding.getKey());
if (wideningResult == null || !wideningResult.canWiden()) {
return true; // No widening possible — skip
}
// Widening IS possible — store the FQN for handleDeclarationRewrite
result.match().putExtraData("__widestType__", //$NON-NLS-1$
wideningResult.getWidestType().getQualifiedName());
return false; // Proceed — create the operation
}
/**
* Generic rewrite operation that replaces matched AST nodes with the
* replacement text from a {@code .sandbox-hint} rule.
*
* <p>This operation handles text-based replacement by parsing the replacement
* text as an expression and replacing the matched AST node. It also handles
* import management via {@link ImportDirective}.</p>
*/
private static class HintFileRewriteOperation extends CompilationUnitRewriteOperation {
private final TransformationResult result;
private final Map<String, String> annotationTypes;
public HintFileRewriteOperation(TransformationResult result, Map<String, String> annotationTypes) {
this.result = result;
this.annotationTypes= Map.copyOf(annotationTypes);
}
@Override
public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) throws CoreException {
ASTRewrite rewrite = cuRewrite.getASTRewrite();
AST ast = cuRewrite.getRoot().getAST();
String description = result.description() != null
? result.description()
: "Apply hint file rule"; //$NON-NLS-1$
TextEditGroup group = createTextEditGroup(description, cuRewrite);
ASTNode matchedNode = result.match().getMatchedNode();
String replacement = result.replacement();
if (matchedNode == null || replacement == null) {
return;
}
boolean sequence= result.rule().sourcePattern().getKind() == PatternKind.STATEMENT_SEQUENCE;
boolean ordinaryStatement= matchedNode instanceof Statement
&& !(matchedNode instanceof VariableDeclarationStatement && replacement.contains("$widestType")); //$NON-NLS-1$
if ((sequence || ordinaryStatement)
&& (hasCommentInRange(cuRewrite.getRoot(), result.match().getOffset(), result.match().getLength())
|| parseStatements(replacement, cuRewrite) == null)) {
// Do not change imports or partially apply a recovered replacement.
return;
}
if (matchedNode instanceof Expression
&& (hasCommentInRange(cuRewrite.getRoot(), result.match().getOffset(), result.match().getLength())
|| parseExpression(replacement, cuRewrite) == null)) {
return;
}
if (matchedNode instanceof org.eclipse.jdt.core.dom.MethodDeclaration
&& parseReplacementAsMethodDeclaration(replacement, cuRewrite.getCu().getJavaProject().getOptions(true)) == null) {
return;
}
// Check for embedded fix function reference: <?fixName?>
// These are dispatched to EmbeddedFixExecutor instead of text-based replacement.
// Currently fix functions are stubs (Phase 1.4 MVP) — log and skip rewrite.
String fixName = extractEmbeddedFixName(replacement);
if (fixName != null) {
if (EmbeddedFixExecutor.hasFix(fixName)) {
EmbeddedFixExecutor.execute(fixName);
}
return;
}
// Handle import directives
if (result.hasImportDirective()) {
ImportDirective imports = result.importDirective();
ImportRewrite importRewrite = cuRewrite.getImportRewrite();
for (String addImport : imports.getAddImports()) {
importRewrite.addImport(addImport);
}
// Use ImportRemover instead of directly calling importRewrite.removeImport().
// Direct removal is unsafe because the same type may still be referenced
// by other nodes in the compilation unit that are NOT being transformed.
// ImportRemover scans the AST and only removes the import when ALL
// references to the type are gone (see eclipse-jdt/eclipse.jdt.ui#121).
// Parsed replacement statements have no bindings for reused simple names.
// Retain their original dependencies instead of removing still-needed imports.
if (!imports.getRemoveImports().isEmpty() && !sequence && !ordinaryStatement) {
ImportRemover remover = cuRewrite.getImportRemover();
remover.registerRemovedNode(matchedNode);
remover.applyRemoves(importRewrite);
}
for (String addStatic : imports.getAddStaticImports()) {
int lastDot = addStatic.lastIndexOf('.');
if (lastDot > 0) {
String type = addStatic.substring(0, lastDot);
String member = addStatic.substring(lastDot + 1);
importRewrite.addStaticImport(type, member, false);
}
}
for (String removeStatic : imports.getRemoveStaticImports()) {
int lastDot = removeStatic.lastIndexOf('.');
if (lastDot > 0) {
String type = removeStatic.substring(0, lastDot);
String member = removeStatic.substring(lastDot + 1);
importRewrite.removeStaticImport(type + "." + member); //$NON-NLS-1$
}
}
// Handle replaceStaticImport directives
// Note: This may execute multiple times for the same imports when multiple
// rules match in the same file. ImportRewrite handles duplicate add/remove
// operations gracefully, so this is an acceptable tradeoff for simplicity.
for (Map.Entry<String, String> entry : imports.getReplaceStaticImports().entrySet()) {
String oldType = entry.getKey();
String newType = entry.getValue();
// Find existing static imports from oldType and replace with newType
CompilationUnit cu = cuRewrite.getRoot();
for (Object importObj : cu.imports()) {
org.eclipse.jdt.core.dom.ImportDeclaration importDecl =
(org.eclipse.jdt.core.dom.ImportDeclaration) importObj;
if (importDecl.isStatic()) {
String importName = importDecl.getName().getFullyQualifiedName();
if (importDecl.isOnDemand() && importName.equals(oldType)) {
// Wildcard: import static org.junit.Assert.* → import static org.junit.jupiter.api.Assertions.*
importRewrite.removeStaticImport(oldType + ".*"); //$NON-NLS-1$
importRewrite.addStaticImport(newType, "*", false); //$NON-NLS-1$
} else if (importName.startsWith(oldType + ".")) { //$NON-NLS-1$
// Specific: import static org.junit.Assert.assertEquals → ...Assertions.assertEquals
String member = importName.substring(oldType.length() + 1);
importRewrite.removeStaticImport(importName);
importRewrite.addStaticImport(newType, member, false);
}
}
}
}
}
// Parse replacement as an expression and replace the matched node
if (sequence || ordinaryStatement && matchedNode.getParent() instanceof Block) {
rewriteStatementSequence(cuRewrite, group);
} else if (matchedNode instanceof Expression) {
// Shorten FQNs to simple names (imports are already added above)
String shortenedReplacement = shortenFqns(replacement, result, cuRewrite.getImportRewrite());
// Try to parse the replacement as an expression
Expression newNode = parseExpression(shortenedReplacement, cuRewrite);
if (newNode != null) {
ASTNode copy = ASTNode.copySubtree(ast, newNode);
int oldCount = countStringLiterals(matchedNode);
int newCount = countStringLiterals(copy);
if (CharsetConstructorMigration.rewriteExpression(matchedNode, copy, replacement, cuRewrite, group)) {
TypeChangeInfo change= TypeChangeDetector.detectCharsetTypeChange(matchedNode, newNode);
if (change != null) ExceptionCleanupHelper.removeCheckedException(matchedNode,
change.exceptionFQN(), change.exceptionSimpleName(), group, rewrite, cuRewrite.getImportRemover());
CharsetConstructorMigration.adapt(matchedNode, copy, replacement, cuRewrite, group);
return;
}
// Auto-detect: did we change a String charset argument to a Charset type?
TypeChangeInfo typeChange = TypeChangeDetector.detectCharsetTypeChange(
matchedNode, newNode);
// Check if the matched node is inside a try body that will be unwrapped
// after removing the checked exception. If so, we must handle the
// replacement AND the try-unwrap atomically to avoid conflicts between
// ASTNodes.replaceAndRemoveNLS (statement-level replacement) and
// createMoveTarget (used by simplifyEmptyTryStatement).
boolean tryAlreadyHandled = false;
if (typeChange != null && oldCount > newCount) {
tryAlreadyHandled = replaceAndUnwrapTryIfNeeded(
matchedNode, copy, shortenedReplacement,
typeChange, rewrite, cuRewrite, group);
}
if (!tryAlreadyHandled) {
if (oldCount > newCount) {
try {
ASTNodes.replaceAndRemoveNLS(rewrite, matchedNode, copy, group, cuRewrite);
} catch (CoreException e) {
// Fall back to plain replace if NLS removal fails
rewrite.replace(matchedNode, copy, group);
}
} else {
rewrite.replace(matchedNode, copy, group);
}
if (typeChange != null) {
ExceptionCleanupHelper.removeCheckedException(
matchedNode,
typeChange.exceptionFQN(),
typeChange.exceptionSimpleName(),
group, rewrite, cuRewrite.getImportRemover());
}
}
CharsetConstructorMigration.adapt(matchedNode, copy, replacement, cuRewrite, group);
}
} else if (matchedNode instanceof Annotation) {
// Handle annotation replacement (e.g., @Before → @BeforeEach)
String annotationName = replacement.trim();
if (annotationName.startsWith("@")) { //$NON-NLS-1$
annotationName = annotationName.substring(1);
}
Annotation newAnnotation;
if (matchedNode instanceof SingleMemberAnnotation oldSingleMember) {
// Preserve the value: @Ignore("reason") → @Disabled("reason")
SingleMemberAnnotation newSingleMember = ast.newSingleMemberAnnotation();
newSingleMember.setTypeName(ast.newName(annotationName));
newSingleMember.setValue((Expression) ASTNode.copySubtree(ast, oldSingleMember.getValue()));
newAnnotation = newSingleMember;
} else if (matchedNode instanceof NormalAnnotation oldNormal) {
// Preserve member-value pairs: @Test(expected=X.class) → @Test(expected=X.class)
NormalAnnotation newNormal = ast.newNormalAnnotation();
newNormal.setTypeName(ast.newName(annotationName));
for (Object obj : oldNormal.values()) {
MemberValuePair oldPair = (MemberValuePair) obj;
MemberValuePair newPair = ast.newMemberValuePair();
newPair.setName(ast.newSimpleName(oldPair.getName().getIdentifier()));
newPair.setValue((Expression) ASTNode.copySubtree(ast, oldPair.getValue()));
newNormal.values().add(newPair);
}
newAnnotation = newNormal;
} else {
// MarkerAnnotation: @Before → @BeforeEach
MarkerAnnotation newMarker = ast.newMarkerAnnotation();
newMarker.setTypeName(ast.newName(annotationName));
newAnnotation = newMarker;
}
rewrite.replace(matchedNode, newAnnotation, group);
} else if (matchedNode instanceof org.eclipse.jdt.core.dom.MethodDeclaration) {
// Handle METHOD_DECLARATION → METHOD_DECLARATION rewrite.
// When the replacement is also a method declaration (e.g.,
// "@Test void $name($params$)"), diff the annotations between
// source and replacement patterns and add the missing ones.
handleMethodDeclarationRewrite(matchedNode, replacement, ast,
rewrite, cuRewrite, group);
} else if (matchedNode instanceof VariableDeclarationStatement declStmt
&& replacement.contains("$widestType")) { //$NON-NLS-1$
// Handle DECLARATION rewrite with $widestType support.
// When the replacement contains $widestType($var), replace the
// declaration type with the widest type computed by canWidenType guard.
handleDeclarationRewrite(declStmt, replacement, ast, rewrite, cuRewrite, group);
} else if (matchedNode instanceof Statement) {
Block parsed= parseStatements(replacement, cuRewrite);
if (parsed != null) {
ASTNode node= parsed.statements().size() == 1 ? (ASTNode) parsed.statements().getFirst() : parsed;
rewrite.replace(matchedNode, ASTNode.copySubtree(ast, node), group);
}
}
}
/** Rewrites only the matched original list window, preserving its siblings. */
private void rewriteStatementSequence(CompilationUnitRewrite cuRewrite, TextEditGroup group) {
ASTNode anchor= result.match().getMatchedNode();
if (!(anchor instanceof Statement) || !(anchor.getParent() instanceof Block block)) {
return;
}
int start= result.match().getOffset();
int end= start + result.match().getLength();
List<Statement> originals= new java.util.ArrayList<>();
for (Object value : block.statements()) {
Statement statement= (Statement) value;
if (statement.getStartPosition() >= start
&& statement.getStartPosition() + statement.getLength() <= end) {
originals.add(statement);
}
}
if (originals.isEmpty() || originals.getFirst() != anchor
|| originals.getLast().getStartPosition() + originals.getLast().getLength() != end) {
return;
}
Block parsed= parseStatements(shortenFqns(result.replacement(), result, cuRewrite.getImportRewrite()), cuRewrite);
if (parsed == null) {
return;
}
ASTRewrite rewrite= cuRewrite.getASTRewrite();
ListRewrite list= rewrite.getListRewrite(block, Block.STATEMENTS_PROPERTY);
for (Object value : parsed.statements()) {
list.insertBefore(ASTNode.copySubtree(block.getAST(), (ASTNode) value), anchor, group);
}
for (Statement original : originals) {
list.remove(original, group);
}
}
private static Block parseStatements(String source, CompilationUnitRewrite cuRewrite) {
return parseBody(source, cuRewrite);
}
private static Expression parseExpression(String source, CompilationUnitRewrite cuRewrite) {
return parseExpression(source, cuRewrite.getCu().getJavaProject().getOptions(true));
}
private static Expression parseExpression(String source, Map<String, String> options) {
Block body= parseBody("return " + source + ";", options); //$NON-NLS-1$ //$NON-NLS-2$
if (body != null && body.statements().size() == 1
&& body.statements().getFirst() instanceof org.eclipse.jdt.core.dom.ReturnStatement statement) {
return statement.getExpression();
}
return null;
}
private static Block parseBody(String source, CompilationUnitRewrite cuRewrite) {
return parseBody(source, cuRewrite.getCu().getJavaProject().getOptions(true));
}
private static Block parseBody(String source, Map<String, String> options) {
org.eclipse.jdt.core.dom.ASTParser parser= org.eclipse.jdt.core.dom.ASTParser.newParser(AST.getJLSLatest());
parser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_COMPILATION_UNIT);
parser.setSource(("class __Hint { void __hint() {\n" + source + "\n} }").toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$
parser.setCompilerOptions(options);
CompilationUnit parsed= (CompilationUnit) parser.createAST(null);
for (var problem : parsed.getProblems()) {
if (problem.isError()) {
return null;
}
}
boolean[] recovered= { false };
parsed.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public void preVisit(ASTNode node) {
recovered[0] |= (node.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) != 0;
}
});
if (recovered[0] || !parsed.getCommentList().isEmpty() || parsed.types().size() != 1
|| !(parsed.types().getFirst() instanceof org.eclipse.jdt.core.dom.TypeDeclaration type)
|| type.bodyDeclarations().size() != 1 || type.getMethods().length != 1) {
return null;
}
return type.getMethods()[0].getBody();
}
private static boolean hasCommentInRange(CompilationUnit root, int offset, int length) {
return root.getCommentList().stream().anyMatch(value -> {
ASTNode comment= (ASTNode) value;
return comment.getStartPosition() < offset + length
&& comment.getStartPosition() + comment.getLength() > offset;
});
}
/**
* Handles DECLARATION rewrite with {@code $widestType} support.
*
* <p>The widest type FQN was pre-computed by {@link #isNonWidenableDeclaration}
* and stored in the match's extra data under {@code "__widestType__"}.
* This method is only called when widening IS possible, so the FQN is
* guaranteed to be present.</p>
*
* @param declStmt the matched VariableDeclarationStatement
* @param replacement the replacement text
* @param ast the AST factory
* @param rewrite the AST rewriter
* @param cuRewrite the compilation unit rewrite context
* @param group the text edit group
* @since 1.3.12
*/
private void handleDeclarationRewrite(VariableDeclarationStatement declStmt,
String replacement, AST ast, ASTRewrite rewrite,
CompilationUnitRewrite cuRewrite, TextEditGroup group) {
// Check if replacement uses $widestType function
if (replacement == null || !replacement.contains("$widestType")) { //$NON-NLS-1$
return;
}
// Retrieve the widest type FQN pre-computed by isNonWidenableDeclaration
Object widestTypeFqn = result.match().getExtraData("__widestType__"); //$NON-NLS-1$
if (!(widestTypeFqn instanceof String fqn) || fqn.isEmpty()) {
return;
}
// Add import for the FQN if available
String simpleName = cuRewrite.getImportRewrite().addImport(fqn);
// Create the new type node
Type newType;
if (simpleName.contains(".")) { //$NON-NLS-1$
// Qualified name: split and create QualifiedName
newType = ast.newSimpleType(ast.newName(simpleName.split("\\."))); //$NON-NLS-1$
} else {
newType = ast.newSimpleType(ast.newSimpleName(simpleName));
}
// Replace the type in the variable declaration statement
rewrite.replace(declStmt.getType(), newType, group);
}
/**
* Handles METHOD_DECLARATION → METHOD_DECLARATION rewrite using natural syntax.
*
* <p>When the replacement is a method declaration with additional annotations
* (e.g., {@code @Test void $name($params$)}), this method diffs the annotations
* between the source pattern and the replacement pattern, then adds any
* annotations that are in the replacement but not in the source.</p>
*
* <p>This is <b>idempotent</b>: annotations already present on the actual
* matched method are not added again.</p>
*
* <p>This approach is <b>NetBeans-compatible</b>: the replacement is just
* valid Java code (a method declaration with annotations), not a custom directive.</p>
*
* @param matchedNode the matched MethodDeclaration AST node
* @param replacement the replacement text (e.g., {@code "@Test void $name($params$)"})
* @param ast the AST factory
* @param rewrite the AST rewriter
* @param cuRewrite the compilation unit rewrite context
* @param group the text edit group
*/
@SuppressWarnings("unchecked")
private void handleMethodDeclarationRewrite(ASTNode matchedNode, String replacement,
AST ast, ASTRewrite rewrite, CompilationUnitRewrite cuRewrite,
TextEditGroup group) {
if (!(matchedNode instanceof org.eclipse.jdt.core.dom.MethodDeclaration methodDecl)) {
return;
}
// Parse the replacement as a method declaration to extract annotations
org.eclipse.jdt.core.dom.MethodDeclaration replacementMethod =
parseReplacementAsMethodDeclaration(replacement, cuRewrite.getCu().getJavaProject().getOptions(true));
if (replacementMethod == null) {
return;
}
// Preserve parsed qualified identities until rendering each added annotation.
java.util.Set<String> replacementAnnotationNames = new java.util.LinkedHashSet<>();
for (Object modifier : replacementMethod.modifiers()) {
if (modifier instanceof Annotation ann) {
replacementAnnotationNames.add(ann.getTypeName().getFullyQualifiedName());
}
}
// Diff against the declared source pattern, retaining distinct qualified types.
String sourcePatternText = result.rule() != null
? result.rule().sourcePattern().getValue() : ""; //$NON-NLS-1$
java.util.Set<String> sourceAnnotationNames = new java.util.LinkedHashSet<>();
org.eclipse.jdt.core.dom.MethodDeclaration sourceMethod =
parseReplacementAsMethodDeclaration(sourcePatternText, cuRewrite.getCu().getJavaProject().getOptions(true));
if (sourceMethod != null) {
for (Object modifier : sourceMethod.modifiers()) {
if (modifier instanceof Annotation ann) {
sourceAnnotationNames.add(ann.getTypeName().getFullyQualifiedName());
}
}
}
// Annotations to add = in replacement but not in source pattern
java.util.Set<String> annotationsToAdd = new java.util.LinkedHashSet<>(replacementAnnotationNames);
annotationsToAdd.removeAll(sourceAnnotationNames);
if (annotationsToAdd.isEmpty()) {
return;
}
// Compare resolved identities so different types sharing a simple name remain distinct.
java.util.Set<String> existingAnnotationTypes = new java.util.LinkedHashSet<>();
for (Object modifier : methodDecl.modifiers()) {
if (modifier instanceof Annotation ann) {
existingAnnotationTypes.add(ann.getTypeName().getFullyQualifiedName());
var binding= ann.resolveTypeBinding();
if (binding != null && !binding.isRecovered()) {
existingAnnotationTypes.add(binding.getQualifiedName());
}
}
}
// Add each missing annotation (idempotent: skip if already present)
ListRewrite modifiersRewrite = rewrite.getListRewrite(
methodDecl, org.eclipse.jdt.core.dom.MethodDeclaration.MODIFIERS2_PROPERTY);
ImportRewrite importRewrite = cuRewrite.getImportRewrite();
var importContext= new ContextSensitiveImportRewriteContext(methodDecl, importRewrite);
for (String annotationName : annotationsToAdd) {
String targetType= annotationTypes.get(annotationName);
if (existingAnnotationTypes.contains(targetType)) {
continue; // Already present on the actual method — skip
}
// Use createStringPlaceholder so the annotation appears on its own
// line above the method declaration (proper formatting).
ASTNode newAnnotation = rewrite.createStringPlaceholder(
"@" + importRewrite.addImport(targetType, importContext) + "\n", ASTNode.MARKER_ANNOTATION); //$NON-NLS-1$ //$NON-NLS-2$
modifiersRewrite.insertFirst(newAnnotation, group);
}
}
/** Resolves unambiguous annotation names while retaining secondary and member type bindings. */
private static String resolveAnnotationType(org.eclipse.jdt.core.dom.MethodDeclaration method, String name) {
var binding= method.resolveBinding();
if (binding == null || binding.isRecovered() || binding.getDeclaringClass() == null
|| binding.getDeclaringClass().isRecovered()) {
return null;
}
boolean qualified= name.indexOf('.') >= 0;
IType contextType= binding.getDeclaringClass().getJavaElement() instanceof IType type ? type : null;
try {
String[][] matches= null;
String resolvedType= name;
if (!qualified) {
if (contextType == null) {
return null;
}
// ScopeAnalyzer hides same-name types; require unique Java-model resolution first.
matches= contextType.resolveType(name);
if (matches == null || matches.length != 1) {
return null;
}
resolvedType= matches[0][0].isEmpty() ? matches[0][1] : matches[0][0] + '.' + matches[0][1];
}
var analyzer= new ScopeAnalyzer((CompilationUnit) method.getRoot());
for (var declaration : analyzer.getDeclarationsInScope(method.getStartPosition(),
ScopeAnalyzer.TYPES | ScopeAnalyzer.CHECK_VISIBILITY)) {
if (declaration instanceof ITypeBinding type && resolvedType.equals(type.getQualifiedName())) {
return !type.isRecovered() && type.isAnnotation() ? resolvedType : null;
}
}
if (contextType == null) {
return null;
}
IType annotation;
if (qualified) {
annotation= contextType.getJavaProject().findType(name, (IProgressMonitor) null);
} else {
// The monitor overload also includes secondary source types in the lookup.
annotation= contextType.getJavaProject().findType(matches[0][0], matches[0][1], (IProgressMonitor) null);
}
if (annotation == null || !annotation.isAnnotation()) {
return null;
}
// Model lookup alone does not establish access to the type or its enclosing types.
for (IType enclosing= annotation; enclosing != null; enclosing= enclosing.getDeclaringType()) {
if (!JavaModelUtil.isVisible(enclosing, contextType.getPackageFragment())) {
return null;
}
}
return annotation.getFullyQualifiedName('.');
} catch (JavaModelException exception) {
return null;
}
}
/**
* Parses a string as a method declaration by wrapping it in a class context.
*
* @param methodSnippet the method snippet (e.g., {@code "@Test void $name($params$)"})
* @param options compiler options of the target project
* @return the parsed MethodDeclaration, or {@code null} if parsing fails
*/
private static org.eclipse.jdt.core.dom.MethodDeclaration parseReplacementAsMethodDeclaration(
String methodSnippet, Map<String, String> options) {
if (methodSnippet == null || methodSnippet.isBlank()) {
return null;
}
String normalized = methodSnippet.trim();
// Add empty body if not present
if (!normalized.endsWith("}") && !normalized.endsWith(";")) { //$NON-NLS-1$ //$NON-NLS-2$
normalized = normalized + " {}"; //$NON-NLS-1$
}
// Handle multi-placeholder parameters for valid Java syntax
normalized = normalized.replaceAll(
"\\(\\s*\\$([a-zA-Z_][a-zA-Z0-9_]*)\\$\\s*\\)", //$NON-NLS-1$
"(Object... \\$$1\\$)"); //$NON-NLS-1$
// Validate the whole normalized declaration before extracting annotations.
String prefix= "class _Pattern { "; //$NON-NLS-1$
String source = prefix + normalized + " }"; //$NON-NLS-1$
org.eclipse.jdt.core.dom.ASTParser parser = org.eclipse.jdt.core.dom.ASTParser
.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(org.eclipse.jdt.core.dom.ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(options);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
for (var problem : cu.getProblems()) {
if (problem.isError()) {
return null;
}
}
boolean[] recovered= { false };
cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public void preVisit(ASTNode node) {
recovered[0] |= (node.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) != 0;
}
});
if (recovered[0] || cu.types().size() != 1
|| !(cu.types().getFirst() instanceof org.eclipse.jdt.core.dom.TypeDeclaration type)
|| type.bodyDeclarations().size() != 1 || type.getMethods().length != 1) {
return null;
}
org.eclipse.jdt.core.dom.MethodDeclaration method= type.getMethods()[0];
return method.getStartPosition() == prefix.length() && method.getLength() == normalized.length() ? method : null;
}
/**
* Shortens fully qualified Java names without changing literals or comments.
*
* <p>Uses the already-inferred {@link ImportDirective#getAddImports()} to know
* which FQNs to shorten. For example, {@code java.nio.charset.StandardCharsets.UTF_8}
* becomes {@code StandardCharsets.UTF_8} since the import for
* {@code java.nio.charset.StandardCharsets} is already being added.</p>
*
* <p>FQNs are processed longest-first to avoid partial-match issues.</p>
*
* @param replacement the raw replacement text containing FQNs
* @param result the transformation result with import directives
* @param imports import rewriter that decides whether a name must stay qualified
* @return the replacement text using the import rewriter's rendering names
*/
private static String shortenFqns(String replacement, TransformationResult result, ImportRewrite imports) {
if (!result.hasImportDirective()) {
return replacement;
}
List<String> addImports = result.importDirective().getAddImports();
if (addImports.isEmpty()) {
return replacement;
}
ASTNode original= result.match().getMatchedNode();
CompilationUnit root= (CompilationUnit) original.getRoot();
Map<String, String> options= root.getJavaElement() == null
? org.sandbox.jdt.triggerpattern.api.SemanticRewritePlanContext.currentCompilerOptions()
: root.getJavaElement().getJavaProject().getOptions(true);
ASTNode parsed= original instanceof Expression
? parseExpression(replacement, options) : parseBody(replacement, options);
if (parsed == null) {
return replacement;
}
java.util.Set<String> declarations= new java.util.HashSet<>();
parsed.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(org.eclipse.jdt.core.dom.SimpleName name) {
if (name.isDeclaration()) {
declarations.add(name.getIdentifier());
}
return true;
}
});
// Sort by length (longest first) to avoid partial replacement issues
List<String> sorted = new java.util.ArrayList<>(addImports);
sorted.sort((a, b) -> Integer.compare(b.length(), a.length()));
String shortened = replacement;
var context= new ContextSensitiveImportRewriteContext(result.match().getMatchedNode(), imports);
for (String fqn : sorted) {
String renderedName= imports.addImport(fqn, context);
int dot= renderedName.indexOf('.');
String firstName= dot < 0 ? renderedName : renderedName.substring(0, dot);
if (declarations.contains(firstName)) {
// Original scope analysis cannot see names introduced by this replacement.
renderedName= fqn;
}
java.util.regex.Matcher names= java.util.regex.Pattern.compile(
"(?<![\\p{javaJavaIdentifierPart}.])" + java.util.regex.Pattern.quote(fqn) //$NON-NLS-1$
+ "(?![\\p{javaJavaIdentifierPart}])") //$NON-NLS-1$
.matcher(JavaSourceMask.codeOnly(shortened));
StringBuilder rewritten= new StringBuilder();
int copiedUntil= 0;
while (names.find()) {
rewritten.append(shortened, copiedUntil, names.start()).append(renderedName);
copiedUntil= names.end();
}
shortened= rewritten.append(shortened, copiedUntil, shortened.length()).toString();
}
return shortened;
}
/**
* Counts the number of {@link StringLiteral} nodes in the given AST subtree.
*
* <p>This is used to determine whether {@code replaceAndRemoveNLS} should be
* used instead of a plain {@code rewrite.replace()}. When a DSL rule replaces
* a string literal with a non-string expression (e.g., {@code "UTF-8"} →
* {@code StandardCharsets.UTF_8}), the count decreases and the associated
* {@code //$NON-NLS-n$} comment must be removed. When the count stays the
* same or increases (e.g., zero-arg expansion adding a new argument),
* existing NLS comments must be preserved.</p>
*
* @param node the AST node to inspect
* @return the number of {@code StringLiteral} nodes found
*/
private static int countStringLiterals(ASTNode node) {
int[] count = { 0 };
node.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
@Override
public boolean visit(StringLiteral literal) {
count[0]++;
return false;
}
});
return count[0];
}
/**
* Extracts the embedded fix function name from a replacement text ({@code <?fixName?>}).
*
* <p>A valid embedded fix reference has the form {@code <?identifier?>} where
* the identifier has no leading/trailing whitespace and is a valid Java identifier.</p>
*
* @return the fix function name, or {@code null} if the replacement is not a valid fix reference
*/
private static String extractEmbeddedFixName(String replacement) {
if (replacement == null || !replacement.startsWith("<?") || !replacement.endsWith("?>")) { //$NON-NLS-1$ //$NON-NLS-2$
return null;
}
String inner = replacement.substring(2, replacement.length() - 2);
String trimmed = inner.trim();
if (trimmed.isEmpty() || !inner.equals(trimmed)) {
return null;
}
for (int i = 0; i < trimmed.length(); i++) {
char ch = trimmed.charAt(i);
if (i == 0) {
if (!Character.isJavaIdentifierStart(ch)) {
return null;
}
} else if (!Character.isJavaIdentifierPart(ch)) {
return null;
}
}
return trimmed;
}
/**
* Checks whether the given statement is directly inside the body of a try statement
* that will be fully unwrapped after removing the target exception. This happens when:
* <ul>
* <li>The statement is in the try body (not a catch/finally block)</li>
* <li>The try has no resources (not try-with-resources)</li>
* <li>The try has no finally block</li>
* <li>The try has exactly one catch clause catching only the target exception</li>
* </ul>
*/
private static boolean isInsideTryBodyWithOnlyTargetExceptionCatch(ASTNode statement, String exceptionSimple) {
ASTNode parent = statement.getParent();
if (!(parent instanceof Block block)) {
return false;
}
ASTNode grandParent = block.getParent();
if (!(grandParent instanceof TryStatement tryStatement)) {
return false;
}
if (tryStatement.getBody() != block) {
return false;
}
if (!tryStatement.resources().isEmpty()) {
return false;
}
if (tryStatement.getFinally() != null) {
return false;
}
@SuppressWarnings("unchecked")
List<CatchClause> catchClauses = tryStatement.catchClauses();
if (catchClauses.size() != 1) {
return false;
}
CatchClause catchClause = catchClauses.get(0);
Type exType = catchClause.getException().getType();
if (exType instanceof SimpleType simpleType) {
return exceptionSimple.equals(simpleType.getName().toString());
}
return false;
}
/**
* If the matched node is inside a try body that will be unwrapped after exception
* removal, registers the replacement before simplifying the proven obsolete handler.
* The shared helper retains pending statement edits during unwrapping.
*
* @return {@code true} if the combined operation was performed (caller should skip
* separate replacement and exception removal), {@code false} if not applicable
*/
private static boolean replaceAndUnwrapTryIfNeeded(
ASTNode matchedNode, ASTNode copy, String shortenedReplacement,
TypeChangeInfo typeChange, ASTRewrite rewrite,
CompilationUnitRewrite cuRewrite, TextEditGroup group) {
ASTNode st = ASTNodes.getFirstAncestorOrNull(matchedNode, Statement.class);
if (st == null || !isInsideTryBodyWithOnlyTargetExceptionCatch(st, typeChange.exceptionSimpleName())) {
return false;
}
Block block = (Block) st.getParent();
TryStatement tryStatement = (TryStatement) block.getParent();
ASTNode tryParent = tryStatement.getParent();
if (!(tryParent instanceof Block)) {
return false;
}
if (!ExceptionCleanupHelper.canRemoveException(tryStatement, matchedNode,
typeChange.exceptionFQN(), rewrite)) {
return false;
}
try {
ASTNodes.replaceAndRemoveNLS(rewrite, matchedNode, copy, group, cuRewrite);
} catch (CoreException exception) {
rewrite.replace(matchedNode, copy, group);
}
ExceptionCleanupHelper.removeCheckedException(matchedNode, typeChange.exceptionFQN(),
typeChange.exceptionSimpleName(), group, rewrite, cuRewrite.getImportRemover());
return true;
}
}
}