dentaku 3.4.2 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +4 -5
  4. data/lib/dentaku/ast/access.rb +6 -0
  5. data/lib/dentaku/ast/arithmetic.rb +23 -18
  6. data/lib/dentaku/ast/array.rb +4 -0
  7. data/lib/dentaku/ast/bitwise.rb +30 -5
  8. data/lib/dentaku/ast/case/case_conditional.rb +4 -0
  9. data/lib/dentaku/ast/case/case_else.rb +6 -0
  10. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  11. data/lib/dentaku/ast/case/case_then.rb +6 -0
  12. data/lib/dentaku/ast/case/case_when.rb +10 -0
  13. data/lib/dentaku/ast/case.rb +6 -0
  14. data/lib/dentaku/ast/comparators.rb +35 -35
  15. data/lib/dentaku/ast/function.rb +6 -8
  16. data/lib/dentaku/ast/functions/all.rb +4 -17
  17. data/lib/dentaku/ast/functions/any.rb +4 -17
  18. data/lib/dentaku/ast/functions/duration.rb +2 -2
  19. data/lib/dentaku/ast/functions/enum.rb +37 -0
  20. data/lib/dentaku/ast/functions/filter.rb +4 -17
  21. data/lib/dentaku/ast/functions/if.rb +4 -0
  22. data/lib/dentaku/ast/functions/map.rb +3 -16
  23. data/lib/dentaku/ast/functions/pluck.rb +8 -7
  24. data/lib/dentaku/ast/functions/ruby_math.rb +3 -2
  25. data/lib/dentaku/ast/functions/xor.rb +44 -0
  26. data/lib/dentaku/ast/identifier.rb +8 -0
  27. data/lib/dentaku/ast/literal.rb +10 -0
  28. data/lib/dentaku/ast/negation.rb +4 -0
  29. data/lib/dentaku/ast/nil.rb +4 -0
  30. data/lib/dentaku/ast/node.rb +4 -0
  31. data/lib/dentaku/ast/operation.rb +9 -0
  32. data/lib/dentaku/ast/string.rb +7 -0
  33. data/lib/dentaku/ast.rb +2 -0
  34. data/lib/dentaku/bulk_expression_solver.rb +1 -5
  35. data/lib/dentaku/exceptions.rb +2 -2
  36. data/lib/dentaku/parser.rb +10 -3
  37. data/lib/dentaku/print_visitor.rb +101 -0
  38. data/lib/dentaku/token_scanner.rb +3 -3
  39. data/lib/dentaku/version.rb +1 -1
  40. data/lib/dentaku/visitor/infix.rb +82 -0
  41. data/spec/ast/all_spec.rb +25 -0
  42. data/spec/ast/any_spec.rb +23 -0
  43. data/spec/ast/arithmetic_spec.rb +7 -0
  44. data/spec/ast/comparator_spec.rb +14 -9
  45. data/spec/ast/filter_spec.rb +7 -0
  46. data/spec/ast/function_spec.rb +5 -0
  47. data/spec/ast/map_spec.rb +12 -0
  48. data/spec/ast/or_spec.rb +1 -1
  49. data/spec/ast/pluck_spec.rb +32 -0
  50. data/spec/ast/xor_spec.rb +35 -0
  51. data/spec/bulk_expression_solver_spec.rb +9 -0
  52. data/spec/calculator_spec.rb +71 -2
  53. data/spec/parser_spec.rb +18 -3
  54. data/spec/print_visitor_spec.rb +66 -0
  55. data/spec/tokenizer_spec.rb +18 -0
  56. data/spec/visitor/infix_spec.rb +31 -0
  57. data/spec/visitor_spec.rb +138 -0
  58. metadata +24 -6
@@ -61,4 +61,9 @@ describe Dentaku::AST::Function do
61
61
  expect { described_class.numeric('7.') }.to raise_error Dentaku::ArgumentError
62
62
  expect { described_class.numeric(true) }.to raise_error Dentaku::ArgumentError
63
63
  end
64
+
65
+ it "allows read access to arguments" do
66
+ fn = described_class.new(1, 2, 3)
67
+ expect(fn.args).to eq([1, 2, 3])
68
+ end
64
69
  end
data/spec/ast/map_spec.rb CHANGED
@@ -3,6 +3,7 @@ require 'dentaku/ast/functions/map'
3
3
  require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Map do
6
+ let(:calculator) { Dentaku::Calculator.new }
6
7
  it 'operates on each value in an array' do
7
8
  result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
8
9
  expect(result).to eq(9)
@@ -12,4 +13,15 @@ describe Dentaku::AST::Map do
12
13
  result = Dentaku('MAP(vals, val, val + 1)', vals: [])
13
14
  expect(result).to eq([])
14
15
  end
16
+
17
+ it 'works with a single value if needed for some reason' do
18
+ result = Dentaku('MAP(vals, val, val + 1)', vals: 1)
19
+ expect(result).to eq([2])
20
+ end
21
+
22
+ it 'raises argument error if a string is passed as identifier' do
23
+ expect { calculator.evaluate!('MAP({1, 2, 3}, "val", val + 1)') }.to raise_error(
24
+ Dentaku::ArgumentError, 'MAP() requires second argument to be an identifier'
25
+ )
26
+ end
15
27
  end
data/spec/ast/or_spec.rb CHANGED
@@ -6,7 +6,7 @@ describe 'Dentaku::AST::Or' do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
7
 
8
8
  it 'returns false if all of the arguments are false' do
9
- result = Dentaku('OR(1 = "1", 0 = 1)')
9
+ result = Dentaku('OR(1 = "2", 0 = 1)')
10
10
  expect(result).to eq(false)
11
11
  end
12
12
 
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/pluck'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::Pluck do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+ it 'operates on each value in an array' do
8
+ result = Dentaku('PLUCK(users, age)', users: [
9
+ {name: "Bob", age: 44},
10
+ {name: "Jane", age: 27}
11
+ ])
12
+ expect(result).to eq([44, 27])
13
+ end
14
+
15
+ it 'works with an empty array' do
16
+ result = Dentaku('PLUCK(users, age)', users: [])
17
+ expect(result).to eq([])
18
+ end
19
+
20
+ it 'raises argument error if a string is passed as identifier' do
21
+ expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
22
+ {name: "Bob", age: 44},
23
+ {name: "Jane", age: 27}
24
+ ]) end.to raise_error(Dentaku::ArgumentError, 'PLUCK() requires second argument to be an identifier')
25
+ end
26
+
27
+ it 'raises argument error if a non array of hashes is passed as collection' do
28
+ expect { calculator.evaluate!('PLUCK({1, 2, 3}, age)') }.to raise_error(
29
+ Dentaku::ArgumentError, 'PLUCK() requires first argument to be an array of hashes'
30
+ )
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/ast/functions/or'
4
+
5
+ describe 'Dentaku::AST::Xor' do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+
8
+ it 'returns false if all of the arguments are false' do
9
+ result = Dentaku('XOR(false, false)')
10
+ expect(result).to eq(false)
11
+ end
12
+
13
+ it 'returns true if only one of the arguments is true' do
14
+ result = Dentaku('XOR(false, true)')
15
+ expect(result).to eq(true)
16
+ end
17
+
18
+ it 'returns false if more than one of the arguments is true' do
19
+ result = Dentaku('XOR(false, true, true)')
20
+ expect(result).to eq(false)
21
+ end
22
+
23
+ it 'supports nested expressions' do
24
+ result = Dentaku('XOR(y = 1, x = 1)', x: 1, y: 2)
25
+ expect(result).to eq(true)
26
+ end
27
+
28
+ it 'raises an error if no arguments are passed' do
29
+ expect { calculator.evaluate!('XOR()') }.to raise_error(Dentaku::ParseError)
30
+ end
31
+
32
+ it 'raises an error if a non logical argument is passed' do
33
+ expect { calculator.evaluate!('XOR("r")') }.to raise_error(Dentaku::ArgumentError)
34
+ end
35
+ end
@@ -143,6 +143,15 @@ RSpec.describe Dentaku::BulkExpressionSolver do
143
143
  expect(exception.recipient_variable).to eq('more_apples')
144
144
  end
145
145
 
146
+ it 'stores the recipient variable on the exception when there is an ArgumentError' do
147
+ expressions = {apples: "NULL", more_apples: "1 + apples"}
148
+ exception = nil
149
+ described_class.new(expressions, calculator).solve do |ex|
150
+ exception = ex
151
+ end
152
+ expect(exception.recipient_variable).to eq('more_apples')
153
+ end
154
+
146
155
  it 'safely handles argument errors' do
147
156
  expressions = {i: "a / 5 + d", a: "m * 12", d: "a + b"}
148
157
  result = described_class.new(expressions, calculator.store(m: 3)).solve
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
  require 'dentaku'
3
3
  describe Dentaku::Calculator do
4
4
  let(:calculator) { described_class.new }
5
+ let(:with_case_sensitivity) { described_class.new(case_sensitive: true) }
5
6
  let(:with_memory) { described_class.new.store(apples: 3) }
6
7
  let(:with_aliases) { described_class.new(aliases: { round: ['rrround'] }) }
7
8
  let(:without_nested_data) { described_class.new(nested_data_support: false) }
@@ -39,6 +40,8 @@ describe Dentaku::Calculator do
39
40
  expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
40
41
  expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
41
42
  expect(calculator.evaluate("5%")).to eq (0.05)
43
+ expect(calculator.evaluate('1 << 3')).to eq (8)
44
+ expect(calculator.evaluate('0xFF >> 6')).to eq (3)
42
45
  end
43
46
 
44
47
  describe 'evaluate' do
@@ -66,6 +69,7 @@ describe Dentaku::Calculator do
66
69
  expect(calculator.evaluate('ROUNDDOWN(a)', a: nil)).to be_nil
67
70
  expect(calculator.evaluate('ROUNDUP(a)', a: nil)).to be_nil
68
71
  expect(calculator.evaluate('SUM(a,b)', a: nil, b: nil)).to be_nil
72
+ expect(calculator.evaluate('1.0 & "bar"')).to be_nil
69
73
  end
70
74
 
71
75
  it 'treats explicit nil as logical false' do
@@ -81,6 +85,13 @@ describe Dentaku::Calculator do
81
85
  end
82
86
  end
83
87
 
88
+ describe 'ast' do
89
+ it 'raises parsing errors' do
90
+ expect { calculator.ast('()') }.to raise_error(Dentaku::ParseError)
91
+ expect { calculator.ast('(}') }.to raise_error(Dentaku::TokenizerError)
92
+ end
93
+ end
94
+
84
95
  describe 'evaluate!' do
85
96
  it 'raises exception when formula has error' do
86
97
  expect { calculator.evaluate!('1 + + 1') }.to raise_error(Dentaku::ParseError)
@@ -107,6 +118,9 @@ describe Dentaku::Calculator do
107
118
  expect { calculator.evaluate!('ROUNDDOWN(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
108
119
  expect { calculator.evaluate!('ROUNDUP(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
109
120
  expect { calculator.evaluate!('SUM(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
121
+ expect { calculator.evaluate!('"foo" & "bar"') }.to raise_error(Dentaku::ArgumentError)
122
+ expect { calculator.evaluate!('1.0 & "bar"') }.to raise_error(Dentaku::ArgumentError)
123
+ expect { calculator.evaluate!('1 & "bar"') }.to raise_error(Dentaku::ArgumentError)
110
124
  end
111
125
 
112
126
  it 'raises argument error if a function is called with incorrect arity' do
@@ -186,6 +200,11 @@ describe Dentaku::Calculator do
186
200
  it "finds no dependencies in array literals" do
187
201
  expect(calculator.dependencies([1, 2, 3])).to eq([])
188
202
  end
203
+
204
+ it "finds dependencies in item expressions" do
205
+ expect(calculator.dependencies('MAP(vals, val, val + step)')).to eq(['vals', 'step'])
206
+ expect(calculator.dependencies('ALL(people, person, person.age < adult)')).to eq(['people', 'adult'])
207
+ end
189
208
  end
190
209
 
191
210
  describe 'solve!' do
@@ -257,8 +276,13 @@ describe Dentaku::Calculator do
257
276
 
258
277
  describe 'solve' do
259
278
  it "returns :undefined when variables are unbound" do
260
- expressions = {more_apples: "apples + 1"}
261
- expect(calculator.solve(expressions)).to eq(more_apples: :undefined)
279
+ expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
280
+ expect(calculator.solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
281
+ end
282
+
283
+ it "returns :undefined when variables are nil" do
284
+ expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
285
+ expect(calculator.store(apples: nil).solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
262
286
  end
263
287
 
264
288
  it "allows passing in a custom value to an error handler" do
@@ -304,6 +328,20 @@ describe Dentaku::Calculator do
304
328
  )
305
329
  }.not_to raise_error
306
330
  end
331
+
332
+ it "integrates with custom functions" do
333
+ calculator.add_function(:custom, :integer, -> { 1 })
334
+
335
+ result = calculator.solve(
336
+ a: "1",
337
+ b: "CUSTOM() - a"
338
+ )
339
+
340
+ expect(result).to eq(
341
+ a: 1,
342
+ b: 0
343
+ )
344
+ end
307
345
  end
308
346
 
309
347
  it 'evaluates a statement with no variables' do
@@ -666,6 +704,31 @@ describe Dentaku::Calculator do
666
704
  expect(value).to eq(5)
667
705
  end
668
706
 
707
+ it 'handles nested case statements with case-sensitivity' do
708
+ formula = <<-FORMULA
709
+ CASE fruit
710
+ WHEN 'apple'
711
+ THEN 1 * quantity
712
+ WHEN 'banana'
713
+ THEN
714
+ CASE QUANTITY
715
+ WHEN 1 THEN 2
716
+ WHEN 10 THEN
717
+ CASE type
718
+ WHEN 'organic' THEN 5
719
+ END
720
+ END
721
+ END
722
+ FORMULA
723
+ value = with_case_sensitivity.evaluate(
724
+ formula,
725
+ type: 'organic',
726
+ quantity: 1,
727
+ QUANTITY: 10,
728
+ fruit: 'banana')
729
+ expect(value).to eq(5)
730
+ end
731
+
669
732
  it 'handles multiple nested case statements' do
670
733
  formula = <<-FORMULA
671
734
  CASE fruit
@@ -710,6 +773,12 @@ describe Dentaku::Calculator do
710
773
  end
711
774
  end
712
775
  end
776
+
777
+ it 'are defined with a properly named class that represents it to support AST marshaling' do
778
+ expect {
779
+ Marshal.dump(calculator.ast('SQRT(20)'))
780
+ }.not_to raise_error
781
+ end
713
782
  end
714
783
 
715
784
  describe 'disable_cache' do
data/spec/parser_spec.rb CHANGED
@@ -71,6 +71,11 @@ describe Dentaku::Parser do
71
71
  expect(node.value("x" => 3)).to eq(4)
72
72
  end
73
73
 
74
+ it 'evaluates a nested case statement with case-sensitivity' do
75
+ node = parse('CASE x WHEN 1 THEN CASE Y WHEN "A" THEN 2 WHEN "B" THEN 3 END END', { case_sensitive: true }, { case_sensitive: true })
76
+ expect(node.value("x" => 1, "y" => "A", "Y" => "B")).to eq(3)
77
+ end
78
+
74
79
  it 'evaluates arrays' do
75
80
  node = parse('{1, 2, 3}')
76
81
  expect(node.value).to eq([1, 2, 3])
@@ -89,6 +94,16 @@ describe Dentaku::Parser do
89
94
  }.to raise_error(Dentaku::ParseError)
90
95
  end
91
96
 
97
+ it 'raises a parse error for too many operands' do
98
+ expect {
99
+ parse("IF(1, 0, IF(1, 2, 3, 4))")
100
+ }.to raise_error(Dentaku::ParseError)
101
+
102
+ expect {
103
+ parse("CASE a WHEN 1 THEN true ELSE THEN IF(1, 2, 3, 4) END")
104
+ }.to raise_error(Dentaku::ParseError)
105
+ end
106
+
92
107
  it 'raises a parse error for bad grouping structure' do
93
108
  expect {
94
109
  parse(",")
@@ -144,8 +159,8 @@ describe Dentaku::Parser do
144
159
 
145
160
  private
146
161
 
147
- def parse(expr)
148
- tokens = Dentaku::Tokenizer.new.tokenize(expr)
149
- described_class.new(tokens).parse
162
+ def parse(expr, parser_options = {}, tokenizer_options = {})
163
+ tokens = Dentaku::Tokenizer.new.tokenize(expr, tokenizer_options)
164
+ described_class.new(tokens, parser_options).parse
150
165
  end
151
166
  end
@@ -0,0 +1,66 @@
1
+ require 'dentaku/print_visitor'
2
+ require 'dentaku/tokenizer'
3
+ require 'dentaku/parser'
4
+
5
+ describe Dentaku::PrintVisitor do
6
+ it 'prints a representation of an AST' do
7
+ repr = roundtrip('5+4')
8
+ expect(repr).to eq('5 + 4')
9
+ end
10
+
11
+ it 'quotes string literals' do
12
+ repr = roundtrip('Concat(\'a\', "B")')
13
+ expect(repr).to eq('CONCAT("a", "B")')
14
+ end
15
+
16
+ it 'handles unary operations on literals' do
17
+ repr = roundtrip('- 4')
18
+ expect(repr).to eq('-4')
19
+ end
20
+
21
+ it 'handles unary operations on trees' do
22
+ repr = roundtrip('- (5 + 5)')
23
+ expect(repr).to eq('-(5 + 5)')
24
+ end
25
+
26
+ it 'handles a complex arithmetic expression' do
27
+ repr = roundtrip('(((1 + 7) * (8 ^ 2)) / - (3.0 - apples))')
28
+ expect(repr).to eq('(1 + 7) * 8 ^ 2 / -(3.0 - apples)')
29
+ end
30
+
31
+ it 'handles a complex logical expression' do
32
+ repr = roundtrip('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
33
+ expect(repr).to eq('1 < 2 and 3 <= 4 or 5 > 6 and 7 >= 8 or 9 != 10 and true')
34
+ end
35
+
36
+ it 'handles a function call' do
37
+ repr = roundtrip('IF(a[0] = NULL, "five", \'seven\')')
38
+ expect(repr).to eq('IF(a[0] = NULL, "five", "seven")')
39
+ end
40
+
41
+ it 'handles a case statement' do
42
+ repr = roundtrip('case (a % 5) when 0 then a else b end')
43
+ expect(repr).to eq('CASE a % 5 WHEN 0 THEN a ELSE b END')
44
+ end
45
+
46
+ it 'handles a bitwise operators' do
47
+ repr = roundtrip('0xCAFE & 0xDECAF | 0xBEEF')
48
+ expect(repr).to eq('0xCAFE & 0xDECAF | 0xBEEF')
49
+ end
50
+
51
+ it 'handles a datetime literal' do
52
+ repr = roundtrip('2017-12-24 23:59:59')
53
+ expect(repr).to eq('2017-12-24 23:59:59')
54
+ end
55
+
56
+ private
57
+
58
+ def roundtrip(string)
59
+ described_class.new(parsed(string)).to_s
60
+ end
61
+
62
+ def parsed(string)
63
+ tokens = Dentaku::Tokenizer.new.tokenize(string)
64
+ Dentaku::Parser.new(tokens).parse
65
+ end
66
+ end
@@ -25,6 +25,12 @@ describe Dentaku::Tokenizer do
25
25
  expect(tokens.map(&:category)).to eq([:numeric])
26
26
  expect(tokens.map(&:value)).to eq([6.02e23])
27
27
  end
28
+
29
+ tokens = tokenizer.tokenize('6E23')
30
+ expect(tokens.map(&:value)).to eq([0.6e24])
31
+
32
+ tokens = tokenizer.tokenize('6e-23')
33
+ expect(tokens.map(&:value)).to eq([0.6e-22])
28
34
  end
29
35
 
30
36
  it 'tokenizes addition' do
@@ -83,6 +89,18 @@ describe Dentaku::Tokenizer do
83
89
  expect(tokens.map(&:value)).to eq([2, :bitand, 3])
84
90
  end
85
91
 
92
+ it 'tokenizes bitwise SHIFT LEFT' do
93
+ tokens = tokenizer.tokenize('2 << 3')
94
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
95
+ expect(tokens.map(&:value)).to eq([2, :bitshiftleft, 3])
96
+ end
97
+
98
+ it 'tokenizes bitwise SHIFT RIGHT' do
99
+ tokens = tokenizer.tokenize('2 >> 3')
100
+ expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
101
+ expect(tokens.map(&:value)).to eq([2, :bitshiftright, 3])
102
+ end
103
+
86
104
  it 'ignores whitespace' do
87
105
  tokens = tokenizer.tokenize('1 / 1 ')
88
106
  expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ require 'dentaku/visitor/infix'
4
+
5
+ class ArrayProcessor
6
+ attr_reader :expression
7
+ include Dentaku::Visitor::Infix
8
+
9
+ def initialize
10
+ @expression = []
11
+ end
12
+
13
+ def process(node)
14
+ @expression << node.to_s
15
+ end
16
+ end
17
+
18
+ RSpec.describe Dentaku::Visitor::Infix do
19
+ it 'generates array representation of operation' do
20
+ processor = ArrayProcessor.new
21
+ processor.visit(ast('5 + 3'))
22
+ expect(processor.expression).to eq ['5', '+', '3']
23
+ end
24
+
25
+ private
26
+
27
+ def ast(expression)
28
+ tokens = Dentaku::Tokenizer.new.tokenize(expression)
29
+ Dentaku::Parser.new(tokens).parse
30
+ end
31
+ end
@@ -0,0 +1,138 @@
1
+ require 'spec_helper'
2
+ require 'set'
3
+
4
+ class TestVisitor
5
+ attr_reader :visited
6
+
7
+ def initialize(node)
8
+ @visited = Set.new
9
+ node.accept(self)
10
+ end
11
+
12
+ def mark_visited(node)
13
+ @visited.add(node.class.to_s.split("::").last.to_sym)
14
+ end
15
+
16
+ def visit_operation(node)
17
+ mark_visited(node)
18
+
19
+ node.left.accept(self) if node.left
20
+ node.right.accept(self) if node.right
21
+ end
22
+
23
+ def visit_function(node)
24
+ mark_visited(node)
25
+ node.args.each { |a| a.accept(self) }
26
+ end
27
+
28
+ def visit_array(node)
29
+ mark_visited(node)
30
+ end
31
+
32
+ def visit_case(node)
33
+ mark_visited(node)
34
+ node.switch.accept(self)
35
+ node.conditions.each { |c| c.accept(self) }
36
+ node.else && node.else.accept(self)
37
+ end
38
+
39
+ def visit_switch(node)
40
+ mark_visited(node)
41
+ node.node.accept(self)
42
+ end
43
+
44
+ def visit_case_conditional(node)
45
+ mark_visited(node)
46
+ node.when.accept(self)
47
+ node.then.accept(self)
48
+ end
49
+
50
+ def visit_when(node)
51
+ mark_visited(node)
52
+ node.node.accept(self)
53
+ end
54
+
55
+ def visit_then(node)
56
+ mark_visited(node)
57
+ node.node.accept(self)
58
+ end
59
+
60
+ def visit_else(node)
61
+ mark_visited(node)
62
+ node.node.accept(self)
63
+ end
64
+
65
+ def visit_negation(node)
66
+ mark_visited(node)
67
+ node.node.accept(self)
68
+ end
69
+
70
+ def visit_access(node)
71
+ mark_visited(node)
72
+ node.structure.accept(self)
73
+ node.index.accept(self)
74
+ end
75
+
76
+ def visit_literal(node)
77
+ mark_visited(node)
78
+ end
79
+
80
+ def visit_identifier(node)
81
+ mark_visited(node)
82
+ end
83
+
84
+ def visit_nil(node)
85
+ mark_visited(node)
86
+ end
87
+ end
88
+
89
+ describe TestVisitor do
90
+ def generic_subclasses
91
+ [
92
+ :Arithmetic,
93
+ :Bitwise,
94
+ :Combinator,
95
+ :Comparator,
96
+ :Function,
97
+ :FunctionRegistry,
98
+ :Grouping,
99
+ :Literal,
100
+ :Node,
101
+ :Operation,
102
+ :StringFunctions,
103
+ :RubyMath,
104
+ :Enum,
105
+ ]
106
+ end
107
+
108
+ it 'visits all concrete AST node types' do
109
+ @visited = Set.new
110
+
111
+ visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples')
112
+ visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
113
+ visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
114
+ visit_nodes('case (a % 5) when 0 then a else b end')
115
+ visit_nodes('0xCAFE & (0xDECAF << 3) | (0xBEEF >> 5)')
116
+ visit_nodes('2017-12-24 23:59:59')
117
+ visit_nodes('ALL({1, 2, 3}, "val", val % 2 == 0)')
118
+ visit_nodes('ANY(vals, val, val > 1)')
119
+ visit_nodes('COUNT({1, 2, 3})')
120
+ visit_nodes('PLUCK(users, age)')
121
+ visit_nodes('XOR(false, false)')
122
+ visit_nodes('duration(1, day)')
123
+ visit_nodes('MAP(vals, val, val + 1)')
124
+ visit_nodes('FILTER(vals, val, val > 1)')
125
+
126
+ @expected = Set.new(Dentaku::AST::constants - generic_subclasses)
127
+ expect(@visited.sort).to eq(@expected.sort)
128
+ end
129
+
130
+ private
131
+
132
+ def visit_nodes(string)
133
+ tokens = Dentaku::Tokenizer.new.tokenize(string)
134
+ node = Dentaku::Parser.new(tokens).parse
135
+ visitor = TestVisitor.new(node)
136
+ @visited += visitor.visited
137
+ end
138
+ end