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 +4 -4
- data/.gitattributes +1 -0
- data/.github/workflows/main.yml +2 -0
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +39 -1
- data/Gemfile.lock +4 -4
- data/README.md +111 -12
- data/lib/syntax_tree/cli.rb +61 -28
- data/lib/syntax_tree/formatter.rb +56 -20
- data/lib/syntax_tree/language_server/inlay_hints.rb +4 -6
- data/lib/syntax_tree/language_server.rb +64 -17
- data/lib/syntax_tree/node.rb +3119 -1409
- data/lib/syntax_tree/parser.rb +118 -92
- data/lib/syntax_tree/pattern.rb +179 -64
- data/lib/syntax_tree/plugin/single_quotes.rb +5 -1
- data/lib/syntax_tree/plugin/trailing_comma.rb +5 -1
- data/lib/syntax_tree/rake/task.rb +1 -1
- data/lib/syntax_tree/version.rb +1 -1
- data/lib/syntax_tree/visitor/environment.rb +7 -4
- data/lib/syntax_tree/visitor/field_visitor.rb +23 -117
- data/lib/syntax_tree/visitor/mutation_visitor.rb +924 -0
- data/lib/syntax_tree/visitor/with_environment.rb +0 -8
- data/lib/syntax_tree/visitor.rb +12 -48
- data/lib/syntax_tree.rb +14 -2
- data/syntax_tree.gemspec +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1bd3ed5ae531078970bd28dd6e2f1eb4e6d759652620b8c6afa339baa66b6302
|
4
|
+
data.tar.gz: 7e0b8d80c5d490ed36baa045cee3c11da9a51bb34baf8915d4514508e1dee14a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d47ecd8be7ae9416005b10c71047cd80b0a06ecfe44b036d690d981a9ccd24dcb9f1d90e98198c29d3d08463ffe119f08edfe64914adf01f5f05770efc34debb
|
7
|
+
data.tar.gz: 1c01da0e77f9b9b633cf6dbce058f8e1aa889572a201ef223455ba0155b68ff978d4088d78e4a4c78e30a853fc994b05d2901f2a189c90beff643653a1b6eb6b
|
data/.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
bin/* linguist-language=Ruby
|
data/.github/workflows/main.yml
CHANGED
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/
|
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 (
|
5
|
-
prettier_print (>= 1.0
|
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
|
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.
|
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
|
-
- [
|
46
|
-
- [
|
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
|
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 #
|
514
|
-
puts local.definitions #
|
515
|
-
puts local.usages #
|
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
|
-
##
|
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
|
-
|
653
|
+
### Plugins
|
555
654
|
|
556
|
-
|
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
|
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
|
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
|
|
data/lib/syntax_tree/cli.rb
CHANGED
@@ -131,9 +131,14 @@ module SyntaxTree
|
|
131
131
|
|
132
132
|
def run(item)
|
133
133
|
source = item.source
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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
|
-
|
196
|
-
|
197
|
-
|
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
|
-
|
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 =
|
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
|
-
:
|
380
|
+
:formatter_options
|
350
381
|
|
351
|
-
def initialize
|
382
|
+
def initialize
|
352
383
|
@ignore_files = []
|
353
384
|
@plugins = []
|
354
|
-
@print_width =
|
385
|
+
@print_width = DEFAULT_PRINT_WIDTH
|
355
386
|
@scripts = []
|
356
|
-
@
|
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
|
-
@
|
407
|
-
|
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
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
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
|
-
|
72
|
+
when Assign, OpAssign
|
73
73
|
parentheses(node.location)
|
74
|
-
|
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
|
-
|
93
|
+
when Assign, Binary, IfOp, OpAssign
|
95
94
|
parentheses(node.location)
|
96
|
-
else
|
97
95
|
end
|
98
96
|
|
99
97
|
super
|