TypeChangeDetector.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
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.cleanup;

import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;

/**
 * Detects whether a DSL replacement changed an argument from a {@code String}
 * charset literal (e.g. {@code "UTF-8"}) to a {@code Charset}-typed
 * expression (e.g. {@code StandardCharsets.UTF_8}).
 *
 * <p>Only a direct argument replacement by a known StandardCharsets field is
 * recognized. Calls such as {@code StandardCharsets.UTF_8.name()}, string content
 * and nested expressions do not authorize exception removal. This syntactic
 * check is used with type-checked encoding rules; it does not establish the
 * exception contract of an arbitrary method or constructor.</p>
 *
 * @since 1.3.5
 */
public class TypeChangeDetector {

	/** Known charset string values that map to StandardCharsets constants. */
	private static final Set<String> CHARSET_STRINGS = Set.of(
			"UTF-8", "UTF-16", "UTF-16BE", "UTF-16LE", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
			"US-ASCII", "ISO-8859-1" //$NON-NLS-1$ //$NON-NLS-2$
	);

	private static final String UNSUPPORTED_ENCODING_EXCEPTION_FQN =
			"java.io.UnsupportedEncodingException"; //$NON-NLS-1$
	private static final String UNSUPPORTED_ENCODING_EXCEPTION_SIMPLE =
			"UnsupportedEncodingException"; //$NON-NLS-1$

	private TypeChangeDetector() {
		// utility class – not instantiable
	}

	/**
	 * Detects if a replacement changed a {@code String} argument to a
	 * {@code Charset} type.
	 *
	 * @param matchedNode the original matched AST node
	 * @param replacement the replacement text
	 * @return info about the type change, or {@code null} if no type change detected
	 */
	public static TypeChangeInfo detectCharsetTypeChange(ASTNode matchedNode, String replacement) {
		if (matchedNode == null || replacement == null) {
			return null;
		}
		ASTParser parser= ASTParser.newParser(AST.getJLSLatest());
		parser.setKind(ASTParser.K_EXPRESSION);
		parser.setSource(replacement.toCharArray());
		return detectCharsetTypeChange(matchedNode, parser.createAST(null));
	}

	/** Uses an already parsed replacement; only a direct changed argument authorizes cleanup. */
	static TypeChangeInfo detectCharsetTypeChange(ASTNode matchedNode, ASTNode replacement) {
		if (replacement == null || (replacement.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) != 0) {
			return null;
		}
		List<?> original= arguments(matchedNode);
		List<?> rewritten= arguments(replacement);
		for (int i= 0; i < Math.min(original.size(), rewritten.size()); i++) {
			if (unparenthesized((ASTNode) original.get(i)) instanceof StringLiteral literal
					&& isCharsetString(literal.getLiteralValue())
					&& unparenthesized((ASTNode) rewritten.get(i)) instanceof QualifiedName name) {
				String owner= name.getQualifier().getFullyQualifiedName();
				if (("StandardCharsets".equals(owner) || "java.nio.charset.StandardCharsets".equals(owner)) //$NON-NLS-1$ //$NON-NLS-2$
						&& CHARSET_STRINGS.contains(name.getName().getIdentifier().replace('_', '-'))) {
					return new TypeChangeInfo(UNSUPPORTED_ENCODING_EXCEPTION_FQN, UNSUPPORTED_ENCODING_EXCEPTION_SIMPLE);
				}
			}
		}
		return null;
	}

	private static List<?> arguments(ASTNode node) {
		if (node instanceof ClassInstanceCreation creation) return creation.arguments();
		if (node instanceof MethodInvocation invocation) return invocation.arguments();
		if (node instanceof SuperMethodInvocation invocation) return invocation.arguments();
		return List.of();
	}

	private static ASTNode unparenthesized(ASTNode node) {
		while (node instanceof ParenthesizedExpression expression) node= expression.getExpression();
		return node;
	}

	private static boolean isCharsetString(String value) {
		return CHARSET_STRINGS.contains(value.toUpperCase(Locale.ROOT));
	}
}