├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── auto-merge.yml │ ├── gh-pages.yml │ └── main.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── bench ├── console ├── profile └── whitequark ├── config └── rubocop.yml ├── doc ├── changing_structure.md └── logo.svg ├── exe ├── stree └── yarv ├── lib ├── syntax_tree.rb └── syntax_tree │ ├── basic_visitor.rb │ ├── cli.rb │ ├── database.rb │ ├── dsl.rb │ ├── field_visitor.rb │ ├── formatter.rb │ ├── index.rb │ ├── json_visitor.rb │ ├── language_server.rb │ ├── match_visitor.rb │ ├── mermaid.rb │ ├── mermaid_visitor.rb │ ├── mutation_visitor.rb │ ├── node.rb │ ├── parser.rb │ ├── pattern.rb │ ├── plugin │ ├── disable_ternary.rb │ ├── single_quotes.rb │ └── trailing_comma.rb │ ├── pretty_print_visitor.rb │ ├── rake │ ├── check_task.rb │ ├── task.rb │ └── write_task.rb │ ├── rake_tasks.rb │ ├── reflection.rb │ ├── search.rb │ ├── translation.rb │ ├── translation │ ├── parser.rb │ └── rubocop_ast.rb │ ├── version.rb │ ├── visitor.rb │ ├── with_scope.rb │ ├── yarv.rb │ └── yarv │ ├── assembler.rb │ ├── basic_block.rb │ ├── bf.rb │ ├── calldata.rb │ ├── compiler.rb │ ├── control_flow_graph.rb │ ├── data_flow_graph.rb │ ├── decompiler.rb │ ├── disassembler.rb │ ├── instruction_sequence.rb │ ├── instructions.rb │ ├── legacy.rb │ ├── local_table.rb │ ├── sea_of_nodes.rb │ └── vm.rb ├── syntax_tree.gemspec ├── tasks ├── sorbet.rake └── whitequark.rake └── test ├── cli_test.rb ├── compiler_test.rb ├── encoded.rb ├── fixtures ├── CHAR.rb ├── access_ctrl.rb ├── alias.rb ├── aref.rb ├── aref_field.rb ├── arg_block.rb ├── arg_paren.rb ├── arg_star.rb ├── args.rb ├── args_forward.rb ├── array_literal.rb ├── aryptn.rb ├── assign.rb ├── assoc.rb ├── assoc_splat.rb ├── backref.rb ├── backtick.rb ├── bare_assoc_hash.rb ├── begin.rb ├── begin_block.rb ├── binary.rb ├── block_arg.rb ├── block_var.rb ├── bodystmt.rb ├── brace_block.rb ├── break.rb ├── call.rb ├── case.rb ├── class.rb ├── command.rb ├── command_call.rb ├── command_def_endless.rb ├── const.rb ├── const_path_field.rb ├── const_path_ref.rb ├── const_ref.rb ├── cvar.rb ├── def.rb ├── def_endless.rb ├── defined.rb ├── defs.rb ├── do_block.rb ├── dot2.rb ├── dot3.rb ├── dyna_symbol.rb ├── else.rb ├── elsif.rb ├── embdoc.rb ├── end_block.rb ├── end_content.rb ├── ensure.rb ├── excessed_comma.rb ├── fcall.rb ├── field.rb ├── float_literal.rb ├── fndptn.rb ├── for.rb ├── gvar.rb ├── hash.rb ├── heredoc.rb ├── heredoc_beg.rb ├── hshptn.rb ├── ident.rb ├── if.rb ├── if_mod.rb ├── ifop.rb ├── imaginary.rb ├── in.rb ├── int.rb ├── ivar.rb ├── kw.rb ├── kwrest_param.rb ├── label.rb ├── lambda.rb ├── massign.rb ├── method_add_arg.rb ├── method_add_block.rb ├── mlhs.rb ├── mlhs_paren.rb ├── module.rb ├── mrhs.rb ├── next.rb ├── not.rb ├── op.rb ├── opassign.rb ├── params.rb ├── paren.rb ├── period.rb ├── pinned_begin.rb ├── program.rb ├── qsymbols.rb ├── qwords.rb ├── rassign.rb ├── rational_literal.rb ├── redo.rb ├── regexp_literal.rb ├── rescue.rb ├── rescue_mod.rb ├── rest_param.rb ├── retry.rb ├── return.rb ├── return0.rb ├── sclass.rb ├── statements.rb ├── string_concat.rb ├── string_dvar.rb ├── string_embexpr.rb ├── string_literal.rb ├── super.rb ├── symbol_literal.rb ├── symbols.rb ├── top_const_field.rb ├── top_const_ref.rb ├── tstring_content.rb ├── unary.rb ├── undef.rb ├── unless.rb ├── unless_mod.rb ├── until.rb ├── until_mod.rb ├── var_alias.rb ├── var_field.rb ├── var_field_rassign.rb ├── var_ref.rb ├── vcall.rb ├── void_stmt.rb ├── when.rb ├── while.rb ├── while_mod.rb ├── word.rb ├── words.rb ├── xstring_literal.rb ├── yield.rb ├── yield0.rb └── zsuper.rb ├── formatting_test.rb ├── idempotency_test.rb ├── index_test.rb ├── language_server └── inlay_hints_test.rb ├── language_server_test.rb ├── location_test.rb ├── mutation_test.rb ├── node_test.rb ├── parser_test.rb ├── plugin ├── disable_ternary_test.rb ├── single_quotes_test.rb └── trailing_comma_test.rb ├── quotes_test.rb ├── ractor_test.rb ├── rake_test.rb ├── search_test.rb ├── syntax_tree_test.rb ├── test_helper.rb ├── translation ├── parser.txt └── parser_test.rb ├── visitor_test.rb ├── with_scope_test.rb └── yarv_test.rb /.gitattributes: -------------------------------------------------------------------------------- 1 | bin/* linguist-language=Ruby 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bundler" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.4.0 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | run: gh pr merge --auto --merge "$PR_URL" 20 | env: 21 | PR_URL: ${{github.event.pull_request.html_url}} 22 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 23 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy rdoc to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow one concurrent deployment 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | # Build job 24 | build: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup Pages 30 | uses: actions/configure-pages@v5 31 | - name: Set up Ruby 32 | uses: ruby/setup-ruby@v1 33 | with: 34 | bundler-cache: true 35 | ruby-version: '3.1' 36 | - name: Generate docs 37 | run: | 38 | gem install rdoc 39 | rdoc --main README.md --op _site --exclude={Gemfile,Rakefile,"coverage/*","vendor/*","bin/*","test/*","tmp/*"} 40 | cp -r doc _site/doc 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | 44 | # Deployment job 45 | deploy: 46 | environment: 47 | name: github-pages 48 | url: ${{ steps.deployment.outputs.page_url }} 49 | runs-on: ubuntu-latest 50 | needs: build 51 | steps: 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v3 55 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | ci: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ruby: 13 | - '2.7.0' 14 | - '3.0' 15 | - '3.1' 16 | - '3.2' 17 | - head 18 | - truffleruby-head 19 | name: CI 20 | runs-on: ubuntu-latest 21 | env: 22 | CI: true 23 | # TESTOPTS: --verbose 24 | steps: 25 | - uses: actions/checkout@master 26 | - uses: ruby/setup-ruby@v1 27 | with: 28 | bundler-cache: true 29 | ruby-version: ${{ matrix.ruby }} 30 | - name: Test 31 | run: bundle exec rake test 32 | 33 | check: 34 | name: Check 35 | runs-on: ubuntu-latest 36 | env: 37 | CI: true 38 | steps: 39 | - uses: actions/checkout@master 40 | - uses: ruby/setup-ruby@v1 41 | with: 42 | bundler-cache: true 43 | ruby-version: '3.2' 44 | - name: Check 45 | run: | 46 | bundle exec rake stree:check 47 | bundle exec rubocop 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /pkg/ 6 | /rdocs/ 7 | /sorbet/ 8 | /spec/reports/ 9 | /tmp/ 10 | /vendor/ 11 | 12 | test.rb 13 | query.txt 14 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: config/rubocop.yml 2 | 3 | AllCops: 4 | DisplayCopNames: true 5 | DisplayStyleGuide: true 6 | NewCops: enable 7 | SuggestExtensions: false 8 | TargetRubyVersion: 2.7 9 | Exclude: 10 | - '{.git,.github,bin,coverage,pkg,sorbet,spec,test/fixtures,vendor,tmp}/**/*' 11 | - test.rb 12 | 13 | Gemspec/DevelopmentDependencies: 14 | Enabled: false 15 | 16 | Layout/LineLength: 17 | Max: 80 18 | 19 | Lint/AmbiguousBlockAssociation: 20 | Enabled: false 21 | 22 | Lint/AmbiguousOperatorPrecedence: 23 | Enabled: false 24 | 25 | Lint/AmbiguousRange: 26 | Enabled: false 27 | 28 | Lint/BooleanSymbol: 29 | Enabled: false 30 | 31 | Lint/Debugger: 32 | Enabled: false 33 | 34 | Lint/DuplicateBranch: 35 | Enabled: false 36 | 37 | Lint/EmptyBlock: 38 | Enabled: false 39 | 40 | Lint/InterpolationCheck: 41 | Enabled: false 42 | 43 | Lint/MissingSuper: 44 | Enabled: false 45 | 46 | Lint/NonLocalExitFromIterator: 47 | Enabled: false 48 | 49 | Lint/RedundantRequireStatement: 50 | Enabled: false 51 | 52 | Lint/RescueException: 53 | Enabled: false 54 | 55 | Lint/SuppressedException: 56 | Enabled: false 57 | 58 | Lint/UnderscorePrefixedVariableName: 59 | Enabled: false 60 | 61 | Lint/UnusedMethodArgument: 62 | AllowUnusedKeywordArguments: true 63 | 64 | Metrics: 65 | Enabled: false 66 | 67 | Naming/MethodName: 68 | Enabled: false 69 | 70 | Naming/MethodParameterName: 71 | Enabled: false 72 | 73 | Naming/RescuedExceptionsVariableName: 74 | PreferredName: error 75 | 76 | Naming/VariableNumber: 77 | Enabled: false 78 | 79 | Security/Eval: 80 | Enabled: false 81 | 82 | Style/AccessorGrouping: 83 | Enabled: false 84 | 85 | Style/Alias: 86 | Enabled: false 87 | 88 | Style/CaseEquality: 89 | Enabled: false 90 | 91 | Style/CaseLikeIf: 92 | Enabled: false 93 | 94 | Style/ClassVars: 95 | Enabled: false 96 | 97 | Style/CombinableLoops: 98 | Enabled: false 99 | 100 | Style/DocumentDynamicEvalDefinition: 101 | Enabled: false 102 | 103 | Style/Documentation: 104 | Enabled: false 105 | 106 | Style/EndBlock: 107 | Enabled: false 108 | 109 | Style/ExplicitBlockArgument: 110 | Enabled: false 111 | 112 | Style/FormatString: 113 | Enabled: false 114 | 115 | Style/FormatStringToken: 116 | Enabled: false 117 | 118 | Style/GuardClause: 119 | Enabled: false 120 | 121 | Style/HashLikeCase: 122 | Enabled: false 123 | 124 | Style/IdenticalConditionalBranches: 125 | Enabled: false 126 | 127 | Style/IfInsideElse: 128 | Enabled: false 129 | 130 | Style/IfWithBooleanLiteralBranches: 131 | Enabled: false 132 | 133 | Style/KeywordParametersOrder: 134 | Enabled: false 135 | 136 | Style/MissingRespondToMissing: 137 | Enabled: false 138 | 139 | Style/MutableConstant: 140 | Enabled: false 141 | 142 | Style/NegatedIfElseCondition: 143 | Enabled: false 144 | 145 | Style/Next: 146 | Enabled: false 147 | 148 | Style/NumericPredicate: 149 | Enabled: false 150 | 151 | Style/ParallelAssignment: 152 | Enabled: false 153 | 154 | Style/PerlBackrefs: 155 | Enabled: false 156 | 157 | Style/SafeNavigation: 158 | Enabled: false 159 | 160 | Style/SpecialGlobalVars: 161 | Enabled: false 162 | 163 | Style/StructInheritance: 164 | Enabled: false 165 | 166 | Style/YodaExpression: 167 | Enabled: false 168 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kddnewton@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | syntax_tree (6.1.1) 5 | prettier_print (>= 1.2.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ast (2.4.3) 11 | docile (1.4.0) 12 | json (2.12.2) 13 | language_server-protocol (3.17.0.5) 14 | lint_roller (1.1.0) 15 | minitest (5.25.5) 16 | parallel (1.27.0) 17 | parser (3.3.8.0) 18 | ast (~> 2.4.1) 19 | racc 20 | prettier_print (1.2.1) 21 | prism (1.4.0) 22 | racc (1.8.1) 23 | rainbow (3.1.1) 24 | rake (13.3.0) 25 | regexp_parser (2.10.0) 26 | rubocop (1.76.1) 27 | json (~> 2.3) 28 | language_server-protocol (~> 3.17.0.2) 29 | lint_roller (~> 1.1.0) 30 | parallel (~> 1.10) 31 | parser (>= 3.3.0.2) 32 | rainbow (>= 2.2.2, < 4.0) 33 | regexp_parser (>= 2.9.3, < 3.0) 34 | rubocop-ast (>= 1.45.0, < 2.0) 35 | ruby-progressbar (~> 1.7) 36 | unicode-display_width (>= 2.4.0, < 4.0) 37 | rubocop-ast (1.45.1) 38 | parser (>= 3.3.7.2) 39 | prism (~> 1.4) 40 | ruby-progressbar (1.13.0) 41 | simplecov (0.22.0) 42 | docile (~> 1.1) 43 | simplecov-html (~> 0.11) 44 | simplecov_json_formatter (~> 0.1) 45 | simplecov-html (0.12.3) 46 | simplecov_json_formatter (0.1.4) 47 | unicode-display_width (3.1.4) 48 | unicode-emoji (~> 4.0, >= 4.0.4) 49 | unicode-emoji (4.0.4) 50 | 51 | PLATFORMS 52 | arm64-darwin-21 53 | ruby 54 | x86_64-darwin-19 55 | x86_64-darwin-21 56 | x86_64-linux 57 | 58 | DEPENDENCIES 59 | bundler 60 | minitest 61 | rake 62 | rubocop 63 | simplecov 64 | syntax_tree! 65 | 66 | BUNDLED WITH 67 | 2.3.6 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present Kevin Newton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | require "syntax_tree/rake_tasks" 6 | 7 | Rake.add_rakelib "tasks" 8 | 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << "test" 11 | t.libs << "lib" 12 | t.test_files = FileList["test/**/*_test.rb"] 13 | end 14 | 15 | task default: :test 16 | 17 | configure = ->(task) do 18 | task.source_files = 19 | FileList[ 20 | %w[ 21 | Gemfile 22 | Rakefile 23 | syntax_tree.gemspec 24 | lib/**/*.rb 25 | tasks/*.rake 26 | test/*.rb 27 | ] 28 | ] 29 | 30 | # Since Syntax Tree supports back to Ruby 2.7.0, we need to make sure that we 31 | # format our code such that it's compatible with that version. This actually 32 | # has very little effect on the output, the only change at the moment is that 33 | # Ruby < 2.7.3 didn't allow a newline before the closing brace of a hash 34 | # pattern. 35 | task.target_ruby_version = Gem::Version.new("2.7.0") 36 | end 37 | 38 | SyntaxTree::Rake::CheckTask.new(&configure) 39 | SyntaxTree::Rake::WriteTask.new(&configure) 40 | -------------------------------------------------------------------------------- /bin/bench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/inline" 5 | 6 | gemfile do 7 | source "https://rubygems.org" 8 | gem "benchmark-ips" 9 | gem "parser", require: "parser/current" 10 | gem "ruby_parser" 11 | end 12 | 13 | $:.unshift(File.expand_path("../lib", __dir__)) 14 | require "syntax_tree" 15 | 16 | def compare(filepath) 17 | prefix = "#{File.expand_path("..", __dir__)}/" 18 | puts "=== #{filepath.delete_prefix(prefix)} ===" 19 | 20 | source = File.read(filepath) 21 | 22 | Benchmark.ips do |x| 23 | x.report("syntax_tree") { SyntaxTree.parse(source) } 24 | x.report("parser") { Parser::CurrentRuby.parse(source) } 25 | x.report("ruby_parser") { RubyParser.new.parse(source) } 26 | x.compare! 27 | end 28 | end 29 | 30 | filepaths = ARGV 31 | 32 | # If the user didn't supply any files to parse to benchmark, then we're going to 33 | # default to parsing this file and the main syntax_tree file (a small and large 34 | # file). 35 | if filepaths.empty? 36 | filepaths = [ 37 | File.expand_path("bench", __dir__), 38 | File.expand_path("../lib/syntax_tree/node.rb", __dir__) 39 | ] 40 | end 41 | 42 | filepaths.each { |filepath| compare(filepath) } 43 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "syntax_tree" 6 | require "syntax_tree/reflection" 7 | 8 | require "irb" 9 | IRB.start(__FILE__) 10 | -------------------------------------------------------------------------------- /bin/profile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/inline" 5 | 6 | gemfile do 7 | source "https://rubygems.org" 8 | gem "stackprof" 9 | gem "prettier_print" 10 | end 11 | 12 | $:.unshift(File.expand_path("../lib", __dir__)) 13 | require "syntax_tree" 14 | 15 | StackProf.run(mode: :cpu, out: "tmp/profile.dump", raw: true) do 16 | Dir[File.join(RbConfig::CONFIG["libdir"], "**/*.rb")].each do |filepath| 17 | SyntaxTree.format(SyntaxTree.read(filepath)) 18 | end 19 | end 20 | 21 | File.open("tmp/flamegraph.html", "w") do |file| 22 | report = Marshal.load(IO.binread("tmp/profile.dump")) 23 | StackProf::Report.new(report).print_text 24 | StackProf::Report.new(report).print_d3_flamegraph(file) 25 | end 26 | 27 | `open tmp/flamegraph.html` 28 | -------------------------------------------------------------------------------- /bin/whitequark: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "parser/current" 6 | 7 | $:.unshift(File.expand_path("../lib", __dir__)) 8 | require "syntax_tree" 9 | 10 | # First, opt in to every AST feature. 11 | Parser::Builders::Default.modernize 12 | 13 | # Modify the source map == check so that it doesn't check against the node 14 | # itself so we don't get into a recursive loop. 15 | Parser::Source::Map.prepend( 16 | Module.new { 17 | def ==(other) 18 | self.class == other.class && 19 | (instance_variables - %i[@node]).map do |ivar| 20 | instance_variable_get(ivar) == other.instance_variable_get(ivar) 21 | end.reduce(:&) 22 | end 23 | } 24 | ) 25 | 26 | # Next, ensure that we're comparing the nodes and also comparing the source 27 | # ranges so that we're getting all of the necessary information. 28 | Parser::AST::Node.prepend( 29 | Module.new { 30 | def ==(other) 31 | super && (location == other.location) 32 | end 33 | } 34 | ) 35 | 36 | source = ARGF.read 37 | 38 | parser = Parser::CurrentRuby.new 39 | parser.diagnostics.all_errors_are_fatal = true 40 | 41 | buffer = Parser::Source::Buffer.new("(string)", 1) 42 | buffer.source = source.dup.force_encoding(parser.default_encoding) 43 | 44 | stree = SyntaxTree::Translation.to_parser(SyntaxTree.parse(source), buffer) 45 | ptree = parser.parse(buffer) 46 | 47 | if stree == ptree 48 | puts "Syntax trees are equivalent." 49 | elsif stree.inspect == ptree.inspect 50 | warn "Syntax tree locations are different." 51 | 52 | queue = [[stree, ptree]] 53 | while (left, right = queue.shift) 54 | if left.location != right.location 55 | warn "Different node:" 56 | pp left 57 | 58 | warn "Different location:" 59 | 60 | warn "Syntax Tree:" 61 | pp left.location 62 | 63 | warn "whitequark/parser:" 64 | pp right.location 65 | 66 | exit 67 | end 68 | 69 | left.children.zip(right.children).each do |left_child, right_child| 70 | queue << [left_child, right_child] if left_child.is_a?(Parser::AST::Node) 71 | end 72 | end 73 | else 74 | warn "Syntax Tree:" 75 | pp stree 76 | 77 | warn "whitequark/parser:" 78 | pp ptree 79 | end 80 | -------------------------------------------------------------------------------- /config/rubocop.yml: -------------------------------------------------------------------------------- 1 | # Disabling all Layout/* rules, as they're unnecessary when the user is using 2 | # Syntax Tree to handle all of the formatting. 3 | Layout: 4 | Enabled: false 5 | 6 | # Re-enable Layout/LineLength because certain cops that most projects use 7 | # (e.g. Style/IfUnlessModifier) require Layout/LineLength to be enabled. 8 | # By leaving it disabled, those rules will mis-fire. 9 | # 10 | # Users can always override these defaults in their own rubocop.yml files. 11 | # https://github.com/prettier/plugin-ruby/issues/825 12 | Layout/LineLength: 13 | Enabled: true 14 | 15 | Style/MultilineIfModifier: 16 | Enabled: false 17 | 18 | # Syntax Tree will expand empty methods to put the end keyword on the subsequent 19 | # line to reduce git diff noise. 20 | Style/EmptyMethod: 21 | EnforcedStyle: expanded 22 | 23 | # lambdas that are constructed with the lambda method call cannot be safely 24 | # turned into lambda literals without removing a method call. 25 | Style/Lambda: 26 | Enabled: false 27 | 28 | # When method chains with multiple blocks are chained together, rubocop will let 29 | # them pass if they're using braces but not if they're using do and end 30 | # keywords. Because we will break individual blocks down to using keywords if 31 | # they are multiline, this conflicts with rubocop. 32 | Style/MultilineBlockChain: 33 | Enabled: false 34 | 35 | # Syntax Tree by default uses double quotes, so changing the configuration here 36 | # to match that. 37 | Style/StringLiterals: 38 | EnforcedStyle: double_quotes 39 | 40 | Style/StringLiteralsInInterpolation: 41 | EnforcedStyle: double_quotes 42 | 43 | Style/QuotedSymbols: 44 | EnforcedStyle: double_quotes 45 | 46 | # We let users have a little more freedom with symbol and words arrays. If the 47 | # user only has an individual item like ["value"] then we don't bother 48 | # converting it because it ends up being just noise. 49 | Style/SymbolArray: 50 | Enabled: false 51 | 52 | Style/WordArray: 53 | Enabled: false 54 | 55 | # We don't support trailing commas in Syntax Tree by default, so just turning 56 | # these off for now. 57 | Style/TrailingCommaInArguments: 58 | Enabled: false 59 | 60 | Style/TrailingCommaInArrayLiteral: 61 | Enabled: false 62 | 63 | Style/TrailingCommaInHashLiteral: 64 | Enabled: false 65 | -------------------------------------------------------------------------------- /doc/changing_structure.md: -------------------------------------------------------------------------------- 1 | # Changing structure 2 | 3 | First and foremost, changing the structure of the tree in any way is a major breaking change. It forces the consumers to update their visitors, pattern matches, and method calls. It should not be taking lightly, and can only happen on a major version change. So keep that in mind. 4 | 5 | That said, if you do want to change the structure of the tree, there are a few steps that you have to take. They are enumerated below. 6 | 7 | 1. Change the structure in the required node classes. This could mean adding/removing classes or adding/removing fields. Be sure to also update the `copy` and `===` methods to be sure that they are correct. 8 | 2. Update the parser to correctly create the new structure. 9 | 3. Update any visitor methods that are affected by the change. For example, if adding a new node make sure to create the new visit method alias in the `Visitor` class. 10 | 4. Update the `FieldVisitor` class to be sure that the various serializers, pretty printers, and matchers all get updated accordingly. 11 | 5. Update the `DSL` module to be sure that folks can correctly create nodes with the new structure. 12 | 6. Ensure the formatting of the code hasn't changed. This can mostly be done by running the tests, but if there's a corner case that we don't cover that is now exposed by your change be sure to add test cases. 13 | 7. Update the translation visitors to ensure we're still translating into other ASTs correctly. 14 | 8. Update the YARV compiler visitor to ensure we're still compiling correctly. 15 | 9. Make sure we aren't referencing the previous structure in any documentation or tests. 16 | 10. Be sure to update `CHANGELOG.md` with a description of the change that you made. 17 | -------------------------------------------------------------------------------- /exe/stree: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $:.unshift(File.expand_path("../lib", __dir__)) 5 | 6 | require "syntax_tree" 7 | require "syntax_tree/cli" 8 | 9 | exit(SyntaxTree::CLI.run(ARGV)) 10 | -------------------------------------------------------------------------------- /exe/yarv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $:.unshift(File.expand_path("../lib", __dir__)) 5 | 6 | require "syntax_tree" 7 | 8 | # Require these here so that we can run binding.irb without having them require 9 | # anything that we've already patched. 10 | require "irb" 11 | require "irb/completion" 12 | require "irb/color_printer" 13 | require "readline" 14 | 15 | # First, create an instance of our virtual machine. 16 | events = 17 | if ENV["DEBUG"] 18 | SyntaxTree::YARV::VM::STDOUTEvents.new 19 | else 20 | SyntaxTree::YARV::VM::NullEvents.new 21 | end 22 | 23 | vm = SyntaxTree::YARV::VM.new(events) 24 | 25 | # Next, set up a bunch of aliases for methods that we're going to hook into in 26 | # order to set up our virtual machine. 27 | class << Kernel 28 | alias yarv_require require 29 | alias yarv_require_relative require_relative 30 | alias yarv_load load 31 | alias yarv_eval eval 32 | alias yarv_throw throw 33 | alias yarv_catch catch 34 | end 35 | 36 | # Next, patch the methods that we just aliased so that they use our virtual 37 | # machine's versions instead. This allows us to load Ruby files and have them 38 | # execute in our virtual machine instead of the runtime environment. 39 | [Kernel, Kernel.singleton_class].each do |klass| 40 | klass.define_method(:require) { |filepath| vm.require(filepath) } 41 | 42 | klass.define_method(:load) { |filepath| vm.load(filepath) } 43 | 44 | # klass.define_method(:require_relative) do |filepath| 45 | # vm.require_relative(filepath) 46 | # end 47 | 48 | # klass.define_method(:eval) do | 49 | # source, 50 | # binding = TOPLEVEL_BINDING, 51 | # filename = "(eval)", 52 | # lineno = 1 53 | # | 54 | # vm.eval(source, binding, filename, lineno) 55 | # end 56 | 57 | # klass.define_method(:throw) { |tag, value = nil| vm.throw(tag, value) } 58 | 59 | # klass.define_method(:catch) { |tag, &block| vm.catch(tag, &block) } 60 | end 61 | 62 | # Finally, require the file that we want to execute. 63 | vm.require_resolved(ARGV.shift) 64 | -------------------------------------------------------------------------------- /lib/syntax_tree.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "prettier_print" 4 | require "pp" 5 | require "ripper" 6 | 7 | require_relative "syntax_tree/node" 8 | require_relative "syntax_tree/basic_visitor" 9 | require_relative "syntax_tree/visitor" 10 | 11 | require_relative "syntax_tree/formatter" 12 | require_relative "syntax_tree/parser" 13 | require_relative "syntax_tree/version" 14 | 15 | # Syntax Tree is a suite of tools built on top of the internal CRuby parser. It 16 | # provides the ability to generate a syntax tree from source, as well as the 17 | # tools necessary to inspect and manipulate that syntax tree. It can be used to 18 | # build formatters, linters, language servers, and more. 19 | module SyntaxTree 20 | # Syntax Tree the library has many features that aren't always used by the 21 | # CLI. Requiring those features takes time, so we autoload as many constants 22 | # as possible in order to keep the CLI as fast as possible. 23 | 24 | autoload :Database, "syntax_tree/database" 25 | autoload :DSL, "syntax_tree/dsl" 26 | autoload :FieldVisitor, "syntax_tree/field_visitor" 27 | autoload :Index, "syntax_tree/index" 28 | autoload :JSONVisitor, "syntax_tree/json_visitor" 29 | autoload :LanguageServer, "syntax_tree/language_server" 30 | autoload :MatchVisitor, "syntax_tree/match_visitor" 31 | autoload :Mermaid, "syntax_tree/mermaid" 32 | autoload :MermaidVisitor, "syntax_tree/mermaid_visitor" 33 | autoload :MutationVisitor, "syntax_tree/mutation_visitor" 34 | autoload :Pattern, "syntax_tree/pattern" 35 | autoload :PrettyPrintVisitor, "syntax_tree/pretty_print_visitor" 36 | autoload :Search, "syntax_tree/search" 37 | autoload :Translation, "syntax_tree/translation" 38 | autoload :WithScope, "syntax_tree/with_scope" 39 | autoload :YARV, "syntax_tree/yarv" 40 | 41 | # This holds references to objects that respond to both #parse and #format 42 | # so that we can use them in the CLI. 43 | HANDLERS = {} 44 | HANDLERS.default = SyntaxTree 45 | 46 | # This is the default print width when formatting. It can be overridden in the 47 | # CLI by passing the --print-width option or here in the API by passing the 48 | # optional second argument to ::format. 49 | DEFAULT_PRINT_WIDTH = 80 50 | 51 | # This is the default ruby version that we're going to target for formatting. 52 | # It shouldn't really be changed except in very niche circumstances. 53 | DEFAULT_RUBY_VERSION = Formatter::SemanticVersion.new(RUBY_VERSION).freeze 54 | 55 | # The default indentation level for formatting. We allow changing this so 56 | # that Syntax Tree can format arbitrary parts of a document. 57 | DEFAULT_INDENTATION = 0 58 | 59 | # Parses the given source and returns the formatted source. 60 | def self.format( 61 | source, 62 | maxwidth = DEFAULT_PRINT_WIDTH, 63 | base_indentation = DEFAULT_INDENTATION, 64 | options: Formatter::Options.new 65 | ) 66 | format_node( 67 | source, 68 | parse(source), 69 | maxwidth, 70 | base_indentation, 71 | options: options 72 | ) 73 | end 74 | 75 | # Parses the given file and returns the formatted source. 76 | def self.format_file( 77 | filepath, 78 | maxwidth = DEFAULT_PRINT_WIDTH, 79 | base_indentation = DEFAULT_INDENTATION, 80 | options: Formatter::Options.new 81 | ) 82 | format(read(filepath), maxwidth, base_indentation, options: options) 83 | end 84 | 85 | # Accepts a node in the tree and returns the formatted source. 86 | def self.format_node( 87 | source, 88 | node, 89 | maxwidth = DEFAULT_PRINT_WIDTH, 90 | base_indentation = DEFAULT_INDENTATION, 91 | options: Formatter::Options.new 92 | ) 93 | formatter = Formatter.new(source, [], maxwidth, options: options) 94 | node.format(formatter) 95 | 96 | formatter.flush(base_indentation) 97 | formatter.output.join 98 | end 99 | 100 | # Indexes the given source code to return a list of all class, module, and 101 | # method definitions. Used to quickly provide indexing capability for IDEs or 102 | # documentation generation. 103 | def self.index(source) 104 | Index.index(source) 105 | end 106 | 107 | # Indexes the given file to return a list of all class, module, and method 108 | # definitions. Used to quickly provide indexing capability for IDEs or 109 | # documentation generation. 110 | def self.index_file(filepath) 111 | Index.index_file(filepath) 112 | end 113 | 114 | # A convenience method for creating a new mutation visitor. 115 | def self.mutation 116 | visitor = MutationVisitor.new 117 | yield visitor 118 | visitor 119 | end 120 | 121 | # Parses the given source and returns the syntax tree. 122 | def self.parse(source) 123 | parser = Parser.new(source) 124 | response = parser.parse 125 | response unless parser.error? 126 | end 127 | 128 | # Parses the given file and returns the syntax tree. 129 | def self.parse_file(filepath) 130 | parse(read(filepath)) 131 | end 132 | 133 | # Returns the source from the given filepath taking into account any potential 134 | # magic encoding comments. 135 | def self.read(filepath) 136 | encoding = 137 | File.open(filepath, "r") do |file| 138 | break Encoding.default_external if file.eof? 139 | 140 | header = file.readline 141 | header += file.readline if !file.eof? && header.start_with?("#!") 142 | Ripper.new(header).tap(&:parse).encoding 143 | end 144 | 145 | File.read(filepath, encoding: encoding) 146 | end 147 | 148 | # This is a hook provided so that plugins can register themselves as the 149 | # handler for a particular file type. 150 | def self.register_handler(extension, handler) 151 | HANDLERS[extension] = handler 152 | end 153 | 154 | # Searches through the given source using the given pattern and yields each 155 | # node in the tree that matches the pattern to the given block. 156 | def self.search(source, query, &block) 157 | pattern = Pattern.new(query).compile 158 | program = parse(source) 159 | 160 | Search.new(pattern).scan(program, &block) 161 | end 162 | 163 | # Searches through the given file using the given pattern and yields each 164 | # node in the tree that matches the pattern to the given block. 165 | def self.search_file(filepath, query, &block) 166 | search(read(filepath), query, &block) 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/syntax_tree/basic_visitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # BasicVisitor is the parent class of the Visitor class that provides the 5 | # ability to walk down the tree. It does not define any handlers, so you 6 | # should extend this class if you want your visitor to raise an error if you 7 | # attempt to visit a node that you don't handle. 8 | class BasicVisitor 9 | # This is raised when you use the Visitor.visit_method method and it fails. 10 | # It is correctable to through DidYouMean. 11 | class VisitMethodError < StandardError 12 | attr_reader :visit_method 13 | 14 | def initialize(visit_method) 15 | @visit_method = visit_method 16 | super("Invalid visit method: #{visit_method}") 17 | end 18 | end 19 | 20 | # This class is used by DidYouMean to offer corrections to invalid visit 21 | # method names. 22 | class VisitMethodChecker 23 | attr_reader :visit_method 24 | 25 | def initialize(error) 26 | @visit_method = error.visit_method 27 | end 28 | 29 | def corrections 30 | @corrections ||= 31 | DidYouMean::SpellChecker.new( 32 | dictionary: BasicVisitor.valid_visit_methods 33 | ).correct(visit_method) 34 | end 35 | 36 | # In some setups with Ruby you can turn off DidYouMean, so we're going to 37 | # respect that setting here. 38 | if defined?(DidYouMean.correct_error) 39 | DidYouMean.correct_error(VisitMethodError, self) 40 | end 41 | end 42 | 43 | # This module is responsible for checking all of the methods defined within 44 | # a given block to ensure that they are valid visit methods. 45 | class VisitMethodsChecker < Module 46 | Status = Struct.new(:checking) 47 | 48 | # This is the status of the checker. It's used to determine whether or not 49 | # we should be checking the methods that are defined. It is kept as an 50 | # instance variable so that it can be disabled later. 51 | attr_reader :status 52 | 53 | def initialize 54 | # We need the status to be an instance variable so that it can be 55 | # accessed by the disable! method, but also a local variable so that it 56 | # can be captured by the define_method block. 57 | status = @status = Status.new(true) 58 | 59 | define_method(:method_added) do |name| 60 | BasicVisitor.visit_method(name) if status.checking 61 | super(name) 62 | end 63 | end 64 | 65 | def disable! 66 | status.checking = false 67 | end 68 | end 69 | 70 | class << self 71 | # This is the list of all of the valid visit methods. 72 | def valid_visit_methods 73 | @valid_visit_methods ||= 74 | Visitor.instance_methods.grep(/^visit_(?!child_nodes)/) 75 | end 76 | 77 | # This method is here to help folks write visitors. 78 | # 79 | # It's not always easy to ensure you're writing the correct method name in 80 | # the visitor since it's perfectly valid to define methods that don't 81 | # override these parent methods. 82 | # 83 | # If you use this method, you can ensure you're writing the correct method 84 | # name. It will raise an error if the visit method you're defining isn't 85 | # actually a method on the parent visitor. 86 | def visit_method(method_name) 87 | return if valid_visit_methods.include?(method_name) 88 | 89 | raise VisitMethodError, method_name 90 | end 91 | 92 | # This method is here to help folks write visitors. 93 | # 94 | # Within the given block, every method that is defined will be checked to 95 | # ensure it's a valid visit method using the BasicVisitor::visit_method 96 | # method defined above. 97 | def visit_methods 98 | checker = VisitMethodsChecker.new 99 | extend(checker) 100 | yield 101 | checker.disable! 102 | end 103 | end 104 | 105 | def visit(node) 106 | node&.accept(self) 107 | end 108 | 109 | def visit_all(nodes) 110 | nodes.map { |node| visit(node) } 111 | end 112 | 113 | def visit_child_nodes(node) 114 | visit_all(node.child_nodes) 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/syntax_tree/formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # A slightly enhanced PP that knows how to format recursively including 5 | # comments. 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 | 17 | # We want to minimize as much as possible the number of options that are 18 | # available in syntax tree. For the most part, if users want non-default 19 | # formatting, they should override the format methods on the specific nodes 20 | # themselves. However, because of some history with prettier and the fact 21 | # that folks have become entrenched in their ways, we decided to provide a 22 | # small amount of configurability. 23 | class Options 24 | attr_reader :quote, 25 | :trailing_comma, 26 | :disable_auto_ternary, 27 | :target_ruby_version 28 | 29 | def initialize( 30 | quote: :default, 31 | trailing_comma: :default, 32 | disable_auto_ternary: :default, 33 | target_ruby_version: :default 34 | ) 35 | @quote = 36 | if quote == :default 37 | # We ship with a single quotes plugin that will define this 38 | # constant. That constant is responsible for determining the default 39 | # quote style. If it's defined, we default to single quotes, 40 | # otherwise we default to double quotes. 41 | defined?(SINGLE_QUOTES) ? "'" : "\"" 42 | else 43 | quote 44 | end 45 | 46 | @trailing_comma = 47 | if trailing_comma == :default 48 | # We ship with a trailing comma plugin that will define this 49 | # constant. That constant is responsible for determining the default 50 | # trailing comma value. If it's defined, then we default to true. 51 | # Otherwise we default to false. 52 | defined?(TRAILING_COMMA) 53 | else 54 | trailing_comma 55 | end 56 | 57 | @disable_auto_ternary = 58 | if disable_auto_ternary == :default 59 | # We ship with a disable ternary plugin that will define this 60 | # constant. That constant is responsible for determining the default 61 | # disable ternary value. If it's defined, then we default to true. 62 | # Otherwise we default to false. 63 | defined?(DISABLE_TERNARY) 64 | else 65 | disable_auto_ternary 66 | end 67 | 68 | @target_ruby_version = 69 | if target_ruby_version == :default 70 | # The default target Ruby version is the current version of Ruby. 71 | # This is really only used for very niche cases, and it shouldn't be 72 | # used by most users. 73 | SemanticVersion.new(RUBY_VERSION) 74 | else 75 | target_ruby_version 76 | end 77 | end 78 | end 79 | 80 | COMMENT_PRIORITY = 1 81 | HEREDOC_PRIORITY = 2 82 | 83 | attr_reader :source, :stack 84 | 85 | # These options are overridden in plugins to we need to make sure they are 86 | # available here. 87 | attr_reader :quote, 88 | :trailing_comma, 89 | :disable_auto_ternary, 90 | :target_ruby_version 91 | 92 | alias trailing_comma? trailing_comma 93 | alias disable_auto_ternary? disable_auto_ternary 94 | 95 | def initialize(source, *args, options: Options.new) 96 | super(*args) 97 | 98 | @source = source 99 | @stack = [] 100 | 101 | # Memoizing these values to make access faster. 102 | @quote = options.quote 103 | @trailing_comma = options.trailing_comma 104 | @disable_auto_ternary = options.disable_auto_ternary 105 | @target_ruby_version = options.target_ruby_version 106 | end 107 | 108 | def self.format(source, node, base_indentation = 0) 109 | q = new(source, []) 110 | q.format(node) 111 | q.flush(base_indentation) 112 | q.output.join 113 | end 114 | 115 | def format(node, stackable: true) 116 | stack << node if stackable 117 | doc = nil 118 | 119 | # If there are comments, then we're going to format them around the node 120 | # so that they get printed properly. 121 | if node.comments.any? 122 | trailing = [] 123 | last_leading = nil 124 | 125 | # First, we're going to print all of the comments that were found before 126 | # the node. We'll also gather up any trailing comments that we find. 127 | node.comments.each do |comment| 128 | if comment.leading? 129 | comment.format(self) 130 | breakable(force: true) 131 | last_leading = comment 132 | else 133 | trailing << comment 134 | end 135 | end 136 | 137 | # If the node has a stree-ignore comment right before it, then we're 138 | # going to just print out the node as it was seen in the source. 139 | doc = 140 | if last_leading&.ignore? 141 | range = source[node.start_char...node.end_char] 142 | first = true 143 | 144 | range.each_line(chomp: true) do |line| 145 | if first 146 | first = false 147 | else 148 | breakable_return 149 | end 150 | 151 | text(line) 152 | end 153 | 154 | breakable_return if range.end_with?("\n") 155 | else 156 | node.format(self) 157 | end 158 | 159 | # Print all comments that were found after the node. 160 | trailing.each do |comment| 161 | line_suffix(priority: COMMENT_PRIORITY) do 162 | comment.inline? ? text(" ") : breakable 163 | comment.format(self) 164 | break_parent 165 | end 166 | end 167 | else 168 | doc = node.format(self) 169 | end 170 | 171 | stack.pop if stackable 172 | doc 173 | end 174 | 175 | def format_each(nodes) 176 | nodes.each { |node| format(node) } 177 | end 178 | 179 | def grandparent 180 | stack[-3] 181 | end 182 | 183 | def parent 184 | stack[-2] 185 | end 186 | 187 | def parents 188 | stack[0...-1].reverse_each 189 | end 190 | 191 | # This is a simplified version of prettyprint's group. It doesn't provide 192 | # any of the more advanced options because we don't need them and they take 193 | # up expensive computation time. 194 | def group 195 | contents = [] 196 | doc = Group.new(0, contents: contents) 197 | 198 | groups << doc 199 | target << doc 200 | 201 | with_target(contents) { yield } 202 | groups.pop 203 | doc 204 | end 205 | 206 | # A similar version to the super, except that it calls back into the 207 | # separator proc with the instance of `self`. 208 | def seplist(list, sep = nil, iter_method = :each) 209 | first = true 210 | list.__send__(iter_method) do |*v| 211 | if first 212 | first = false 213 | elsif sep 214 | sep.call(self) 215 | else 216 | comma_breakable 217 | end 218 | yield(*v) 219 | end 220 | end 221 | 222 | # This is a much simplified version of prettyprint's text. It avoids 223 | # calculating width by pushing the string directly onto the target. 224 | def text(string) 225 | target << string 226 | end 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/syntax_tree/json_visitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "json" 4 | 5 | module SyntaxTree 6 | # This visitor transforms the AST into a hash that contains only primitives 7 | # that can be easily serialized into JSON. 8 | class JSONVisitor < FieldVisitor 9 | attr_reader :target 10 | 11 | def initialize 12 | @target = nil 13 | end 14 | 15 | private 16 | 17 | def comments(node) 18 | target[:comments] = visit_all(node.comments) 19 | end 20 | 21 | def field(name, value) 22 | target[name] = value.is_a?(Node) ? visit(value) : value 23 | end 24 | 25 | def list(name, values) 26 | target[name] = visit_all(values) 27 | end 28 | 29 | def node(node, type) 30 | previous = @target 31 | @target = { type: type, location: visit_location(node.location) } 32 | yield 33 | @target 34 | ensure 35 | @target = previous 36 | end 37 | 38 | def pairs(name, values) 39 | target[name] = values.map { |(key, value)| [visit(key), visit(value)] } 40 | end 41 | 42 | def text(name, value) 43 | target[name] = value 44 | end 45 | 46 | def visit_location(location) 47 | [ 48 | location.start_line, 49 | location.start_char, 50 | location.end_line, 51 | location.end_char 52 | ] 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/syntax_tree/match_visitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # This visitor transforms the AST into a Ruby pattern matching expression that 5 | # would match correctly against the AST. 6 | class MatchVisitor < FieldVisitor 7 | attr_reader :q 8 | 9 | def initialize(q) 10 | @q = q 11 | end 12 | 13 | def visit(node) 14 | case node 15 | when Node 16 | super 17 | when String 18 | # pp will split up a string on newlines and concat them together using a 19 | # "+" operator. This breaks the pattern matching expression. So instead 20 | # we're going to check here for strings and manually put the entire 21 | # value into the output buffer. 22 | q.text(node.inspect) 23 | else 24 | node.pretty_print(q) 25 | end 26 | end 27 | 28 | private 29 | 30 | def comments(node) 31 | return if node.comments.empty? 32 | 33 | q.nest(0) do 34 | q.text("comments: [") 35 | q.indent do 36 | q.breakable("") 37 | q.seplist(node.comments) { |comment| visit(comment) } 38 | end 39 | q.breakable("") 40 | q.text("]") 41 | end 42 | end 43 | 44 | def field(name, value) 45 | q.nest(0) do 46 | q.text(name) 47 | q.text(": ") 48 | visit(value) 49 | end 50 | end 51 | 52 | def list(name, values) 53 | q.group do 54 | q.text(name) 55 | q.text(": [") 56 | q.indent do 57 | q.breakable("") 58 | q.seplist(values) { |value| visit(value) } 59 | end 60 | q.breakable("") 61 | q.text("]") 62 | end 63 | end 64 | 65 | def node(node, _type) 66 | items = [] 67 | q.with_target(items) { yield } 68 | 69 | if items.empty? 70 | q.text(node.class.name) 71 | return 72 | end 73 | 74 | q.group do 75 | q.text(node.class.name) 76 | q.text("[") 77 | q.indent do 78 | q.breakable("") 79 | q.seplist(items) { |item| q.target << item } 80 | end 81 | q.breakable("") 82 | q.text("]") 83 | end 84 | end 85 | 86 | def pairs(name, values) 87 | q.group do 88 | q.text(name) 89 | q.text(": [") 90 | q.indent do 91 | q.breakable("") 92 | q.seplist(values) do |(key, value)| 93 | q.group do 94 | q.text("[") 95 | q.indent do 96 | q.breakable("") 97 | visit(key) 98 | q.text(",") 99 | q.breakable 100 | visit(value || nil) 101 | end 102 | q.breakable("") 103 | q.text("]") 104 | end 105 | end 106 | end 107 | q.breakable("") 108 | q.text("]") 109 | end 110 | end 111 | 112 | def text(name, value) 113 | q.nest(0) do 114 | q.text(name) 115 | q.text(": ") 116 | value.pretty_print(q) 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/syntax_tree/mermaid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cgi" 4 | require "stringio" 5 | 6 | module SyntaxTree 7 | # This module is responsible for rendering mermaid (https://mermaid.js.org/) 8 | # flow charts. 9 | module Mermaid 10 | # This is the main class that handles rendering a flowchart. It keeps track 11 | # of its nodes and links and renders them according to the mermaid syntax. 12 | class FlowChart 13 | attr_reader :output, :prefix, :nodes, :links 14 | 15 | def initialize 16 | @output = StringIO.new 17 | @output.puts("flowchart TD") 18 | @prefix = " " 19 | 20 | @nodes = {} 21 | @links = [] 22 | end 23 | 24 | # Retrieve a node that has already been added to the flowchart by its id. 25 | def fetch(id) 26 | nodes.fetch(id) 27 | end 28 | 29 | # Add a link to the flowchart between two nodes with an optional label. 30 | def link(from, to, label = nil, type: :directed, color: nil) 31 | link = Link.new(from, to, label, type, color) 32 | links << link 33 | 34 | output.puts("#{prefix}#{link.render}") 35 | link 36 | end 37 | 38 | # Add a node to the flowchart with an optional label. 39 | def node(id, label = " ", shape: :rectangle) 40 | node = Node.new(id, label, shape) 41 | nodes[id] = node 42 | 43 | output.puts("#{prefix}#{nodes[id].render}") 44 | node 45 | end 46 | 47 | # Add a subgraph to the flowchart. Within the given block, all of the 48 | # nodes will be rendered within the subgraph. 49 | def subgraph(label) 50 | output.puts("#{prefix}subgraph #{Mermaid.escape(label)}") 51 | 52 | previous = prefix 53 | @prefix = "#{prefix} " 54 | 55 | begin 56 | yield 57 | ensure 58 | @prefix = previous 59 | output.puts("#{prefix}end") 60 | end 61 | end 62 | 63 | # Return the rendered flowchart. 64 | def render 65 | links.each_with_index do |link, index| 66 | if link.color 67 | output.puts("#{prefix}linkStyle #{index} stroke:#{link.color}") 68 | end 69 | end 70 | 71 | output.string 72 | end 73 | end 74 | 75 | # This class represents a link between two nodes in a flowchart. It is not 76 | # meant to be interacted with directly, but rather used as a data structure 77 | # by the FlowChart class. 78 | class Link 79 | TYPES = %i[directed dotted].freeze 80 | COLORS = %i[green red].freeze 81 | 82 | attr_reader :from, :to, :label, :type, :color 83 | 84 | def initialize(from, to, label, type, color) 85 | raise unless TYPES.include?(type) 86 | raise if color && !COLORS.include?(color) 87 | 88 | @from = from 89 | @to = to 90 | @label = label 91 | @type = type 92 | @color = color 93 | end 94 | 95 | def render 96 | left_side, right_side, full_side = sides 97 | 98 | if label 99 | escaped = Mermaid.escape(label) 100 | "#{from.id} #{left_side} #{escaped} #{right_side} #{to.id}" 101 | else 102 | "#{from.id} #{full_side} #{to.id}" 103 | end 104 | end 105 | 106 | private 107 | 108 | def sides 109 | case type 110 | when :directed 111 | %w[-- --> -->] 112 | when :dotted 113 | %w[-. .-> -.->] 114 | end 115 | end 116 | end 117 | 118 | # This class represents a node in a flowchart. Unlike the Link class, it can 119 | # be used directly. It is the return value of the #node method, and is meant 120 | # to be passed around to #link methods to create links between nodes. 121 | class Node 122 | SHAPES = %i[circle rectangle rounded stadium].freeze 123 | 124 | attr_reader :id, :label, :shape 125 | 126 | def initialize(id, label, shape) 127 | raise unless SHAPES.include?(shape) 128 | 129 | @id = id 130 | @label = label 131 | @shape = shape 132 | end 133 | 134 | def render 135 | left_bound, right_bound = bounds 136 | "#{id}#{left_bound}#{Mermaid.escape(label)}#{right_bound}" 137 | end 138 | 139 | private 140 | 141 | def bounds 142 | case shape 143 | when :circle 144 | %w[(( ))] 145 | when :rectangle 146 | ["[", "]"] 147 | when :rounded 148 | %w[( )] 149 | when :stadium 150 | ["([", "])"] 151 | end 152 | end 153 | end 154 | 155 | class << self 156 | # Escape a label to be used in the mermaid syntax. This is used to escape 157 | # HTML entities such that they render properly within the quotes. 158 | def escape(label) 159 | "\"#{CGI.escapeHTML(label)}\"" 160 | end 161 | 162 | # Create a new flowchart. If a block is given, it will be yielded to and 163 | # the flowchart will be rendered. Otherwise, the flowchart will be 164 | # returned. 165 | def flowchart 166 | flowchart = FlowChart.new 167 | 168 | if block_given? 169 | yield flowchart 170 | flowchart.render 171 | else 172 | flowchart 173 | end 174 | end 175 | end 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /lib/syntax_tree/mermaid_visitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # This visitor transforms the AST into a mermaid flow chart. 5 | class MermaidVisitor < FieldVisitor 6 | attr_reader :flowchart, :target 7 | 8 | def initialize 9 | @flowchart = Mermaid.flowchart 10 | @target = nil 11 | end 12 | 13 | def visit_program(node) 14 | super 15 | flowchart.render 16 | end 17 | 18 | private 19 | 20 | def comments(node) 21 | # Ignore 22 | end 23 | 24 | def field(name, value) 25 | case value 26 | when nil 27 | # skip 28 | when Node 29 | flowchart.link(target, visit(value), name) 30 | else 31 | to = 32 | flowchart.node("#{target.id}_#{name}", value.inspect, shape: :stadium) 33 | flowchart.link(target, to, name) 34 | end 35 | end 36 | 37 | def list(name, values) 38 | values.each_with_index do |value, index| 39 | field("#{name}[#{index}]", value) 40 | end 41 | end 42 | 43 | def node(node, type) 44 | previous_target = target 45 | 46 | begin 47 | @target = flowchart.node("node_#{node.object_id}", type) 48 | yield 49 | @target 50 | ensure 51 | @target = previous_target 52 | end 53 | end 54 | 55 | def pairs(name, values) 56 | values.each_with_index do |(key, value), index| 57 | to = flowchart.node("#{target.id}_#{name}_#{index}", shape: :circle) 58 | 59 | flowchart.link(target, to, "#{name}[#{index}]") 60 | flowchart.link(to, visit(key), "[0]") 61 | flowchart.link(to, visit(value), "[1]") if value 62 | end 63 | end 64 | 65 | def text(name, value) 66 | field(name, value) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/syntax_tree/plugin/disable_ternary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | class Formatter 5 | DISABLE_TERNARY = true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/syntax_tree/plugin/single_quotes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | class Formatter 5 | SINGLE_QUOTES = true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/syntax_tree/plugin/trailing_comma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | class Formatter 5 | TRAILING_COMMA = true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/syntax_tree/pretty_print_visitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # This visitor pretty-prints the AST into an equivalent s-expression. 5 | class PrettyPrintVisitor < FieldVisitor 6 | attr_reader :q 7 | 8 | def initialize(q) 9 | @q = q 10 | end 11 | 12 | # This is here because we need to make sure the operator is cast to a string 13 | # before we print it out. 14 | def visit_binary(node) 15 | node(node, "binary") do 16 | field("left", node.left) 17 | text("operator", node.operator.to_s) 18 | field("right", node.right) 19 | comments(node) 20 | end 21 | end 22 | 23 | # This is here to make it a little nicer to look at labels since they 24 | # typically have their : at the end of the value. 25 | def visit_label(node) 26 | node(node, "label") do 27 | q.breakable 28 | q.text(":") 29 | q.text(node.value[0...-1]) 30 | comments(node) 31 | end 32 | end 33 | 34 | private 35 | 36 | def comments(node) 37 | return if node.comments.empty? 38 | 39 | q.breakable 40 | q.group(2, "(", ")") do 41 | q.seplist(node.comments) { |comment| q.pp(comment) } 42 | end 43 | end 44 | 45 | def field(_name, value) 46 | q.breakable 47 | q.pp(value) 48 | end 49 | 50 | def list(_name, values) 51 | q.breakable 52 | q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } } 53 | end 54 | 55 | def node(_node, type) 56 | q.group(2, "(", ")") do 57 | q.text(type) 58 | yield 59 | end 60 | end 61 | 62 | def pairs(_name, values) 63 | q.group(2, "(", ")") do 64 | q.seplist(values) do |(key, value)| 65 | q.pp(key) 66 | 67 | if value 68 | q.text("=") 69 | q.group(2) do 70 | q.breakable("") 71 | q.pp(value) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | 78 | def text(_name, value) 79 | q.breakable 80 | q.text(value) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/syntax_tree/rake/check_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "task" 4 | 5 | module SyntaxTree 6 | module Rake 7 | # A Rake task that runs check on a set of source files. 8 | # 9 | # Example: 10 | # 11 | # require "syntax_tree/rake/check_task" 12 | # 13 | # SyntaxTree::Rake::CheckTask.new do |t| 14 | # t.source_files = "{app,config,lib}/**/*.rb" 15 | # end 16 | # 17 | # This will create task that can be run with: 18 | # 19 | # rake stree:check 20 | # 21 | class CheckTask < Task 22 | private 23 | 24 | def command 25 | "check" 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/syntax_tree/rake/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rake" 4 | require "rake/tasklib" 5 | 6 | require "syntax_tree" 7 | require "syntax_tree/cli" 8 | 9 | module SyntaxTree 10 | module Rake 11 | # A parent Rake task that runs a command on a set of source files. 12 | class Task < ::Rake::TaskLib 13 | # Name of the task. 14 | attr_accessor :name 15 | 16 | # Glob pattern to match source files. 17 | # Defaults to 'lib/**/*.rb'. 18 | attr_accessor :source_files 19 | 20 | # The set of plugins to require. 21 | # Defaults to []. 22 | attr_accessor :plugins 23 | 24 | # Max line length. 25 | # Defaults to 80. 26 | attr_accessor :print_width 27 | 28 | # The target Ruby version to use for formatting. 29 | # Defaults to Gem::Version.new(RUBY_VERSION). 30 | attr_accessor :target_ruby_version 31 | 32 | # Glob pattern to ignore source files. 33 | # Defaults to ''. 34 | attr_accessor :ignore_files 35 | 36 | def initialize( 37 | name = :"stree:#{command}", 38 | source_files = ::Rake::FileList["lib/**/*.rb"], 39 | plugins = [], 40 | print_width = DEFAULT_PRINT_WIDTH, 41 | target_ruby_version = Gem::Version.new(RUBY_VERSION), 42 | ignore_files = "" 43 | ) 44 | @name = name 45 | @source_files = source_files 46 | @plugins = plugins 47 | @print_width = print_width 48 | @target_ruby_version = target_ruby_version 49 | @ignore_files = ignore_files 50 | 51 | yield self if block_given? 52 | define_task 53 | end 54 | 55 | private 56 | 57 | # This method needs to be overridden in the child tasks. 58 | def command 59 | raise NotImplementedError 60 | end 61 | 62 | def define_task 63 | desc "Runs `stree #{command}` over source files" 64 | task(name) { run_task } 65 | end 66 | 67 | def run_task 68 | arguments = [command] 69 | arguments << "--plugins=#{plugins.join(",")}" if plugins.any? 70 | 71 | if print_width != DEFAULT_PRINT_WIDTH 72 | arguments << "--print-width=#{print_width}" 73 | end 74 | 75 | if target_ruby_version != Gem::Version.new(RUBY_VERSION) 76 | arguments << "--target-ruby-version=#{target_ruby_version}" 77 | end 78 | 79 | arguments << "--ignore-files=#{ignore_files}" if ignore_files != "" 80 | 81 | abort if SyntaxTree::CLI.run(arguments + Array(source_files)) != 0 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/syntax_tree/rake/write_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "task" 4 | 5 | module SyntaxTree 6 | module Rake 7 | # A Rake task that runs write on a set of source files. 8 | # 9 | # Example: 10 | # 11 | # require "syntax_tree/rake/write_task" 12 | # 13 | # SyntaxTree::Rake::WriteTask.new do |t| 14 | # t.source_files = "{app,config,lib}/**/*.rb" 15 | # end 16 | # 17 | # This will create task that can be run with: 18 | # 19 | # rake stree:write 20 | # 21 | class WriteTask < Task 22 | private 23 | 24 | def command 25 | "write" 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/syntax_tree/rake_tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "rake/check_task" 4 | require_relative "rake/write_task" 5 | -------------------------------------------------------------------------------- /lib/syntax_tree/reflection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # This module is used to provide some reflection on the various types of nodes 5 | # and their attributes. As soon as it is required it collects all of its 6 | # information. 7 | module Reflection 8 | # This module represents the type of the values being passed to attributes 9 | # of nodes. It is used as part of the documentation of the attributes. 10 | module Type 11 | CONSTANTS = SyntaxTree.constants.to_h { [_1, SyntaxTree.const_get(_1)] } 12 | 13 | # Represents an array type that holds another type. 14 | class ArrayType 15 | attr_reader :type 16 | 17 | def initialize(type) 18 | @type = type 19 | end 20 | 21 | def ===(value) 22 | value.is_a?(Array) && value.all? { type === _1 } 23 | end 24 | 25 | def inspect 26 | "Array<#{type.inspect}>" 27 | end 28 | end 29 | 30 | # Represents a tuple type that holds a number of types in order. 31 | class TupleType 32 | attr_reader :types 33 | 34 | def initialize(types) 35 | @types = types 36 | end 37 | 38 | def ===(value) 39 | value.is_a?(Array) && value.length == types.length && 40 | value.zip(types).all? { |item, type| type === item } 41 | end 42 | 43 | def inspect 44 | "[#{types.map(&:inspect).join(", ")}]" 45 | end 46 | end 47 | 48 | # Represents a union type that can be one of a number of types. 49 | class UnionType 50 | attr_reader :types 51 | 52 | def initialize(types) 53 | @types = types 54 | end 55 | 56 | def ===(value) 57 | types.any? { _1 === value } 58 | end 59 | 60 | def inspect 61 | types.map(&:inspect).join(" | ") 62 | end 63 | end 64 | 65 | class << self 66 | def parse(comment) 67 | comment = comment.gsub(/\n/, " ") 68 | 69 | unless comment.start_with?("[") 70 | raise "Comment does not start with a bracket: #{comment.inspect}" 71 | end 72 | 73 | count = 1 74 | found = 75 | comment.chars[1..] 76 | .find 77 | .with_index(1) do |char, index| 78 | count += { "[" => 1, "]" => -1 }.fetch(char, 0) 79 | break index if count == 0 80 | end 81 | 82 | # If we weren't able to find the end of the balanced brackets, then 83 | # the comment is malformed. 84 | if found.nil? 85 | raise "Comment does not have balanced brackets: #{comment.inspect}" 86 | end 87 | 88 | parse_type(comment[1...found].strip) 89 | end 90 | 91 | private 92 | 93 | def parse_type(value) 94 | case value 95 | when "Integer" 96 | Integer 97 | when "String" 98 | String 99 | when "Symbol" 100 | Symbol 101 | when "boolean" 102 | UnionType.new([TrueClass, FalseClass]) 103 | when "nil" 104 | NilClass 105 | when ":\"::\"" 106 | :"::" 107 | when ":call" 108 | :call 109 | when ":nil" 110 | :nil 111 | when /\AArray\[(.+)\]\z/ 112 | ArrayType.new(parse_type($1.strip)) 113 | when /\A\[(.+)\]\z/ 114 | TupleType.new($1.strip.split(/\s*,\s*/).map { parse_type(_1) }) 115 | else 116 | if value.include?("|") 117 | UnionType.new(value.split(/\s*\|\s*/).map { parse_type(_1) }) 118 | else 119 | CONSTANTS.fetch(value.to_sym) 120 | end 121 | end 122 | end 123 | end 124 | end 125 | 126 | # This class represents one of the attributes on a node in the tree. 127 | class Attribute 128 | attr_reader :name, :comment, :type 129 | 130 | def initialize(name, comment) 131 | @name = name 132 | @comment = comment 133 | @type = Type.parse(comment) 134 | end 135 | end 136 | 137 | # This class represents one of our nodes in the tree. We're going to use it 138 | # as a placeholder for collecting all of the various places that nodes are 139 | # used. 140 | class Node 141 | attr_reader :name, :comment, :attributes, :visitor_method 142 | 143 | def initialize(name, comment, attributes, visitor_method) 144 | @name = name 145 | @comment = comment 146 | @attributes = attributes 147 | @visitor_method = visitor_method 148 | end 149 | end 150 | 151 | class << self 152 | # This is going to hold a hash of all of the nodes in the tree. The keys 153 | # are the names of the nodes as symbols. 154 | attr_reader :nodes 155 | 156 | # This expects a node name as a symbol and returns the node object for 157 | # that node. 158 | def node(name) 159 | nodes.fetch(name) 160 | end 161 | 162 | private 163 | 164 | def parse_comments(statements, index) 165 | statements[0...index] 166 | .reverse_each 167 | .take_while { _1.is_a?(SyntaxTree::Comment) } 168 | .reverse_each 169 | .map { _1.value[2..] } 170 | end 171 | end 172 | 173 | @nodes = {} 174 | 175 | # For each node, we're going to parse out its attributes and other metadata. 176 | # We'll use this as the basis for our report. 177 | program = 178 | SyntaxTree.parse(SyntaxTree.read(File.expand_path("node.rb", __dir__))) 179 | 180 | program_statements = program.statements 181 | main_statements = program_statements.body.last.bodystmt.statements.body 182 | main_statements.each_with_index do |main_statement, main_statement_index| 183 | # Ensure we are only looking at class declarations. 184 | next unless main_statement.is_a?(SyntaxTree::ClassDeclaration) 185 | 186 | # Ensure we're looking at class declarations with superclasses. 187 | superclass = main_statement.superclass 188 | next unless superclass.is_a?(SyntaxTree::VarRef) 189 | 190 | # Ensure we're looking at class declarations that inherit from Node. 191 | next unless superclass.value.value == "Node" 192 | 193 | # All child nodes inherit the location attr_reader from Node, so we'll add 194 | # that to the list of attributes first. 195 | attributes = { 196 | location: 197 | Attribute.new(:location, "[Location] the location of this node") 198 | } 199 | 200 | # This is the name of the method tha gets called on the given visitor when 201 | # the accept method is called on this node. 202 | visitor_method = nil 203 | 204 | statements = main_statement.bodystmt.statements.body 205 | statements.each_with_index do |statement, statement_index| 206 | case statement 207 | when SyntaxTree::Command 208 | # We only use commands in node classes to define attributes. So, we 209 | # can safely assume that we're looking at an attribute definition. 210 | unless %w[attr_reader attr_accessor].include?(statement.message.value) 211 | raise "Unexpected command: #{statement.message.value.inspect}" 212 | end 213 | 214 | # The arguments to the command are the attributes that we're defining. 215 | # We want to ensure that we're only defining one at a time. 216 | if statement.arguments.parts.length != 1 217 | raise "Declaring more than one attribute at a time is not permitted" 218 | end 219 | 220 | attribute = 221 | Attribute.new( 222 | statement.arguments.parts.first.value.value.to_sym, 223 | "#{parse_comments(statements, statement_index).join("\n")}\n" 224 | ) 225 | 226 | # Ensure that we don't already have an attribute named the same as 227 | # this one, and then add it to the list of attributes. 228 | if attributes.key?(attribute.name) 229 | raise "Duplicate attribute: #{attribute.name}" 230 | end 231 | 232 | attributes[attribute.name] = attribute 233 | when SyntaxTree::DefNode 234 | if statement.name.value == "accept" 235 | call_node = statement.bodystmt.statements.body.first 236 | visitor_method = call_node.message.value.to_sym 237 | end 238 | end 239 | end 240 | 241 | # If we never found a visitor method, then we have an error. 242 | raise if visitor_method.nil? 243 | 244 | # Finally, set it up in the hash of nodes so that we can use it later. 245 | comments = parse_comments(main_statements, main_statement_index) 246 | node = 247 | Node.new( 248 | main_statement.constant.constant.value.to_sym, 249 | "#{comments.join("\n")}\n", 250 | attributes, 251 | visitor_method 252 | ) 253 | 254 | @nodes[node.name] = node 255 | end 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /lib/syntax_tree/search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # Provides an interface for searching for a pattern of nodes against a 5 | # subtree of an AST. 6 | class Search 7 | attr_reader :pattern 8 | 9 | def initialize(pattern) 10 | @pattern = pattern 11 | end 12 | 13 | def scan(root) 14 | return to_enum(__method__, root) unless block_given? 15 | queue = [root] 16 | 17 | until queue.empty? 18 | node = queue.shift 19 | next unless node 20 | 21 | yield node if pattern.call(node) 22 | queue += node.child_nodes 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/syntax_tree/translation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | # This module is responsible for translating the Syntax Tree syntax tree into 5 | # other representations. 6 | module Translation 7 | # This method translates the given node into the representation defined by 8 | # the whitequark/parser gem. We don't explicitly list it as a dependency 9 | # because it's not required for the core functionality of Syntax Tree. 10 | def self.to_parser(node, buffer) 11 | require "parser" 12 | require_relative "translation/parser" 13 | 14 | node.accept(Parser.new(buffer)) 15 | end 16 | 17 | # This method translates the given node into the representation defined by 18 | # the rubocop/rubocop-ast gem. We don't explicitly list it as a dependency 19 | # because it's not required for the core functionality of Syntax Tree. 20 | def self.to_rubocop_ast(node, buffer) 21 | require "rubocop/ast" 22 | require_relative "translation/parser" 23 | require_relative "translation/rubocop_ast" 24 | 25 | node.accept(RuboCopAST.new(buffer)) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/syntax_tree/translation/rubocop_ast.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | module Translation 5 | # This visitor is responsible for converting the syntax tree produced by 6 | # Syntax Tree into the syntax tree produced by the rubocop/rubocop-ast gem. 7 | class RuboCopAST < Parser 8 | private 9 | 10 | # This method is effectively the same thing as the parser gem except that 11 | # it uses the rubocop-ast specializations of the nodes. 12 | def s(type, children, location) 13 | ::RuboCop::AST::Builder::NODE_MAP.fetch(type, ::RuboCop::AST::Node).new( 14 | type, 15 | children, 16 | location: location 17 | ) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/syntax_tree/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | VERSION = "6.1.1" 5 | end 6 | -------------------------------------------------------------------------------- /lib/syntax_tree/yarv.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "stringio" 4 | 5 | require_relative "yarv/basic_block" 6 | require_relative "yarv/bf" 7 | require_relative "yarv/calldata" 8 | require_relative "yarv/compiler" 9 | require_relative "yarv/control_flow_graph" 10 | require_relative "yarv/data_flow_graph" 11 | require_relative "yarv/decompiler" 12 | require_relative "yarv/disassembler" 13 | require_relative "yarv/instruction_sequence" 14 | require_relative "yarv/instructions" 15 | require_relative "yarv/legacy" 16 | require_relative "yarv/local_table" 17 | require_relative "yarv/sea_of_nodes" 18 | require_relative "yarv/assembler" 19 | require_relative "yarv/vm" 20 | 21 | module SyntaxTree 22 | # This module provides an object representation of the YARV bytecode. 23 | module YARV 24 | # Compile the given source into a YARV instruction sequence. 25 | def self.compile(source, options = Compiler::Options.new) 26 | SyntaxTree.parse(source).accept(Compiler.new(options)) 27 | end 28 | 29 | # Compile and interpret the given source. 30 | def self.interpret(source, options = Compiler::Options.new) 31 | iseq = RubyVM::InstructionSequence.compile(source, **options) 32 | iseq = InstructionSequence.from(iseq.to_a) 33 | VM.new.run_top_frame(iseq) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/syntax_tree/yarv/basic_block.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | module YARV 5 | # This object represents a single basic block, wherein all contained 6 | # instructions do not branch except for the last one. 7 | class BasicBlock 8 | # This is the unique identifier for this basic block. 9 | attr_reader :id 10 | 11 | # This is the index into the list of instructions where this block starts. 12 | attr_reader :block_start 13 | 14 | # This is the set of instructions that this block contains. 15 | attr_reader :insns 16 | 17 | # This is an array of basic blocks that lead into this block. 18 | attr_reader :incoming_blocks 19 | 20 | # This is an array of basic blocks that this block leads into. 21 | attr_reader :outgoing_blocks 22 | 23 | def initialize(block_start, insns) 24 | @id = "block_#{block_start}" 25 | 26 | @block_start = block_start 27 | @insns = insns 28 | 29 | @incoming_blocks = [] 30 | @outgoing_blocks = [] 31 | end 32 | 33 | # Yield each instruction in this basic block along with its index from the 34 | # original instruction sequence. 35 | def each_with_length 36 | return enum_for(:each_with_length) unless block_given? 37 | 38 | length = block_start 39 | insns.each do |insn| 40 | yield insn, length 41 | length += insn.length 42 | end 43 | end 44 | 45 | # This method is used to verify that the basic block is well formed. It 46 | # checks that the only instruction in this basic block that branches is 47 | # the last instruction. 48 | def verify 49 | insns[0...-1].each { |insn| raise unless insn.branch_targets.empty? } 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/syntax_tree/yarv/bf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | module YARV 5 | # Parses the given source code into a syntax tree, compiles that syntax tree 6 | # into YARV bytecode. 7 | class Bf 8 | attr_reader :source 9 | 10 | def initialize(source) 11 | @source = source 12 | end 13 | 14 | def compile 15 | # Set up the top-level instruction sequence that will be returned. 16 | iseq = InstructionSequence.new("", "", 1, :top) 17 | 18 | # Set up the $tape global variable that will hold our state. 19 | iseq.duphash({ 0 => 0 }) 20 | iseq.setglobal(:$tape) 21 | iseq.getglobal(:$tape) 22 | iseq.putobject(0) 23 | iseq.send(YARV.calldata(:default=, 1)) 24 | 25 | # Set up the $cursor global variable that will hold the current position 26 | # in the tape. 27 | iseq.putobject(0) 28 | iseq.setglobal(:$cursor) 29 | 30 | stack = [] 31 | source 32 | .each_char 33 | .chunk do |char| 34 | # For each character, we're going to assign a type to it. This 35 | # allows a couple of optimizations to be made by combining multiple 36 | # instructions into single instructions, e.g., +++ becomes a single 37 | # change_by(3) instruction. 38 | case char 39 | when "+", "-" 40 | :change 41 | when ">", "<" 42 | :shift 43 | when "." 44 | :output 45 | when "," 46 | :input 47 | when "[", "]" 48 | :loop 49 | else 50 | :ignored 51 | end 52 | end 53 | .each do |type, chunk| 54 | # For each chunk, we're going to emit the appropriate instruction. 55 | case type 56 | when :change 57 | change_by(iseq, chunk.count("+") - chunk.count("-")) 58 | when :shift 59 | shift_by(iseq, chunk.count(">") - chunk.count("<")) 60 | when :output 61 | chunk.length.times { output_char(iseq) } 62 | when :input 63 | chunk.length.times { input_char(iseq) } 64 | when :loop 65 | chunk.each do |char| 66 | case char 67 | when "[" 68 | stack << loop_start(iseq) 69 | when "]" 70 | loop_end(iseq, *stack.pop) 71 | end 72 | end 73 | end 74 | end 75 | 76 | iseq.leave 77 | iseq.compile! 78 | iseq 79 | end 80 | 81 | private 82 | 83 | # $tape[$cursor] += value 84 | def change_by(iseq, value) 85 | iseq.getglobal(:$tape) 86 | iseq.getglobal(:$cursor) 87 | 88 | iseq.getglobal(:$tape) 89 | iseq.getglobal(:$cursor) 90 | iseq.send(YARV.calldata(:[], 1)) 91 | 92 | if value < 0 93 | iseq.putobject(-value) 94 | iseq.send(YARV.calldata(:-, 1)) 95 | else 96 | iseq.putobject(value) 97 | iseq.send(YARV.calldata(:+, 1)) 98 | end 99 | 100 | iseq.send(YARV.calldata(:[]=, 2)) 101 | iseq.pop 102 | end 103 | 104 | # $cursor += value 105 | def shift_by(iseq, value) 106 | iseq.getglobal(:$cursor) 107 | 108 | if value < 0 109 | iseq.putobject(-value) 110 | iseq.send(YARV.calldata(:-, 1)) 111 | else 112 | iseq.putobject(value) 113 | iseq.send(YARV.calldata(:+, 1)) 114 | end 115 | 116 | iseq.setglobal(:$cursor) 117 | end 118 | 119 | # $stdout.putc($tape[$cursor].chr) 120 | def output_char(iseq) 121 | iseq.getglobal(:$stdout) 122 | 123 | iseq.getglobal(:$tape) 124 | iseq.getglobal(:$cursor) 125 | iseq.send(YARV.calldata(:[], 1)) 126 | iseq.send(YARV.calldata(:chr)) 127 | 128 | iseq.send(YARV.calldata(:putc, 1)) 129 | iseq.pop 130 | end 131 | 132 | # $tape[$cursor] = $stdin.getc.ord 133 | def input_char(iseq) 134 | iseq.getglobal(:$tape) 135 | iseq.getglobal(:$cursor) 136 | 137 | iseq.getglobal(:$stdin) 138 | iseq.send(YARV.calldata(:getc)) 139 | iseq.send(YARV.calldata(:ord)) 140 | 141 | iseq.send(YARV.calldata(:[]=, 2)) 142 | iseq.pop 143 | end 144 | 145 | # unless $tape[$cursor] == 0 146 | def loop_start(iseq) 147 | start_label = iseq.label 148 | end_label = iseq.label 149 | 150 | iseq.push(start_label) 151 | iseq.getglobal(:$tape) 152 | iseq.getglobal(:$cursor) 153 | iseq.send(YARV.calldata(:[], 1)) 154 | 155 | iseq.putobject(0) 156 | iseq.send(YARV.calldata(:==, 1)) 157 | iseq.branchif(end_label) 158 | 159 | [start_label, end_label] 160 | end 161 | 162 | # Jump back to the start of the loop. 163 | def loop_end(iseq, start_label, end_label) 164 | iseq.getglobal(:$tape) 165 | iseq.getglobal(:$cursor) 166 | iseq.send(YARV.calldata(:[], 1)) 167 | 168 | iseq.putobject(0) 169 | iseq.send(YARV.calldata(:==, 1)) 170 | iseq.branchunless(start_label) 171 | 172 | iseq.push(end_label) 173 | end 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /lib/syntax_tree/yarv/calldata.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | module YARV 5 | # This is an operand to various YARV instructions that represents the 6 | # information about a specific call site. 7 | class CallData 8 | flags = %i[ 9 | CALL_ARGS_SPLAT 10 | CALL_ARGS_BLOCKARG 11 | CALL_FCALL 12 | CALL_VCALL 13 | CALL_ARGS_SIMPLE 14 | CALL_KWARG 15 | CALL_KW_SPLAT 16 | CALL_TAILCALL 17 | CALL_SUPER 18 | CALL_ZSUPER 19 | CALL_OPT_SEND 20 | CALL_KW_SPLAT_MUT 21 | ] 22 | 23 | # Insert the legacy CALL_BLOCKISEQ flag for Ruby 3.2 and earlier. 24 | flags.insert(5, :CALL_BLOCKISEQ) if RUBY_VERSION < "3.3" 25 | 26 | # Set the flags as constants on the class. 27 | flags.each_with_index { |name, index| const_set(name, 1 << index) } 28 | 29 | attr_reader :method, :argc, :flags, :kw_arg 30 | 31 | def initialize( 32 | method, 33 | argc = 0, 34 | flags = CallData::CALL_ARGS_SIMPLE, 35 | kw_arg = nil 36 | ) 37 | @method = method 38 | @argc = argc 39 | @flags = flags 40 | @kw_arg = kw_arg 41 | end 42 | 43 | def flag?(mask) 44 | (flags & mask) > 0 45 | end 46 | 47 | def to_h 48 | result = { mid: method, flag: flags, orig_argc: argc } 49 | result[:kw_arg] = kw_arg if kw_arg 50 | result 51 | end 52 | 53 | def inspect 54 | names = [] 55 | names << :ARGS_SPLAT if flag?(CALL_ARGS_SPLAT) 56 | names << :ARGS_BLOCKARG if flag?(CALL_ARGS_BLOCKARG) 57 | names << :FCALL if flag?(CALL_FCALL) 58 | names << :VCALL if flag?(CALL_VCALL) 59 | names << :ARGS_SIMPLE if flag?(CALL_ARGS_SIMPLE) 60 | names << :KWARG if flag?(CALL_KWARG) 61 | names << :KW_SPLAT if flag?(CALL_KW_SPLAT) 62 | names << :TAILCALL if flag?(CALL_TAILCALL) 63 | names << :SUPER if flag?(CALL_SUPER) 64 | names << :ZSUPER if flag?(CALL_ZSUPER) 65 | names << :OPT_SEND if flag?(CALL_OPT_SEND) 66 | names << :KW_SPLAT_MUT if flag?(CALL_KW_SPLAT_MUT) 67 | 68 | parts = [] 69 | parts << "mid:#{method}" if method 70 | parts << "argc:#{argc}" 71 | parts << "kw:[#{kw_arg.join(", ")}]" if kw_arg 72 | parts << names.join("|") if names.any? 73 | 74 | "" 75 | end 76 | 77 | def self.from(serialized) 78 | new( 79 | serialized[:mid], 80 | serialized[:orig_argc], 81 | serialized[:flag], 82 | serialized[:kw_arg] 83 | ) 84 | end 85 | end 86 | 87 | # A convenience method for creating a CallData object. 88 | def self.calldata( 89 | method, 90 | argc = 0, 91 | flags = CallData::CALL_ARGS_SIMPLE, 92 | kw_arg = nil 93 | ) 94 | CallData.new(method, argc, flags, kw_arg) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/syntax_tree/yarv/disassembler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | module YARV 5 | class Disassembler 6 | # This class is another object that handles disassembling a YARV 7 | # instruction sequence but it renders it without any of the extra spacing 8 | # or alignment. 9 | class Squished 10 | def calldata(value) 11 | value.inspect 12 | end 13 | 14 | def enqueue(iseq) 15 | end 16 | 17 | def event(name) 18 | end 19 | 20 | def inline_storage(cache) 21 | "" 22 | end 23 | 24 | def instruction(name, operands = []) 25 | operands.empty? ? name : "#{name} #{operands.join(", ")}" 26 | end 27 | 28 | def label(value) 29 | "%04d" % value.name["label_".length..] 30 | end 31 | 32 | def local(index, **) 33 | index.inspect 34 | end 35 | 36 | def object(value) 37 | value.inspect 38 | end 39 | end 40 | 41 | attr_reader :output, :queue 42 | 43 | attr_reader :current_prefix 44 | attr_accessor :current_iseq 45 | 46 | def initialize(current_iseq = nil) 47 | @output = StringIO.new 48 | @queue = [] 49 | 50 | @current_prefix = "" 51 | @current_iseq = current_iseq 52 | end 53 | 54 | ######################################################################## 55 | # Helpers for various instructions 56 | ######################################################################## 57 | 58 | def calldata(value) 59 | value.inspect 60 | end 61 | 62 | def enqueue(iseq) 63 | queue << iseq 64 | end 65 | 66 | def event(name) 67 | case name 68 | when :RUBY_EVENT_B_CALL 69 | "Bc" 70 | when :RUBY_EVENT_B_RETURN 71 | "Br" 72 | when :RUBY_EVENT_CALL 73 | "Ca" 74 | when :RUBY_EVENT_CLASS 75 | "Cl" 76 | when :RUBY_EVENT_END 77 | "En" 78 | when :RUBY_EVENT_LINE 79 | "Li" 80 | when :RUBY_EVENT_RETURN 81 | "Re" 82 | else 83 | raise "Unknown event: #{name}" 84 | end 85 | end 86 | 87 | def inline_storage(cache) 88 | "" 89 | end 90 | 91 | def instruction(name, operands = []) 92 | operands.empty? ? name : "%-38s %s" % [name, operands.join(", ")] 93 | end 94 | 95 | def label(value) 96 | value.name["label_".length..] 97 | end 98 | 99 | def local(index, explicit: nil, implicit: nil) 100 | current = current_iseq 101 | (explicit || implicit).times { current = current.parent_iseq } 102 | 103 | value = "#{current.local_table.name_at(index)}@#{index}" 104 | value << ", #{explicit}" if explicit 105 | value 106 | end 107 | 108 | def object(value) 109 | value.inspect 110 | end 111 | 112 | ######################################################################## 113 | # Entrypoints 114 | ######################################################################## 115 | 116 | def format! 117 | while (@current_iseq = queue.shift) 118 | output << "\n" if output.pos > 0 119 | format_iseq(@current_iseq) 120 | end 121 | end 122 | 123 | def format_insns!(insns, length = 0) 124 | events = [] 125 | lines = [] 126 | 127 | insns.each do |insn| 128 | case insn 129 | when Integer 130 | lines << insn 131 | when Symbol 132 | events << event(insn) 133 | when InstructionSequence::Label 134 | # skip 135 | else 136 | output << "#{current_prefix}%04d " % length 137 | 138 | disasm = insn.disasm(self) 139 | output << disasm 140 | 141 | if lines.any? 142 | output << " " * (65 - disasm.length) if disasm.length < 65 143 | elsif events.any? 144 | output << " " * (39 - disasm.length) if disasm.length < 39 145 | end 146 | 147 | if lines.any? 148 | output << "(%4d)" % lines.last 149 | lines.clear 150 | end 151 | 152 | if events.any? 153 | output << "[#{events.join}]" 154 | events.clear 155 | end 156 | 157 | # A hook here to allow for custom formatting of instructions after 158 | # the main body has been processed. 159 | yield insn, length if block_given? 160 | 161 | output << "\n" 162 | length += insn.length 163 | end 164 | end 165 | end 166 | 167 | def print(string) 168 | output.print(string) 169 | end 170 | 171 | def puts(string) 172 | output.puts(string) 173 | end 174 | 175 | def string 176 | output.string 177 | end 178 | 179 | def with_prefix(value) 180 | previous = @current_prefix 181 | 182 | begin 183 | @current_prefix = value 184 | yield value 185 | ensure 186 | @current_prefix = previous 187 | end 188 | end 189 | 190 | private 191 | 192 | def format_iseq(iseq) 193 | output << "#{current_prefix}== disasm: #{iseq.inspect} " 194 | 195 | if iseq.catch_table.any? 196 | output << "(catch: TRUE)\n" 197 | output << "#{current_prefix}== catch table\n" 198 | 199 | with_prefix("#{current_prefix}| ") do 200 | iseq.catch_table.each do |entry| 201 | case entry 202 | when InstructionSequence::CatchBreak 203 | output << "#{current_prefix}catch type: break\n" 204 | format_iseq(entry.iseq) 205 | when InstructionSequence::CatchNext 206 | output << "#{current_prefix}catch type: next\n" 207 | when InstructionSequence::CatchRedo 208 | output << "#{current_prefix}catch type: redo\n" 209 | when InstructionSequence::CatchRescue 210 | output << "#{current_prefix}catch type: rescue\n" 211 | format_iseq(entry.iseq) 212 | end 213 | end 214 | end 215 | 216 | output << "#{current_prefix}|#{"-" * 72}\n" 217 | else 218 | output << "(catch: FALSE)\n" 219 | end 220 | 221 | if (local_table = iseq.local_table) && !local_table.empty? 222 | output << "#{current_prefix}local table (size: #{local_table.size})\n" 223 | 224 | locals = 225 | local_table.locals.each_with_index.map do |local, index| 226 | "[%2d] %s@%d" % [local_table.offset(index), local.name, index] 227 | end 228 | 229 | output << "#{current_prefix}#{locals.join(" ")}\n" 230 | end 231 | 232 | format_insns!(iseq.insns) 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /lib/syntax_tree/yarv/legacy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | module YARV 5 | # This module contains the instructions that used to be a part of YARV but 6 | # have been replaced or removed in more recent versions. 7 | module Legacy 8 | # ### Summary 9 | # 10 | # `getclassvariable` looks for a class variable in the current class and 11 | # pushes its value onto the stack. 12 | # 13 | # This version of the `getclassvariable` instruction is no longer used 14 | # since in Ruby 3.0 it gained an inline cache.` 15 | # 16 | # ### Usage 17 | # 18 | # ~~~ruby 19 | # @@class_variable 20 | # ~~~ 21 | # 22 | class GetClassVariable < Instruction 23 | attr_reader :name 24 | 25 | def initialize(name) 26 | @name = name 27 | end 28 | 29 | def disasm(fmt) 30 | fmt.instruction("getclassvariable", [fmt.object(name)]) 31 | end 32 | 33 | def to_a(_iseq) 34 | [:getclassvariable, name] 35 | end 36 | 37 | def deconstruct_keys(_keys) 38 | { name: name } 39 | end 40 | 41 | def ==(other) 42 | other.is_a?(GetClassVariable) && other.name == name 43 | end 44 | 45 | def length 46 | 2 47 | end 48 | 49 | def pushes 50 | 1 51 | end 52 | 53 | def canonical 54 | YARV::GetClassVariable.new(name, nil) 55 | end 56 | 57 | def call(vm) 58 | canonical.call(vm) 59 | end 60 | end 61 | 62 | # ### Summary 63 | # 64 | # `opt_getinlinecache` is a wrapper around a series of `putobject` and 65 | # `getconstant` instructions that allows skipping past them if the inline 66 | # cache is currently set. It pushes the value of the cache onto the stack 67 | # if it is set, otherwise it pushes `nil`. 68 | # 69 | # This instruction is no longer used since in Ruby 3.2 it was replaced by 70 | # the consolidated `opt_getconstant_path` instruction. 71 | # 72 | # ### Usage 73 | # 74 | # ~~~ruby 75 | # Constant 76 | # ~~~ 77 | # 78 | class OptGetInlineCache < Instruction 79 | attr_reader :label, :cache 80 | 81 | def initialize(label, cache) 82 | @label = label 83 | @cache = cache 84 | end 85 | 86 | def disasm(fmt) 87 | fmt.instruction( 88 | "opt_getinlinecache", 89 | [fmt.label(label), fmt.inline_storage(cache)] 90 | ) 91 | end 92 | 93 | def to_a(_iseq) 94 | [:opt_getinlinecache, label.name, cache] 95 | end 96 | 97 | def deconstruct_keys(_keys) 98 | { label: label, cache: cache } 99 | end 100 | 101 | def ==(other) 102 | other.is_a?(OptGetInlineCache) && other.label == label && 103 | other.cache == cache 104 | end 105 | 106 | def length 107 | 3 108 | end 109 | 110 | def pushes 111 | 1 112 | end 113 | 114 | def call(vm) 115 | vm.push(nil) 116 | end 117 | 118 | def branch_targets 119 | [label] 120 | end 121 | 122 | def falls_through? 123 | true 124 | end 125 | end 126 | 127 | # ### Summary 128 | # 129 | # `opt_setinlinecache` sets an inline cache for a constant lookup. It pops 130 | # the value it should set off the top of the stack. It uses this value to 131 | # set the cache. It then pushes that value back onto the top of the stack. 132 | # 133 | # This instruction is no longer used since in Ruby 3.2 it was replaced by 134 | # the consolidated `opt_getconstant_path` instruction. 135 | # 136 | # ### Usage 137 | # 138 | # ~~~ruby 139 | # Constant 140 | # ~~~ 141 | # 142 | class OptSetInlineCache < Instruction 143 | attr_reader :cache 144 | 145 | def initialize(cache) 146 | @cache = cache 147 | end 148 | 149 | def disasm(fmt) 150 | fmt.instruction("opt_setinlinecache", [fmt.inline_storage(cache)]) 151 | end 152 | 153 | def to_a(_iseq) 154 | [:opt_setinlinecache, cache] 155 | end 156 | 157 | def deconstruct_keys(_keys) 158 | { cache: cache } 159 | end 160 | 161 | def ==(other) 162 | other.is_a?(OptSetInlineCache) && other.cache == cache 163 | end 164 | 165 | def length 166 | 2 167 | end 168 | 169 | def pops 170 | 1 171 | end 172 | 173 | def pushes 174 | 1 175 | end 176 | 177 | def call(vm) 178 | end 179 | end 180 | 181 | # ### Summary 182 | # 183 | # `setclassvariable` looks for a class variable in the current class and 184 | # sets its value to the value it pops off the top of the stack. 185 | # 186 | # This version of the `setclassvariable` instruction is no longer used 187 | # since in Ruby 3.0 it gained an inline cache. 188 | # 189 | # ### Usage 190 | # 191 | # ~~~ruby 192 | # @@class_variable = 1 193 | # ~~~ 194 | # 195 | class SetClassVariable < Instruction 196 | attr_reader :name 197 | 198 | def initialize(name) 199 | @name = name 200 | end 201 | 202 | def disasm(fmt) 203 | fmt.instruction("setclassvariable", [fmt.object(name)]) 204 | end 205 | 206 | def to_a(_iseq) 207 | [:setclassvariable, name] 208 | end 209 | 210 | def deconstruct_keys(_keys) 211 | { name: name } 212 | end 213 | 214 | def ==(other) 215 | other.is_a?(SetClassVariable) && other.name == name 216 | end 217 | 218 | def length 219 | 2 220 | end 221 | 222 | def pops 223 | 1 224 | end 225 | 226 | def canonical 227 | YARV::SetClassVariable.new(name, nil) 228 | end 229 | 230 | def call(vm) 231 | canonical.call(vm) 232 | end 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /lib/syntax_tree/yarv/local_table.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SyntaxTree 4 | module YARV 5 | # This represents every local variable associated with an instruction 6 | # sequence. There are two kinds of locals: plain locals that are what you 7 | # expect, and block proxy locals, which represent local variables 8 | # associated with blocks that were passed into the current instruction 9 | # sequence. 10 | class LocalTable 11 | # A local representing a block passed into the current instruction 12 | # sequence. 13 | class BlockLocal 14 | attr_reader :name 15 | 16 | def initialize(name) 17 | @name = name 18 | end 19 | end 20 | 21 | # A regular local variable. 22 | class PlainLocal 23 | attr_reader :name 24 | 25 | def initialize(name) 26 | @name = name 27 | end 28 | end 29 | 30 | # The result of looking up a local variable in the current local table. 31 | class Lookup 32 | attr_reader :local, :index, :level 33 | 34 | def initialize(local, index, level) 35 | @local = local 36 | @index = index 37 | @level = level 38 | end 39 | end 40 | 41 | attr_reader :locals 42 | 43 | def initialize 44 | @locals = [] 45 | end 46 | 47 | def empty? 48 | locals.empty? 49 | end 50 | 51 | def find(name, level = 0) 52 | index = locals.index { |local| local.name == name } 53 | Lookup.new(locals[index], index, level) if index 54 | end 55 | 56 | def has?(name) 57 | locals.any? { |local| local.name == name } 58 | end 59 | 60 | def names 61 | locals.map(&:name) 62 | end 63 | 64 | def name_at(index) 65 | locals[index].name 66 | end 67 | 68 | def size 69 | locals.length 70 | end 71 | 72 | # Add a BlockLocal to the local table. 73 | def block(name) 74 | locals << BlockLocal.new(name) unless has?(name) 75 | end 76 | 77 | # Add a PlainLocal to the local table. 78 | def plain(name) 79 | locals << PlainLocal.new(name) unless has?(name) 80 | end 81 | 82 | # This is the offset from the top of the stack where this local variable 83 | # lives. 84 | def offset(index) 85 | size - (index - 3) - 1 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /syntax_tree.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/syntax_tree/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "syntax_tree" 7 | spec.version = SyntaxTree::VERSION 8 | spec.authors = ["Kevin Newton"] 9 | spec.email = ["kddnewton@gmail.com"] 10 | 11 | spec.summary = "A parser based on ripper" 12 | spec.homepage = "https://github.com/kddnewton/syntax_tree" 13 | spec.license = "MIT" 14 | spec.metadata = { "rubygems_mfa_required" => "true" } 15 | 16 | spec.files = 17 | Dir.chdir(__dir__) do 18 | `git ls-files -z`.split("\x0") 19 | .reject { |f| f.match(%r{^(test|spec|features)/}) } 20 | end 21 | 22 | spec.required_ruby_version = ">= 2.7.0" 23 | 24 | spec.bindir = "exe" 25 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 26 | spec.require_paths = %w[lib] 27 | 28 | spec.add_dependency "prettier_print", ">= 1.2.0" 29 | 30 | spec.add_development_dependency "bundler" 31 | spec.add_development_dependency "minitest" 32 | spec.add_development_dependency "rake" 33 | spec.add_development_dependency "rubocop" 34 | spec.add_development_dependency "simplecov" 35 | end 36 | -------------------------------------------------------------------------------- /tasks/whitequark.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file's purpose is to extract the examples from the whitequark/parser 4 | # gem and generate a test file that we can use to ensure that our parser 5 | # generates equivalent syntax trees when translating. To do this, it runs the 6 | # parser's test suite but overrides the `assert_parses` method to collect the 7 | # examples into a hash. Then, it writes out the hash to a file that we can use 8 | # to generate our own tests. 9 | # 10 | # To run the test suite, it's important to note that we have to mirror both any 11 | # APIs provided to the test suite (for example the ParseHelper module below). 12 | # This is obviously relatively brittle, but it's effective for now. 13 | 14 | require "ast" 15 | 16 | module ParseHelper 17 | # This object is going to collect all of the examples from the parser gem into 18 | # a hash that we can use to generate our own tests. 19 | COLLECTED = Hash.new { |hash, key| hash[key] = [] } 20 | 21 | include AST::Sexp 22 | ALL_VERSIONS = %w[3.1 3.2] 23 | 24 | private 25 | 26 | def assert_context(*) 27 | end 28 | 29 | def assert_diagnoses(*) 30 | end 31 | 32 | def assert_diagnoses_many(*) 33 | end 34 | 35 | def refute_diagnoses(*) 36 | end 37 | 38 | def with_versions(*) 39 | end 40 | 41 | def assert_parses(_ast, code, _source_maps = "", versions = ALL_VERSIONS) 42 | # We're going to skip any examples that are for older Ruby versions 43 | # that we do not support. 44 | return if (versions & %w[3.1 3.2]).empty? 45 | 46 | entry = caller.find { _1.include?("test_parser.rb") } 47 | _, lineno, name = *entry.match(/(\d+):in `(.+)'/) 48 | 49 | COLLECTED["#{name}:#{lineno}"] << code 50 | end 51 | end 52 | 53 | namespace :extract do 54 | desc "Extract the whitequark/parser tests" 55 | task :whitequark do 56 | directory = File.expand_path("../tmp/parser", __dir__) 57 | unless File.directory?(directory) 58 | sh "git clone --depth 1 https://github.com/whitequark/parser #{directory}" 59 | end 60 | 61 | mkdir_p "#{directory}/extract" 62 | touch "#{directory}/extract/helper.rb" 63 | touch "#{directory}/extract/parse_helper.rb" 64 | touch "#{directory}/extract/extracted.txt" 65 | $:.unshift "#{directory}/extract" 66 | 67 | require "parser/current" 68 | require "minitest/autorun" 69 | require_relative "#{directory}/test/test_parser" 70 | 71 | Minitest.after_run do 72 | filepath = File.expand_path("../test/translation/parser.txt", __dir__) 73 | 74 | File.open(filepath, "w") do |file| 75 | ParseHelper::COLLECTED.sort.each do |(key, codes)| 76 | if codes.length == 1 77 | file.puts("!!! #{key}\n#{codes.first}") 78 | else 79 | codes.each_with_index do |code, index| 80 | file.puts("!!! #{key}:#{index}\n#{code}") 81 | end 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/encoded.rb: -------------------------------------------------------------------------------- 1 | # encoding: Shift_JIS 2 | # frozen_string_literal: true 3 | -------------------------------------------------------------------------------- /test/fixtures/CHAR.rb: -------------------------------------------------------------------------------- 1 | % 2 | ?a 3 | - 4 | "a" 5 | % 6 | ?\C-a 7 | % 8 | ?\M-a 9 | % 10 | ?\M-\C-a 11 | % 12 | ?a # comment 13 | - 14 | "a" # comment 15 | -------------------------------------------------------------------------------- /test/fixtures/access_ctrl.rb: -------------------------------------------------------------------------------- 1 | % 2 | class Foo 3 | private 4 | end 5 | % 6 | class Foo 7 | private 8 | def foo 9 | end 10 | end 11 | - 12 | class Foo 13 | private 14 | 15 | def foo 16 | end 17 | end 18 | % 19 | class Foo 20 | def foo 21 | end 22 | private 23 | end 24 | - 25 | class Foo 26 | def foo 27 | end 28 | 29 | private 30 | end 31 | -------------------------------------------------------------------------------- /test/fixtures/alias.rb: -------------------------------------------------------------------------------- 1 | % 2 | alias foo bar 3 | % 4 | alias << push 5 | % 6 | alias in within 7 | % 8 | alias in IN 9 | % 10 | alias :foo :bar 11 | - 12 | alias foo bar 13 | % 14 | alias :"foo" :bar 15 | - 16 | alias :"foo" bar 17 | % 18 | alias :foo :"bar" 19 | - 20 | alias foo :"bar" 21 | % 22 | alias foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo bar 23 | - 24 | alias foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 25 | bar 26 | % 27 | alias foo bar # comment 28 | % 29 | alias foo # comment 30 | bar 31 | % 32 | alias foo # comment1 33 | bar # comment2 34 | -------------------------------------------------------------------------------- /test/fixtures/aref.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo[bar] 3 | % 4 | foo[] 5 | % 6 | foo[ 7 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 8 | ] 9 | -------------------------------------------------------------------------------- /test/fixtures/aref_field.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo[bar] = baz 3 | % 4 | foo[] = baz 5 | % 6 | foo[ 7 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 8 | ] = baz 9 | % 10 | foo[bar] # comment 11 | % 12 | foo[bar] += baz 13 | -------------------------------------------------------------------------------- /test/fixtures/arg_block.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo(&bar) 3 | % 4 | foo( 5 | &bar 6 | ) 7 | - 8 | foo(&bar) 9 | % 10 | foo(&bar.baz) 11 | % 12 | foo(&bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) 13 | - 14 | foo( 15 | &bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 16 | ) 17 | % # >= 3.1.0 18 | def foo(&) 19 | bar(&) 20 | end 21 | % # https://github.com/ruby-syntax-tree/syntax_tree/issues/45 22 | foo.instance_exec(&T.must(block)) 23 | -------------------------------------------------------------------------------- /test/fixtures/arg_paren.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo(bar) 3 | % 4 | foo() 5 | % 6 | foo(barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr) 7 | - 8 | foo( 9 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 10 | ) 11 | % 12 | foo( 13 | bar 14 | ) 15 | - 16 | foo(bar) 17 | -------------------------------------------------------------------------------- /test/fixtures/arg_star.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo(*bar) 3 | % 4 | foo( 5 | *bar 6 | ) 7 | - 8 | foo(*bar) 9 | % 10 | foo(*bar.baz) 11 | % 12 | foo(*bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) 13 | - 14 | foo( 15 | *bar.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 16 | ) 17 | -------------------------------------------------------------------------------- /test/fixtures/args.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo(bar, baz) 3 | % 4 | foo(barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz) 5 | - 6 | foo( 7 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, 8 | baz 9 | ) 10 | % 11 | foo( 12 | bar, 13 | baz 14 | ) 15 | - 16 | foo(bar, baz) 17 | % 18 | foo( 19 | bar, # comment 20 | baz 21 | ) 22 | -------------------------------------------------------------------------------- /test/fixtures/args_forward.rb: -------------------------------------------------------------------------------- 1 | % # >= 2.7.3 2 | def foo(...) 3 | bar(:baz, ...) 4 | end 5 | % # >= 3.1.0 6 | def foo(foo, bar = baz, ...) 7 | bar(:baz, ...) 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/array_literal.rb: -------------------------------------------------------------------------------- 1 | % 2 | [] 3 | % 4 | [foo, bar, baz] 5 | % 6 | [foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, bar, baz] 7 | - 8 | [ 9 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo, 10 | bar, 11 | baz 12 | ] 13 | % 14 | [ 15 | foo, 16 | bar, 17 | baz 18 | ] 19 | - 20 | [foo, bar, baz] 21 | % 22 | fooooooooooooooooo = 1 23 | [fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo] 24 | - 25 | fooooooooooooooooo = 1 26 | [ 27 | fooooooooooooooooo, 28 | fooooooooooooooooo, 29 | fooooooooooooooooo, 30 | fooooooooooooooooo, 31 | fooooooooooooooooo, 32 | fooooooooooooooooo, 33 | fooooooooooooooooo, 34 | fooooooooooooooooo, 35 | fooooooooooooooooo, 36 | fooooooooooooooooo 37 | ] 38 | % 39 | [ 40 | # comment 41 | ] 42 | % 43 | ["foo"] 44 | % 45 | ["foo", "bar"] 46 | - 47 | %w[foo bar] 48 | % 49 | ["f", ?b] 50 | - 51 | %w[f b] 52 | % 53 | [ 54 | "foo", 55 | "bar" # comment 56 | ] 57 | % 58 | ["foo", "bar"] # comment 59 | - 60 | %w[foo bar] # comment 61 | % 62 | ["foo", :bar] 63 | % 64 | ["foo", "#{bar}"] 65 | % 66 | ["foo", " bar "] 67 | % 68 | ["foo", "bar\n"] 69 | % 70 | ["foo", "bar]"] 71 | % 72 | [:foo] 73 | % 74 | [:foo, :bar] 75 | - 76 | %i[foo bar] 77 | % 78 | [ 79 | :foo, 80 | :bar # comment 81 | ] 82 | % 83 | [:foo, :bar] # comment 84 | - 85 | %i[foo bar] # comment 86 | % 87 | [:foo, "bar"] 88 | % 89 | [:foo, :"bar"] 90 | % 91 | [foo, bar] # comment 92 | -------------------------------------------------------------------------------- /test/fixtures/aryptn.rb: -------------------------------------------------------------------------------- 1 | % 2 | case foo 3 | in [] 4 | end 5 | % 6 | case foo 7 | in [] then 8 | end 9 | - 10 | case foo 11 | in [] 12 | end 13 | % 14 | case foo 15 | in * then 16 | end 17 | - 18 | case foo 19 | in [*] 20 | end 21 | % 22 | case foo 23 | in _, _ 24 | end 25 | - 26 | case foo 27 | in [_, _] 28 | end 29 | % 30 | case foo 31 | in bar, baz 32 | end 33 | - 34 | case foo 35 | in [bar, baz] 36 | end 37 | % 38 | case foo 39 | in [bar] 40 | end 41 | % 42 | case foo 43 | in [bar] 44 | in [baz] 45 | end 46 | % 47 | case foo 48 | in [bar, baz] 49 | end 50 | % 51 | case foo 52 | in bar, *baz 53 | end 54 | - 55 | case foo 56 | in [bar, *baz] 57 | end 58 | % 59 | case foo 60 | in *bar, baz 61 | end 62 | - 63 | case foo 64 | in [*bar, baz] 65 | end 66 | % 67 | case foo 68 | in bar, *, baz 69 | end 70 | - 71 | case foo 72 | in [bar, *, baz] 73 | end 74 | % 75 | case foo 76 | in *, bar, baz 77 | end 78 | - 79 | case foo 80 | in [*, bar, baz] 81 | end 82 | % 83 | case foo 84 | in Constant[bar] 85 | end 86 | % 87 | case foo 88 | in Constant(bar) 89 | end 90 | - 91 | case foo 92 | in Constant[bar] 93 | end 94 | % 95 | case foo 96 | in Constant[bar, baz] 97 | end 98 | % 99 | case foo 100 | in bar, [baz, _] => qux 101 | end 102 | - 103 | case foo 104 | in [bar, [baz, _] => qux] 105 | end 106 | % 107 | case foo 108 | in bar, baz if bar == baz 109 | end 110 | - 111 | case foo 112 | in [bar, baz] if bar == baz 113 | end 114 | -------------------------------------------------------------------------------- /test/fixtures/assign.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo = bar 3 | % 4 | foo = 5 | begin 6 | bar 7 | end 8 | % 9 | foo = <<~HERE 10 | bar 11 | HERE 12 | % 13 | foo = %s[ 14 | bar 15 | ] 16 | % 17 | foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 18 | - 19 | foo = 20 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 21 | % 22 | foo = [barrrrrrrrrrrrrrrrrrrrr, barrrrrrrrrrrrrrrrrrrrr, barrrrrrrrrrrrrrrrrrrrr] 23 | - 24 | foo = [ 25 | barrrrrrrrrrrrrrrrrrrrr, 26 | barrrrrrrrrrrrrrrrrrrrr, 27 | barrrrrrrrrrrrrrrrrrrrr 28 | ] 29 | % 30 | foo = { bar1: bazzzzzzzzzzzzzzz, bar2: bazzzzzzzzzzzzzzz, bar3: bazzzzzzzzzzzzzzz } 31 | - 32 | foo = { 33 | bar1: bazzzzzzzzzzzzzzz, 34 | bar2: bazzzzzzzzzzzzzzz, 35 | bar3: bazzzzzzzzzzzzzzz 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/assoc.rb: -------------------------------------------------------------------------------- 1 | % 2 | { foo: bar } 3 | % 4 | { foo: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } 5 | - 6 | { 7 | foo: 8 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 9 | } 10 | % 11 | { 12 | foo: 13 | bar 14 | } 15 | - 16 | { foo: bar } 17 | % 18 | { 19 | foo: [ 20 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 21 | ] 22 | } 23 | % 24 | { 25 | foo: { 26 | fooooooooooooooooooooooooooooooooo: ooooooooooooooooooooooooooooooooooooooo 27 | } 28 | } 29 | % 30 | { 31 | foo: -> do 32 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 33 | end 34 | } 35 | % 36 | { 37 | foo: %w[ 38 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 39 | ] 40 | } 41 | % # >= 3.1.0 42 | { foo: } 43 | % 44 | { "foo": "bar" } 45 | - 46 | { foo: "bar" } 47 | % 48 | { "foo #{bar}": "baz" } 49 | % 50 | { "foo=": "baz" } 51 | -------------------------------------------------------------------------------- /test/fixtures/assoc_splat.rb: -------------------------------------------------------------------------------- 1 | % 2 | { **foo } 3 | % 4 | { **foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } 5 | - 6 | { 7 | **foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 8 | } 9 | % 10 | { 11 | **foo 12 | } 13 | - 14 | { **foo } 15 | % # >= 3.2.0 16 | def foo(**) 17 | bar(**) 18 | end 19 | -------------------------------------------------------------------------------- /test/fixtures/backref.rb: -------------------------------------------------------------------------------- 1 | % 2 | $1 3 | % 4 | $1 # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/backtick.rb: -------------------------------------------------------------------------------- 1 | % 2 | def `(value) 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/bare_assoc_hash.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo(bar: bar) 3 | % 4 | foo(:bar => bar) 5 | - 6 | foo(bar: bar) 7 | % 8 | foo(:"bar" => bar) 9 | - 10 | foo(bar: bar) 11 | % 12 | foo(bar => bar, baz: baz) 13 | - 14 | foo(bar => bar, :baz => baz) 15 | % 16 | foo(bar => bar, "baz": baz) 17 | - 18 | foo(bar => bar, :"baz" => baz) 19 | % 20 | foo(bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzz) 21 | - 22 | foo( 23 | bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, 24 | baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 25 | ) 26 | -------------------------------------------------------------------------------- /test/fixtures/begin.rb: -------------------------------------------------------------------------------- 1 | % 2 | begin 3 | end 4 | % 5 | begin 6 | expression 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/begin_block.rb: -------------------------------------------------------------------------------- 1 | % 2 | BEGIN { foo } 3 | % 4 | BEGIN { 5 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 6 | } 7 | % 8 | BEGIN { 9 | foo 10 | } 11 | - 12 | BEGIN { foo } 13 | % 14 | BEGIN { foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } 15 | - 16 | BEGIN { 17 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 18 | } 19 | % 20 | BEGIN { # comment 21 | foo 22 | } 23 | % 24 | BEGIN { 25 | # comment 26 | foo 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/binary.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo + bar 3 | % 4 | foo << bar 5 | % 6 | foo << barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr << barrrrrrrrrrrrr << barrrrrrrrrrrrrrrrrr 7 | - 8 | foo << barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr << barrrrrrrrrrrrr << 9 | barrrrrrrrrrrrrrrrrr 10 | % 11 | foo**bar 12 | % 13 | foo * barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 14 | - 15 | foo * 16 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 17 | -------------------------------------------------------------------------------- /test/fixtures/block_arg.rb: -------------------------------------------------------------------------------- 1 | % 2 | def foo(&bar) 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/block_var.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo { |bar, baz| } 3 | % 4 | foo { |bar; baz| } 5 | % 6 | foo { |bar, baz; qux, qaz| } 7 | -------------------------------------------------------------------------------- /test/fixtures/bodystmt.rb: -------------------------------------------------------------------------------- 1 | % 2 | begin 3 | foo 4 | rescue Foo 5 | foo 6 | rescue Bar 7 | foo 8 | else 9 | foo 10 | ensure 11 | foo 12 | end 13 | % 14 | begin 15 | foo 16 | rescue Foo 17 | foo 18 | rescue Bar 19 | foo 20 | end 21 | % 22 | begin 23 | foo 24 | rescue Foo 25 | foo 26 | rescue Bar 27 | foo 28 | else 29 | foo 30 | end 31 | % 32 | begin 33 | foo 34 | ensure 35 | foo 36 | end 37 | % 38 | begin 39 | else # else 40 | end 41 | % 42 | begin 43 | ensure # ensure 44 | end 45 | % 46 | begin 47 | rescue # rescue 48 | else # else 49 | ensure # ensure 50 | end 51 | - 52 | begin 53 | rescue StandardError # rescue 54 | else # else 55 | ensure # ensure 56 | end 57 | -------------------------------------------------------------------------------- /test/fixtures/brace_block.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo {} 3 | % 4 | foo { # comment 5 | } 6 | - 7 | foo do # comment 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/break.rb: -------------------------------------------------------------------------------- 1 | % 2 | break 3 | % 4 | break foo 5 | % 6 | break foo, bar 7 | % 8 | break(foo) 9 | % 10 | break fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 11 | - 12 | break( 13 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 14 | ) 15 | % 16 | break(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) 17 | - 18 | break( 19 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 20 | ) 21 | % 22 | break (foo), bar 23 | % 24 | break( 25 | foo 26 | bar 27 | ) 28 | % 29 | break foo.bar :baz do |qux| qux end 30 | - 31 | break( 32 | foo.bar :baz do |qux| 33 | qux 34 | end 35 | ) 36 | % 37 | break :foo => "bar" 38 | -------------------------------------------------------------------------------- /test/fixtures/call.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo.bar 3 | % 4 | foo.bar(baz) 5 | % 6 | foo.() 7 | % 8 | foo::() 9 | - 10 | foo.() 11 | % 12 | foo.(1) 13 | % 14 | foo::(1) 15 | - 16 | foo.(1) 17 | % 18 | foo.bar.baz.qux 19 | % 20 | fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr {}.bazzzzzzzzzzzzzzzzzzzzzzzzzz.quxxxxxxxxx 21 | - 22 | fooooooooooooooooo 23 | .barrrrrrrrrrrrrrrrrrr {} 24 | .bazzzzzzzzzzzzzzzzzzzzzzzzzz 25 | .quxxxxxxxxx 26 | % 27 | foo. # comment 28 | bar 29 | % 30 | foo 31 | .bar 32 | .baz # comment 33 | .qux 34 | .quux 35 | % 36 | foo 37 | .bar 38 | .baz. 39 | # comment 40 | qux 41 | .quux 42 | % 43 | { a: 1, b: 2 }.fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr.bazzzzzzzzzzzz.quxxxxxxxxxxxx 44 | - 45 | { a: 1, b: 2 }.fooooooooooooooooo 46 | .barrrrrrrrrrrrrrrrrrr 47 | .bazzzzzzzzzzzz 48 | .quxxxxxxxxxxxx 49 | % 50 | fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr.bazzzzzzzzzzzz.quxxxxxxxxxxxx.each { block } 51 | - 52 | fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr.bazzzzzzzzzzzz.quxxxxxxxxxxxx.each do 53 | block 54 | end 55 | % 56 | foo.bar.baz.each do 57 | block1 58 | block2 59 | end 60 | % 61 | a b do 62 | end.c d 63 | % 64 | self. 65 | =begin 66 | =end 67 | to_s 68 | % 69 | fooooooooooooooooooooooooooooooooooo.barrrrrrrrrrrrrrrrrrrrrrrrrrrrrr.where.not(:id).order(:id) 70 | - 71 | fooooooooooooooooooooooooooooooooooo 72 | .barrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 73 | .where.not(:id) 74 | .order(:id) 75 | -------------------------------------------------------------------------------- /test/fixtures/case.rb: -------------------------------------------------------------------------------- 1 | % 2 | case foo 3 | when bar 4 | baz 5 | end 6 | % 7 | case 8 | when bar 9 | baz 10 | end 11 | % 12 | case # comment 13 | when foo 14 | bar 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/class.rb: -------------------------------------------------------------------------------- 1 | % 2 | class Foo 3 | end 4 | % 5 | class Foo 6 | foo 7 | end 8 | % 9 | class Foo 10 | # comment 11 | end 12 | % 13 | class Foo # comment 14 | end 15 | % 16 | module Foo 17 | class Bar 18 | end 19 | end 20 | % 21 | class Foo < foo 22 | end 23 | % 24 | class Foo < foo 25 | foo 26 | end 27 | % 28 | class Foo < foo 29 | # comment 30 | end 31 | % 32 | class Foo < foo # comment 33 | end 34 | % 35 | module Foo 36 | class Bar < foo 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/fixtures/command.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo bar 3 | % 4 | foo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 5 | - 6 | foo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, 7 | bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 8 | % 9 | meta1 def foo 10 | end 11 | % 12 | meta2 meta1 def foo 13 | end 14 | % 15 | meta3 meta2 meta1 def foo 16 | end 17 | % 18 | meta1 def self.foo 19 | end 20 | % 21 | meta2 meta1 def self.foo 22 | end 23 | % 24 | meta3 meta2 meta1 def self.foo 25 | end 26 | % 27 | foo bar {} 28 | % 29 | foo bar do 30 | end 31 | -------------------------------------------------------------------------------- /test/fixtures/command_call.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo.bar baz 3 | % 4 | foo.bar barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 5 | - 6 | foo.bar barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, 7 | bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 8 | % 9 | expect(foo).to receive(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) 10 | - 11 | expect(foo).to receive( 12 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 13 | ) 14 | % 15 | expect(foo).not_to receive(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) 16 | - 17 | expect(foo).not_to receive( 18 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 19 | ) 20 | % 21 | expect(foo).to_not receive(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) 22 | - 23 | expect(foo).to_not receive( 24 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 25 | ) 26 | % 27 | foo.bar baz {} 28 | % 29 | foo.bar baz do 30 | end 31 | % 32 | foo. 33 | # comment 34 | bar baz 35 | % 36 | foo.bar baz ? qux : qaz 37 | % 38 | expect foo, bar.map { |i| { quux: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz } } 39 | - 40 | expect foo, 41 | bar.map { |i| 42 | { 43 | quux: 44 | bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 45 | } 46 | } 47 | % 48 | expect(foo, bar.map { |i| {quux: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz} }) 49 | - 50 | expect( 51 | foo, 52 | bar.map do |i| 53 | { 54 | quux: 55 | bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 56 | } 57 | end 58 | ) 59 | % 60 | expect(foo.map { |i| { bar: i.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz } } ).to match(baz.map { |i| { bar: i.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz } }) 61 | - 62 | expect( 63 | foo.map do |i| 64 | { 65 | bar: 66 | i.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 67 | } 68 | end 69 | ).to match( 70 | baz.map do |i| 71 | { 72 | bar: 73 | i.bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 74 | } 75 | end 76 | ) 77 | -------------------------------------------------------------------------------- /test/fixtures/command_def_endless.rb: -------------------------------------------------------------------------------- 1 | % 2 | meta1 def foo = 1 3 | % 4 | meta2 meta1 def foo = 1 5 | % 6 | meta3 meta2 meta1 def foo = 1 7 | -------------------------------------------------------------------------------- /test/fixtures/const.rb: -------------------------------------------------------------------------------- 1 | % 2 | Foo 3 | % 4 | Foo # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/const_path_field.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo::Bar = baz 3 | -------------------------------------------------------------------------------- /test/fixtures/const_path_ref.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo::Bar 3 | -------------------------------------------------------------------------------- /test/fixtures/const_ref.rb: -------------------------------------------------------------------------------- 1 | % 2 | class Foo 3 | end 4 | % 5 | class Foo::Bar 6 | end 7 | -------------------------------------------------------------------------------- /test/fixtures/cvar.rb: -------------------------------------------------------------------------------- 1 | % 2 | @@foo 3 | % 4 | @@foo # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/def.rb: -------------------------------------------------------------------------------- 1 | % 2 | def foo(bar) 3 | baz 4 | end 5 | % 6 | def foo bar 7 | baz 8 | end 9 | - 10 | def foo(bar) 11 | baz 12 | end 13 | % 14 | def foo(bar) # comment 15 | end 16 | % 17 | def foo() 18 | end 19 | % 20 | def foo() # comment 21 | end 22 | % 23 | def foo( # comment 24 | ) 25 | end 26 | % 27 | def 28 | =begin 29 | =end 30 | a 31 | end 32 | -------------------------------------------------------------------------------- /test/fixtures/def_endless.rb: -------------------------------------------------------------------------------- 1 | % 2 | def foo = bar 3 | % 4 | def foo(bar) = baz 5 | % 6 | def foo() = bar 7 | % # >= 3.1.0 8 | def foo = bar baz 9 | % # >= 3.1.0 10 | def self.foo = bar 11 | % # >= 3.1.0 12 | def self.foo(bar) = baz 13 | % # >= 3.1.0 14 | def self.foo() = bar 15 | % # >= 3.1.0 16 | def self.foo = bar baz 17 | % 18 | begin 19 | true 20 | rescue StandardError 21 | false 22 | end 23 | 24 | def foo? = true 25 | % 26 | def a() 27 | =begin 28 | =end 29 | =1 30 | - 31 | def a() = 32 | =begin 33 | =end 34 | 1 35 | -------------------------------------------------------------------------------- /test/fixtures/defined.rb: -------------------------------------------------------------------------------- 1 | % 2 | defined?(foo) 3 | % 4 | defined?(foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) 5 | - 6 | defined?( 7 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 8 | ) 9 | % 10 | defined?( 11 | foo 12 | ) 13 | - 14 | defined?(foo) 15 | -------------------------------------------------------------------------------- /test/fixtures/defs.rb: -------------------------------------------------------------------------------- 1 | % 2 | def foo.foo(bar) 3 | baz 4 | end 5 | % 6 | def foo.foo bar 7 | baz 8 | end 9 | - 10 | def foo.foo(bar) 11 | baz 12 | end 13 | % 14 | def foo.foo(bar) # comment 15 | end 16 | % 17 | def foo.foo() 18 | end 19 | % 20 | def foo.foo() # comment 21 | end 22 | % 23 | def foo.foo( # comment 24 | ) 25 | end 26 | % 27 | def foo::foo 28 | end 29 | - 30 | def foo.foo 31 | end 32 | -------------------------------------------------------------------------------- /test/fixtures/do_block.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo do 3 | end 4 | - 5 | foo {} 6 | % 7 | foo do 8 | # comment 9 | end 10 | % 11 | foo do # comment 12 | end 13 | % 14 | foo :bar do 15 | baz 16 | end 17 | % 18 | sig do 19 | override.params(contacts: Contact::ActiveRecord_Relation).returns( 20 | Customer::ActiveRecord_Relation 21 | ) 22 | end 23 | - 24 | sig do 25 | override 26 | .params(contacts: Contact::ActiveRecord_Relation) 27 | .returns(Customer::ActiveRecord_Relation) 28 | end 29 | -------------------------------------------------------------------------------- /test/fixtures/dot2.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo..bar 3 | % 4 | foo.. 5 | % 6 | ..bar 7 | % 8 | foo..bar # comment 9 | % 10 | foo.. # comment 11 | % 12 | ..bar # comment 13 | % 14 | if foo == bar .. foo == baz 15 | end 16 | % 17 | unless foo == bar .. foo == baz 18 | end 19 | -------------------------------------------------------------------------------- /test/fixtures/dot3.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo...bar 3 | % 4 | foo... 5 | % 6 | ...bar 7 | % 8 | foo...bar # comment 9 | % 10 | foo... # comment 11 | % 12 | ...bar # comment 13 | % 14 | if foo == bar ... foo == baz 15 | end 16 | % 17 | unless foo == bar ... foo == baz 18 | end 19 | -------------------------------------------------------------------------------- /test/fixtures/dyna_symbol.rb: -------------------------------------------------------------------------------- 1 | % 2 | :'foo' 3 | - 4 | :"foo" 5 | % 6 | :"foo" 7 | % 8 | :'foo #{bar}' 9 | % 10 | :"foo #{bar}" 11 | % 12 | %s[foo #{bar}] 13 | - 14 | :'foo #{bar}' 15 | % 16 | { %s[foo] => bar } 17 | - 18 | { foo: bar } 19 | % 20 | %s[ 21 | foo 22 | ] 23 | -------------------------------------------------------------------------------- /test/fixtures/else.rb: -------------------------------------------------------------------------------- 1 | % 2 | case 3 | when foo 4 | else 5 | end 6 | % 7 | if foo 8 | else 9 | end 10 | % 11 | case 12 | when foo 13 | else 14 | bar 15 | end 16 | % 17 | if foo 18 | else 19 | bar 20 | end 21 | % 22 | if foo 23 | else # bar 24 | end 25 | -------------------------------------------------------------------------------- /test/fixtures/elsif.rb: -------------------------------------------------------------------------------- 1 | % 2 | if foo 3 | bar 4 | elsif baz 5 | end 6 | % 7 | if foo 8 | bar 9 | elsif baz 10 | qux 11 | end 12 | % 13 | if foo 14 | bar 15 | elsif baz 16 | qux 17 | else 18 | qyz 19 | end 20 | % 21 | if true 22 | elsif false # comment1 23 | # comment2 24 | end 25 | -------------------------------------------------------------------------------- /test/fixtures/embdoc.rb: -------------------------------------------------------------------------------- 1 | % 2 | =begin 3 | comment 4 | =end 5 | % 6 | module Foo 7 | =begin 8 | comment 9 | =end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/end_block.rb: -------------------------------------------------------------------------------- 1 | % 2 | END { foo } 3 | % 4 | END { 5 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 6 | } 7 | % 8 | END { 9 | foo 10 | } 11 | - 12 | END { foo } 13 | % 14 | END { foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } 15 | - 16 | END { 17 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 18 | } 19 | % 20 | END { # comment 21 | foo 22 | } 23 | % 24 | END { 25 | # comment 26 | foo 27 | } 28 | -------------------------------------------------------------------------------- /test/fixtures/end_content.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo = bar 3 | 4 | __END__ 5 | /‾‾‾‾‾\ /‾/ /‾/ /‾‾‾‾‾\ |‾| /‾/ 6 | / /‾‾/ / / / / / / /‾‾/ / | |/ / 7 | / ‾‾‾ / / / / / / ‾‾‾_/ | / 8 | / /‾\ \‾ / /_/ / / /‾‾/ | / / 9 | |_/ /_/ |_____/ |__‾_‾_/ |__/ 10 | -------------------------------------------------------------------------------- /test/fixtures/ensure.rb: -------------------------------------------------------------------------------- 1 | % 2 | begin 3 | ensure 4 | end 5 | % 6 | begin 7 | ensure 8 | foo 9 | end 10 | % 11 | begin 12 | ensure 13 | # comment 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/excessed_comma.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo.each do |bar, baz,| 3 | # comment 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/fcall.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo(bar) 3 | -------------------------------------------------------------------------------- /test/fixtures/field.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo.bar = baz 3 | -------------------------------------------------------------------------------- /test/fixtures/float_literal.rb: -------------------------------------------------------------------------------- 1 | % 2 | 1.0 3 | % 4 | 1.0 # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/fndptn.rb: -------------------------------------------------------------------------------- 1 | % 2 | case foo 3 | in *, bar, * then 4 | end 5 | - 6 | case foo 7 | in [*, bar, *] 8 | end 9 | % 10 | case foo 11 | in *, bar, *baz 12 | end 13 | - 14 | case foo 15 | in [*, bar, *baz] 16 | end 17 | % 18 | case foo 19 | in *foo, bar, *baz 20 | end 21 | - 22 | case foo 23 | in [*foo, bar, *baz] 24 | end 25 | % 26 | case foo 27 | in [*, bar, *] 28 | end 29 | % 30 | case foo 31 | in [*, bar, baz, qux, *] 32 | end 33 | % 34 | case foo 35 | in [*foo, bar, *] 36 | end 37 | % 38 | case foo 39 | in [*, bar, *baz] 40 | end 41 | % 42 | case foo 43 | in [*foo, bar, *baz] 44 | end 45 | % 46 | case foo 47 | in Foo[*, bar, *] 48 | end 49 | % 50 | case foo 51 | in Foo[*, bar, baz, qux, *] 52 | end 53 | % 54 | case foo 55 | in Foo[*foo, bar, *] 56 | end 57 | % 58 | case foo 59 | in Foo[*, bar, *baz] 60 | end 61 | % 62 | case foo 63 | in Foo[*foo, bar, *baz] 64 | end 65 | % 66 | case foo 67 | in Foo(*, bar, *) 68 | end 69 | - 70 | case foo 71 | in Foo[*, bar, *] 72 | end 73 | % 74 | case foo 75 | in Foo(*, bar, baz, qux, *) 76 | end 77 | - 78 | case foo 79 | in Foo[*, bar, baz, qux, *] 80 | end 81 | % 82 | case foo 83 | in Foo(*foo, bar, *) 84 | end 85 | - 86 | case foo 87 | in Foo[*foo, bar, *] 88 | end 89 | % 90 | case foo 91 | in Foo(*, bar, *baz) 92 | end 93 | - 94 | case foo 95 | in Foo[*, bar, *baz] 96 | end 97 | % 98 | case foo 99 | in Foo(*foo, bar, *baz) 100 | end 101 | - 102 | case foo 103 | in Foo[*foo, bar, *baz] 104 | end 105 | -------------------------------------------------------------------------------- /test/fixtures/for.rb: -------------------------------------------------------------------------------- 1 | % 2 | for foo in bar 3 | end 4 | % 5 | for foo in bar 6 | foo 7 | end 8 | % 9 | for foo in bar 10 | # comment 11 | end 12 | % 13 | for foo, bar, baz in bar 14 | end 15 | % 16 | for foo, bar, baz in bar 17 | foo 18 | end 19 | % 20 | for foo, bar, baz in bar 21 | # comment 22 | end 23 | % 24 | foo do 25 | # comment 26 | for bar in baz do 27 | bar 28 | end 29 | end 30 | - 31 | foo do 32 | # comment 33 | for bar in baz 34 | bar 35 | end 36 | end 37 | % 38 | for foo, in [[foo, bar]] 39 | foo 40 | end 41 | % 42 | for foo in bar # comment1 43 | # comment2 44 | end 45 | -------------------------------------------------------------------------------- /test/fixtures/gvar.rb: -------------------------------------------------------------------------------- 1 | % 2 | $foo 3 | % 4 | $foo # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/hash.rb: -------------------------------------------------------------------------------- 1 | % 2 | {} 3 | % 4 | { bar: bar } 5 | % 6 | { :bar => bar } 7 | - 8 | { bar: bar } 9 | % 10 | { :"bar" => bar } 11 | - 12 | { bar: bar } 13 | % 14 | { bar => bar, baz: baz } 15 | - 16 | { bar => bar, :baz => baz } 17 | % 18 | { bar => bar, "baz": baz } 19 | - 20 | { bar => bar, :"baz" => baz } 21 | % 22 | { bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz } 23 | - 24 | { 25 | bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, 26 | baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 27 | } 28 | % 29 | { 30 | # comment 31 | } 32 | % # >= 3.1.0 33 | { foo:, "bar" => "baz" } 34 | -------------------------------------------------------------------------------- /test/fixtures/heredoc.rb: -------------------------------------------------------------------------------- 1 | % 2 | <<-FOO 3 | bar 4 | FOO 5 | % 6 | <<-FOO 7 | bar 8 | #{baz} 9 | FOO 10 | % 11 | <<-FOO 12 | foo 13 | #{<<-BAR} 14 | bar 15 | BAR 16 | FOO 17 | % 18 | <<-FOO 19 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 20 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 21 | #{foo} 22 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 23 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 24 | FOO 25 | % 26 | def foo 27 | <<~FOO.strip 28 | foo 29 | FOO 30 | end 31 | % 32 | <<~FOO 33 | bar 34 | FOO 35 | % 36 | <<~FOO 37 | bar 38 | #{baz} 39 | FOO 40 | % 41 | <<~FOO 42 | foo 43 | #{<<~BAR} 44 | bar 45 | BAR 46 | FOO 47 | % 48 | <<~FOO 49 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 50 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 51 | #{foo} 52 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 53 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 54 | FOO 55 | % 56 | def foo 57 | <<~FOO.strip 58 | foo 59 | FOO 60 | end 61 | % 62 | call(foo, bar, baz, <<~FOO) 63 | foo 64 | FOO 65 | % 66 | call(foo, bar, baz, <<~FOO, <<~BAR) 67 | foo 68 | FOO 69 | bar 70 | BAR 71 | % 72 | command foo, bar, baz, <<~FOO 73 | foo 74 | FOO 75 | % 76 | command foo, bar, baz, <<~FOO, <<~BAR 77 | foo 78 | FOO 79 | bar 80 | BAR 81 | % 82 | command.call foo, bar, baz, <<~FOO 83 | foo 84 | FOO 85 | % 86 | command.call foo, bar, baz, <<~FOO, <<~BAR 87 | foo 88 | FOO 89 | bar 90 | BAR 91 | % 92 | foo = <<~FOO.strip 93 | foo 94 | FOO 95 | % 96 | foo( 97 | <<~FOO, 98 | foo 99 | FOO 100 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo: 101 | :bar 102 | ) 103 | % 104 | foo(<<~FOO 105 | foo 106 | FOO 107 | ) { "foo" } 108 | - 109 | foo(<<~FOO) { "foo" } 110 | foo 111 | FOO 112 | -------------------------------------------------------------------------------- /test/fixtures/heredoc_beg.rb: -------------------------------------------------------------------------------- 1 | % 2 | <<-FOO 3 | FOO 4 | % 5 | <<~FOO 6 | FOO 7 | % 8 | <<-`FOO` 9 | FOO 10 | % 11 | <<-FOO.strip 12 | FOO 13 | % 14 | <<~FOO.strip 15 | FOO 16 | % 17 | <<-`FOO`.strip 18 | FOO 19 | -------------------------------------------------------------------------------- /test/fixtures/hshptn.rb: -------------------------------------------------------------------------------- 1 | % 2 | case foo 3 | in ** then 4 | end 5 | % 6 | case foo 7 | in bar: 8 | end 9 | % 10 | case foo 11 | in bar: bar 12 | end 13 | % 14 | case foo 15 | in bar:, baz: 16 | end 17 | - 18 | case foo 19 | in { bar:, baz: } 20 | end 21 | % 22 | case foo 23 | in bar: bar, baz: baz 24 | end 25 | - 26 | case foo 27 | in { bar: bar, baz: baz } 28 | end 29 | % 30 | case foo 31 | in **bar 32 | end 33 | % # >= 2.7.3 34 | case foo 35 | in { 36 | foo:, # comment1 37 | bar: # comment2 38 | } 39 | baz 40 | end 41 | % 42 | case foo 43 | in Foo[bar:] 44 | end 45 | % 46 | case foo 47 | in Foo[bar: bar] 48 | end 49 | % 50 | case foo 51 | in Foo[bar:, baz:] 52 | end 53 | % 54 | case foo 55 | in Foo[bar: bar, baz: baz] 56 | end 57 | % 58 | case foo 59 | in Foo[**bar] 60 | end 61 | % 62 | case foo 63 | in {} 64 | end 65 | % 66 | case foo 67 | in {} then 68 | end 69 | - 70 | case foo 71 | in {} 72 | end 73 | % 74 | case foo 75 | in **nil 76 | end 77 | % 78 | case foo 79 | in bar, { baz:, **nil } 80 | in qux: 81 | end 82 | - 83 | case foo 84 | in [bar, { baz:, **nil }] 85 | in qux: 86 | end 87 | -------------------------------------------------------------------------------- /test/fixtures/ident.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo 3 | -------------------------------------------------------------------------------- /test/fixtures/if.rb: -------------------------------------------------------------------------------- 1 | % 2 | if foo 3 | end 4 | % 5 | if foo 6 | else 7 | end 8 | % 9 | if foo 10 | bar 11 | end 12 | - 13 | bar if foo 14 | % 15 | if foo 16 | bar 17 | else 18 | end 19 | % 20 | foo = if bar then baz end 21 | - 22 | foo = (baz if bar) 23 | % 24 | if foo += 1 25 | foo 26 | end 27 | % 28 | if (foo += 1) 29 | foo 30 | end 31 | % 32 | if foo 33 | a ? b : c 34 | end 35 | % 36 | if foo {} 37 | end 38 | % 39 | if not a 40 | b 41 | else 42 | c 43 | end 44 | % 45 | if not(a) 46 | b 47 | else 48 | c 49 | end 50 | - 51 | not(a) ? b : c 52 | % 53 | (if foo then bar else baz end) 54 | - 55 | ( 56 | if foo 57 | bar 58 | else 59 | baz 60 | end 61 | ) 62 | % 63 | if (x = x + 1).to_i 64 | x 65 | end 66 | % 67 | if true # comment1 68 | # comment2 69 | end 70 | % 71 | result = 72 | if false && val = 1 73 | "A" 74 | else 75 | "B" 76 | end 77 | -------------------------------------------------------------------------------- /test/fixtures/if_mod.rb: -------------------------------------------------------------------------------- 1 | % 2 | bar if foo 3 | % 4 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr if foo 5 | - 6 | if foo 7 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 8 | end 9 | % 10 | bar if foo # comment 11 | % 12 | foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr if foo 13 | - 14 | foo = 15 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr if foo 16 | -------------------------------------------------------------------------------- /test/fixtures/ifop.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo ? bar : baz 3 | % 4 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ? bar : baz 5 | - 6 | if foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 7 | bar 8 | else 9 | baz 10 | end 11 | % 12 | foo bar ? 1 : 2 13 | % 14 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ? break : baz 15 | - 16 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ? 17 | break : 18 | baz 19 | -------------------------------------------------------------------------------- /test/fixtures/imaginary.rb: -------------------------------------------------------------------------------- 1 | % 2 | 1i 3 | % 4 | 1i # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/in.rb: -------------------------------------------------------------------------------- 1 | % 2 | case foo 3 | in foo 4 | end 5 | % 6 | case foo 7 | in foo 8 | baz 9 | end 10 | % 11 | case foo 12 | in fooooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 13 | baz 14 | end 15 | - 16 | case foo 17 | in [ 18 | fooooooooooooooooooooooooooooooooooooo, 19 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 20 | ] 21 | baz 22 | end 23 | % 24 | case foo 25 | in foo 26 | in bar 27 | end 28 | % 29 | case foo 30 | in bar 31 | # comment 32 | end 33 | % 34 | case foo 35 | in bar if baz 36 | end 37 | -------------------------------------------------------------------------------- /test/fixtures/int.rb: -------------------------------------------------------------------------------- 1 | % 2 | 1 3 | % 4 | 1 # comment 5 | % 6 | 12345 7 | - 8 | 12_345 9 | % 10 | 2020_01_01 11 | % 12 | 0b11111 13 | -------------------------------------------------------------------------------- /test/fixtures/ivar.rb: -------------------------------------------------------------------------------- 1 | % 2 | @foo 3 | % 4 | @foo # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/kw.rb: -------------------------------------------------------------------------------- 1 | % 2 | :if 3 | % 4 | def if 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/kwrest_param.rb: -------------------------------------------------------------------------------- 1 | % 2 | def foo(**bar) 3 | end 4 | % 5 | def foo(**) 6 | end 7 | % 8 | def foo( 9 | **bar # comment 10 | ) 11 | end 12 | % 13 | def foo( 14 | ** # comment 15 | ) 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/label.rb: -------------------------------------------------------------------------------- 1 | % 2 | { foo: bar } 3 | % 4 | case foo 5 | in bar: 6 | end 7 | -------------------------------------------------------------------------------- /test/fixtures/lambda.rb: -------------------------------------------------------------------------------- 1 | % 2 | -> {} 3 | % 4 | -> { foo } 5 | % 6 | ->(foo, bar) { baz } 7 | % 8 | -> foo { bar } 9 | - 10 | ->(foo) { bar } 11 | % 12 | -> () { foo } 13 | - 14 | -> { foo } 15 | % 16 | -> { fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } 17 | - 18 | -> do 19 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 20 | end 21 | % 22 | ->(foo) { foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo } 23 | - 24 | ->(foo) do 25 | foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 26 | end 27 | % 28 | command foo, ->(bar) { bar } 29 | % 30 | command foo, ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } 31 | - 32 | command foo, 33 | ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } 34 | % 35 | command.call foo, ->(bar) { bar } 36 | % 37 | command.call foo, ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } 38 | - 39 | command.call foo, 40 | ->(bar) { barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr } 41 | % 42 | -> { -> foo do bar end.baz }.qux 43 | - 44 | -> { ->(foo) { bar }.baz }.qux 45 | % 46 | ->(;a) {} 47 | - 48 | ->(; a) {} 49 | % 50 | ->(; a) {} 51 | % 52 | ->(; a,b) {} 53 | - 54 | ->(; a, b) {} 55 | % 56 | ->(; a, b) {} 57 | % 58 | ->(; 59 | a 60 | ) {} 61 | - 62 | ->(; a) {} 63 | % 64 | ->(; a , 65 | b 66 | ) {} 67 | - 68 | ->(; a, b) {} 69 | % 70 | ->(a = (b; c)) {} 71 | - 72 | ->( 73 | a = ( 74 | b 75 | c 76 | ) 77 | ) do 78 | end 79 | % 80 | -> do # comment1 81 | # comment2 82 | end 83 | -------------------------------------------------------------------------------- /test/fixtures/massign.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo, bar = baz, qux 3 | % 4 | foo, bar, = baz, qux 5 | -------------------------------------------------------------------------------- /test/fixtures/method_add_arg.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo(bar) 3 | % 4 | foo.bar(baz) 5 | % 6 | foo.() 7 | % 8 | foo? 9 | -------------------------------------------------------------------------------- /test/fixtures/method_add_block.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo {} 3 | -------------------------------------------------------------------------------- /test/fixtures/mlhs.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo, bar = baz 3 | % 4 | foo, bar, = baz 5 | % 6 | foo, *bar, baz = baz 7 | % 8 | foo, *bar, baz = baz 9 | % 10 | foo1, foo2, *bar, baz1, baz2 = baz 11 | -------------------------------------------------------------------------------- /test/fixtures/mlhs_paren.rb: -------------------------------------------------------------------------------- 1 | % 2 | (foo, bar) = baz 3 | - 4 | foo, bar = baz 5 | % 6 | foo, (bar, baz) = baz 7 | % 8 | (foo, bar), baz = baz 9 | % 10 | foo, (bar, baz,) = baz 11 | % 12 | ((foo,)) = bar 13 | - 14 | foo, = bar 15 | -------------------------------------------------------------------------------- /test/fixtures/module.rb: -------------------------------------------------------------------------------- 1 | % 2 | module Foo 3 | end 4 | % 5 | module Foo 6 | foo 7 | end 8 | % 9 | module Foo 10 | # comment 11 | end 12 | % 13 | module Foo # comment 14 | end 15 | % 16 | module Foo 17 | module Bar 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/fixtures/mrhs.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo = bar, baz 3 | % 4 | foo = bar, *baz, qux 5 | % 6 | foo = *bar, baz 7 | % 8 | foo = bar, *baz 9 | -------------------------------------------------------------------------------- /test/fixtures/next.rb: -------------------------------------------------------------------------------- 1 | % 2 | next 3 | % 4 | next foo 5 | % 6 | next foo, bar 7 | % 8 | next(foo) 9 | % 10 | next fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 11 | - 12 | next( 13 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 14 | ) 15 | % 16 | next(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) 17 | - 18 | next( 19 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 20 | ) 21 | % 22 | next (foo), bar 23 | % 24 | next( 25 | foo 26 | bar 27 | ) 28 | % 29 | next(1) 30 | - 31 | next 1 32 | % 33 | next(1.0) 34 | - 35 | next 1.0 36 | % 37 | next($a) 38 | - 39 | next $a 40 | % 41 | next(@@a) 42 | - 43 | next @@a 44 | % 45 | next(self) 46 | - 47 | next self 48 | % 49 | next(@a) 50 | - 51 | next @a 52 | % 53 | next(A) 54 | - 55 | next A 56 | % 57 | next([]) 58 | - 59 | next [] 60 | % 61 | next([1]) 62 | - 63 | next [1] 64 | % 65 | next([1, 2]) 66 | - 67 | next 1, 2 68 | % 69 | next fun foo do end 70 | - 71 | next( 72 | fun foo do 73 | end 74 | ) 75 | % 76 | next :foo => "bar" 77 | -------------------------------------------------------------------------------- /test/fixtures/not.rb: -------------------------------------------------------------------------------- 1 | % 2 | not() 3 | % 4 | not () 5 | % 6 | not foo 7 | % 8 | not(foo) 9 | % 10 | not (foo) 11 | % 12 | if foo 13 | not bar 14 | else 15 | baz 16 | end 17 | - 18 | foo ? not(bar) : baz 19 | % 20 | if foooooooooooooooooooooooooooooooooooooooooo 21 | not bar 22 | else 23 | bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 24 | end 25 | -------------------------------------------------------------------------------- /test/fixtures/op.rb: -------------------------------------------------------------------------------- 1 | % 2 | def +(other) 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/opassign.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo += bar 3 | % 4 | foo += # comment 5 | bar 6 | -------------------------------------------------------------------------------- /test/fixtures/params.rb: -------------------------------------------------------------------------------- 1 | % 2 | def foo(req) 3 | end 4 | % 5 | def foo(req1, req2) 6 | end 7 | % 8 | def foo(optl = foo) 9 | end 10 | % 11 | def foo(optl1 = foo, optl2 = bar) 12 | end 13 | % 14 | def foo(*) 15 | end 16 | % 17 | def foo(*rest) 18 | end 19 | % # >= 2.7.3 20 | def foo(...) 21 | end 22 | % 23 | def foo(*, post) 24 | end 25 | % 26 | def foo(*, post1, post2) 27 | end 28 | % 29 | def foo(key:) 30 | end 31 | % 32 | def foo(key1:, key2:) 33 | end 34 | % 35 | def foo(key: foo) 36 | end 37 | % 38 | def foo(key1: foo, key2: bar) 39 | end 40 | % 41 | def foo(**) 42 | end 43 | % 44 | def foo(**kwrest) 45 | end 46 | % 47 | def foo(&block) 48 | end 49 | % 50 | def foo(req1, req2, optl = foo, *rest, key1:, key2: bar, **kwrest, &block) 51 | end 52 | % 53 | foo { |req| } 54 | % 55 | foo { |req1, req2| } 56 | % 57 | foo { |optl = foo| } 58 | % 59 | foo { |optl1 = foo, optl2 = bar| } 60 | % 61 | foo { |*| } 62 | % 63 | foo { |*rest| } 64 | % 65 | foo { |req,| } 66 | % 67 | foo { |*, post| } 68 | % 69 | foo { |*, post1, post2| } 70 | % 71 | foo { |key:| } 72 | % 73 | foo { |key1:, key2:| } 74 | % 75 | foo { |key: foo| } 76 | % 77 | foo { |key1: foo, key2: bar| } 78 | % 79 | foo { |**| } 80 | % 81 | foo { |**kwrest| } 82 | % 83 | foo { |&block| } 84 | % 85 | foo { |req1, req2, optl = foo, *rest, key1:, key2: bar, **kwrest, &block| } 86 | % 87 | foo do |foooooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrr| 88 | end 89 | -------------------------------------------------------------------------------- /test/fixtures/paren.rb: -------------------------------------------------------------------------------- 1 | % 2 | (foo + bar) 3 | % 4 | ( 5 | foo 6 | bar 7 | ) 8 | % 9 | (foo) 10 | -------------------------------------------------------------------------------- /test/fixtures/period.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo.bar 3 | -------------------------------------------------------------------------------- /test/fixtures/pinned_begin.rb: -------------------------------------------------------------------------------- 1 | % 2 | case value 3 | in ^(expression) 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/program.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo 3 | -------------------------------------------------------------------------------- /test/fixtures/qsymbols.rb: -------------------------------------------------------------------------------- 1 | % 2 | %i[foo bar] 3 | % 4 | %i[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] 5 | - 6 | %i[ 7 | fooooooooooooooooooooooooooooooooooooo 8 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 9 | ] 10 | % 11 | %i[ 12 | foo 13 | ] 14 | - 15 | %i[foo] 16 | % 17 | %i[foo] # comment 18 | % 19 | %i{foo[]} 20 | -------------------------------------------------------------------------------- /test/fixtures/qwords.rb: -------------------------------------------------------------------------------- 1 | % 2 | %w[foo bar] 3 | % 4 | %w[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] 5 | - 6 | %w[ 7 | fooooooooooooooooooooooooooooooooooooo 8 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 9 | ] 10 | % 11 | %w[ 12 | foo 13 | ] 14 | - 15 | %w[foo] 16 | % 17 | %w[foo] # comment 18 | % 19 | %w{foo[]} 20 | -------------------------------------------------------------------------------- /test/fixtures/rassign.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo in bar 3 | % 4 | foo => bar 5 | % 6 | foooooooooooooooooooooooooooooooooooooo in barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 7 | - 8 | foooooooooooooooooooooooooooooooooooooo in 9 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 10 | % 11 | foooooooooooooooooooooooooooooooooooooo => barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 12 | - 13 | foooooooooooooooooooooooooooooooooooooo => 14 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 15 | % 16 | foo => [ 17 | ConstantConstantConstant, 18 | ConstantConstantConstant, 19 | ConstantConstantConstant, 20 | ConstantConstantConstant, 21 | ConstantConstantConstant 22 | ] 23 | % 24 | a in Integer 25 | b => [Integer => c] 26 | % 27 | case [0] 28 | when 0 29 | { a: 0 } => { a: } 30 | puts a 31 | end 32 | -------------------------------------------------------------------------------- /test/fixtures/rational_literal.rb: -------------------------------------------------------------------------------- 1 | % 2 | 1r 3 | % 4 | 1r # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/redo.rb: -------------------------------------------------------------------------------- 1 | % 2 | redo 3 | % 4 | redo # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/regexp_literal.rb: -------------------------------------------------------------------------------- 1 | % 2 | /foo/ 3 | % 4 | %r{foo} 5 | - 6 | /foo/ 7 | % 8 | %r/foo/ 9 | - 10 | /foo/ 11 | % 12 | %r[foo] 13 | - 14 | /foo/ 15 | % 16 | %r(foo) 17 | - 18 | /foo/ 19 | % 20 | %r{foo/bar/baz} 21 | % 22 | /foo #{bar} baz/ 23 | % 24 | /foo/i 25 | % 26 | %r{foo/bar/baz}mi 27 | % 28 | /#$&/ 29 | - 30 | /#{$&}/ 31 | % 32 | %r(a{b/c}) 33 | % 34 | %r[a}b/c] 35 | % 36 | %r(a}bc) 37 | - 38 | /a}bc/ 39 | % 40 | /\\A 41 | [[:digit:]]+ # 1 or more digits before the decimal point 42 | (\\. # Decimal point 43 | [[:digit:]]+ # 1 or more digits after the decimal point 44 | )? # The decimal point and following digits are optional 45 | \\Z/x 46 | % 47 | foo %r{ bar} 48 | % 49 | foo %r{= bar} 50 | % 51 | foo(/ bar/) 52 | % 53 | /foo\/bar/ 54 | - 55 | %r{foo/bar} 56 | % 57 | /foo\/bar\/#{baz}/ 58 | - 59 | %r{foo/bar/#{baz}} 60 | -------------------------------------------------------------------------------- /test/fixtures/rescue.rb: -------------------------------------------------------------------------------- 1 | % 2 | begin 3 | rescue 4 | end 5 | - 6 | begin 7 | rescue StandardError 8 | end 9 | % 10 | begin 11 | rescue => foo 12 | bar 13 | end 14 | % 15 | begin 16 | rescue Foo 17 | bar 18 | end 19 | % 20 | begin 21 | rescue Foo => foo 22 | bar 23 | end 24 | % 25 | begin 26 | rescue Foo, Bar 27 | end 28 | % 29 | begin 30 | rescue Foo, *Bar 31 | end 32 | % 33 | begin 34 | rescue Foo, Bar => foo 35 | end 36 | % 37 | begin 38 | rescue Foo, *Bar => foo 39 | end 40 | % # https://github.com/prettier/plugin-ruby/pull/1000 41 | begin 42 | rescue ::Foo 43 | end 44 | % 45 | begin 46 | rescue Foo 47 | rescue Bar 48 | end 49 | % 50 | begin 51 | rescue Foo # comment 52 | end 53 | % 54 | begin 55 | rescue Foo, *Bar # comment 56 | end 57 | -------------------------------------------------------------------------------- /test/fixtures/rescue_mod.rb: -------------------------------------------------------------------------------- 1 | % 2 | bar rescue foo 3 | - 4 | begin 5 | bar 6 | rescue StandardError 7 | foo 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/rest_param.rb: -------------------------------------------------------------------------------- 1 | % 2 | def foo(*bar) 3 | end 4 | % 5 | def foo(*) 6 | end 7 | % 8 | def foo( 9 | *bar # comment 10 | ) 11 | end 12 | % 13 | def foo( 14 | * # comment 15 | ) 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/retry.rb: -------------------------------------------------------------------------------- 1 | % 2 | retry 3 | % 4 | retry # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/return.rb: -------------------------------------------------------------------------------- 1 | % 2 | return 3 | % 4 | return foo 5 | % 6 | return foo, bar 7 | % 8 | return(foo) 9 | % 10 | return fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 11 | - 12 | return( 13 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 14 | ) 15 | % 16 | return(fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo) 17 | - 18 | return( 19 | fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 20 | ) 21 | % 22 | return (foo), bar 23 | % 24 | return( 25 | foo 26 | bar 27 | ) 28 | % 29 | return([1, 2, 3]) 30 | - 31 | return 1, 2, 3 32 | % 33 | return [1, 2, 3] 34 | - 35 | return 1, 2, 3 36 | % 37 | return [] 38 | % 39 | return [1] 40 | % 41 | return :foo => "bar" 42 | -------------------------------------------------------------------------------- /test/fixtures/return0.rb: -------------------------------------------------------------------------------- 1 | % 2 | return 3 | % 4 | return # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/sclass.rb: -------------------------------------------------------------------------------- 1 | % 2 | class << self 3 | foo 4 | end 5 | % 6 | class << foo 7 | bar 8 | end 9 | % 10 | class << self # comment 11 | foo 12 | end 13 | % 14 | class << self 15 | # comment 16 | end 17 | % 18 | class << self 19 | # comment1 20 | # comment2 21 | end 22 | -------------------------------------------------------------------------------- /test/fixtures/statements.rb: -------------------------------------------------------------------------------- 1 | % 2 | # comment1 3 | # comment2 4 | % 5 | foo do 6 | # comment1 7 | # comment2 8 | end 9 | % 10 | foo 11 | 12 | 13 | bar 14 | - 15 | foo 16 | 17 | bar 18 | % 19 | foo; bar 20 | - 21 | foo 22 | bar 23 | % 24 | "#{foo; bar}" 25 | -------------------------------------------------------------------------------- /test/fixtures/string_concat.rb: -------------------------------------------------------------------------------- 1 | % 2 | "foo" \ 3 | "bar" \ 4 | "baz" 5 | -------------------------------------------------------------------------------- /test/fixtures/string_dvar.rb: -------------------------------------------------------------------------------- 1 | % 2 | "#@foo" 3 | - 4 | "#{@foo}" 5 | % 6 | "#@foo" # comment 7 | - 8 | "#{@foo}" # comment 9 | -------------------------------------------------------------------------------- /test/fixtures/string_embexpr.rb: -------------------------------------------------------------------------------- 1 | % 2 | "foo #{bar}" 3 | % 4 | "foo #{super}" 5 | % 6 | "#{bar} foo" 7 | % 8 | "foo #{"bar #{baz} bar"} foo" 9 | % 10 | "#{foo; bar}" 11 | % 12 | "#{if foo; foooooooooooooooooooooooooooooooooooooo; else; barrrrrrrrrrrrrrrr; end}" 13 | -------------------------------------------------------------------------------- /test/fixtures/string_literal.rb: -------------------------------------------------------------------------------- 1 | % 2 | %(foo \\ bar) 3 | % 4 | %[foo \\ bar] 5 | % 6 | %{foo \\ bar} 7 | % 8 | % 9 | % 10 | %|foo \\ bar| 11 | % 12 | %q(foo \\ bar) 13 | % 14 | %q[foo \\ bar] 15 | % 16 | %q{foo \\ bar} 17 | % 18 | %q 19 | % 20 | %q|foo \\ bar| 21 | % 22 | %Q(foo \\ bar) 23 | % 24 | %Q[foo \\ bar] 25 | % 26 | %Q{foo \\ bar} 27 | % 28 | %Q 29 | % 30 | %Q|foo \\ bar| 31 | % 32 | '' 33 | - 34 | "" 35 | % 36 | 'foo' 37 | - 38 | "foo" 39 | % 40 | 'foo #{bar}' 41 | % 42 | '"foo"' 43 | - 44 | '"foo"' 45 | % 46 | "'foo'" 47 | - 48 | "'foo'" 49 | -------------------------------------------------------------------------------- /test/fixtures/super.rb: -------------------------------------------------------------------------------- 1 | % 2 | super() 3 | % 4 | super foo 5 | % 6 | super(foo) 7 | % 8 | super foo, bar 9 | % 10 | super(foo, bar) 11 | % 12 | super() # comment 13 | % 14 | super foo # comment 15 | % 16 | super(foo) # comment 17 | % 18 | super foo, bar # comment 19 | % 20 | super(foo, bar) # comment 21 | % 22 | super foo, # comment1 23 | bar # comment2 24 | % 25 | super( 26 | foo, # comment1 27 | bar # comment2 28 | ) 29 | -------------------------------------------------------------------------------- /test/fixtures/symbol_literal.rb: -------------------------------------------------------------------------------- 1 | % 2 | :foo 3 | % 4 | :foo # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/symbols.rb: -------------------------------------------------------------------------------- 1 | % 2 | %I[foo bar] 3 | % 4 | %I[foo #{bar}] 5 | % 6 | %I[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] 7 | - 8 | %I[ 9 | fooooooooooooooooooooooooooooooooooooo 10 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 11 | ] 12 | % 13 | %I[ 14 | foo 15 | ] 16 | - 17 | %I[foo] 18 | % 19 | %I[foo] # comment 20 | % 21 | %I{foo[]} 22 | % 23 | :\ 24 | =begin 25 | =end 26 | symbol 27 | -------------------------------------------------------------------------------- /test/fixtures/top_const_field.rb: -------------------------------------------------------------------------------- 1 | % 2 | ::Foo = baz 3 | -------------------------------------------------------------------------------- /test/fixtures/top_const_ref.rb: -------------------------------------------------------------------------------- 1 | % 2 | ::Foo 3 | -------------------------------------------------------------------------------- /test/fixtures/tstring_content.rb: -------------------------------------------------------------------------------- 1 | % 2 | "foo" 3 | -------------------------------------------------------------------------------- /test/fixtures/unary.rb: -------------------------------------------------------------------------------- 1 | % 2 | !foo 3 | % # https://github.com/prettier/plugin-ruby/issues/764 4 | !(foo&.>(0)) 5 | -------------------------------------------------------------------------------- /test/fixtures/undef.rb: -------------------------------------------------------------------------------- 1 | % 2 | undef foo 3 | % 4 | undef foo, bar 5 | % 6 | undef foooooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 7 | - 8 | undef foooooooooooooooooooooooooooooooooooooo, 9 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 10 | % 11 | undef foo # comment 12 | % 13 | undef foo, # comment 14 | bar 15 | % 16 | undef foo, # comment1 17 | bar, # comment2 18 | baz 19 | % 20 | undef foo, 21 | bar # comment 22 | - 23 | undef foo, bar # comment 24 | % 25 | undef :"foo", :"bar" 26 | -------------------------------------------------------------------------------- /test/fixtures/unless.rb: -------------------------------------------------------------------------------- 1 | % 2 | unless foo 3 | end 4 | % 5 | unless foo 6 | else 7 | end 8 | % 9 | unless foo 10 | bar 11 | end 12 | - 13 | bar unless foo 14 | % 15 | unless foo 16 | bar 17 | else 18 | end 19 | % 20 | foo = unless bar then baz end 21 | - 22 | foo = (baz unless bar) 23 | % 24 | unless foo += 1 25 | foo 26 | end 27 | % 28 | unless (foo += 1) 29 | foo 30 | end 31 | % 32 | unless foo 33 | a ? b : c 34 | end 35 | % 36 | unless true # comment1 37 | # comment2 38 | end 39 | -------------------------------------------------------------------------------- /test/fixtures/unless_mod.rb: -------------------------------------------------------------------------------- 1 | % 2 | bar unless foo 3 | % 4 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr unless foo 5 | - 6 | unless foo 7 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 8 | end 9 | % 10 | bar unless foo # comment 11 | % 12 | foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr unless foo 13 | - 14 | foo = 15 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr unless foo 16 | -------------------------------------------------------------------------------- /test/fixtures/until.rb: -------------------------------------------------------------------------------- 1 | % 2 | until foo 3 | end 4 | % 5 | until foo 6 | bar 7 | end 8 | - 9 | bar until foo 10 | % 11 | until fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 12 | bar 13 | end 14 | % 15 | foo = until bar do baz end 16 | - 17 | foo = (baz until bar) 18 | % 19 | until foo += 1 20 | foo 21 | end 22 | % 23 | until (foo += 1) 24 | foo 25 | end 26 | % 27 | until true # comment1 28 | # comment2 29 | end 30 | -------------------------------------------------------------------------------- /test/fixtures/until_mod.rb: -------------------------------------------------------------------------------- 1 | % 2 | bar until foo 3 | % 4 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr until foo 5 | - 6 | until foo 7 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 8 | end 9 | % 10 | bar until foo # comment 11 | % 12 | foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr until foo 13 | - 14 | foo = 15 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr until foo 16 | -------------------------------------------------------------------------------- /test/fixtures/var_alias.rb: -------------------------------------------------------------------------------- 1 | % 2 | alias $1 $foo 3 | % 4 | alias $foo $bar 5 | % 6 | alias $1 $foo # comment 7 | % 8 | alias $foo $bar # comment 9 | -------------------------------------------------------------------------------- /test/fixtures/var_field.rb: -------------------------------------------------------------------------------- 1 | % 2 | Foo = bar 3 | % 4 | @@foo = bar 5 | % 6 | $foo = bar 7 | % 8 | foo = bar 9 | % 10 | @foo = bar 11 | -------------------------------------------------------------------------------- /test/fixtures/var_field_rassign.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo in bar 3 | % 4 | foo in ^bar 5 | % 6 | foo in ^@bar 7 | % 8 | foo in ^@@bar 9 | % 10 | foo in ^$gvar 11 | -------------------------------------------------------------------------------- /test/fixtures/var_ref.rb: -------------------------------------------------------------------------------- 1 | % 2 | Foo 3 | % 4 | @@foo 5 | % 6 | $foo 7 | % 8 | foo 9 | % 10 | @foo 11 | % 12 | self 13 | % 14 | true 15 | % 16 | false 17 | % 18 | nil 19 | -------------------------------------------------------------------------------- /test/fixtures/vcall.rb: -------------------------------------------------------------------------------- 1 | % 2 | foo 3 | % 4 | foo # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/void_stmt.rb: -------------------------------------------------------------------------------- 1 | % 2 | ;;; 3 | - 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/when.rb: -------------------------------------------------------------------------------- 1 | % 2 | case 3 | when foo 4 | end 5 | % 6 | case 7 | when foo, bar 8 | baz 9 | end 10 | % 11 | case 12 | when foooooooooooooooooooooooooooooooooooo, barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 13 | baz 14 | end 15 | - 16 | case 17 | when foooooooooooooooooooooooooooooooooooo, 18 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 19 | baz 20 | end 21 | % 22 | case 23 | when foo then bar 24 | end 25 | - 26 | case 27 | when foo 28 | bar 29 | end 30 | % 31 | case 32 | when foooooooooooooooooo, barrrrrrrrrrrrrrrrrr, bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 33 | end 34 | - 35 | case 36 | when foooooooooooooooooo, barrrrrrrrrrrrrrrrrr, 37 | bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 38 | end 39 | % 40 | case 41 | when foo 42 | when bar 43 | end 44 | % 45 | case 46 | when foo 47 | else 48 | end 49 | % 50 | case 51 | when foo.. then 52 | end 53 | % 54 | case 55 | when foo... then 56 | end 57 | % 58 | case 59 | when foo # comment 60 | end 61 | -------------------------------------------------------------------------------- /test/fixtures/while.rb: -------------------------------------------------------------------------------- 1 | % 2 | while foo 3 | end 4 | % 5 | while foo 6 | bar 7 | end 8 | - 9 | bar while foo 10 | % 11 | while fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo 12 | bar 13 | end 14 | % 15 | foo = while bar do baz end 16 | - 17 | foo = (baz while bar) 18 | % 19 | while foo += 1 20 | foo 21 | end 22 | % 23 | while (foo += 1) 24 | foo 25 | end 26 | % 27 | while true # comment1 28 | # comment2 29 | end 30 | -------------------------------------------------------------------------------- /test/fixtures/while_mod.rb: -------------------------------------------------------------------------------- 1 | % 2 | bar while foo 3 | % 4 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr while foo 5 | - 6 | while foo 7 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 8 | end 9 | % 10 | bar while foo # comment 11 | % 12 | foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr while foo 13 | - 14 | foo = 15 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr while foo 16 | -------------------------------------------------------------------------------- /test/fixtures/word.rb: -------------------------------------------------------------------------------- 1 | % 2 | %W[foo] 3 | % 4 | %W[foo\ bar] 5 | % 6 | %W[foo#{bar}baz] 7 | -------------------------------------------------------------------------------- /test/fixtures/words.rb: -------------------------------------------------------------------------------- 1 | % 2 | %W[foo bar] 3 | % 4 | %W[foo #{bar}] 5 | % 6 | %W[fooooooooooooooooooooooooooooooooooooo barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr] 7 | - 8 | %W[ 9 | fooooooooooooooooooooooooooooooooooooo 10 | barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr 11 | ] 12 | % 13 | %W[ 14 | foo 15 | ] 16 | - 17 | %W[foo] 18 | % 19 | %W[foo] # comment 20 | % 21 | %W{foo[]} 22 | -------------------------------------------------------------------------------- /test/fixtures/xstring_literal.rb: -------------------------------------------------------------------------------- 1 | % 2 | `foo` 3 | % 4 | `foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo` 5 | % 6 | `foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo`.to_s 7 | % 8 | %x[foo] 9 | - 10 | `foo` 11 | % 12 | %x[foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo] 13 | - 14 | `foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo` 15 | % 16 | %x[foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo].to_s 17 | - 18 | `foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo`.to_s 19 | % 20 | `foo` # comment 21 | -------------------------------------------------------------------------------- /test/fixtures/yield.rb: -------------------------------------------------------------------------------- 1 | % 2 | yield foo 3 | % 4 | yield(foo) 5 | % 6 | yield foo, bar 7 | % 8 | yield(foo, bar) 9 | % 10 | yield foo # comment 11 | % 12 | yield(foo) # comment 13 | % 14 | yield( # comment 15 | foo 16 | ) 17 | -------------------------------------------------------------------------------- /test/fixtures/yield0.rb: -------------------------------------------------------------------------------- 1 | % 2 | yield 3 | % 4 | yield # comment 5 | -------------------------------------------------------------------------------- /test/fixtures/zsuper.rb: -------------------------------------------------------------------------------- 1 | % 2 | super 3 | % 4 | super # comment 5 | -------------------------------------------------------------------------------- /test/formatting_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class FormattingTest < Minitest::Test 7 | Fixtures.each_fixture do |fixture| 8 | define_method(:"test_formatted_#{fixture.name}") do 9 | assert_equal(fixture.formatted, SyntaxTree.format(fixture.source)) 10 | assert_syntax_tree(SyntaxTree.parse(fixture.source)) 11 | end 12 | end 13 | 14 | def test_format_class_level 15 | source = "1+1" 16 | 17 | assert_equal( 18 | "1 + 1\n", 19 | Formatter.format(source, SyntaxTree.parse(source)) 20 | ) 21 | end 22 | 23 | def test_stree_ignore 24 | source = <<~SOURCE 25 | # stree-ignore 26 | 1+1 27 | SOURCE 28 | 29 | assert_equal(source, SyntaxTree.format(source)) 30 | end 31 | 32 | def test_formatting_with_different_indentation_level 33 | source = <<~SOURCE 34 | def foo 35 | puts "a" 36 | end 37 | SOURCE 38 | 39 | # Default indentation 40 | assert_equal(source, SyntaxTree.format(source)) 41 | 42 | # Level 2 43 | assert_equal(<<-EXPECTED.chomp, SyntaxTree.format(source, 80, 2).rstrip) 44 | def foo 45 | puts "a" 46 | end 47 | EXPECTED 48 | 49 | # Level 4 50 | assert_equal(<<-EXPECTED.chomp, SyntaxTree.format(source, 80, 4).rstrip) 51 | def foo 52 | puts "a" 53 | end 54 | EXPECTED 55 | 56 | # Level 6 57 | assert_equal(<<-EXPECTED.chomp, SyntaxTree.format(source, 80, 6).rstrip) 58 | def foo 59 | puts "a" 60 | end 61 | EXPECTED 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/idempotency_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | return if !ENV["CI"] || RUBY_ENGINE == "truffleruby" 4 | require_relative "test_helper" 5 | 6 | module SyntaxTree 7 | class IdempotencyTest < Minitest::Test 8 | Dir[File.join(RbConfig::CONFIG["libdir"], "**/*.rb")].each do |filepath| 9 | define_method(:"test_#{filepath}") do 10 | source = SyntaxTree.read(filepath) 11 | formatted = SyntaxTree.format(source) 12 | 13 | assert_equal( 14 | formatted, 15 | SyntaxTree.format(formatted), 16 | "expected #{filepath} to be formatted idempotently" 17 | ) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/index_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class IndexTest < Minitest::Test 7 | def test_module 8 | index_each("module Foo; end") do |entry| 9 | assert_equal :Foo, entry.name 10 | assert_equal [[:Foo]], entry.nesting 11 | end 12 | end 13 | 14 | def test_module_nested 15 | index_each("module Foo; module Bar; end; end") do |entry| 16 | assert_equal :Bar, entry.name 17 | assert_equal [[:Foo], [:Bar]], entry.nesting 18 | end 19 | end 20 | 21 | def test_module_comments 22 | index_each("# comment1\n# comment2\nmodule Foo; end") do |entry| 23 | assert_equal :Foo, entry.name 24 | assert_equal ["# comment1", "# comment2"], entry.comments.to_a 25 | end 26 | end 27 | 28 | def test_class 29 | index_each("class Foo; end") do |entry| 30 | assert_equal :Foo, entry.name 31 | assert_equal [[:Foo]], entry.nesting 32 | end 33 | end 34 | 35 | def test_class_paths_2 36 | index_each("class Foo::Bar; end") do |entry| 37 | assert_equal :Bar, entry.name 38 | assert_equal [%i[Foo Bar]], entry.nesting 39 | end 40 | end 41 | 42 | def test_class_paths_3 43 | index_each("class Foo::Bar::Baz; end") do |entry| 44 | assert_equal :Baz, entry.name 45 | assert_equal [%i[Foo Bar Baz]], entry.nesting 46 | end 47 | end 48 | 49 | def test_class_nested 50 | index_each("class Foo; class Bar; end; end") do |entry| 51 | assert_equal :Bar, entry.name 52 | assert_equal [[:Foo], [:Bar]], entry.nesting 53 | end 54 | end 55 | 56 | def test_class_paths_nested 57 | index_each("class Foo; class Bar::Baz::Qux; end; end") do |entry| 58 | assert_equal :Qux, entry.name 59 | assert_equal [[:Foo], %i[Bar Baz Qux]], entry.nesting 60 | end 61 | end 62 | 63 | def test_class_superclass 64 | index_each("class Foo < Bar; end") do |entry| 65 | assert_equal :Foo, entry.name 66 | assert_equal [[:Foo]], entry.nesting 67 | assert_equal [:Bar], entry.superclass 68 | end 69 | end 70 | 71 | def test_class_path_superclass 72 | index_each("class Foo::Bar < Baz::Qux; end") do |entry| 73 | assert_equal :Bar, entry.name 74 | assert_equal [%i[Foo Bar]], entry.nesting 75 | assert_equal %i[Baz Qux], entry.superclass 76 | end 77 | end 78 | 79 | def test_class_comments 80 | index_each("# comment1\n# comment2\nclass Foo; end") do |entry| 81 | assert_equal :Foo, entry.name 82 | assert_equal ["# comment1", "# comment2"], entry.comments.to_a 83 | end 84 | end 85 | 86 | def test_method 87 | index_each("def foo; end") do |entry| 88 | assert_equal :foo, entry.name 89 | assert_empty entry.nesting 90 | end 91 | end 92 | 93 | def test_method_nested 94 | index_each("class Foo; def foo; end; end") do |entry| 95 | assert_equal :foo, entry.name 96 | assert_equal [[:Foo]], entry.nesting 97 | end 98 | end 99 | 100 | def test_method_comments 101 | index_each("# comment1\n# comment2\ndef foo; end") do |entry| 102 | assert_equal :foo, entry.name 103 | assert_equal ["# comment1", "# comment2"], entry.comments.to_a 104 | end 105 | end 106 | 107 | def test_singleton_method 108 | index_each("def self.foo; end") do |entry| 109 | assert_equal :foo, entry.name 110 | assert_empty entry.nesting 111 | end 112 | end 113 | 114 | def test_singleton_method_nested 115 | index_each("class Foo; def self.foo; end; end") do |entry| 116 | assert_equal :foo, entry.name 117 | assert_equal [[:Foo]], entry.nesting 118 | end 119 | end 120 | 121 | def test_singleton_method_comments 122 | index_each("# comment1\n# comment2\ndef self.foo; end") do |entry| 123 | assert_equal :foo, entry.name 124 | assert_equal ["# comment1", "# comment2"], entry.comments.to_a 125 | end 126 | end 127 | 128 | def test_alias_method 129 | index_each("alias foo bar") do |entry| 130 | assert_equal :foo, entry.name 131 | assert_empty entry.nesting 132 | end 133 | end 134 | 135 | def test_attr_reader 136 | index_each("attr_reader :foo") do |entry| 137 | assert_equal :foo, entry.name 138 | assert_empty entry.nesting 139 | end 140 | end 141 | 142 | def test_attr_writer 143 | index_each("attr_writer :foo") do |entry| 144 | assert_equal :foo=, entry.name 145 | assert_empty entry.nesting 146 | end 147 | end 148 | 149 | def test_attr_accessor 150 | index_each("attr_accessor :foo") do |entry| 151 | assert_equal :foo=, entry.name 152 | assert_empty entry.nesting 153 | end 154 | end 155 | 156 | def test_constant 157 | index_each("FOO = 1") do |entry| 158 | assert_equal :FOO, entry.name 159 | assert_empty entry.nesting 160 | end 161 | end 162 | 163 | def test_this_file 164 | entries = Index.index_file(__FILE__, backend: Index::ParserBackend.new) 165 | 166 | if defined?(RubyVM::InstructionSequence) 167 | entries += Index.index_file(__FILE__, backend: Index::ISeqBackend.new) 168 | end 169 | 170 | entries.map { |entry| entry.comments.to_a } 171 | end 172 | 173 | private 174 | 175 | def index_each(source) 176 | yield Index.index(source, backend: Index::ParserBackend.new).last 177 | 178 | if defined?(RubyVM::InstructionSequence) 179 | yield Index.index(source, backend: Index::ISeqBackend.new).last 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /test/language_server/inlay_hints_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | require "syntax_tree/language_server" 5 | 6 | module SyntaxTree 7 | class LanguageServer 8 | class InlayHintsTest < Minitest::Test 9 | def test_assignments_in_parameters 10 | assert_hints(2, "def foo(a = b = c); end") 11 | end 12 | 13 | def test_operators_in_binaries 14 | assert_hints(2, "1 + 2 * 3") 15 | end 16 | 17 | def test_binaries_in_assignments 18 | assert_hints(2, "a = 1 + 2") 19 | end 20 | 21 | def test_nested_ternaries 22 | assert_hints(2, "a ? b : c ? d : e") 23 | end 24 | 25 | def test_bare_rescue 26 | assert_hints(1, "begin; rescue; end") 27 | end 28 | 29 | def test_unary_in_binary 30 | assert_hints(2, "-a + b") 31 | end 32 | 33 | private 34 | 35 | def assert_hints(expected, source) 36 | visitor = InlayHints.new 37 | SyntaxTree.parse(source).accept(visitor) 38 | 39 | assert_equal(expected, visitor.hints.length) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/location_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class LocationTest < Minitest::Test 7 | def test_lines 8 | location = Location.fixed(line: 1, char: 0, column: 0) 9 | location = location.to(Location.fixed(line: 3, char: 3, column: 3)) 10 | 11 | assert_equal(1..3, location.lines) 12 | end 13 | 14 | def test_deconstruct 15 | location = Location.fixed(line: 1, char: 0, column: 0) 16 | 17 | assert_equal(1, location.start_line) 18 | assert_equal(0, location.start_char) 19 | assert_equal(0, location.start_column) 20 | end 21 | 22 | def test_deconstruct_keys 23 | location = Location.fixed(line: 1, char: 0, column: 0) 24 | 25 | assert_equal(1, location.start_line) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/mutation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class MutationTest < Minitest::Test 7 | def test_mutates_based_on_patterns 8 | source = <<~RUBY 9 | if a = b 10 | c 11 | end 12 | RUBY 13 | 14 | expected = <<~RUBY 15 | if (a = b) 16 | c 17 | end 18 | RUBY 19 | 20 | program = SyntaxTree.parse(source).accept(build_mutation) 21 | assert_equal(expected, SyntaxTree::Formatter.format(source, program)) 22 | end 23 | 24 | private 25 | 26 | def build_mutation 27 | SyntaxTree.mutation do |mutation| 28 | mutation.mutate("IfNode[predicate: Assign | OpAssign]") do |node| 29 | # Get the existing If's predicate node 30 | predicate = node.predicate 31 | 32 | # Create a new predicate node that wraps the existing predicate node 33 | # in parentheses 34 | predicate = 35 | SyntaxTree::Paren.new( 36 | lparen: SyntaxTree::LParen.default, 37 | contents: predicate, 38 | location: predicate.location 39 | ) 40 | 41 | # Return a copy of this node with the new predicate 42 | node.copy(predicate: predicate) 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/parser_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class ParserTest < Minitest::Test 7 | def test_parses_ripper_methods 8 | # First, get a list of all of the dispatched events from ripper. 9 | events = Ripper::EVENTS 10 | 11 | # Next, subtract all of the events that we have explicitly defined. 12 | events -= 13 | Parser.private_instance_methods(false).grep(/^on_(\w+)/) { $1.to_sym } 14 | 15 | # Next, subtract the list of events that we purposefully skipped. 16 | events -= %i[ 17 | arg_ambiguous 18 | assoclist_from_args 19 | ignored_nl 20 | ignored_sp 21 | magic_comment 22 | nl 23 | nokw_param 24 | operator_ambiguous 25 | semicolon 26 | sp 27 | words_sep 28 | ] 29 | 30 | # Finally, assert that we have no remaining events. 31 | assert_empty(events) 32 | end 33 | 34 | def test_errors_on_missing_token_with_location 35 | error = assert_raises(Parser::ParseError) { SyntaxTree.parse("f+\"foo") } 36 | assert_equal(2, error.column) 37 | end 38 | 39 | def test_errors_on_missing_end_with_location 40 | error = assert_raises(Parser::ParseError) { SyntaxTree.parse("foo do 1") } 41 | assert_equal(4, error.column) 42 | end 43 | 44 | def test_errors_on_missing_regexp_ending 45 | error = 46 | assert_raises(Parser::ParseError) { SyntaxTree.parse("a =~ /foo") } 47 | 48 | assert_equal(5, error.column) 49 | end 50 | 51 | def test_errors_on_missing_token_without_location 52 | assert_raises(Parser::ParseError) { SyntaxTree.parse(":\"foo") } 53 | end 54 | 55 | def test_handles_strings_with_non_terminated_embedded_expressions 56 | assert_raises(Parser::ParseError) { SyntaxTree.parse('"#{"') } 57 | end 58 | 59 | def test_errors_on_else_missing_two_ends 60 | assert_raises(Parser::ParseError) { SyntaxTree.parse(<<~RUBY) } 61 | def foo 62 | if something 63 | else 64 | call do 65 | end 66 | RUBY 67 | end 68 | 69 | def test_does_not_choke_on_invalid_characters_in_source_string 70 | SyntaxTree.parse(<<~RUBY) 71 | # comment 72 | # comment 73 | __END__ 74 | \xC5 75 | RUBY 76 | end 77 | 78 | def test_lambda_vars_with_parameters_location 79 | tree = SyntaxTree.parse(<<~RUBY) 80 | # comment 81 | # comment 82 | ->(_i; a) { a } 83 | RUBY 84 | 85 | local_location = 86 | tree.statements.body.last.params.contents.locals.first.location 87 | 88 | assert_equal(3, local_location.start_line) 89 | assert_equal(3, local_location.end_line) 90 | assert_equal(7, local_location.start_column) 91 | assert_equal(8, local_location.end_column) 92 | end 93 | 94 | def test_lambda_vars_location 95 | tree = SyntaxTree.parse(<<~RUBY) 96 | # comment 97 | # comment 98 | ->(; a) { a } 99 | RUBY 100 | 101 | local_location = 102 | tree.statements.body.last.params.contents.locals.first.location 103 | 104 | assert_equal(3, local_location.start_line) 105 | assert_equal(3, local_location.end_line) 106 | assert_equal(5, local_location.start_column) 107 | assert_equal(6, local_location.end_column) 108 | end 109 | 110 | def test_multiple_lambda_vars_location 111 | tree = SyntaxTree.parse(<<~RUBY) 112 | # comment 113 | # comment 114 | ->(; a, b, c) { a } 115 | RUBY 116 | 117 | local_location = 118 | tree.statements.body.last.params.contents.locals.last.location 119 | 120 | assert_equal(3, local_location.start_line) 121 | assert_equal(3, local_location.end_line) 122 | assert_equal(11, local_location.start_column) 123 | assert_equal(12, local_location.end_column) 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/plugin/disable_ternary_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | module SyntaxTree 6 | class DisableTernaryTest < Minitest::Test 7 | def test_short_if_else_unchanged 8 | assert_format(<<~RUBY) 9 | if true 10 | 1 11 | else 12 | 2 13 | end 14 | RUBY 15 | end 16 | 17 | def test_short_ternary_unchanged 18 | assert_format("true ? 1 : 2\n") 19 | end 20 | 21 | private 22 | 23 | def assert_format(expected, source = expected) 24 | options = Formatter::Options.new(disable_auto_ternary: true) 25 | formatter = Formatter.new(source, [], options: options) 26 | SyntaxTree.parse(source).format(formatter) 27 | 28 | formatter.flush 29 | assert_equal(expected, formatter.output.join) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/plugin/single_quotes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | module SyntaxTree 6 | class SingleQuotesTest < Minitest::Test 7 | def test_empty_string_literal 8 | assert_format("''\n", "\"\"") 9 | end 10 | 11 | def test_string_literal 12 | assert_format("'string'\n", "\"string\"") 13 | end 14 | 15 | def test_string_literal_with_interpolation 16 | assert_format("\"\#{foo}\"\n") 17 | end 18 | 19 | def test_dyna_symbol 20 | assert_format(":'symbol'\n", ":\"symbol\"") 21 | end 22 | 23 | def test_single_quote_in_string 24 | assert_format("\"str'ing\"\n") 25 | end 26 | 27 | def test_label 28 | assert_format( 29 | "{ foo => foo, :'bar' => bar }\n", 30 | "{ foo => foo, \"bar\": bar }" 31 | ) 32 | end 33 | 34 | private 35 | 36 | def assert_format(expected, source = expected) 37 | options = Formatter::Options.new(quote: "'") 38 | formatter = Formatter.new(source, [], options: options) 39 | SyntaxTree.parse(source).format(formatter) 40 | 41 | formatter.flush 42 | assert_equal(expected, formatter.output.join) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/plugin/trailing_comma_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | 5 | module SyntaxTree 6 | class TrailingCommaTest < Minitest::Test 7 | def test_arg_paren_flat 8 | assert_format("foo(a)\n") 9 | end 10 | 11 | def test_arg_paren_break 12 | assert_format(<<~EXPECTED, <<~SOURCE) 13 | foo( 14 | #{"a" * 80}, 15 | ) 16 | EXPECTED 17 | foo(#{"a" * 80}) 18 | SOURCE 19 | end 20 | 21 | def test_arg_paren_block 22 | assert_format(<<~EXPECTED, <<~SOURCE) 23 | foo( 24 | &#{"a" * 80} 25 | ) 26 | EXPECTED 27 | foo(&#{"a" * 80}) 28 | SOURCE 29 | end 30 | 31 | def test_arg_paren_command 32 | assert_format(<<~EXPECTED, <<~SOURCE) 33 | foo( 34 | bar #{"a" * 80} 35 | ) 36 | EXPECTED 37 | foo(bar #{"a" * 80}) 38 | SOURCE 39 | end 40 | 41 | def test_arg_paren_command_call 42 | assert_format(<<~EXPECTED, <<~SOURCE) 43 | foo( 44 | bar.baz #{"a" * 80} 45 | ) 46 | EXPECTED 47 | foo(bar.baz #{"a" * 80}) 48 | SOURCE 49 | end 50 | 51 | def test_array_literal_flat 52 | assert_format("[a]\n") 53 | end 54 | 55 | def test_array_literal_break 56 | assert_format(<<~EXPECTED, <<~SOURCE) 57 | [ 58 | #{"a" * 80}, 59 | ] 60 | EXPECTED 61 | [#{"a" * 80}] 62 | SOURCE 63 | end 64 | 65 | def test_hash_literal_flat 66 | assert_format("{ a: a }\n") 67 | end 68 | 69 | def test_hash_literal_break 70 | assert_format(<<~EXPECTED, <<~SOURCE) 71 | { 72 | a: 73 | #{"a" * 80}, 74 | } 75 | EXPECTED 76 | { a: #{"a" * 80} } 77 | SOURCE 78 | end 79 | 80 | private 81 | 82 | def assert_format(expected, source = expected) 83 | options = Formatter::Options.new(trailing_comma: true) 84 | formatter = Formatter.new(source, [], options: options) 85 | SyntaxTree.parse(source).format(formatter) 86 | 87 | formatter.flush 88 | assert_equal(expected, formatter.output.join) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/quotes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class QuotesTest < Minitest::Test 7 | def test_normalize 8 | content = "'aaa' \"bbb\" \\'ccc\\' \\\"ddd\\\"" 9 | enclosing = "\"" 10 | 11 | result = Quotes.normalize(content, enclosing) 12 | assert_equal "'aaa' \\\"bbb\\\" \\'ccc\\' \\\"ddd\\\"", result 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/ractor_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Don't run this test if we're in a version of Ruby that doesn't have Ractors. 4 | return unless defined?(Ractor) 5 | 6 | # Don't run this version on Ruby 3.0.0. For some reason it just hangs within the 7 | # main Ractor waiting for this children. Not going to investigate it since it's 8 | # already been fixed in 3.1.0. 9 | return if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1.0") 10 | 11 | require_relative "test_helper" 12 | 13 | module SyntaxTree 14 | class RactorTest < Minitest::Test 15 | def test_formatting 16 | ractors = 17 | filepaths.map do |filepath| 18 | # At the moment we have to parse in the main Ractor because Ripper is 19 | # not marked as a Ractor-safe extension. 20 | source = SyntaxTree.read(filepath) 21 | program = SyntaxTree.parse(source) 22 | 23 | with_silenced_warnings do 24 | Ractor.new(source, program, name: filepath) do |source, program| 25 | SyntaxTree::Formatter.format(source, program) 26 | end 27 | end 28 | end 29 | 30 | ractors.each { |ractor| assert_kind_of String, ractor.take } 31 | end 32 | 33 | private 34 | 35 | def filepaths 36 | Dir.glob(File.expand_path("../lib/syntax_tree/plugin/*.rb", __dir__)) 37 | end 38 | 39 | # Ractors still warn about usage, so I'm disabling that warning here just to 40 | # have clean test output. 41 | def with_silenced_warnings 42 | previous = $VERBOSE 43 | 44 | begin 45 | $VERBOSE = nil 46 | yield 47 | ensure 48 | $VERBOSE = previous 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/rake_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | require "syntax_tree/rake_tasks" 5 | 6 | module SyntaxTree 7 | module Rake 8 | class CheckTaskTest < Minitest::Test 9 | Invocation = Struct.new(:args) 10 | 11 | def test_task_command 12 | assert_raises(NotImplementedError) { Task.new.command } 13 | end 14 | 15 | def test_check_task 16 | source_files = "{app,config,lib}/**/*.rb" 17 | 18 | CheckTask.new do |t| 19 | t.source_files = source_files 20 | t.print_width = 100 21 | t.target_ruby_version = Gem::Version.new("2.6.0") 22 | end 23 | 24 | expected = [ 25 | "check", 26 | "--print-width=100", 27 | "--target-ruby-version=2.6.0", 28 | source_files 29 | ] 30 | 31 | invocation = invoke("stree:check") 32 | assert_equal(expected, invocation.args) 33 | end 34 | 35 | def test_write_task 36 | source_files = "{app,config,lib}/**/*.rb" 37 | WriteTask.new { |t| t.source_files = source_files } 38 | 39 | invocation = invoke("stree:write") 40 | assert_equal(["write", source_files], invocation.args) 41 | end 42 | 43 | private 44 | 45 | def invoke(task_name) 46 | invocation = nil 47 | stub = ->(args) { invocation = Invocation.new(args) } 48 | 49 | assert_raises SystemExit do 50 | SyntaxTree::CLI.stub(:run, stub) { ::Rake::Task[task_name].invoke } 51 | end 52 | 53 | invocation 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/search_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class SearchTest < Minitest::Test 7 | def test_search_invalid_syntax 8 | assert_raises(Pattern::CompilationError) { search("", "<>") } 9 | end 10 | 11 | def test_search_invalid_constant 12 | assert_raises(Pattern::CompilationError) { search("", "Foo") } 13 | end 14 | 15 | def test_search_invalid_nested_constant 16 | assert_raises(Pattern::CompilationError) { search("", "Foo::Bar") } 17 | end 18 | 19 | def test_search_regexp_with_interpolation 20 | assert_raises(Pattern::CompilationError) { search("", "/\#{foo}/") } 21 | end 22 | 23 | def test_search_string_with_interpolation 24 | assert_raises(Pattern::CompilationError) { search("", '"#{foo}"') } 25 | end 26 | 27 | def test_search_symbol_with_interpolation 28 | assert_raises(Pattern::CompilationError) { search("", ":\"\#{foo}\"") } 29 | end 30 | 31 | def test_search_invalid_node 32 | assert_raises(Pattern::CompilationError) { search("", "Int[^foo]") } 33 | end 34 | 35 | def test_search_self 36 | assert_raises(Pattern::CompilationError) { search("", "self") } 37 | end 38 | 39 | def test_search_array_pattern_no_constant 40 | results = search("1 + 2", "[Int, Int]") 41 | 42 | assert_equal 1, results.length 43 | end 44 | 45 | def test_search_array_pattern 46 | results = search("1 + 2", "Binary[Int, Int]") 47 | 48 | assert_equal 1, results.length 49 | end 50 | 51 | def test_search_binary_or 52 | results = search("Foo + Bar + 1", "VarRef | Int") 53 | 54 | assert_equal 3, results.length 55 | assert_equal "1", results.min_by { |node| node.class.name }.value 56 | end 57 | 58 | def test_search_const 59 | results = search("Foo + Bar + Baz", "VarRef") 60 | 61 | assert_equal 3, results.length 62 | assert_equal %w[Bar Baz Foo], results.map { |node| node.value.value }.sort 63 | end 64 | 65 | def test_search_object_const 66 | results = search("1 + 2 + 3", "Int[value: String]") 67 | 68 | assert_equal 3, results.length 69 | end 70 | 71 | def test_search_syntax_tree_const 72 | results = search("Foo + Bar + Baz", "SyntaxTree::VarRef") 73 | 74 | assert_equal 3, results.length 75 | end 76 | 77 | def test_search_hash_pattern_no_constant 78 | results = search("Foo + Bar + Baz", "{ value: Const }") 79 | 80 | assert_equal 3, results.length 81 | end 82 | 83 | def test_search_hash_pattern_string 84 | results = search("Foo + Bar + Baz", "VarRef[value: Const[value: 'Foo']]") 85 | 86 | assert_equal 1, results.length 87 | assert_equal "Foo", results.first.value.value 88 | end 89 | 90 | def test_search_hash_pattern_regexp 91 | results = search("Foo + Bar + Baz", "VarRef[value: Const[value: /^Ba/]]") 92 | 93 | assert_equal 2, results.length 94 | assert_equal %w[Bar Baz], results.map { |node| node.value.value }.sort 95 | end 96 | 97 | def test_search_string_empty 98 | results = search("", "''") 99 | 100 | assert_empty results 101 | end 102 | 103 | def test_search_symbol_empty 104 | results = search("", ":''") 105 | 106 | assert_empty results 107 | end 108 | 109 | def test_search_symbol_plain 110 | results = search("1 + 2", "Binary[operator: :'+']") 111 | 112 | assert_equal 1, results.length 113 | end 114 | 115 | def test_search_symbol 116 | results = search("1 + 2", "Binary[operator: :+]") 117 | 118 | assert_equal 1, results.length 119 | end 120 | 121 | private 122 | 123 | def search(source, query) 124 | SyntaxTree.search(source, query).to_a 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/syntax_tree_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class SyntaxTreeTest < Minitest::Test 7 | def test_empty 8 | void_stmt = SyntaxTree.parse("").statements.body.first 9 | assert_kind_of(VoidStmt, void_stmt) 10 | end 11 | 12 | def test_multibyte 13 | assign = SyntaxTree.parse("🎉 + 🎉").statements.body.first 14 | assert_equal(5, assign.location.end_char) 15 | end 16 | 17 | def test_next_statement_start 18 | source = <<~SOURCE 19 | def method # comment 20 | expression 21 | end 22 | SOURCE 23 | 24 | bodystmt = SyntaxTree.parse(source).statements.body.first.bodystmt 25 | assert_equal(20, bodystmt.start_char) 26 | end 27 | 28 | def test_parse_error 29 | assert_raises(Parser::ParseError) { SyntaxTree.parse("<>") } 30 | end 31 | 32 | def test_marshalable 33 | node = SyntaxTree.parse("1 + 2") 34 | assert_operator(node, :===, Marshal.load(Marshal.dump(node))) 35 | end 36 | 37 | def test_maxwidth_format 38 | assert_equal("foo +\n bar\n", SyntaxTree.format("foo + bar", 5)) 39 | end 40 | 41 | def test_read 42 | source = SyntaxTree.read(__FILE__) 43 | assert_equal(Encoding.default_external, source.encoding) 44 | 45 | source = SyntaxTree.read(File.expand_path("encoded.rb", __dir__)) 46 | assert_equal(Encoding::Shift_JIS, source.encoding) 47 | end 48 | 49 | def test_version 50 | refute_nil(VERSION) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | unless RUBY_ENGINE == "truffleruby" 4 | require "simplecov" 5 | SimpleCov.start do 6 | add_filter("idempotency_test.rb") unless ENV["CI"] 7 | add_group("lib", "lib") 8 | add_group("test", "test") 9 | end 10 | end 11 | 12 | $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) 13 | require "syntax_tree" 14 | require "syntax_tree/cli" 15 | 16 | unless RUBY_ENGINE == "truffleruby" 17 | # Here we are going to establish type verification whenever a new node is 18 | # created. We do this through the reflection module, which in turn parses the 19 | # source code of the node classes. 20 | require "syntax_tree/reflection" 21 | SyntaxTree::Reflection.nodes.each do |name, node| 22 | next if name == :Statements 23 | 24 | clazz = SyntaxTree.const_get(name) 25 | parameters = clazz.instance_method(:initialize).parameters 26 | 27 | # First, verify that all of the parameters listed in the list of attributes. 28 | # If there are any parameters that aren't listed in the attributes, then 29 | # something went wrong with the parsing in the reflection module. 30 | raise unless (parameters.map(&:last) - node.attributes.keys).empty? 31 | 32 | # Now we're going to use an alias chain to redefine the initialize method to 33 | # include type checking. 34 | clazz.alias_method(:initialize_without_verify, :initialize) 35 | clazz.define_method(:initialize) do |**kwargs| 36 | kwargs.each do |kwarg, value| 37 | attribute = node.attributes.fetch(kwarg) 38 | 39 | unless attribute.type === value 40 | raise TypeError, 41 | "invalid type for #{name}##{kwarg}, expected " \ 42 | "#{attribute.type.inspect}, got #{value.inspect}" 43 | end 44 | end 45 | 46 | initialize_without_verify(**kwargs) 47 | end 48 | end 49 | end 50 | 51 | require "json" 52 | require "tempfile" 53 | require "pp" 54 | require "minitest/autorun" 55 | 56 | module SyntaxTree 57 | module Assertions 58 | class Recorder 59 | attr_reader :called 60 | 61 | def initialize 62 | @called = nil 63 | end 64 | 65 | def method_missing(called, *, **) 66 | @called = called 67 | end 68 | end 69 | 70 | private 71 | 72 | # This is a special kind of assertion that is going to get loaded into all 73 | # of test cases. It asserts against a whole bunch of stuff that every node 74 | # type should be able to handle. It's here so that we can use it in a bunch 75 | # of tests. 76 | def assert_syntax_tree(node) 77 | # First, get the visit method name. 78 | recorder = Recorder.new 79 | node.accept(recorder) 80 | 81 | # Next, get the "type" which is effectively an underscored version of 82 | # the name of the class. 83 | type = recorder.called[/^visit_(.+)$/, 1] 84 | 85 | # Test that the method that is called when you call accept is a valid 86 | # visit method on the visitor. 87 | assert_respond_to(Visitor.new, recorder.called) 88 | 89 | # Test that you can call child_nodes and the pattern matching methods on 90 | # this class. 91 | assert_kind_of(Array, node.child_nodes) 92 | assert_kind_of(Array, node.deconstruct) 93 | assert_kind_of(Hash, node.deconstruct_keys([])) 94 | 95 | # Assert that it can be pretty printed to a string. 96 | pretty = PP.singleline_pp(node, +"") 97 | refute_includes(pretty, "#<") 98 | assert_includes(pretty, type) 99 | 100 | # Assert that we can get back a new tree by using the mutation visitor. 101 | assert_operator node, :===, node.accept(MutationVisitor.new) 102 | 103 | # Serialize the node to JSON, parse it back out, and assert that we have 104 | # found the expected type. 105 | json = node.to_json 106 | refute_includes(json, "#<") 107 | assert_equal(type, JSON.parse(json)["type"]) 108 | 109 | if RUBY_ENGINE != "truffleruby" 110 | # Get a match expression from the node, then assert that it can in fact 111 | # match the node. 112 | # rubocop:disable all 113 | assert(eval(<<~RUBY)) 114 | case node 115 | in #{node.construct_keys} 116 | true 117 | end 118 | RUBY 119 | end 120 | end 121 | 122 | Minitest::Test.include(self) 123 | end 124 | end 125 | 126 | # There are a bunch of fixtures defined in test/fixtures. They exercise every 127 | # possible combination of syntax that leads to variations in the types of nodes. 128 | # They are used for testing various parts of Syntax Tree, including formatting, 129 | # serialization, and parsing. This module provides a single each_fixture method 130 | # that can be used to drive tests on each fixture. 131 | module Fixtures 132 | FIXTURES_3_0_0 = %w[ 133 | command_def_endless 134 | def_endless 135 | fndptn 136 | rassign 137 | rassign_rocket 138 | ].freeze 139 | 140 | FIXTURES_3_1_0 = %w[pinned_begin var_field_rassign].freeze 141 | 142 | Fixture = Struct.new(:name, :source, :formatted, keyword_init: true) 143 | 144 | def self.each_fixture 145 | ruby_version = Gem::Version.new(RUBY_VERSION) 146 | 147 | # First, get a list of the basenames of all of the fixture files. 148 | fixtures = 149 | Dir[File.expand_path("fixtures/*.rb", __dir__)].map do |filepath| 150 | File.basename(filepath, ".rb") 151 | end 152 | 153 | # Next, subtract out any fixtures that aren't supported by the current Ruby 154 | # version. 155 | fixtures -= FIXTURES_3_1_0 if ruby_version < Gem::Version.new("3.1.0") 156 | fixtures -= FIXTURES_3_0_0 if ruby_version < Gem::Version.new("3.0.0") 157 | 158 | delimiter = /%(?: # (.+?))?\n/ 159 | fixtures.each do |fixture| 160 | filepath = File.expand_path("fixtures/#{fixture}.rb", __dir__) 161 | 162 | # For each fixture in the fixture file yield a Fixture object. 163 | File 164 | .readlines(filepath) 165 | .slice_before(delimiter) 166 | .each_with_index do |source, index| 167 | comment = source.shift.match(delimiter)[1] 168 | source, formatted = source.join.split("-\n") 169 | 170 | # If there's a comment starting with >= that starts after the % that 171 | # delineates the test, then we're going to check if the version 172 | # satisfies that constraint. 173 | if comment&.start_with?(">=") 174 | next if ruby_version < Gem::Version.new(comment.split[1]) 175 | end 176 | 177 | name = :"#{fixture}_#{index}" 178 | yield( 179 | Fixture.new( 180 | name: name, 181 | source: source, 182 | formatted: formatted || source 183 | ) 184 | ) 185 | end 186 | end 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /test/translation/parser_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../test_helper" 4 | require "parser/current" 5 | 6 | Parser::Builders::Default.modernize 7 | 8 | module SyntaxTree 9 | module Translation 10 | class ParserTest < Minitest::Test 11 | known_failures = [ 12 | # I think this may be a bug in the parser gem's precedence calculation. 13 | # Unary plus appears to be parsed as part of the number literal in 14 | # CRuby, but parser is parsing it as a separate operator. 15 | "test_unary_num_pow_precedence:3505", 16 | 17 | # Not much to be done about this. Basically, regular expressions with 18 | # named capture groups that use the =~ operator inject local variables 19 | # into the current scope. In the parser gem, it detects this and changes 20 | # future references to that name to be a local variable instead of a 21 | # potential method call. CRuby does not do this. 22 | "test_lvar_injecting_match:3778", 23 | 24 | # This is failing because CRuby is not marking values captured in hash 25 | # patterns as local variables, while the parser gem is. 26 | "test_pattern_matching_hash:8971", 27 | 28 | # This is not actually allowed in the CRuby parser but the parser gem 29 | # thinks it is allowed. 30 | "test_pattern_matching_hash_with_string_keys:9016", 31 | "test_pattern_matching_hash_with_string_keys:9027", 32 | "test_pattern_matching_hash_with_string_keys:9038", 33 | "test_pattern_matching_hash_with_string_keys:9060", 34 | "test_pattern_matching_hash_with_string_keys:9071", 35 | "test_pattern_matching_hash_with_string_keys:9082", 36 | 37 | # This happens with pattern matching where you're matching a literal 38 | # value inside parentheses, which doesn't really do anything. Ripper 39 | # doesn't capture that this value is inside a parentheses, so it's hard 40 | # to translate properly. 41 | "test_pattern_matching_expr_in_paren:9206", 42 | 43 | # These are also failing because of CRuby not marking values captured in 44 | # hash patterns as local variables. 45 | "test_pattern_matching_single_line_allowed_omission_of_parentheses:*", 46 | 47 | # I'm not even sure what this is testing, because the code is invalid in 48 | # CRuby. 49 | "test_control_meta_escape_chars_in_regexp__since_31:*", 50 | ] 51 | 52 | todo_failures = [ 53 | "test_dedenting_heredoc:334", 54 | "test_dedenting_heredoc:390", 55 | "test_dedenting_heredoc:399", 56 | "test_slash_newline_in_heredocs:7194", 57 | "test_parser_slash_slash_n_escaping_in_literals:*", 58 | "test_forwarded_restarg:*", 59 | "test_forwarded_kwrestarg:*", 60 | "test_forwarded_argument_with_restarg:*", 61 | "test_forwarded_argument_with_kwrestarg:*" 62 | ] 63 | 64 | current_version = RUBY_VERSION.split(".")[0..1].join(".") 65 | 66 | if current_version <= "2.7" 67 | # I'm not sure why this is failing on 2.7.0, but we'll turn it off for 68 | # now until we have more time to investigate. 69 | todo_failures.push( 70 | "test_pattern_matching_hash:*", 71 | "test_pattern_matching_single_line:9552" 72 | ) 73 | end 74 | 75 | if current_version <= "3.0" 76 | # In < 3.0, there are some changes to the way the parser gem handles 77 | # forwarded args. We should eventually support this, but for now we're 78 | # going to mark them as todo. 79 | todo_failures.push( 80 | "test_forward_arg:*", 81 | "test_forward_args_legacy:*", 82 | "test_endless_method_forwarded_args_legacy:*", 83 | "test_trailing_forward_arg:*", 84 | "test_forward_arg_with_open_args:10770", 85 | ) 86 | end 87 | 88 | if current_version == "3.1" 89 | # This test actually fails on 3.1.0, even though it's marked as being 90 | # since 3.1. So we're going to skip this test on 3.1, but leave it in 91 | # for other versions. 92 | known_failures.push( 93 | "test_multiple_pattern_matches:11086", 94 | "test_multiple_pattern_matches:11102" 95 | ) 96 | end 97 | 98 | if current_version < "3.2" || RUBY_ENGINE == "truffleruby" 99 | known_failures.push( 100 | "test_if_while_after_class__since_32:11004", 101 | "test_if_while_after_class__since_32:11014", 102 | "test_newline_in_hash_argument:11057" 103 | ) 104 | end 105 | 106 | all_failures = known_failures + todo_failures 107 | 108 | File 109 | .foreach(File.expand_path("parser.txt", __dir__), chomp: true) 110 | .slice_before { |line| line.start_with?("!!!") } 111 | .each do |(prefix, *lines)| 112 | name = prefix[4..] 113 | next if all_failures.any? { |pattern| File.fnmatch?(pattern, name) } 114 | 115 | define_method(name) { assert_parses("#{lines.join("\n")}\n") } 116 | end 117 | 118 | private 119 | 120 | def assert_parses(source) 121 | parser = ::Parser::CurrentRuby.default_parser 122 | parser.diagnostics.consumer = ->(*) {} 123 | 124 | buffer = ::Parser::Source::Buffer.new("(string)", 1) 125 | buffer.source = source 126 | 127 | expected = 128 | begin 129 | parser.parse(buffer) 130 | rescue ::Parser::SyntaxError 131 | # We can get a syntax error if we're parsing a fixture that was 132 | # designed for a later Ruby version but we're running an earlier 133 | # Ruby version. In this case we can just return early from the test. 134 | end 135 | 136 | return if expected.nil? 137 | node = SyntaxTree.parse(source) 138 | assert_equal expected, SyntaxTree::Translation.to_parser(node, buffer) 139 | end 140 | end 141 | end 142 | end 143 | 144 | if ENV["PARSER_LOCATION"] 145 | # Modify the source map == check so that it doesn't check against the node 146 | # itself so we don't get into a recursive loop. 147 | Parser::Source::Map.prepend( 148 | Module.new do 149 | def ==(other) 150 | self.class == other.class && 151 | (instance_variables - %i[@node]).map do |ivar| 152 | instance_variable_get(ivar) == other.instance_variable_get(ivar) 153 | end.reduce(:&) 154 | end 155 | end 156 | ) 157 | 158 | # Next, ensure that we're comparing the nodes and also comparing the source 159 | # ranges so that we're getting all of the necessary information. 160 | Parser::AST::Node.prepend( 161 | Module.new do 162 | def ==(other) 163 | super && (location == other.location) 164 | end 165 | end 166 | ) 167 | end 168 | -------------------------------------------------------------------------------- /test/visitor_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "test_helper" 4 | 5 | module SyntaxTree 6 | class VisitorTest < Minitest::Test 7 | def test_visit_tree 8 | parsed_tree = SyntaxTree.parse(<<~RUBY) 9 | class Foo 10 | def foo; end 11 | 12 | class Bar 13 | def bar; end 14 | end 15 | end 16 | 17 | def baz; end 18 | RUBY 19 | 20 | visitor = DummyVisitor.new 21 | visitor.visit(parsed_tree) 22 | assert_equal(%w[Foo foo Bar bar baz], visitor.visited_nodes) 23 | end 24 | 25 | class DummyVisitor < Visitor 26 | attr_reader :visited_nodes 27 | 28 | def initialize 29 | super 30 | @visited_nodes = [] 31 | end 32 | 33 | visit_methods do 34 | def visit_class(node) 35 | @visited_nodes << node.constant.constant.value 36 | super 37 | end 38 | 39 | def visit_def(node) 40 | @visited_nodes << node.name.value 41 | end 42 | end 43 | end 44 | 45 | if defined?(DidYouMean.correct_error) 46 | def test_visit_method_correction 47 | error = assert_raises { Visitor.visit_method(:visit_binar) } 48 | message = 49 | if Exception.method_defined?(:detailed_message) 50 | error.detailed_message 51 | else 52 | error.message 53 | end 54 | 55 | assert_match(/visit_binary/, message) 56 | end 57 | end 58 | 59 | class VisitMethodsTestVisitor < BasicVisitor 60 | end 61 | 62 | def test_visit_methods 63 | VisitMethodsTestVisitor.visit_methods do 64 | assert_raises(BasicVisitor::VisitMethodError) do 65 | # In reality, this would be a method defined using the def keyword, 66 | # but we're using method_added here to trigger the checker so that we 67 | # aren't defining methods dynamically in the test suite. 68 | VisitMethodsTestVisitor.method_added(:visit_foo) 69 | end 70 | end 71 | end 72 | end 73 | end 74 | --------------------------------------------------------------------------------