Skip to content

Commit 9a49f30

Browse files
committed
Add MutationVisitor#remove API to remove nodes from the tree
1 parent 98f4a8b commit 9a49f30

File tree

5 files changed

+92
-4
lines changed

5 files changed

+92
-4
lines changed

Diff for: README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -611,14 +611,17 @@ visitor.mutate("IfNode[predicate: Assign | OpAssign]") do |node|
611611
node.copy(predicate: predicate)
612612
end
613613

614-
source = "if a = 1; end"
614+
# remove `do_more_work` method call node
615+
visitor.remove("SyntaxTree::VCall[value: SyntaxTree::Ident[value: 'do_more_work']]")
616+
617+
source = "if a = 1; perform_work; do_more_work; end"
615618
program = SyntaxTree.parse(source)
616619

617620
SyntaxTree::Formatter.format(source, program)
618-
# => "if a = 1\nend\n"
621+
# => "if a = 1\n perform_work\n do_more_work\nend\n"
619622

620623
SyntaxTree::Formatter.format(source, program.accept(visitor))
621-
# => "if (a = 1)\nend\n"
624+
# => "if (a = 1)\n perform_work\nend\n"
622625
```
623626

624627
### WithScope

Diff for: lib/syntax_tree/mutation_visitor.rb

+12-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ module SyntaxTree
44
# This visitor walks through the tree and copies each node as it is being
55
# visited. This is useful for mutating the tree before it is formatted.
66
class MutationVisitor < BasicVisitor
7-
attr_reader :mutations
7+
attr_reader :mutations, :removals
88

99
def initialize
1010
@mutations = []
11+
@removals = []
1112
end
1213

1314
# Create a new mutation based on the given query that will mutate the node
@@ -19,13 +20,23 @@ def mutate(query, &block)
1920
mutations << [Pattern.new(query).compile, block]
2021
end
2122

23+
def remove(query)
24+
@removals << Pattern.new(query).compile
25+
end
26+
2227
# This is the base visit method for each node in the tree. It first creates
2328
# a copy of the node using the visit_* methods defined below. Then it checks
2429
# each mutation in sequence and calls it if it finds a match.
2530
def visit(node)
2631
return unless node
2732
result = node.accept(self)
2833

34+
removals.each do |removal_pattern|
35+
if removal_pattern.call(result)
36+
return RemovedNode.new(location: result.location)
37+
end
38+
end
39+
2940
mutations.each do |(pattern, mutation)|
3041
result = mutation.call(result) if pattern.call(result)
3142
end

Diff for: lib/syntax_tree/node.rb

+42
Original file line numberDiff line numberDiff line change
@@ -9324,6 +9324,48 @@ def ambiguous?(q)
93249324
end
93259325
end
93269326

9327+
# RemovedNode is a blank node used in places of nodes that have been removed.
9328+
class RemovedNode < Node
9329+
# [Array[ Comment | EmbDoc ]] the comments attached to this node
9330+
attr_reader :comments
9331+
9332+
def initialize(location:)
9333+
@location = location
9334+
@comments = []
9335+
end
9336+
9337+
def accept(visitor)
9338+
visitor.visit_removed_node(self)
9339+
end
9340+
9341+
def child_nodes
9342+
[]
9343+
end
9344+
9345+
def copy(location: self.location)
9346+
node = RemovedNode.new(
9347+
location: location
9348+
)
9349+
9350+
node.comments.concat(comments.map(&:copy))
9351+
9352+
node
9353+
end
9354+
9355+
alias deconstruct child_nodes
9356+
9357+
def deconstruct_keys(_keys)
9358+
{ location: location, comments: comments }
9359+
end
9360+
9361+
def format(_q)
9362+
end
9363+
9364+
def ===(other)
9365+
other.is_a?(RemovedNode)
9366+
end
9367+
end
9368+
93279369
# RescueEx represents the list of exceptions being rescued in a rescue clause.
93289370
#
93299371
# begin

Diff for: lib/syntax_tree/visitor.rb

+3
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,9 @@ class Visitor < BasicVisitor
320320
# Visit a RegexpLiteral node.
321321
alias visit_regexp_literal visit_child_nodes
322322

323+
# Visit a RemovedNode node.
324+
alias visit_removed_node visit_child_nodes
325+
323326
# Visit a Rescue node.
324327
alias visit_rescue visit_child_nodes
325328

Diff for: test/mutation_test.rb

+29
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,35 @@ def test_mutates_based_on_patterns
2121
assert_equal(expected, SyntaxTree::Formatter.format(source, program))
2222
end
2323

24+
def test_removes_node
25+
source = <<~RUBY
26+
App.configure do |config|
27+
config.config_value_a = 1
28+
config.config_value_b = 2
29+
config.config_value_c = 2
30+
end
31+
RUBY
32+
33+
expected = <<~RUBY
34+
App.configure do |config|
35+
config.config_value_a = 1
36+
37+
config.config_value_c = 2
38+
end
39+
RUBY
40+
41+
mutation_visitor = SyntaxTree.mutation do |mutation|
42+
mutation.remove("SyntaxTree::Assign[
43+
target: SyntaxTree::Field[
44+
name: SyntaxTree::Ident[value: 'config_value_b']
45+
],
46+
]")
47+
end
48+
49+
program = SyntaxTree.parse(source).accept(mutation_visitor)
50+
assert_equal(expected, SyntaxTree::Formatter.format(source, program))
51+
end
52+
2453
private
2554

2655
def build_mutation

0 commit comments

Comments
 (0)