metamorpher 0.1.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.
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