diff --git a/README.md b/README.md index 6e1119df..c64ef099 100644 --- a/README.md +++ b/README.md @@ -611,14 +611,17 @@ visitor.mutate("IfNode[predicate: Assign | OpAssign]") do |node| node.copy(predicate: predicate) end -source = "if a = 1; end" +# remove `do_more_work` method call node +visitor.remove("SyntaxTree::VCall[value: SyntaxTree::Ident[value: 'do_more_work']]") + +source = "if a = 1; perform_work; do_more_work; end" program = SyntaxTree.parse(source) SyntaxTree::Formatter.format(source, program) -# => "if a = 1\nend\n" +# => "if a = 1\n perform_work\n do_more_work\nend\n" SyntaxTree::Formatter.format(source, program.accept(visitor)) -# => "if (a = 1)\nend\n" +# => "if (a = 1)\n perform_work\nend\n" ``` ### WithScope diff --git a/lib/syntax_tree/mutation_visitor.rb b/lib/syntax_tree/mutation_visitor.rb index 0b4b9357..8390ca54 100644 --- a/lib/syntax_tree/mutation_visitor.rb +++ b/lib/syntax_tree/mutation_visitor.rb @@ -4,10 +4,11 @@ module SyntaxTree # This visitor walks through the tree and copies each node as it is being # visited. This is useful for mutating the tree before it is formatted. class MutationVisitor < BasicVisitor - attr_reader :mutations + attr_reader :mutations, :removals def initialize @mutations = [] + @removals = [] end # Create a new mutation based on the given query that will mutate the node @@ -19,6 +20,10 @@ def mutate(query, &block) mutations << [Pattern.new(query).compile, block] end + def remove(query) + @removals << Pattern.new(query).compile + end + # This is the base visit method for each node in the tree. It first creates # a copy of the node using the visit_* methods defined below. Then it checks # each mutation in sequence and calls it if it finds a match. @@ -26,6 +31,12 @@ def visit(node) return unless node result = node.accept(self) + removals.each do |removal_pattern| + if removal_pattern.call(result) + return RemovedNode.new(location: result.location) + end + end + mutations.each do |(pattern, mutation)| result = mutation.call(result) if pattern.call(result) end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 3b676552..4401a7a1 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -9324,6 +9324,48 @@ def ambiguous?(q) end end + # RemovedNode is a blank node used in places of nodes that have been removed. + class RemovedNode < Node + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(location:) + @location = location + @comments = [] + end + + def accept(visitor) + visitor.visit_removed_node(self) + end + + def child_nodes + [] + end + + def copy(location: self.location) + node = RemovedNode.new( + location: location + ) + + node.comments.concat(comments.map(&:copy)) + + node + end + + alias deconstruct child_nodes + + def deconstruct_keys(_keys) + { location: location, comments: comments } + end + + def format(_q) + end + + def ===(other) + other.is_a?(RemovedNode) + end + end + # RescueEx represents the list of exceptions being rescued in a rescue clause. # # begin diff --git a/lib/syntax_tree/visitor.rb b/lib/syntax_tree/visitor.rb index eb57acd2..294ddf9c 100644 --- a/lib/syntax_tree/visitor.rb +++ b/lib/syntax_tree/visitor.rb @@ -320,6 +320,9 @@ class Visitor < BasicVisitor # Visit a RegexpLiteral node. alias visit_regexp_literal visit_child_nodes + # Visit a RemovedNode node. + alias visit_removed_node visit_child_nodes + # Visit a Rescue node. alias visit_rescue visit_child_nodes diff --git a/test/mutation_test.rb b/test/mutation_test.rb index ab9dd019..870a9e0a 100644 --- a/test/mutation_test.rb +++ b/test/mutation_test.rb @@ -21,6 +21,35 @@ def test_mutates_based_on_patterns assert_equal(expected, SyntaxTree::Formatter.format(source, program)) end + def test_removes_node + source = <<~RUBY + App.configure do |config| + config.config_value_a = 1 + config.config_value_b = 2 + config.config_value_c = 2 + end + RUBY + + expected = <<~RUBY + App.configure do |config| + config.config_value_a = 1 + + config.config_value_c = 2 + end + RUBY + + mutation_visitor = SyntaxTree.mutation do |mutation| + mutation.remove("SyntaxTree::Assign[ + target: SyntaxTree::Field[ + name: SyntaxTree::Ident[value: 'config_value_b'] + ], + ]") + end + + program = SyntaxTree.parse(source).accept(mutation_visitor) + assert_equal(expected, SyntaxTree::Formatter.format(source, program)) + end + private def build_mutation