metamorpher 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/.travis.yml +3 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +22 -0
- data/README.md +541 -0
- data/Rakefile +23 -0
- data/examples/refactorings/rails/where_first/app.rb +50 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_mocks.rb +31 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_not_called_expectations.rb +14 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_strict_mocks.rb +27 -0
- data/examples/refactorings/rails/where_first/refactorers/refactor_where_first_to_find_by.rb +14 -0
- data/examples/refactorings/rails/where_first/sample_controller.rb +184 -0
- data/lib/metamorpher/builders/ast/builder.rb +50 -0
- data/lib/metamorpher/builders/ast/derivation_builder.rb +20 -0
- data/lib/metamorpher/builders/ast/greedy_variable_builder.rb +29 -0
- data/lib/metamorpher/builders/ast/literal_builder.rb +31 -0
- data/lib/metamorpher/builders/ast/variable_builder.rb +29 -0
- data/lib/metamorpher/builders/ast.rb +11 -0
- data/lib/metamorpher/builders/ruby/builder.rb +38 -0
- data/lib/metamorpher/builders/ruby/deriving_visitor.rb +13 -0
- data/lib/metamorpher/builders/ruby/ensuring_visitor.rb +13 -0
- data/lib/metamorpher/builders/ruby/term.rb +35 -0
- data/lib/metamorpher/builders/ruby/uppercase_constant_rewriter.rb +31 -0
- data/lib/metamorpher/builders/ruby/uppercase_rewriter.rb +28 -0
- data/lib/metamorpher/builders/ruby/variable_replacement_visitor.rb +32 -0
- data/lib/metamorpher/builders/ruby.rb +11 -0
- data/lib/metamorpher/drivers/parse_error.rb +5 -0
- data/lib/metamorpher/drivers/ruby.rb +78 -0
- data/lib/metamorpher/matcher/match.rb +26 -0
- data/lib/metamorpher/matcher/matching.rb +61 -0
- data/lib/metamorpher/matcher/no_match.rb +18 -0
- data/lib/metamorpher/matcher.rb +6 -0
- data/lib/metamorpher/refactorer/merger.rb +18 -0
- data/lib/metamorpher/refactorer/site.rb +29 -0
- data/lib/metamorpher/refactorer.rb +48 -0
- data/lib/metamorpher/rewriter/replacement.rb +18 -0
- data/lib/metamorpher/rewriter/rule.rb +38 -0
- data/lib/metamorpher/rewriter/substitution.rb +45 -0
- data/lib/metamorpher/rewriter/traverser.rb +26 -0
- data/lib/metamorpher/rewriter.rb +12 -0
- data/lib/metamorpher/support/map_at.rb +8 -0
- data/lib/metamorpher/terms/derived.rb +13 -0
- data/lib/metamorpher/terms/literal.rb +47 -0
- data/lib/metamorpher/terms/term.rb +40 -0
- data/lib/metamorpher/terms/variable.rb +17 -0
- data/lib/metamorpher/version.rb +3 -0
- data/lib/metamorpher/visitable/visitable.rb +7 -0
- data/lib/metamorpher/visitable/visitor.rb +21 -0
- data/lib/metamorpher.rb +30 -0
- data/metamorpher.gemspec +30 -0
- data/spec/integration/ast/builder_spec.rb +13 -0
- data/spec/integration/ast/matcher_spec.rb +132 -0
- data/spec/integration/ast/rewriter_spec.rb +138 -0
- data/spec/integration/ruby/builder_spec.rb +125 -0
- data/spec/integration/ruby/refactorer_spec.rb +192 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/helpers/silence_stream.rb +10 -0
- data/spec/support/matchers/have_matched_matcher.rb +22 -0
- data/spec/support/matchers/have_substitution_matcher.rb +15 -0
- data/spec/support/shared_examples/shared_examples_for_derivation_builders.rb +53 -0
- data/spec/support/shared_examples/shared_examples_for_greedy_variable_builders.rb +49 -0
- data/spec/support/shared_examples/shared_examples_for_literal_builders.rb +93 -0
- data/spec/support/shared_examples/shared_examples_for_variable_builders.rb +49 -0
- data/spec/unit/builders/ast/derivation_builder_spec.rb +5 -0
- data/spec/unit/builders/ast/greedy_variable_builder_spec.rb +9 -0
- data/spec/unit/builders/ast/literal_builder_spec.rb +9 -0
- data/spec/unit/builders/ast/variable_builder_spec.rb +9 -0
- data/spec/unit/builders/ruby/variable_replacement_visitor_spec.rb +48 -0
- data/spec/unit/drivers/ruby_spec.rb +91 -0
- data/spec/unit/matcher/matching_spec.rb +230 -0
- data/spec/unit/metamorpher_spec.rb +22 -0
- data/spec/unit/refactorer/merger_spec.rb +84 -0
- data/spec/unit/refactorer/site_spec.rb +52 -0
- data/spec/unit/rewriter/replacement_spec.rb +73 -0
- data/spec/unit/rewriter/substitution_spec.rb +97 -0
- data/spec/unit/rewriter/traverser_spec.rb +51 -0
- data/spec/unit/support/map_at_spec.rb +18 -0
- data/spec/unit/terms/literal_spec.rb +60 -0
- data/spec/unit/terms/term_spec.rb +59 -0
- data/spec/unit/visitable/visitor_spec.rb +35 -0
- 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,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 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,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,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,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
|
data/lib/metamorpher.rb
ADDED
@@ -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
|