cucumber-cucumber-expressions 10.2.0 → 11.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rsync +1 -0
- data/VERSION +1 -1
- data/cucumber-cucumber-expressions.gemspec +2 -2
- data/default.mk +12 -0
- data/examples.txt +13 -1
- data/lib/cucumber/cucumber_expressions/ast.rb +201 -0
- data/lib/cucumber/cucumber_expressions/cucumber_expression.rb +80 -76
- data/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb +4 -8
- data/lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb +219 -0
- data/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rb +95 -0
- data/lib/cucumber/cucumber_expressions/errors.rb +175 -4
- data/lib/cucumber/cucumber_expressions/group.rb +1 -1
- data/lib/cucumber/cucumber_expressions/parameter_type.rb +12 -7
- data/lib/cucumber/cucumber_expressions/parameter_type_matcher.rb +2 -2
- data/lib/cucumber/cucumber_expressions/parameter_type_registry.rb +1 -1
- data/lib/cucumber/cucumber_expressions/tree_regexp.rb +53 -46
- data/spec/cucumber/cucumber_expressions/cucumber_expression_parser_spec.rb +24 -0
- data/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb +73 -124
- data/spec/cucumber/cucumber_expressions/cucumber_expression_tokenizer_spec.rb +24 -0
- data/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb +1 -1
- data/spec/cucumber/cucumber_expressions/expression_examples_spec.rb +1 -1
- data/spec/cucumber/cucumber_expressions/expression_factory_spec.rb +0 -4
- data/spec/cucumber/cucumber_expressions/tree_regexp_spec.rb +58 -5
- data/testdata/ast/alternation-followed-by-optional.yaml +17 -0
- data/testdata/ast/alternation-phrase.yaml +16 -0
- data/testdata/ast/alternation-with-parameter.yaml +27 -0
- data/testdata/ast/alternation-with-unused-end-optional.yaml +15 -0
- data/testdata/ast/alternation-with-unused-start-optional.yaml +8 -0
- data/testdata/ast/alternation-with-white-space.yaml +12 -0
- data/testdata/ast/alternation.yaml +12 -0
- data/testdata/ast/anonymous-parameter.yaml +5 -0
- data/testdata/ast/closing-brace.yaml +5 -0
- data/testdata/ast/closing-parenthesis.yaml +5 -0
- data/testdata/ast/empty-alternation.yaml +8 -0
- data/testdata/ast/empty-alternations.yaml +9 -0
- data/testdata/ast/empty-string.yaml +3 -0
- data/testdata/ast/escaped-alternation.yaml +5 -0
- data/testdata/ast/escaped-backslash.yaml +5 -0
- data/testdata/ast/escaped-opening-parenthesis.yaml +5 -0
- data/testdata/ast/escaped-optional-followed-by-optional.yaml +15 -0
- data/testdata/ast/escaped-optional-phrase.yaml +10 -0
- data/testdata/ast/escaped-optional.yaml +7 -0
- data/testdata/ast/opening-brace.yaml +8 -0
- data/testdata/ast/opening-parenthesis.yaml +8 -0
- data/testdata/ast/optional-containing-nested-optional.yaml +15 -0
- data/testdata/ast/optional-phrase.yaml +12 -0
- data/testdata/ast/optional.yaml +7 -0
- data/testdata/ast/parameter.yaml +7 -0
- data/testdata/ast/phrase.yaml +9 -0
- data/testdata/ast/unfinished-parameter.yaml +8 -0
- data/testdata/expression/allows-escaped-optional-parameter-types.yaml +4 -0
- data/testdata/expression/allows-parameter-type-in-alternation-1.yaml +4 -0
- data/testdata/expression/allows-parameter-type-in-alternation-2.yaml +4 -0
- data/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml +5 -0
- data/testdata/expression/does-not-allow-alternation-in-optional.yaml +9 -0
- data/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml +10 -0
- data/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml +9 -0
- data/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml +9 -0
- data/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml +9 -0
- data/testdata/expression/does-not-allow-empty-optional.yaml +9 -0
- data/testdata/expression/does-not-allow-nested-optional.yaml +8 -0
- data/testdata/expression/does-not-allow-optional-parameter-types.yaml +9 -0
- data/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml +10 -0
- data/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml +8 -0
- data/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml +8 -0
- data/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml +8 -0
- data/testdata/expression/does-not-match-misquoted-string.yaml +4 -0
- data/testdata/expression/doesnt-match-float-as-int.yaml +5 -0
- data/testdata/expression/matches-alternation.yaml +4 -0
- data/testdata/expression/matches-anonymous-parameter-type.yaml +5 -0
- data/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml +4 -0
- data/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml +4 -0
- data/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml +4 -0
- data/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml +4 -0
- data/testdata/expression/matches-double-quoted-string.yaml +4 -0
- data/testdata/expression/matches-doubly-escaped-parenthesis.yaml +4 -0
- data/testdata/expression/matches-doubly-escaped-slash-1.yaml +4 -0
- data/testdata/expression/matches-doubly-escaped-slash-2.yaml +4 -0
- data/testdata/expression/matches-escaped-parenthesis-1.yaml +4 -0
- data/testdata/expression/matches-escaped-parenthesis-2.yaml +4 -0
- data/testdata/expression/matches-escaped-parenthesis-3.yaml +4 -0
- data/testdata/expression/matches-escaped-slash.yaml +4 -0
- data/testdata/expression/matches-float-1.yaml +5 -0
- data/testdata/expression/matches-float-2.yaml +5 -0
- data/testdata/expression/matches-int.yaml +5 -0
- data/testdata/expression/matches-multiple-double-quoted-strings.yaml +4 -0
- data/testdata/expression/matches-multiple-single-quoted-strings.yaml +4 -0
- data/testdata/expression/matches-optional-before-alternation-1.yaml +4 -0
- data/testdata/expression/matches-optional-before-alternation-2.yaml +4 -0
- data/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml +4 -0
- data/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml +4 -0
- data/testdata/expression/matches-optional-in-alternation-1.yaml +5 -0
- data/testdata/expression/matches-optional-in-alternation-2.yaml +5 -0
- data/testdata/expression/matches-optional-in-alternation-3.yaml +5 -0
- data/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml +4 -0
- data/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml +4 -0
- data/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml +4 -0
- data/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml +4 -0
- data/testdata/expression/matches-single-quoted-string.yaml +4 -0
- data/testdata/expression/matches-word.yaml +4 -0
- data/testdata/expression/throws-unknown-parameter-type.yaml +10 -0
- data/testdata/regex/alternation-with-optional.yaml +2 -0
- data/testdata/regex/alternation.yaml +2 -0
- data/testdata/regex/empty.yaml +2 -0
- data/testdata/regex/escape-regex-characters.yaml +2 -0
- data/testdata/regex/optional.yaml +2 -0
- data/testdata/regex/parameter.yaml +2 -0
- data/testdata/regex/text.yaml +2 -0
- data/testdata/regex/unicode.yaml +2 -0
- data/testdata/tokens/alternation-phrase.yaml +13 -0
- data/testdata/tokens/alternation.yaml +9 -0
- data/testdata/tokens/empty-string.yaml +6 -0
- data/testdata/tokens/escape-non-reserved-character.yaml +8 -0
- data/testdata/tokens/escaped-alternation.yaml +9 -0
- data/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml +9 -0
- data/testdata/tokens/escaped-end-of-line.yaml +8 -0
- data/testdata/tokens/escaped-optional.yaml +7 -0
- data/testdata/tokens/escaped-parameter.yaml +7 -0
- data/testdata/tokens/escaped-space.yaml +7 -0
- data/testdata/tokens/optional-phrase.yaml +13 -0
- data/testdata/tokens/optional.yaml +9 -0
- data/testdata/tokens/parameter-phrase.yaml +13 -0
- data/testdata/tokens/parameter.yaml +9 -0
- data/testdata/tokens/phrase.yaml +11 -0
- metadata +118 -13
- data/spec/cucumber/cucumber_expressions/cucumber_expression_regexp_spec.rb +0 -57
@@ -59,9 +59,7 @@ module Cucumber
|
|
59
59
|
break
|
60
60
|
end
|
61
61
|
|
62
|
-
if pos >= text.length
|
63
|
-
break
|
64
|
-
end
|
62
|
+
break if pos >= text.length
|
65
63
|
end
|
66
64
|
|
67
65
|
expression_template += escape(text.slice(pos..-1))
|
@@ -85,19 +83,17 @@ module Cucumber
|
|
85
83
|
end
|
86
84
|
|
87
85
|
def create_parameter_type_matchers2(parameter_type, text)
|
88
|
-
result = []
|
89
86
|
regexps = parameter_type.regexps
|
90
|
-
regexps.
|
87
|
+
regexps.map do |regexp|
|
91
88
|
regexp = Regexp.new("(#{regexp})")
|
92
|
-
|
89
|
+
ParameterTypeMatcher.new(parameter_type, regexp, text, 0)
|
93
90
|
end
|
94
|
-
result
|
95
91
|
end
|
96
92
|
|
97
93
|
def escape(s)
|
98
94
|
s.gsub(/%/, '%%')
|
99
95
|
.gsub(/\(/, '\\(')
|
100
|
-
.gsub(
|
96
|
+
.gsub(/{/, '\\{')
|
101
97
|
.gsub(/\//, '\\/')
|
102
98
|
end
|
103
99
|
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
require 'cucumber/cucumber_expressions/ast'
|
2
|
+
require 'cucumber/cucumber_expressions/errors'
|
3
|
+
require 'cucumber/cucumber_expressions/cucumber_expression_tokenizer'
|
4
|
+
|
5
|
+
module Cucumber
|
6
|
+
module CucumberExpressions
|
7
|
+
class CucumberExpressionParser
|
8
|
+
def parse(expression)
|
9
|
+
# text := whitespace | ')' | '}' | .
|
10
|
+
parse_text = lambda do |_, tokens, current|
|
11
|
+
token = tokens[current]
|
12
|
+
case token.type
|
13
|
+
when TokenType::WHITE_SPACE, TokenType::TEXT, TokenType::END_PARAMETER, TokenType::END_OPTIONAL
|
14
|
+
return 1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)]
|
15
|
+
when TokenType::ALTERNATION
|
16
|
+
raise AlternationNotAllowedInOptional.new(expression, token)
|
17
|
+
when TokenType::BEGIN_PARAMETER, TokenType::START_OF_LINE, TokenType::END_OF_LINE, TokenType::BEGIN_OPTIONAL
|
18
|
+
else
|
19
|
+
# If configured correctly this will never happen
|
20
|
+
return 0, nil
|
21
|
+
end
|
22
|
+
# If configured correctly this will never happen
|
23
|
+
return 0, nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# name := whitespace | .
|
27
|
+
parse_name = lambda do |_, tokens, current|
|
28
|
+
token = tokens[current]
|
29
|
+
case token.type
|
30
|
+
when TokenType::WHITE_SPACE, TokenType::TEXT
|
31
|
+
return 1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)]
|
32
|
+
when TokenType::BEGIN_PARAMETER, TokenType::END_PARAMETER, TokenType::BEGIN_OPTIONAL, TokenType::END_OPTIONAL, TokenType::ALTERNATION
|
33
|
+
raise InvalidParameterTypeNameInNode.new(expression, token)
|
34
|
+
when TokenType::START_OF_LINE, TokenType::END_OF_LINE
|
35
|
+
# If configured correctly this will never happen
|
36
|
+
return 0, nil
|
37
|
+
else
|
38
|
+
# If configured correctly this will never happen
|
39
|
+
return 0, nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# parameter := '{' + name* + '}'
|
44
|
+
parse_parameter = parse_between(
|
45
|
+
NodeType::PARAMETER,
|
46
|
+
TokenType::BEGIN_PARAMETER,
|
47
|
+
TokenType::END_PARAMETER,
|
48
|
+
[parse_name]
|
49
|
+
)
|
50
|
+
|
51
|
+
# optional := '(' + option* + ')'
|
52
|
+
# option := optional | parameter | text
|
53
|
+
optional_sub_parsers = []
|
54
|
+
parse_optional = parse_between(
|
55
|
+
NodeType::OPTIONAL,
|
56
|
+
TokenType::BEGIN_OPTIONAL,
|
57
|
+
TokenType::END_OPTIONAL,
|
58
|
+
optional_sub_parsers
|
59
|
+
)
|
60
|
+
optional_sub_parsers << parse_optional << parse_parameter << parse_text
|
61
|
+
|
62
|
+
# alternation := alternative* + ( '/' + alternative* )+
|
63
|
+
parse_alternative_separator = lambda do |_, tokens, current|
|
64
|
+
unless looking_at(tokens, current, TokenType::ALTERNATION)
|
65
|
+
return 0, nil
|
66
|
+
end
|
67
|
+
token = tokens[current]
|
68
|
+
return 1, [Node.new(NodeType::ALTERNATIVE, nil, token.text, token.start, token.end)]
|
69
|
+
end
|
70
|
+
|
71
|
+
alternative_parsers = [
|
72
|
+
parse_alternative_separator,
|
73
|
+
parse_optional,
|
74
|
+
parse_parameter,
|
75
|
+
parse_text,
|
76
|
+
]
|
77
|
+
|
78
|
+
# alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary)
|
79
|
+
# left-boundary := whitespace | } | ^
|
80
|
+
# right-boundary := whitespace | { | $
|
81
|
+
# alternative: = optional | parameter | text
|
82
|
+
parse_alternation = lambda do |expr, tokens, current|
|
83
|
+
previous = current - 1
|
84
|
+
unless looking_at_any(tokens, previous, [TokenType::START_OF_LINE, TokenType::WHITE_SPACE, TokenType::END_PARAMETER])
|
85
|
+
return 0, nil
|
86
|
+
end
|
87
|
+
|
88
|
+
consumed, ast = parse_tokens_until(expr, alternative_parsers, tokens, current, [TokenType::WHITE_SPACE, TokenType::END_OF_LINE, TokenType::BEGIN_PARAMETER])
|
89
|
+
sub_current = current + consumed
|
90
|
+
unless ast.map { |astNode| astNode.type }.include? NodeType::ALTERNATIVE
|
91
|
+
return 0, nil
|
92
|
+
end
|
93
|
+
|
94
|
+
start = tokens[current].start
|
95
|
+
_end = tokens[sub_current].start
|
96
|
+
# Does not consume right hand boundary token
|
97
|
+
return consumed, [Node.new(NodeType::ALTERNATION, split_alternatives(start, _end, ast), nil, start, _end)]
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# cucumber-expression := ( alternation | optional | parameter | text )*
|
102
|
+
#
|
103
|
+
parse_cucumber_expression = parse_between(
|
104
|
+
NodeType::EXPRESSION,
|
105
|
+
TokenType::START_OF_LINE,
|
106
|
+
TokenType::END_OF_LINE,
|
107
|
+
[parse_alternation, parse_optional, parse_parameter, parse_text]
|
108
|
+
)
|
109
|
+
|
110
|
+
tokenizer = CucumberExpressionTokenizer.new
|
111
|
+
tokens = tokenizer.tokenize(expression)
|
112
|
+
_, ast = parse_cucumber_expression.call(expression, tokens, 0)
|
113
|
+
ast[0]
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def parse_between(type, begin_token, end_token, parsers)
|
119
|
+
lambda do |expression, tokens, current|
|
120
|
+
unless looking_at(tokens, current, begin_token)
|
121
|
+
return 0, nil
|
122
|
+
end
|
123
|
+
sub_current = current + 1
|
124
|
+
consumed, ast = parse_tokens_until(expression, parsers, tokens, sub_current, [end_token, TokenType::END_OF_LINE])
|
125
|
+
sub_current += consumed
|
126
|
+
|
127
|
+
# endToken not found
|
128
|
+
unless looking_at(tokens, sub_current, end_token)
|
129
|
+
raise MissingEndToken.new(expression, begin_token, end_token, tokens[current])
|
130
|
+
end
|
131
|
+
# consumes endToken
|
132
|
+
start = tokens[current].start
|
133
|
+
_end = tokens[sub_current].end
|
134
|
+
consumed = sub_current + 1 - current
|
135
|
+
ast = [Node.new(type, ast, nil, start, _end)]
|
136
|
+
return consumed, ast
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def parse_token(expression, parsers, tokens, start_at)
|
141
|
+
parsers.each do |parser|
|
142
|
+
consumed, ast = parser.call(expression, tokens, start_at)
|
143
|
+
return consumed, ast unless consumed == 0
|
144
|
+
end
|
145
|
+
# If configured correctly this will never happen
|
146
|
+
raise 'No eligible parsers for ' + tokens
|
147
|
+
end
|
148
|
+
|
149
|
+
def parse_tokens_until(expression, parsers, tokens, start_at, end_tokens)
|
150
|
+
current = start_at
|
151
|
+
size = tokens.length
|
152
|
+
ast = []
|
153
|
+
while current < size do
|
154
|
+
if looking_at_any(tokens, current, end_tokens)
|
155
|
+
break
|
156
|
+
end
|
157
|
+
consumed, sub_ast = parse_token(expression, parsers, tokens, current)
|
158
|
+
if consumed == 0
|
159
|
+
# If configured correctly this will never happen
|
160
|
+
# Keep to avoid infinite loops
|
161
|
+
raise 'No eligible parsers for ' + tokens
|
162
|
+
end
|
163
|
+
current += consumed
|
164
|
+
ast += sub_ast
|
165
|
+
end
|
166
|
+
[current - start_at, ast]
|
167
|
+
end
|
168
|
+
|
169
|
+
def looking_at_any(tokens, at, token_types)
|
170
|
+
token_types.detect { |token_type| looking_at(tokens, at, token_type) }
|
171
|
+
end
|
172
|
+
|
173
|
+
def looking_at(tokens, at, token)
|
174
|
+
if at < 0
|
175
|
+
# If configured correctly this will never happen
|
176
|
+
# Keep for completeness
|
177
|
+
return token == TokenType::START_OF_LINE
|
178
|
+
end
|
179
|
+
if at >= tokens.length
|
180
|
+
return token == TokenType::END_OF_LINE
|
181
|
+
end
|
182
|
+
tokens[at].type == token
|
183
|
+
end
|
184
|
+
|
185
|
+
def split_alternatives(start, _end, alternation)
|
186
|
+
separators = []
|
187
|
+
alternatives = []
|
188
|
+
alternative = []
|
189
|
+
alternation.each { |n|
|
190
|
+
if NodeType::ALTERNATIVE == n.type
|
191
|
+
separators.push(n)
|
192
|
+
alternatives.push(alternative)
|
193
|
+
alternative = []
|
194
|
+
else
|
195
|
+
alternative.push(n)
|
196
|
+
end
|
197
|
+
}
|
198
|
+
alternatives.push(alternative)
|
199
|
+
create_alternative_nodes(start, _end, separators, alternatives)
|
200
|
+
end
|
201
|
+
|
202
|
+
def create_alternative_nodes(start, _end, separators, alternatives)
|
203
|
+
alternatives.each_with_index.map do |n, i|
|
204
|
+
if i == 0
|
205
|
+
right_separator = separators[i]
|
206
|
+
Node.new(NodeType::ALTERNATIVE, n, nil, start, right_separator.start)
|
207
|
+
elsif i == alternatives.length - 1
|
208
|
+
left_separator = separators[i - 1]
|
209
|
+
Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, _end)
|
210
|
+
else
|
211
|
+
left_separator = separators[i - 1]
|
212
|
+
right_separator = separators[i]
|
213
|
+
Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, right_separator.start)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'cucumber/cucumber_expressions/ast'
|
2
|
+
require 'cucumber/cucumber_expressions/errors'
|
3
|
+
|
4
|
+
module Cucumber
|
5
|
+
module CucumberExpressions
|
6
|
+
class CucumberExpressionTokenizer
|
7
|
+
def tokenize(expression)
|
8
|
+
@expression = expression
|
9
|
+
tokens = []
|
10
|
+
@buffer = []
|
11
|
+
previous_token_type = TokenType::START_OF_LINE
|
12
|
+
treat_as_text = false
|
13
|
+
@escaped = 0
|
14
|
+
@buffer_start_index = 0
|
15
|
+
|
16
|
+
codepoints = expression.codepoints
|
17
|
+
|
18
|
+
if codepoints.empty?
|
19
|
+
tokens.push(Token.new(TokenType::START_OF_LINE, '', 0, 0))
|
20
|
+
end
|
21
|
+
|
22
|
+
codepoints.each do |codepoint|
|
23
|
+
if !treat_as_text && Token.is_escape_character(codepoint)
|
24
|
+
@escaped += 1
|
25
|
+
treat_as_text = true
|
26
|
+
next
|
27
|
+
end
|
28
|
+
current_token_type = token_type_of(codepoint, treat_as_text)
|
29
|
+
treat_as_text = false
|
30
|
+
|
31
|
+
if should_create_new_token?(previous_token_type, current_token_type)
|
32
|
+
token = convert_buffer_to_token(previous_token_type)
|
33
|
+
previous_token_type = current_token_type
|
34
|
+
@buffer.push(codepoint)
|
35
|
+
tokens.push(token)
|
36
|
+
else
|
37
|
+
previous_token_type = current_token_type
|
38
|
+
@buffer.push(codepoint)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if @buffer.length > 0
|
43
|
+
token = convert_buffer_to_token(previous_token_type)
|
44
|
+
tokens.push(token)
|
45
|
+
end
|
46
|
+
|
47
|
+
raise TheEndOfLineCannotBeEscaped.new(expression) if treat_as_text
|
48
|
+
|
49
|
+
tokens.push(Token.new(TokenType::END_OF_LINE, '', codepoints.length, codepoints.length))
|
50
|
+
tokens
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# TODO: Make these lambdas
|
56
|
+
|
57
|
+
def convert_buffer_to_token(token_type)
|
58
|
+
escape_tokens = 0
|
59
|
+
if token_type == TokenType::TEXT
|
60
|
+
escape_tokens = @escaped
|
61
|
+
@escaped = 0
|
62
|
+
end
|
63
|
+
|
64
|
+
consumed_index = @buffer_start_index + @buffer.length + escape_tokens
|
65
|
+
t = Token.new(
|
66
|
+
token_type,
|
67
|
+
@buffer.map { |codepoint| codepoint.chr(Encoding::UTF_8) }.join(''),
|
68
|
+
@buffer_start_index,
|
69
|
+
consumed_index
|
70
|
+
)
|
71
|
+
@buffer = []
|
72
|
+
@buffer_start_index = consumed_index
|
73
|
+
t
|
74
|
+
end
|
75
|
+
|
76
|
+
def token_type_of(codepoint, treat_as_text)
|
77
|
+
unless treat_as_text
|
78
|
+
return Token.type_of(codepoint)
|
79
|
+
end
|
80
|
+
if Token.can_escape(codepoint)
|
81
|
+
return TokenType::TEXT
|
82
|
+
end
|
83
|
+
raise CantEscape.new(
|
84
|
+
@expression,
|
85
|
+
@buffer_start_index + @buffer.length + @escaped
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def should_create_new_token?(previous_token_type, current_token_type)
|
90
|
+
current_token_type != previous_token_type ||
|
91
|
+
(current_token_type != TokenType::WHITE_SPACE && current_token_type != TokenType::TEXT)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -1,11 +1,182 @@
|
|
1
|
+
require 'cucumber/cucumber_expressions/ast'
|
2
|
+
|
1
3
|
module Cucumber
|
2
4
|
module CucumberExpressions
|
3
5
|
class CucumberExpressionError < StandardError
|
6
|
+
|
7
|
+
def build_message(
|
8
|
+
index,
|
9
|
+
expression,
|
10
|
+
pointer,
|
11
|
+
problem,
|
12
|
+
solution
|
13
|
+
)
|
14
|
+
m = <<-EOF
|
15
|
+
This Cucumber Expression has a problem at column #{index + 1}:
|
16
|
+
|
17
|
+
#{expression}
|
18
|
+
#{pointer}
|
19
|
+
#{problem}.
|
20
|
+
#{solution}
|
21
|
+
EOF
|
22
|
+
m.strip
|
23
|
+
end
|
24
|
+
|
25
|
+
def point_at(index)
|
26
|
+
' ' * index + '^'
|
27
|
+
end
|
28
|
+
|
29
|
+
def point_at_located(node)
|
30
|
+
pointer = [point_at(node.start)]
|
31
|
+
if node.start + 1 < node.end
|
32
|
+
for _ in node.start + 1...node.end - 1
|
33
|
+
pointer.push('-')
|
34
|
+
end
|
35
|
+
pointer.push('^')
|
36
|
+
end
|
37
|
+
pointer.join('')
|
38
|
+
end
|
4
39
|
end
|
5
40
|
|
6
|
-
class
|
41
|
+
class AlternativeMayNotExclusivelyContainOptionals < CucumberExpressionError
|
42
|
+
def initialize(node, expression)
|
43
|
+
super(build_message(
|
44
|
+
node.start,
|
45
|
+
expression,
|
46
|
+
point_at_located(node),
|
47
|
+
'An alternative may not exclusively contain optionals',
|
48
|
+
"If you did not mean to use an optional you can use '\\(' to escape the the '('"
|
49
|
+
))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class AlternativeMayNotBeEmpty < CucumberExpressionError
|
54
|
+
def initialize(node, expression)
|
55
|
+
super(build_message(
|
56
|
+
node.start,
|
57
|
+
expression,
|
58
|
+
point_at_located(node),
|
59
|
+
'Alternative may not be empty',
|
60
|
+
"If you did not mean to use an alternative you can use '\\/' to escape the the '/'"
|
61
|
+
))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class CantEscape < CucumberExpressionError
|
66
|
+
def initialize(expression, index)
|
67
|
+
super(build_message(
|
68
|
+
index,
|
69
|
+
expression,
|
70
|
+
point_at(index),
|
71
|
+
"Only the characters '{', '}', '(', ')', '\\', '/' and whitespace can be escaped",
|
72
|
+
"If you did mean to use an '\\' you can use '\\\\' to escape it"
|
73
|
+
))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class OptionalMayNotBeEmpty < CucumberExpressionError
|
78
|
+
def initialize(node, expression)
|
79
|
+
super(build_message(
|
80
|
+
node.start,
|
81
|
+
expression,
|
82
|
+
point_at_located(node),
|
83
|
+
'An optional must contain some text',
|
84
|
+
"If you did not mean to use an optional you can use '\\(' to escape the the '('"
|
85
|
+
))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class ParameterIsNotAllowedInOptional < CucumberExpressionError
|
90
|
+
def initialize(node, expression)
|
91
|
+
super(build_message(
|
92
|
+
node.start,
|
93
|
+
expression,
|
94
|
+
point_at_located(node),
|
95
|
+
'An optional may not contain a parameter type',
|
96
|
+
"If you did not mean to use an parameter type you can use '\\{' to escape the the '{'"
|
97
|
+
))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class OptionalIsNotAllowedInOptional < CucumberExpressionError
|
102
|
+
def initialize(node, expression)
|
103
|
+
super(build_message(
|
104
|
+
node.start,
|
105
|
+
expression,
|
106
|
+
point_at_located(node),
|
107
|
+
'An optional may not contain an other optional',
|
108
|
+
"If you did not mean to use an optional type you can use '\\(' to escape the the '('. For more complicated expressions consider using a regular expression instead."
|
109
|
+
))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class TheEndOfLineCannotBeEscaped < CucumberExpressionError
|
114
|
+
def initialize(expression)
|
115
|
+
index = expression.codepoints.length - 1
|
116
|
+
super(build_message(
|
117
|
+
index,
|
118
|
+
expression,
|
119
|
+
point_at(index),
|
120
|
+
'The end of line can not be escaped',
|
121
|
+
"You can use '\\\\' to escape the the '\\'"
|
122
|
+
))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class MissingEndToken < CucumberExpressionError
|
127
|
+
def initialize(expression, begin_token, end_token, current)
|
128
|
+
begin_symbol = Token::symbol_of(begin_token)
|
129
|
+
end_symbol = Token::symbol_of(end_token)
|
130
|
+
purpose = Token::purpose_of(begin_token)
|
131
|
+
super(build_message(
|
132
|
+
current.start,
|
133
|
+
expression,
|
134
|
+
point_at_located(current),
|
135
|
+
"The '#{begin_symbol}' does not have a matching '#{end_symbol}'",
|
136
|
+
"If you did not intend to use #{purpose} you can use '\\#{begin_symbol}' to escape the #{purpose}"
|
137
|
+
))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class AlternationNotAllowedInOptional < CucumberExpressionError
|
142
|
+
def initialize(expression, current)
|
143
|
+
super(build_message(
|
144
|
+
current.start,
|
145
|
+
expression,
|
146
|
+
point_at_located(current),
|
147
|
+
"An alternation can not be used inside an optional",
|
148
|
+
"You can use '\\/' to escape the the '/'"
|
149
|
+
))
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class InvalidParameterTypeName < CucumberExpressionError
|
7
154
|
def initialize(type_name)
|
8
|
-
super("
|
155
|
+
super("Illegal character in parameter name {#{type_name}}. " +
|
156
|
+
"Parameter names may not contain '{', '}', '(', ')', '\\' or '/'")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
class InvalidParameterTypeNameInNode < CucumberExpressionError
|
162
|
+
def initialize(expression, token)
|
163
|
+
super(build_message(
|
164
|
+
token.start,
|
165
|
+
expression,
|
166
|
+
point_at_located(token),
|
167
|
+
"Parameter names may not contain '{', '}', '(', ')', '\\' or '/'",
|
168
|
+
"Did you mean to use a regular expression?"
|
169
|
+
))
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class UndefinedParameterTypeError < CucumberExpressionError
|
174
|
+
def initialize(node, expression, parameter_type_name)
|
175
|
+
super(build_message(node.start,
|
176
|
+
expression,
|
177
|
+
point_at_located(node),
|
178
|
+
"Undefined parameter type '#{parameter_type_name}'",
|
179
|
+
"Please register a ParameterType for '#{parameter_type_name}'"))
|
9
180
|
end
|
10
181
|
end
|
11
182
|
|
@@ -29,11 +200,11 @@ I couldn't decide which one to use. You have two options:
|
|
29
200
|
private
|
30
201
|
|
31
202
|
def parameter_type_names(parameter_types)
|
32
|
-
parameter_types.map{|p| "{#{p.name}}"}.join("\n ")
|
203
|
+
parameter_types.map { |p| "{#{p.name}}" }.join("\n ")
|
33
204
|
end
|
34
205
|
|
35
206
|
def expressions(generated_expressions)
|
36
|
-
generated_expressions.map{|ge| ge.source}.join("\n ")
|
207
|
+
generated_expressions.map { |ge| ge.source }.join("\n ")
|
37
208
|
end
|
38
209
|
end
|
39
210
|
end
|