syntax_tree 5.3.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -1
  3. data/CHANGELOG.md +64 -1
  4. data/Gemfile.lock +2 -2
  5. data/README.md +28 -9
  6. data/Rakefile +12 -8
  7. data/bin/console +1 -0
  8. data/bin/whitequark +79 -0
  9. data/doc/changing_structure.md +16 -0
  10. data/lib/syntax_tree/basic_visitor.rb +44 -5
  11. data/lib/syntax_tree/cli.rb +2 -2
  12. data/lib/syntax_tree/dsl.rb +23 -11
  13. data/lib/syntax_tree/{visitor/field_visitor.rb → field_visitor.rb} +54 -55
  14. data/lib/syntax_tree/formatter.rb +1 -1
  15. data/lib/syntax_tree/index.rb +56 -54
  16. data/lib/syntax_tree/json_visitor.rb +55 -0
  17. data/lib/syntax_tree/language_server.rb +157 -2
  18. data/lib/syntax_tree/match_visitor.rb +120 -0
  19. data/lib/syntax_tree/mermaid.rb +177 -0
  20. data/lib/syntax_tree/mermaid_visitor.rb +69 -0
  21. data/lib/syntax_tree/{visitor/mutation_visitor.rb → mutation_visitor.rb} +27 -27
  22. data/lib/syntax_tree/node.rb +198 -107
  23. data/lib/syntax_tree/parser.rb +322 -118
  24. data/lib/syntax_tree/pretty_print_visitor.rb +83 -0
  25. data/lib/syntax_tree/reflection.rb +241 -0
  26. data/lib/syntax_tree/translation/parser.rb +3019 -0
  27. data/lib/syntax_tree/translation/rubocop_ast.rb +21 -0
  28. data/lib/syntax_tree/translation.rb +28 -0
  29. data/lib/syntax_tree/version.rb +1 -1
  30. data/lib/syntax_tree/with_scope.rb +244 -0
  31. data/lib/syntax_tree/yarv/basic_block.rb +53 -0
  32. data/lib/syntax_tree/yarv/calldata.rb +91 -0
  33. data/lib/syntax_tree/yarv/compiler.rb +110 -100
  34. data/lib/syntax_tree/yarv/control_flow_graph.rb +257 -0
  35. data/lib/syntax_tree/yarv/data_flow_graph.rb +338 -0
  36. data/lib/syntax_tree/yarv/decompiler.rb +1 -1
  37. data/lib/syntax_tree/yarv/disassembler.rb +104 -80
  38. data/lib/syntax_tree/yarv/instruction_sequence.rb +43 -18
  39. data/lib/syntax_tree/yarv/instructions.rb +203 -649
  40. data/lib/syntax_tree/yarv/legacy.rb +12 -24
  41. data/lib/syntax_tree/yarv/sea_of_nodes.rb +534 -0
  42. data/lib/syntax_tree/yarv.rb +18 -0
  43. data/lib/syntax_tree.rb +88 -56
  44. data/tasks/sorbet.rake +277 -0
  45. data/tasks/whitequark.rake +87 -0
  46. metadata +23 -11
  47. data/.gitmodules +0 -9
  48. data/lib/syntax_tree/language_server/inlay_hints.rb +0 -159
  49. data/lib/syntax_tree/visitor/environment.rb +0 -84
  50. data/lib/syntax_tree/visitor/json_visitor.rb +0 -55
  51. data/lib/syntax_tree/visitor/match_visitor.rb +0 -122
  52. data/lib/syntax_tree/visitor/pretty_print_visitor.rb +0 -85
  53. data/lib/syntax_tree/visitor/with_environment.rb +0 -140
@@ -1,55 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxTree
4
- class Visitor
5
- # This is the parent class of a lot of built-in visitors for Syntax Tree. It
6
- # reflects visiting each of the fields on every node in turn. It itself does
7
- # not do anything with these fields, it leaves that behavior up to the
8
- # subclass to implement.
9
- #
10
- # In order to properly use this class, you will need to subclass it and
11
- # implement #comments, #field, #list, #node, #pairs, and #text. Those are
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
- #
52
- class FieldVisitor < BasicVisitor
4
+ # This is the parent class of a lot of built-in visitors for Syntax Tree. It
5
+ # reflects visiting each of the fields on every node in turn. It itself does
6
+ # not do anything with these fields, it leaves that behavior up to the
7
+ # subclass to implement.
8
+ #
9
+ # In order to properly use this class, you will need to subclass it and
10
+ # implement #comments, #field, #list, #node, #pairs, and #text. Those are
11
+ # documented here.
12
+ #
13
+ # == comments(node)
14
+ #
15
+ # This accepts the node that is being visited and does something depending on
16
+ # the comments attached to the node.
17
+ #
18
+ # == field(name, value)
19
+ #
20
+ # This accepts the name of the field being visited as a string (like "value")
21
+ # and the actual value of that field. The value can be a subclass of Node or
22
+ # any other type that can be held within the tree.
23
+ #
24
+ # == list(name, values)
25
+ #
26
+ # This accepts the name of the field being visited as well as a list of
27
+ # values. This is used, for example, when visiting something like the body of
28
+ # a Statements node.
29
+ #
30
+ # == node(name, node)
31
+ #
32
+ # This is the parent serialization method for each node. It is called with the
33
+ # node itself, as well as the type of the node as a string. The type is an
34
+ # internally used value that usually resembles the name of the ripper event
35
+ # that generated the node. The method should yield to the given block which
36
+ # then calls through to visit each of the fields on the node.
37
+ #
38
+ # == text(name, value)
39
+ #
40
+ # This accepts the name of the field being visited as well as a string value
41
+ # representing the value of the field.
42
+ #
43
+ # == pairs(name, values)
44
+ #
45
+ # This accepts the name of the field being visited as well as a list of pairs
46
+ # that represent the value of the field. It is used only in a couple of
47
+ # circumstances, like when visiting the list of optional parameters defined on
48
+ # a method.
49
+ #
50
+ class FieldVisitor < BasicVisitor
51
+ visit_methods do
53
52
  def visit_aref(node)
54
53
  node(node, "aref") do
55
54
  field("collection", node.collection)
@@ -1017,14 +1016,14 @@ module SyntaxTree
1017
1016
  def visit___end__(node)
1018
1017
  visit_token(node, "__end__")
1019
1018
  end
1019
+ end
1020
1020
 
1021
- private
1021
+ private
1022
1022
 
1023
- def visit_token(node, type)
1024
- node(node, type) do
1025
- field("value", node.value)
1026
- comments(node)
1027
- end
1023
+ def visit_token(node, type)
1024
+ node(node, type) do
1025
+ field("value", node.value)
1026
+ comments(node)
1028
1027
  end
1029
1028
  end
1030
1029
  end
@@ -138,7 +138,7 @@ module SyntaxTree
138
138
  # going to just print out the node as it was seen in the source.
139
139
  doc =
140
140
  if last_leading&.ignore?
141
- range = source[node.location.start_char...node.location.end_char]
141
+ range = source[node.start_char...node.end_char]
142
142
  first = true
143
143
 
144
144
  range.each_line(chomp: true) do |line|
@@ -257,74 +257,76 @@ module SyntaxTree
257
257
  @statements = nil
258
258
  end
259
259
 
260
- def visit_class(node)
261
- name = visit(node.constant).to_sym
262
- location =
263
- Location.new(node.location.start_line, node.location.start_column)
264
-
265
- results << ClassDefinition.new(
266
- nesting.dup,
267
- name,
268
- location,
269
- comments_for(node)
270
- )
271
-
272
- nesting << name
273
- super
274
- nesting.pop
275
- end
276
-
277
- def visit_const_ref(node)
278
- node.constant.value
279
- end
260
+ visit_methods do
261
+ def visit_class(node)
262
+ name = visit(node.constant).to_sym
263
+ location =
264
+ Location.new(node.location.start_line, node.location.start_column)
280
265
 
281
- def visit_def(node)
282
- name = node.name.value.to_sym
283
- location =
284
- Location.new(node.location.start_line, node.location.start_column)
285
-
286
- results << if node.target.nil?
287
- MethodDefinition.new(
266
+ results << ClassDefinition.new(
288
267
  nesting.dup,
289
268
  name,
290
269
  location,
291
270
  comments_for(node)
292
271
  )
293
- else
294
- SingletonMethodDefinition.new(
272
+
273
+ nesting << name
274
+ super
275
+ nesting.pop
276
+ end
277
+
278
+ def visit_const_ref(node)
279
+ node.constant.value
280
+ end
281
+
282
+ def visit_def(node)
283
+ name = node.name.value.to_sym
284
+ location =
285
+ Location.new(node.location.start_line, node.location.start_column)
286
+
287
+ results << if node.target.nil?
288
+ MethodDefinition.new(
289
+ nesting.dup,
290
+ name,
291
+ location,
292
+ comments_for(node)
293
+ )
294
+ else
295
+ SingletonMethodDefinition.new(
296
+ nesting.dup,
297
+ name,
298
+ location,
299
+ comments_for(node)
300
+ )
301
+ end
302
+ end
303
+
304
+ def visit_module(node)
305
+ name = visit(node.constant).to_sym
306
+ location =
307
+ Location.new(node.location.start_line, node.location.start_column)
308
+
309
+ results << ModuleDefinition.new(
295
310
  nesting.dup,
296
311
  name,
297
312
  location,
298
313
  comments_for(node)
299
314
  )
300
- end
301
- end
302
-
303
- def visit_module(node)
304
- name = visit(node.constant).to_sym
305
- location =
306
- Location.new(node.location.start_line, node.location.start_column)
307
315
 
308
- results << ModuleDefinition.new(
309
- nesting.dup,
310
- name,
311
- location,
312
- comments_for(node)
313
- )
314
-
315
- nesting << name
316
- super
317
- nesting.pop
318
- end
316
+ nesting << name
317
+ super
318
+ nesting.pop
319
+ end
319
320
 
320
- def visit_program(node)
321
- super
322
- results
323
- end
321
+ def visit_program(node)
322
+ super
323
+ results
324
+ end
324
325
 
325
- def visit_statements(node)
326
- @statements = node
327
- super
326
+ def visit_statements(node)
327
+ @statements = node
328
+ super
329
+ end
328
330
  end
329
331
 
330
332
  private
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SyntaxTree
6
+ # This visitor transforms the AST into a hash that contains only primitives
7
+ # that can be easily serialized into JSON.
8
+ class JSONVisitor < FieldVisitor
9
+ attr_reader :target
10
+
11
+ def initialize
12
+ @target = nil
13
+ end
14
+
15
+ private
16
+
17
+ def comments(node)
18
+ target[:comments] = visit_all(node.comments)
19
+ end
20
+
21
+ def field(name, value)
22
+ target[name] = value.is_a?(Node) ? visit(value) : value
23
+ end
24
+
25
+ def list(name, values)
26
+ target[name] = visit_all(values)
27
+ end
28
+
29
+ def node(node, type)
30
+ previous = @target
31
+ @target = { type: type, location: visit_location(node.location) }
32
+ yield
33
+ @target
34
+ ensure
35
+ @target = previous
36
+ end
37
+
38
+ def pairs(name, values)
39
+ target[name] = values.map { |(key, value)| [visit(key), visit(value)] }
40
+ end
41
+
42
+ def text(name, value)
43
+ target[name] = value
44
+ end
45
+
46
+ def visit_location(location)
47
+ [
48
+ location.start_line,
49
+ location.start_char,
50
+ location.end_line,
51
+ location.end_char
52
+ ]
53
+ end
54
+ end
55
+ end
@@ -2,10 +2,9 @@
2
2
 
3
3
  require "cgi"
4
4
  require "json"
5
+ require "pp"
5
6
  require "uri"
6
7
 
7
- require_relative "language_server/inlay_hints"
8
-
9
8
  module SyntaxTree
10
9
  # Syntax Tree additionally ships with a language server conforming to the
11
10
  # language server protocol. It can be invoked through the CLI by running:
@@ -13,6 +12,162 @@ module SyntaxTree
13
12
  # stree lsp
14
13
  #
15
14
  class LanguageServer
15
+ # This class provides inlay hints for the language server. For more
16
+ # information, see the spec here:
17
+ # https://github.com/microsoft/language-server-protocol/issues/956.
18
+ class InlayHints < Visitor
19
+ # This represents a hint that is going to be displayed in the editor.
20
+ class Hint
21
+ attr_reader :line, :character, :label
22
+
23
+ def initialize(line:, character:, label:)
24
+ @line = line
25
+ @character = character
26
+ @label = label
27
+ end
28
+
29
+ # This is the shape that the LSP expects.
30
+ def to_json(*opts)
31
+ {
32
+ position: {
33
+ line: line,
34
+ character: character
35
+ },
36
+ label: label
37
+ }.to_json(*opts)
38
+ end
39
+ end
40
+
41
+ attr_reader :stack, :hints
42
+
43
+ def initialize
44
+ @stack = []
45
+ @hints = []
46
+ end
47
+
48
+ def visit(node)
49
+ stack << node
50
+ result = super
51
+ stack.pop
52
+ result
53
+ end
54
+
55
+ visit_methods do
56
+ # Adds parentheses around assignments contained within the default
57
+ # values of parameters. For example,
58
+ #
59
+ # def foo(a = b = c)
60
+ # end
61
+ #
62
+ # becomes
63
+ #
64
+ # def foo(a = ₍b = c₎)
65
+ # end
66
+ #
67
+ def visit_assign(node)
68
+ parentheses(node.location) if stack[-2].is_a?(Params)
69
+ super
70
+ end
71
+
72
+ # Adds parentheses around binary expressions to make it clear which
73
+ # subexpression will be evaluated first. For example,
74
+ #
75
+ # a + b * c
76
+ #
77
+ # becomes
78
+ #
79
+ # a + ₍b * c₎
80
+ #
81
+ def visit_binary(node)
82
+ case stack[-2]
83
+ when Assign, OpAssign
84
+ parentheses(node.location)
85
+ when Binary
86
+ parentheses(node.location) if stack[-2].operator != node.operator
87
+ end
88
+
89
+ super
90
+ end
91
+
92
+ # Adds parentheses around ternary operators contained within certain
93
+ # expressions where it could be confusing which subexpression will get
94
+ # evaluated first. For example,
95
+ #
96
+ # a ? b : c ? d : e
97
+ #
98
+ # becomes
99
+ #
100
+ # a ? b : ₍c ? d : e₎
101
+ #
102
+ def visit_if_op(node)
103
+ case stack[-2]
104
+ when Assign, Binary, IfOp, OpAssign
105
+ parentheses(node.location)
106
+ end
107
+
108
+ super
109
+ end
110
+
111
+ # Adds the implicitly rescued StandardError into a bare rescue clause.
112
+ # For example,
113
+ #
114
+ # begin
115
+ # rescue
116
+ # end
117
+ #
118
+ # becomes
119
+ #
120
+ # begin
121
+ # rescue StandardError
122
+ # end
123
+ #
124
+ def visit_rescue(node)
125
+ if node.exception.nil?
126
+ hints << Hint.new(
127
+ line: node.location.start_line - 1,
128
+ character: node.location.start_column + "rescue".length,
129
+ label: " StandardError"
130
+ )
131
+ end
132
+
133
+ super
134
+ end
135
+
136
+ # Adds parentheses around unary statements using the - operator that are
137
+ # contained within Binary nodes. For example,
138
+ #
139
+ # -a + b
140
+ #
141
+ # becomes
142
+ #
143
+ # ₍-a₎ + b
144
+ #
145
+ def visit_unary(node)
146
+ if stack[-2].is_a?(Binary) && (node.operator == "-")
147
+ parentheses(node.location)
148
+ end
149
+
150
+ super
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def parentheses(location)
157
+ hints << Hint.new(
158
+ line: location.start_line - 1,
159
+ character: location.start_column,
160
+ label: "₍"
161
+ )
162
+
163
+ hints << Hint.new(
164
+ line: location.end_line - 1,
165
+ character: location.end_column,
166
+ label: "₎"
167
+ )
168
+ end
169
+ end
170
+
16
171
  # This is a small module that effectively mirrors pattern matching. We're
17
172
  # using it so that we can support truffleruby without having to ignore the
18
173
  # language server.
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ # This visitor transforms the AST into a Ruby pattern matching expression that
5
+ # would match correctly against the AST.
6
+ class MatchVisitor < FieldVisitor
7
+ attr_reader :q
8
+
9
+ def initialize(q)
10
+ @q = q
11
+ end
12
+
13
+ def visit(node)
14
+ case node
15
+ when Node
16
+ super
17
+ when String
18
+ # pp will split up a string on newlines and concat them together using a
19
+ # "+" operator. This breaks the pattern matching expression. So instead
20
+ # we're going to check here for strings and manually put the entire
21
+ # value into the output buffer.
22
+ q.text(node.inspect)
23
+ else
24
+ node.pretty_print(q)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def comments(node)
31
+ return if node.comments.empty?
32
+
33
+ q.nest(0) do
34
+ q.text("comments: [")
35
+ q.indent do
36
+ q.breakable("")
37
+ q.seplist(node.comments) { |comment| visit(comment) }
38
+ end
39
+ q.breakable("")
40
+ q.text("]")
41
+ end
42
+ end
43
+
44
+ def field(name, value)
45
+ q.nest(0) do
46
+ q.text(name)
47
+ q.text(": ")
48
+ visit(value)
49
+ end
50
+ end
51
+
52
+ def list(name, values)
53
+ q.group do
54
+ q.text(name)
55
+ q.text(": [")
56
+ q.indent do
57
+ q.breakable("")
58
+ q.seplist(values) { |value| visit(value) }
59
+ end
60
+ q.breakable("")
61
+ q.text("]")
62
+ end
63
+ end
64
+
65
+ def node(node, _type)
66
+ items = []
67
+ q.with_target(items) { yield }
68
+
69
+ if items.empty?
70
+ q.text(node.class.name)
71
+ return
72
+ end
73
+
74
+ q.group do
75
+ q.text(node.class.name)
76
+ q.text("[")
77
+ q.indent do
78
+ q.breakable("")
79
+ q.seplist(items) { |item| q.target << item }
80
+ end
81
+ q.breakable("")
82
+ q.text("]")
83
+ end
84
+ end
85
+
86
+ def pairs(name, values)
87
+ q.group do
88
+ q.text(name)
89
+ q.text(": [")
90
+ q.indent do
91
+ q.breakable("")
92
+ q.seplist(values) do |(key, value)|
93
+ q.group do
94
+ q.text("[")
95
+ q.indent do
96
+ q.breakable("")
97
+ visit(key)
98
+ q.text(",")
99
+ q.breakable
100
+ visit(value || nil)
101
+ end
102
+ q.breakable("")
103
+ q.text("]")
104
+ end
105
+ end
106
+ end
107
+ q.breakable("")
108
+ q.text("]")
109
+ end
110
+ end
111
+
112
+ def text(name, value)
113
+ q.nest(0) do
114
+ q.text(name)
115
+ q.text(": ")
116
+ value.pretty_print(q)
117
+ end
118
+ end
119
+ end
120
+ end