sexp_path 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +167 -0
- data/Rakefile +41 -0
- data/TODO +4 -0
- data/VERSION.yml +4 -0
- data/examples/print_methods.rb +28 -0
- data/examples/sexp_grep.rb +46 -0
- data/lib/sexp_path.rb +57 -0
- data/lib/sexp_path/line_numbering_processor.rb +60 -0
- data/lib/sexp_path/matcher/all.rb +20 -0
- data/lib/sexp_path/matcher/any.rb +20 -0
- data/lib/sexp_path/matcher/atom.rb +14 -0
- data/lib/sexp_path/matcher/base.rb +54 -0
- data/lib/sexp_path/matcher/block.rb +16 -0
- data/lib/sexp_path/matcher/child.rb +24 -0
- data/lib/sexp_path/matcher/include.rb +22 -0
- data/lib/sexp_path/matcher/not.rb +20 -0
- data/lib/sexp_path/matcher/pattern.rb +20 -0
- data/lib/sexp_path/matcher/sibling.rb +54 -0
- data/lib/sexp_path/matcher/type.rb +21 -0
- data/lib/sexp_path/matcher/wild.rb +12 -0
- data/lib/sexp_path/sexp_collection.rb +16 -0
- data/lib/sexp_path/sexp_query_builder.rb +137 -0
- data/lib/sexp_path/sexp_result.rb +21 -0
- data/lib/sexp_path/traverse.rb +72 -0
- data/test/line_numbering_processor_test.rb +55 -0
- data/test/sample.rb +25 -0
- data/test/sexp_path_capture_test.rb +131 -0
- data/test/sexp_path_matching_test.rb +211 -0
- data/test/sexp_replacement_test.rb +20 -0
- data/test/use_case_test.rb +127 -0
- metadata +108 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
# See SexpQueryBuilder.atom
|
2
|
+
class SexpPath::Matcher::Atom < SexpPath::Matcher::Base
|
3
|
+
# Satisfied when +o+ is an atom (anything that is not an S-Expression)
|
4
|
+
def satisfy?(o, data={})
|
5
|
+
return nil if o.is_a? Sexp
|
6
|
+
|
7
|
+
capture_match o, data
|
8
|
+
end
|
9
|
+
|
10
|
+
# Prints as +atom+
|
11
|
+
def inspect
|
12
|
+
"atom"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# This is the base class for all Sexp matchers.
|
2
|
+
#
|
3
|
+
# A matcher should implement the following methods:
|
4
|
+
#
|
5
|
+
# * satisfy?
|
6
|
+
# * inspect
|
7
|
+
#
|
8
|
+
# +satisfy?+ determines whether the matcher matches a given input,
|
9
|
+
# and +inspect+ will print the matcher nicely in a user's console.
|
10
|
+
#
|
11
|
+
# The base matcher is created with the SexpQueryBuilder as follows
|
12
|
+
# Q?{ s() }
|
13
|
+
class SexpPath::Matcher::Base < Sexp
|
14
|
+
# Combines the Matcher with another Matcher, the resulting one will
|
15
|
+
# be satisfied if either Matcher would be satisfied.
|
16
|
+
#
|
17
|
+
# Example:
|
18
|
+
# s(:a) | s(:b)
|
19
|
+
def | o
|
20
|
+
SexpPath::Matcher::Any.new(self, o)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Combines the Matcher with another Matcher, the resulting one will
|
24
|
+
# be satisfied only if both Matchers would be satisfied.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
# t(:a) & include(:b)
|
28
|
+
def & o
|
29
|
+
SexpPath::Matcher::All.new(self, o)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a Matcher that matches whenever this Matcher would not have matched
|
33
|
+
#
|
34
|
+
# Example:
|
35
|
+
# -s(:a)
|
36
|
+
def -@
|
37
|
+
SexpPath::Matcher::Not.new(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a Matcher that matches if this has a sibling +o+
|
41
|
+
#
|
42
|
+
# Example:
|
43
|
+
# s(:a) >> s(:b)
|
44
|
+
def >> o
|
45
|
+
SexpPath::Matcher::Sibling.new(self, o)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Formats the matcher as:
|
49
|
+
# q(:a, :b)
|
50
|
+
def inspect
|
51
|
+
children = map{|e| e.inspect}.join(', ')
|
52
|
+
"q(#{children})"
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class SexpPath::Matcher::Block < SexpPath::Matcher::Base
|
2
|
+
attr_reader :exp
|
3
|
+
def initialize &block
|
4
|
+
@exp = block
|
5
|
+
end
|
6
|
+
|
7
|
+
def satisfy?(o, data={})
|
8
|
+
return nil unless @exp[o]
|
9
|
+
|
10
|
+
capture_match o, data
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect
|
14
|
+
"<custom>"
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# See SexpQueryBuilder.child
|
2
|
+
class SexpPath::Matcher::Child < SexpPath::Matcher::Base
|
3
|
+
attr_reader :child
|
4
|
+
|
5
|
+
# Create a Child matcher which will match anything having a descendant matching +child+.
|
6
|
+
def initialize(child)
|
7
|
+
@child = child
|
8
|
+
end
|
9
|
+
|
10
|
+
# Satisfied if matches +child+ or +o+ has a descendant matching +child+.
|
11
|
+
def satisfy?(o, data={})
|
12
|
+
if child.satisfy?(o,data)
|
13
|
+
capture_match o, data
|
14
|
+
elsif o.is_a? Sexp
|
15
|
+
o.search_each(child,data) do
|
16
|
+
return capture_match(o, data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def inspect
|
22
|
+
"child(#{child.inspect})"
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# See SexpQueryBuilder.include
|
2
|
+
class SexpPath::Matcher::Include < SexpPath::Matcher::Base
|
3
|
+
attr_reader :value
|
4
|
+
|
5
|
+
# Creates a Matcher which will match any Sexp that contains the +value+
|
6
|
+
def initialize(value)
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
# Satisfied if a +o+ is a Sexp and one of +o+'s elements matches value
|
11
|
+
def satisfy?(o, data={})
|
12
|
+
if o.is_a? Sexp
|
13
|
+
return nil unless o.any?{|c| value.is_a?(Sexp) ? value.satisfy?(c, data) : value == c}
|
14
|
+
end
|
15
|
+
|
16
|
+
capture_match o, data
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
"include(#{value.inspect})"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# See SexpQueryBuilder.not
|
2
|
+
class SexpPath::Matcher::Not < SexpPath::Matcher::Base
|
3
|
+
attr_reader :value
|
4
|
+
|
5
|
+
# Creates a Matcher which will match any Sexp that does not match the +value+
|
6
|
+
def initialize(value)
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
# Satisfied if a +o+ does not match the +value+
|
11
|
+
def satisfy?(o, data={})
|
12
|
+
return nil if value.is_a?(Sexp) ? value.satisfy?(o, data) : value == o
|
13
|
+
|
14
|
+
capture_match o, {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"is_not(#{value.inspect})"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# See SexpQueryBuilder.m
|
2
|
+
class SexpPath::Matcher::Pattern < SexpPath::Matcher::Base
|
3
|
+
attr_reader :pattern
|
4
|
+
|
5
|
+
# Create a Patten matcher which will match any atom that either matches the input +pattern+.
|
6
|
+
def initialize(pattern)
|
7
|
+
@pattern = pattern
|
8
|
+
end
|
9
|
+
|
10
|
+
# Satisfied if +o+ is an atom, and +o+ matches +pattern+
|
11
|
+
def satisfy?(o, data={})
|
12
|
+
return nil unless !o.is_a?(Sexp) && o.to_s =~ pattern
|
13
|
+
|
14
|
+
capture_match o, data
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"m(#{pattern.inspect})"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# See SexpPath::Matcher::Base for sibling relations: <,<<,>>,>
|
2
|
+
#
|
3
|
+
class SexpPath::Matcher::Sibling < SexpPath::Matcher::Base
|
4
|
+
attr_reader :subject, :sibling, :distance
|
5
|
+
|
6
|
+
# Creates a Matcher which will match any pair of Sexps that are siblings.
|
7
|
+
# Defaults to matching the immediate following sibling.
|
8
|
+
def initialize(subject, sibling, distance=nil)
|
9
|
+
@subject = subject
|
10
|
+
@sibling = sibling
|
11
|
+
@distance = distance
|
12
|
+
end
|
13
|
+
|
14
|
+
# Satisfied if o contains +subject+ followed by +sibling+
|
15
|
+
def satisfy?(o, data={})
|
16
|
+
# Future optimizations:
|
17
|
+
# * Shortcut matching sibling
|
18
|
+
subject_matches = index_matches(subject, o)
|
19
|
+
return nil if subject_matches.empty?
|
20
|
+
|
21
|
+
sibling_matches = index_matches(sibling, o)
|
22
|
+
return nil if sibling_matches.empty?
|
23
|
+
|
24
|
+
subject_matches.each do |i1, data_1|
|
25
|
+
sibling_matches.each do |i2, data_2|
|
26
|
+
if (distance ? (i2-i1 == distance) : i2 > i1)
|
27
|
+
data = data.merge(data_1).merge(data_2)
|
28
|
+
return capture_match(o, data)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
"#{subject.inspect} >> #{sibling.inspect}"
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def index_matches(pattern, o)
|
42
|
+
indexes = []
|
43
|
+
return indexes unless o.is_a? Sexp
|
44
|
+
|
45
|
+
o.each_with_index do |e,i|
|
46
|
+
data = {}
|
47
|
+
if pattern.is_a?(Sexp) ? pattern.satisfy?(o[i],data) : pattern == o[i]
|
48
|
+
indexes << [i, data]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
indexes
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# See SexpQueryBuilder.t
|
2
|
+
class SexpPath::Matcher::Type < SexpPath::Matcher::Base
|
3
|
+
attr_reader :sexp_type
|
4
|
+
|
5
|
+
# Creates a Matcher which will match any Sexp who's type is +type+, where a type is
|
6
|
+
# the first element in the Sexp.
|
7
|
+
def initialize(type)
|
8
|
+
@sexp_type = type
|
9
|
+
end
|
10
|
+
|
11
|
+
# Satisfied if the sexp_type of +o+ is +type+.
|
12
|
+
def satisfy?(o, data={})
|
13
|
+
return nil unless o.is_a?(Sexp) && o.sexp_type == sexp_type
|
14
|
+
|
15
|
+
capture_match o, data
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
"t(#{sexp_type.inspect})"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module SexpPath
|
2
|
+
# Wraps the results of a SexpPath query.
|
3
|
+
# SexpCollection defines SexpCollection#search so that you can
|
4
|
+
# chain queries.
|
5
|
+
#
|
6
|
+
# For instance:
|
7
|
+
# res = s(:a, s(:b)) / Q?{ s(:a,_) } / Q?{ s(:b) }
|
8
|
+
#
|
9
|
+
class SexpCollection < Array
|
10
|
+
# See Traverse#search
|
11
|
+
def search(pattern)
|
12
|
+
inject(SexpCollection.new){|collection, match| collection.concat match.sexp.search(pattern, match) }
|
13
|
+
end
|
14
|
+
alias_method :/, :search
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module SexpPath
|
2
|
+
|
3
|
+
# The SexpQueryBuilder is the simplest way to build SexpPath queries.
|
4
|
+
#
|
5
|
+
# Typically, one access the SexpQueryBuilder through the helper method
|
6
|
+
# Q? which accepts a block, and will return a SexpPath matcher.
|
7
|
+
#
|
8
|
+
# For example here is a SexpPath query that looks for s(:a):
|
9
|
+
#
|
10
|
+
# query = Q?{ s(:a) }
|
11
|
+
#
|
12
|
+
# A more interesting query might look for classes with names starting
|
13
|
+
# in Test:
|
14
|
+
#
|
15
|
+
# query = Q?{ s(:class, m(/^Test\w+/ % 'name', _, _)) }
|
16
|
+
#
|
17
|
+
# This makes use of a SexpPath::Matcher::Pattern, two SexpPath::Matcher::Wild
|
18
|
+
# matchers and SexpPath::Traverse#capture_as for capturing the name to a
|
19
|
+
# variable 'name'.
|
20
|
+
#
|
21
|
+
# For more examples, see the various SexpQueryBuilder class methods, the
|
22
|
+
# examples, and the tests supplied with SexpPath.
|
23
|
+
#
|
24
|
+
class SexpQueryBuilder
|
25
|
+
class << self
|
26
|
+
|
27
|
+
# This is the longhand method for create a SexpPath query, normally
|
28
|
+
# one would use Q?{ ... }, however it is also possible to do:
|
29
|
+
#
|
30
|
+
# SexpPath::SexpQueryBuilder.do{ s() }
|
31
|
+
#
|
32
|
+
def do(&block)
|
33
|
+
instance_eval(&block)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Matches an S-Expression.
|
37
|
+
#
|
38
|
+
# example
|
39
|
+
# s(:a) / Q?{ s(:a) } #=> [s(:a)]
|
40
|
+
# s(:a) / Q?{ s() } #=> []
|
41
|
+
# s(:a, s(:b) / Q?{ s(:b) } #=> [s(:b)]
|
42
|
+
#
|
43
|
+
def s(*args)
|
44
|
+
SexpPath::Matcher::Base.new(*args)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Matches anything.
|
48
|
+
#
|
49
|
+
# example:
|
50
|
+
# s(:a) / Q?{ _ } #=> [s(:a)]
|
51
|
+
# s(:a, s(s(:b))) / Q?{ s(_) } #=> [s(s(:b))]
|
52
|
+
#
|
53
|
+
# Can also be called with +wild+
|
54
|
+
# s(:a) / Q?{ wild } #=> [s(:a)]
|
55
|
+
#
|
56
|
+
def wild()
|
57
|
+
SexpPath::Matcher::Wild.new
|
58
|
+
end
|
59
|
+
alias_method :_, :wild
|
60
|
+
|
61
|
+
def include(child)
|
62
|
+
SexpPath::Matcher::Include.new(child)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Matches any atom.
|
66
|
+
#
|
67
|
+
# example:
|
68
|
+
# s(:a) / Q?{ s(atom) } #=> [s(:a)]
|
69
|
+
# s(:a, s(:b)) / Q?{ s(atom) } #=> [s(:b)]
|
70
|
+
#
|
71
|
+
def atom
|
72
|
+
SexpPath::Matcher::Atom.new
|
73
|
+
end
|
74
|
+
|
75
|
+
# Matches when any sub expression match
|
76
|
+
#
|
77
|
+
# example:
|
78
|
+
# s(:a) / Q?{ any(s(:a), s(:b)) } #=> [s(:a)]
|
79
|
+
# s(:a) / Q?{ any(s(:b), s(:c)) } #=> []
|
80
|
+
#
|
81
|
+
def any(*args)
|
82
|
+
SexpPath::Matcher::Any.new(*args)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Matches when all sub expression match
|
86
|
+
#
|
87
|
+
# example:
|
88
|
+
# s(:a) / Q?{ all(s(:a), s(:b)) } #=> []
|
89
|
+
# s(:a,:b) / Q?{ t(:a), include(:b)) } #=> [s(:a,:b)]
|
90
|
+
#
|
91
|
+
def all(*args)
|
92
|
+
SexpPath::Matcher::All.new(*args)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Matches when sub expression does not match, see SexpPath::Matcher::Base#-@
|
96
|
+
#
|
97
|
+
# example:
|
98
|
+
# s(:a) / Q?{ is_not(s(:b)) } #=> [s(:a)]
|
99
|
+
# s(:a) / Q?{ s(is_not :a) } #=> []
|
100
|
+
#
|
101
|
+
def is_not(arg)
|
102
|
+
SexpPath::Matcher::Not.new(arg)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Matches anything that has a child matching the sub expression
|
106
|
+
#
|
107
|
+
# example:
|
108
|
+
# s(s(s(s(s(:a))))) / Q?{ child(s(:a)) } #=> [s(s(s(s(s(:a)))))]
|
109
|
+
#
|
110
|
+
def child(child)
|
111
|
+
SexpPath::Matcher::Child.new(child)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Matches anything having the same sexp_type, which is the first value in a Sexp.
|
115
|
+
#
|
116
|
+
# example:
|
117
|
+
# s(:a, :b) / Q?{ t(:a) } #=> [s(:a, :b)]
|
118
|
+
# s(:a, :b) / Q?{ t(:b) } #=> []
|
119
|
+
# s(:a, s(:b, :c)) / Q?{ t(:b) } #=> [s(:b, :c)]
|
120
|
+
def t(name)
|
121
|
+
SexpPath::Matcher::Type.new(name)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Matches any atom who's string representation matches the patterns passed in.
|
125
|
+
#
|
126
|
+
# example:
|
127
|
+
# s(:a) / Q?{ m('a') } #=> [s(:a)]
|
128
|
+
# s(:a) / Q?{ m(/\w/,/\d/) } #=> [s(:a)]
|
129
|
+
# s(:tests, s(s(:test_a), s(:test_b))) / Q?{ m(/test_\w/) } #=> [s(:test_a), s(:test_b)]
|
130
|
+
def m(* patterns)
|
131
|
+
patterns = patterns.map{|p| p.is_a?(Regexp) ? p : Regexp.new("\\A"+Regexp.escape(p.to_s)+"\\Z")}
|
132
|
+
regexp = Regexp.union(*patterns)
|
133
|
+
SexpPath::Matcher::Pattern.new(regexp)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SexpPath
|
2
|
+
|
3
|
+
# Wraps the results of a SexpPath query. The matching Sexp
|
4
|
+
# is placed in SexpResult#sexp. Any named captures will be
|
5
|
+
# available with SexpResult#[].
|
6
|
+
#
|
7
|
+
# For instance:
|
8
|
+
# res = s(:a) / Q?{ s( _ % 'name') }
|
9
|
+
#
|
10
|
+
# res.first.sexp == s(:a)
|
11
|
+
# res.first['name'] == :a
|
12
|
+
#
|
13
|
+
class SexpResult < Hash
|
14
|
+
attr_accessor :sexp # Matched Sexp
|
15
|
+
|
16
|
+
def initialize(sexp, data={})
|
17
|
+
@sexp = sexp
|
18
|
+
merge! data
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|