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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd50c1e02d54c7357dfd594b076344ecdb3735fd7e598a772ba63185dc420d80
|
4
|
+
data.tar.gz: c48124ac1ce6ccfed189c04a2ea21fcfdd61e659488792e4b0876134422f4086
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41ac685e10b0c5c243faed9af265d3ab63e8bc6d01bf98c96b2e5bd8d077c5b1d842c0eb9b67a73eb05749851314155026117f686f756f97ada664eab66ce181
|
7
|
+
data.tar.gz: b2e3f96adecda3a12ca136c575c43fb29d28ef44018bb2a662be7bab10222e7a69217b4ff924b51d807a8166fce18b8d577cc3f80a8dfd4b12255ee629722cd8
|
data/.rsync
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
11.0.1
|
@@ -22,8 +22,8 @@ Gem::Specification.new do |s|
|
|
22
22
|
'source_code_uri' => 'https://github.com/cucumber/cucumber/blob/master/cucumber-expressions/ruby',
|
23
23
|
}
|
24
24
|
|
25
|
-
s.add_development_dependency 'rake', '~> 13.0', '>= 13.0.
|
26
|
-
s.add_development_dependency 'rspec', '~> 3.
|
25
|
+
s.add_development_dependency 'rake', '~> 13.0', '>= 13.0.3'
|
26
|
+
s.add_development_dependency 'rspec', '~> 3.10', '>= 3.10.0'
|
27
27
|
|
28
28
|
s.rubygems_version = ">= 1.6.1"
|
29
29
|
s.files = `git ls-files`.split("\n").reject {|path| path =~ /\.gitignore$/ }
|
data/default.mk
CHANGED
@@ -3,6 +3,10 @@ RUBY_SOURCE_FILES = $(shell find . -name "*.rb")
|
|
3
3
|
GEMSPEC = $(shell find . -name "*.gemspec")
|
4
4
|
LIBNAME := $(shell basename $$(dirname $$(pwd)))
|
5
5
|
GEM := cucumber-$(LIBNAME)-$(NEW_VERSION).gem
|
6
|
+
IS_TESTDATA = $(findstring -testdata,${CURDIR})
|
7
|
+
|
8
|
+
# https://stackoverflow.com/questions/2483182/recursive-wildcards-in-gnu-make
|
9
|
+
rwildcard=$(foreach d,$(wildcard $(1:=/*)),$(call rwildcard,$d,$2) $(filter $(subst *,%,$2),$d))
|
6
10
|
|
7
11
|
default: .tested
|
8
12
|
.PHONY: default
|
@@ -45,17 +49,25 @@ pre-release: remove-local-dependencies update-version update-dependencies gem
|
|
45
49
|
.PHONY: pre-release
|
46
50
|
|
47
51
|
update-version:
|
52
|
+
ifeq ($(IS_TESTDATA),-testdata)
|
53
|
+
# no-op
|
54
|
+
else
|
48
55
|
ifdef NEW_VERSION
|
49
56
|
@echo "$(NEW_VERSION)" > VERSION
|
50
57
|
endif
|
58
|
+
endif
|
51
59
|
.PHONY: update-version
|
52
60
|
|
53
61
|
publish: gem
|
62
|
+
ifeq ($(IS_TESTDATA),-testdata)
|
63
|
+
# no-op
|
64
|
+
else
|
54
65
|
ifneq (,$(GEMSPEC))
|
55
66
|
gem push $(GEM)
|
56
67
|
else
|
57
68
|
@echo "Not publishing because there is no gemspec"
|
58
69
|
endif
|
70
|
+
endif
|
59
71
|
.PHONY: publish
|
60
72
|
|
61
73
|
post-release:
|
data/examples.txt
CHANGED
@@ -2,7 +2,7 @@ I have {int} cuke(s)
|
|
2
2
|
I have 22 cukes
|
3
3
|
[22]
|
4
4
|
---
|
5
|
-
I have {int} cuke(s) and some
|
5
|
+
I have {int} cuke(s) and some \\[]^$.|?*+
|
6
6
|
I have 1 cuke and some \[]^$.|?*+
|
7
7
|
[1]
|
8
8
|
---
|
@@ -29,3 +29,15 @@ I have 22 cukes in my belly now
|
|
29
29
|
I have {} cuke(s) in my {} now
|
30
30
|
I have 22 cukes in my belly now
|
31
31
|
["22","belly"]
|
32
|
+
---
|
33
|
+
/^a (pre-commercial transaction |pre buyer fee model )?purchase(?: for \$(\d+))?$/
|
34
|
+
a purchase for $33
|
35
|
+
[null,33]
|
36
|
+
---
|
37
|
+
Some ${float} of cukes at {int}° Celsius
|
38
|
+
Some $3.50 of cukes at 42° Celsius
|
39
|
+
[3.5,42]
|
40
|
+
---
|
41
|
+
I select the {int}st/nd/rd/th Cucumber
|
42
|
+
I select the 3rd Cucumber
|
43
|
+
[3]
|
@@ -0,0 +1,201 @@
|
|
1
|
+
module Cucumber
|
2
|
+
module CucumberExpressions
|
3
|
+
ESCAPE_CHARACTER = '\\'
|
4
|
+
ALTERNATION_CHARACTER = '/'
|
5
|
+
BEGIN_PARAMETER_CHARACTER = '{'
|
6
|
+
END_PARAMETER_CHARACTER = '}'
|
7
|
+
BEGIN_OPTIONAL_CHARACTER = '('
|
8
|
+
END_OPTIONAL_CHARACTER = ')'
|
9
|
+
|
10
|
+
class Node
|
11
|
+
def initialize(type, nodes, token, start, _end)
|
12
|
+
if nodes.nil? && token.nil?
|
13
|
+
raise 'Either nodes or token must be defined'
|
14
|
+
end
|
15
|
+
@type = type
|
16
|
+
@nodes = nodes
|
17
|
+
@token = token
|
18
|
+
@start = start
|
19
|
+
@end = _end
|
20
|
+
end
|
21
|
+
|
22
|
+
def type
|
23
|
+
@type
|
24
|
+
end
|
25
|
+
|
26
|
+
def nodes
|
27
|
+
@nodes
|
28
|
+
end
|
29
|
+
|
30
|
+
def token
|
31
|
+
@token
|
32
|
+
end
|
33
|
+
|
34
|
+
def start
|
35
|
+
@start
|
36
|
+
end
|
37
|
+
|
38
|
+
def end
|
39
|
+
@end
|
40
|
+
end
|
41
|
+
|
42
|
+
def text
|
43
|
+
if @token.nil?
|
44
|
+
return @nodes.map { |value| value.text }.join('')
|
45
|
+
end
|
46
|
+
@token
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_hash
|
50
|
+
hash = Hash.new
|
51
|
+
hash["type"] = @type
|
52
|
+
unless @nodes.nil?
|
53
|
+
hash["nodes"] = @nodes.map { |node| node.to_hash }
|
54
|
+
end
|
55
|
+
unless @token.nil?
|
56
|
+
hash["token"] = @token
|
57
|
+
end
|
58
|
+
hash["start"] = @start
|
59
|
+
hash["end"] = @end
|
60
|
+
hash
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module NodeType
|
65
|
+
TEXT = 'TEXT_NODE'
|
66
|
+
OPTIONAL = 'OPTIONAL_NODE'
|
67
|
+
ALTERNATION = 'ALTERNATION_NODE'
|
68
|
+
ALTERNATIVE = 'ALTERNATIVE_NODE'
|
69
|
+
PARAMETER = 'PARAMETER_NODE'
|
70
|
+
EXPRESSION = 'EXPRESSION_NODE'
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
class Token
|
75
|
+
def initialize(type, text, start, _end)
|
76
|
+
@type, @text, @start, @end = type, text, start, _end
|
77
|
+
end
|
78
|
+
|
79
|
+
def type
|
80
|
+
@type
|
81
|
+
end
|
82
|
+
|
83
|
+
def text
|
84
|
+
@text
|
85
|
+
end
|
86
|
+
|
87
|
+
def start
|
88
|
+
@start
|
89
|
+
end
|
90
|
+
|
91
|
+
def end
|
92
|
+
@end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.is_escape_character(codepoint)
|
96
|
+
codepoint.chr(Encoding::UTF_8) == ESCAPE_CHARACTER
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.can_escape(codepoint)
|
100
|
+
c = codepoint.chr(Encoding::UTF_8)
|
101
|
+
if c == ' '
|
102
|
+
# TODO: Unicode whitespace?
|
103
|
+
return true
|
104
|
+
end
|
105
|
+
case c
|
106
|
+
when ESCAPE_CHARACTER
|
107
|
+
true
|
108
|
+
when ALTERNATION_CHARACTER
|
109
|
+
true
|
110
|
+
when BEGIN_PARAMETER_CHARACTER
|
111
|
+
true
|
112
|
+
when END_PARAMETER_CHARACTER
|
113
|
+
true
|
114
|
+
when BEGIN_OPTIONAL_CHARACTER
|
115
|
+
true
|
116
|
+
when END_OPTIONAL_CHARACTER
|
117
|
+
true
|
118
|
+
else
|
119
|
+
false
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.type_of(codepoint)
|
124
|
+
c = codepoint.chr(Encoding::UTF_8)
|
125
|
+
if c == ' '
|
126
|
+
# TODO: Unicode whitespace?
|
127
|
+
return TokenType::WHITE_SPACE
|
128
|
+
end
|
129
|
+
case c
|
130
|
+
when ALTERNATION_CHARACTER
|
131
|
+
TokenType::ALTERNATION
|
132
|
+
when BEGIN_PARAMETER_CHARACTER
|
133
|
+
TokenType::BEGIN_PARAMETER
|
134
|
+
when END_PARAMETER_CHARACTER
|
135
|
+
TokenType::END_PARAMETER
|
136
|
+
when BEGIN_OPTIONAL_CHARACTER
|
137
|
+
TokenType::BEGIN_OPTIONAL
|
138
|
+
when END_OPTIONAL_CHARACTER
|
139
|
+
TokenType::END_OPTIONAL
|
140
|
+
else
|
141
|
+
TokenType::TEXT
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.symbol_of(token)
|
146
|
+
case token
|
147
|
+
when TokenType::BEGIN_OPTIONAL
|
148
|
+
return BEGIN_OPTIONAL_CHARACTER
|
149
|
+
when TokenType::END_OPTIONAL
|
150
|
+
return END_OPTIONAL_CHARACTER
|
151
|
+
when TokenType::BEGIN_PARAMETER
|
152
|
+
return BEGIN_PARAMETER_CHARACTER
|
153
|
+
when TokenType::END_PARAMETER
|
154
|
+
return END_PARAMETER_CHARACTER
|
155
|
+
when TokenType::ALTERNATION
|
156
|
+
return ALTERNATION_CHARACTER
|
157
|
+
else
|
158
|
+
return ''
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.purpose_of(token)
|
163
|
+
case token
|
164
|
+
when TokenType::BEGIN_OPTIONAL
|
165
|
+
return 'optional text'
|
166
|
+
when TokenType::END_OPTIONAL
|
167
|
+
return 'optional text'
|
168
|
+
when TokenType::BEGIN_PARAMETER
|
169
|
+
return 'a parameter'
|
170
|
+
when TokenType::END_PARAMETER
|
171
|
+
return 'a parameter'
|
172
|
+
when TokenType::ALTERNATION
|
173
|
+
return 'alternation'
|
174
|
+
else
|
175
|
+
return ''
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def to_hash
|
180
|
+
{
|
181
|
+
"type" => @type,
|
182
|
+
"text" => @text,
|
183
|
+
"start" => @start,
|
184
|
+
"end" => @end
|
185
|
+
}
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
module TokenType
|
190
|
+
START_OF_LINE = 'START_OF_LINE'
|
191
|
+
END_OF_LINE = 'END_OF_LINE'
|
192
|
+
WHITE_SPACE = 'WHITE_SPACE'
|
193
|
+
BEGIN_OPTIONAL = 'BEGIN_OPTIONAL'
|
194
|
+
END_OPTIONAL = 'END_OPTIONAL'
|
195
|
+
BEGIN_PARAMETER = 'BEGIN_PARAMETER'
|
196
|
+
END_PARAMETER = 'END_PARAMETER'
|
197
|
+
ALTERNATION = 'ALTERNATION'
|
198
|
+
TEXT = 'TEXT'
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -1,38 +1,32 @@
|
|
1
1
|
require 'cucumber/cucumber_expressions/argument'
|
2
2
|
require 'cucumber/cucumber_expressions/tree_regexp'
|
3
3
|
require 'cucumber/cucumber_expressions/errors'
|
4
|
+
require 'cucumber/cucumber_expressions/cucumber_expression_parser'
|
4
5
|
|
5
6
|
module Cucumber
|
6
7
|
module CucumberExpressions
|
7
8
|
class CucumberExpression
|
8
|
-
# Does not include (){} characters because they have special meaning
|
9
|
-
ESCAPE_REGEXP = /([\\^\[$.|?*+\]])/
|
10
|
-
PARAMETER_REGEXP = /(\\\\)?{([^}]*)}/
|
11
|
-
OPTIONAL_REGEXP = /(\\\\)?\(([^)]+)\)/
|
12
|
-
ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = /([^\s^\/]+)((\/[^\s^\/]+)+)/
|
13
|
-
DOUBLE_ESCAPE = '\\\\'
|
14
|
-
PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE = 'Parameter types cannot be alternative: '
|
15
|
-
PARAMETER_TYPES_CANNOT_BE_OPTIONAL = 'Parameter types cannot be optional: '
|
16
9
|
|
17
|
-
|
10
|
+
ESCAPE_PATTERN = /([\\^\[({$.|?*+})\]])/
|
18
11
|
|
19
12
|
def initialize(expression, parameter_type_registry)
|
20
|
-
@
|
13
|
+
@expression = expression
|
14
|
+
@parameter_type_registry = parameter_type_registry
|
21
15
|
@parameter_types = []
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
expression = process_parameters(expression, parameter_type_registry)
|
27
|
-
expression = "^#{expression}$"
|
28
|
-
|
29
|
-
@tree_regexp = TreeRegexp.new(expression)
|
16
|
+
parser = CucumberExpressionParser.new
|
17
|
+
ast = parser.parse(expression)
|
18
|
+
pattern = rewrite_to_regex(ast)
|
19
|
+
@tree_regexp = TreeRegexp.new(pattern)
|
30
20
|
end
|
31
21
|
|
32
22
|
def match(text)
|
33
23
|
Argument.build(@tree_regexp, text, @parameter_types)
|
34
24
|
end
|
35
25
|
|
26
|
+
def source
|
27
|
+
@expression
|
28
|
+
end
|
29
|
+
|
36
30
|
def regexp
|
37
31
|
@tree_regexp.regexp
|
38
32
|
end
|
@@ -43,76 +37,86 @@ module Cucumber
|
|
43
37
|
|
44
38
|
private
|
45
39
|
|
46
|
-
def
|
47
|
-
|
40
|
+
def rewrite_to_regex(node)
|
41
|
+
case node.type
|
42
|
+
when NodeType::TEXT
|
43
|
+
return escape_regex(node.text)
|
44
|
+
when NodeType::OPTIONAL
|
45
|
+
return rewrite_optional(node)
|
46
|
+
when NodeType::ALTERNATION
|
47
|
+
return rewrite_alternation(node)
|
48
|
+
when NodeType::ALTERNATIVE
|
49
|
+
return rewrite_alternative(node)
|
50
|
+
when NodeType::PARAMETER
|
51
|
+
return rewrite_parameter(node)
|
52
|
+
when NodeType::EXPRESSION
|
53
|
+
return rewrite_expression(node)
|
54
|
+
else
|
55
|
+
# Can't happen as long as the switch case is exhaustive
|
56
|
+
raise "#{node.type}"
|
57
|
+
end
|
48
58
|
end
|
49
59
|
|
50
|
-
def
|
51
|
-
|
52
|
-
expression.gsub(OPTIONAL_REGEXP) do
|
53
|
-
g2 = $2
|
54
|
-
# When using Parameter Types, the () characters are used to represent an optional
|
55
|
-
# item such as (a ) which would be equivalent to (?:a )? in regex
|
56
|
-
#
|
57
|
-
# You cannot have optional Parameter Types i.e. ({int}) as this causes
|
58
|
-
# problems during the conversion phase to regex. So we check for that here
|
59
|
-
#
|
60
|
-
# One exclusion to this rule is if you actually want the brackets i.e. you
|
61
|
-
# want to capture (3) then we still permit this as an individual rule
|
62
|
-
# See: https://github.com/cucumber/cucumber-ruby/issues/1337 for more info
|
63
|
-
# look for double-escaped parentheses
|
64
|
-
if $1 == DOUBLE_ESCAPE
|
65
|
-
"\\(#{g2}\\)"
|
66
|
-
else
|
67
|
-
check_no_parameter_type(g2, PARAMETER_TYPES_CANNOT_BE_OPTIONAL)
|
68
|
-
"(?:#{g2})?"
|
69
|
-
end
|
70
|
-
end
|
60
|
+
def escape_regex(expression)
|
61
|
+
expression.gsub(ESCAPE_PATTERN, '\\\\\1')
|
71
62
|
end
|
72
63
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
replacement.split(/\|/).each do |part|
|
80
|
-
check_no_parameter_type(part, PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE)
|
81
|
-
end
|
82
|
-
"(?:#{replacement})"
|
83
|
-
else
|
84
|
-
replacement
|
85
|
-
end
|
86
|
-
end
|
64
|
+
def rewrite_optional(node)
|
65
|
+
assert_no_parameters(node) { |astNode| raise ParameterIsNotAllowedInOptional.new(astNode, @expression) }
|
66
|
+
assert_no_optionals(node) { |astNode| raise OptionalIsNotAllowedInOptional.new(astNode, @expression) }
|
67
|
+
assert_not_empty(node) { |astNode| raise OptionalMayNotBeEmpty.new(astNode, @expression) }
|
68
|
+
regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('')
|
69
|
+
"(?:#{regex})?"
|
87
70
|
end
|
88
71
|
|
89
|
-
def
|
90
|
-
#
|
91
|
-
|
92
|
-
if
|
93
|
-
|
94
|
-
else
|
95
|
-
type_name = $2
|
96
|
-
ParameterType.check_parameter_type_name(type_name)
|
97
|
-
parameter_type = parameter_type_registry.lookup_by_type_name(type_name)
|
98
|
-
raise UndefinedParameterTypeError.new(type_name) if parameter_type.nil?
|
99
|
-
@parameter_types.push(parameter_type)
|
100
|
-
|
101
|
-
build_capture_regexp(parameter_type.regexps)
|
72
|
+
def rewrite_alternation(node)
|
73
|
+
# Make sure the alternative parts aren't empty and don't contain parameter types
|
74
|
+
node.nodes.each { |alternative|
|
75
|
+
if alternative.nodes.length == 0
|
76
|
+
raise AlternativeMayNotBeEmpty.new(alternative, @expression)
|
102
77
|
end
|
103
|
-
|
78
|
+
assert_not_empty(alternative) { |astNode| raise AlternativeMayNotExclusivelyContainOptionals.new(astNode, @expression) }
|
79
|
+
}
|
80
|
+
regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('|')
|
81
|
+
"(?:#{regex})"
|
104
82
|
end
|
105
83
|
|
106
|
-
def
|
107
|
-
|
108
|
-
capture_groups = regexps.map { |group| "(?:#{group})" }
|
109
|
-
"(#{capture_groups.join('|')})"
|
84
|
+
def rewrite_alternative(node)
|
85
|
+
node.nodes.map { |lastNode| rewrite_to_regex(lastNode) }.join('')
|
110
86
|
end
|
111
87
|
|
112
|
-
def
|
113
|
-
|
114
|
-
|
88
|
+
def rewrite_parameter(node)
|
89
|
+
name = node.text
|
90
|
+
parameter_type = @parameter_type_registry.lookup_by_type_name(name)
|
91
|
+
if parameter_type.nil?
|
92
|
+
raise UndefinedParameterTypeError.new(node, @expression, name)
|
115
93
|
end
|
94
|
+
@parameter_types.push(parameter_type)
|
95
|
+
regexps = parameter_type.regexps
|
96
|
+
if regexps.length == 1
|
97
|
+
return "(#{regexps[0]})"
|
98
|
+
end
|
99
|
+
"((?:#{regexps.join(')|(?:')}))"
|
100
|
+
end
|
101
|
+
|
102
|
+
def rewrite_expression(node)
|
103
|
+
regex = node.nodes.map { |n| rewrite_to_regex(n) }.join('')
|
104
|
+
"^#{regex}$"
|
105
|
+
end
|
106
|
+
|
107
|
+
def assert_not_empty(node, &raise_error)
|
108
|
+
text_nodes = node.nodes.filter { |astNode| NodeType::TEXT == astNode.type }
|
109
|
+
raise_error.call(node) if text_nodes.length == 0
|
110
|
+
end
|
111
|
+
|
112
|
+
def assert_no_parameters(node, &raise_error)
|
113
|
+
nodes = node.nodes.filter { |astNode| NodeType::PARAMETER == astNode.type }
|
114
|
+
raise_error.call(nodes[0]) if nodes.length > 0
|
115
|
+
end
|
116
|
+
|
117
|
+
def assert_no_optionals(node, &raise_error)
|
118
|
+
nodes = node.nodes.filter { |astNode| NodeType::OPTIONAL == astNode.type }
|
119
|
+
raise_error.call(nodes[0]) if nodes.length > 0
|
116
120
|
end
|
117
121
|
end
|
118
122
|
end
|