FixUtilities.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.triggerpattern.api;

import java.util.HashMap;
import java.util.Map;

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.Annotation;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.sandbox.jdt.triggerpattern.internal.PatternParser;

/**
 * Utilities for creating declarative fix templates.
 * 
 * <p>This class provides methods to create fixes using pattern-based
 * replacement templates, similar to NetBeans' JavaFixUtilities.</p>
 * 
 * @since 1.2.2
 */
public final class FixUtilities {
	
	private FixUtilities() {
		// Utility class, no instances
	}
	
	/**
	 * Creates a declarative rewrite fix from a replacement template.
	 * 
	 * <p>Placeholders from the match are substituted into the replacement template.
	 * The matched node is replaced with the result.</p>
	 * 
	 * <p>Example:</p>
	 * <pre>
	 * Match pattern:    "$x + 1"
	 * Replacement:      "++$x"
	 * For code "count + 1", produces "++count"
	 * </pre>
	 * 
	 * <p>Supports both single placeholders ($x) and multi-placeholders ($args$).
	 * The replacement pattern is parsed using the same syntax as trigger patterns.</p>
	 * 
	 * @param match the pattern match result
	 * @param rewrite the AST rewrite for making changes
	 * @param replacementPattern the replacement pattern with placeholders
	 * @throws IllegalArgumentException if the replacement pattern cannot be parsed
	 */
	public static void rewriteFix(Match match, ASTRewrite rewrite, String replacementPattern) {
		if (match == null || rewrite == null || replacementPattern == null) {
			throw new IllegalArgumentException("Match, rewrite, and replacement pattern must not be null"); //$NON-NLS-1$
		}
		
		ASTNode matchedNode = match.getMatchedNode();
		
		// Determine pattern kind from matched node type
		PatternKind kind = determinePatternKind(matchedNode);
		
		// Parse the replacement pattern
		PatternParser parser = new PatternParser();
		Pattern pattern = Pattern.of(replacementPattern, kind);
		ASTNode replacementNode = parser.parse(pattern);
		
		if (replacementNode == null) {
			throw new IllegalArgumentException("Could not parse replacement pattern: " + replacementPattern); //$NON-NLS-1$
		}
		
		// Substitute placeholders with actual bindings
		ASTNode substituted = substitutePlaceholders(replacementNode, match.getBindings(), rewrite.getAST());
		
		// Replace the matched node with the substituted replacement
		rewrite.replace(matchedNode, substituted, null);
	}
	
	/**
	 * Determines the PatternKind from a matched node type.
	 * 
	 * @param node the matched node
	 * @return the appropriate PatternKind
	 */
	public static PatternKind determinePatternKindFromNode(ASTNode node) {
		return determinePatternKind(node);
	}
	
	/**
	 * Determines the PatternKind from a matched node type.
	 * 
	 * @param node the matched node
	 * @return the appropriate PatternKind
	 */
	private static PatternKind determinePatternKind(ASTNode node) {
		// Check ClassInstanceCreation before Expression since it extends Expression
		if (node instanceof ClassInstanceCreation) {
			return PatternKind.CONSTRUCTOR;
		} else if (node instanceof Annotation) {
			return PatternKind.ANNOTATION;
		} else if (node instanceof MethodInvocation) {
			return PatternKind.METHOD_CALL;
		} else if (node instanceof ImportDeclaration) {
			return PatternKind.IMPORT;
		} else if (node instanceof FieldDeclaration) {
			return PatternKind.FIELD;
		} else if (node instanceof VariableDeclarationStatement) {
			return PatternKind.DECLARATION;
		} else if (node instanceof Expression) {
			return PatternKind.EXPRESSION;
		}
		return PatternKind.STATEMENT;
	}
	
	/**
	 * Substitutes placeholders in a template node with actual bindings.
	 * 
	 * @param template the template node with placeholders
	 * @param bindings the placeholder bindings from the match
	 * @param ast the AST for creating new nodes
	 * @return a new node with placeholders replaced
	 */
	private static ASTNode substitutePlaceholders(ASTNode template, Map<String, Object> bindings, AST ast) {
		// For simple cases, if the entire template is a placeholder, return its binding directly
		String placeholderName = extractPlaceholderName(template);
		if (placeholderName != null) {
			Object binding = bindings.get(placeholderName);
			if (binding instanceof ASTNode) {
				return ASTNode.copySubtree(ast, (ASTNode) binding);
			}
		}
		
		// For complex templates, perform a recursive substitution
		ASTNode copy = ASTNode.copySubtree(ast, template);
		
		// Build a map of placeholder SimpleNames to their replacements
		Map<SimpleName, ASTNode> replacements = new HashMap<>();
		
		copy.accept(new ASTVisitor() {
			@Override
			public boolean visit(SimpleName node) {
				String name = node.getIdentifier();
				if (name.startsWith("$")) { //$NON-NLS-1$
					Object binding = bindings.get(name);
					if (binding instanceof ASTNode) {
						replacements.put(node, (ASTNode) binding);
					}
					// Multi-placeholders are handled separately as they need list context
				}
				return true;
			}
		});
		
		// Apply replacements using structural replace
		for (Map.Entry<SimpleName, ASTNode> entry : replacements.entrySet()) {
			SimpleName placeholder = entry.getKey();
			ASTNode replacement = entry.getValue();
			ASTNode parent = placeholder.getParent();
			
			if (parent != null) {
				StructuralPropertyDescriptor location = placeholder.getLocationInParent();
				if (location.isChildProperty()) {
					parent.setStructuralProperty(location, ASTNode.copySubtree(ast, replacement));
				}
			}
		}
		
		return copy;
	}
	
	/**
	 * Extracts the placeholder name from a node if it is a simple placeholder.
	 * 
	 * @param node the node to check
	 * @return the placeholder name (e.g., "$x"), or null if not a placeholder
	 */
	private static String extractPlaceholderName(ASTNode node) {
		// SimpleName nodes with names starting with $ are placeholders
		if (node.getNodeType() == ASTNode.SIMPLE_NAME) {
			String name = node.toString();
			if (name.startsWith("$")) { //$NON-NLS-1$
				return name;
			}
		}
		return null;
	}
}