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,32 @@
1
+ module Metamorpher
2
+ module Builders
3
+ module Ruby
4
+ class VariableReplacementVisitor < Visitable::Visitor
5
+ attr_accessor :variable_name, :replacement
6
+
7
+ def initialize(variable_name, replacement)
8
+ @variable_name, @replacement = variable_name, replacement
9
+ end
10
+
11
+ def visit_literal(literal)
12
+ Terms::Literal.new(
13
+ name: literal.name,
14
+ children: literal.children.map { |child| visit(child) }
15
+ )
16
+ end
17
+
18
+ def visit_variable(variable)
19
+ if variable.name == variable_name
20
+ replacement
21
+ else
22
+ variable
23
+ end
24
+ end
25
+
26
+ def visit_term(term)
27
+ term
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ require "metamorpher/builders/ruby/builder"
2
+
3
+ module Metamorpher
4
+ module Builders
5
+ module Ruby
6
+ def builder
7
+ @builder ||= Builder.new
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Metamorpher
2
+ module Drivers
3
+ class ParseError < ArgumentError; end
4
+ end
5
+ end
@@ -0,0 +1,78 @@
1
+ require "metamorpher/drivers/parse_error"
2
+ require "metamorpher/terms/literal"
3
+ require "parser/current"
4
+ require "unparser"
5
+
6
+ module Metamorpher
7
+ module Drivers
8
+ class Ruby
9
+ def parse(src)
10
+ import(@root = parser.parse(src))
11
+ rescue Parser::SyntaxError
12
+ raise ParseError
13
+ end
14
+
15
+ def unparse(literal)
16
+ unparser.unparse(export(literal))
17
+ end
18
+
19
+ def source_location_for(literal)
20
+ ast = ast_for(literal)
21
+ (ast.loc.expression.begin_pos..(ast.loc.expression.end_pos - 1))
22
+ end
23
+
24
+ private
25
+
26
+ def import(ast)
27
+ create_literal_for(ast)
28
+ end
29
+
30
+ def create_literal_for(ast)
31
+ if ast.respond_to? :type
32
+ Terms::Literal.new(name: ast.type, children: ast.children.map { |c| import(c) })
33
+ else
34
+ Terms::Literal.new(name: ast)
35
+ end
36
+ end
37
+
38
+ def export(literal)
39
+ if literal.branch?
40
+ Parser::AST::Node.new(literal.name, literal.children.map { |c| export(c) })
41
+
42
+ elsif keyword?(literal)
43
+ # Unparser requires leaf nodes containing keywords to be represented as nodes.
44
+ Parser::AST::Node.new(literal.name)
45
+
46
+ else
47
+ # Unparser requires all other leaf nodes to be represented as primitives.
48
+ literal.name
49
+ end
50
+ end
51
+
52
+ def keyword?(literal)
53
+ literal.leaf? && !literal.child_of?(:sym) && keywords.include?(literal.name)
54
+ end
55
+
56
+ def keywords
57
+ # The symbols used by Parser for Ruby keywords. The current implementation
58
+ # is not a definitive list. If unparsing fails, it might be due to this list
59
+ # omitting a necessary keyword. Note that these are the symbols produced
60
+ # by Parser which are not necessarily the same as Ruby keywords (e.g.,
61
+ # Parser sometimes produces a :zsuper node for a program of the form "super")
62
+ @keywords ||= %i(nil false true self)
63
+ end
64
+
65
+ def ast_for(literal)
66
+ literal.path.reduce(@root) { |a, e| a.children[e] }
67
+ end
68
+
69
+ def parser
70
+ @parser ||= Parser::CurrentRuby
71
+ end
72
+
73
+ def unparser
74
+ @unparser ||= Unparser
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,26 @@
1
+ require "attributable"
2
+
3
+ module Metamorpher
4
+ module Matcher
5
+ class Match
6
+ extend Attributable
7
+ attributes :root, substitution: {}
8
+
9
+ def matches?
10
+ true
11
+ end
12
+
13
+ def match_for(variable)
14
+ substitution[variable.name]
15
+ end
16
+
17
+ def combine(combinee)
18
+ if combinee.matches?
19
+ Match.new(root: root, substitution: combinee.substitution.merge(substitution))
20
+ else
21
+ NoMatch.new
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,61 @@
1
+ require "metamorpher/visitable/visitor"
2
+ require "metamorpher/matcher/match"
3
+ require "metamorpher/matcher/no_match"
4
+
5
+ module Metamorpher
6
+ module Matcher
7
+ module Matching
8
+ def match(other)
9
+ if other.nil?
10
+ Matcher::NoMatch.new
11
+ else
12
+ accept MatchingVisitor.new(other)
13
+ end
14
+ end
15
+ end
16
+
17
+ class MatchingVisitor < Visitable::Visitor
18
+ attr_accessor :other
19
+
20
+ def initialize(other)
21
+ @other = other
22
+ end
23
+
24
+ def visit_variable(variable)
25
+ captured = variable.greedy? ? other.with_younger_siblings : other
26
+ if variable.condition.call(captured)
27
+ Matcher::Match.new(root: captured, substitution: { variable.name => captured })
28
+ else
29
+ Matcher::NoMatch.new
30
+ end
31
+ end
32
+
33
+ def visit_literal(literal)
34
+ if other.name == literal.name && expected_number_of_children?(literal)
35
+ literal.children
36
+ .zip(other.children)
37
+ .map { |child, other_child| child.match(other_child) }
38
+ .reduce(Matcher::Match.new(root: other), :combine)
39
+ else
40
+ Matcher::NoMatch.new
41
+ end
42
+ end
43
+
44
+ def visit_derived(derived)
45
+ fail MatchingError, "Cannot match against a derived variable."
46
+ end
47
+
48
+ private
49
+
50
+ def expected_number_of_children?(literal)
51
+ other.children.size == literal.children.size || greedy_child?(literal)
52
+ end
53
+
54
+ def greedy_child?(literal)
55
+ literal.children.any? { |c| c.is_a?(Terms::Variable) && c.greedy? }
56
+ end
57
+ end
58
+
59
+ class MatchingError < ArgumentError; end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ require "attributable"
2
+
3
+ module Metamorpher
4
+ module Matcher
5
+ class NoMatch
6
+ extend Attributable
7
+ attributes
8
+
9
+ def matches?
10
+ false
11
+ end
12
+
13
+ def combine(_)
14
+ NoMatch.new
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ module Metamorpher
2
+ module Matcher
3
+ extend Forwardable
4
+ def_delegator :pattern, :match, :run
5
+ end
6
+ end
@@ -0,0 +1,18 @@
1
+ require "attributable"
2
+
3
+ module Metamorpher
4
+ module Refactorer
5
+ Merger = Struct.new(:original) do
6
+ def merge(*replacements, &block)
7
+ original.dup.tap do |merged|
8
+ replacements.sort.reduce(0) do |offset, replacement|
9
+ yield replacement if block
10
+ replacement = replacement.slide(offset)
11
+ replacement.merge_into(merged)
12
+ offset + replacement.offset
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ require "attributable"
2
+
3
+ module Metamorpher
4
+ module Refactorer
5
+ Site = Struct.new(:original_position, :original_code, :refactored_code) do
6
+ def slide(offset)
7
+ new_position = (original_position.begin + offset)..(original_position.end + offset)
8
+ Site.new(new_position, original_code, refactored_code)
9
+ end
10
+
11
+ def merge_into(destination)
12
+ if original_position.begin > destination.size
13
+ fail ArgumentError, "Position #{original_position} does not exist in: #{destination}"
14
+ end
15
+
16
+ destination[original_position] = refactored_code
17
+ destination
18
+ end
19
+
20
+ def offset
21
+ refactored_code.size - original_code.size
22
+ end
23
+
24
+ def <=>(other)
25
+ original_position.begin <=> other.original_position.begin
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ require "metamorpher/refactorer/merger"
2
+ require "metamorpher/refactorer/site"
3
+ require "metamorpher/rewriter/rule"
4
+ require "metamorpher/drivers/ruby"
5
+
6
+ module Metamorpher
7
+ module Refactorer
8
+ def refactor(src, &block)
9
+ literal = driver.parse(src)
10
+ replacements = reduce_to_replacements(src, literal)
11
+ Merger.new(src).merge(*replacements, &block)
12
+ end
13
+
14
+ def refactor_file(path, &block)
15
+ refactor(File.read(path), &block)
16
+ end
17
+
18
+ def refactor_files(paths, &block)
19
+ paths.reduce({}) do |result, path|
20
+ changes = []
21
+ result[path] = refactor_file(path) { |change| changes << change }
22
+ block.call(path, result[path], changes) if block
23
+ result
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
+ rule.reduce(literal) do |original, rewritten|
36
+ original_position = driver.source_location_for(original)
37
+ original_code = src[original_position]
38
+ refactored_code = driver.unparse(rewritten)
39
+ replacements << Site.new(original_position, original_code, refactored_code)
40
+ end
41
+ end
42
+ end
43
+
44
+ def rule
45
+ @rule ||= Rewriter::Rule.new(pattern: pattern, replacement: replacement)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ module Metamorpher
2
+ module Rewriter
3
+ module Replacement
4
+ def replace(path, replacement)
5
+ if path.empty?
6
+ replacement
7
+ else
8
+ Terms::Literal.new(
9
+ name: name,
10
+ children: children.map_at(path.first) { |e| e.replace(path.drop(1), replacement) }
11
+ )
12
+ end
13
+ end
14
+ end
15
+
16
+ class ReplacementError < ArgumentError; end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ require "attributable"
2
+ require "metamorpher/rewriter/traverser"
3
+
4
+ module Metamorpher
5
+ module Rewriter
6
+ class Rule
7
+ extend Attributable
8
+ attributes :pattern, :replacement, traverser: Traverser.new
9
+
10
+ def apply(ast, &block)
11
+ rewrite_all(ast, matches_for(ast).take(1), &block)
12
+ end
13
+
14
+ def reduce(ast, &block)
15
+ rewrite_all(ast, matches_for(ast), &block)
16
+ end
17
+
18
+ private
19
+
20
+ def rewrite_all(ast, matches, &block)
21
+ matches.reduce(ast) { |a, e| rewrite(a, e, &block) }
22
+ end
23
+
24
+ def rewrite(ast, match, &block)
25
+ original, rewritten = match.root, replacement.substitute(match.substitution)
26
+ block.call(original, rewritten) if block
27
+ ast.replace(original.path, rewritten)
28
+ end
29
+
30
+ def matches_for(ast)
31
+ traverser.traverse(ast)
32
+ .lazy # only compute the next match when needed
33
+ .map { |current| pattern.match(current) }
34
+ .select { |result| result.matches? }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,45 @@
1
+ require "metamorpher/visitable/visitor"
2
+
3
+ module Metamorpher
4
+ module Rewriter
5
+ module Substitution
6
+ def substitute(substitution)
7
+ accept SubstitutionVisitor.new(substitution)
8
+ end
9
+ end
10
+
11
+ class SubstitutionVisitor < Visitable::Visitor
12
+ attr_accessor :substitution
13
+
14
+ def initialize(substitution)
15
+ @substitution = substitution
16
+ end
17
+
18
+ def visit_variable(variable)
19
+ substitution_for_variable(variable.name)
20
+ end
21
+
22
+ def visit_literal(literal)
23
+ Terms::Literal.new(
24
+ name: literal.name,
25
+ children: literal.children.flat_map { |child| visit(child) }
26
+ )
27
+ end
28
+
29
+ def visit_derived(derived)
30
+ substitutes = derived.base.map { |v| substitution_for_variable(v) }
31
+ derived.derivation.call(*substitutes)
32
+ end
33
+
34
+ private
35
+
36
+ def substitution_for_variable(name)
37
+ substitution.fetch(name) do
38
+ fail SubstitutionError, "No substitution found for variable '#{name}'"
39
+ end
40
+ end
41
+ end
42
+
43
+ class SubstitutionError < ArgumentError; end
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ module Metamorpher
2
+ module Rewriter
3
+ class Traverser
4
+ def traverse(tree)
5
+ Enumerator.new(count(tree)) do |yielder|
6
+ waiting = [tree]
7
+ until waiting.empty?
8
+ current = waiting.shift
9
+ yielder << current
10
+ waiting.concat(children(current))
11
+ end
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def count(tree)
18
+ children(tree).flat_map { |child| count(child) }.inject(1, :+)
19
+ end
20
+
21
+ def children(node)
22
+ node.respond_to?(:children) ? node.children : []
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ require "metamorpher/rewriter/rule"
2
+
3
+ module Metamorpher
4
+ module Rewriter
5
+ extend Forwardable
6
+ def_delegators :rule, :apply, :reduce
7
+
8
+ def rule
9
+ @rule ||= Rewriter::Rule.new(pattern: pattern, replacement: replacement)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module Enumerable
2
+ # Returns a new array with the element at _index_ replaced by the result of
3
+ # running _block_ on that element.
4
+ def map_at(index, &block)
5
+ fail IndexError if index < 0 || index >= size
6
+ each_with_index.map { |e, i| i == index ? block.call(e) : e }
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ require "metamorpher/terms/term"
2
+
3
+ module Metamorpher
4
+ module Terms
5
+ class Derived < Term
6
+ attributes :base, :derivation
7
+
8
+ def inspect
9
+ "[#{base.map(&:upcase).join(", ")}] -> ..."
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ require "metamorpher/terms/term"
2
+ require "metamorpher/matcher/match"
3
+ require "metamorpher/matcher/no_match"
4
+
5
+ module Metamorpher
6
+ module Terms
7
+ class Literal < Term
8
+ attributes children: []
9
+
10
+ def initialize(attributes = {})
11
+ initialize_attributes(attributes)
12
+ children.each { |child| child.parent = self }
13
+ end
14
+
15
+ def inspect
16
+ if leaf?
17
+ "#{name}"
18
+ else
19
+ "#{name}(#{children.map(&:inspect).join(', ')})"
20
+ end
21
+ end
22
+
23
+ def leaf?
24
+ children.empty?
25
+ end
26
+
27
+ def branch?
28
+ !leaf?
29
+ end
30
+
31
+ def child_of?(parent_name)
32
+ parent && parent.name == parent_name
33
+ end
34
+
35
+ def children_younger_than_or_equal_to(child)
36
+ children[(index(child))..-1]
37
+ end
38
+
39
+ private
40
+
41
+ def index(child)
42
+ children.index(child) ||
43
+ fail(ArgumentError, "#{child.inspect} is not a child of #{inspect}")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ require "attributable"
2
+ require "metamorpher/visitable/visitable"
3
+ require "metamorpher/matcher/matching"
4
+ require "metamorpher/rewriter/replacement"
5
+ require "metamorpher/rewriter/substitution"
6
+
7
+ module Metamorpher
8
+ module Terms
9
+ class Term
10
+ extend Attributable
11
+ attributes :name
12
+ attr_accessor :parent
13
+
14
+ include Visitable
15
+ include Matcher::Matching
16
+ include Rewriter::Replacement
17
+ include Rewriter::Substitution
18
+
19
+ def inspect
20
+ name
21
+ end
22
+
23
+ def path
24
+ if parent
25
+ parent.path << parent.children.index { |c| c.equal?(self) }
26
+ else
27
+ []
28
+ end
29
+ end
30
+
31
+ def with_younger_siblings
32
+ if parent
33
+ parent.children_younger_than_or_equal_to(self)
34
+ else
35
+ [self]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ require "metamorpher/terms/term"
2
+ require "metamorpher/matcher/match"
3
+
4
+ module Metamorpher
5
+ module Terms
6
+ class Variable < Term
7
+ DEFAULT_CONDITION = ->(_) { true }
8
+ attributes greedy?: false, condition: DEFAULT_CONDITION
9
+
10
+ def inspect
11
+ name.to_s.upcase +
12
+ (greedy? ? "+" : "") +
13
+ (condition != DEFAULT_CONDITION ? "?" : "")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Metamorpher
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ module Metamorpher
2
+ module Visitable
3
+ def accept(visitor)
4
+ visitor.visit(self)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # Based on http://blog.rubybestpractices.com/posts/aaronp/001_double_dispatch_dance.html
2
+
3
+ module Metamorpher
4
+ module Visitable
5
+ class Visitor
6
+ ###
7
+ # This method will examine the class and ancestors of +thing+. For each
8
+ # class in the "ancestors" list, it will check to see if the visitor knows
9
+ # how to handle that particular class. If it can't find a handler for the
10
+ # +thing+ it will raise an exception.
11
+ def visit(thing)
12
+ thing.class.ancestors.each do |ancestor|
13
+ method_name = :"visit_#{ancestor.name.split("::").last.downcase}"
14
+ return send(method_name, thing) if respond_to?(method_name)
15
+ end
16
+
17
+ fail ArgumentError, "Can't visit #{thing.class}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ require "metamorpher/version"
2
+ require "metamorpher/builders/ruby"
3
+
4
+ require "metamorpher/support/map_at"
5
+
6
+ require "metamorpher/matcher"
7
+ require "metamorpher/rewriter"
8
+ require "metamorpher/refactorer"
9
+
10
+ module Metamorpher
11
+ def self.builder
12
+ @builder ||= Builders::Ruby::Builder.new
13
+ end
14
+
15
+ def self.configure(builder: :ast)
16
+ configure_builder(builder)
17
+ end
18
+
19
+ private
20
+
21
+ def self.configure_builder(builder)
22
+ require "metamorpher/builders/#{builder}/builder"
23
+ @builder = builder_class_for(builder).new
24
+ end
25
+
26
+ def self.builder_class_for(name)
27
+ namespace = name == :ast ? "AST" : name.to_s.capitalize
28
+ Builders.const_get(namespace).const_get("Builder")
29
+ end
30
+ end