HintStructuredActionPreprocessor.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.internal;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.sandbox.jdt.triggerpattern.api.RewriteActionCatalog;
import org.sandbox.jdt.triggerpattern.api.StructuredRewriteAction;
import org.sandbox.jdt.triggerpattern.internal.HintFileParser.HintParseException;

/** Converts high-level {@code =>! action(...)} alternatives into parser sentinels. */
final class HintStructuredActionPreprocessor {

	record Result(String parserSource,
			Map<String, List<StructuredRewriteAction>> actionsBySentinel) {
		Result {
			actionsBySentinel= Map.copyOf(actionsBySentinel);
		}
	}

	private HintStructuredActionPreprocessor() {
	}

	static Result preprocess(String source, RewriteActionCatalog catalog) throws HintParseException {
		String normalized= source == null ? "" : source.replace("\r\n", "\n").replace('\r', '\n'); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		String[] lines= normalized.split("\n", -1); //$NON-NLS-1$
		List<String> output= new ArrayList<>(lines.length);
		Map<String, List<StructuredRewriteAction>> actions= new LinkedHashMap<>();
		boolean inBlockComment= false;
		boolean inEmbeddedJava= false;
		int actionIndex= 0;

		for (int lineIndex= 0; lineIndex < lines.length; lineIndex++) {
			String line= lines[lineIndex];
			String trimmed= line.stripLeading();
			if (inEmbeddedJava) {
				output.add(line);
				if (trimmed.contains("?>")) { //$NON-NLS-1$
					inEmbeddedJava= false;
				}
				continue;
			}
			if (inBlockComment) {
				output.add(line);
				if (trimmed.contains("*/")) { //$NON-NLS-1$
					inBlockComment= false;
				}
				continue;
			}
			if (trimmed.startsWith("<?") && !trimmed.contains("?>")) { //$NON-NLS-1$ //$NON-NLS-2$
				inEmbeddedJava= true;
				output.add(line);
				continue;
			}
			if (trimmed.startsWith("/*") && !trimmed.contains("*/")) { //$NON-NLS-1$ //$NON-NLS-2$
				inBlockComment= true;
				output.add(line);
				continue;
			}
			if (!trimmed.startsWith("=>!")) { //$NON-NLS-1$
				output.add(line);
				continue;
			}

			int firstLine= lineIndex + 1;
			int indentationLength= line.length() - trimmed.length();
			String indentation= line.substring(0, indentationLength);
			StringBuilder alternative= new StringBuilder(trimmed.substring(3).trim());
			int lastLine= lineIndex;
			while (lastLine + 1 < lines.length) {
				String nextTrimmed= lines[lastLine + 1].stripLeading();
				if (nextTrimmed.startsWith("=>") || ";;".equals(nextTrimmed)) { //$NON-NLS-1$ //$NON-NLS-2$
					break;
				}
				lastLine++;
				alternative.append('\n').append(nextTrimmed);
			}

			ActionAndGuard split= splitGuard(alternative.toString());
			List<StructuredRewriteAction> parsed= StructuredRewriteActionParser.parse(
					split.actionText().trim(), firstLine, catalog);
			String sentinel;
			do {
				sentinel= "__sandbox_structured_action_" + actionIndex++ + "__"; //$NON-NLS-1$ //$NON-NLS-2$
			} while (normalized.contains(sentinel));
			actions.put(sentinel, parsed);
			String guardSuffix= split.guardText() == null ? "" : " :: " + split.guardText().trim(); //$NON-NLS-1$ //$NON-NLS-2$
			output.add(indentation + "=> " + sentinel + guardSuffix); //$NON-NLS-1$
			for (int consumed= lineIndex + 1; consumed <= lastLine; consumed++) {
				output.add(""); //$NON-NLS-1$
			}
			lineIndex= lastLine;
		}
		return new Result(String.join("\n", output), actions); //$NON-NLS-1$
	}

	private static ActionAndGuard splitGuard(String text) {
		int depth= 0;
		boolean inQuote= false;
		boolean escaped= false;
		for (int index= 0; index < text.length() - 1; index++) {
			char current= text.charAt(index);
			if (inQuote) {
				if (escaped) {
					escaped= false;
				} else if (current == '\\') {
					escaped= true;
				} else if (current == '"') {
					inQuote= false;
				}
				continue;
			}
			if (current == '"') {
				inQuote= true;
			} else if (current == '(') {
				depth++;
			} else if (current == ')') {
				depth--;
			} else if (depth == 0 && current == ':' && text.charAt(index + 1) == ':') {
				return new ActionAndGuard(text.substring(0, index), text.substring(index + 2));
			}
		}
		return new ActionAndGuard(text, null);
	}

	private record ActionAndGuard(String actionText, String guardText) {
	}
}