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
@@ -0,0 +1,84 @@
1
+ require "metamorpher/refactorer/merger"
2
+ require "metamorpher/refactorer/site"
3
+
4
+ module Metamorpher
5
+ module Refactorer
6
+ describe Merger do
7
+ let(:original) { "The quick brown fox jumps over the lazy dog." }
8
+ subject { Merger.new(original) }
9
+
10
+ describe "for a single replacement" do
11
+ it "should be able to rewrite at the start of the string" do
12
+ merged = merge(Site.new(0..2, "The", "A"))
13
+
14
+ expect(merged).to eq("A quick brown fox jumps over the lazy dog.")
15
+ end
16
+
17
+ it "should be able to rewrite in the middle of the string" do
18
+ merged = merge(Site.new(4..8, "quick", "swift"))
19
+
20
+ expect(merged).to eq("The swift brown fox jumps over the lazy dog.")
21
+ end
22
+
23
+ it "should be able to rewrite at the end of the string" do
24
+ merged = merge(Site.new(43..43, ".", "!"))
25
+
26
+ expect(merged).to eq("The quick brown fox jumps over the lazy dog!")
27
+ end
28
+
29
+ it "should not alter the original string" do
30
+ merge(Site.new(0..2, "The", "A"))
31
+
32
+ expect(original).to eq("The quick brown fox jumps over the lazy dog.")
33
+ end
34
+
35
+ it "should yield before performing the replacement" do
36
+ replacement = Site.new(0..2, "The", "A")
37
+
38
+ expect { |b| merge(replacement, &b) }.to yield_with_args(replacement)
39
+ end
40
+ end
41
+
42
+ describe "for multiple replacements" do
43
+ it "should merge all replacements" do
44
+ merged = merge(
45
+ Site.new(4..8, "quick", "swift"),
46
+ Site.new(20..24, "jumps", "walks"),
47
+ Site.new(40..42, "dog", "cat")
48
+ )
49
+
50
+ expect(merged).to eq("The swift brown fox walks over the lazy cat.")
51
+ end
52
+
53
+ it "should determine position of all replacements based on the original string" do
54
+ merged = merge(
55
+ Site.new(4..8, "quick", "fast"),
56
+ Site.new(20..24, "jumps", "springs"),
57
+ Site.new(40..42, "dog", "cat")
58
+ )
59
+
60
+ # note that "fast" is 1 char shorter than its replacee "quick"
61
+ # and hence the second substring has to be repositioned by -1 char
62
+ # and that "springs" is 2 chars longer than its replacee "jumps"
63
+ # and hence the third substring has to be repositioned by +1 char (as -1 + +2 = +1)
64
+
65
+ expect(merged).to eq("The fast brown fox springs over the lazy cat.")
66
+ end
67
+
68
+ it "should yield before performing each replacement" do
69
+ replacements = [
70
+ Site.new(4..8, "quick", "fast"),
71
+ Site.new(20..24, "jumps", "springs"),
72
+ Site.new(40..42, "dog", "cat")
73
+ ]
74
+
75
+ expect { |b| merge(*replacements, &b) }.to yield_successive_args(*replacements)
76
+ end
77
+ end
78
+
79
+ def merge(*replacements, &block)
80
+ subject.merge(*replacements, &block)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,52 @@
1
+ require "metamorpher/refactorer/site"
2
+
3
+ module Metamorpher
4
+ module Refactorer
5
+ describe Site do
6
+ subject { Site.new(4..6, "foo", "bar") }
7
+
8
+ describe "slide" do
9
+ it "should return a replacement with the new position" do
10
+ expect(subject.slide(2).original_position).to eq(6..8)
11
+ end
12
+
13
+ it "should not alter the code" do
14
+ expect(subject.slide(2).original_code).to eq("foo")
15
+ expect(subject.slide(2).refactored_code).to eq("bar")
16
+ end
17
+
18
+ it "should be chainable" do
19
+ expect(subject.slide(2).slide(10).original_position).to eq(16..18)
20
+ end
21
+ end
22
+
23
+ describe "merge_into" do
24
+ it "should apply change to argument" do
25
+ expect(subject.merge_into("foo foo")).to eq("foo bar")
26
+ end
27
+
28
+ it "should raise error when mergee is shorter than start of position" do
29
+ expect { subject.merge_into("foo") }.to raise_error(ArgumentError)
30
+ end
31
+
32
+ it "should not raise error when mergee is same length as start of position" do
33
+ expect { subject.merge_into("foo ") }.to_not raise_error
34
+ end
35
+ end
36
+
37
+ describe "offset" do
38
+ it "should be 0 when position and value are the same size" do
39
+ expect(subject.offset).to eq(0)
40
+ end
41
+
42
+ it "should be -ve when position's size is larger than value's size" do
43
+ expect(Site.new(4..6, "foo", "b").offset).to eq(-2)
44
+ end
45
+
46
+ it "should be +ve when position's size is smaller than value's size" do
47
+ expect(Site.new(4..6, "foo", "baaz").offset).to eq(1)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,73 @@
1
+ require "metamorpher/terms/variable"
2
+ require "metamorpher/terms/derived"
3
+ require "metamorpher/terms/literal"
4
+
5
+ module Metamorpher
6
+ module Terms
7
+ describe "replace" do
8
+ describe "with no children" do
9
+ subject { Literal.new(name: :top) }
10
+
11
+ it "should be possible to replace at the top" do
12
+ replacement = Literal.new(name: :root)
13
+
14
+ expect(subject.replace(subject.path, replacement)).to eq(replacement)
15
+ end
16
+ end
17
+
18
+ describe "with children" do
19
+ subject do
20
+ Literal.new(
21
+ name: :root,
22
+ children: [
23
+ Literal.new(name: :first_child),
24
+ Variable.new(name: :second_child),
25
+ Derived.new(name: :third_child)
26
+ ]
27
+ )
28
+ end
29
+
30
+ let(:replacement) { Literal.new(name: :root) }
31
+
32
+ it "should be possible to replace literal child" do
33
+ expect(subject.replace(subject.children[0].path, replacement)).to eq(
34
+ Literal.new(
35
+ name: :root,
36
+ children: [
37
+ replacement,
38
+ Variable.new(name: :second_child),
39
+ Derived.new(name: :third_child)
40
+ ]
41
+ )
42
+ )
43
+ end
44
+
45
+ it "should be possible to replace literal child" do
46
+ expect(subject.replace(subject.children[1].path, replacement)).to eq(
47
+ Literal.new(
48
+ name: :root,
49
+ children: [
50
+ Literal.new(name: :first_child),
51
+ replacement,
52
+ Derived.new(name: :third_child)
53
+ ]
54
+ )
55
+ )
56
+ end
57
+
58
+ it "should be possible to replace derived child" do
59
+ expect(subject.replace(subject.children[2].path, replacement)).to eq(
60
+ Literal.new(
61
+ name: :root,
62
+ children: [
63
+ Literal.new(name: :first_child),
64
+ Variable.new(name: :second_child),
65
+ replacement
66
+ ]
67
+ )
68
+ )
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,97 @@
1
+ require "metamorpher/terms/variable"
2
+ require "metamorpher/terms/derived"
3
+ require "metamorpher/terms/literal"
4
+
5
+ module Metamorpher
6
+ module Terms
7
+ describe Variable do
8
+ subject { Variable.new(name: :type) }
9
+
10
+ it "should return the element of the substitution with the correct name" do
11
+ substitution = { type: Literal.new(name: :sub) }
12
+ expect(subject.substitute(substitution)).to eq(substitution[:type])
13
+ end
14
+
15
+ it "should raise if the substitution contains no value for variable's name" do
16
+ expect { subject.substitute({}) }.to raise_error(Rewriter::SubstitutionError)
17
+ end
18
+ end
19
+
20
+ describe Derived do
21
+ subject do
22
+ Derived.new(
23
+ base: [:type],
24
+ derivation: -> (type) { Literal.new(name: type.name.reverse) }
25
+ )
26
+ end
27
+
28
+ it "should return the element of the substitution after calling derivation" do
29
+ substitution = { type: Literal.new(name: "reverse_me") }
30
+
31
+ expect(subject.substitute(substitution)).to eq(
32
+ Literal.new(name: "reverse_me".reverse)
33
+ )
34
+ end
35
+
36
+ it "should raise if the substitution contains no value for variable's name" do
37
+ expect { subject.substitute({}) }.to raise_error(Rewriter::SubstitutionError)
38
+ end
39
+ end
40
+
41
+ describe Literal do
42
+ describe "with no children" do
43
+ subject { Literal.new(name: :root) }
44
+
45
+ it "should return the original literal" do
46
+ expect(subject.substitute({})).to eq(subject)
47
+ end
48
+ end
49
+
50
+ describe "with children" do
51
+ subject do
52
+ Literal.new(
53
+ name: :root,
54
+ children: [
55
+ Literal.new(name: :child, children: [Variable.new(name: :foo)])
56
+ ]
57
+ )
58
+ end
59
+
60
+ let(:child) { literal.children.first }
61
+ let(:grandchild) { child.children.first }
62
+
63
+ it "should return the original literal with substituted descendants" do
64
+ substitution = { foo: Literal.new(name: :bar) }
65
+
66
+ expect(subject.substitute(substitution)).to eq(
67
+ Literal.new(
68
+ name: :root,
69
+ children: [
70
+ Literal.new(
71
+ name: :child,
72
+ children: [Literal.new(name: :bar)]
73
+ )
74
+ ]
75
+ )
76
+ )
77
+ end
78
+
79
+ it "should contain all elements if the substituted value is an array" do
80
+ substitution = { foo: [Literal.new(name: :bar), Literal.new(name: :baz)] }
81
+
82
+ expect(subject.substitute(substitution)).to eq(
83
+ Literal.new(
84
+ name: :root,
85
+ children: [
86
+ Literal.new(
87
+ name: :child,
88
+ children: [Literal.new(name: :bar), Literal.new(name: :baz)]
89
+ )
90
+ ]
91
+ )
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,51 @@
1
+ require "metamorpher/rewriter/traverser"
2
+
3
+ module Metamorpher
4
+ module Rewriter
5
+ describe Traverser do
6
+ describe "traversing a flat tree" do
7
+ let(:tree) { t(1, 2, 3, 4) }
8
+
9
+ it "correctly reports number of nodes" do
10
+ expect(subject.traverse(tree).size).to eq(5)
11
+ end
12
+
13
+ it "returns nodes in left-to-right order" do
14
+ expect(subject.traverse(tree).take(5)).to eq([tree, 1, 2, 3, 4])
15
+ end
16
+ end
17
+
18
+ describe "traversing a skinny tree" do
19
+ let(:tree) { t(1, t(2, t(3))) }
20
+
21
+ it "correctly reports number of nodes" do
22
+ expect(subject.traverse(tree).size).to eq(6)
23
+ end
24
+
25
+ it "returns nodes in outermost (root-to-leaves) order" do
26
+ expect(subject.traverse(tree).take(6)).to eq(
27
+ [tree, 1, tree.children.last, 2, tree.children.last.children.last, 3]
28
+ )
29
+ end
30
+ end
31
+
32
+ describe "traversing the empty tree" do
33
+ let(:tree) { t }
34
+
35
+ it "correctly reports number of nodes" do
36
+ expect(subject.traverse(tree).size).to eq(1)
37
+ end
38
+
39
+ it "returns only the original tree" do
40
+ expect(subject.traverse(tree).take(1)).to eq([tree])
41
+ end
42
+ end
43
+
44
+ def t(*children)
45
+ Tree.new(children)
46
+ end
47
+
48
+ class Tree < Struct.new(:children); end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,18 @@
1
+ require "metamorpher/support/map_at"
2
+
3
+ describe Enumerable do
4
+ subject { %w(foo bar baz) }
5
+
6
+ describe "map_at" do
7
+ it "should return a new array with the specified replacement" do
8
+ expect(subject.map_at(0) { |w| w.reverse }).to eq(%w(oof bar baz))
9
+ expect(subject.map_at(1) { |w| w.reverse }).to eq(%w(foo rab baz))
10
+ expect(subject.map_at(2) { |w| w.reverse }).to eq(%w(foo bar zab))
11
+ end
12
+
13
+ it "should raise when index is out of range" do
14
+ expect { subject.map_at(-1) }.to raise_error(IndexError)
15
+ expect { subject.map_at(3) }.to raise_error(IndexError)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,60 @@
1
+ require "metamorpher/terms/literal"
2
+
3
+ module Metamorpher
4
+ module Terms
5
+ describe Literal do
6
+ describe "leaf? and branch?" do
7
+ let(:parent) { Literal.new(name: :parent, children: [Literal.new(name: :child)]) }
8
+ let(:child) { parent.children.first }
9
+
10
+ it "should correctly identify childless literals as leaves not branches" do
11
+ expect(child).to be_leaf
12
+ expect(child).not_to be_branch
13
+ end
14
+
15
+ it "should correctly identify literals with children as branches not leaves" do
16
+ expect(parent).to be_branch
17
+ expect(parent).not_to be_leaf
18
+ end
19
+ end
20
+
21
+ describe "child_of?" do
22
+ let(:parent) { Literal.new(name: :parent, children: [Literal.new(name: :child)]) }
23
+ let(:child) { parent.children.first }
24
+
25
+ it "should return true when parent's name is parameter" do
26
+ expect(child).to be_child_of(:parent)
27
+ end
28
+
29
+ it "should return false when parent's name is not parameter" do
30
+ expect(child).not_to be_child_of(:root)
31
+ end
32
+
33
+ it "should false when literal has no parent" do
34
+ expect(parent).not_to be_child_of(:root)
35
+ end
36
+ end
37
+
38
+ describe "children younger than or equal to" do
39
+ let(:eldest) { Term.new(name: :eldest) }
40
+ let(:middle) { Term.new(name: :middle) }
41
+ let(:youngest) { Term.new(name: :youngest) }
42
+
43
+ subject { Literal.new(name: :parent, children: [eldest, middle, youngest]) }
44
+
45
+ it "should return all children not to the 'left' of argument" do
46
+ expect(subject.children_younger_than_or_equal_to(middle)).to eq([middle, youngest])
47
+ end
48
+
49
+ it "should return an only argument when argument is the youngest" do
50
+ expect(subject.children_younger_than_or_equal_to(youngest)).to eq([youngest])
51
+ end
52
+
53
+ it "should raise when argument is not a child" do
54
+ expect { subject.children_younger_than_or_equal_to(Term.new(name: :unknown)) }
55
+ .to raise_error(ArgumentError)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ require "metamorpher/terms/term"
2
+ require "metamorpher/terms/literal"
3
+
4
+ module Metamorpher
5
+ module Terms
6
+ describe Term do
7
+ describe "path" do
8
+ let(:root) do
9
+ Literal.new(
10
+ name: :root,
11
+ children: [
12
+ Literal.new(
13
+ name: :child,
14
+ children: [
15
+ Term.new(name: :grandchild),
16
+ Term.new(name: :grandchild)
17
+ ]
18
+ ),
19
+ Literal.new(
20
+ name: :child,
21
+ children: [
22
+ Term.new(name: :grandchild),
23
+ Term.new(name: :grandchild),
24
+ Term.new(name: :grandchild)
25
+ ]
26
+ )
27
+ ]
28
+ )
29
+ end
30
+
31
+ let(:first_child) { root.children.first }
32
+ let(:second_child) { root.children.last }
33
+
34
+ let(:leftmost_grandchild) { first_child.children.first }
35
+ let(:rightmost_grandchild) { second_child.children.last }
36
+
37
+ it "should return [] for root" do
38
+ expect(root.path).to eq([])
39
+ end
40
+
41
+ it "should return [0] for first child" do
42
+ expect(first_child.path).to eq([0])
43
+ end
44
+
45
+ it "should return [1] for second child" do
46
+ expect(second_child.path).to eq([1])
47
+ end
48
+
49
+ it "should return [0, 0] for leftmost grandchild" do
50
+ expect(leftmost_grandchild.path).to eq([0, 0])
51
+ end
52
+
53
+ it "should return [1, 2] for rightmost grandchild" do
54
+ expect(rightmost_grandchild.path).to eq([1, 2])
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,35 @@
1
+ require "metamorpher/visitable/visitor"
2
+
3
+ module Metamorpher
4
+ module Visitable
5
+ describe Visitor do
6
+ it "should call visitor based on the type of the visitee" do
7
+ subject = Visitor.new
8
+ subject.stub(visit_string: true)
9
+ subject.visit("foo")
10
+ expect(subject).to have_received(:visit_string)
11
+ end
12
+
13
+ it "should call visitor on ancestor of visitee if necessary" do
14
+ subject = Visitor.new
15
+ subject.stub(visit_numeric: true)
16
+ subject.visit(3) # Fixnum < Integer < Numeric
17
+ expect(subject).to have_received(:visit_numeric)
18
+ end
19
+
20
+ it "should call visitor based on unqualified type of the visitee" do
21
+ subject = Visitor.new
22
+ subject.stub(visit_dummy: true)
23
+ subject.visit(Dummy.new)
24
+ expect(subject).to have_received(:visit_dummy)
25
+ end
26
+
27
+ it "should raise if no appropriate visit method is defined" do
28
+ subject = Visitor.new
29
+ expect { subject.visit("foo") }.to raise_error(ArgumentError)
30
+ end
31
+ end
32
+
33
+ class Dummy; end
34
+ end
35
+ end