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..3c57136c 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] +## [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. +- [#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 ### Added @@ -224,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/README.md b/README.md index 8955a310..81dfdd71 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,10 @@ 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) - [VSCode](#vscode) - [Contributing](#contributing) @@ -223,7 +226,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 @@ -408,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 @@ -436,6 +442,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` 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 argument, 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/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..0a1fc394 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 @@ -3030,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 @@ -4751,6 +4785,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 @@ -5877,7 +5912,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 @@ -5932,24 +5967,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..6bff0838 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,83 @@ 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. 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 + + 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/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/lib/syntax_tree/rake/check_task.rb b/lib/syntax_tree/rake/check_task.rb new file mode 100644 index 00000000..354cd172 --- /dev/null +++ b/lib/syntax_tree/rake/check_task.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +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. + # + # Example: + # + # require 'syntax_tree/rake/check_task' + # + # SyntaxTree::Rake::CheckTask.new do |t| + # t.source_files = '{app,config,lib}/**/*.rb' + # end + # + # This will create task that can be run with: + # + # 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 + + # 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 = source_files + @plugins = plugins + + yield self if block_given? + define_task + end + + private + + def define_task + desc "Runs `stree check` over source files" + task(name) { run_task } + end + + def run_task + arguments = ["check"] + arguments << "--plugins=#{plugins.join(",")}" if plugins.any? + + SyntaxTree::CLI.run(arguments + Array(source_files)) + end + end + 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..5a957480 --- /dev/null +++ b/lib/syntax_tree/rake/write_task.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +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. + # + # 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 + + # 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 = source_files + @plugins = plugins + + 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 + arguments = ["write"] + arguments << "--plugins=#{plugins.join(",")}" if plugins.any? + + SyntaxTree::CLI.run(arguments + Array(source_files)) + end + 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/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 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/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 diff --git a/test/fixtures/lambda.rb b/test/fixtures/lambda.rb index 043ceb5a..d0cc6f9b 100644 --- a/test/fixtures/lambda.rb +++ b/test/fixtures/lambda.rb @@ -1,4 +1,6 @@ % +-> {} +% -> { foo } % ->(foo, bar) { baz } @@ -40,3 +42,37 @@ -> { -> 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) {} +% +->(a = (b; c)) {} +- +->( + a = ( + b + c + ) +) do +end 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 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