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
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
|