Skip to content

Commit eb1e717

Browse files
authored
Improve error message for missing operands (#221)
Improved the error message for cases where an operand is missing. Unfortunately the parsing algorithm can't detect which operator is missing operands so the error message must remain fairly generic.
1 parent a7f794d commit eb1e717

File tree

10 files changed

+133
-82
lines changed

10 files changed

+133
-82
lines changed

.github/workflows/test-go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
test-go:
2424
runs-on: ${{ matrix.os }}
2525
strategy:
26-
fail-fast: false
26+
fail-fast: true
2727
matrix:
2828
os:
2929
- ubuntu-latest

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
9+
### Fixed
10+
- Improve error message for missing operands ([#221](https://github.com/cucumber/tag-expressions/pull/221))
911

1012
## [8.0.0] - 2025-10-14
1113
### Fixed

go/parser.go

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ func Parse(infix string) (Evaluatable, error) {
4242
isOp(operators.Peek()) &&
4343
((ASSOC[token] == "left" && PREC[token] <= PREC[operators.Peek()]) ||
4444
(ASSOC[token] == "right" && PREC[token] < PREC[operators.Peek()])) {
45-
pushExpr(operators.Pop(), expressions)
45+
if err := pushExpr(infix, operators.Pop(), expressions); err != nil {
46+
return nil, err
47+
}
4648
}
4749
operators.Push(token)
4850
expectedTokenType = OPERAND
@@ -57,7 +59,9 @@ func Parse(infix string) (Evaluatable, error) {
5759
return nil, err
5860
}
5961
for operators.Len() > 0 && operators.Peek() != "(" {
60-
pushExpr(operators.Pop(), expressions)
62+
if err := pushExpr(infix, operators.Pop(), expressions); err != nil {
63+
return nil, err
64+
}
6165
}
6266
if operators.Len() == 0 {
6367
return nil, fmt.Errorf("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched ).", infix)
@@ -70,7 +74,9 @@ func Parse(infix string) (Evaluatable, error) {
7074
if err := check(infix, expectedTokenType, OPERAND); err != nil {
7175
return nil, err
7276
}
73-
pushExpr(token, expressions)
77+
if err := pushExpr(infix, token, expressions); err != nil {
78+
return nil, err
79+
}
7480
expectedTokenType = OPERATOR
7581
}
7682
}
@@ -79,7 +85,9 @@ func Parse(infix string) (Evaluatable, error) {
7985
if operators.Peek() == "(" {
8086
return nil, fmt.Errorf("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched (.", infix)
8187
}
82-
pushExpr(operators.Pop(), expressions)
88+
if err := pushExpr(infix, operators.Pop(), expressions); err != nil {
89+
return nil, err
90+
}
8391
}
8492

8593
return expressions.Pop(), nil
@@ -153,24 +161,51 @@ func check(infix, expectedTokenType, tokenType string) error {
153161
return nil
154162
}
155163

156-
func pushExpr(token string, stack *EvaluatableStack) {
164+
func pushExpr(infix string, token string, stack *EvaluatableStack) error {
157165
if token == "and" {
158-
rightAndExpr := stack.Pop()
166+
rightAndExpr, err := popOperand(infix, stack)
167+
if err != nil {
168+
return err
169+
}
170+
leftAndExpr, err := popOperand(infix, stack)
171+
if err != nil {
172+
return err
173+
}
159174
stack.Push(&andExpr{
160-
leftExpr: stack.Pop(),
175+
leftExpr: leftAndExpr,
161176
rightExpr: rightAndExpr,
162177
})
163178
} else if token == "or" {
164-
rightOrExpr := stack.Pop()
179+
rightOrExpr, err := popOperand(infix, stack)
180+
if err != nil {
181+
return err
182+
}
183+
leftOrExpr, err := popOperand(infix, stack)
184+
if err != nil {
185+
return err
186+
}
165187
stack.Push(&orExpr{
166-
leftExpr: stack.Pop(),
188+
leftExpr: leftOrExpr,
167189
rightExpr: rightOrExpr,
168190
})
169191
} else if token == "not" {
170-
stack.Push(&notExpr{expr: stack.Pop()})
192+
expr, err := popOperand(infix, stack)
193+
if err != nil {
194+
return err
195+
}
196+
stack.Push(&notExpr{expr: expr})
171197
} else {
172198
stack.Push(&literalExpr{value: token})
173199
}
200+
201+
return nil
202+
}
203+
204+
func popOperand(infix string, stack *EvaluatableStack) (Evaluatable, error) {
205+
if stack.Len() > 0 {
206+
return stack.Pop(), nil
207+
}
208+
return nil, fmt.Errorf("Tag expression \"%s\" could not be parsed because of syntax error: Expected operand.", infix)
174209
}
175210

176211
type literalExpr struct {

java/src/main/java/io/cucumber/tagexpressions/TagExpressionParser.java

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ private Expression parse() {
5252
||
5353
(ASSOC.get(token) == Assoc.RIGHT && PREC.get(token) < PREC.get(operators.peek())))
5454
) {
55-
pushExpr(pop(operators), expressions);
55+
pushExpr(operators.pop(), expressions);
5656
}
5757
operators.push(token);
5858
expectedTokenType = TokenType.OPERAND;
@@ -63,13 +63,13 @@ private Expression parse() {
6363
} else if (")".equals(token)) {
6464
check(expectedTokenType, TokenType.OPERATOR);
6565
while (operators.size() > 0 && !"(".equals(operators.peek())) {
66-
pushExpr(pop(operators), expressions);
66+
pushExpr(operators.pop(), expressions);
6767
}
6868
if (operators.size() == 0) {
6969
throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched ).", this.infix);
7070
}
7171
if ("(".equals(operators.peek())) {
72-
pop(operators);
72+
operators.pop();
7373
}
7474
expectedTokenType = TokenType.OPERATOR;
7575
} else {
@@ -83,7 +83,7 @@ private Expression parse() {
8383
if ("(".equals(operators.peek())) {
8484
throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Unmatched (.", infix);
8585
}
86-
pushExpr(pop(operators), expressions);
86+
pushExpr(operators.pop(), expressions);
8787
}
8888

8989
return expressions.pop();
@@ -128,31 +128,34 @@ private void check(TokenType expectedTokenType, TokenType tokenType) {
128128
}
129129
}
130130

131-
private <T> T pop(Deque<T> stack) {
132-
if (stack.isEmpty())
133-
throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of an empty stack", infix);
134-
return stack.pop();
135-
}
136-
137-
private void pushExpr(String token, Deque<Expression> stack) {
131+
private void pushExpr(String token, Deque<Expression> expressions) {
138132
switch (token) {
139133
case "and":
140-
Expression rightAndExpr = pop(stack);
141-
stack.push(new And(pop(stack), rightAndExpr));
134+
Expression rightAndExpr = popOperand(expressions);
135+
Expression leftAndExpr = popOperand(expressions);
136+
expressions.push(new And(leftAndExpr, rightAndExpr));
142137
break;
143138
case "or":
144-
Expression rightOrExpr = pop(stack);
145-
stack.push(new Or(pop(stack), rightOrExpr));
139+
Expression rightOrExpr = popOperand(expressions);
140+
Expression leftOrExpr = popOperand(expressions);
141+
expressions.push(new Or(leftOrExpr, rightOrExpr));
146142
break;
147143
case "not":
148-
stack.push(new Not(pop(stack)));
144+
Expression expression = popOperand(expressions);
145+
expressions.push(new Not(expression));
149146
break;
150147
default:
151-
stack.push(new Literal(token));
148+
expressions.push(new Literal(token));
152149
break;
153150
}
154151
}
155152

153+
private <T> T popOperand(Deque<T> stack) {
154+
if (stack.isEmpty())
155+
throw new TagExpressionException("Tag expression \"%s\" could not be parsed because of syntax error: Expected operand.", infix);
156+
return stack.pop();
157+
}
158+
156159
private boolean isUnary(String token) {
157160
return "not".equals(token);
158161
}

javascript/src/index.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export default function parse(infix: string): Node {
4141
((ASSOC[token] === 'left' && PREC[token] <= PREC[peek(operators)]) ||
4242
(ASSOC[token] === 'right' && PREC[token] < PREC[peek(operators)]))
4343
) {
44-
pushExpr(pop(operators), expressions)
44+
pushExpr(operators.pop() as string, expressions)
4545
}
4646
operators.push(token)
4747
expectedTokenType = OPERAND
@@ -52,15 +52,15 @@ export default function parse(infix: string): Node {
5252
} else if (')' === token) {
5353
check(expectedTokenType, OPERATOR)
5454
while (operators.length > 0 && peek(operators) !== '(') {
55-
pushExpr(pop(operators), expressions)
55+
pushExpr(operators.pop() as string, expressions)
5656
}
5757
if (operators.length === 0) {
5858
throw new Error(
5959
`Tag expression "${infix}" could not be parsed because of syntax error: Unmatched ).`
6060
)
6161
}
6262
if (peek(operators) === '(') {
63-
pop(operators)
63+
operators.pop()
6464
}
6565
expectedTokenType = OPERATOR
6666
} else {
@@ -76,10 +76,10 @@ export default function parse(infix: string): Node {
7676
`Tag expression "${infix}" could not be parsed because of syntax error: Unmatched (.`
7777
)
7878
}
79-
pushExpr(pop(operators), expressions)
79+
pushExpr(operators.pop() as string, expressions)
8080
}
8181

82-
return pop(expressions)
82+
return expressions.pop() as Node
8383

8484
function check(expectedTokenType: string, tokenType: string) {
8585
if (expectedTokenType !== tokenType) {
@@ -88,6 +88,29 @@ export default function parse(infix: string): Node {
8888
)
8989
}
9090
}
91+
92+
function pushExpr(token: string, stack: Node[]) {
93+
if (token === 'and') {
94+
const rightAndExpr = popOperand(stack)
95+
stack.push(new And(popOperand(stack), rightAndExpr))
96+
} else if (token === 'or') {
97+
const rightOrExpr = popOperand(stack)
98+
stack.push(new Or(popOperand(stack), rightOrExpr))
99+
} else if (token === 'not') {
100+
stack.push(new Not(popOperand(stack)))
101+
} else {
102+
stack.push(new Literal(token))
103+
}
104+
}
105+
106+
function popOperand<T>(stack: T[]): T {
107+
if (stack.length === 0) {
108+
throw new Error(
109+
`Tag expression "${infix}" could not be parsed because of syntax error: Expected operand.`
110+
)
111+
}
112+
return stack.pop() as T
113+
}
91114
}
92115

93116
function tokenize(expr: string): string[] {
@@ -141,27 +164,6 @@ function peek(stack: string[]) {
141164
return stack[stack.length - 1]
142165
}
143166

144-
function pop<T>(stack: T[]): T {
145-
if (stack.length === 0) {
146-
throw new Error('empty stack')
147-
}
148-
return stack.pop() as T
149-
}
150-
151-
function pushExpr(token: string, stack: Node[]) {
152-
if (token === 'and') {
153-
const rightAndExpr = pop(stack)
154-
stack.push(new And(pop(stack), rightAndExpr))
155-
} else if (token === 'or') {
156-
const rightOrExpr = pop(stack)
157-
stack.push(new Or(pop(stack), rightOrExpr))
158-
} else if (token === 'not') {
159-
stack.push(new Not(pop(stack)))
160-
} else {
161-
stack.push(new Literal(token))
162-
}
163-
}
164-
165167
interface Node {
166168
evaluate(variables: string[]): boolean
167169
}

perl/lib/Cucumber/TagExpressions.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ sub _term_expr {
104104

105105
my $token = _get_token( $state );
106106

107-
die 'Unexpected end of input parsing tag expression'
107+
die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operand.}
108108
if not defined $token;
109109

110110
if ( $token eq '(' ) {

perl/t/02-evaluate.t

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ for my $ex (@good) {
103103
my %bad_syntax = (
104104
'@a @b' => q{Expected operator.},
105105
'@a not' => q{Expected operator.},
106-
'@a or' => 'Unexpected end of input parsing tag expression',
106+
'@a or' => q{Expected operand.},
107107
'@a not @b' => q{Expected operator.},
108-
'@a or (' => 'Unexpected end of input parsing tag expression',
108+
'@a or (' => q{Expected operand.},
109109
'@a and @b)' => q{Unmatched ).},
110110
"\@a\\" => q{Illegal escape before "<end-of-input>"},
111111
);

0 commit comments

Comments
 (0)