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