Skip to content

Commit 6ca43fa

Browse files
committed
Increase test coverage
1 parent 6fcf82c commit 6ca43fa

31 files changed

+757
-378
lines changed

.rubocop.yml

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ Layout/LineLength:
1616
Lint/DuplicateBranch:
1717
Enabled: false
1818

19+
Lint/EmptyBlock:
20+
Enabled: false
21+
1922
Lint/InterpolationCheck:
2023
Enabled: false
2124

@@ -55,6 +58,9 @@ Style/IfInsideElse:
5558
Style/KeywordParametersOrder:
5659
Enabled: false
5760

61+
Style/MissingRespondToMissing:
62+
Enabled: false
63+
5864
Style/MutableConstant:
5965
Enabled: false
6066

README.md

+22
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ It is built with only standard library dependencies. It additionally ships with
2929
- [pretty_print(q)](#pretty_printq)
3030
- [to_json(*opts)](#to_jsonopts)
3131
- [format(q)](#formatq)
32+
- [construct_keys](#construct_keys)
3233
- [Visitor](#visitor)
3334
- [visit_method](#visit_method)
3435
- [Language server](#language-server)
@@ -295,6 +296,27 @@ formatter.output.join
295296
# => "1 + 1"
296297
```
297298

299+
### construct_keys
300+
301+
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.
302+
303+
```ruby
304+
program = SyntaxTree.parse("1 + 1")
305+
puts program.construct_keys
306+
307+
# SyntaxTree::Program[
308+
# statements: SyntaxTree::Statements[
309+
# body: [
310+
# SyntaxTree::Binary[
311+
# left: SyntaxTree::Int[value: "1"],
312+
# operator: :+,
313+
# right: SyntaxTree::Int[value: "1"]
314+
# ]
315+
# ]
316+
# ]
317+
# ]
318+
```
319+
298320
## Visitor
299321

300322
If you want to operate over a set of nodes in the tree but don't want to walk the tree manually, the `Visitor` class makes it easy. `SyntaxTree::Visitor` is an implementation of the double dispatch visitor pattern. It works by the user defining visit methods that process nodes in the tree, which then call back to other visit methods to continue the descent. This is easier shown in code.

lib/syntax_tree.rb

+13-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@
3030
end
3131
end
3232

33+
# When PP is running, it expects that everything that interacts with it is going
34+
# to flow through PP.pp, since that's the main entry into the module from the
35+
# perspective of its uses in core Ruby. In doing so, it calls guard_inspect_key
36+
# at the top of the PP.pp method, which establishes some thread-local hashes to
37+
# check for cycles in the pretty printed tree. This means that if you want to
38+
# manually call pp on some object _before_ you have established these hashes,
39+
# you're going to break everything. So this call ensures that those hashes have
40+
# been set up before anything uses pp manually.
41+
PP.new(+"", 0).guard_inspect_key {}
42+
3343
# Syntax Tree is a suite of tools built on top of the internal CRuby parser. It
3444
# provides the ability to generate a syntax tree from source, as well as the
3545
# tools necessary to inspect and manipulate that syntax tree. It can be used to
@@ -67,8 +77,10 @@ def self.format(source)
6777
def self.read(filepath)
6878
encoding =
6979
File.open(filepath, "r") do |file|
80+
break Encoding.default_external if file.eof?
81+
7082
header = file.readline
71-
header += file.readline if header.start_with?("#!")
83+
header += file.readline if !file.eof? && header.start_with?("#!")
7284
Ripper.new(header).tap(&:parse).encoding
7385
end
7486

lib/syntax_tree/cli.rb

+2-12
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,7 @@ def run(handler, _filepath, source)
129129
# would match the input given.
130130
class Match < Action
131131
def run(handler, _filepath, source)
132-
formatter = Formatter.new(source, [])
133-
Visitor::MatchVisitor.new(formatter).visit(handler.parse(source))
134-
formatter.flush
135-
puts formatter.output.join
132+
puts handler.parse(source).construct_keys
136133
end
137134
end
138135

@@ -269,14 +266,7 @@ def run(argv)
269266
action.run(handler, filepath, source)
270267
rescue Parser::ParseError => error
271268
warn("Error: #{error.message}")
272-
273-
if error.lineno
274-
highlight_error(error, source)
275-
else
276-
warn(error.message)
277-
warn(error.backtrace)
278-
end
279-
269+
highlight_error(error, source)
280270
errored = true
281271
rescue Check::UnformattedError, Debug::NonIdempotentFormatError
282272
errored = true

lib/syntax_tree/node.rb

+4-9
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ def to_json(*opts)
121121
visitor = Visitor::JSONVisitor.new
122122
visitor.visit(self).to_json(*opts)
123123
end
124+
125+
def construct_keys
126+
PP.format(+"") { |q| Visitor::MatchVisitor.new(q).visit(self) }
127+
end
124128
end
125129

126130
# BEGINBlock represents the use of the +BEGIN+ keyword, which hooks into the
@@ -7090,15 +7094,6 @@ def child_nodes
70907094
def deconstruct_keys(_keys)
70917095
{ value: value, location: location }
70927096
end
7093-
7094-
def pretty_print(q)
7095-
q.group(2, "(", ")") do
7096-
q.text("qsymbols_beg")
7097-
7098-
q.breakable
7099-
q.pp(value)
7100-
end
7101-
end
71027097
end
71037098

71047099
# QWords represents a string literal array without interpolation.

lib/syntax_tree/visitor/field_visitor.rb

+42-47
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,46 @@ class Visitor
99
#
1010
# In order to properly use this class, you will need to subclass it and
1111
# implement #comments, #field, #list, #node, #pairs, and #text. Those are
12-
# documented at the bottom of this file.
12+
# documented here.
13+
#
14+
# == comments(node)
15+
#
16+
# This accepts the node that is being visited and does something depending
17+
# on the comments attached to the node.
18+
#
19+
# == field(name, value)
20+
#
21+
# This accepts the name of the field being visited as a string (like
22+
# "value") and the actual value of that field. The value can be a subclass
23+
# of Node or any other type that can be held within the tree.
24+
#
25+
# == list(name, values)
26+
#
27+
# This accepts the name of the field being visited as well as a list of
28+
# values. This is used, for example, when visiting something like the body
29+
# of a Statements node.
30+
#
31+
# == node(name, node)
32+
#
33+
# This is the parent serialization method for each node. It is called with
34+
# the node itself, as well as the type of the node as a string. The type
35+
# is an internally used value that usually resembles the name of the
36+
# ripper event that generated the node. The method should yield to the
37+
# given block which then calls through to visit each of the fields on the
38+
# node.
39+
#
40+
# == text(name, value)
41+
#
42+
# This accepts the name of the field being visited as well as a string
43+
# value representing the value of the field.
44+
#
45+
# == pairs(name, values)
46+
#
47+
# This accepts the name of the field being visited as well as a list of
48+
# pairs that represent the value of the field. It is used only in a couple
49+
# of circumstances, like when visiting the list of optional parameters
50+
# defined on a method.
51+
#
1352
class FieldVisitor < Visitor
1453
attr_reader :q
1554

@@ -220,7 +259,7 @@ def visit_class(node)
220259
end
221260

222261
def visit_comma(node)
223-
node(node, "comma") { field("value", node) }
262+
node(node, "comma") { field("value", node.value) }
224263
end
225264

226265
def visit_command(node)
@@ -491,7 +530,7 @@ def visit_if_mod(node)
491530
end
492531

493532
def visit_if_op(node)
494-
node(node, "ifop") do
533+
node(node, "if_op") do
495534
field("predicate", node.predicate)
496535
field("truthy", node.truthy)
497536
field("falsy", node.falsy)
@@ -1065,50 +1104,6 @@ def visit___end__(node)
10651104

10661105
private
10671106

1068-
# This accepts the node that is being visited and does something depending
1069-
# on the comments attached to the node.
1070-
def comments(node)
1071-
raise NotImplementedError
1072-
end
1073-
1074-
# This accepts the name of the field being visited as a string (like
1075-
# "value") and the actual value of that field. The value can be a subclass
1076-
# of Node or any other type that can be held within the tree.
1077-
def field(name, value)
1078-
raise NotImplementedError
1079-
end
1080-
1081-
# This accepts the name of the field being visited as well as a list of
1082-
# values. This is used, for example, when visiting something like the body
1083-
# of a Statements node.
1084-
def list(name, values)
1085-
raise NotImplementedError
1086-
end
1087-
1088-
# This is the parent serialization method for each node. It is called with
1089-
# the node itself, as well as the type of the node as a string. The type
1090-
# is an internally used value that usually resembles the name of the
1091-
# ripper event that generated the node. The method should yield to the
1092-
# given block which then calls through to visit each of the fields on the
1093-
# node.
1094-
def node(node, type)
1095-
raise NotImplementedError
1096-
end
1097-
1098-
# This accepts the name of the field being visited as well as a string
1099-
# value representing the value of the field.
1100-
def text(name, value)
1101-
raise NotImplementedError
1102-
end
1103-
1104-
# This accepts the name of the field being visited as well as a list of
1105-
# pairs that represent the value of the field. It is used only in a couple
1106-
# of circumstances, like when visiting the list of optional parameters
1107-
# defined on a method.
1108-
def pairs(name, values)
1109-
raise NotImplementedError
1110-
end
1111-
11121107
def visit_token(node, type)
11131108
node(node, type) do
11141109
field("value", node.value)

lib/syntax_tree/visitor/match_visitor.rb

+11-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ def initialize(q)
1212
end
1313

1414
def visit(node)
15-
if node.is_a?(Node)
15+
case node
16+
when Node
1617
super
18+
when String
19+
# pp will split up a string on newlines and concat them together using
20+
# a "+" operator. This breaks the pattern matching expression. So
21+
# instead we're going to check here for strings and manually put the
22+
# entire value into the output buffer.
23+
q.text(node.inspect)
1724
else
18-
node.pretty_print(q)
25+
q.pp(node)
1926
end
2027
end
2128

@@ -28,7 +35,7 @@ def comments(node)
2835
q.text("comments: [")
2936
q.indent do
3037
q.breakable("")
31-
q.seplist(node.comments) { |comment| comment.pretty_print(q) }
38+
q.seplist(node.comments) { |comment| visit(comment) }
3239
end
3340
q.breakable("")
3441
q.text("]")
@@ -107,7 +114,7 @@ def text(name, value)
107114
q.nest(0) do
108115
q.text(name)
109116
q.text(": ")
110-
value.pretty_print(q)
117+
q.pp(value)
111118
end
112119
end
113120
end

lib/syntax_tree/visitor/pretty_print_visitor.rb

+18-12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ def initialize(q)
1010
@q = q
1111
end
1212

13+
# This is here because we need to make sure the operator is cast to a
14+
# string before we print it out.
15+
def visit_binary(node)
16+
node(node, "binary") do
17+
field("left", node.left)
18+
text("operator", node.operator.to_s)
19+
field("right", node.right)
20+
comments(node)
21+
end
22+
end
23+
24+
# This is here to make it a little nicer to look at labels since they
25+
# typically have their : at the end of the value.
1326
def visit_label(node)
1427
node(node, "label") do
1528
q.breakable
@@ -26,25 +39,18 @@ def comments(node)
2639

2740
q.breakable
2841
q.group(2, "(", ")") do
29-
q.seplist(node.comments) { |comment| comment.pretty_print(q) }
42+
q.seplist(node.comments) { |comment| q.pp(comment) }
3043
end
3144
end
3245

3346
def field(_name, value)
3447
q.breakable
35-
36-
# I don't entirely know why this is necessary, but in Ruby 2.7 there is
37-
# an issue with calling q.pp on strings that somehow involves inspect
38-
# keys. I'm purposefully avoiding the inspect key stuff here because I
39-
# know the tree does not contain any cycles.
40-
value.is_a?(String) ? q.text(value.inspect) : value.pretty_print(q)
48+
q.pp(value)
4149
end
4250

4351
def list(_name, values)
4452
q.breakable
45-
q.group(2, "(", ")") do
46-
q.seplist(values) { |value| value.pretty_print(q) }
47-
end
53+
q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } }
4854
end
4955

5056
def node(_node, type)
@@ -57,13 +63,13 @@ def node(_node, type)
5763
def pairs(_name, values)
5864
q.group(2, "(", ")") do
5965
q.seplist(values) do |(key, value)|
60-
key.pretty_print(q)
66+
q.pp(key)
6167

6268
if value
6369
q.text("=")
6470
q.group(2) do
6571
q.breakable("")
66-
value.pretty_print(q)
72+
q.pp(value)
6773
end
6874
end
6975
end

0 commit comments

Comments
 (0)