StatusRewriteSupport.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.internal.corext.fix.helper;

import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
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.Javadoc;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.SimpleName;
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.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;

/** Target-API and binding housekeeping for the actual Status rewrite only. */
final class StatusRewriteSupport {
	private static final String STATE_KEY= StatusRewriteSupport.class.getName();

	private StatusRewriteSupport() {
	}

	/** Do not drop evaluation or orphan a documented private constant. */
	static boolean canDiscard(Expression expression) {
		if (!ASTNodes.isPassive(expression)) {
			return false;
		}
		boolean[] documented= { false };
		expression.accept(new ASTVisitor(true) {
			@Override
			public boolean visit(SimpleName name) {
				if (name.resolveBinding() instanceof IVariableBinding variable
						&& variable.isField() && Modifier.isPrivate(variable.getModifiers())
						&& variable.getConstantValue() != null) {
					expression.getRoot().accept(new ASTVisitor(true) {
						@Override
						public boolean visit(SimpleName reference) {
							if (variable.isEqualTo(reference.resolveBinding())
									&& ASTNodes.getFirstAncestorOrNull(reference, Javadoc.class) != null) {
								documented[0]= true;
							}
							return !documented[0];
						}
					});
				}
				return !documented[0];
			}
		});
		return !documented[0];
	}

	static boolean hasPreservingConstructor(ClassInstanceCreation creation) {
		IMethodBinding original= creation.resolveConstructorBinding();
		if (original == null || original.isRecovered()) {
			return false;
		}
		ITypeBinding[] old= original.getParameterTypes();
		if (old.length != 5) {
			return false;
		}
		for (IMethodBinding candidate : original.getDeclaringClass().getDeclaredMethods()) {
			ITypeBinding[] parameters= candidate.getParameterTypes();
			if (candidate.isConstructor() && !candidate.isRecovered() && Modifier.isPublic(candidate.getModifiers())
					&& parameters.length == 4
					&& parameters[0].getErasure().isEqualTo(old[0].getErasure())
					&& parameters[1].getErasure().isEqualTo(old[1].getErasure())
					&& parameters[2].getErasure().isEqualTo(old[3].getErasure())
					&& parameters[3].getErasure().isEqualTo(old[4].getErasure())
					&& candidate.getExceptionTypes().length == 0) {
				return true;
			}
		}
		return false;
	}

	/** Avoid creating a newly unused local when a factory replaces an allocation. */
	static boolean canUseFactory(ClassInstanceCreation creation) {
		VariableDeclarationFragment fragment= localInitializer(creation);
		if (fragment == null || hasReference((CompilationUnit) creation.getRoot(), fragment, Set.of())) {
			return true;
		}
		VariableDeclarationStatement declaration= (VariableDeclarationStatement) fragment.getParent();
		// Preserve annotated declarations and multi-fragment evaluation order by
		// using the shorter constructor there rather than a discarded local binding.
		return declaration.fragments().size() == 1 && declaration.modifiers().stream().allMatch(Modifier.class::isInstance);
	}

	static void replaceWithFactory(CompilationUnitRewrite cuRewrite, ClassInstanceCreation creation,
			MethodInvocation factory, TextEditGroup group) {
		VariableDeclarationFragment fragment= localInitializer(creation);
		if (fragment != null && !hasReference(cuRewrite.getRoot(), fragment, Set.of())) {
			VariableDeclarationStatement declaration= (VariableDeclarationStatement) fragment.getParent();
			ASTNodes.replaceButKeepComment(cuRewrite.getASTRewrite(), declaration,
					creation.getAST().newExpressionStatement(factory), group);
			cuRewrite.getImportRemover().registerRemovedNode(declaration);
		} else {
			ASTNodes.replaceButKeepComment(cuRewrite.getASTRewrite(), creation, factory, group);
		}
	}

	static void finish(CompilationUnitRewrite cuRewrite, ClassInstanceCreation creation,
			List<Expression> retained, List<Expression> discarded, TextEditGroup group, String... addedImports) {
		var remover= cuRewrite.getImportRemover();
		remover.registerRemovedNode(creation);
		retained.forEach(expression -> remover.registerRetainedNode(ASTNodes.getUnparenthesedExpression(expression)));
		for (String type : addedImports) {
			remover.registerAddedImport(type);
		}
		ASTRewrite rewrite= cuRewrite.getASTRewrite();
		State state= (State) rewrite.getProperty(STATE_KEY);
		if (state == null) {
			state= new State();
			rewrite.setProperty(STATE_KEY, state);
		}
		for (Expression expression : discarded) {
			state.removed.add(expression);
			state.collect(cuRewrite.getRoot(), expression);
		}
		state.removeUnreferencedConstants(cuRewrite, group);
	}

	private static VariableDeclarationFragment localInitializer(Expression expression) {
		ASTNode current= expression;
		while (current.getParent() instanceof ParenthesizedExpression) {
			current= current.getParent();
		}
		if (current.getParent() instanceof VariableDeclarationFragment fragment
				&& fragment.getInitializer() == current
				&& fragment.getParent() instanceof VariableDeclarationStatement) {
			return fragment;
		}
		return null;
	}

	private static boolean hasReference(CompilationUnit root, VariableDeclarationFragment declaration,
			Set<ASTNode> removed) {
		IVariableBinding binding= declaration.resolveBinding();
		if (binding == null || binding.isRecovered()) {
			return true;
		}
		boolean[] found= { false };
		root.accept(new ASTVisitor(true) {
			@Override
			public boolean preVisit2(ASTNode node) {
				return !found[0] && !removed.contains(node);
			}

			@Override
			public boolean visit(SimpleName name) {
				if (name != declaration.getName() && name.getIdentifier().equals(binding.getName())) {
					var reference= name.resolveBinding();
					found[0]= reference == null || reference.isRecovered() || binding.isEqualTo(reference);
				}
				return !found[0];
			}
		});
		return found[0];
	}

	private static final class State {
		final Set<ASTNode> removed= Collections.newSetFromMap(new IdentityHashMap<>());
		final Set<VariableDeclarationFragment> candidates= new LinkedHashSet<>();

		void collect(CompilationUnit root, ASTNode expression) {
			expression.accept(new ASTVisitor(true) {
				@Override
				public boolean visit(SimpleName name) {
					if (name.resolveBinding() instanceof IVariableBinding variable && !variable.isRecovered()
							&& variable.isField() && Modifier.isPrivate(variable.getModifiers())
							&& Modifier.isFinal(variable.getModifiers()) && variable.getConstantValue() != null
							&& root.findDeclaringNode(variable.getVariableDeclaration()) instanceof VariableDeclarationFragment fragment
							&& fragment.getParent() instanceof FieldDeclaration field
							&& field.modifiers().stream().allMatch(Modifier.class::isInstance)
							&& fragment.getInitializer() != null && ASTNodes.isPassive(fragment.getInitializer())) {
						candidates.add(fragment);
					}
					return true;
				}
			});
		}

		void removeUnreferencedConstants(CompilationUnitRewrite cuRewrite, TextEditGroup group) {
			boolean changed;
			do {
				changed= false;
				for (VariableDeclarationFragment fragment : List.copyOf(candidates)) {
					if (removed.contains(fragment) || hasReference(cuRewrite.getRoot(), fragment, removed)) {
						continue;
					}
					FieldDeclaration field= (FieldDeclaration) fragment.getParent();
					var fragments= cuRewrite.getASTRewrite().getListRewrite(field, FieldDeclaration.FRAGMENTS_PROPERTY);
					ASTNode deleted= fragments.getRewrittenList().size() == 1 ? field : fragment;
					ASTNodes.removeButKeepComment(cuRewrite.getASTRewrite(), deleted, group);
					cuRewrite.getImportRemover().registerRemovedNode(deleted);
					removed.add(fragment);
					removed.add(deleted);
					collect(cuRewrite.getRoot(), fragment.getInitializer());
					changed= true;
				}
			} while (changed);
		}
	}
}