sexp_path 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,72 @@
1
+ module SexpPath
2
+ module Traverse
3
+ # Searches for the +pattern+ returning a SexpCollection containing
4
+ # a SexpResult for each match.
5
+ #
6
+ # Example:
7
+ # s(:a, s(:b)) / Q{ s(:b) } => [s(:b)]
8
+ def search(pattern, data={})
9
+ collection = SexpCollection.new
10
+ search_each(pattern,data){|match| collection << match}
11
+ collection
12
+ end
13
+ alias_method :/, :search
14
+
15
+ # Searches for the +pattern+ yielding a SexpResult
16
+ # for each match.
17
+ #
18
+ def search_each(pattern, data={}, &block)
19
+ return false unless pattern.is_a? Sexp
20
+
21
+ if pattern.satisfy?(self, data)
22
+ block.call(SexpResult.new(self, data))
23
+ end
24
+
25
+ self.each do |subset|
26
+ case subset
27
+ when Sexp then subset.search_each(pattern, data, &block)
28
+ end
29
+ end
30
+ end
31
+
32
+ # Searches for the +pattern+ yielding a SexpResult
33
+ # for each match, and replacing it with the result of
34
+ # the block.
35
+ #
36
+ # There is no guarantee that the result will or will not
37
+ # be the same object passed in, meaning this mutates, or replaces
38
+ # the original sexp.
39
+ def replace_sexp(pattern, data={}, &block)
40
+ return self unless pattern.is_a? Sexp
41
+
42
+ if pattern.satisfy?(self, data)
43
+ return block.call(SexpResult.new(self, data))
44
+ end
45
+
46
+ self.each_with_index do |subset, i|
47
+ case subset
48
+ when Sexp then self[i] = (subset.replace_sexp(pattern, data, &block))
49
+ end
50
+ end
51
+
52
+ return self
53
+ end
54
+
55
+ # Sets a named capture for the Matcher. If a SexpResult is returned
56
+ # any named captures will be available it.
57
+ def capture_as(name)
58
+ @capture_name = name
59
+ self
60
+ end
61
+ alias_method :%, :capture_as
62
+
63
+ private
64
+ def capture_match(matching_object, data)
65
+ if @capture_name
66
+ data[@capture_name] = matching_object
67
+ end
68
+
69
+ data
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,55 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/sexp_path'
3
+ require 'parse_tree'
4
+
5
+ class LineNumberingProcessorTest < Test::Unit::TestCase
6
+ def setup
7
+ @path = File.dirname(__FILE__) + '/sample.rb'
8
+ end
9
+
10
+ def test_processing_a_file_fills_in_all_the_line_numbers
11
+ sexp = LineNumberingProcessor.rewrite_file(@path)
12
+ assert !sexp.empty?
13
+ sexp.search_each(Q?{_}) do |s|
14
+ assert !sexp.line.nil?, "Expected a line number for: #{s.sexp.inspect}"
15
+ assert !sexp.file.nil?, "Expected a file for: #{s.sexp.inspect}"
16
+ end
17
+ end
18
+
19
+ # This test may break if sample.rb changes
20
+ def test_finding_known_lines
21
+ sexp = LineNumberingProcessor.rewrite_file(@path)
22
+ lines = open(@path,'r'){|io| io.readlines}
23
+
24
+ assert_line_numbers_equal(
25
+ lines, 'def test_b()',
26
+ sexp, Q?{ s(:defn, :test_b, _) }
27
+ )
28
+
29
+ assert_line_numbers_equal(
30
+ lines, '[apples, oranges, cakes]',
31
+ sexp, Q?{ s(:array, s(:lvar, :apples), s(:lvar, :oranges), s(:lvar, :cakes)) }
32
+ )
33
+
34
+ assert_line_numbers_equal(
35
+ lines, "require 'test/unit'",
36
+ sexp, Q?{ s(:fcall, :require, s(:array, s(:str, "test/unit"))) }
37
+ )
38
+ end
39
+
40
+ private
41
+ def assert_line_numbers_equal(lines, code, sexp, pattern)
42
+ string_line = find_line(lines, code)
43
+ sexp_line = (sexp / pattern).first.sexp.line
44
+
45
+ assert_equal string_line, sexp_line, "Expected to find #{code} at line #{string_line}"
46
+ end
47
+
48
+ def find_line(lines, code)
49
+
50
+ lines.each_with_index do |line,i|
51
+ return i+1 if line.index(code) # 1 based indexing
52
+ end
53
+ return nil
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ require 'test/unit'
2
+ require 'test/unit/testcase'
3
+
4
+ class ExampleTest < Test::Unit::TestCase
5
+ def setup
6
+ 1 + 2
7
+ end
8
+
9
+ def test_a
10
+ assert_equal 1+2, 4
11
+ end
12
+
13
+ def test_b()
14
+ # assert 1+1
15
+ end
16
+
17
+ def test_a
18
+ assert_equal 1+2, 3
19
+ end
20
+
21
+ private
22
+ def helper_method apples, oranges, cakes=nil
23
+ [apples, oranges, cakes].compact.map{|food| food.to_s.upcase}
24
+ end
25
+ end
@@ -0,0 +1,131 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/sexp_path'
3
+
4
+ class SexpPathCaptureTest < Test::Unit::TestCase
5
+ def setup
6
+ @ast_sexp = # Imagine it looks like a ruby AST
7
+ s(:class, :cake,
8
+ s(
9
+ s(:defn, :foo,
10
+ s(:add, :a, :b)
11
+ ),
12
+ s(:defn, :bar,
13
+ s(:sub, :a, :b)
14
+ )
15
+ )
16
+ )
17
+ end
18
+
19
+ def test_match_with_no_capture_defined_should_not_capture
20
+ sexp = s(:a, :b, :c)
21
+ assert_equal({}, Q?{ sexp }.satisfy?(sexp))
22
+ end
23
+
24
+ def test_match_with_capture_defined_should_capture
25
+ sexp = s(:a, :b, :c)
26
+ assert res = Q?{ sexp % 'cake' }.satisfy?(sexp)
27
+ assert_equal(sexp, res['cake'])
28
+ end
29
+
30
+ def test_match_with_multiple_captures
31
+ assert res = Q?{ s(:add, atom % 'A', atom % 'B') }.satisfy?( s(:add, :a, :b) )
32
+ assert_equal(:a, res['A'])
33
+ assert_equal(:b, res['B'])
34
+ end
35
+
36
+ def test_deep_matches
37
+ assert res = Q?{ s(:class, atom % 'name', s( _ % 'def1', _ % 'def2')) }.satisfy?( @ast_sexp )
38
+ assert_equal(:cake, res['name'])
39
+ assert_equal(s(:defn, :foo, s(:add, :a, :b)), res['def1'])
40
+ assert_equal(s(:defn, :bar, s(:sub, :a, :b)), res['def2'])
41
+ end
42
+
43
+ def test_simple_searching
44
+ res = @ast_sexp / Q?{ s(:class, atom % 'name', _) }
45
+ assert_equal 1, res.length
46
+ assert_equal :cake, res.first['name']
47
+ end
48
+
49
+ def test_iterative_searching
50
+ result = @ast_sexp / Q?{ s(:class, atom % 'class', _) } / Q?{ s(:defn, atom % 'method', _) }
51
+ assert_equal 2, result.length, "Should have matched both defn nodes"
52
+
53
+ result.each do |match_data|
54
+ assert match_data['method'], "Should have captured to 'method'"
55
+ assert match_data['class'], "Should have propogated 'class' capture"
56
+ end
57
+ end
58
+
59
+ def test_capturing_any_matchers
60
+ sexp = s(:add, :a, :b)
61
+ assert res = Q?{ any(s(:add, :a, :b), s(:sub, :a, :b)) % 'match' }.satisfy?( sexp )
62
+ assert_equal sexp, res['match']
63
+
64
+ assert res = Q?{ any(s(atom % 'name', :a, :b), s(:sub, :a, :b)) % 'match' }.satisfy?( sexp )
65
+ assert_equal sexp, res['match']
66
+ assert_equal :add, res['name']
67
+ end
68
+
69
+ def test_capturing_all_matchers
70
+ sexp = s(:add, :a, :b)
71
+ assert res = Q?{ all(s(_, :a, :b), s(atom, :a, :b)) % 'match' }.satisfy?( sexp )
72
+ assert_equal sexp, res['match']
73
+
74
+ assert res = Q?{ all(s(_ % 'wild', :a, :b), s(atom % 'atom', :a, :b)) % 'match' }.satisfy?( sexp )
75
+ assert_equal sexp, res['match']
76
+ assert_equal :add, res['wild']
77
+ assert_equal :add, res['atom']
78
+ end
79
+
80
+ def test_capturing_type_matches
81
+ sexp = s(:add, :a, :b)
82
+ assert res = Q?{ t(:add) % 'match' }.satisfy?( sexp )
83
+ assert_equal sexp, res['match']
84
+ end
85
+
86
+ def test_capturing_child_matches
87
+ sexp = s(:a, s(:b, s(:c)))
88
+ assert res = Q?{ s(:a, child( s(atom % 'atom') ) % 'child' ) }.satisfy?( sexp )
89
+ assert_equal s(:b, s(:c)), res['child']
90
+ assert_equal :c, res['atom']
91
+ end
92
+
93
+ def test_catpuring_pattern_matches
94
+ sexp = s(:add, :a, :b)
95
+ assert res = Q?{ s(m(/a../) % 'regexp', :a, :b) }.satisfy?( sexp )
96
+ assert_equal :add, res['regexp']
97
+ end
98
+
99
+ def test_catpuring_include_matches
100
+ sexp = s(:add, :a, :b)
101
+ assert res = Q?{ include(:a) % 'include' }.satisfy?( sexp )
102
+ assert_equal sexp, res['include']
103
+ end
104
+
105
+ def test_catpuring_nested_include_matches
106
+ sexp = s(:add, s(:a), :b)
107
+ assert res = Q?{ include(s(atom % 'atom' )) % 'include' }.satisfy?( sexp )
108
+ assert_equal sexp, res['include']
109
+ assert_equal :a, res['atom']
110
+ end
111
+
112
+ def test_capturing_negations
113
+ sexp = s(:b)
114
+ assert res = Q?{ (-s(:a)) % 'not' }.satisfy?( sexp )
115
+ assert_equal s(:b), res['not']
116
+ end
117
+
118
+ def test_capturing_negation_contents
119
+ sexp = s(:a, :b)
120
+ assert res = Q?{ -((include(:b) % 'b') & t(:c)) }.satisfy?( sexp )
121
+ assert !res['b'], 'b should not be included'
122
+ end
123
+
124
+ def test_capturing_siblings
125
+ sexp = s(s(:a), s(s(:b)), s(:c))
126
+ assert res = Q?{ (s(atom) % 'a') >> (s(atom) % 'c') }.satisfy?( sexp )
127
+ assert_equal s(:a), res['a']
128
+ assert_equal s(:c), res['c']
129
+ end
130
+
131
+ end
@@ -0,0 +1,211 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/sexp_path'
3
+
4
+ class SexpMatchingPathTest < Test::Unit::TestCase
5
+ def setup
6
+ @ast_sexp = # Imagine it looks like a ruby AST
7
+ s(:class, :cake,
8
+ s(
9
+ s(:defn, :foo,
10
+ s(:add, :a, :b)
11
+ ),
12
+ s(:defn, :bar,
13
+ s(:sub, :a, :b)
14
+ )
15
+ )
16
+ )
17
+ end
18
+
19
+ def test_searching_simple_examples
20
+ assert_search_count @ast_sexp, :class, 0,
21
+ "Literal should match nothing"
22
+
23
+ assert_search_count @ast_sexp, Q?{s(:class)}, 0,
24
+ "Should not exactly match anything"
25
+
26
+ assert_search_count @ast_sexp, Q?{s(:add, :a, :b)}, 1,
27
+ "Should exactly match once"
28
+
29
+ assert_search_count s(:a, s(:b, s(:c))), Q?{s(:b, s(:c))}, 1,
30
+ "Should match an exact subset"
31
+
32
+ assert_search_count s(:a, s(:b, s(:c))), Q?{s(:a, s(:c))}, 0,
33
+ "Should not match the child s(:c)"
34
+
35
+ assert_search_count @ast_sexp, Q?{s(:defn, :bar, s(:sub, :a, :b))}, 1,
36
+ "Nested sexp should exactly match once"
37
+ end
38
+
39
+ def test_equality_of_atom
40
+ a = SexpPath::Matcher::Atom.new
41
+ assert a.satisfy?(:a), "Should match a symbol"
42
+ assert a.satisfy?(1), "Should match a number"
43
+ assert a.satisfy?(nil), "Should match nil"
44
+ assert !a.satisfy?(s()), "Should not match an sexp"
45
+ end
46
+
47
+ def test_searching_with_atom
48
+ assert_search_count s(:add, :a, :b), Q?{s(:add, atom, :b)} , 1,
49
+ "atom should match :a"
50
+
51
+ assert_search_count @ast_sexp, Q?{s(:defn, atom, s(atom, :a, :b) )}, 2,
52
+ "atoms should match :foo/:bar and :add/:sub"
53
+
54
+ assert_search_count s(:a, s()), Q?{s(:a, atom)}, 0,
55
+ "atom should not match s()"
56
+ end
57
+
58
+ def test_searching_with_any
59
+ assert_search_count s(:foo, s(:a), s(:b)), Q?{s(any(:a,:b))}, 2,
60
+ "should not match either :a or :b"
61
+
62
+ assert_search_count s(:foo, s(:a), s(:b)), Q?{any( s(:a) ,s(:c))}, 1,
63
+ "sexp should not match s(:a)"
64
+ end
65
+
66
+ def test_equality_of_wildacard
67
+ w = SexpPath::Matcher::Wild.new
68
+ assert w.satisfy?(:a ), "Should match a symbol"
69
+ assert w.satisfy?(1 ), "Should match a number"
70
+ assert w.satisfy?(nil ), "Should match nil"
71
+ assert w.satisfy?([] ), "Should match an array"
72
+ assert w.satisfy?(s() ), "Should match an sexp"
73
+ end
74
+
75
+ def test_searching_with_wildcard
76
+ assert_search_count s(:add, :a, :b), Q?{s(:add, wild, :b)} , 1,
77
+ "wild should match :a"
78
+
79
+ assert_search_count @ast_sexp, Q?{s(:defn, :bar, _)}, 1,
80
+ "should match s(:defn, :bar, s(..))"
81
+
82
+ assert_search_count @ast_sexp, Q?{s(:defn, _, s(_, :a, :b) )}, 2,
83
+ "wilds should match :foo/:bar and :add/:sub"
84
+
85
+ assert_search_count s(:a, s()), Q?{s(:a, _)}, 1,
86
+ "wild should match s()"
87
+
88
+ assert_search_count s(:a, :b, :c), Q?{s(_,_,_)}, 1,
89
+ "multiple wilds should work"
90
+
91
+ assert_search_count @ast_sexp, Q?{wild}, 6,
92
+ "wild should match every sub expression"
93
+ end
94
+
95
+ def test_searching_with_include
96
+ assert_search_count s(:add, :a, :b), Q?{include(:a)} , 1,
97
+ "Sexp should include atom :a"
98
+
99
+ assert_search_count @ast_sexp, Q?{include(:bar)}, 1,
100
+ "Sexp should include atom :bar"
101
+
102
+ assert_search_count @ast_sexp, Q?{s(:defn, atom, include(:a))}, 2,
103
+ "Sexp should match :defn with an sexp including :a"
104
+
105
+ assert_search_count @ast_sexp, Q?{include(:a)}, 2,
106
+ "Sexp should match an sexp including :a"
107
+
108
+ assert_search_count s(:a, s(:b, s(:c))), Q?{s(:a, include(:c))}, 0,
109
+ "Include should not descend"
110
+ end
111
+
112
+ def test_or_matcher
113
+ assert Q?{s(:a) | s(:b)}.satisfy?( s(:a) ), "q(:a) should match s(:a)"
114
+ assert !Q?{s(:a) | s(:b)}.satisfy?( s(:c) ), "Should not match s(:c)"
115
+
116
+ assert_search_count s(:a, s(:b, :c), s(:b, :d)), Q?{s(:b, :c) | s(:b, :d)}, 2,
117
+ "Should match both (:b, :c) and (:b, :d)"
118
+
119
+ assert_search_count @ast_sexp, Q?{s(:add, :a, :b) | s(:defn, :bar, _)}, 2,
120
+ "Should match at any level"
121
+ end
122
+
123
+ # For symetry, kind of silly examples
124
+ def test_and_matcher
125
+ assert !Q?{s(:a) & s(:b)}.satisfy?(s(:a)), "s(:a) is not both s(:a) and s(:b)"
126
+ assert Q?{s(:a) & s(atom)}.satisfy?(s(:a)), "s(:a) matches both criteria"
127
+ end
128
+
129
+ def test_child_matcher
130
+ assert_search_count @ast_sexp, Q?{s(:class, :cake, child( s(:add, :a, :b) ) )}, 1,
131
+ "Should match s(:class, :cake ...) and descend to find s(:add, :a, :b)"
132
+
133
+ assert_search_count @ast_sexp, Q?{s(:class, :cake, child(include(:a)))}, 1,
134
+ "Should match once since there exists a child which includes :a"
135
+ end
136
+
137
+ def test_not_matcher
138
+ assert !Q?{-wild}.satisfy?(s(:a)), "wild should match s(:a)"
139
+ assert Q?{-(s(:b))}.satisfy?(s(:a)), "s(:b) should not match s(:b)"
140
+ assert Q?{is_not(s(:b))}.satisfy?(s(:a)),"should behave the same as unary minus"
141
+ assert !Q?{-(s(atom))}.satisfy?(s(:a)), "should not match, :a is an atom"
142
+ assert Q?{s(is_not(:b))}.satisfy?(s(:a)), "should match s(:a) since the atom is not :b"
143
+ end
144
+
145
+ def test_sibling_matcher
146
+ assert_equal SexpPath::Matcher::Sibling, Q?{(s(:a) >> s(:b))}.class
147
+
148
+ assert Q?{s(:a) >> s(:b)}.satisfy?( s(s(:a), s(:b)) ), "should match s(:a) has an immediate sibling s(:b)"
149
+ assert Q?{s(:a) >> s(:b)}.satisfy?( s(s(:a), s(:b), s(:c)) ), "should match s(:a) has an immediate sibling s(:b)"
150
+ assert Q?{s(:a) >> s(:c)}.satisfy?( s(s(:a), s(:b), s(:c)) ), "should match s(:a) a sibling s(:b)"
151
+ assert !Q?{s(:c) >> s(:a)}.satisfy?( s(s(:a), s(:b), s(:c)) ), "should not match s(:a) does not follow s(:c)"
152
+ assert !Q?{s(:a) >> s(:a)}.satisfy?( s(s(:a)) ), "should not match s(:a) has no siblings"
153
+ assert Q?{s(:a) >> s(:a)}.satisfy?( s(s(:a), s(:b), s(:a)) ), "should match s(:a) has another sibling s(:a)"
154
+
155
+ assert_search_count @ast_sexp, Q?{t(:defn) >> t(:defn)}, 1,
156
+ "Should match s(:add, :a, :b) followed by s(:sub, :a, :b)"
157
+ end
158
+
159
+ def test_pattern_matcher
160
+ assert Q?{m(/a/)}.satisfy?(:a), "Should match :a"
161
+ assert Q?{m(/^test/)}.satisfy?(:test_case), "Should match :test_case"
162
+ assert Q?{m('test')}.satisfy?(:test), "Should match :test #{Q?{m('test')}.inspect}"
163
+ assert !Q?{m('test')}.satisfy?(:test_case), "Should only match whole word 'test'"
164
+ assert !Q?{m(/a/)}.satisfy?(s(:a)), "Should not match s(:a)"
165
+
166
+ assert_search_count @ast_sexp, Q?{s(m(/\w{3}/), :a, :b)}, 2,
167
+ "Should match s(:add, :a, :b) and s(:sub, :a, :b)"
168
+ end
169
+
170
+ def test_search_method
171
+ assert_equal 1, @ast_sexp.search( s(:sub, :a, :b)).length
172
+ assert_equal 2, @ast_sexp.search( Q?{s(:defn, atom, wild)} ).length
173
+ end
174
+
175
+ def test_search_collection
176
+ # test method
177
+ assert_equal SexpPath::SexpCollection, @ast_sexp.search( s(:sub, :a, :b)).class
178
+ # test binary operator
179
+ assert_equal SexpPath::SexpCollection, (@ast_sexp / s(:sub, :a, :b)).class
180
+ # test sub searches
181
+ collection = @ast_sexp / Q?{s(:defn, atom, _)} / Q?{s(atom, :a, :b)}
182
+ assert_equal SexpPath::SexpCollection, collection.class
183
+ assert_equal 2, collection.length
184
+ assert_equal [s(:add, :a, :b), s(:sub, :a, :b)], collection.map{|m| m.sexp}
185
+ end
186
+
187
+ def test_sexp_type_matching
188
+ assert Q?{t(:a)}.satisfy?( s(:a) )
189
+ assert Q?{t(:a)}.satisfy?( s(:a, :b, s(:oh_hai), :d) )
190
+ assert_search_count @ast_sexp, Q?{t(:defn)}, 2,
191
+ "Should match s(:defn, _, _)"
192
+ end
193
+
194
+ # Still not sure if I like this
195
+ def test_block_matching
196
+ sb = SexpPath::Matcher::Block
197
+
198
+ assert sb.new{|o| o == s(:a)}.satisfy?(s(:a)), "Should match simple equality"
199
+ assert sb.new{|o| o.length == 1}.satisfy?(s(:a)), "Should match length check"
200
+
201
+ assert_search_count s(:a, s(:b), s(:c), s(:d,:t) ), sb.new{|o| o.length == 2 }, 1,
202
+ "Should match s(:d, :t)"
203
+ end
204
+
205
+ private
206
+ def assert_search_count(sexp, example, count, message)
207
+ i = 0
208
+ sexp.search_each(example){|match| i += 1}
209
+ assert_equal count, i, message + "\nSearching for: #{example.inspect}\nIn: #{sexp.inspect}"
210
+ end
211
+ end