cucumber-cucumber-expressions 8.3.1 → 19.0.0
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/LICENSE +2 -2
- data/lib/cucumber/cucumber_expressions/argument.rb +8 -4
- data/lib/cucumber/cucumber_expressions/ast.rb +166 -0
- data/lib/cucumber/cucumber_expressions/combinatorial_generated_expression_factory.rb +6 -13
- data/lib/cucumber/cucumber_expressions/cucumber_expression.rb +82 -80
- data/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb +14 -26
- data/lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb +203 -0
- data/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rb +90 -0
- data/lib/cucumber/cucumber_expressions/errors.rb +205 -13
- data/lib/cucumber/cucumber_expressions/expression_factory.rb +24 -0
- data/lib/cucumber/cucumber_expressions/generated_expression.rb +2 -0
- data/lib/cucumber/cucumber_expressions/group.rb +7 -1
- data/lib/cucumber/cucumber_expressions/group_builder.rb +9 -2
- data/lib/cucumber/cucumber_expressions/parameter_type.rb +14 -21
- data/lib/cucumber/cucumber_expressions/parameter_type_matcher.rb +11 -9
- data/lib/cucumber/cucumber_expressions/parameter_type_registry.rb +28 -16
- data/lib/cucumber/cucumber_expressions/regular_expression.rb +3 -2
- data/lib/cucumber/cucumber_expressions/tree_regexp.rb +54 -46
- metadata +76 -77
- data/.github/ISSUE_TEMPLATE.md +0 -5
- data/.github/PULL_REQUEST_TEMPLATE.md +0 -5
- data/.rspec +0 -1
- data/.rsync +0 -4
- data/.subrepo +0 -1
- data/Gemfile +0 -3
- data/Makefile +0 -1
- data/README.md +0 -5
- data/Rakefile +0 -27
- data/cucumber-cucumber-expressions.gemspec +0 -33
- data/default.mk +0 -70
- data/examples.txt +0 -31
- data/scripts/update-gemspec +0 -32
- data/spec/capture_warnings.rb +0 -74
- data/spec/coverage.rb +0 -7
- data/spec/cucumber/cucumber_expressions/argument_spec.rb +0 -17
- data/spec/cucumber/cucumber_expressions/combinatorial_generated_expression_factory_test.rb +0 -43
- data/spec/cucumber/cucumber_expressions/cucumber_expression_generator_spec.rb +0 -231
- data/spec/cucumber/cucumber_expressions/cucumber_expression_regexp_spec.rb +0 -57
- data/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb +0 -212
- data/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb +0 -202
- data/spec/cucumber/cucumber_expressions/expression_examples_spec.rb +0 -30
- data/spec/cucumber/cucumber_expressions/parameter_type_registry_spec.rb +0 -86
- data/spec/cucumber/cucumber_expressions/parameter_type_spec.rb +0 -15
- data/spec/cucumber/cucumber_expressions/regular_expression_spec.rb +0 -80
- data/spec/cucumber/cucumber_expressions/tree_regexp_spec.rb +0 -133
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cucumber/cucumber_expressions/ast'
|
|
4
|
+
require 'cucumber/cucumber_expressions/errors'
|
|
5
|
+
require 'cucumber/cucumber_expressions/cucumber_expression_tokenizer'
|
|
6
|
+
|
|
7
|
+
module Cucumber
|
|
8
|
+
module CucumberExpressions
|
|
9
|
+
class CucumberExpressionParser
|
|
10
|
+
def parse(expression)
|
|
11
|
+
# text := whitespace | ')' | '}' | .
|
|
12
|
+
parse_text = lambda do |_, tokens, current|
|
|
13
|
+
token = tokens[current]
|
|
14
|
+
case token.type
|
|
15
|
+
when TokenType::WHITE_SPACE, TokenType::TEXT, TokenType::END_PARAMETER, TokenType::END_OPTIONAL
|
|
16
|
+
return [1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)]]
|
|
17
|
+
when TokenType::ALTERNATION
|
|
18
|
+
raise AlternationNotAllowedInOptional.new(expression, token)
|
|
19
|
+
when TokenType::BEGIN_PARAMETER, TokenType::START_OF_LINE, TokenType::END_OF_LINE, TokenType::BEGIN_OPTIONAL
|
|
20
|
+
else
|
|
21
|
+
# If configured correctly this will never happen
|
|
22
|
+
return [0, nil]
|
|
23
|
+
end
|
|
24
|
+
# If configured correctly this will never happen
|
|
25
|
+
return [0, nil]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# name := whitespace | .
|
|
29
|
+
parse_name = lambda do |_, tokens, current|
|
|
30
|
+
token = tokens[current]
|
|
31
|
+
case token.type
|
|
32
|
+
when TokenType::WHITE_SPACE, TokenType::TEXT
|
|
33
|
+
return [1, [Node.new(NodeType::TEXT, nil, token.text, token.start, token.end)]]
|
|
34
|
+
when TokenType::BEGIN_PARAMETER, TokenType::END_PARAMETER, TokenType::BEGIN_OPTIONAL, TokenType::END_OPTIONAL, TokenType::ALTERNATION
|
|
35
|
+
raise InvalidParameterTypeNameInNode.new(expression, token)
|
|
36
|
+
when TokenType::START_OF_LINE, TokenType::END_OF_LINE
|
|
37
|
+
# If configured correctly this will never happen
|
|
38
|
+
return [0, nil]
|
|
39
|
+
else
|
|
40
|
+
# If configured correctly this will never happen
|
|
41
|
+
return [0, nil]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# parameter := '{' + name* + '}'
|
|
46
|
+
parse_parameter = parse_between(NodeType::PARAMETER, TokenType::BEGIN_PARAMETER, TokenType::END_PARAMETER, [parse_name])
|
|
47
|
+
|
|
48
|
+
# optional := '(' + option* + ')'
|
|
49
|
+
# option := optional | parameter | text
|
|
50
|
+
optional_sub_parsers = []
|
|
51
|
+
parse_optional = parse_between(NodeType::OPTIONAL, TokenType::BEGIN_OPTIONAL, TokenType::END_OPTIONAL, optional_sub_parsers)
|
|
52
|
+
optional_sub_parsers << parse_optional << parse_parameter << parse_text
|
|
53
|
+
|
|
54
|
+
# alternation := alternative* + ( '/' + alternative* )+
|
|
55
|
+
parse_alternative_separator = lambda do |_, tokens, current|
|
|
56
|
+
return [0, nil] unless looking_at(tokens, current, TokenType::ALTERNATION)
|
|
57
|
+
|
|
58
|
+
token = tokens[current]
|
|
59
|
+
return [1, [Node.new(NodeType::ALTERNATIVE, nil, token.text, token.start, token.end)]]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alternative_parsers = [
|
|
63
|
+
parse_alternative_separator,
|
|
64
|
+
parse_optional,
|
|
65
|
+
parse_parameter,
|
|
66
|
+
parse_text,
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# alternation := (?<=left-boundary) + alternative* + ( '/' + alternative* )+ + (?=right-boundary)
|
|
70
|
+
# left-boundary := whitespace | } | ^
|
|
71
|
+
# right-boundary := whitespace | { | $
|
|
72
|
+
# alternative: = optional | parameter | text
|
|
73
|
+
parse_alternation = lambda do |expr, tokens, current|
|
|
74
|
+
previous = current - 1
|
|
75
|
+
return [0, nil] unless looking_at_any(tokens, previous, [TokenType::START_OF_LINE, TokenType::WHITE_SPACE, TokenType::END_PARAMETER])
|
|
76
|
+
|
|
77
|
+
consumed, ast = parse_tokens_until(expr, alternative_parsers, tokens, current, [TokenType::WHITE_SPACE, TokenType::END_OF_LINE, TokenType::BEGIN_PARAMETER])
|
|
78
|
+
sub_current = current + consumed
|
|
79
|
+
return [0, nil] unless ast.map { |ast_node| ast_node.type }.include? NodeType::ALTERNATIVE
|
|
80
|
+
|
|
81
|
+
start = tokens[current].start
|
|
82
|
+
_end = tokens[sub_current].start
|
|
83
|
+
# Does not consume right hand boundary token
|
|
84
|
+
return [consumed, [Node.new(NodeType::ALTERNATION, split_alternatives(start, _end, ast), nil, start, _end)]]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#
|
|
88
|
+
# cucumber-expression := ( alternation | optional | parameter | text )*
|
|
89
|
+
#
|
|
90
|
+
parse_cucumber_expression = parse_between(
|
|
91
|
+
NodeType::EXPRESSION,
|
|
92
|
+
TokenType::START_OF_LINE,
|
|
93
|
+
TokenType::END_OF_LINE,
|
|
94
|
+
[parse_alternation, parse_optional, parse_parameter, parse_text]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
tokenizer = CucumberExpressionTokenizer.new
|
|
98
|
+
tokens = tokenizer.tokenize(expression)
|
|
99
|
+
_, ast = parse_cucumber_expression.call(expression, tokens, 0)
|
|
100
|
+
ast[0]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def parse_between(type, begin_token, end_token, parsers)
|
|
106
|
+
lambda do |expression, tokens, current|
|
|
107
|
+
return [0, nil] unless looking_at(tokens, current, begin_token)
|
|
108
|
+
|
|
109
|
+
sub_current = current + 1
|
|
110
|
+
consumed, ast = parse_tokens_until(expression, parsers, tokens, sub_current, [end_token, TokenType::END_OF_LINE])
|
|
111
|
+
sub_current += consumed
|
|
112
|
+
|
|
113
|
+
# endToken not found
|
|
114
|
+
raise MissingEndToken.new(expression, begin_token, end_token, tokens[current]) unless looking_at(tokens, sub_current, end_token)
|
|
115
|
+
|
|
116
|
+
# consumes endToken
|
|
117
|
+
start = tokens[current].start
|
|
118
|
+
_end = tokens[sub_current].end
|
|
119
|
+
consumed = sub_current + 1 - current
|
|
120
|
+
ast = [Node.new(type, ast, nil, start, _end)]
|
|
121
|
+
return [consumed, ast]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_token(expression, parsers, tokens, start_at)
|
|
126
|
+
parsers.each do |parser|
|
|
127
|
+
consumed, ast = parser.call(expression, tokens, start_at)
|
|
128
|
+
return [consumed, ast] unless consumed == 0
|
|
129
|
+
end
|
|
130
|
+
# If configured correctly this will never happen
|
|
131
|
+
raise 'No eligible parsers for ' + tokens
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def parse_tokens_until(expression, parsers, tokens, start_at, end_tokens)
|
|
135
|
+
current = start_at
|
|
136
|
+
size = tokens.length
|
|
137
|
+
ast = []
|
|
138
|
+
while current < size do
|
|
139
|
+
break if looking_at_any(tokens, current, end_tokens)
|
|
140
|
+
|
|
141
|
+
consumed, sub_ast = parse_token(expression, parsers, tokens, current)
|
|
142
|
+
if consumed == 0
|
|
143
|
+
# If configured correctly this will never happen
|
|
144
|
+
# Keep to avoid infinite loops
|
|
145
|
+
raise 'No eligible parsers for ' + tokens
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
current += consumed
|
|
149
|
+
ast += sub_ast
|
|
150
|
+
end
|
|
151
|
+
[current - start_at, ast]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def looking_at_any(tokens, at, token_types)
|
|
155
|
+
token_types.detect { |token_type| looking_at(tokens, at, token_type) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def looking_at(tokens, at, token)
|
|
159
|
+
if at < 0
|
|
160
|
+
# If configured correctly this will never happen
|
|
161
|
+
# Keep for completeness
|
|
162
|
+
return token == TokenType::START_OF_LINE
|
|
163
|
+
end
|
|
164
|
+
return token == TokenType::END_OF_LINE if at >= tokens.length
|
|
165
|
+
|
|
166
|
+
tokens[at].type == token
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def split_alternatives(start, _end, alternation)
|
|
170
|
+
separators = []
|
|
171
|
+
alternatives = []
|
|
172
|
+
alternative = []
|
|
173
|
+
alternation.each do |n|
|
|
174
|
+
if NodeType::ALTERNATIVE == n.type
|
|
175
|
+
separators.push(n)
|
|
176
|
+
alternatives.push(alternative)
|
|
177
|
+
alternative = []
|
|
178
|
+
else
|
|
179
|
+
alternative.push(n)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
alternatives.push(alternative)
|
|
183
|
+
create_alternative_nodes(start, _end, separators, alternatives)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def create_alternative_nodes(start, _end, separators, alternatives)
|
|
187
|
+
alternatives.each_with_index.map do |n, i|
|
|
188
|
+
if i == 0
|
|
189
|
+
right_separator = separators[i]
|
|
190
|
+
Node.new(NodeType::ALTERNATIVE, n, nil, start, right_separator.start)
|
|
191
|
+
elsif i == alternatives.length - 1
|
|
192
|
+
left_separator = separators[i - 1]
|
|
193
|
+
Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, _end)
|
|
194
|
+
else
|
|
195
|
+
left_separator = separators[i - 1]
|
|
196
|
+
right_separator = separators[i]
|
|
197
|
+
Node.new(NodeType::ALTERNATIVE, n, nil, left_separator.end, right_separator.start)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cucumber/cucumber_expressions/ast'
|
|
4
|
+
require 'cucumber/cucumber_expressions/errors'
|
|
5
|
+
|
|
6
|
+
module Cucumber
|
|
7
|
+
module CucumberExpressions
|
|
8
|
+
class CucumberExpressionTokenizer
|
|
9
|
+
def tokenize(expression)
|
|
10
|
+
@expression = expression
|
|
11
|
+
tokens = []
|
|
12
|
+
@buffer = []
|
|
13
|
+
previous_token_type = TokenType::START_OF_LINE
|
|
14
|
+
treat_as_text = false
|
|
15
|
+
@escaped = 0
|
|
16
|
+
@buffer_start_index = 0
|
|
17
|
+
|
|
18
|
+
codepoints = expression.codepoints
|
|
19
|
+
|
|
20
|
+
tokens.push(Token.new(TokenType::START_OF_LINE, '', 0, 0)) if codepoints.empty?
|
|
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
|
+
return Token.type_of(codepoint) unless treat_as_text
|
|
78
|
+
|
|
79
|
+
return TokenType::TEXT if Token.can_escape(codepoint)
|
|
80
|
+
|
|
81
|
+
raise CantEscape.new(@expression, @buffer_start_index + @buffer.length + @escaped)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def should_create_new_token?(previous_token_type, current_token_type)
|
|
85
|
+
current_token_type != previous_token_type ||
|
|
86
|
+
(current_token_type != TokenType::WHITE_SPACE && current_token_type != TokenType::TEXT)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -1,39 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cucumber/cucumber_expressions/ast'
|
|
4
|
+
|
|
1
5
|
module Cucumber
|
|
2
6
|
module CucumberExpressions
|
|
3
7
|
class CucumberExpressionError < StandardError
|
|
8
|
+
def build_message(index, expression, pointer, problem, solution)
|
|
9
|
+
m = <<~ERROR
|
|
10
|
+
This Cucumber Expression has a problem at column #{index + 1}:
|
|
11
|
+
|
|
12
|
+
#{expression}
|
|
13
|
+
#{pointer}
|
|
14
|
+
#{problem}.
|
|
15
|
+
#{solution}
|
|
16
|
+
ERROR
|
|
17
|
+
m.strip
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def point_at(index)
|
|
21
|
+
' ' * index + '^'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def point_at_located(node)
|
|
25
|
+
pointer = [point_at(node.start)]
|
|
26
|
+
if node.start + 1 < node.end
|
|
27
|
+
for _ in node.start + 1...node.end - 1
|
|
28
|
+
pointer.push('-')
|
|
29
|
+
end
|
|
30
|
+
pointer.push('^')
|
|
31
|
+
end
|
|
32
|
+
pointer.join('')
|
|
33
|
+
end
|
|
4
34
|
end
|
|
5
35
|
|
|
6
|
-
class
|
|
36
|
+
class AlternativeMayNotExclusivelyContainOptionals < CucumberExpressionError
|
|
37
|
+
def initialize(node, expression)
|
|
38
|
+
super(
|
|
39
|
+
build_message(
|
|
40
|
+
node.start,
|
|
41
|
+
expression,
|
|
42
|
+
point_at_located(node),
|
|
43
|
+
'An alternative may not exclusively contain optionals',
|
|
44
|
+
"If you did not mean to use an optional you can use '\\(' to escape the '('"
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class AlternativeMayNotBeEmpty < CucumberExpressionError
|
|
51
|
+
def initialize(node, expression)
|
|
52
|
+
super(
|
|
53
|
+
build_message(
|
|
54
|
+
node.start,
|
|
55
|
+
expression,
|
|
56
|
+
point_at_located(node),
|
|
57
|
+
'Alternative may not be empty',
|
|
58
|
+
"If you did not mean to use an alternative you can use '\\/' to escape the '/'"
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class CantEscape < CucumberExpressionError
|
|
65
|
+
def initialize(expression, index)
|
|
66
|
+
super(
|
|
67
|
+
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
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class OptionalMayNotBeEmpty < CucumberExpressionError
|
|
79
|
+
def initialize(node, expression)
|
|
80
|
+
super(
|
|
81
|
+
build_message(
|
|
82
|
+
node.start,
|
|
83
|
+
expression,
|
|
84
|
+
point_at_located(node),
|
|
85
|
+
'An optional must contain some text',
|
|
86
|
+
"If you did not mean to use an optional you can use '\\(' to escape the '('"
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class ParameterIsNotAllowedInOptional < CucumberExpressionError
|
|
93
|
+
def initialize(node, expression)
|
|
94
|
+
super(
|
|
95
|
+
build_message(
|
|
96
|
+
node.start,
|
|
97
|
+
expression,
|
|
98
|
+
point_at_located(node),
|
|
99
|
+
'An optional may not contain a parameter type',
|
|
100
|
+
"If you did not mean to use an parameter type you can use '\\{' to escape the '{'"
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
class OptionalIsNotAllowedInOptional < CucumberExpressionError
|
|
107
|
+
def initialize(node, expression)
|
|
108
|
+
super(
|
|
109
|
+
build_message(
|
|
110
|
+
node.start,
|
|
111
|
+
expression,
|
|
112
|
+
point_at_located(node),
|
|
113
|
+
'An optional may not contain an other optional',
|
|
114
|
+
"If you did not mean to use an optional type you can use '\\(' to escape the '('. For more complicated expressions consider using a regular expression instead."
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class TheEndOfLineCannotBeEscaped < CucumberExpressionError
|
|
121
|
+
def initialize(expression)
|
|
122
|
+
index = expression.codepoints.length - 1
|
|
123
|
+
super(
|
|
124
|
+
build_message(
|
|
125
|
+
index,
|
|
126
|
+
expression,
|
|
127
|
+
point_at(index),
|
|
128
|
+
'The end of line can not be escaped',
|
|
129
|
+
"You can use '\\\\' to escape the '\\'"
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class MissingEndToken < CucumberExpressionError
|
|
136
|
+
def initialize(expression, begin_token, end_token, current)
|
|
137
|
+
begin_symbol = Token::symbol_of(begin_token)
|
|
138
|
+
end_symbol = Token::symbol_of(end_token)
|
|
139
|
+
purpose = Token::purpose_of(begin_token)
|
|
140
|
+
|
|
141
|
+
super(
|
|
142
|
+
build_message(
|
|
143
|
+
current.start,
|
|
144
|
+
expression,
|
|
145
|
+
point_at_located(current),
|
|
146
|
+
"The '#{begin_symbol}' does not have a matching '#{end_symbol}'",
|
|
147
|
+
"If you did not intend to use #{purpose} you can use '\\#{begin_symbol}' to escape the #{purpose}"
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class AlternationNotAllowedInOptional < CucumberExpressionError
|
|
154
|
+
def initialize(expression, current)
|
|
155
|
+
super(
|
|
156
|
+
build_message(
|
|
157
|
+
current.start,
|
|
158
|
+
expression,
|
|
159
|
+
point_at_located(current),
|
|
160
|
+
'An alternation can not be used inside an optional',
|
|
161
|
+
"If you did not mean to use an alternation you can use '\\/' to escape the '/'. Otherwise rephrase your expression or consider using a regular expression instead."
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class InvalidParameterTypeName < CucumberExpressionError
|
|
7
168
|
def initialize(type_name)
|
|
8
|
-
super("
|
|
169
|
+
super("Illegal character in parameter name {#{type_name}}. Parameter names may not contain '{', '}', '(', ')', '\\' or '/'")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class InvalidParameterTypeNameInNode < CucumberExpressionError
|
|
174
|
+
def initialize(expression, token)
|
|
175
|
+
super(
|
|
176
|
+
build_message(
|
|
177
|
+
token.start,
|
|
178
|
+
expression,
|
|
179
|
+
point_at_located(token),
|
|
180
|
+
"Parameter names may not contain '{', '}', '(', ')', '\\' or '/'",
|
|
181
|
+
'Did you mean to use a regular expression?'
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class UndefinedParameterTypeError < CucumberExpressionError
|
|
188
|
+
attr_reader :undefined_parameter_type_name
|
|
189
|
+
|
|
190
|
+
def initialize(node, expression, undefined_parameter_type_name)
|
|
191
|
+
super(
|
|
192
|
+
build_message(
|
|
193
|
+
node.start,
|
|
194
|
+
expression,
|
|
195
|
+
point_at_located(node),
|
|
196
|
+
"Undefined parameter type '#{undefined_parameter_type_name}'",
|
|
197
|
+
"Please register a ParameterType for '#{undefined_parameter_type_name}'"
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
@undefined_parameter_type_name = undefined_parameter_type_name
|
|
9
201
|
end
|
|
10
202
|
end
|
|
11
203
|
|
|
12
204
|
class AmbiguousParameterTypeError < CucumberExpressionError
|
|
13
205
|
def initialize(parameter_type_regexp, expression_regexp, parameter_types, generated_expressions)
|
|
14
|
-
super(
|
|
15
|
-
Your Regular Expression /#{expression_regexp.source}/
|
|
16
|
-
matches multiple parameter types with regexp /#{parameter_type_regexp}/:
|
|
17
|
-
|
|
206
|
+
super(<<~ERROR)
|
|
207
|
+
Your Regular Expression /#{expression_regexp.source}/
|
|
208
|
+
matches multiple parameter types with regexp /#{parameter_type_regexp}/:
|
|
209
|
+
#{parameter_type_names(parameter_types)}
|
|
18
210
|
|
|
19
|
-
I couldn't decide which one to use. You have two options:
|
|
211
|
+
I couldn't decide which one to use. You have two options:
|
|
20
212
|
|
|
21
|
-
1) Use a Cucumber Expression instead of a Regular Expression. Try one of these:
|
|
22
|
-
|
|
213
|
+
1) Use a Cucumber Expression instead of a Regular Expression. Try one of these:
|
|
214
|
+
#{expressions(generated_expressions)}
|
|
23
215
|
|
|
24
|
-
2) Make one of the parameter types preferential and continue to use a Regular Expression.
|
|
216
|
+
2) Make one of the parameter types preferential and continue to use a Regular Expression.
|
|
25
217
|
|
|
26
|
-
|
|
218
|
+
ERROR
|
|
27
219
|
end
|
|
28
220
|
|
|
29
221
|
private
|
|
30
222
|
|
|
31
223
|
def parameter_type_names(parameter_types)
|
|
32
|
-
parameter_types.map{|p| "{#{p.name}}"}.join("\n ")
|
|
224
|
+
parameter_types.map { |p| "{#{p.name}}" }.join("\n ")
|
|
33
225
|
end
|
|
34
226
|
|
|
35
227
|
def expressions(generated_expressions)
|
|
36
|
-
generated_expressions.map{|ge| ge.source}.join("\n ")
|
|
228
|
+
generated_expressions.map { |ge| ge.source }.join("\n ")
|
|
37
229
|
end
|
|
38
230
|
end
|
|
39
231
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cucumber/cucumber_expressions/errors'
|
|
4
|
+
require 'cucumber/cucumber_expressions/cucumber_expression'
|
|
5
|
+
require 'cucumber/cucumber_expressions/regular_expression'
|
|
6
|
+
|
|
7
|
+
module Cucumber
|
|
8
|
+
module CucumberExpressions
|
|
9
|
+
class ExpressionFactory
|
|
10
|
+
def initialize(parameter_type_registry)
|
|
11
|
+
@parameter_type_registry = parameter_type_registry
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create_expression(string_or_regexp)
|
|
15
|
+
case string_or_regexp
|
|
16
|
+
when String then CucumberExpression.new(string_or_regexp, @parameter_type_registry)
|
|
17
|
+
when Regexp then RegularExpression.new(string_or_regexp, @parameter_type_registry)
|
|
18
|
+
else
|
|
19
|
+
raise CucumberExpressionError.new("Can't create an expression from #{string_or_regexp.inspect}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Cucumber
|
|
2
4
|
module CucumberExpressions
|
|
3
5
|
class Group
|
|
@@ -11,7 +13,11 @@ module Cucumber
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def values
|
|
14
|
-
|
|
16
|
+
if children.nil?
|
|
17
|
+
[self.value]
|
|
18
|
+
else
|
|
19
|
+
children.map(&:value)
|
|
20
|
+
end
|
|
15
21
|
end
|
|
16
22
|
end
|
|
17
23
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'cucumber/cucumber_expressions/group'
|
|
2
4
|
|
|
3
5
|
module Cucumber
|
|
@@ -16,8 +18,13 @@ module Cucumber
|
|
|
16
18
|
|
|
17
19
|
def build(match, group_indices)
|
|
18
20
|
group_index = group_indices.next
|
|
19
|
-
children = @group_builders.map {|gb| gb.build(match, group_indices)}
|
|
20
|
-
Group.new(
|
|
21
|
+
children = @group_builders.map { |gb| gb.build(match, group_indices) }
|
|
22
|
+
Group.new(
|
|
23
|
+
match[group_index],
|
|
24
|
+
match.offset(group_index)[0],
|
|
25
|
+
match.offset(group_index)[1],
|
|
26
|
+
children.empty? ? nil : children
|
|
27
|
+
)
|
|
21
28
|
end
|
|
22
29
|
|
|
23
30
|
def set_non_capturing!
|
|
@@ -1,28 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'cucumber/cucumber_expressions/errors'
|
|
2
4
|
|
|
3
5
|
module Cucumber
|
|
4
6
|
module CucumberExpressions
|
|
5
7
|
class ParameterType
|
|
6
|
-
ILLEGAL_PARAMETER_NAME_PATTERN = /([\[\]()$.|?*+])
|
|
7
|
-
UNESCAPE_PATTERN = /(\\([\[$.|?*+\]]))
|
|
8
|
+
ILLEGAL_PARAMETER_NAME_PATTERN = /([\[\]()$.|?*+])/.freeze
|
|
9
|
+
UNESCAPE_PATTERN = /(\\([\[$.|?*+\]]))/.freeze
|
|
8
10
|
|
|
9
|
-
attr_reader :name, :type, :regexps
|
|
11
|
+
attr_reader :name, :type, :transformer, :use_for_snippets, :prefer_for_regexp_match, :regexps
|
|
10
12
|
|
|
11
13
|
def self.check_parameter_type_name(type_name)
|
|
12
|
-
|
|
13
|
-
$2
|
|
14
|
-
end
|
|
15
|
-
if ILLEGAL_PARAMETER_NAME_PATTERN =~ unescaped_type_name
|
|
16
|
-
raise CucumberExpressionError.new("Illegal character '#{$1}' in parameter name {#{unescaped_type_name}}")
|
|
17
|
-
end
|
|
14
|
+
raise CucumberExpressionError.new("Illegal character in parameter name {#{type_name}}. Parameter names may not contain '[]()$.|?*+'") unless is_valid_parameter_type_name(type_name)
|
|
18
15
|
end
|
|
19
16
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def use_for_snippets?
|
|
25
|
-
@use_for_snippets
|
|
17
|
+
def self.is_valid_parameter_type_name(type_name)
|
|
18
|
+
unescaped_type_name = type_name.gsub(UNESCAPE_PATTERN) { Regexp.last_match(2) }
|
|
19
|
+
!(ILLEGAL_PARAMETER_NAME_PATTERN =~ unescaped_type_name)
|
|
26
20
|
end
|
|
27
21
|
|
|
28
22
|
# Create a new Parameter
|
|
@@ -51,8 +45,9 @@ module Cucumber
|
|
|
51
45
|
end
|
|
52
46
|
|
|
53
47
|
def <=>(other)
|
|
54
|
-
return -1 if prefer_for_regexp_match
|
|
55
|
-
return 1 if other.prefer_for_regexp_match
|
|
48
|
+
return -1 if prefer_for_regexp_match && !other.prefer_for_regexp_match
|
|
49
|
+
return 1 if other.prefer_for_regexp_match && !prefer_for_regexp_match
|
|
50
|
+
|
|
56
51
|
return name <=> other.name
|
|
57
52
|
end
|
|
58
53
|
|
|
@@ -60,7 +55,7 @@ module Cucumber
|
|
|
60
55
|
|
|
61
56
|
def string_array(regexps)
|
|
62
57
|
array = regexps.is_a?(Array) ? regexps : [regexps]
|
|
63
|
-
array.map {|regexp| regexp.is_a?(String) ? regexp : regexp_source(regexp)}
|
|
58
|
+
array.map { |regexp| regexp.is_a?(String) ? regexp : regexp_source(regexp) }
|
|
64
59
|
end
|
|
65
60
|
|
|
66
61
|
def regexp_source(regexp)
|
|
@@ -70,9 +65,7 @@ module Cucumber
|
|
|
70
65
|
'MULTILINE'
|
|
71
66
|
].each do |option_name|
|
|
72
67
|
option = Regexp.const_get(option_name)
|
|
73
|
-
if regexp.options & option != 0
|
|
74
|
-
raise CucumberExpressionError.new("ParameterType Regexps can't use option Regexp::#{option_name}")
|
|
75
|
-
end
|
|
68
|
+
raise CucumberExpressionError.new("ParameterType Regexps can't use option Regexp::#{option_name}") if regexp.options & option != 0
|
|
76
69
|
end
|
|
77
70
|
regexp.source
|
|
78
71
|
end
|