cucumber-cucumber-expressions 10.2.0 → 11.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|