PatternParser.java
/*******************************************************************************
* Copyright (c) 2025 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 - initial API and implementation
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.internal;
import org.eclipse.jdt.core.JavaCore;
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.Annotation;
import org.eclipse.jdt.core.dom.Block;
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.FieldDeclaration;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
/**
* Parses pattern strings into AST nodes.
*
* <p>This parser handles both expression and statement patterns by embedding them
* in a minimal wrapper structure (fake class and method) to create valid Java code
* that can be parsed by the Eclipse JDT parser.</p>
*
* @since 1.2.2
*/
public class PatternParser {
/**
* Parses a pattern into an AST node.
*
* @param pattern the pattern to parse
* @return the parsed AST node (Expression, Statement, Annotation, MethodInvocation, ImportDeclaration,
* FieldDeclaration, ClassInstanceCreation, or MethodDeclaration), or {@code null} if parsing fails
*/
public ASTNode parse(Pattern pattern) {
if (pattern == null) {
return null;
}
String patternValue = pattern.getValue();
PatternKind kind = pattern.getKind();
if (kind == PatternKind.EXPRESSION) {
return parseExpression(patternValue);
} else if (kind == PatternKind.STATEMENT) {
return parseStatement(patternValue);
} else if (kind == PatternKind.ANNOTATION) {
return parseAnnotation(patternValue);
} else if (kind == PatternKind.METHOD_CALL) {
return parseMethodCall(patternValue);
} else if (kind == PatternKind.IMPORT) {
return parseImport(patternValue);
} else if (kind == PatternKind.FIELD) {
return parseField(patternValue);
} else if (kind == PatternKind.CONSTRUCTOR) {
return parseConstructor(patternValue);
} else if (kind == PatternKind.METHOD_DECLARATION) {
return parseMethodDeclaration(patternValue);
} else if (kind == PatternKind.BLOCK) {
return parseBlock(patternValue);
} else if (kind == PatternKind.STATEMENT_SEQUENCE) {
return parseBlock(patternValue);
} else if (kind == PatternKind.DECLARATION) {
// DECLARATION is parsed as a regular statement; the pattern engine
// will only match it against VariableDeclarationStatement nodes.
return parseStatement(patternValue);
}
return null;
}
/**
* Parses an expression pattern.
*
* @param expressionSnippet the expression snippet (e.g., {@code "$x + 1"})
* @return the parsed Expression node, or {@code null} if parsing fails
*/
private Expression parseExpression(String expressionSnippet) {
// Wrap the expression in a minimal method context
String source = "class _Pattern { void _method() { _result = " + expressionSnippet + "; } }"; //$NON-NLS-1$ //$NON-NLS-2$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the expression: CompilationUnit -> TypeDeclaration -> MethodDeclaration -> Block -> ExpressionStatement -> Assignment -> right-hand side
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.getMethods().length == 0) {
return null;
}
MethodDeclaration method = typeDecl.getMethods()[0];
if (method.getBody() == null || method.getBody().statements().isEmpty()) {
return null;
}
Statement stmt = (Statement) method.getBody().statements().get(0);
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement) {
org.eclipse.jdt.core.dom.ExpressionStatement exprStmt = (org.eclipse.jdt.core.dom.ExpressionStatement) stmt;
Expression expr = exprStmt.getExpression();
// If it's an assignment, extract the right-hand side
if (expr instanceof org.eclipse.jdt.core.dom.Assignment) {
org.eclipse.jdt.core.dom.Assignment assignment = (org.eclipse.jdt.core.dom.Assignment) expr;
return assignment.getRightHandSide();
}
return expr;
}
return null;
}
/**
* Parses a statement pattern.
*
* @param statementSnippet the statement snippet (e.g., {@code "if ($cond) $then;"})
* @return the parsed Statement node, or {@code null} if parsing fails
*/
private Statement parseStatement(String statementSnippet) {
// Extract placeholder names and declare them as Object variables
// so the parser doesn't drop them as undeclared references.
StringBuilder declarations = new StringBuilder();
java.util.regex.Matcher m = java.util.regex.Pattern.compile("\\$[a-zA-Z_][a-zA-Z0-9_]*\\$?").matcher(statementSnippet); //$NON-NLS-1$
java.util.Set<String> declared = new java.util.HashSet<>();
while (m.find()) {
String placeholder = m.group();
if (!declared.contains(placeholder)) {
declarations.append("Object ").append(placeholder).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
declared.add(placeholder);
}
}
// Wrap the statement in a minimal method context with placeholder declarations as fields
String source = "class _Pattern { " + declarations + "void _method() { " + statementSnippet + " } }"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
parser.setStatementsRecovery(true);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the statement: CompilationUnit -> TypeDeclaration -> MethodDeclaration -> Block -> Statement
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.getMethods().length == 0) {
return null;
}
MethodDeclaration method = typeDecl.getMethods()[0];
if (method.getBody() == null || method.getBody().statements().isEmpty()) {
return null;
}
return (Statement) method.getBody().statements().get(0);
}
/**
* Parses an annotation pattern.
*
* @param annotationSnippet the annotation snippet (e.g., {@code "@Before"}, {@code "@Test(expected=$ex)"})
* @return the parsed Annotation node, or {@code null} if parsing fails
* @since 1.2.3
*/
private Annotation parseAnnotation(String annotationSnippet) {
// Wrap the annotation in a minimal class context
String source = annotationSnippet + " class _Pattern {}"; //$NON-NLS-1$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the annotation: CompilationUnit -> TypeDeclaration -> modifiers
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.modifiers().isEmpty()) {
return null;
}
Object firstModifier = typeDecl.modifiers().get(0);
if (!(firstModifier instanceof Annotation)) {
return null;
}
return (Annotation) firstModifier;
}
/**
* Parses a method call pattern.
*
* @param methodCallSnippet the method call snippet (e.g., {@code "Assert.assertEquals($a, $b)"})
* @return the parsed MethodInvocation node, or {@code null} if parsing fails
* @since 1.2.3
*/
private MethodInvocation parseMethodCall(String methodCallSnippet) {
// Wrap the method call in a minimal method context
String source = "class _Pattern { void _method() { " + methodCallSnippet + "; } }"; //$NON-NLS-1$ //$NON-NLS-2$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the method invocation: CompilationUnit -> TypeDeclaration -> MethodDeclaration -> Block -> ExpressionStatement -> Expression
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.getMethods().length == 0) {
return null;
}
MethodDeclaration method = typeDecl.getMethods()[0];
if (method.getBody() == null || method.getBody().statements().isEmpty()) {
return null;
}
Statement stmt = (Statement) method.getBody().statements().get(0);
if (stmt instanceof org.eclipse.jdt.core.dom.ExpressionStatement) {
org.eclipse.jdt.core.dom.ExpressionStatement exprStmt = (org.eclipse.jdt.core.dom.ExpressionStatement) stmt;
Expression expr = exprStmt.getExpression();
if (expr instanceof MethodInvocation) {
return (MethodInvocation) expr;
}
}
return null;
}
/**
* Parses an import pattern.
*
* @param importSnippet the import snippet (e.g., {@code "import org.junit.Assert"})
* @return the parsed ImportDeclaration node, or {@code null} if parsing fails
* @since 1.2.3
*/
private ImportDeclaration parseImport(String importSnippet) {
// Ensure the import statement ends with a semicolon
String importStatement = importSnippet.endsWith(";") ? importSnippet : importSnippet + ";"; //$NON-NLS-1$ //$NON-NLS-2$
String source = importStatement + " class _Pattern {}"; //$NON-NLS-1$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the import: CompilationUnit -> imports
if (cu.imports().isEmpty()) {
return null;
}
return (ImportDeclaration) cu.imports().get(0);
}
/**
* Parses a field pattern.
*
* @param fieldSnippet the field snippet (e.g., {@code "@Rule public TemporaryFolder $name"})
* @return the parsed FieldDeclaration node, or {@code null} if parsing fails
* @since 1.2.3
*/
private FieldDeclaration parseField(String fieldSnippet) {
// Ensure the field declaration ends with a semicolon
String fieldStatement = fieldSnippet.endsWith(";") ? fieldSnippet : fieldSnippet + ";"; //$NON-NLS-1$ //$NON-NLS-2$
String source = "class _Pattern { " + fieldStatement + " }"; //$NON-NLS-1$ //$NON-NLS-2$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the field: CompilationUnit -> TypeDeclaration -> FieldDeclaration
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.getFields().length == 0) {
return null;
}
return typeDecl.getFields()[0];
}
/**
* Parses a constructor pattern.
*
* @param constructorSnippet the constructor snippet (e.g., {@code "new String($bytes, $enc)"})
* @return the parsed ClassInstanceCreation node, or {@code null} if parsing fails
* @since 1.2.5
*/
private ClassInstanceCreation parseConstructor(String constructorSnippet) {
// Wrap the constructor expression in a minimal method context
String source = "class _Pattern { void _method() { Object _result = " + constructorSnippet + "; } }"; //$NON-NLS-1$ //$NON-NLS-2$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the constructor: CompilationUnit -> TypeDeclaration -> MethodDeclaration -> Block -> VariableDeclarationStatement -> VariableDeclarationFragment -> initializer (ClassInstanceCreation)
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.getMethods().length == 0) {
return null;
}
MethodDeclaration method = typeDecl.getMethods()[0];
if (method.getBody() == null || method.getBody().statements().isEmpty()) {
return null;
}
Statement stmt = (Statement) method.getBody().statements().get(0);
if (stmt instanceof org.eclipse.jdt.core.dom.VariableDeclarationStatement) {
org.eclipse.jdt.core.dom.VariableDeclarationStatement varDeclStmt = (org.eclipse.jdt.core.dom.VariableDeclarationStatement) stmt;
if (!varDeclStmt.fragments().isEmpty()) {
Object fragment = varDeclStmt.fragments().get(0);
if (fragment instanceof org.eclipse.jdt.core.dom.VariableDeclarationFragment) {
org.eclipse.jdt.core.dom.VariableDeclarationFragment varFrag = (org.eclipse.jdt.core.dom.VariableDeclarationFragment) fragment;
Expression initializer = varFrag.getInitializer();
if (initializer instanceof ClassInstanceCreation) {
return (ClassInstanceCreation) initializer;
}
}
}
}
return null;
}
/**
* Parses a method declaration pattern.
*
* @param methodSnippet the method declaration snippet (e.g., {@code "void dispose()"}, {@code "void $name($params$)"})
* @return the parsed MethodDeclaration node, or {@code null} if parsing fails
* @since 1.2.6
*/
private MethodDeclaration parseMethodDeclaration(String methodSnippet) {
// Normalize the snippet: add empty body if not present
String normalizedSnippet = methodSnippet.trim();
if (!normalizedSnippet.endsWith("}") && !normalizedSnippet.endsWith(";")) { //$NON-NLS-1$ //$NON-NLS-2$
normalizedSnippet = normalizedSnippet + " {}"; //$NON-NLS-1$
}
// Handle multi-placeholder parameters: "$params$" -> "Object... $params$"
// This makes the pattern syntactically valid for the Java parser
normalizedSnippet = normalizedSnippet.replaceAll("\\(\\s*\\$([a-zA-Z_][a-zA-Z0-9_]*)\\$\\s*\\)", "(Object... \\$$1\\$)"); //$NON-NLS-1$ //$NON-NLS-2$
// Wrap the method declaration in a class context
String source = "class _Pattern { " + normalizedSnippet + " }"; //$NON-NLS-1$ //$NON-NLS-2$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the method: CompilationUnit -> TypeDeclaration -> MethodDeclaration
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.getMethods().length == 0) {
return null;
}
return typeDecl.getMethods()[0];
}
/**
* Parses a block pattern containing multiple statements with potential variadic placeholders.
*
* @param blockSnippet the block snippet (e.g., {@code "{ $before$; return $x; }"})
* @return the parsed Block node, or {@code null} if parsing fails
* @since 1.3.2
*/
private Block parseBlock(String blockSnippet) {
// Ensure the block is wrapped in braces
String normalizedSnippet = blockSnippet.trim();
if (!normalizedSnippet.startsWith("{")) { //$NON-NLS-1$
normalizedSnippet = "{ " + normalizedSnippet + " }"; //$NON-NLS-1$ //$NON-NLS-2$
}
// Extract placeholder names (starting with $) from the snippet and declare them
// as Object variables so the parser doesn't drop them as undeclared references.
StringBuilder declarations = new StringBuilder();
java.util.regex.Matcher m = java.util.regex.Pattern.compile("\\$[a-zA-Z_][a-zA-Z0-9_]*\\$?").matcher(normalizedSnippet); //$NON-NLS-1$
java.util.Set<String> declared = new java.util.HashSet<>();
while (m.find()) {
String placeholder = m.group();
if (!declared.contains(placeholder)) {
declarations.append("Object ").append(placeholder).append("; "); //$NON-NLS-1$ //$NON-NLS-2$
declared.add(placeholder);
}
}
// Wrap the block in a minimal method context with placeholder declarations
// The declarations go into a separate setup method so they don't pollute the block
String source = "class _Pattern { " + declarations + "void _method() " + normalizedSnippet + " }"; //$NON-NLS-1$ //$NON-NLS-2$
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
parser.setSource(source.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setCompilerOptions(JavaCore.getOptions());
parser.setStatementsRecovery(true);
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
// Navigate to the block: CompilationUnit -> TypeDeclaration -> MethodDeclaration -> Block
if (cu.types().isEmpty()) {
return null;
}
TypeDeclaration typeDecl = (TypeDeclaration) cu.types().get(0);
if (typeDecl.getMethods().length == 0) {
return null;
}
MethodDeclaration method = typeDecl.getMethods()[0];
return method.getBody();
}
}