syntax_tree 4.3.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +26 -1
- data/Gemfile.lock +4 -4
- data/README.md +111 -12
- data/lib/syntax_tree/cli.rb +57 -25
- data/lib/syntax_tree/formatter.rb +56 -20
- data/lib/syntax_tree/node.rb +3119 -1409
- data/lib/syntax_tree/parser.rb +118 -92
- data/lib/syntax_tree/pattern.rb +3 -3
- data/lib/syntax_tree/plugin/single_quotes.rb +5 -1
- data/lib/syntax_tree/plugin/trailing_comma.rb +5 -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/.rubocop.yml
    CHANGED
    
    
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -6,6 +6,30 @@ 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 | 
            +
             | 
| 9 33 | 
             
            ## [4.3.0] - 2022-10-28
         | 
| 10 34 |  | 
| 11 35 | 
             
            ### Added
         | 
| @@ -426,7 +450,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a | |
| 426 450 |  | 
| 427 451 | 
             
            - 🎉 Initial release! 🎉
         | 
| 428 452 |  | 
| 429 | 
            -
            [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
         | 
| 430 455 | 
             
            [4.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.2.0...v4.3.0
         | 
| 431 456 | 
             
            [4.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.1.0...v4.2.0
         | 
| 432 457 | 
             
            [4.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.2...v4.1.0
         | 
    
        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
         | 
| @@ -206,7 +223,14 @@ module SyntaxTree | |
| 206 223 | 
             
                # An action of the CLI that formats the input source and prints it out.
         | 
| 207 224 | 
             
                class Format < Action
         | 
| 208 225 | 
             
                  def run(item)
         | 
| 209 | 
            -
                     | 
| 226 | 
            +
                    formatted =
         | 
| 227 | 
            +
                      item.handler.format(
         | 
| 228 | 
            +
                        item.source,
         | 
| 229 | 
            +
                        options.print_width,
         | 
| 230 | 
            +
                        options: options.formatter_options
         | 
| 231 | 
            +
                      )
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                    puts formatted
         | 
| 210 234 | 
             
                  end
         | 
| 211 235 | 
             
                end
         | 
| 212 236 |  | 
| @@ -273,7 +297,13 @@ module SyntaxTree | |
| 273 297 | 
             
                    start = Time.now
         | 
| 274 298 |  | 
| 275 299 | 
             
                    source = item.source
         | 
| 276 | 
            -
                    formatted = | 
| 300 | 
            +
                    formatted =
         | 
| 301 | 
            +
                      item.handler.format(
         | 
| 302 | 
            +
                        source,
         | 
| 303 | 
            +
                        options.print_width,
         | 
| 304 | 
            +
                        options: options.formatter_options
         | 
| 305 | 
            +
                      )
         | 
| 306 | 
            +
             | 
| 277 307 | 
             
                    File.write(filepath, formatted) if item.writable?
         | 
| 278 308 |  | 
| 279 309 | 
             
                    color = source == formatted ? Color.gray(filepath) : filepath
         | 
| @@ -347,20 +377,16 @@ module SyntaxTree | |
| 347 377 | 
             
                              :plugins,
         | 
| 348 378 | 
             
                              :print_width,
         | 
| 349 379 | 
             
                              :scripts,
         | 
| 350 | 
            -
                              : | 
| 380 | 
            +
                              :formatter_options
         | 
| 351 381 |  | 
| 352 | 
            -
                  def initialize | 
| 382 | 
            +
                  def initialize
         | 
| 353 383 | 
             
                    @ignore_files = []
         | 
| 354 384 | 
             
                    @plugins = []
         | 
| 355 | 
            -
                    @print_width =  | 
| 385 | 
            +
                    @print_width = DEFAULT_PRINT_WIDTH
         | 
| 356 386 | 
             
                    @scripts = []
         | 
| 357 | 
            -
                    @ | 
| 387 | 
            +
                    @formatter_options = Formatter::Options.new
         | 
| 358 388 | 
             
                  end
         | 
| 359 389 |  | 
| 360 | 
            -
                  # TODO: This function causes a couple of side-effects that I really don't
         | 
| 361 | 
            -
                  # like to have here. It mutates the global state by requiring the plugins,
         | 
| 362 | 
            -
                  # and mutates the global options hash by adding the target ruby version.
         | 
| 363 | 
            -
                  # That should be done on a config-by-config basis, not here.
         | 
| 364 390 | 
             
                  def parse(arguments)
         | 
| 365 391 | 
             
                    parser.parse!(arguments)
         | 
| 366 392 | 
             
                  end
         | 
| @@ -404,8 +430,10 @@ module SyntaxTree | |
| 404 430 | 
             
                      # If there is a target ruby version specified on the command line,
         | 
| 405 431 | 
             
                      # parse that out and use it when formatting.
         | 
| 406 432 | 
             
                      opts.on("--target-ruby-version=VERSION") do |version|
         | 
| 407 | 
            -
                        @ | 
| 408 | 
            -
             | 
| 433 | 
            +
                        @formatter_options =
         | 
| 434 | 
            +
                          Formatter::Options.new(
         | 
| 435 | 
            +
                            target_ruby_version: Formatter::SemanticVersion.new(version)
         | 
| 436 | 
            +
                          )
         | 
| 409 437 | 
             
                      end
         | 
| 410 438 | 
             
                    end
         | 
| 411 439 | 
             
                  end
         | 
| @@ -497,10 +525,14 @@ module SyntaxTree | |
| 497 525 | 
             
                        Dir
         | 
| 498 526 | 
             
                          .glob(pattern)
         | 
| 499 527 | 
             
                          .each do |filepath|
         | 
| 500 | 
            -
                             | 
| 501 | 
            -
             | 
| 502 | 
            -
             | 
| 503 | 
            -
                             | 
| 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)
         | 
| 504 536 | 
             
                          end
         | 
| 505 537 | 
             
                      end
         | 
| 506 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)
         |