metamorpher 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +80 -37
- data/RELEASES.md +14 -0
- data/lib/metamorpher.rb +1 -0
- data/lib/metamorpher/builders/ruby/builder.rb +4 -0
- data/lib/metamorpher/builders/ruby/uppercase_constant_rewriter.rb +3 -17
- data/lib/metamorpher/mutator.rb +15 -0
- data/lib/metamorpher/refactorer.rb +10 -36
- data/lib/metamorpher/transformer/base.rb +51 -0
- data/lib/metamorpher/{refactorer → transformer}/merger.rb +1 -1
- data/lib/metamorpher/{refactorer → transformer}/site.rb +1 -1
- data/lib/metamorpher/version.rb +1 -1
- data/spec/integration/ruby/mutator_spec.rb +215 -0
- data/spec/integration/ruby/refactorer_spec.rb +1 -1
- data/spec/unit/{refactorer → transformer}/merger_spec.rb +3 -3
- data/spec/unit/{refactorer → transformer}/site_spec.rb +2 -2
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07d706738851b486c6fc3a58140b5046a1569a6e
|
4
|
+
data.tar.gz: 77e1d643b5816016c0d054954af1d9f7f9e6bbae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a15ddbe30e40c36ec9d4ea427d6e5558650b00d6aabb907d2954cd5327abe5effc232355fc249fe0faa59191fe9546dadb2846ae4ef7e7f56796b52446ed3fe
|
7
|
+
data.tar.gz: 063d0829bb4dbc7de52d3a50b78599d752d395a94f0b3e5cb9dd9067dfb3f8eae389ec6d3380db1a6b865f9b13f2fb9fc75c0bf1fa7606c427de4cfb4ed3d115
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -36,7 +36,7 @@ This simple example is short, but terse! To fully understand it, you might now w
|
|
36
36
|
|
37
37
|
* [Practicalities](#practicalities) - Information on how to use metamorpher to manipulate (Ruby) programs:
|
38
38
|
* [Building Ruby terms](#building-ruby-terms) - how to concisely create terms that represent Ruby programs.
|
39
|
-
* [
|
39
|
+
* [Transformers](#transformers) - how to use rewriters to transform Ruby programs.
|
40
40
|
|
41
41
|
|
42
42
|
## Fundamentals
|
@@ -54,7 +54,7 @@ The primary data structure used for [rewriting](#rewriters) and for [matching](#
|
|
54
54
|
* Greedy variable - a variable that is bound to a set of subterms during [matching](#matchers).
|
55
55
|
* Derivation - a placeholder node, which is replaced during [rewriting](#rewriters).
|
56
56
|
|
57
|
-
To simplify the construction of terms, metamorpher provides the `Metamorpher::Builders::AST::Builder` class, which is demonstrated below.
|
57
|
+
To simplify the construction of terms, metamorpher provides the `Metamorpher::Builders::AST::Builder` class, which is demonstrated below.
|
58
58
|
|
59
59
|
```ruby
|
60
60
|
require "metamorpher"
|
@@ -71,7 +71,7 @@ builder.derivation! :singular do |singular, builder|
|
|
71
71
|
builder.literal!(singular.name + "s")
|
72
72
|
end
|
73
73
|
# [SINGULAR] -> ...
|
74
|
-
|
74
|
+
|
75
75
|
builder.derivation! :key, :value do |key, value, builder|
|
76
76
|
builder.pair(key, value)
|
77
77
|
end
|
@@ -91,7 +91,7 @@ The builder provides a method missing shorthand for constructing literals, varia
|
|
91
91
|
|
92
92
|
```ruby
|
93
93
|
builder.succ # => succ
|
94
|
-
builder.N # => N
|
94
|
+
builder.N # => N
|
95
95
|
builder.N_ # => N+
|
96
96
|
```
|
97
97
|
|
@@ -137,7 +137,7 @@ require "metamorpher"
|
|
137
137
|
class SuccZeroMatcher
|
138
138
|
include Metamorpher::Matcher
|
139
139
|
include Metamorpher::Builders::AST
|
140
|
-
|
140
|
+
|
141
141
|
def pattern
|
142
142
|
builder.succ(0)
|
143
143
|
end
|
@@ -145,7 +145,7 @@ end
|
|
145
145
|
|
146
146
|
expression = Metamorpher.builder.succ(0) # => succ(0)
|
147
147
|
result = SuccZeroMatcher.new.run(expression)
|
148
|
-
# => <Metamorpher::Matcher::Match root=succ(0), substitution={}>
|
148
|
+
# => <Metamorpher::Matcher::Match root=succ(0), substitution={}>
|
149
149
|
result.matches? # => true
|
150
150
|
|
151
151
|
expression = Metamorpher.builder.succ(1) # => succ(1)
|
@@ -164,7 +164,7 @@ For example, suppose we wish to match expressions of the form `succ(X)` where X
|
|
164
164
|
class SuccMatcher
|
165
165
|
include Metamorpher::Matcher
|
166
166
|
include Metamorpher::Builders::AST
|
167
|
-
|
167
|
+
|
168
168
|
def pattern
|
169
169
|
builder.succ(builder.X)
|
170
170
|
end
|
@@ -172,21 +172,21 @@ end
|
|
172
172
|
|
173
173
|
expression = Metamorpher.builder.succ(0) # => succ(0)
|
174
174
|
SuccMatcher.new.run(expression)
|
175
|
-
# => <Metamorpher::Matcher::Match root=succ(0), substitution={:x=>0}>
|
175
|
+
# => <Metamorpher::Matcher::Match root=succ(0), substitution={:x=>0}>
|
176
176
|
|
177
177
|
expression = Metamorpher.builder.succ(1) # => succ(1)
|
178
178
|
SuccMatcher.new.run(expression)
|
179
179
|
# => <Metamorpher::Matcher::Match root=succ(0), substitution={:x=>1}>
|
180
|
-
|
180
|
+
|
181
181
|
expression = Metamorpher.builder.succ(:n) # => succ(n)
|
182
182
|
SuccMatcher.new.run(expression)
|
183
183
|
# => <Metamorpher::Matcher::Match root=succ(n), substitution={:x=>n}>
|
184
184
|
|
185
185
|
expression = Metamorpher.builder.succ(Metamorpher.builder.succ(:n)) # => succ(succ(n))
|
186
186
|
SuccMatcher.new.run(expression)
|
187
|
-
# => <Metamorpher::Matcher::Match root=succ(succ(n)), substitution={:x=>succ(n)}>
|
187
|
+
# => <Metamorpher::Matcher::Match root=succ(succ(n)), substitution={:x=>succ(n)}>
|
188
188
|
```
|
189
|
-
|
189
|
+
|
190
190
|
#### Conditional variables
|
191
191
|
|
192
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.
|
@@ -197,7 +197,7 @@ For example, suppose that we wish to create a matcher that only matches method c
|
|
197
197
|
class DynamicFinderMatcher
|
198
198
|
include Metamorpher::Matcher
|
199
199
|
include Metamorpher::Builders::AST
|
200
|
-
|
200
|
+
|
201
201
|
def pattern
|
202
202
|
builder.literal!(
|
203
203
|
:".",
|
@@ -209,7 +209,7 @@ end
|
|
209
209
|
|
210
210
|
expression = Metamorpher.builder.literal!(:".", :User, :find_by_name) # => .(User, find_by_name)
|
211
211
|
DynamicFinderMatcher.new.run(expression)
|
212
|
-
# => #<Metamorpher::Matcher::Match root=.(User, find_by_name), substitution={:method=>find_by_name}>
|
212
|
+
# => #<Metamorpher::Matcher::Match root=.(User, find_by_name), substitution={:method=>find_by_name}>
|
213
213
|
|
214
214
|
expression = Metamorpher.builder.literal!(:".", :User, :find) # => .(User, find)
|
215
215
|
DynamicFinderMatcher.new.run(expression)
|
@@ -226,7 +226,7 @@ For example, suppose that we wish to create a matcher that works for an expressi
|
|
226
226
|
class MultiAddMatcher
|
227
227
|
include Metamorpher::Matcher
|
228
228
|
include Metamorpher::Builders::AST
|
229
|
-
|
229
|
+
|
230
230
|
def pattern
|
231
231
|
builder.add(
|
232
232
|
builder.ARGS_
|
@@ -235,10 +235,10 @@ class MultiAddMatcher
|
|
235
235
|
end
|
236
236
|
|
237
237
|
MultiAddMatcher.new.run(Metamorpher.builder.add(1,2))
|
238
|
-
# => #<Metamorpher::Matcher::Match root=add(1,2), substitution={:args=>[1, 2]}>
|
238
|
+
# => #<Metamorpher::Matcher::Match root=add(1,2), substitution={:args=>[1, 2]}>
|
239
239
|
|
240
240
|
MultiAddMatcher.new.run(Metamorpher.builder.add(1,2,3))
|
241
|
-
# => #<Metamorpher::Matcher::Match root=add(1,2,3), substitution={:args=>[1, 2, 3]}>
|
241
|
+
# => #<Metamorpher::Matcher::Match root=add(1,2,3), substitution={:args=>[1, 2, 3]}>
|
242
242
|
```
|
243
243
|
|
244
244
|
### Rewriters
|
@@ -253,11 +253,11 @@ require "metamorpher"
|
|
253
253
|
class SuccZeroRewriter
|
254
254
|
include Metamorpher::Rewriter
|
255
255
|
include Metamorpher::Builders::AST
|
256
|
-
|
256
|
+
|
257
257
|
def pattern
|
258
258
|
builder.literal! :succ, 0
|
259
259
|
end
|
260
|
-
|
260
|
+
|
261
261
|
def replacement
|
262
262
|
builder.literal! 1
|
263
263
|
end
|
@@ -300,7 +300,7 @@ SuccZeroRewriter.new.reduce(expression) do |matching, rewritten|
|
|
300
300
|
end
|
301
301
|
# About to replace 'succ(0)' at position [0] with '1'
|
302
302
|
# About to replace 'succ(0)' at position [1] with '1'
|
303
|
-
# =>
|
303
|
+
# =>
|
304
304
|
```
|
305
305
|
|
306
306
|
#### Derivations
|
@@ -313,11 +313,11 @@ For example, suppose that we wish to create a rewriter that pluralises any liter
|
|
313
313
|
class PluraliseRewriter
|
314
314
|
include Metamorpher::Rewriter
|
315
315
|
include Metamorpher::Builders::AST
|
316
|
-
|
316
|
+
|
317
317
|
def pattern
|
318
318
|
builder.SINGULAR
|
319
319
|
end
|
320
|
-
|
320
|
+
|
321
321
|
def replacement
|
322
322
|
builder.derivation! :singular do |singular|
|
323
323
|
builder.literal!(singular.name + "s")
|
@@ -398,11 +398,23 @@ builder
|
|
398
398
|
# [KEY, VALUE] -> ...
|
399
399
|
```
|
400
400
|
|
401
|
-
###
|
401
|
+
### Transformers
|
402
|
+
|
403
|
+
Transformers are [rewriters](#rewriters) that are specialised for rewriting program source code. A transformer parses a program's source code, rewrites the source code, and returns the unparsed, rewritten source code.
|
404
|
+
|
405
|
+
Metamorpher provides two types of transformers:
|
406
|
+
|
407
|
+
* Refactorer - produces a single transformed program that contains all rewritings together
|
408
|
+
* Mutator - produces a set of transformed programs where each program contains a single rewriting
|
409
|
+
|
410
|
+
For example, for the pattern `1 + 1`, the replacement `2` and the input program `(1 + 1) * (1 + 1)`:
|
402
411
|
|
403
|
-
|
412
|
+
* A refactorer will produce `(2) * (2)`
|
413
|
+
* A mutator will produce `[(2) * (1 + 1), (1 + 1) * (2)].`
|
404
414
|
|
405
|
-
Metamorpher provides the `Metamorpher::Refactorer`
|
415
|
+
Metamorpher provides the `Metamorpher::Refactorer` and `Metamorpher::Mutator` modules for constructing classes that perform refactorings or mutations.
|
416
|
+
|
417
|
+
To construct a refactorer, include the relevant module, specify a `pattern` and a `replacement`, and then call `refactor(src)`:
|
406
418
|
|
407
419
|
```ruby
|
408
420
|
require "metamorpher"
|
@@ -420,11 +432,42 @@ class UnnecessaryConditionalRefactorer
|
|
420
432
|
end
|
421
433
|
end
|
422
434
|
|
423
|
-
program = "a = if some_predicate then true else false end"
|
435
|
+
program = "a = if some_predicate then true else false end; " \
|
436
|
+
"b = if another_predicate then true else false end"
|
424
437
|
UnnecessaryConditionalRefactorer.new.refactor(program)
|
425
|
-
# => "a = some_predicate"
|
438
|
+
# => "a = some_predicate; b = another_predicate"
|
426
439
|
```
|
427
440
|
|
441
|
+
Similarly to construct a mutator, include the relevant module, specify a `pattern` and an array of `replacements`, and then call `mutate(src)`:
|
442
|
+
|
443
|
+
```ruby
|
444
|
+
require "metamorpher"
|
445
|
+
|
446
|
+
class LessThanMutator
|
447
|
+
include Metamorpher::Mutator
|
448
|
+
include Metamorpher::Builders::Ruby
|
449
|
+
|
450
|
+
def pattern
|
451
|
+
builder.build("A < B")
|
452
|
+
end
|
453
|
+
|
454
|
+
def replacements
|
455
|
+
builder.build_all("A > B", "A == B")
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
program = "a = foo < bar; b = bar < baz"
|
460
|
+
LessThanMutator.new.mutate(program)
|
461
|
+
# => [
|
462
|
+
# "a = foo > bar; b = bar < baz",
|
463
|
+
# "a = foo == bar; b = bar < baz",
|
464
|
+
# "a = foo < bar; b = bar > baz",
|
465
|
+
# "a = foo < bar; b = bar == baz"
|
466
|
+
# ]
|
467
|
+
```
|
468
|
+
|
469
|
+
The remainder of this section discusses only refactorers, but note that mutators have all of the same functionality as refactorers (but provides methods prefixed with `mutate` rather than `refactor`).
|
470
|
+
|
428
471
|
The `refactor` method can optionally take a block, which is called immediately before the matching code is replaced with the refactored code:
|
429
472
|
|
430
473
|
```ruby
|
@@ -445,11 +488,11 @@ The `Metamorpher::Refactorer` module also defines a `refactor_file(path)` method
|
|
445
488
|
|
446
489
|
```ruby
|
447
490
|
path = File.expand_path("refactorable.rb", "/Users/louis/code/mutiny")
|
448
|
-
# => "/Users/louis/code/mutiny/refactorable.rb"
|
491
|
+
# => "/Users/louis/code/mutiny/refactorable.rb"
|
449
492
|
|
450
493
|
UnnecessaryConditionalRefactorer.new.refactor_file(path)
|
451
494
|
# => ... (refactored code)
|
452
|
-
|
495
|
+
|
453
496
|
UnnecessaryConditionalRefactorer.new.refactor_file(path) do |refactoring|
|
454
497
|
# works just like the block passed to refactor
|
455
498
|
end
|
@@ -460,7 +503,7 @@ You might prefer the `refactor_files(paths)` method, if you'd like to refactor s
|
|
460
503
|
|
461
504
|
```ruby
|
462
505
|
paths = Dir.glob(File.expand_path(File.join("**", "*.rb"), "/Users/louis/code/mutiny"))
|
463
|
-
# => ["/Users/louis/code/mutiny/lib/mutiny.rb", ...]
|
506
|
+
# => ["/Users/louis/code/mutiny/lib/mutiny.rb", ...]
|
464
507
|
|
465
508
|
# Note that refactor_files returns a Hash rather than a String
|
466
509
|
UnnecessaryConditionalRefactorer.new.refactor_files(paths)
|
@@ -489,22 +532,22 @@ By default, `Metamorpher::Refactorer` assumes that you wish to refactor Ruby pro
|
|
489
532
|
```ruby
|
490
533
|
class JavaRefactorer
|
491
534
|
include Metamorpher::Refactorer
|
492
|
-
|
535
|
+
|
493
536
|
def driver
|
494
537
|
YourTool::MetamorpherDrivers::Java.new
|
495
538
|
end
|
496
|
-
|
539
|
+
|
497
540
|
def pattern
|
498
541
|
...
|
499
542
|
end
|
500
|
-
|
543
|
+
|
501
544
|
def replacement
|
502
545
|
...
|
503
546
|
end
|
504
547
|
end
|
505
548
|
```
|
506
549
|
|
507
|
-
#### Examples
|
550
|
+
#### Examples
|
508
551
|
|
509
552
|
Below are a few examples of using metamorpher to perform refactorings on Ruby code.
|
510
553
|
|
@@ -558,21 +601,21 @@ class RefactorWhereFirstToFindBy
|
|
558
601
|
builder.build(create_hash_string(keys, values))
|
559
602
|
end
|
560
603
|
end
|
561
|
-
|
604
|
+
|
562
605
|
private
|
563
|
-
|
606
|
+
|
564
607
|
# Extracts an array of attributes from the name of a dynamic
|
565
608
|
# finder, such as find_by_asset_id_and_object_path.
|
566
609
|
def attributes_from_dynamic_finder(dynamic_finder)
|
567
610
|
dynamic_finder["find_by_".length..-1].split("_and_")
|
568
611
|
end
|
569
|
-
|
612
|
+
|
570
613
|
# Builds a string representation of a hash from a set of keys
|
571
614
|
# and a corresponding set of values
|
572
615
|
def create_hash_string(keys, values)
|
573
616
|
"{" + create_pairs_string(keys, values) + "}"
|
574
617
|
end
|
575
|
-
|
618
|
+
|
576
619
|
def create_pairs_string(keys, values)
|
577
620
|
keys
|
578
621
|
.zip(values)
|
data/RELEASES.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Release History
|
2
|
+
|
3
|
+
## v0.2.0 (9 May 2015)
|
4
|
+
* Provide support for mutators which are similar to refactorers but produce multiple transformed programs
|
5
|
+
|
6
|
+
## v0.1.1 (8 May 2015)
|
7
|
+
* Update dependencies
|
8
|
+
|
9
|
+
## v0.1.0 (6 May 2014)
|
10
|
+
Provide support for:
|
11
|
+
* A generic mechanism for specifying terms
|
12
|
+
* Language-independent term matching and rewriting
|
13
|
+
* Language-specific refactoring
|
14
|
+
* A Ruby builder that enables program matching, rewriting and refactoring of Ruby programs.
|
data/lib/metamorpher.rb
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
require "metamorpher/builders/ast"
|
2
|
+
require "metamorpher/builders/ruby/uppercase_rewriter"
|
2
3
|
|
3
4
|
module Metamorpher
|
4
5
|
module Builders
|
5
6
|
module Ruby
|
6
|
-
class UppercaseConstantRewriter
|
7
|
+
class UppercaseConstantRewriter < UppercaseRewriter
|
7
8
|
include Metamorpher::Rewriter
|
8
9
|
include Metamorpher::Builders::AST
|
9
10
|
|
10
11
|
def pattern
|
11
|
-
builder.const(
|
12
|
-
builder.literal!(nil),
|
13
|
-
builder.VARIABLE_TO_BE { |v| v.name && v.name.to_s[/^[A-Z_]*$/] }
|
14
|
-
)
|
15
|
-
end
|
16
|
-
|
17
|
-
def replacement
|
18
|
-
builder.derivation!(:variable_to_be) do |variable_to_be, builder|
|
19
|
-
name = variable_to_be.name.to_s
|
20
|
-
|
21
|
-
if name.end_with?("_")
|
22
|
-
builder.greedy_variable! name.chomp("_").downcase.to_sym
|
23
|
-
else
|
24
|
-
builder.variable! name.downcase.to_sym
|
25
|
-
end
|
26
|
-
end
|
12
|
+
builder.const(builder.literal!(nil), super)
|
27
13
|
end
|
28
14
|
end
|
29
15
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "metamorpher/transformer/base"
|
2
|
+
require "metamorpher/transformer/merger"
|
3
|
+
|
4
|
+
module Metamorpher
|
5
|
+
module Mutator
|
6
|
+
include Transformer::Base
|
7
|
+
alias_method :mutate, :transform
|
8
|
+
alias_method :mutate_file, :transform_file
|
9
|
+
alias_method :mutate_files, :transform_files
|
10
|
+
|
11
|
+
def merge(src, replacements, &block)
|
12
|
+
replacements.map { |replacement| Transformer::Merger.new(src).merge(replacement, &block) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -1,47 +1,21 @@
|
|
1
|
-
require "metamorpher/
|
2
|
-
require "metamorpher/
|
3
|
-
require "metamorpher/rewriter/rule"
|
4
|
-
require "metamorpher/drivers/ruby"
|
1
|
+
require "metamorpher/transformer/base"
|
2
|
+
require "metamorpher/transformer/merger"
|
5
3
|
|
6
4
|
module Metamorpher
|
7
5
|
module Refactorer
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
13
|
-
|
14
|
-
def refactor_file(path, &block)
|
15
|
-
refactor(File.read(path), &block)
|
16
|
-
end
|
6
|
+
include Transformer::Base
|
7
|
+
alias_method :refactor, :transform
|
8
|
+
alias_method :refactor_file, :transform_file
|
9
|
+
alias_method :refactor_files, :transform_files
|
17
10
|
|
18
|
-
def
|
19
|
-
|
20
|
-
changes = []
|
21
|
-
result[path] = refactor_file(path) { |change| changes << change }
|
22
|
-
block.call(path, result[path], changes) if block
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def driver
|
27
|
-
@driver ||= Metamorpher::Drivers::Ruby.new
|
11
|
+
def merge(src, replacements, &block)
|
12
|
+
Transformer::Merger.new(src).merge(*replacements, &block)
|
28
13
|
end
|
29
14
|
|
30
15
|
private
|
31
16
|
|
32
|
-
def
|
33
|
-
[]
|
34
|
-
rule.reduce(literal) do |original, rewritten|
|
35
|
-
original_position = driver.source_location_for(original)
|
36
|
-
original_code = src[original_position]
|
37
|
-
refactored_code = driver.unparse(rewritten)
|
38
|
-
replacements << Site.new(original_position, original_code, refactored_code)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def rule
|
44
|
-
@rule ||= Rewriter::Rule.new(pattern: pattern, replacement: replacement)
|
17
|
+
def replacements
|
18
|
+
@replacements ||= [replacement]
|
45
19
|
end
|
46
20
|
end
|
47
21
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "metamorpher/transformer/merger"
|
2
|
+
require "metamorpher/transformer/site"
|
3
|
+
require "metamorpher/rewriter/rule"
|
4
|
+
require "metamorpher/drivers/ruby"
|
5
|
+
|
6
|
+
module Metamorpher
|
7
|
+
module Transformer
|
8
|
+
module Base
|
9
|
+
def transform(src, &block)
|
10
|
+
literal = driver.parse(src)
|
11
|
+
replacements = reduce_to_replacements(src, literal)
|
12
|
+
merge(src, replacements, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def transform_file(path, &block)
|
16
|
+
transform(File.read(path), &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def transform_files(paths, &block)
|
20
|
+
paths.each_with_object({}) do |path, result|
|
21
|
+
changes = []
|
22
|
+
result[path] = transform_file(path) { |change| changes << change }
|
23
|
+
block.call(path, result[path], changes) if block
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def driver
|
28
|
+
@driver ||= Metamorpher::Drivers::Ruby.new
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def reduce_to_replacements(src, literal)
|
34
|
+
[].tap do |replacements|
|
35
|
+
rules.each do |rule| # FIXME : change to inject?
|
36
|
+
rule.reduce(literal) do |original, rewritten|
|
37
|
+
original_position = driver.source_location_for(original)
|
38
|
+
original_code = src[original_position]
|
39
|
+
transformed_code = driver.unparse(rewritten)
|
40
|
+
replacements << Site.new(original_position, original_code, transformed_code)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def rules
|
47
|
+
@rules ||= replacements.map { |r| Rewriter::Rule.new(pattern: pattern, replacement: r) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require "attributable"
|
2
2
|
|
3
3
|
module Metamorpher
|
4
|
-
module
|
4
|
+
module Transformer
|
5
5
|
Site = Struct.new(:original_position, :original_code, :refactored_code) do
|
6
6
|
def slide(offset)
|
7
7
|
new_position = (original_position.begin + offset)..(original_position.end + offset)
|
data/lib/metamorpher/version.rb
CHANGED
@@ -0,0 +1,215 @@
|
|
1
|
+
require "metamorpher"
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
describe "Mutator" do
|
5
|
+
describe "for Ruby" do
|
6
|
+
class LessThanMutator
|
7
|
+
include Metamorpher::Mutator
|
8
|
+
include Metamorpher::Builders::Ruby
|
9
|
+
|
10
|
+
def pattern
|
11
|
+
builder.build("A < B")
|
12
|
+
end
|
13
|
+
|
14
|
+
def replacements
|
15
|
+
builder.build_all("A > B", "A == B")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
subject { LessThanMutator.new }
|
20
|
+
|
21
|
+
let(:mutatable) do
|
22
|
+
"def compare\n" \
|
23
|
+
" foo < bar\n" \
|
24
|
+
" bar < baz\n" \
|
25
|
+
"end"
|
26
|
+
end
|
27
|
+
|
28
|
+
let(:mutated) do
|
29
|
+
[
|
30
|
+
"def compare\n" \
|
31
|
+
" foo > bar\n" \
|
32
|
+
" bar < baz\n" \
|
33
|
+
"end",
|
34
|
+
|
35
|
+
"def compare\n" \
|
36
|
+
" foo < bar\n" \
|
37
|
+
" bar > baz\n" \
|
38
|
+
"end",
|
39
|
+
|
40
|
+
"def compare\n" \
|
41
|
+
" foo == bar\n" \
|
42
|
+
" bar < baz\n" \
|
43
|
+
"end",
|
44
|
+
|
45
|
+
"def compare\n" \
|
46
|
+
" foo < bar\n" \
|
47
|
+
" bar == baz\n" \
|
48
|
+
"end"
|
49
|
+
]
|
50
|
+
end
|
51
|
+
|
52
|
+
let(:not_mutatable) { "foo == bar" }
|
53
|
+
|
54
|
+
describe "by calling mutate" do
|
55
|
+
describe "for code that can be mutated"do
|
56
|
+
it "should return the mutated code" do
|
57
|
+
expect(subject.mutate(mutatable)).to eq(mutated)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should yield for each mutation site" do
|
61
|
+
expect { |b| subject.mutate(mutatable, &b) }.to yield_successive_args(
|
62
|
+
site_for(14..22, "foo < bar", "foo > bar"),
|
63
|
+
site_for(26..34, "bar < baz", "bar > baz"),
|
64
|
+
site_for(14..22, "foo < bar", "foo == bar"),
|
65
|
+
site_for(26..34, "bar < baz", "bar == baz")
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "for code that cannot be mutated" do
|
71
|
+
it "should return no mutants" do
|
72
|
+
expect(subject.mutate(not_mutatable)).to eq([])
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should not yield when there are no mutants" do
|
76
|
+
expect { |b| subject.mutate(not_mutatable, &b) }.not_to yield_control
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "mutate_file" do
|
82
|
+
describe "for code that can be mutated" do
|
83
|
+
let(:mutatable_file) { create_temporary_ruby_file("mutatable", mutatable) }
|
84
|
+
|
85
|
+
it "should return the mutated code" do
|
86
|
+
expect(subject.mutate_file(mutatable_file)).to eq(mutated)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should yield for each mutating site" do
|
90
|
+
expect { |b| subject.mutate_file(mutatable_file, &b) }.to yield_successive_args(
|
91
|
+
site_for(14..22, "foo < bar", "foo > bar"),
|
92
|
+
site_for(26..34, "bar < baz", "bar > baz"),
|
93
|
+
site_for(14..22, "foo < bar", "foo == bar"),
|
94
|
+
site_for(26..34, "bar < baz", "bar == baz")
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "for code that cannot be mutated" do
|
100
|
+
let(:not_mutatable_file) do
|
101
|
+
create_temporary_ruby_file("not_mutatable", not_mutatable)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should return no mutants" do
|
105
|
+
expect(subject.mutate_file(not_mutatable_file)).to eq([])
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should not yield when there are no mutating site" do
|
109
|
+
expect { |b| subject.mutate_file(not_mutatable_file, &b) }.not_to yield_control
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "mutate_files" do
|
115
|
+
let(:mutatable_file) { create_temporary_ruby_file("mutatable", mutatable) }
|
116
|
+
let(:clone_of_mutatable_file) { create_temporary_ruby_file("mutatable", mutatable) }
|
117
|
+
|
118
|
+
let(:different_mutating_sites_file) do
|
119
|
+
create_temporary_ruby_file(
|
120
|
+
"differently_mutatable",
|
121
|
+
"result = foo < bar"
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
let(:not_mutatable_file) do
|
126
|
+
create_temporary_ruby_file(
|
127
|
+
"not_mutatable",
|
128
|
+
"nothing_to_see_here = 42"
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
let(:files) do
|
133
|
+
[
|
134
|
+
mutatable_file,
|
135
|
+
clone_of_mutatable_file,
|
136
|
+
different_mutating_sites_file,
|
137
|
+
not_mutatable_file
|
138
|
+
]
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should return a map of the paths and mutated code" do
|
142
|
+
mutated_files = {
|
143
|
+
mutatable_file => mutated,
|
144
|
+
clone_of_mutatable_file => mutated,
|
145
|
+
different_mutating_sites_file => ["result = foo > bar", "result = foo == bar"],
|
146
|
+
not_mutatable_file => []
|
147
|
+
}
|
148
|
+
|
149
|
+
expect(subject.mutate_files(files)).to eq(mutated_files)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should yield for each file" do
|
153
|
+
mutatable_file_details = [
|
154
|
+
mutatable_file,
|
155
|
+
mutated,
|
156
|
+
[
|
157
|
+
site_for(14..22, "foo < bar", "foo > bar"),
|
158
|
+
site_for(26..34, "bar < baz", "bar > baz"),
|
159
|
+
site_for(14..22, "foo < bar", "foo == bar"),
|
160
|
+
site_for(26..34, "bar < baz", "bar == baz")
|
161
|
+
]
|
162
|
+
]
|
163
|
+
|
164
|
+
clone_of_mutatable_file_details = [
|
165
|
+
clone_of_mutatable_file,
|
166
|
+
mutated,
|
167
|
+
[
|
168
|
+
site_for(14..22, "foo < bar", "foo > bar"),
|
169
|
+
site_for(26..34, "bar < baz", "bar > baz"),
|
170
|
+
site_for(14..22, "foo < bar", "foo == bar"),
|
171
|
+
site_for(26..34, "bar < baz", "bar == baz")
|
172
|
+
]
|
173
|
+
]
|
174
|
+
|
175
|
+
different_mutating_sites_file_details = [
|
176
|
+
different_mutating_sites_file,
|
177
|
+
["result = foo > bar", "result = foo == bar"],
|
178
|
+
[
|
179
|
+
site_for(9..17, "foo < bar", "foo > bar"),
|
180
|
+
site_for(9..17, "foo < bar", "foo == bar")
|
181
|
+
]
|
182
|
+
]
|
183
|
+
|
184
|
+
not_mutatable_file_details = [
|
185
|
+
not_mutatable_file,
|
186
|
+
[],
|
187
|
+
[]
|
188
|
+
]
|
189
|
+
|
190
|
+
summary = []
|
191
|
+
subject.mutate_files(files) { |*args| summary << args }
|
192
|
+
|
193
|
+
expect(summary[0]).to eq(mutatable_file_details)
|
194
|
+
expect(summary[1]).to eq(clone_of_mutatable_file_details)
|
195
|
+
expect(summary[2]).to eq(different_mutating_sites_file_details)
|
196
|
+
expect(summary[3]).to eq(not_mutatable_file_details)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def site_for(original_position, original_code, mutated_code)
|
201
|
+
Metamorpher::Transformer::Site.new(
|
202
|
+
original_position,
|
203
|
+
original_code,
|
204
|
+
mutated_code
|
205
|
+
)
|
206
|
+
end
|
207
|
+
|
208
|
+
def create_temporary_ruby_file(filename, contents)
|
209
|
+
Tempfile.new([filename, ".rb"]).tap do |tempfile|
|
210
|
+
tempfile.write(contents)
|
211
|
+
tempfile.close
|
212
|
+
end.path
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
|
-
require "metamorpher/
|
2
|
-
require "metamorpher/
|
1
|
+
require "metamorpher/transformer/merger"
|
2
|
+
require "metamorpher/transformer/site"
|
3
3
|
|
4
4
|
module Metamorpher
|
5
|
-
module
|
5
|
+
module Transformer
|
6
6
|
describe Merger do
|
7
7
|
let(:original) { "The quick brown fox jumps over the lazy dog." }
|
8
8
|
subject { Merger.new(original) }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: metamorpher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Louis Rose
|
@@ -138,6 +138,7 @@ files:
|
|
138
138
|
- Gemfile
|
139
139
|
- LICENSE.txt
|
140
140
|
- README.md
|
141
|
+
- RELEASES.md
|
141
142
|
- Rakefile
|
142
143
|
- examples/refactorings/rails/where_first/app.rb
|
143
144
|
- examples/refactorings/rails/where_first/refactorers/refactor_where_first_mocks.rb
|
@@ -166,9 +167,8 @@ files:
|
|
166
167
|
- lib/metamorpher/matcher/match.rb
|
167
168
|
- lib/metamorpher/matcher/matching.rb
|
168
169
|
- lib/metamorpher/matcher/no_match.rb
|
170
|
+
- lib/metamorpher/mutator.rb
|
169
171
|
- lib/metamorpher/refactorer.rb
|
170
|
-
- lib/metamorpher/refactorer/merger.rb
|
171
|
-
- lib/metamorpher/refactorer/site.rb
|
172
172
|
- lib/metamorpher/rewriter.rb
|
173
173
|
- lib/metamorpher/rewriter/replacement.rb
|
174
174
|
- lib/metamorpher/rewriter/rule.rb
|
@@ -179,6 +179,9 @@ files:
|
|
179
179
|
- lib/metamorpher/terms/literal.rb
|
180
180
|
- lib/metamorpher/terms/term.rb
|
181
181
|
- lib/metamorpher/terms/variable.rb
|
182
|
+
- lib/metamorpher/transformer/base.rb
|
183
|
+
- lib/metamorpher/transformer/merger.rb
|
184
|
+
- lib/metamorpher/transformer/site.rb
|
182
185
|
- lib/metamorpher/version.rb
|
183
186
|
- lib/metamorpher/visitable/visitable.rb
|
184
187
|
- lib/metamorpher/visitable/visitor.rb
|
@@ -187,6 +190,7 @@ files:
|
|
187
190
|
- spec/integration/ast/matcher_spec.rb
|
188
191
|
- spec/integration/ast/rewriter_spec.rb
|
189
192
|
- spec/integration/ruby/builder_spec.rb
|
193
|
+
- spec/integration/ruby/mutator_spec.rb
|
190
194
|
- spec/integration/ruby/refactorer_spec.rb
|
191
195
|
- spec/spec_helper.rb
|
192
196
|
- spec/support/helpers/silence_stream.rb
|
@@ -204,14 +208,14 @@ files:
|
|
204
208
|
- spec/unit/drivers/ruby_spec.rb
|
205
209
|
- spec/unit/matcher/matching_spec.rb
|
206
210
|
- spec/unit/metamorpher_spec.rb
|
207
|
-
- spec/unit/refactorer/merger_spec.rb
|
208
|
-
- spec/unit/refactorer/site_spec.rb
|
209
211
|
- spec/unit/rewriter/replacement_spec.rb
|
210
212
|
- spec/unit/rewriter/substitution_spec.rb
|
211
213
|
- spec/unit/rewriter/traverser_spec.rb
|
212
214
|
- spec/unit/support/map_at_spec.rb
|
213
215
|
- spec/unit/terms/literal_spec.rb
|
214
216
|
- spec/unit/terms/term_spec.rb
|
217
|
+
- spec/unit/transformer/merger_spec.rb
|
218
|
+
- spec/unit/transformer/site_spec.rb
|
215
219
|
- spec/unit/visitable/visitor_spec.rb
|
216
220
|
homepage: https://github.com/mutiny/metamorpher
|
217
221
|
licenses:
|
@@ -242,6 +246,7 @@ test_files:
|
|
242
246
|
- spec/integration/ast/matcher_spec.rb
|
243
247
|
- spec/integration/ast/rewriter_spec.rb
|
244
248
|
- spec/integration/ruby/builder_spec.rb
|
249
|
+
- spec/integration/ruby/mutator_spec.rb
|
245
250
|
- spec/integration/ruby/refactorer_spec.rb
|
246
251
|
- spec/spec_helper.rb
|
247
252
|
- spec/support/helpers/silence_stream.rb
|
@@ -259,12 +264,12 @@ test_files:
|
|
259
264
|
- spec/unit/drivers/ruby_spec.rb
|
260
265
|
- spec/unit/matcher/matching_spec.rb
|
261
266
|
- spec/unit/metamorpher_spec.rb
|
262
|
-
- spec/unit/refactorer/merger_spec.rb
|
263
|
-
- spec/unit/refactorer/site_spec.rb
|
264
267
|
- spec/unit/rewriter/replacement_spec.rb
|
265
268
|
- spec/unit/rewriter/substitution_spec.rb
|
266
269
|
- spec/unit/rewriter/traverser_spec.rb
|
267
270
|
- spec/unit/support/map_at_spec.rb
|
268
271
|
- spec/unit/terms/literal_spec.rb
|
269
272
|
- spec/unit/terms/term_spec.rb
|
273
|
+
- spec/unit/transformer/merger_spec.rb
|
274
|
+
- spec/unit/transformer/site_spec.rb
|
270
275
|
- spec/unit/visitable/visitor_spec.rb
|