tessl install tessl/maven-io-cucumber--cucumber-expressions@19.0.0Cucumber Expressions are simple patterns for matching Step Definitions with Gherkin steps
The parsing API provides AST-based (Abstract Syntax Tree) parsing for Cucumber Expressions, enabling advanced expression analysis, manipulation, and tooling. This API is experimental and may change in future versions.
Note: This API is marked as @API(status = EXPERIMENTAL) since version 18.1 and is subject to change.
Parse Cucumber Expressions into an Abstract Syntax Tree representation.
package io.cucumber.cucumberexpressions;
import org.apiguardian.api.API;
/**
* Parse cucumber expressions into AST
* Experimental API - subject to change
* Stateless parser - thread-safe
*/
@API(since = "18.1", status = API.Status.EXPERIMENTAL)
public final class CucumberExpressionParser {
/**
* Create a new parser
* No configuration required
*/
public CucumberExpressionParser();
/**
* Parse an expression into an AST
* Thread-safe operation - stateless parser
* @param expression - Cucumber Expression string to parse
* @return Root Node representing the expression structure
*/
public Node parse(String expression);
}Usage Examples:
import io.cucumber.cucumberexpressions.*;
// Create parser
CucumberExpressionParser parser = new CucumberExpressionParser();
// Parse simple expression
Node ast = parser.parse("I have {int} cucumbers");
// Analyze AST structure
System.out.println("AST: " + ast.toString());
// JSON representation of the AST
// Access root node properties
Node.Type type = ast.type();
// type = Node.Type.EXPRESSION_NODE
List<Node> children = ast.nodes();
// Children contain text nodes and parameter nodesComplex Expression:
// Parse expression with optional and alternative text
String expr = "I have {int} cucumber(s) in my belly/stomach";
Node ast = parser.parse(expr);
// Traverse AST
for (Node child : ast.nodes()) {
switch (child.type()) {
case TEXT_NODE:
System.out.println("Text: " + child.token());
break;
case PARAMETER_NODE:
System.out.println("Parameter: " + child.token());
break;
case OPTIONAL_NODE:
System.out.println("Optional: " + child.token());
break;
case ALTERNATION_NODE:
System.out.println("Alternation with " + child.nodes().size() + " alternatives");
break;
}
}AST node representing parsed expression structure with position information.
package io.cucumber.cucumberexpressions;
import org.apiguardian.api.API;
import org.jspecify.annotations.Nullable;
import java.util.List;
/**
* AST node representing parsed expression structure
* Immutable value object with position information
* Experimental API - subject to change
*/
@API(since = "18.1", status = API.Status.EXPERIMENTAL)
public final class Node implements Located {
/**
* Node type enumeration
*/
public enum Type {
/** Plain text node */
TEXT_NODE,
/** Optional text node (parentheses) */
OPTIONAL_NODE,
/** Alternation container node */
ALTERNATION_NODE,
/** Single alternative within alternation */
ALTERNATIVE_NODE,
/** Parameter type reference node */
PARAMETER_NODE,
/** Root expression node */
EXPRESSION_NODE
}
/**
* Get start position in source expression
* @return Zero-based start index (inclusive)
*/
public int start();
/**
* Get end position in source expression
* End position is exclusive
* @return Zero-based end index (exclusive)
*/
public int end();
/**
* Get child nodes
* Null for leaf nodes (TEXT_NODE, PARAMETER_NODE)
* @return List of child nodes, or null for leaf nodes
*/
@Nullable
public List<Node> nodes();
/**
* Get node type
* @return Node type enum value
*/
public Node.Type type();
/**
* Get token text
* Contains text for TEXT_NODE and OPTIONAL_NODE
* Returns null for container nodes (EXPRESSION_NODE, ALTERNATION_NODE, ALTERNATIVE_NODE)
* Returns null for PARAMETER_NODE (use position and source text to extract parameter type name)
* @return Token text or null
*/
@Nullable
public String token();
/**
* JSON representation of the AST
* @return JSON string representation
*/
public String toString();
/**
* Equality check
* @param o - Object to compare
* @return true if equal
*/
public boolean equals(Object o);
/**
* Hash code
* @return Hash code value
*/
public int hashCode();
}Marker interface for types with position information.
package io.cucumber.cucumberexpressions;
/**
* Marker interface for types with position information
*/
public interface Located {
/**
* Get start position
* @return Zero-based start index (inclusive)
*/
int start();
/**
* Get end position
* @return Zero-based end index (exclusive)
*/
int end();
}EXPRESSION_NODE:
Root node of the AST, contains all top-level elements.
Node ast = parser.parse("I have {int} cucumbers");
assert ast.type() == Node.Type.EXPRESSION_NODE;
assert ast.token() == null; // Container node
assert ast.nodes() != null; // Has childrenTEXT_NODE:
Plain text content.
Node ast = parser.parse("I have {int} cucumbers");
Node firstChild = ast.nodes().get(0);
assert firstChild.type() == Node.Type.TEXT_NODE;
assert firstChild.token().equals("I have ");
assert firstChild.nodes() == null; // Leaf nodePARAMETER_NODE:
Parameter type reference like {int} or {color}.
Note: token() returns null for PARAMETER_NODE types. To identify the parameter type name, extract it from the source expression using the node's position information.
String expression = "I have {int} cucumbers";
Node ast = parser.parse(expression);
Node paramNode = ast.nodes().get(1);
assert paramNode.type() == Node.Type.PARAMETER_NODE;
assert paramNode.token() == null; // token() returns null for parameter nodes
assert paramNode.nodes() == null; // Leaf node
// To get the parameter type name, extract from source using position:
int start = paramNode.start();
int end = paramNode.end();
String parameterText = expression.substring(start, end);
// parameterText = "{int}"
// To get just the type name without braces:
String typeName = parameterText.substring(1, parameterText.length() - 1);
// typeName = "int"OPTIONAL_NODE:
Optional text enclosed in parentheses.
Node ast = parser.parse("I have {int} cucumber(s)");
// Find optional node
Node optionalNode = findNodeByType(ast, Node.Type.OPTIONAL_NODE);
assert optionalNode.token().equals("s");ALTERNATION_NODE:
Container for alternative text options.
Node ast = parser.parse("I have {int} in my belly/stomach");
// Find alternation node
Node alternationNode = findNodeByType(ast, Node.Type.ALTERNATION_NODE);
assert alternationNode.nodes() != null;
assert alternationNode.nodes().size() == 2; // Two alternativesALTERNATIVE_NODE:
Single alternative within an alternation.
Node ast = parser.parse("I have {int} in my belly/stomach");
Node alternationNode = findNodeByType(ast, Node.Type.ALTERNATION_NODE);
Node alt1 = alternationNode.nodes().get(0);
Node alt2 = alternationNode.nodes().get(1);
assert alt1.type() == Node.Type.ALTERNATIVE_NODE;
assert alt2.type() == Node.Type.ALTERNATIVE_NODE;Recursive Traversal:
import io.cucumber.cucumberexpressions.*;
import java.util.List;
public class ASTVisitor {
public void visit(Node node, int depth) {
String indent = " ".repeat(depth);
System.out.println(indent + node.type() +
(node.token() != null ? ": " + node.token() : ""));
if (node.nodes() != null) {
for (Node child : node.nodes()) {
visit(child, depth + 1);
}
}
}
public static void main(String[] args) {
CucumberExpressionParser parser = new CucumberExpressionParser();
Node ast = parser.parse("I have {int} cucumber(s) in my belly/stomach");
ASTVisitor visitor = new ASTVisitor();
visitor.visit(ast, 0);
}
}
// Output:
// EXPRESSION_NODE
// TEXT_NODE: I have
// PARAMETER_NODE
// TEXT_NODE: cucumber
// OPTIONAL_NODE: s
// TEXT_NODE: in my
// ALTERNATION_NODE
// ALTERNATIVE_NODE
// TEXT_NODE: belly
// ALTERNATIVE_NODE
// TEXT_NODE: stomachCollecting Specific Nodes:
public List<Node> findNodesByType(Node root, Node.Type targetType) {
List<Node> results = new ArrayList<>();
collectNodesByType(root, targetType, results);
return results;
}
private void collectNodesByType(Node node, Node.Type targetType, List<Node> results) {
if (node.type() == targetType) {
results.add(node);
}
if (node.nodes() != null) {
for (Node child : node.nodes()) {
collectNodesByType(child, targetType, results);
}
}
}
// Usage
Node ast = parser.parse("I have {int} items and {string} name");
List<Node> paramNodes = findNodesByType(ast, Node.Type.PARAMETER_NODE);
// paramNodes contains nodes for "int" and "string"Use position information for error reporting, highlighting, or transformation:
String expression = "I have {int} cucumbers";
Node ast = parser.parse(expression);
// Find parameter node
List<Node> params = findNodesByType(ast, Node.Type.PARAMETER_NODE);
Node paramNode = params.get(0);
// Get position
int start = paramNode.start();
int end = paramNode.end();
// Extract from source
String paramText = expression.substring(start, end);
// paramText = "{int}"
// Highlight in source
String before = expression.substring(0, start);
String after = expression.substring(end);
String highlighted = before + "[" + paramText + "]" + after;
// highlighted = "I have [{int}] cucumbers"Extract All Parameters:
public List<String> extractParameterTypes(String expression) {
CucumberExpressionParser parser = new CucumberExpressionParser();
Node ast = parser.parse(expression);
List<String> paramTypes = new ArrayList<>();
collectParameterTypes(ast, expression, paramTypes);
return paramTypes;
}
private void collectParameterTypes(Node node, String expression, List<String> types) {
if (node.type() == Node.Type.PARAMETER_NODE) {
// Extract parameter type name from source using position
String paramText = expression.substring(node.start(), node.end());
// Remove braces to get type name: "{int}" -> "int"
String typeName = paramText.substring(1, paramText.length() - 1);
types.add(typeName);
}
if (node.nodes() != null) {
for (Node child : node.nodes()) {
collectParameterTypes(child, expression, types);
}
}
}
// Usage
List<String> types = extractParameterTypes("User {string} has {int} items");
// types = ["string", "int"]Validate Expression Structure:
public List<String> validateExpression(String expression) {
List<String> errors = new ArrayList<>();
try {
CucumberExpressionParser parser = new CucumberExpressionParser();
Node ast = parser.parse(expression);
// Check for empty parameters
List<Node> params = findNodesByType(ast, Node.Type.PARAMETER_NODE);
for (Node param : params) {
// Extract parameter text from source using position
String paramText = expression.substring(param.start(), param.end());
// Check if empty: "{}"
if (paramText.equals("{}")) {
errors.add("Anonymous parameter at position " + param.start());
}
}
// Check for nested optional/alternation (if not allowed)
validateNoNestedOptionals(ast, errors);
} catch (Exception e) {
errors.add("Parse error: " + e.getMessage());
}
return errors;
}Transform Expression:
public String replaceParameters(String expression, String targetType) {
CucumberExpressionParser parser = new CucumberExpressionParser();
Node ast = parser.parse(expression);
StringBuilder result = new StringBuilder();
int lastEnd = 0;
List<Node> params = findNodesByType(ast, Node.Type.PARAMETER_NODE);
for (Node param : params) {
// Copy text before parameter
result.append(expression, lastEnd, param.start());
// Replace parameter
result.append("{").append(targetType).append("}");
lastEnd = param.end();
}
// Copy remaining text
result.append(expression.substring(lastEnd));
return result.toString();
}
// Usage
String transformed = replaceParameters("I have {int} and {float}", "number");
// transformed = "I have {number} and {number}"The toString() method returns a JSON representation of the AST:
CucumberExpressionParser parser = new CucumberExpressionParser();
Node ast = parser.parse("I have {int} cucumbers");
String json = ast.toString();
System.out.println(json);
// Output (formatted):
// {
// "type": "EXPRESSION_NODE",
// "start": 0,
// "end": 21,
// "nodes": [
// {
// "type": "TEXT_NODE",
// "start": 0,
// "end": 7,
// "token": "I have "
// },
// {
// "type": "PARAMETER_NODE",
// "start": 7,
// "end": 12,
// "token": null
// },
// {
// "type": "TEXT_NODE",
// "start": 12,
// "end": 21,
// "token": " cucumbers"
// }
// ]
// }
// Note: token is null for PARAMETER_NODE - use start/end to extract "{int}" from sourceImportant Considerations:
Checking API Status:
// The experimental annotation is part of the API Guardian annotations
import org.apiguardian.api.API;
// Check class annotation
API annotation = CucumberExpressionParser.class.getAnnotation(API.class);
if (annotation != null) {
System.out.println("Status: " + annotation.status());
System.out.println("Since: " + annotation.since());
}
// Status: EXPERIMENTAL
// Since: 18.1IDE Integration:
Code Generation:
Expression Analysis:
Expression Transformation: