diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c451d287 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: CI + +on: + push: + pull_request: + +jobs: + base: + name: Base steps + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check Whitespace + run: git diff --check -- HEAD~1 + ruby-unit-spec: + name: Unit Specs + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + ruby: [ruby-3.2, ruby-3.3, ruby-3.4] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - run: bundle exec rspec spec/unit + ruby-mutant: + name: Mutant + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + ruby: [ruby-3.2, ruby-3.3, ruby-3.4] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - run: bundle exec mutant run --since HEAD~1 --zombie + ruby-integration-spec: + name: Integration Specs + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + ruby: [ruby-3.2, ruby-3.3, ruby-3.4] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - run: bundle exec bin/corpus + ruby-parser-round-trip-tests: + name: Parser Round Trip Tests + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + ruby: [ruby-3.2, ruby-3.3, ruby-3.4] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - run: bundle exec ruby bin/parser-round-trip-test + ruby-rubocop: + name: Rubocop + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + ruby: [ruby-3.2, ruby-3.3, ruby-3.4] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + - run: bundle exec rubocop diff --git a/.rspec b/.rspec index 2ac79342..4df29cc9 100644 --- a/.rspec +++ b/.rspec @@ -2,3 +2,4 @@ --format progress --warnings --order random +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml index dc630307..62416515 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,8 +1,117 @@ AllCops: Include: + - 'lib/unparser.rb' + - 'lib/unparser/**/*.rb' + - '**/*.rake' - 'Gemfile' + - 'Gemfile.triage' + TargetRubyVersion: 3.1 Exclude: - - 'Gemfile.devtools' - - 'vendor/**/*' - - 'benchmarks/**/*' - - 'tmp/**/*' + - tmp/**/* + - vendor/**/* + NewCops: enable + SuggestExtensions: false + +# Avoid parameter lists longer than five parameters. +Metrics/ParameterLists: + Max: 3 + CountKeywordArgs: true + +Metrics/MethodLength: + CountComments: false + Max: 17 + +Metrics/AbcSize: + Max: 18 + +# Avoid more than `Max` levels of nesting. +Metrics/BlockNesting: + Max: 3 + +# Align with the style guide. +Style/CollectionMethods: + PreferredMethods: + collect: 'map' + inject: 'reduce' + find: 'detect' + find_all: 'select' + +# Use `fail` as `raise` implies re-raising +Style/SignalException: + EnforcedStyle: semantic + +# Limit line length +Layout/LineLength: + Max: 113 # TODO: lower to 79 once the rubocop branch in shared/Gemfile is removed + +Metrics/ClassLength: + Max: 205 + +# Prefer modifiers and explicit if statements over returning early for small methods +Style/GuardClause: + Enabled: false + +Metrics/BlockLength: + Exclude: + # Ignore RSpec DSL + - spec/**/* + +# Flags freezes for singletons that could still be mutated like Regexps +Style/RedundantFreeze: + Enabled: false + +# Disabled because of indenting with private keyword in class bodies. +Layout/IndentationWidth: + Enabled: false + +# Disable documentation checking until a class needs to be documented once +Style/Documentation: + Enabled: false + +# Do not favor modifier if/unless usage when you have a single-line body +Style/IfUnlessModifier: + Enabled: false + +# I like to have an empty line before closing the currently opened body +Layout/EmptyLinesAroundBlockBody: + Enabled: false + +Layout/EmptyLinesAroundClassBody: + Enabled: false + +Layout/EmptyLinesAroundModuleBody: + Enabled: false + +# I like my style more +Layout/AccessModifierIndentation: + Enabled: false + +Style/CommentedKeyword: + Enabled: false + +Style/MixinGrouping: + Enabled: false + +Lint/BooleanSymbol: + Enabled: false + +Style/AccessModifierDeclarations: + Enabled: false + +Layout/HashAlignment: + EnforcedColonStyle: table + EnforcedHashRocketStyle: table + +Naming/RescuedExceptionsVariableName: + Enabled: false + +Layout/MultilineMethodCallIndentation: + Enabled: false + +# Useful for code structure +Lint/UselessConstantScoping: + Enabled: false + +# Lots of false positives, trying to do static analysis on a dynlang is not going to work +Naming/PredicateMethod: + Enabled: false diff --git a/Changelog.md b/Changelog.md index 73f7e72a..b61fc80b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,228 @@ +# v0.8.0 unreleased + +[#387](https://github.com/mbj/unparser/pull/387) + +* Add `prism` parser support for Ruby 3.4. ([viralpraxis](https://github.com/viralpraxis)) + +# v0.7.0 2025-03-16 + +[#366](https://github.com/mbj/unparser/pull/366) + +* Fix all known dstring issues. +* Interface changes. + +# v0.6.15 2024-06-10 + +[#373](https://github.com/mbj/unparser/pull/373) + +* Fix additonal keyword dispatch + +# v0.6.14 2024-06-10 + +[#369](https://github.com/mbj/unparser/pull/369) + +* Remove support for ruby-3.0, its EOL. + +# v0.6.13 2024-02-01 + +[#361](https://github.com/mbj/unparser/pull/361) + +* Fix unparsing of symbols that do not round trip under `#inspect` + +# v0.6.12 2024-01-10 + +[#355](https://github.com/mbj/unparser/pull/355) + +* Fix conditionals with empty bodies. + +# v0.6.11 2023-10-31 + +[#354](https://github.com/mbj/unparser/pull/354) + +* Remove support for ruby 2.7 + +# v0.6.10 2023-10-31 + +[#351](https://github.com/mbj/unparser/pull/351) + +* Fix missing heredocs on op-assign rhs + +# v0.6.9 2023-10-08 + +[#348](https://github.com/mbj/unparser/pull/348) + +* Fix crash on kwrestarg hash member + +[#349](https://github.com/mbj/unparser/pull/349) + +* Fix {begin,end}less {i,e}range flipflops + +# v0.6.8 2023-06-14 + +[#347](https://github.com/mbj/unparser/pull/347) + +* Fix binary operator with csend receiver. [Fix #345] + +# v0.6.7 2023-01-08 + +[#338](https://github.com/mbj/unparser/pull/338) + +* Add required MFA for rubygems pushes. + +# v0.6.6 2023-01-06 + +[#336](https://github.com/mbj/unparser/pull/336) + +* Add support for ruby-3.2 syntax. + +# v0.6.5 2022-04-17 + +[#312](https://github.com/mbj/unparser/pull/312) + +* Fix #311, emitting of heredocs within block that has arguments. + +[#313](https://github.com/mbj/unparser/pull/313) + +* Remove Ruby-2.6 support as its EOL + +# v0.6.4 2022-02-12 + +[#299](https://github.com/mbj/unparser/pull/299) + +* Add 3.1+ syntax support. + +[#300](https://github.com/mbj/unparser/pull/300) + +* Add 3.0+ node support for `find_pattern` and `match_rest` + +[#298](https://github.com/mbj/unparser/pull/298) + +* Add `parser` gem derived round trip tests. + +[#297](https://github.com/mbj/unparser/pull/297) + +* Fix emit of of `match_pattern` vs `match_pattern_p` + +[#298](https://github.com/mbj/unparser/pull/298) + +* Add round trip tests dynamically derived from the `parser` gems test suite to CI + +# v0.6.3 2022-01-16 + +[#290](https://github.com/mbj/unparser/pull/290) + +* Depend on parser-3.1.0. +* This is not yet Ruby 3.1 syntax support, only + supporting the existing feature set on an updated `parser` gem. + +# v0.6.2 2021-11-09 + +[#281](https://github.com/mbj/unparser/pull/268) + +* Fix unary operator with argument. + +# v0.6.1 2021-11-08 + +[#279](https://github.com/mbj/unparser/pull/279) + +* Fix binary operator with kwargs argument. + +[#268](https://github.com/mbj/unparser/pull/268) + +* Remove ruby 2.5 support since its EOL. + +# v0.6.0 2021-01-06 + +[#245](https://github.com/mbj/unparser/pull/245) + +* Change to raise Unparser::InvalidNode error in some cases when unparsing invalid AST. +* Change `Unparser.unparse` into an official public API. +* Remove lots of dependencies. + +# v0.5.7 2020-12-25 + +* Fix heredocs in return arguments [#244](https://github.com/mbj/unparser/pull/244) + +# v0.5.6 2020-12-25 + +* Add full Ruby 3.0 Syntax support [#233](https://github.com/mbj/unparser/pull/233) + +# v0.5.5 2020-12-24 + +* Fix in-pattern without body [#231](https://github.com/mbj/unparser/pull/231) + +# v0.5.4 2020-11-04 + +* Fix forced ternary on control keyword [#191](https://github.com/mbj/unparser/pull/191) + +# v0.5.3 2020-10-18 + +* Add required ruby version '>= 2.5' to gemspec. + +# v0.5.2 2020-10-16 + +* Fix unary csends to emit correctly. +* Add `Unparser.unparse_validate` interface + +# v0.5.1 2020-10-09 + +* Change to emit empty `dstr` as `%()` + +# v0.5.0 2020-10-08 + +* Add 2.7 syntax support +* Fix lots of edge cases via leveraging parser specs +* Add `--literal` mode for CLI + +# v0.4.9 2020-09-10 + +* Change packaging to avoid git in gemspec. + +# v0.4.8 2020-05-25 + +* Change to specific node type when unparser fails on an unknown node type: [#150](https://github.com/mbj/unparser/pull/150) +* Significantly improve verifier (only useful for debugging) +* Add `Unparser::Color` module for colorized source diffs + +# v0.4.7 2020-01-03 + +* Add support for endless ranges +* Change to allow parser 2.7, even while syntax is not yet supported. + This reduces downstream complexity. + +# v0.4.6 2020-01-02 + +* Upgrades to allow parser dependency to ~> 2.6.5 + +# v0.4.5 2019-05-10 + +* Bump parser dependency to ~> 2.6.3 + +# v0.4.4 2019-03-27 + +* Bump parser dependency to ~> 2.6.2 + +# v0.4.3 2019-02-24 + +* Bump parser dependency to ~> 2.6.0 + +# v0.4.2 2018-12-04 + +* Drop hard ruby version requirement. Still officially I'll only support 2.5. + +# v0.4.1 2018-12-03 + +* Fix unparsing of `def foo(bar: bar())` + +# v0.4.0 2018-12-03 + +* Change to modern AST format. +* Add experimental `Unparser.{parser,parse,parse_with_comments}` + +# v0.3.0 2018-11-16 + +* Drop support for Ruby < 2.5 + # v0.2.7 2018-07-18 * Add emitters for `__FILE__` and `__LINE__` diff --git a/Gemfile b/Gemfile index fa75df15..7f4f5e95 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 95fa84ee..800cb605 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,166 +1,107 @@ PATH remote: . specs: - unparser (0.2.6) - abstract_type (~> 0.0.7) - adamantium (~> 0.2.0) - concord (~> 0.1.5) - diff-lcs (~> 1.3) - equalizer (~> 0.0.9) - parser (>= 2.3.1.2, < 2.6) - procto (~> 0.0.2) + unparser (0.8.0) + diff-lcs (~> 1.6) + parser (>= 3.3.0) + prism (>= 1.4) GEM remote: https://rubygems.org/ specs: - abstract_type (0.0.7) - adamantium (0.2.0) - ice_nine (~> 0.11.0) - memoizable (~> 0.4.0) - anima (0.3.0) - abstract_type (~> 0.0.7) - adamantium (~> 0.2) - equalizer (~> 0.0.11) - ast (2.3.0) - axiom-types (0.1.1) - descendants_tracker (~> 0.0.4) - ice_nine (~> 0.11.0) - thread_safe (~> 0.3, >= 0.3.1) - codeclimate-engine-rb (0.4.0) - virtus (~> 1.0) - coercible (1.0.0) - descendants_tracker (~> 0.0.1) - concord (0.1.5) - adamantium (~> 0.2.0) - equalizer (~> 0.0.9) - descendants_tracker (0.0.4) - thread_safe (~> 0.3, >= 0.3.1) - devtools (0.1.16) - adamantium (~> 0.2.0) - anima (~> 0.3.0) - concord (~> 0.1.5) - flay (~> 2.8.1) - flog (~> 4.4.0) - mutant (~> 0.8.11) - mutant-rspec (~> 0.8.11) - procto (~> 0.0.3) - rake (~> 11.3.0) - reek (~> 4.5.0) - rspec (~> 3.5.0) - rspec-core (~> 3.5.4) - rspec-its (~> 1.2.0) - rubocop (~> 0.47.0) - simplecov (~> 0.12.0) - yard (~> 0.9.1) - yardstick (~> 0.9.9) - diff-lcs (1.3) - docile (1.1.5) - equalizer (0.0.11) - erubis (2.7.0) - flay (2.8.1) - erubis (~> 2.7.0) - path_expander (~> 1.0) - ruby_parser (~> 3.0) - sexp_processor (~> 4.0) - flog (4.4.1) - path_expander (~> 1.0) - ruby_parser (~> 3.1, > 3.1.0) - sexp_processor (~> 4.4) - ice_nine (0.11.2) - json (2.0.3) - memoizable (0.4.2) - thread_safe (~> 0.3, >= 0.3.1) - morpher (0.2.6) - abstract_type (~> 0.0.7) - adamantium (~> 0.2.0) - anima (~> 0.3.0) - ast (~> 2.2) - concord (~> 0.1.5) - equalizer (~> 0.0.9) - ice_nine (~> 0.11.0) - procto (~> 0.0.2) - mutant (0.8.12) - abstract_type (~> 0.0.7) - adamantium (~> 0.2.0) - anima (~> 0.3.0) - ast (~> 2.2) - concord (~> 0.1.5) - diff-lcs (~> 1.2) - equalizer (~> 0.0.9) - ice_nine (~> 0.11.1) - memoizable (~> 0.4.2) - morpher (~> 0.2.6) - parallel (~> 1.3) - parser (~> 2.3.1, >= 2.3.1.4) - procto (~> 0.0.2) - regexp_parser (~> 0.3.6) - unparser (~> 0.2.5) - mutant-rspec (0.8.11) - mutant (~> 0.8.11) - rspec-core (>= 3.4.0, < 3.6.0) - parallel (1.10.0) - parser (2.3.3.1) - ast (~> 2.2) - path_expander (1.0.1) - powerpack (0.1.1) - procto (0.0.3) - rainbow (2.2.1) - rake (11.3.0) - reek (4.5.4) - codeclimate-engine-rb (~> 0.4.0) - parser (~> 2.3.1, >= 2.3.1.2) - rainbow (~> 2.0) - regexp_parser (0.3.6) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.4) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + ast (2.4.3) + date (3.4.1) + diff-lcs (1.6.2) + erb (5.0.1) + io-console (0.8.0) + irb (1.15.2) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.12.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + mutant (0.13.2) + diff-lcs (~> 1.3) + irb (~> 1.15.2) + parser (~> 3.3.0) + regexp_parser (~> 2.10) + sorbet-runtime (~> 0.5.0) + unparser (~> 0.8.0) + mutant-rspec (0.13.2) + mutant (= 0.13.2) + rspec-core (>= 3.8.0, < 4.0.0) + parallel (1.27.0) + parser (3.3.8.0) + ast (~> 2.4.1) + racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + prism (1.4.0) + psych (5.2.6) + date + stringio + racc (1.8.1) + rainbow (3.1.1) + rdoc (6.14.1) + erb + psych (>= 4.0.0) + regexp_parser (2.10.0) + reline (0.6.1) + io-console (~> 0.5) + rspec (3.13.1) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.4) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-its (1.2.0) + rspec-support (~> 3.13.0) + rspec-its (1.3.1) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.5.0) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) - rubocop (0.47.1) - parser (>= 2.3.3.1, < 3.0) - powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.4) + rubocop (1.76.2) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.45.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.8.1) - ruby_parser (3.8.4) - sexp_processor (~> 4.1) - sexp_processor (4.7.0) - simplecov (0.12.0) - docile (~> 1.1.0) - json (>= 1.8, < 3) - simplecov-html (~> 0.10.0) - simplecov-html (0.10.0) - thread_safe (0.3.5) - unicode-display_width (1.1.3) - virtus (1.0.5) - axiom-types (~> 0.1) - coercible (~> 1.0) - descendants_tracker (~> 0.0, >= 0.0.3) - equalizer (~> 0.0, >= 0.0.9) - yard (0.9.8) - yardstick (0.9.9) - yard (~> 0.8, >= 0.8.7.2) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.45.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-packaging (0.6.0) + lint_roller (~> 1.1.0) + rubocop (>= 1.72.1, < 2.0) + ruby-progressbar (1.13.0) + sorbet-runtime (0.5.12194) + stringio (3.1.7) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) PLATFORMS - ruby + x86_64-linux DEPENDENCIES - anima (~> 0.3.0) - devtools (~> 0.1.3) - morpher (~> 0.2.6) + mutant (~> 0.13.2) + mutant-rspec (~> 0.13.0) + rspec (~> 3.13) + rspec-core (~> 3.13) + rspec-its (~> 1.3.0) + rubocop (~> 1.7) + rubocop-packaging (~> 0.5) unparser! BUNDLED WITH - 1.13.7 + 2.6.2 diff --git a/README.md b/README.md index b9f2c458..b37568cc 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,41 @@ unparser ======== -[![Build Status](https://secure.travis-ci.org/mbj/unparser.png?branch=master)](http://travis-ci.org/mbj/unparser) -[![Dependency Status](https://gemnasium.com/mbj/unparser.png)](https://gemnasium.com/mbj/unparser) -[![Code Climate](https://codeclimate.com/github/mbj/unparser.png)](https://codeclimate.com/github/mbj/unparser) +![CI](https://github.com/mbj/unparser/workflows/CI/badge.svg) [![Gem Version](https://img.shields.io/gem/v/unparser.svg)](https://rubygems.org/gems/unparser) -Generate equivalent source for ASTs from whitequarks [parser](https://github.com/whitequark/parser). -Excluding the macruby extensions the parser gem implemnents on top of ruby syntax. +Generate equivalent source for ASTs from [parser](https://github.com/whitequark/parser). -Excluding the MacRuby / RubyMotion extensions the parser gem implemenents on top of MRI Ruby -syntax starting with parser release 2.3. If you feel the need to get them supported, contact me. +The following constraints apply: -This library is able to reproduce 100% of Ruby 2.1 - 2.3 syntax. Including its own source code. +* No support for macruby extensions +* Only support for the [modern AST](https://github.com/whitequark/parser/#usage) format +* Only support for Ruby >= 3.2 -It serves well for [mutant](https://github.com/mbj/mutant) mutators and the in-memory vendoring for self hosting, -and other tooling. +Notable Users: + +* [mutant](https://github.com/mbj/mutant) - Code review engine via mutation testing. +* [ruby-next](https://github.com/ruby-next/ruby-next) - Ruby Syntax Backports. +* Many other [reverse-dependencies](https://rubygems.org/gems/unparser/reverse_dependencies). + +(if you want your tool to be mentioned here please PR the addition with a TLDR of your use case). Public API: ----------- -While unparser is in the `0.x` versions its public API can change any moment. I recommend to use `~> 0.x.y` style -version constraints that should give the best mileage. +While unparser is in the `0.x` versions its public API can change any moment. +I recommend to use `~> 0.x.y` style version constraints that should give the best mileage. Usage ----- ```ruby +require 'parser/current' require 'unparser' -Unparser.unparse(your_ast) # => "the code" + +ast = Unparser.parse('your(ruby(code))') + +Unparser.unparse(ast) # => 'your(ruby(code))' ``` To preserve the comments from the source: @@ -36,8 +43,10 @@ To preserve the comments from the source: ```ruby require 'parser/current' require 'unparser' -ast, comments = Parser::CurrentRuby.parse_with_comments(your_source) -Unparser.unparse(ast, comments) # => "the code # with comments" + +ast, comments = Unparser.parser.parse_with_comments(Unparser.buffer('your(ruby(code)) # with comments')) + +Unparser.unparse(ast, comments: comments) # => 'your(ruby(code)) # with comments' ``` Passing in manually constructed AST: @@ -71,34 +80,41 @@ Unparser.unparse(node) # => "def foo(x)\n x + 3\nend" Note: DO NOT attempt to pass in nodes generated via `AST::Sexp#s`, these ones return API incompatible `AST::Node` instances, unparser needs `Parser::AST::Node` instances. - Equivalent vs identical: ```ruby require 'unparser' -code = <<-RUBY -%w(foo bar) +node = Unparser.parse(<<~'RUBY') + %w[foo bar] RUBY -node = Parser::CurrentRuby.parse(code) +generated = Unparser.unparse(node) # ["foo", "bar"], NOT %w[foo bar] ! -generated = Unparser.unparse(node) # ["foo", "bar"], NOT %w(foo bar) ! - -code == generated # false, not identical code -Parser::CurrentRuby.parse(generated) == node # true, but identical AST +code == generated # false, not identical code +Unparser.parse(generated) == node # true, but identical AST ``` Summary: unparser does not reproduce your source! It produces equivalent source. +Ruby Versions: +-------------- + +Unparsers primay reason for existance is mutant and its +supported [Ruby-Versions](https://github.com/mbj/mutant#ruby-versions). + +Basically: All non EOL MRI releases. + +If you need to generate Ruby Syntax outside of this band feel free to contact me (email in gemspec). + Testing: -------- -Unparser currently successfully round trips almost all ruby code around. Using MRI-2.0.0. +Unparser currently successfully round trips almost all ruby code around. Using Ruby >= 2.6. If there is a non round trippable example that is NOT subjected to known [Limitations](#limitations). please report a bug. -On CI unparser is currently tested against rubyspec with minor [excludes](https://github.com/mbj/unparser/blob/master/spec/integrations.yml). +On CI unparser is currently tested against rubyspec with minor [excludes](https://github.com/mbj/unparser/blob/main/spec/integrations.yml). Limitations: ------------ @@ -106,10 +122,7 @@ Limitations: Source parsed with magic encoding headers other than UTF-8 and that have literal strings. where parts can be represented in UTF-8 will fail to get reproduced. -Please note: If you are on 1.9.3 or any 1.9 mode ruby and use UTF-8 encoded source via the magic encoding header: -Unparser does not reproduce these. - -A fix might be possible and requires some guessing or parser metadata the raw AST does not carry. +A fix is possible as with latest updates the parser gem carries the information. Example: @@ -155,6 +168,18 @@ People Various people contributed to this repository. See [Contributors](https://github.com/mbj/unparser/graphs/contributors). +Included Libraries +------------------ + +For dependency reduction reasons unparser ships vendored (and reduced) versions of: + +* [abstract_type](https://github.com/mbj/concord) -> Unparser::AbstractType +* [adamantium](https://github.com/dkubb/adamantium) -> Unparser::Adamantium +* [anima](https://github.com/mbj/concord) -> Unparser::Anima +* [concord](https://github.com/mbj/concord) -> Unparser::Concord +* [memoizable](https://github.com/dkubb/memoizable) -> Unparser::Adamantium +* [mprelude](https://github.com/dkubb/memoizable) -> Unparser::Either + Contributing ------------- @@ -162,10 +187,15 @@ Contributing * Make your feature addition or bug fix. * Add tests for it. This is important so I don't break it in a future version unintentionally. -* Commit, do not mess with Rakefile or version +* Commit, do not mess with version (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) * Send me a pull request. Bonus points for topic branches. +Known Users +------------- + +* [RailsRocket](https://www.railsrocket.app) - A no-code app builder that creates Rails apps + License ------- diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 1c1978e8..00000000 --- a/Rakefile +++ /dev/null @@ -1,21 +0,0 @@ -require 'devtools' -Devtools.init_rake_tasks - -Rake.application.load_imports -task('metrics:mutant').clear - -namespace :metrics do - task mutant: :coverage do - args = %w[ - bundle exec mutant - --include lib - --require unparser - --use rspec - --zombie - --since HEAD~1 - ] - args.concat(%w[--jobs 4]) if ENV.key?('CIRCLECI') - - system(*args.concat(%w[-- Unparser*])) or fail "Mutant task failed" - end -end diff --git a/bin/corpus b/bin/corpus new file mode 100755 index 00000000..b7dc73f9 --- /dev/null +++ b/bin/corpus @@ -0,0 +1,252 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'etc' +require 'mutant' +require 'optparse' +require 'pathname' +require 'unparser' + +Thread.abort_on_exception = true + +module Unparser + module Corpus + ROOT = Pathname.new(__dir__).parent + TMP = ROOT.join('tmp') + + class Project + include Unparser::Anima.new(:name, :repo_uri, :repo_ref, :exclude) + + # Perform verification via unparser cli + # + # @return [Boolean] + # + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def verify + puts("Verifying: #{name}") + checkout + + paths = Pathname.glob(Pathname.new(repo_path).join('**/*.rb')) + + driver = Mutant::Parallel.async( + config: Mutant::Parallel::Config.new( + block: method(:verify_path), + jobs: Etc.nprocessors, + on_process_start: ->(*) {}, + process_name: 'unparser-corpus-test', + sink: Sink.new, + source: Mutant::Parallel::Source::Array.new(jobs: paths), + thread_name: 'unparser-corpus-test', + timeout: nil + ), + world: Mutant::WORLD + ) + + loop do + status = driver.wait_timeout(1) + + puts("Processed: #{status.payload.total}") + + # rubocop:disable Lint/UnreachableLoop + status.payload.errors.each do |report| + puts report + fail + end + # rubocop:enable Lint/UnreachableLoop + + break if status.done? + end + + true + end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + + private + + class Sink + include Mutant::Parallel::Sink + + attr_reader :errors, :total + + def initialize + @errors = [] + @total = 0 + end + + def stop? + !@errors.empty? + end + + def status + self + end + + def response(response) + if response.error + Mutant::WORLD.stderr.puts(response.log) + fail response.error + end + + @total += 1 + + if response.result + @errors << response.result + end + end + end + + def verify_path(path) + validation = Validation.from_path(path) + + if original_syntax_error?(validation) || generated_encoding_error?(validation) || validation.success? + return + end + + validation.report + end + + def checkout + TMP.mkdir unless TMP.directory? + + if repo_path.exist? + Dir.chdir(repo_path) do + system(%w[git fetch]) + system(%w[git clean -f -d -x]) + end + else + system(%W[git clone #{repo_uri} #{repo_path}]) + end + + Dir.chdir(repo_path) do + system(%W[git checkout #{repo_ref}]) + system(%w[git reset --hard]) + system(%w[git clean -f -d -x]) + end + end + + def repo_path + TMP.join(name) + end + + # This happens if the original source contained a non UTF charset meta comment. + # These are not exposed to the AST in a way unparser could know about to generate a non UTF-8 + # target and emit that meta comment itself. + # For the purpose of corpus testing these cases are ignored. + def generated_encoding_error?(validation) + exception = validation.generated_node.from_left { return false } + exception.instance_of?(Parser::SyntaxError) && + exception.message.eql?('literal contains escape sequences incompatible with UTF-8') + end + + def original_syntax_error?(validation) + validation.original_node.from_left { return false }.instance_of?(Parser::SyntaxError) + end + + def system(arguments) + return if Kernel.system(*arguments) + + fail "System command #{arguments.inspect} failed!" + end + + transform = Mutant::Transform + string = transform::Primitive.new(primitive: String) + string_array = transform::Array.new(transform: string) + path = ROOT.join('spec', 'integrations.yml') + + loader = + transform::Named.new( + name: path.to_s, + transform: transform::Sequence.new( + steps: [ + transform::Exception.new( + block: :read.to_proc, + error_class: SystemCallError + ), + transform::Exception.new( + block: YAML.method(:safe_load), + error_class: YAML::SyntaxError + ), + transform::Array.new( + transform: transform::Sequence.new( + steps: [ + transform::Hash.new( + optional: [], + required: [ + transform::Hash::Key.new(value: 'exclude', transform: string_array), + transform::Hash::Key.new(value: 'name', transform: string), + transform::Hash::Key.new(value: 'repo_ref', transform: string), + transform::Hash::Key.new(value: 'repo_uri', transform: string) + ] + ), + transform::Hash::Symbolize.new, + transform::Exception.new( + block: Project.public_method(:new), + error_class: Unparser::Anima::Error + ) + ] + ) + ) + ] + ) + ) + + ALL = loader.call(path).lmap(&:compact_message).from_right + end + + # Unparser corpus CLI implementation + class CLI + def self.run(*arguments) + new(*arguments).exit_status + end + + def initialize(arguments) + @projects = [] + + options = OptionParser.new do |builder| + builder.on('--list', 'List registered projects') do + Project::ALL.each do |project| + puts(project.name) + end + + Kernel.exit + end + end + + options.parse!(arguments).each do |name| + @projects << project(name) + end + end + + def project(name) + Project::ALL.find { |project| project.name.eql?(name) } || fail("Unregistered project: #{name}") + end + + def effective_projects + if @projects.empty? + Project::ALL + else + @projects + end + end + + # Return exit status + # + # @return [Integer] + # + # @api private + # + def exit_status + effective_projects.each do |project| + project.verify || Kernel.exit(false) + end + + Kernel.exit + end + + end # CLI + end # Corpus +end # Unparser + +Unparser::Corpus::CLI.run(ARGV) diff --git a/bin/parser-prism-round-trip-test b/bin/parser-prism-round-trip-test new file mode 100755 index 00000000..a9a22242 --- /dev/null +++ b/bin/parser-prism-round-trip-test @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'unparser' +require 'prism' +require 'fileutils' +require 'pathname' + +module PrismParser + PARSER = 'ruby/prism' + PARSER_VERSION = Prism::VERSION + PARSER_PATH = Pathname('tmp/parser-prism') + + PRISM_INVALID = Set[ + 'def __ENCODING__.a', + 'def __FILE__.a', + 'def __LINE__.a', + '\777', + 'ち\xE3\x81\xFF', + '\x8E\x01', + 'a\xE9b', + 'a\247b', + '\xE3\xD3\x8B\xE3\x83\xBC\x83\xE3\x83\xE3\x82\xB3\xA3\x82\x99', + 'hello \u{fc}', + '# encoding: sjis' + ].freeze + + PRISM_TODO = Set[ + 'def a(...); "foo#{b(...)}"; end', + '` +foo\ +b\nar +`' + ].freeze + + PRISM_NO_ROUND_TRIP = (PRISM_INVALID + PRISM_TODO).to_set.freeze + + private_constant :PRISM_INVALID, :PRISM_TODO + + class << self + def prepare + FileUtils.rm_rf("#{PARSER_PATH}/test/prism/fixtures-tmp") + Dir.mkdir("#{PARSER_PATH}/test/prism/fixtures-tmp") + + Dir.glob("#{PARSER_PATH}/test/prism/fixtures/**/*.txt") + .then(&method(:select_fixtures)) + .each do |path| + examples = File.read(path).split(/(?<=\n\n)/).then(&method(:reject_no_round_trip_examples)) + output_path = path.gsub('prism/fixtures', 'prism/fixtures-tmp') + dirname = File.dirname(output_path) + FileUtils.mkdir_p(dirname) unless File.directory?(dirname) + File.write(output_path, examples.join("\n\n")) + end + end + + def target_glob + "#{PARSER_PATH}/test/prism/fixtures-tmp/**/*.txt" + end + + def excludes + %w[ + spanning_heredoc + heredocs_with_fake_newlines + heredocs_nested + ].to_set { |file| "#{PARSER_PATH}/test/prism/fixtures-tmp/#{file}.txt" } + end + + private + + def select_fixtures(paths) + paths.reject { _1.include?('fixtures/unparser/') } + end + + def reject_no_round_trip_examples(examples) + examples.reject do |example| + PRISM_NO_ROUND_TRIP.any? { |snippet| example.include?(snippet) } + end + end + end +end + +unless PrismParser::PARSER_PATH.exist? + Kernel.system( + *%W[ + git + clone + https://github.com/#{PrismParser::PARSER} + #{PrismParser::PARSER_PATH} + ], + exception: true + ) +end + +Dir.chdir(PrismParser::PARSER_PATH) do + Kernel.system( + *%W[ + git + checkout + v#{PrismParser::PARSER_VERSION} + ], + exception: true + ) + Kernel.system(*%w[git clean --force -d -X], exception: true) +end + +ignores_cli_option = PrismParser.excludes.flat_map { |file| ['--ignore', file] } + +PrismParser.prepare +exit Unparser::CLI.run([PrismParser.target_glob, *ignores_cli_option]) diff --git a/bin/parser-round-trip-test b/bin/parser-round-trip-test new file mode 100755 index 00000000..7b5f0bc6 --- /dev/null +++ b/bin/parser-round-trip-test @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +if Gem::Version.new(RUBY_VERSION) <= '3.4' + load 'bin/parser-whitequark-round-trip-test' +else + load 'bin/parser-prism-round-trip-test' +end diff --git a/bin/parser-whitequark-round-trip-test b/bin/parser-whitequark-round-trip-test new file mode 100755 index 00000000..a87cf1b6 --- /dev/null +++ b/bin/parser-whitequark-round-trip-test @@ -0,0 +1,301 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'unparser' + +# Hack to dynamically re-use the `parser` gems test suite on CI. +# The main idea is create a fake minitet runner to capture the +# signature of the examples encoded in the parsers test suite dynamically. +# +# This makes maintenance much more easier, especially on tracking new ruby +# syntax addtions. +# +# The API surface of the parser tests so far is low churn, while it may still +# make sense to provide the parser tests as an more easy to re-use data upstream. + +$LOAD_PATH << Pathname.new(__dir__).parent.join('test') + +test_builder = Class.new(Parser::Builders::Default) +test_builder.modernize + +MODERN_ATTRIBUTES = test_builder.instance_variables.to_h do |instance_variable| + attribute_name = instance_variable.to_s[1..].to_sym + [attribute_name, test_builder.public_send(attribute_name)] +end + +# Overwrite global scope method in the parser test suite +def default_builder_attributes + MODERN_ATTRIBUTES.keys.to_h do |attribute_name| + [attribute_name, Parser::Builders::Default.public_send(attribute_name)] + end +end + +class Test + include Unparser::Adamantium, Unparser::Anima.new( + :default_builder_attributes, + :group_index, + :name, + :node, + :parser_source, + :rubies + ) + + EXPECT_FAILURE = {}.freeze + STATIC_LOCAL_VARIABLES = %w[foo bar baz].to_set.freeze + + NO_ROUND_TRIP = %i[ + test_int___LINE__ + test_pattern_matching__FILE__LINE_literals + test_string___FILE__ + ].freeze + + def legacy_attributes + default_builder_attributes.reject do |attribute_name, value| + MODERN_ATTRIBUTES.fetch(attribute_name).equal?(value) + end.to_h + end + memoize :legacy_attributes + + def skip_reason + if !legacy_attributes.empty? + "Legacy parser attributes: #{legacy_attributes}" + elsif !allow_ruby? + "Non targeted rubies: #{rubies.join(',')}" + elsif validation.original_node.left? + 'Test specifies a syntax error' + elsif NO_ROUND_TRIP.include?(name) + 'Test not round trippable' + end + end + + def success? + validation.success? + end + + def expect_failure? + EXPECT_FAILURE.key?([name, group_index]) + end + + def allow_ruby? + rubies.empty? || rubies.include?(RUBY_VERSION.split('.').take(2).join('.')) + end + + def right(value) + Unparser::Either::Right.new(value) + end + + # rubocop:disable Metrics/AbcSize + def validation + identification = name.to_s + + ast = Unparser::AST.new( + comments: [], + explicit_encoding: nil, + node: node, + static_local_variables: STATIC_LOCAL_VARIABLES + ) + + generated_source = Unparser.unparse_ast_either(ast) + .fmap { |string| string.dup.force_encoding(parser_source.encoding).freeze } + + generated_node = generated_source.bind { |source| parse_either(source, identification) } + + Unparser::Validation.new( + generated_node: generated_node, + generated_source: generated_source, + identification: identification, + original_ast: parse_either_ast(parser_source, identification), + original_source: right(parser_source) + ) + end + # rubocop:enable Metrics/AbcSize + memoize :validation + + def parser + Unparser.parser.tap do |parser| + STATIC_LOCAL_VARIABLES.each(&parser.static_env.method(:declare)) + end + end + + def parse_either(source, identification) + Unparser::Either.wrap_error(Parser::SyntaxError) do + parser.parse(Unparser.buffer(source, identification)) + end + end + + def parse_either_ast(source, identification) + parse_either(source, identification).fmap do |node| + Unparser::AST.new( + comments: [], + explicit_encoding: nil, + node: node, + static_local_variables: Set.new + ) + end + end +end + +class Execution + include Unparser::Anima.new(:number, :total, :test) + + def call + skip_reason = test.skip_reason + if skip_reason + print('Skip', skip_reason) + return + end + + if test.expect_failure? + expect_failure + else + expect_success + end + end + +private + + def expect_failure + if test.success? + message('Expected Failure', 'but got success') + else + print('Expected Failure') + end + end + + def expect_success + if test.success? + print('Success') + else + puts(test.validation.report) + fail message('Failure') + end + end + + def message(status, message = '') + format( + '%3d/%3d: %-16s %s[%02d] %s', + number: number, + total: total, + status: status, + name: test.name, + group_index: test.group_index, + message: message + ) + end + + def print(status, message = '') + puts(message(status, message)) + end +end + +module Minitest + # Stub parent class + # rubocop:disable Lint/EmptyClass + class Test; end # Test + # rubocop:enable Lint/EmptyClass +end # Minitest + +class Extractor + class Capture + include Unparser::Anima.new( + :default_builder_attributes, + :node, + :parser_source, + :rubies + ) + + end + + attr_reader :tests + + def initialize + @captures = [] + @tests = [] + end + + def capture(**attributes) + @captures << Capture.new(attributes) + end + + def reset + @captures = [] + end + + def call(name) + reset + + TestParser.new.send(name) + + @captures.each_with_index do |capture, index| + @tests << Test.new(name: name, group_index: index, **capture.to_h) + end + + reset + end +end + +PARSER_PATH = Pathname.new('tmp/parser') + +unless PARSER_PATH.exist? + Kernel.system( + *%W[ + git + clone + https://github.com/whitequark/parser + #{PARSER_PATH} + ], + exception: true + ) +end + +Dir.chdir(PARSER_PATH) do + Kernel.system( + *%W[ + git + checkout + v#{Parser::VERSION} + ], + exception: true + ) + Kernel.system(*%w[git clean --force -d -X], exception: true) +end + +require "./#{PARSER_PATH}/test/parse_helper" +require "./#{PARSER_PATH}/test/test_parser" + +EXTRACTOR = Extractor.new + +module ParseHelper + def assert_diagnoses(*arguments); end + + def s(type, *children) + Parser::AST::Node.new(type, children) + end + + # rubocop:disable Metrics/ParameterLists + def assert_parses(node, parser_source, _diagnostics = nil, rubies = []) + EXTRACTOR.capture( + default_builder_attributes: default_builder_attributes, + node: node, + parser_source: parser_source, + rubies: rubies + ) + end + # rubocop:enable Metrics/ParameterLists + + def test_clrf_line_endings(*arguments); end + + def with_versions(*arguments); end + + def assert_context(*arguments); end + + def refute_diagnoses(*arguments); end + + def assert_diagnoses_many(*arguments); end +end + +TestParser.instance_methods.grep(/\Atest_/).each(&EXTRACTOR.method(:call)) + +EXTRACTOR.tests.sort_by(&:name).each_with_index do |test, index| + Execution.new(number: index.succ, total: EXTRACTOR.tests.length, test: test).call +end diff --git a/bin/unparser b/bin/unparser index 501b9126..26d049e4 100755 --- a/bin/unparser +++ b/bin/unparser @@ -1,8 +1,10 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + trap('INT') do |status| - exit! 128 + status + exit! status + 128 end -require 'unparser/cli' +require 'unparser' exit Unparser::CLI.run(ARGV) diff --git a/circle.yml b/circle.yml deleted file mode 100644 index d24b5628..00000000 --- a/circle.yml +++ /dev/null @@ -1,6 +0,0 @@ -machine: - ruby: - version: '2.2' -test: - override: - - bundle exec rake ci diff --git a/config/devtools.yml b/config/devtools.yml deleted file mode 100644 index dbdc79af..00000000 --- a/config/devtools.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -unit_test_timeout: 0.1 diff --git a/config/flay.yml b/config/flay.yml deleted file mode 100644 index 2a8c1ee9..00000000 --- a/config/flay.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -threshold: 13 -total_score: 659 diff --git a/config/flog.yml b/config/flog.yml deleted file mode 100644 index 1174a8fe..00000000 --- a/config/flog.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -threshold: 21.3 diff --git a/config/mutant.yml b/config/mutant.yml index 42a64793..8d4cc456 100644 --- a/config/mutant.yml +++ b/config/mutant.yml @@ -1,3 +1,14 @@ --- -name: unparser -namespace: Unparser +usage: opensource +includes: +- lib +integration: rspec +requires: +- unparser +mutation: + timeout: 10.0 +coverage_criteria: + timeout: true +matcher: + subjects: + - 'Unparser*' diff --git a/config/reek.yml b/config/reek.yml deleted file mode 100644 index 08b7ff85..00000000 --- a/config/reek.yml +++ /dev/null @@ -1,137 +0,0 @@ ---- -UnusedParameters: - exclude: [] -UncommunicativeParameterName: - accept: [] - exclude: [] - enabled: true - reject: - - !ruby/regexp /^.$/ - - !ruby/regexp /[0-9]$/ - - !ruby/regexp /[A-Z]/ -TooManyInstanceVariables: - exclude: - - Unparser::CLI - enabled: true - max_instance_variables: 3 -TooManyMethods: - exclude: - - Unparser::Emitter # TODO: 13 methods, mostly helpers for deduplicate sublcasses - - Unparser::Buffer # 11 methods - - Unparser::CLI::Source # 11 methods - enabled: true - max_methods: 10 -UncommunicativeMethodName: - accept: - - s - - n - exclude: [] - enabled: true - reject: - - !ruby/regexp /^[a-z]$/ - - !ruby/regexp /[0-9]$/ - - !ruby/regexp /[A-Z]/ -LongParameterList: - max_params: 3 - exclude: - - Unparser#self.transquote - enabled: true - overrides: {} -FeatureEnvy: - exclude: - # False positives - - Unparser::CLI::Differ#collapsed_hunks - - Unparser::Emitter::Send::Arguments#effective_arguments - # Helper methods, false positive - - Unparser::CLI::Source#strip - - Unparser::CLI::Source#report_unparser - - Unparser::CLI#sources - enabled: true -ClassVariable: - exclude: [] - enabled: true -BooleanParameter: - exclude: [] - enabled: true -IrresponsibleModule: - exclude: [] - enabled: false # buggy / broken -UncommunicativeModuleName: - accept: [] - exclude: [] - enabled: true - reject: - - !ruby/regexp /^.$/ - - !ruby/regexp /[0-9]$/ -NestedIterators: - ignore_iterators: [] - exclude: - # Acceptable cases: - - Unparser::Emitter::Literal::Regexp#dispatch - - Unparser::CLI::Preprocessor::Dstr#collapsed_children - - Unparser::Preprocessor::CollapseStrChildren#collapsed_children - enabled: true - max_allowed_nesting: 1 -TooManyStatements: - max_statements: 6 - enabled: true - exclude: - - Unparser::CLI#add_options - - Unparser::CLI#initialize - - Unparser::CLI::Source#report_unparser - - Unparser::Emitter#delimited -DuplicateMethodCall: - allow_calls: [] - exclude: [] - enabled: false - max_calls: 1 -UtilityFunction: - max_helper_calls: 1 - exclude: - # Intent to be helper methods - - Unparser::CLI::Source#strip - - Unparser::CLI::Source#parse - - Unparser::NodeHelpers#n - - Unparser::NodeHelpers#s - - Unparser::CLI#sources - enabled: true -Attribute: - exclude: [] - enabled: false -UncommunicativeVariableName: - accept: [] - exclude: [] - enabled: true - reject: - - !ruby/regexp /^.$/ - - !ruby/regexp /[0-9]$/ - - !ruby/regexp /[A-Z]/ -RepeatedConditional: - exclude: - # TODO: - - Unparser::Comments - # False positives - - Unparser::Emitter::If - - Unparser::CLI - enabled: true - max_ifs: 1 -DataClump: - exclude: [] - enabled: true - max_copies: 1 - min_clump_size: 3 -ControlParameter: - exclude: - # False positive: - - Unparser::Emitter#emit_body - - Unparser::Emitter#conditional_parentheses - - Unparser::AST::LocalVariableScope#match - enabled: true -NilCheck: - enabled: false -LongYieldList: - max_params: 1 - exclude: - - Unparser::AST::LocalVariableScopeEnumerator#visit - - Unparser::AST::LocalVariableScope#match - enabled: true diff --git a/config/rubocop.yml b/config/rubocop.yml deleted file mode 100644 index 74fb2f86..00000000 --- a/config/rubocop.yml +++ /dev/null @@ -1,105 +0,0 @@ -inherit_from: ../.rubocop.yml - -AllCops: - Include: - - '**/*.rake' - - 'Gemfile' - - 'Gemfile.triage' - TargetRubyVersion: 2.1 - -# Avoid parameter lists longer than five parameters. -ParameterLists: - Max: 3 - CountKeywordArgs: true - -MethodLength: - CountComments: false - Max: 17 - -AbcSize: - Max: 18 - -# Avoid more than `Max` levels of nesting. -BlockNesting: - Max: 3 - -# Align with the style guide. -CollectionMethods: - PreferredMethods: - collect: 'map' - inject: 'reduce' - find: 'detect' - find_all: 'select' - -# Limit line length -LineLength: - Max: 113 # TODO: lower to 79 once the rubocop branch in shared/Gemfile is removed - -ClassLength: - Max: 204 - -# Prefer modifiers and explicit if statements over returning early for small methods -GuardClause: - Enabled: false - -Metrics/BlockLength: - Exclude: - # Ignore RSpec DSL - - spec/**/* - -# Flags freezes for singletons that could still be mutated like Regexps -RedundantFreeze: - Enabled: false - -# Allow Fixnum and Bignum. This Gem supports versions before 2.4 -UnifiedInteger: - Enabled: false - -# Disabled because of indenting with private keyword in class bodies. -IndentationWidth: - Enabled: false - -# I like raise more -SignalException: - Enabled: false - -# False positive in unparser source -OneLineConditional: - Enabled: false - -Documentation: - Enabled: false - -# Disable documentation checking until a class needs to be documented once -Documentation: - Enabled: false - -# Do not favor modifier if/unless usage when you have a single-line body -IfUnlessModifier: - Enabled: false - -# Allow case equality operator (in limited use within the specs) -CaseEquality: - Enabled: false - -# Constants do not always have to use SCREAMING_SNAKE_CASE -ConstantName: - Enabled: false - -# Not all trivial readers/writers can be defined with attr_* methods -TrivialAccessors: - Enabled: false - -# I like to have an empty line before closing the currently opened body -EmptyLinesAroundBlockBody: - Enabled: false - -EmptyLinesAroundClassBody: - Enabled: false - -EmptyLinesAroundModuleBody: - Enabled: false - -# I like my style more -AccessModifierIndentation: - Enabled: false diff --git a/config/yardstick.yml b/config/yardstick.yml deleted file mode 100644 index a6b63e85..00000000 --- a/config/yardstick.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -threshold: 100 diff --git a/lib/unparser.rb b/lib/unparser.rb index 45f35245..5616f888 100644 --- a/lib/unparser.rb +++ b/lib/unparser.rb @@ -1,95 +1,351 @@ +# frozen_string_literal: true + +require 'diff/lcs' +require 'diff/lcs/hunk' +require 'optparse' require 'set' -require 'abstract_type' -require 'procto' -require 'concord' -require 'parser/current' + +require 'unparser/equalizer' +require 'unparser/adamantium' +require 'unparser/adamantium/method_builder' +require 'unparser/abstract_type' + +require 'unparser/concord' +require 'unparser/either' +require 'unparser/anima' +require 'unparser/anima/attribute' +require 'unparser/anima/error' # Library namespace -module Unparser +module Unparser # rubocop:disable Metrics/ModuleLength + # Unparser specific AST builder defaulting to modern AST format + if Gem::Version.new(RUBY_VERSION) <= '3.4' + require 'parser/current' + class Builder < Parser::Builders::Default + modernize + + # mutant:disable + def initialize + super + + self.emit_file_line_as_literals = false + end + end + else + require 'prism' + class Builder < Prism::Translation::Parser::Builder + modernize + + # mutant:disable + def initialize + super + + self.emit_file_line_as_literals = false + end + end + end + + PARSER_CLASS = + if Gem::Version.new(RUBY_VERSION) <= '3.4' + Class.new(Parser::CurrentRuby) do + def declare_local_variable(local_variable) + static_env.declare(local_variable) + end + end + else + Class.new(Prism::Translation::Parser34) do + def declare_local_variable(local_variable) + (@local_variables ||= Set.new) << local_variable + end + + def prism_options + super.merge(scopes: [@local_variables.to_a]) + end + end + end EMPTY_STRING = ''.freeze + EMPTY_ARRAY = [].freeze + + private_constant(*constants(false) - %i[Adamantium AbstractType Anima Concord Either Equalizer Memoizable]) - EMPTY_ARRAY = [].freeze + # Error raised when unparser encounters an invalid AST + class InvalidNodeError < RuntimeError + attr_reader :node + + def initialize(message, node) + super(message) + @node = node + freeze + end + end # InvalidNodeError + + # Error raised when unparser encounders AST it cannot generate source for that would parse to the same AST. + class UnsupportedNodeError < RuntimeError + end # UnsupportedNodeError # Unparse an AST (and, optionally, comments) into a string # # @param [Parser::AST::Node, nil] node - # @param [Array] comment_array + # @param [Array] comments + # @param [Encoding, nil] explicit_encoding + # @param [Set] static_local_variables # # @return [String] # - # @api private + # @raise InvalidNodeError + # if the node passed is invalid + # + # @api public + # + # mutant:disable + # rubocop:disable Metrics/ParameterLists + def self.unparse( + node, + comments: EMPTY_ARRAY, + explicit_encoding: nil, + static_local_variables: Set.new + ) + unparse_ast( + AST.new( + comments: comments, + explicit_encoding: explicit_encoding, + node: node, + static_local_variables: static_local_variables + ) + ) + end + # rubocop:enable Metrics/ParameterLists + + # Unparse an AST + # + # @param [AST] ast + # + # @return [String] + # + # @raise InvalidNodeError + # if the node passed is invalid + # + # @raise UnsupportedNodeError + # if the node passed is valid but unparser cannot unparse it + # + # @api public + def self.unparse_ast(ast) + return EMPTY_STRING if ast.node.nil? + + local_variable_scope = AST::LocalVariableScope.new( + node: ast.node, + static_local_variables: ast.static_local_variables + ) + + Buffer.new.tap do |buffer| + Emitter::Root.new( + buffer: buffer, + comments: Comments.new(ast.comments), + explicit_encoding: ast.explicit_encoding, + local_variable_scope: local_variable_scope, + node: ast.node + ).write_to_buffer + end.content + end + + # Unparse AST either + # + # @param [AST] ast + # + # @return [Either] + def self.unparse_ast_either(ast) + Either.wrap_error(Exception) { unparse_ast(ast) } + end + + # Unparse AST either + # + # @param [AST] ast + # + # @return [Either] + # + # mutant:disable + def self.unparse_validate_ast_either(ast:) + validation = Validation.from_ast(ast:) + + if validation.success? + Either::Right.new(validation.generated_source.from_right) + else + Either::Left.new(validation) + end + end + + # Unparse with validation + # + # @param [Parser::AST::Node, nil] node + # @param [Array] comments + # + # @return [Either] + def self.unparse_validate(node, comments: EMPTY_ARRAY) + generated = unparse(node, comments:) + validation = Validation.from_string(generated) + + if validation.success? + Either::Right.new(generated) + else + Either::Left.new(validation) + end + end + + # Parse string into AST + # + # @param [String] source + # + # @return [Parser::AST::Node, nil] + def self.parse(source) + parse_ast(source).node + end + + # Parse string into either syntax error or AST + # + # @param [String] source + # + # @return [Either] + def self.parse_ast_either(source) + Either.wrap_error(Exception) do + parse_ast(source) + end + end + + # Parse source with ast details + # + # @param [String] source + # + # @return [AST] + # + # mutant:disable + def self.parse_ast(source, static_local_variables: Set.new) + explicit_encoding = Parser::Source::Buffer.recognize_encoding(source.dup.force_encoding(Encoding::BINARY)) + node, comments = parser.parse_with_comments(buffer(source)) + + AST.new( + comments: comments, + explicit_encoding: explicit_encoding, + node: node, + static_local_variables: static_local_variables + ) + end + + # Parser instance that produces AST unparser understands # - def self.unparse(node, comment_array = []) - node = Preprocessor.run(node) - buffer = Buffer.new - comments = Comments.new(comment_array) - root = Emitter::Root.new(Parser::AST::Node.new(:root, [node]), buffer, comments) - Emitter.emitter(node, root).write_to_buffer - buffer.content + # @return [Parser::Base] + # + # @api private + # mutant:disable + def self.parser + PARSER_CLASS.new(Builder.new).tap do |parser| + parser.diagnostics.tap do |diagnostics| + diagnostics.all_errors_are_fatal = true + end + end end + # Construct a parser buffer from string + # + # @param [String] source + # + # @return [Parser::Source::Buffer] + def self.buffer(source, identification = '(string)') + Parser::Source::Buffer.new(identification, source: source) + end end # Unparser -require 'unparser/buffer' require 'unparser/node_helpers' -require 'unparser/preprocessor' +require 'unparser/ast' +require 'unparser/ast/local_variable_scope' +require 'unparser/buffer' +require 'unparser/generation' +require 'unparser/color' require 'unparser/comments' require 'unparser/constants' +require 'unparser/diff' require 'unparser/dsl' -require 'unparser/ast' -require 'unparser/ast/local_variable_scope' require 'unparser/emitter' -require 'unparser/emitter/literal' -require 'unparser/emitter/literal/primitive' -require 'unparser/emitter/literal/singleton' -require 'unparser/emitter/literal/dynamic' -require 'unparser/emitter/literal/regexp' -require 'unparser/emitter/literal/array' -require 'unparser/emitter/literal/hash' -require 'unparser/emitter/literal/range' -require 'unparser/emitter/literal/dynamic_body' -require 'unparser/emitter/literal/execute_string' -require 'unparser/emitter/meta' -require 'unparser/emitter/send' -require 'unparser/emitter/send/unary' -require 'unparser/emitter/send/binary' -require 'unparser/emitter/send/index' -require 'unparser/emitter/send/regular' -require 'unparser/emitter/send/conditional' -require 'unparser/emitter/send/arguments' -require 'unparser/emitter/send/attribute_assignment' -require 'unparser/emitter/block' -require 'unparser/emitter/assignment' -require 'unparser/emitter/variable' -require 'unparser/emitter/splat' -require 'unparser/emitter/cbase' +require 'unparser/emitter/alias' +require 'unparser/emitter/args' require 'unparser/emitter/argument' +require 'unparser/emitter/array' +require 'unparser/emitter/array_pattern' +require 'unparser/emitter/assignment' require 'unparser/emitter/begin' -require 'unparser/emitter/flow_modifier' -require 'unparser/emitter/undef' -require 'unparser/emitter/def' +require 'unparser/emitter/binary' +require 'unparser/emitter/block' +require 'unparser/emitter/case' +require 'unparser/emitter/case_guard' +require 'unparser/emitter/case_match' +require 'unparser/emitter/cbase' require 'unparser/emitter/class' -require 'unparser/emitter/module' -require 'unparser/emitter/op_assign' +require 'unparser/emitter/const_pattern' +require 'unparser/emitter/def' require 'unparser/emitter/defined' +require 'unparser/emitter/dstr' +require 'unparser/emitter/dsym' +require 'unparser/emitter/ensure' +require 'unparser/emitter/flipflop' +require 'unparser/emitter/float' +require 'unparser/emitter/flow_modifier' +require 'unparser/emitter/for' +require 'unparser/emitter/hash' +require 'unparser/emitter/hash_pattern' require 'unparser/emitter/hookexe' -require 'unparser/emitter/super' -require 'unparser/emitter/retry' -require 'unparser/emitter/redo' require 'unparser/emitter/if' -require 'unparser/emitter/alias' -require 'unparser/emitter/yield' -require 'unparser/emitter/binary' -require 'unparser/emitter/case' -require 'unparser/emitter/for' -require 'unparser/emitter/repetition' -require 'unparser/emitter/root' +require 'unparser/emitter/in_match' +require 'unparser/emitter/in_pattern' +require 'unparser/emitter/index' +require 'unparser/emitter/kwbegin' +require 'unparser/emitter/lambda' +require 'unparser/emitter/masgn' require 'unparser/emitter/match' -require 'unparser/emitter/empty' -require 'unparser/emitter/flipflop' +require 'unparser/emitter/match_alt' +require 'unparser/emitter/match_as' +require 'unparser/emitter/match_rest' +require 'unparser/emitter/match_var' +require 'unparser/emitter/mlhs' +require 'unparser/emitter/module' +require 'unparser/emitter/op_assign' +require 'unparser/emitter/pin' +require 'unparser/emitter/primitive' +require 'unparser/emitter/range' +require 'unparser/emitter/regexp' +require 'unparser/emitter/repetition' require 'unparser/emitter/rescue' -require 'unparser/emitter/resbody' -require 'unparser/emitter/ensure' +require 'unparser/emitter/root' +require 'unparser/emitter/send' +require 'unparser/emitter/simple' +require 'unparser/emitter/string' +require 'unparser/emitter/splat' +require 'unparser/emitter/super' +require 'unparser/emitter/undef' +require 'unparser/emitter/variable' +require 'unparser/emitter/xstr' +require 'unparser/emitter/yield' +require 'unparser/emitter/kwargs' +require 'unparser/emitter/pair' +require 'unparser/emitter/find_pattern' +require 'unparser/emitter/match_pattern' +require 'unparser/emitter/match_pattern_p' +require 'unparser/writer' +require 'unparser/writer/array' +require 'unparser/writer/binary' +require 'unparser/writer/dynamic_string' +require 'unparser/writer/regexp' +require 'unparser/writer/resbody' +require 'unparser/writer/rescue' +require 'unparser/writer/send' +require 'unparser/writer/send/attribute_assignment' +require 'unparser/writer/send/binary' +require 'unparser/writer/send/regular' +require 'unparser/writer/send/unary' +require 'unparser/node_details' +require 'unparser/node_details/send' +require 'unparser/cli' +require 'unparser/util' + +require 'unparser/validation' # make it easy for zombie require 'unparser/finalize' diff --git a/lib/unparser/abstract_type.rb b/lib/unparser/abstract_type.rb new file mode 100644 index 00000000..2a91ed4f --- /dev/null +++ b/lib/unparser/abstract_type.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +module Unparser + # Module to allow class and methods to be abstract + # + # Original code before vendoring and reduction from: https://github.com/dkubb/abstract_type. + module AbstractType + + # Hook called when module is included + # + # @param [Module] descendant + # the module or class including AbstractType + # + # @return [undefined] + # + # @api private + def self.included(descendant) + super + create_new_method(descendant) + descendant.extend(AbstractMethodDeclarations) + end + + private_class_method :included + + # Define the new method on the abstract type + # + # Ensures that the instance cannot be of the abstract type + # and must be a descendant. + # + # @param [Class] abstract_class + # + # @return [undefined] + # + # @api private + def self.create_new_method(abstract_class) + abstract_class.define_singleton_method(:new) do |*args, &block| + if equal?(abstract_class) + fail NotImplementedError, "#{self} is an abstract type" + else + super(*args, &block) + end + end + end + + private_class_method :create_new_method + + module AbstractMethodDeclarations + + # Create abstract instance methods + # + # @example + # class Foo + # include AbstractType + # + # # Create an abstract instance method + # abstract_method :some_method + # end + # + # @param [Array<#to_s>] names + # + # @return [self] + # + # @api public + def abstract_method(*names) + names.each(&method(:create_abstract_instance_method)) + self + end + + # Create abstract singleton methods + # + # @example + # class Foo + # include AbstractType + # + # # Create an abstract instance method + # abstract_singleton_method :some_method + # end + # + # @param [Array<#to_s>] names + # + # @return [self] + # + # @api private + def abstract_singleton_method(*names) + names.each(&method(:create_abstract_singleton_method)) + self + end + + private + + # Create abstract singleton method + # + # @param [#to_s] name + # the name of the method to create + # + # @return [undefined] + # + # @api private + def create_abstract_singleton_method(name) + define_singleton_method(name) do |*| + fail NotImplementedError, "#{self}.#{name} is not implemented" + end + end + + # Create abstract instance method + # + # @param [#to_s] name + # the name of the method to create + # + # @return [undefined] + # + # @api private + def create_abstract_instance_method(name) + define_method(name) do |*| + fail NotImplementedError, "#{self.class}##{name} is not implemented" + end + end + + end # AbstractMethodDeclarations + end # AbstractType +end # Unparser diff --git a/lib/unparser/adamantium.rb b/lib/unparser/adamantium.rb new file mode 100644 index 00000000..e5dd323d --- /dev/null +++ b/lib/unparser/adamantium.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +module Unparser + # Allows objects to be made immutable + # + # Original code before vendoring and reduction from: https://github.com/dkubb/adamantium. + module Adamantium + module InstanceMethods + # A noop #dup for immutable objects + # + # @return [self] + # + # @api public + def dup + self + end + + # Freeze the object + # + # @return [Object] + # + # @api public + # + # mutant:disable + def freeze + memoized_method_cache + super + end + + private + + def memoized_method_cache + @memoized_method_cache ||= Memory.new({}) + end + + end # InstanceMethods + + # Storage for memoized methods + class Memory + + # Initialize the memory storage for memoized methods + # + # @return [undefined] + # + # @api private + def initialize(values) + @values = values + @monitor = Monitor.new + freeze + end + + # Fetch the value from memory, or evaluate if it does not exist + # + # @param [Symbol] name + # + # @yieldreturn [Object] + # the value to memoize + # + # @api public + def fetch(name) + @values.fetch(name) do # check for the key + @monitor.synchronize do # acquire a lock if the key is not found + @values.fetch(name) do # recheck under lock + @values[name] = yield # set the value + end + end + end + end + end # Memory + + # Methods mixed in to adamantium classes + module ClassMethods + + # Instantiate a new frozen object + # + # @return [Object] + # + # @api public + def new(*) + super.freeze + end + + end # ClassMethods + + # Methods mixed in to adamantium modules + module ModuleMethods + + # Memoize a list of methods + # + # @param [Array<#to_s>] methods + # a list of methods to memoize + # + # @return [self] + # + # @api public + def memoize(*methods) + methods.each(&method(:memoize_method)) + self + end + + # Test if method is memoized + # + # @param [Symbol] name + # + # @return [Bool] + def memoized?(method_name) + memoized_methods.key?(method_name) + end + + # Return unmemoized instance method + # + # @param [Symbol] name + # + # @return [UnboundMethod] + # the memoized method + # + # @raise [NameError] + # raised if the method is unknown + # + # @api public + def unmemoized_instance_method(method_name) + memoized_methods.fetch(method_name) do + fail ArgumentError, "##{method_name} is not memoized" + end + end + + private + + def memoize_method(method_name) + if memoized_methods.key?(method_name) + fail ArgumentError, "##{method_name} is already memoized" + end + + memoized_methods[method_name] = MethodBuilder.new(self, method_name).call + end + + def memoized_methods + @memoized_methods ||= {} + end + + end # ModuleMethods + + def self.included(descendant) + descendant.class_eval do + include InstanceMethods + extend ModuleMethods + extend ClassMethods if instance_of?(Class) + end + end + private_class_method :included + end # Adamantium +end # Unparser diff --git a/lib/unparser/adamantium/method_builder.rb b/lib/unparser/adamantium/method_builder.rb new file mode 100644 index 00000000..7ae4b22b --- /dev/null +++ b/lib/unparser/adamantium/method_builder.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Unparser + module Adamantium + # Build the memoized method + class MethodBuilder + + # Raised when the method arity is invalid + class InvalidArityError < ArgumentError + + # Initialize an invalid arity exception + # + # @param [Module] descendant + # @param [Symbol] method + # @param [Integer] arity + # + # @api private + def initialize(descendant, method, arity) + super("Cannot memoize #{descendant}##{method}, its arity is #{arity}") + end + + end # InvalidArityError + + # Raised when a block is passed to a memoized method + class BlockNotAllowedError < ArgumentError + + # Initialize a block not allowed exception + # + # @param [Module] descendant + # @param [Symbol] method + # + # @api private + def initialize(descendant, method) + super("Cannot pass a block to #{descendant}##{method}, it is memoized") + end + + end # BlockNotAllowedError + + # Initialize an object to build a memoized method + # + # @param [Module] descendant + # @param [Symbol] method_name + # + # @return [undefined] + # + # @api private + def initialize(descendant, method_name) + @descendant = descendant + @method_name = method_name + @original_visibility = visibility + @original_method = @descendant.instance_method(@method_name) + assert_arity(@original_method.arity) + end + + # Build a new memoized method + # + # @example + # method_builder.call # => creates new method + # + # @return [UnboundMethod] + # + # @api public + def call + remove_original_method + create_memoized_method + set_method_visibility + @original_method + end + + private + + def assert_arity(arity) + if arity.nonzero? + fail InvalidArityError.new(@descendant, @method_name, arity) + end + end + + def remove_original_method + name = @method_name + @descendant.module_eval { undef_method(name) } + end + + def create_memoized_method + name = @method_name + method = @original_method + @descendant.module_eval do + define_method(name) do |&block| + fail BlockNotAllowedError.new(self.class, name) if block + + memoized_method_cache.fetch(name) do + method.bind(self).call.freeze + end + end + end + end + + def set_method_visibility + @descendant.__send__(@original_visibility, @method_name) + end + + def visibility + if @descendant.private_method_defined?(@method_name) then :private + elsif @descendant.protected_method_defined?(@method_name) then :protected + else + :public + end + end + + end # MethodBuilder + end # Adamantium +end # Unparser diff --git a/lib/unparser/anima.rb b/lib/unparser/anima.rb new file mode 100644 index 00000000..20e215c1 --- /dev/null +++ b/lib/unparser/anima.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +module Unparser + # Original code before vendoring and reduction from: https://github.com/mbj/anima. + class Anima < Module + include Adamantium, Equalizer.new(:attributes) + + # Return names + # + # @return [AttributeSet] + attr_reader :attributes + + # Initialize object + # + # @return [undefined] + # + # rubocop:disable Lint/MissingSuper + def initialize(*names) + @attributes = names.uniq.map(&Attribute.public_method(:new)).freeze + end + # rubocop:enable Lint/MissingSuper + + # Return new anima with attributes added + # + # @return [Anima] + # + # @example + # anima = Anima.new(:foo) + # anima.add(:bar) # equals Anima.new(:foo, :bar) + # + def add(*names) + new(attribute_names + names) + end + + # Return new anima with attributes removed + # + # @return [Anima] + # + # @example + # anima = Anima.new(:foo, :bar) + # anima.remove(:bar) # equals Anima.new(:foo) + # + def remove(*names) + new(attribute_names - names) + end + + # Return attributes hash for instance + # + # @param [Object] object + # + # @return [Hash] + def attributes_hash(object) + attributes.each_with_object({}) do |attribute, attributes_hash| + attributes_hash[attribute.name] = attribute.get(object) + end + end + + # Return attribute names + # + # @return [Enumerable] + def attribute_names + attributes.map(&:name) + end + memoize :attribute_names + + # Initialize instance + # + # @param [Object] object + # + # @param [Hash] attribute_hash + # + # @return [self] + def initialize_instance(object, attribute_hash) + assert_known_attributes(object.class, attribute_hash) + attributes.each do |attribute| + attribute.load(object, attribute_hash) + end + self + end + + # Static instance methods for anima infected classes + module InstanceMethods + # Initialize an anima infected object + # + # @param [#to_h] attributes + # a hash that matches anima defined attributes + # + # @return [undefined] + # + # rubocop:disable Lint/MissingSuper + def initialize(attributes) + self.class.anima.initialize_instance(self, attributes) + end + # rubocop:enable Lint/MissingSuper + + # Return a hash representation of an anima infected object + # + # @example + # anima.to_h # => { :foo => : bar } + # + # @return [Hash] + # + # @api public + def to_h + self.class.anima.attributes_hash(self) + end + + # Return updated instance + # + # @example + # klass = Class.new do + # include Anima.new(:foo, :bar) + # end + # + # foo = klass.new(:foo => 1, :bar => 2) + # updated = foo.with(:foo => 3) + # updated.foo # => 3 + # updated.bar # => 2 + # + # @param [Hash] attributes + # + # @return [Anima] + # + # @api public + def with(attributes) + self.class.new(to_h.update(attributes)) + end + end # InstanceMethods + + private + + # Infect the instance with anima + # + # @param [Class, Module] scope + # + # @return [undefined] + # + # mutant:disable + def included(descendant) + descendant.instance_exec(self, attribute_names) do |anima, names| + # Define anima method + + class << self + undef_method(:anima) if method_defined?(:anima) + end + + define_singleton_method(:anima) { anima } + + # Define instance methods + include InstanceMethods + + names.each do |name| + undef_method(name) if method_defined?(name) + end + + # Define attribute readers + attr_reader(*names) + + # Define equalizer + include Equalizer.new(*names) + end + end + + # Fail unless keys in +attribute_hash+ matches #attribute_names + # + # @param [Class] klass + # the class being initialized + # + # @param [Hash] attribute_hash + # the attributes to initialize +object+ with + # + # @return [undefined] + # + # @raise [Error] + def assert_known_attributes(klass, attribute_hash) + keys = attribute_hash.keys + + unknown = keys - attribute_names + missing = attribute_names - keys + + unless unknown.empty? && missing.empty? + fail Error.new(klass, missing, unknown) + end + end + + # Return new instance + # + # @param [Enumerable] attributes + # + # @return [Anima] + def new(attributes) + self.class.new(*attributes) + end + end # Anima +end # Unparser diff --git a/lib/unparser/anima/attribute.rb b/lib/unparser/anima/attribute.rb new file mode 100644 index 00000000..ff6f0c4b --- /dev/null +++ b/lib/unparser/anima/attribute.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Unparser + class Anima + # An attribute + class Attribute + include Adamantium, Equalizer.new(:name) + + # Initialize attribute + # + # @param [Symbol] name + def initialize(name) + @name = name + @instance_variable_name = :"@#{name}" + end + + # Return attribute name + # + # @return [Symbol] + attr_reader :name + + # Return instance variable name + # + # @return [Symbol] + attr_reader :instance_variable_name + + # Load attribute + # + # @param [Object] object + # @param [Hash] attributes + # + # @return [self] + def load(object, attributes) + set(object, attributes.fetch(name)) + end + + # Get attribute value from object + # + # @param [Object] object + # + # @return [Object] + def get(object) + object.public_send(name) + end + + # Set attribute value in object + # + # @param [Object] object + # @param [Object] value + # + # @return [self] + def set(object, value) + object.instance_variable_set(instance_variable_name, value) + + self + end + end # Attribute + end # Anima +end # Unparser diff --git a/lib/unparser/anima/error.rb b/lib/unparser/anima/error.rb new file mode 100644 index 00000000..f77daa86 --- /dev/null +++ b/lib/unparser/anima/error.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Unparser + class Anima + # Abstract base class for anima errors + class Error < RuntimeError + FORMAT = '%s attributes missing: %s, unknown: %s'.freeze + private_constant(*constants(false)) + + # Initialize object + # + # @param [Class] klass + # the class being initialized + # @param [Enumerable] missing + # @param [Enumerable] unknown + # + # @return [undefined] + def initialize(klass, missing, unknown) + super(format(FORMAT, klass, missing, unknown)) + end + end # Error + end # Anima +end # Unparser diff --git a/lib/unparser/ast.rb b/lib/unparser/ast.rb index ce3d8c11..2a83d4b9 100644 --- a/lib/unparser/ast.rb +++ b/lib/unparser/ast.rb @@ -1,20 +1,37 @@ +# frozen_string_literal: true + module Unparser # Namespace for AST processing tools - # :reek:TooManyConstants - module AST + class AST + include Anima.new(:comments, :explicit_encoding, :node, :static_local_variables) FIRST_CHILD = ->(node) { node.children.first }.freeze - TAUTOLOGY = ->(_node) { true }.freeze - RESET_NODES = [:module, :class, :sclass, :def, :defs].freeze + RESET_NODES = %i[module class sclass def defs].freeze INHERIT_NODES = [:block].freeze CLOSE_NODES = (RESET_NODES + INHERIT_NODES).freeze # Nodes that assign a local variable - # - # FIXME: Kwargs are missing. - # - ASSIGN_NODES = [:lvasgn, :arg, :optarg, :restarg].freeze + ASSIGN_NODES = + %i[ + arg + kwarg + kwoptarg + kwrestarg + lvasgn + optarg + restarg + ].to_set.freeze + + # mutant:disable + def self.from_node(node:) + new( + comments: EMPTY_ARRAY, + explicit_encoding: nil, + node:, + static_local_variables: Set.new + ) + end # Test for local variable inherited scope reset # @@ -42,7 +59,7 @@ def self.not_reset_scope?(node) # Return local variables that get assigned in scope # - # @param [Parser::AST::Node] + # @param [Parser::AST::Node] node # # @return [Set] # @@ -63,6 +80,7 @@ def self.local_variable_assignments(node) # # @api private # + # mutant:disable def self.local_variable_reads(node) Enumerator.new( node, @@ -72,20 +90,7 @@ def self.local_variable_reads(node) # AST enumerator class Enumerator - include Adamantium::Flat, Concord.new(:node, :controller), Enumerable - - # Return new instance - # - # @param [Parser::AST::Node] node - # @param [#call(node)] controller - # - # @return [Enumerator] - # - # @api private - # - def self.new(node, controller = TAUTOLOGY) - super - end + include Adamantium, Concord.new(:node, :controller), Enumerable # Return each node # @@ -97,9 +102,8 @@ def self.new(node, controller = TAUTOLOGY) # # @api private # - def each(&block) - return to_enum unless block_given? - Walker.call(node, controller, &block) + def each(&) + Walker.call(node, controller, &) end # Return nodes selected by types @@ -163,31 +167,29 @@ class Walker # # @param [Parser::AST::Node] node # - # @return [self] - # # @api private # - def self.call(node, controller = TAUTOLOGY, &block) + def self.call(node, controller, &block) new(block, controller).call(node) - self end # Call walker with node # # @param [Parser::AST::Node] node # - # @return [self] + # @return [undefined] # # @api private # def call(node) return unless controller.call(node) + block.call(node) node.children.each do |child| - next unless child.is_a?(Parser::AST::Node) + break unless child.instance_of?(Parser::AST::Node) + call(child) end - self end end # Walker diff --git a/lib/unparser/ast/local_variable_scope.rb b/lib/unparser/ast/local_variable_scope.rb index 57b55da5..8edaf10e 100644 --- a/lib/unparser/ast/local_variable_scope.rb +++ b/lib/unparser/ast/local_variable_scope.rb @@ -1,9 +1,10 @@ -module Unparser - module AST +# frozen_string_literal: true +module Unparser + class AST # Calculated local variable scope for a given node class LocalVariableScope - include Enumerable, Adamantium, Concord.new(:node) + include Adamantium, Anima.new(:static_local_variables, :node) # Initialize object # @@ -11,15 +12,18 @@ class LocalVariableScope # # @return [undefined] # - # @api private - # - def initialize(node) + # mutant:disable + def initialize(*arguments) + super + items = [] - LocalVariableScopeEnumerator.each(node) do |*scope| - items << scope - end + + LocalVariableScopeEnumerator.each( + node: node, + stack: static_local_variables.dup + ) { |*scope| items << scope } + @items = items - super(node) end # Test if local variable was first at given assignment @@ -52,42 +56,38 @@ def local_variable_defined_for_node?(node, name) end end + # mutant:disable + def local_variables_for_node(needle) + @items.each do |node, current| + return current if node.equal?(needle) + end + + Set.new + end + # Test if local variables where first assigned in body and read by conditional # - # @param [Parser::AST::Node] conditional # @param [Parser::AST::Node] body + # @param [Parser::AST::Node] condition # # @api private # - def first_assignment_in_body_and_used_in_condition?(body, condition) - condition_reads = AST.local_variable_reads(condition) + def first_assignment_in?(left, right) + condition_reads = AST.local_variable_reads(right) - candidates = AST.local_variable_assignments(body).select do |node| - name = node.children.first - condition_reads.include?(name) + candidates = AST.local_variable_assignments(left).select do |node| + condition_reads.include?(node.children.first) end - candidates.any? do |node| - first_assignment?(node) - end + candidates.any?(&public_method(:first_assignment?)) end private - # Match node - # - # @param [Parser::AST::Node] node - # if block given - # - # @return [Boolean] - # - # @api private - # - def match(neddle) + def match(needle) @items.each do |node, current, before| - return yield(current, before) if node.equal?(neddle) + return yield(current, before) if node.equal?(needle) end - false end end # LocalVariableScope @@ -102,21 +102,13 @@ class LocalVariableScopeEnumerator # # @api private # - def initialize - @stack = [Set.new] + def initialize(stack:) + @stack = [stack] end # Enumerate each node with its local variable scope - # - # @param [Parser::AST::Node] node - # - # @return [self] - # - # @api private - # - def self.each(node, &block) - new.each(node, &block) - self + def self.each(node:, stack:, &block) + new(stack: stack).each(node: node, &block) end # Enumerate local variable scope scope @@ -129,110 +121,53 @@ def self.each(node, &block) # # @api private # - def each(node, &block) - return to_enum(__method__, node) unless block_given? + def each(node:, &block) visit(node, &block) end private - # Return current set of local variables - # - # @return [Set] - # - # @api private - # def current @stack.last end - # Visit node and record local variable state - # - # @param [Parser::AST::Node] - # - # @return [undefined] - # - # @api private - # def visit(node, &block) before = current.dup enter(node) yield node, current.dup, before node.children.each do |child| - visit(child, &block) if child.is_a?(Parser::AST::Node) + visit(child, &block) if child.instance_of?(Parser::AST::Node) end leave(node) end - # Record local variable state - # - # @param [Parser::AST::Node] - # - # @return [undefined] - # - # @api private - # def enter(node) case node.type when *RESET_NODES push_reset - when *ASSIGN_NODES - define(node.children.first) + when ASSIGN_NODES + value = node.children.first and define(value) when *INHERIT_NODES push_inherit end end - # Pop from local variable state - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # def leave(node) pop if CLOSE_NODES.include?(node.type) end - # Define a local variable on current stack - # - # @param [Symbol] name - # - # @return [undefined] - # - # @api private - # def define(name) current << name end - # Push reset scope on stack - # - # @return [undefined] - # - # @api private - # def push_reset @stack << Set.new end - # Push inherited lvar scope on stack - # - # @return [undefined] - # - # @api private - # def push_inherit @stack << current.dup end - # Pop lvar scope from stack - # - # @return [undefined] - # - # @api private - # def pop @stack.pop end diff --git a/lib/unparser/buffer.rb b/lib/unparser/buffer.rb index 20f2ecfa..989e1d58 100644 --- a/lib/unparser/buffer.rb +++ b/lib/unparser/buffer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Unparser # Buffer used to emit into @@ -12,8 +14,10 @@ class Buffer # @api private # def initialize - @content = '' - @indent = 0 + @content = +'' + @heredocs = [] + @indent = 0 + @no_nl = true end # Append string @@ -32,6 +36,13 @@ def append(string) self end + # Push to heredoc stack + # + # @param [String] heredoc + def push_heredoc(heredoc) + @heredocs << heredoc + end + # Append a string without an indentation prefix # # @param [String] string @@ -42,7 +53,6 @@ def append(string) # def append_without_prefix(string) write(string) - self end # Increase indent @@ -67,6 +77,10 @@ def unindent self end + def ensure_nl + nl unless fresh_line? + end + # Write newline # # @return [self] @@ -74,10 +88,36 @@ def unindent # @api private # def nl + @no_nl = false write(NL) + flush_heredocs self end + # Write final newline + def final_newline + return if fresh_line? || @no_nl + + write(NL) + end + + def nl_flush_heredocs + return if @heredocs.empty? + + if fresh_line? + flush_heredocs + else + nl + end + end + + def root_indent + before = @indent + @indent = 0 + yield + @indent = before + end + # Test for a fresh line # # @return [Boolean] @@ -98,38 +138,33 @@ def content @content.dup.freeze end - # Capture the content written to the buffer within the block + # Write raw fragment to buffer # - # @return [String] + # Does not do indentation logic. # - # @api private + # @param [String] fragment # - def capture_content - size_before = @content.size - yield - @content[size_before..-1] + # @return [self] + def write(fragment) + @content << fragment + self + end + + def write_encoding(encoding) + write("# -*- encoding: #{encoding} -*-\n") end private INDENT_SPACE = ' '.freeze - # Write prefix - # - # @return [String] - # - # @api private - # def prefix write(INDENT_SPACE * @indent) end - # Write to content buffer - # - # @param [String] fragment - # - def write(fragment) - @content << fragment + def flush_heredocs + @heredocs.each(&public_method(:write)) + @heredocs = [] end end # Buffer diff --git a/lib/unparser/cli.rb b/lib/unparser/cli.rb index 43c18612..36402026 100644 --- a/lib/unparser/cli.rb +++ b/lib/unparser/cli.rb @@ -1,30 +1,68 @@ -require 'unparser' -require 'optparse' -require 'diff/lcs' -require 'diff/lcs/hunk' +# frozen_string_literal: true -require 'unparser/cli/source' -require 'unparser/cli/differ' -require 'unparser/cli/color' +require 'pathname' module Unparser # Unparser CLI implementation - # - # :reek:InstanceVariableAssumption class CLI EXIT_SUCCESS = 0 EXIT_FAILURE = 1 + class Target + include AbstractType + + # Path target + class Path < self + include Concord.new(:path) + + # Validation for this target + # + # @return [Validation] + def validation + Validation.from_path(path) + end + + # Literal for this target + # + # @return [Validation] + def literal_validation + Validation::Literal.from_path(path) + end + end + + # String target + class String + include Concord.new(:string) + + # Validation for this target + # + # @return [Validation] + def validation + Validation.from_string(string) + end + + # Literal for this target + # + # @return [Validation] + def literal_validation + Validation::Literal.from_string(path) + end + end # String + end # Target + + private_constant(*constants(false)) + # Run CLI # # @param [Array] arguments # - # @return [Fixnum] + # @return [Integer] # the exit status # # @api private # + # mutant:disable def self.run(*arguments) new(*arguments).exit_status end @@ -36,62 +74,74 @@ def self.run(*arguments) # @return [undefined] # # @api private - # + # mutant:disable def initialize(arguments) - @sources = [] - @ignore = Set.new + @ignore = Set.new + @targets = [] - @success = true - @fail_fast = false - @verbose = false + @fail_fast = false + @start_with = nil + @success = true + @validation = :validation + @verbose = false + @ignore_original_syntax_error = false opts = OptionParser.new do |builder| add_options(builder) end opts.parse!(arguments).each do |name| - @sources.concat(sources(name)) + @targets.concat(targets(name)) end end # Add options # - # @param [Optparse::Builder] builder + # @param [OptionParser] builder # # @return [undefined] # # @api private # + # mutant:disable + # rubocop:disable Metrics/MethodLength def add_options(builder) builder.banner = 'usage: unparse [options] FILE [FILE]' builder.separator('') builder.on('-e', '--evaluate SOURCE') do |source| - @sources << Source::String.new(source) + @targets << Target::String.new(source) end - builder.on('--start-with FILE') do |file| - @start_with = sources(file).first + builder.on('--start-with FILE') do |path| + @start_with = targets(path).first end builder.on('-v', '--verbose') do @verbose = true end + builder.on('-l', '--literal') do + @validation = :literal_validation + end + builder.on('--ignore-original-syntax-error') do + @ignore_original_syntax_error = true + end builder.on('--ignore FILE') do |file| - @ignore.merge(sources(file)) + @ignore.merge(targets(file)) end builder.on('--fail-fast') do @fail_fast = true end end + # rubocop:enable Metrics/MethodLength # Return exit status # - # @return [Fixnum] + # @return [Integer] # # @api private # + # mutant:disable def exit_status - effective_sources.each do |source| - next if @ignore.include?(source) - process_source(source) + effective_targets.each do |target| + process_target(target) break if @fail_fast && !@success end @@ -100,66 +150,54 @@ def exit_status private - # Process source - # - # @param [CLI::Source] - # - # @return [undefined] - # - # @api private - # - def process_source(source) - if source.success? - puts source.report if @verbose - puts "Success: #{source.identification}" + # mutant:disable + def process_target(target) + validation = target.public_send(@validation) + if validation.success? + puts validation.report if @verbose + puts "Success: #{validation.identification}" + elsif ignore_original_syntax_error?(validation) + exception = validation.original_node.from_left + puts "#{exception.class}: #{validation.identification} #{exception}" else - puts source.report - puts "Error: #{source.identification}" + puts validation.report + puts "Error: #{validation.identification}" @success = false end end - # Return effective sources - # - # @return [Enumerable] - # - # @api private - # - def effective_sources + # mutant:disable + def ignore_original_syntax_error?(validation) + @ignore_original_syntax_error && validation.original_node.from_left do + nil + end.instance_of?(Parser::SyntaxError) + end + + # mutant:disable + def effective_targets if @start_with reject = true - @sources.reject do |source| - if reject && source.eql?(@start_with) + @targets.reject do |targets| + if reject && targets.eql?(@start_with) reject = false end reject end else - @sources - end + @targets + end.reject(&@ignore.method(:include?)) end - # Return sources for file name - # - # @param [String] file_name - # - # @return [Enumerable] - # - # @api private - # - def sources(file_name) - files = - if File.directory?(file_name) - Dir.glob(File.join(file_name, '**/*.rb')).sort - elsif File.file?(file_name) - [file_name] - else - Dir.glob(file_name).sort - end - - files.map(&Source::File.method(:new)) + # mutant:disable + def targets(file_name) + if File.directory?(file_name) + Dir.glob(File.join(file_name, '**/*.rb')) + elsif File.file?(file_name) + [file_name] + else + Dir.glob(file_name) + end.map { |file| Target::Path.new(Pathname.new(file)) } end - end # CLI end # Unparser diff --git a/lib/unparser/cli/differ.rb b/lib/unparser/cli/differ.rb deleted file mode 100644 index 61db9cdb..00000000 --- a/lib/unparser/cli/differ.rb +++ /dev/null @@ -1,150 +0,0 @@ -module Unparser - class CLI - # Class to create diffs from source code - class Differ - include Adamantium::Flat, Concord.new(:old, :new), Procto.call(:colorized_diff) - - CONTEXT_LINES = 5 - - # Return new object - # - # @param [String] old - # @param [String] new - # - # @return [Differ] - # - # @api private - # - def self.build(old, new) - new(lines(old), lines(new)) - end - - # Return colorized diff line - # - # @param [String] line - # - # @return [String] - # - # @api private - # - def self.colorize_line(line) - case line[0] - when '+' - Color::GREEN - when '-' - Color::RED - else - Color::NONE - end.format(line) - end - - # Break up source into lines - # - # @param [String] source - # - # @return [Array] - # - # @api private - # - def self.lines(source) - source.lines.map(&:chomp) - end - private_class_method :lines - - # Return hunks - # - # @return [Array] - # - # @api private - # - def hunks - file_length_difference = new.length - old.length - diffs.map do |piece| - hunk = Diff::LCS::Hunk.new(old, new, piece, CONTEXT_LINES, file_length_difference) - file_length_difference = hunk.file_length_difference - hunk - end - end - - # Return collapsed hunks - # - # @return [Enumerable] - # - # @api private - # - def collapsed_hunks - hunks.each_with_object([]) do |hunk, output| - last = output.last - - if last && hunk.merge(last) - output.pop - end - - output << hunk - end - end - - # Return source diff - # - # @return [String] - # if there is a diff - # - # @return [nil] - # otherwise - # - # @api private - # - def diff - output = '' - - collapsed_hunks.each do |hunk| - output << hunk.diff(:unified) << "\n" - end - - output - end - memoize :diff - - # Return colorized source diff - # - # @return [String] - # if there is a diff - # - # @return [nil] - # otherwise - # - # @api private - # - def colorized_diff - diff.lines.map do |line| - self.class.colorize_line(line) - end.join - end - memoize :colorized_diff - - private - - # Return diffs - # - # @return [Array] - # - # @api private - # - def diffs - Diff::LCS.diff(old, new) - end - memoize :diffs - - # Return max length - # - # @return [Fixnum] - # - # @api private - # - def max_length - [old, new].map(&:length).max - end - - end # CLI - end # Differ -end # Unparser diff --git a/lib/unparser/cli/source.rb b/lib/unparser/cli/source.rb deleted file mode 100644 index c10ea635..00000000 --- a/lib/unparser/cli/source.rb +++ /dev/null @@ -1,274 +0,0 @@ -module Unparser - class CLI - # Source representation for CLI sources - class Source - include AbstractType, Adamantium::Flat, NodeHelpers - - # Source state generated after first unparse - class Generated - include Concord::Public.new(:source, :ast, :error) - - # Test if source was generated successfully - # - # @return [Boolean] - # - # @api private - # - def success? - !error - end - - # Build generated source - # - # @param [Parser::AST::Node] - # - # @api private - # - def self.build(ast) - source = Unparser.unparse(ast) - new(source, ast, nil) - rescue => exception - new(nil, ast, exception) - end - end - - # Test if source could be unparsed successfully - # - # @return [Boolean] - # - # @api private - # - def success? - generated.success? && original_ast && generated_ast && original_ast.eql?(generated_ast) - end - - # Return error report - # - # @return [String] - # - # @api private - # - def report - if original_ast && generated_ast - report_with_ast_diff - elsif !original_ast - report_original - elsif !generated.success? - report_unparser - elsif !generated_ast - report_generated - else - raise - end - end - memoize :report - - private - - # Return generated source - # - # @return [String] - # - # @api private - # - def generated - Source::Generated.build(original_ast) - end - memoize :generated - - # Return stripped source - # - # @param [String] string - # - # @return [String] - # - # @api private - # - def strip(source) - source = source.rstrip - indent = source.scan(/^\s*/).first - source.gsub(/^#{indent}/, '') - end - - # Return error report for parsing original - # - # @return [String] - # - # @api private - # - def report_original - strip(<<-MESSAGE) - Parsing of original source failed: - #{original_source} - MESSAGE - end - - # Report unparser bug - # - # @return [String] - # - # @api private - # - def report_unparser - message = ['Unparsing parsed AST failed'] - error = generated.error - message << error - error.backtrace.take(20).each(&message.method(:<<)) - message << 'Original-AST:' - message << original_ast.inspect - message.join("\n") - end - - # Return error report for parsing generated - # - # @return [String] - # - # @api private - # - def report_generated - strip(<<-MESSAGE) - Parsing of generated source failed: - Original-source: - #{original_source} - Original-AST: - #{original_ast.inspect} - Source: - #{generated.source} - MESSAGE - end - - # Return error report with AST difference - # - # @return [String] - # - # @api private - # - def report_with_ast_diff - strip(<<-MESSAGE) - #{ast_diff} - Original-Source:\n#{original_source} - Original-AST:\n#{original_ast.inspect} - Generated-Source:\n#{generated.source} - Generated-AST:\n#{generated_ast.inspect} - MESSAGE - end - - # Return ast diff - # - # @return [String] - # - # @api private - # - def ast_diff - Differ.call( - original_ast.inspect.lines.map(&:chomp), - generated_ast.inspect.lines.map(&:chomp) - ) - end - - # Return generated AST - # - # @return [Parser::AST::Node] - # if parser was sucessful for generated ast - # - # @return [nil] - # otherwise - # - # @api private - # - def generated_ast - generated.success? && Preprocessor.run(parse(generated.source)) - rescue Parser::SyntaxError - nil - end - memoize :generated_ast - - # Return original AST - # - # @return [Parser::AST::Node] - # - # @api private - # - def original_ast - Preprocessor.run(parse(original_source)) - rescue Parser::SyntaxError - nil - end - memoize :original_ast - - # Parse source with current ruby - # - # @param [String] source - # - # @return [Parser::AST::Node] - # - # @api private - # - def parse(source) - Parser::CurrentRuby.parse(source) - end - - # CLI source from string - class String < self - include Concord.new(:original_source) - - # Return identification - # - # @return [String] - # - # @api private - # - def identification - '(string)' - end - - end # String - - # CLI source from file - class File < self - include Concord.new(:file_name) - - # Return identification - # - # @return [String] - # - # @api private - # - def identification - "(#{file_name})" - end - - private - - # Return original source - # - # @return [String] - # - # @api private - # - def original_source - ::File.read(file_name) - end - memoize :original_source - - end # File - - # Source passed in as node - class Node < self - include Concord.new(:original_ast) - - # Return original source - # - # @return [String] - # - # @api private - # - def original_source - Unparser.unparse(original_ast) - end - memoize :original_source - end # Node - - end # Source - end # CLI -end # Unparser diff --git a/lib/unparser/cli/color.rb b/lib/unparser/color.rb similarity index 62% rename from lib/unparser/cli/color.rb rename to lib/unparser/color.rb index d26e40a9..7db7ad50 100644 --- a/lib/unparser/cli/color.rb +++ b/lib/unparser/color.rb @@ -1,16 +1,15 @@ +# frozen_string_literal: true + module Unparser # Class to colorize strings class Color - include Adamantium::Flat, Concord.new(:code) + include Adamantium, Concord.new(:code) # Format text with color # # @param [String] text # # @return [String] - # - # @api private - # def format(text) "\e[#{code}m#{text}\e[0m" end @@ -23,28 +22,23 @@ def format(text) # # @return [String] # the argument string - # - # @api private - # def format(text) text end private - # Initialize null color - # - # @return [undefined] - # - # @api private - # + # Well rubocop you are static so you do not have a clue here ;) + # rubocop:disable Style/RedundantInitialize + # rubocop:disable Lint/MissingSuper def initialize; end + # rubocop:enable Style/RedundantInitialize + # rubocop:enable Lint/MissingSuper end.new RED = Color.new(31) GREEN = Color.new(32) - BLUE = Color.new(34) end # Color end # Unparser diff --git a/lib/unparser/comments.rb b/lib/unparser/comments.rb index af358f86..5a7696bd 100644 --- a/lib/unparser/comments.rb +++ b/lib/unparser/comments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Unparser # Holds the comments that remain to be emitted @@ -52,6 +54,7 @@ def consume(node, source_part = :expression) # def take_eol_comments return EMPTY_ARRAY unless @last_range_consumed + comments = take_up_to_line(@last_range_consumed.end.line) unshift_documents(comments) end @@ -108,39 +111,15 @@ def self.source_range(node, part) private - # Take comments while the provided block returns true - # - # @yield [Parser::Source::Comment] - # - # @return [Array] - # - # @api private - # def take_while number_to_take = @comments.index { |comment| !yield(comment) } || @comments.size @comments.shift(number_to_take) end - # Take comments up to the line number - # - # @param [Fixnum] line - # - # @return [Array] - # - # @api private - # def take_up_to_line(line) take_while { |comment| comment.location.expression.line <= line } end - # Unshift document comments and return the rest - # - # @param [Array] comments - # - # @return [Array] - # - # @api private - # def unshift_documents(comments) doc_comments, other_comments = comments.partition(&:document?) doc_comments.reverse_each { |comment| @comments.unshift(comment) } diff --git a/lib/unparser/concord.rb b/lib/unparser/concord.rb new file mode 100644 index 00000000..f43227dc --- /dev/null +++ b/lib/unparser/concord.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +module Unparser + # A mixin to define a composition + # + # Original code before vendoring and reduction from: https://github.com/mbj/concord. + class Concord < Module + include Adamantium, Equalizer.new(:names) + + # The maximum number of objects the hosting class is composed of + MAX_NR_OF_OBJECTS = 3 + + # Return names + # + # @return [Enumerable] + # + # @api private + # + attr_reader :names + + private + + # Initialize object + # + # @return [undefined] + # + # @api private + # + # rubocop:disable Lint/MissingSuper + def initialize(*names) + if names.length > MAX_NR_OF_OBJECTS + fail "Composition of more than #{MAX_NR_OF_OBJECTS} objects is not allowed" + end + + @names = names + define_initialize + define_readers + define_equalizer + end + # rubocop:enable Lint/MissingSuper + + # Define equalizer + # + # @return [undefined] + # + # @api private + # + def define_equalizer + include(Equalizer.new(*names)) + end + + # Define readers + # + # @return [undefined] + # + # @api private + # + def define_readers + attribute_names = names + attr_reader(*attribute_names) + + protected(*attribute_names) if attribute_names.any? + end + + # Define initialize method + # + # @return [undefined] + # + # @api private + # + # + def define_initialize + ivars = instance_variable_names + size = names.size + + define_method :initialize do |*args| + args_size = args.size + unless args_size.equal?(size) + fail ArgumentError, "wrong number of arguments (#{args_size} for #{size})" + end + + ivars.zip(args) { |ivar, arg| instance_variable_set(ivar, arg) } + end + end + + # Return instance variable names + # + # @return [String] + # + # @api private + # + def instance_variable_names + names.map { |name| "@#{name}" } + end + + # Mixin for public attribute readers + class Public < self + + # Hook called when module is included + # + # @param [Class,Module] descendant + # + # @return [undefined] + # + # @api private + # + def included(descendant) + names.each do |name| + descendant.__send__(:public, name) + end + end + end # Public + end # Concord +end # Unparser diff --git a/lib/unparser/constants.rb b/lib/unparser/constants.rb index 226e1fbf..41a7fb8d 100644 --- a/lib/unparser/constants.rb +++ b/lib/unparser/constants.rb @@ -1,62 +1,20 @@ +# frozen_string_literal: true + module Unparser # All unparser constants maybe included in other libraries. - # - # rubocop:disable MutableConstant - # False positive since constants are frozen dynamically - # to avoid duplication of `.freeze` calls - # - # :reek:TooManyConstants module Constants - # Return frozen symbol set from enumerable - # - # @param [Enumerable] - # - # @return [Set] - # - # @api private - # - def self.symbol_set(enumerable) - enumerable.map(&:to_sym).freeze - end - private_class_method :symbol_set - # All unary operators of the ruby language - UNARY_OPERATORS = symbol_set %w( + UNARY_OPERATORS = %i[ ! ~ -@ +@ - ) + ].to_set.freeze # All binary operators of the ruby language - BINARY_OPERATORS = symbol_set %w( + BINARY_OPERATORS = %i[ + - * / & | && || << >> == === != <= < <=> > >= =~ !~ ^ ** % - ) - - COMMENT = '#' - - WS = ' ' - NL = "\n" - T_DOT = '.' - T_LT = '<' - T_DLT = '<<' - T_AMP = '&' - T_ASN = '=' - T_SPLAT = '*' - T_DSPLAT = '**' - T_ASR = '=>' - T_PIPE = '|' - T_DCL = '::' - T_NEG = '!' - T_OR = '||' - T_AND = '&&' - T_COLON = ':' - - M_PO = '(' - M_PC = ')' - - SNGL_QUOTE = "'" - DBL_QUOTE = '"' + ].to_set.freeze # Keywords K_DO = 'do' @@ -102,13 +60,10 @@ def self.symbol_set(enumerable) K_FILE = '__FILE__' K_THEN = 'then' - DEFAULT_DELIMITER = ', '.freeze - - CURLY_BRACKETS = IceNine.deep_freeze(%w({ })) - KEYWORDS = constants.each_with_object([]) do |name, keywords| value = const_get(name).freeze next unless name.to_s.start_with?('K_') + keywords << value.to_sym end.to_set.freeze diff --git a/lib/unparser/diff.rb b/lib/unparser/diff.rb new file mode 100644 index 00000000..52a29048 --- /dev/null +++ b/lib/unparser/diff.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Unparser + # Class to create diffs from source code + class Diff + include Adamantium, Concord.new(:old, :new) + + ADDITION = '+' + DELETION = '-' + NEWLINE = "\n" + + # Unified source diff between old and new + # + # @return [String] + # if there is exactly one diff + # + # @return [nil] + # otherwise + def diff + return if diffs.empty? + + minimized_hunk.diff(:unified) + NEWLINE + end + memoize :diff + + # Colorized unified source diff between old and new + # + # @return [String] + # if there is a diff + # + # @return [nil] + # otherwise + def colorized_diff + return unless diff + + diff.lines.map(&self.class.method(:colorize_line)).join + end + memoize :colorized_diff + + # Build new object from source strings + # + # @param [String] old + # @param [String] new + # + # @return [Diff] + def self.build(old, new) + new(lines(old), lines(new)) + end + + # Break up source into lines + # + # @param [String] source + # + # @return [Array] + def self.lines(source) + source.lines.map(&:chomp) + end + private_class_method :lines + + private + + def diffs + ::Diff::LCS.diff(old, new) + end + + def hunks + diffs.map do |diff| + ::Diff::LCS::Hunk.new(old.map(&:dup), new, diff, max_length, 0) + end + end + + def minimized_hunk + head, *tail = hunks + + tail.reduce(head) do |left, right| + right.merge(left) + right + end + end + + def max_length + [old, new].map(&:length).max + end + + def self.colorize_line(line) + case line[0] + when ADDITION + Color::GREEN + when DELETION + Color::RED + else + Color::NONE + end.format(line) + end + private_class_method :colorize_line + + end # Diff +end # Unparser diff --git a/lib/unparser/dsl.rb b/lib/unparser/dsl.rb index ca2e48f9..48b2927e 100644 --- a/lib/unparser/dsl.rb +++ b/lib/unparser/dsl.rb @@ -1,17 +1,11 @@ +# frozen_string_literal: true + module Unparser # DSL to help defining emitters module DSL private - # Define remaining children - # - # @param [Enumerable] names - # - # @return [undefined] - # - # @api private - # def define_remaining_children(names) range = names.length..-1 define_method(:remaining_children) do @@ -20,15 +14,6 @@ def define_remaining_children(names) private :remaining_children end - # Define named child - # - # @param [Symbol] name - # @param [Fixnum] index - # - # @return [undefined] - # - # @api private - # def define_child(name, index) define_method(name) do children.at(index) @@ -36,15 +21,6 @@ def define_child(name, index) private name end - # Define a group of children - # - # @param [Symbol] name - # @param [Range] range - # - # @return [undefined] - # - # @pai private - # def define_group(name, range) define_method(name) do children[range] @@ -53,12 +29,6 @@ def define_group(name, range) memoize(name) end - # Create name helpers - # - # @return [undefined] - # - # @api private - # def children(*names) define_remaining_children(names) diff --git a/lib/unparser/either.rb b/lib/unparser/either.rb new file mode 100644 index 00000000..5a35b1cd --- /dev/null +++ b/lib/unparser/either.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +module Unparser + module RequireBlock + + private + + # Raise error unless block is provided + # + # @raise [MissingBlockError] + # if no block is given + # + # @return [self] + def require_block + fail LocalJumpError unless block_given? + + self + end + end # RequireBLock + + class Either + include( + Adamantium, + Concord.new(:value), + RequireBlock + ) + + # Execute block and wrap error in left + # + # @param [Class] exception + # + # @return [Either] + def self.wrap_error(*exceptions) + Right.new(yield) + rescue *exceptions => error + Left.new(error) + end + + # Test for left constructor + # + # @return [Boolean] + def left? + instance_of?(Left) + end + + # Test for right constructor + # + # @return [Boolean] + def right? + instance_of?(Right) + end + + class Left < self + # Evaluate functor block + # + # @return [Either::Left] + def fmap(&) + require_block(&) + end + + # Evaluate applicative block + # + # @return [Either::Left] + def bind(&) + require_block(&) + end + + # Unwrap value from left + # + # @return [Object] + def from_left + value + end + + # Unwrap value from right + # + # @return [Object] + # + def from_right + if block_given? + yield(value) + else + fail "Expected right value, got #{inspect}" + end + end + + # Map over left value + # + # @return [Either::Right] + def lmap + Left.new(yield(value)) + end + + # Evaluate left side of branch + # + # @param [#call] left + # @param [#call] _right + def either(left, _right) + left.call(value) + end + end # Left + + class Right < self + # Evaluate functor block + # + # @return [Either::Right] + def fmap + Right.new(yield(value)) + end + + # Evaluate applicative block + # + # @return [Either] + def bind + yield(value) + end + + # Unwrap value from left + # + # @return [Object] + # + def from_left + if block_given? + yield(value) + else + fail "Expected left value, got #{inspect}" + end + end + + # Unwrap value from right + # + # @return [Object] + def from_right + value + end + + # Map over left value + # + # @return [Either::Right] + def lmap(&) + require_block(&) + end + + # Evaluate right side of branch + # + # @param [#call] _left + # @param [#call] right + def either(_left, right) + right.call(value) + end + end # Right + end # Either +end # Unparser diff --git a/lib/unparser/emitter.rb b/lib/unparser/emitter.rb index 69dda540..e47b1b2e 100644 --- a/lib/unparser/emitter.rb +++ b/lib/unparser/emitter.rb @@ -1,45 +1,30 @@ +# frozen_string_literal: true + module Unparser + UnknownNodeError = Class.new(ArgumentError) # Emitter base class - # - # buggy, argument values are sends to self - # rubocop:disable CircularArgumentReference class Emitter - include Adamantium::Flat, AbstractType, Constants, NodeHelpers - include Concord.new(:node, :parent) - extend DSL - - # Registry for node emitters - REGISTRY = {} # rubocop:disable MutableConstant + include Adamantium, AbstractType, Constants, Generation, NodeHelpers + include Anima.new(:buffer, :comments, :explicit_encoding, :local_variable_scope, :node) - NOINDENT = [:rescue, :ensure].to_set.freeze + public :node - DEFAULT_DELIMITER = ', '.freeze - - CURLY_BRACKETS = IceNine.deep_freeze(%w({ })) + extend DSL - module Unterminated - def terminated? - false - end - end + # Registry for node emitters + REGISTRY = {} # rubocop:disable Style/MutableConstant - module Terminated - def terminated? - true - end - end + NO_INDENT = %i[ensure rescue].freeze module LocalVariableRoot - # Return local variable root # # @return [Parser::AST::Node] # - # @api private - # + # mutant:disable def local_variable_scope - AST::LocalVariableScope.new(node) + AST::LocalVariableScope.new(node: node, static_local_variables: Set.new) end def self.included(descendant) @@ -47,40 +32,15 @@ def self.included(descendant) memoize :local_variable_scope end end - end # LocalVariableRoot - # Return local variable root - # - # @return [Parser::AST::Node] - # - # @api private - # - def local_variable_scope - parent.local_variable_scope - end - - # Return assigned lvars - # - # @return [Array] - # - # @api private - # - abstract_method :local_variables - - # Return node type - # - # @return [Symbol] - # - # @api private - # def node_type node.type end # Register emitter for type # - # @param [Symbol] type + # @param [Symbol] types # # @return [undefined] # @@ -88,34 +48,15 @@ def node_type # def self.handle(*types) types.each do |type| + fail "Handler for type: #{type} already registered" if REGISTRY.key?(type) + REGISTRY[type] = self end end private_class_method :handle - # Trigger write to buffer - # - # @return [self] - # - # @api private - # - def write_to_buffer - emit_comments_before if buffer.fresh_line? + def emit_mlhs dispatch - comments.consume(node) - emit_eof_comments if parent.is_a?(Root) - self - end - memoize :write_to_buffer - - # Emit node - # - # @return [self] - # - # @api private - # - def self.emit(*arguments) - new(*arguments).write_to_buffer end # Return emitter @@ -124,380 +65,30 @@ def self.emit(*arguments) # # @api private # - def self.emitter(node, parent) + # rubocop:disable Metrics/ParameterLists + def self.emitter(buffer:, explicit_encoding:, comments:, node:, local_variable_scope:) type = node.type - klass = REGISTRY.fetch(type) do - raise ArgumentError, "No emitter for node: #{type.inspect}" - end - klass.new(node, parent) - end - - # Dispatch node - # - # @return [undefined] - # - # @api private - # - abstract_method :dispatch - - # Test if node is emitted as terminated expression - # - # @return [Boolean] - # - # @api private - # - abstract_method :terminated? - - protected - - # Return buffer - # - # @return [Buffer] buffer - # - # @api private - # - def buffer - parent.buffer - end - memoize :buffer, freezer: :noop - - # Return comments - # - # @return [Comments] comments - # - # @api private - # - def comments - parent.comments - end - memoize :comments, freezer: :noop - - private - - # Emit contents of block within parentheses - # - # @return [undefined] - # - # @api private - # - def parentheses(open = M_PO, close = M_PC) - write(open) - yield - write(close) - end - - # Visit node - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def visit_plain(node) - emitter = emitter(node) - emitter.write_to_buffer - end - - # Visit ambiguous node - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def visit(node) - emitter = emitter(node) - conditional_parentheses(!emitter.terminated?) do - emitter.write_to_buffer - end - end - - # Visit within parentheses - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def visit_parentheses(node, *arguments) - parentheses(*arguments) do - visit_plain(node) - end - end - - # Call block in optional parentheses - # - # @param [true, false] flag - # - # @return [undefined] - # - # @api private - # - def conditional_parentheses(flag) - if flag - parentheses { yield } - else - yield - end - end - - # Return emitter for node - # - # @param [Parser::AST::Node] node - # - # @return [Emitter] - # - # @api private - # - def emitter(node) - self.class.emitter(node, self) - end - - # Emit delimited body - # - # @param [Enumerable] nodes - # @param [String] delimiter - # - # @return [undefined] - # - # @api private - # - def delimited_plain(nodes, delimiter = DEFAULT_DELIMITER) - delimited(nodes, delimiter, &method(:visit_plain)) - end - - # Emit delimited body - # - # @param [Enumerable] nodes - # @param [String] delimiter - # - # @return [undefined] - # - # @api private - # - def delimited(nodes, delimiter = DEFAULT_DELIMITER, &block) - return if nodes.empty? - block ||= method(:visit) - head, *tail = nodes - block.call(head) - tail.each do |node| - write(delimiter) - block.call(node) - end - end - - # Return children of node - # - # @return [Array] - # - # @api private - # - def children - node.children - end - - # Write newline - # - # @return [undefined] - # - # @api private - # - def nl - emit_eol_comments - buffer.nl - end - - # Write comments that appeared before source_part in the source - # - # @param [Symbol] source_part - # - # @return [undefined] - # - # @api private - # - def emit_comments_before(source_part = :expression) - comments_before = comments.take_before(node, source_part) - return if comments_before.empty? - emit_comments(comments_before) - buffer.nl - end - - # Write end-of-line comments - # - # @return [undefined] - # - # @api private - # - def emit_eol_comments - comments.take_eol_comments.each do |comment| - write(WS, comment.text) - end - end - # Write end-of-file comments - # - # @return [undefined] - # - # @api private - # - def emit_eof_comments - emit_eol_comments - comments_left = comments.take_all - return if comments_left.empty? - buffer.nl - emit_comments(comments_left) - end - - # Write each comment to a separate line - # - # @param [Array] comments - # - # @return [undefined] - # - # @api private - # - def emit_comments(comments) - max = comments.size - 1 - comments.each_with_index do |comment, index| - if comment.type.equal?(:document) - buffer.append_without_prefix(comment.text.chomp) - else - write(comment.text) - end - buffer.nl if index < max - end - end - - # Write strings into buffer - # - # @return [undefined] - # - # @api private - # - def write(*strings) - strings.each do |string| - buffer.append(string) - end - end - - # Write end keyword - # - # @return [undefined] - # - # @api private - # - def k_end - buffer.indent - emit_comments_before(:end) - buffer.unindent - write(K_END) - end - - # Return first child - # - # @return [Parser::AST::Node] - # if present - # - # @return [nil] - # otherwise - # - # @api private - # - def first_child - children.first - end - - # Write whitespace - # - # @return [undefined] - # - # @api private - # - def ws - write(WS) - end - - # Call emit contents of block indented - # - # @return [undefined] - # - # @api private - # - # False positive: - # - # rubocop:disable MethodCallWithoutArgsParentheses - # - def indented - buffer = buffer() - buffer.indent - nl - yield - nl - buffer.unindent - end - - # Emit non nil body - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def emit_body(body = body()) - unless body - buffer.indent - nl - buffer.unindent - return - end - visit_indented(body) - end - - # Visit indented node - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def visit_indented(node) - if NOINDENT.include?(node.type) - visit_plain(node) - else - indented { visit_plain(node) } + klass = REGISTRY.fetch(type) do + fail UnknownNodeError, "Unknown node type: #{type.inspect}" end - end - # Return parent type - # - # @return [Symbol] - # if parent is present - # - # @return [nil] - # otherwise - # - # @api private - # - def parent_type - parent.node_type + klass.new( + buffer:, + comments:, + explicit_encoding:, + local_variable_scope:, + node: + ) end + # rubocop:enable Metrics/ParameterLists - # Delegate to emitter - # - # @param [Class:Emitter] emitter + # Dispatch node write as statement # # @return [undefined] # # @api private # - def run(emitter, node = node()) - emitter.new(node, self).write_to_buffer - end - + abstract_method :dispatch end # Emitter end # Unparser diff --git a/lib/unparser/emitter/alias.rb b/lib/unparser/emitter/alias.rb index 2f102b40..9b9189d5 100644 --- a/lib/unparser/emitter/alias.rb +++ b/lib/unparser/emitter/alias.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for alias nodes @@ -9,16 +11,10 @@ class Alias < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_ALIAS, WS) + write('alias ') visit(target) - write(WS) + ws visit(source) end diff --git a/lib/unparser/emitter/args.rb b/lib/unparser/emitter/args.rb new file mode 100644 index 00000000..7f28ff22 --- /dev/null +++ b/lib/unparser/emitter/args.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Arguments emitter + class Args < self + def emit_block_arguments + delimited(normal_arguments) + + write(',') if normal_arguments.one? && n_arg?(normal_arguments.first) + + emit_shadowargs + end + + def emit_def_arguments + if children.one? && n_mlhs?(Util.one(children)) + emitter(Util.one(children)).dispatch_def + else + delimited(normal_arguments) + end + end + + def emit_lambda_arguments + delimited(normal_arguments) + emit_shadowargs + end + + private + + def emit_shadowargs + return if shadowargs.empty? + + write('; ') + delimited(shadowargs) + end + + def normal_arguments + children.reject(&method(:n_shadowarg?)) + end + memoize :normal_arguments + + def shadowargs + children.select(&method(:n_shadowarg?)) + end + memoize :shadowargs + + end # Arguments + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/argument.rb b/lib/unparser/emitter/argument.rb index 473e346e..0a47bfd3 100644 --- a/lib/unparser/emitter/argument.rb +++ b/lib/unparser/emitter/argument.rb @@ -1,94 +1,22 @@ +# frozen_string_literal: true + module Unparser class Emitter - - # Arg expr (pattern args) emitter - class ArgExpr < self - - handle :arg_expr - - children :body - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - visit_parentheses(body) - end - end # ArgExpr - - # Arguments emitter - class Arguments < self - include Terminated - - handle :args - - SHADOWARGS = ->(node) { node.type.equal?(:shadowarg) }.freeze - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - delimited(normal_arguments) - return if shadowargs.empty? - write('; ') - delimited(shadowargs) - end - - # Return normal arguments - # - # @return [Enumerable] - # - # @api private - # - def normal_arguments - children.reject(&SHADOWARGS) - end - - # Return shadow args - # - # @return [Enumerable] - # - # @api private - # - def shadowargs - children.select(&SHADOWARGS) - end - memoize :shadowargs - - end # Arguments - - # Emitter for block and kwrestarg arguments - class Morearg < self - include Terminated - + # Emitter for forwarding arguments + class ForwardArg < self MAP = { - blockarg: T_AMP, - kwrestarg: T_DSPLAT + blockarg: '&', + forwarded_kwrestarg: '**', + forwarded_restarg: '*', + kwrestarg: '**' }.freeze - handle :blockarg - handle :kwrestarg + handle(*MAP.keys) children :name private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch write(MAP.fetch(node_type), name.to_s) end @@ -97,44 +25,28 @@ def dispatch # Optional argument emitter class Optarg < self - include Terminated - handle :optarg children :name, :value private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(name.to_s, WS, T_ASN, WS) + write(name.to_s, ' = ') visit(value) end end # Optional keyword argument emitter class KeywordOptional < self - include Terminated - handle :kwoptarg children :name, :value private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(name.to_s, T_COLON, WS) + write(name.to_s, ': ') visit(value) end @@ -142,89 +54,82 @@ def dispatch # Keyword argument emitter class Kwarg < self - include Terminated - handle :kwarg children :name private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(name.to_s, T_COLON) + write(name.to_s, ':') end end # Restarg # Rest argument emitter class Restarg < self - include Terminated - handle :restarg children :name private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(T_SPLAT, name.to_s) + write('*', name.to_s) end end # Restarg # Argument emitter class Argument < self - include Terminated - handle :arg, :shadowarg children :name private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch write(name.to_s) end end # Argument + # Progarg emitter + class Procarg < self + handle :procarg0 + + PARENS = %i[restarg mlhs].freeze + + private + + def dispatch + if needs_parens? + parentheses do + delimited(children) + end + else + delimited(children) + end + end + + def needs_parens? + children.length > 1 || children.any? do |node| + PARENS.include?(node.type) + end + end + end + # Block pass node emitter class BlockPass < self - include Terminated - handle :block_pass children :name private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(T_AMP) - visit(name) + write('&') + visit(name) if name end end # BlockPass diff --git a/lib/unparser/emitter/array.rb b/lib/unparser/emitter/array.rb new file mode 100644 index 00000000..8be9ee78 --- /dev/null +++ b/lib/unparser/emitter/array.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Array literal emitter + class Array < self + handle :array + + private + + def dispatch + parentheses('[', ']') do + delimited(emitters, &:write_to_buffer) + end + end + + def emitters + children.map(&method(:emitter)) + end + memoize :emitters + end # Array + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/array_pattern.rb b/lib/unparser/emitter/array_pattern.rb new file mode 100644 index 00000000..994224ae --- /dev/null +++ b/lib/unparser/emitter/array_pattern.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for array patterns + class ArrayPattern < self + + handle :array_pattern + handle :array_pattern_with_tail + + private + + def dispatch + write('[') + delimited(children) + write(', ') if node_type.equal?(:array_pattern_with_tail) + write(']') + end + end # Pin + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/assignment.rb b/lib/unparser/emitter/assignment.rb index edaf4a06..7c173b7f 100644 --- a/lib/unparser/emitter/assignment.rb +++ b/lib/unparser/emitter/assignment.rb @@ -1,176 +1,85 @@ +# frozen_string_literal: true + module Unparser class Emitter # Base class for assignment emitters class Assignment < self + BINARY_OPERATOR = %i[and or].freeze + + def symbol_name + true + end private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch emit_left emit_right end - # Single assignment emitter - class Single < self - - # Test for terminated emit - # - # @return [Boolean] - # - # @api private - # - def terminated? - right_node.nil? - end + def emit_right + return unless right - private + write(' = ') - # Emit right - # - # @return [undefined] - # - # @api private - # - def emit_right - right = right_node - return unless right - write(WS, T_ASN, WS) - visit(right) + if BINARY_OPERATOR.include?(right.type) + writer_with(Writer::Binary, node: right).emit_operator + elsif n_array?(right) + emit_array + else + right_emitter.write_to_buffer end + end - abstract_method :emit_left - - # Variable assignment emitter - class Variable < self - - handle :lvasgn, :ivasgn, :cvasgn, :gvasgn - - children :name, :right_node - - private - - # Emit left - # - # @return [undefined] - # - # @api private - # - def emit_left - write(name.to_s) - end - - end # Variable - - # Constant assignment emitter - class Constant < self - - handle :casgn - - children :base, :name, :right_node - - private + def emit_array + if right.children.size > 1 + delimited(right.children) + else + right_emitter.write_to_buffer + end + end - # Emit left - # - # @return [undefined] - # - # @api private - # - def emit_left - if base - visit(base) - write(T_DCL) if base.type != :cbase - end - write(name.to_s) - end + def right_emitter + emitter(right) + end + memoize :right_emitter - end # Constant - end # Single + abstract_method :emit_left - # Multiple assignment - class Multiple < self - include Unterminated + # Variable assignment emitter + class Variable < self - handle :masgn + handle :lvasgn, :ivasgn, :cvasgn, :gvasgn - PARENS = IceNine.deep_freeze(%w([ ])) + children :name, :right private - # Emit left - # - # @return [undefined] - # - # @api private - # def emit_left - visit_plain(first_child) + write(name.to_s) end - # Emit right - # - # @return [undefined] - # - # @api private - # - def emit_right - write(WS, T_ASN, WS) - right = children.last - case right.type - when :array - children = right.children - parentheses(*PARENS) do - delimited(children) - end - else - visit(right) - end - end + end # Variable - end # Multiple + # Constant assignment emitter + class Constant < self - # Emitter for multiple assignment left hand side - class MLHS < Emitter - include Unterminated + handle :casgn - handle :mlhs + children :base, :name, :right private - NO_COMMA = [:splat, :restarg].to_set.freeze - PARENT_MLHS = [:mlhs, :masgn].freeze - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - delimited(children) - - write(',') if children.one? && mlhs? - end - - # Test for mlhs context - # - # @return [undefined] - # - # @api private - # - def mlhs? - !NO_COMMA.include?(first_child.type) && PARENT_MLHS.include?(parent_type) + def emit_left + if base + visit(base) + write('::') unless n_cbase?(base) + end + write(name.to_s) end - end # MLHS - + end # Constant end # Assignment end # Emitter end # Unparser diff --git a/lib/unparser/emitter/begin.rb b/lib/unparser/emitter/begin.rb index 701935ff..bc423b12 100644 --- a/lib/unparser/emitter/begin.rb +++ b/lib/unparser/emitter/begin.rb @@ -1,99 +1,20 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for begin nodes class Begin < self - + handle :begin children :body private - # Emit inner nodes - # - # @return [undefined] - # - # @api private - # - def emit_inner - children.each_with_index do |child, index| - visit_plain(child) - write(NL) if index < children.length - 1 + def dispatch + parentheses do + delimited(children, '; ') end end - - # Emitter for implicit begins - class Implicit < self - - handle :begin - - # Test if begin is terminated - # - # @return [Boolean] - # - # @api private - # - def terminated? - children.empty? - end - - TERMINATING_PARENT = %i(root interpolated dyn_str_body).to_set.freeze - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - if terminated? && !TERMINATING_PARENT.include?(parent_type) - write('()') - else - emit_inner - end - end - - end # Implicit - - # Emitter for explicit begins - class Explicit < self - include Terminated - - handle :kwbegin - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(K_BEGIN) - emit_body - k_end - end - - # Emit body - # - # @return [undefined] - # - # @api private - # - def emit_body - if body.nil? - nl - elsif NOINDENT.include?(body.type) - emit_inner - else - indented { emit_inner } - end - end - - end # Explicit - end # Begin end # Emitter end # Unparser diff --git a/lib/unparser/emitter/binary.rb b/lib/unparser/emitter/binary.rb index 218c59ce..27345c6b 100644 --- a/lib/unparser/emitter/binary.rb +++ b/lib/unparser/emitter/binary.rb @@ -1,32 +1,21 @@ +# frozen_string_literal: true + module Unparser class Emitter - # Base class for binary emitters + # Non send binary operator / keyword emitter class Binary < self - include Unterminated - - children :left, :right - - MAP = { - or: T_OR, - and: T_AND - }.freeze - - handle(*MAP.keys) + handle :and, :or private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - visit(left) - write(WS, MAP.fetch(node.type), WS) - visit(right) + writer.dispatch end + def writer + writer_with(Writer::Binary, node:) + end + memoize :writer end # Binary end # Emitter end # Unparser diff --git a/lib/unparser/emitter/block.rb b/lib/unparser/emitter/block.rb index 75f940e0..ad2172b0 100644 --- a/lib/unparser/emitter/block.rb +++ b/lib/unparser/emitter/block.rb @@ -1,41 +1,92 @@ +# frozen_string_literal: true + module Unparser class Emitter # Block emitter class Block < self - include Terminated - - handle :block + handle :block, :numblock, :itblock - children :send, :arguments, :body + children :target, :arguments, :body private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - visit(send) - write(WS, K_DO) - comments.consume(node, :begin) - emit_block_arguments - emit_body - k_end - end - - # Emit arguments - # - # @return [undefined] - # - # @api private - # + emit_target + ws + write_open + emit_block_arguments unless n_lambda?(target) + emit_optional_body_ensure_rescue(body) + write_close + end + + def need_do? + body && (n_rescue?(body) || n_ensure?(body)) + end + + def write_open + if need_do? + write('do') + else + write('{') + end + end + + def write_close + if need_do? + k_end + else + write('}') + end + end + + def target_writer + writer_with(Writer::Send::Regular, node: target) + end + memoize :target_writer + + def emit_target + case target.type + when :send + emit_send_target + when :lambda + visit(target) + emit_lambda_arguments unless node.type.equal?(:numblock) + else + visit(target) + end + end + + def emit_send_target + target_writer.emit_receiver + target_writer.emit_selector + target_writer.emit_arguments_without_heredoc_body + end + + def emit_lambda_arguments + parentheses { writer_with(Args, node: arguments).emit_lambda_arguments } + end + + def numblock? + node.type.equal?(:numblock) + end + + # NOTE: mutant fails on Ruby < 3.4 + # mutant:disable + def itblock? + node.type.equal?(:itblock) + end + + # NOTE: mutant fails on Ruby < 3.4 + # mutant:disable def emit_block_arguments - return if arguments.children.empty? + return if numblock? || itblock? || arguments.children.empty? + ws - visit_parentheses(arguments, T_PIPE, T_PIPE) + + parentheses('|', '|') do + writer_with(Args, node: arguments).emit_block_arguments + end end end # Block diff --git a/lib/unparser/emitter/case.rb b/lib/unparser/emitter/case.rb index 39264ba6..6d0a511f 100644 --- a/lib/unparser/emitter/case.rb +++ b/lib/unparser/emitter/case.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for case nodes class Case < self - include Terminated - handle :case children :condition @@ -11,87 +11,49 @@ class Case < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_CASE) + write('case') emit_condition emit_whens emit_else k_end end - # Emit else - # - # @return [undefined] - # - # @api private - # def emit_else else_branch = children.last return unless else_branch - write(K_ELSE) - visit_indented(else_branch) + + write('else') + emit_body(else_branch) end - # Emit whens - # - # @return [undefined] - # - # @api private - # def emit_whens nl whens.each(&method(:visit)) end - # Emit condition - # - # @return [undefined] - # - # @api private - # def emit_condition return unless condition - write(WS) + + ws visit(condition) end - end # Case # Emitter for when nodes class When < self - include Terminated - handle :when define_group :captures, 0..-2 private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_WHEN, WS) + write('when ') emit_captures - body = children.last - emit_body(body) + emit_optional_body(children.last) end - # Emit captures - # - # @return [undefined] - # - # @api private - # def emit_captures delimited(captures) end diff --git a/lib/unparser/emitter/case_guard.rb b/lib/unparser/emitter/case_guard.rb new file mode 100644 index 00000000..b507a9e8 --- /dev/null +++ b/lib/unparser/emitter/case_guard.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for case guards + class CaseGuard < self + + handle :if_guard, :unless_guard + + MAP = { + if_guard: 'if', + unless_guard: 'unless' + }.freeze + + children :condition + + private + + def dispatch + write(MAP.fetch(node_type)) + ws + visit(condition) + end + + end # UnlessGuard + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/case_match.rb b/lib/unparser/emitter/case_match.rb new file mode 100644 index 00000000..5714f2e5 --- /dev/null +++ b/lib/unparser/emitter/case_match.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for case matches + class CaseMatch < self + + handle :case_match + + children :target + + define_group :patterns, 1..-2 + + private + + def else_branch + children.last + end + + def dispatch + write('case ') + visit(target) + nl + patterns.each(&method(:visit)) + nl unless buffer.fresh_line? + emit_else_branch + k_end + end + + def emit_else_branch + if else_branch + write('else') + emit_body(else_branch) unless n_empty_else?(else_branch) + nl unless buffer.fresh_line? + end + end + + end # CaseMatch + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/cbase.rb b/lib/unparser/emitter/cbase.rb index d52c99e9..219c87a9 100644 --- a/lib/unparser/emitter/cbase.rb +++ b/lib/unparser/emitter/cbase.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for toplevel constant reference nodes class CBase < self - include Terminated - handle :cbase private @@ -15,7 +15,7 @@ class CBase < self # @api private # def dispatch - write(T_DCL) + write('::') end end # CBase diff --git a/lib/unparser/emitter/class.rb b/lib/unparser/emitter/class.rb index deaecbcf..ccdd26bf 100644 --- a/lib/unparser/emitter/class.rb +++ b/lib/unparser/emitter/class.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for class nodes class Class < self - include LocalVariableRoot, Terminated + include LocalVariableRoot handle :class @@ -10,29 +12,18 @@ class Class < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_CLASS, WS) + write('class ') visit(name) emit_superclass - emit_body + emit_optional_body(body) k_end end - # Emit superclass - # - # @return [undefined] - # - # @api private - # def emit_superclass return unless superclass - write(WS, T_LT, WS) + + write(' < ') visit(superclass) end @@ -40,24 +31,16 @@ def emit_superclass # Emitter for sclass nodes class SClass < self - include Terminated - handle :sclass children :object, :body private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_CLASS, WS, T_DLT, WS) + write('class << ') visit(object) - emit_body + emit_optional_body(body) k_end end diff --git a/lib/unparser/emitter/const_pattern.rb b/lib/unparser/emitter/const_pattern.rb new file mode 100644 index 00000000..f5fcce46 --- /dev/null +++ b/lib/unparser/emitter/const_pattern.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for const pattern node + class ConstPattern < self + + handle :const_pattern + + children :const, :pattern + + private + + def dispatch + visit(const) + if n_hash_pattern?(pattern) + emitter(pattern).emit_const_pattern + else + visit(pattern) + end + end + end # ConstPattern + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/def.rb b/lib/unparser/emitter/def.rb index f99b4b1b..284ad884 100644 --- a/lib/unparser/emitter/def.rb +++ b/lib/unparser/emitter/def.rb @@ -1,70 +1,43 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for def node class Def < self - include LocalVariableRoot, Terminated + include LocalVariableRoot private - # Emit name - # - # @return [undefined] - # - # @api private - # abstract_method :emit_name private :emit_name - # Return body node - # - # @return [Parser::AST::Node] - # - # @api private - # abstract_method :body private :body - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_DEF, WS) + write('def ') emit_name - comments.consume(node, :name) emit_arguments - emit_body + emit_optional_body_ensure_rescue(body) k_end end - # Emit arguments - # - # @return [undefined] - # - # @api private - # def emit_arguments return if arguments.children.empty? - visit_parentheses(arguments) + + parentheses do + writer_with(Args, node: arguments).emit_def_arguments + end end # Instance def emitter class Instance < self - handle :def children :name, :arguments, :body private - # Emit name - # - # @return [undefined] - # - # @api private - # def emit_name write(name.to_s) end @@ -80,25 +53,13 @@ class Singleton < self private - # Return mame - # - # @return [String] - # - # @api private - # def emit_name conditional_parentheses(!subject_without_parens?) do visit(subject) end - write(T_DOT, name.to_s) + write('.', name.to_s) end - # Test if subject needs parentheses - # - # @return [Boolean] - # - # @api private - # def subject_without_parens? case subject.type when :self @@ -108,8 +69,6 @@ def subject_without_parens? when :send receiver, _selector, *arguments = *subject !receiver && arguments.empty? - else - false end end diff --git a/lib/unparser/emitter/defined.rb b/lib/unparser/emitter/defined.rb index b593c2ff..c848741a 100644 --- a/lib/unparser/emitter/defined.rb +++ b/lib/unparser/emitter/defined.rb @@ -1,26 +1,18 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for defined? nodes class Defined < self - include Terminated - handle :defined? children :subject private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_DEFINED) - parentheses do - visit(subject) - end + write('defined?') + parentheses { visit(subject) } end end # Defined diff --git a/lib/unparser/emitter/dstr.rb b/lib/unparser/emitter/dstr.rb new file mode 100644 index 00000000..e1356094 --- /dev/null +++ b/lib/unparser/emitter/dstr.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Dynamic string emitter + class DStr < self + + handle :dstr + + private + + def dispatch + dstr_writer.dispatch + end + + def dstr_writer + writer_with(Writer::DynamicString, node:) + end + memoize :dstr_writer + + end # DStr + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/dsym.rb b/lib/unparser/emitter/dsym.rb new file mode 100644 index 00000000..8b01cbed --- /dev/null +++ b/lib/unparser/emitter/dsym.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Dynamic symbol literal emitter + class DSym < self + handle :dsym + + private + + def dispatch + write(':"') + children.each do |child| + case child.type + when :str + emit_str_child(child) + when :begin + emit_begin_child(child) + end + end + write('"') + end + + def emit_str_child(value) + string = value.children.first + if string.end_with?("\n") + write(string.inspect[1..-4]) + nl + else + write(string.inspect[1..-2]) + end + end + + def emit_begin_child(component) + write('#{') + visit(Util.one(component.children)) if component.children.any? + write('}') + end + end # DSym + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/empty.rb b/lib/unparser/emitter/empty.rb deleted file mode 100644 index bc1fd6cc..00000000 --- a/lib/unparser/emitter/empty.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Unparser - class Emitter - - # Emitter for artifical empty node - class Empty < self - - handle :empty - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch; end - - end - end -end # Unparser diff --git a/lib/unparser/emitter/ensure.rb b/lib/unparser/emitter/ensure.rb index 276ff5f1..747e7800 100644 --- a/lib/unparser/emitter/ensure.rb +++ b/lib/unparser/emitter/ensure.rb @@ -1,35 +1,16 @@ +# frozen_string_literal: true + module Unparser class Emitter - # Emitter for ensure nodes class Ensure < self - handle :ensure - children :body, :ensure_body - private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - if body - visit_indented(body) - else - nl - end - write(K_ENSURE) - if ensure_body - visit_indented(ensure_body) - else - nl - end + emit_ensure(node) end - end # Ensure end # Emitter end # Unparser diff --git a/lib/unparser/emitter/find_pattern.rb b/lib/unparser/emitter/find_pattern.rb new file mode 100644 index 00000000..751519b7 --- /dev/null +++ b/lib/unparser/emitter/find_pattern.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for in pattern nodes + class FindPattern < self + handle :find_pattern + + private + + def dispatch + write('[') + delimited(children) + write(']') + end + end # FindPattern + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/flipflop.rb b/lib/unparser/emitter/flipflop.rb index 7736d902..bd93df9d 100644 --- a/lib/unparser/emitter/flipflop.rb +++ b/lib/unparser/emitter/flipflop.rb @@ -1,13 +1,22 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for flip flops class FlipFlop < self - include Unterminated - - MAP = IceNine.deep_freeze( + MAP = { iflipflop: '..', eflipflop: '...' - ).freeze + }.freeze + + SYMBOLS = { + eflipflop: :tDOT3, + iflipflop: :tDOT2 + }.freeze + + def symbol_name + true + end handle(*MAP.keys) @@ -15,16 +24,15 @@ class FlipFlop < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - visit(left) + visit(left) if left write(MAP.fetch(node.type)) - visit(right) + + if right + visit(right) + else + write(';') + end end end # FlipFLop end # Emitter diff --git a/lib/unparser/emitter/float.rb b/lib/unparser/emitter/float.rb new file mode 100644 index 00000000..e03c934c --- /dev/null +++ b/lib/unparser/emitter/float.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emiter for float literals + class Float < self + handle :float + + children :value + + INFINITY = ::Float::INFINITY + NEG_INFINITY = -::Float::INFINITY + + private + + def dispatch + case value + when INFINITY + write('10e1000000000000000000') + when NEG_INFINITY + write('-10e1000000000000000000') + else + write(value.inspect) + end + end + + end # Float + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/flow_modifier.rb b/lib/unparser/emitter/flow_modifier.rb index 104eba47..5c646af9 100644 --- a/lib/unparser/emitter/flow_modifier.rb +++ b/lib/unparser/emitter/flow_modifier.rb @@ -1,78 +1,36 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter control flow modifiers class FlowModifier < self - MAP = { - return: K_RETURN, - next: K_NEXT, - break: K_BREAK + return: 'return', + next: 'next', + break: 'break' }.freeze - handle(*MAP.keys) + private_constant(*constants(false)) - def terminated? - children.empty? - end + handle(*MAP.keys) private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch write(MAP.fetch(node.type)) - case children.length - when 0 # rubocop:disable Lint/EmptyWhen - when 1 - emit_single_argument + + if children.one? && n_if?(children.first) + ws + emitter(Util.one(children)).emit_ternary else - emit_arguments + emit_arguments unless children.empty? end end - # Emit break or return arguments - # - # @return [undefined] - # - # @api private - # def emit_arguments ws - head, *tail = children - emit_argument(head) - tail.each do |node| - write(DEFAULT_DELIMITER) - emit_argument(node) - end + delimited(children) end - - PARENS = [:if, :case, :begin].to_set.freeze - - # Emit argument - # - # @param [Parser::AST::Node] node - # - # @api private - # - def emit_argument(node) - visit_plain(node) - end - - # Emit single argument - # - # @api private - # - def emit_single_argument - ws - conditional_parentheses(PARENS.include?(first_child.type)) do - visit_plain(first_child) - end - end - end # Return end # Emitter end # Unparser diff --git a/lib/unparser/emitter/for.rb b/lib/unparser/emitter/for.rb index f5d68e4e..12d34e44 100644 --- a/lib/unparser/emitter/for.rb +++ b/lib/unparser/emitter/for.rb @@ -1,39 +1,27 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for for nodes class For < self - include Terminated - handle :for children :condition, :assignment, :body private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_FOR, WS) + write('for ') emit_condition - emit_body + emit_optional_body(body) k_end end - # Emit assignment - # - # @return [undefined] - # - # @api private - # def emit_condition - visit_plain(condition) - write(WS, K_IN, WS) + emitter(condition).emit_mlhs + write(' in ') visit(assignment) - write(WS, K_DO) + write(' do') end end # For diff --git a/lib/unparser/emitter/hash.rb b/lib/unparser/emitter/hash.rb new file mode 100644 index 00000000..91e9ed9e --- /dev/null +++ b/lib/unparser/emitter/hash.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for Hash literals + class Hash < self + handle :hash + + private + + def dispatch + if children.empty? + write('{}') + else + parentheses('{', '}') do + write(' ') + emit_hash_body + write(' ') + end + end + end + + def emit_hash_body + delimited(children) + end + end # Hash + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/hash_pattern.rb b/lib/unparser/emitter/hash_pattern.rb new file mode 100644 index 00000000..614aa782 --- /dev/null +++ b/lib/unparser/emitter/hash_pattern.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for hash patterns + class HashPattern < self + + handle :hash_pattern + + def emit_const_pattern + parentheses do + emit_hash_body + end + end + + private + + def dispatch + parentheses('{', '}') do + emit_hash_body + end + end + + def emit_hash_body + delimited(children, &method(:emit_member)) + end + + def emit_member(node) + case node.type + when :pair + emit_pair(node) + when :match_var + emit_match_var(node) + when :match_rest + writer_with(MatchRest, node:).emit_hash_pattern + else + visit(node) + end + end + + def emit_match_var(node) + write_symbol_body(node.children.first) + write(':') + end + + def emit_pair(node) + key, value = node.children + + if n_sym?(key) + write_symbol_body(key.children.first) + else + visit(s(:dstr, *key)) + end + + write(':') + + ws + + visit(value) + end + + def write_symbol_body(symbol) + write(symbol.inspect[1..]) + end + end # Pin + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/hookexe.rb b/lib/unparser/emitter/hookexe.rb index d18074fe..de2a2e8d 100644 --- a/lib/unparser/emitter/hookexe.rb +++ b/lib/unparser/emitter/hookexe.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true + module Unparser class Emitter # Base class for pre and postexe emitters class Hookexe < self MAP = { - preexe: K_PREEXE, - postexe: K_POSTEXE + preexe: 'BEGIN', + postexe: 'END' }.freeze handle(*MAP.keys) @@ -14,16 +16,10 @@ class Hookexe < self private - # Perfrom dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(MAP.fetch(node.type), WS) - parentheses(*CURLY_BRACKETS) do - emit_body + write(MAP.fetch(node.type), ' ') + parentheses('{', '}') do + emit_body(body) end end diff --git a/lib/unparser/emitter/if.rb b/lib/unparser/emitter/if.rb index a1cd4221..4919a0f7 100644 --- a/lib/unparser/emitter/if.rb +++ b/lib/unparser/emitter/if.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter if nodes @@ -6,18 +8,16 @@ class If < self children :condition, :if_branch, :else_branch - def terminated? - !postcondition? + def emit_ternary + visit(condition) + write(' ? ') + visit(if_branch) + write(' : ') + visit(else_branch) end private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch if postcondition? emit_postcondition @@ -26,104 +26,53 @@ def dispatch end end - # Test for postcondition - # - # @return [Boolean] - # - # @api private - # def postcondition? return false unless if_branch.nil? ^ else_branch.nil? body = if_branch || else_branch - local_variable_scope.first_assignment_in_body_and_used_in_condition?(body, condition) + local_variable_scope.first_assignment_in?(body, condition) end - # Emit in postcondition style - # - # @return [undefined] - # - # @api private - # def emit_postcondition - visit_plain(if_branch || else_branch) - write(WS, keyword, WS) + visit(if_branch || else_branch) + write(' ', keyword, ' ') emit_condition end - # Emit in normal style - # - # @return [undefined] - # - # @api private - # def emit_normal - write(keyword, WS) + write(keyword, ' ') emit_condition emit_if_branch emit_else_branch k_end end - # Test if AST can be emitted as unless - # - # @return [Boolean] - # - # @api private - # def unless? !if_branch && else_branch end - # Return keyword - # - # @return [String] - # - # @api private - # def keyword - unless? ? K_UNLESS : K_IF + unless? ? 'unless' : 'if' end - # Emit condition - # - # @return [undefined] - # - # @api private - # def emit_condition - if condition.type.equal?(:match_current_line) - visit_plain(condition) - else - visit(condition) - end + visit(condition) end - # Emit if branch - # - # @return [undefined] - # - # @api private - # def emit_if_branch if if_branch - visit_indented(if_branch) + emit_body(if_branch) end nl if !if_branch && !else_branch end - # Emit else branch - # - # @return [undefined] - # - # @api private - # def emit_else_branch return unless else_branch - write(K_ELSE) unless unless? - visit_indented(else_branch) + + write('else') unless unless? + emit_body(else_branch) end end # If diff --git a/lib/unparser/emitter/in_match.rb b/lib/unparser/emitter/in_match.rb new file mode 100644 index 00000000..5027fa0e --- /dev/null +++ b/lib/unparser/emitter/in_match.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for in pattern nodes + class InMatch < self + + handle :in_match + + children :target, :pattern + + private + + def dispatch + visit(target) + write(' in ') + visit(pattern) + end + end # InMatch + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/in_pattern.rb b/lib/unparser/emitter/in_pattern.rb new file mode 100644 index 00000000..2f97ed51 --- /dev/null +++ b/lib/unparser/emitter/in_pattern.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for in pattern nodes + class InPattern < self + + handle :in_pattern + + children :target, :unless_guard, :branch, :else_branch + + private + + def dispatch + write('in') + + ws + + dispatch_target(target) + + if unless_guard + ws + visit(unless_guard) + end + + if branch + ws + write('then') + emit_body(branch) + else + nl + end + end + + def dispatch_target(target) + if n_array?(target) + writer_with(Writer::Array, node: target).emit_compact + else + visit(target) + end + end + end # InPattern + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/index.rb b/lib/unparser/emitter/index.rb new file mode 100644 index 00000000..9a90cdf4 --- /dev/null +++ b/lib/unparser/emitter/index.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for send to index references + class Index < self + + private + + def dispatch + emit_receiver + emit_operation + end + + def emit_receiver + visit(first_child) + end + + class Reference < self + define_group(:indices, 1..-1) + + handle :index + + private + + def emit_operation + parentheses('[', ']') do + delimited(indices) + end + end + end # Reference + + # Emitter for assign to index nodes + class Assign < self + + handle :indexasgn + + VALUE_RANGE = (1..-2).freeze + NO_VALUE_PARENT = %i[and_asgn op_asgn or_asgn].to_set.freeze + + private_constant(*constants(false)) + + def dispatch + emit_receiver + emit_operation(children[VALUE_RANGE]) + write(' = ') + visit(children.last) + end + + def emit_mlhs + emit_receiver + emit_operation(children.drop(1)) + end + + private + + def emit_operation(indices) + parentheses('[', ']') do + delimited(indices) + end + end + end # Assign + end # Index + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/kwargs.rb b/lib/unparser/emitter/kwargs.rb new file mode 100644 index 00000000..bffffc93 --- /dev/null +++ b/lib/unparser/emitter/kwargs.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + class Kwargs < self + handle :kwargs + + def dispatch + delimited(children) + end + end # Kwargs + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/kwbegin.rb b/lib/unparser/emitter/kwbegin.rb new file mode 100644 index 00000000..6589357b --- /dev/null +++ b/lib/unparser/emitter/kwbegin.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for explicit begins + class KWBegin < self + handle :kwbegin + + private + + def dispatch + write('begin') + + if children.one? + emit_body_ensure_rescue(Util.one(children)) + else + indented do + emit_multiple_body + end + end + + k_end + end + + def emit_multiple_body + emit_join(children, method(:emit_body_member), method(:nl)) + end + + end # KWBegin + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/lambda.rb b/lib/unparser/emitter/lambda.rb new file mode 100644 index 00000000..4e3bb735 --- /dev/null +++ b/lib/unparser/emitter/lambda.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for lambda nodes + class Lambda < self + handle :lambda + + private + + def dispatch + write('->') + end + + end # Lambda + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/literal.rb b/lib/unparser/emitter/literal.rb deleted file mode 100644 index 77c49e52..00000000 --- a/lib/unparser/emitter/literal.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Unparser - class Emitter - # Namespace class for literal emiters - class Literal < self - include Terminated - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/array.rb b/lib/unparser/emitter/literal/array.rb deleted file mode 100644 index 0bc95066..00000000 --- a/lib/unparser/emitter/literal/array.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Unparser - class Emitter - class Literal - - # Array literal emitter - class Array < self - OPEN = '['.freeze - CLOSE = ']'.freeze - DELIMITER = ', '.freeze - - handle :array - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - parentheses(OPEN, CLOSE) do - delimited(children, DELIMITER) - end - end - - end # Array - - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/dynamic.rb b/lib/unparser/emitter/literal/dynamic.rb deleted file mode 100644 index 0bea236d..00000000 --- a/lib/unparser/emitter/literal/dynamic.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Unparser - class Emitter - class Literal - - # Base class for dynamic literal emitters - class Dynamic < self - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - util = self.class - visit_parentheses(dynamic_body, util::OPEN, util::CLOSE) - end - - # Return dynamic body - # - # @return [Parser::AST::Node] - # - # @api private - # - def dynamic_body - Parser::AST::Node.new(:dyn_str_body, children) - end - - # Dynamic string literal emitter - class String < self - - OPEN = CLOSE = '"'.freeze - handle :dstr - - end # String - - # Dynamic symbol literal emitter - class Symbol < self - - OPEN = ':"'.freeze - CLOSE = '"'.freeze - - handle :dsym - - end # Symbol - end # Dynamic - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/dynamic_body.rb b/lib/unparser/emitter/literal/dynamic_body.rb deleted file mode 100644 index 01779155..00000000 --- a/lib/unparser/emitter/literal/dynamic_body.rb +++ /dev/null @@ -1,130 +0,0 @@ -module Unparser - class Emitter - class Literal - - # Emitter for dynamic bodies - class DynamicBody < self - - OPEN = '#{'.freeze - CLOSE = '}'.freeze - - # Emitter for interpolated nodes - class Interpolation < self - - handle :interpolated - - children :subject - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(OPEN) - visit_plain(subject) - write(CLOSE) - end - - end - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - children.each(&method(:emit_segment)) - end - - # Emit segment - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def emit_segment(node) - emit_interpolated_segment(node) - end - - pairs = Parser::Lexer::ESCAPES.invert.map do |key, value| - [key, "\\#{value}"] unless key.eql?(WS) - end.compact - - pairs << ['#{', '\#{'] - - ESCAPES = ::Hash[pairs] - - REPLACEMENTS = ::Regexp.union(ESCAPES.keys) - - # Emit str segment - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def emit_str_segment(node) - util = self.class - string = node.children.first - segment = string - .gsub(REPLACEMENTS, ESCAPES) - .gsub(util::DELIMITER, util::REPLACEMENT) - write(segment) - end - - # Emit interpolated segment - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def emit_interpolated_segment(node) - visit_parentheses(node, OPEN, CLOSE) - end - - # Dynamic string body - class String < self - - handle :dyn_str_body - - DELIMITER = '"'.freeze - REPLACEMENT = '\"'.freeze - - end # String - - # Dynamic regexp body - class Regexp < self - - handle :dyn_regexp_body - - DELIMITER = '/'.freeze - REPLACEMENT = '\/'.freeze - - end # Regexp - - # Dynamic regexp body - class ExecuteString < self - - handle :dyn_xstr_body - - DELIMITER = '`'.freeze - REPLACEMENT = '\`'.freeze - - end # ExecuteString - - end # DynamicBody - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/execute_string.rb b/lib/unparser/emitter/literal/execute_string.rb deleted file mode 100644 index 47377472..00000000 --- a/lib/unparser/emitter/literal/execute_string.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Unparser - class Emitter - class Literal - # Emitter for execute strings (xstr) nodes - class ExecuteString < self - - OPEN = CLOSE = '`'.freeze - - handle :xstr - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - visit_parentheses(dynamic_body, OPEN, CLOSE) - end - - # Return dynamic body - # - # @return [Parser::AST::Node] - # - # @api private - # - def dynamic_body - Parser::AST::Node.new(:dyn_xstr_body, children) - end - - end # ExecuteString - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/hash.rb b/lib/unparser/emitter/literal/hash.rb deleted file mode 100644 index 8cdc0ebc..00000000 --- a/lib/unparser/emitter/literal/hash.rb +++ /dev/null @@ -1,155 +0,0 @@ -module Unparser - class Emitter - class Literal - - # Abstract namespace class for hash pair emitters - class HashPair < self - - children :key, :value - - private - - # Emit value - # - # @return [undefined] - # - # @api private - # - def emit_value - value_type = value.type - conditional_parentheses(value_type.equal?(:if)) do - visit(value) - end - end - - # Pair emitter that emits hash-rocket separated key values - class Rocket < self - HASHROCKET = ' => '.freeze - - handle :pair_rocket, :pair - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - visit(key) - write(HASHROCKET) - emit_value - end - - end # Rocket - - # Pair emitter that emits colon separated key values - class Colon < self - COLON = ': '.freeze - - handle :pair_colon - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(key.children.first.to_s, COLON) - emit_value - end - - end # Colon - - end # HashPair - - # Emitter for hash bodies - class HashBody < self - - DELIMITER = ', '.freeze - BAREWORD = /\A[A-Za-z_][A-Za-z_0-9]*[?!]?\z/.freeze - - handle :hash_body - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - delimited(effective_body, DELIMITER) - end - - # Return effective body - # - # @return [Enumerable] - # - # @api private - # - def effective_body - children.map do |pair| - if pair.type.equal?(:kwsplat) - pair - else - key, _value = *pair - if key.type.equal?(:sym) && key.children.first.to_s =~ BAREWORD - n(:pair_colon, pair.children) - else - n(:pair_rocket, pair.children) - end - end - end - end - - end # HashBody - - # Emitter for Hash literals - class Hash < self - - DELIMITER = ', '.freeze - OPEN = '{'.freeze - CLOSE = '}'.freeze - - handle :hash - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - if children.empty? - write(OPEN, CLOSE) - else - emit_hash_body - end - end - - # Emit hash body - # - # @return [undefined] - # - # @api private - # - def emit_hash_body - parentheses(OPEN, CLOSE) do - write(WS) - run(HashBody) - write(WS) - end - end - - end # Hash - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/primitive.rb b/lib/unparser/emitter/literal/primitive.rb deleted file mode 100644 index 132acfe3..00000000 --- a/lib/unparser/emitter/literal/primitive.rb +++ /dev/null @@ -1,143 +0,0 @@ -module Unparser - class Emitter - class Literal - - # Base class for primitive emitters - class Primitive < self - - children :value - - # Emitter for primitives based on Object#inspect - class Inspect < self - - handle :sym, :str - - private - - # Dispatch value - # - # @return [undefined] - # - # @api private - # - def dispatch - write(value.inspect) - end - - end # Inspect - - # Emitter for complex literals - class Complex < self - - handle :complex - - RATIONAL_FORMAT = 'i'.freeze - - MAP = - if 0.class.equal?(Integer) - IceNine.deep_freeze( - Float => :float, - Rational => :rational, - Integer => :int - ) - else - IceNine.deep_freeze( - Float => :float, - Rational => :rational, - Fixnum => :int, - Bignum => :int - ) - end - - private - - # Dispatch value - # - # @return [undefined] - # - def dispatch - emit_imaginary - write(RATIONAL_FORMAT) - end - - # Emit imaginary component - # - # @return [undefined] - # - # @api private - # - def emit_imaginary - visit(imaginary_node) - end - - # Return imaginary node - # - # @return [Parser::AST::Node] - # - # @api private - # - def imaginary_node - imaginary = value.imaginary - s(MAP.fetch(imaginary.class), imaginary) - end - - end # Rational - - # Emitter for rational literals - class Rational < self - - handle :rational - - RATIONAL_FORMAT = 'r'.freeze - - private - - # Dispatch value - # - # @return [undefined] - # - def dispatch - integer = value.to_i - float = value.to_f - - write_rational(integer.to_f.eql?(float) ? integer : float) - end - - # Write rational format - # - # @param [#to_s] - # - # @return [undefined] - # - # @api private - # - def write_rational(value) - write(value.to_s, RATIONAL_FORMAT) - end - - end # Rational - - # Emiter for numeric literals - class Numeric < self - - handle :int, :float - - private - - # Dispatch value - # - # @return [undefined] - # - # @api private - # - def dispatch - conditional_parentheses(parent.is_a?(Emitter::Send) && value < 0) do - write(value.inspect) - end - end - - end # Numeric - end # Primitive - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/range.rb b/lib/unparser/emitter/literal/range.rb deleted file mode 100644 index 87764b9a..00000000 --- a/lib/unparser/emitter/literal/range.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Unparser - class Emitter - class Literal - # Abstract base class for literal range emitter - class Range < self - include Unterminated - - TOKENS = IceNine.deep_freeze( - irange: '..', - erange: '...' - ) - - handle(*TOKENS.keys) - - children :begin_node, :end_node - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - visit(begin_node) - write(TOKENS.fetch(node.type)) - visit(end_node) - end - - end # Range - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/regexp.rb b/lib/unparser/emitter/literal/regexp.rb deleted file mode 100644 index 58e538fc..00000000 --- a/lib/unparser/emitter/literal/regexp.rb +++ /dev/null @@ -1,112 +0,0 @@ -module Unparser - class Emitter - class Literal - - # Emitter for regexp literals - class Regexp < self - DELIMITER = '/'.freeze - ESCAPED_DELIMITER = '\/'.freeze - - handle :regexp - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - parentheses(DELIMITER, DELIMITER) do - body.each(&method(:write_body)) - end - visit(children.last) - end - - # Return non regopt children - # - # @return [Array] - # - # @api private - # - def body - children[0..-2] - end - - # Write specific body component - # - # @param [Parser::AST::Node] node - # - # @return [undefined] - # - # @api private - # - def write_body(node) - case node.type - when :str - buffer.append_without_prefix(escape(node).children.first) - else - visit(s(:interpolated, node)) - end - end - - # Return dynamic body - # - # @return [undefined] - # - # @api private - # - def dynamic_body - Parser::AST::Node.new(:dyn_regexp_body, dynamic_body_children) - end - - # Return dynamic body children - # - # @return [Enumerable] - # - # @api private - # - def dynamic_body_children - children[0..-2].map do |child| - escape(child) - end - end - - # Return escaped child - # - # @param [Parser::AST::Node] child - # - # @return [Parser::AST::Node] - # - # @api private - # - def escape(child) - source = child.children.first - s(:str, source.gsub(DELIMITER, ESCAPED_DELIMITER)) - end - - end # Regexp - - # Emitter for regexp options - class Regopt < self - - handle :regopt - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(children.map(&:to_s).join) - end - - end # Regopt - - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/literal/singleton.rb b/lib/unparser/emitter/literal/singleton.rb deleted file mode 100644 index 1b53858e..00000000 --- a/lib/unparser/emitter/literal/singleton.rb +++ /dev/null @@ -1,24 +0,0 @@ -module Unparser - class Emitter - class Literal - # Emiter for literal singletons - class Singleton < self - - handle :self, :true, :false, :nil - - private - - # Perform dispatco - # - # @return [undefined] - # - # @api private - # - def dispatch - buffer.append(node.type.to_s) - end - - end # Singleton - end # Literal - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/masgn.rb b/lib/unparser/emitter/masgn.rb new file mode 100644 index 00000000..7eb2d49a --- /dev/null +++ b/lib/unparser/emitter/masgn.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for multiple assignment nodes + class MASGN < self + handle :masgn + + children :target, :source + + private + + def dispatch + visit(target) + write(' = ') + visit(source) + end + end # MLHS + end # Emitter +end # Unaprser diff --git a/lib/unparser/emitter/match.rb b/lib/unparser/emitter/match.rb index ccad2348..ec37bf38 100644 --- a/lib/unparser/emitter/match.rb +++ b/lib/unparser/emitter/match.rb @@ -1,12 +1,10 @@ +# frozen_string_literal: true + module Unparser class Emitter # Base class for special match node emitters class Match < self - include Unterminated - - OPERATOR = '=~'.freeze - # Emitter for match with local variable assignment class Lvasgn < self handle :match_with_lvasgn @@ -15,15 +13,9 @@ class Lvasgn < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch visit(regexp) - write(WS, OPERATOR, WS) + write(' =~ ') visit(lvasgn) end @@ -35,12 +27,8 @@ class CurrentLine < self children :regexp - # Perform dispatch - # - # @return [undefined] - # - # @api private - # + private + def dispatch visit(regexp) end diff --git a/lib/unparser/emitter/match_alt.rb b/lib/unparser/emitter/match_alt.rb new file mode 100644 index 00000000..ff877abe --- /dev/null +++ b/lib/unparser/emitter/match_alt.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for in pattern nodes + class MatchAlt < self + + handle :match_alt + + children :left, :right + + private + + def dispatch + visit(left) + ws + write('|') + ws + visit(right) + end + end # MatchAlt + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/match_as.rb b/lib/unparser/emitter/match_as.rb new file mode 100644 index 00000000..59eb0d8d --- /dev/null +++ b/lib/unparser/emitter/match_as.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for in pattern nodes + class MatchAs < self + + handle :match_as + + children :left, :right + + private + + def dispatch + visit(left) + write(' => ') + visit(right) + end + end # MatchAs + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/match_pattern.rb b/lib/unparser/emitter/match_pattern.rb new file mode 100644 index 00000000..af847f00 --- /dev/null +++ b/lib/unparser/emitter/match_pattern.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for in pattern nodes + class MatchPattern < self + + handle :match_pattern + + children :target, :pattern + + private + + def dispatch + visit(target) + write(' => ') + + if n_array?(pattern) + writer_with(Writer::Array, node: pattern).emit_compact + else + visit(pattern) + end + end + end # MatchPattern + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/match_pattern_p.rb b/lib/unparser/emitter/match_pattern_p.rb new file mode 100644 index 00000000..91dc4f5a --- /dev/null +++ b/lib/unparser/emitter/match_pattern_p.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + class MatchPatternP < self + + handle :match_pattern_p + + children :target, :pattern + + private + + def dispatch + visit(target) + write(' in ') + + if n_array?(pattern) + writer_with(Writer::Array, node: pattern).emit_compact + else + visit(pattern) + end + end + end # MatchPatternP + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/match_rest.rb b/lib/unparser/emitter/match_rest.rb new file mode 100644 index 00000000..b9a2f1c3 --- /dev/null +++ b/lib/unparser/emitter/match_rest.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emiter for match rest nodes + class MatchRest < self + handle :match_rest + + children :match_var + + def dispatch + write('*') + visit(match_var) if match_var + end + + def emit_array_pattern + write('*') + emit_match_var + end + + def emit_hash_pattern + write('**') + emit_match_var + end + + private + + def emit_match_var + visit(match_var) if match_var + end + end # MatchRest + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/match_var.rb b/lib/unparser/emitter/match_var.rb new file mode 100644 index 00000000..5c895901 --- /dev/null +++ b/lib/unparser/emitter/match_var.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for in pattern nodes + class MatchVar < self + + handle :match_var + + children :name + + private + + def dispatch + write(name.to_s) + end + end # MatchVar + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/meta.rb b/lib/unparser/emitter/meta.rb deleted file mode 100644 index 48a9fc54..00000000 --- a/lib/unparser/emitter/meta.rb +++ /dev/null @@ -1,14 +0,0 @@ -module Unparser - class Emitter - # Namespace class for meta emitters - class Meta < self - include Terminated - - handle(:__FILE__, :__LINE__) - - def dispatch - write(node.type.to_s) # (e.g. literally write '__FILE__' or '__LINE__') - end - end # Meta - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/mlhs.rb b/lib/unparser/emitter/mlhs.rb new file mode 100644 index 00000000..ecde2503 --- /dev/null +++ b/lib/unparser/emitter/mlhs.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for multiple assignment left hand side + class MLHS < self + handle :mlhs + + NO_COMMA = %i[arg splat restarg].freeze + + private_constant(*constants(false)) + + def dispatch_def + parentheses do + delimited(children) + end + end + + private + + def dispatch + if children.one? + emit_one_child_mlhs + else + emit_many + end + end + + def emit_one_child_mlhs + child = children.first + parentheses do + emitter(child).emit_mlhs + write(',') unless NO_COMMA.include?(child.type) + end + end + + def emit_many + parentheses do + delimited(children) do |node| + emitter(node).emit_mlhs + end + end + end + end # MLHS + end # Emitter +end # Unaprser diff --git a/lib/unparser/emitter/module.rb b/lib/unparser/emitter/module.rb index e41ccc27..06b3d9b3 100644 --- a/lib/unparser/emitter/module.rb +++ b/lib/unparser/emitter/module.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for module nodes class Module < self - include LocalVariableRoot, Terminated + include LocalVariableRoot handle :module @@ -10,16 +12,10 @@ class Module < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_MODULE, WS) + write('module ') visit(name) - emit_body + emit_optional_body(body) k_end end diff --git a/lib/unparser/emitter/op_assign.rb b/lib/unparser/emitter/op_assign.rb index 8b02dc76..3f40c089 100644 --- a/lib/unparser/emitter/op_assign.rb +++ b/lib/unparser/emitter/op_assign.rb @@ -1,30 +1,24 @@ +# frozen_string_literal: true + module Unparser class Emitter # Base class for and and or op-assign class BinaryAssign < self - include Unterminated - children :target, :expression - MAP = IceNine.deep_freeze( + MAP = { and_asgn: '&&=', or_asgn: '||=' - ) + }.freeze handle(*MAP.keys) private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - visit(target) - write(WS, MAP.fetch(node.type), WS) + emitter(target).emit_mlhs + write(' ', MAP.fetch(node.type), ' ') visit(expression) end @@ -32,32 +26,20 @@ def dispatch # Emitter for op assign class OpAssign < self - include Unterminated - handle :op_asgn + children :target, :operator, :value + private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - visit(first_child) + emitter(first_child).emit_mlhs emit_operator - visit(children[2]) + visit(value) end - # Emit operator - # - # @return [undefined] - # - # @api private - # def emit_operator - write(WS, children[1].to_s, T_ASN, WS) + write(' ', operator.to_s, '= ') end end # OpAssign diff --git a/lib/unparser/emitter/pair.rb b/lib/unparser/emitter/pair.rb new file mode 100644 index 00000000..8f73bea1 --- /dev/null +++ b/lib/unparser/emitter/pair.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for key value pairs in hash literals or kwargs + class Pair < self + BAREWORD = /\A[A-Za-z_][A-Za-z_0-9]*[?!]?\z/.freeze + + private_constant(*constants(false)) + + handle :pair + + children :key, :value + + private + + def dispatch + if colon? + emit_colon + unless implicit_value_lvar? || implicit_value_send? + write(' ') + visit(value) + end + else + visit(key) + write(' => ') + visit(value) + end + end + + def colon? + n_sym?(key) && BAREWORD.match?(key.children.first) + end + + def emit_colon + write(key.children.first.to_s, ':') + end + + def key_value + key.children.first + end + + def implicit_value_lvar? + n_lvar?(value) && value.children.first.equal?(key_value) + end + + def implicit_value_send? + children = value.children + + n_send?(value) \ + && !key_value.end_with?('?') \ + && !key_value.end_with?('!') \ + && children.fetch(0).nil? \ + && children.fetch(1).equal?(key_value) \ + && children.at(2).nil? + end + end + end +end diff --git a/lib/unparser/emitter/pin.rb b/lib/unparser/emitter/pin.rb new file mode 100644 index 00000000..444469ea --- /dev/null +++ b/lib/unparser/emitter/pin.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for pin nodes + class Pin < self + handle :pin + + children :target + + private + + def dispatch + write('^') + visit(target) + end + end # Pin + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/primitive.rb b/lib/unparser/emitter/primitive.rb new file mode 100644 index 00000000..0bc0bcbf --- /dev/null +++ b/lib/unparser/emitter/primitive.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Base class for primitive emitters + class Primitive < self + + children :value + + class Symbol < self + + handle :sym + + private + + # mutant:disable + def dispatch + if inspect_breaks_parsing? + write(":#{value.name.inspect}") + else + write(value.inspect) + end + end + + # mutant:disable + def inspect_breaks_parsing? + return false unless RUBY_VERSION < '3.2.' + + Unparser.parse(value.inspect) + false + rescue Parser::SyntaxError + true + end + end # Symbol + + # Emitter for complex literals + class Complex < self + + handle :complex + + RATIONAL_FORMAT = 'i'.freeze + + MAP = + { + ::Float => :float, + ::Rational => :rational, + ::Integer => :int + }.freeze + + private + + def dispatch + emit_imaginary + write(RATIONAL_FORMAT) + end + + def emit_imaginary + visit(imaginary_node) + end + + def imaginary_node + imaginary = value.imaginary + s(MAP.fetch(imaginary.class), imaginary) + end + + end # Rational + + # Emitter for rational literals + class Rational < self + + handle :rational + + RATIONAL_FORMAT = 'r'.freeze + + private + + # rubocop:disable Lint/FloatComparison + def dispatch + integer = Integer(value) + float = value.to_f + + write_rational(integer.to_f.equal?(float) ? integer : float) + end + # rubocop:enable Lint/FloatComparison + + def write_rational(value) + write(value.to_s, RATIONAL_FORMAT) + end + + end # Rational + + # Emiter for numeric literals + class Numeric < self + + handle :int + + private + + def dispatch + write(value.inspect) + end + + end # Numeric + end # Primitive + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/range.rb b/lib/unparser/emitter/range.rb new file mode 100644 index 00000000..c8cd7356 --- /dev/null +++ b/lib/unparser/emitter/range.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Range emitters + class Range < self + TOKENS = { + irange: '..', + erange: '...' + }.freeze + + SYMBOLS = { + erange: :tDOT3, + irange: :tDOT2 + }.freeze + + def symbol_name + true + end + + handle(*TOKENS.keys) + + children :begin_node, :end_node + + private + + def dispatch + visit_begin_node(begin_node) + write(TOKENS.fetch(node.type)) + visit_end_node(end_node) + end + + def visit_begin_node(node) + return unless node + + if n_array?(begin_node) + writer_with(Writer::Array, node: begin_node).emit_compact + else + visit(begin_node) + end + end + + def visit_end_node(node) + return unless node + + write(' ') if n_range?(node) + if n_array?(node) + writer_with(Writer::Array, node: node).emit_compact + else + visit(node) + end + end + + end # Range + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/redo.rb b/lib/unparser/emitter/redo.rb deleted file mode 100644 index ad8a8edc..00000000 --- a/lib/unparser/emitter/redo.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Unparser - class Emitter - # Emitter for redo nodes - class Redo < self - include Terminated - - handle :redo - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(K_REDO) - end - - end # Redo - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/regexp.rb b/lib/unparser/emitter/regexp.rb new file mode 100644 index 00000000..a4f2f412 --- /dev/null +++ b/lib/unparser/emitter/regexp.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for regexp literals + class Regexp < self + + handle :regexp + + private + + def dispatch + writer.dispatch + end + + def writer + writer_with(Writer::Regexp, node:) + end + memoize :writer + + end # Regexp + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/repetition.rb b/lib/unparser/emitter/repetition.rb index ac8c23c4..9ecb9934 100644 --- a/lib/unparser/emitter/repetition.rb +++ b/lib/unparser/emitter/repetition.rb @@ -1,41 +1,33 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for postconditions class Post < self - include Unterminated - - handle :while_post, :until_post - children :condition, :body MAP = { - while_post: K_WHILE, - until_post: K_UNTIL + while_post: 'while', + until_post: 'until' }.freeze handle(*MAP.keys) - # Perform dispatch - # - # @return [undefined] - # - # @api private - # + private + def dispatch visit(body) - write(WS, MAP.fetch(node.type), WS) + write(' ', MAP.fetch(node.type), ' ') visit(condition) end end - # Base class for while and until emitters + # Emitter for while and until nodes class Repetition < self - include Terminated - MAP = { - while: K_WHILE, - until: K_UNTIL + while: 'while', + until: 'until' }.freeze handle(*MAP.keys) @@ -44,12 +36,6 @@ class Repetition < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch if postcontrol? emit_postcontrol @@ -58,53 +44,30 @@ def dispatch end end - # Test if node must be emitted in postcontrol form - # - # @return [Boolean] - # - # @api private - # def postcontrol? - return false unless body - local_variable_scope.first_assignment_in_body_and_used_in_condition?(body, condition) + body && local_variable_scope.first_assignment_in?(body, condition) end - # Emit keyword - # - # @return [undefined] - # - # @api private - # def emit_keyword - write(MAP.fetch(node.type), WS) + write(MAP.fetch(node.type), ' ') end - # Emit embedded - # - # @return [undefned] - # - # @api private - # def emit_normal emit_keyword - conditional_parentheses(condition.type.equal?(:block)) do - visit(condition) + visit(condition) + if body + emit_body(body) + else + nl end - emit_body k_end end - # Emit postcontrol - # - # @return [undefined] - # - # @api private - # def emit_postcontrol - visit_plain(body) + visit(body) ws emit_keyword - visit_plain(condition) + visit(condition) end end # Repetition diff --git a/lib/unparser/emitter/resbody.rb b/lib/unparser/emitter/resbody.rb deleted file mode 100644 index 6e5f7c0f..00000000 --- a/lib/unparser/emitter/resbody.rb +++ /dev/null @@ -1,96 +0,0 @@ -module Unparser - class Emitter - # Emitter for rescue body nodes - class Resbody < self - - children :exception, :assignment, :body - - # Emitter for resbody in standalone form - class Standalone < self - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(K_RESCUE, WS) - visit_plain(body) - end - - # Emit exception - # - # @return [undefined] - # - # @api private - # - def emit_exception - return unless exception - ws - delimited(exception.children) - end - - # Emit assignment - # - # @return [undefined] - # - # @api private - # - def emit_assignment - return unless assignment - write(WS, T_ASR, WS) - visit(assignment) - end - end - - # Emitter for resbody in keyworkd-embedded form - class Embedded < self - - handle :resbody - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(K_RESCUE) - emit_exception - emit_assignment - emit_body - end - - # Emit exception - # - # @return [undefined] - # - # @api private - # - def emit_exception - return unless exception - ws - delimited(exception.children) - end - - # Emit assignment - # - # @return [undefined] - # - # @api private - # - def emit_assignment - return unless assignment - write(WS, T_ASR, WS) - visit(assignment) - end - - end # Resbody - end - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/rescue.rb b/lib/unparser/emitter/rescue.rb index 9c1b48b0..3b60b25f 100644 --- a/lib/unparser/emitter/rescue.rb +++ b/lib/unparser/emitter/rescue.rb @@ -1,109 +1,22 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for rescue nodes class Rescue < self - include Unterminated - handle :rescue - children :body, :rescue_body - - define_group :rescue_bodies, 1..-2 - - EMBEDDED_TYPES = [:def, :defs, :kwbegin, :ensure].to_set.freeze - - NOINDENT_STANDALONE_RESCUE = [:root, :begin, :pair_rocket, :pair_colon, :lvasgn, :ivasgn].to_set.freeze - private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - if standalone? - if NOINDENT_STANDALONE_RESCUE.include?(parent_type) - emit_standalone - else - indented { emit_standalone } - end - else - emit_embedded - end - end + resbody = children.fetch(1) - # Test if rescue node ist standalone - # - # @return [Boolean] - # - # @api private - # - def standalone? - if parent_type.equal?(:ensure) - !parent.node.children.first.equal?(node) + if resbody.children.fetch(1) + emit_rescue_regular(node) else - !EMBEDDED_TYPES.include?(parent_type) + emit_rescue_postcontrol(node) end end - - # Emit standalone form - # - # @return [undefined] - # - # @api private - # - def emit_standalone - visit_plain(body) - ws - run(Resbody::Standalone, rescue_body) - end - - # Emit embedded form - # - # @return [undefined] - # - # @api private - # - def emit_embedded - if body - visit_indented(body) - else - nl - end - rescue_bodies.each do |child| - run(Resbody::Embedded, child) - end - emit_else - end - - # Emit else - # - # @return [undefined] - # - # @api private - # - def emit_else - return unless else_branch - write(K_ELSE) - visit_indented(else_branch) - end - - # Return else body - # - # @return [Parser::AST::Node] - # if else body is present - # - # @return [nil] - # otherwise - # - # @api private - # - def else_branch - children.last - end - end # Rescue end # Emitter end # Unparser diff --git a/lib/unparser/emitter/retry.rb b/lib/unparser/emitter/retry.rb deleted file mode 100644 index 3f05112f..00000000 --- a/lib/unparser/emitter/retry.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Unparser - class Emitter - # Emitter for retry nodes - class Retry < self - include Terminated - - handle :retry - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(K_RETRY) - end - - end # Break - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/root.rb b/lib/unparser/emitter/root.rb index 44fa7ed2..0e2134e9 100644 --- a/lib/unparser/emitter/root.rb +++ b/lib/unparser/emitter/root.rb @@ -1,9 +1,20 @@ +# frozen_string_literal: true + module Unparser class Emitter - # Root emitter a special case class Root < self - include Concord::Public.new(:node, :buffer, :comments) - include LocalVariableRoot + END_NL = %i[class sclass module begin].freeze + + private_constant(*constants(false)) + + def dispatch + emit_body(node, indent: false) + + buffer.nl_flush_heredocs + emit_eof_comments + + nl if END_NL.include?(node.type) && !buffer.fresh_line? + end end # Root end # Emitter end # Unparser diff --git a/lib/unparser/emitter/send.rb b/lib/unparser/emitter/send.rb index e6d48d43..aa790c43 100644 --- a/lib/unparser/emitter/send.rb +++ b/lib/unparser/emitter/send.rb @@ -1,201 +1,25 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for send class Send < self + handle :csend, :send - handle :send - - INDEX_PARENS = IceNine.deep_freeze(%w([ ])) - NORMAL_PARENS = IceNine.deep_freeze(%w[( )]) - - INDEX_REFERENCE = :'[]' - INDEX_ASSIGN = :'[]=' - ASSIGN_SUFFIX = '='.freeze - - children :receiver, :selector - - def terminated? - effective_emitter.terminated? + def emit_mlhs + writer.emit_mlhs end private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - effective_emitter.write_to_buffer - end - - # Return effective emitter - # - # @return [Emitter] - # - # @api private - # - def effective_emitter - effective_emitter_class.new(node, parent) - end - - # Return effective emitter - # - # @return [Class:Emitter] - # - # @api private - # - def effective_emitter_class - case selector - when INDEX_REFERENCE - Index::Reference - when INDEX_ASSIGN - Index::Assign - else - non_index_emitter - end - end - - # Return non index emitter - # - # @return [Class:Emitter] - # - # @api private - # - def non_index_emitter - if binary_operator? - Binary - elsif unary_operator? - Unary - elsif attribute_assignment? - AttributeAssignment - else - Regular - end - end - - # Return string selector - # - # @return [String] - # - # @api private - # - def string_selector - selector.to_s - end - - # Test for unary operator implemented as method - # - # @return [Boolean] - # - # @api private - # - def unary_operator? - UNARY_OPERATORS.include?(selector) - end - - # Test for binary operator implemented as method - # - # @return [Boolean] - # - # @api private - # - def binary_operator? - BINARY_OPERATORS.include?(selector) && arguments.one? && !arguments.first.type.equal?(:splat) - end - - # Emit selector - # - # @return [undefined] - # - # @api private - # - def emit_selector - name = string_selector - if mlhs? - name = name[0..-2] - end - write(name) - end - - # Test for mlhs - # - # @return [Boolean] - # - # @api private - # - def mlhs? - parent_type.equal?(:mlhs) - end - - # Test for assignment - # - # FIXME: This also returns true for <= operator! - # - # @return [Boolean] - # - # @api private - # - def assignment? - string_selector[-1].eql?(ASSIGN_SUFFIX) - end - - # Test for attribute assignment - # - # @return [Boolean] - # - # @api private - # - def attribute_assignment? - !BINARY_OPERATORS.include?(selector) && !UNARY_OPERATORS.include?(selector) && assignment? && !mlhs? - end - - # Test for empty arguments - # - # @return [Boolean] - # - # @api private - # - def arguments? - arguments.any? - end - - # Return argument nodes - # - # @return [Array] - # - # @api private - # - def arguments - children[2..-1] - end - memoize :arguments - - # Emit arguments - # - # @return [undefined] - # - # @api private - # - def emit_arguments - if arguments.empty? && receiver.nil? && local_variable_clash? - write('()') - else - run(Arguments, n(:arguments, arguments)) - end + writer.dispatch end - # Test for local variable clash - # - # @return [Boolean] - # - # @api private - # - def local_variable_clash? - local_variable_scope.local_variable_defined_for_node?(node, selector) + def writer + writer_with(Writer::Send, node:) end - + memoize :writer end # Send end # Emitter end # Unparser diff --git a/lib/unparser/emitter/send/arguments.rb b/lib/unparser/emitter/send/arguments.rb deleted file mode 100644 index 207d75de..00000000 --- a/lib/unparser/emitter/send/arguments.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Unparser - class Emitter - class Send - # Emitter for arguments of send nodes - class Arguments < Emitter - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - return if children.empty? - - parentheses do - delimited_plain(effective_arguments) - end - end - - # Return effective arguments - # - # @return [Parser::AST::Node] - # - # @api private - # - def effective_arguments - last = children.length - 1 - children.each_with_index.map do |argument, index| - if last.equal?(index) && argument.type.equal?(:hash) && argument.children.any? - argument.updated(:hash_body) - else - argument - end - end - end - - end # Arguments - end # Send - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/send/attribute_assignment.rb b/lib/unparser/emitter/send/attribute_assignment.rb deleted file mode 100644 index 8687b4aa..00000000 --- a/lib/unparser/emitter/send/attribute_assignment.rb +++ /dev/null @@ -1,67 +0,0 @@ -module Unparser - class Emitter - class Send - # Emitter for send as attribute assignment - class AttributeAssignment < self - include Unterminated - - # Perform regular dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - emit_receiver - emit_attribute - emit_operator - visit(arguments.first) - end - - private - - # Emit receiver - # - # @return [Parser::AST::Node] - # - # @api private - # - def emit_receiver - visit(receiver) - write(T_DOT) - end - - # Emit attribute - # - # @return [undefined] - # - # @api private - # - def emit_attribute - write(attribute_name) - end - - # Emit assignment operator - # - # @return [undefined] - # - # @api private - # - def emit_operator - write(WS, T_ASN, WS) - end - - # Return attribute name - # - # @return [String] - # - # @api private - # - def attribute_name - string_selector[0..-2] - end - - end # AttributeAssignment - end # Send - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/send/binary.rb b/lib/unparser/emitter/send/binary.rb deleted file mode 100644 index 656cbc1c..00000000 --- a/lib/unparser/emitter/send/binary.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Unparser - class Emitter - class Send - # Emitter for binary sends - class Binary < self - include Unterminated - - private - - # Return undefined - # - # @return [undefined] - # - # @api private - # - def dispatch - visit(receiver) - emit_operator - emit_right - end - - # Emit operator - # - # @return [undefined] - # - # @api private - # - def emit_operator - write(WS, string_selector, WS) - end - - # Return right node - # - # @return [Parser::AST::Node] - # - # @api private - # - def right_node - children[2] - end - - # Emit right - # - # @return [undefined] - # - # @api private - # - def emit_right - visit(right_node) - end - - end # Binary - end # Send - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/send/conditional.rb b/lib/unparser/emitter/send/conditional.rb deleted file mode 100644 index e75b05e5..00000000 --- a/lib/unparser/emitter/send/conditional.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Unparser - class Emitter - class Send - # Emitter for "conditional" receiver&.selector(arguments...) case - class Conditional < self - include Terminated - - handle :csend - - private - - # Perform regular dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - emit_receiver - emit_selector - emit_arguments - end - - # Emit receiver - # - # @return [undefined] - # - # @api private - # - def emit_receiver - visit(receiver) - write(T_AMP, T_DOT) - end - - end # Regular - end # Send - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/send/index.rb b/lib/unparser/emitter/send/index.rb deleted file mode 100644 index 1cae6a85..00000000 --- a/lib/unparser/emitter/send/index.rb +++ /dev/null @@ -1,125 +0,0 @@ -module Unparser - class Emitter - class Send - # Emitter for send to index references - class Index < self - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - emit_receiver - emit_operation - end - - # Emit receiver - # - # @return [undefined] - # - # @api private - # - def emit_receiver - visit(first_child) - end - - # Emitter for index reference nodes - class Reference < self - include Terminated - - private - - # Emit arguments - # - # @return [undefined] - # - # @api private - # - def emit_operation - parentheses(*INDEX_PARENS) do - delimited_plain(arguments) - end - end - end # Reference - - # Emitter for assign to index nodes - class Assign < self - include Unterminated - - # Test if assign will be emitted terminated - # - # @return [Boolean] - # - # @api private - # - def terminated? - mlhs? - end - - private - - define_group(:indices, 2..-2) - define_child(:value, -1) - - # Emit arguments - # - # @return [undefined] - # - # @api private - # - def emit_operation - if arguments.empty? - emit_regular_with_empty_args - elsif mlhs? - emit_mlhs_operation - else - emit_normal_operation - end - end - - # Emit mlhs arguments - # - # @return [undefined] - # - # @api private - # - def emit_mlhs_operation - parentheses(*INDEX_PARENS) do - delimited(arguments) - end - end - - # Emit normal arguments - # - # @return [undefined] - # - # @api private - # - def emit_normal_operation - parentheses(*INDEX_PARENS) do - delimited_plain(indices) - end - write(WS, T_ASN, WS) - visit(value) - end - - # Emit regular with empty ars - # - # @return [undefined] - # - # @api private - # - def emit_regular_with_empty_args - write(T_DOT, '[]=()') - end - - end # Assign - - end # Index - end # Send - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/send/regular.rb b/lib/unparser/emitter/send/regular.rb deleted file mode 100644 index d3e1ffc2..00000000 --- a/lib/unparser/emitter/send/regular.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Unparser - class Emitter - class Send - # Emitter for "regular" receiver.selector(arguments...) case - class Regular < self - include Terminated - - private - - # Perform regular dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - emit_receiver - emit_selector - emit_arguments - end - - # Emit receiver - # - # @return [undefined] - # - # @api private - # - def emit_receiver - return unless first_child - visit(receiver) - write(T_DOT) - end - - end # Regular - end # Send - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/send/unary.rb b/lib/unparser/emitter/send/unary.rb deleted file mode 100644 index d8819be5..00000000 --- a/lib/unparser/emitter/send/unary.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Unparser - class Emitter - class Send - # Emitter for unary sends - class Unary < self - include Unterminated - - private - - MAP = IceNine.deep_freeze( - :'-@' => '-', - :'+@' => '+' - ) - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - name = selector - write(MAP.fetch(name, name).to_s) - if receiver.type.equal?(:int) && selector.equal?(:'+@') && receiver.children.first > 0 - write('+') - end - - visit(receiver) - end - - end # Unary - end # Send - end # Emitter -end # Unparser diff --git a/lib/unparser/emitter/simple.rb b/lib/unparser/emitter/simple.rb new file mode 100644 index 00000000..b3dfe12b --- /dev/null +++ b/lib/unparser/emitter/simple.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Emitter for simple nodes that generate a single token + class Simple < self + MAP = { + __ENCODING__: '__ENCODING__', + __FILE__: '__FILE__', + __LINE__: '__LINE__', + false: 'false', + forward_arg: '...', + forwarded_args: '...', + kwnilarg: '**nil', + match_nil_pattern: '**nil', + nil: 'nil', + redo: 'redo', + retry: 'retry', + self: 'self', + true: 'true', + zsuper: 'super' + }.freeze + + handle(*MAP.keys) + + private + + def dispatch + write(MAP.fetch(node_type)) + end + end # Simple + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/splat.rb b/lib/unparser/emitter/splat.rb index 675097bb..5129fa5a 100644 --- a/lib/unparser/emitter/splat.rb +++ b/lib/unparser/emitter/splat.rb @@ -1,47 +1,43 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for splats class KwSplat < self - include Terminated - handle :kwsplat children :subject private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(T_SPLAT, T_SPLAT) + write('**') visit(subject) end end # Emitter for splats class Splat < self - include Terminated - handle :splat children :subject + def emit_mlhs + write('*') + subject_emitter.emit_mlhs if subject + end + private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(T_SPLAT) - visit(subject) if subject + write('*') + subject_emitter.write_to_buffer + end + + def subject_emitter + emitter(subject) end + memoize :subject_emitter end end end # Unparser diff --git a/lib/unparser/emitter/string.rb b/lib/unparser/emitter/string.rb new file mode 100644 index 00000000..1c81c591 --- /dev/null +++ b/lib/unparser/emitter/string.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Base class for primitive emitters + class String < self + children :value + + handle :str + + private + + def dispatch + if explicit_encoding && !value.encoding.equal?(explicit_encoding) + write_utf8_escaped + else + write(value.inspect) + end + end + + def write_utf8_escaped + write('"') + value.each_codepoint do |codepoint| + write("\\u{#{codepoint.to_s(16)}}") + end + write('"') + end + + end # String + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/super.rb b/lib/unparser/emitter/super.rb index 83abe488..f78729f3 100644 --- a/lib/unparser/emitter/super.rb +++ b/lib/unparser/emitter/super.rb @@ -1,42 +1,16 @@ +# frozen_string_literal: true + module Unparser class Emitter - # Emitter for zsuper nodes - class ZSuper < self - include Terminated - - handle :zsuper - - private - - # Perform dispatch - # - # @return [undefined] - # - # @api private - # - def dispatch - write(K_SUPER) - end - - end # ZSuper - # Emitter for super nodes class Super < self - include Terminated - handle :super private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_SUPER) + write('super') parentheses do delimited(children) end diff --git a/lib/unparser/emitter/undef.rb b/lib/unparser/emitter/undef.rb index 33597855..cda2297b 100644 --- a/lib/unparser/emitter/undef.rb +++ b/lib/unparser/emitter/undef.rb @@ -1,21 +1,15 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for undef nodes class Undef < self - include Unterminated - handle :undef private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_UNDEF, WS) + write('undef ') delimited(children) end diff --git a/lib/unparser/emitter/variable.rb b/lib/unparser/emitter/variable.rb index f02ee9d3..e4915bac 100644 --- a/lib/unparser/emitter/variable.rb +++ b/lib/unparser/emitter/variable.rb @@ -1,22 +1,16 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for various variable accesses class Variable < self - include Terminated - handle :ivar, :lvar, :cvar, :gvar, :back_ref children :name private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch write(name.to_s) end @@ -25,42 +19,27 @@ def dispatch # Emitter for constant access class Const < self - include Terminated - handle :const children :scope, :name private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch emit_scope write(name.to_s) end - # Emit parent - # - # @return [undefined] - # - # @api private - # def emit_scope return unless scope + visit(scope) - write(T_DCL) unless scope.type.equal?(:cbase) + write('::') unless n_cbase?(scope) end end # Emitter for nth_ref nodes (regexp captures) class NthRef < self - include Terminated - PREFIX = '$'.freeze handle :nth_ref @@ -68,12 +47,6 @@ class NthRef < self private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch write(PREFIX) write(name.to_s) diff --git a/lib/unparser/emitter/xstr.rb b/lib/unparser/emitter/xstr.rb new file mode 100644 index 00000000..127d287c --- /dev/null +++ b/lib/unparser/emitter/xstr.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Unparser + class Emitter + # Dynamic execute string literal emitter + class XStr < self + + handle :xstr + + private + + def dispatch + if heredoc? + emit_heredoc + else + emit_xstr + end + end + + def heredoc? + children.any? { |node| node.eql?(s(:str, '')) } + end + + def emit_heredoc + write(%(<<~`HEREDOC`)) + buffer.indent + nl + children.each do |child| + if n_str?(child) + write(child.children.first) + else + emit_begin(child) + end + end + buffer.unindent + write("HEREDOC\n") + end + + def emit_xstr + write('`') + children.each do |child| + if n_begin?(child) + emit_begin(child) + elsif n_gvar?(child) + emit_gvar(child) + else + emit_string(child) + end + end + write('`') + end + + def emit_string(value) + write(escape_xstr(value.children.first)) + end + + def escape_xstr(input) + input.chars.map do |char| + if char.eql?('`') + '\\`' + else + char + end + end.join + end + + def emit_begin(component) + write('#{') + visit(Util.one(component.children)) if component.children.any? + write('}') + end + + def emit_gvar(component) + write('#') + write(Util.one(component.children).to_s) + end + end # XStr + end # Emitter +end # Unparser diff --git a/lib/unparser/emitter/yield.rb b/lib/unparser/emitter/yield.rb index f9f41c9c..4b6f8df3 100644 --- a/lib/unparser/emitter/yield.rb +++ b/lib/unparser/emitter/yield.rb @@ -1,23 +1,18 @@ +# frozen_string_literal: true + module Unparser class Emitter # Emitter for yield node class Yield < self - include Terminated - handle :yield private - # Perform dispatch - # - # @return [undefined] - # - # @api private - # def dispatch - write(K_YIELD) + write('yield') return if children.empty? + parentheses do delimited(children) end diff --git a/lib/unparser/equalizer.rb b/lib/unparser/equalizer.rb new file mode 100644 index 00000000..ab7e1c75 --- /dev/null +++ b/lib/unparser/equalizer.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Unparser + # Define equality, equivalence and inspection methods + # + # Original code before vendoring and reduction from: https://github.com/dkubb/equalizer. + class Equalizer < Module + # Initialize an Equalizer with the given keys + # + # Will use the keys with which it is initialized to define #cmp?, + # #hash, and #inspect + # + # @param [Array] keys + # + # @return [undefined] + # + # @api private + # + # rubocop:disable Lint/MissingSuper + def initialize(*keys) + @keys = keys + define_methods + freeze + end + # rubocop:enable Lint/MissingSuper + + private + + def included(descendant) + descendant.include(Methods) + end + + def define_methods + define_cmp_method + define_hash_method + define_inspect_method + end + + def define_cmp_method + keys = @keys + define_method(:cmp?) do |comparator, other| + keys.all? do |key| + __send__(key).public_send(comparator, other.__send__(key)) + end + end + private :cmp? + end + + def define_hash_method + keys = @keys + define_method(:hash) do + keys.map(&public_method(:__send__)).push(self.class).hash + end + end + + def define_inspect_method + keys = @keys + define_method(:inspect) do + klass = self.class + name = klass.name || klass.inspect + "#<#{name}#{keys.map { |key| " #{key}=#{__send__(key).inspect}" }.join}>" + end + end + + # The comparison methods + module Methods + # Compare the object with other object for equality + # + # @example + # object.eql?(other) # => true or false + # + # @param [Object] other + # the other object to compare with + # + # @return [Boolean] + # + # @api public + def eql?(other) + instance_of?(other.class) && cmp?(__method__, other) + end + + # Compare the object with other object for equivalency + # + # @example + # object == other # => true or false + # + # @param [Object] other + # the other object to compare with + # + # @return [Boolean] + # + # @api public + def ==(other) + instance_of?(other.class) && cmp?(__method__, other) + end + end # module Methods + end # class Equalizer +end # Unparser diff --git a/lib/unparser/finalize.rb b/lib/unparser/finalize.rb index d279bc51..11f8c990 100644 --- a/lib/unparser/finalize.rb +++ b/lib/unparser/finalize.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + Unparser::Emitter::REGISTRY.freeze diff --git a/lib/unparser/generation.rb b/lib/unparser/generation.rb new file mode 100644 index 00000000..a3183f3e --- /dev/null +++ b/lib/unparser/generation.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +module Unparser + # rubocop:disable Metrics/ModuleLength + module Generation + EXTRA_NL = %i[kwbegin def defs module class sclass].freeze + + private_constant(*constants(false)) + + def symbol_name; end + + def write_to_buffer + with_comments { dispatch } + self + end + + private + + def delimited(nodes, delimiter = ', ', &block) + return if nodes.empty? + + emit_join(nodes, block || method(:visit), -> { write(delimiter) }) + end + + def emit_join(nodes, emit_node, emit_delimiter) + return if nodes.empty? + + head, *tail = nodes + emit_node.call(head) + + tail.each do |node| + emit_delimiter.call + emit_node.call(node) + end + end + + def nl + emit_eol_comments + buffer.nl + end + + def with_comments + emit_comments_before if buffer.fresh_line? + yield + comments.consume(node) + end + + def ws + write(' ') + end + + def emit_eol_comments + comments.take_eol_comments.each do |comment| + write(' ', comment.text) + end + end + + def emit_eof_comments + emit_eol_comments + comments_left = comments.take_all + return if comments_left.empty? + + buffer.nl + emit_comments(comments_left) + end + + def emit_comments_before(source_part = :expression) + comments_before = comments.take_before(node, source_part) + return if comments_before.empty? + + emit_comments(comments_before) + buffer.nl + end + + def emit_comments(comments) + max = comments.size - 1 + comments.each_with_index do |comment, index| + if comment.type.equal?(:document) + buffer.append_without_prefix(comment.text.chomp) + else + write(comment.text) + end + buffer.nl if index < max + end + end + + def write(*strings) + strings.each(&buffer.method(:append)) + end + + def k_end + buffer.indent + emit_comments_before(:end) + buffer.unindent + write('end') + end + + def parentheses(open = '(', close = ')') + write(open) + yield + write(close) + end + + def indented + buffer = buffer() + buffer.indent + nl + yield + nl + buffer.unindent + end + + def emit_optional_body(node, indent: true) + if node + emit_body(node, indent: indent) + else + nl + end + end + + def emit_body(node, indent: true) + with_indent(indent: indent) do + if n_begin?(node) + if node.children.empty? + write('()') + elsif node.children.one? + visit_deep(node) + else + emit_body_inner(node) + end + else + visit_deep(node) + end + end + end + + def with_indent(indent:) + return yield unless indent + + buffer.indent + nl + yield + buffer.unindent + nl + end + + def emit_body_inner(node) + head, *tail = node.children + emit_body_member(head) + write(';') if requires_explicit_statement_terminator?(head, tail) + + tail.each do |child| + buffer.ensure_nl + + nl if EXTRA_NL.include?(child.type) + + emit_body_member(child) + write(';') if requires_explicit_statement_terminator?(child, tail) + end + end + + def requires_explicit_statement_terminator?(node, nodes_group) + n_range?(node) && node.children.fetch(1).nil? && !node.eql?(nodes_group.fetch(-1)) + end + + def emit_body_member(node) + if n_rescue?(node) + emit_rescue_postcontrol(node) + else + visit_deep(node) + end + end + + def emit_ensure(node) + body, ensure_body = node.children + + if body + emit_body_rescue(body) + else + nl + end + + write('ensure') + + emit_optional_body(ensure_body) + end + + def emit_body_rescue(node) + if n_rescue?(node) + emit_rescue_regular(node) + else + emit_body(node) + end + end + + def emit_optional_body_ensure_rescue(node) + if node + emit_body_ensure_rescue(node) + else + nl + end + end + + def emit_body_ensure_rescue(node) + if n_ensure?(node) + emit_ensure(node) + elsif n_rescue?(node) + emit_rescue_regular(node) + else + emit_body(node) + end + end + + def emit_rescue_postcontrol(node) + writer = writer_with(Writer::Rescue, node:) + writer.emit_postcontrol + end + + def emit_rescue_regular(node) + writer_with(Writer::Rescue, node:).emit_regular + end + + def emitter(node) + Emitter.emitter(**to_h, node: node) + end + + def writer_with(klass, node:, **attributes) + klass.new(to_h.merge(node: node, **attributes)) + end + + def visit(node) + emitter(node).write_to_buffer + end + + def visit_deep(node) + emitter(node).tap(&:write_to_buffer) + end + + def first_child + children.first + end + + def conditional_parentheses(flag, &block) + if flag + parentheses(&block) + else + block.call + end + end + + def children + node.children + end + end # Generation + # rubocop:enable Metrics/ModuleLength +end # Unparser diff --git a/lib/unparser/node_details.rb b/lib/unparser/node_details.rb new file mode 100644 index 00000000..0721984d --- /dev/null +++ b/lib/unparser/node_details.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Unparser + module NodeDetails + include Constants, NodeHelpers + + # mutant:disable + def self.included(descendant) + descendant.class_eval do + include Adamantium, Concord.new(:node) + + extend DSL + end + end + + private + + def children + node.children + end + end # NodeDetails +end # Unparser diff --git a/lib/unparser/node_details/send.rb b/lib/unparser/node_details/send.rb new file mode 100644 index 00000000..082d9427 --- /dev/null +++ b/lib/unparser/node_details/send.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Unparser + module NodeDetails + class Send + include NodeDetails + + ASSIGN_SUFFIX = '='.freeze + NON_ASSIGN_RANGE = (0..-2).freeze + + private_constant(*constants(false)) + + children :receiver, :selector + + public :receiver, :selector + + def selector_binary_operator? + BINARY_OPERATORS.include?(selector) + end + + def binary_syntax_allowed? + selector_binary_operator? \ + && n_send?(node) \ + && arguments.one? \ + && !n_splat?(arguments.first) \ + && !n_kwargs?(arguments.first) + end + + def selector_unary_operator? + UNARY_OPERATORS.include?(selector) + end + + def assignment_operator? + assignment? && !selector_binary_operator? && !selector_unary_operator? + end + + def arguments? + arguments.any? + end + + def non_assignment_selector + if assignment? + string_selector[NON_ASSIGN_RANGE] + else + string_selector + end + end + + def assignment? + string_selector[-1].eql?(ASSIGN_SUFFIX) + end + memoize :assignment? + + def arguments + children[2..] + end + memoize :arguments + + def string_selector + selector.to_s + end + memoize :string_selector + + end # Send + end # NodeDetails +end # Unparser diff --git a/lib/unparser/node_helpers.rb b/lib/unparser/node_helpers.rb index a896b6bb..83f6405f 100644 --- a/lib/unparser/node_helpers.rb +++ b/lib/unparser/node_helpers.rb @@ -1,29 +1,89 @@ +# frozen_string_literal: true + module Unparser module NodeHelpers # Helper for building nodes # - # @param [Symbol] + # @param [Symbol] type + # @param [Parser::AST::Node] children # # @return [Parser::AST::Node] # # @api private - # def s(type, *children) Parser::AST::Node.new(type, children) end # Helper for building nodes # - # @param [Symbol] + # @param [Symbol] type # # @return [Parser::AST::Node] + # @param [Array] children # # @api private - # def n(type, children = []) Parser::AST::Node.new(type, children) end + def n?(type, node) + node.type.equal?(type) + end + + def n_flipflop?(node) + n_iflipflop?(node) || n_eflipflop?(node) + end + + def n_range?(node) + n_irange?(node) || n_erange?(node) + end + + %i[ + and + arg + args + array + array_pattern + begin + block + cbase + const + dstr + eflipflop + empty_else + erange + ensure + gvar + hash + hash_pattern + if + iflipflop + in_pattern + int + irange + kwarg + kwargs + kwsplat + lambda + lvar + match_rest + mlhs + or + pair + rescue + send + shadowarg + splat + str + sym + xstr + ].to_set.each do |type| + name = "n_#{type}?" + define_method(name) do |node| + n?(type, node) + end + private(name) + end end # NodeHelpers end # Unparser diff --git a/lib/unparser/preprocessor.rb b/lib/unparser/preprocessor.rb deleted file mode 100644 index 4b39e115..00000000 --- a/lib/unparser/preprocessor.rb +++ /dev/null @@ -1,156 +0,0 @@ -module Unparser - # Preprocessor to normalize AST generated by parser - class Preprocessor - include Adamantium::Flat, NodeHelpers, AbstractType, Concord.new(:node, :parent_type), Procto.call(:result) - - # Return preprocessor result - # - # @return [Parser::AST::Node] - # - # @api private - # - abstract_method :result - - EMPTY = Parser::AST::Node.new(:empty) - - # Run preprocessor for node - # - # @param [Parser::AST::Node, nil] node - # - # @return [Parser::AST::Node, nil] - # - # @api private - # - def self.run(node, parent_type = nil) - return EMPTY if node.nil? - REGISTRY.fetch(node.type, [Noop]).reduce(node) do |current, processor| - processor.call(current, parent_type) - end - end - - REGISTRY = Hash.new { |hash, key| hash[key] = [] } - - # Register preprocessor - # - # @param [Symbol] type - # - # @return [undefined] - # - # @api private - # - def self.register(type) - REGISTRY[type] << self - end - private_class_method :register - - private - - # Visit node - # - # @param [Parser::AST::Node] child - # - # @return [undefined] - # - # @api private - # - def visit(child) - self.class.run(child, node.type) - end - - # Return children - # - # @return [Array] - # - # @api private - # - def children - node.children - end - - # Return visited children - # - # @return [Array] - # - # @api private - # - def visited_children - children.map do |node| - if node.is_a?(Parser::AST::Node) - visit(node) - else - node - end - end - end - - # Noop preprocessor that just passes node through. - class Noop < self - - register :int - register :str - - # Return preprocessor result - # - # @return [Parser::AST::Node] - # - # @api private - # - def result - node.updated(nil, visited_children) - end - - end # Noop - - # Preprocessor transforming numeric nodes with infinity as value to round trippable equivalent. - class Infinity < self - - register :float - register :int - - NEG_INFINITY = -(Float::INFINITY - 1) - - # Return preprocessor result - # - # @param [Parser::AST::Node] - # - # @api private - # - def result - value = node.children.first - case value - when Float::INFINITY - s(:const, s(:const, nil, :Float), :INFINITY) - when NEG_INFINITY - s(:send, s(:const, s(:const, nil, :Float), :INFINITY), :-@) - else - node - end - end - end - - # Preprocessor for begin nodes. Removes begin nodes with one child. - # - # This reduces the amount of complex logic needed inside unparser to emit "nice" syntax with minimal - # tokens. - # - class Begin < self - - register :begin - - # Return preprocessor result - # - # @return [Parser::AST::Node] - # - # @api private - # - def result - if children.one? && !parent_type.equal?(:regexp) - visit(children.first) - else - Noop.call(node, parent_type) - end - end - - end # Begin - end # Preprocessor -end # Unparser diff --git a/lib/unparser/util.rb b/lib/unparser/util.rb new file mode 100644 index 00000000..9a890a60 --- /dev/null +++ b/lib/unparser/util.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Unparser + # Original code before vendoring and reduction from: https://github.com/mbj/mutant/blob/main/lib/mutant/util.rb + module Util + # Error raised by `Util.one` if size is not exactly one + SizeError = Class.new(IndexError) + + # Return only element in array if it contains exactly one member + # + # @param array [Array] + # + # @return [Object] first entry + def self.one(array) + case array + in [value] + value + else + fail SizeError, "expected size to be exactly 1 but size was #{array.size}" + end + end + end +end diff --git a/lib/unparser/validation.rb b/lib/unparser/validation.rb new file mode 100644 index 00000000..0c9eb87e --- /dev/null +++ b/lib/unparser/validation.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +module Unparser + # Validation of unparser results + class Validation + include Adamantium, Anima.new( + :generated_node, + :generated_source, + :identification, + :original_ast, + :original_source + ) + + class PhaseException + include Anima.new(:exception, :phase) + end + + # Test if source could be unparsed successfully + # + # @return [Boolean] + # + # @api private + # + # rubocop:disable Style/OperatorMethodCall + # mutant:disable + def success? + [ + original_source, + original_ast, + generated_source, + generated_node + ].all?(&:right?) && generated_node.from_right.==(original_node.from_right) + end + # rubocop:enable Style/OperatorMethodCall + + # Return error report + # + # @return [String] + # + # @api private + # mutant:disable + def report + message = [identification] + + message.concat(make_report('Original-Source', :original_source)) + message.concat(make_report('Generated-Source', :generated_source)) + message.concat(make_report('Original-Node', :original_node)) + message.concat(make_report('Generated-Node', :generated_node)) + message.concat(node_diff_report) + + message.join("\n") + end + memoize :report + + # mutant:disable + def original_node + original_ast.fmap(&:node) + end + + # Create validator from string + # + # @param [String] original_source + # + # @return [Validator] + # mutant:disable + def self.from_string(original_source) + original_ast = parse_ast_either(original_source) + + generated_source = original_ast + .lmap(&method(:const_unit)) + .bind(&method(:unparse_ast_either)) + + generated_node = generated_source + .lmap(&method(:const_unit)) + .bind(&method(:parse_ast_either)) + .fmap(&:node) + + new( + generated_node: generated_node, + generated_source: generated_source, + identification: '(string)', + original_ast: original_ast, + original_source: Either::Right.new(original_source) + ) + end + + # Create validator from ast + # + # @param [Unparser::AST] ast + # + # @return [Validator] + # + # mutant:disable + def self.from_ast(ast:) + generated_source = Unparser.unparse_ast_either(ast) + + generated_node = generated_source + .lmap(&method(:const_unit)) + .bind(&method(:parse_ast_either)) + .fmap(&:node) + + new( + identification: '(string)', + original_source: generated_source, + original_ast: Either::Right.new(ast), + generated_source: generated_source, + generated_node: generated_node + ) + end + + # Create validator from file + # + # @param [Pathname] path + # + # @return [Validator] + # + # mutant:disable + def self.from_path(path) + from_string(path.read.freeze).with(identification: path.to_s) + end + + # mutant:disable + def self.unparse_ast_either(ast) + Unparser.unparse_ast_either(ast) + end + private_class_method :unparse_ast_either + + # mutant:disable + def self.parse_ast_either(source) + Unparser.parse_ast_either(source) + end + private_class_method :parse_ast_either + + # mutant:disable + def self.const_unit(_); end + private_class_method :const_unit + + private + + # mutant:disable + def make_report(label, attribute_name) + ["#{label}:"].concat(public_send(attribute_name).either(method(:report_exception), ->(value) { [value] })) + end + + # mutant:disable + def report_exception(phase_exception) + if phase_exception + [phase_exception.inspect].concat(phase_exception.backtrace.take(20)) + else + %w[undefined] + end + end + + # mutant:disable + def node_diff_report + diff = nil + + original_node.fmap do |original| + generated_node.fmap do |generated| + diff = Diff.new( + original.to_s.lines.map(&:chomp), + generated.to_s.lines.map(&:chomp) + ).colorized_diff + end + end + + diff ? ['Node-Diff:', diff] : [] + end + + class Literal < self + # mutant:disable + def success? + original_source.eql?(generated_source) + end + + # mutant:disable + def report + message = [identification] + + message.concat(make_report('Original-Source', :original_source)) + message.concat(make_report('Generated-Source', :generated_source)) + message.concat(make_report('Original-Node', :original_node)) + message.concat(make_report('Generated-Node', :generated_node)) + message.concat(node_diff_report) + message.concat(source_diff_report) + + message.join("\n") + end + + private + + # mutant:disable + def source_diff_report + diff = nil + + original_source.fmap do |original| + generated_source.fmap do |generated| + diff = Diff.new( + encode(original).split("\n", -1), + encode(generated).split("\n", -1) + ).colorized_diff + end + end + + diff ? ['Source-Diff:', diff] : [] + end + + # mutant:disable + def encode(string) + string.encode('UTF-8', invalid: :replace, undef: :replace) + end + end # Literal + end # Validation +end # Unparser diff --git a/lib/unparser/writer.rb b/lib/unparser/writer.rb new file mode 100644 index 00000000..2e761014 --- /dev/null +++ b/lib/unparser/writer.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Unparser + module Writer + include Generation, NodeHelpers + + # mutant:disable + def self.included(descendant) + descendant.class_eval do + include Adamantium, Anima.new(:buffer, :comments, :explicit_encoding, :node, :local_variable_scope) + + extend DSL + end + end + + private + + # mutant:disable + def emitter(node) + Emitter.emitter( + buffer: buffer, + comments: comments, + explicit_encoding: explicit_encoding, + local_variable_scope: local_variable_scope, + node: node + ) + end + + # mutant:disable + def round_trips?(source:) + parser = Unparser.parser + local_variable_scope.local_variables_for_node(node).each do |local_variable| + parser.declare_local_variable(local_variable) + end + + buffer = Buffer.new + buffer.write_encoding(explicit_encoding) if explicit_encoding + buffer.write(source) + + node.eql?(parser.parse(Unparser.buffer(buffer.content))) + rescue Parser::SyntaxError + false + end + end # Writer +end # Unparser diff --git a/lib/unparser/writer/array.rb b/lib/unparser/writer/array.rb new file mode 100644 index 00000000..3779a8e6 --- /dev/null +++ b/lib/unparser/writer/array.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Array + include Writer, Adamantium + + MAP = { + dsym: '%I', + sym: '%i', + dstr: '%W', + str: '%w' + }.freeze + private_constant(*constants(false)) + + def emit_compact # rubocop:disable Metrics/AbcSize + children_generic_type = array_elements_generic_type + + write(MAP.fetch(children_generic_type)) + + parentheses('[', ']') do + delimited(children, ' ') do |child| + if n_sym?(child) || n_str?(child) + write(Util.one(child.children).to_s) + else + write('#{') + emitter(Util.one(Util.one(child.children).children)).write_to_buffer + write('}') + end + end + end + end + + private + + def array_elements_generic_type + children_types = children.to_set(&:type) + + if children_types == Set[:sym, :dsym] + :dsym + elsif children_types == Set[:str, :dstr] + :dstr + elsif children_types == Set[] + :sym + else + Util.one(children_types.to_a) + end + end + end # Array + end # Writer +end # Unparser diff --git a/lib/unparser/writer/binary.rb b/lib/unparser/writer/binary.rb new file mode 100644 index 00000000..2b9344b5 --- /dev/null +++ b/lib/unparser/writer/binary.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Binary + include Writer, Adamantium + + children :left, :right + + OPERATOR_TOKENS = + { + and: '&&', + or: '||' + }.freeze + + KEYWORD_TOKENS = + { + and: 'and', + or: 'or' + }.freeze + + KEYWORD_SYMBOLS = + { + and: :kAND, + or: :kOR + }.freeze + + OPERATOR_SYMBOLS = + { + and: :tANDOP, + or: :tOROP + }.freeze + + MAP = + { + kAND: 'and', + kOR: 'or', + tOROP: '||', + tANDOP: '&&' + }.freeze + + NEED_KEYWORD = %i[return break next match_pattern_p].freeze + + private_constant(*constants(false)) + + def emit_operator + emit_with(OPERATOR_TOKENS) + end + + def symbol_name + true + end + + def dispatch + if node.type.eql?(:and) && left.type.equal?(:or) + emit_with(KEYWORD_TOKENS) + else + left_emitter.write_to_buffer + write(' ', MAP.fetch(effective_symbol), ' ') + visit(right) + end + end + + private + + def effective_symbol + if NEED_KEYWORD.include?(right.type) || NEED_KEYWORD.include?(left.type) + return keyword_symbol + end + + unless left_emitter.symbol_name + return operator_symbol + end + + keyword_symbol + end + + def emit_with(map) + visit(left) + write(' ', map.fetch(node.type), ' ') + visit(right) + end + + def keyword_symbol + KEYWORD_SYMBOLS.fetch(node.type) + end + + def operator_symbol + OPERATOR_SYMBOLS.fetch(node.type) + end + + def left_emitter + emitter(left) + end + memoize :left_emitter + + def right_emitter + emitter(right) + end + memoize :right_emitter + end # Binary + end # Writer +end # Unparser diff --git a/lib/unparser/writer/dynamic_string.rb b/lib/unparser/writer/dynamic_string.rb new file mode 100644 index 00000000..7619ce59 --- /dev/null +++ b/lib/unparser/writer/dynamic_string.rb @@ -0,0 +1,204 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class DynamicString + include Writer, Adamantium + + FLAT_INTERPOLATION = %i[ivar cvar gvar nth_ref].to_set.freeze + + # Amount of dstr children at which heredoc emitting is + # preferred, but not guaranteed. + HEREDOC_THRESHOLD = 8 + HEREDOC_DELIMITER = 'HEREDOC' + HEREDOC_HEADER = "<<-#{HEREDOC_DELIMITER}".freeze + HEREDOC_FOOTER = "#{HEREDOC_DELIMITER}\n".freeze + + private_constant(*constants(false)) + + # The raise below is not reachable if unparser is correctly implemented + # but has to exist as I have to assume unparser still has bugs. + # + # But unless I had such a bug in my test corpus: I cannot enable mutant, and if I + # knew about such a bug: I'd fix it so would be back at the start. + # + # TLDR: Good case for a mutant disable. + # + # mutant:disable + def dispatch + if heredoc? + write(HEREDOC_HEADER) + buffer.push_heredoc(heredoc_body) + elsif round_tripping_segmented_source + write(round_tripping_segmented_source) + else + fail UnsupportedNodeError, "Unparser cannot round trip this node: #{node.inspect}" + end + end + + private + + def heredoc? + if children.length >= HEREDOC_THRESHOLD + round_trips_heredoc? + else + round_tripping_segmented_source.nil? # && round_trips_heredoc? + end + end + memoize :heredoc? + + def round_trips_heredoc? + round_trips?(source: heredoc_source) + end + memoize :round_trips_heredoc? + + def round_tripping_segmented_source + each_segments(children) do |segments| + + source = segmented_source(segments: segments) + + return source if round_trips?(source: source) + end + nil + end + memoize :round_tripping_segmented_source + + def each_segments(array) + yield [array] + + 1.upto(array.length) do |take| + prefix = [array.take(take)] + suffix = array.drop(take) + each_segments(suffix) do |items| + yield(prefix + items) + end + end + end + + def segmented_source(segments:) + buffer = Buffer.new + + Segmented.new( + buffer:, + comments:, + explicit_encoding: nil, + local_variable_scope:, + node:, + segments: + ).dispatch + + buffer.content + end + + def heredoc_body + buffer = Buffer.new + + writer = Heredoc.new( + buffer:, + comments:, + explicit_encoding: nil, + local_variable_scope:, + node: + ) + + writer.emit + buffer.content + end + memoize :heredoc_body + + def heredoc_source + "#{HEREDOC_HEADER}\n#{heredoc_body}" + end + memoize :heredoc_source + + class Heredoc + include Writer, Adamantium + + def emit + emit_heredoc_body + write(HEREDOC_FOOTER) + end + + private + + def emit_heredoc_body + children.each do |child| + if n_str?(child) + write(escape_dynamic(child.children.first)) + else + emit_dynamic(child) + end + end + end + + def escape_dynamic(string) + string.gsub('#', '\#') + end + + def emit_dynamic(child) + write('#{') + emit_dynamic_component(child.children.first) + write('}') + end + + def emit_dynamic_component(node) + visit(node) if node + end + end # Heredoc + + class Segmented + include Writer, Adamantium + + include anima.add(:segments) + + def dispatch + if children.empty? + write('%()') + else + segments.each_with_index { |segment, index| emit_segment(segment, index) } + end + end + + private + + def emit_segment(children, index) + write(' ') unless index.zero? + + write('"') + emit_segment_body(children) + write('"') + end + + def emit_segment_body(children) + children.each_with_index do |child, index| + case child.type + when :begin + write('#{') + visit(child.children.first) if child.children.first + write('}') + when FLAT_INTERPOLATION + write('#') + visit(child) + when :str + visit_str(children, child, index) + when :dstr + emit_segment_body(child.children) + end + end + end + + def visit_str(children, child, index) + string = child.children.first + + next_child = children.at(index.succ) + + if next_child && next_child.type.equal?(:str) + write(string.gsub('"', '\\"')) + else + write(child.children.first.inspect[1..-2]) + end + end + end + end # DynamicString + end # Writer +end # Unparser diff --git a/lib/unparser/writer/regexp.rb b/lib/unparser/writer/regexp.rb new file mode 100644 index 00000000..eac25c71 --- /dev/null +++ b/lib/unparser/writer/regexp.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Unparser + module Writer + # Writer for regexp literals + class Regexp + include Writer, Adamantium + + CANDIDATES = [ + ['/', '/'].freeze, + ['%r{', '}'].freeze + ].freeze + + define_group(:body, 0..-2) + + def dispatch + effective_writer.write_to_buffer + end + + private + + # mutant:disable + def effective_writer + CANDIDATES.each do |token_open, token_close| + source = render_with_delimiter(token_close:, token_open:) + + next unless round_trips?(source:) + + return writer_with(Effective, node:, token_close:, token_open:) + end + + fail 'Could not find a round tripping solution for regexp' + end + + class Effective + include Writer, Adamantium + + include anima.add(:token_close, :token_open) + + define_group(:body, 0..-2) + + def dispatch + buffer.root_indent do + write(token_open) + body.each(&method(:emit_body)) + write(token_close) + emit_options + end + end + + private + + def emit_body(node) + if n_begin?(node) + write('#{') + node.children.each(&method(:visit)) + write('}') + elsif n_gvar?(node) + write('#') + write_regular(node.children.first.to_s) + else + write_regular(node.children.first) + end + end + + def write_regular(string) + if string.length > 1 && string.start_with?("\n") + string.each_char do |char| + buffer.append_without_prefix(char.eql?("\n") ? '\c*' : char) + end + else + buffer.append_without_prefix(string) + end + end + + def emit_options + write(children.last.children.join) + end + end + + # mutant:disable + def render_with_delimiter(token_close:, token_open:) + buffer = Buffer.new + + writer = Effective.new( + buffer:, + comments:, + explicit_encoding:, + local_variable_scope:, + node:, + token_close:, + token_open: + ) + + writer.dispatch + buffer.nl_flush_heredocs + buffer.content + end + end # Regexp + end # Emitter +end # Unparser diff --git a/lib/unparser/writer/resbody.rb b/lib/unparser/writer/resbody.rb new file mode 100644 index 00000000..bde3989a --- /dev/null +++ b/lib/unparser/writer/resbody.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Unparser + module Writer + # Writer for rescue bodies + class Resbody + include Writer + + OPERATORS = { + csend: '&.', + send: '.' + }.freeze + + children :exception, :assignment, :body + + def emit_postcontrol + if body + write(' rescue ') + visit(body) + else + nl + write('rescue') + end + end + + def emit_regular + write('rescue') + emit_exception + emit_assignment + emit_optional_body(body) + end + + private + + def emit_exception + return unless exception + + ws + delimited(exception.children) + end + + def emit_assignment + return unless assignment + + write(' => ') + + case assignment.type + when :send, :csend + write_send_assignment + when :indexasgn + write_index_assignment + else + visit(assignment) + end + end + + def write_send_assignment + details = NodeDetails::Send.new(assignment) + + visit(details.receiver) + write(OPERATORS.fetch(assignment.type)) + write(details.non_assignment_selector) + end + + def write_index_assignment + receiver, *indexes = assignment.children + visit(receiver) + write('[') + delimited(indexes) + write(']') + end + end # Resbody + end # Writer +end # Unparser diff --git a/lib/unparser/writer/rescue.rb b/lib/unparser/writer/rescue.rb new file mode 100644 index 00000000..05f78e67 --- /dev/null +++ b/lib/unparser/writer/rescue.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Rescue + include Writer, Adamantium + + children :body, :rescue_body + + define_group :rescue_bodies, 1..-2 + + def emit_regular + emit_optional_body(body) + + rescue_bodies.each(&method(:emit_rescue_body)) + + if else_node + write('else') + emit_body(else_node) + end + end + + def emit_postcontrol + visit(body) if body + writer_with(Resbody, node: rescue_body).emit_postcontrol + end + + private + + def else_node + children.last + end + + def emit_rescue_body(node) + writer_with(Resbody, node:).emit_regular + end + end # Rescue + end # Writer +end # Unparser diff --git a/lib/unparser/writer/send.rb b/lib/unparser/writer/send.rb new file mode 100644 index 00000000..4462939e --- /dev/null +++ b/lib/unparser/writer/send.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Unparser + module Writer + # Writer for send + class Send + include Writer, Adamantium, Constants, Generation + + INDEX_ASSIGN = :[]= + INDEX_REFERENCE = :[] + + OPERATORS = { + csend: '&.', + send: '.' + }.freeze + + private_constant(*constants(false)) + + children :receiver, :selector + + def dispatch + effective_writer.dispatch + end + + def emit_mlhs + effective_writer.emit_send_mlhs + end + + def emit_selector + write(details.string_selector) + end + + private + + def effective_writer + writer_with(effective_writer_class, node:) + end + memoize :effective_writer + + def effective_writer_class + if details.binary_syntax_allowed? + Binary + elsif details.selector_unary_operator? && n_send?(node) && arguments.empty? + Unary + elsif write_as_attribute_assignment? + AttributeAssignment + else + Regular + end + end + + def write_as_attribute_assignment? + details.assignment_operator? + end + + def emit_operator + write(OPERATORS.fetch(node.type)) + end + + def emit_arguments + if arguments.empty? + write('()') if receiver.nil? && avoid_clash? + else + emit_normal_arguments + end + end + + def arguments + details.arguments + end + + def emit_normal_arguments + parentheses { delimited(arguments) } + end + + def avoid_clash? + local_variable_clash? || parses_as_constant? + end + + def local_variable_clash? + local_variable_scope.local_variable_defined_for_node?(node, selector) + end + + def parses_as_constant? + test = Unparser + .parse_ast_either(selector.to_s) + .fmap(&:node) + .from_right do + fail InvalidNodeError.new("Invalid selector for send node: #{selector.inspect}", node) + end + + n_const?(test) + end + + def details + NodeDetails::Send.new(node) + end + memoize :details + + def emit_send_regular(node) + if n_send?(node) + writer_with(Regular, node:).dispatch + else + visit(node) + end + end + end # Send + end # Writer +end # Unparser diff --git a/lib/unparser/writer/send/attribute_assignment.rb b/lib/unparser/writer/send/attribute_assignment.rb new file mode 100644 index 00000000..fe404c55 --- /dev/null +++ b/lib/unparser/writer/send/attribute_assignment.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Send + # Writer for send as attribute assignment + class AttributeAssignment < self + children :receiver, :selector, :first_argument + + def dispatch + emit_receiver + emit_attribute + write('=') + + if arguments.one? + visit(first_argument) + else + parentheses { delimited(arguments) } + end + end + + def emit_send_mlhs + emit_receiver + write(details.non_assignment_selector) + end + + private + + def emit_receiver + visit(receiver) + emit_operator + end + + def emit_attribute + write(details.non_assignment_selector) + end + end # AttributeAssignment + end # Send + end # Writer +end # Unparser diff --git a/lib/unparser/writer/send/binary.rb b/lib/unparser/writer/send/binary.rb new file mode 100644 index 00000000..c3cc46be --- /dev/null +++ b/lib/unparser/writer/send/binary.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Send + # Writer for binary sends + class Binary < self + def dispatch + visit(receiver) + emit_operator + emit_right + end + + private + + def emit_operator + write(' ', details.string_selector, ' ') + end + + def emit_right + emit_send_regular(children.fetch(2)) + end + + end # Binary + end # Send + end # Writer +end # Unparser diff --git a/lib/unparser/writer/send/conditional.rb b/lib/unparser/writer/send/conditional.rb new file mode 100644 index 00000000..c3ddcf68 --- /dev/null +++ b/lib/unparser/writer/send/conditional.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Send + # Writer for "conditional" receiver&.selector(arguments...) case + class Conditional < self + + private + + def dispatch + emit_receiver + emit_selector + emit_arguments + end + + def emit_receiver + visit(receiver) + write('&.') + end + + end # Regular + end # Send + end # Writer +end # Unparser diff --git a/lib/unparser/writer/send/regular.rb b/lib/unparser/writer/send/regular.rb new file mode 100644 index 00000000..f5642b74 --- /dev/null +++ b/lib/unparser/writer/send/regular.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Send + # Writer for "regular" receiver.selector(arguments...) case + class Regular < self + def dispatch + emit_receiver + emit_selector + emit_arguments + end + + def emit_send_mlhs + dispatch + end + + def emit_arguments_without_heredoc_body + emit_normal_arguments if arguments.any? + end + + def emit_receiver + return unless receiver + + emit_send_regular(receiver) + + emit_operator + end + + end # Regular + end # Send + end # Writer +end # Unparser diff --git a/lib/unparser/writer/send/unary.rb b/lib/unparser/writer/send/unary.rb new file mode 100644 index 00000000..3111c4e8 --- /dev/null +++ b/lib/unparser/writer/send/unary.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Unparser + module Writer + class Send + # Writer for unary sends + class Unary < self + MAP = { + '-@': '-', + '+@': '+' + }.freeze + + private_constant(*constants(false)) + + def dispatch # rubocop:disable Metrics/AbcSize + name = selector + first_child = children.fetch(0) + + if n_flipflop?(first_child) || n_and?(first_child) || n_or?(first_child) + write 'not ' + else + write(MAP.fetch(name, name).to_s) + + if n_int?(receiver) && selector.equal?(:+@) + write('+') + end + end + + visit(receiver) + end + end # Unary + end # Send + end # Writer +end # Unparser diff --git a/scripts/devloop.sh b/scripts/devloop.sh new file mode 100755 index 00000000..d4295682 --- /dev/null +++ b/scripts/devloop.sh @@ -0,0 +1,7 @@ +while inotifywait bin/corpus lib/**/*.rb test/**/*.rb spec/**/*.rb Gemfile unparser.gemspec; do + bundle exec rspec spec/unit -fd --fail-fast --order defined \ + && bundle exec mutant run --zombie --since main --fail-fast -- 'Unparser*' +done + # && bundle exec ./bin/parser-round-trip-test \ + # && bundle exec rubocop \ + # && bundle exec ./bin/corpus \ diff --git a/spec/integration/unparser/corpus_spec.rb b/spec/integration/unparser/corpus_spec.rb deleted file mode 100644 index 58cc86fb..00000000 --- a/spec/integration/unparser/corpus_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'spec_helper' -describe 'Unparser on ruby corpus', mutant: false do - ROOT = Pathname.new(__FILE__).parent.parent.parent.parent - - TMP = ROOT.join('tmp') - - class Project - include Anima.new(:name, :repo_uri, :repo_ref, :exclude) - - # Perform verification via unparser cli - # - # @return [self] - # if successful - # - # @raise [Exception] - # otherwise - # - def verify - checkout - command = %W(unparser #{repo_path}) - exclude.each do |name| - command.concat(%W(--ignore #{repo_path.join(name)})) - end - system(command) do - raise "Verifing #{name} failed!" - end - self - end - - # Checkout repository - # - # @return [self] - # - # @api private - # - def checkout - TMP.mkdir unless TMP.directory? - if repo_path.exist? - Dir.chdir(repo_path) do - system(%w(git pull origin master)) - system(%w(git clean -f -d -x)) - end - else - system(%W(git clone #{repo_uri} #{repo_path})) - end - - Dir.chdir(repo_path) do - system(%W(git checkout #{repo_ref})) - system(%w(git reset --hard)) - system(%w(git clean -f -d -x)) - end - - self - end - - private - - # Return repository path - # - # @return [Pathname] - # - # @api private - # - def repo_path - TMP.join(name) - end - - # Helper method to execute system commands - # - # @param [Array] arguments - # - # @api private - # - def system(arguments) - return if Kernel.system(*arguments) - - if block_given? - yield - else - raise "System command #{arguments.inspect} failed!" - end - end - - LOADER = Morpher.build do - s(:block, - s(:guard, s(:primitive, Array)), - s(:map, - s(:block, - s(:guard, s(:primitive, Hash)), - s(:hash_transform, - s(:key_symbolize, :repo_uri, s(:guard, s(:primitive, String))), - s(:key_symbolize, :repo_ref, s(:guard, s(:primitive, String))), - s(:key_symbolize, :name, s(:guard, s(:primitive, String))), - s(:key_symbolize, :exclude, s(:map, s(:guard, s(:primitive, String))))), - s(:load_attribute_hash, - # NOTE: The domain param has no DSL currently! - Morpher::Evaluator::Transformer::Domain::Param.new( - Project, - [:repo_uri, :repo_ref, :name, :exclude] - ))))) - end - - ALL = LOADER.call(YAML.load_file(ROOT.join('spec', 'integrations.yml'))) - end - - Project::ALL.each do |project| - specify "unparsing #{project.name}" do - project.verify - end - end -end diff --git a/spec/integrations.yml b/spec/integrations.yml index d1683640..475f1890 100644 --- a/spec/integrations.yml +++ b/spec/integrations.yml @@ -1,85 +1,18 @@ --- -- name: anima - repo_uri: 'https://github.com/mbj/anima.git' - repo_ref: 'origin/master' - exclude: [] - name: mutant repo_uri: 'https://github.com/mbj/mutant.git' - repo_ref: 'origin/master' + repo_ref: main + exclude: [] +- name: deepcover + repo_uri: 'https://github.com/deep-cover/deep-cover.git' + repo_ref: master exclude: [] -- name: yaks - repo_uri: 'https://github.com/plexus/yaks.git' - repo_ref: 'origin/master' +- name: activemerchant + repo_uri: 'https://github.com/activemerchant/active_merchant.git' + repo_ref: master exclude: [] -- name: chassis - repo_uri: 'https://github.com/ahawkins/chassis.git' - repo_ref: 'origin/master' exclude: [] - name: rubyspec repo_uri: 'https://github.com/ruby/spec.git' - # Revision of rubyspec on the last CI build of unparser that passed - repo_ref: 'origin/master' - exclude: - - command_line/fixtures/bad_syntax.rb - - core/array/pack/shared/float.rb - - core/array/pack/shared/integer.rb - - core/array/pack/shared/string.rb - - core/array/pack/{b,c,h,m}_spec.rb - - core/array/pack/{u,w}_spec.rb - - core/encoding/compatible_spec.rb - - core/encoding/converter/convert_spec.rb - - core/encoding/converter/last_error_spec.rb - - core/encoding/converter/primitive_convert_spec.rb - - core/encoding/converter/primitive_errinfo_spec.rb - - core/encoding/converter/putback_spec.rb - - core/encoding/fixtures/classes.rb - - core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb - - core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb - - core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb - - core/encoding/replicate_spec.rb - - core/env/element_reference_spec.rb - - core/io/readpartial_spec.rb - - core/io/shared/gets_ascii.rb - - core/marshal/dump_spec.rb - - core/marshal/fixtures/marshal_data.rb - - core/marshal/shared/load.rb - - core/random/bytes_spec.rb - - core/regexp/shared/new_ascii.rb - - core/regexp/shared/new_ascii_8bit.rb - - core/regexp/shared/quote.rb - - core/string/byteslice_spec.rb - - core/string/casecmp_spec.rb - - core/string/codepoints_spec.rb - - core/string/count_spec.rb - - core/string/shared/codepoints.rb - - core/string/shared/each_codepoint_without_block.rb - - core/string/shared/eql.rb - - core/string/shared/succ.rb - - core/string/shared/to_sym.rb - - core/string/squeeze_spec.rb - - core/string/unpack/{b,c,h,m}_spec.rb - - core/string/unpack/shared/float.rb - - core/string/unpack/shared/integer.rb - - core/string/unpack/{u,w}_spec.rb - - core/symbol/casecmp_spec.rb - - core/time/_dump_spec.rb - - core/time/_load_spec.rb - - language/fixtures/binary_symbol.rb - - language/fixtures/squiggly_heredoc.rb - - language/for_spec.rb - - language/regexp/encoding_spec.rb - - language/regexp/escapes_spec.rb - - language/string_spec.rb - - library/digest/md5/shared/constants.rb - - library/digest/md5/shared/sample.rb - - library/digest/sha1/shared/constants.rb - - library/digest/sha256/shared/constants.rb - - library/digest/sha384/shared/constants.rb - - library/digest/sha512/shared/constants.rb - - library/openssl/shared/constants.rb - - library/socket/basicsocket/recv_spec.rb - - library/socket/socket/gethostbyname_spec.rb - - library/stringscanner/getch_spec.rb - - library/stringscanner/shared/get_byte.rb - - library/zlib/inflate/set_dictionary_spec.rb - - optional/capi/integer_spec.rb + repo_ref: master + exclude: [] diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 590c8234..fd8f4ef5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,27 +1,41 @@ -require 'yaml' require 'pathname' +require 'rspec/its' require 'unparser' -require 'anima' -require 'morpher' -require 'devtools/spec_helper' +require 'mutant' +require 'yaml' require 'parser/current' -require 'parser/ruby19' -require 'parser/ruby20' -require 'parser/ruby21' -require 'parser/ruby22' + +RSpec.shared_examples_for 'a command method' do + it 'returns self' do + should equal(object) + end +end + +RSpec.shared_examples_for 'an idempotent method' do + it 'is idempotent' do + first = subject + fail 'RSpec not configured for threadsafety' unless RSpec.configuration.threadsafe? + mutex = __memoized.instance_variable_get(:@mutex) + memoized = __memoized.instance_variable_get(:@memoized) + + mutex.synchronize { memoized.delete(:subject) } + should equal(first) + end +end module SpecHelper def s(type, *children) Parser::AST::Node.new(type, children) end - def strip(source) - source = source.rstrip - indent = source.scan(/^\s*/).min_by(&:length) - source.gsub(/^#{indent}/, '') + def right(value) + Unparser::Either::Right.new(value) end + def left(value) + Unparser::Either::Left.new(value) + end end RSpec.configure do |config| diff --git a/spec/support/parser_class_generator.rb b/spec/support/parser_class_generator.rb deleted file mode 100644 index 7a7c5148..00000000 --- a/spec/support/parser_class_generator.rb +++ /dev/null @@ -1,21 +0,0 @@ -module ParserClassGenerator - def self.generate_with_options(base_parser_class, builder_options) - # This builds a dynamic subclass of the base_parser_class (e.g. Parser::Ruby23) - # and overrides the default_parser method to return a parser whose builder - # has various options set. - # - # Currently the only builder option is :emit_file_line_as_literals - - Class.new(base_parser_class) do - define_singleton_method(:default_parser) do |*args| - super(*args).tap do |parser| - parser.builder.emit_file_line_as_literals = builder_options[:emit_file_line_as_literals] - end - end - - define_singleton_method(:inspect) do - "#{base_parser_class.inspect} with builder options: #{builder_options.inspect}" - end - end - end -end diff --git a/spec/unit/unparser/abstract_type_spec.rb b/spec/unit/unparser/abstract_type_spec.rb new file mode 100644 index 00000000..8416ba50 --- /dev/null +++ b/spec/unit/unparser/abstract_type_spec.rb @@ -0,0 +1,174 @@ +RSpec.shared_examples 'AbstractType.create_new_method' do + context 'called on a subclass' do + let(:object) { Class.new(abstract_type) } + + it { should be_instance_of(object) } + end + + context 'called on the class' do + let(:object) { abstract_type } + + specify do + expect { subject }.to raise_error( + NotImplementedError, + "#{object} is an abstract type" + ) + end + end +end + +RSpec.describe Unparser::AbstractType::AbstractMethodDeclarations, '#abstract_method' do + subject { object.abstract_method(:some_method) } + + let(:object) { Class.new { include Unparser::AbstractType } } + let(:subclass) { Class.new(object) } + + before do + Subclass = subclass + end + + after do + Object.class_eval { remove_const(:Subclass) } + end + + it { should equal(object) } + + it 'creates an abstract method' do + expect { subject }.to change { subclass.method_defined?(:some_method) } + .from(false) + .to(true) + end + + it 'creates an abstract method with the expected arity' do + subject + expect(object.instance_method(:some_method).arity).to be(-1) + end + + it 'creates a method that raises an exception' do + subject + expect { subclass.new.some_method }.to raise_error( + NotImplementedError, + 'Subclass#some_method is not implemented' + ) + end +end + +RSpec.describe( + Unparser::AbstractType::AbstractMethodDeclarations, + '#abstract_singleton_method' +) do + subject { object.abstract_singleton_method(:some_method) } + + let(:object) { Class.new { include Unparser::AbstractType } } + let(:subclass) { Class.new(object) } + + before do + Subclass = subclass + end + + after do + Object.class_eval { remove_const(:Subclass) } + end + + it { should equal(object) } + + it 'creates an abstract method' do + expect { subject }.to change { subclass.respond_to?(:some_method) } + .from(false) + .to(true) + end + + it 'creates an abstract method with the expected arity' do + subject + expect(object.method(:some_method).arity).to be(-1) + end + + it 'creates a method that raises an exception' do + subject + expect { subclass.some_method }.to raise_error( + NotImplementedError, + 'Subclass.some_method is not implemented' + ) + end +end + +RSpec.describe Unparser::AbstractType, '.included' do + subject { object } + + let(:object) { described_class } + let(:klass) { Class.new } + + it 'extends the klass' do + expect(klass.singleton_class) + .to_not include(described_class::AbstractMethodDeclarations) + klass.send(:include, subject) + expect(klass.singleton_class) + .to include(described_class::AbstractMethodDeclarations) + end + + it 'overrides the new singleton method' do + expect(klass.method(:new).owner).to eq(Class) + klass.send(:include, subject) + expect(klass.method(:new).owner).to eq(klass.singleton_class) + end + + it 'delegates to the ancestor' do + included_ancestor = false + subject.extend Module.new { + define_method(:included) { |_| included_ancestor = true } + } + expect { klass.send(:include, subject) } + .to change { included_ancestor }.from(false).to(true) + end +end + +RSpec.describe Unparser::AbstractType, '.create_new_method' do + context 'with arguments' do + subject { object.new(:foo) } + + let(:abstract_type) do + Class.new do + include Unparser::AbstractType + + def initialize(foo) + @foo = foo + end + end + end + + it_behaves_like 'AbstractType.create_new_method' + end + + context 'with a block' do + subject { object.new(:foo) { nil } } + + let(:abstract_type) do + Class.new do + include Unparser::AbstractType + + def initialize(foo) + @foo = foo + yield + end + end + end + + it_behaves_like 'AbstractType.create_new_method' + end + + context 'without arguments' do + subject { object.new } + + let(:abstract_type) { Class.new { include Unparser::AbstractType } } + + it_behaves_like 'AbstractType.create_new_method' + end + + context 'on an class that doesn\'t have Object as its superclass' do + subject { object.new } + + let(:abstract_type) { Class.new(RuntimeError) { include Unparser::AbstractType } } + + it_behaves_like 'AbstractType.create_new_method' + end +end diff --git a/spec/unit/unparser/adamantium/memory_spec.rb b/spec/unit/unparser/adamantium/memory_spec.rb new file mode 100644 index 00000000..74cc2784 --- /dev/null +++ b/spec/unit/unparser/adamantium/memory_spec.rb @@ -0,0 +1,130 @@ +RSpec.describe Unparser::Adamantium::Memory do + describe '#fetch' do + let(:events) { [] } + let(:monitor) { instance_double(Monitor) } + let(:name) { :some_name } + let(:object) { described_class.new(proxy) } + let(:value_was_read) { -> {} } + + let(:block) do + lambda do + events << :block_call + @counter += 1 + end + end + + let(:proxy) do + proxy = instance_double(Hash) + values = values() + + allow(proxy).to receive(:fetch) do |name, &block| + events << :fetch + values.fetch(name) do + value_was_read.call + block.call + end + end + + allow(proxy).to receive(:[]=) do |name, value| + events << :set + values[name] = value + end + + proxy + end + + + def apply + object.fetch(name, &block) + end + + before do + allow(Monitor).to receive_messages(new: monitor) + + allow(monitor).to receive(:synchronize) do |&block| + events << :synchronize_start + block.call.tap do + events << :synchronize_end + end + end + + @counter = 0 + end + + shared_examples 'expected events' do + it 'triggers expected events' do + expect { apply } + .to change(events, :to_a) + .from([]).to(expected_events) + end + + it 'returns expected value' do + expect(apply).to be(1) + end + + it 'creates frozen objects' do + expect(object.frozen?).to be(true) + end + end + + context 'when value is present in memory' do + let(:values) { { name => 1 } } + let(:expected_events) { %i[fetch] } + + include_examples 'expected events' + end + + context 'when value is not present in memory initially' do + let(:values) { {} } + + let(:expected_events) do + %i[ + fetch + synchronize_start + fetch + block_call + set + synchronize_end + ] + end + + include_examples 'expected events' + + context 'but is present inside the lock' do + let(:value_was_read) { ->() { values[name] = 1 } } + + let(:expected_events) do + %i[ + fetch + synchronize_start + fetch + synchronize_end + ] + end + + include_examples 'expected events' + end + + context 'and is re-read after initial generation' do + def apply + super() + super() + end + + let(:expected_events) do + %i[ + fetch + synchronize_start + fetch + block_call + set + synchronize_end + fetch + ] + end + + include_examples 'expected events' + end + end + end +end diff --git a/spec/unit/unparser/adamantium_spec.rb b/spec/unit/unparser/adamantium_spec.rb new file mode 100644 index 00000000..53780067 --- /dev/null +++ b/spec/unit/unparser/adamantium_spec.rb @@ -0,0 +1,414 @@ +RSpec.describe Unparser::Adamantium do + describe '.included' do + subject { descendant.instance_exec(object) { |mod| include mod } } + + let(:object) { described_class } + let(:superclass) { Module } + + around do |example| + # Restore included method after each example + superclass.class_eval do + alias_method :original_included, :included + example.call + undef_method :included + alias_method :included, :original_included + end + end + + shared_examples_for 'all descendant types' do + it 'delegates to the superclass #included method' do + # This is the most succinct approach I could think of to test whether the + # superclass#included method is called. All of the built-in rspec helpers + # did not seem to work for this. + included = 0 + superclass.class_eval { define_method(:included) { |_| included += 1 } } + expect(included).to be(0) + subject + expect(included).to be(1) + end + + it 'extends the descendant with Unparser::Adamantium::ModuleMethods' do + subject + expect(descendant.singleton_class.included_modules) + .to include(Unparser::Adamantium::ModuleMethods) + end + end + + context 'with a class descendant' do + let(:descendant) { Class.new } + + it_behaves_like 'all descendant types' + + it 'extends a class descendant with Unparser::Adamantium::ClassMethods' do + subject + expect(descendant.singleton_class.included_modules) + .to include(Unparser::Adamantium::ClassMethods) + end + end + + context 'with a module descendant' do + let(:descendant) { Module.new } + + it_behaves_like 'all descendant types' + + it 'does not extends a module descendant with Unparser::Adamantium::ClassMethods' do + subject + expect(descendant.singleton_class.included_modules) + .to_not include(Unparser::Adamantium::ClassMethods) + end + end + end + + describe '#new' do + let(:argument) { 'argument' } + + subject { class_under_test.new(argument) } + + let(:class_under_test) do + Class.new do + include Unparser::Adamantium + + attr_reader :argument + + def initialize(argument) + @argument = argument + end + end + end + + it { should be_frozen } + + its(:argument) { should be(argument) } + end + + describe '#dup' do + subject { object.dup } + + let(:object) do + Class.new do + include Unparser::Adamantium + end.new + end + + it { should equal(object) } + end + + describe '#freeze' do + subject { object.freeze } + + let(:class_under_test) do + Class.new do + include Unparser::Adamantium + + def test + end + end + end + + context 'with an unfrozen object' do + let(:object) { class_under_test.allocate } + + it_should_behave_like 'a command method' + + it 'freezes the object' do + expect { subject }.to change(object, :frozen?) + .from(false) + .to(true) + end + end + + context 'with a frozen object' do + let(:object) { class_under_test.new } + + it_should_behave_like 'a command method' + + it 'does not change the frozen state of the object' do + expect { subject }.to_not change(object, :frozen?) + end + end + end + + describe '#memoized?' do + subject { object.memoized?(method) } + + let(:object) do + Class.new do + include Unparser::Adamantium + + def some_method + end + + def some_memoized_method + end + memoize :some_memoized_method + end + end + + context 'when method is not memoized' do + let(:method) { :some_method } + + it { should be(false) } + end + + context 'when method is memoized' do + let(:method) { :some_memoized_method } + + it { should be(true) } + end + end + + describe '#unmemoized_instance_method' do + subject { object.unmemoized_instance_method(method) } + + let(:object) do + Class.new do + include Unparser::Adamantium + + def some_method + end + + def some_memoized_method + +'foo' + end + memoize :some_memoized_method + end + end + + context 'when method is not memoized' do + let(:method) { :some_method } + + it 'raises error' do + expect { subject }.to raise_error( + ArgumentError, + '#some_method is not memoized' + ) + end + end + + context 'when method is memoized' do + let(:method) { :some_memoized_method } + + it 'returns unmemoized method' do + unmemoized = subject + + expect(unmemoized.name).to eql(method) + + instance = object.new + + bound = unmemoized.bind(instance) + + first = bound.call + second = bound.call + + expect(first).to eql('foo') + expect(first).to_not be(second) + end + end + end + + describe '#memoize' do + subject { object.memoize(method) } + + let(:object) do + Class.new do + include Unparser::Adamantium + + def argumented(x) + end + + def some_state + +'' + end + + def some_other_state + end + memoize :some_other_state + + def public_method + end + + protected def protected_method + end + + private def private_method + end + end + end + + shared_examples_for 'memoizes method' do + it 'memoizes the instance method' do + subject + instance = object.new + expect(instance.send(method)).to be(instance.send(method)) + end + + let(:fake) do + Class.new do + attr_reader :messages + + def initialize + @messages = [] + end + + def write(message) + @messages << message + end + end + end + + it 'does not trigger warnings' do + begin + original = $stderr + $stderr = fake.new + subject + expect($stderr.messages).to eql([]) + ensure + $stderr = original + end + end + + it 'does not allow to call memoized method with blocks' do + subject + + expect do + object.new.send(method) { } + end.to raise_error do |error| + expect(error).to be_a( + Unparser::Adamantium::MethodBuilder::BlockNotAllowedError + ) + + expect(error.message).to eql( + "Cannot pass a block to #{object.inspect}##{method}, it is memoized" + ) + end + end + end + + shared_examples_for 'wraps original method' do + it 'creates a method with an arity of 0' do + subject + expect(object.new.method(method).arity).to be_zero + end + + context 'when the initializer calls the memoized method' do + it_should_behave_like 'memoizes method' + + before do + method = self.method + object.send(:define_method, :initialize) { send(method) } + end + + it 'allows the memoized method to be called within the initializer' do + subject + expect { object.new }.to_not raise_error + end + end + end + + context 'on method with arguments' do + let(:method) { :argumented } + + it 'should raise error' do + expect { subject }.to raise_error( + ArgumentError, "Cannot memoize #{object}#argumented, its arity is 1" + ) + end + end + + context 'memoized method that returns generated values' do + let(:method) { :some_state } + + it_should_behave_like 'a command method' + it_should_behave_like 'memoizes method' + it_should_behave_like 'wraps original method' + + it 'creates a method that returns a frozen value' do + subject + expect(object.new.send(method)).to be_frozen + end + + it 'creates a method that returns expected value' do + subject + + expect(object.new.some_state).to eql('') + end + + it 'creates a method that returns same value for each call' do + subject + + instance = object.new + + expect(instance.some_state).to be(instance.some_state) + end + + it 'does not get confused with sibling memoized methods' do + subject + + instance = object.new + instance.some_other_state + + expect(instance.some_other_state).to_not be(instance.some_state) + + expect(instance.some_state).to be(instance.some_state) + end + + it 'does not allow repated memoization' do + subject + + expect { subject.memoize(method) }.to raise_error( + ArgumentError, + "##{method} is already memoized" + ) + end + end + + context 'public method' do + let(:method) { :public_method } + + it_should_behave_like 'a command method' + it_should_behave_like 'memoizes method' + it_should_behave_like 'wraps original method' + + it 'is still a public method' do + should be_public_method_defined(method) + end + + it 'creates a method that returns a frozen value' do + subject + expect(object.new.send(method)).to be_frozen + end + end + + context 'protected method' do + let(:method) { :protected_method } + + it_should_behave_like 'a command method' + it_should_behave_like 'memoizes method' + + it 'is still a protected method' do + should be_protected_method_defined(method) + end + + it 'creates a method that returns a frozen value' do + subject + expect(object.new.send(method)).to be_frozen + end + end + + context 'private method' do + let(:method) { :private_method } + + it_should_behave_like 'a command method' + it_should_behave_like 'memoizes method' + + it 'is still a private method' do + should be_private_method_defined(method) + end + + it 'creates a method that returns a frozen value' do + subject + expect(object.new.send(method)).to be_frozen + end + end + end +end diff --git a/spec/unit/unparser/anima/attribute_spec.rb b/spec/unit/unparser/anima/attribute_spec.rb new file mode 100644 index 00000000..deead1a6 --- /dev/null +++ b/spec/unit/unparser/anima/attribute_spec.rb @@ -0,0 +1,62 @@ +describe Unparser::Anima::Attribute do + let(:object) { described_class.new(:foo) } + + describe '#get' do + subject { object.get(target) } + + let(:target_class) do + Class.new do + attr_reader :foo + + def initialize(foo) + @foo = foo + end + end + end + + let(:target) { target_class.new(value) } + let(:value) { double('Value') } + + it 'should return value' do + should be(value) + end + end + + describe '#load' do + subject { object.load(target, attribute_hash) } + + let(:target) { Object.new } + let(:value) { double('Value') } + let(:attribute_hash) { { foo: value } } + + it 'should set value as instance variable' do + subject + expect(target.instance_variable_get(:@foo)).to be(value) + end + + it_should_behave_like 'a command method' + end + + describe '#instance_variable_name' do + subject { object.instance_variable_name } + + it { should be(:@foo) } + + it_should_behave_like 'an idempotent method' + end + + describe '#set' do + subject { object.set(target, value) } + + let(:target) { Object.new } + + let(:value) { double('Value') } + + it_should_behave_like 'a command method' + + it 'should set value as instance variable' do + subject + expect(target.instance_variable_get(:@foo)).to be(value) + end + end +end diff --git a/spec/unit/unparser/anima/error_spec.rb b/spec/unit/unparser/anima/error_spec.rb new file mode 100644 index 00000000..4724b3a3 --- /dev/null +++ b/spec/unit/unparser/anima/error_spec.rb @@ -0,0 +1,16 @@ +describe Unparser::Anima::Error do + describe '#message' do + let(:object) { described_class.new(Unparser::Anima, missing, unknown) } + + let(:missing) { %i[missing] } + let(:unknown) { %i[unknown] } + + subject { object.message } + + it 'should return the message string' do + should eql('Unparser::Anima attributes missing: [:missing], unknown: [:unknown]') + end + + it_should_behave_like 'an idempotent method' + end +end diff --git a/spec/unit/unparser/anima_spec.rb b/spec/unit/unparser/anima_spec.rb new file mode 100644 index 00000000..ff3fd1cd --- /dev/null +++ b/spec/unit/unparser/anima_spec.rb @@ -0,0 +1,210 @@ +describe Unparser::Anima do + let(:object) { described_class.new(:foo) } + + describe '#attributes_hash' do + let(:value) { double('Value') } + let(:instance) { double(foo: value) } + + subject { object.attributes_hash(instance) } + + it { should eql(foo: value) } + end + + describe '#remove' do + let(:object) { described_class.new(:foo, :bar) } + + context 'with single attribute' do + subject { object.remove(:bar) } + + it { should eql(described_class.new(:foo)) } + end + + context 'with multiple attributes' do + subject { object.remove(:foo, :bar) } + + it { should eql(described_class.new) } + end + + context 'with inexisting attribute' do + subject { object.remove(:baz) } + + it { should eql(object) } + end + end + + describe '#add' do + context 'with single attribute' do + subject { object.add(:bar) } + + it { should eql(described_class.new(:foo, :bar)) } + end + + context 'with multiple attributes' do + subject { object.add(:bar, :baz) } + + it { should eql(described_class.new(:foo, :bar, :baz)) } + end + + context 'with duplicate attribute ' do + subject { object.add(:foo) } + + it { should eql(object) } + end + end + + describe '#attributes' do + subject { object.attributes } + + it { should eql([Unparser::Anima::Attribute.new(:foo)]) } + it { should be_frozen } + end + + describe '#included' do + let(:target) do + object = self.object + Class.new do + include object + end + end + + let(:value) { double('Value') } + let(:instance) { target.new(foo: value) } + let(:instance_b) { target.new(foo: value) } + let(:instance_c) { target.new(foo: double('Bar')) } + + context 'on instance' do + subject { instance } + + it { should eql(instance_b) } + it { should_not eql(instance_c) } + + it 'returns expected value' do + expect(instance.foo).to be(value) + end + end + + context 'on singleton' do + subject { target } + + it 'should define attribute hash reader' do + expect(instance.to_h).to eql(foo: value) + end + + specify { expect(subject.anima).to be(object) } + end + end + + describe '#initialize_instance' do + let(:object) { Unparser::Anima.new(:foo, :bar) } + let(:target) { Object.new } + + let(:foo) { double('Foo') } + let(:bar) { double('Bar') } + + subject { object.initialize_instance(target, attribute_hash) } + + context 'when all keys are present in attribute hash' do + let(:attribute_hash) { { foo: foo, bar: bar } } + + it 'should initialize target instance variables' do + subject + + expect( + target + .instance_variables + .map(&:to_sym) + .to_set + ).to eql(%i[@foo @bar].to_set) + expect(target.instance_variable_get(:@foo)).to be(foo) + expect(target.instance_variable_get(:@bar)).to be(bar) + end + + it_should_behave_like 'a command method' + end + + context 'when an extra key is present in attribute hash' do + let(:attribute_hash) { { foo: foo, bar: bar, baz: double('Baz') } } + + it 'should raise error' do + expect { subject }.to raise_error( + Unparser::Anima::Error, + Unparser::Anima::Error.new(target.class, [], [:baz]).message + ) + end + + context 'and the extra key is falsy' do + let(:attribute_hash) { { foo: foo, bar: bar, nil => double('Baz') } } + + it 'should raise error' do + expect { subject }.to raise_error( + Unparser::Anima::Error, + Unparser::Anima::Error.new(target.class, [], [nil]).message + ) + end + end + end + + context 'when a key is missing in attribute hash' do + let(:attribute_hash) { { bar: bar } } + + it 'should raise error' do + expect { subject }.to raise_error( + Unparser::Anima::Error.new(target.class, [:foo], []).message + ) + end + end + end + + describe 'using super in initialize' do + subject { klass.new } + + let(:klass) do + Class.new do + include Unparser::Anima.new(:foo) + def initialize(attributes = { foo: :bar }) + super + end + end + end + + specify { expect(subject.foo).to eql(:bar) } + end + + describe '#to_h on an anima infected instance' do + subject { instance.to_h } + + let(:instance) { klass.new(params) } + let(:params) { Hash[foo: :bar] } + let(:klass) do + Class.new do + include Unparser::Anima.new(:foo) + end + end + + it { should eql(params) } + end + + describe '#with' do + subject { object.with(attributes) } + + let(:klass) do + Class.new do + include Unparser::Anima.new(:foo, :bar) + end + end + + let(:object) { klass.new(foo: 1, bar: 2) } + + context 'with empty attributes' do + let(:attributes) { {} } + + it { should eql(object) } + end + + context 'with updated attribute' do + let(:attributes) { { foo: 3 } } + + it { should eql(klass.new(foo: 3, bar: 2)) } + end + end +end diff --git a/spec/unit/unparser/buffer/append_spec.rb b/spec/unit/unparser/buffer/append_spec.rb deleted file mode 100644 index 1c315b63..00000000 --- a/spec/unit/unparser/buffer/append_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#append' do - subject { object.append(string) } - - let(:object) { described_class.new } - let(:string) { 'foo' } - - specify do - expect { subject }.to change { object.content }.from('').to('foo') - end - - # Yeah duplicate, mutant will be improved ;) - it 'should prefix with indentation if line is empty' do - object.append('foo') - object.nl - object.indent - object.append('bar') - object.append('baz') - expect(object.content).to eql("foo\n barbaz") - end - - it_should_behave_like 'a command method' -end diff --git a/spec/unit/unparser/buffer/append_without_prefix_spec.rb b/spec/unit/unparser/buffer/append_without_prefix_spec.rb deleted file mode 100644 index 695c1bb4..00000000 --- a/spec/unit/unparser/buffer/append_without_prefix_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#append_without_prefix' do - subject { object.append_without_prefix(string) } - - let(:object) { described_class.new } - let(:string) { 'foo' } - - specify do - expect { subject }.to change { object.content }.from('').to('foo') - end - - it 'should not prefix with indentation' do - object.append_without_prefix('foo') - object.nl - object.indent - object.append_without_prefix('bar') - object.append_without_prefix('baz') - expect(object.content).to eql("foo\nbarbaz") - end - - it_should_behave_like 'a command method' -end diff --git a/spec/unit/unparser/buffer/capture_content_spec.rb b/spec/unit/unparser/buffer/capture_content_spec.rb deleted file mode 100644 index 8937e6df..00000000 --- a/spec/unit/unparser/buffer/capture_content_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#capture_content' do - - let(:object) { described_class.new } - - it 'should capture only the content appended within the block' do - object.append('foo') - object.nl - object.indent - captured = object.capture_content do - object.append('bar') - object.nl - end - expect(captured).to eql(" bar\n") - end -end diff --git a/spec/unit/unparser/buffer/content_spec.rb b/spec/unit/unparser/buffer/content_spec.rb deleted file mode 100644 index 067d9269..00000000 --- a/spec/unit/unparser/buffer/content_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#content' do - subject { object.content } - - let(:object) { described_class.new } - - shared_examples_for 'buffer content' do - it 'contains expected content' do - should eql(expected_content) - end - - it { should be_frozen } - - it 'returns fresh string copies' do - first = object.content - second = object.content - expect(first).to eql(second) - expect(first).not_to be(second) - end - end - - context 'with empty buffer' do - let(:expected_content) { '' } - - it_should_behave_like 'buffer content' - end - - context 'with filled buffer' do - before do - object.append('foo') - end - - let(:expected_content) { 'foo' } - - it_behaves_like 'buffer content' - end -end diff --git a/spec/unit/unparser/buffer/fresh_line_spec.rb b/spec/unit/unparser/buffer/fresh_line_spec.rb deleted file mode 100644 index dc01499e..00000000 --- a/spec/unit/unparser/buffer/fresh_line_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#fresh_line?' do - let(:object) { described_class.new } - - it 'should return true while buffer is empty' do - expect(object.fresh_line?).to eql(true) - end - - it 'should return false after content has been appended' do - object.append('foo') - expect(object.fresh_line?).to eql(false) - end - - it 'should return true after a nl has been appended' do - object.append('foo') - object.nl - expect(object.fresh_line?).to eql(true) - end -end diff --git a/spec/unit/unparser/buffer/indent_spec.rb b/spec/unit/unparser/buffer/indent_spec.rb deleted file mode 100644 index e38084b4..00000000 --- a/spec/unit/unparser/buffer/indent_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#indent' do - let(:object) { described_class.new } - - subject { object.indent } - - it 'should indent with two spaces' do - object.append('foo') - object.nl - object.indent - object.append('bar') - object.nl - object.indent - object.append('baz') - expect(object.content).to eql("foo\n bar\n baz") - end - - it_should_behave_like 'a command method' -end diff --git a/spec/unit/unparser/buffer/nl_spec.rb b/spec/unit/unparser/buffer/nl_spec.rb deleted file mode 100644 index 1917c047..00000000 --- a/spec/unit/unparser/buffer/nl_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#nl' do - let(:object) { described_class.new } - - subject { object.nl } - - it 'writes a newline' do - object.append('foo') - subject - object.append('bar') - expect(object.content).to eql("foo\nbar") - end - - it_should_behave_like 'a command method' -end diff --git a/spec/unit/unparser/buffer/unindent_spec.rb b/spec/unit/unparser/buffer/unindent_spec.rb deleted file mode 100644 index cc8202e4..00000000 --- a/spec/unit/unparser/buffer/unindent_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe Unparser::Buffer, '#unindent' do - let(:object) { described_class.new } - - subject { object.unindent } - - it 'unindents two chars' do - object.append('foo') - object.nl - object.indent - object.append('bar') - object.nl - object.unindent - object.append('baz') - expect(object.content).to eql("foo\n bar\nbaz") - end - - it_should_behave_like 'a command method' -end diff --git a/spec/unit/unparser/buffer_spec.rb b/spec/unit/unparser/buffer_spec.rb new file mode 100644 index 00000000..af608f20 --- /dev/null +++ b/spec/unit/unparser/buffer_spec.rb @@ -0,0 +1,287 @@ +require 'spec_helper' + +describe Unparser::Buffer do + describe '#append' do + subject { object.append(string) } + + let(:object) { described_class.new } + let(:string) { 'foo' } + + specify do + expect { subject }.to change { object.content }.from('').to('foo') + end + + it 'should prefix with indentation if line is empty' do + object.append('foo') + object.nl + object.indent + object.append('bar') + object.append('baz') + expect(object.content).to eql("foo\n barbaz") + end + + it_should_behave_like 'a command method' + end + + describe '#append_without_prefix' do + subject { object.append_without_prefix(string) } + + let(:object) { described_class.new } + let(:string) { 'foo' } + + specify do + expect { subject }.to change { object.content }.from('').to('foo') + end + + it 'should not prefix with indentation' do + object.append_without_prefix('foo') + object.nl + object.indent + object.append_without_prefix('bar') + object.append_without_prefix('baz') + expect(object.content).to eql("foo\nbarbaz") + end + + it_should_behave_like 'a command method' + end + + describe '#content' do + subject { object.content } + + let(:object) { described_class.new } + + shared_examples_for 'buffer content' do + it 'contains expected content' do + should eql(expected_content) + end + + it { should be_frozen } + + it 'returns fresh string copies' do + first = object.content + second = object.content + expect(first).to eql(second) + expect(first).not_to be(second) + end + end + + context 'with empty buffer' do + let(:expected_content) { '' } + + it_should_behave_like 'buffer content' + end + + context 'with filled buffer' do + before do + object.append('foo') + end + + let(:expected_content) { 'foo' } + + it_behaves_like 'buffer content' + end + end + + describe '#fresh_line?' do + let(:object) { described_class.new } + + it 'should return true while buffer is empty' do + expect(object.fresh_line?).to eql(true) + end + + it 'should return false after content has been appended' do + object.append('foo') + expect(object.fresh_line?).to eql(false) + end + + it 'should return true after a nl has been appended' do + object.append('foo') + object.nl + expect(object.fresh_line?).to eql(true) + end + end + + describe '#indent' do + let(:object) { described_class.new } + + subject { object.indent } + + it 'should indent with two spaces' do + object.append('foo') + object.nl + object.indent + object.append('bar') + object.nl + object.indent + object.append('baz') + expect(object.content).to eql("foo\n bar\n baz") + end + + it_should_behave_like 'a command method' + end + + describe '#nl' do + let(:object) { described_class.new } + + subject { object.nl } + + it 'writes a newline' do + object.append('foo') + subject + object.append('bar') + expect(object.content).to eql("foo\nbar") + end + + it 'keeps track to allow to write final' do + object.append('foo') + subject + object.append('bar') + object.final_newline + expect(object.content).to eql("foo\nbar\n") + end + + it 'flushes heredocs' do + object.push_heredoc('HEREDOC') + subject + object.nl + expect(object.content).to eql("\nHEREDOC\n") + end + + it_should_behave_like 'a command method' + end + + describe '#unindent' do + let(:object) { described_class.new } + + subject { object.unindent } + + it 'unindents two chars' do + object.append('foo') + object.nl + object.indent + object.append('bar') + object.nl + object.unindent + object.append('baz') + expect(object.content).to eql("foo\n bar\nbaz") + end + + it_should_behave_like 'a command method' + end + + describe '#write_encoding' do + let(:object) { described_class.new } + + subject { object.write_encoding(Encoding::ASCII) } + + it 'unindents two chars' do + subject + expect(object.content).to eql("# -*- encoding: US-ASCII -*-\n") + end + + it_should_behave_like 'a command method' + end + + describe '#nl_flush_heredocs' do + let(:object) { described_class.new } + + subject { object.nl_flush_heredocs } + + context 'on unbuffered heredoc' do + context 'on fresh line' do + it 'does nothing' do + subject + expect(object.content).to eql('') + end + end + + context 'outside fresh line' do + it 'does nothing' do + object.write('foo') + subject + expect(object.content).to eql('foo') + end + end + end + + context 'on buffered heredocs' do + context 'on fresh line' do + it 'flushes heredoc' do + object.push_heredoc('HEREDOC') + subject + expect(object.content).to eql('HEREDOC') + end + end + + context 'outside fresh line' do + it 'flushes heredoc, with new line' do + object.write('foo') + object.push_heredoc('HEREDOC') + subject + expect(object.content).to eql("foo\nHEREDOC") + end + end + end + end + + describe '#final_newline' do + let(:object) { described_class.new } + + subject { object.final_newline } + + context 'when empty' do + it 'does nothing' do + subject + expect(object.content).to eql('') + end + end + + context 'on one line without newline' do + it 'does not create a new line' do + object.write('foo') + subject + expect(object.content).to eql('foo') + end + end + + context 'on one line with newline' do + it 'does not create a new line' do + object.write('foo') + object.nl + subject + expect(object.content).to eql("foo\n") + end + end + + context 'more than one line, without terminating newline' do + it 'does terminate with newline' do + object.write('foo') + object.nl + object.write('bar') + subject + expect(object.content).to eql("foo\nbar\n") + end + end + end + + describe '#ensure_nl' do + let(:object) { described_class.new } + + subject { object.ensure_nl } + + context 'when on a new line' do + it 'crates a new line' do + subject + expect(object.content).to eql('') + end + end + + context 'when not on a new line' do + it 'crates a new line' do + object.write('foo') + subject + expect(object.content).to eql("foo\n") + end + end + end +end diff --git a/spec/unit/unparser/color_spec.rb b/spec/unit/unparser/color_spec.rb new file mode 100644 index 00000000..348384bc --- /dev/null +++ b/spec/unit/unparser/color_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Unparser::Color do + shared_examples 'actual color' do |code| + describe '#format' do + + it 'returns formatted string' do + expect(apply).to eql("\e[#{code}mexample-string\e[0m") + end + end + end + + describe '#format' do + let(:input) { 'example-string' } + + def apply + object.format(input) + end + + context 'RED' do + let(:object) { described_class::RED } + + include_examples 'actual color', 31 + end + + context 'GREEN' do + let(:object) { described_class::GREEN } + + include_examples 'actual color', 32 + end + + context 'NONE' do + let(:object) { described_class::NONE } + + it 'returns original input' do + expect(apply).to be(input) + end + end + end +end diff --git a/spec/unit/unparser/comments/consume_spec.rb b/spec/unit/unparser/comments/consume_spec.rb index 1feb715f..ada981ef 100644 --- a/spec/unit/unparser/comments/consume_spec.rb +++ b/spec/unit/unparser/comments/consume_spec.rb @@ -2,21 +2,20 @@ describe Unparser::Comments, '#consume' do - let(:ast_and_comments) do - Parser::CurrentRuby.parse_with_comments(<<-RUBY) + let(:ast) do + Unparser.parse_ast(<<~'RUBY') def hi # EOL 1 end # EOL 2 RUBY end - let(:ast) { ast_and_comments[0] } - let(:comments) { ast_and_comments[1] } - let(:object) { described_class.new(comments) } + + let(:object) { described_class.new(ast.comments) } it 'should cause further EOL comments to be returned' do expect(object.take_eol_comments).to eql([]) - object.consume(ast, :name) - expect(object.take_eol_comments).to eql([comments[0]]) - object.consume(ast, :end) - expect(object.take_eol_comments).to eql([comments[1]]) + object.consume(ast.node, :name) + expect(object.take_eol_comments).to eql([ast.comments[0]]) + object.consume(ast.node, :end) + expect(object.take_eol_comments).to eql([ast.comments[1]]) end end diff --git a/spec/unit/unparser/comments/take_all_spec.rb b/spec/unit/unparser/comments/take_all_spec.rb index 23f023c9..b28382e3 100644 --- a/spec/unit/unparser/comments/take_all_spec.rb +++ b/spec/unit/unparser/comments/take_all_spec.rb @@ -1,19 +1,17 @@ require 'spec_helper' describe Unparser::Comments, '#take_all' do - - let(:ast_and_comments) do - Parser::CurrentRuby.parse_with_comments(<<-RUBY) + let(:ast) do + Unparser.parse_ast(<<~'RUBY') def hi # EOL 1 end # EOL 2 RUBY end - let(:ast) { ast_and_comments[0] } - let(:comments) { ast_and_comments[1] } - let(:object) { described_class.new(comments) } + + let(:object) { described_class.new(ast.comments) } it 'should take all comments' do - expect(object.take_all).to eql(comments) + expect(object.take_all).to eql(ast.comments) expect(object.take_all).to eql([]) end end diff --git a/spec/unit/unparser/comments/take_before_spec.rb b/spec/unit/unparser/comments/take_before_spec.rb index afa14ec4..de681d4e 100644 --- a/spec/unit/unparser/comments/take_before_spec.rb +++ b/spec/unit/unparser/comments/take_before_spec.rb @@ -1,15 +1,11 @@ require 'spec_helper' describe Unparser::Comments, '#take_before' do - - let(:ast) { ast_and_comments[0] } - let(:comments) { ast_and_comments[1] } - let(:object) { described_class.new(comments) } + let(:object) { described_class.new(ast.comments) } context 'usual case' do - - let(:ast_and_comments) do - Parser::CurrentRuby.parse_with_comments(<<-RUBY) + let(:ast) do + Unparser.parse_ast(<<~'RUBY') def hi # EOL 1 # comment end # EOL 2 @@ -17,30 +13,30 @@ def hi # EOL 1 end it 'should return no comments if none are before the node' do - expect(object.take_before(ast, :expression)).to eql([]) + expect(object.take_before(ast.node, :expression)).to eql([]) end it 'should return only the comments that are before the specified part of the node' do - expect(object.take_before(ast, :end)).to eql(comments.first(2)) - expect(object.take_all).to eql([comments[2]]) + expect(object.take_before(ast.node, :end)).to eql(ast.comments.first(2)) + expect(object.take_all).to eql([ast.comments[2]]) end end context 'when node does not respond to source part' do - let(:ast_and_comments) do - Parser::CurrentRuby.parse_with_comments(<<-RUBY) + let(:ast) do + Unparser.parse_ast(<<~'RUBY') expression ? :foo : :bar # EOL 1 # EOL 2 RUBY end it 'should return no comments if none are before the node' do - expect(object.take_before(ast, :expression)).to eql([]) + expect(object.take_before(ast.node, :expression)).to eql([]) end it 'should return only the comments that are before the specified part of the node' do - expect(object.take_before(ast, :end)).to eql([]) + expect(object.take_before(ast.node, :end)).to eql([]) end end end diff --git a/spec/unit/unparser/comments/take_eol_comments_spec.rb b/spec/unit/unparser/comments/take_eol_comments_spec.rb index 727ab1bf..22ea3f21 100644 --- a/spec/unit/unparser/comments/take_eol_comments_spec.rb +++ b/spec/unit/unparser/comments/take_eol_comments_spec.rb @@ -1,32 +1,30 @@ require 'spec_helper' describe Unparser::Comments, '#take_eol_comments' do - - let(:ast_and_comments) do - Parser::CurrentRuby.parse_with_comments(<<-RUBY) -def hi # EOL 1 -=begin -doc comment -=end -end # EOL 2 + let(:ast) do + Unparser.parse_ast(<<~'RUBY') + def hi # EOL 1 + =begin + doc comment + =end + end # EOL 2 RUBY end - let(:ast) { ast_and_comments[0] } - let(:comments) { ast_and_comments[1] } - let(:object) { described_class.new(comments) } + + let(:object) { described_class.new(ast.comments) } it 'should return no comments if nothing has been consumed' do expect(object.take_eol_comments).to eql([]) end it 'should return comments once their line has been consumed' do - object.consume(ast, :name) - expect(object.take_eol_comments).to eql([comments[0]]) + object.consume(ast.node, :name) + expect(object.take_eol_comments).to eql([ast.comments[0]]) end it 'should leave doc comments to be taken later' do - object.consume(ast) - expect(object.take_eol_comments).to eql([comments[0], comments[2]]) - expect(object.take_all).to eql([comments[1]]) + object.consume(ast.node) + expect(object.take_eol_comments).to eql([ast.comments[0], ast.comments[2]]) + expect(object.take_all).to eql([ast.comments[1]]) end end diff --git a/spec/unit/unparser/concord_spec.rb b/spec/unit/unparser/concord_spec.rb new file mode 100644 index 00000000..548b84a3 --- /dev/null +++ b/spec/unit/unparser/concord_spec.rb @@ -0,0 +1,143 @@ +RSpec.describe Unparser::Concord do + let(:class_under_test) do + Class.new do + include Unparser::Concord.new(:foo, :bar) + end + end + + let(:instance_a) { class_under_test.new(foo, bar) } + let(:instance_b) { class_under_test.new(foo, bar) } + let(:instance_c) { class_under_test.new(foo, double('Baz')) } + + let(:foo) { double('Foo') } + let(:bar) { double('Bar') } + + context 'initializer' do + it 'creates a private #initialize method' do + mod = Module.new + expect { mod.send(:include, Unparser::Concord.new) } + .to change { mod.private_method_defined?(:initialize) } + .from(false).to(true) + end + + it 'does not cause warnings' do + begin + original = $stderr + $stderr = StringIO.new + Class.new do + include Unparser::Concord.new + end + expect($stderr.tap(&:rewind).read).to eql('') + ensure + $stderr = original + end + end + + it 'creates an initializer that asserts the number of arguments' do + expect { class_under_test.new(1) } + .to raise_error(ArgumentError, 'wrong number of arguments (1 for 2)') + end + + it 'creates an initializer that allows 2 arguments' do + expect { class_under_test.new(1, 2) }.to_not raise_error + end + + it 'creates an initializer that is callable via super' do + class_under_test.class_eval do + attr_reader :baz + public :foo + public :bar + + def initialize(foo, bar) + @baz = foo + bar + super(foo, bar) + end + end + + instance = class_under_test.new(1, 2) + expect(instance.foo).to eql(1) + expect(instance.bar).to eql(2) + expect(instance.baz).to eql(3) + end + + it 'creates an initializer that is callable via zsuper' do + class_under_test.class_eval do + attr_reader :baz + public :foo + public :bar + + def initialize(foo, bar) + @baz = foo + bar + super + end + end + + instance = class_under_test.new(1, 2) + expect(instance.foo).to eql(1) + expect(instance.bar).to eql(2) + expect(instance.baz).to eql(3) + end + + it 'creates an initializer that sets the instance variables' do + instance = class_under_test.new(1, 2) + expect(instance.instance_variable_get(:@foo)).to be(1) + expect(instance.instance_variable_get(:@bar)).to be(2) + end + end + + context 'with no objects to compose' do + it 'assigns no ivars' do + instance = Class.new { include Unparser::Concord.new }.new + expect(instance.instance_variables).to be_empty + end + end + + context 'visibility' do + it 'should set attribute readers to protected' do + protected_methods = class_under_test.protected_instance_methods + expect(protected_methods).to match_array([:foo, :bar]) + end + end + + context 'attribute behavior' do + subject { instance_a } + + specify { expect(subject.send(:foo)).to be(foo) } + specify { expect(subject.send(:bar)).to be(bar) } + end + + context 'equalization behavior' do + specify 'composed objects are equalized on attributes' do + expect(instance_a).to eql(instance_b) + expect(instance_a.hash).to eql(instance_b.hash) + expect(instance_a).to eql(instance_b) + expect(instance_a).to_not be(instance_c) + expect(instance_a).to_not eql(instance_c) + end + end + + context 'when composing too many objects' do + specify 'it raises an error' do + expect do + Unparser::Concord.new(:a, :b, :c, :d) + end.to raise_error(RuntimeError, 'Composition of more than 3 objects is not allowed') + expect do + Unparser::Concord.new(:a, :b, :c) + end.to_not raise_error + end + end + + context Unparser::Concord::Public do + let(:class_under_test) do + Class.new do + include Unparser::Concord::Public.new(:foo, :bar) + end + end + + it 'should create public attr readers' do + object = class_under_test.new(:foo, :bar) + expect(object.foo).to eql(:foo) + expect(object.bar).to eql(:bar) + end + end +end diff --git a/spec/unit/unparser/diff_spec.rb b/spec/unit/unparser/diff_spec.rb new file mode 100644 index 00000000..a25b4752 --- /dev/null +++ b/spec/unit/unparser/diff_spec.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +RSpec.describe Unparser::Diff do + let(:object) { described_class } + + describe '.build' do + + subject { object.build(old_string, new_string) } + + let(:old_string) { "foo\nbar" } + let(:new_string) { "bar\nbaz" } + + it { should eql(described_class.new(%w[foo bar], %w[bar baz])) } + + end + + describe '#colorized_diff' do + let(:object) { described_class.new(old, new) } + + subject { object.colorized_diff } + + context 'when there is a diff at begin of hunk' do + let(:old) { %w[foo bar] } + let(:new) { %w[baz bar] } + + let(:expectation) do + [ + "@@ -1,2 +1,2 @@\n", + Unparser::Color::RED.format("-foo\n"), + Unparser::Color::GREEN.format("+baz\n"), + " bar\n" + ].join + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when there is no diff' do + let(:old) { '' } + let(:new) { '' } + + it { should be(nil) } + + it_should_behave_like 'an idempotent method' + end + end + + describe '#diff' do + let(:object) { described_class.new(old, new) } + + subject { object.diff } + + context 'when there is a diff at begin and end' do + let(:old) { %w[foo bar foo] } + let(:new) { %w[baz bar baz] } + + let(:expectation) do + <<~STR + @@ -1,3 +1,3 @@ + -foo + +baz + bar + -foo + +baz + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when there is a diff at begin of hunk' do + let(:old) { %w[foo bar] } + let(:new) { %w[baz bar] } + + let(:expectation) do + <<~STR + @@ -1,2 +1,2 @@ + -foo + +baz + bar + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when there is a diff NOT at begin of hunk' do + let(:old) { %w[foo bar] } + let(:new) { %w[foo baz bar] } + + let(:expectation) do + <<~STR + @@ -1,2 +1,3 @@ + foo + +baz + bar + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when the diff has a long context at begin' do + let(:old) { %w[foo bar baz boz a b c] } + let(:new) { %w[foo bar baz boz a b c other] } + + let(:expectation) do + <<~STR + @@ -1,7 +1,8 @@ + foo + bar + baz + boz + a + b + c + +other + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when the diff has a long context at end, deleting' do + let(:old) { %w[other foo bar baz boz a b c] } + let(:new) { %w[foo bar baz boz a b c] } + + let(:expectation) do + <<~STR + @@ -1,8 +1,7 @@ + -other + foo + bar + baz + boz + a + b + c + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when the diff has a long context at end, inserting' do + let(:old) { %w[foo bar baz boz a b c] } + let(:new) { %w[other foo bar baz boz a b c] } + + let(:expectation) do + <<~STR + @@ -1,7 +1,8 @@ + +other + foo + bar + baz + boz + a + b + c + STR + end + + it { should eql(expectation) } + + it_should_behave_like 'an idempotent method' + end + + context 'when there is no diff' do + let(:old) { '' } + let(:new) { '' } + + it { should be(nil) } + + it_should_behave_like 'an idempotent method' + end + end +end diff --git a/spec/unit/unparser/either_spec.rb b/spec/unit/unparser/either_spec.rb new file mode 100644 index 00000000..5dd7bcc9 --- /dev/null +++ b/spec/unit/unparser/either_spec.rb @@ -0,0 +1,378 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'no block evaluation' do + context 'with block' do + let(:block) { -> { fail } } + + it 'does not evaluate block' do + apply + end + end +end + +RSpec.shared_examples 'requires block' do + context 'without block' do + let(:block) { nil } + + specify do + expect { apply }.to raise_error(LocalJumpError) + end + end +end + +RSpec.shared_examples 'returns self' do + it 'returns self' do + expect(apply).to be(subject) + end +end + +RSpec.shared_examples '#bind block evaluation' do + it 'evaluates block and returns its wrapped result' do + expect { expect(apply).to eql(block_result) } + .to change(yields, :to_a) + .from([]) + .to([value]) + end +end + +RSpec.shared_examples 'Functor#fmap block evaluation' do + it 'evaluates block and returns its wrapped result' do + expect { expect(apply).to eql(described_class.new(block_result)) } + .to change(yields, :to_a) + .from([]) + .to([value]) + end +end + +RSpec.describe Unparser::Either do + describe '.wrap_error' do + let(:block) { -> { fail error } } + let(:error) { exception.new } + let(:exception) { TestError } + let(:other_exception) { OtherTestError } + + class TestError < RuntimeError; end + + class OtherTestError < RuntimeError; end + + shared_examples 'block returns' do + let(:value) { instance_double(Object, 'value') } + let(:block) { -> { value } } + + it 'returns right wrapping block value' do + expect(apply).to eql(described_class::Right.new(value)) + end + end + + shared_examples 'covered exception' do + it 'returns left wrapping exception' do + expect(apply).to eql(described_class::Left.new(error)) + end + end + + shared_examples 'uncovered exception' do + let(:unexpected_exception) { StandardError } + + let(:block) { -> { fail unexpected_exception } } + + it 'returns raises error' do + expect { apply }.to raise_error(unexpected_exception) + end + end + + context 'on single exception argument' do + def apply + described_class.wrap_error(exception, &block) + end + + context 'when block returns' do + include_examples 'block returns' + end + + context 'when block raises' do + context 'with covered exception' do + include_examples 'covered exception' + end + + context 'with uncovered exception' do + include_examples 'uncovered exception' + end + end + end + + context 'on multiple exception arguments' do + def apply + described_class.wrap_error(exception, other_exception, &block) + end + + context 'when block returns' do + include_examples 'block returns' + end + + context 'when block raises' do + context 'with covered exception' do + include_examples 'covered exception' + end + + context 'with uncovered exception' do + include_examples 'uncovered exception' + end + + context 'with other covered exception' do + let(:block) { -> { fail other_error } } + let(:other_error) { other_exception.new } + + it 'returns left wrapping exception' do + expect(apply).to eql(described_class::Left.new(other_error)) + end + end + end + end + end +end + +RSpec.describe Unparser::Either::Left do + subject { described_class.new(value) } + + let(:block_result) { instance_double(Object, 'block result') } + let(:value) { instance_double(Object, 'value') } + let(:yields) { [] } + + let(:block) do + lambda do |value| + yields << value + block_result + end + end + + class TestError < RuntimeError; end + + describe '#fmap' do + def apply + subject.fmap(&block) + end + + include_examples 'no block evaluation' + include_examples 'requires block' + include_examples 'returns self' + end + + describe '#bind' do + def apply + subject.bind(&block) + end + + include_examples 'no block evaluation' + include_examples 'requires block' + include_examples 'returns self' + end + + describe '#from_left' do + def apply + subject.from_left(&block) + end + + it 'returns left value' do + expect(apply).to be(value) + end + + include_examples 'no block evaluation' + end + + describe '#from_right' do + def apply + subject.from_right(&block) + end + + context 'without block' do + let(:block) { nil } + + it 'raises RuntimeError error' do + expect { apply }.to raise_error( + RuntimeError, + "Expected right value, got #{subject.inspect}" + ) + end + end + + context 'with block' do + let(:yields) { [] } + let(:block_return) { instance_double(Object, 'block-return') } + + let(:block) do + lambda do |value| + yields << value + block_return + end + end + + it 'calls block with left value' do + expect { apply }.to change(yields, :to_a).from([]).to([value]) + end + + it 'returns block value' do + expect(apply).to be(block_return) + end + end + end + + describe '#lmap' do + def apply + subject.lmap(&block) + end + + include_examples 'requires block' + include_examples 'Functor#fmap block evaluation' + end + + describe '#either' do + def apply + subject.either(block, -> { fail }) + end + + include_examples '#bind block evaluation' + end + + describe '#left?' do + def apply + subject.left? + end + + it 'returns true' do + expect(apply).to be(true) + end + end + + describe '#right?' do + def apply + subject.right? + end + + it 'returns false' do + expect(apply).to be(false) + end + end +end + +RSpec.describe Unparser::Either::Right do + subject { described_class.new(value) } + + let(:block_result) { instance_double(Object, 'block result') } + let(:value) { instance_double(Object, 'value') } + let(:yields) { [] } + + let(:block) do + lambda do |value| + yields << value + block_result + end + end + + describe '#fmap' do + def apply + subject.fmap(&block) + end + + include_examples 'requires block' + include_examples 'Functor#fmap block evaluation' + end + + describe '#bind' do + def apply + subject.bind(&block) + end + + include_examples 'requires block' + include_examples '#bind block evaluation' + end + + describe '#from_left' do + def apply + subject.from_left(&block) + end + + context 'without block' do + let(:block) { nil } + + it 'raises RuntimeError error' do + expect { apply }.to raise_error( + RuntimeError, + "Expected left value, got #{subject.inspect}" + ) + end + end + + context 'with block' do + let(:yields) { [] } + let(:block_return) { instance_double(Object, 'block-return') } + + let(:block) do + lambda do |value| + yields << value + block_return + end + end + + it 'calls block with right value' do + expect { apply }.to change(yields, :to_a).from([]).to([value]) + end + + it 'returns block value' do + expect(apply).to be(block_return) + end + end + end + + describe '#from_right' do + def apply + subject.from_right(&block) + end + + it 'returns right value' do + expect(apply).to be(value) + end + + include_examples 'no block evaluation' + end + + describe '#lmap' do + def apply + subject.lmap(&block) + end + + include_examples 'requires block' + include_examples 'no block evaluation' + + it 'returns self' do + expect(apply).to be(subject) + end + end + + describe '#either' do + def apply + subject.either(-> { fail }, block) + end + + include_examples '#bind block evaluation' + end + + describe '#left?' do + def apply + subject.left? + end + + it 'returns false' do + expect(apply).to be(false) + end + end + + describe '#right?' do + def apply + subject.right? + end + + it 'returns true' do + expect(apply).to be(true) + end + end +end diff --git a/spec/unit/unparser/emitter/class_methods/handle_spec.rb b/spec/unit/unparser/emitter/class_methods/handle_spec.rb index 026697f7..6fa42a98 100644 --- a/spec/unit/unparser/emitter/class_methods/handle_spec.rb +++ b/spec/unit/unparser/emitter/class_methods/handle_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Unparser::Emitter, '.handle', mutant_expression: 'Unparser::Emitter*' do +describe Unparser::Emitter, '.handle', mutant_expression: 'Unparser*' do subject { class_under_test.class_eval { handle :foo } } let(:class_under_test) do diff --git a/spec/unit/unparser/equalizer_spec.rb b/spec/unit/unparser/equalizer_spec.rb new file mode 100644 index 00000000..b4a272a3 --- /dev/null +++ b/spec/unit/unparser/equalizer_spec.rb @@ -0,0 +1,165 @@ +describe Unparser::Equalizer, '.new' do + let(:object) { described_class } + let(:name) { 'User' } + let(:klass) { ::Class.new } + + context 'with no keys' do + subject { object.new } + + before do + # specify the class #name method + allow(klass).to receive(:name).and_return(name) + klass.send(:include, subject) + end + + let(:instance) { klass.new } + + it { should be_instance_of(object) } + + it { should be_frozen } + + it 'defines #hash and #inspect methods dynamically' do + expect(subject.public_instance_methods(false).map(&:to_s).sort) + .to eql(%w[hash inspect]) + end + + describe '#eql?' do + context 'when the objects are similar' do + let(:other) { instance.dup } + + it { expect(instance.eql?(other)).to be(true) } + end + + context 'when the objects are different' do + let(:other) { double('other') } + + it { expect(instance.eql?(other)).to be(false) } + end + end + + describe '#==' do + context 'when the objects are similar' do + let(:other) { instance.dup } + + it { expect(instance == other).to be(true) } + end + + context 'when the objects are different' do + let(:other) { double('other') } + + it { expect(instance == other).to be(false) } + end + end + + describe '#hash' do + it 'has the expected arity' do + expect(klass.instance_method(:hash).arity).to be(0) + end + + it { expect(instance.hash).to eql([klass].hash) } + end + + describe '#inspect' do + it 'has the expected arity' do + expect(klass.instance_method(:inspect).arity).to be(0) + end + + it { expect(instance.inspect).to eql('#') } + end + end + + context 'with keys' do + subject { object.new(*keys) } + + let(:keys) { %i[firstname lastname].freeze } + let(:firstname) { 'John' } + let(:lastname) { 'Doe' } + let(:instance) { klass.new(firstname, lastname) } + + let(:klass) do + ::Class.new do + attr_reader :firstname, :lastname + private :firstname, :lastname + + def initialize(firstname, lastname) + @firstname = firstname + @lastname = lastname + end + end + end + + before do + # specify the class #inspect method + allow(klass).to receive_messages(name: nil, inspect: name) + klass.send(:include, subject) + end + + it { should be_instance_of(object) } + + it { should be_frozen } + + it 'defines #hash and #inspect methods dynamically' do + expect(subject.public_instance_methods(false).map(&:to_s).sort) + .to eql(%w[hash inspect]) + end + + describe '#eql?' do + context 'when the objects are of the same class with the same values' do + let(:other) { instance.dup } + + it { expect(instance.eql?(other)).to be(true) } + end + + context 'when the objects are of the same class with different values' do + let(:other) { klass.new('Sue', 'Doe') } + + it { expect(instance.eql?(other)).to be(false) } + end + + context 'when the objects are different classes' do + let(:other) { double('other') } + + it { expect(instance.eql?(other)).to be(false) } + end + end + + describe '#==' do + context 'when the objects of the same class with the same values' do + let(:other) { instance.dup } + + it { expect(instance == other).to be(true) } + end + + context 'when the objects are of the same class with different values' do + let(:other) { klass.new('Sue', 'Doe') } + + it { expect(instance.eql?(other)).to be(false) } + end + + context 'when the objects are different type' do + let(:other) { klass.new('Foo', 'Bar') } + + it { expect(instance == other).to be(false) } + end + + context 'when the objects are from different type' do + let(:other) { double('other') } + + it { expect(instance == other).to be(false) } + end + end + + describe '#hash' do + it 'returns the expected hash' do + expect(instance.hash).to eql([firstname, lastname, klass].hash) + end + end + + describe '#inspect' do + it 'returns the expected string' do + expect(instance.inspect) + .to eql('#') + end + end + end +end diff --git a/spec/unit/unparser/invalid_node_error_spec.rb b/spec/unit/unparser/invalid_node_error_spec.rb new file mode 100644 index 00000000..e723552c --- /dev/null +++ b/spec/unit/unparser/invalid_node_error_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.describe Unparser::InvalidNodeError do + let(:node) { s(:some_node) } + let(:message) { 'message'.dup } + + subject { described_class.new(message, node) } + + its(:node) { should be(node) } + its(:message) { should be(message) } + its(:frozen?) { should be(true) } +end diff --git a/spec/unit/unparser/util_spec.rb b/spec/unit/unparser/util_spec.rb new file mode 100644 index 00000000..ef67775e --- /dev/null +++ b/spec/unit/unparser/util_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.describe Unparser::Util, '.one' do + let(:item) { instance_double(Object) } + + def apply + described_class.one(array) + end + + context 'when array has exactly one element' do + context 'and that element is nil' do + let(:array) { [nil] } + + it 'returns nil' do + expect(apply).to be(nil) + end + end + + context 'and that element is false' do + let(:array) { [false] } + + it 'returns false' do + expect(apply).to be(false) + end + end + + context 'and that element is a regular object' do + let(:array) { [item] } + + it 'returns first element' do + expect(apply).to be(item) + end + end + end + + context 'when array is empty' do + let(:array) { [] } + + it 'raises expected error' do + expect { apply } + .to raise_error(described_class::SizeError) + .with_message('expected size to be exactly 1 but size was 0') + end + end + + context 'when array has more than one element' do + let(:array) { [1, 2] } + + it 'raises expected error' do + expect { apply } + .to raise_error(described_class::SizeError) + .with_message('expected size to be exactly 1 but size was 2') + end + end +end diff --git a/spec/unit/unparser/validation_spec.rb b/spec/unit/unparser/validation_spec.rb new file mode 100644 index 00000000..7a2cc469 --- /dev/null +++ b/spec/unit/unparser/validation_spec.rb @@ -0,0 +1,353 @@ +require 'spec_helper' + +RSpec.describe Unparser::Validation do + let(:object) do + described_class.new( + identification: identification, + generated_node: generated_node, + generated_source: generated_source, + original_ast: right(Unparser::AST.from_node(node: original_node)), + original_source: original_source + ) + end + + let(:generated_node) { right(s(:send, s(:int, 1), :foo)) } + let(:generated_source) { right('1.foo') } + let(:identification) { 'example-identification' } + let(:original_node) { s(:send, s(:int, 1), :foo) } + let(:original_source) { right('1.foo') } + + let(:exception) do + left( + instance_double( + RuntimeError, + message: 'foo', + backtrace: Array.new(21, &'line-%02d'.method(:%)) + ) + ) + end + + let(:exception_report) do + <<~'REPORT'.strip + # + line-00 + line-01 + line-02 + line-03 + line-04 + line-05 + line-06 + line-07 + line-08 + line-09 + line-10 + line-11 + line-12 + line-13 + line-14 + line-15 + line-16 + line-17 + line-18 + line-19 + REPORT + end + + def report + object.report + end + + shared_examples 'not successful' do + it 'is not successful' do + expect(object.success?).to be(false) + end + end + + context 'on success' do + # it 'is successful' do + # expect(object.success?).to be(true) + # end + + it 'returns expected report' do + expect(report).to eql(<<~'REPORT'.strip) + example-identification + Original-Source: + 1.foo + Generated-Source: + 1.foo + Original-Node: + (send + (int 1) :foo) + Generated-Node: + (send + (int 1) :foo) + REPORT + end + end + + context 'on failing to generate original source with exception' do + let(:original_source) { exception } + + include_examples 'not successful' + + it 'returns expected report' do + expect(report).to eql(<<~REPORT.strip) + example-identification + Original-Source: + #{exception_report} + Generated-Source: + 1.foo + Original-Node: + (send + (int 1) :foo) + Generated-Node: + (send + (int 1) :foo) + REPORT + end + end + +# context 'on failing to parse generated source due precondition error' do +# let(:generated_node) { left(nil) } + +# include_examples 'not successful' + +# it 'returns expected report' do +# expect(report).to eql(<<~REPORT.strip) +# example-identification +# Original-Source: +# 1.foo +# Generated-Source: +# 1.foo +# Original-Node: +# (send +# (int 1) :foo) +# Generated-Node: +# undefined +# REPORT +# end +# end + +# context 'on failing to parse original source' do +# let(:original_node) { exception } + +# include_examples 'not successful' + +# it 'returns expected report' do +# expect(report).to eql(<<~REPORT.strip) +# example-identification +# Original-Source: +# 1.foo +# Generated-Source: +# 1.foo +# Original-Node: +# #{exception_report} +# Generated-Node: +# (send +# (int 1) :foo) +# REPORT +# end +# end + +# context 'on failing to generate generated source' do +# let(:generated_source) { exception } + +# include_examples 'not successful' + +# it 'returns expected report' do +# expect(report).to eql(<<~REPORT.strip) +# example-identification +# Original-Source: +# 1.foo +# Generated-Source: +# #{exception_report} +# Original-Node: +# (send +# (int 1) :foo) +# Generated-Node: +# (send +# (int 1) :foo) +# REPORT +# end +# end + +# context 'on failing to parse generated source' do +# let(:generated_node) { exception } + +# include_examples 'not successful' + +# it 'returns expected report' do +# expect(report).to eql(<<~REPORT.strip) +# example-identification +# Original-Source: +# 1.foo +# Generated-Source: +# 1.foo +# Original-Node: +# (send +# (int 1) :foo) +# Generated-Node: +# #{exception_report} +# REPORT +# end +# end + +# context 'on generating different node' do +# let(:generated_node) { right(s(:send, s(:int, 1), :bar)) } + +# include_examples 'not successful' + +# it 'returns expected report' do +# diff = [ +# Unparser::Color::NONE.format(" (send\n"), +# Unparser::Color::RED.format("- (int 1) :foo)\n"), +# Unparser::Color::GREEN.format("+ (int 1) :bar)\n") +# ] + +# expect(report).to eql(<<~'REPORT' + diff.join) +# example-identification +# Original-Source: +# 1.foo +# Generated-Source: +# 1.foo +# Original-Node: +# (send +# (int 1) :foo) +# Generated-Node: +# (send +# (int 1) :bar) +# Node-Diff: +# @@ -1,2 +1,2 @@ +# REPORT +# end +# end +# expect(report).to eql(<<~'REPORT' + diff.join) +# example-identification +# Original-Source: +# 1.foo +# Generated-Source: +# 1.foo +# Original-Node: +# (send +# (int 1) :foo) +# Generated-Node: +# (send +# (int 1) :bar) +# Node-Diff: +# @@ -1,3 +1,3 @@ +# REPORT +# end +# end + +# describe '.from_path' do +# def apply +# described_class.from_path(path) +# end + +# let(:path) { instance_double(Pathname, read: source, to_s: '/some/file') } +# let(:source) { 'true' } + +# it 'returns expected validator' do +# expect(apply).to eql( +# described_class.new( +# generated_node: right(s(:true)), +# generated_source: right(source), +# identification: '/some/file', +# original_node: right(s(:true)), +# original_source: right(source) +# ) +# ) +# end +# end + +# describe '.from_string' do +# def apply +# described_class.from_string(source) +# end + +# let(:attributes) do +# { +# generated_node: right(s(:true)), +# generated_source: right(source), +# identification: '(string)', +# original_node: right(s(:true)), +# original_source: right(source) +# } +# end + +# context 'on valid original source' do +# let(:source) { 'true' } + +# it 'returns expected validator' do +# expect(apply).to eql(described_class.new(attributes)) +# end + +# context 'with unparsing error' do +# let(:exception) { RuntimeError.new('example-error') } + +# before do +# allow(Unparser).to receive(:unparse).and_raise(exception) +# end + +# it 'returns expected validator' do +# validator = apply + +# expect(validator.generated_node).to eql(left(nil)) +# expect(validator.generated_source.from_left.class).to be(RuntimeError) +# expect(validator.original_source).to eql(right(source)) +# expect(validator.original_node).to eql(right(s(:true))) +# end +# end +# end + +# context 'on invalid original source' do +# let(:source) { '(' } + +# it 'returns expected validator' do +# validator = apply + +# expect(validator.generated_node).to eql(left(nil)) +# expect(validator.generated_source).to eql(left(nil)) +# expect(validator.original_source).to eql(right(source)) +# expect(validator.original_node.from_left.class).to be(Parser::SyntaxError) +# end +# end +# end + +# describe '.from_node' do +# def apply +# described_class.from_node(node) +# end + +# let(:attributes) do +# { +# generated_node: right(s(:true)), +# generated_source: right('true'), +# identification: '(string)', +# original_node: right(node), +# original_source: right('true') +# } +# end + +# context 'on valid original node' do +# let(:node) { s(:true) } + +# it 'returns expected validator' do +# expect(apply).to eql(described_class.new(attributes)) +# end +# end + +# context 'on invalid original node' do +# let(:node) { s(:foo) } + +# it 'returns expected validator' do +# validator = apply + +# expect(validator.generated_node).to eql(left(nil)) +# expect(validator.generated_source.lmap(&:inspect)).to eql(left(Unparser::UnknownNodeError.new('Unknown node type: :foo').inspect)) +# expect(validator.original_source).to eql(validator.generated_source) +# expect(validator.original_node).to eql(right(node)) +# end +# end +# end +end diff --git a/spec/unit/unparser_spec.rb b/spec/unit/unparser_spec.rb index 2b7b581e..a2499ee1 100644 --- a/spec/unit/unparser_spec.rb +++ b/spec/unit/unparser_spec.rb @@ -1,1405 +1,313 @@ require 'spec_helper' -require 'parser/all' -require 'support/parser_class_generator' -describe Unparser, mutant_expression: 'Unparser::Emitter*' do - describe '.unparse' do - - RUBY_VERSION_PARSERS = IceNine.deep_freeze( - '2.1' => Parser::Ruby21, - '2.2' => Parser::Ruby22, - '2.3' => Parser::Ruby23 - ) +describe Unparser, mutant_expression: 'Unparser*' do + describe '.buffer' do + let(:source) { 'a + b' } - RUBY_VERSIONS = RUBY_VERSION_PARSERS.keys.freeze - - def self.builder_options - @builder_options ||= {} + def apply + described_class.buffer(source) end - def self.builder_options=(options) - @builder_options = options + it 'returns parser buffer with expected name' do + expect(apply.name).to eql('(string)') end - def self.ruby_versions - @ruby_versions ||= RUBY_VERSIONS + it 'returns parser buffer with pre-filled source' do + expect(apply.source).to eql(source) end - def self.ruby_versions=(versions) - @ruby_versions = versions - end - - def self.with_ruby_versions(beginning_at: nil, ending_at: nil, only: nil) - original_ruby_versions = ruby_versions - if only - self.ruby_versions = only & ruby_versions # intersection - else - if ending_at - idx = ruby_versions.index(ending_at) || fail('Invalid Ruby specified') - self.ruby_versions = ruby_versions[0..idx] - end - if beginning_at - idx = ruby_versions.index(beginning_at) || fail('Invalid Ruby specified') - self.ruby_versions = ruby_versions[idx..-1] - end + context 'on non default identification' do + def apply + described_class.buffer(source, '(foo)') end - yield - - self.ruby_versions = original_ruby_versions - end - - def self.current_parsers - ruby_versions.map do |ruby_version| - if builder_options != {} - ParserClassGenerator.generate_with_options(parser_for_ruby_version(ruby_version), builder_options) - else - parser_for_ruby_version(ruby_version) - end + it 'returns parser buffer with expected name' do + expect(apply.name).to eql('(foo)') end end + end - def self.with_builder_options(options) - original_options = builder_options - self.builder_options = builder_options.merge(options) - - yield + describe '.parser' do + let(:invalid_source_buffer) { Unparser.buffer('a +') } - self.builder_options = original_options + def apply + described_class.parser end - def self.parser_for_ruby_version(version) - RUBY_VERSION_PARSERS.fetch(version) do - raise "Unrecognized Ruby version #{version}" + context 'failure' do + def apply + super.tap do |parser| + parser.diagnostics.consumer = ->(_) {} + end end - end - def assert_round_trip(input, parser) - ast, comments = parser.parse_with_comments(input) - generated = Unparser.unparse(ast, comments) - expect(generated).to eql(input) - generated_ast, _comments = parser.parse_with_comments(generated) - expect(ast == generated_ast).to be(true) + it 'returns a parser that fails with syntax error' do + expect { apply.parse(invalid_source_buffer) } + .to raise_error(Parser::SyntaxError) + end end + end - def assert_generates_from_string(parser, string, expected) - string = strip(string) - ast_with_comments = parser.parse_with_comments(string) - assert_generates_from_ast(parser, ast_with_comments, expected) + describe '.parse' do + def apply + described_class.parse(source) end - def assert_generates_from_ast(parser, ast_with_comments, expected) - generated = Unparser.unparse(*ast_with_comments) - expect(generated).to eql(expected) - ast, comments = parser.parse_with_comments(generated) - expect(Unparser.unparse(ast, comments)).to eql(expected) - end + context 'on present source' do + let(:source) { 'self[1]=2' } - def self.assert_unterminated(expression) - assert_source(expression) - assert_source("(#{expression}).foo") + it 'returns expected AST' do + expect(apply).to eql(s(:indexasgn, s(:self), s(:int, 1), s(:int, 2))) + end end - def self.assert_terminated(expression) - assert_source(expression) - assert_source("foo(#{expression})") - assert_source("#{expression}.foo") - end + context 'on empty source' do + let(:source) { '' } - def self.assert_generates(ast_or_string, expected) - current_parsers.each do |parser| - it "should generate #{ast_or_string} as #{expected} under #{parser.inspect}" do - if ast_or_string.is_a?(String) - expected = strip(expected) - assert_generates_from_string(parser, ast_or_string, expected) - else - assert_generates_from_ast(parser, [ast_or_string, []], expected) - end - end + it 'returns ni' do + expect(apply).to be(nil) end end - def self.assert_round_trip(input) - current_parsers.each do |parser| - it "should round trip #{input} under #{parser.inspect}" do - assert_round_trip(input, parser) - end - end - end + context 'on syntax error' do + let(:source) { '[' } - def self.assert_source(input) - assert_round_trip(strip(input)) + it 'raises error' do + expect { apply }.to raise_error(Parser::SyntaxError) + end end + end - context 'kwargs' do - assert_source <<-RUBY - def foo(bar:, baz:) - end - RUBY - - assert_source <<-RUBY - foo(**bar) - RUBY - - assert_source <<-RUBY - def foo(bar:, baz: "value") - end - RUBY + context '.parse_ast_either' do + def apply + described_class.parse_ast_either(source) end - context 'literal' do - context 'int' do - assert_generates '-0', '0' - assert_source '++1' - assert_terminated '1' - assert_unterminated '-1' - assert_generates '0x1', '1' - assert_generates '1_000', '1000' - assert_generates '1e10', '10000000000.0' - assert_generates '10e10000000000', 'Float::INFINITY' - assert_generates '-10e10000000000', '-Float::INFINITY' - end - - context 'rational' do - assert_terminated '1r' - assert_generates '1.0r', '1r' - assert_generates '-0r', '0r' - - assert_terminated '1.5r' - assert_terminated '1.3r' - end + context 'on present source' do + let(:source) { 'self[1]=2' } - context 'complex' do - %w( - 5i - -5i - 0.6i - -0.6i - 1000000000000000000000000000000i - 1ri - ).each do |expression| - assert_terminated(expression) - end - end - - context 'string' do - assert_generates '?c', '"c"' - assert_generates '"foo" "bar"', '"#{"foo"}#{"bar"}"' - assert_generates '"foo" "bar #{baz}"', '"#{"foo"}#{"#{"bar "}#{baz}"}"' - assert_generates '%Q(foo"#{@bar})', '"#{"foo\\""}#{@bar}"' - assert_generates '"foo#{1}bar"', '"#{"foo"}#{1}#{"bar"}"' - assert_generates '"\\\\#{}"', '"#{"\\\\"}#{}"' - assert_generates '"#{}\#{}"', '"#{}#{"\#{}"}"' - assert_generates '"\#{}#{}"', '"#{"\#{}"}#{}"' - assert_terminated '"\""' - assert_terminated '"foo bar"' - assert_terminated '"foo\nbar"' - # Within indentation - assert_generates <<-'RUBY', <<-'RUBY' - if foo - " - #{foo} - " - end - RUBY - if foo - "#{"\n"}#{" "}#{foo}#{"\n"}#{" "}" - end - RUBY + it 'returns right value with expected AST' do + expect(apply.fmap(&:node)).to eql(right(s(:indexasgn, s(:self), s(:int, 1), s(:int, 2)))) end + end - context 'execute string' do - assert_generates '`foo`', '`#{"foo"}`' - assert_generates '`foo#{@bar}`', '`#{"foo"}#{@bar}`' - assert_generates '%x(\))', '`#{")"}`' - assert_generates '%x(`)', '`#{"`"}`' - assert_generates '`"`', '`#{"\\""}`' - end + context 'on empty source' do + let(:source) { '' } - context 'symbol' do - assert_generates s(:sym, :foo), ':foo' - assert_generates s(:sym, :"A B"), ':"A B"' - assert_terminated ':foo' - assert_terminated ':"A B"' - assert_terminated ':"A\"B"' - assert_terminated ':""' + it 'returns right value with nil' do + expect(apply.fmap(&:node)).to eql(right(nil)) end + end - context 'regexp' do - assert_terminated '/foo/' - assert_terminated %q(/[^-+',.\/:@[:alnum:]\[\]]+/) - assert_terminated '/foo#{@bar}/' - assert_terminated '/foo#{@bar}/imx' - assert_terminated '/#{"\u0000"}/' - assert_terminated "/\n/" - assert_terminated '/\n/' - assert_terminated "/\n/x" - # Within indentation - assert_source <<-RUBY - if foo - / - / - end - RUBY - assert_generates '%r(/)', '/\//' - assert_generates '%r(\))', '/\)/' - assert_generates '%r(#{@bar}baz)', '/#{@bar}baz/' - assert_terminated '/\/\//x' - end + context 'on syntax error' do + let(:source) { '[' } - context 'dynamic symbol' do - assert_generates ':"foo#{bar}baz"', ':"#{"foo"}#{bar}#{"baz"}"' - assert_source ':"#{"foo"}"' - end + it 'returns left value with syntax error' do + result = apply - context 'irange' do - assert_unterminated '1..2' - assert_unterminated '(0.0 / 0.0)..1' - assert_unterminated '1..(0.0 / 0.0)' - assert_unterminated '(0.0 / 0.0)..100' + # Syntax errors that compare nicely under #eql? are hard to construct + expect(result).to be_instance_of(Unparser::Either::Left) + expect(result.from_left).to be_instance_of(Parser::SyntaxError) end + end + end - context 'erange' do - assert_unterminated '1...2' - end + describe '.unparse_validate' do + def apply + Unparser.unparse_validate(s(:true)) + end - context 'float' do - assert_source '-0.1' - assert_terminated '0.1' - assert_terminated '0.1' - assert_generates '10.2e10000000000', 'Float::INFINITY' - assert_generates '-10.2e10000000000', '-Float::INFINITY' - assert_generates s(:float, -0.1), '-0.1' - assert_generates s(:float, 0.1), '0.1' - end + context 'on successful validation' do + context 'with comments' do + def apply + node, comments = Unparser.parser.parse_with_comments(Unparser.buffer('true # foo')) + Unparser.unparse_validate(node, comments:) + end - context 'array' do - assert_terminated '[1, 2]' - assert_terminated '[1, (), n2]' - assert_terminated '[1]' - assert_terminated '[]' - assert_terminated '[1, *@foo]' - assert_terminated '[*@foo, 1]' - assert_terminated '[*@foo, *@baz]' - assert_generates '%w(foo bar)', '["foo", "bar"]' + it 'returns right value with generated source' do + expect(apply).to eql(right('true # foo')) + end end - context 'hash' do - assert_terminated '{}' - assert_source '{ () => () }' - assert_source '{ 1 => 2 }' - assert_source '{ 1 => 2, 3 => 4 }' - - # special case for 2.1.3 - assert_source "{ foo: (if true\nend) }" - - context 'with symbol keys' do - assert_source '{ a: (1 rescue foo), b: 2 }' - assert_source '{ a: 1, b: 2 }' - assert_source '{ a: :a }' - assert_source '{ :"a b" => 1 }' - assert_source '{ :-@ => 1 }' + context 'without comments' do + it 'returns right value with generated source' do + expect(apply).to eql(right('true')) end end end - context 'access' do - %w(@a @@a $a $1 $` CONST SCOPED::CONST ::TOPLEVEL ::TOPLEVEL::CONST).each do |expression| - assert_terminated(expression) + context 'on unsuccessful validation' do + before do + allow(Unparser::Validation).to receive_messages(from_string: validation) end - end - context 'control keywords' do - %w(retry redo).each do |expression| - assert_terminated(expression) + let(:validation) do + instance_double(Unparser::Validation, success?: false) end - end - context 'singletons' do - %w(self true false nil).each do |expression| - assert_terminated(expression) + it 'returns left value with validation' do + expect(apply).to eql(left(validation)) end end + end - context 'magic keywords' do - assert_generates '__ENCODING__', 'Encoding::UTF_8' - - # These two assertions don't actually need to be wrapped in this block since `true` is the default, - # but it is helpful to contrast with the assertions farther down. - with_builder_options(emit_file_line_as_literals: true) do - assert_generates '__FILE__', '"(string)"' - assert_generates '__LINE__', '1' - end - - with_builder_options(emit_file_line_as_literals: false) do - assert_source '__FILE__' - assert_source '__LINE__' - end + describe '.unparse_ast_either' do + def apply + described_class.unparse_ast_either(ast) end - context 'assignment' do - context 'single' do - assert_unterminated 'a = 1' - assert_unterminated '@a = 1' - assert_unterminated '@@a = 1' - assert_unterminated '$a = 1' - assert_unterminated 'CONST = 1' - assert_unterminated 'Name::Spaced::CONST = 1' - assert_unterminated '::Foo = ::Bar' - end - - context 'lvar assigned from method with same name' do - assert_unterminated 'foo = foo()' - end - - context 'lvar introduction from condition' do - assert_source 'foo = bar while foo' - assert_source 'foo = bar until foo' - assert_source <<-'RUBY' - foo = exp - while foo - foo = bar - end - RUBY - - # Ugly I know. But its correct :D - # - # if foo { |pair| } - # pair = :foo - # foo - # end - assert_source <<-'RUBY' - if foo do |pair| - pair - end - pair = :foo - foo - end - RUBY - - assert_source <<-'RUBY' - while foo - foo = bar - end - RUBY - - assert_source <<-'RUBY' - each do |bar| - while foo - foo = bar - end - end - RUBY - - assert_source <<-'RUBY' - def foo - foo = bar while foo != baz - end - RUBY - - assert_source <<-'RUBY' - each do |baz| - while foo - foo = bar - end - end - RUBY + let(:ast) do + described_class::AST.new( + node: node, + comments: [], + explicit_encoding: nil, + static_local_variables: Set.new + ) + end - assert_source <<-'RUBY' - each do |foo| - while foo - foo = bar - end - end - RUBY - end + context 'on valid node' do + let(:node) { s(:true) } - context 'multiple' do - assert_source 'a, * = [1, 2]' - assert_source 'a, *foo = [1, 2]' - assert_source '*a = []' - assert_source '*foo = [1, 2]' - assert_source 'a, = foo' - assert_unterminated 'a, b = [1, 2]' - assert_unterminated '@a, @b = [1, 2]' - assert_unterminated 'a.foo, a.bar = [1, 2]' - assert_unterminated 'a[0], a[1] = [1, 2]' - assert_unterminated 'a[*foo], a[1] = [1, 2]' - assert_unterminated '@@a, @@b = [1, 2]' - assert_unterminated '$a, $b = [1, 2]' - assert_unterminated 'a, b = foo' - assert_unterminated 'a, (b, c) = [1, [2, 3]]' - assert_unterminated 'a = (b, c = 1)' - assert_unterminated '(a,), b = 1' + it 'returns expected source' do + expect(apply).to eql(right('true')) end end - %w(next return break).each do |keyword| + context 'on invalid node' do + let(:node) { s(:unsupported) } - context keyword do - assert_terminated keyword.to_s - assert_unterminated "#{keyword} 1" - assert_unterminated "#{keyword} 2, 3" - assert_unterminated "#{keyword} *nil" - assert_unterminated "#{keyword} *foo, bar" - - assert_generates <<-RUBY, <<-RUBY - foo do |bar| - bar =~ // || #{keyword} - baz - end - RUBY - foo do |bar| - (bar =~ //) || #{keyword} - baz - end - RUBY - - assert_generates <<-RUBY, <<-RUBY - #{keyword}(a ? b : c) - RUBY - #{keyword} (if a - b - else - c - end) - RUBY + it 'returns expected error' do + expect(apply.lmap { |value| [value.class, value.message] }).to eql( + left( + [ + described_class::UnknownNodeError, + 'Unknown node type: :unsupported' + ] + ) + ) end end + end - context 'conditional send (csend)' do - with_ruby_versions(beginning_at: '2.3') do - assert_terminated 'a&.b' - assert_terminated 'a&.b(c)' + describe '.unparse' do + context 'on unknown node type' do + def apply + Unparser.unparse(node) end - end - - context 'send' do - assert_terminated 'foo' - assert_terminated 'self.foo' - assert_terminated 'a.foo' - assert_terminated 'A.foo' - assert_terminated 'foo[]' - assert_terminated 'foo[1]' - assert_terminated 'foo[*baz]' - assert_terminated 'foo(1)' - assert_terminated 'foo(bar)' - assert_terminated 'foo(&block)' - assert_terminated 'foo(&(foo || bar))' - assert_terminated 'foo(*arguments)' - assert_terminated 'foo(*arguments)' - assert_source <<-'RUBY' - foo do - end - RUBY - - assert_source <<-'RUBY' - foo(1) do - nil - end - RUBY - - assert_source <<-'RUBY' - foo do |a, b| - nil - end - RUBY - - assert_source <<-'RUBY' - foo do |a, *b| - nil - end - RUBY - - assert_source <<-'RUBY' - foo do |a, *| - nil - end - RUBY - - assert_source <<-'RUBY' - foo do - bar - end - RUBY - - assert_source <<-'RUBY' - foo.bar(*args) - RUBY - - assert_source <<-'RUBY' - foo.bar do |(a)| - d - end - RUBY - - assert_source <<-'RUBY' - foo.bar do |(a, b), c| - d - end - RUBY - - assert_source <<-'RUBY' - foo.bar do |*a; b| - end - RUBY - - assert_source <<-'RUBY' - foo.bar do |a; b| - end - RUBY - - assert_source <<-'RUBY' - foo.bar do |; a, b| - end - RUBY - - assert_source <<-'RUBY' - foo.bar do |((*))| - d - end - RUBY - - assert_source <<-'RUBY' - foo.bar do |(a, (*))| - d - end - RUBY - - assert_source <<-'RUBY' - foo.bar do |(a, b)| - d - end - RUBY - - assert_source <<-'RUBY' - foo.bar do - end.baz - RUBY - - assert_terminated '(1..2).max' - assert_terminated '1..2.max' - assert_unterminated 'a || return' - assert_unterminated 'foo << (bar * baz)' - - assert_source <<-'RUBY' - foo ||= (a, _ = b) - RUBY - - assert_source <<-'RUBY' - begin - rescue - end.bar - RUBY - - assert_source <<-'RUBY' - case (def foo - end - :bar) - when bar - end.baz - RUBY - - assert_source <<-'RUBY' - case foo - when bar - end.baz - RUBY - - assert_source <<-'RUBY' - class << self - end.bar - RUBY - - assert_source <<-'RUBY' - def self.foo - end.bar - RUBY - - assert_source <<-'RUBY' - def foo - end.bar - RUBY - - assert_source <<-'RUBY' - until foo - end.bar - RUBY - - assert_source <<-'RUBY' - while foo - end.bar - RUBY - - assert_source <<-'RUBY' - loop do - end.bar - RUBY - - assert_source <<-'RUBY' - class Foo - end.bar - RUBY - - assert_source <<-'RUBY' - module Foo - end.bar - RUBY - - assert_source <<-'RUBY' - if foo - end.baz - RUBY - - assert_source <<-'RUBY' - local = 1 - local.bar - RUBY - - assert_terminated 'foo.bar(*args)' - assert_terminated 'foo.bar(*arga, foo, *argb)' - assert_terminated 'foo.bar(*args, foo)' - assert_terminated 'foo.bar(foo, *args)' - assert_terminated 'foo.bar(foo, *args, &block)' - assert_source <<-'RUBY' - foo(bar, *args) - RUBY - - assert_terminated 'foo(*args, &block)' - assert_terminated 'foo.bar(&baz)' - assert_terminated 'foo.bar(:baz, &baz)' - assert_terminated 'foo.bar = :baz' - assert_unterminated 'self.foo = :bar' - - assert_terminated 'foo.bar(baz: boz)' - assert_terminated 'foo.bar(foo, "baz" => boz)' - assert_terminated 'foo.bar({ foo: boz }, boz)' - assert_terminated 'foo.bar(foo, {})' - end - - context 'begin; end' do - assert_generates s(:begin), '' - assert_source <<-'RUBY' - begin - end - RUBY + let(:node) { s(:example_node) } - assert_source <<-'RUBY' - foo - bar - RUBY - - assert_source <<-'RUBY' - begin - foo - bar - end.blah - RUBY + it 'raises UnknownNodeError' do + expect { apply }.to raise_error( + Unparser::UnknownNodeError, + 'Unknown node type: :example_node' + ) + end end - context 'begin / rescue / ensure' do - assert_source <<-'RUBY' - begin - foo - ensure - bar - baz - end - RUBY - - assert_source <<-'RUBY' - begin - foo - rescue - baz - end - RUBY - - assert_source <<-'RUBY' - begin - begin - foo - bar - rescue - end - rescue - baz - bar - end - RUBY - - assert_source <<-'RUBY' - begin - raise(Exception) rescue foo = bar - rescue Exception - end - RUBY - - assert_source <<-'RUBY' - begin - foo - bar - rescue - baz - bar - end - RUBY - - assert_source <<-'RUBY' - begin - foo - rescue Exception - bar - end - RUBY - - assert_source <<-'RUBY' - begin - foo - rescue => bar - bar - end - RUBY - - assert_source <<-'RUBY' - begin - foo - rescue Exception, Other => bar - bar - end - RUBY - - assert_source <<-'RUBY' - class << self - undef :bar rescue nil - end - RUBY - - assert_source <<-'RUBY' - module Foo - undef :bar rescue nil - end - RUBY - - assert_source <<-'RUBY' - class Foo - undef :bar rescue nil - end - RUBY - - assert_source <<-'RUBY' - begin - rescue Exception => e - end - RUBY - - assert_source <<-'RUBY' - begin - ensure - end - RUBY - - assert_source <<-'RUBY' - begin - rescue - ensure - end - RUBY - - assert_source <<-'RUBY' - begin - foo - rescue Exception => bar - bar - end - RUBY - - assert_source <<-'RUBY' - begin - bar - rescue SomeError, *bar - baz - end - RUBY - - assert_source <<-'RUBY' - begin - bar - rescue SomeError, *bar => exception - baz - end - RUBY - - assert_source <<-'RUBY' - begin - bar - rescue *bar - baz - end - RUBY - - assert_source <<-'RUBY' - begin - bar - rescue LoadError - end - RUBY - - assert_source <<-'RUBY' - begin - bar - rescue - else - baz - end - RUBY - - assert_source <<-'RUBY' - begin - bar - rescue *bar => exception - baz - end - RUBY - - assert_source 'foo rescue bar' - assert_source 'foo rescue return bar' - assert_source 'x = (foo rescue return bar)' - - %w(while until if).each do |keyword| - assert_source <<-RUBY - #{keyword} ( - foo rescue false - ) - end - RUBY - - assert_generates <<-RUBY, <<-GENERATED - foo rescue false #{keyword} true - RUBY - #{keyword} true - foo rescue false - end - GENERATED + context 'with comments' do + def apply + node, comments = Unparser.parser.parse_with_comments(Unparser.buffer('true # foo')) + Unparser.unparse(node, comments:) end - assert_generates <<-'RUBY', <<-GENERATED - case (foo rescue false) - when true - end - RUBY - case ( - foo rescue false - ) - when true - end - GENERATED + it 'returns right value with generated source' do + expect(apply).to eql('true # foo') + end end - context 'super' do - assert_source 'super' - - assert_source 'super()' - assert_source 'super(a)' - assert_source 'super(a, b)' - assert_source 'super(&block)' - assert_source 'super(a, &block)' - - assert_source <<-'RUBY' - super(a do - foo - end) - RUBY - - assert_source <<-'RUBY' - super do - foo - end - RUBY - - assert_source <<-'RUBY' - super(a) do - foo - end - RUBY - - assert_source <<-'RUBY' - super() do - foo - end - RUBY - - assert_source <<-'RUBY' - super(a, b) do - foo - end - RUBY - + def parser + Unparser.parser end - context 'undef' do - assert_source 'undef :foo' - assert_source 'undef :foo, :bar' + def buffer(input) + Unparser.buffer(input) end - context 'BEGIN' do - assert_source <<-'RUBY' - BEGIN { - foo - } - RUBY + def parse_with_comments(string) + parser.parse_with_comments(buffer(string)) end - context 'END' do - assert_source <<-'RUBY' - END { - foo - } - RUBY + def assert_generates_from_string(parser, string, expected) + node, comments = parse_with_comments(string) + assert_generates_from_ast(parser, node, comments, expected.chomp) end - context 'alias' do - assert_source <<-'RUBY' - alias $foo $bar - RUBY - - assert_source <<-'RUBY' - alias :foo :bar - RUBY + def assert_generates_from_ast(parser, node, comments, expected) + generated = Unparser.unparse(node, comments: comments).chomp + expect(generated).to eql(expected) + ast, comments = parse_with_comments(generated) + expect(ast).to eql(ast) + expect(Unparser.unparse(ast, comments:).chomp).to eql(expected) end - context 'yield' do - context 'without arguments' do - assert_source 'yield' - end - - context 'with argument' do - assert_source 'yield(a)' + def self.assert_generates(input, expected) + it "should generate #{input} as #{expected}" do + if input.is_a?(String) + assert_generates_from_string(parser, input, expected) + else + assert_generates_from_ast(parser, [input, []], expected) + end end + end - context 'with arguments' do - assert_source 'yield(a, b)' + def self.assert_source(string) + it 'round trips' do + ast, comments = parse_with_comments(string) + generated = Unparser.unparse(ast, comments:).chomp + expect(generated).to eql(string.chomp) + generated_ast, _comments = parse_with_comments(generated) + expect(ast == generated_ast).to be(true) end end - context 'if statement' do - assert_source <<-'RUBY' - if /foo/ - bar - end - RUBY - - assert_source <<-'RUBY' - if 3 - 9 - end - RUBY - - assert_source <<-'RUBY' - if 4 - 5 - else - 6 - end - RUBY - - assert_source <<-'RUBY' - unless 3 - nil - end - RUBY - - assert_source <<-'RUBY' - unless 3 - 9 - end - RUBY - - assert_source <<-'RUBY' - if foo - end - RUBY - - assert_source <<-'RUBY' - foo = bar if foo - RUBY - - assert_source <<-'RUBY' - foo = bar unless foo - RUBY - - assert_source <<-'RUBY' - def foo(*foo) - unless foo - foo = bar - end - end - RUBY - - assert_source <<-'RUBY' - each do |foo| - unless foo - foo = bar - end - end - RUBY + context 'on empty source' do + assert_source '' end - context 'def' do - context 'on instance' do - - assert_source <<-'RUBY' - def foo - end - RUBY - - assert_source <<-'RUBY' - def foo - bar - end - RUBY + context 'invalid send selector' do + let(:node) { s(:send, nil, :module) } - assert_source <<-'RUBY' - def foo - foo - rescue - bar - ensure - baz - end - RUBY - - assert_source <<-'RUBY' - begin - foo - ensure - bar rescue nil - end - RUBY - - assert_source <<-'RUBY' - def foo - bar - ensure - baz - end - RUBY - - assert_source <<-'RUBY' - def self.foo - bar - rescue - baz - end - RUBY - - assert_source <<-'RUBY' - def foo - bar - rescue - baz - end - RUBY - - assert_source <<-'RUBY' - def foo(bar) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar, baz) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar = ()) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar = (baz - nil)) - end - RUBY - - assert_source <<-'RUBY' - def foo(bar = true) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar, baz = true) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar: 1) - end - RUBY - - assert_source <<-'RUBY' - def foo(*) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(*bar) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar, *baz) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(baz = true, *bor) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(baz = true, *bor, &block) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar, baz = true, *bor) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(&block) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo(bar, &block) - bar - end - RUBY - - assert_source <<-'RUBY' - def foo - bar - baz - end - RUBY - - assert_source <<-'RUBY' - def (foo do |bar| - end).bar - bar - end - RUBY - - assert_source <<-'RUBY' - def (foo(1)).bar - bar - end - RUBY - - assert_source <<-'RUBY' - def (Foo::Bar.baz).bar - baz - end - RUBY - - assert_source <<-'RUBY' - def (Foo::Bar).bar - baz - end - RUBY - - assert_source <<-'RUBY' - def Foo.bar - baz - end - RUBY - - assert_source <<-'RUBY' - def foo.bar - baz - end - RUBY + it 'raises InvalidNode error' do + expect { Unparser.unparse(node) }.to raise_error do |error| + expect(error).to be_a(Unparser::InvalidNodeError) + expect(error.message).to eql('Invalid selector for send node: :module') + expect(error.node).to be(node) + end end + end - context 'on singleton' do - assert_source <<-'RUBY' - def self.foo - end - RUBY - - assert_source <<-'RUBY' - def self.foo - bar - end - RUBY - - assert_source <<-'RUBY' - def self.foo - bar + %w(next return break).each do |keyword| + context keyword do + assert_source "#{keyword} 1" + assert_source "#{keyword} 2, 3" + assert_source "#{keyword} *nil" + assert_source "#{keyword} *foo, bar" + + assert_source <<~RUBY + foo { |bar| + bar =~ // or #{keyword} baz - end + } RUBY - - assert_source <<-'RUBY' - def Foo.bar - bar - end - RUBY - end + end - context 'class' do - assert_source <<-'RUBY' - class TestClass - end - RUBY - - assert_source <<-'RUBY' - class << some_object - end - RUBY - - assert_source <<-'RUBY' - class << some_object - the_body - end - RUBY - - assert_source <<-'RUBY' - class SomeNameSpace::TestClass - end - RUBY - - assert_source <<-'RUBY' - class Some::Name::Space::TestClass - end - RUBY - - assert_source <<-'RUBY' - class TestClass < Object - end - RUBY - - assert_source <<-'RUBY' - class TestClass < SomeNameSpace::Object - end - RUBY - - assert_source <<-'RUBY' - class TestClass - def foo - :bar - end - end - RUBY - - assert_source <<-'RUBY' - class ::TestClass - end - RUBY - end - - context 'module' do - - assert_source <<-'RUBY' - module TestModule - end - RUBY - - assert_source <<-'RUBY' - module SomeNameSpace::TestModule - end - RUBY - - assert_source <<-'RUBY' - module Some::Name::Space::TestModule - end - RUBY - - assert_source <<-'RUBY' - module TestModule - def foo - :bar - end - end - RUBY - - end - - context 'op assign' do - %w(|= ||= &= &&= += -= *= /= **= %=).each do |op| - assert_source "self.foo #{op} bar" - assert_source "foo[key] #{op} bar" - assert_source "a #{op} (true\nfalse)" - end - end - - context 'element assignment' do - assert_source 'foo[index] = value' - assert_source 'foo[*index] = value' - assert_source 'foo[a, b] = value' - assert_source 'foo[1..2] = value' - assert_source 'foo.[]=()' - assert_source 'foo[] = 1' - - %w(+ - * / % & | || &&).each do |operator| - context "with #{operator}" do - assert_source "foo[index] #{operator}= 2" - assert_source "foo[] #{operator}= 2" - end - end - end - - context 'defined?' do - assert_source <<-'RUBY' - defined?(@foo) - RUBY - - assert_source <<-'RUBY' - defined?(Foo) - RUBY - - assert_source <<-'RUBY' - defined?((a, b = [1, 2])) - RUBY + context 'op assign' do + %w(|= ||= &= &&= += -= *= /= **= %=).each do |op| + assert_source "self.foo #{op} bar" + assert_source "foo[key] #{op} bar" + assert_source "a #{op} (true; false)" end end - context 'lambda' do - assert_source <<-'RUBY' - lambda do + context 'element assignment' do + %w(+ - * / % & | || &&).each do |operator| + context "with #{operator}" do + assert_source "foo[index] #{operator}= 2" + assert_source "foo[] #{operator}= 2" end - RUBY - - assert_source <<-'RUBY' - lambda do |a, b| - a - end - RUBY - end - - context 'match operators' do - assert_source '/bar/ =~ foo' - assert_source '/bar/ =~ :foo' - assert_source '(/bar/ =~ :foo).foo' - assert_source 'foo =~ /bar/' - assert_source 'foo(foo =~ /bar/)' - assert_source '(foo =~ /bar/).foo' + end end context 'binary operator methods' do @@ -1416,242 +324,25 @@ def foo assert_source 'left / right' end - context 'nested binary operators' do - assert_source '(a + b) / (c - d)' - assert_source '(a + b) / c.-(e, f)' - assert_source '(a + b) / c.-(*f)' - end - - context 'binary operator' do - assert_source 'a || (return foo)' - assert_source '(return foo) || a' - assert_source 'a || (break foo)' - assert_source '(break foo) || a' - assert_source '(a || b).foo' - assert_source 'a || (b || c)' - end - - { or: :'||', and: :'&&' }.each do |word, symbol| - assert_generates "a #{word} return foo", "a #{symbol} (return foo)" - assert_generates "a #{word} break foo", "a #{symbol} (break foo)" - assert_generates "a #{word} next foo", "a #{symbol} (next foo)" - end - - context 'expansion of shortcuts' do - assert_source 'a += 2' - assert_source 'a -= 2' - assert_source 'a **= 2' - assert_source 'a *= 2' - assert_source 'a /= 2' - end - - context 'shortcuts' do - assert_source 'a &&= b' - assert_source 'a ||= 2' - assert_source '(a ||= 2).bar' - assert_source '(h ||= {})[k] = v' - end - - context 'flip flops' do - context 'inclusive' do - assert_source <<-'RUBY' - if ((i == 4)..(i == 4)) - foo - end - RUBY - end - - context 'exclusive' do - assert_source <<-'RUBY' - if ((i == 4)...(i == 4)) - foo - end - RUBY - end - end - - context 'case statement' do - assert_source <<-'RUBY' - case - when bar - baz - when baz - bar - end - RUBY - - assert_source <<-'RUBY' - case foo - when bar - when baz - bar - end - RUBY - - assert_source <<-'RUBY' - case foo - when bar - baz - when baz - bar - end - RUBY - - assert_source <<-'RUBY' - case foo - when bar, baz - :other - end - RUBY - - assert_source <<-'RUBY' - case foo - when *bar - :value - end - RUBY - - assert_source <<-'RUBY' - case foo - when bar - baz - else - :foo - end - RUBY - end - - context 'for' do - assert_source <<-'RUBY' - bar(for a in bar do - baz - end) - RUBY - assert_source <<-'RUBY' - for a in bar do - baz - end - RUBY - - assert_source <<-'RUBY' - for a, *b in bar do - baz - end - RUBY - - assert_source <<-'RUBY' - for a, b in bar do - baz - end - RUBY - end - - context 'unary operators' do - assert_source '!1' - assert_source '!(!1)' - assert_source '!(!(foo || bar))' - assert_source '!(!1).baz' - assert_source '~a' - assert_source '-a' - assert_source '+a' - assert_source '-(-a).foo' - end - - context 'loop' do - assert_source <<-'RUBY' - loop do - foo - end - RUBY - end - - context 'post conditions' do - assert_source <<-'RUBY' - x = (begin - foo - end while baz) - RUBY - - assert_source <<-'RUBY' - begin - foo - end while baz - RUBY - - assert_source <<-'RUBY' - begin - foo - bar - end until baz - RUBY - - assert_source <<-'RUBY' - begin - foo - bar - end while baz - RUBY - end - - context 'while' do - assert_source <<-'RUBY' - while false - end - RUBY - - assert_source <<-'RUBY' - while false - 3 - end - RUBY - - assert_source <<-'RUBY' - while (foo do - end) - :body - end - RUBY - end - - context 'until' do - assert_source <<-'RUBY' - until false - end - RUBY - - assert_source <<-'RUBY' - until false - 3 - end - RUBY - - assert_source <<-'RUBY' - until (foo do - end) - :body - end - RUBY - end - - assert_source <<-'RUBY' + assert_source <<~'RUBY' # comment before a_line_of_code RUBY - assert_source <<-'RUBY' + assert_source <<~'RUBY' a_line_of_code # comment after RUBY - assert_source <<-'RUBY' - nested do # first + assert_source <<~'RUBY' + nested { + # first # second something # comment - # another - end + } # another # last RUBY - assert_generates <<-'RUBY', <<-'RUBY' + assert_generates <<~'RUBY', <<~'RUBY' foo if bar # comment RUBY @@ -1661,35 +352,35 @@ def foo # comment RUBY - assert_source <<-'RUBY' + assert_source <<~'RUBY' def noop # do nothing end RUBY - assert_source <<-'RUBY' + assert_source <<~'RUBY' =begin block comment =end - nested do + nested { =begin another block comment =end something + } =begin last block comment =end - end RUBY - assert_generates(<<-'RUBY', <<-'RUBY') + assert_generates(<<~'RUBY', <<~'RUBY') 1 + # first 2 # second RUBY 1 + 2 # first # second RUBY - assert_generates(<<-'RUBY', <<-'RUBY') + assert_generates(<<~'RUBY', <<~'RUBY') 1 + # first 2 # second @@ -1697,7 +388,7 @@ def noop 1 + 2 # first # second RUBY - assert_generates(<<-'RUBY', <<-'RUBY') + assert_generates(<<~'RUBY', <<~'RUBY') 1 + =begin block comment @@ -1710,5 +401,97 @@ def noop =end RUBY + assert_generates(<<~'RUBY', <<~'RUBY') + true ? "true" : () + RUBY + if true + "true" + else + () + end + RUBY + + assert_generates(<<~'RUBY', <<~'RUBY') + true ? () : "false" + RUBY + if true + () + else + "false" + end + RUBY + + assert_source(<<~'RUBY') + if true + "true" + else + () + end + RUBY + + assert_source(<<~'RUBY') + if true + () + else + "false" + end + RUBY + + assert_source(<<~'RUBY') + def foo(bar) + bar() + end + RUBY + + assert_source(<<~'RUBY') + foo { |bar| + bar() + } + RUBY + + # Test Symbol#inspect Ruby bug: https://bugs.ruby-lang.org/issues/18905 + assert_source(':"@="') + assert_source(':"$$$$="') + assert_source(':"8 >="') + end + + describe 'corpus' do + let(:version_excludes) do + excludes = [] + + if RUBY_VERSION >= '3.4.' + excludes.concat( + %w[ + test/corpus/literal/before/34.rb + ] + ) + end + + if RUBY_VERSION < '3.2.' + excludes.concat( + %w[ + test/corpus/literal/since/32.rb + ] + ) + end + + if RUBY_VERSION < '3.1.' + excludes.concat( + %w[ + test/corpus/literal/since/31.rb + ] + ) + end + + excludes.flat_map { |file| ['--ignore', file] } + end + + it 'passes the literal corpus' do + expect(Unparser::CLI.run(%w[test/corpus/literal --literal] + version_excludes)).to be(0) + end + + it 'passes the semantic corpus' do + expect(Unparser::CLI.run(%w[test/corpus/semantic] + version_excludes)).to be(0) + end end end diff --git a/tea.yaml b/tea.yaml new file mode 100644 index 00000000..61a0151e --- /dev/null +++ b/tea.yaml @@ -0,0 +1,5 @@ +--- +version: 1.0.0 +codeOwners: + - '0x8Ef65b8e231dF5bd0138a7390a406a582f7191C7' +quorum: 1 diff --git a/test/corpus/literal/alias.rb b/test/corpus/literal/alias.rb new file mode 100644 index 00000000..fb06a295 --- /dev/null +++ b/test/corpus/literal/alias.rb @@ -0,0 +1,2 @@ +alias $foo $bar +alias :foo :bar diff --git a/test/corpus/literal/assignment.rb b/test/corpus/literal/assignment.rb new file mode 100644 index 00000000..57c430a3 --- /dev/null +++ b/test/corpus/literal/assignment.rb @@ -0,0 +1,43 @@ +$a = 1 +($a, $b) = [1, 2] +((a,), b) = 1 +(*a) = [] +(*foo) = [1, 2] +(@@a, @@b) = [1, 2] +(@a, @b) = [1, 2] +(a, (b, c)) = [1, [2, 3]] +(a, *) = [1, 2] +(a, *foo) = [1, 2] +(a, b) = [1, 2] +(a, b) = foo +(a,) = foo +(a.foo, a.bar) = [1, 2] +(a[*foo], a[1]) = [1, 2] +(a[0], a[1]) = [1, 2] +(*c.foo) = 1 +::Foo = ::Bar +@@a = 1 +@a = 1 +CONST = 1 +Name::Spaced::CONST = 1 +a = ((b, c) = 1) +a = 1 +foo = foo() +foo.[]=() +foo.[]=(1, 2) +foo.[]=true +foo[*index] = value +foo[1..2] = value +foo[] = 1 +foo[a, b] = value +foo[index] = value +x = %() +x.x=%() +x[%()] = bar +a[%()] ||= bar +@a ||= %() +x = " #{}\n" +x.x=" #{}\n" +x[] = " #{}\n" +a[" #{}\n"] ||= bar +@a ||= " #{}\n" diff --git a/test/corpus/literal/before/34.rb b/test/corpus/literal/before/34.rb new file mode 100644 index 00000000..60e2e7ad --- /dev/null +++ b/test/corpus/literal/before/34.rb @@ -0,0 +1,27 @@ +retry +case foo +in {"#{"a"}": 1} then + true +end +/\c*a/ +/\c*a\c*/ +/\c*\c*\c*/ +(break foo) || a +(return foo) || a +a = b || break +a = b || next +a || (break foo) +b or break +b or next +break or b +next or b +return or a + +begin +rescue => A[] +end +for foo[] in m do +end +for (a, b) in bar do + baz +end diff --git a/test/corpus/literal/binary.rb b/test/corpus/literal/binary.rb new file mode 100644 index 00000000..d529d181 --- /dev/null +++ b/test/corpus/literal/binary.rb @@ -0,0 +1,10 @@ +(a || b).foo +a && b && c +a = b || return +a and return foo +a or return +a or return foo +a || (b || c) +a || (return foo) +a = b or c +a = b and c diff --git a/test/corpus/literal/block.rb b/test/corpus/literal/block.rb new file mode 100644 index 00000000..a953def4 --- /dev/null +++ b/test/corpus/literal/block.rb @@ -0,0 +1,100 @@ +foo { +} +foo { |a| +} +foo { |a,| +} +foo { |a,; x| +} +foo { |a, b| +} +foo(1) { + nil +} +foo { |a, *b| + nil +} +foo { |a, *| + nil +} +foo { + bar +} +foo.bar { |(a, b), c| + d +} +foo.bar { |*a; b| +} +foo.bar { |a; b| +} +foo.bar { |; a, b| +} +foo.bar { |*| + d +} +foo.bar { |(*)| + d +} +foo.bar { |((*))| + d +} +foo.bar { |(a, (*))| + d +} +foo.bar { |(a, b)| + d +} +foo.bar { +}.baz +m do +rescue Exception => e +end +m do + foo +rescue Exception => bar + bar +end +m do + bar +rescue SomeError, *bar + baz +end +m do + bar +rescue SomeError, *bar => exception + baz +end +m do + bar +rescue *bar + baz +end +m do + bar +rescue LoadError +end +m do + bar +rescue +else + baz +end +m do + bar +rescue *bar => exception + baz +end +m do +ensure +end +m do +rescue +ensure +end + +begin +rescue => self&.a +end +bar { + _1 + _2 +} diff --git a/test/corpus/literal/case.rb b/test/corpus/literal/case.rb new file mode 100644 index 00000000..c455fd7c --- /dev/null +++ b/test/corpus/literal/case.rb @@ -0,0 +1,37 @@ +case +when bar + baz +when baz + bar +end +case foo +when bar +when baz + bar +end +case foo +when bar + baz +when baz + bar +end +case foo +when bar, baz + :other +end +case foo +when *bar + :value +end +case foo +when bar + baz +else + :foo +end +case foo +when *bar | baz +end +case foo +when *bar.baz=1 +end diff --git a/test/corpus/literal/class.rb b/test/corpus/literal/class.rb new file mode 100644 index 00000000..f0198625 --- /dev/null +++ b/test/corpus/literal/class.rb @@ -0,0 +1,35 @@ +class A +end + +class << a +end + +class << a + b +end + +class A::B +end + +class A::B::C +end + +class A < B +end + +class A < B::C +end + +class A::B < C::D +end + +class A + include(B.new) + + def foo + :bar + end +end + +class ::A +end diff --git a/test/corpus/literal/control.rb b/test/corpus/literal/control.rb new file mode 100644 index 00000000..ce413fc8 --- /dev/null +++ b/test/corpus/literal/control.rb @@ -0,0 +1,14 @@ +next +return +break +redo +return 1 +return 1, 2 +return true ? 1 : 2 +break true ? 1 : 2 +next true ? 1 : 2 +return true, if true + 1 +else + 2 +end diff --git a/test/corpus/literal/def.rb b/test/corpus/literal/def.rb new file mode 100644 index 00000000..76ac5403 --- /dev/null +++ b/test/corpus/literal/def.rb @@ -0,0 +1,142 @@ +def foo + a +rescue + b +else + c +ensure + d +end + +def foo + a rescue b +rescue + b +else + c +ensure + d +end + +def foo(bar:, baz:) +end + +def foo +end + +def foo + bar +end + +def foo + foo +rescue + bar +ensure + baz +end + +def foo + bar +ensure + baz +end + +def foo + bar +rescue + baz +end + +def foo(bar) + bar +end + +def foo(bar, baz) + bar +end + +def foo(bar = ()) + bar +end + +def foo(bar = (baz; nil)) +end + +def foo(bar = true) + bar +end + +def foo(bar, baz = true) + bar +end + +def foo(bar: 1) +end + +def foo(bar: baz) +end + +def foo(bar: bar()) +end + +def foo(*) + bar +end + +def foo(*bar) + bar +end + +def foo(bar, *baz) + bar +end + +def foo(baz = true, *bor) + bar +end + +def foo(baz = true, *bor, &block) + bar +end + +def foo(bar, baz = true, *bor) + bar +end + +def foo(&block) + bar +end + +def foo(bar, &block) + bar +end + +def foo + bar + baz +end + +def f(((a))) +end + +def f((a, b)) +end + +def f(((a, b))) +end + +def foo(bar:, baz: "value") +end + +def f + " #{}\n" +end + +def f + %() +end + +def foo(return:) + { return: } +end diff --git a/test/corpus/literal/defined.rb b/test/corpus/literal/defined.rb new file mode 100644 index 00000000..65e7c370 --- /dev/null +++ b/test/corpus/literal/defined.rb @@ -0,0 +1,3 @@ +defined?(@foo) +defined?(Foo) +defined?(((a, b) = [1, 2])) diff --git a/test/corpus/literal/defs.rb b/test/corpus/literal/defs.rb new file mode 100644 index 00000000..b70aa9ef --- /dev/null +++ b/test/corpus/literal/defs.rb @@ -0,0 +1,40 @@ +def self.foo +end + +def self.foo + bar +end + +def self.foo + bar + baz +end + +def Foo.bar + bar +end + +def (foo { |bar| +}).bar + bar +end + +def (foo(1)).bar + bar +end + +def (Foo::Bar.baz).bar + baz +end + +def (Foo::Bar).bar + baz +end + +def Foo.bar + baz +end + +def foo.bar + baz +end diff --git a/test/corpus/literal/dstr.rb b/test/corpus/literal/dstr.rb new file mode 100644 index 00000000..a610c799 --- /dev/null +++ b/test/corpus/literal/dstr.rb @@ -0,0 +1,49 @@ +"foo\n" "#{baz}\n" "bar\n" +"#{baz}" "foo\n" "bar\n" +"foo +bar\n" +%() +"a +b +c\n" +"a{foo}n" +"a\n#{foo} +b\n" +if true + "#{}a" +end +if true + "a\n#{}a ++b\n" +end +"\#{}\#{}\n#{}\n#{}\n#{}\n" +"#{} +a\n" rescue nil +"a#$1" +"a#$a" +"a#@a" +"a#@@a" +if true + return " #{42}\n" +end +foo(" #{bar}\n") +foo(" #{bar}\n") { |x| +} +"\"\" +\n#{}\n" +"<#{}#{}>#{}" +<<-HEREDOC + "#{a}, + a + #{b} + b + #{c}" +HEREDOC +<<-HEREDOC + a + b + \#{}:\#{} + #{} + end + f +HEREDOC diff --git a/test/corpus/literal/empty.rb b/test/corpus/literal/empty.rb new file mode 100644 index 00000000..e69de29b diff --git a/test/corpus/literal/empty_begin.rb b/test/corpus/literal/empty_begin.rb new file mode 100644 index 00000000..6a452c18 --- /dev/null +++ b/test/corpus/literal/empty_begin.rb @@ -0,0 +1 @@ +() diff --git a/test/corpus/literal/flipflop.rb b/test/corpus/literal/flipflop.rb new file mode 100644 index 00000000..565755b0 --- /dev/null +++ b/test/corpus/literal/flipflop.rb @@ -0,0 +1,14 @@ +if ((i == 4)..(i == 4)) + foo +end +if ((i == 4)...(i == 4)) + foo +end +if ..foo +end +if foo..; +end +not foo..bar +not foo...bar +!(foo..bar) +!(foo...bar) diff --git a/test/corpus/literal/for.rb b/test/corpus/literal/for.rb new file mode 100644 index 00000000..26a46def --- /dev/null +++ b/test/corpus/literal/for.rb @@ -0,0 +1,9 @@ +bar(for a in bar do + baz +end) +for a in bar do + baz +end +for (a, *b) in bar do + baz +end diff --git a/test/corpus/literal/heredoc.rb b/test/corpus/literal/heredoc.rb new file mode 100644 index 00000000..80024945 --- /dev/null +++ b/test/corpus/literal/heredoc.rb @@ -0,0 +1,109 @@ +<<-HEREDOC.foo(//, nil) +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +line_9 +HEREDOC +<<-HEREDOC && bar +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +{ bar => 1, foo => <<-HEREDOC } +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +foo(bar: <<-HEREDOC) +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +[<<-HEREDOC] +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +foo(<<-HEREDOC) { + a + + b + f + e + + d + f +HEREDOC +} +foo = <<-HEREDOC +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +foo(<<-HEREDOC) +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +<<-HEREDOC +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +"segment_1" "segment_2" "segment_3" "segment_4" +foo[<<-HEREDOC] +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +<<-HEREDOC +a b + a + "$! }:\#{}" + #{} +HEREDOC diff --git a/test/corpus/literal/hookexe.rb b/test/corpus/literal/hookexe.rb new file mode 100644 index 00000000..08f14f47 --- /dev/null +++ b/test/corpus/literal/hookexe.rb @@ -0,0 +1,7 @@ +BEGIN { + foo +} +bar +END { + baz +} diff --git a/test/corpus/literal/if.rb b/test/corpus/literal/if.rb new file mode 100644 index 00000000..0c13801f --- /dev/null +++ b/test/corpus/literal/if.rb @@ -0,0 +1,36 @@ +if /foo/ + bar +end +if 3 + 9 +end +if 4 + 5 +else + 6 +end +unless 3 + nil +end +unless 3 + 9 +end +if foo +end + +module A + foo = bar if foo +end + +module B + foo = bar unless foo +end +unless foo + foo = bar +end +if foo { |pair| + pair +} + pair = :foo + foo +end diff --git a/test/corpus/literal/kwbegin.rb b/test/corpus/literal/kwbegin.rb new file mode 100644 index 00000000..6cc1e74c --- /dev/null +++ b/test/corpus/literal/kwbegin.rb @@ -0,0 +1,80 @@ +begin +rescue +end + +begin +ensure +end + +begin + a +end + +begin + a +rescue + b +end + +begin + a + b +rescue + b +end + +begin +rescue A +end + +begin +rescue A => foo +end + +begin + a +rescue A + b +rescue B + c +ensure + d +end + +begin + begin + foo + bar + rescue + end +rescue + baz + bar +end + +begin + raise(Exception) rescue foo = bar +rescue Exception +end + +begin + foo +rescue => bar + bar +end + +begin + foo +rescue Exception, Other => bar + bar +end + +begin + bar +rescue SomeError, *bar => exception + baz +end + +class << self + undef :bar rescue nil +end diff --git a/test/corpus/literal/lambda.rb b/test/corpus/literal/lambda.rb new file mode 100644 index 00000000..aba17817 --- /dev/null +++ b/test/corpus/literal/lambda.rb @@ -0,0 +1,16 @@ +lambda { +} +lambda { |a, b| + a +} +->() { +} +->(a) { +} +->(a, b) { +} +->(a, b; c) { +} +-> { + _1 + _2 +} diff --git a/test/corpus/literal/literal.rb b/test/corpus/literal/literal.rb new file mode 100644 index 00000000..a063c5c4 --- /dev/null +++ b/test/corpus/literal/literal.rb @@ -0,0 +1,77 @@ +{ "foo" => " #{}\n", "bar" => :baz } +{ "foo" => %(), "bar" => :baz } +["foo", %()] +a(" #{}\n").a +a(%()).a +{ "foo" => " #{}\n", **baz } +{ "foo" => %(), **baz } +"#@a #@@a #$a" +0 +++1 +1 +1 +1r +1.5r +1.3r +5i +-5i +0.6i +-0.6i +1000000000000000000000000000000i +1ri +"foo" "bar" +"foobar #{baz}" +"foo#{1}bar" +"\\\\#{}" +"#{}\#{}" +"\#{}#{}" +"foo\\\#{@bar}" +"\"" +"foo bar" +"foo\nbar" +`foo` +`foo#{@bar}` +`)` +`\`` +`"` +:foo +:"A B" +:foo +:"A B" +:"A\"B" +:"" +:"foo#{bar}baz" +:"#{"foo"}" +(0.0 / 0.0)..1 +1..(0.0 / 0.0) +(0.0 / 0.0)..100 +-0.1 +0.1 +[1, 2] +[1, (), n2] +[1] +[] +[1, *@foo] +[*@foo, 1] +[*@foo, *@baz] +{} +{ () => () } +{ 1 => 2 } +{ 1 => 2, 3 => 4 } +{ a: (1 rescue foo), b: 2 } +{ a: 1, b: 2 } +{ a: :a } +{ :"a b" => 1 } +{ :-@ => 1 } +"#{}\n#{}\na" +foo { + "#{}\n#{}\na" +} +:"a\\ +b" +` x +#{foo} +#` +`#{}` +`#G` +`#$G` diff --git a/test/corpus/literal/module.rb b/test/corpus/literal/module.rb new file mode 100644 index 00000000..cec03f3b --- /dev/null +++ b/test/corpus/literal/module.rb @@ -0,0 +1,16 @@ +module A +end + +module A::B +end + +module A::B::C +end + +module A + include(B.new) + + def foo + :bar + end +end diff --git a/test/corpus/literal/opasgn.rb b/test/corpus/literal/opasgn.rb new file mode 100644 index 00000000..5858d773 --- /dev/null +++ b/test/corpus/literal/opasgn.rb @@ -0,0 +1,24 @@ +a += 2 +a -= 2 +a **= 2 +a *= 2 +a /= 2 +a &&= b +a ||= 2 +(a ||= 2).bar +(h ||= {})[k] = v +a.b += 2 +a.b -= 2 +a.b **= 2 +a.b *= 2 +a.b /= 2 +a.b &&= b +a.b ||= 2 +a[b] += 2 +a[b] -= 2 +a[b] **= 2 +a[b] *= 2 +a[b] /= 2 +a[b] &&= b +a[b] ||= 2 +foo.A += 1 diff --git a/test/corpus/literal/pattern.rb b/test/corpus/literal/pattern.rb new file mode 100644 index 00000000..d4aac3d3 --- /dev/null +++ b/test/corpus/literal/pattern.rb @@ -0,0 +1,80 @@ +case foo +in A[1, 2, *a, 3] then + true +in [1, 2, ] then + y +in A(x:) then + true +in {**a} then + true +in {} if true then + true +in [x, y, *] then + true +in {a: 1, aa: 2} then + true +in {} then + true +in {**nil} then + true +in 1 | 2 then + true +in 1 => a then + true +in ^x then + true +in 1 +in 2 then + true +else + true +end +case foo +in A[1, 2, *a, 3] +end +case foo +in A +else +end +1 in [a] +1 => [a] +1 => [*] +1 in [*, 42, *] +1 in [*, a, *foo] +a => %i[a b] +a => %w[a b] +a => ["a", "b"] +a => [:a, :b] +a => [:a, "b"] +a => [true, false, nil] +a => {a: 1, b: 2} +a in %i[a b] +a in %w[a b] +a in ["a", "b"] +a in [:a, :b] +a in [:a, "b"] +a in [true, false, nil] +a in {a: 1, b: 2} +case foo +in %i[a b] +end +case foo +in %w[a b] +end +case foo +in [:a, "b"] +end +case foo +in [1, 2] +end +case foo +in [true, false, nil] +end +a => %I[a b #{foo(1)}] +a => %W[a b #{foo(1)}] +case a +in %I[#{1 + 1}] +end +case foo +in %i[a b c $FILE] +end diff --git a/test/corpus/literal/pragma.rb b/test/corpus/literal/pragma.rb new file mode 100644 index 00000000..4f6dd71b --- /dev/null +++ b/test/corpus/literal/pragma.rb @@ -0,0 +1,4 @@ +__ENCODING__ +__FILE__ +__LINE__ +__dir__ diff --git a/test/corpus/literal/range.rb b/test/corpus/literal/range.rb new file mode 100644 index 00000000..e8f50c8f --- /dev/null +++ b/test/corpus/literal/range.rb @@ -0,0 +1,36 @@ +(1..) +1..2 +(1...) +1...2 +(..1) +(...2) +(..2) +foo((1..1)) +1...2 +foo[...2] +{ foo: ...bar } +(1...) +1..2 +{ foo: ..bar } +(1..) +1.. ..1 +1.. and 2 +1.. == 2 +1.. != 2 +1.. === 2 +1.. <=> 2 +1.. =~ 2 +1.. !~ 2 +1.. < 2 +1.. > 2 +1.. <= 2 +1.. >= 2 +1.. << 2 +1.. >> 2 +1..2 +1..-2 +1.. ..1 +1... ...1 +%i[a b]..%i[c d] +..%i[c d] +%i[a b].. diff --git a/test/corpus/literal/regexp.rb b/test/corpus/literal/regexp.rb new file mode 100644 index 00000000..9771357d --- /dev/null +++ b/test/corpus/literal/regexp.rb @@ -0,0 +1,31 @@ +// +/foo/ +/#{foo}/ +/#{<<-HEREDOC}/ +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC +%r{[^-+',./:@[:alnum:]\[\]]+} +/foo/ +/foo#{@bar}/ +/foo#{@bar}/imx +/#{"\u0000"}/ +/\n/ +/\n/ +/\n/x +%r{//}x +/ +/ +/a +/ +/ +a +/ +/aaa #{$bbb}/ +/aaa #$bbb/ diff --git a/test/corpus/literal/rescue.rb b/test/corpus/literal/rescue.rb new file mode 100644 index 00000000..c87ff883 --- /dev/null +++ b/test/corpus/literal/rescue.rb @@ -0,0 +1,10 @@ +foo rescue bar +foo rescue return bar +x = (foo rescue return bar) +foo = [] rescue nil +foo = 1 rescue nil +foo = [1] rescue nil +foo = 1, 2 rescue nil +(foo = []) rescue nil +(foo = 1) rescue nil +(foo = [1]) rescue nil diff --git a/test/corpus/literal/send.rb b/test/corpus/literal/send.rb new file mode 100644 index 00000000..409e8242 --- /dev/null +++ b/test/corpus/literal/send.rb @@ -0,0 +1,93 @@ +module A + foo ||= ((a, _) = b) +end + +module A + local = 1 + local.bar +end +class A +end.bar +module A +end.bar +begin +rescue +end.bar +case (def foo +end; :bar) +when bar +end.baz +case foo +when bar +end.baz +class << self +end.bar +def self.foo +end.bar +def foo +end.bar +until foo +end.bar +while foo +end.bar +loop { +}.bar +if foo +end.baz +(/bar/ =~ :foo).foo +(1..2).max +(foo =~ /bar/).foo +/bar/ =~ :foo +/bar/ =~ foo +1..2.max +A.foo +FOO() +a&.b +a.foo +foo +foo << (bar * baz) +foo =~ /bar/ +foo(&(foo || bar)) +foo(&block) +foo(*args, &block) +foo(*arguments) +foo(1, 2) +foo(bar) +foo(bar, *args) +foo(foo =~ /bar/) +foo.bar(&baz) +foo.bar(*arga, foo, *argb) +foo.bar(*args) +foo.bar(*args, foo) +foo.bar(:baz, &baz) +foo.bar(baz: boz) +foo.bar(foo, "baz" => boz) +foo.bar(foo, *args) +foo.bar(foo, *args, &block) +foo.bar(foo, {}) +foo.bar({ foo: boz }, boz) +foo.bar=:baz +foo(a: b) +foo.&(a: b) +foo.&(**a) +foo[*baz] +foo[1, 2] +foo[] +self.foo +self.foo=:bar +(a + b) / (c - d) +(a + b) / c.-(e, f) +(a + b) / c.-(*f) +x(**foo) +foo&.! +foo.~(b) +a&.+(b) +a = nil +foo(bar: a) +foo(return:) +foo(bar: Bar.bar) +foo(bar: bar(1)) +foo(bar: 1) +foo(do: true) +foo(a?: a?) +foo(a!: a!) diff --git a/test/corpus/literal/since/31.rb b/test/corpus/literal/since/31.rb new file mode 100644 index 00000000..504eb94d --- /dev/null +++ b/test/corpus/literal/since/31.rb @@ -0,0 +1,7 @@ +def foo(&) + bar(&) +end + +def foo(a, &) + bar(&) +end diff --git a/test/corpus/literal/since/32.rb b/test/corpus/literal/since/32.rb new file mode 100644 index 00000000..b8e096d8 --- /dev/null +++ b/test/corpus/literal/since/32.rb @@ -0,0 +1,11 @@ +def foo(argument, **) + bar(argument, **) +end + +def foo(argument, *) + bar(argument, *) +end + +def foo(**) + { default: 1, ** } +end diff --git a/test/corpus/literal/single-heredoc.rb b/test/corpus/literal/single-heredoc.rb new file mode 100644 index 00000000..64c833a6 --- /dev/null +++ b/test/corpus/literal/single-heredoc.rb @@ -0,0 +1,10 @@ +<<-HEREDOC +line_1 +line_2 +line_3 +line_4 +line_5 +line_6 +line_7 +line_8 +HEREDOC diff --git a/test/corpus/literal/singletons.rb b/test/corpus/literal/singletons.rb new file mode 100644 index 00000000..496e6a41 --- /dev/null +++ b/test/corpus/literal/singletons.rb @@ -0,0 +1,4 @@ +false +nil +self +true diff --git a/test/corpus/literal/super.rb b/test/corpus/literal/super.rb new file mode 100644 index 00000000..0e73e6f0 --- /dev/null +++ b/test/corpus/literal/super.rb @@ -0,0 +1,21 @@ +super +super() +super(a) +super(a, b) +super(&block) +super(a, &block) +super(a { + foo +}) +super { + foo +} +super(a) { + foo +} +super() { + foo +} +super(a, b) { + foo +} diff --git a/test/corpus/literal/unary.rb b/test/corpus/literal/unary.rb new file mode 100644 index 00000000..77685cb7 --- /dev/null +++ b/test/corpus/literal/unary.rb @@ -0,0 +1,8 @@ +!1 +!(!1) +!(!(foo || bar)) +!(!1).baz +~a +-a ++a +-(-a).foo diff --git a/test/corpus/literal/undef.rb b/test/corpus/literal/undef.rb new file mode 100644 index 00000000..a65d8d0c --- /dev/null +++ b/test/corpus/literal/undef.rb @@ -0,0 +1,2 @@ +undef :foo +undef :foo, :bar diff --git a/test/corpus/literal/variables.rb b/test/corpus/literal/variables.rb new file mode 100644 index 00000000..1de938f3 --- /dev/null +++ b/test/corpus/literal/variables.rb @@ -0,0 +1,10 @@ +a +@a +@@a +$a +$1 +$` +CONST +SCOPED::CONST +::TOPLEVEL +::TOPLEVEL::CONST diff --git a/test/corpus/literal/while.rb b/test/corpus/literal/while.rb new file mode 100644 index 00000000..19a60ef5 --- /dev/null +++ b/test/corpus/literal/while.rb @@ -0,0 +1,73 @@ +module A + foo { |bar| + while foo + foo = bar + end + } +end + +def foo + foo = bar while foo != baz +end + +module A + foo = bar while foo +end + +module A + foo = bar until foo +end + +module A + while foo + foo = bar + end +end + +module A + each { |baz| + while foo + foo = bar + end + } +end + +module A + each { |foo| + while foo + foo = bar + end + } +end +x = (begin + foo +end while baz) +begin + foo +end while baz +begin + foo + bar +end until baz +begin + foo + bar +end while baz +while false +end +while false + 3 +end +while (foo { +}) + :body +end +until false +end +until false + 3 +end +until (foo { +}) + :body +end diff --git a/test/corpus/literal/yield.rb b/test/corpus/literal/yield.rb new file mode 100644 index 00000000..c0b58208 --- /dev/null +++ b/test/corpus/literal/yield.rb @@ -0,0 +1,3 @@ +yield +yield(a) +yield(a, b) diff --git a/test/corpus/semantic/and.rb b/test/corpus/semantic/and.rb new file mode 100644 index 00000000..bdfa8bcd --- /dev/null +++ b/test/corpus/semantic/and.rb @@ -0,0 +1,18 @@ +a...b or c...d +a...b and c...d + +if a...b or c...d +end + +if a...b and c...d +end + +a and b and c +(a and b) and c +a and (b and c) +a and b or c +(a and b) or c +a and (b or c) +a or b and c +(a or b) and c +a or (b and c) diff --git a/test/corpus/semantic/block.rb b/test/corpus/semantic/block.rb new file mode 100644 index 00000000..58916900 --- /dev/null +++ b/test/corpus/semantic/block.rb @@ -0,0 +1,26 @@ +foo do +end + +foo do +rescue +end + +foo do + nil rescue nil + nil +end + +foo do |a| +end + +foo(<<-DOC) do |a| + b +DOC + a +end + +foo(<<-DOC) do + b +DOC + a +end diff --git a/test/corpus/semantic/def.rb b/test/corpus/semantic/def.rb new file mode 100644 index 00000000..75746193 --- /dev/null +++ b/test/corpus/semantic/def.rb @@ -0,0 +1,7 @@ +def foo + (a - b) +end + +def foo + a rescue Exception +end diff --git a/test/corpus/semantic/dstr.rb b/test/corpus/semantic/dstr.rb new file mode 100644 index 00000000..919e7360 --- /dev/null +++ b/test/corpus/semantic/dstr.rb @@ -0,0 +1,127 @@ +< self.foo +end + +begin +rescue => A.foo +end + +begin +rescue => A[i] +end diff --git a/test/corpus/semantic/literal.rb b/test/corpus/semantic/literal.rb new file mode 100644 index 00000000..640914f1 --- /dev/null +++ b/test/corpus/semantic/literal.rb @@ -0,0 +1,15 @@ +1.0r +-0r +0x1 +1_000 +1e10 +10e10000000000 +-10e10000000000 +?c +%r(/) +%r(\)) +%r(#{@bar}baz) +10.2e10000000000 +-10.2e10000000000 +w(foo bar) +{ "#{}": {} } diff --git a/test/corpus/semantic/masgn.rb b/test/corpus/semantic/masgn.rb new file mode 100644 index 00000000..2119117e --- /dev/null +++ b/test/corpus/semantic/masgn.rb @@ -0,0 +1,5 @@ +(a,), = [] +(a, b), = [] +(a, b), c = [] +((a,),), = [] +((a,),), b = [] diff --git a/test/corpus/semantic/not.rb b/test/corpus/semantic/not.rb new file mode 100644 index 00000000..79554eba --- /dev/null +++ b/test/corpus/semantic/not.rb @@ -0,0 +1,16 @@ +!(foo and bar) +!(foo or bar) +not(foo and bar) +not(foo or bar) +! foo and bar +! foo or bar +not foo and bar +not foo or bar +!(foo && bar) +!(foo || bar) +not(foo && bar) +not(foo || bar) +! foo && bar +! foo || bar +not foo && bar +not foo || bar diff --git a/test/corpus/semantic/opasgn.rb b/test/corpus/semantic/opasgn.rb new file mode 100644 index 00000000..8b4bc5d2 --- /dev/null +++ b/test/corpus/semantic/opasgn.rb @@ -0,0 +1 @@ +y["#{42}\n"] += "#{42}\n" diff --git a/test/corpus/semantic/or.rb b/test/corpus/semantic/or.rb new file mode 100644 index 00000000..5d404df1 --- /dev/null +++ b/test/corpus/semantic/or.rb @@ -0,0 +1 @@ +a[:x] = b[:x] || c[:x] || d(:new) diff --git a/test/corpus/semantic/pattern.rb b/test/corpus/semantic/pattern.rb new file mode 100644 index 00000000..35b7950f --- /dev/null +++ b/test/corpus/semantic/pattern.rb @@ -0,0 +1,28 @@ +case foo +in %s[a b] +end +case foo +in %r[foo] +end +case foo +in %r[/foo.+bar/] +end +case foo +in %r:/foo.+bar/: +end +case foo +in %x(rm -rf /) +end +case foo +in %i[] +end +case foo +in %w[] +end +case foo +in %q[a b c #{foo}] +end +case foo +in %q[a b c $FILE] +end +a in b, and c diff --git a/test/corpus/semantic/range.rb b/test/corpus/semantic/range.rb new file mode 100644 index 00000000..b081a375 --- /dev/null +++ b/test/corpus/semantic/range.rb @@ -0,0 +1,7 @@ +1...; +2...; +3...; + +1..; +2..; +3..; diff --git a/test/corpus/semantic/regexp.rb b/test/corpus/semantic/regexp.rb new file mode 100644 index 00000000..8043e8d9 --- /dev/null +++ b/test/corpus/semantic/regexp.rb @@ -0,0 +1,4 @@ +if foo + /\n +/ +end diff --git a/test/corpus/semantic/rescue.rb b/test/corpus/semantic/rescue.rb new file mode 100644 index 00000000..a8d5dffa --- /dev/null +++ b/test/corpus/semantic/rescue.rb @@ -0,0 +1,19 @@ +module M + raise +rescue => e + e +end +class A + class << self + rescue + else + ensure + end +end +module A +rescue +end +begin; rescue => A[1]; end +begin; rescue => A[1, 2]; end +begin; rescue => A[1, 2, 3]; end +module A; x = 1; rescue; end diff --git a/test/corpus/semantic/send.rb b/test/corpus/semantic/send.rb new file mode 100644 index 00000000..a65b27d2 --- /dev/null +++ b/test/corpus/semantic/send.rb @@ -0,0 +1,6 @@ +foo +foo(1) + +a.===(b).c == d + +a == d.c.===(c) diff --git a/test/corpus/semantic/undef.rb b/test/corpus/semantic/undef.rb new file mode 100644 index 00000000..47debc31 --- /dev/null +++ b/test/corpus/semantic/undef.rb @@ -0,0 +1,2 @@ +undef foo +undef foo, bar diff --git a/test/corpus/semantic/while.rb b/test/corpus/semantic/while.rb new file mode 100644 index 00000000..a55dcc52 --- /dev/null +++ b/test/corpus/semantic/while.rb @@ -0,0 +1,25 @@ +a until b? {} + +until b? {} + a +end + +foo = bar while foo + +a until b && a { } + +while a = b + a +end + +a until b(<<-FOO) do +FOO + c +end + +module A + foo = exp + while foo + foo = bar + end +end diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 00000000..e69de29b diff --git a/test/parse_helper.rb b/test/parse_helper.rb new file mode 100644 index 00000000..e69de29b diff --git a/unparser.gemspec b/unparser.gemspec index 8f621b9b..bea31e69 100644 --- a/unparser.gemspec +++ b/unparser.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |gem| gem.name = 'unparser' - gem.version = '0.2.8' + gem.version = '0.8.0' gem.authors = ['Markus Schirp'] gem.email = 'mbj@schirp-dso.com' @@ -10,23 +10,30 @@ Gem::Specification.new do |gem| gem.homepage = 'http://github.com/mbj/unparser' gem.license = 'MIT' - gem.files = `git ls-files`.split("\n") - gem.test_files = `git ls-files -- {spec,features}/*`.split("\n") + gem.metadata = { + 'bug_tracker_uri' => 'https://github.com/mbj/unparser/issues', + 'changelog_uri' => 'https://github.com/mbj/unparser/blob/main/Changelog.md', + 'funding_uri' => 'https://github.com/sponsors/mbj', + 'source_code_uri' => 'https://github.com/mbj/unparser', + 'rubygems_mfa_required' => 'true' + } + + gem.files = Dir.glob('lib/**/*') gem.require_paths = %w[lib] gem.extra_rdoc_files = %w[README.md] gem.executables = %w[unparser] - gem.required_ruby_version = '>= 2.1' + gem.required_ruby_version = '>= 3.1' - gem.add_dependency('abstract_type', '~> 0.0.7') - gem.add_dependency('adamantium', '~> 0.2.0') - gem.add_dependency('equalizer', '~> 0.0.9') - gem.add_dependency('diff-lcs', '~> 1.3') - gem.add_dependency('concord', '~> 0.1.5') - gem.add_dependency('parser', '>= 2.3.1.2', '< 2.6') - gem.add_dependency('procto', '~> 0.0.2') + gem.add_dependency('diff-lcs', '~> 1.6') + gem.add_dependency('parser', '>= 3.3.0') + gem.add_dependency('prism', '>= 1.4') - gem.add_development_dependency('anima', '~> 0.3.0') - gem.add_development_dependency('devtools', '~> 0.1.3') - gem.add_development_dependency('morpher', '~> 0.2.6') + gem.add_development_dependency('mutant', '~> 0.13.2') + gem.add_development_dependency('mutant-rspec', '~> 0.13.0') + gem.add_development_dependency('rspec', '~> 3.13') + gem.add_development_dependency('rspec-core', '~> 3.13') + gem.add_development_dependency('rspec-its', '~> 1.3.0') + gem.add_development_dependency('rubocop', '~> 1.7') + gem.add_development_dependency('rubocop-packaging', '~> 0.5') end