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