syntax_tree 6.0.0 → 6.0.2

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: eb083ad4f07db18f2d09a8fef263962bd3593b8e93be19d241c7f4cc254a02bb
4
- data.tar.gz: 68c448add2745ffbb26f5200b888e241f8ac56d2f8cb7c6ee6ae675dffb0f3b6
3
+ metadata.gz: 20b5dfab3c47b63217fc1b3a6f1fbbd56f51e8358e4b3514e6bf8fd9da083ed6
4
+ data.tar.gz: f4c18e39ae2b5cb14aae4de6d95494959d41ac78757b01da621b68d514c59bfb
5
5
  SHA512:
6
- metadata.gz: 1e930dfe285bc15786b4d4de401144cc4b5ae90a1724ea9d5d9065b556ec7c9a238e4e9dfa47d72aff563bbc5a07269b2027916ab5e74bdf48896ff65c811e68
7
- data.tar.gz: e148cc8409cbe08e1fcbc72f9cf5018d018264f996b79c9093fc51e588ea8836dbccbaa320f423a013341031369ba2047a0014c47116a65ca10f9b7add3ea9ec
6
+ metadata.gz: 35bff4e2d47a2141e5014771cd30f268408e44ff59d247895dc52bee18f4ff7ebcd163e6ce464eb9908e2027ac7a86785cd5c5573baadfaadb3f690e3689526d
7
+ data.tar.gz: 82722a335946c31d43753a51efc954e8f28a4e39015dd01c3a983f7cdcd6f2dff9e90cb9046b00c2508ee6d9654e33f1760e40c9b1fd70fdf6343fa74855f8b1
data/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ 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.0.2] - 2023-03-03
10
+
11
+ ### Added
12
+
13
+ - The `WithScope` visitor mixin will now additionally report local variables defined through regular expression named captures.
14
+ - The `WithScope` visitor mixin now properly handles destructured splat arguments in required positions.
15
+
16
+ ### Changed
17
+
18
+ - Fixed the AST output by adding blocks to `Command` and `CommandCall` nodes in the `FieldVisitor`.
19
+ - Fixed the location of lambda local variables (e.g., `->(; a) {}`).
20
+
21
+ ## [6.0.1] - 2023-02-26
22
+
23
+ ### Added
24
+
25
+ - The class declarations returned as the result of the indexing operation now have their superclass as a field. It is returned as an array of constants. If the superclass is anything other than a constant lookup, then it raises an error.
26
+
27
+ ### Changed
28
+
29
+ - The `nesting` field on the results of the indexing operation is no longer a single flat array. Instead it is an array of arrays, where each array is a single nesting level. This more accurately reflects the nesting of the nodes in the tree. For example, `class Foo::Bar::Baz; end` would result in `[Foo, Bar, Baz]`, but that incorrectly implies that you can see constants at each of those levels. Now this would result in `[[Foo, Bar, Baz]]` to indicate that it can see either the top level or constants within the scope of `Foo::Bar::Baz` only.
30
+ - When formatting hashes that have omitted values and mixed hash rockets with labels, the formatting now maintains whichever delimiter was used in the source. This is because forcing the use of hash rockets with omitted values results in a syntax error.
31
+ - Handle the case where a bare hash is used after the `break`, `next`, or `return` keywords. Previously this would result in hash labels which is not valid syntax. Now it maintains the delimiters used in the source.
32
+ - The `<<` operator will now break on chained `<<` expressions. Previously it would always stay flat.
33
+
9
34
  ## [6.0.0] - 2023-02-10
10
35
 
11
36
  ### Added
@@ -559,7 +584,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
559
584
 
560
585
  - 🎉 Initial release! 🎉
561
586
 
562
- [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.0...HEAD
587
+ [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.2...HEAD
588
+ [6.0.2]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.1...v6.0.2
589
+ [6.0.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v6.0.0...v6.0.1
563
590
  [6.0.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.3.0...v6.0.0
564
591
  [5.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.2.0...v5.3.0
565
592
  [5.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.1.0...v5.2.0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_tree (6.0.0)
4
+ syntax_tree (6.0.2)
5
5
  prettier_print (>= 1.2.0)
6
6
 
7
7
  GEM
@@ -12,26 +12,26 @@ GEM
12
12
  json (2.6.3)
13
13
  minitest (5.17.0)
14
14
  parallel (1.22.1)
15
- parser (3.2.0.0)
15
+ parser (3.2.1.0)
16
16
  ast (~> 2.4.1)
17
17
  prettier_print (1.2.0)
18
18
  rainbow (3.1.1)
19
19
  rake (13.0.6)
20
- regexp_parser (2.6.2)
20
+ regexp_parser (2.7.0)
21
21
  rexml (3.2.5)
22
- rubocop (1.45.1)
22
+ rubocop (1.47.0)
23
23
  json (~> 2.3)
24
24
  parallel (~> 1.10)
25
25
  parser (>= 3.2.0.0)
26
26
  rainbow (>= 2.2.2, < 4.0)
27
27
  regexp_parser (>= 1.8, < 3.0)
28
28
  rexml (>= 3.2.5, < 4.0)
29
- rubocop-ast (>= 1.24.1, < 2.0)
29
+ rubocop-ast (>= 1.26.0, < 2.0)
30
30
  ruby-progressbar (~> 1.7)
31
31
  unicode-display_width (>= 2.4.0, < 3.0)
32
- rubocop-ast (1.24.1)
33
- parser (>= 3.1.1.0)
34
- ruby-progressbar (1.11.0)
32
+ rubocop-ast (1.27.0)
33
+ parser (>= 3.2.1.0)
34
+ ruby-progressbar (1.12.0)
35
35
  simplecov (0.22.0)
36
36
  docile (~> 1.1)
37
37
  simplecov-html (~> 0.11)
data/README.md CHANGED
@@ -29,6 +29,7 @@ It is built with only standard library dependencies. It additionally ships with
29
29
  - [SyntaxTree.format(source)](#syntaxtreeformatsource)
30
30
  - [SyntaxTree.mutation(&block)](#syntaxtreemutationblock)
31
31
  - [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block)
32
+ - [SyntaxTree.index(source)](#syntaxtreeindexsource)
32
33
  - [Nodes](#nodes)
33
34
  - [child_nodes](#child_nodes)
34
35
  - [copy(**attrs)](#copyattrs)
@@ -347,6 +348,10 @@ This function yields a new mutation visitor to the block, and then returns the i
347
348
 
348
349
  This function takes an input string containing Ruby code, an input string containing a valid Ruby `in` clause expression that can be used to match against nodes in the tree (can be generated using `stree expr`, `stree match`, or `Node#construct_keys`), and a block. Each node that matches the given query will be yielded to the block. The block will receive the node as its only argument.
349
350
 
351
+ ### SyntaxTree.index(source)
352
+
353
+ This function takes an input string containing Ruby code and returns a list of all of the class declarations, module declarations, and method definitions within a file. Each of the entries also has access to its associated comments. This is useful for generating documentation or index information for a file to support something like go-to-definition.
354
+
350
355
  ## Nodes
351
356
 
352
357
  There are many different node types in the syntax tree. They are meant to be treated as immutable structs containing links to child nodes with minimal logic contained within their implementation. However, for the most part they all respond to a certain set of APIs, listed below.
@@ -263,6 +263,7 @@ module SyntaxTree
263
263
  node(node, "command") do
264
264
  field("message", node.message)
265
265
  field("arguments", node.arguments)
266
+ field("block", node.block) if node.block
266
267
  comments(node)
267
268
  end
268
269
  end
@@ -273,6 +274,7 @@ module SyntaxTree
273
274
  field("operator", node.operator)
274
275
  field("message", node.message)
275
276
  field("arguments", node.arguments) if node.arguments
277
+ field("block", node.block) if node.block
276
278
  comments(node)
277
279
  end
278
280
  end
@@ -20,11 +20,12 @@ module SyntaxTree
20
20
 
21
21
  # This entry represents a class definition using the class keyword.
22
22
  class ClassDefinition
23
- attr_reader :nesting, :name, :location, :comments
23
+ attr_reader :nesting, :name, :superclass, :location, :comments
24
24
 
25
- def initialize(nesting, name, location, comments)
25
+ def initialize(nesting, name, superclass, location, comments)
26
26
  @nesting = nesting
27
27
  @name = name
28
+ @superclass = superclass
28
29
  @location = location
29
30
  @comments = comments
30
31
  end
@@ -176,30 +177,101 @@ module SyntaxTree
176
177
  Location.new(code_location[0], code_location[1])
177
178
  end
178
179
 
180
+ def find_constant_path(insns, index)
181
+ index -= 1 while insns[index].is_a?(Integer)
182
+ insn = insns[index]
183
+
184
+ if insn.is_a?(Array) && insn[0] == :opt_getconstant_path
185
+ # In this case we're on Ruby 3.2+ and we have an opt_getconstant_path
186
+ # instruction, so we already know all of the symbols in the nesting.
187
+ [index - 1, insn[1]]
188
+ elsif insn.is_a?(Symbol) && insn.match?(/\Alabel_\d+/)
189
+ # Otherwise, if we have a label then this is very likely the
190
+ # destination of an opt_getinlinecache instruction, in which case
191
+ # we'll walk backwards to grab up all of the constants.
192
+ names = []
193
+
194
+ index -= 1
195
+ until insns[index].is_a?(Array) &&
196
+ insns[index][0] == :opt_getinlinecache
197
+ if insns[index].is_a?(Array) && insns[index][0] == :getconstant
198
+ names.unshift(insns[index][1])
199
+ end
200
+
201
+ index -= 1
202
+ end
203
+
204
+ [index - 1, names]
205
+ else
206
+ [index, []]
207
+ end
208
+ end
209
+
179
210
  def index_iseq(iseq, file_comments)
180
211
  results = []
181
212
  queue = [[iseq, []]]
182
213
 
183
214
  while (current_iseq, current_nesting = queue.shift)
184
- current_iseq[13].each_with_index do |insn, index|
185
- next unless insn.is_a?(Array)
215
+ line = current_iseq[8]
216
+ insns = current_iseq[13]
217
+
218
+ insns.each_with_index do |insn, index|
219
+ case insn
220
+ when Integer
221
+ line = insn
222
+ next
223
+ when Array
224
+ # continue on
225
+ else
226
+ # skip everything else
227
+ next
228
+ end
186
229
 
187
230
  case insn[0]
188
231
  when :defineclass
189
232
  _, name, class_iseq, flags = insn
233
+ next_nesting = current_nesting.dup
234
+
235
+ # This is the index we're going to search for the nested constant
236
+ # path within the declaration name.
237
+ constant_index = index - 2
238
+
239
+ # This is the superclass of the class being defined.
240
+ superclass = []
241
+
242
+ # If there is a superclass, then we're going to find it here and
243
+ # then update the constant_index as necessary.
244
+ if flags & VM_DEFINECLASS_FLAG_HAS_SUPERCLASS > 0
245
+ constant_index, superclass =
246
+ find_constant_path(insns, index - 1)
247
+
248
+ if superclass.empty?
249
+ raise NotImplementedError,
250
+ "superclass with non constant path on line #{line}"
251
+ end
252
+ end
253
+
254
+ if (_, nesting = find_constant_path(insns, constant_index))
255
+ # If there is a constant path in the class name, then we need to
256
+ # handle that by updating the nesting.
257
+ next_nesting << (nesting << name)
258
+ else
259
+ # Otherwise we'll add the class name to the nesting.
260
+ next_nesting << [name]
261
+ end
190
262
 
191
263
  if flags == VM_DEFINECLASS_TYPE_SINGLETON_CLASS
192
264
  # At the moment, we don't support singletons that aren't
193
265
  # defined on self. We could, but it would require more
194
266
  # emulation.
195
- if current_iseq[13][index - 2] != [:putself]
267
+ if insns[index - 2] != [:putself]
196
268
  raise NotImplementedError,
197
269
  "singleton class with non-self receiver"
198
270
  end
199
271
  elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0
200
272
  location = location_for(class_iseq)
201
273
  results << ModuleDefinition.new(
202
- current_nesting,
274
+ next_nesting,
203
275
  name,
204
276
  location,
205
277
  EntryComments.new(file_comments, location)
@@ -207,14 +279,15 @@ module SyntaxTree
207
279
  else
208
280
  location = location_for(class_iseq)
209
281
  results << ClassDefinition.new(
210
- current_nesting,
282
+ next_nesting,
211
283
  name,
284
+ superclass,
212
285
  location,
213
286
  EntryComments.new(file_comments, location)
214
287
  )
215
288
  end
216
289
 
217
- queue << [class_iseq, current_nesting + [name]]
290
+ queue << [class_iseq, next_nesting]
218
291
  when :definemethod
219
292
  location = location_for(insn[2])
220
293
  results << MethodDefinition.new(
@@ -259,24 +332,43 @@ module SyntaxTree
259
332
 
260
333
  visit_methods do
261
334
  def visit_class(node)
262
- name = visit(node.constant).to_sym
335
+ names = visit(node.constant)
336
+ nesting << names
337
+
263
338
  location =
264
339
  Location.new(node.location.start_line, node.location.start_column)
265
340
 
341
+ superclass =
342
+ if node.superclass
343
+ visited = visit(node.superclass)
344
+
345
+ if visited == [[]]
346
+ raise NotImplementedError, "superclass with non constant path"
347
+ end
348
+
349
+ visited
350
+ else
351
+ []
352
+ end
353
+
266
354
  results << ClassDefinition.new(
267
355
  nesting.dup,
268
- name,
356
+ names.last,
357
+ superclass,
269
358
  location,
270
359
  comments_for(node)
271
360
  )
272
361
 
273
- nesting << name
274
362
  super
275
363
  nesting.pop
276
364
  end
277
365
 
278
366
  def visit_const_ref(node)
279
- node.constant.value
367
+ [node.constant.value.to_sym]
368
+ end
369
+
370
+ def visit_const_path_ref(node)
371
+ visit(node.parent) << node.constant.value.to_sym
280
372
  end
281
373
 
282
374
  def visit_def(node)
@@ -302,18 +394,19 @@ module SyntaxTree
302
394
  end
303
395
 
304
396
  def visit_module(node)
305
- name = visit(node.constant).to_sym
397
+ names = visit(node.constant)
398
+ nesting << names
399
+
306
400
  location =
307
401
  Location.new(node.location.start_line, node.location.start_column)
308
402
 
309
403
  results << ModuleDefinition.new(
310
404
  nesting.dup,
311
- name,
405
+ names.last,
312
406
  location,
313
407
  comments_for(node)
314
408
  )
315
409
 
316
- nesting << name
317
410
  super
318
411
  nesting.pop
319
412
  end
@@ -327,6 +420,10 @@ module SyntaxTree
327
420
  @statements = node
328
421
  super
329
422
  end
423
+
424
+ def visit_var_ref(node)
425
+ [node.value.value.to_sym]
426
+ end
330
427
  end
331
428
 
332
429
  private
@@ -1780,13 +1780,25 @@ module SyntaxTree
1780
1780
  end
1781
1781
 
1782
1782
  def self.for(container)
1783
- labels =
1784
- container.assocs.all? do |assoc|
1785
- next true if assoc.is_a?(AssocSplat)
1786
-
1783
+ container.assocs.each do |assoc|
1784
+ if assoc.is_a?(AssocSplat)
1785
+ # Splat nodes do not impact the formatting choice.
1786
+ elsif assoc.value.nil?
1787
+ # If the value is nil, then it has been omitted. In this case we have
1788
+ # to match the existing formatting because standardizing would
1789
+ # potentially break the code. For example:
1790
+ #
1791
+ # { first:, "second" => "value" }
1792
+ #
1793
+ return Identity.new
1794
+ else
1795
+ # Otherwise, we need to check the type of the key. If it's a label or
1796
+ # dynamic symbol, we can use labels. If it's a symbol literal then it
1797
+ # needs to match a certain pattern to be used as a label. If it's
1798
+ # anything else, then we need to use hash rockets.
1787
1799
  case assoc.key
1788
- when Label
1789
- true
1800
+ when Label, DynaSymbol
1801
+ # Here labels can be used.
1790
1802
  when SymbolLiteral
1791
1803
  # When attempting to convert a hash rocket into a hash label,
1792
1804
  # you need to take care because only certain patterns are
@@ -1794,15 +1806,18 @@ module SyntaxTree
1794
1806
  # arguments to methods, but don't specify what that is. After
1795
1807
  # some experimentation, it looks like it's:
1796
1808
  value = assoc.key.value.value
1797
- value.match?(/^[_A-Za-z]/) && !value.end_with?("=")
1798
- when DynaSymbol
1799
- true
1809
+
1810
+ if !value.match?(/^[_A-Za-z]/) || value.end_with?("=")
1811
+ return Rockets.new
1812
+ end
1800
1813
  else
1801
- false
1814
+ # If the value is anything else, we have to use hash rockets.
1815
+ return Rockets.new
1802
1816
  end
1803
1817
  end
1818
+ end
1804
1819
 
1805
- (labels ? Labels : Rockets).new
1820
+ Labels.new
1806
1821
  end
1807
1822
  end
1808
1823
 
@@ -1859,7 +1874,15 @@ module SyntaxTree
1859
1874
  end
1860
1875
 
1861
1876
  def format_key(q, key)
1862
- (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key)
1877
+ @key_formatter ||=
1878
+ case q.parents.take(3).last
1879
+ when Break, Next, ReturnNode
1880
+ HashKeyFormatter::Identity.new
1881
+ else
1882
+ HashKeyFormatter.for(self)
1883
+ end
1884
+
1885
+ @key_formatter.format_key(q, key)
1863
1886
  end
1864
1887
  end
1865
1888
 
@@ -2074,10 +2097,15 @@ module SyntaxTree
2074
2097
  q.group { q.format(left) }
2075
2098
  q.text(" ") unless power
2076
2099
 
2077
- if operator == :<<
2078
- q.text("<< ")
2079
- q.format(right)
2080
- else
2100
+ if operator != :<<
2101
+ q.group do
2102
+ q.text(operator.name)
2103
+ q.indent do
2104
+ power ? q.breakable_empty : q.breakable_space
2105
+ q.format(right)
2106
+ end
2107
+ end
2108
+ elsif left.is_a?(Binary) && left.operator == :<<
2081
2109
  q.group do
2082
2110
  q.text(operator.name)
2083
2111
  q.indent do
@@ -2085,6 +2113,9 @@ module SyntaxTree
2085
2113
  q.format(right)
2086
2114
  end
2087
2115
  end
2116
+ else
2117
+ q.text("<< ")
2118
+ q.format(right)
2088
2119
  end
2089
2120
  end
2090
2121
  end
@@ -1559,7 +1559,14 @@ module SyntaxTree
1559
1559
  beginning = consume_keyword(:elsif)
1560
1560
  ending = consequent || consume_keyword(:end)
1561
1561
 
1562
- start_char = find_next_statement_start(predicate.location.end_char)
1562
+ delimiter =
1563
+ find_keyword_between(:then, predicate, statements) ||
1564
+ find_token_between(Semicolon, predicate, statements)
1565
+
1566
+ tokens.delete(delimiter) if delimiter
1567
+ start_char =
1568
+ find_next_statement_start((delimiter || predicate).location.end_char)
1569
+
1563
1570
  statements.bind(
1564
1571
  self,
1565
1572
  start_char,
@@ -2045,6 +2052,7 @@ module SyntaxTree
2045
2052
 
2046
2053
  start_char =
2047
2054
  find_next_statement_start((keyword || predicate).location.end_char)
2055
+
2048
2056
  statements.bind(
2049
2057
  self,
2050
2058
  start_char,
@@ -2383,8 +2391,14 @@ module SyntaxTree
2383
2391
  }
2384
2392
  }
2385
2393
 
2394
+ parent_line = lineno - 1
2395
+ parent_column =
2396
+ consume_token(Semicolon).location.start_column - tokens[index][0][1]
2397
+
2386
2398
  tokens[(index + 1)..].each_with_object([]) do |token, locals|
2387
2399
  (lineno, column), type, value, = token
2400
+ column += parent_column if lineno == 1
2401
+ lineno += parent_line
2388
2402
 
2389
2403
  # Make the state transition for the parser. If there isn't a transition
2390
2404
  # from the current state to a new state for this type, then we're in a
@@ -3805,6 +3819,7 @@ module SyntaxTree
3805
3819
 
3806
3820
  start_char =
3807
3821
  find_next_statement_start((keyword || predicate).location.end_char)
3822
+
3808
3823
  statements.bind(
3809
3824
  self,
3810
3825
  start_char,
@@ -176,7 +176,8 @@ module SyntaxTree
176
176
  program =
177
177
  SyntaxTree.parse(SyntaxTree.read(File.expand_path("node.rb", __dir__)))
178
178
 
179
- main_statements = program.statements.body.last.bodystmt.statements.body
179
+ program_statements = program.statements
180
+ main_statements = program_statements.body.last.bodystmt.statements.body
180
181
  main_statements.each_with_index do |main_statement, main_statement_index|
181
182
  # Ensure we are only looking at class declarations.
182
183
  next unless main_statement.is_a?(SyntaxTree::ClassDeclaration)
@@ -336,8 +336,8 @@ module SyntaxTree
336
336
  # Visit an Assoc node.
337
337
  def visit_assoc(node)
338
338
  if node.value.nil?
339
+ # { foo: }
339
340
  expression = srange(node.start_char, node.end_char - 1)
340
-
341
341
  type, location =
342
342
  if node.key.value.start_with?(/[A-Z]/)
343
343
  [:const, smap_constant(nil, expression, expression)]
@@ -356,13 +356,38 @@ module SyntaxTree
356
356
  srange_node(node)
357
357
  )
358
358
  )
359
- else
359
+ elsif node.key.is_a?(Label)
360
+ # { foo: 1 }
360
361
  s(
361
362
  :pair,
362
363
  [visit(node.key), visit(node.value)],
363
364
  smap_operator(
364
- srange_search_between(node.key, node.value, "=>") ||
365
- srange_length(node.key.end_char, -1),
365
+ srange_length(node.key.end_char, -1),
366
+ srange_node(node)
367
+ )
368
+ )
369
+ elsif (operator = srange_search_between(node.key, node.value, "=>"))
370
+ # { :foo => 1 }
371
+ s(
372
+ :pair,
373
+ [visit(node.key), visit(node.value)],
374
+ smap_operator(operator, srange_node(node))
375
+ )
376
+ else
377
+ # { "foo": 1 }
378
+ key = visit(node.key)
379
+ key_location =
380
+ smap_collection(
381
+ key.location.begin,
382
+ srange_length(node.key.end_char - 2, 1),
383
+ srange(node.key.start_char, node.key.end_char - 1)
384
+ )
385
+
386
+ s(
387
+ :pair,
388
+ [s(key.type, key.children, key_location), visit(node.value)],
389
+ smap_operator(
390
+ srange_length(node.key.end_char, -1),
366
391
  srange_node(node)
367
392
  )
368
393
  )
@@ -769,7 +794,11 @@ module SyntaxTree
769
794
 
770
795
  srange(node.start_char, end_char)
771
796
  elsif node.block
772
- srange_node(node.message)
797
+ if node.receiver
798
+ srange(node.receiver.start_char, node.message.end_char)
799
+ else
800
+ srange_node(node.message)
801
+ end
773
802
  else
774
803
  srange_node(node)
775
804
  end
@@ -1010,6 +1039,21 @@ module SyntaxTree
1010
1039
 
1011
1040
  # Visit an Elsif node.
1012
1041
  def visit_elsif(node)
1042
+ begin_start = node.predicate.end_char
1043
+ begin_end =
1044
+ if node.statements.empty?
1045
+ node.statements.end_char
1046
+ else
1047
+ node.statements.body.first.start_char
1048
+ end
1049
+
1050
+ begin_token =
1051
+ if buffer.source[begin_start...begin_end].include?("then")
1052
+ srange_find(begin_start, begin_end, "then")
1053
+ elsif buffer.source[begin_start...begin_end].include?(";")
1054
+ srange_find(begin_start, begin_end, ";")
1055
+ end
1056
+
1013
1057
  else_token =
1014
1058
  case node.consequent
1015
1059
  when Elsif
@@ -1029,7 +1073,7 @@ module SyntaxTree
1029
1073
  ],
1030
1074
  smap_condition(
1031
1075
  srange_length(node.start_char, 5),
1032
- nil,
1076
+ begin_token,
1033
1077
  else_token,
1034
1078
  nil,
1035
1079
  expression
@@ -1287,35 +1331,13 @@ module SyntaxTree
1287
1331
 
1288
1332
  # Visit an IfNode node.
1289
1333
  def visit_if(node)
1290
- predicate =
1291
- case node.predicate
1292
- when RangeNode
1293
- type =
1294
- node.predicate.operator.value == ".." ? :iflipflop : :eflipflop
1295
- s(type, visit(node.predicate).children, nil)
1296
- when RegexpLiteral
1297
- s(:match_current_line, [visit(node.predicate)], nil)
1298
- when Unary
1299
- if node.predicate.operator.value == "!" &&
1300
- node.predicate.statement.is_a?(RegexpLiteral)
1301
- s(
1302
- :send,
1303
- [
1304
- s(:match_current_line, [visit(node.predicate.statement)]),
1305
- :!
1306
- ],
1307
- nil
1308
- )
1309
- else
1310
- visit(node.predicate)
1311
- end
1312
- else
1313
- visit(node.predicate)
1314
- end
1315
-
1316
1334
  s(
1317
1335
  :if,
1318
- [predicate, visit(node.statements), visit(node.consequent)],
1336
+ [
1337
+ visit_predicate(node.predicate),
1338
+ visit(node.statements),
1339
+ visit(node.consequent)
1340
+ ],
1319
1341
  if node.modifier?
1320
1342
  smap_keyword_bare(
1321
1343
  srange_find_between(node.statements, node.predicate, "if"),
@@ -1551,12 +1573,14 @@ module SyntaxTree
1551
1573
  location =
1552
1574
  if node.start_char == node.end_char
1553
1575
  smap_collection_bare(nil)
1554
- else
1576
+ elsif buffer.source[node.start_char - 1] == "("
1555
1577
  smap_collection(
1556
1578
  srange_length(node.start_char, 1),
1557
1579
  srange_length(node.end_char, -1),
1558
1580
  srange_node(node)
1559
1581
  )
1582
+ else
1583
+ smap_collection_bare(srange_node(node))
1560
1584
  end
1561
1585
 
1562
1586
  s(:args, visit(node.params).children + shadowargs, location)
@@ -1577,28 +1601,20 @@ module SyntaxTree
1577
1601
  # Visit a MethodAddBlock node.
1578
1602
  def visit_method_add_block(node)
1579
1603
  case node.call
1580
- when Break, Next, ReturnNode
1581
- type, arguments = block_children(node.block)
1582
- call = visit(node.call)
1583
-
1584
- s(
1585
- call.type,
1586
- [
1587
- s(
1588
- type,
1589
- [*call.children, arguments, visit(node.block.bodystmt)],
1590
- nil
1591
- )
1592
- ],
1593
- nil
1594
- )
1595
1604
  when ARef, Super, ZSuper
1596
1605
  type, arguments = block_children(node.block)
1597
1606
 
1598
1607
  s(
1599
1608
  type,
1600
1609
  [visit(node.call), arguments, visit(node.block.bodystmt)],
1601
- nil
1610
+ smap_collection(
1611
+ srange_node(node.block.opening),
1612
+ srange_length(
1613
+ node.block.end_char,
1614
+ node.block.keywords? ? -3 : -1
1615
+ ),
1616
+ srange_node(node)
1617
+ )
1602
1618
  )
1603
1619
  else
1604
1620
  visit_command_call(
@@ -2274,7 +2290,16 @@ module SyntaxTree
2274
2290
  )
2275
2291
  )
2276
2292
  when ArgsForward
2277
- s(:super, [visit(node.arguments.arguments)], nil)
2293
+ s(
2294
+ :super,
2295
+ [visit(node.arguments.arguments)],
2296
+ smap_keyword(
2297
+ srange_length(node.start_char, 5),
2298
+ srange_find(node.start_char + 5, node.end_char, "("),
2299
+ srange_length(node.end_char, -1),
2300
+ srange_node(node)
2301
+ )
2302
+ )
2278
2303
  else
2279
2304
  s(
2280
2305
  :super,
@@ -2376,22 +2401,58 @@ module SyntaxTree
2376
2401
  # Visit a Unary node.
2377
2402
  def visit_unary(node)
2378
2403
  # Special handling here for flipflops
2379
- if node.statement.is_a?(Paren) &&
2380
- node.statement.contents.is_a?(Statements) &&
2381
- node.statement.contents.body.length == 1 &&
2382
- (range = node.statement.contents.body.first).is_a?(RangeNode) &&
2404
+ if (paren = node.statement).is_a?(Paren) &&
2405
+ paren.contents.is_a?(Statements) &&
2406
+ paren.contents.body.length == 1 &&
2407
+ (range = paren.contents.body.first).is_a?(RangeNode) &&
2383
2408
  node.operator == "!"
2384
- type = range.operator.value == ".." ? :iflipflop : :eflipflop
2385
- return(
2386
- s(
2387
- :send,
2388
- [s(:begin, [s(type, visit(range).children, nil)], nil), :!],
2389
- nil
2409
+ s(
2410
+ :send,
2411
+ [
2412
+ s(
2413
+ :begin,
2414
+ [
2415
+ s(
2416
+ range.operator.value == ".." ? :iflipflop : :eflipflop,
2417
+ visit(range).children,
2418
+ smap_operator(
2419
+ srange_node(range.operator),
2420
+ srange_node(range)
2421
+ )
2422
+ )
2423
+ ],
2424
+ smap_collection(
2425
+ srange_length(paren.start_char, 1),
2426
+ srange_length(paren.end_char, -1),
2427
+ srange_node(paren)
2428
+ )
2429
+ ),
2430
+ :!
2431
+ ],
2432
+ smap_send_bare(
2433
+ srange_length(node.start_char, 1),
2434
+ srange_node(node)
2435
+ )
2436
+ )
2437
+ elsif node.operator == "!" && node.statement.is_a?(RegexpLiteral)
2438
+ s(
2439
+ :send,
2440
+ [
2441
+ s(
2442
+ :match_current_line,
2443
+ [visit(node.statement)],
2444
+ smap(srange_node(node.statement))
2445
+ ),
2446
+ :!
2447
+ ],
2448
+ smap_send_bare(
2449
+ srange_length(node.start_char, 1),
2450
+ srange_node(node)
2390
2451
  )
2391
2452
  )
2453
+ else
2454
+ visit(canonical_unary(node))
2392
2455
  end
2393
-
2394
- visit(canonical_unary(node))
2395
2456
  end
2396
2457
 
2397
2458
  # Visit an Undef node.
@@ -2408,41 +2469,43 @@ module SyntaxTree
2408
2469
 
2409
2470
  # Visit an UnlessNode node.
2410
2471
  def visit_unless(node)
2411
- predicate =
2412
- case node.predicate
2413
- when RegexpLiteral
2414
- s(:match_current_line, [visit(node.predicate)], nil)
2415
- when Unary
2416
- if node.predicate.operator.value == "!" &&
2417
- node.predicate.statement.is_a?(RegexpLiteral)
2418
- s(
2419
- :send,
2420
- [
2421
- s(:match_current_line, [visit(node.predicate.statement)]),
2422
- :!
2423
- ],
2424
- nil
2425
- )
2426
- else
2427
- visit(node.predicate)
2428
- end
2429
- else
2430
- visit(node.predicate)
2431
- end
2432
-
2433
2472
  s(
2434
2473
  :if,
2435
- [predicate, visit(node.consequent), visit(node.statements)],
2474
+ [
2475
+ visit_predicate(node.predicate),
2476
+ visit(node.consequent),
2477
+ visit(node.statements)
2478
+ ],
2436
2479
  if node.modifier?
2437
2480
  smap_keyword_bare(
2438
2481
  srange_find_between(node.statements, node.predicate, "unless"),
2439
2482
  srange_node(node)
2440
2483
  )
2441
2484
  else
2485
+ begin_start = node.predicate.end_char
2486
+ begin_end =
2487
+ if node.statements.empty?
2488
+ node.statements.end_char
2489
+ else
2490
+ node.statements.body.first.start_char
2491
+ end
2492
+
2493
+ begin_token =
2494
+ if buffer.source[begin_start...begin_end].include?("then")
2495
+ srange_find(begin_start, begin_end, "then")
2496
+ elsif buffer.source[begin_start...begin_end].include?(";")
2497
+ srange_find(begin_start, begin_end, ";")
2498
+ end
2499
+
2500
+ else_token =
2501
+ if node.consequent
2502
+ srange_length(node.consequent.start_char, 4)
2503
+ end
2504
+
2442
2505
  smap_condition(
2443
2506
  srange_length(node.start_char, 6),
2444
- srange_search_between(node.predicate, node.statements, "then"),
2445
- nil,
2507
+ begin_token,
2508
+ else_token,
2446
2509
  srange_length(node.end_char, -3),
2447
2510
  srange_node(node)
2448
2511
  )
@@ -3014,6 +3077,31 @@ module SyntaxTree
3014
3077
  location = node.location
3015
3078
  srange(location.start_char, location.end_char)
3016
3079
  end
3080
+
3081
+ def visit_predicate(node)
3082
+ case node
3083
+ when RangeNode
3084
+ s(
3085
+ node.operator.value == ".." ? :iflipflop : :eflipflop,
3086
+ visit(node).children,
3087
+ smap_operator(srange_node(node.operator), srange_node(node))
3088
+ )
3089
+ when RegexpLiteral
3090
+ s(:match_current_line, [visit(node)], smap(srange_node(node)))
3091
+ when Unary
3092
+ if node.operator.value == "!" && node.statement.is_a?(RegexpLiteral)
3093
+ s(
3094
+ :send,
3095
+ [s(:match_current_line, [visit(node.statement)]), :!],
3096
+ smap_send_bare(srange_node(node.operator), srange_node(node))
3097
+ )
3098
+ else
3099
+ visit(node)
3100
+ end
3101
+ else
3102
+ visit(node)
3103
+ end
3104
+ end
3017
3105
  end
3018
3106
  end
3019
3107
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxTree
4
- VERSION = "6.0.0"
4
+ VERSION = "6.0.2"
5
5
  end
@@ -189,6 +189,15 @@ module SyntaxTree
189
189
  super
190
190
  end
191
191
 
192
+ def visit_block_var(node)
193
+ node.locals.each do |local|
194
+ current_scope.add_local_definition(local, :variable)
195
+ end
196
+
197
+ super
198
+ end
199
+ alias visit_lambda_var visit_block_var
200
+
192
201
  # Visit for keeping track of local variable definitions
193
202
  def visit_var_field(node)
194
203
  value = node.value
@@ -217,11 +226,72 @@ module SyntaxTree
217
226
  super
218
227
  end
219
228
 
229
+ # When using regex named capture groups, vcalls might actually be a variable
230
+ def visit_vcall(node)
231
+ value = node.value
232
+ definition = current_scope.find_local(value.value)
233
+ current_scope.add_local_usage(value, definition.type) if definition
234
+
235
+ super
236
+ end
237
+
238
+ # Visit for capturing local variables defined in regex named capture groups
239
+ def visit_binary(node)
240
+ if node.operator == :=~
241
+ left = node.left
242
+
243
+ if left.is_a?(RegexpLiteral) && left.parts.length == 1 &&
244
+ left.parts.first.is_a?(TStringContent)
245
+ content = left.parts.first
246
+
247
+ value = content.value
248
+ location = content.location
249
+ start_line = location.start_line
250
+
251
+ Regexp
252
+ .new(value, Regexp::FIXEDENCODING)
253
+ .names
254
+ .each do |name|
255
+ offset = value.index(/\(\?<#{Regexp.escape(name)}>/)
256
+ line = start_line + value[0...offset].count("\n")
257
+
258
+ # We need to add 3 to account for these three characters
259
+ # prefixing a named capture (?<
260
+ column = location.start_column + offset + 3
261
+ if value[0...offset].include?("\n")
262
+ column =
263
+ value[0...offset].length - value[0...offset].rindex("\n") +
264
+ 3 - 1
265
+ end
266
+
267
+ ident_location =
268
+ Location.new(
269
+ start_line: line,
270
+ start_char: location.start_char + offset,
271
+ start_column: column,
272
+ end_line: line,
273
+ end_char: location.start_char + offset + name.length,
274
+ end_column: column + name.length
275
+ )
276
+
277
+ identifier = Ident.new(value: name, location: ident_location)
278
+ current_scope.add_local_definition(identifier, :variable)
279
+ end
280
+ end
281
+ end
282
+
283
+ super
284
+ end
285
+
220
286
  private
221
287
 
222
288
  def add_argument_definitions(list)
223
289
  list.each do |param|
224
- if param.is_a?(SyntaxTree::MLHSParen)
290
+ case param
291
+ when ArgStar
292
+ value = param.value
293
+ current_scope.add_local_definition(value, :argument) if value
294
+ when MLHSParen
225
295
  add_argument_definitions(param.contents.parts)
226
296
  else
227
297
  current_scope.add_local_definition(param, :argument)
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.0
4
+ version: 6.0.2
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-02-10 00:00:00.000000000 Z
11
+ date: 2023-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prettier_print