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,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