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