metamorpher 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.travis.yml +3 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +541 -0
- data/Rakefile +23 -0
- data/examples/refactorings/rails/where_first/app.rb +50 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_mocks.rb +31 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_not_called_expectations.rb +14 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_strict_mocks.rb +27 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_to_find_by.rb +14 -0
- data/examples/refactorings/rails/where_first/sample_controller.rb +184 -0
- data/lib/metamorpher/builders/ast/builder.rb +50 -0
- data/lib/metamorpher/builders/ast/derivation_builder.rb +20 -0
- data/lib/metamorpher/builders/ast/greedy_variable_builder.rb +29 -0
- data/lib/metamorpher/builders/ast/literal_builder.rb +31 -0
- data/lib/metamorpher/builders/ast/variable_builder.rb +29 -0
- data/lib/metamorpher/builders/ast.rb +11 -0
- data/lib/metamorpher/builders/ruby/builder.rb +38 -0
- data/lib/metamorpher/builders/ruby/deriving_visitor.rb +13 -0
- data/lib/metamorpher/builders/ruby/ensuring_visitor.rb +13 -0
- data/lib/metamorpher/builders/ruby/term.rb +35 -0
- data/lib/metamorpher/builders/ruby/uppercase_constant_rewriter.rb +31 -0
- data/lib/metamorpher/builders/ruby/uppercase_rewriter.rb +28 -0
- data/lib/metamorpher/builders/ruby/variable_replacement_visitor.rb +32 -0
- data/lib/metamorpher/builders/ruby.rb +11 -0
- data/lib/metamorpher/drivers/parse_error.rb +5 -0
- data/lib/metamorpher/drivers/ruby.rb +78 -0
- data/lib/metamorpher/matcher/match.rb +26 -0
- data/lib/metamorpher/matcher/matching.rb +61 -0
- data/lib/metamorpher/matcher/no_match.rb +18 -0
- data/lib/metamorpher/matcher.rb +6 -0
- data/lib/metamorpher/refactorer/merger.rb +18 -0
- data/lib/metamorpher/refactorer/site.rb +29 -0
- data/lib/metamorpher/refactorer.rb +48 -0
- data/lib/metamorpher/rewriter/replacement.rb +18 -0
- data/lib/metamorpher/rewriter/rule.rb +38 -0
- data/lib/metamorpher/rewriter/substitution.rb +45 -0
- data/lib/metamorpher/rewriter/traverser.rb +26 -0
- data/lib/metamorpher/rewriter.rb +12 -0
- data/lib/metamorpher/support/map_at.rb +8 -0
- data/lib/metamorpher/terms/derived.rb +13 -0
- data/lib/metamorpher/terms/literal.rb +47 -0
- data/lib/metamorpher/terms/term.rb +40 -0
- data/lib/metamorpher/terms/variable.rb +17 -0
- data/lib/metamorpher/version.rb +3 -0
- data/lib/metamorpher/visitable/visitable.rb +7 -0
- data/lib/metamorpher/visitable/visitor.rb +21 -0
- data/lib/metamorpher.rb +30 -0
- data/metamorpher.gemspec +30 -0
- data/spec/integration/ast/builder_spec.rb +13 -0
- data/spec/integration/ast/matcher_spec.rb +132 -0
- data/spec/integration/ast/rewriter_spec.rb +138 -0
- data/spec/integration/ruby/builder_spec.rb +125 -0
- data/spec/integration/ruby/refactorer_spec.rb +192 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/helpers/silence_stream.rb +10 -0
- data/spec/support/matchers/have_matched_matcher.rb +22 -0
- data/spec/support/matchers/have_substitution_matcher.rb +15 -0
- data/spec/support/shared_examples/shared_examples_for_derivation_builders.rb +53 -0
- data/spec/support/shared_examples/shared_examples_for_greedy_variable_builders.rb +49 -0
- data/spec/support/shared_examples/shared_examples_for_literal_builders.rb +93 -0
- data/spec/support/shared_examples/shared_examples_for_variable_builders.rb +49 -0
- data/spec/unit/builders/ast/derivation_builder_spec.rb +5 -0
- data/spec/unit/builders/ast/greedy_variable_builder_spec.rb +9 -0
- data/spec/unit/builders/ast/literal_builder_spec.rb +9 -0
- data/spec/unit/builders/ast/variable_builder_spec.rb +9 -0
- data/spec/unit/builders/ruby/variable_replacement_visitor_spec.rb +48 -0
- data/spec/unit/drivers/ruby_spec.rb +91 -0
- data/spec/unit/matcher/matching_spec.rb +230 -0
- data/spec/unit/metamorpher_spec.rb +22 -0
- data/spec/unit/refactorer/merger_spec.rb +84 -0
- data/spec/unit/refactorer/site_spec.rb +52 -0
- data/spec/unit/rewriter/replacement_spec.rb +73 -0
- data/spec/unit/rewriter/substitution_spec.rb +97 -0
- data/spec/unit/rewriter/traverser_spec.rb +51 -0
- data/spec/unit/support/map_at_spec.rb +18 -0
- data/spec/unit/terms/literal_spec.rb +60 -0
- data/spec/unit/terms/term_spec.rb +59 -0
- data/spec/unit/visitable/visitor_spec.rb +35 -0
- metadata +269 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ba3cbe5b4caa4cc89ef42987cbc19f1c8f93fa11
|
4
|
+
data.tar.gz: 439e71e7d526596c23842f9affad63995455bac7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b10b8f2f4344c760f772f4fe11af3de5a7a1d4c8fcac847be6210eb44c42ab9dc1360c6111326cb8f40a127bda8d3c0a783f5f8683f97e223b5fedc4c461e6b7
|
7
|
+
data.tar.gz: 6efc0d4e4dd097c5df447c723e855dcba610f9283d3766193799d205963f9843dc686a578b4c382124e12c68ea91f47ff56ca0d4cd0c97822ed3dca7a69521c9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
AllCops:
|
2
|
+
Includes:
|
3
|
+
- Gemfile
|
4
|
+
- Rakefile
|
5
|
+
Excludes:
|
6
|
+
- vendor/*
|
7
|
+
- examples/refactorings/rails/where_first/sample_controller.rb
|
8
|
+
|
9
|
+
StringLiterals:
|
10
|
+
EnforcedStyle: double_quotes
|
11
|
+
|
12
|
+
LineLength:
|
13
|
+
Max: 99
|
14
|
+
|
15
|
+
Documentation:
|
16
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Louis Rose
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,541 @@
|
|
1
|
+
# Metamorpher [![Build Status](https://travis-ci.org/mutiny/metamorpher.png)](https://travis-ci.org/mutiny/metamorpher) [![Code Climate](https://codeclimate.com/github/mutiny/metamorpher.png)](https://codeclimate.com/github/mutiny/metamorpher) [![Dependency Status](https://gemnasium.com/mutiny/metamorpher.png)](https://gemnasium.com/mutiny/metamorpher) [![Coverage Status](https://coveralls.io/repos/mutiny/metamorpher/badge.png?branch=master)](https://coveralls.io/r/mutiny/metamorpher?branch=master)
|
2
|
+
|
3
|
+
A term rewriting library for transforming (Ruby) programs.
|
4
|
+
|
5
|
+
## Basic usage
|
6
|
+
|
7
|
+
Here's a very simple example that refactors Ruby code of the form `if some_predicate then true else false end` to `some_predicate`:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require "metamorpher"
|
11
|
+
|
12
|
+
class UnnecessaryConditionalRefactorer
|
13
|
+
include Metamorpher::Refactorer
|
14
|
+
include Metamorpher::Builders::Ruby
|
15
|
+
|
16
|
+
def pattern
|
17
|
+
builder.build("if CONDITION then true else false end")
|
18
|
+
end
|
19
|
+
|
20
|
+
def replacement
|
21
|
+
builder.build("CONDITION")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
program = "result = if some_predicate then true else false end"
|
26
|
+
UnnecessaryConditionalRefactorer.new.refactor(program)
|
27
|
+
# => "result = some_predicate"
|
28
|
+
```
|
29
|
+
|
30
|
+
This simple example is short, but terse! To fully understand it, you might now want to read about:
|
31
|
+
|
32
|
+
* [Fundamentals](#fundamentals) - Information on the core of metamorpher and the theory of term rewriting:
|
33
|
+
* [Building terms](#building-terms) - how to create the data structure (terms) used by Rewriters and Matchers.
|
34
|
+
* [Matchers](#matchers) - how to determine whether an expression adheres to a pattern (i.e., matches a term).
|
35
|
+
* [Rewriters](#rewriters) - how to transform expressions into other expressions.
|
36
|
+
|
37
|
+
* [Practicalities](#practicalities) - Information on how to use metamorpher to manipulate (Ruby) programs:
|
38
|
+
* [Building Ruby terms](#building-ruby-terms) - how to concisely create terms that represent Ruby programs.
|
39
|
+
* [Refactorers](#refactorers) - how to use rewriters to refactor Ruby programs.
|
40
|
+
|
41
|
+
|
42
|
+
## Fundamentals
|
43
|
+
|
44
|
+
Metamorpher is based on the theory of [term rewriting](http://www.meta-environment.org/doc/books/extraction-transformation/term-rewriting/term-rewriting.html). The following sections describe how to build terms (the key data structure used in metamorpher), how to use terms to search over programs using matchers, and how to transform parts of a program using rewriters.
|
45
|
+
|
46
|
+
**Note** that the examples in this section operate on a fictional programming language (i.e., not Ruby). For examples that manipulate Ruby programs, see the [practicalities](#practicalities) section.
|
47
|
+
|
48
|
+
### Building terms
|
49
|
+
|
50
|
+
The primary data structure used for [rewriting](#rewriters) and for [matching](#matchers) is a term. A term is a tree (i.e., an acyclic graph). The nodes of the tree are either a:
|
51
|
+
|
52
|
+
* Literal - a node of the abstract-syntax tree of a program.
|
53
|
+
* Variable - a named node, which is bound to a subterm (subtree) during [matching](#matchers).
|
54
|
+
* Greedy variable - a variable that is bound to a set of subterms during [matching](#matchers).
|
55
|
+
* Derivation - a placeholder node, which is replaced during [rewriting](#rewriters).
|
56
|
+
|
57
|
+
To simplify the construction of terms, metamorpher provides the `Metamorpher::Builders::AST::Builder` class, which is demonstrated below.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
require "metamorpher"
|
61
|
+
|
62
|
+
include Metamorpher::Builder::AST
|
63
|
+
|
64
|
+
builder.literal! :succ # => succ
|
65
|
+
builder.literal! 4 # => 4
|
66
|
+
|
67
|
+
builder.variable! :n # => N
|
68
|
+
builder.greedy_variable! :n # => N+
|
69
|
+
|
70
|
+
builder.derivation! :singular do |singular, builder|
|
71
|
+
builder.literal!(singular.name + "s")
|
72
|
+
end
|
73
|
+
# [SINGULAR] -> ...
|
74
|
+
|
75
|
+
builder.derivation! :key, :value do |key, value, builder|
|
76
|
+
builder.pair(key, value)
|
77
|
+
end
|
78
|
+
# [KEY, VALUE] -> ...
|
79
|
+
```
|
80
|
+
|
81
|
+
Variables can be conditional, in which case they are specified by passing a block:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
builder.variable!(:method) { |literal| literal.name =~ /^find_by_/ } # => METHOD?
|
85
|
+
builder.greedy_variable!(:pairs) { |literals| literals.size.even? } #=> PAIRS+?
|
86
|
+
```
|
87
|
+
|
88
|
+
#### Shorthands
|
89
|
+
|
90
|
+
The builder provides a method missing shorthand for constructing literals, variables and greedy variables:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
builder.succ # => succ
|
94
|
+
builder.N # => N
|
95
|
+
builder.N_ # => N+
|
96
|
+
```
|
97
|
+
|
98
|
+
Conditional variables can also be constructed using this shorthand:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
builder.METHOD { |literal| literal.name =~ /^find_by_/ } #=> METHOD?
|
102
|
+
builder.PAIRS_ { |literal| literal.name =~ /^find_by_/ } #=> PAIRS+?
|
103
|
+
```
|
104
|
+
|
105
|
+
#### Coercion of non-terms to literals
|
106
|
+
|
107
|
+
When constructing a literal, the builder ensures that any children are converted to literals if they are not already a term:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
builder.literal!(:add, :x, :y) # => add(x, y)
|
111
|
+
builder.add(:x, :y) # => add(x, y)
|
112
|
+
```
|
113
|
+
|
114
|
+
Without automatic coercion, the statements above would be written as follows. Note that they are more verbose:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
builder.literal!(:add, builder.literal!(:x), builder.literal!(:y)) # => add(x, y)
|
118
|
+
builder.add(builder.x, builder.y) # => add(x, y)
|
119
|
+
```
|
120
|
+
|
121
|
+
Note that coercion isn't necessary (and isn't applied) when the children of a literal are already terms:
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
builder.literal!(:add, builder.variable!(:n), builder.variable!(:m)) # => add(N, M)
|
125
|
+
builder.add(builder.N, builder.M) # => add(N, M)
|
126
|
+
```
|
127
|
+
|
128
|
+
### Matchers
|
129
|
+
|
130
|
+
Matchers search for subexpressions that adhere to a specified pattern. They are used by rewriters to find transformation sites in expressions, and can also be used to search programs. For simple searches over a program's source code, a regular expression can be used. For more complicated searches, a term matching system (such as the one provided by `Metamorpher::Matcher`) is likely to be a better fit.
|
131
|
+
|
132
|
+
Metamorpher provides the `Metamorpher::Matcher` module for specifying matchers. Include it, specify a `pattern` and then call `run(expression)`:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
require "metamorpher"
|
136
|
+
|
137
|
+
class SuccZeroMatcher
|
138
|
+
include Metamorpher::Matcher
|
139
|
+
include Metamorpher::Builders::AST
|
140
|
+
|
141
|
+
def pattern
|
142
|
+
builder.succ(0)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
expression = Metamorpher.builder.succ(0) # => succ(0)
|
147
|
+
result = SuccZeroMatcher.new.run(expression)
|
148
|
+
# => <Metamorpher::Matcher::Match root=succ(0), substitution={}>
|
149
|
+
result.matches? # => true
|
150
|
+
|
151
|
+
expression = Metamorpher.builder.succ(1) # => succ(1)
|
152
|
+
result = SuccZeroMatcher.new.run(expression)
|
153
|
+
# => <Metamorpher::Matcher::NoMatch>
|
154
|
+
result.matches? # => false
|
155
|
+
```
|
156
|
+
|
157
|
+
#### Variables
|
158
|
+
|
159
|
+
Matching is more powerful when we can allow for some variability in the expressions that we wish to match. Metamorpher provides variables for this purpose.
|
160
|
+
|
161
|
+
For example, suppose we wish to match expressions of the form `succ(X)` where X could be any subexpression. The following matcher achieves this, by using a variable (`x`) to match the argument to `succ`:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
class SuccMatcher
|
165
|
+
include Metamorpher::Matcher
|
166
|
+
include Metamorpher::Builders::AST
|
167
|
+
|
168
|
+
def pattern
|
169
|
+
builder.succ(builder.X)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
expression = Metamorpher.builder.succ(0) # => succ(0)
|
174
|
+
SuccMatcher.new.run(expression)
|
175
|
+
# => <Metamorpher::Matcher::Match root=succ(0), substitution={:x=>0}>
|
176
|
+
|
177
|
+
expression = Metamorpher.builder.succ(1) # => succ(1)
|
178
|
+
SuccMatcher.new.run(expression)
|
179
|
+
# => <Metamorpher::Matcher::Match root=succ(0), substitution={:x=>1}>
|
180
|
+
|
181
|
+
expression = Metamorpher.builder.succ(:n) # => succ(n)
|
182
|
+
SuccMatcher.new.run(expression)
|
183
|
+
# => <Metamorpher::Matcher::Match root=succ(n), substitution={:x=>n}>
|
184
|
+
|
185
|
+
expression = Metamorpher.builder.succ(Metamorpher.builder.succ(:n)) # => succ(succ(n))
|
186
|
+
SuccMatcher.new.run(expression)
|
187
|
+
# => <Metamorpher::Matcher::Match root=succ(succ(n)), substitution={:x=>succ(n)}>
|
188
|
+
```
|
189
|
+
|
190
|
+
#### Conditional variables
|
191
|
+
|
192
|
+
By default, a variable matches any literal. Matching is more powerful when variables are able to match only those literals that satisfy some condition. Metamorpher provides conditional variables for this purpose.
|
193
|
+
|
194
|
+
For example, suppose that we wish to create a matcher that only matches method calls of the form `User.find_by_XXX`, but not calls to `User.find`, `User.where` or `User.find_by`. The following matcher achieves this, by using a conditional variable (`method`). Note that the condition is specified via the block passed when building the variable:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
class DynamicFinderMatcher
|
198
|
+
include Metamorpher::Matcher
|
199
|
+
include Metamorpher::Builders::AST
|
200
|
+
|
201
|
+
def pattern
|
202
|
+
builder.literal!(
|
203
|
+
:".",
|
204
|
+
:User,
|
205
|
+
builder.METHOD { |literal| literal.name =~ /^find_by_/ }
|
206
|
+
)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
expression = Metamorpher.builder.literal!(:".", :User, :find_by_name) # => .(User, find_by_name)
|
211
|
+
DynamicFinderMatcher.new.run(expression)
|
212
|
+
# => #<Metamorpher::Matcher::Match root=.(User, find_by_name), substitution={:method=>find_by_name}>
|
213
|
+
|
214
|
+
expression = Metamorpher.builder.literal!(:".", :User, :find) # => .(User, find)
|
215
|
+
DynamicFinderMatcher.new.run(expression)
|
216
|
+
# => #<Metamorpher::Matcher::NoMatch>
|
217
|
+
```
|
218
|
+
|
219
|
+
#### Greedy variables
|
220
|
+
|
221
|
+
Sometimes a matcher needs to be able to match an expression that contains a variable number of subexpressions. Metamorpher provides greedy variables for this purpose.
|
222
|
+
|
223
|
+
For example, suppose that we wish to create a matcher that works for an expression, `add`, that can have 1 or more children. The following matcher achieves this, by using a greedy variable (`args`).
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
class MultiAddMatcher
|
227
|
+
include Metamorpher::Matcher
|
228
|
+
include Metamorpher::Builders::AST
|
229
|
+
|
230
|
+
def pattern
|
231
|
+
builder.add(
|
232
|
+
builder.ARGS_
|
233
|
+
)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
MultiAddMatcher.new.run(Metamorpher.builder.add(1,2))
|
238
|
+
# => #<Metamorpher::Matcher::Match root=add(1,2), substitution={:args=>[1, 2]}>
|
239
|
+
|
240
|
+
MultiAddMatcher.new.run(Metamorpher.builder.add(1,2,3))
|
241
|
+
# => #<Metamorpher::Matcher::Match root=add(1,2,3), substitution={:args=>[1, 2, 3]}>
|
242
|
+
```
|
243
|
+
|
244
|
+
### Rewriters
|
245
|
+
|
246
|
+
Rewriters perform small, in-place changes to an expression. They can be used for program transformations, such as refactorings. For some simple program transformations, a regular expression can be used on the program source. For more complicated transformations, a term rewriting system (such as the one provided by `Metamorpher::Rewriter`) is likely to be a better fit.
|
247
|
+
|
248
|
+
Metamorpher provides the `Metamorpher::Rewriter` module for specifying rewriters. Include it, specify a `pattern` and a `replacement`, and then call `reduce(expression)`:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
require "metamorpher"
|
252
|
+
|
253
|
+
class SuccZeroRewriter
|
254
|
+
include Metamorpher::Rewriter
|
255
|
+
include Metamorpher::Builders::AST
|
256
|
+
|
257
|
+
def pattern
|
258
|
+
builder.literal! :succ, 0
|
259
|
+
end
|
260
|
+
|
261
|
+
def replacement
|
262
|
+
builder.literal! 1
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
expression = Metamorpher.builder.succ(0) # => succ(0)
|
267
|
+
SuccZeroRewriter.new.reduce(expression) # => 1
|
268
|
+
```
|
269
|
+
|
270
|
+
Note that `reduce` has no effect when called on an expression that does not match `pattern`:
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
expression = Metamorpher.builder.succ(1) # => succ(1)
|
274
|
+
SuccZeroRewriter.new.reduce(expression) # => succ(1)
|
275
|
+
```
|
276
|
+
|
277
|
+
A call to `reduce` will return a literal that cannot be reduced any further by this rewriter:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
expression = Metamorpher.builder.add(
|
281
|
+
Metamorpher.builder.succ(0),
|
282
|
+
Metamorpher.builder.succ(0)
|
283
|
+
)
|
284
|
+
# => succ(0)
|
285
|
+
|
286
|
+
SuccZeroRewriter.new.reduce(expression) # => add(1, 1)
|
287
|
+
```
|
288
|
+
|
289
|
+
A call to `apply` will instead return a literal after a single application of the rewriter:
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
SuccZeroRewriter.new.apply(expression) # => add(1, succ(0))
|
293
|
+
```
|
294
|
+
|
295
|
+
Both `reduce` and `apply` can optionally take a block, which is called immediately before the matching term is replaced with the rewritten term:
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
SuccZeroRewriter.new.reduce(expression) do |matching, rewritten|
|
299
|
+
puts "About to replace #{matching.inspect} at position #{matching.path} with #{rewritten.inspect}"
|
300
|
+
end
|
301
|
+
# About to replace 'succ(0)' at position [0] with '1'
|
302
|
+
# About to replace 'succ(0)' at position [1] with '1'
|
303
|
+
# =>
|
304
|
+
```
|
305
|
+
|
306
|
+
#### Derivations
|
307
|
+
|
308
|
+
Rewriting is more powerful when we are able to adjust the expression that is substituted for a captured variable. Metamorpher provides derivations for this purpose. (You may wish to read the section on [variables](#variables) before looking at the following example).
|
309
|
+
|
310
|
+
For example, suppose that we wish to create a rewriter that pluralises any literal. The following rewriter achieves this, by using a derivation (see the implementation of `replacement`) to create a new literal after an expression has been matched. Crucially, the derivation uses data from the captured literal when building the replacement literal:
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
class PluraliseRewriter
|
314
|
+
include Metamorpher::Rewriter
|
315
|
+
include Metamorpher::Builders::AST
|
316
|
+
|
317
|
+
def pattern
|
318
|
+
builder.SINGULAR
|
319
|
+
end
|
320
|
+
|
321
|
+
def replacement
|
322
|
+
builder.derivation! :singular do |singular|
|
323
|
+
builder.literal!(singular.name + "s")
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
PluraliseRewriter.new.apply(Metamorpher.builder.literal! "dog") # => "dogs"
|
329
|
+
```
|
330
|
+
|
331
|
+
Derivations can be based on more than one captured variable. In which case the call to `derivation!` and the block take more than one argument:
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
builder.derivation! :key, :value do |key, value|
|
335
|
+
builder.literal!(:pair, key, value)
|
336
|
+
end
|
337
|
+
```
|
338
|
+
|
339
|
+
## Practicalities
|
340
|
+
|
341
|
+
Metamorpher provides modules that can be used to simplify the transformation of Ruby programs. This section describes how to build metamorpher terms that represent Ruby programs, and how to refactor Ruby programs. [Matchers](#matchers) and [Rewriters](#rewriters) can be used to manipulate Ruby programs too.
|
342
|
+
|
343
|
+
**Note** that metamorpher is not limited to manipulating Ruby programs. For more details on how metamorpher works and its language-independent core, see the [fundamentals](#fundamentals) section.
|
344
|
+
|
345
|
+
### Building Ruby terms
|
346
|
+
|
347
|
+
To match, rewrite or refactor Ruby programs, it's necessary to create [terms](#building-terms) that represent Ruby programs. Metamorpher provides the `Metamorpher::Builders::Ruby::Builder` class to simplify this process.
|
348
|
+
|
349
|
+
Recall that term is a tree (i.e., an acyclic graph), whose nodes are either a:
|
350
|
+
|
351
|
+
* Literal - a node of the abstract-syntax tree of a program.
|
352
|
+
* Variable - a named node, which is bound to a subterm (subtree) during [matching](#matchers).
|
353
|
+
* Greedy variable - a variable that is bound to a set of subterms during [matching](#matchers).
|
354
|
+
* Derivation - a placeholder node, which is replaced during [rewriting](#rewriters).
|
355
|
+
|
356
|
+
The following examples demonstrate the way in which terms can built from strings that resemble Ruby programs:
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
require "metamorpher"
|
360
|
+
|
361
|
+
include Metamorpher::Builders::Ruby
|
362
|
+
|
363
|
+
builder.build("2") # => int(2)
|
364
|
+
builder.build("2 + 2") # => send(int(2), +, int(2))
|
365
|
+
```
|
366
|
+
|
367
|
+
To build terms that contain variables, use uppercase characters. To build a greedy variable, ensure the name of the variable ends with an underscore:
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
builder.build("2 + ADDEND") # => send(int(2), +, ADDEND)
|
371
|
+
builder.build("hello(PARAMS_)") # => send(, hello, PARAMS+)
|
372
|
+
```
|
373
|
+
|
374
|
+
Variables can be conditional, in which case they are specified by appending a call to `ensuring`:
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
builder
|
378
|
+
.build("METHOD_CALL(:foo, :bar)")
|
379
|
+
.ensuring(METHOD_CALL) { |m| m.name =~ /^find_by_/ }
|
380
|
+
# => METHOD?
|
381
|
+
```
|
382
|
+
|
383
|
+
Similar, derivations can be specified by appending a call to `deriving`:
|
384
|
+
|
385
|
+
```ruby
|
386
|
+
builder
|
387
|
+
.build("PLURAL(:foo, :bar)")
|
388
|
+
.deriving("PLURAL", "SINGULAR") do |singular|
|
389
|
+
builder.build(singular.name.to_s + "s")
|
390
|
+
end
|
391
|
+
# [SINGULAR] -> ...
|
392
|
+
|
393
|
+
builder
|
394
|
+
.build("HASH")
|
395
|
+
.deriving("HASH", "KEY", "VALUE") do |key, value|
|
396
|
+
builder.build("[#{key}, #{value}]")
|
397
|
+
end
|
398
|
+
# [KEY, VALUE] -> ...
|
399
|
+
```
|
400
|
+
|
401
|
+
### Refactorers
|
402
|
+
|
403
|
+
Refactorers are [rewriters](#rewriters) that are specialised for rewriting program source code. A refactorer parses a program's source code, rewrites the source code, and returns the unparsed, rewritten source code.
|
404
|
+
|
405
|
+
Metamorpher provides the `Metamorpher::Refactorer` module for constructing classes that perform refactorings. Include it, specify a `pattern` and a `replacement`, and then call `refactor(src)`:
|
406
|
+
|
407
|
+
```ruby
|
408
|
+
require "metamorpher"
|
409
|
+
|
410
|
+
class UnnecessaryConditionalRefactorer
|
411
|
+
include Metamorpher::Refactorer
|
412
|
+
include Metamorpher::Builders::Ruby
|
413
|
+
|
414
|
+
def pattern
|
415
|
+
builder.build("if CONDITION then true else false end")
|
416
|
+
end
|
417
|
+
|
418
|
+
def replacement
|
419
|
+
builder.build("CONDITION")
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
program = "a = if some_predicate then true else false end"
|
424
|
+
UnnecessaryConditionalRefactorer.new.refactor(program)
|
425
|
+
# => "a = some_predicate"
|
426
|
+
```
|
427
|
+
|
428
|
+
The `refactor` method can optionally take a block, which is called immediately before the matching code is replaced with the refactored code:
|
429
|
+
|
430
|
+
```ruby
|
431
|
+
source = "a = if some_predicate then true else false end;" \
|
432
|
+
"b = if some_other_predicate then true else false end;"
|
433
|
+
|
434
|
+
UnnecessaryConditionalRefactorer.new.refactor(source) do |refactoring|
|
435
|
+
puts "About to replace '#{refactoring.original_code}' " \
|
436
|
+
"at position #{refactoring.original_position} " \
|
437
|
+
"with '#{refactoring.refactored_code}'"
|
438
|
+
end
|
439
|
+
# About to replace 'if some_predicate then true else false end' at position 4..45 with 'some_predicate'
|
440
|
+
# About to replace 'if some_other_predicate then true else false end' at position 51..98 with 'some_other_predicate'
|
441
|
+
# => "a = some_predicate;b = some_other_predicate;"
|
442
|
+
```
|
443
|
+
|
444
|
+
The `Metamorpher::Refactorer` module also defines a `refactor_file(path)` method, which can be used to apply refactoring to a file stored on disk:
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
path = File.expand_path("refactorable.rb", "/Users/louis/code/mutiny")
|
448
|
+
# => "/Users/louis/code/mutiny/refactorable.rb"
|
449
|
+
|
450
|
+
UnnecessaryConditionalRefactorer.new.refactor_file(path)
|
451
|
+
# => ... (refactored code)
|
452
|
+
|
453
|
+
UnnecessaryConditionalRefactorer.new.refactor_file(path) do |refactoring|
|
454
|
+
# works just like the block passed to refactor
|
455
|
+
end
|
456
|
+
# => ... (refactored code)
|
457
|
+
```
|
458
|
+
|
459
|
+
You might prefer the `refactor_files(paths)` method, if you'd like to refactor several files at once:
|
460
|
+
|
461
|
+
```ruby
|
462
|
+
paths = Dir.glob(File.expand_path(File.join("**", "*.rb"), "/Users/louis/code/mutiny"))
|
463
|
+
# => ["/Users/louis/code/mutiny/lib/mutiny.rb", ...]
|
464
|
+
|
465
|
+
# Note that refactor_files returns a Hash rather than a String
|
466
|
+
UnnecessaryConditionalRefactorer.new.refactor_files(paths)
|
467
|
+
# => { "/Users/louis/code/mutiny/lib/mutiny.rb" => (refactored code), ... }
|
468
|
+
|
469
|
+
# Note that refactor_files yields for each file: its path, its new contents, and its refactoring sites
|
470
|
+
UnnecessaryConditionalRefactorer.new.refactor_files(path) do |path, new_contents, sites|
|
471
|
+
puts "In #{path}:"
|
472
|
+
|
473
|
+
sites.each do |site|
|
474
|
+
puts "\tAbout to replace '#{refactoring.original_code}' " \
|
475
|
+
"at position #{refactoring.original_position} " \
|
476
|
+
"with '#{refactoring.refactored_code}'"
|
477
|
+
end
|
478
|
+
end
|
479
|
+
# In /Users/louis/code/mutiny/lib/mutiny.rb:
|
480
|
+
# About to replace 'if some_predicate then true else false end' at position 4..45 with 'some_predicate'
|
481
|
+
# ...
|
482
|
+
# => { "/Users/louis/code/mutiny/lib/mutiny.rb" => (refactored code), ... }
|
483
|
+
```
|
484
|
+
|
485
|
+
#### Refactoring programs written in other languages
|
486
|
+
|
487
|
+
By default, `Metamorpher::Refactorer` assumes that you wish to refactor Ruby programs, and will attempt to `require` the [parser](https://github.com/whitequark/parser) and [unparser](https://github.com/mbj/unparser) gems. If instead you wish to use a different Ruby parser / unparser or you wish to refactor a program written in a language other than Ruby, you should specify a different `driver`, as shown below. (A `Metamorpher::Driver` is responsible for transforming source code to metamorpher [terms](#building-terms), and vice-versa).
|
488
|
+
|
489
|
+
```ruby
|
490
|
+
class JavaRefactorer
|
491
|
+
include Metamorpher::Refactorer
|
492
|
+
|
493
|
+
def driver
|
494
|
+
YourTool::MetamorpherDrivers::Java.new
|
495
|
+
end
|
496
|
+
|
497
|
+
def pattern
|
498
|
+
...
|
499
|
+
end
|
500
|
+
|
501
|
+
def replacement
|
502
|
+
...
|
503
|
+
end
|
504
|
+
end
|
505
|
+
```
|
506
|
+
|
507
|
+
#### Examples
|
508
|
+
|
509
|
+
##### Refactor Rails where(...).first
|
510
|
+
|
511
|
+
##### Refactor Rails dynamic find_by
|
512
|
+
|
513
|
+
|
514
|
+
## Installation
|
515
|
+
|
516
|
+
Add these line to your application's Gemfile:
|
517
|
+
|
518
|
+
gem 'metamorpher'
|
519
|
+
|
520
|
+
And then execute:
|
521
|
+
|
522
|
+
$ bundle
|
523
|
+
|
524
|
+
Or install it yourself as:
|
525
|
+
|
526
|
+
$ gem install metamorpher
|
527
|
+
|
528
|
+
## Contributing
|
529
|
+
|
530
|
+
1. Fork it
|
531
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
532
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
533
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
534
|
+
5. Create new Pull Request
|
535
|
+
|
536
|
+
## Acknowledgments
|
537
|
+
|
538
|
+
Thank-you to the authors of other projects and resources that have inspired metamorpher, including:
|
539
|
+
|
540
|
+
* Paul Klint's [tutorial on term rewriting](http://www.meta-environment.org/doc/books/extraction-transformation/term-rewriting/term-rewriting.html), which metamorpher is heavily based on.
|
541
|
+
* Jim Weirich's [Builder](https://github.com/jimweirich/builder) gem, which heavily influenced the design of `Metamorpher::Builders::AST::Builder`.
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
task default: ["test:unit", "test:integration", "style:check"]
|
5
|
+
|
6
|
+
namespace :test do
|
7
|
+
RSpec::Core::RakeTask.new(:unit) do |task|
|
8
|
+
task.pattern = "./spec/unit{,/*/**}/*_spec.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:integration) do |task|
|
12
|
+
task.pattern = "./spec/integration{,/*/**}/*_spec.rb"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
namespace :style do
|
17
|
+
require "rubocop/rake_task"
|
18
|
+
|
19
|
+
desc "Run RuboCop on the lib directory"
|
20
|
+
Rubocop::RakeTask.new(:check) do |task|
|
21
|
+
task.options = ["--auto-correct"]
|
22
|
+
end
|
23
|
+
end
|