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.
Files changed (127) hide show
  1. checksums.yaml +4 -4
  2. data/.rsync +1 -0
  3. data/VERSION +1 -1
  4. data/cucumber-cucumber-expressions.gemspec +2 -2
  5. data/default.mk +12 -0
  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 +118 -13
  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: 5807f447d5dce48e6b1153547119c4ca61b20003f470728ec3e3d6675265aef2
4
- data.tar.gz: 12896256efd40509d7267019c02e3561b81d4f2be1b481fac1696400e5be7d04
3
+ metadata.gz: fd50c1e02d54c7357dfd594b076344ecdb3735fd7e598a772ba63185dc420d80
4
+ data.tar.gz: c48124ac1ce6ccfed189c04a2ea21fcfdd61e659488792e4b0876134422f4086
5
5
  SHA512:
6
- metadata.gz: 3c2eaff08f07d7cacc6d8f643b30dc51941c3bc2acb38e3f63f5d7c3dac7239c04982f41f2da73837c643a11d8c9ad55aee72944e31c1c6e8bd070e4e3ad6973
7
- data.tar.gz: 1ef0bb4db4c9ba93a4baa484cf4158169fcd4174659fa6e3d41588f8bee3307928d508319699957d75dd3f63d2242dc0dc481263ad2a07c3574511ac0051d9d7
6
+ metadata.gz: 41ac685e10b0c5c243faed9af265d3ab63e8bc6d01bf98c96b2e5bd8d077c5b1d842c0eb9b67a73eb05749851314155026117f686f756f97ada664eab66ce181
7
+ data.tar.gz: b2e3f96adecda3a12ca136c575c43fb29d28ef44018bb2a662be7bab10222e7a69217b4ff924b51d807a8166fce18b8d577cc3f80a8dfd4b12255ee629722cd8
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 CHANGED
@@ -1 +1 @@
1
- 10.2.0
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.1'
26
- s.add_development_dependency 'rspec', '~> 3.9', '>= 3.9.0'
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
- 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