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.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 or Statement), 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);
}
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) {
// Wrap the statement in a minimal method context
String source = "class _Pattern { void _method() { " + statementSnippet + " } }"; //$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 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];
}
}