metamorpher 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|