cucumber-cucumber-expressions 10.1.0 → 11.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.rsync +1 -0
  3. data/VERSION +1 -0
  4. data/cucumber-cucumber-expressions.gemspec +5 -2
  5. data/default.mk +13 -6
  6. data/examples.txt +13 -1
  7. data/lib/cucumber/cucumber_expressions/ast.rb +201 -0
  8. data/lib/cucumber/cucumber_expressions/cucumber_expression.rb +80 -76
  9. data/lib/cucumber/cucumber_expressions/cucumber_expression_generator.rb +4 -8
  10. data/lib/cucumber/cucumber_expressions/cucumber_expression_parser.rb +219 -0
  11. data/lib/cucumber/cucumber_expressions/cucumber_expression_tokenizer.rb +95 -0
  12. data/lib/cucumber/cucumber_expressions/errors.rb +175 -4
  13. data/lib/cucumber/cucumber_expressions/group.rb +1 -1
  14. data/lib/cucumber/cucumber_expressions/parameter_type.rb +12 -7
  15. data/lib/cucumber/cucumber_expressions/parameter_type_matcher.rb +2 -2
  16. data/lib/cucumber/cucumber_expressions/parameter_type_registry.rb +1 -1
  17. data/lib/cucumber/cucumber_expressions/tree_regexp.rb +53 -46
  18. data/spec/cucumber/cucumber_expressions/cucumber_expression_parser_spec.rb +24 -0
  19. data/spec/cucumber/cucumber_expressions/cucumber_expression_spec.rb +73 -124
  20. data/spec/cucumber/cucumber_expressions/cucumber_expression_tokenizer_spec.rb +24 -0
  21. data/spec/cucumber/cucumber_expressions/custom_parameter_type_spec.rb +1 -1
  22. data/spec/cucumber/cucumber_expressions/expression_examples_spec.rb +1 -1
  23. data/spec/cucumber/cucumber_expressions/expression_factory_spec.rb +0 -4
  24. data/spec/cucumber/cucumber_expressions/tree_regexp_spec.rb +58 -5
  25. data/testdata/ast/alternation-followed-by-optional.yaml +17 -0
  26. data/testdata/ast/alternation-phrase.yaml +16 -0
  27. data/testdata/ast/alternation-with-parameter.yaml +27 -0
  28. data/testdata/ast/alternation-with-unused-end-optional.yaml +15 -0
  29. data/testdata/ast/alternation-with-unused-start-optional.yaml +8 -0
  30. data/testdata/ast/alternation-with-white-space.yaml +12 -0
  31. data/testdata/ast/alternation.yaml +12 -0
  32. data/testdata/ast/anonymous-parameter.yaml +5 -0
  33. data/testdata/ast/closing-brace.yaml +5 -0
  34. data/testdata/ast/closing-parenthesis.yaml +5 -0
  35. data/testdata/ast/empty-alternation.yaml +8 -0
  36. data/testdata/ast/empty-alternations.yaml +9 -0
  37. data/testdata/ast/empty-string.yaml +3 -0
  38. data/testdata/ast/escaped-alternation.yaml +5 -0
  39. data/testdata/ast/escaped-backslash.yaml +5 -0
  40. data/testdata/ast/escaped-opening-parenthesis.yaml +5 -0
  41. data/testdata/ast/escaped-optional-followed-by-optional.yaml +15 -0
  42. data/testdata/ast/escaped-optional-phrase.yaml +10 -0
  43. data/testdata/ast/escaped-optional.yaml +7 -0
  44. data/testdata/ast/opening-brace.yaml +8 -0
  45. data/testdata/ast/opening-parenthesis.yaml +8 -0
  46. data/testdata/ast/optional-containing-nested-optional.yaml +15 -0
  47. data/testdata/ast/optional-phrase.yaml +12 -0
  48. data/testdata/ast/optional.yaml +7 -0
  49. data/testdata/ast/parameter.yaml +7 -0
  50. data/testdata/ast/phrase.yaml +9 -0
  51. data/testdata/ast/unfinished-parameter.yaml +8 -0
  52. data/testdata/expression/allows-escaped-optional-parameter-types.yaml +4 -0
  53. data/testdata/expression/allows-parameter-type-in-alternation-1.yaml +4 -0
  54. data/testdata/expression/allows-parameter-type-in-alternation-2.yaml +4 -0
  55. data/testdata/expression/does-allow-parameter-adjacent-to-alternation.yaml +5 -0
  56. data/testdata/expression/does-not-allow-alternation-in-optional.yaml +9 -0
  57. data/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-left-parameter.yaml +10 -0
  58. data/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-optional.yaml +9 -0
  59. data/testdata/expression/does-not-allow-alternation-with-empty-alternative-by-adjacent-right-parameter.yaml +9 -0
  60. data/testdata/expression/does-not-allow-alternation-with-empty-alternative.yaml +9 -0
  61. data/testdata/expression/does-not-allow-empty-optional.yaml +9 -0
  62. data/testdata/expression/does-not-allow-nested-optional.yaml +8 -0
  63. data/testdata/expression/does-not-allow-optional-parameter-types.yaml +9 -0
  64. data/testdata/expression/does-not-allow-parameter-name-with-reserved-characters.yaml +10 -0
  65. data/testdata/expression/does-not-allow-unfinished-parenthesis-1.yaml +8 -0
  66. data/testdata/expression/does-not-allow-unfinished-parenthesis-2.yaml +8 -0
  67. data/testdata/expression/does-not-allow-unfinished-parenthesis-3.yaml +8 -0
  68. data/testdata/expression/does-not-match-misquoted-string.yaml +4 -0
  69. data/testdata/expression/doesnt-match-float-as-int.yaml +5 -0
  70. data/testdata/expression/matches-alternation.yaml +4 -0
  71. data/testdata/expression/matches-anonymous-parameter-type.yaml +5 -0
  72. data/testdata/expression/matches-double-quoted-empty-string-as-empty-string-along-with-other-strings.yaml +4 -0
  73. data/testdata/expression/matches-double-quoted-empty-string-as-empty-string.yaml +4 -0
  74. data/testdata/expression/matches-double-quoted-string-with-escaped-double-quote.yaml +4 -0
  75. data/testdata/expression/matches-double-quoted-string-with-single-quotes.yaml +4 -0
  76. data/testdata/expression/matches-double-quoted-string.yaml +4 -0
  77. data/testdata/expression/matches-doubly-escaped-parenthesis.yaml +4 -0
  78. data/testdata/expression/matches-doubly-escaped-slash-1.yaml +4 -0
  79. data/testdata/expression/matches-doubly-escaped-slash-2.yaml +4 -0
  80. data/testdata/expression/matches-escaped-parenthesis-1.yaml +4 -0
  81. data/testdata/expression/matches-escaped-parenthesis-2.yaml +4 -0
  82. data/testdata/expression/matches-escaped-parenthesis-3.yaml +4 -0
  83. data/testdata/expression/matches-escaped-slash.yaml +4 -0
  84. data/testdata/expression/matches-float-1.yaml +5 -0
  85. data/testdata/expression/matches-float-2.yaml +5 -0
  86. data/testdata/expression/matches-int.yaml +5 -0
  87. data/testdata/expression/matches-multiple-double-quoted-strings.yaml +4 -0
  88. data/testdata/expression/matches-multiple-single-quoted-strings.yaml +4 -0
  89. data/testdata/expression/matches-optional-before-alternation-1.yaml +4 -0
  90. data/testdata/expression/matches-optional-before-alternation-2.yaml +4 -0
  91. data/testdata/expression/matches-optional-before-alternation-with-regex-characters-1.yaml +4 -0
  92. data/testdata/expression/matches-optional-before-alternation-with-regex-characters-2.yaml +4 -0
  93. data/testdata/expression/matches-optional-in-alternation-1.yaml +5 -0
  94. data/testdata/expression/matches-optional-in-alternation-2.yaml +5 -0
  95. data/testdata/expression/matches-optional-in-alternation-3.yaml +5 -0
  96. data/testdata/expression/matches-single-quoted-empty-string-as-empty-string-along-with-other-strings.yaml +4 -0
  97. data/testdata/expression/matches-single-quoted-empty-string-as-empty-string.yaml +4 -0
  98. data/testdata/expression/matches-single-quoted-string-with-double-quotes.yaml +4 -0
  99. data/testdata/expression/matches-single-quoted-string-with-escaped-single-quote.yaml +4 -0
  100. data/testdata/expression/matches-single-quoted-string.yaml +4 -0
  101. data/testdata/expression/matches-word.yaml +4 -0
  102. data/testdata/expression/throws-unknown-parameter-type.yaml +10 -0
  103. data/testdata/regex/alternation-with-optional.yaml +2 -0
  104. data/testdata/regex/alternation.yaml +2 -0
  105. data/testdata/regex/empty.yaml +2 -0
  106. data/testdata/regex/escape-regex-characters.yaml +2 -0
  107. data/testdata/regex/optional.yaml +2 -0
  108. data/testdata/regex/parameter.yaml +2 -0
  109. data/testdata/regex/text.yaml +2 -0
  110. data/testdata/regex/unicode.yaml +2 -0
  111. data/testdata/tokens/alternation-phrase.yaml +13 -0
  112. data/testdata/tokens/alternation.yaml +9 -0
  113. data/testdata/tokens/empty-string.yaml +6 -0
  114. data/testdata/tokens/escape-non-reserved-character.yaml +8 -0
  115. data/testdata/tokens/escaped-alternation.yaml +9 -0
  116. data/testdata/tokens/escaped-char-has-start-index-of-text-token.yaml +9 -0
  117. data/testdata/tokens/escaped-end-of-line.yaml +8 -0
  118. data/testdata/tokens/escaped-optional.yaml +7 -0
  119. data/testdata/tokens/escaped-parameter.yaml +7 -0
  120. data/testdata/tokens/escaped-space.yaml +7 -0
  121. data/testdata/tokens/optional-phrase.yaml +13 -0
  122. data/testdata/tokens/optional.yaml +9 -0
  123. data/testdata/tokens/parameter-phrase.yaml +13 -0
  124. data/testdata/tokens/parameter.yaml +9 -0
  125. data/testdata/tokens/phrase.yaml +11 -0
  126. metadata +117 -11
  127. 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: ed8966625e5c34e68e12635a42b5abcb4cb02767825e12d5dc6605175ccddfe8
4
- data.tar.gz: 280642a65a2aab021bd9402455617fdd1560a506ae3dc241e90b46a489ddef26
3
+ metadata.gz: 2d322bd3d9bfcf753132e6f053b408e261a65622719d46f821c3ccece9ecbb83
4
+ data.tar.gz: 34ad192b82c6786750f71b85038eee3fd95a56d583ce197ebdaf53aeeccd95ce
5
5
  SHA512:
6
- metadata.gz: bfa789a7499f47745bf427803456e98d0583a601c83e2af8b9d3e9563cd09ae00ad014f1238be00e6a607c946cf058654f0ff54284bac5748241f9e36d27dcc3
7
- data.tar.gz: 31b396476b60e5d6b49397fd49f1b36d926abcaeabef1998baa24482a15938b6e61801c415b00688f925ddf5caa8a13bd68124b4a92eb82be42f2c4bdef33a5f
6
+ metadata.gz: bc1ed4bbfe1d4da2974619df9f47ec450c26f1b4881bc3041483f923854361aee757e459c2dc66a563aecb130f722a933778d68bfcdb6a177e6abadfaff5b851
7
+ data.tar.gz: b0384445624c1344f8830f4da5dafcc8de9deb63d21169b272518562a169ed40e1f2bbcb99e6a0d6ec66cbcca926f7dc379428bdfaa13ef4426945ddf522d268
data/.rsync CHANGED
@@ -2,3 +2,4 @@
2
2
  ../../.templates/github/ .github/
3
3
  ../../.templates/ruby/ .
4
4
  ../examples.txt examples.txt
5
+ ../testdata .
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 11.0.0
@@ -1,7 +1,10 @@
1
1
  # -*- encoding: utf-8 -*-
2
+
3
+ version = File.read(File.expand_path("VERSION", __dir__)).strip
4
+
2
5
  Gem::Specification.new do |s|
3
6
  s.name = 'cucumber-cucumber-expressions'
4
- s.version = '10.1.0'
7
+ s.version = version
5
8
  s.authors = ["Aslak Hellesøy"]
6
9
  s.description = 'Cucumber Expressions - a simpler alternative to Regular Expressions'
7
10
  s.summary = "cucumber-expressions-#{s.version}"
@@ -20,7 +23,7 @@ Gem::Specification.new do |s|
20
23
  }
21
24
 
22
25
  s.add_development_dependency 'rake', '~> 13.0', '>= 13.0.1'
23
- s.add_development_dependency 'rspec', '~> 3.9', '>= 3.9.0'
26
+ s.add_development_dependency 'rspec', '~> 3.10', '>= 3.10.0'
24
27
 
25
28
  s.rubygems_version = ">= 1.6.1"
26
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,22 +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
- ifneq (,$(GEMSPEC))
50
- sed -i "s/\(s\.version *= *'\)[0-9]*\.[0-9]*\.[0-9]*\('\)/\1$(NEW_VERSION)\2/" $(GEMSPEC)
56
+ @echo "$(NEW_VERSION)" > VERSION
51
57
  endif
52
- else
53
- @echo -e "\033[0;31mNEW_VERSION is not defined. Can't update version :-(\033[0m"
54
- exit 1
55
58
  endif
56
59
  .PHONY: update-version
57
60
 
58
61
  publish: gem
62
+ ifeq ($(IS_TESTDATA),-testdata)
63
+ # no-op
64
+ else
59
65
  ifneq (,$(GEMSPEC))
60
66
  gem push $(GEM)
61
67
  else
62
68
  @echo "Not publishing because there is no gemspec"
63
69
  endif
70
+ endif
64
71
  .PHONY: publish
65
72
 
66
73
  post-release:
@@ -72,5 +79,5 @@ clean: clean-ruby
72
79
  .PHONY: clean
73
80
 
74
81
  clean-ruby:
75
- rm -f .deps .linked .tested* Gemfile.lock *.gem
82
+ rm -rf .deps .linked .tested* Gemfile.lock *.gem acceptance
76
83
  .PHONY: clean-ruby
@@ -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
- attr_reader :source
10
+ ESCAPE_PATTERN = /([\\^\[({$.|?*+})\]])/
18
11
 
19
12
  def initialize(expression, parameter_type_registry)
20
- @source = expression
13
+ @expression = expression
14
+ @parameter_type_registry = parameter_type_registry
21
15
  @parameter_types = []
22
-
23
- expression = process_escapes(expression)
24
- expression = process_optional(expression)
25
- expression = process_alternation(expression)
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 process_escapes(expression)
47
- expression.gsub(ESCAPE_REGEXP, '\\\\\1')
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 process_optional(expression)
51
- # Create non-capturing, optional capture groups from parenthesis
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 process_alternation(expression)
74
- expression.gsub(ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP) do
75
- # replace \/ with /
76
- # replace / with |
77
- replacement = $&.tr('/', '|').gsub(/\\\|/, '/')
78
- if replacement.include?('|')
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 process_parameters(expression, parameter_type_registry)
90
- # Create non-capturing, optional capture groups from parenthesis
91
- expression.gsub(PARAMETER_REGEXP) do
92
- if ($1 == DOUBLE_ESCAPE)
93
- "\\{#{$2}\\}"
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
- end
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 build_capture_regexp(regexps)
107
- return "(#{regexps[0]})" if regexps.size == 1
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 check_no_parameter_type(s, message)
113
- if PARAMETER_REGEXP =~ s
114
- raise CucumberExpressionError.new("#{message}#{source}")
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