From b39b1bf44142cfdb95743b4e2798c9d9cb3f177b Mon Sep 17 00:00:00 2001 From: Wender Freese Date: Tue, 10 May 2022 15:58:37 -0300 Subject: [PATCH 01/12] Add Rake test to run check and format commands --- lib/syntax_tree/rake/task.rb | 50 ++++++++++++++++++++++++++++++++++++ test/task_test.rb | 27 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 lib/syntax_tree/rake/task.rb create mode 100644 test/task_test.rb diff --git a/lib/syntax_tree/rake/task.rb b/lib/syntax_tree/rake/task.rb new file mode 100644 index 00000000..12279e0f --- /dev/null +++ b/lib/syntax_tree/rake/task.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rake" +require "rake/tasklib" + +module SyntaxTree + module Rake + # A Rake task that runs check and format on a set of source files. + # + # Example: + # + # require 'syntax_tree/rake/task' + # + # SyntaxTree::Rake::Task.new do |t| + # t.source_files = '{app,config,lib}/**/*.rb' + # end + # + # This will create task that can be run with: + # + # rake syntax_tree:check_and_format + class Task < ::Rake::TaskLib + # Glob pattern to match source files. + # Defaults to 'lib/**/*.rb'. + attr_accessor :source_files + + def initialize + @source_files = "lib/**/*.rb" + + yield self if block_given? + define_task + end + + private + + def define_task + desc "Runs syntax_tree over source files" + task(:check_and_format) { run_task } + end + + def run_task + %w[check format].each do |command| + SyntaxTree::CLI.run([command, source_files].compact) + end + + # TODO: figure this out + # exit($?.exitstatus) if $?&.exited? + end + end + end +end diff --git a/test/task_test.rb b/test/task_test.rb new file mode 100644 index 00000000..c1e00b8b --- /dev/null +++ b/test/task_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "syntax_tree/rake/task" + +module SyntaxTree + class TaskTest < Minitest::Test + Invoke = Struct.new(:args) + + def test_task + source_files = "{app,config,lib}/**/*.rb" + + SyntaxTree::Rake::Task.new do |t| + t.source_files = source_files + end + + invoke = [] + SyntaxTree::CLI.stub(:run, ->(args) { invoke << Invoke.new(args) }) do + ::Rake::Task["check_and_format"].invoke + end + + assert_equal( + [["check", source_files], ["format", source_files]], invoke.map(&:args) + ) + end + end +end From f27590c43e61cdd2977c01d56601c3ba33ff00fa Mon Sep 17 00:00:00 2001 From: Wender Freese Date: Fri, 13 May 2022 16:05:25 -0300 Subject: [PATCH 02/12] Split "check_and_format" task into two different files "check" and "write" --- .../rake/{task.rb => check_task.rb} | 27 +++++----- lib/syntax_tree/rake/write_task.rb | 53 +++++++++++++++++++ test/check_task_test.rb | 23 ++++++++ test/task_test.rb | 27 ---------- test/write_task_test.rb | 23 ++++++++ 5 files changed, 114 insertions(+), 39 deletions(-) rename lib/syntax_tree/rake/{task.rb => check_task.rb} (54%) create mode 100644 lib/syntax_tree/rake/write_task.rb create mode 100644 test/check_task_test.rb delete mode 100644 test/task_test.rb create mode 100644 test/write_task_test.rb diff --git a/lib/syntax_tree/rake/task.rb b/lib/syntax_tree/rake/check_task.rb similarity index 54% rename from lib/syntax_tree/rake/task.rb rename to lib/syntax_tree/rake/check_task.rb index 12279e0f..0c0dc860 100644 --- a/lib/syntax_tree/rake/task.rb +++ b/lib/syntax_tree/rake/check_task.rb @@ -5,25 +5,31 @@ module SyntaxTree module Rake - # A Rake task that runs check and format on a set of source files. + # A Rake task that runs check on a set of source files. # # Example: # - # require 'syntax_tree/rake/task' + # require 'syntax_tree/rake/check_task' # - # SyntaxTree::Rake::Task.new do |t| + # SyntaxTree::Rake::CheckTask.new do |t| # t.source_files = '{app,config,lib}/**/*.rb' # end # # This will create task that can be run with: # - # rake syntax_tree:check_and_format - class Task < ::Rake::TaskLib + # rake stree_check + # + class CheckTask < ::Rake::TaskLib + # Name of the task. + # Defaults to :stree_check. + attr_accessor :name + # Glob pattern to match source files. # Defaults to 'lib/**/*.rb'. attr_accessor :source_files - def initialize + def initialize(name = :stree_check) + @name = name @source_files = "lib/**/*.rb" yield self if block_given? @@ -33,16 +39,13 @@ def initialize private def define_task - desc "Runs syntax_tree over source files" - task(:check_and_format) { run_task } + desc "Runs `stree check` over source files" + task(name) { run_task } end def run_task - %w[check format].each do |command| - SyntaxTree::CLI.run([command, source_files].compact) - end + SyntaxTree::CLI.run(["check", source_files].compact) - # TODO: figure this out # exit($?.exitstatus) if $?&.exited? end end diff --git a/lib/syntax_tree/rake/write_task.rb b/lib/syntax_tree/rake/write_task.rb new file mode 100644 index 00000000..08b6018c --- /dev/null +++ b/lib/syntax_tree/rake/write_task.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "rake" +require "rake/tasklib" + +module SyntaxTree + module Rake + # A Rake task that runs format on a set of source files. + # + # Example: + # + # require 'syntax_tree/rake/write_task' + # + # SyntaxTree::Rake::WriteTask.new do |t| + # t.source_files = '{app,config,lib}/**/*.rb' + # end + # + # This will create task that can be run with: + # + # rake stree_write + # + class WriteTask < ::Rake::TaskLib + # Name of the task. + # Defaults to :stree_write. + attr_accessor :name + + # Glob pattern to match source files. + # Defaults to 'lib/**/*.rb'. + attr_accessor :source_files + + def initialize(name = :stree_write) + @name = name + @source_files = "lib/**/*.rb" + + yield self if block_given? + define_task + end + + private + + def define_task + desc "Runs `stree write` over source files" + task(name) { run_task } + end + + def run_task + SyntaxTree::CLI.run(["write", source_files].compact) + + # exit($?.exitstatus) if $?&.exited? + end + end + end +end diff --git a/test/check_task_test.rb b/test/check_task_test.rb new file mode 100644 index 00000000..33333241 --- /dev/null +++ b/test/check_task_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "syntax_tree/rake/check_task" + +module SyntaxTree + class CheckTaskTest < Minitest::Test + Invoke = Struct.new(:args) + + def test_task + source_files = "{app,config,lib}/**/*.rb" + + SyntaxTree::Rake::CheckTask.new { |t| t.source_files = source_files } + + invoke = nil + SyntaxTree::CLI.stub(:run, ->(args) { invoke = Invoke.new(args) }) do + ::Rake::Task["stree_check"].invoke + end + + assert_equal(["check", source_files], invoke.args) + end + end +end diff --git a/test/task_test.rb b/test/task_test.rb deleted file mode 100644 index c1e00b8b..00000000 --- a/test/task_test.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require_relative "test_helper" -require "syntax_tree/rake/task" - -module SyntaxTree - class TaskTest < Minitest::Test - Invoke = Struct.new(:args) - - def test_task - source_files = "{app,config,lib}/**/*.rb" - - SyntaxTree::Rake::Task.new do |t| - t.source_files = source_files - end - - invoke = [] - SyntaxTree::CLI.stub(:run, ->(args) { invoke << Invoke.new(args) }) do - ::Rake::Task["check_and_format"].invoke - end - - assert_equal( - [["check", source_files], ["format", source_files]], invoke.map(&:args) - ) - end - end -end diff --git a/test/write_task_test.rb b/test/write_task_test.rb new file mode 100644 index 00000000..deb5acfd --- /dev/null +++ b/test/write_task_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "syntax_tree/rake/write_task" + +module SyntaxTree + class WriteTaskTest < Minitest::Test + Invoke = Struct.new(:args) + + def test_task + source_files = "{app,config,lib}/**/*.rb" + + SyntaxTree::Rake::WriteTask.new { |t| t.source_files = source_files } + + invoke = nil + SyntaxTree::CLI.stub(:run, ->(args) { invoke = Invoke.new(args) }) do + ::Rake::Task["stree_write"].invoke + end + + assert_equal(["write", source_files], invoke.args) + end + end +end From dd1a0fe60955ae9e021c5863dfe5b8c9418f0e2c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 13 May 2022 21:55:42 -0400 Subject: [PATCH 03/12] Document maxwidth on format --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8955a310..b1523080 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ This function takes an input string containing Ruby code and returns the syntax ### SyntaxTree.format(source) -This function takes an input string containing Ruby code, parses it into its underlying syntax tree, and formats it back out to a string. +This function takes an input string containing Ruby code, parses it into its underlying syntax tree, and formats it back out to a string. You can optionally pass a second argument to this method as well that is the maximum width to print. It defaults to `80`. ## Nodes From 66f708bd7f196c97a5a6889ace263b731a4779cc Mon Sep 17 00:00:00 2001 From: Wender Freese Date: Mon, 16 May 2022 09:44:04 -0300 Subject: [PATCH 04/12] Remove explicit call for exit() --- lib/syntax_tree/rake/check_task.rb | 2 -- lib/syntax_tree/rake/write_task.rb | 2 -- 2 files changed, 4 deletions(-) diff --git a/lib/syntax_tree/rake/check_task.rb b/lib/syntax_tree/rake/check_task.rb index 0c0dc860..5fc4ce56 100644 --- a/lib/syntax_tree/rake/check_task.rb +++ b/lib/syntax_tree/rake/check_task.rb @@ -45,8 +45,6 @@ def define_task def run_task SyntaxTree::CLI.run(["check", source_files].compact) - - # exit($?.exitstatus) if $?&.exited? end end end diff --git a/lib/syntax_tree/rake/write_task.rb b/lib/syntax_tree/rake/write_task.rb index 08b6018c..6143d6b9 100644 --- a/lib/syntax_tree/rake/write_task.rb +++ b/lib/syntax_tree/rake/write_task.rb @@ -45,8 +45,6 @@ def define_task def run_task SyntaxTree::CLI.run(["write", source_files].compact) - - # exit($?.exitstatus) if $?&.exited? end end end From cff7953adf0fad6125073f440459acf0e671c55b Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 10:12:17 -0400 Subject: [PATCH 05/12] Document rake task --- .github/workflows/main.yml | 2 +- CHANGELOG.md | 4 +++ README.md | 41 ++++++++++++++++++++++++++++++ Rakefile | 25 ++++-------------- lib/syntax_tree/rake/check_task.rb | 23 ++++++++++++++--- lib/syntax_tree/rake/write_task.rb | 23 ++++++++++++++--- lib/syntax_tree/rake_tasks.rb | 4 +++ test/check_task_test.rb | 23 ----------------- test/rake_test.rb | 36 ++++++++++++++++++++++++++ test/write_task_test.rb | 23 ----------------- 10 files changed, 129 insertions(+), 75 deletions(-) create mode 100644 lib/syntax_tree/rake_tasks.rb delete mode 100644 test/check_task_test.rb create mode 100644 test/rake_test.rb delete mode 100644 test/write_task_test.rb diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f5ac15c..ed3c51fd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: ruby-version: '3.1' - name: Check run: | - bundle exec rake check + bundle exec rake stree:check bundle exec rubocop automerge: diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c8781d..479863f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +### Added + +- [#74](https://github.com/ruby-syntax-tree/syntax_tree/pull/74) - Add Rake test to run check and format commands. + ## [2.5.0] - 2022-05-13 ### Added diff --git a/README.md b/README.md index b1523080..2657852d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ It is built with only standard library dependencies. It additionally ships with - [syntaxTree/visualizing](#syntaxtreevisualizing) - [Plugins](#plugins) - [Integration](#integration) + - [Rake](#rake) - [RuboCop](#rubocop) - [VSCode](#vscode) - [Contributing](#contributing) @@ -436,6 +437,46 @@ Below are listed all of the "official" language plugins hosted under the same Gi Syntax Tree's goal is to seemlessly integrate into your workflow. To this end, it provides a couple of additional tools beyond the CLI and the Ruby library. +### Rake + +Syntax Tree ships with the ability to define [rake](https://github.com/ruby/rake) tasks that will trigger runs of the CLI. To define them in your application, add the following configuration to your `Rakefile`: + +```ruby +require "syntax_tree/rake_tasks" +SyntaxTree::Rake::CheckTask.new +SyntaxTree::Rake::WriteTask.new +``` + +These calls will define `rake stree:check` and `rake stree:write` (equivalent to calling `stree check` and `stree write` respectively). You can configure them by either passing arguments to the `new` method or by using a block. + +#### `name` + +If you'd like to change the default name of the rake task, you can pass that as the first optioon, as in: + +```ruby +SyntaxTree::Rake::WriteTask.new(:format) +``` + +#### `source_files` + +If you wanted to configure Syntax Tree to check or write different files than the default (`lib/**/*.rb`), you can set the `source_files` field, as in: + +```ruby +SyntaxTree::Rake::WriteTask.new do |t| + t.source_files = FileList[%w[Gemfile Rakefile lib/**/*.rb test/**/*.rb]] +end +``` + +#### `plugins` + +If you're running Syntax Tree with plugins (either your own or the pre-built ones), you can pass that to the `plugins` field, as in: + +```ruby +SyntaxTree::Rake::WriteTask.new do |t| + t.plugins = ["plugin/single_quotes"] +end +``` + ### RuboCop RuboCop and Syntax Tree serve different purposes, but there is overlap with some of RuboCop's functionality. Syntax Tree provides a RuboCop configuration file to disable rules that are redundant with Syntax Tree. To use this configuration file, add the following snippet to the top of your project's `.rubocop.yml`: diff --git a/Rakefile b/Rakefile index 4b3de39a..6ba17fe9 100644 --- a/Rakefile +++ b/Rakefile @@ -2,6 +2,7 @@ require "bundler/gem_tasks" require "rake/testtask" +require "syntax_tree/rake_tasks" Rake::TestTask.new(:test) do |t| t.libs << "test" @@ -11,24 +12,8 @@ end task default: :test -FILEPATHS = %w[ - Gemfile - Rakefile - syntax_tree.gemspec - lib/**/*.rb - test/*.rb -].freeze +SOURCE_FILES = + FileList[%w[Gemfile Rakefile syntax_tree.gemspec lib/**/*.rb test/*.rb]] -task :syntax_tree do - $:.unshift File.expand_path("lib", __dir__) - require "syntax_tree" - require "syntax_tree/cli" -end - -task check: :syntax_tree do - exit SyntaxTree::CLI.run(["check"] + FILEPATHS) -end - -task format: :syntax_tree do - exit SyntaxTree::CLI.run(["write"] + FILEPATHS) -end +SyntaxTree::Rake::CheckTask.new { |t| t.source_files = SOURCE_FILES } +SyntaxTree::Rake::WriteTask.new { |t| t.source_files = SOURCE_FILES } diff --git a/lib/syntax_tree/rake/check_task.rb b/lib/syntax_tree/rake/check_task.rb index 5fc4ce56..354cd172 100644 --- a/lib/syntax_tree/rake/check_task.rb +++ b/lib/syntax_tree/rake/check_task.rb @@ -3,6 +3,9 @@ require "rake" require "rake/tasklib" +require "syntax_tree" +require "syntax_tree/cli" + module SyntaxTree module Rake # A Rake task that runs check on a set of source files. @@ -21,16 +24,25 @@ module Rake # class CheckTask < ::Rake::TaskLib # Name of the task. - # Defaults to :stree_check. + # Defaults to :"stree:check". attr_accessor :name # Glob pattern to match source files. # Defaults to 'lib/**/*.rb'. attr_accessor :source_files - def initialize(name = :stree_check) + # The set of plugins to require. + # Defaults to []. + attr_accessor :plugins + + def initialize( + name = :"stree:check", + source_files = ::Rake::FileList["lib/**/*.rb"], + plugins = [] + ) @name = name - @source_files = "lib/**/*.rb" + @source_files = source_files + @plugins = plugins yield self if block_given? define_task @@ -44,7 +56,10 @@ def define_task end def run_task - SyntaxTree::CLI.run(["check", source_files].compact) + arguments = ["check"] + arguments << "--plugins=#{plugins.join(",")}" if plugins.any? + + SyntaxTree::CLI.run(arguments + Array(source_files)) end end end diff --git a/lib/syntax_tree/rake/write_task.rb b/lib/syntax_tree/rake/write_task.rb index 6143d6b9..5a957480 100644 --- a/lib/syntax_tree/rake/write_task.rb +++ b/lib/syntax_tree/rake/write_task.rb @@ -3,6 +3,9 @@ require "rake" require "rake/tasklib" +require "syntax_tree" +require "syntax_tree/cli" + module SyntaxTree module Rake # A Rake task that runs format on a set of source files. @@ -21,16 +24,25 @@ module Rake # class WriteTask < ::Rake::TaskLib # Name of the task. - # Defaults to :stree_write. + # Defaults to :"stree:write". attr_accessor :name # Glob pattern to match source files. # Defaults to 'lib/**/*.rb'. attr_accessor :source_files - def initialize(name = :stree_write) + # The set of plugins to require. + # Defaults to []. + attr_accessor :plugins + + def initialize( + name = :"stree:write", + source_files = ::Rake::FileList["lib/**/*.rb"], + plugins = [] + ) @name = name - @source_files = "lib/**/*.rb" + @source_files = source_files + @plugins = plugins yield self if block_given? define_task @@ -44,7 +56,10 @@ def define_task end def run_task - SyntaxTree::CLI.run(["write", source_files].compact) + arguments = ["write"] + arguments << "--plugins=#{plugins.join(",")}" if plugins.any? + + SyntaxTree::CLI.run(arguments + Array(source_files)) end end end diff --git a/lib/syntax_tree/rake_tasks.rb b/lib/syntax_tree/rake_tasks.rb new file mode 100644 index 00000000..b53743e5 --- /dev/null +++ b/lib/syntax_tree/rake_tasks.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require_relative "rake/check_task" +require_relative "rake/write_task" diff --git a/test/check_task_test.rb b/test/check_task_test.rb deleted file mode 100644 index 33333241..00000000 --- a/test/check_task_test.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require_relative "test_helper" -require "syntax_tree/rake/check_task" - -module SyntaxTree - class CheckTaskTest < Minitest::Test - Invoke = Struct.new(:args) - - def test_task - source_files = "{app,config,lib}/**/*.rb" - - SyntaxTree::Rake::CheckTask.new { |t| t.source_files = source_files } - - invoke = nil - SyntaxTree::CLI.stub(:run, ->(args) { invoke = Invoke.new(args) }) do - ::Rake::Task["stree_check"].invoke - end - - assert_equal(["check", source_files], invoke.args) - end - end -end diff --git a/test/rake_test.rb b/test/rake_test.rb new file mode 100644 index 00000000..57364859 --- /dev/null +++ b/test/rake_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "syntax_tree/rake_tasks" + +module SyntaxTree + module Rake + class CheckTaskTest < Minitest::Test + Invoke = Struct.new(:args) + + def test_check_task + source_files = "{app,config,lib}/**/*.rb" + CheckTask.new { |t| t.source_files = source_files } + + invoke = nil + SyntaxTree::CLI.stub(:run, ->(args) { invoke = Invoke.new(args) }) do + ::Rake::Task["stree:check"].invoke + end + + assert_equal(["check", source_files], invoke.args) + end + + def test_write_task + source_files = "{app,config,lib}/**/*.rb" + WriteTask.new { |t| t.source_files = source_files } + + invoke = nil + SyntaxTree::CLI.stub(:run, ->(args) { invoke = Invoke.new(args) }) do + ::Rake::Task["stree:write"].invoke + end + + assert_equal(["write", source_files], invoke.args) + end + end + end +end diff --git a/test/write_task_test.rb b/test/write_task_test.rb deleted file mode 100644 index deb5acfd..00000000 --- a/test/write_task_test.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require_relative "test_helper" -require "syntax_tree/rake/write_task" - -module SyntaxTree - class WriteTaskTest < Minitest::Test - Invoke = Struct.new(:args) - - def test_task - source_files = "{app,config,lib}/**/*.rb" - - SyntaxTree::Rake::WriteTask.new { |t| t.source_files = source_files } - - invoke = nil - SyntaxTree::CLI.stub(:run, ->(args) { invoke = Invoke.new(args) }) do - ::Rake::Task["stree_write"].invoke - end - - assert_equal(["write", source_files], invoke.args) - end - end -end From 744e5eb4ed17b8b40dd419ecc08e963a7b4366ba Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 10:13:45 -0400 Subject: [PATCH 06/12] Fix up documentation typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2657852d..3fed780c 100644 --- a/README.md +++ b/README.md @@ -447,11 +447,11 @@ SyntaxTree::Rake::CheckTask.new SyntaxTree::Rake::WriteTask.new ``` -These calls will define `rake stree:check` and `rake stree:write` (equivalent to calling `stree check` and `stree write` respectively). You can configure them by either passing arguments to the `new` method or by using a block. +These calls will define `rake stree:check` and `rake stree:write` (equivalent to calling `stree check` and `stree write` with the CLI respectively). You can configure them by either passing arguments to the `new` method or by using a block. #### `name` -If you'd like to change the default name of the rake task, you can pass that as the first optioon, as in: +If you'd like to change the default name of the rake task, you can pass that as the first argument, as in: ```ruby SyntaxTree::Rake::WriteTask.new(:format) From 8b8cecc345ca4393e6d5d46955ad9869fc3a6ea2 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 11:17:42 -0400 Subject: [PATCH 07/12] Support trailing commas --- README.md | 7 +- lib/syntax_tree/formatter.rb | 8 +- lib/syntax_tree/formatter/trailing_comma.rb | 13 +++ lib/syntax_tree/node.rb | 23 +++++ lib/syntax_tree/plugin/trailing_comma.rb | 4 + test/formatter/single_quotes_test.rb | 72 +++++++-------- test/formatter/trailing_comma_test.rb | 97 +++++++++++++++++++++ 7 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 lib/syntax_tree/formatter/trailing_comma.rb create mode 100644 lib/syntax_tree/plugin/trailing_comma.rb create mode 100644 test/formatter/trailing_comma_test.rb diff --git a/README.md b/README.md index 3fed780c..81dfdd71 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ It is built with only standard library dependencies. It additionally ships with - [textDocument/inlayHints](#textdocumentinlayhints) - [syntaxTree/visualizing](#syntaxtreevisualizing) - [Plugins](#plugins) + - [Configuration](#configuration) + - [Languages](#languages) - [Integration](#integration) - [Rake](#rake) - [RuboCop](#rubocop) @@ -409,9 +411,12 @@ You can register additional configuration and additional languages that can flow ### Configuration -To register additional configuration, define a file somewhere in your load path named `syntax_tree/my_plugin` directory. Then when invoking the CLI, you will pass `--plugins=my_plugin`. That will get required. In this way, you can modify Syntax Tree however you would like. Some plugins ship with Syntax Tree itself. They are: +To register additional configuration, define a file somewhere in your load path named `syntax_tree/my_plugin`. Then when invoking the CLI, you will pass `--plugins=my_plugin`. To require multiple, separate them by a comma. In this way, you can modify Syntax Tree however you would like. Some plugins ship with Syntax Tree itself. They are: * `plugin/single_quotes` - This will change all of your string literals to use single quotes instead of the default double quotes. +* `plugin/trailing_comma` - This will put trailing commas into multiline array literals, hash literals, and method calls that can support trailing commas. + +If you're using Syntax Tree as a library, you should require those files directly. ### Languages diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index 88974be4..5d362129 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -7,7 +7,12 @@ class Formatter < PrettierPrint COMMENT_PRIORITY = 1 HEREDOC_PRIORITY = 2 - attr_reader :source, :stack, :quote + attr_reader :source, :stack + + # These options are overridden in plugins to we need to make sure they are + # available here. + attr_reader :quote, :trailing_comma + alias trailing_comma? trailing_comma def initialize(source, ...) super(...) @@ -15,6 +20,7 @@ def initialize(source, ...) @source = source @stack = [] @quote = "\"" + @trailing_comma = false end def self.format(source, node) diff --git a/lib/syntax_tree/formatter/trailing_comma.rb b/lib/syntax_tree/formatter/trailing_comma.rb new file mode 100644 index 00000000..63fe2e9a --- /dev/null +++ b/lib/syntax_tree/formatter/trailing_comma.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SyntaxTree + class Formatter + # This module overrides the trailing_comma? method on the formatter to + # return true. + module TrailingComma + def trailing_comma? + true + end + end + end +end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 7667378d..a96b9794 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -597,10 +597,30 @@ def format(q) q.indent do q.breakable("") q.format(arguments) + q.if_break { q.text(",") } if q.trailing_comma? && trailing_comma? end q.breakable("") end end + + private + + def trailing_comma? + case arguments + in Args[parts: [*, ArgBlock]] + # If the last argument is a block, then we can't put a trailing comma + # after it without resulting in a syntax error. + false + in Args[parts: [Command | CommandCall]] + # If the only argument is a command or command call, then a trailing + # comma would be parsed as part of that expression instead of on this + # one, so we don't want to add a trailing comma. + false + else + # Otherwise, we should be okay to add a trailing comma. + true + end + end end # Args represents a list of arguments being passed to a method call or array @@ -859,6 +879,7 @@ def format(q) end q.seplist(contents.parts, separator) { |part| q.format(part) } + q.if_break { q.text(",") } if q.trailing_comma? end q.breakable("") end @@ -954,6 +975,7 @@ def format(q) q.indent do q.breakable("") q.format(contents) + q.if_break { q.text(",") } if q.trailing_comma? end end @@ -4751,6 +4773,7 @@ def format_contents(q) q.indent do q.breakable q.seplist(assocs) { |assoc| q.format(assoc) } + q.if_break { q.text(",") } if q.trailing_comma? end q.breakable end diff --git a/lib/syntax_tree/plugin/trailing_comma.rb b/lib/syntax_tree/plugin/trailing_comma.rb new file mode 100644 index 00000000..eaa8cb6a --- /dev/null +++ b/lib/syntax_tree/plugin/trailing_comma.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require "syntax_tree/formatter/trailing_comma" +SyntaxTree::Formatter.prepend(SyntaxTree::Formatter::TrailingComma) diff --git a/test/formatter/single_quotes_test.rb b/test/formatter/single_quotes_test.rb index 8bf82cb8..ac5103a1 100644 --- a/test/formatter/single_quotes_test.rb +++ b/test/formatter/single_quotes_test.rb @@ -5,41 +5,43 @@ module SyntaxTree class Formatter - class TestFormatter < Formatter - prepend Formatter::SingleQuotes - end - - def test_empty_string_literal - assert_format("''\n", "\"\"") - end - - def test_string_literal - assert_format("'string'\n", "\"string\"") - end - - def test_string_literal_with_interpolation - assert_format("\"\#{foo}\"\n") - end - - def test_dyna_symbol - assert_format(":'symbol'\n", ":\"symbol\"") - end - - def test_label - assert_format( - "{ foo => foo, :'bar' => bar }\n", - "{ foo => foo, \"bar\": bar }" - ) - end - - private - - def assert_format(expected, source = expected) - formatter = TestFormatter.new(source, []) - SyntaxTree.parse(source).format(formatter) - - formatter.flush - assert_equal(expected, formatter.output.join) + class SingleQuotesTest < Minitest::Test + class TestFormatter < Formatter + prepend Formatter::SingleQuotes + end + + def test_empty_string_literal + assert_format("''\n", "\"\"") + end + + def test_string_literal + assert_format("'string'\n", "\"string\"") + end + + def test_string_literal_with_interpolation + assert_format("\"\#{foo}\"\n") + end + + def test_dyna_symbol + assert_format(":'symbol'\n", ":\"symbol\"") + end + + def test_label + assert_format( + "{ foo => foo, :'bar' => bar }\n", + "{ foo => foo, \"bar\": bar }" + ) + end + + private + + def assert_format(expected, source = expected) + formatter = TestFormatter.new(source, []) + SyntaxTree.parse(source).format(formatter) + + formatter.flush + assert_equal(expected, formatter.output.join) + end end end end diff --git a/test/formatter/trailing_comma_test.rb b/test/formatter/trailing_comma_test.rb new file mode 100644 index 00000000..f6585772 --- /dev/null +++ b/test/formatter/trailing_comma_test.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require_relative "../test_helper" +require "syntax_tree/formatter/trailing_comma" + +module SyntaxTree + class Formatter + class TrailingCommaTest < Minitest::Test + class TestFormatter < Formatter + prepend Formatter::TrailingComma + end + + def test_arg_paren_flat + assert_format("foo(a)\n") + end + + def test_arg_paren_break + assert_format(<<~EXPECTED, <<~SOURCE) + foo( + #{"a" * 80}, + ) + EXPECTED + foo(#{"a" * 80}) + SOURCE + end + + def test_arg_paren_block + assert_format(<<~EXPECTED, <<~SOURCE) + foo( + &#{"a" * 80} + ) + EXPECTED + foo(&#{"a" * 80}) + SOURCE + end + + def test_arg_paren_command + assert_format(<<~EXPECTED, <<~SOURCE) + foo( + bar #{"a" * 80} + ) + EXPECTED + foo(bar #{"a" * 80}) + SOURCE + end + + def test_arg_paren_command_call + assert_format(<<~EXPECTED, <<~SOURCE) + foo( + bar.baz #{"a" * 80} + ) + EXPECTED + foo(bar.baz #{"a" * 80}) + SOURCE + end + + def test_array_literal_flat + assert_format("[a]\n") + end + + def test_array_literal_break + assert_format(<<~EXPECTED, <<~SOURCE) + [ + #{"a" * 80}, + ] + EXPECTED + [#{"a" * 80}] + SOURCE + end + + def test_hash_literal_flat + assert_format("{ a: a }\n") + end + + def test_hash_literal_break + assert_format(<<~EXPECTED, <<~SOURCE) + { + a: + #{"a" * 80}, + } + EXPECTED + { a: #{"a" * 80} } + SOURCE + end + + private + + def assert_format(expected, source = expected) + formatter = TestFormatter.new(source, []) + SyntaxTree.parse(source).format(formatter) + + formatter.flush + assert_equal(expected, formatter.output.join) + end + end + end +end From 5ee6fae73c30042b7be6a19f6763c2a138942349 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 13:51:54 -0400 Subject: [PATCH 08/12] Handle lambda-local variables --- lib/syntax_tree/node.rb | 96 +++++++++++++++++-- lib/syntax_tree/parser.rb | 113 +++++++++++++++++++++++ lib/syntax_tree/visitor.rb | 3 + lib/syntax_tree/visitor/field_visitor.rb | 8 ++ test/fixtures/lambda.rb | 26 ++++++ 5 files changed, 236 insertions(+), 10 deletions(-) diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index a96b9794..5663fac3 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -5900,7 +5900,7 @@ def deconstruct_keys(_keys) # ->(value) { value * 2 } # class Lambda < Node - # [Params | Paren] the parameter declaration for this lambda + # [LambdaVar | Paren] the parameter declaration for this lambda attr_reader :params # [BodyStmt | Statements] the expressions to be executed in this lambda @@ -5955,24 +5955,100 @@ def format(q) node.is_a?(Command) || node.is_a?(CommandCall) end - q.text(force_parens ? "{" : "do") - q.indent do + if force_parens + q.text("{") + + unless statements.empty? + q.indent do + q.breakable + q.format(statements) + end + q.breakable + end + + q.text("}") + else + q.text("do") + + unless statements.empty? + q.indent do + q.breakable + q.format(statements) + end + end + q.breakable - q.format(statements) + q.text("end") end - - q.breakable - q.text(force_parens ? "}" : "end") end .if_flat do - q.text("{ ") - q.format(statements) - q.text(" }") + q.text("{") + + unless statements.empty? + q.text(" ") + q.format(statements) + q.text(" ") + end + + q.text("}") end end end end + # LambdaVar represents the parameters being declared for a lambda. Effectively + # this node is everything contained within the parentheses. This includes all + # of the various parameter types, as well as block-local variable + # declarations. + # + # -> (positional, optional = value, keyword:, █ local) do + # end + # + class LambdaVar < Node + # [Params] the parameters being declared with the block + attr_reader :params + + # [Array[ Ident ]] the list of block-local variable declarations + attr_reader :locals + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(params:, locals:, location:, comments: []) + @params = params + @locals = locals + @location = location + @comments = comments + end + + def accept(visitor) + visitor.visit_lambda_var(self) + end + + def child_nodes + [params, *locals] + end + + alias deconstruct child_nodes + + def deconstruct_keys(_keys) + { params: params, locals: locals, location: location, comments: comments } + end + + def empty? + params.empty? && locals.empty? + end + + def format(q) + q.format(params) + + if locals.any? + q.text("; ") + q.seplist(locals, -> { q.text(", ") }) { |local| q.format(local) } + end + end + end + # LBrace represents the use of a left brace, i.e., {. class LBrace < Node # [String] the left brace diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index f5ffe47d..2de295f3 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -1940,6 +1940,41 @@ def on_lambda(params, statements) token.location.start_char > beginning.location.start_char end + # We need to do some special mapping here. Since ripper doesn't support + # capturing lambda var until 3.2, we need to normalize all of that here. + params = + case params + in Paren[contents: Params] + # In this case we've gotten to the <3.2 parentheses wrapping a set of + # parameters case. Here we need to manually scan for lambda locals. + range = (params.location.start_char + 1)...params.location.end_char + locals = lambda_locals(source[range]) + + location = params.contents.location + location = location.to(locals.last.location) if locals.any? + + Paren.new( + lparen: params.lparen, + contents: + LambdaVar.new( + params: params.contents, + locals: locals, + location: location + ), + location: params.location, + comments: params.comments + ) + in Params + # In this case we've gotten to the <3.2 plain set of parameters. In + # this case there cannot be lambda locals, so we will wrap the + # parameters into a lambda var that has no locals. + LambdaVar.new(params: params, locals: [], location: params.location) + in LambdaVar + # In this case we've gotten to 3.2+ lambda var. In this case we don't + # need to do anything and can just the value as given. + params + end + if braces opening = find_token(TLamBeg) closing = find_token(RBrace) @@ -1962,6 +1997,84 @@ def on_lambda(params, statements) ) end + # :call-seq: + # on_lambda_var: (Params params, Array[ Ident ] locals) -> LambdaVar + def on_lambda_var(params, locals) + location = params.location + location = location.to(locals.last.location) if locals.any? + + LambdaVar.new(params: params, locals: locals || [], location: location) + end + + # Ripper doesn't support capturing lambda local variables until 3.2. To + # mitigate this, we have to parse that code for ourselves. We use the range + # from the parentheses to find where we _should_ be looking. Then we check + # if the resulting tokens match a pattern that we determine means that the + # declaration has block-local variables. Once it does, we parse those out + # and convert them into Ident nodes. + def lambda_locals(source) + tokens = Ripper.lex(source) + + # First, check that we have a semi-colon. If we do, then we can start to + # parse the tokens _after_ the semicolon. + index = tokens.rindex { |token| token[1] == :on_semicolon } + return [] unless index + + # Next, map over the tokens and convert them into Ident nodes. Bail out + # midway through if we encounter a token we didn't expect. Basically we're + # making our own mini-parser here. To do that we'll walk through a small + # state machine: + # + # ┌────────┐ ┌────────┐ ┌─────────┐ + # │ │ │ │ │┌───────┐│ + # ──> │ item │ ─── ident ──> │ next │ ─── rparen ──> ││ final ││ + # │ │ <── comma ─── │ │ │└───────┘│ + # └────────┘ └────────┘ └─────────┘ + # │ ^ │ ^ + # └──┘ └──┘ + # ignored_nl, sp nl, sp + # + state = :item + transitions = { + item: { + on_ignored_nl: :item, + on_sp: :item, + on_ident: :next + }, + next: { + on_nl: :next, + on_sp: :next, + on_comma: :item, + on_rparen: :final + }, + final: { + } + } + + tokens[(index + 1)..].each_with_object([]) do |token, locals| + (lineno, column), type, value, = token + + # Make the state transition for the parser. This is going to raise a + # KeyError if we don't have a transition for the current state and type. + # But that shouldn't actually be possible because ripper would have + # found a syntax error by then. + state = transitions[state].fetch(type) + + # If we hit an identifier, then add it to our list. + next if type != :on_ident + + location = + Location.token( + line: lineno, + char: line_counts[lineno - 1][column], + column: column, + size: value.size + ) + + locals << Ident.new(value: value, location: location) + end + end + # :call-seq: # on_lbrace: (String value) -> LBrace def on_lbrace(value) diff --git a/lib/syntax_tree/visitor.rb b/lib/syntax_tree/visitor.rb index 57794ddb..fa1173eb 100644 --- a/lib/syntax_tree/visitor.rb +++ b/lib/syntax_tree/visitor.rb @@ -301,6 +301,9 @@ def visit_child_nodes(node) # Visit a Lambda node. alias visit_lambda visit_child_nodes + # Visit a LambdaVar node. + alias visit_lambda_var visit_child_nodes + # Visit a LBrace node. alias visit_lbrace visit_child_nodes diff --git a/lib/syntax_tree/visitor/field_visitor.rb b/lib/syntax_tree/visitor/field_visitor.rb index 631084e8..4527e0d3 100644 --- a/lib/syntax_tree/visitor/field_visitor.rb +++ b/lib/syntax_tree/visitor/field_visitor.rb @@ -586,6 +586,14 @@ def visit_lambda(node) end end + def visit_lambda_var(node) + node(node, "lambda_var") do + field("params", node.params) + list("locals", node.locals) if node.locals.any? + comments(node) + end + end + def visit_lbrace(node) visit_token(node, "lbrace") end diff --git a/test/fixtures/lambda.rb b/test/fixtures/lambda.rb index 043ceb5a..50e406b1 100644 --- a/test/fixtures/lambda.rb +++ b/test/fixtures/lambda.rb @@ -1,4 +1,6 @@ % +-> {} +% -> { foo } % ->(foo, bar) { baz } @@ -40,3 +42,27 @@ -> { -> foo do bar end.baz }.qux - -> { ->(foo) { bar }.baz }.qux +% +->(;a) {} +- +->(; a) {} +% +->(; a) {} +% +->(; a,b) {} +- +->(; a, b) {} +% +->(; a, b) {} +% +->(; +a +) {} +- +->(; a) {} +% +->(; a , +b +) {} +- +->(; a, b) {} From a487b9a71f3091a0e53c0de35f9fd353ffa02eca Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 16:30:24 -0400 Subject: [PATCH 09/12] Trailing operators on command calls --- lib/syntax_tree/node.rb | 16 ++++++++++++++-- test/fixtures/command_call.rb | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 5663fac3..0a1fc394 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -3052,8 +3052,20 @@ def format(q) doc = q.nest(0) do q.format(receiver) - q.format(CallOperatorFormatter.new(operator), stackable: false) - q.format(message) + + # If there are leading comments on the message then we know we have + # a newline in the source that is forcing these things apart. In + # this case we will have to use a trailing operator. + if message.comments.any?(&:leading?) + q.format(CallOperatorFormatter.new(operator), stackable: false) + q.indent do + q.breakable("") + q.format(message) + end + else + q.format(CallOperatorFormatter.new(operator), stackable: false) + q.format(message) + end end case arguments diff --git a/test/fixtures/command_call.rb b/test/fixtures/command_call.rb index 5060ffa4..fb0d084a 100644 --- a/test/fixtures/command_call.rb +++ b/test/fixtures/command_call.rb @@ -28,3 +28,7 @@ % foo.bar baz do end +% +foo. + # comment + bar baz From 0c5728f718870208ee80a318ac8d3aced63a166c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 19:34:21 -0400 Subject: [PATCH 10/12] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 479863f3..65ca8eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ### Added - [#74](https://github.com/ruby-syntax-tree/syntax_tree/pull/74) - Add Rake test to run check and format commands. +- [#83](https://github.com/ruby-syntax-tree/syntax_tree/pull/83) - Add a trailing commas plugin. +- [#84](https://github.com/ruby-syntax-tree/syntax_tree/pull/84) - Handle lambda block-local variables. + +### Changed + +- [#85](https://github.com/ruby-syntax-tree/syntax_tree/pull/85) - Better handle trailing operators on command calls. ## [2.5.0] - 2022-05-13 From 0fd7a5fcf45a4372aac23424c751c6b9e5984548 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 19:38:42 -0400 Subject: [PATCH 11/12] Ensure lambda block-local variables are handled properly --- lib/syntax_tree/parser.rb | 9 ++++----- test/fixtures/lambda.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 2de295f3..6bff0838 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -2054,11 +2054,10 @@ def lambda_locals(source) tokens[(index + 1)..].each_with_object([]) do |token, locals| (lineno, column), type, value, = token - # Make the state transition for the parser. This is going to raise a - # KeyError if we don't have a transition for the current state and type. - # But that shouldn't actually be possible because ripper would have - # found a syntax error by then. - state = transitions[state].fetch(type) + # Make the state transition for the parser. If there isn't a transition + # from the current state to a new state for this type, then we're in a + # pattern that isn't actually locals. In that case we can return []. + state = transitions[state].fetch(type) { return [] } # If we hit an identifier, then add it to our list. next if type != :on_ident diff --git a/test/fixtures/lambda.rb b/test/fixtures/lambda.rb index 50e406b1..d0cc6f9b 100644 --- a/test/fixtures/lambda.rb +++ b/test/fixtures/lambda.rb @@ -66,3 +66,13 @@ ) {} - ->(; a, b) {} +% +->(a = (b; c)) {} +- +->( + a = ( + b + c + ) +) do +end From 3ee5d0cb94c3ec471740ade57de42ff424172423 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 16 May 2022 19:39:33 -0400 Subject: [PATCH 12/12] Bump to v2.6.0 --- CHANGELOG.md | 5 ++++- Gemfile.lock | 2 +- lib/syntax_tree/version.rb | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65ca8eab..3c57136c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [2.6.0] - 2022-05-16 + ### Added - [#74](https://github.com/ruby-syntax-tree/syntax_tree/pull/74) - Add Rake test to run check and format commands. @@ -234,7 +236,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/v2.5.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.6.0...HEAD +[2.6.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.5.0...v2.6.0 [2.5.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.4.1...v2.5.0 [2.4.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.4.0...v2.4.1 [2.4.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.3.1...v2.4.0 diff --git a/Gemfile.lock b/Gemfile.lock index 220985eb..b642d5dc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (2.5.0) + syntax_tree (2.6.0) prettier_print GEM diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index d12b4964..afa6cc12 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "2.5.0" + VERSION = "2.6.0" end