Skip to content

Commit

Permalink
Add MutationVisitor#remove API to remove nodes from the tree
Browse files Browse the repository at this point in the history
  • Loading branch information
nvasilevski committed Jul 28, 2023
1 parent 98f4a8b commit b1db957
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 4 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion lib/syntax_tree/mutation_visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,13 +20,23 @@ 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.
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
Expand Down
42 changes: 42 additions & 0 deletions lib/syntax_tree/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/syntax_tree/visitor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 29 additions & 0 deletions test/mutation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b1db957

Please sign in to comment.