machete 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ lib/machete/parser.rb
2
+ doc/
3
+ .yardoc
4
+ *.rbc
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 SUSE
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ Machete
2
+ =======
3
+
4
+ Machete is a simple tool for matching Rubinius AST nodes against patterns. You can use it if you are writing any kind of tool that processes Ruby code and needs to do some work on specific types of nodes, needs to find patterns in the code, etc.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ You need to install [Rubinius](http://rubini.us/) first. You can then install Machete:
10
+
11
+ $ gem install machete
12
+
13
+ Usage
14
+ -----
15
+
16
+ First, require the library:
17
+
18
+ require "machete"
19
+
20
+ You can now use one of two methods Machete offers: `Machete.matches?` and `Machete.find`.
21
+
22
+ The `Machete.matches?` method matches a Rubinus AST node against a pattern:
23
+
24
+ Machete.matches?('foo.bar'.to_ast, 'Send<receiver = Send<receiver = Self, name = :foo>, name = :bar>')
25
+ # => true
26
+
27
+ Machete.matches?('42'.to_ast, 'Send<receiver = Send<receiver = Self, name = :foo>, name = :bar>')
28
+ # => false
29
+
30
+ (See below for pattern syntax description.)
31
+
32
+ The `Machete.find` method finds all nodes in a Rubinius AST tree matching a pattern:
33
+
34
+ Machete.find('42 + 43 + 44'.to_ast, 'FixnumLiteral')
35
+ # => [
36
+ # #<Rubinius::AST::FixnumLiteral:0x10b0 @value=44 @line=1>,
37
+ # #<Rubinius::AST::FixnumLiteral:0x10b8 @value=43 @line=1>,
38
+ # #<Rubinius::AST::FixnumLiteral:0x10c0 @value=42 @line=1>
39
+ # ]
40
+
41
+ Pattern Syntax
42
+ --------------
43
+
44
+ Rubinius AST consists of instances of classes that represent various types of nodes:
45
+
46
+ '42'.to_ast # => #<Rubinius::AST::FixnumLiteral:0xf28 @value=42 @line=1>
47
+ '"abcd"'.to_ast # => #<Rubinius::AST::StringLiteral:0xf60 @line=1 @string="abcd">
48
+
49
+ To match a specific node type, just use its class name in the pattern:
50
+
51
+ Machete.matches?('42'.to_ast, 'FixnumLiteral') # => true
52
+ Machete.matches?('"abcd"'.to_ast, 'FixnumLiteral') # => false
53
+
54
+ If you want to match specific attribute of the node, specify its value inside `<...>` right after the node name:
55
+
56
+ Machete.matches?('42'.to_ast, 'FixnumLiteral<value = 42>') # => true
57
+ Machete.matches?('45'.to_ast, 'FixnumLiteral<value = 42>') # => false
58
+
59
+ The attribute value can be an integer, string, symbol or other pattern. This means you can easily match nested nodes recursively. You can also specify multiple attributes:
60
+
61
+ Machete.matches?('foo.bar'.to_ast, 'Send<receiver = Send<receiver = Self, name = :foo>, name = :bar>')
62
+ # => true
63
+
64
+ Machete.matches?('42'.to_ast, 'Send<receiver = Send<receiver = Self, name = :foo>, name = :bar>')
65
+ # => false
66
+
67
+ To specify multiple alternatives, use the choice operator:
68
+
69
+ Machete.matches?('42'.to_ast, 'FixnumLiteral | StringLiteral') # => true
70
+ Machete.matches?('"abcd"'.to_ast, 'FixnumLiteral | StringLiteral') # => true
71
+
72
+ FAQ
73
+ ---
74
+
75
+ **Why did you chose Rubinius AST as a base? Aren't there other tools for Ruby parsing which are not VM-specific?**
76
+
77
+ There are three other tools which were considered but each has its issues:
78
+
79
+ * [parse_tree](http://parsetree.rubyforge.org/) — unmaintained and unsupported for 1.9
80
+ * [ruby_parser](http://parsetree.rubyforge.org/) — sometimes reports wrong line numbers for the nodes (this is a killer for some use cases)
81
+ * [Ripper](http://rubyforge.org/projects/ripper/) — usable but the generated AST is too low level (the patterns would be too complex and low-level)
82
+
83
+ Rubinius AST is also by far the easiest to work with.
84
+
85
+ Acknowledgement
86
+ ---------------
87
+
88
+ The general idea and inspiration for the pattern syntax was taken form Python's [2to3](http://docs.python.org/library/2to3.html) tool.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require "rspec/core/rake_task"
2
+ require "yard"
3
+
4
+ desc "Generate the expression parser"
5
+ task :parser do
6
+ source = "lib/machete/parser.y"
7
+ target = "lib/machete/parser.rb"
8
+ unless uptodate?(target, source)
9
+ system "racc -o #{target} #{source}" or exit 1
10
+ end
11
+ end
12
+
13
+ RSpec::Core::RakeTask.new
14
+ task :spec => :parser
15
+
16
+ YARD::Rake::YardocTask.new
17
+ task :yard => :parser
18
+
19
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/machete.rb ADDED
@@ -0,0 +1,55 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/machete/matchers")
2
+ require File.expand_path(File.dirname(__FILE__) + "/machete/parser")
3
+ require File.expand_path(File.dirname(__FILE__) + "/machete/version")
4
+
5
+ module Machete
6
+ # Matches a Rubinius AST node against a pattern.
7
+ #
8
+ # @param [Rubinius::AST::Node] node node to match
9
+ # @param [String] pattern pattern to match the node against (see {file:README.md} for syntax description)
10
+ #
11
+ # @example Succesfull match
12
+ # Machete.matches?('foo.bar'.to_ast, 'Send<receiver = Send<receiver = Self, name = :foo>, name = :bar>')
13
+ # # => true
14
+ #
15
+ # @example Failed match
16
+ # Machete.matches?('42'.to_ast, 'Send<receiver = Send<receiver = Self, name = :foo>, name = :bar>')
17
+ # # => false
18
+ #
19
+ # @return [Boolean] +true+ if the node matches the pattern, +false+ otherwise
20
+ #
21
+ # @raise [Matchete::Parser::SyntaxError] if the pattern is invalid
22
+ def self.matches?(node, pattern)
23
+ Parser.new.parse(pattern).matches?(node)
24
+ end
25
+
26
+ # Finds all nodes in a Rubinius AST matching a pattern.
27
+ #
28
+ # @param [Rubinius::AST::Node] ast tree to search
29
+ # @param [String] pattern pattern to match the nodes against (see {file:README.md} for syntax description)
30
+ #
31
+ # @example
32
+ # Machete.find('42 + 43 + 44'.to_ast, 'FixnumLiteral')
33
+ # # => [
34
+ # # #<Rubinius::AST::FixnumLiteral:0x10b0 @value=44 @line=1>,
35
+ # # #<Rubinius::AST::FixnumLiteral:0x10b8 @value=43 @line=1>,
36
+ # # #<Rubinius::AST::FixnumLiteral:0x10c0 @value=42 @line=1>
37
+ # # ]
38
+ #
39
+ # @return [Array] list of matching nodes (in unspecified order)
40
+ #
41
+ # @raise [Matchete::Parser::SyntaxError] if the pattern is invalid
42
+ def self.find(ast, pattern)
43
+ matcher = Parser.new.parse(pattern)
44
+
45
+ result = []
46
+ result << ast if matcher.matches?(ast)
47
+
48
+ ast.walk(true) do |dummy, node|
49
+ result << node if matcher.matches?(node)
50
+ true
51
+ end
52
+
53
+ result
54
+ end
55
+ end
@@ -0,0 +1,58 @@
1
+ module Machete
2
+ # @private
3
+ module Matchers
4
+ # @private
5
+ class ChoiceMatcher
6
+ attr_reader :alternatives
7
+
8
+ def initialize(alternatives)
9
+ @alternatives = alternatives
10
+ end
11
+
12
+ def ==(other)
13
+ other.instance_of?(self.class) && @alternatives == other.alternatives
14
+ end
15
+
16
+ def matches?(node)
17
+ alternatives.any? { |a| a.matches?(node) }
18
+ end
19
+ end
20
+
21
+ # @private
22
+ class NodeMatcher
23
+ attr_reader :class_name, :attrs
24
+
25
+ def initialize(class_name, attrs = {})
26
+ @class_name, @attrs = class_name, attrs
27
+ end
28
+
29
+ def ==(other)
30
+ other.instance_of?(self.class) &&
31
+ @class_name == other.class_name &&
32
+ @attrs == other.attrs
33
+ end
34
+
35
+ def matches?(node)
36
+ node.class == Rubinius::AST.const_get(@class_name) &&
37
+ attrs.all? { |name, matcher| matcher.matches?(node.send(name)) }
38
+ end
39
+ end
40
+
41
+ # @private
42
+ class LiteralMatcher
43
+ attr_reader :literal
44
+
45
+ def initialize(literal)
46
+ @literal = literal
47
+ end
48
+
49
+ def ==(other)
50
+ other.instance_of?(self.class) && @literal == other.literal
51
+ end
52
+
53
+ def matches?(node)
54
+ @literal == node
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,184 @@
1
+ class Machete::Parser
2
+
3
+ token VAR_NAME
4
+ token CLASS_NAME
5
+ token SYMBOL
6
+ token INTEGER
7
+ token STRING
8
+
9
+ start expression
10
+
11
+ rule
12
+
13
+ expression : primary
14
+ | expression "|" primary {
15
+ result = if val[0].is_a?(ChoiceMatcher)
16
+ ChoiceMatcher.new(val[0].alternatives << val[2])
17
+ else
18
+ ChoiceMatcher.new([val[0], val[2]])
19
+ end
20
+ }
21
+
22
+ primary : node
23
+ | literal
24
+
25
+ node : CLASS_NAME {
26
+ result = NodeMatcher.new(val[0].to_sym)
27
+ }
28
+ | CLASS_NAME "<" attrs ">" {
29
+ result = NodeMatcher.new(val[0].to_sym, val[2])
30
+ }
31
+
32
+ attrs : attr
33
+ | attrs "," attr { result = val[0].merge(val[2]) }
34
+
35
+ attr : VAR_NAME "=" expression { result = { val[0].to_sym => val[2] } }
36
+
37
+ literal : SYMBOL { result = LiteralMatcher.new(val[0][1..-1].to_sym) }
38
+ | INTEGER {
39
+ value = if val[0] =~ /^0[bB]/
40
+ val[0][2..-1].to_i(2)
41
+ elsif val[0] =~ /^0[oO]/
42
+ val[0][2..-1].to_i(8)
43
+ elsif val[0] =~ /^0[dD]/
44
+ val[0][2..-1].to_i(10)
45
+ elsif val[0] =~ /^0[xX]/
46
+ val[0][2..-1].to_i(16)
47
+ elsif val[0] =~ /^0/
48
+ val[0][1..-1].to_i(8)
49
+ else
50
+ val[0].to_i
51
+ end
52
+ result = LiteralMatcher.new(value)
53
+ }
54
+ | STRING {
55
+ quote = val[0][0..0]
56
+ value = if quote == "'"
57
+ val[0][1..-2].gsub("\\\\", "\\").gsub("\\'", "'")
58
+ elsif quote == '"'
59
+ val[0][1..-2].
60
+ gsub("\\\\", "\\").
61
+ gsub('\\"', '"').
62
+ gsub("\\n", "\n").
63
+ gsub("\\t", "\t").
64
+ gsub("\\r", "\r").
65
+ gsub("\\f", "\f").
66
+ gsub("\\v", "\v").
67
+ gsub("\\a", "\a").
68
+ gsub("\\e", "\e").
69
+ gsub("\\b", "\b").
70
+ gsub("\\s", "\s").
71
+ gsub(/\\([0-7]{1,3})/) { $1.to_i(8).chr }.
72
+ gsub(/\\x([0-9a-fA-F]{1,2})/) { $1.to_i(16).chr }
73
+ else
74
+ raise "Unknown quote: #{quote.inspect}."
75
+ end
76
+ result = LiteralMatcher.new(value)
77
+ }
78
+
79
+ ---- inner
80
+
81
+ include Matchers
82
+
83
+ class SyntaxError < StandardError; end
84
+
85
+ def parse(input)
86
+ @input = input
87
+ @pos = 0
88
+
89
+ do_parse
90
+ end
91
+
92
+ private
93
+
94
+ SIMPLE_TOKENS = ["|", "<", ">", ",", "="]
95
+
96
+ COMPLEX_TOKENS = [
97
+ [:VAR_NAME, /^[a-z_][a-zA-Z0-9_]*/],
98
+ [:CLASS_NAME, /^[A-Z][a-zA-Z0-9_]*/],
99
+ [:SYMBOL, /^:[a-zA-Z_][a-zA-Z0-9_]*/],
100
+ [
101
+ :INTEGER,
102
+ /^
103
+ [+-]? # sign
104
+ (
105
+ 0[bB][01]+(_[01]+)* # binary (prefixed)
106
+ |
107
+ 0[oO][0-7]+(_[0-7]+)* # octal (prefixed)
108
+ |
109
+ 0[dD]\d+(_\d+)* # decimal (prefixed)
110
+ |
111
+ 0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)* # hexadecimal (prefixed)
112
+ |
113
+ 0[0-7]*(_[0-7]+)* # octal (unprefixed)
114
+ |
115
+ [1-9]\d*(_\d+)* # decimal (unprefixed)
116
+ )
117
+ /x
118
+ ],
119
+ [
120
+ :STRING,
121
+ /^
122
+ (
123
+ ' # sinqle-quoted string
124
+ (
125
+ \\[\\'] # escape
126
+ |
127
+ [^'] # regular character
128
+ )*
129
+ '
130
+ |
131
+ " # double-quoted string
132
+ (
133
+ \\ # escape
134
+ (
135
+ [\\"ntrfvaebs] # one-character escape
136
+ |
137
+ [0-7]{1,3} # octal number escape
138
+ |
139
+ x[0-9a-fA-F]{1,2} # hexadecimal number escape
140
+ )
141
+ |
142
+ [^"] # regular character
143
+ )*
144
+ "
145
+ )
146
+ /x
147
+ ]
148
+ ]
149
+
150
+ def next_token
151
+ skip_whitespace
152
+
153
+ return false if remaining_input.empty?
154
+
155
+ SIMPLE_TOKENS.each do |token|
156
+ if remaining_input[0...token.length] == token
157
+ @pos += token.length
158
+ return [token, token]
159
+ end
160
+ end
161
+
162
+ COMPLEX_TOKENS.each do |type, regexp|
163
+ if remaining_input =~ regexp
164
+ @pos += $&.length
165
+ return [type, $&]
166
+ end
167
+ end
168
+
169
+ raise SyntaxError, "Unexpected character: #{remaining_input[0..0].inspect}."
170
+ end
171
+
172
+ def skip_whitespace
173
+ if remaining_input =~ /^[ \t\r\n]+/
174
+ @pos += $&.length
175
+ end
176
+ end
177
+
178
+ def remaining_input
179
+ @input[@pos..-1]
180
+ end
181
+
182
+ def on_error(error_token_id, error_value, value_stack)
183
+ raise SyntaxError, "Unexpected token: #{error_value.inspect}."
184
+ end
@@ -0,0 +1,3 @@
1
+ module Machete
2
+ VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").strip
3
+ end
data/machete.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + "/lib/machete/version")
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "machete"
7
+ s.version = Machete::VERSION
8
+ s.summary = "Simple tool for matching Rubinius AST nodes against patterns"
9
+ s.description = <<-EOT.split("\n").map(&:strip).join(" ")
10
+ Machete is a simple tool for matching Rubinius AST nodes against patterns.
11
+ You can use it if you are writing any kind of tool that processes Ruby code
12
+ and needs to do some work on specific types of nodes, needs to find patterns
13
+ in the code, etc.
14
+ EOT
15
+
16
+ s.author = "David Majda"
17
+ s.email = "dmajda@suse.de"
18
+ s.homepage = "https://github.com/openSUSE/machete"
19
+ s.license = "MIT"
20
+
21
+ s.files = `git ls-files`.split("\n") + ["lib/machete/parser.rb"]
22
+
23
+ s.add_development_dependency "racc"
24
+ s.add_development_dependency "rspec"
25
+ s.add_development_dependency "rdoc"
26
+ s.add_development_dependency "rdiscount"
27
+ s.add_development_dependency "yard"
28
+ end
@@ -0,0 +1,186 @@
1
+ require "spec_helper"
2
+
3
+ module Machete::Matchers
4
+ describe ChoiceMatcher do
5
+ before :each do
6
+ @alternatives = [
7
+ LiteralMatcher.new(42),
8
+ LiteralMatcher.new(43),
9
+ LiteralMatcher.new(44)
10
+ ]
11
+ @matcher = ChoiceMatcher.new(@alternatives)
12
+ end
13
+
14
+ describe "initialize" do
15
+ it "sets attributes correctly" do
16
+ @matcher.alternatives.should == @alternatives
17
+ end
18
+ end
19
+
20
+ describe "==" do
21
+ it "returns true when passed the same object" do
22
+ @matcher.should == @matcher
23
+ end
24
+
25
+ it "returns true when passed a ChoiceMatcher initialized with the same parameters" do
26
+ @matcher.should == ChoiceMatcher.new(@alternatives)
27
+ end
28
+
29
+ it "returns false when passed some random object" do
30
+ @matcher.should_not == Object.new
31
+ end
32
+
33
+ it "returns false when passed a subclass of ChoiceMatcher initialized with the same parameters" do
34
+ class SubclassedChoiceMatcher < ChoiceMatcher
35
+ end
36
+
37
+ @matcher.should_not == SubclassedChoiceMatcher.new(@alternatives)
38
+ end
39
+
40
+ it "returns false when passed a ChoiceMatcher initialized with different parameters" do
41
+ @matcher.should_not == ChoiceMatcher.new([
42
+ LiteralMatcher.new(45),
43
+ LiteralMatcher.new(46),
44
+ LiteralMatcher.new(47)
45
+ ])
46
+ end
47
+ end
48
+
49
+ describe "matches?" do
50
+ it "matches any alternative" do
51
+ @matcher.matches?(42).should be_true
52
+ @matcher.matches?(43).should be_true
53
+ @matcher.matches?(44).should be_true
54
+ end
55
+
56
+ it "does not match a non-alternative" do
57
+ @matcher.matches?(45).should be_false
58
+ end
59
+ end
60
+ end
61
+
62
+ describe NodeMatcher do
63
+ before :each do
64
+ @attrs = {
65
+ :source => LiteralMatcher.new("abcd"),
66
+ :options => LiteralMatcher.new(128)
67
+ }
68
+ @matcher = NodeMatcher.new(:RegexLiteral, @attrs)
69
+ end
70
+
71
+ describe "initializa" do
72
+ describe "when passed one parameter" do
73
+ it "sets attributes correctly" do
74
+ matcher = NodeMatcher.new(:RegexLiteral)
75
+
76
+ matcher.class_name.should == :RegexLiteral
77
+ matcher.attrs.should == {}
78
+ end
79
+ end
80
+
81
+ describe "when passed two parameters" do
82
+ it "sets attributes correctly" do
83
+ matcher = NodeMatcher.new(:RegexLiteral, @attrs)
84
+
85
+ matcher.class_name.should == :RegexLiteral
86
+ matcher.attrs.should == @attrs
87
+ end
88
+ end
89
+ end
90
+
91
+ describe "==" do
92
+ it "returns true when passed the same object" do
93
+ @matcher.should == @matcher
94
+ end
95
+
96
+ it "returns true when passed a NodeMatcher initialized with the same parameters" do
97
+ @matcher.should == NodeMatcher.new(:RegexLiteral, @attrs)
98
+ end
99
+
100
+ it "returns false when passed some random object" do
101
+ @matcher.should_not == Object.new
102
+ end
103
+
104
+ it "returns false when passed a subclass of NodeMatcher initialized with the same parameters" do
105
+ class SubclassedNodeMatcher < NodeMatcher
106
+ end
107
+
108
+ @matcher.should_not ==
109
+ SubclassedNodeMatcher.new(:RegexLiteral, @attrs)
110
+ end
111
+
112
+ it "returns false when passed a NodeMatcher initialized with different parameters" do
113
+ @matcher.should_not == NodeMatcher.new(:StringLiteral, {
114
+ :source => LiteralMatcher.new("abcd"),
115
+ :options => LiteralMatcher.new(128)
116
+ })
117
+ @matcher.should_not == NodeMatcher.new(:RegexLiteral, {
118
+ :source => LiteralMatcher.new("efgh"),
119
+ :options => LiteralMatcher.new(256)
120
+ })
121
+ end
122
+ end
123
+
124
+ describe "matches?" do
125
+ it "matches a node with correct class and matching attributes" do
126
+ @matcher.matches?(Rubinius::AST::RegexLiteral.new(0, "abcd", 128)).should be_true
127
+ end
128
+
129
+ it "does not match a node with incorrect class" do
130
+ @matcher.matches?(Rubinius::AST::StringLiteral.new(0, "abcd")).should be_false
131
+ end
132
+
133
+ it "does not match a node with non-matching attributes" do
134
+ @matcher.matches?(Rubinius::AST::RegexLiteral.new(0, "efgh", 128)).should be_false
135
+ @matcher.matches?(Rubinius::AST::RegexLiteral.new(0, "efgh", 256)).should be_false
136
+ end
137
+ end
138
+ end
139
+
140
+ describe LiteralMatcher do
141
+ before :each do
142
+ @matcher = LiteralMatcher.new(42)
143
+ end
144
+
145
+ describe "initialize" do
146
+ it "sets attributes correctly" do
147
+ @matcher.literal.should == 42
148
+ end
149
+ end
150
+
151
+ describe "==" do
152
+ it "returns true when passed the same object" do
153
+ @matcher.should == @matcher
154
+ end
155
+
156
+ it "returns true when passed a LiteralMatcher initialized with the same parameters" do
157
+ @matcher.should == LiteralMatcher.new(42)
158
+ end
159
+
160
+ it "returns false when passed some random object" do
161
+ @matcher.should_not == Object.new
162
+ end
163
+
164
+ it "returns false when passed a subclass of LiteralMatcher initialized with the same parameters" do
165
+ class SubclassedLiteralMatcher < LiteralMatcher
166
+ end
167
+
168
+ @matcher.should_not == SubclassedLiteralMatcher.new(42)
169
+ end
170
+
171
+ it "returns false when passed a LiteralMatcher initialized with different parameters" do
172
+ @matcher.should_not == LiteralMatcher.new(43)
173
+ end
174
+ end
175
+
176
+ describe "matches?" do
177
+ it "matches an object equivalent to the literal" do
178
+ @matcher.matches?(42).should be_true
179
+ end
180
+
181
+ it "does not match an object not equivalent to the literal" do
182
+ @matcher.matches?(43).should be_false
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,262 @@
1
+ require "spec_helper"
2
+
3
+ module Machete
4
+ include Matchers
5
+
6
+ describe Parser do
7
+ RSpec::Matchers.define :be_parsed_as do |ast|
8
+ match do |input|
9
+ Parser.new.parse(input).should == ast
10
+ end
11
+ end
12
+
13
+ RSpec::Matchers.define :not_be_parsed do |message|
14
+ match do |input|
15
+ lambda {
16
+ Parser.new.parse(input)
17
+ }.should raise_exception(Parser::SyntaxError, message)
18
+ end
19
+ end
20
+
21
+ before :each do
22
+ @i42 = LiteralMatcher.new(42)
23
+ @i43 = LiteralMatcher.new(43)
24
+ @i44 = LiteralMatcher.new(44)
25
+
26
+ @foo = NodeMatcher.new(:Foo)
27
+ @foo_a = NodeMatcher.new(:Foo, :a => @i42)
28
+ @foo_ab = NodeMatcher.new(:Foo, :a => @i42, :b => @i43)
29
+
30
+ @ch4243 = ChoiceMatcher.new([@i42, @i43])
31
+ @ch424344 = ChoiceMatcher.new([@i42, @i43, @i44])
32
+ end
33
+
34
+ def node_matcher_with_attr(attr)
35
+ NodeMatcher.new(:Foo, { attr => @i42 })
36
+ end
37
+
38
+ # Canonical expression is "42 | 43".
39
+ it "parses expression" do
40
+ '42'.should be_parsed_as(@i42)
41
+ '42 | 43'.should be_parsed_as(@ch4243)
42
+ '42 | 43 | 44'.should be_parsed_as(@ch424344)
43
+ end
44
+
45
+ # Canonical primary is "42".
46
+ it "parses primary" do
47
+ 'Foo<a = 42>'.should be_parsed_as(@foo_a)
48
+ '42'.should be_parsed_as(@i42)
49
+ end
50
+
51
+ # Canonical node is "Foo".
52
+ it "parses node" do
53
+ 'Foo'.should be_parsed_as(@foo)
54
+ 'Foo<a = 42, b = 43>'.should be_parsed_as(@foo_ab)
55
+ end
56
+
57
+ # Canonical attrs is "a = 42, b = 43".
58
+ it "parses attrs" do
59
+ 'Foo<a = 42>'.should be_parsed_as(@foo_a)
60
+ 'Foo<a = 42, b = 43>'.should be_parsed_as(@foo_ab)
61
+ end
62
+
63
+ # Canonical attr is "a = 42".
64
+ it "parses attr" do
65
+ 'Foo<a = 42 | 43>'.should be_parsed_as(NodeMatcher.new(:Foo, :a => @ch4243))
66
+ end
67
+
68
+ # Canonical literal is "42".
69
+ it "parses literal" do
70
+ ':a'.should be_parsed_as(LiteralMatcher.new(:a))
71
+ '42'.should be_parsed_as(@i42)
72
+ '"abcd"'.should be_parsed_as(LiteralMatcher.new("abcd"))
73
+ end
74
+
75
+ # Canonical VAR_NAME is "a".
76
+ it "parses VAR_NAME" do
77
+ 'Foo<a = 42>'.should be_parsed_as(node_matcher_with_attr(:a))
78
+ 'Foo<z = 42>'.should be_parsed_as(node_matcher_with_attr(:z))
79
+ 'Foo<_ = 42>'.should be_parsed_as(node_matcher_with_attr(:_))
80
+ 'Foo<aa = 42>'.should be_parsed_as(node_matcher_with_attr(:aa))
81
+ 'Foo<az = 42>'.should be_parsed_as(node_matcher_with_attr(:az))
82
+ 'Foo<aA = 42>'.should be_parsed_as(node_matcher_with_attr(:aA))
83
+ 'Foo<aZ = 42>'.should be_parsed_as(node_matcher_with_attr(:aZ))
84
+ 'Foo<a0 = 42>'.should be_parsed_as(node_matcher_with_attr(:a0))
85
+ 'Foo<a9 = 42>'.should be_parsed_as(node_matcher_with_attr(:a9))
86
+ 'Foo<a_ = 42>'.should be_parsed_as(node_matcher_with_attr(:a_))
87
+ 'Foo<abcd = 42>'.should be_parsed_as(node_matcher_with_attr(:abcd))
88
+ end
89
+
90
+ # Canonical CLASS_NAME is "A".
91
+ it "parses CLASS_NAME" do
92
+ 'A'.should be_parsed_as(NodeMatcher.new(:A))
93
+ 'Z'.should be_parsed_as(NodeMatcher.new(:Z))
94
+ 'Aa'.should be_parsed_as(NodeMatcher.new(:Aa))
95
+ 'Az'.should be_parsed_as(NodeMatcher.new(:Az))
96
+ 'AA'.should be_parsed_as(NodeMatcher.new(:AA))
97
+ 'AZ'.should be_parsed_as(NodeMatcher.new(:AZ))
98
+ 'A0'.should be_parsed_as(NodeMatcher.new(:A0))
99
+ 'A9'.should be_parsed_as(NodeMatcher.new(:A9))
100
+ 'A_'.should be_parsed_as(NodeMatcher.new(:A_))
101
+ 'Abcd'.should be_parsed_as(NodeMatcher.new(:Abcd))
102
+ end
103
+
104
+ # Canonical SYMBOL is ":a".
105
+ it "parses SYMBOL" do
106
+ ':a'.should be_parsed_as(LiteralMatcher.new(:a))
107
+ ':z'.should be_parsed_as(LiteralMatcher.new(:z))
108
+ ':A'.should be_parsed_as(LiteralMatcher.new(:A))
109
+ ':Z'.should be_parsed_as(LiteralMatcher.new(:Z))
110
+ ':_'.should be_parsed_as(LiteralMatcher.new(:_))
111
+ ':aa'.should be_parsed_as(LiteralMatcher.new(:aa))
112
+ ':az'.should be_parsed_as(LiteralMatcher.new(:az))
113
+ ':aA'.should be_parsed_as(LiteralMatcher.new(:aA))
114
+ ':aZ'.should be_parsed_as(LiteralMatcher.new(:aZ))
115
+ ':a0'.should be_parsed_as(LiteralMatcher.new(:a0))
116
+ ':a9'.should be_parsed_as(LiteralMatcher.new(:a9))
117
+ ':a_'.should be_parsed_as(LiteralMatcher.new(:a_))
118
+ ':abcd'.should be_parsed_as(LiteralMatcher.new(:abcd))
119
+ end
120
+
121
+ # Canonical INTEGER is "42".
122
+ it "parses INTEGER" do
123
+ # Sign
124
+ '+1'.should be_parsed_as(LiteralMatcher.new(1))
125
+ '-1'.should be_parsed_as(LiteralMatcher.new(-1))
126
+ '1'.should be_parsed_as(LiteralMatcher.new(1))
127
+
128
+ # Binary (prefixed)
129
+ '0b1'.should be_parsed_as(LiteralMatcher.new(0b1))
130
+ '0B1'.should be_parsed_as(LiteralMatcher.new(0b1))
131
+ '0b0'.should be_parsed_as(LiteralMatcher.new(0b0))
132
+ '0b1'.should be_parsed_as(LiteralMatcher.new(0b1))
133
+ '0b101'.should be_parsed_as(LiteralMatcher.new(0b101))
134
+ '0b1_0'.should be_parsed_as(LiteralMatcher.new(0b10))
135
+ '0b1_1'.should be_parsed_as(LiteralMatcher.new(0b11))
136
+ '0b1_101'.should be_parsed_as(LiteralMatcher.new(0b1101))
137
+ '0b1_0_1_0'.should be_parsed_as(LiteralMatcher.new(0b1010))
138
+
139
+ # Octall (prefixed)
140
+ '0o1'.should be_parsed_as(LiteralMatcher.new(0o1))
141
+ '0O1'.should be_parsed_as(LiteralMatcher.new(0o1))
142
+ '0o0'.should be_parsed_as(LiteralMatcher.new(0o0))
143
+ '0o7'.should be_parsed_as(LiteralMatcher.new(0o7))
144
+ '0o123'.should be_parsed_as(LiteralMatcher.new(0o123))
145
+ '0o1_0'.should be_parsed_as(LiteralMatcher.new(0o10))
146
+ '0o1_7'.should be_parsed_as(LiteralMatcher.new(0o17))
147
+ '0o1_123'.should be_parsed_as(LiteralMatcher.new(0o1123))
148
+ '0o1_2_3_4'.should be_parsed_as(LiteralMatcher.new(0o1234))
149
+
150
+ # Decimal (prefixed)
151
+ '0d1'.should be_parsed_as(LiteralMatcher.new(0d1))
152
+ '0D1'.should be_parsed_as(LiteralMatcher.new(0d1))
153
+ '0d0'.should be_parsed_as(LiteralMatcher.new(0d0))
154
+ '0d9'.should be_parsed_as(LiteralMatcher.new(0d9))
155
+ '0d123'.should be_parsed_as(LiteralMatcher.new(0d123))
156
+ '0d1_0'.should be_parsed_as(LiteralMatcher.new(0d10))
157
+ '0d1_9'.should be_parsed_as(LiteralMatcher.new(0d19))
158
+ '0d1_123'.should be_parsed_as(LiteralMatcher.new(0d1123))
159
+ '0d1_2_3_4'.should be_parsed_as(LiteralMatcher.new(0d1234))
160
+
161
+ # Hexadecimal (prefixed)
162
+ '0x1'.should be_parsed_as(LiteralMatcher.new(0x1))
163
+ '0X1'.should be_parsed_as(LiteralMatcher.new(0x1))
164
+ '0x0'.should be_parsed_as(LiteralMatcher.new(0x0))
165
+ '0x9'.should be_parsed_as(LiteralMatcher.new(0x9))
166
+ '0xa'.should be_parsed_as(LiteralMatcher.new(0xA))
167
+ '0xf'.should be_parsed_as(LiteralMatcher.new(0xF))
168
+ '0xA'.should be_parsed_as(LiteralMatcher.new(0xA))
169
+ '0xF'.should be_parsed_as(LiteralMatcher.new(0xF))
170
+ '0x123'.should be_parsed_as(LiteralMatcher.new(0x123))
171
+ '0x1_0'.should be_parsed_as(LiteralMatcher.new(0x10))
172
+ '0x1_9'.should be_parsed_as(LiteralMatcher.new(0x19))
173
+ '0x1_a'.should be_parsed_as(LiteralMatcher.new(0x1A))
174
+ '0x1_f'.should be_parsed_as(LiteralMatcher.new(0x1F))
175
+ '0x1_A'.should be_parsed_as(LiteralMatcher.new(0x1A))
176
+ '0x1_F'.should be_parsed_as(LiteralMatcher.new(0x1F))
177
+ '0x1_123'.should be_parsed_as(LiteralMatcher.new(0x1123))
178
+ '0x1_2_3_4'.should be_parsed_as(LiteralMatcher.new(0x1234))
179
+
180
+ # Octal (unprefixed)
181
+ '0'.should be_parsed_as(LiteralMatcher.new(0))
182
+ '00'.should be_parsed_as(LiteralMatcher.new(0))
183
+ '07'.should be_parsed_as(LiteralMatcher.new(07))
184
+ '0123'.should be_parsed_as(LiteralMatcher.new(0123))
185
+ '0_0'.should be_parsed_as(LiteralMatcher.new(0))
186
+ '0_7'.should be_parsed_as(LiteralMatcher.new(07))
187
+ '0_123'.should be_parsed_as(LiteralMatcher.new(0123))
188
+ '0_1_2_3'.should be_parsed_as(LiteralMatcher.new(0123))
189
+
190
+ # Decimal (unprefixed)
191
+ '1'.should be_parsed_as(LiteralMatcher.new(1))
192
+ '9'.should be_parsed_as(LiteralMatcher.new(9))
193
+ '10'.should be_parsed_as(LiteralMatcher.new(10))
194
+ '19'.should be_parsed_as(LiteralMatcher.new(19))
195
+ '1234'.should be_parsed_as(LiteralMatcher.new(1234))
196
+ '1_0'.should be_parsed_as(LiteralMatcher.new(10))
197
+ '1_9'.should be_parsed_as(LiteralMatcher.new(19))
198
+ '1_123'.should be_parsed_as(LiteralMatcher.new(1123))
199
+ '1_2_3_4'.should be_parsed_as(LiteralMatcher.new(1234))
200
+ end
201
+
202
+ # Canonical STRING is "\"abcd\"".
203
+ it "parses STRING" do
204
+ "''".should be_parsed_as(LiteralMatcher.new(""))
205
+ "'a'".should be_parsed_as(LiteralMatcher.new("a"))
206
+ "'\\\\'".should be_parsed_as(LiteralMatcher.new("\\"))
207
+ "'\\''".should be_parsed_as(LiteralMatcher.new("'"))
208
+ "'abc'".should be_parsed_as(LiteralMatcher.new("abc"))
209
+
210
+ '""'.should be_parsed_as(LiteralMatcher.new(""))
211
+ '"a"'.should be_parsed_as(LiteralMatcher.new("a"))
212
+ '"\\\\"'.should be_parsed_as(LiteralMatcher.new("\\"))
213
+ '"\\""'.should be_parsed_as(LiteralMatcher.new('"'))
214
+ '"\\n"'.should be_parsed_as(LiteralMatcher.new("\n"))
215
+ '"\\t"'.should be_parsed_as(LiteralMatcher.new("\t"))
216
+ '"\\r"'.should be_parsed_as(LiteralMatcher.new("\r"))
217
+ '"\\f"'.should be_parsed_as(LiteralMatcher.new("\f"))
218
+ '"\\v"'.should be_parsed_as(LiteralMatcher.new("\v"))
219
+ '"\\a"'.should be_parsed_as(LiteralMatcher.new("\a"))
220
+ '"\\e"'.should be_parsed_as(LiteralMatcher.new("\e"))
221
+ '"\\b"'.should be_parsed_as(LiteralMatcher.new("\b"))
222
+ '"\\s"'.should be_parsed_as(LiteralMatcher.new("\s"))
223
+ '"\\0"'.should be_parsed_as(LiteralMatcher.new("\0"))
224
+ '"\\7"'.should be_parsed_as(LiteralMatcher.new("\7"))
225
+ '"\\123"'.should be_parsed_as(LiteralMatcher.new("\123"))
226
+ '"\\x0"'.should be_parsed_as(LiteralMatcher.new("\x0"))
227
+ '"\\x9"'.should be_parsed_as(LiteralMatcher.new("\x9"))
228
+ '"\\xa"'.should be_parsed_as(LiteralMatcher.new("\xa"))
229
+ '"\\xf"'.should be_parsed_as(LiteralMatcher.new("\xf"))
230
+ '"\\xA"'.should be_parsed_as(LiteralMatcher.new("\xA"))
231
+ '"\\xF"'.should be_parsed_as(LiteralMatcher.new("\xF"))
232
+ '"\\x12"'.should be_parsed_as(LiteralMatcher.new("\x12"))
233
+ '"abc"'.should be_parsed_as(LiteralMatcher.new("abc"))
234
+ end
235
+
236
+ it "skips whitespace before tokens" do
237
+ '42'.should be_parsed_as(@i42)
238
+ ' 42'.should be_parsed_as(@i42)
239
+ "\t42".should be_parsed_as(@i42)
240
+ "\r42".should be_parsed_as(@i42)
241
+ "\n42".should be_parsed_as(@i42)
242
+ ' 42'.should be_parsed_as(@i42)
243
+ end
244
+
245
+ it "skips whitespace after tokens" do
246
+ '42'.should be_parsed_as(@i42)
247
+ '42 '.should be_parsed_as(@i42)
248
+ "42\t".should be_parsed_as(@i42)
249
+ "42\r".should be_parsed_as(@i42)
250
+ "42\n".should be_parsed_as(@i42)
251
+ '42 '.should be_parsed_as(@i42)
252
+ end
253
+
254
+ it "handles lexical errors" do
255
+ '@#%'.should not_be_parsed("Unexpected character: \"@\".")
256
+ end
257
+
258
+ it "handles syntax errors" do
259
+ '42 43'.should not_be_parsed("Unexpected token: \"43\".")
260
+ end
261
+ end
262
+ end
@@ -0,0 +1,39 @@
1
+ require "spec_helper"
2
+
3
+ describe Machete do
4
+ describe "matches?" do
5
+ it "returns true when passed matching node and pattern" do
6
+ Machete.matches?('42'.to_ast, 'FixnumLiteral<value = 42>').should be_true
7
+ end
8
+
9
+ it "returns false when passed non-matching node and pattern" do
10
+ Machete.matches?('43'.to_ast, 'FixnumLiteral<value = 42>').should be_false
11
+ end
12
+ end
13
+
14
+ describe "find" do
15
+ it "returns [] when no node matches the pattern" do
16
+ Machete.find('"abcd"'.to_ast, 'FixnumLiteral').should == []
17
+ end
18
+
19
+ it "returns root node if it matches the pattern" do
20
+ nodes = Machete.find('42'.to_ast, 'FixnumLiteral')
21
+
22
+ nodes.size.should == 1
23
+ nodes[0].should be_instance_of(Rubinius::AST::FixnumLiteral)
24
+ nodes[0].value.should == 42
25
+ end
26
+
27
+ it "returns child nodes if they match the pattern" do
28
+ nodes = Machete.find('42 + 43 + 44'.to_ast, 'FixnumLiteral').sort_by(&:value)
29
+
30
+ nodes.size.should == 3
31
+ nodes[0].should be_instance_of(Rubinius::AST::FixnumLiteral)
32
+ nodes[0].value.should == 42
33
+ nodes[1].should be_instance_of(Rubinius::AST::FixnumLiteral)
34
+ nodes[1].value.should == 43
35
+ nodes[2].should be_instance_of(Rubinius::AST::FixnumLiteral)
36
+ nodes[2].value.should == 44
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ require "machete"
2
+
3
+ RSpec.configure do |c|
4
+ c.color_enabled = true
5
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: machete
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - David Majda
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-27 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: racc
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rdoc
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: rdiscount
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :development
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: yard
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :development
90
+ version_requirements: *id005
91
+ description: Machete is a simple tool for matching Rubinius AST nodes against patterns. You can use it if you are writing any kind of tool that processes Ruby code and needs to do some work on specific types of nodes, needs to find patterns in the code, etc.
92
+ email: dmajda@suse.de
93
+ executables: []
94
+
95
+ extensions: []
96
+
97
+ extra_rdoc_files: []
98
+
99
+ files:
100
+ - .gitignore
101
+ - .yardopts
102
+ - LICENSE
103
+ - README.md
104
+ - Rakefile
105
+ - VERSION
106
+ - lib/machete.rb
107
+ - lib/machete/matchers.rb
108
+ - lib/machete/parser.y
109
+ - lib/machete/version.rb
110
+ - machete.gemspec
111
+ - spec/machete/matchers_spec.rb
112
+ - spec/machete/parser_spec.rb
113
+ - spec/machete_spec.rb
114
+ - spec/spec_helper.rb
115
+ - lib/machete/parser.rb
116
+ has_rdoc: true
117
+ homepage: https://github.com/openSUSE/machete
118
+ licenses:
119
+ - MIT
120
+ post_install_message:
121
+ rdoc_options: []
122
+
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ hash: 3
131
+ segments:
132
+ - 0
133
+ version: "0"
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ hash: 3
140
+ segments:
141
+ - 0
142
+ version: "0"
143
+ requirements: []
144
+
145
+ rubyforge_project:
146
+ rubygems_version: 1.5.2
147
+ signing_key:
148
+ specification_version: 3
149
+ summary: Simple tool for matching Rubinius AST nodes against patterns
150
+ test_files: []
151
+