Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 6481453

Browse files
committed
Final tests, documentation, changelog for mutations
1 parent 3086f41 commit 6481453

File tree

6 files changed

+188
-17
lines changed

6 files changed

+188
-17
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Every node now implements the `#copy(**)` method, which provides a copy of the node with the given attributes replaced.
12+
- Every node now implements the `#===(other)` method, which checks if the given node matches the current node for all attributes except for comments and location.
13+
- There is a new `SyntaxTree::Visitor::MutationVisitor` and its convenience method `SyntaxTree.mutation` which can be used to mutate a syntax tree. For details on how to use this visitor, check the README.
14+
915
### Changed
1016

1117
- Nodes no longer have a `comments:` keyword on their initializers. By default, they initialize to an empty array. If you were previously passing comments into the initializer, you should now create the node first, then call `node.comments.concat` to add your comments.

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,21 @@ It is built with only standard library dependencies. It additionally ships with
2727
- [SyntaxTree.read(filepath)](#syntaxtreereadfilepath)
2828
- [SyntaxTree.parse(source)](#syntaxtreeparsesource)
2929
- [SyntaxTree.format(source)](#syntaxtreeformatsource)
30+
- [SyntaxTree.mutation(&block)](#syntaxtreemutationblock)
3031
- [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block)
3132
- [Nodes](#nodes)
3233
- [child_nodes](#child_nodes)
34+
- [copy(**)](#copy)
3335
- [Pattern matching](#pattern-matching)
3436
- [pretty_print(q)](#pretty_printq)
3537
- [to_json(*opts)](#to_jsonopts)
3638
- [format(q)](#formatq)
39+
- [===(other)](#other)
3740
- [construct_keys](#construct_keys)
3841
- [Visitor](#visitor)
3942
- [visit_method](#visit_method)
4043
- [BasicVisitor](#basicvisitor)
44+
- [MutationVisitor](#mutationvisitor)
4145
- [Language server](#language-server)
4246
- [textDocument/formatting](#textdocumentformatting)
4347
- [textDocument/inlayHint](#textdocumentinlayhint)
@@ -332,6 +336,10 @@ This function takes an input string containing Ruby code and returns the syntax
332336

333337
This function takes an input string containing Ruby code, parses it into its underlying syntax tree, and formats it back out to a string. You can optionally pass a second argument to this method as well that is the maximum width to print. It defaults to `80`.
334338

339+
### SyntaxTree.mutation(&block)
340+
341+
This function yields a new mutation visitor to the block, and then returns the initialized visitor. It's effectively a shortcut for creating a `SyntaxTree::Visitor::MutationVisitor` without having to remember the class name. For more information on that visitor, see the definition below.
342+
335343
### SyntaxTree.search(source, query, &block)
336344

337345
This function takes an input string containing Ruby code, an input string containing a valid Ruby `in` clause expression that can be used to match against nodes in the tree (can be generated using `stree expr`, `stree match`, or `Node#construct_keys`), and a block. Each node that matches the given query will be yielded to the block. The block will receive the node as its only argument.
@@ -350,6 +358,20 @@ program.child_nodes.first.child_nodes.first
350358
# => (binary (int "1") :+ (int "1"))
351359
```
352360

361+
### copy
362+
363+
This method returns a copy of the node, with the given attributes replaced.
364+
365+
```ruby
366+
program = SyntaxTree.parse("1 + 1")
367+
368+
binary = program.statements.body.first
369+
# => (binary (int "1") + (int "1"))
370+
371+
binary.copy(operator: :-)
372+
# => (binary (int "1") - (int "1"))
373+
```
374+
353375
### Pattern matching
354376

355377
Pattern matching is another way to descend the tree which is more specific than using `child_nodes`. Using Ruby's built-in pattern matching, you can extract the same information but be as specific about your constraints as you like. For example, with minimal constraints:
@@ -407,6 +429,18 @@ formatter.output.join
407429
# => "1 + 1"
408430
```
409431

432+
### ===(other)
433+
434+
Every node responds to `===`, which is used to check if the given other node matches all of the attributes of the current node except for location and comments. For example:
435+
436+
```ruby
437+
program1 = SyntaxTree.parse("1 + 1")
438+
program2 = SyntaxTree.parse("1 + 1")
439+
440+
program1 === program2
441+
# => true
442+
```
443+
410444
### construct_keys
411445

412446
Every node responds to `construct_keys`, which will return a string that contains a Ruby pattern-matching expression that could be used to match against the current node. It's meant to be used in tooling and through the CLI mostly.
@@ -495,6 +529,42 @@ end
495529

496530
The visitor defined above will error out unless it's only visiting a `SyntaxTree::Int` node. This is useful in a couple of ways, e.g., if you're trying to define a visitor to handle the whole tree but it's currently a work-in-progress.
497531

532+
### MutationVisitor
533+
534+
The `MutationVisitor` is a visitor that can be used to mutate the tree. It works by defining a default `visit_*` method that returns a copy of the given node with all of its attributes visited. This new node will replace the old node in the tree. Typically, you use the `#mutate` method on it to define mutations using patterns. For example:
535+
536+
```ruby
537+
# Create a new visitor
538+
visitor = SyntaxTree::Visitor::MutationVisitor.new
539+
540+
# Specify that it should mutate If nodes with assignments in their predicates
541+
visitor.mutate("If[predicate: Assign | OpAssign]") do |node|
542+
# Get the existing If's predicate node
543+
predicate = node.predicate
544+
545+
# Create a new predicate node that wraps the existing predicate node
546+
# in parentheses
547+
predicate =
548+
SyntaxTree::Paren.new(
549+
lparen: SyntaxTree::LParen.default,
550+
contents: predicate,
551+
location: predicate.location
552+
)
553+
554+
# Return a copy of this node with the new predicate
555+
node.copy(predicate: predicate)
556+
end
557+
558+
source = "if a = 1; end"
559+
program = SyntaxTree.parse(source)
560+
561+
SyntaxTree::Formatter.format(source, program)
562+
# => "if a = 1\nend\n"
563+
564+
SyntaxTree::Formatter.format(source, program.accept(visitor))
565+
# => "if (a = 1)\nend\n"
566+
```
567+
498568
### WithEnvironment
499569

500570
The `WithEnvironment` module can be included in visitors to automatically keep track of local variables and arguments

lib/syntax_tree.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ def self.format(source, maxwidth = DEFAULT_PRINT_WIDTH)
6262
formatter.output.join
6363
end
6464

65+
# A convenience method for creating a new mutation visitor.
66+
def self.mutation
67+
visitor = Visitor::MutationVisitor.new
68+
yield visitor
69+
visitor
70+
end
71+
6572
# Returns the source from the given filepath taking into account any potential
6673
# magic encoding comments.
6774
def self.read(filepath)

lib/syntax_tree/node.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def self.fixed(line:, char:, column:)
8484
)
8585
end
8686

87+
# A convenience method that is typically used when you don't care about the
88+
# location of a node, but need to create a Location instance to pass to a
89+
# constructor.
8790
def self.default
8891
new(
8992
start_line: 1,
@@ -7239,6 +7242,15 @@ def format(q)
72397242
def ===(other)
72407243
other.is_a?(LBrace) && value === other.value
72417244
end
7245+
7246+
# Because some nodes keep around a { token so that comments can be attached
7247+
# to it if they occur in the source, oftentimes an LBrace is a child of
7248+
# another node. This means it's required at initialization time. To make it
7249+
# easier to create LBrace nodes without any specific value, this method
7250+
# provides a default node.
7251+
def self.default
7252+
new(value: "{", location: Location.default)
7253+
end
72427254
end
72437255

72447256
# LBracket represents the use of a left bracket, i.e., [.
@@ -7287,6 +7299,15 @@ def format(q)
72877299
def ===(other)
72887300
other.is_a?(LBracket) && value === other.value
72897301
end
7302+
7303+
# Because some nodes keep around a [ token so that comments can be attached
7304+
# to it if they occur in the source, oftentimes an LBracket is a child of
7305+
# another node. This means it's required at initialization time. To make it
7306+
# easier to create LBracket nodes without any specific value, this method
7307+
# provides a default node.
7308+
def self.default
7309+
new(value: "[", location: Location.default)
7310+
end
72907311
end
72917312

72927313
# LParen represents the use of a left parenthesis, i.e., (.
@@ -7335,6 +7356,15 @@ def format(q)
73357356
def ===(other)
73367357
other.is_a?(LParen) && value === other.value
73377358
end
7359+
7360+
# Because some nodes keep around a ( token so that comments can be attached
7361+
# to it if they occur in the source, oftentimes an LParen is a child of
7362+
# another node. This means it's required at initialization time. To make it
7363+
# easier to create LParen nodes without any specific value, this method
7364+
# provides a default node.
7365+
def self.default
7366+
new(value: "(", location: Location.default)
7367+
end
73387368
end
73397369

73407370
# MAssign is a parent node of any kind of multiple assignment. This includes

lib/syntax_tree/visitor/mutation_visitor.rb

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,33 @@ class Visitor
55
# This visitor walks through the tree and copies each node as it is being
66
# visited. This is useful for mutating the tree before it is formatted.
77
class MutationVisitor < BasicVisitor
8-
# Here we maintain a stack of parent nodes so that it's easy to reflect on
9-
# the context of a given node while mutating it.
10-
attr_reader :stack
8+
attr_reader :mutations
119

1210
def initialize
13-
@stack = []
11+
@mutations = []
1412
end
1513

16-
# This is the main entrypoint that's going to be called when we're
17-
# recursing down through the tree.
14+
# Create a new mutation based on the given query that will mutate the node
15+
# using the given block. The block should return a new node that will take
16+
# the place of the given node in the tree. These blocks frequently make
17+
# use of the `copy` method on nodes to create a new node with the same
18+
# properties as the original node.
19+
def mutate(query, &block)
20+
mutations << [Pattern.new(query).compile, block]
21+
end
22+
23+
# This is the base visit method for each node in the tree. It first
24+
# creates a copy of the node using the visit_* methods defined below. Then
25+
# it checks each mutation in sequence and calls it if it finds a match.
1826
def visit(node)
1927
return unless node
20-
21-
stack << node
2228
result = node.accept(self)
2329

24-
stack.pop
25-
result
26-
end
30+
mutations.each do |(pattern, mutation)|
31+
result = mutation.call(result) if pattern.call(result)
32+
end
2733

28-
# This is a small helper to visit an array of nodes and return the result
29-
# of visiting them all.
30-
def visit_all(nodes)
31-
nodes.map { |node| visit(node) }
34+
result
3235
end
3336

3437
# Visit a BEGINBlock node.
@@ -435,6 +438,7 @@ def visit_ident(node)
435438
# Visit a If node.
436439
def visit_if(node)
437440
node.copy(
441+
predicate: visit(node.predicate),
438442
statements: visit(node.statements),
439443
consequent: visit(node.consequent)
440444
)
@@ -822,14 +826,18 @@ def visit_undef(node)
822826
# Visit a Unless node.
823827
def visit_unless(node)
824828
node.copy(
829+
predicate: visit(node.predicate),
825830
statements: visit(node.statements),
826831
consequent: visit(node.consequent)
827832
)
828833
end
829834

830835
# Visit a Until node.
831836
def visit_until(node)
832-
node.copy(statements: visit(node.statements))
837+
node.copy(
838+
predicate: visit(node.predicate),
839+
statements: visit(node.statements)
840+
)
833841
end
834842

835843
# Visit a VarField node.
@@ -868,7 +876,10 @@ def visit_when(node)
868876

869877
# Visit a While node.
870878
def visit_while(node)
871-
node.copy(statements: visit(node.statements))
879+
node.copy(
880+
predicate: visit(node.predicate),
881+
statements: visit(node.statements)
882+
)
872883
end
873884

874885
# Visit a Word node.

test/mutation_test.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "test_helper"
4+
5+
module SyntaxTree
6+
class MutationTest < Minitest::Test
7+
def test_mutates_based_on_patterns
8+
source = <<~RUBY
9+
if a = b
10+
c
11+
end
12+
RUBY
13+
14+
expected = <<~RUBY
15+
if (a = b)
16+
c
17+
end
18+
RUBY
19+
20+
program = SyntaxTree.parse(source).accept(build_mutation)
21+
assert_equal(expected, SyntaxTree::Formatter.format(source, program))
22+
end
23+
24+
private
25+
26+
def build_mutation
27+
SyntaxTree.mutation do |mutation|
28+
mutation.mutate("If[predicate: Assign | OpAssign]") do |node|
29+
# Get the existing If's predicate node
30+
predicate = node.predicate
31+
32+
# Create a new predicate node that wraps the existing predicate node
33+
# in parentheses
34+
predicate =
35+
SyntaxTree::Paren.new(
36+
lparen: SyntaxTree::LParen.default,
37+
contents: predicate,
38+
location: predicate.location
39+
)
40+
41+
# Return a copy of this node with the new predicate
42+
node.copy(predicate: predicate)
43+
end
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)