diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 00000000..9b28abf4 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,22 @@ +name: Dependabot auto-merge +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.3.3 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed3c51fd..d35471fa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,14 +1,14 @@ name: Main on: - push -- pull_request_target +- pull_request jobs: ci: strategy: fail-fast: false matrix: ruby: - - '2.7' + - '2.7.0' - '3.0' - '3.1' - head @@ -40,20 +40,3 @@ jobs: run: | bundle exec rake stree:check bundle exec rubocop - - automerge: - name: AutoMerge - needs: - - ci - - check - runs-on: ubuntu-latest - if: github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' - steps: - - uses: actions/github-script@v3 - with: - script: | - github.pulls.merge({ - owner: context.payload.repository.owner.login, - repo: context.payload.repository.name, - pull_number: context.payload.pull_request.number - }) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d430d2..9884a16b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [3.5.0] - 2022-08-26 + +### Added + +- [#148](https://github.com/ruby-syntax-tree/syntax_tree/pull/148) - Support Ruby 2.7.0 (previously we only supported back to 2.7.3). +- [#152](https://github.com/ruby-syntax-tree/syntax_tree/pull/152) - Support the `-e` inline script option for the `stree` CLI. + +### Changed + +- [#141](https://github.com/ruby-syntax-tree/syntax_tree/pull/141) - Use `q.format` for `SyntaxTree.format` so that the main node gets pushed onto the stack for checking parent nodes. +- [#147](https://github.com/ruby-syntax-tree/syntax_tree/pull/147) - Fix rightward assignment token management such that `in` and `=>` stay the same regardless of their context. + ## [3.4.0] - 2022-08-19 ### Added @@ -332,7 +344,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v3.4.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v3.5.0...HEAD +[3.5.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v3.4.0...v3.5.0 [3.4.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v3.3.0...v3.4.0 [3.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v3.2.1...v3.3.0 [3.2.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v3.2.0...v3.2.1 diff --git a/Gemfile.lock b/Gemfile.lock index dd10aacb..1d69d297 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (3.4.0) + syntax_tree (3.5.0) prettier_print GEM @@ -10,7 +10,7 @@ GEM ast (2.4.2) docile (1.4.0) json (2.6.2) - minitest (5.16.2) + minitest (5.16.3) parallel (1.22.1) parser (3.1.2.1) ast (~> 2.4.1) @@ -19,7 +19,7 @@ GEM rake (13.0.6) regexp_parser (2.5.0) rexml (3.2.5) - rubocop (1.35.0) + rubocop (1.35.1) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) diff --git a/Rakefile b/Rakefile index 6ba17fe9..4973d45e 100644 --- a/Rakefile +++ b/Rakefile @@ -12,8 +12,17 @@ end task default: :test -SOURCE_FILES = - FileList[%w[Gemfile Rakefile syntax_tree.gemspec lib/**/*.rb test/*.rb]] +configure = ->(task) do + task.source_files = + FileList[%w[Gemfile Rakefile syntax_tree.gemspec lib/**/*.rb test/*.rb]] -SyntaxTree::Rake::CheckTask.new { |t| t.source_files = SOURCE_FILES } -SyntaxTree::Rake::WriteTask.new { |t| t.source_files = SOURCE_FILES } + # Since Syntax Tree supports back to Ruby 2.7.0, we need to make sure that we + # format our code such that it's compatible with that version. This actually + # has very little effect on the output, the only change at the moment is that + # Ruby < 2.7.3 didn't allow a newline before the closing brace of a hash + # pattern. + task.target_ruby_version = Gem::Version.new("2.7.0") +end + +SyntaxTree::Rake::CheckTask.new(&configure) +SyntaxTree::Rake::WriteTask.new(&configure) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index a9ebdef7..2de20e78 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -53,18 +53,23 @@ def source end end - # An item of work that corresponds to the stdin content. - class STDINItem + # An item of work that corresponds to a script content passed via the + # command line. + class ScriptItem + FILEPATH = :script + + attr_reader :source + + def initialize(source) + @source = source + end + def handler HANDLERS[".rb"] end def filepath - :stdin - end - - def source - $stdin.read + FILEPATH end end @@ -191,7 +196,7 @@ def run(item) source = item.source formatted = item.handler.format(source, options.print_width) - File.write(filepath, formatted) if filepath != :stdin + File.write(filepath, formatted) if item.filepath != :script color = source == formatted ? Color.gray(filepath) : filepath delta = ((Time.now - start) * 1000).round @@ -206,25 +211,25 @@ def run(item) # The help message displayed if the input arguments are not correctly # ordered or formatted. HELP = <<~HELP - #{Color.bold("stree ast [--plugins=...] [--print-width=NUMBER] FILE")} + #{Color.bold("stree ast [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Print out the AST corresponding to the given files - #{Color.bold("stree check [--plugins=...] [--print-width=NUMBER] FILE")} + #{Color.bold("stree check [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Check that the given files are formatted as syntax tree would format them - #{Color.bold("stree debug [--plugins=...] [--print-width=NUMBER] FILE")} + #{Color.bold("stree debug [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Check that the given files can be formatted idempotently - #{Color.bold("stree doc [--plugins=...] FILE")} + #{Color.bold("stree doc [--plugins=...] [-e SCRIPT] FILE")} Print out the doc tree that would be used to format the given files - #{Color.bold("stree format [--plugins=...] [--print-width=NUMBER] FILE")} + #{Color.bold("stree format [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Print out the formatted version of the given files - #{Color.bold("stree json [--plugins=...] FILE")} + #{Color.bold("stree json [--plugins=...] [-e SCRIPT] FILE")} Print out the JSON representation of the given files - #{Color.bold("stree match [--plugins=...] FILE")} + #{Color.bold("stree match [--plugins=...] [-e SCRIPT] FILE")} Print out a pattern-matching Ruby expression that would match the given files #{Color.bold("stree help")} @@ -236,7 +241,7 @@ def run(item) #{Color.bold("stree version")} Output the current version of syntax tree - #{Color.bold("stree write [--plugins=...] [--print-width=NUMBER] FILE")} + #{Color.bold("stree write [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Read, format, and write back the source of the given files --plugins=... @@ -244,20 +249,30 @@ def run(item) --print-width=NUMBER The maximum line width to use when formatting. + + -e SCRIPT + Parse an inline Ruby string. HELP # This represents all of the options that can be passed to the CLI. It is # responsible for parsing the list and then returning the file paths at the # end. class Options - attr_reader :print_width + attr_reader :plugins, :print_width, :scripts, :target_ruby_version def initialize(print_width: DEFAULT_PRINT_WIDTH) + @plugins = [] @print_width = print_width + @scripts = [] + @target_ruby_version = nil end + # TODO: This function causes a couple of side-effects that I really don't + # like to have here. It mutates the global state by requiring the plugins, + # and mutates the global options hash by adding the target ruby version. + # That should be done on a config-by-config basis, not here. def parse(arguments) - parser.parse(arguments) + parser.parse!(arguments) end private @@ -275,7 +290,8 @@ def parser # require "syntax_tree/haml" # opts.on("--plugins=PLUGINS") do |plugins| - plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" } + @plugins = plugins.split(",") + @plugins.each { |plugin| require "syntax_tree/#{plugin}" } end # If there is a print width specified on the command line, then @@ -283,6 +299,17 @@ def parser opts.on("--print-width=NUMBER", Integer) do |print_width| @print_width = print_width end + + # If there is a script specified on the command line, then parse + # it and add it to the list of scripts to run. + opts.on("-e SCRIPT") { |script| @scripts << script } + + # If there is a target ruby version specified on the command line, + # parse that out and use it when formatting. + opts.on("--target-ruby-version=VERSION") do |version| + @target_ruby_version = Gem::Version.new(version) + Formatter::OPTIONS[:target_ruby_version] = @target_ruby_version + end end end end @@ -361,7 +388,7 @@ def run(argv) # If we're not reading from stdin and the user didn't supply and # filepaths to be read, then we exit with the usage message. - if $stdin.tty? && arguments.empty? + if $stdin.tty? && arguments.empty? && options.scripts.empty? warn(HELP) return 1 end @@ -371,7 +398,7 @@ def run(argv) # If we're reading from stdin, then we'll just add the stdin object to # the queue. Otherwise, we'll add each of the filepaths to the queue. - if $stdin.tty? || arguments.any? + if $stdin.tty? && (arguments.any? || options.scripts.any?) arguments.each do |pattern| Dir .glob(pattern) @@ -379,8 +406,9 @@ def run(argv) queue << FileItem.new(filepath) if File.file?(filepath) end end + options.scripts.each { |script| queue << ScriptItem.new(script) } else - queue << STDINItem.new + queue << ScriptItem.new($stdin.read) end # At the end, we're going to return whether or not this worker ever diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index 6efad8d8..4c7a00db 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -14,7 +14,11 @@ class Formatter < PrettierPrint # Note that we're keeping this in a global-ish hash instead of just # overriding methods on classes so that other plugins can reference this if # necessary. For example, the RBS plugin references the quote style. - OPTIONS = { quote: "\"", trailing_comma: false } + OPTIONS = { + quote: "\"", + trailing_comma: false, + target_ruby_version: Gem::Version.new(RUBY_VERSION) + } COMMENT_PRIORITY = 1 HEREDOC_PRIORITY = 2 @@ -23,14 +27,15 @@ class Formatter < PrettierPrint # These options are overridden in plugins to we need to make sure they are # available here. - attr_reader :quote, :trailing_comma + attr_reader :quote, :trailing_comma, :target_ruby_version alias trailing_comma? trailing_comma def initialize( source, *args, quote: OPTIONS[:quote], - trailing_comma: OPTIONS[:trailing_comma] + trailing_comma: OPTIONS[:trailing_comma], + target_ruby_version: OPTIONS[:target_ruby_version] ) super(*args) @@ -40,13 +45,14 @@ def initialize( # Memoizing these values per formatter to make access faster. @quote = quote @trailing_comma = trailing_comma + @target_ruby_version = target_ruby_version end def self.format(source, node) - formatter = new(source, []) - node.format(formatter) - formatter.flush - formatter.output.join + q = new(source, []) + q.format(node) + q.flush + q.output.join end def format(node, stackable: true) diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index d7b6d6cf..47c534d1 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -2132,8 +2132,7 @@ def format(q) in [ Paren[ contents: { - body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array] - } + body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array] } ] ] # Here we have a single argument that is a set of parentheses wrapping @@ -5116,8 +5115,13 @@ def format(q) q.breakable contents.call end - q.breakable - q.text("}") + + if q.target_ruby_version < Gem::Version.new("2.7.3") + q.text(" }") + else + q.breakable + q.text("}") + end end end end @@ -5204,8 +5208,7 @@ def call(q, node) false in { statements: { body: [truthy] }, - consequent: Else[statements: { body: [falsy] }] - } + consequent: Else[statements: { body: [falsy] }] } ternaryable?(truthy) && ternaryable?(falsy) else false diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 8af0b8ed..ed9de499 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -910,7 +910,12 @@ def on_case(value, consequent) location: keyword.location.to(consequent.location) ) else - operator = find_token(Kw, "in", consume: false) || find_token(Op, "=>") + operator = + if (keyword = find_token(Kw, "in", consume: false)) + tokens.delete(keyword) + else + find_token(Op, "=>") + end RAssign.new( value: value, diff --git a/lib/syntax_tree/rake/check_task.rb b/lib/syntax_tree/rake/check_task.rb index afe5013c..48247718 100644 --- a/lib/syntax_tree/rake/check_task.rb +++ b/lib/syntax_tree/rake/check_task.rb @@ -39,16 +39,22 @@ class CheckTask < ::Rake::TaskLib # Defaults to 80. attr_accessor :print_width + # The target Ruby version to use for formatting. + # Defaults to Gem::Version.new(RUBY_VERSION). + attr_accessor :target_ruby_version + def initialize( name = :"stree:check", source_files = ::Rake::FileList["lib/**/*.rb"], plugins = [], - print_width = DEFAULT_PRINT_WIDTH + print_width = DEFAULT_PRINT_WIDTH, + target_ruby_version = Gem::Version.new(RUBY_VERSION) ) @name = name @source_files = source_files @plugins = plugins @print_width = print_width + @target_ruby_version = target_ruby_version yield self if block_given? define_task @@ -64,10 +70,15 @@ def define_task def run_task arguments = ["check"] arguments << "--plugins=#{plugins.join(",")}" if plugins.any? + if print_width != DEFAULT_PRINT_WIDTH arguments << "--print-width=#{print_width}" end + if target_ruby_version != Gem::Version.new(RUBY_VERSION) + arguments << "--target-ruby-version=#{target_ruby_version}" + end + SyntaxTree::CLI.run(arguments + Array(source_files)) end end diff --git a/lib/syntax_tree/rake/write_task.rb b/lib/syntax_tree/rake/write_task.rb index 9a9e8330..69ce97e7 100644 --- a/lib/syntax_tree/rake/write_task.rb +++ b/lib/syntax_tree/rake/write_task.rb @@ -39,16 +39,22 @@ class WriteTask < ::Rake::TaskLib # Defaults to 80. attr_accessor :print_width + # The target Ruby version to use for formatting. + # Defaults to Gem::Version.new(RUBY_VERSION). + attr_accessor :target_ruby_version + def initialize( name = :"stree:write", source_files = ::Rake::FileList["lib/**/*.rb"], plugins = [], - print_width = DEFAULT_PRINT_WIDTH + print_width = DEFAULT_PRINT_WIDTH, + target_ruby_version = Gem::Version.new(RUBY_VERSION) ) @name = name @source_files = source_files @plugins = plugins @print_width = print_width + @target_ruby_version = target_ruby_version yield self if block_given? define_task @@ -64,10 +70,15 @@ def define_task def run_task arguments = ["write"] arguments << "--plugins=#{plugins.join(",")}" if plugins.any? + if print_width != DEFAULT_PRINT_WIDTH arguments << "--print-width=#{print_width}" end + if target_ruby_version != Gem::Version.new(RUBY_VERSION) + arguments << "--target-ruby-version=#{target_ruby_version}" + end + SyntaxTree::CLI.run(arguments + Array(source_files)) end end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index c5675bac..42aa2b6c 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "3.4.0" + VERSION = "3.5.0" end diff --git a/syntax_tree.gemspec b/syntax_tree.gemspec index 820a61a0..2b461dfd 100644 --- a/syntax_tree.gemspec +++ b/syntax_tree.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |spec| .reject { |f| f.match(%r{^(test|spec|features)/}) } end - spec.required_ruby_version = ">= 2.7.3" + spec.required_ruby_version = ">= 2.7.0" spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } diff --git a/test/cli_test.rb b/test/cli_test.rb index b48ea575..aec8f820 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -133,6 +133,16 @@ def test_no_arguments_no_tty $stdin = stdin end + def test_inline_script + stdio, = capture_io { SyntaxTree::CLI.run(%w[format -e 1+1]) } + assert_equal("1 + 1\n", stdio) + end + + def test_multiple_inline_scripts + stdio, = capture_io { SyntaxTree::CLI.run(%w[format -e 1+1 -e 2+2]) } + assert_equal("1 + 1\n2 + 2\n", stdio) + end + def test_generic_error SyntaxTree.stub(:format, ->(*) { raise }) do result = run_cli("format") diff --git a/test/fixtures/args_forward.rb b/test/fixtures/args_forward.rb index 5ba618a8..cc538f44 100644 --- a/test/fixtures/args_forward.rb +++ b/test/fixtures/args_forward.rb @@ -1,4 +1,4 @@ -% +% # >= 2.7.3 def foo(...) bar(:baz, ...) end diff --git a/test/fixtures/hshptn.rb b/test/fixtures/hshptn.rb index 505336b8..02d1cf75 100644 --- a/test/fixtures/hshptn.rb +++ b/test/fixtures/hshptn.rb @@ -30,7 +30,7 @@ case foo in **bar end -% +% # >= 2.7.3 case foo in { foo:, # comment1 diff --git a/test/fixtures/params.rb b/test/fixtures/params.rb index 67b6ec90..551aa9a5 100644 --- a/test/fixtures/params.rb +++ b/test/fixtures/params.rb @@ -16,7 +16,7 @@ def foo(*) % def foo(*rest) end -% +% # >= 2.7.3 def foo(...) end % diff --git a/test/fixtures/rassign.rb b/test/fixtures/rassign.rb index ce749550..3db52b18 100644 --- a/test/fixtures/rassign.rb +++ b/test/fixtures/rassign.rb @@ -20,3 +20,6 @@ ConstantConstantConstant, ConstantConstantConstant ] +% +a in Integer +b => [Integer => c] diff --git a/test/node_test.rb b/test/node_test.rb index 30776f9d..07c2fe26 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -104,16 +104,18 @@ def test_arg_star end end - def test_args_forward - source = <<~SOURCE - def get(...) - request(:GET, ...) - end - SOURCE + guard_version("2.7.3") do + def test_args_forward + source = <<~SOURCE + def get(...) + request(:GET, ...) + end + SOURCE - at = location(lines: 2..2, chars: 29..32) - assert_node(ArgsForward, source, at: at) do |node| - node.bodystmt.statements.body.first.arguments.arguments.parts.last + at = location(lines: 2..2, chars: 29..32) + assert_node(ArgsForward, source, at: at) do |node| + node.bodystmt.statements.body.first.arguments.arguments.parts.last + end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 895fbc82..80e514f0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -26,7 +26,7 @@ def initialize @called = nil end - def method_missing(called, ...) + def method_missing(called, *, **) @called = called end end