syntax_tree 6.0.2 → 6.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20b5dfab3c47b63217fc1b3a6f1fbbd56f51e8358e4b3514e6bf8fd9da083ed6
4
- data.tar.gz: f4c18e39ae2b5cb14aae4de6d95494959d41ac78757b01da621b68d514c59bfb
3
+ metadata.gz: 7515a827509d352259e6cae476930093fe382fcc35c248fba3ab716019e69ca9
4
+ data.tar.gz: add2625387abe5616f90d28843f4fcebac5b68222f9a6258cbfb04fbd41b4772
5
5
  SHA512:
6
- metadata.gz: 35bff4e2d47a2141e5014771cd30f268408e44ff59d247895dc52bee18f4ff7ebcd163e6ce464eb9908e2027ac7a86785cd5c5573baadfaadb3f690e3689526d
7
- data.tar.gz: 82722a335946c31d43753a51efc954e8f28a4e39015dd01c3a983f7cdcd6f2dff9e90cb9046b00c2508ee6d9654e33f1760e40c9b1fd70fdf6343fa74855f8b1
6
+ metadata.gz: ea0a666bd65180facaab89acd7b7bd87d3f5208c69a0fe80b91d1d25d57cdc2c281ac301070da8d73c7d119574a7f7de30c0facb0d7131f895bbc8fa7fe582c5
7
+ data.tar.gz: 4c9df489dcb876280a219a8e213a800f0544a0aaa02a303d8fc3ba948e07e195dbfb21da8f8f04825abc4bf61fb9ec92141d5a00cc7b3d53b1d6103d58d10be3
data/.gitignore CHANGED
@@ -4,6 +4,7 @@
4
4
  /coverage/
5
5
  /pkg/
6
6
  /rdocs/
7
+ /sorbet/
7
8
  /spec/reports/
8
9
  /tmp/
9
10
  /vendor/
data/.rubocop.yml CHANGED
@@ -7,7 +7,7 @@ AllCops:
7
7
  SuggestExtensions: false
8
8
  TargetRubyVersion: 2.7
9
9
  Exclude:
10
- - '{.git,.github,bin,coverage,pkg,spec,test/fixtures,vendor,tmp}/**/*'
10
+ - '{.git,.github,bin,coverage,pkg,sorbet,spec,test/fixtures,vendor,tmp}/**/*'
11
11
  - test.rb
12
12
 
13
13
  Gemspec/DevelopmentDependencies:
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.0
data/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [6.1.0] - 2023-03-20
10
+
11
+ ### Added
12
+
13
+ - The `stree ctags` command for generating ctags like `universal-ctags` or `ripper-tags` would.
14
+ - The `definedivar` YARV instruction has been added to match CRuby's implementation.
15
+ - We now generate better Sorbet RBI files for the nodes in the tree and the visitors.
16
+ - `SyntaxTree::Reflection.nodes` now includes the visitor method.
17
+
18
+ ### Changed
19
+
20
+ - We now explicitly require `pp` in environments that need it.
21
+
9
22
  ## [6.0.2] - 2023-03-03
10
23
 
11
24
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_tree (6.0.2)
4
+ syntax_tree (6.1.0)
5
5
  prettier_print (>= 1.2.0)
6
6
 
7
7
  GEM
@@ -10,16 +10,16 @@ GEM
10
10
  ast (2.4.2)
11
11
  docile (1.4.0)
12
12
  json (2.6.3)
13
- minitest (5.17.0)
13
+ minitest (5.18.0)
14
14
  parallel (1.22.1)
15
- parser (3.2.1.0)
15
+ parser (3.2.1.1)
16
16
  ast (~> 2.4.1)
17
- prettier_print (1.2.0)
17
+ prettier_print (1.2.1)
18
18
  rainbow (3.1.1)
19
19
  rake (13.0.6)
20
20
  regexp_parser (2.7.0)
21
21
  rexml (3.2.5)
22
- rubocop (1.47.0)
22
+ rubocop (1.48.1)
23
23
  json (~> 2.3)
24
24
  parallel (~> 1.10)
25
25
  parser (>= 3.2.0.0)
@@ -31,7 +31,7 @@ GEM
31
31
  unicode-display_width (>= 2.4.0, < 3.0)
32
32
  rubocop-ast (1.27.0)
33
33
  parser (>= 3.2.1.0)
34
- ruby-progressbar (1.12.0)
34
+ ruby-progressbar (1.13.0)
35
35
  simplecov (0.22.0)
36
36
  docile (~> 1.1)
37
37
  simplecov-html (~> 0.11)
data/README.md CHANGED
@@ -15,6 +15,7 @@ It is built with only standard library dependencies. It additionally ships with
15
15
  - [CLI](#cli)
16
16
  - [ast](#ast)
17
17
  - [check](#check)
18
+ - [ctags](#ctags)
18
19
  - [expr](#expr)
19
20
  - [format](#format)
20
21
  - [json](#json)
@@ -139,6 +140,33 @@ To change the print width that you are checking against, specify the `--print-wi
139
140
  stree check --print-width=100 path/to/file.rb
140
141
  ```
141
142
 
143
+ ### ctags
144
+
145
+ This command will output to stdout a set of tags suitable for usage with [ctags](https://github.com/universal-ctags/ctags).
146
+
147
+ ```sh
148
+ stree ctags path/to/file.rb
149
+ ```
150
+
151
+ For a file containing the following Ruby code:
152
+
153
+ ```ruby
154
+ class Foo
155
+ end
156
+
157
+ class Bar < Foo
158
+ end
159
+ ```
160
+
161
+ you will receive:
162
+
163
+ ```
164
+ !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
165
+ !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
166
+ Bar test.rb /^class Bar < Foo$/;" c inherits:Foo
167
+ Foo test.rb /^class Foo$/;" c
168
+ ```
169
+
142
170
  ### expr
143
171
 
144
172
  This command will output a Ruby case-match expression that would match correctly against the first expression of the input.
@@ -788,6 +816,7 @@ inherit_gem:
788
816
  * [Neovim](https://neovim.io/) - [neovim/nvim-lspconfig](https://github.com/neovim/nvim-lspconfig).
789
817
  * [Vim](https://www.vim.org/) - [dense-analysis/ale](https://github.com/dense-analysis/ale).
790
818
  * [VSCode](https://code.visualstudio.com/) - [ruby-syntax-tree/vscode-syntax-tree](https://github.com/ruby-syntax-tree/vscode-syntax-tree).
819
+ * [Emacs](https://www.gnu.org/software/emacs/) - [emacs-format-all-the-code](https://github.com/lassik/emacs-format-all-the-code).
791
820
 
792
821
  ## Contributing
793
822
 
@@ -154,6 +154,92 @@ module SyntaxTree
154
154
  end
155
155
  end
156
156
 
157
+ # An action of the CLI that generates ctags for the given source.
158
+ class CTags < Action
159
+ attr_reader :entries
160
+
161
+ def initialize(options)
162
+ super(options)
163
+ @entries = []
164
+ end
165
+
166
+ def run(item)
167
+ lines = item.source.lines(chomp: true)
168
+
169
+ SyntaxTree
170
+ .index(item.source)
171
+ .each do |entry|
172
+ line = lines[entry.location.line - 1]
173
+ pattern = "/^#{line.gsub("\\", "\\\\\\\\").gsub("/", "\\/")}$/;\""
174
+
175
+ entries << case entry
176
+ when SyntaxTree::Index::ModuleDefinition
177
+ parts = [entry.name, item.filepath, pattern, "m"]
178
+
179
+ if entry.nesting != [[entry.name]]
180
+ parts << "class:#{entry.nesting.flatten.tap(&:pop).join(".")}"
181
+ end
182
+
183
+ parts.join("\t")
184
+ when SyntaxTree::Index::ClassDefinition
185
+ parts = [entry.name, item.filepath, pattern, "c"]
186
+
187
+ if entry.nesting != [[entry.name]]
188
+ parts << "class:#{entry.nesting.flatten.tap(&:pop).join(".")}"
189
+ end
190
+
191
+ unless entry.superclass.empty?
192
+ inherits = entry.superclass.join(".").delete_prefix(".")
193
+ parts << "inherits:#{inherits}"
194
+ end
195
+
196
+ parts.join("\t")
197
+ when SyntaxTree::Index::MethodDefinition
198
+ parts = [entry.name, item.filepath, pattern, "f"]
199
+
200
+ unless entry.nesting.empty?
201
+ parts << "class:#{entry.nesting.flatten.join(".")}"
202
+ end
203
+
204
+ parts.join("\t")
205
+ when SyntaxTree::Index::SingletonMethodDefinition
206
+ parts = [entry.name, item.filepath, pattern, "F"]
207
+
208
+ unless entry.nesting.empty?
209
+ parts << "class:#{entry.nesting.flatten.join(".")}"
210
+ end
211
+
212
+ parts.join("\t")
213
+ when SyntaxTree::Index::AliasMethodDefinition
214
+ parts = [entry.name, item.filepath, pattern, "a"]
215
+
216
+ unless entry.nesting.empty?
217
+ parts << "class:#{entry.nesting.flatten.join(".")}"
218
+ end
219
+
220
+ parts.join("\t")
221
+ when SyntaxTree::Index::ConstantDefinition
222
+ parts = [entry.name, item.filepath, pattern, "C"]
223
+
224
+ unless entry.nesting.empty?
225
+ parts << "class:#{entry.nesting.flatten.join(".")}"
226
+ end
227
+
228
+ parts.join("\t")
229
+ end
230
+ end
231
+ end
232
+
233
+ def success
234
+ puts(<<~HEADER)
235
+ !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
236
+ !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
237
+ HEADER
238
+
239
+ entries.sort.each { |entry| puts(entry) }
240
+ end
241
+ end
242
+
157
243
  # An action of the CLI that formats the source twice to check if the first
158
244
  # format is not idempotent.
159
245
  class Debug < Action
@@ -327,6 +413,9 @@ module SyntaxTree
327
413
  #{Color.bold("stree check [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
328
414
  Check that the given files are formatted as syntax tree would format them
329
415
 
416
+ #{Color.bold("stree ctags [-e SCRIPT] FILE")}
417
+ Print out a ctags-compatible index of the given files
418
+
330
419
  #{Color.bold("stree debug [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
331
420
  Check that the given files can be formatted idempotently
332
421
 
@@ -488,6 +577,8 @@ module SyntaxTree
488
577
  AST.new(options)
489
578
  when "c", "check"
490
579
  Check.new(options)
580
+ when "ctags"
581
+ CTags.new(options)
491
582
  when "debug"
492
583
  Debug.new(options)
493
584
  when "doc"
@@ -31,6 +31,18 @@ module SyntaxTree
31
31
  end
32
32
  end
33
33
 
34
+ # This entry represents a constant assignment.
35
+ class ConstantDefinition
36
+ attr_reader :nesting, :name, :location, :comments
37
+
38
+ def initialize(nesting, name, location, comments)
39
+ @nesting = nesting
40
+ @name = name
41
+ @location = location
42
+ @comments = comments
43
+ end
44
+ end
45
+
34
46
  # This entry represents a module definition using the module keyword.
35
47
  class ModuleDefinition
36
48
  attr_reader :nesting, :name, :location, :comments
@@ -68,6 +80,19 @@ module SyntaxTree
68
80
  end
69
81
  end
70
82
 
83
+ # This entry represents a method definition that was created using the alias
84
+ # keyword.
85
+ class AliasMethodDefinition
86
+ attr_reader :nesting, :name, :location, :comments
87
+
88
+ def initialize(nesting, name, location, comments)
89
+ @nesting = nesting
90
+ @name = name
91
+ @location = location
92
+ @comments = comments
93
+ end
94
+ end
95
+
71
96
  # When you're using the instruction sequence backend, this class is used to
72
97
  # lazily parse comments out of the source code.
73
98
  class FileComments
@@ -178,7 +203,14 @@ module SyntaxTree
178
203
  end
179
204
 
180
205
  def find_constant_path(insns, index)
181
- index -= 1 while insns[index].is_a?(Integer)
206
+ index -= 1 while index >= 0 &&
207
+ (
208
+ insns[index].is_a?(Integer) ||
209
+ (
210
+ insns[index].is_a?(Array) &&
211
+ %i[swap topn].include?(insns[index][0])
212
+ )
213
+ )
182
214
  insn = insns[index]
183
215
 
184
216
  if insn.is_a?(Array) && insn[0] == :opt_getconstant_path
@@ -207,11 +239,43 @@ module SyntaxTree
207
239
  end
208
240
  end
209
241
 
242
+ def find_attr_arguments(insns, index)
243
+ orig_argc = insns[index][1][:orig_argc]
244
+ names = []
245
+
246
+ current = index - 1
247
+ while current >= 0 && names.length < orig_argc
248
+ if insns[current].is_a?(Array) && insns[current][0] == :putobject
249
+ names.unshift(insns[current][1])
250
+ end
251
+
252
+ current -= 1
253
+ end
254
+
255
+ names if insns[current] == [:putself] && names.length == orig_argc
256
+ end
257
+
258
+ def method_definition(nesting, name, location, file_comments)
259
+ comments = EntryComments.new(file_comments, location)
260
+
261
+ if nesting.last == [:singletonclass]
262
+ SingletonMethodDefinition.new(
263
+ nesting[0...-1],
264
+ name,
265
+ location,
266
+ comments
267
+ )
268
+ else
269
+ MethodDefinition.new(nesting, name, location, comments)
270
+ end
271
+ end
272
+
210
273
  def index_iseq(iseq, file_comments)
211
274
  results = []
212
275
  queue = [[iseq, []]]
213
276
 
214
277
  while (current_iseq, current_nesting = queue.shift)
278
+ file = current_iseq[5]
215
279
  line = current_iseq[8]
216
280
  insns = current_iseq[13]
217
281
 
@@ -246,8 +310,8 @@ module SyntaxTree
246
310
  find_constant_path(insns, index - 1)
247
311
 
248
312
  if superclass.empty?
249
- raise NotImplementedError,
250
- "superclass with non constant path on line #{line}"
313
+ warn("#{file}:#{line}: superclass with non constant path")
314
+ next
251
315
  end
252
316
  end
253
317
 
@@ -265,8 +329,10 @@ module SyntaxTree
265
329
  # defined on self. We could, but it would require more
266
330
  # emulation.
267
331
  if insns[index - 2] != [:putself]
268
- raise NotImplementedError,
269
- "singleton class with non-self receiver"
332
+ warn(
333
+ "#{file}:#{line}: singleton class with non-self receiver"
334
+ )
335
+ next
270
336
  end
271
337
  elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0
272
338
  location = location_for(class_iseq)
@@ -290,16 +356,16 @@ module SyntaxTree
290
356
  queue << [class_iseq, next_nesting]
291
357
  when :definemethod
292
358
  location = location_for(insn[2])
293
- results << MethodDefinition.new(
359
+ results << method_definition(
294
360
  current_nesting,
295
361
  insn[1],
296
362
  location,
297
- EntryComments.new(file_comments, location)
363
+ file_comments
298
364
  )
299
365
  when :definesmethod
300
- if current_iseq[13][index - 1] != [:putself]
301
- raise NotImplementedError,
302
- "singleton method with non-self receiver"
366
+ if insns[index - 1] != [:putself]
367
+ warn("#{file}:#{line}: singleton method with non-self receiver")
368
+ next
303
369
  end
304
370
 
305
371
  location = location_for(insn[2])
@@ -309,6 +375,69 @@ module SyntaxTree
309
375
  location,
310
376
  EntryComments.new(file_comments, location)
311
377
  )
378
+ when :setconstant
379
+ next_nesting = current_nesting.dup
380
+ name = insn[1]
381
+
382
+ _, nesting = find_constant_path(insns, index - 1)
383
+ next_nesting << nesting if nesting.any?
384
+
385
+ location = Location.new(line, :unknown)
386
+ results << ConstantDefinition.new(
387
+ next_nesting,
388
+ name,
389
+ location,
390
+ EntryComments.new(file_comments, location)
391
+ )
392
+ when :opt_send_without_block, :send
393
+ case insn[1][:mid]
394
+ when :attr_reader, :attr_writer, :attr_accessor
395
+ attr_names = find_attr_arguments(insns, index)
396
+ next unless attr_names
397
+
398
+ location = Location.new(line, :unknown)
399
+ attr_names.each do |attr_name|
400
+ if insn[1][:mid] != :attr_writer
401
+ results << method_definition(
402
+ current_nesting,
403
+ attr_name,
404
+ location,
405
+ file_comments
406
+ )
407
+ end
408
+
409
+ if insn[1][:mid] != :attr_reader
410
+ results << method_definition(
411
+ current_nesting,
412
+ :"#{attr_name}=",
413
+ location,
414
+ file_comments
415
+ )
416
+ end
417
+ end
418
+ when :"core#set_method_alias"
419
+ # Now we have to validate that the alias is happening with a
420
+ # non-interpolated value. To do this we'll match the specific
421
+ # pattern we're expecting.
422
+ values =
423
+ insns[(index - 4)...index].map do |previous|
424
+ previous.is_a?(Array) ? previous[0] : previous
425
+ end
426
+ if values !=
427
+ %i[putspecialobject putspecialobject putobject putobject]
428
+ next
429
+ end
430
+
431
+ # Now that we know it's in the structure we want it, we can use
432
+ # the values of the putobject to determine the alias.
433
+ location = Location.new(line, :unknown)
434
+ results << AliasMethodDefinition.new(
435
+ current_nesting,
436
+ insns[index - 2][1],
437
+ location,
438
+ EntryComments.new(file_comments, location)
439
+ )
440
+ end
312
441
  end
313
442
  end
314
443
  end
@@ -321,6 +450,20 @@ module SyntaxTree
321
450
  # It is not as fast as using the instruction sequences directly, but is
322
451
  # supported on all runtimes.
323
452
  class ParserBackend
453
+ class ConstantNameVisitor < Visitor
454
+ def visit_const_ref(node)
455
+ [node.constant.value.to_sym]
456
+ end
457
+
458
+ def visit_const_path_ref(node)
459
+ visit(node.parent) << node.constant.value.to_sym
460
+ end
461
+
462
+ def visit_var_ref(node)
463
+ [node.value.value.to_sym]
464
+ end
465
+ end
466
+
324
467
  class IndexVisitor < Visitor
325
468
  attr_reader :results, :nesting, :statements
326
469
 
@@ -331,8 +474,46 @@ module SyntaxTree
331
474
  end
332
475
 
333
476
  visit_methods do
477
+ def visit_alias(node)
478
+ if node.left.is_a?(SymbolLiteral) && node.right.is_a?(SymbolLiteral)
479
+ location =
480
+ Location.new(
481
+ node.location.start_line,
482
+ node.location.start_column
483
+ )
484
+
485
+ results << AliasMethodDefinition.new(
486
+ nesting.dup,
487
+ node.left.value.value.to_sym,
488
+ location,
489
+ comments_for(node)
490
+ )
491
+ end
492
+
493
+ super
494
+ end
495
+
496
+ def visit_assign(node)
497
+ if node.target.is_a?(VarField) && node.target.value.is_a?(Const)
498
+ location =
499
+ Location.new(
500
+ node.location.start_line,
501
+ node.location.start_column
502
+ )
503
+
504
+ results << ConstantDefinition.new(
505
+ nesting.dup,
506
+ node.target.value.value.to_sym,
507
+ location,
508
+ comments_for(node)
509
+ )
510
+ end
511
+
512
+ super
513
+ end
514
+
334
515
  def visit_class(node)
335
- names = visit(node.constant)
516
+ names = node.constant.accept(ConstantNameVisitor.new)
336
517
  nesting << names
337
518
 
338
519
  location =
@@ -340,7 +521,7 @@ module SyntaxTree
340
521
 
341
522
  superclass =
342
523
  if node.superclass
343
- visited = visit(node.superclass)
524
+ visited = node.superclass.accept(ConstantNameVisitor.new)
344
525
 
345
526
  if visited == [[]]
346
527
  raise NotImplementedError, "superclass with non constant path"
@@ -363,12 +544,41 @@ module SyntaxTree
363
544
  nesting.pop
364
545
  end
365
546
 
366
- def visit_const_ref(node)
367
- [node.constant.value.to_sym]
368
- end
547
+ def visit_command(node)
548
+ case node.message.value
549
+ when "attr_reader", "attr_writer", "attr_accessor"
550
+ comments = comments_for(node)
551
+ location =
552
+ Location.new(
553
+ node.location.start_line,
554
+ node.location.start_column
555
+ )
556
+
557
+ node.arguments.parts.each do |argument|
558
+ next unless argument.is_a?(SymbolLiteral)
559
+ name = argument.value.value.to_sym
560
+
561
+ if node.message.value != "attr_writer"
562
+ results << MethodDefinition.new(
563
+ nesting.dup,
564
+ name,
565
+ location,
566
+ comments
567
+ )
568
+ end
569
+
570
+ if node.message.value != "attr_reader"
571
+ results << MethodDefinition.new(
572
+ nesting.dup,
573
+ :"#{name}=",
574
+ location,
575
+ comments
576
+ )
577
+ end
578
+ end
579
+ end
369
580
 
370
- def visit_const_path_ref(node)
371
- visit(node.parent) << node.constant.value.to_sym
581
+ super
372
582
  end
373
583
 
374
584
  def visit_def(node)
@@ -391,10 +601,12 @@ module SyntaxTree
391
601
  comments_for(node)
392
602
  )
393
603
  end
604
+
605
+ super
394
606
  end
395
607
 
396
608
  def visit_module(node)
397
- names = visit(node.constant)
609
+ names = node.constant.accept(ConstantNameVisitor.new)
398
610
  nesting << names
399
611
 
400
612
  location =
@@ -420,10 +632,6 @@ module SyntaxTree
420
632
  @statements = node
421
633
  super
422
634
  end
423
-
424
- def visit_var_ref(node)
425
- [node.value.value.to_sym]
426
- end
427
635
  end
428
636
 
429
637
  private
@@ -433,8 +641,10 @@ module SyntaxTree
433
641
 
434
642
  body = statements.body
435
643
  line = node.location.start_line - 1
436
- index = body.index(node) - 1
644
+ index = body.index(node)
645
+ return comments if index.nil?
437
646
 
647
+ index -= 1
438
648
  while index >= 0 && body[index].is_a?(Comment) &&
439
649
  (line - body[index].location.start_line < 2)
440
650
  comments.unshift(body[index].value)
@@ -792,9 +792,10 @@ module SyntaxTree
792
792
  private
793
793
 
794
794
  def trailing_comma?
795
+ arguments = self.arguments
795
796
  return false unless arguments.is_a?(Args)
796
- parts = arguments.parts
797
797
 
798
+ parts = arguments.parts
798
799
  if parts.last.is_a?(ArgBlock)
799
800
  # If the last argument is a block, then we can't put a trailing comma
800
801
  # after it without resulting in a syntax error.
@@ -1188,8 +1189,11 @@ module SyntaxTree
1188
1189
  end
1189
1190
 
1190
1191
  def format(q)
1191
- if lbracket.comments.empty? && contents && contents.comments.empty? &&
1192
- contents.parts.length > 1
1192
+ lbracket = self.lbracket
1193
+ contents = self.contents
1194
+
1195
+ if lbracket.is_a?(LBracket) && lbracket.comments.empty? && contents &&
1196
+ contents.comments.empty? && contents.parts.length > 1
1193
1197
  if qwords?
1194
1198
  QWordsFormatter.new(contents).format(q)
1195
1199
  return
@@ -2091,6 +2095,7 @@ module SyntaxTree
2091
2095
  end
2092
2096
 
2093
2097
  def format(q)
2098
+ left = self.left
2094
2099
  power = operator == :**
2095
2100
 
2096
2101
  q.group do
@@ -2307,6 +2312,8 @@ module SyntaxTree
2307
2312
  end
2308
2313
 
2309
2314
  def bind(parser, start_char, start_column, end_char, end_column)
2315
+ rescue_clause = self.rescue_clause
2316
+
2310
2317
  @location =
2311
2318
  Location.new(
2312
2319
  start_line: location.start_line,
@@ -2330,6 +2337,7 @@ module SyntaxTree
2330
2337
  # Next we're going to determine the rescue clause if there is one
2331
2338
  if rescue_clause
2332
2339
  consequent = else_clause || ensure_clause
2340
+
2333
2341
  rescue_clause.bind_end(
2334
2342
  consequent ? consequent.location.start_char : end_char,
2335
2343
  consequent ? consequent.location.start_column : end_column
@@ -2735,7 +2743,7 @@ module SyntaxTree
2735
2743
  children << receiver
2736
2744
  end
2737
2745
  when MethodAddBlock
2738
- if receiver.call.is_a?(CallNode) && !receiver.call.receiver.nil?
2746
+ if (call = receiver.call).is_a?(CallNode) && !call.receiver.nil?
2739
2747
  children << receiver
2740
2748
  else
2741
2749
  break
@@ -2744,8 +2752,8 @@ module SyntaxTree
2744
2752
  break
2745
2753
  end
2746
2754
  when MethodAddBlock
2747
- if child.call.is_a?(CallNode) && !child.call.receiver.nil?
2748
- children << child.call
2755
+ if (call = child.call).is_a?(CallNode) && !call.receiver.nil?
2756
+ children << call
2749
2757
  else
2750
2758
  break
2751
2759
  end
@@ -2767,8 +2775,8 @@ module SyntaxTree
2767
2775
  # of just Statements nodes.
2768
2776
  parent = parents[3] if parent.is_a?(BlockNode) && parent.keywords?
2769
2777
 
2770
- if parent.is_a?(MethodAddBlock) && parent.call.is_a?(CallNode) &&
2771
- parent.call.message.value == "sig"
2778
+ if parent.is_a?(MethodAddBlock) &&
2779
+ (call = parent.call).is_a?(CallNode) && call.message.value == "sig"
2772
2780
  threshold = 2
2773
2781
  end
2774
2782
  end
@@ -2813,10 +2821,10 @@ module SyntaxTree
2813
2821
 
2814
2822
  while (child = children.pop)
2815
2823
  if child.is_a?(CallNode)
2816
- if child.receiver.is_a?(CallNode) &&
2817
- (child.receiver.message != :call) &&
2818
- (child.receiver.message.value == "where") &&
2819
- (child.message.value == "not")
2824
+ if (receiver = child.receiver).is_a?(CallNode) &&
2825
+ (receiver.message != :call) &&
2826
+ (receiver.message.value == "where") &&
2827
+ (message.value == "not")
2820
2828
  # This is very specialized behavior wherein we group
2821
2829
  # .where.not calls together because it looks better. For more
2822
2830
  # information, see
@@ -2872,7 +2880,8 @@ module SyntaxTree
2872
2880
  when CallNode
2873
2881
  !node.receiver.nil?
2874
2882
  when MethodAddBlock
2875
- node.call.is_a?(CallNode) && !node.call.receiver.nil?
2883
+ call = node.call
2884
+ call.is_a?(CallNode) && !call.receiver.nil?
2876
2885
  else
2877
2886
  false
2878
2887
  end
@@ -3629,6 +3638,10 @@ module SyntaxTree
3629
3638
  end
3630
3639
 
3631
3640
  def format(q)
3641
+ message = self.message
3642
+ arguments = self.arguments
3643
+ block = self.block
3644
+
3632
3645
  q.group do
3633
3646
  doc =
3634
3647
  q.nest(0) do
@@ -3637,7 +3650,7 @@ module SyntaxTree
3637
3650
  # If there are leading comments on the message then we know we have
3638
3651
  # a newline in the source that is forcing these things apart. In
3639
3652
  # this case we will have to use a trailing operator.
3640
- if message.comments.any?(&:leading?)
3653
+ if message != :call && message.comments.any?(&:leading?)
3641
3654
  q.format(CallOperatorFormatter.new(operator), stackable: false)
3642
3655
  q.indent do
3643
3656
  q.breakable_empty
@@ -4153,6 +4166,9 @@ module SyntaxTree
4153
4166
  end
4154
4167
 
4155
4168
  def format(q)
4169
+ params = self.params
4170
+ bodystmt = self.bodystmt
4171
+
4156
4172
  q.group do
4157
4173
  q.group do
4158
4174
  q.text("def")
@@ -4209,6 +4225,8 @@ module SyntaxTree
4209
4225
  end
4210
4226
 
4211
4227
  def arity
4228
+ params = self.params
4229
+
4212
4230
  case params
4213
4231
  when Params
4214
4232
  params.arity
@@ -5293,6 +5311,7 @@ module SyntaxTree
5293
5311
  end
5294
5312
 
5295
5313
  def child_nodes
5314
+ operator = self.operator
5296
5315
  [parent, (operator if operator != :"::"), name]
5297
5316
  end
5298
5317
 
@@ -5674,7 +5693,7 @@ module SyntaxTree
5674
5693
  end
5675
5694
 
5676
5695
  def child_nodes
5677
- [lbrace] + assocs
5696
+ [lbrace].concat(assocs)
5678
5697
  end
5679
5698
 
5680
5699
  def copy(lbrace: nil, assocs: nil, location: nil)
@@ -5766,7 +5785,7 @@ module SyntaxTree
5766
5785
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
5767
5786
  attr_reader :comments
5768
5787
 
5769
- def initialize(beginning:, ending: nil, dedent: 0, parts: [], location:)
5788
+ def initialize(beginning:, location:, ending: nil, dedent: 0, parts: [])
5770
5789
  @beginning = beginning
5771
5790
  @ending = ending
5772
5791
  @dedent = dedent
@@ -6134,6 +6153,8 @@ module SyntaxTree
6134
6153
  private
6135
6154
 
6136
6155
  def format_contents(q, parts, nested)
6156
+ keyword_rest = self.keyword_rest
6157
+
6137
6158
  q.group { q.seplist(parts) { |part| q.format(part, stackable: false) } }
6138
6159
 
6139
6160
  # If there isn't a constant, and there's a blank keyword_rest, then we
@@ -6763,10 +6784,13 @@ module SyntaxTree
6763
6784
 
6764
6785
  def format(q)
6765
6786
  keyword = "in "
6787
+ pattern = self.pattern
6788
+ consequent = self.consequent
6766
6789
 
6767
6790
  q.group do
6768
6791
  q.text(keyword)
6769
6792
  q.nest(keyword.length) { q.format(pattern) }
6793
+ q.text(" then") if pattern.is_a?(RangeNode) && pattern.right.nil?
6770
6794
 
6771
6795
  unless statements.empty?
6772
6796
  q.indent do
@@ -7164,6 +7188,8 @@ module SyntaxTree
7164
7188
  end
7165
7189
 
7166
7190
  def format(q)
7191
+ params = self.params
7192
+
7167
7193
  q.text("->")
7168
7194
  q.group do
7169
7195
  if params.is_a?(Paren)
@@ -7642,7 +7668,7 @@ module SyntaxTree
7642
7668
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
7643
7669
  attr_reader :comments
7644
7670
 
7645
- def initialize(parts:, comma: false, location:)
7671
+ def initialize(parts:, location:, comma: false)
7646
7672
  @parts = parts
7647
7673
  @comma = comma
7648
7674
  @location = location
@@ -7703,7 +7729,7 @@ module SyntaxTree
7703
7729
  # [Array[ Comment | EmbDoc ]] the comments attached to this node
7704
7730
  attr_reader :comments
7705
7731
 
7706
- def initialize(contents:, comma: false, location:)
7732
+ def initialize(contents:, location:, comma: false)
7707
7733
  @contents = contents
7708
7734
  @comma = comma
7709
7735
  @location = location
@@ -8286,14 +8312,14 @@ module SyntaxTree
8286
8312
  attr_reader :comments
8287
8313
 
8288
8314
  def initialize(
8315
+ location:,
8289
8316
  requireds: [],
8290
8317
  optionals: [],
8291
8318
  rest: nil,
8292
8319
  posts: [],
8293
8320
  keywords: [],
8294
8321
  keyword_rest: nil,
8295
- block: nil,
8296
- location:
8322
+ block: nil
8297
8323
  )
8298
8324
  @requireds = requireds
8299
8325
  @optionals = optionals
@@ -8320,6 +8346,8 @@ module SyntaxTree
8320
8346
  end
8321
8347
 
8322
8348
  def child_nodes
8349
+ keyword_rest = self.keyword_rest
8350
+
8323
8351
  [
8324
8352
  *requireds,
8325
8353
  *optionals.flatten(1),
@@ -8374,16 +8402,19 @@ module SyntaxTree
8374
8402
  end
8375
8403
 
8376
8404
  def format(q)
8405
+ rest = self.rest
8406
+ keyword_rest = self.keyword_rest
8407
+
8377
8408
  parts = [
8378
8409
  *requireds,
8379
8410
  *optionals.map { |(name, value)| OptionalFormatter.new(name, value) }
8380
8411
  ]
8381
8412
 
8382
8413
  parts << rest if rest && !rest.is_a?(ExcessedComma)
8383
- parts += [
8384
- *posts,
8385
- *keywords.map { |(name, value)| KeywordFormatter.new(name, value) }
8386
- ]
8414
+ parts.concat(posts)
8415
+ parts.concat(
8416
+ keywords.map { |(name, value)| KeywordFormatter.new(name, value) }
8417
+ )
8387
8418
 
8388
8419
  parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
8389
8420
  parts << block if block
@@ -8510,6 +8541,8 @@ module SyntaxTree
8510
8541
  end
8511
8542
 
8512
8543
  def format(q)
8544
+ contents = self.contents
8545
+
8513
8546
  q.group do
8514
8547
  q.format(lparen)
8515
8548
 
@@ -9424,11 +9457,11 @@ module SyntaxTree
9424
9457
  end_column: end_column
9425
9458
  )
9426
9459
 
9427
- if consequent
9428
- consequent.bind_end(end_char, end_column)
9460
+ if (next_node = consequent)
9461
+ next_node.bind_end(end_char, end_column)
9429
9462
  statements.bind_end(
9430
- consequent.location.start_char,
9431
- consequent.location.start_column
9463
+ next_node.location.start_char,
9464
+ next_node.location.start_column
9432
9465
  )
9433
9466
  else
9434
9467
  statements.bind_end(end_char, end_column)
@@ -9871,8 +9904,8 @@ module SyntaxTree
9871
9904
  end_column: end_column
9872
9905
  )
9873
9906
 
9874
- if body[0].is_a?(VoidStmt)
9875
- location = body[0].location
9907
+ if (void_stmt = body[0]).is_a?(VoidStmt)
9908
+ location = void_stmt.location
9876
9909
  location =
9877
9910
  Location.new(
9878
9911
  start_line: location.start_line,
@@ -10351,7 +10384,7 @@ module SyntaxTree
10351
10384
  opening_quote, closing_quote =
10352
10385
  if !Quotes.locked?(self, q.quote)
10353
10386
  [q.quote, q.quote]
10354
- elsif quote.start_with?("%")
10387
+ elsif quote&.start_with?("%")
10355
10388
  [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])]
10356
10389
  else
10357
10390
  [quote, quote]
@@ -11520,7 +11553,7 @@ module SyntaxTree
11520
11553
  end
11521
11554
 
11522
11555
  def child_nodes
11523
- [value]
11556
+ value == :nil ? [] : [value]
11524
11557
  end
11525
11558
 
11526
11559
  def copy(value: nil, location: nil)
@@ -2132,13 +2132,20 @@ module SyntaxTree
2132
2132
  ending = consequent || consume_keyword(:end)
2133
2133
 
2134
2134
  statements_start = pattern
2135
- if (token = find_keyword(:then))
2135
+ if (token = find_keyword_between(:then, pattern, statements))
2136
2136
  tokens.delete(token)
2137
2137
  statements_start = token
2138
2138
  end
2139
2139
 
2140
2140
  start_char =
2141
2141
  find_next_statement_start((token || statements_start).location.end_char)
2142
+
2143
+ # Ripper ignores parentheses on patterns, so we need to do the same in
2144
+ # order to attach comments correctly to the pattern.
2145
+ if source[start_char] == ")"
2146
+ start_char = find_next_statement_start(start_char + 1)
2147
+ end
2148
+
2142
2149
  statements.bind(
2143
2150
  self,
2144
2151
  start_char,
@@ -138,12 +138,13 @@ module SyntaxTree
138
138
  # as a placeholder for collecting all of the various places that nodes are
139
139
  # used.
140
140
  class Node
141
- attr_reader :name, :comment, :attributes
141
+ attr_reader :name, :comment, :attributes, :visitor_method
142
142
 
143
- def initialize(name, comment, attributes)
143
+ def initialize(name, comment, attributes, visitor_method)
144
144
  @name = name
145
145
  @comment = comment
146
146
  @attributes = attributes
147
+ @visitor_method = visitor_method
147
148
  end
148
149
  end
149
150
 
@@ -183,10 +184,11 @@ module SyntaxTree
183
184
  next unless main_statement.is_a?(SyntaxTree::ClassDeclaration)
184
185
 
185
186
  # Ensure we're looking at class declarations with superclasses.
186
- next unless main_statement.superclass.is_a?(SyntaxTree::VarRef)
187
+ superclass = main_statement.superclass
188
+ next unless superclass.is_a?(SyntaxTree::VarRef)
187
189
 
188
190
  # Ensure we're looking at class declarations that inherit from Node.
189
- next unless main_statement.superclass.value.value == "Node"
191
+ next unless superclass.value.value == "Node"
190
192
 
191
193
  # All child nodes inherit the location attr_reader from Node, so we'll add
192
194
  # that to the list of attributes first.
@@ -195,6 +197,10 @@ module SyntaxTree
195
197
  Attribute.new(:location, "[Location] the location of this node")
196
198
  }
197
199
 
200
+ # This is the name of the method tha gets called on the given visitor when
201
+ # the accept method is called on this node.
202
+ visitor_method = nil
203
+
198
204
  statements = main_statement.bodystmt.statements.body
199
205
  statements.each_with_index do |statement, statement_index|
200
206
  case statement
@@ -224,16 +230,25 @@ module SyntaxTree
224
230
  end
225
231
 
226
232
  attributes[attribute.name] = attribute
233
+ when SyntaxTree::DefNode
234
+ if statement.name.value == "accept"
235
+ call_node = statement.bodystmt.statements.body.first
236
+ visitor_method = call_node.message.value.to_sym
237
+ end
227
238
  end
228
239
  end
229
240
 
241
+ # If we never found a visitor method, then we have an error.
242
+ raise if visitor_method.nil?
243
+
230
244
  # Finally, set it up in the hash of nodes so that we can use it later.
231
245
  comments = parse_comments(main_statements, main_statement_index)
232
246
  node =
233
247
  Node.new(
234
248
  main_statement.constant.constant.value.to_sym,
235
249
  "#{comments.join("\n")}\n",
236
- attributes
250
+ attributes,
251
+ visitor_method
237
252
  )
238
253
 
239
254
  @nodes[node.name] = node
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxTree
4
- VERSION = "6.0.2"
4
+ VERSION = "6.1.0"
5
5
  end
@@ -875,8 +875,7 @@ module SyntaxTree
875
875
  when Ident
876
876
  iseq.putobject("local-variable")
877
877
  when IVar
878
- iseq.putnil
879
- iseq.defined(Defined::TYPE_IVAR, name, "instance-variable")
878
+ iseq.definedivar(name, iseq.inline_storage, "instance-variable")
880
879
  when Kw
881
880
  case name
882
881
  when :false
@@ -50,7 +50,7 @@ module SyntaxTree
50
50
  @tail_node = nil
51
51
  end
52
52
 
53
- def each
53
+ def each(&_blk)
54
54
  return to_enum(__method__) unless block_given?
55
55
  each_node { |node| yield node.value }
56
56
  end
@@ -673,12 +673,21 @@ module SyntaxTree
673
673
  push(ConcatStrings.new(number))
674
674
  end
675
675
 
676
+ def defineclass(name, class_iseq, flags)
677
+ push(DefineClass.new(name, class_iseq, flags))
678
+ end
679
+
676
680
  def defined(type, name, message)
677
681
  push(Defined.new(type, name, message))
678
682
  end
679
683
 
680
- def defineclass(name, class_iseq, flags)
681
- push(DefineClass.new(name, class_iseq, flags))
684
+ def definedivar(name, cache, message)
685
+ if RUBY_VERSION < "3.3"
686
+ push(PutNil.new)
687
+ push(Defined.new(Defined::TYPE_IVAR, name, message))
688
+ else
689
+ push(DefinedIVar.new(name, cache, message))
690
+ end
682
691
  end
683
692
 
684
693
  def definemethod(name, method_iseq)
@@ -1058,6 +1067,8 @@ module SyntaxTree
1058
1067
  iseq.defineclass(opnds[0], from(opnds[1], options, iseq), opnds[2])
1059
1068
  when :defined
1060
1069
  iseq.defined(opnds[0], opnds[1], opnds[2])
1070
+ when :definedivar
1071
+ iseq.definedivar(opnds[0], opnds[1], opnds[2])
1061
1072
  when :definemethod
1062
1073
  iseq.definemethod(opnds[0], from(opnds[1], options, iseq))
1063
1074
  when :definesmethod
@@ -994,6 +994,64 @@ module SyntaxTree
994
994
  end
995
995
  end
996
996
 
997
+ # ### Summary
998
+ #
999
+ # `definedivar` checks if an instance variable is defined. It is a
1000
+ # specialization of the `defined` instruction. It accepts three arguments:
1001
+ # the name of the instance variable, an inline cache, and the string that
1002
+ # should be pushed onto the stack in the event that the instance variable
1003
+ # is defined.
1004
+ #
1005
+ # ### Usage
1006
+ #
1007
+ # ~~~ruby
1008
+ # defined?(@value)
1009
+ # ~~~
1010
+ #
1011
+ class DefinedIVar < Instruction
1012
+ attr_reader :name, :cache, :message
1013
+
1014
+ def initialize(name, cache, message)
1015
+ @name = name
1016
+ @cache = cache
1017
+ @message = message
1018
+ end
1019
+
1020
+ def disasm(fmt)
1021
+ fmt.instruction(
1022
+ "definedivar",
1023
+ [fmt.object(name), fmt.inline_storage(cache), fmt.object(message)]
1024
+ )
1025
+ end
1026
+
1027
+ def to_a(_iseq)
1028
+ [:definedivar, name, cache, message]
1029
+ end
1030
+
1031
+ def deconstruct_keys(_keys)
1032
+ { name: name, cache: cache, message: message }
1033
+ end
1034
+
1035
+ def ==(other)
1036
+ other.is_a?(DefinedIVar) && other.name == name &&
1037
+ other.cache == cache && other.message == message
1038
+ end
1039
+
1040
+ def length
1041
+ 4
1042
+ end
1043
+
1044
+ def pushes
1045
+ 1
1046
+ end
1047
+
1048
+ def call(vm)
1049
+ result = (message if vm.frame._self.instance_variable_defined?(name))
1050
+
1051
+ vm.push(result)
1052
+ end
1053
+ end
1054
+
997
1055
  # ### Summary
998
1056
  #
999
1057
  # `definemethod` defines a method on the class of the current value of
data/lib/syntax_tree.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "prettier_print"
4
+ require "pp"
4
5
  require "ripper"
5
6
 
6
7
  require_relative "syntax_tree/node"
data/tasks/sorbet.rake CHANGED
@@ -20,6 +20,26 @@ module SyntaxTree
20
20
  generate_parent
21
21
  Reflection.nodes.sort.each { |(_, node)| generate_node(node) }
22
22
 
23
+ body << ClassDeclaration(
24
+ ConstPathRef(VarRef(Const("SyntaxTree")), Const("BasicVisitor")),
25
+ nil,
26
+ BodyStmt(
27
+ Statements(generate_visitor("overridable")),
28
+ nil,
29
+ nil,
30
+ nil,
31
+ nil
32
+ ),
33
+ location
34
+ )
35
+
36
+ body << ClassDeclaration(
37
+ ConstPathRef(VarRef(Const("SyntaxTree")), Const("Visitor")),
38
+ ConstPathRef(VarRef(Const("SyntaxTree")), Const("BasicVisitor")),
39
+ BodyStmt(Statements(generate_visitor("override")), nil, nil, nil, nil),
40
+ location
41
+ )
42
+
23
43
  Formatter.format(nil, Program(Statements(body)))
24
44
  end
25
45
 
@@ -122,8 +142,41 @@ module SyntaxTree
122
142
  @line += 1
123
143
 
124
144
  node_body << generate_def_node("child_nodes", nil)
145
+ @line += 2
146
+
147
+ node_body << sig_block do
148
+ CallNode(
149
+ sig_params do
150
+ BareAssocHash(
151
+ [
152
+ Assoc(
153
+ Label("other:"),
154
+ CallNode(
155
+ VarRef(Const("T")),
156
+ Period("."),
157
+ Ident("untyped"),
158
+ nil
159
+ )
160
+ )
161
+ ]
162
+ )
163
+ end,
164
+ Period("."),
165
+ sig_returns { ConstPathRef(VarRef(Const("T")), Const("Boolean")) },
166
+ nil
167
+ )
168
+ end
125
169
  @line += 1
126
170
 
171
+ node_body << generate_def_node(
172
+ "==",
173
+ Paren(
174
+ LParen("("),
175
+ Params.new(location: location, requireds: [Ident("other")])
176
+ )
177
+ )
178
+ @line += 2
179
+
127
180
  node_body
128
181
  end
129
182
 
@@ -195,6 +248,49 @@ module SyntaxTree
195
248
  )
196
249
  end
197
250
 
251
+ def generate_visitor(override)
252
+ body = []
253
+
254
+ Reflection.nodes.each do |name, node|
255
+ body << sig_block do
256
+ CallNode(
257
+ CallNode(
258
+ Ident(override),
259
+ Period("."),
260
+ sig_params do
261
+ BareAssocHash(
262
+ [
263
+ Assoc(
264
+ Label("node:"),
265
+ sig_type_for(SyntaxTree.const_get(name))
266
+ )
267
+ ]
268
+ )
269
+ end,
270
+ nil
271
+ ),
272
+ Period("."),
273
+ sig_returns do
274
+ CallNode(VarRef(Const("T")), Period("."), Ident("untyped"), nil)
275
+ end,
276
+ nil
277
+ )
278
+ end
279
+
280
+ body << generate_def_node(
281
+ node.visitor_method,
282
+ Paren(
283
+ LParen("("),
284
+ Params.new(requireds: [Ident("node")], location: location)
285
+ )
286
+ )
287
+
288
+ @line += 2
289
+ end
290
+
291
+ body
292
+ end
293
+
198
294
  def sig_block
199
295
  MethodAddBlock(
200
296
  CallNode(nil, nil, Ident("sig"), nil),
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntax_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.2
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Newton
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-03 00:00:00.000000000 Z
11
+ date: 2023-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prettier_print
@@ -110,6 +110,7 @@ files:
110
110
  - ".github/workflows/main.yml"
111
111
  - ".gitignore"
112
112
  - ".rubocop.yml"
113
+ - ".ruby-version"
113
114
  - CHANGELOG.md
114
115
  - CODE_OF_CONDUCT.md
115
116
  - Gemfile