HintProgramParser.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.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.sandbox.jdt.triggerpattern.api.HintBindingPolicy;
import org.sandbox.jdt.triggerpattern.api.HintFile;
import org.sandbox.jdt.triggerpattern.api.HintPlanRequirement;
import org.sandbox.jdt.triggerpattern.api.HintPredicateDefinition;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.RewriteActionCatalog;
import org.sandbox.jdt.triggerpattern.api.RewriteAlternative;
import org.sandbox.jdt.triggerpattern.api.StructuredRewriteAction;
import org.sandbox.jdt.triggerpattern.api.TransformationRule;
import org.sandbox.jdt.triggerpattern.internal.HintFileParser.HintParseException;
/**
* Composes high-level declarative language features before delegating ordinary
* rule parsing to the stable {@link HintFileParser}.
*/
public final class HintProgramParser {
/** Immutable declarations and parser-compatible expanded source. */
public record ParsedProgram(List<HintPredicateDefinition> predicates, String expandedSource,
Map<String, List<StructuredRewriteAction>> actionsBySentinel,
Map<String, PatternKind> kindsByRuleId) {
public ParsedProgram {
predicates= List.copyOf(predicates);
Map<String, List<StructuredRewriteAction>> actionCopy= new LinkedHashMap<>();
actionsBySentinel.forEach((sentinel, actions) ->
actionCopy.put(sentinel, List.copyOf(actions)));
actionsBySentinel= Map.copyOf(actionCopy);
kindsByRuleId= Map.copyOf(kindsByRuleId);
}
/** Backward-compatible constructor for programs without explicit rule kinds. */
public ParsedProgram(List<HintPredicateDefinition> predicates, String expandedSource,
Map<String, List<StructuredRewriteAction>> actionsBySentinel) {
this(predicates, expandedSource, actionsBySentinel, Map.of());
}
/** Returns a fresh validated rule model with high-level features restored. */
public HintFile hintFile() {
try {
return parseComposed(expandedSource, actionsBySentinel, kindsByRuleId);
} catch (HintParseException exception) {
throw new IllegalStateException("Validated expanded hint source can no longer be parsed", exception); //$NON-NLS-1$
}
}
}
private record PreparedProgram(List<HintPredicateDefinition> predicates, String expandedSource,
Map<String, List<StructuredRewriteAction>> actionsBySentinel,
Map<String, PatternKind> kindsByRuleId) {
PreparedProgram {
predicates= List.copyOf(predicates);
actionsBySentinel= Map.copyOf(actionsBySentinel);
kindsByRuleId= Map.copyOf(kindsByRuleId);
}
}
private final HintFileParser ruleParser;
private final RewriteActionCatalog actionCatalog;
public HintProgramParser() {
this(new HintFileParser(), RewriteActionCatalog.standard());
}
public HintProgramParser(RewriteActionCatalog actionCatalog) {
this(new HintFileParser(), actionCatalog);
}
HintProgramParser(HintFileParser ruleParser) {
this(ruleParser, RewriteActionCatalog.standard());
}
HintProgramParser(HintFileParser ruleParser, RewriteActionCatalog actionCatalog) {
this.ruleParser= ruleParser;
this.actionCatalog= actionCatalog;
}
public ParsedProgram parse(String source) throws HintParseException {
PreparedProgram prepared= prepare(source, actionCatalog);
parseComposed(ruleParser, prepared.expandedSource(), prepared.actionsBySentinel(),
prepared.kindsByRuleId());
return new ParsedProgram(prepared.predicates(), prepared.expandedSource(),
prepared.actionsBySentinel(), prepared.kindsByRuleId());
}
public HintFile parseHintFile(String source) throws HintParseException {
PreparedProgram prepared= prepare(source, actionCatalog);
return parseComposed(ruleParser, prepared.expandedSource(), prepared.actionsBySentinel(),
prepared.kindsByRuleId());
}
private static PreparedProgram prepare(String source, RewriteActionCatalog catalog)
throws HintParseException {
try {
Optional<String> planRequirement= HintPlanRequirement.fromContent(source);
Optional<HintBindingPolicy> bindingPolicy= HintBindingPolicy.fromContent(source);
if (planRequirement.isPresent() && bindingPolicy.orElse(HintBindingPolicy.REQUIRED)
== HintBindingPolicy.OPTIONAL) {
throw new IllegalArgumentException(
"Plan-aware hint programs cannot declare binding-policy optional"); //$NON-NLS-1$
}
} catch (IllegalArgumentException exception) {
throw parseFailure(exception);
}
HintPredicatePreprocessor.Result predicates= HintPredicatePreprocessor.preprocess(source);
validatePredicateNames(predicates.predicates());
HintRuleKindPreprocessor.Result kinds=
HintRuleKindPreprocessor.preprocess(predicates.expandedSource());
String declarationDiffs= HintDeclarationDiffPreprocessor.preprocess(kinds.parserSource());
String normalizedTypeSources= HintTypeDeclarationSourcePreprocessor.preprocess(
declarationDiffs, kinds.kindsByRuleId());
HintStructuredActionPreprocessor.Result actions=
HintStructuredActionPreprocessor.preprocess(normalizedTypeSources, catalog);
return new PreparedProgram(predicates.predicates(), actions.parserSource(),
actions.actionsBySentinel(), kinds.kindsByRuleId());
}
private static HintFile parseComposed(String source,
Map<String, List<StructuredRewriteAction>> actionsBySentinel,
Map<String, PatternKind> kindsByRuleId) throws HintParseException {
return parseComposed(new HintFileParser(), source, actionsBySentinel, kindsByRuleId);
}
private static HintFile parseComposed(HintFileParser parser, String source,
Map<String, List<StructuredRewriteAction>> actionsBySentinel,
Map<String, PatternKind> kindsByRuleId) throws HintParseException {
HintFile parsed= parser.parse(source);
try {
HintBindingPolicy.fromContent(source).ifPresent(parsed::setBindingPolicy);
} catch (IllegalArgumentException exception) {
throw parseFailure(exception);
}
if (actionsBySentinel.isEmpty() && kindsByRuleId.isEmpty()) {
return parsed;
}
HintFile result= copyMetadata(parsed);
Set<String> appliedKinds= new LinkedHashSet<>();
for (TransformationRule rule : parsed.getRules()) {
List<RewriteAlternative> alternatives= restoreActions(rule, actionsBySentinel);
Pattern sourcePattern= rule.sourcePattern();
PatternKind explicitKind= rule.getRuleId() == null ? null : kindsByRuleId.get(rule.getRuleId());
if (explicitKind != null) {
appliedKinds.add(rule.getRuleId());
sourcePattern= withKind(sourcePattern, explicitKind, rule.getRuleId());
}
result.addRule(new TransformationRule(rule.getRuleId(), rule.getDescription(),
sourcePattern, rule.sourceGuard(), alternatives,
rule.getImportDirective(), rule.getSeverity()));
}
if (!appliedKinds.equals(kindsByRuleId.keySet())) {
Set<String> missing= new LinkedHashSet<>(kindsByRuleId.keySet());
missing.removeAll(appliedKinds);
throw new HintParseException("Explicit pattern kinds reference missing rule ids " + missing, 0); //$NON-NLS-1$
}
return result;
}
private static List<RewriteAlternative> restoreActions(TransformationRule rule,
Map<String, List<StructuredRewriteAction>> actionsBySentinel) {
List<RewriteAlternative> alternatives= new ArrayList<>();
for (RewriteAlternative alternative : rule.alternatives()) {
List<StructuredRewriteAction> actions=
actionsBySentinel.get(alternative.replacementPattern());
if (actions == null) {
alternatives.add(alternative);
} else {
alternatives.add(RewriteAlternative.structured(actions, alternative.condition()));
}
}
return alternatives;
}
private static Pattern withKind(Pattern source, PatternKind kind, String ruleId)
throws HintParseException {
if (kind == PatternKind.TYPE_DECLARATION
&& new TypeDeclarationPatternParser().parse(source.getValue()) == null) {
throw new HintParseException("Rule " + ruleId //$NON-NLS-1$
+ " must contain one syntactically valid type header with an empty body", 0); //$NON-NLS-1$
}
return new Pattern(source.getValue(), kind, source.getId(), source.getDisplayName(),
source.getQualifiedType(), source.getOverridesType(), source.getConstraints());
}
private static HintFile copyMetadata(HintFile source) {
HintFile copy= new HintFile();
copy.setId(source.getId());
copy.setDescription(source.getDescription());
copy.setSeverity(source.getSeverity());
copy.setMinJavaVersion(source.getMinJavaVersion());
copy.setBindingPolicy(source.getBindingPolicy());
copy.setTags(source.getTags());
copy.setCaseInsensitive(source.isCaseInsensitive());
copy.setSuppressWarnings(source.getSuppressWarnings());
copy.setTreeKindNodeTypes(source.getTreeKindNodeTypes());
source.getIncludes().forEach(copy::addInclude);
source.getEmbeddedJavaBlocks().forEach(copy::addEmbeddedJavaBlock);
return copy;
}
private static HintParseException parseFailure(IllegalArgumentException cause) {
return new HintParseException(cause.getMessage(), 0, cause);
}
private static void validatePredicateNames(Iterable<HintPredicateDefinition> predicates)
throws HintParseException {
for (HintPredicateDefinition predicate : predicates) {
if (HintLanguageVocabulary.builtInGuardNames().contains(predicate.name())) {
throw new HintParseException("Predicate name shadows built-in guard " //$NON-NLS-1$
+ predicate.name(), predicate.lineNumber());
}
}
}
}