syntax_tree 6.0.2 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
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