FqnAwarePlaceholderAstMatcher.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.internal;
import java.util.List;
import org.eclipse.jdt.core.dom.ArrayType;
import org.eclipse.jdt.core.dom.ASTMatcher;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CastExpression;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NameQualifiedType;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.PrimitiveType;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.core.dom.WildcardType;
/**
* FQN-aware variant of {@link PlaceholderAstMatcher}.
*
* <p>DSL snippets should express concrete APIs using fully qualified names. This
* matcher accepts source code that uses the same API via imports and simple
* names. It applies that principle to type references, parameterized types,
* arrays, constructors, type literals, casts and method receivers.</p>
*/
public final class FqnAwarePlaceholderAstMatcher extends PlaceholderAstMatcher {
private final ASTMatcher structuralMatcher = new ASTMatcher();
@Override
public boolean match(SimpleType patternNode, Object other) {
if (super.match(patternNode, other)) {
return true;
}
return other instanceof SimpleType otherType
&& namesEquivalent(patternNode.getName(), otherType.getName());
}
@Override
public boolean match(QualifiedType patternNode, Object other) {
return other instanceof Type otherType && typesEquivalent(patternNode, otherType);
}
@Override
public boolean match(NameQualifiedType patternNode, Object other) {
return other instanceof Type otherType && typesEquivalent(patternNode, otherType);
}
@Override
public boolean match(ParameterizedType patternNode, Object other) {
return other instanceof Type otherType && typesEquivalent(patternNode, otherType);
}
@Override
public boolean match(ArrayType patternNode, Object other) {
return other instanceof Type otherType && typesEquivalent(patternNode, otherType);
}
@Override
public boolean match(PrimitiveType patternNode, Object other) {
return other instanceof PrimitiveType otherType
&& patternNode.getPrimitiveTypeCode().equals(otherType.getPrimitiveTypeCode());
}
@Override
public boolean match(WildcardType patternNode, Object other) {
if (!(other instanceof WildcardType otherType)) {
return false;
}
if (patternNode.isUpperBound() != otherType.isUpperBound()) {
return false;
}
return typesEquivalent(patternNode.getBound(), otherType.getBound());
}
@Override
public boolean match(TypeLiteral patternNode, Object other) {
return other instanceof TypeLiteral otherLiteral
&& typesEquivalent(patternNode.getType(), otherLiteral.getType());
}
@Override
public boolean match(CastExpression patternNode, Object other) {
return other instanceof CastExpression otherCast
&& typesEquivalent(patternNode.getType(), otherCast.getType())
&& patternNode.getExpression().subtreeMatch(this, otherCast.getExpression());
}
@Override
public boolean match(ClassInstanceCreation patternNode, Object other) {
if (super.match(patternNode, other)) {
return true;
}
if (!(other instanceof ClassInstanceCreation otherCreation)) {
return false;
}
if (!typesEquivalent(patternNode.getType(), otherCreation.getType())) {
return false;
}
@SuppressWarnings("unchecked")
List<Type> patternTypeArgs = patternNode.typeArguments();
@SuppressWarnings("unchecked")
List<Type> otherTypeArgs = otherCreation.typeArguments();
if (!typeListsEquivalent(patternTypeArgs, otherTypeArgs)) {
return false;
}
if (!subtreeNullable(patternNode.getExpression(), otherCreation.getExpression())) {
return false;
}
@SuppressWarnings("unchecked")
List<Expression> patternArgs = patternNode.arguments();
@SuppressWarnings("unchecked")
List<Expression> otherArgs = otherCreation.arguments();
return expressionListsEquivalent(patternArgs, otherArgs);
}
@Override
public boolean match(MethodInvocation patternNode, Object other) {
if (super.match(patternNode, other)) {
return true;
}
if (!(other instanceof MethodInvocation otherInvocation)) {
return false;
}
if (!patternNode.getName().subtreeMatch(this, otherInvocation.getName())) {
return false;
}
if (!receiversEquivalent(patternNode.getExpression(), otherInvocation.getExpression())) {
return false;
}
if (!methodBindingCompatible(patternNode, otherInvocation)) {
return false;
}
@SuppressWarnings("unchecked")
List<Type> patternTypeArgs = patternNode.typeArguments();
@SuppressWarnings("unchecked")
List<Type> otherTypeArgs = otherInvocation.typeArguments();
if (!typeListsEquivalent(patternTypeArgs, otherTypeArgs)) {
return false;
}
@SuppressWarnings("unchecked")
List<Expression> patternArgs = patternNode.arguments();
@SuppressWarnings("unchecked")
List<Expression> otherArgs = otherInvocation.arguments();
return expressionListsEquivalent(patternArgs, otherArgs);
}
private boolean typesEquivalent(Type patternType, Type sourceType) {
if (patternType == null) {
return sourceType == null;
}
if (sourceType == null) {
return false;
}
if (patternType.subtreeMatch(structuralMatcher, sourceType)) {
return true;
}
if (patternType instanceof SimpleType patternSimple && sourceType instanceof SimpleType sourceSimple) {
return namesEquivalent(patternSimple.getName(), sourceSimple.getName());
}
if (patternType instanceof ParameterizedType patternParam && sourceType instanceof ParameterizedType sourceParam) {
return typesEquivalent(patternParam.getType(), sourceParam.getType())
&& typeListsEquivalent(patternParam.typeArguments(), sourceParam.typeArguments());
}
if (patternType instanceof ParameterizedType patternParam) {
return typesEquivalent(patternParam.getType(), sourceType);
}
if (sourceType instanceof ParameterizedType sourceParam) {
return typesEquivalent(patternType, sourceParam.getType());
}
if (patternType instanceof ArrayType patternArray && sourceType instanceof ArrayType sourceArray) {
return patternArray.getDimensions() == sourceArray.getDimensions()
&& typesEquivalent(patternArray.getElementType(), sourceArray.getElementType());
}
String patternIdentity = typeIdentity(patternType);
String sourceIdentity = typeIdentity(sourceType);
return patternIdentity != null && patternIdentity.equals(sourceIdentity);
}
private boolean typeListsEquivalent(List<?> patternTypes, List<?> sourceTypes) {
if (patternTypes.size() != sourceTypes.size()) {
return false;
}
for (int i = 0; i < patternTypes.size(); i++) {
if (!typesEquivalent((Type) patternTypes.get(i), (Type) sourceTypes.get(i))) {
return false;
}
}
return true;
}
private boolean expressionListsEquivalent(List<Expression> patternArgs, List<Expression> sourceArgs) {
if (patternArgs.size() != sourceArgs.size()) {
return false;
}
for (int i = 0; i < patternArgs.size(); i++) {
if (!patternArgs.get(i).subtreeMatch(this, sourceArgs.get(i))) {
return false;
}
}
return true;
}
private boolean subtreeNullable(ASTNode patternNode, ASTNode sourceNode) {
if (patternNode == null) {
return sourceNode == null;
}
return patternNode.subtreeMatch(this, sourceNode);
}
private boolean receiversEquivalent(Expression patternExpr, Expression sourceExpr) {
if (patternExpr == null) {
return sourceExpr == null;
}
if (sourceExpr == null) {
return false;
}
if (patternExpr.subtreeMatch(this, sourceExpr)) {
return true;
}
return patternExpr instanceof Name patternName
&& sourceExpr instanceof Name sourceName
&& namesEquivalent(patternName, sourceName);
}
private boolean methodBindingCompatible(MethodInvocation patternNode, MethodInvocation sourceNode) {
if (!(patternNode.getExpression() instanceof Name patternReceiver)
|| !isFullyQualified(patternReceiver.getFullyQualifiedName())) {
return true;
}
IMethodBinding binding = sourceNode.resolveMethodBinding();
if (binding == null || binding.getDeclaringClass() == null) {
return true;
}
String declaringClass = binding.getDeclaringClass().getQualifiedName();
return patternReceiver.getFullyQualifiedName().equals(declaringClass);
}
private boolean namesEquivalent(Name patternName, Name sourceName) {
if (patternName.subtreeMatch(structuralMatcher, sourceName)) {
return true;
}
if (patternName instanceof SimpleName patternSimple && patternSimple.getIdentifier().startsWith("$")) { //$NON-NLS-1$
return patternName.subtreeMatch(this, sourceName);
}
String patternFqn = patternName.getFullyQualifiedName();
String sourceFqn = sourceName.getFullyQualifiedName();
if (patternFqn.equals(sourceFqn)) {
return true;
}
if (isFullyQualified(patternFqn) && sourceName instanceof SimpleName sourceSimple) {
return simpleNameResolvesTo(sourceSimple, patternFqn);
}
if (isFullyQualified(sourceFqn) && patternName instanceof SimpleName patternSimple) {
return simpleNameResolvesTo(patternSimple, sourceFqn);
}
return false;
}
private String typeIdentity(Type type) {
if (type == null) {
return null;
}
if (type instanceof SimpleType simpleType) {
return nameIdentity(simpleType.getName());
}
if (type instanceof ParameterizedType parameterizedType) {
String raw = typeIdentity(parameterizedType.getType());
@SuppressWarnings("unchecked")
List<Type> args = parameterizedType.typeArguments();
if (args.isEmpty()) {
return raw;
}
StringBuilder sb = new StringBuilder(raw == null ? "" : raw); //$NON-NLS-1$
sb.append('<');
for (int i = 0; i < args.size(); i++) {
if (i > 0) {
sb.append(',');
}
sb.append(typeIdentity(args.get(i)));
}
sb.append('>');
return sb.toString();
}
if (type instanceof ArrayType arrayType) {
return typeIdentity(arrayType.getElementType()) + "[]".repeat(arrayType.getDimensions()); //$NON-NLS-1$
}
if (type instanceof PrimitiveType primitiveType) {
return primitiveType.getPrimitiveTypeCode().toString();
}
if (type instanceof WildcardType wildcardType) {
if (wildcardType.getBound() == null) {
return "?"; //$NON-NLS-1$
}
return wildcardType.isUpperBound()
? "? extends " + typeIdentity(wildcardType.getBound()) //$NON-NLS-1$
: "? super " + typeIdentity(wildcardType.getBound()); //$NON-NLS-1$
}
return type.toString();
}
private String nameIdentity(Name name) {
if (name instanceof SimpleName simpleName) {
String resolved = resolveSimpleNameViaImports(simpleName);
return resolved != null ? resolved : simpleName.getIdentifier();
}
return name.getFullyQualifiedName();
}
private boolean simpleNameResolvesTo(SimpleName simpleName, String expectedFqn) {
String identifier = simpleName.getIdentifier();
int lastDot = expectedFqn.lastIndexOf('.');
String expectedSimpleName = lastDot >= 0 ? expectedFqn.substring(lastDot + 1) : expectedFqn;
if (!identifier.equals(expectedSimpleName)) {
return false;
}
String resolved = resolveSimpleNameViaImports(simpleName);
if (expectedFqn.equals(resolved)) {
return true;
}
if (expectedFqn.startsWith("java.lang.")) { //$NON-NLS-1$
return true;
}
CompilationUnit cu = findCompilationUnit(simpleName);
if (cu != null && cu.getPackage() != null) {
String packageName = cu.getPackage().getName().getFullyQualifiedName();
if (expectedFqn.equals(packageName + '.' + identifier)) {
return true;
}
}
return importOnDemandCanResolve(simpleName, expectedFqn);
}
private String resolveSimpleNameViaImports(SimpleName simpleName) {
CompilationUnit cu = findCompilationUnit(simpleName);
if (cu == null) {
return null;
}
@SuppressWarnings("unchecked")
List<ImportDeclaration> imports = cu.imports();
for (ImportDeclaration importDecl : imports) {
if (importDecl.isStatic() || importDecl.isOnDemand()) {
continue;
}
String importFqn = importDecl.getName().getFullyQualifiedName();
int lastDot = importFqn.lastIndexOf('.');
String importSimpleName = lastDot >= 0 ? importFqn.substring(lastDot + 1) : importFqn;
if (simpleName.getIdentifier().equals(importSimpleName)) {
return importFqn;
}
}
return null;
}
private boolean importOnDemandCanResolve(SimpleName simpleName, String expectedFqn) {
CompilationUnit cu = findCompilationUnit(simpleName);
if (cu == null) {
return false;
}
@SuppressWarnings("unchecked")
List<ImportDeclaration> imports = cu.imports();
for (ImportDeclaration importDecl : imports) {
if (importDecl.isStatic() || !importDecl.isOnDemand()) {
continue;
}
String packageName = importDecl.getName().getFullyQualifiedName();
if (expectedFqn.equals(packageName + '.' + simpleName.getIdentifier())) {
return true;
}
}
return false;
}
private CompilationUnit findCompilationUnit(ASTNode node) {
ASTNode current = node;
while (current != null) {
if (current instanceof CompilationUnit cu) {
return cu;
}
current = current.getParent();
}
return null;
}
private boolean isFullyQualified(String name) {
return name != null && name.indexOf('.') > 0;
}
}