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