metamorpher 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +16 -0
  5. data/.travis.yml +3 -0
  6. data/Gemfile +5 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +541 -0
  9. data/Rakefile +23 -0
  10. data/examples/refactorings/rails/where_first/app.rb +50 -0
  11. data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_mocks.rb +31 -0
  12. data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_not_called_expectations.rb +14 -0
  13. data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_strict_mocks.rb +27 -0
  14. data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_to_find_by.rb +14 -0
  15. data/examples/refactorings/rails/where_first/sample_controller.rb +184 -0
  16. data/lib/metamorpher/builders/ast/builder.rb +50 -0
  17. data/lib/metamorpher/builders/ast/derivation_builder.rb +20 -0
  18. data/lib/metamorpher/builders/ast/greedy_variable_builder.rb +29 -0
  19. data/lib/metamorpher/builders/ast/literal_builder.rb +31 -0
  20. data/lib/metamorpher/builders/ast/variable_builder.rb +29 -0
  21. data/lib/metamorpher/builders/ast.rb +11 -0
  22. data/lib/metamorpher/builders/ruby/builder.rb +38 -0
  23. data/lib/metamorpher/builders/ruby/deriving_visitor.rb +13 -0
  24. data/lib/metamorpher/builders/ruby/ensuring_visitor.rb +13 -0
  25. data/lib/metamorpher/builders/ruby/term.rb +35 -0
  26. data/lib/metamorpher/builders/ruby/uppercase_constant_rewriter.rb +31 -0
  27. data/lib/metamorpher/builders/ruby/uppercase_rewriter.rb +28 -0
  28. data/lib/metamorpher/builders/ruby/variable_replacement_visitor.rb +32 -0
  29. data/lib/metamorpher/builders/ruby.rb +11 -0
  30. data/lib/metamorpher/drivers/parse_error.rb +5 -0
  31. data/lib/metamorpher/drivers/ruby.rb +78 -0
  32. data/lib/metamorpher/matcher/match.rb +26 -0
  33. data/lib/metamorpher/matcher/matching.rb +61 -0
  34. data/lib/metamorpher/matcher/no_match.rb +18 -0
  35. data/lib/metamorpher/matcher.rb +6 -0
  36. data/lib/metamorpher/refactorer/merger.rb +18 -0
  37. data/lib/metamorpher/refactorer/site.rb +29 -0
  38. data/lib/metamorpher/refactorer.rb +48 -0
  39. data/lib/metamorpher/rewriter/replacement.rb +18 -0
  40. data/lib/metamorpher/rewriter/rule.rb +38 -0
  41. data/lib/metamorpher/rewriter/substitution.rb +45 -0
  42. data/lib/metamorpher/rewriter/traverser.rb +26 -0
  43. data/lib/metamorpher/rewriter.rb +12 -0
  44. data/lib/metamorpher/support/map_at.rb +8 -0
  45. data/lib/metamorpher/terms/derived.rb +13 -0
  46. data/lib/metamorpher/terms/literal.rb +47 -0
  47. data/lib/metamorpher/terms/term.rb +40 -0
  48. data/lib/metamorpher/terms/variable.rb +17 -0
  49. data/lib/metamorpher/version.rb +3 -0
  50. data/lib/metamorpher/visitable/visitable.rb +7 -0
  51. data/lib/metamorpher/visitable/visitor.rb +21 -0
  52. data/lib/metamorpher.rb +30 -0
  53. data/metamorpher.gemspec +30 -0
  54. data/spec/integration/ast/builder_spec.rb +13 -0
  55. data/spec/integration/ast/matcher_spec.rb +132 -0
  56. data/spec/integration/ast/rewriter_spec.rb +138 -0
  57. data/spec/integration/ruby/builder_spec.rb +125 -0
  58. data/spec/integration/ruby/refactorer_spec.rb +192 -0
  59. data/spec/spec_helper.rb +29 -0
  60. data/spec/support/helpers/silence_stream.rb +10 -0
  61. data/spec/support/matchers/have_matched_matcher.rb +22 -0
  62. data/spec/support/matchers/have_substitution_matcher.rb +15 -0
  63. data/spec/support/shared_examples/shared_examples_for_derivation_builders.rb +53 -0
  64. data/spec/support/shared_examples/shared_examples_for_greedy_variable_builders.rb +49 -0
  65. data/spec/support/shared_examples/shared_examples_for_literal_builders.rb +93 -0
  66. data/spec/support/shared_examples/shared_examples_for_variable_builders.rb +49 -0
  67. data/spec/unit/builders/ast/derivation_builder_spec.rb +5 -0
  68. data/spec/unit/builders/ast/greedy_variable_builder_spec.rb +9 -0
  69. data/spec/unit/builders/ast/literal_builder_spec.rb +9 -0
  70. data/spec/unit/builders/ast/variable_builder_spec.rb +9 -0
  71. data/spec/unit/builders/ruby/variable_replacement_visitor_spec.rb +48 -0
  72. data/spec/unit/drivers/ruby_spec.rb +91 -0
  73. data/spec/unit/matcher/matching_spec.rb +230 -0
  74. data/spec/unit/metamorpher_spec.rb +22 -0
  75. data/spec/unit/refactorer/merger_spec.rb +84 -0
  76. data/spec/unit/refactorer/site_spec.rb +52 -0
  77. data/spec/unit/rewriter/replacement_spec.rb +73 -0
  78. data/spec/unit/rewriter/substitution_spec.rb +97 -0
  79. data/spec/unit/rewriter/traverser_spec.rb +51 -0
  80. data/spec/unit/support/map_at_spec.rb +18 -0
  81. data/spec/unit/terms/literal_spec.rb +60 -0
  82. data/spec/unit/terms/term_spec.rb +59 -0
  83. data/spec/unit/visitable/visitor_spec.rb +35 -0
  84. 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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format progress
3
+ --require spec_helper
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
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ ruby "2.1.1"
2
+ source "https://rubygems.org"
3
+
4
+ # Specify your gem's dependencies in metamorpher.gemspec
5
+ gemspec
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