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.Locale;
import java.util.Set;

import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.StringLiteral;

/**
 * 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>The detection is deliberately conservative — pure syntactic checks,
 * no binding resolution. If it cannot determine the type change it returns
 * {@code null} (no false positives).</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 STANDARD_CHARSETS_PREFIX = "StandardCharsets."; //$NON-NLS-1$

	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;
		}

		// 1. Does the replacement contain "StandardCharsets."?
		if (!replacement.contains(STANDARD_CHARSETS_PREFIX)) {
			return null;
		}

		// 2. Does the matched node contain a StringLiteral whose value is a known charset?
		if (!containsCharsetStringLiteral(matchedNode)) {
			return null;
		}

		return new TypeChangeInfo(UNSUPPORTED_ENCODING_EXCEPTION_FQN, UNSUPPORTED_ENCODING_EXCEPTION_SIMPLE);
	}

	/**
	 * Walks the matched node's children looking for a {@link StringLiteral}
	 * whose value (upper-cased) is in {@link #CHARSET_STRINGS}.
	 */
	static boolean containsCharsetStringLiteral(ASTNode node) {
		if (node instanceof StringLiteral literal) {
			return isCharsetString(literal.getLiteralValue());
		}
		boolean[] found = { false };
		node.accept(new ASTVisitor() {
			@Override
			public boolean visit(StringLiteral literal) {
				if (isCharsetString(literal.getLiteralValue())) {
					found[0] = true;
				}
				return !found[0];
			}
		});
		return found[0];
	}

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