syntax_tree 2.6.0 → 2.8.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: f3d700916c1fab01ddc92117fd2b99c26b699df9e03f06b83eafa1415167ef7d
4
- data.tar.gz: f5499912fe7bf422b44360bf66ce3f7e74145f3a2cc3ef5668e20fc97bd61948
3
+ metadata.gz: ad3c35843b6e3148499ac001d05a9c5d613d69debff5fdc411dff89a9adc5ca4
4
+ data.tar.gz: 570ae99edf8b17b5d205142872621b0551632d29ac4671ee14fed8415cf9cfe4
5
5
  SHA512:
6
- metadata.gz: e287bcace78e800140ce6b30044a684587e8514f7f0b4c8a57f7b4f3926717a96eb966828e42325a44c1af23715cc602c3cb327dcab27d5390b0353b6a595744
7
- data.tar.gz: 7cb34f0d9d423b3129033f04dc543d1bb9b19e62dcc7a9a140eb5c3a3159caaf389746930d15e6556df753e10f0651e38ab5e4db95bad63737836e14f14a86c1
6
+ metadata.gz: 7ec0e9d8f5f4f828ba0d451d49bd7fa9f5576570f7427c91973503e795fe068e21a61dac4f1880be12dbd551855eb0b8d27f359a9a682b964f3183aaf687e1da
7
+ data.tar.gz: b8ae5c0132ebc445d060b1d5cef477253c724ea2c7a22a536d4ea78b3fb2d905c560e1f044f8843b92f79e0dbc1a434cda2a38e76b2732b426719150785a59af
data/.rubocop.yml CHANGED
@@ -78,3 +78,6 @@ Style/PerlBackrefs:
78
78
 
79
79
  Style/SpecialGlobalVars:
80
80
  Enabled: false
81
+
82
+ Style/StructInheritance:
83
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -6,6 +6,42 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.8.0] - 2022-06-21
10
+
11
+ ### Added
12
+
13
+ - [#95](https://github.com/ruby-syntax-tree/syntax_tree/pull/95) - The `HeredocEnd` node has been added which effectively results in the ability to determine the location of the ending of a heredoc from source.
14
+ - [#99](https://github.com/ruby-syntax-tree/syntax_tree/pull/99) - The LSP now allows you to pass the same configuration options as the other CLI commands which allows formatting to be modified in the VSCode extension.
15
+ - [#100](https://github.com/ruby-syntax-tree/syntax_tree/pull/100) - The LSP now explicitly responds to the shutdown request so that VSCode never deadlocks.
16
+
17
+ ### Changed
18
+
19
+ - [#96](https://github.com/ruby-syntax-tree/syntax_tree/pull/96) - The CLI now runs in parallel by default. There is a worker created for each processor on the running machine (as determined by `Etc.nprocessors`).
20
+ - [#97](https://github.com/ruby-syntax-tree/syntax_tree/pull/97) - Syntax Tree now handles the case where `DidYouMean` is not available for whatever reason, as well as handles the newer `detailed_message` API for errors.
21
+
22
+ ## [2.7.1] - 2022-05-25
23
+
24
+ ### Added
25
+
26
+ - [#92](https://github.com/ruby-syntax-tree/syntax_tree/pull/92) - (Internal) Drastically increase test coverage, including many more tests for the language server and the CLI.
27
+
28
+ ### Changed
29
+
30
+ - [#87](https://github.com/ruby-syntax-tree/syntax_tree/pull/87) - Don't convert quotes on strings if it would result in more escapes.
31
+ - [#91](https://github.com/ruby-syntax-tree/syntax_tree/pull/91) - Always use `[]` with array patterns. There are just too many edge cases where you have to use them anyway. This simplifies the look and makes it more consistent.
32
+ - [#92](https://github.com/ruby-syntax-tree/syntax_tree/pull/92) - Remodel the currently shipped plugins such that they're modifying an options hash instead of overriding methods. This should make it easier for other plugins to reference the already loaded plugins, e.g., the RBS plugin referencing the quotes.
33
+ - [#92](https://github.com/ruby-syntax-tree/syntax_tree/pull/92) - Fix up the language server inlay hints to continue walking the tree once a pattern is found. This should increase useability.
34
+
35
+ ## [2.7.0] - 2022-05-19
36
+
37
+ ### Added
38
+
39
+ - [#88](https://github.com/ruby-syntax-tree/syntax_tree/pull/88) - Provide a `SyntaxTree::BasicVisitor` that has no visit methods implemented.
40
+
41
+ ### Changed
42
+
43
+ - [#90](https://github.com/ruby-syntax-tree/syntax_tree/pull/90) - Provide better formatting for `SyntaxTree::AryPtn` when its nested inside a `SyntaxTree::RAssign`.
44
+
9
45
  ## [2.6.0] - 2022-05-16
10
46
 
11
47
  ### Added
@@ -236,7 +272,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
236
272
 
237
273
  - 🎉 Initial release! 🎉
238
274
 
239
- [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.6.0...HEAD
275
+ [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.8.0...HEAD
276
+ [2.8.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.7.1...v2.8.0
277
+ [2.7.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.7.0...v2.7.1
278
+ [2.7.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.6.0...v2.7.0
240
279
  [2.6.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.5.0...v2.6.0
241
280
  [2.5.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.4.1...v2.5.0
242
281
  [2.4.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.4.0...v2.4.1
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_tree (2.6.0)
4
+ syntax_tree (2.8.0)
5
5
  prettier_print
6
6
 
7
7
  GEM
@@ -9,25 +9,25 @@ GEM
9
9
  specs:
10
10
  ast (2.4.2)
11
11
  docile (1.4.0)
12
- minitest (5.15.0)
12
+ minitest (5.16.0)
13
13
  parallel (1.22.1)
14
14
  parser (3.1.2.0)
15
15
  ast (~> 2.4.1)
16
16
  prettier_print (0.1.0)
17
17
  rainbow (3.1.1)
18
18
  rake (13.0.6)
19
- regexp_parser (2.4.0)
19
+ regexp_parser (2.5.0)
20
20
  rexml (3.2.5)
21
- rubocop (1.29.1)
21
+ rubocop (1.30.1)
22
22
  parallel (~> 1.10)
23
23
  parser (>= 3.1.0.0)
24
24
  rainbow (>= 2.2.2, < 4.0)
25
25
  regexp_parser (>= 1.8, < 3.0)
26
26
  rexml (>= 3.2.5, < 4.0)
27
- rubocop-ast (>= 1.17.0, < 2.0)
27
+ rubocop-ast (>= 1.18.0, < 2.0)
28
28
  ruby-progressbar (~> 1.7)
29
29
  unicode-display_width (>= 1.4.0, < 3.0)
30
- rubocop-ast (1.17.0)
30
+ rubocop-ast (1.18.0)
31
31
  parser (>= 3.1.1.0)
32
32
  ruby-progressbar (1.11.0)
33
33
  simplecov (0.21.2)
data/README.md CHANGED
@@ -32,6 +32,7 @@ It is built with only standard library dependencies. It additionally ships with
32
32
  - [construct_keys](#construct_keys)
33
33
  - [Visitor](#visitor)
34
34
  - [visit_method](#visit_method)
35
+ - [BasicVisitor](#basicvisitor)
35
36
  - [Language server](#language-server)
36
37
  - [textDocument/formatting](#textdocumentformatting)
37
38
  - [textDocument/inlayHints](#textdocumentinlayhints)
@@ -373,6 +374,20 @@ Did you mean? visit_binary
373
374
  from bin/console:8:in `<main>'
374
375
  ```
375
376
 
377
+ ### BasicVisitor
378
+
379
+ When you're defining your own visitor, by default it will walk down the tree even if you don't define `visit_*` methods. This is to ensure you can define a subset of the necessary methods in order to only interact with the nodes you're interested in. If you'd like to change this default to instead raise an error if you visit a node you haven't explicitly handled, you can instead inherit from `BasicVisitor`.
380
+
381
+ ```ruby
382
+ class MyVisitor < SyntaxTree::BasicVisitor
383
+ def visit_int(node)
384
+ # ...
385
+ end
386
+ end
387
+ ```
388
+
389
+ The visitor defined above will error out unless it's only visiting a `SyntaxTree::Int` node. This is useful in a couple of ways, e.g., if you're trying to define a visitor to handle the whole tree but it's currently a work-in-progress.
390
+
376
391
  ## Language server
377
392
 
378
393
  Syntax Tree additionally ships with a language server conforming to the [language server protocol](https://microsoft.github.io/language-server-protocol/). It can be invoked through the CLI by running:
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxTree
4
+ # BasicVisitor is the parent class of the Visitor class that provides the
5
+ # ability to walk down the tree. It does not define any handlers, so you
6
+ # should extend this class if you want your visitor to raise an error if you
7
+ # attempt to visit a node that you don't handle.
8
+ class BasicVisitor
9
+ # This is raised when you use the Visitor.visit_method method and it fails.
10
+ # It is correctable to through DidYouMean.
11
+ class VisitMethodError < StandardError
12
+ attr_reader :visit_method
13
+
14
+ def initialize(visit_method)
15
+ @visit_method = visit_method
16
+ super("Invalid visit method: #{visit_method}")
17
+ end
18
+ end
19
+
20
+ # This class is used by DidYouMean to offer corrections to invalid visit
21
+ # method names.
22
+ class VisitMethodChecker
23
+ attr_reader :visit_method
24
+
25
+ def initialize(error)
26
+ @visit_method = error.visit_method
27
+ end
28
+
29
+ def corrections
30
+ @corrections ||=
31
+ DidYouMean::SpellChecker.new(
32
+ dictionary: Visitor.visit_methods
33
+ ).correct(visit_method)
34
+ end
35
+
36
+ # In some setups with Ruby you can turn off DidYouMean, so we're going to
37
+ # respect that setting here.
38
+ if defined?(DidYouMean) && DidYouMean.method_defined?(:correct_error)
39
+ DidYouMean.correct_error(VisitMethodError, self)
40
+ end
41
+ end
42
+
43
+ class << self
44
+ # This method is here to help folks write visitors.
45
+ #
46
+ # It's not always easy to ensure you're writing the correct method name in
47
+ # the visitor since it's perfectly valid to define methods that don't
48
+ # override these parent methods.
49
+ #
50
+ # If you use this method, you can ensure you're writing the correct method
51
+ # name. It will raise an error if the visit method you're defining isn't
52
+ # actually a method on the parent visitor.
53
+ def visit_method(method_name)
54
+ return if visit_methods.include?(method_name)
55
+
56
+ raise VisitMethodError, method_name
57
+ end
58
+
59
+ # This is the list of all of the valid visit methods.
60
+ def visit_methods
61
+ @visit_methods ||=
62
+ Visitor.instance_methods.grep(/^visit_(?!child_nodes)/)
63
+ end
64
+ end
65
+
66
+ def visit(node)
67
+ node&.accept(self)
68
+ end
69
+
70
+ def visit_all(nodes)
71
+ nodes.map { |node| visit(node) }
72
+ end
73
+
74
+ def visit_child_nodes(node)
75
+ visit_all(node.child_nodes)
76
+ end
77
+ end
78
+ end
@@ -34,9 +34,41 @@ module SyntaxTree
34
34
  end
35
35
  end
36
36
 
37
+ # An item of work that corresponds to a file to be processed.
38
+ class FileItem
39
+ attr_reader :filepath
40
+
41
+ def initialize(filepath)
42
+ @filepath = filepath
43
+ end
44
+
45
+ def handler
46
+ HANDLERS[File.extname(filepath)]
47
+ end
48
+
49
+ def source
50
+ handler.read(filepath)
51
+ end
52
+ end
53
+
54
+ # An item of work that corresponds to the stdin content.
55
+ class STDINItem
56
+ def handler
57
+ HANDLERS[".rb"]
58
+ end
59
+
60
+ def filepath
61
+ :stdin
62
+ end
63
+
64
+ def source
65
+ $stdin.read
66
+ end
67
+ end
68
+
37
69
  # The parent action class for the CLI that implements the basics.
38
70
  class Action
39
- def run(handler, filepath, source)
71
+ def run(item)
40
72
  end
41
73
 
42
74
  def success
@@ -48,8 +80,8 @@ module SyntaxTree
48
80
 
49
81
  # An action of the CLI that prints out the AST for the given source.
50
82
  class AST < Action
51
- def run(handler, _filepath, source)
52
- pp handler.parse(source)
83
+ def run(item)
84
+ pp item.handler.parse(item.source)
53
85
  end
54
86
  end
55
87
 
@@ -59,10 +91,11 @@ module SyntaxTree
59
91
  class UnformattedError < StandardError
60
92
  end
61
93
 
62
- def run(handler, filepath, source)
63
- raise UnformattedError if source != handler.format(source)
94
+ def run(item)
95
+ source = item.source
96
+ raise UnformattedError if source != item.handler.format(source)
64
97
  rescue StandardError
65
- warn("[#{Color.yellow("warn")}] #{filepath}")
98
+ warn("[#{Color.yellow("warn")}] #{item.filepath}")
66
99
  raise
67
100
  end
68
101
 
@@ -81,9 +114,11 @@ module SyntaxTree
81
114
  class NonIdempotentFormatError < StandardError
82
115
  end
83
116
 
84
- def run(handler, filepath, source)
85
- warning = "[#{Color.yellow("warn")}] #{filepath}"
86
- formatted = handler.format(source)
117
+ def run(item)
118
+ handler = item.handler
119
+
120
+ warning = "[#{Color.yellow("warn")}] #{item.filepath}"
121
+ formatted = handler.format(item.source)
87
122
 
88
123
  raise NonIdempotentFormatError if formatted != handler.format(formatted)
89
124
  rescue StandardError
@@ -102,25 +137,27 @@ module SyntaxTree
102
137
 
103
138
  # An action of the CLI that prints out the doc tree IR for the given source.
104
139
  class Doc < Action
105
- def run(handler, _filepath, source)
140
+ def run(item)
141
+ source = item.source
142
+
106
143
  formatter = Formatter.new(source, [])
107
- handler.parse(source).format(formatter)
144
+ item.handler.parse(source).format(formatter)
108
145
  pp formatter.groups.first
109
146
  end
110
147
  end
111
148
 
112
149
  # An action of the CLI that formats the input source and prints it out.
113
150
  class Format < Action
114
- def run(handler, _filepath, source)
115
- puts handler.format(source)
151
+ def run(item)
152
+ puts item.handler.format(item.source)
116
153
  end
117
154
  end
118
155
 
119
156
  # An action of the CLI that converts the source into its equivalent JSON
120
157
  # representation.
121
158
  class Json < Action
122
- def run(handler, _filepath, source)
123
- object = Visitor::JSONVisitor.new.visit(handler.parse(source))
159
+ def run(item)
160
+ object = Visitor::JSONVisitor.new.visit(item.handler.parse(item.source))
124
161
  puts JSON.pretty_generate(object)
125
162
  end
126
163
  end
@@ -128,27 +165,28 @@ module SyntaxTree
128
165
  # An action of the CLI that outputs a pattern-matching Ruby expression that
129
166
  # would match the input given.
130
167
  class Match < Action
131
- def run(handler, _filepath, source)
132
- puts handler.parse(source).construct_keys
168
+ def run(item)
169
+ puts item.handler.parse(item.source).construct_keys
133
170
  end
134
171
  end
135
172
 
136
173
  # An action of the CLI that formats the input source and writes the
137
174
  # formatted output back to the file.
138
175
  class Write < Action
139
- def run(handler, filepath, source)
140
- print filepath
176
+ def run(item)
177
+ filepath = item.filepath
141
178
  start = Time.now
142
179
 
143
- formatted = handler.format(source)
180
+ source = item.source
181
+ formatted = item.handler.format(source)
144
182
  File.write(filepath, formatted) if filepath != :stdin
145
183
 
146
184
  color = source == formatted ? Color.gray(filepath) : filepath
147
185
  delta = ((Time.now - start) * 1000).round
148
186
 
149
- puts "\r#{color} #{delta}ms"
187
+ puts "#{color} #{delta}ms"
150
188
  rescue StandardError
151
- puts "\r#{filepath}"
189
+ puts filepath
152
190
  raise
153
191
  end
154
192
  end
@@ -180,7 +218,7 @@ module SyntaxTree
180
218
  #{Color.bold("stree help")}
181
219
  Display this help message
182
220
 
183
- #{Color.bold("stree lsp")}
221
+ #{Color.bold("stree lsp [OPTIONS]")}
184
222
  Run syntax tree in language server mode
185
223
 
186
224
  #{Color.bold("stree version")}
@@ -201,6 +239,20 @@ module SyntaxTree
201
239
  def run(argv)
202
240
  name, *arguments = argv
203
241
 
242
+ # If there are any plugins specified on the command line, then load them
243
+ # by requiring them here. We do this by transforming something like
244
+ #
245
+ # stree format --plugins=haml template.haml
246
+ #
247
+ # into
248
+ #
249
+ # require "syntax_tree/haml"
250
+ #
251
+ if arguments.first&.start_with?("--plugins=")
252
+ plugins = arguments.shift[/^--plugins=(.*)$/, 1]
253
+ plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" }
254
+ end
255
+
204
256
  case name
205
257
  when "help"
206
258
  puts HELP
@@ -244,38 +296,41 @@ module SyntaxTree
244
296
  return 1
245
297
  end
246
298
 
247
- # If there are any plugins specified on the command line, then load them
248
- # by requiring them here. We do this by transforming something like
249
- #
250
- # stree format --plugins=haml template.haml
251
- #
252
- # into
253
- #
254
- # require "syntax_tree/haml"
255
- #
256
- if arguments.first&.start_with?("--plugins=")
257
- plugins = arguments.shift[/^--plugins=(.*)$/, 1]
258
- plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" }
259
- end
299
+ # We're going to build up a queue of items to process.
300
+ queue = Queue.new
260
301
 
261
- # Track whether or not there are any errors from any of the files that
262
- # we take action on so that we can properly clean up and exit.
263
- errored = false
264
-
265
- each_file(arguments) do |handler, filepath, source|
266
- action.run(handler, filepath, source)
267
- rescue Parser::ParseError => error
268
- warn("Error: #{error.message}")
269
- highlight_error(error, source)
270
- errored = true
271
- rescue Check::UnformattedError, Debug::NonIdempotentFormatError
272
- errored = true
273
- rescue StandardError => error
274
- warn(error.message)
275
- warn(error.backtrace)
276
- errored = true
302
+ # If we're reading from stdin, then we'll just add the stdin object to
303
+ # the queue. Otherwise, we'll add each of the filepaths to the queue.
304
+ if $stdin.tty? || arguments.any?
305
+ arguments.each do |pattern|
306
+ Dir
307
+ .glob(pattern)
308
+ .each do |filepath|
309
+ queue << FileItem.new(filepath) if File.file?(filepath)
310
+ end
311
+ end
312
+ else
313
+ queue << STDINItem.new
277
314
  end
278
315
 
316
+ # At the end, we're going to return whether or not this worker ever
317
+ # encountered an error.
318
+ errored =
319
+ with_workers(queue) do |item|
320
+ action.run(item)
321
+ false
322
+ rescue Parser::ParseError => error
323
+ warn("Error: #{error.message}")
324
+ highlight_error(error, item.source)
325
+ true
326
+ rescue Check::UnformattedError, Debug::NonIdempotentFormatError
327
+ true
328
+ rescue StandardError => error
329
+ warn(error.message)
330
+ warn(error.backtrace)
331
+ true
332
+ end
333
+
279
334
  if errored
280
335
  action.failure
281
336
  1
@@ -287,22 +342,33 @@ module SyntaxTree
287
342
 
288
343
  private
289
344
 
290
- def each_file(arguments)
291
- if $stdin.tty? || arguments.any?
292
- arguments.each do |pattern|
293
- Dir
294
- .glob(pattern)
295
- .each do |filepath|
296
- next unless File.file?(filepath)
297
-
298
- handler = HANDLERS[File.extname(filepath)]
299
- source = handler.read(filepath)
300
- yield handler, filepath, source
301
- end
345
+ def with_workers(queue)
346
+ # If the queue is just 1 item, then we're not going to bother going
347
+ # through the whole ceremony of parallelizing the work.
348
+ return yield queue.shift if queue.size == 1
349
+
350
+ workers =
351
+ Etc.nprocessors.times.map do
352
+ Thread.new do
353
+ # Propagate errors in the worker threads up to the parent thread.
354
+ Thread.current.abort_on_exception = true
355
+
356
+ # Track whether or not there are any errors from any of the files
357
+ # that we take action on so that we can properly clean up and
358
+ # exit.
359
+ errored = false
360
+
361
+ # While there is still work left to do, shift off the queue and
362
+ # process the item.
363
+ (errored ||= yield queue.shift) until queue.empty?
364
+
365
+ # At the end, we're going to return whether or not this worker
366
+ # ever encountered an error.
367
+ errored
368
+ end
302
369
  end
303
- else
304
- yield HANDLERS[".rb"], :stdin, $stdin.read
305
- end
370
+
371
+ workers.inject(false) { |accum, thread| accum || thread.value }
306
372
  end
307
373
 
308
374
  # Highlights a snippet from a source and parse error.
@@ -4,6 +4,18 @@ module SyntaxTree
4
4
  # A slightly enhanced PP that knows how to format recursively including
5
5
  # comments.
6
6
  class Formatter < PrettierPrint
7
+ # We want to minimize as much as possible the number of options that are
8
+ # available in syntax tree. For the most part, if users want non-default
9
+ # formatting, they should override the format methods on the specific nodes
10
+ # themselves. However, because of some history with prettier and the fact
11
+ # that folks have become entrenched in their ways, we decided to provide a
12
+ # small amount of configurability.
13
+ #
14
+ # Note that we're keeping this in a global-ish hash instead of just
15
+ # overriding methods on classes so that other plugins can reference this if
16
+ # necessary. For example, the RBS plugin references the quote style.
17
+ OPTIONS = { quote: "\"", trailing_comma: false }
18
+
7
19
  COMMENT_PRIORITY = 1
8
20
  HEREDOC_PRIORITY = 2
9
21
 
@@ -14,13 +26,20 @@ module SyntaxTree
14
26
  attr_reader :quote, :trailing_comma
15
27
  alias trailing_comma? trailing_comma
16
28
 
17
- def initialize(source, ...)
18
- super(...)
29
+ def initialize(
30
+ source,
31
+ *args,
32
+ quote: OPTIONS[:quote],
33
+ trailing_comma: OPTIONS[:trailing_comma]
34
+ )
35
+ super(*args)
19
36
 
20
37
  @source = source
21
38
  @stack = []
22
- @quote = "\""
23
- @trailing_comma = false
39
+
40
+ # Memoizing these values per formatter to make access faster.
41
+ @quote = quote
42
+ @trailing_comma = trailing_comma
24
43
  end
25
44
 
26
45
  def self.format(source, node)
@@ -38,6 +38,7 @@ module SyntaxTree
38
38
  #
39
39
  def visit_assign(node)
40
40
  parentheses(node.location) if stack[-2].is_a?(Params)
41
+ super
41
42
  end
42
43
 
43
44
  # Adds parentheses around binary expressions to make it clear which
@@ -57,6 +58,8 @@ module SyntaxTree
57
58
  parentheses(node.location)
58
59
  else
59
60
  end
61
+
62
+ super
60
63
  end
61
64
 
62
65
  # Adds parentheses around ternary operators contained within certain
@@ -70,9 +73,13 @@ module SyntaxTree
70
73
  # a ? b : ₍c ? d : e₎
71
74
  #
72
75
  def visit_if_op(node)
73
- if stack[-2] in Assign | Binary | IfOp | OpAssign
76
+ case stack[-2]
77
+ in Assign | Binary | IfOp | OpAssign
74
78
  parentheses(node.location)
79
+ else
75
80
  end
81
+
82
+ super
76
83
  end
77
84
 
78
85
  # Adds the implicitly rescued StandardError into a bare rescue clause. For
@@ -92,6 +99,8 @@ module SyntaxTree
92
99
  if node.exception.nil?
93
100
  after[node.location.start_char + "rescue".length] << " StandardError"
94
101
  end
102
+
103
+ super
95
104
  end
96
105
 
97
106
  # Adds parentheses around unary statements using the - operator that are
@@ -107,6 +116,8 @@ module SyntaxTree
107
116
  if stack[-2].is_a?(Binary) && (node.operator == "-")
108
117
  parentheses(node.location)
109
118
  end
119
+
120
+ super
110
121
  end
111
122
 
112
123
  def self.find(program)
@@ -36,8 +36,9 @@ module SyntaxTree
36
36
  write(id: id, result: { capabilities: capabilities })
37
37
  in method: "initialized"
38
38
  # ignored
39
- in method: "shutdown"
39
+ in method: "shutdown" # tolerate missing ID to be a good citizen
40
40
  store.clear
41
+ write(id: request[:id], result: {})
41
42
  return
42
43
  in {
43
44
  method: "textDocument/didChange",
@@ -70,13 +71,11 @@ module SyntaxTree
70
71
  id:,
71
72
  params: { textDocument: { uri: } }
72
73
  }
73
- output = []
74
- PP.pp(SyntaxTree.parse(store[uri]), output)
75
- write(id: id, result: output.join)
74
+ write(id: id, result: PP.pp(SyntaxTree.parse(store[uri]), +""))
76
75
  in method: %r{\$/.+}
77
76
  # ignored
78
77
  else
79
- raise "Unhandled: #{request}"
78
+ raise ArgumentError, "Unhandled: #{request}"
80
79
  end
81
80
  end
82
81
  end
@@ -109,10 +108,6 @@ module SyntaxTree
109
108
  }
110
109
  end
111
110
 
112
- def log(message)
113
- write(method: "window/logMessage", params: { type: 4, message: message })
114
- end
115
-
116
111
  def inlay_hints(source)
117
112
  inlay_hints = InlayHints.find(SyntaxTree.parse(source))
118
113
  serialize = ->(position, text) { { position: position, text: text } }
@@ -1123,30 +1123,20 @@ module SyntaxTree
1123
1123
  end
1124
1124
 
1125
1125
  def format(q)
1126
- parts = [*requireds]
1127
- parts << RestFormatter.new(rest) if rest
1128
- parts += posts
1126
+ q.group do
1127
+ q.format(constant) if constant
1128
+ q.text("[")
1129
+ q.indent do
1130
+ q.breakable("")
1131
+
1132
+ parts = [*requireds]
1133
+ parts << RestFormatter.new(rest) if rest
1134
+ parts += posts
1129
1135
 
1130
- if constant
1131
- q.group do
1132
- q.format(constant)
1133
- q.text("[")
1134
1136
  q.seplist(parts) { |part| q.format(part) }
1135
- q.text("]")
1136
1137
  end
1137
-
1138
- return
1139
- end
1140
-
1141
- parent = q.parent
1142
- if parts.length == 1 || PATTERNS.include?(parent.class)
1143
- q.text("[")
1144
- q.seplist(parts) { |part| q.format(part) }
1138
+ q.breakable("")
1145
1139
  q.text("]")
1146
- elsif parts.empty?
1147
- q.text("[]")
1148
- else
1149
- q.group { q.seplist(parts) { |part| q.format(part) } }
1150
1140
  end
1151
1141
  end
1152
1142
  end
@@ -2139,11 +2129,13 @@ module SyntaxTree
2139
2129
  #
2140
2130
  # break
2141
2131
  #
2142
- in [Paren[
2143
- contents: {
2144
- body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array]
2145
- }
2146
- ]]
2132
+ in [
2133
+ Paren[
2134
+ contents: {
2135
+ body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array]
2136
+ }
2137
+ ]
2138
+ ]
2147
2139
  # Here we have a single argument that is a set of parentheses wrapping
2148
2140
  # an array literal that has at least 2 elements. We're going to print
2149
2141
  # the contents of the array directly. This would be like if we had:
@@ -2777,10 +2769,17 @@ module SyntaxTree
2777
2769
  q.format(value)
2778
2770
  q.text(" ")
2779
2771
  q.format(operator)
2780
- q.group do
2781
- q.indent do
2782
- q.breakable
2783
- q.format(pattern)
2772
+
2773
+ case pattern
2774
+ in AryPtn | FndPtn | HshPtn
2775
+ q.text(" ")
2776
+ q.format(pattern)
2777
+ else
2778
+ q.group do
2779
+ q.indent do
2780
+ q.breakable
2781
+ q.format(pattern)
2782
+ end
2784
2783
  end
2785
2784
  end
2786
2785
  end
@@ -3864,9 +3863,9 @@ module SyntaxTree
3864
3863
  # whichever quote the user chose. (If they chose single quotes, then double
3865
3864
  # quoting would activate the escape sequence, and if they chose double
3866
3865
  # quotes, then single quotes would deactivate it.)
3867
- def self.locked?(node)
3866
+ def self.locked?(node, quote)
3868
3867
  node.parts.any? do |part|
3869
- !part.is_a?(TStringContent) || part.value.match?(/\\|#[@${]/)
3868
+ !part.is_a?(TStringContent) || part.value.match?(/\\|#[@${]|#{quote}/)
3870
3869
  end
3871
3870
  end
3872
3871
 
@@ -3981,12 +3980,12 @@ module SyntaxTree
3981
3980
 
3982
3981
  if matched
3983
3982
  [quote, matching]
3984
- elsif Quotes.locked?(self)
3983
+ elsif Quotes.locked?(self, q.quote)
3985
3984
  ["#{":" unless hash_key}'", "'"]
3986
3985
  else
3987
3986
  ["#{":" unless hash_key}#{q.quote}", q.quote]
3988
3987
  end
3989
- elsif Quotes.locked?(self)
3988
+ elsif Quotes.locked?(self, q.quote)
3990
3989
  if quote.start_with?(":")
3991
3990
  [hash_key ? quote[1..] : quote, quote[1..]]
3992
3991
  else
@@ -4573,16 +4572,26 @@ module SyntaxTree
4573
4572
 
4574
4573
  def format(q)
4575
4574
  q.format(constant) if constant
4576
- q.group(0, "[", "]") do
4577
- q.text("*")
4578
- q.format(left)
4579
- q.comma_breakable
4580
4575
 
4581
- q.seplist(values) { |value| q.format(value) }
4582
- q.comma_breakable
4576
+ q.group do
4577
+ q.text("[")
4583
4578
 
4584
- q.text("*")
4585
- q.format(right)
4579
+ q.indent do
4580
+ q.breakable("")
4581
+
4582
+ q.text("*")
4583
+ q.format(left)
4584
+ q.comma_breakable
4585
+
4586
+ q.seplist(values) { |value| q.format(value) }
4587
+ q.comma_breakable
4588
+
4589
+ q.text("*")
4590
+ q.format(right)
4591
+ end
4592
+
4593
+ q.breakable("")
4594
+ q.text("]")
4586
4595
  end
4587
4596
  end
4588
4597
  end
@@ -4804,7 +4813,7 @@ module SyntaxTree
4804
4813
  # [HeredocBeg] the opening of the heredoc
4805
4814
  attr_reader :beginning
4806
4815
 
4807
- # [String] the ending of the heredoc
4816
+ # [HeredocEnd] the ending of the heredoc
4808
4817
  attr_reader :ending
4809
4818
 
4810
4819
  # [Integer] how far to dedent the heredoc
@@ -4838,7 +4847,7 @@ module SyntaxTree
4838
4847
  end
4839
4848
 
4840
4849
  def child_nodes
4841
- [beginning, *parts]
4850
+ [beginning, *parts, ending]
4842
4851
  end
4843
4852
 
4844
4853
  alias deconstruct child_nodes
@@ -4874,7 +4883,7 @@ module SyntaxTree
4874
4883
  end
4875
4884
  end
4876
4885
 
4877
- q.text(ending)
4886
+ q.format(ending)
4878
4887
  end
4879
4888
  end
4880
4889
  end
@@ -4920,6 +4929,45 @@ module SyntaxTree
4920
4929
  end
4921
4930
  end
4922
4931
 
4932
+ # HeredocEnd represents the closing declaration of a heredoc.
4933
+ #
4934
+ # <<~DOC
4935
+ # contents
4936
+ # DOC
4937
+ #
4938
+ # In the example above the HeredocEnd node represents the closing DOC.
4939
+ class HeredocEnd < Node
4940
+ # [String] the closing declaration of the heredoc
4941
+ attr_reader :value
4942
+
4943
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
4944
+ attr_reader :comments
4945
+
4946
+ def initialize(value:, location:, comments: [])
4947
+ @value = value
4948
+ @location = location
4949
+ @comments = comments
4950
+ end
4951
+
4952
+ def accept(visitor)
4953
+ visitor.visit_heredoc_end(self)
4954
+ end
4955
+
4956
+ def child_nodes
4957
+ []
4958
+ end
4959
+
4960
+ alias deconstruct child_nodes
4961
+
4962
+ def deconstruct_keys(_keys)
4963
+ { value: value, location: location, comments: comments }
4964
+ end
4965
+
4966
+ def format(q)
4967
+ q.text(value)
4968
+ end
4969
+ end
4970
+
4923
4971
  # HshPtn represents matching against a hash pattern using the Ruby 2.7+
4924
4972
  # pattern matching syntax.
4925
4973
  #
@@ -5465,12 +5513,14 @@ module SyntaxTree
5465
5513
  q.format(predicate)
5466
5514
  q.text(" ?")
5467
5515
 
5468
- q.breakable
5469
- q.format(truthy)
5470
- q.text(" :")
5516
+ q.indent do
5517
+ q.breakable
5518
+ q.format(truthy)
5519
+ q.text(" :")
5471
5520
 
5472
- q.breakable
5473
- q.format(falsy)
5521
+ q.breakable
5522
+ q.format(falsy)
5523
+ end
5474
5524
  end
5475
5525
  end
5476
5526
 
@@ -8404,7 +8454,7 @@ module SyntaxTree
8404
8454
  end
8405
8455
 
8406
8456
  opening_quote, closing_quote =
8407
- if !Quotes.locked?(self)
8457
+ if !Quotes.locked?(self, q.quote)
8408
8458
  [q.quote, q.quote]
8409
8459
  elsif quote.start_with?("%")
8410
8460
  [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])]
@@ -548,13 +548,6 @@ module SyntaxTree
548
548
  parts[0].location.to(parts[-1].location)
549
549
  end
550
550
 
551
- # If there's the optional then keyword, then we'll delete that and use it
552
- # as the end bounds of the location.
553
- if (token = find_token(Kw, "then", consume: false))
554
- tokens.delete(token)
555
- location = location.to(token.location)
556
- end
557
-
558
551
  # If there is a plain *, then we're going to fix up the location of it
559
552
  # here because it currently doesn't have anything to use for its precise
560
553
  # location. If we hit a comma, then we've gone too far.
@@ -1647,9 +1640,19 @@ module SyntaxTree
1647
1640
  def on_heredoc_end(value)
1648
1641
  heredoc = @heredocs[-1]
1649
1642
 
1643
+ location =
1644
+ Location.token(
1645
+ line: lineno,
1646
+ char: char_pos,
1647
+ column: current_column,
1648
+ size: value.size + 1
1649
+ )
1650
+
1651
+ heredoc_end = HeredocEnd.new(value: value.chomp, location: location)
1652
+
1650
1653
  @heredocs[-1] = Heredoc.new(
1651
1654
  beginning: heredoc.beginning,
1652
- ending: value.chomp,
1655
+ ending: heredoc_end,
1653
1656
  dedent: heredoc.dedent,
1654
1657
  parts: heredoc.parts,
1655
1658
  location:
@@ -1698,12 +1701,6 @@ module SyntaxTree
1698
1701
  end
1699
1702
  end
1700
1703
 
1701
- # Delete the optional then keyword
1702
- if (token = find_token(Kw, "then", consume: false))
1703
- parts << token
1704
- tokens.delete(token)
1705
- end
1706
-
1707
1704
  HshPtn.new(
1708
1705
  constant: constant,
1709
1706
  keywords: keywords || [],
@@ -3013,6 +3010,11 @@ module SyntaxTree
3013
3010
  # (StringEmbExpr | StringDVar | TStringContent) part
3014
3011
  # ) -> StringContent
3015
3012
  def on_string_add(string, part)
3013
+ # Due to some eccentricities in how ripper works, you need this here in
3014
+ # case you have a syntax error with an embedded expression that doesn't
3015
+ # finish, as in: "#{"
3016
+ return string if part.is_a?(String)
3017
+
3016
3018
  location =
3017
3019
  string.parts.any? ? string.location.to(part.location) : part.location
3018
3020
 
@@ -1,4 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "syntax_tree/formatter/single_quotes"
4
- SyntaxTree::Formatter.prepend(SyntaxTree::Formatter::SingleQuotes)
3
+ SyntaxTree::Formatter::OPTIONS[:quote] = "'"
@@ -1,4 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "syntax_tree/formatter/trailing_comma"
4
- SyntaxTree::Formatter.prepend(SyntaxTree::Formatter::TrailingComma)
3
+ SyntaxTree::Formatter::OPTIONS[:trailing_comma] = true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SyntaxTree
4
- VERSION = "2.6.0"
4
+ VERSION = "2.8.0"
5
5
  end
@@ -49,9 +49,7 @@ module SyntaxTree
49
49
  # of circumstances, like when visiting the list of optional parameters
50
50
  # defined on a method.
51
51
  #
52
- class FieldVisitor < Visitor
53
- attr_reader :q
54
-
52
+ class FieldVisitor < BasicVisitor
55
53
  def visit_aref(node)
56
54
  node(node, "aref") do
57
55
  field("collection", node.collection)
@@ -499,6 +497,10 @@ module SyntaxTree
499
497
  visit_token(node, "heredoc_beg")
500
498
  end
501
499
 
500
+ def visit_heredoc_end(node)
501
+ visit_token(node, "heredoc_end")
502
+ end
503
+
502
504
  def visit_hshptn(node)
503
505
  node(node, "hshptn") do
504
506
  field("constant", node.constant) if node.constant
@@ -4,72 +4,7 @@ module SyntaxTree
4
4
  # Visitor is a parent class that provides the ability to walk down the tree
5
5
  # and handle a subset of nodes. By defining your own subclass, you can
6
6
  # explicitly handle a node type by defining a visit_* method.
7
- class Visitor
8
- # This is raised when you use the Visitor.visit_method method and it fails.
9
- # It is correctable to through DidYouMean.
10
- class VisitMethodError < StandardError
11
- attr_reader :visit_method
12
-
13
- def initialize(visit_method)
14
- @visit_method = visit_method
15
- super("Invalid visit method: #{visit_method}")
16
- end
17
- end
18
-
19
- # This class is used by DidYouMean to offer corrections to invalid visit
20
- # method names.
21
- class VisitMethodChecker
22
- attr_reader :visit_method
23
-
24
- def initialize(error)
25
- @visit_method = error.visit_method
26
- end
27
-
28
- def corrections
29
- @corrections ||=
30
- DidYouMean::SpellChecker.new(
31
- dictionary: Visitor.visit_methods
32
- ).correct(visit_method)
33
- end
34
-
35
- DidYouMean.correct_error(VisitMethodError, self)
36
- end
37
-
38
- class << self
39
- # This method is here to help folks write visitors.
40
- #
41
- # It's not always easy to ensure you're writing the correct method name in
42
- # the visitor since it's perfectly valid to define methods that don't
43
- # override these parent methods.
44
- #
45
- # If you use this method, you can ensure you're writing the correct method
46
- # name. It will raise an error if the visit method you're defining isn't
47
- # actually a method on the parent visitor.
48
- def visit_method(method_name)
49
- return if visit_methods.include?(method_name)
50
-
51
- raise VisitMethodError, method_name
52
- end
53
-
54
- # This is the list of all of the valid visit methods.
55
- def visit_methods
56
- @visit_methods ||=
57
- Visitor.instance_methods.grep(/^visit_(?!child_nodes)/)
58
- end
59
- end
60
-
61
- def visit(node)
62
- node&.accept(self)
63
- end
64
-
65
- def visit_all(nodes)
66
- nodes.map { |node| visit(node) }
67
- end
68
-
69
- def visit_child_nodes(node)
70
- visit_all(node.child_nodes)
71
- end
72
-
7
+ class Visitor < BasicVisitor
73
8
  # Visit an ARef node.
74
9
  alias visit_aref visit_child_nodes
75
10
 
@@ -259,6 +194,9 @@ module SyntaxTree
259
194
  # Visit a HeredocBeg node.
260
195
  alias visit_heredoc_beg visit_child_nodes
261
196
 
197
+ # Visit a HeredocEnd node.
198
+ alias visit_heredoc_end visit_child_nodes
199
+
262
200
  # Visit a HshPtn node.
263
201
  alias visit_hshptn visit_child_nodes
264
202
 
data/lib/syntax_tree.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "etc"
3
4
  require "json"
4
5
  require "pp"
5
6
  require "prettier_print"
@@ -10,6 +11,8 @@ require_relative "syntax_tree/formatter"
10
11
  require_relative "syntax_tree/node"
11
12
  require_relative "syntax_tree/parser"
12
13
  require_relative "syntax_tree/version"
14
+
15
+ require_relative "syntax_tree/basic_visitor"
13
16
  require_relative "syntax_tree/visitor"
14
17
  require_relative "syntax_tree/visitor/field_visitor"
15
18
  require_relative "syntax_tree/visitor/json_visitor"
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: 2.6.0
4
+ version: 2.8.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: 2022-05-16 00:00:00.000000000 Z
11
+ date: 2022-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prettier_print
@@ -121,10 +121,9 @@ files:
121
121
  - doc/logo.svg
122
122
  - exe/stree
123
123
  - lib/syntax_tree.rb
124
+ - lib/syntax_tree/basic_visitor.rb
124
125
  - lib/syntax_tree/cli.rb
125
126
  - lib/syntax_tree/formatter.rb
126
- - lib/syntax_tree/formatter/single_quotes.rb
127
- - lib/syntax_tree/formatter/trailing_comma.rb
128
127
  - lib/syntax_tree/language_server.rb
129
128
  - lib/syntax_tree/language_server/inlay_hints.rb
130
129
  - lib/syntax_tree/node.rb
@@ -161,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
160
  - !ruby/object:Gem::Version
162
161
  version: '0'
163
162
  requirements: []
164
- rubygems_version: 3.4.0.dev
163
+ rubygems_version: 3.3.3
165
164
  signing_key:
166
165
  specification_version: 4
167
166
  summary: A parser based on ripper
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SyntaxTree
4
- class Formatter
5
- # This module overrides the quote method on the formatter to use single
6
- # quotes for everything instead of double quotes.
7
- module SingleQuotes
8
- def quote
9
- "'"
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SyntaxTree
4
- class Formatter
5
- # This module overrides the trailing_comma? method on the formatter to
6
- # return true.
7
- module TrailingComma
8
- def trailing_comma?
9
- true
10
- end
11
- end
12
- end
13
- end