syntax_tree 4.2.0 → 5.0.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: 444c1c74319e55d9c3a656ff1796e6658237c0433a257eb6e2c66d8625d47243
4
- data.tar.gz: 2662a60c92265c6b257f6545ee1947269e1990f96003c60caef790eeb871eb32
3
+ metadata.gz: 1bd3ed5ae531078970bd28dd6e2f1eb4e6d759652620b8c6afa339baa66b6302
4
+ data.tar.gz: 7e0b8d80c5d490ed36baa045cee3c11da9a51bb34baf8915d4514508e1dee14a
5
5
  SHA512:
6
- metadata.gz: 9dddc08b2d7c2c3c7cec089f301b88220cf18a625ef7b155ad71810eaefd4cfe54c063279a4f817bc823b1eb586a222b106aa5889f3b736da901ca0404feedb5
7
- data.tar.gz: eaabea185c2f53aaf62ea76b1bb89987d7c472538025ae3f3e457f97eafe38b2533895f504100ed534785ffebaa18a013dd8d1f27432da974beedf0b7202861c
6
+ metadata.gz: d47ecd8be7ae9416005b10c71047cd80b0a06ecfe44b036d690d981a9ccd24dcb9f1d90e98198c29d3d08463ffe119f08edfe64914adf01f5f05770efc34debb
7
+ data.tar.gz: 1c01da0e77f9b9b633cf6dbce058f8e1aa889572a201ef223455ba0155b68ff978d4088d78e4a4c78e30a853fc994b05d2901f2a189c90beff643653a1b6eb6b
data/.gitattributes ADDED
@@ -0,0 +1 @@
1
+ bin/* linguist-language=Ruby
@@ -12,10 +12,12 @@ jobs:
12
12
  - '3.0'
13
13
  - '3.1'
14
14
  - head
15
+ - truffleruby-head
15
16
  name: CI
16
17
  runs-on: ubuntu-latest
17
18
  env:
18
19
  CI: true
20
+ TESTOPTS: --verbose
19
21
  steps:
20
22
  - uses: actions/checkout@master
21
23
  - uses: ruby/setup-ruby@v1
data/.rubocop.yml CHANGED
@@ -7,7 +7,7 @@ AllCops:
7
7
  SuggestExtensions: false
8
8
  TargetRubyVersion: 2.7
9
9
  Exclude:
10
- - '{bin,coverage,pkg,test/fixtures,vendor,tmp}/**/*'
10
+ - '{.git,.github,bin,coverage,pkg,test/fixtures,vendor,tmp}/**/*'
11
11
  - test.rb
12
12
 
13
13
  Layout/LineLength:
@@ -46,6 +46,9 @@ Naming/MethodParameterName:
46
46
  Naming/RescuedExceptionsVariableName:
47
47
  PreferredName: error
48
48
 
49
+ Style/CaseEquality:
50
+ Enabled: false
51
+
49
52
  Style/ExplicitBlockArgument:
50
53
  Enabled: false
51
54
 
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
+ ## [5.0.0] - 2022-11-09
10
+
11
+ ### Added
12
+
13
+ - Every node now implements the `#copy(**)` method, which provides a copy of the node with the given attributes replaced.
14
+ - Every node now implements the `#===(other)` method, which checks if the given node matches the current node for all attributes except for comments and location.
15
+ - There is a new `SyntaxTree::Visitor::MutationVisitor` and its convenience method `SyntaxTree.mutation` which can be used to mutate a syntax tree. For details on how to use this visitor, check the README.
16
+
17
+ ### Changed
18
+
19
+ - Nodes no longer have a `comments:` keyword on their initializers. By default, they initialize to an empty array. If you were previously passing comments into the initializer, you should now create the node first, then call `node.comments.concat` to add your comments.
20
+ - A lot of nodes have been folded into other nodes to make it easier to interact with the AST. This means that a lot of visit methods have been removed from the visitor and a lot of class definitions are no longer present. This also means that the nodes that received more function now have additional methods or fields to be able to differentiate them. Note that none of these changes have resulted in different formatting. The changes are listed below:
21
+ - `IfMod`, `UnlessMod`, `WhileMod`, `UntilMod` have been folded into `IfNode`, `UnlessNode`, `WhileNode`, and `UntilNode`. Each of the nodes now have a `modifier?` method to tell if it was originally in the modifier form. Consequently, the `visit_if_mod`, `visit_unless_mod`, `visit_while_mod`, and `visit_until_mod` methods have been removed from the visitor.
22
+ - `VarAlias` is no longer a node, and the `Alias` node has been renamed. They have been folded into the `AliasNode` node. The `AliasNode` node now has a `var_alias?` method to tell you if it is aliasing a global variable. Consequently, the `visit_var_alias` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_alias` instead.
23
+ - `Yield0` is no longer a node, and the `Yield` node has been renamed. They has been folded into the `YieldNode` node. The `YieldNode` node can now have its `arguments` field be `nil`. Consequently, the `visit_yield0` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_yield` instead.
24
+ - `FCall` is no longer a node, and the `Call` node has been renamed. They have been folded into the `CallNode` node. The `CallNode` node can now have its `receiver` and `operator` fields be `nil`. Consequently, the `visit_fcall` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_call` instead.
25
+ - `Dot2` and `Dot3` are no longer nodes. Instead they have become a single new `RangeNode` node. This node looks the same as `Dot2` and `Dot3`, except that it additionally has an `operator` field that contains the operator that created the node. Consequently, the `visit_dot2` and `visit_dot3` methods have been removed from the visitor interface. If you were previously using these methods, you should now use `visit_range` instead.
26
+ - `Def`, `DefEndless`, and `Defs` have been folded into the `DefNode` node. The `DefNode` node now has the `target` and `operator` fields which originally came from `Defs` which can both be `nil`. It also now has an `endless?` method on it to tell if the original node was found in the endless form. Finally the `bodystmt` field can now either be a `BodyStmt` as it was or any other kind of node since that was the body of the `DefEndless` node. The `visit_defs` and `visit_def_endless` methods on the visitor have therefore been removed.
27
+ - `DoBlock` and `BraceBlock` have now been folded into a `BlockNode` node. The `BlockNode` node now has a `keywords?` method on it that returns true if the block was constructed with the `do`..`end` keywords. The `visit_do_block` and `visit_brace_block` methods on the visitor have therefore been removed and replaced with the `visit_block` method.
28
+ - `Return0` is no longer a node, and the `Return` node has been renamed. They have been folded into the `ReturnNode` node. The `ReturnNode` node can now have its `arguments` field be `nil`. Consequently, the `visit_return0` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_return` instead.
29
+ - The `ArgsForward`, `Redo`, `Retry`, and `ZSuper` nodes no longer have `value` fields associated with them (which were always string literals corresponding to the keyword being used).
30
+ - The `Command` and `CommandCall` nodes now has `block` attributes on them. These attributes are used in the place where you would previously have had a `MethodAddBlock` structure. Where before the `MethodAddBlock` would have the command and block as its two children, you now just have one command node with the `block` attribute set to the `Block` node.
31
+ - Previously the formatting options were defined on an unfrozen hash called `SyntaxTree::Formatter::OPTIONS`. It was globally mutable, which made it impossible to reference from within a Ractor. As such, it has now been replaced with `SyntaxTree::Formatter::Options.new` which creates a new options object instance that can be modified without impacting global state. As a part of this change, formatting can now be performed from within a non-main Ractor. In order to check if the `plugin/single_quotes` plugin has been loaded, check if `SyntaxTree::Formatter::SINGLE_QUOTES` is defined. In order to check if the `plugin/trailing_comma` plugin has been loaded, check if `SyntaxTree::Formatter::TRAILING_COMMA` is defined.
32
+
33
+ ## [4.3.0] - 2022-10-28
34
+
35
+ ### Added
36
+
37
+ - [#183](https://github.com/ruby-syntax-tree/syntax_tree/pull/183) - Support TruffleRuby by eliminating internal pattern matching in some places and stopping some tests from running in other places.
38
+ - [#184](https://github.com/ruby-syntax-tree/syntax_tree/pull/184) - Remove internal pattern matching entirely.
39
+
40
+ ### Changed
41
+
42
+ - [#183](https://github.com/ruby-syntax-tree/syntax_tree/pull/183) - Pattern matching works against dynamic symbols now.
43
+ - [#184](https://github.com/ruby-syntax-tree/syntax_tree/pull/184) - Exit with the correct exit status within the rake tasks.
44
+
9
45
  ## [4.2.0] - 2022-10-25
10
46
 
11
47
  ### Added
@@ -414,7 +450,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
414
450
 
415
451
  - 🎉 Initial release! 🎉
416
452
 
417
- [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.2.0...HEAD
453
+ [unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.0.0...HEAD
454
+ [5.0.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.3.0...v5.0.0
455
+ [4.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.2.0...v4.3.0
418
456
  [4.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.1.0...v4.2.0
419
457
  [4.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.2...v4.1.0
420
458
  [4.0.2]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.1...v4.0.2
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_tree (4.2.0)
5
- prettier_print (>= 1.0.2)
4
+ syntax_tree (5.0.0)
5
+ prettier_print (>= 1.1.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
@@ -14,12 +14,12 @@ GEM
14
14
  parallel (1.22.1)
15
15
  parser (3.1.2.1)
16
16
  ast (~> 2.4.1)
17
- prettier_print (1.0.2)
17
+ prettier_print (1.1.0)
18
18
  rainbow (3.1.1)
19
19
  rake (13.0.6)
20
20
  regexp_parser (2.6.0)
21
21
  rexml (3.2.5)
22
- rubocop (1.37.1)
22
+ rubocop (1.38.0)
23
23
  json (~> 2.3)
24
24
  parallel (~> 1.10)
25
25
  parser (>= 3.1.2.1)
data/README.md CHANGED
@@ -27,23 +27,29 @@ It is built with only standard library dependencies. It additionally ships with
27
27
  - [SyntaxTree.read(filepath)](#syntaxtreereadfilepath)
28
28
  - [SyntaxTree.parse(source)](#syntaxtreeparsesource)
29
29
  - [SyntaxTree.format(source)](#syntaxtreeformatsource)
30
+ - [SyntaxTree.mutation(&block)](#syntaxtreemutationblock)
30
31
  - [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block)
31
32
  - [Nodes](#nodes)
32
33
  - [child_nodes](#child_nodes)
34
+ - [copy(**attrs)](#copyattrs)
33
35
  - [Pattern matching](#pattern-matching)
34
36
  - [pretty_print(q)](#pretty_printq)
35
37
  - [to_json(*opts)](#to_jsonopts)
36
38
  - [format(q)](#formatq)
39
+ - [===(other)](#other)
37
40
  - [construct_keys](#construct_keys)
38
41
  - [Visitor](#visitor)
39
42
  - [visit_method](#visit_method)
40
43
  - [BasicVisitor](#basicvisitor)
44
+ - [MutationVisitor](#mutationvisitor)
45
+ - [WithEnvironment](#withenvironment)
41
46
  - [Language server](#language-server)
42
47
  - [textDocument/formatting](#textdocumentformatting)
43
48
  - [textDocument/inlayHint](#textdocumentinlayhint)
44
49
  - [syntaxTree/visualizing](#syntaxtreevisualizing)
45
- - [Plugins](#plugins)
46
- - [Customization](#customization)
50
+ - [Customization](#customization)
51
+ - [Ignoring code](#ignoring-code)
52
+ - [Plugins](#plugins)
47
53
  - [Languages](#languages)
48
54
  - [Integration](#integration)
49
55
  - [Rake](#rake)
@@ -332,6 +338,10 @@ This function takes an input string containing Ruby code and returns the syntax
332
338
 
333
339
  This function takes an input string containing Ruby code, parses it into its underlying syntax tree, and formats it back out to a string. You can optionally pass a second argument to this method as well that is the maximum width to print. It defaults to `80`.
334
340
 
341
+ ### SyntaxTree.mutation(&block)
342
+
343
+ This function yields a new mutation visitor to the block, and then returns the initialized visitor. It's effectively a shortcut for creating a `SyntaxTree::Visitor::MutationVisitor` without having to remember the class name. For more information on that visitor, see the definition below.
344
+
335
345
  ### SyntaxTree.search(source, query, &block)
336
346
 
337
347
  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.
@@ -350,6 +360,20 @@ program.child_nodes.first.child_nodes.first
350
360
  # => (binary (int "1") :+ (int "1"))
351
361
  ```
352
362
 
363
+ ### copy(**attrs)
364
+
365
+ This method returns a copy of the node, with the given attributes replaced.
366
+
367
+ ```ruby
368
+ program = SyntaxTree.parse("1 + 1")
369
+
370
+ binary = program.statements.body.first
371
+ # => (binary (int "1") + (int "1"))
372
+
373
+ binary.copy(operator: :-)
374
+ # => (binary (int "1") - (int "1"))
375
+ ```
376
+
353
377
  ### Pattern matching
354
378
 
355
379
  Pattern matching is another way to descend the tree which is more specific than using `child_nodes`. Using Ruby's built-in pattern matching, you can extract the same information but be as specific about your constraints as you like. For example, with minimal constraints:
@@ -407,6 +431,18 @@ formatter.output.join
407
431
  # => "1 + 1"
408
432
  ```
409
433
 
434
+ ### ===(other)
435
+
436
+ Every node responds to `===`, which is used to check if the given other node matches all of the attributes of the current node except for location and comments. For example:
437
+
438
+ ```ruby
439
+ program1 = SyntaxTree.parse("1 + 1")
440
+ program2 = SyntaxTree.parse("1 + 1")
441
+
442
+ program1 === program2
443
+ # => true
444
+ ```
445
+
410
446
  ### construct_keys
411
447
 
412
448
  Every node responds to `construct_keys`, which will return a string that contains a Ruby pattern-matching expression that could be used to match against the current node. It's meant to be used in tooling and through the CLI mostly.
@@ -495,6 +531,42 @@ end
495
531
 
496
532
  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.
497
533
 
534
+ ### MutationVisitor
535
+
536
+ The `MutationVisitor` is a visitor that can be used to mutate the tree. It works by defining a default `visit_*` method that returns a copy of the given node with all of its attributes visited. This new node will replace the old node in the tree. Typically, you use the `#mutate` method on it to define mutations using patterns. For example:
537
+
538
+ ```ruby
539
+ # Create a new visitor
540
+ visitor = SyntaxTree::Visitor::MutationVisitor.new
541
+
542
+ # Specify that it should mutate If nodes with assignments in their predicates
543
+ visitor.mutate("IfNode[predicate: Assign | OpAssign]") do |node|
544
+ # Get the existing If's predicate node
545
+ predicate = node.predicate
546
+
547
+ # Create a new predicate node that wraps the existing predicate node
548
+ # in parentheses
549
+ predicate =
550
+ SyntaxTree::Paren.new(
551
+ lparen: SyntaxTree::LParen.default,
552
+ contents: predicate,
553
+ location: predicate.location
554
+ )
555
+
556
+ # Return a copy of this node with the new predicate
557
+ node.copy(predicate: predicate)
558
+ end
559
+
560
+ source = "if a = 1; end"
561
+ program = SyntaxTree.parse(source)
562
+
563
+ SyntaxTree::Formatter.format(source, program)
564
+ # => "if a = 1\nend\n"
565
+
566
+ SyntaxTree::Formatter.format(source, program.accept(visitor))
567
+ # => "if (a = 1)\nend\n"
568
+ ```
569
+
498
570
  ### WithEnvironment
499
571
 
500
572
  The `WithEnvironment` module can be included in visitors to automatically keep track of local variables and arguments
@@ -506,13 +578,13 @@ class MyVisitor < Visitor
506
578
  include WithEnvironment
507
579
 
508
580
  def visit_ident(node)
509
- # find_local will return a Local for any local variables or arguments present in the current environment or nil if
510
- # the identifier is not a local
581
+ # find_local will return a Local for any local variables or arguments
582
+ # present in the current environment or nil if the identifier is not a local
511
583
  local = current_environment.find_local(node)
512
584
 
513
- puts local.type # print the type of the local (:variable or :argument)
514
- puts local.definitions # print the array of locations where this local is defined
515
- puts local.usages # print the array of locations where this local occurs
585
+ puts local.type # the type of the local (:variable or :argument)
586
+ puts local.definitions # the array of locations where this local is defined
587
+ puts local.usages # the array of locations where this local occurs
516
588
  end
517
589
  end
518
590
  ```
@@ -549,18 +621,45 @@ Implicity, the `2 * 3` is going to be executed first because the `*` operator ha
549
621
 
550
622
  The language server additionally includes this custom request to return a textual representation of the syntax tree underlying the source code of a file. Language server clients can use this to (for example) open an additional tab with this information displayed.
551
623
 
552
- ## Plugins
624
+ ## Customization
625
+
626
+ There are multiple ways to customize Syntax Tree's behavior when parsing and formatting code. You can ignore certain sections of the source code, you can register plugins to provide custom formatting behavior, and you can register additional languages to be parsed and formatted.
627
+
628
+ ### Ignoring code
629
+
630
+ To ignore a section of source code, you can use a special `# stree-ignore` comment. This comment should be placed immediately above the code that you want to ignore. For example:
631
+
632
+ ```ruby
633
+ numbers = [
634
+ 10000,
635
+ 20000,
636
+ 30000
637
+ ]
638
+ ```
639
+
640
+ Normally the snippet above would be formatted as `numbers = [10_000, 20_000, 30_000]`. However, sometimes you want to keep the original formatting to improve readability or maintainability. In that case, you can put the ignore comment before it, as in:
641
+
642
+ ```ruby
643
+ # stree-ignore
644
+ numbers = [
645
+ 10000,
646
+ 20000,
647
+ 30000
648
+ ]
649
+ ```
650
+
651
+ Now when Syntax Tree goes to format that code, it will copy the source code exactly as it is, including the newlines and indentation.
553
652
 
554
- You can register additional customization and additional languages that can flow through the same CLI with Syntax Tree's plugin system. When invoking the CLI, you pass through the list of plugins with the `--plugins` options to the commands that accept them. They should be a comma-delimited list. When the CLI first starts, it will require the files corresponding to those names.
653
+ ### Plugins
555
654
 
556
- ### Customization
655
+ You can register additional customization that can flow through the same CLI with Syntax Tree's plugin system. When invoking the CLI, you pass through the list of plugins with the `--plugins` options to the commands that accept them. They should be a comma-delimited list. When the CLI first starts, it will require the files corresponding to those names.
557
656
 
558
- To register additional customization, define a file somewhere in your load path named `syntax_tree/my_plugin`. Then when invoking the CLI, you will pass `--plugins=my_plugin`. To require multiple, separate them by a comma. In this way, you can modify Syntax Tree however you would like. Some plugins ship with Syntax Tree itself. They are:
657
+ To register plugins, define a file somewhere in your load path named `syntax_tree/my_plugin`. Then when invoking the CLI, you will pass `--plugins=my_plugin`. To require multiple, separate them by a comma. In this way, you can modify Syntax Tree however you would like. Some plugins ship with Syntax Tree itself. They are:
559
658
 
560
659
  * `plugin/single_quotes` - This will change all of your string literals to use single quotes instead of the default double quotes.
561
660
  * `plugin/trailing_comma` - This will put trailing commas into multiline array literals, hash literals, and method calls that can support trailing commas.
562
661
 
563
- If you're using Syntax Tree as a library, you should require those files directly.
662
+ If you're using Syntax Tree as a library, you can require those files directly or manually pass those options to the formatter initializer through the `SyntaxTree::Formatter::Options` class.
564
663
 
565
664
  ### Languages
566
665
 
@@ -131,9 +131,14 @@ module SyntaxTree
131
131
 
132
132
  def run(item)
133
133
  source = item.source
134
- if source != item.handler.format(source, options.print_width)
135
- raise UnformattedError
136
- end
134
+ formatted =
135
+ item.handler.format(
136
+ source,
137
+ options.print_width,
138
+ options: options.formatter_options
139
+ )
140
+
141
+ raise UnformattedError if source != formatted
137
142
  rescue StandardError
138
143
  warn("[#{Color.yellow("warn")}] #{item.filepath}")
139
144
  raise
@@ -156,13 +161,23 @@ module SyntaxTree
156
161
 
157
162
  def run(item)
158
163
  handler = item.handler
159
-
160
164
  warning = "[#{Color.yellow("warn")}] #{item.filepath}"
161
- formatted = handler.format(item.source, options.print_width)
162
165
 
163
- if formatted != handler.format(formatted, options.print_width)
164
- raise NonIdempotentFormatError
165
- end
166
+ formatted =
167
+ handler.format(
168
+ item.source,
169
+ options.print_width,
170
+ options: options.formatter_options
171
+ )
172
+
173
+ double_formatted =
174
+ handler.format(
175
+ formatted,
176
+ options.print_width,
177
+ options: options.formatter_options
178
+ )
179
+
180
+ raise NonIdempotentFormatError if formatted != double_formatted
166
181
  rescue StandardError
167
182
  warn(warning)
168
183
  raise
@@ -182,7 +197,9 @@ module SyntaxTree
182
197
  def run(item)
183
198
  source = item.source
184
199
 
185
- formatter = Formatter.new(source, [])
200
+ formatter_options = options.formatter_options
201
+ formatter = Formatter.new(source, [], options: formatter_options)
202
+
186
203
  item.handler.parse(source).format(formatter)
187
204
  pp formatter.groups.first
188
205
  end
@@ -192,9 +209,10 @@ module SyntaxTree
192
209
  # would match the first expression of the input given.
193
210
  class Expr < Action
194
211
  def run(item)
195
- case item.handler.parse(item.source)
196
- in Program[statements: Statements[body: [expression]]]
197
- puts expression.construct_keys
212
+ program = item.handler.parse(item.source)
213
+
214
+ if (expressions = program.statements.body) && expressions.size == 1
215
+ puts expressions.first.construct_keys
198
216
  else
199
217
  warn("The input to `stree expr` must be a single expression.")
200
218
  exit(1)
@@ -205,7 +223,14 @@ module SyntaxTree
205
223
  # An action of the CLI that formats the input source and prints it out.
206
224
  class Format < Action
207
225
  def run(item)
208
- puts item.handler.format(item.source, options.print_width)
226
+ formatted =
227
+ item.handler.format(
228
+ item.source,
229
+ options.print_width,
230
+ options: options.formatter_options
231
+ )
232
+
233
+ puts formatted
209
234
  end
210
235
  end
211
236
 
@@ -272,7 +297,13 @@ module SyntaxTree
272
297
  start = Time.now
273
298
 
274
299
  source = item.source
275
- formatted = item.handler.format(source, options.print_width)
300
+ formatted =
301
+ item.handler.format(
302
+ source,
303
+ options.print_width,
304
+ options: options.formatter_options
305
+ )
306
+
276
307
  File.write(filepath, formatted) if item.writable?
277
308
 
278
309
  color = source == formatted ? Color.gray(filepath) : filepath
@@ -346,20 +377,16 @@ module SyntaxTree
346
377
  :plugins,
347
378
  :print_width,
348
379
  :scripts,
349
- :target_ruby_version
380
+ :formatter_options
350
381
 
351
- def initialize(print_width: DEFAULT_PRINT_WIDTH)
382
+ def initialize
352
383
  @ignore_files = []
353
384
  @plugins = []
354
- @print_width = print_width
385
+ @print_width = DEFAULT_PRINT_WIDTH
355
386
  @scripts = []
356
- @target_ruby_version = nil
387
+ @formatter_options = Formatter::Options.new
357
388
  end
358
389
 
359
- # TODO: This function causes a couple of side-effects that I really don't
360
- # like to have here. It mutates the global state by requiring the plugins,
361
- # and mutates the global options hash by adding the target ruby version.
362
- # That should be done on a config-by-config basis, not here.
363
390
  def parse(arguments)
364
391
  parser.parse!(arguments)
365
392
  end
@@ -403,8 +430,10 @@ module SyntaxTree
403
430
  # If there is a target ruby version specified on the command line,
404
431
  # parse that out and use it when formatting.
405
432
  opts.on("--target-ruby-version=VERSION") do |version|
406
- @target_ruby_version = Gem::Version.new(version)
407
- Formatter::OPTIONS[:target_ruby_version] = @target_ruby_version
433
+ @formatter_options =
434
+ Formatter::Options.new(
435
+ target_ruby_version: Formatter::SemanticVersion.new(version)
436
+ )
408
437
  end
409
438
  end
410
439
  end
@@ -496,10 +525,14 @@ module SyntaxTree
496
525
  Dir
497
526
  .glob(pattern)
498
527
  .each do |filepath|
499
- if File.readable?(filepath) &&
500
- options.ignore_files.none? { File.fnmatch?(_1, filepath) }
501
- queue << FileItem.new(filepath)
502
- end
528
+ # Skip past invalid filepaths by default.
529
+ next unless File.readable?(filepath)
530
+
531
+ # Skip past any ignored filepaths.
532
+ next if options.ignore_files.any? { File.fnmatch(_1, filepath) }
533
+
534
+ # Otherwise, a new file item for the given filepath to the list.
535
+ queue << FileItem.new(filepath)
503
536
  end
504
537
  end
505
538
 
@@ -4,21 +4,63 @@ module SyntaxTree
4
4
  # A slightly enhanced PP that knows how to format recursively including
5
5
  # comments.
6
6
  class Formatter < PrettierPrint
7
+ # Unfortunately, Gem::Version.new is not ractor-safe because it performs
8
+ # global caching using a class variable. This works around that by just
9
+ # setting the instance variables directly.
10
+ class SemanticVersion < ::Gem::Version
11
+ def initialize(version)
12
+ @version = version
13
+ @segments = nil
14
+ end
15
+ end
16
+
7
17
  # We want to minimize as much as possible the number of options that are
8
18
  # available in syntax tree. For the most part, if users want non-default
9
19
  # formatting, they should override the format methods on the specific nodes
10
20
  # themselves. However, because of some history with prettier and the fact
11
21
  # that folks have become entrenched in their ways, we decided to provide a
12
22
  # 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 = {
18
- quote: "\"",
19
- trailing_comma: false,
20
- target_ruby_version: Gem::Version.new(RUBY_VERSION)
21
- }
23
+ class Options
24
+ attr_reader :quote, :trailing_comma, :target_ruby_version
25
+
26
+ def initialize(
27
+ quote: :default,
28
+ trailing_comma: :default,
29
+ target_ruby_version: :default
30
+ )
31
+ @quote =
32
+ if quote == :default
33
+ # We ship with a single quotes plugin that will define this
34
+ # constant. That constant is responsible for determining the default
35
+ # quote style. If it's defined, we default to single quotes,
36
+ # otherwise we default to double quotes.
37
+ defined?(SINGLE_QUOTES) ? "'" : "\""
38
+ else
39
+ quote
40
+ end
41
+
42
+ @trailing_comma =
43
+ if trailing_comma == :default
44
+ # We ship with a trailing comma plugin that will define this
45
+ # constant. That constant is responsible for determining the default
46
+ # trailing comma value. If it's defined, then we default to true.
47
+ # Otherwise we default to false.
48
+ defined?(TRAILING_COMMA)
49
+ else
50
+ trailing_comma
51
+ end
52
+
53
+ @target_ruby_version =
54
+ if target_ruby_version == :default
55
+ # The default target Ruby version is the current version of Ruby.
56
+ # This is really only used for very niche cases, and it shouldn't be
57
+ # used by most users.
58
+ SemanticVersion.new(RUBY_VERSION)
59
+ else
60
+ target_ruby_version
61
+ end
62
+ end
63
+ end
22
64
 
23
65
  COMMENT_PRIORITY = 1
24
66
  HEREDOC_PRIORITY = 2
@@ -30,22 +72,16 @@ module SyntaxTree
30
72
  attr_reader :quote, :trailing_comma, :target_ruby_version
31
73
  alias trailing_comma? trailing_comma
32
74
 
33
- def initialize(
34
- source,
35
- *args,
36
- quote: OPTIONS[:quote],
37
- trailing_comma: OPTIONS[:trailing_comma],
38
- target_ruby_version: OPTIONS[:target_ruby_version]
39
- )
75
+ def initialize(source, *args, options: Options.new)
40
76
  super(*args)
41
77
 
42
78
  @source = source
43
79
  @stack = []
44
80
 
45
- # Memoizing these values per formatter to make access faster.
46
- @quote = quote
47
- @trailing_comma = trailing_comma
48
- @target_ruby_version = target_ruby_version
81
+ # Memoizing these values to make access faster.
82
+ @quote = options.quote
83
+ @trailing_comma = options.trailing_comma
84
+ @target_ruby_version = options.target_ruby_version
49
85
  end
50
86
 
51
87
  def self.format(source, node)
@@ -69,11 +69,10 @@ module SyntaxTree
69
69
  #
70
70
  def visit_binary(node)
71
71
  case stack[-2]
72
- in Assign | OpAssign
72
+ when Assign, OpAssign
73
73
  parentheses(node.location)
74
- in Binary[operator: operator] if operator != node.operator
75
- parentheses(node.location)
76
- else
74
+ when Binary
75
+ parentheses(node.location) if stack[-2].operator != node.operator
77
76
  end
78
77
 
79
78
  super
@@ -91,9 +90,8 @@ module SyntaxTree
91
90
  #
92
91
  def visit_if_op(node)
93
92
  case stack[-2]
94
- in Assign | Binary | IfOp | OpAssign
93
+ when Assign, Binary, IfOp, OpAssign
95
94
  parentheses(node.location)
96
- else
97
95
  end
98
96
 
99
97
  super