sexp_path 0.4.0

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