CheckedExceptionMigration.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer and others.
*
* 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
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.cleanup;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotatableType;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.CatchClause;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ConstructorInvocation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.InstanceofExpression;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.MethodReference;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.SuperConstructorInvocation;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
import org.eclipse.jdt.core.dom.ThrowStatement;
import org.eclipse.jdt.core.dom.TryStatement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.TextElement;
import org.eclipse.jdt.core.dom.UnionType;
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.ListRewrite;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
/**
* Plans a newly required checked exception through handlers and source-owned
* callable contracts. The plan retains binding keys/source identities, not ASTs.
* No source is changed during planning. Unresolved or out-of-scope contracts are
* reported rather than replaced by an unchecked wrapper or a String overload.
*/
public final class CheckedExceptionMigration {
private CheckedExceptionMigration() { }
private static final class AppliedChanges {
final Set<String> targets= new HashSet<>();
}
private record UnitPlan(String source, Set<String> methods, Set<Integer> catches) {
UnitPlan {
methods= Set.copyOf(methods);
catches= Set.copyOf(catches);
}
}
/** Immutable local parts of one checked-exception propagation. */
public static final class Plan {
private final Map<String, UnitPlan> units;
private final String addedType;
private final String oldHandlerType;
private final Set<String> requiredHandles;
private final List<String> conflicts;
private final Set<String> affected;
private Plan(Map<String, UnitPlan> units, String addedType, String oldHandlerType,
Set<String> requiredHandles, List<String> conflicts, Set<String> affected) {
this.units= Map.copyOf(units);
this.addedType= addedType;
this.oldHandlerType= oldHandlerType;
this.requiredHandles= Set.copyOf(requiredHandles);
this.conflicts= List.copyOf(conflicts);
this.affected= Set.copyOf(affected);
}
public Set<String> affectedCompilationUnits() { return affected; }
public Set<String> requiredCompilationUnits() { return requiredHandles; }
public List<String> conflicts() { return conflicts; }
public boolean hasChanges(String handle) {
UnitPlan unit= units.get(handle);
return unit != null && (!unit.methods().isEmpty() || !unit.catches().isEmpty());
}
public void requireComplete() throws CoreException {
if (!conflicts.isEmpty() || !requiredHandles.isEmpty()) {
throw new CoreException(Status.error("Cannot complete checked-exception migration: " //$NON-NLS-1$
+ String.join("; ", conflicts) //$NON-NLS-1$
+ (requiredHandles.isEmpty() ? "" : "; additional source units required: " + requiredHandles))); //$NON-NLS-1$ //$NON-NLS-2$
}
}
/** Apply after expression rewrites and removal of obsolete exception handlers. */
public void apply(CompilationUnitRewrite cuRewrite, TextEditGroup group) throws CoreException {
requireComplete();
CompilationUnit root= cuRewrite.getRoot();
ICompilationUnit unit= (ICompilationUnit) root.getJavaElement();
UnitPlan local= units.get(unit.getHandleIdentifier());
if (local == null) {
throw new CoreException(Status.error("Compilation unit is outside the planned source scope")); //$NON-NLS-1$
}
if (!local.source().equals(unit.getSource())) {
throw new CoreException(Status.error("Source changed after checked-exception planning: " + unit.getElementName())); //$NON-NLS-1$
}
if (!hasChanges(unit.getHandleIdentifier())) return;
ASTRewrite rewrite= cuRewrite.getASTRewrite();
String property= CheckedExceptionMigration.class.getName();
if (!(rewrite.getProperty(property) instanceof AppliedChanges)) rewrite.setProperty(property, new AppliedChanges());
AppliedChanges applied= (AppliedChanges) rewrite.getProperty(property);
Set<String> foundMethods= new HashSet<>();
Set<Integer> foundCatches= new HashSet<>();
root.accept(new ASTVisitor() {
@Override public boolean visit(MethodDeclaration method) {
IMethodBinding binding= method.resolveBinding();
if (binding != null && local.methods().contains(key(binding))) {
foundMethods.add(key(binding));
if (!applied.targets.add(addedType + ":method:" + key(binding))) return true; //$NON-NLS-1$
ListRewrite list= rewrite.getListRewrite(method, MethodDeclaration.THROWN_EXCEPTION_TYPES_PROPERTY);
boolean covered= false;
boolean documented= false;
for (Object item : new ArrayList<>(list.getRewrittenList())) {
Type type= (Type) item;
ITypeBinding declared= type.resolveBinding();
if (declared == null) continue;
if (addedType.equals(declared.getQualifiedName())) covered= true;
else if (isSubtype(declared, addedType)) {
list.remove(type, group);
cuRewrite.getImportRemover().registerRemovedNode(type);
if (!documented) documented= ExceptionCleanupHelper.updateThrowsJavadoc(method,
declared.getQualifiedName(), cuRewrite.getImportRewrite().addImport(addedType),
rewrite, group, cuRewrite.getImportRemover());
}
}
if (!covered) {
list.insertLast(newType(cuRewrite), group);
if (!documented && method.getJavadoc() != null) {
TagElement tag= root.getAST().newTagElement();
tag.setTagName(TagElement.TAG_THROWS);
tag.fragments().add(root.getAST().newName(cuRewrite.getImportRewrite().addImport(addedType)));
TextElement description= root.getAST().newTextElement();
description.setText(" if the migrated operation fails"); //$NON-NLS-1$
tag.fragments().add(description);
rewrite.getListRewrite(method.getJavadoc(), Javadoc.TAGS_PROPERTY).insertLast(tag, group);
}
}
}
return true;
}
@Override public boolean visit(CatchClause clause) {
if (!local.catches().contains(clause.getStartPosition())) return true;
foundCatches.add(clause.getStartPosition());
if (!applied.targets.add(addedType + ":catch:" + clause.getStartPosition())) return true; //$NON-NLS-1$
if (!rewrite.getListRewrite(clause.getParent(), TryStatement.CATCH_CLAUSES_PROPERTY)
.getRewrittenList().contains(clause)) return true;
SingleVariableDeclaration parameter= clause.getException();
Type effective= (Type) rewrite.get(parameter, SingleVariableDeclaration.TYPE_PROPERTY);
List<?> alternatives= effective instanceof UnionType union
? rewrite.getListRewrite(union, UnionType.TYPES_PROPERTY).getRewrittenList()
: List.of(effective);
List<Type> updated= new ArrayList<>();
for (Object value : alternatives) {
Type type= (Type) value;
ITypeBinding binding= type.resolveBinding();
if (binding != null && oldHandlerType.equals(binding.getQualifiedName())) {
Type replacement= newType(cuRewrite);
if (type instanceof AnnotatableType old && replacement instanceof AnnotatableType target)
target.annotations().addAll(ASTNode.copySubtrees(root.getAST(), old.annotations()));
updated.add(replacement);
cuRewrite.getImportRemover().registerRemovedNode(type);
} else if (isSubtype(binding, addedType)) {
cuRewrite.getImportRemover().registerRemovedNode(type);
} else updated.add((Type) ASTNode.copySubtree(root.getAST(), type));
}
Type replacement;
if (updated.size() == 1) replacement= updated.get(0);
else {
UnionType union= root.getAST().newUnionType();
union.types().addAll(updated);
replacement= union;
}
rewrite.set(parameter, SingleVariableDeclaration.TYPE_PROPERTY, replacement, group);
return true;
}
});
if (!foundMethods.equals(local.methods()) || !foundCatches.equals(local.catches())) {
throw new CoreException(Status.error("A checked-exception plan target could not be resolved")); //$NON-NLS-1$
}
}
private Type newType(CompilationUnitRewrite rewrite) {
AST ast= rewrite.getRoot().getAST();
return ast.newSimpleType(ast.newName(rewrite.getImportRewrite().addImport(addedType)));
}
}
/**
* Plan one additional checked type for the supplied changed invocations.
* Only the specified former handler type may be widened, and only when its
* parameter uses remain valid without selecting different narrow overloads.
*/
public static Plan plan(Collection<CompilationUnit> scope, Collection<? extends ASTNode> changes,
ITypeBinding additionalException, String oldHandlerType) throws CoreException {
return plan(scope, changes, additionalException, oldHandlerType, new NullProgressMonitor());
}
public static Plan plan(Collection<CompilationUnit> scope, Collection<? extends ASTNode> changes,
ITypeBinding additionalException, String oldHandlerType, IProgressMonitor monitor) throws CoreException {
Analysis analysis= new Analysis(scope, changes, additionalException, oldHandlerType, monitor);
for (ASTNode changed : changes) {
analysis.affected.add(((ICompilationUnit) ((CompilationUnit) changed.getRoot()).getJavaElement()).getHandleIdentifier());
analysis.requireHandling(changed);
}
analysis.findOutsideReferences();
return analysis.finish();
}
private static final class Analysis {
private final List<CompilationUnit> roots;
private final ITypeBinding added;
private final String oldHandler;
private final Set<String> addedSupertypes;
private final Map<String, MethodDeclaration> declarations= new LinkedHashMap<>();
private final Map<String, List<ASTNode>> references= new HashMap<>();
private final Set<MethodDeclaration> methods= new LinkedHashSet<>();
private final Set<CatchClause> catches= new LinkedHashSet<>();
private final Set<String> required= new LinkedHashSet<>();
private final Set<String> projects= new HashSet<>();
private final List<String> conflicts= new ArrayList<>();
private final Set<String> affected= new LinkedHashSet<>();
private final IProgressMonitor monitor;
private final Collection<? extends ASTNode> changedCalls;
Analysis(Collection<CompilationUnit> scope, Collection<? extends ASTNode> changes, ITypeBinding added, String oldHandler, IProgressMonitor monitor) {
this.changedCalls= List.copyOf(changes);
this.monitor= monitor == null ? new NullProgressMonitor() : monitor;
this.roots= List.copyOf(scope);
this.added= added;
this.oldHandler= oldHandler;
this.addedSupertypes= supertypes(added);
for (CompilationUnit root : roots) {
if (root.getJavaElement() instanceof ICompilationUnit unit) projects.add(unit.getJavaProject().getHandleIdentifier());
root.accept(new ASTVisitor() {
@Override public boolean visit(MethodDeclaration node) {
IMethodBinding method= node.resolveBinding();
if (method != null && !method.isRecovered()) declarations.put(key(method), node);
return true;
}
@Override public void preVisit(ASTNode node) {
IMethodBinding method= invocationBinding(node);
if (method != null && !method.isRecovered()) references.computeIfAbsent(key(method), k -> new ArrayList<>()).add(node);
}
});
}
}
private boolean covered(ITypeBinding type) {
return type != null && !type.isRecovered() && !type.isTypeVariable()
&& addedSupertypes.contains(type.getErasure().getQualifiedName());
}
void requireHandling(ASTNode invocation) {
if (monitor.isCanceled()) throw new OperationCanceledException();
if (invocation instanceof MethodReference reference) {
ITypeBinding functionalType= reference.resolveTypeBinding();
requireContract(functionalType == null ? null : functionalType.getFunctionalInterfaceMethod());
return;
}
ASTNode child= invocation;
for (ASTNode parent= invocation.getParent(); parent != null; child= parent, parent= parent.getParent()) {
if (parent instanceof TryStatement statement
&& (child == statement.getBody() || statement.resources().contains(child))) {
List<CatchClause> clauses= statement.catchClauses();
for (CatchClause clause : clauses) {
if (catches.contains(clause) || types(clause).stream().anyMatch(t -> covered(t.resolveBinding()))) return;
}
List<CatchClause> narrow= clauses.stream()
.filter(c -> types(c).stream().anyMatch(t -> isSubtype(t.resolveBinding(), added.getQualifiedName())))
.toList();
if (narrow.size() == 1) {
CatchClause clause= narrow.get(0);
if (types(clause).stream().anyMatch(t -> t.resolveBinding() != null
&& oldHandler.equals(t.resolveBinding().getQualifiedName())) && canWiden(clause)
&& CheckedExceptionAnalysis.canWidenCatch(statement, clause, changedCalls, added)) {
catches.add(clause);
// Precise rethrow must be propagated beyond, not back into, this try.
clause.getBody().accept(new ASTVisitor() {
@Override public boolean visit(ThrowStatement node) {
Expression expression= node.getExpression();
while (expression instanceof ParenthesizedExpression nested) expression= nested.getExpression();
if (expression instanceof Name name && sameVariable(name.resolveBinding(), clause.getException().resolveBinding()))
requireHandling(node);
return true;
}
});
return;
}
}
}
if (parent instanceof LambdaExpression lambda) {
requireContract(lambda.resolveMethodBinding());
return;
}
if (parent instanceof MethodDeclaration method) {
requireContract(method.resolveBinding());
return;
}
if (parent instanceof Initializer || parent instanceof FieldDeclaration) {
// A caught exception returns above. An uncaught instance initializer
// belongs to every constructor, never to an enclosing method.
if (Modifier.isStatic(((BodyDeclaration) parent).getModifiers())) {
conflicts.add("A static initializer has no checked-exception contract"); //$NON-NLS-1$
return;
}
ASTNode owner= parent.getParent();
ITypeBinding type= owner instanceof AbstractTypeDeclaration declaration ? declaration.resolveBinding()
: owner instanceof AnonymousClassDeclaration anonymous ? anonymous.resolveBinding() : null;
if (type == null || type.isAnonymous()) {
conflicts.add("Cannot update an implicit/anonymous constructor contract"); //$NON-NLS-1$
return;
}
for (IMethodBinding method : type.getDeclaredMethods()) if (method.isConstructor()) requireContract(method);
return;
}
}
conflicts.add("No callable contract for changed invocation at " + invocation.getStartPosition()); //$NON-NLS-1$
}
private void requireContract(IMethodBinding binding) {
if (binding == null || binding.isRecovered()) {
conflicts.add("Unresolved callable contract"); //$NON-NLS-1$
return;
}
for (ITypeBinding exception : binding.getExceptionTypes()) if (covered(exception)) return;
MethodDeclaration declaration= declarations.get(key(binding));
if (declaration == null) {
IJavaElement element= binding.getMethodDeclaration().getJavaElement();
ICompilationUnit unit= element == null ? null : (ICompilationUnit) element.getAncestor(IJavaElement.COMPILATION_UNIT);
if (unit != null && projects.contains(unit.getJavaProject().getHandleIdentifier())) required.add(unit.getHandleIdentifier());
else if (unit != null) conflicts.add("Callable contract outside selected Java project scope: " + unit.getHandleIdentifier()); //$NON-NLS-1$
else conflicts.add("Fixed external contract: " + binding.getDeclaringClass().getQualifiedName() + '.' + binding.getName()); //$NON-NLS-1$
return;
}
if (!methods.add(declaration)) return;
// All inherited contracts constrain an override, not just the first one.
Set<String> seen= new HashSet<>();
List<ITypeBinding> hierarchy= new ArrayList<>();
ITypeBinding owner= binding.getDeclaringClass();
if (owner.getSuperclass() != null) hierarchy.add(owner.getSuperclass());
hierarchy.addAll(List.of(owner.getInterfaces()));
for (int i= 0; i < hierarchy.size(); i++) {
ITypeBinding type= hierarchy.get(i);
if (!seen.add(type.getKey())) continue;
for (IMethodBinding inherited : type.getDeclaredMethods()) if (binding.overrides(inherited)) requireContract(inherited);
if (type.getSuperclass() != null) hierarchy.add(type.getSuperclass());
hierarchy.addAll(List.of(type.getInterfaces()));
}
for (ASTNode reference : references.getOrDefault(key(binding), List.of())) requireHandling(reference);
}
private boolean canWiden(CatchClause clause) {
IVariableBinding variable= clause.getException().resolveBinding();
if (variable == null) return false;
boolean[] safe= { true };
clause.getBody().accept(new ASTVisitor() {
@Override public boolean visit(SimpleName node) {
if (!sameVariable(node.resolveBinding(), variable)) return true;
ASTNode value= node;
while (value.getParent() instanceof ParenthesizedExpression) value= value.getParent();
ASTNode parent= value.getParent();
boolean accepted= false;
if (parent instanceof MethodInvocation call) {
IMethodBinding method= call.resolveMethodBinding();
if (method != null && method.getTypeArguments().length == 0) {
if (call.getExpression() == value) accepted= addedSupertypes.contains(method.getDeclaringClass().getErasure().getQualifiedName())
&& !"getClass".equals(method.getName()); //$NON-NLS-1$
else accepted= acceptsArgument(method, call.arguments().indexOf(value));
}
} else if (parent instanceof ClassInstanceCreation call) {
accepted= acceptsArgument(call.resolveConstructorBinding(), call.arguments().indexOf(value));
} else if (parent instanceof ThrowStatement) accepted= true;
else if (parent instanceof ReturnStatement) {
ASTNode enclosing= parent.getParent();
while (enclosing != null && !(enclosing instanceof MethodDeclaration) && !(enclosing instanceof LambdaExpression)) enclosing= enclosing.getParent();
IMethodBinding method= enclosing instanceof MethodDeclaration m ? m.resolveBinding()
: enclosing instanceof LambdaExpression l ? l.resolveMethodBinding() : null;
accepted= method != null && covered(method.getReturnType());
} else if (parent instanceof Assignment assignment && assignment.getRightHandSide() == value) {
accepted= covered(assignment.getLeftHandSide().resolveTypeBinding());
} else if (parent instanceof VariableDeclarationFragment fragment && fragment.getInitializer() == value) {
IVariableBinding target= fragment.resolveBinding();
ASTNode declaration= fragment.getParent();
boolean inferred= declaration instanceof VariableDeclarationStatement statement && statement.getType().isVar();
accepted= !inferred && target != null && covered(target.getType());
} else if (parent instanceof InstanceofExpression || parent instanceof InfixExpression) {
accepted= true;
}
if (!accepted) safe[0]= false;
return true;
}
});
return safe[0];
}
private boolean acceptsArgument(IMethodBinding method, int index) {
if (method == null || method.isRecovered() || index < 0 || method.getTypeArguments().length != 0) return false;
ITypeBinding[] parameters= method.getParameterTypes();
if (index >= parameters.length) return false;
// Do not infer a new varargs/overload selection while changing a handler.
return !method.isVarargs() && covered(parameters[index]);
}
void findOutsideReferences() throws CoreException {
Set<String> selected= new HashSet<>();
// Search workspace-wide for safety; only projects represented by the planned roots may expand.
List<ICompilationUnit> working= new ArrayList<>();
for (CompilationUnit root : roots) if (root.getJavaElement() instanceof ICompilationUnit unit) {
selected.add(unit.getHandleIdentifier()); working.add(unit);
}
SearchEngine engine= new SearchEngine(working.toArray(ICompilationUnit[]::new));
for (MethodDeclaration declaration : methods) {
IMethodBinding binding= declaration.resolveBinding();
if (!(binding.getJavaElement() instanceof IMethod method)) {
conflicts.add("Source method has no Java model handle: " + binding.getName()); //$NON-NLS-1$
continue;
}
SearchPattern pattern= SearchPattern.createPattern(method, IJavaSearchConstants.REFERENCES,
SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_ERASURE_MATCH);
if (pattern == null) { conflicts.add("Cannot search callers of " + method.getElementName()); continue; } //$NON-NLS-1$
engine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() },
SearchEngine.createWorkspaceScope(), new SearchRequestor() {
@Override public void acceptSearchMatch(SearchMatch match) {
if (match.isInsideDocComment()) return;
if (match.getAccuracy() != SearchMatch.A_ACCURATE) {
conflicts.add("Unresolved caller of " + method.getElementName()); return; //$NON-NLS-1$
}
IJavaElement element= match.getElement() instanceof IJavaElement e ? e : null;
ICompilationUnit unit= element == null ? null : (ICompilationUnit) element.getAncestor(IJavaElement.COMPILATION_UNIT);
if (unit != null && !selected.contains(unit.getHandleIdentifier())) {
if (projects.contains(unit.getJavaProject().getHandleIdentifier())) required.add(unit.getHandleIdentifier());
else conflicts.add("Caller outside selected Java project scope: " + unit.getHandleIdentifier()); //$NON-NLS-1$
}
}
}, monitor);
}
}
Plan finish() throws CoreException {
Map<String, UnitPlan> result= new LinkedHashMap<>();
for (CompilationUnit root : roots) {
if (!(root.getJavaElement() instanceof ICompilationUnit unit)) continue;
Set<String> methodKeys= new LinkedHashSet<>();
Set<Integer> catchOffsets= new LinkedHashSet<>();
for (MethodDeclaration method : methods) if (method.getRoot() == root) methodKeys.add(key(method.resolveBinding()));
for (CatchClause clause : catches) if (clause.getRoot() == root) catchOffsets.add(clause.getStartPosition());
if (!methodKeys.isEmpty() || !catchOffsets.isEmpty()) affected.add(unit.getHandleIdentifier());
result.put(unit.getHandleIdentifier(), new UnitPlan(unit.getSource(), methodKeys, catchOffsets));
}
return new Plan(result, added.getQualifiedName(), oldHandler, required, conflicts, affected);
}
}
private static boolean sameVariable(IBinding first, IVariableBinding second) {
return first instanceof IVariableBinding variable && second != null
&& variable.getVariableDeclaration().isEqualTo(second.getVariableDeclaration());
}
private static String key(IMethodBinding method) { return method.getMethodDeclaration().getKey(); }
private static List<Type> types(CatchClause clause) {
Type type= clause.getException().getType();
return type instanceof UnionType union ? union.types() : List.of(type);
}
private static boolean isSubtype(ITypeBinding type, String parent) {
return type != null && !type.isRecovered() && !type.isTypeVariable() && supertypes(type).contains(parent);
}
private static Set<String> supertypes(ITypeBinding type) {
Set<String> names= new HashSet<>();
List<ITypeBinding> pending= new ArrayList<>();
pending.add(type);
for (int i= 0; i < pending.size(); i++) {
ITypeBinding next= pending.get(i);
if (next == null || next.isRecovered() || !names.add(next.getErasure().getQualifiedName())) continue;
if (next.getSuperclass() != null) pending.add(next.getSuperclass());
pending.addAll(List.of(next.getInterfaces()));
}
return names;
}
private static IMethodBinding invocationBinding(ASTNode node) {
if (node instanceof MethodInvocation call) return call.resolveMethodBinding();
if (node instanceof SuperMethodInvocation call) return call.resolveMethodBinding();
if (node instanceof ClassInstanceCreation call) return call.resolveConstructorBinding();
if (node instanceof ConstructorInvocation call) return call.resolveConstructorBinding();
if (node instanceof SuperConstructorInvocation call) return call.resolveConstructorBinding();
if (node instanceof MethodReference reference) return reference.resolveMethodBinding();
return null;
}
}