Skip to content

Commit 38ea44b

Browse files
authored
Merge pull request #83 from ruby-syntax-tree/trailing-comma
Support trailing commas
2 parents 744e5eb + 8b8cecc commit 38ea44b

File tree

7 files changed

+187
-37
lines changed

7 files changed

+187
-37
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ It is built with only standard library dependencies. It additionally ships with
3737
- [textDocument/inlayHints](#textdocumentinlayhints)
3838
- [syntaxTree/visualizing](#syntaxtreevisualizing)
3939
- [Plugins](#plugins)
40+
- [Configuration](#configuration)
41+
- [Languages](#languages)
4042
- [Integration](#integration)
4143
- [Rake](#rake)
4244
- [RuboCop](#rubocop)
@@ -409,9 +411,12 @@ You can register additional configuration and additional languages that can flow
409411

410412
### Configuration
411413

412-
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:
414+
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:
413415

414416
* `plugin/single_quotes` - This will change all of your string literals to use single quotes instead of the default double quotes.
417+
* `plugin/trailing_comma` - This will put trailing commas into multiline array literals, hash literals, and method calls that can support trailing commas.
418+
419+
If you're using Syntax Tree as a library, you should require those files directly.
415420

416421
### Languages
417422

lib/syntax_tree/formatter.rb

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ class Formatter < PrettierPrint
77
COMMENT_PRIORITY = 1
88
HEREDOC_PRIORITY = 2
99

10-
attr_reader :source, :stack, :quote
10+
attr_reader :source, :stack
11+
12+
# These options are overridden in plugins to we need to make sure they are
13+
# available here.
14+
attr_reader :quote, :trailing_comma
15+
alias trailing_comma? trailing_comma
1116

1217
def initialize(source, ...)
1318
super(...)
1419

1520
@source = source
1621
@stack = []
1722
@quote = "\""
23+
@trailing_comma = false
1824
end
1925

2026
def self.format(source, node)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
module SyntaxTree
4+
class Formatter
5+
# This module overrides the trailing_comma? method on the formatter to
6+
# return true.
7+
module TrailingComma
8+
def trailing_comma?
9+
true
10+
end
11+
end
12+
end
13+
end

lib/syntax_tree/node.rb

+23
Original file line numberDiff line numberDiff line change
@@ -597,10 +597,30 @@ def format(q)
597597
q.indent do
598598
q.breakable("")
599599
q.format(arguments)
600+
q.if_break { q.text(",") } if q.trailing_comma? && trailing_comma?
600601
end
601602
q.breakable("")
602603
end
603604
end
605+
606+
private
607+
608+
def trailing_comma?
609+
case arguments
610+
in Args[parts: [*, ArgBlock]]
611+
# If the last argument is a block, then we can't put a trailing comma
612+
# after it without resulting in a syntax error.
613+
false
614+
in Args[parts: [Command | CommandCall]]
615+
# If the only argument is a command or command call, then a trailing
616+
# comma would be parsed as part of that expression instead of on this
617+
# one, so we don't want to add a trailing comma.
618+
false
619+
else
620+
# Otherwise, we should be okay to add a trailing comma.
621+
true
622+
end
623+
end
604624
end
605625

606626
# Args represents a list of arguments being passed to a method call or array
@@ -859,6 +879,7 @@ def format(q)
859879
end
860880

861881
q.seplist(contents.parts, separator) { |part| q.format(part) }
882+
q.if_break { q.text(",") } if q.trailing_comma?
862883
end
863884
q.breakable("")
864885
end
@@ -954,6 +975,7 @@ def format(q)
954975
q.indent do
955976
q.breakable("")
956977
q.format(contents)
978+
q.if_break { q.text(",") } if q.trailing_comma?
957979
end
958980
end
959981

@@ -4751,6 +4773,7 @@ def format_contents(q)
47514773
q.indent do
47524774
q.breakable
47534775
q.seplist(assocs) { |assoc| q.format(assoc) }
4776+
q.if_break { q.text(",") } if q.trailing_comma?
47544777
end
47554778
q.breakable
47564779
end
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# frozen_string_literal: true
2+
3+
require "syntax_tree/formatter/trailing_comma"
4+
SyntaxTree::Formatter.prepend(SyntaxTree::Formatter::TrailingComma)

test/formatter/single_quotes_test.rb

+37-35
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,43 @@
55

66
module SyntaxTree
77
class Formatter
8-
class TestFormatter < Formatter
9-
prepend Formatter::SingleQuotes
10-
end
11-
12-
def test_empty_string_literal
13-
assert_format("''\n", "\"\"")
14-
end
15-
16-
def test_string_literal
17-
assert_format("'string'\n", "\"string\"")
18-
end
19-
20-
def test_string_literal_with_interpolation
21-
assert_format("\"\#{foo}\"\n")
22-
end
23-
24-
def test_dyna_symbol
25-
assert_format(":'symbol'\n", ":\"symbol\"")
26-
end
27-
28-
def test_label
29-
assert_format(
30-
"{ foo => foo, :'bar' => bar }\n",
31-
"{ foo => foo, \"bar\": bar }"
32-
)
33-
end
34-
35-
private
36-
37-
def assert_format(expected, source = expected)
38-
formatter = TestFormatter.new(source, [])
39-
SyntaxTree.parse(source).format(formatter)
40-
41-
formatter.flush
42-
assert_equal(expected, formatter.output.join)
8+
class SingleQuotesTest < Minitest::Test
9+
class TestFormatter < Formatter
10+
prepend Formatter::SingleQuotes
11+
end
12+
13+
def test_empty_string_literal
14+
assert_format("''\n", "\"\"")
15+
end
16+
17+
def test_string_literal
18+
assert_format("'string'\n", "\"string\"")
19+
end
20+
21+
def test_string_literal_with_interpolation
22+
assert_format("\"\#{foo}\"\n")
23+
end
24+
25+
def test_dyna_symbol
26+
assert_format(":'symbol'\n", ":\"symbol\"")
27+
end
28+
29+
def test_label
30+
assert_format(
31+
"{ foo => foo, :'bar' => bar }\n",
32+
"{ foo => foo, \"bar\": bar }"
33+
)
34+
end
35+
36+
private
37+
38+
def assert_format(expected, source = expected)
39+
formatter = TestFormatter.new(source, [])
40+
SyntaxTree.parse(source).format(formatter)
41+
42+
formatter.flush
43+
assert_equal(expected, formatter.output.join)
44+
end
4345
end
4446
end
4547
end

test/formatter/trailing_comma_test.rb

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../test_helper"
4+
require "syntax_tree/formatter/trailing_comma"
5+
6+
module SyntaxTree
7+
class Formatter
8+
class TrailingCommaTest < Minitest::Test
9+
class TestFormatter < Formatter
10+
prepend Formatter::TrailingComma
11+
end
12+
13+
def test_arg_paren_flat
14+
assert_format("foo(a)\n")
15+
end
16+
17+
def test_arg_paren_break
18+
assert_format(<<~EXPECTED, <<~SOURCE)
19+
foo(
20+
#{"a" * 80},
21+
)
22+
EXPECTED
23+
foo(#{"a" * 80})
24+
SOURCE
25+
end
26+
27+
def test_arg_paren_block
28+
assert_format(<<~EXPECTED, <<~SOURCE)
29+
foo(
30+
&#{"a" * 80}
31+
)
32+
EXPECTED
33+
foo(&#{"a" * 80})
34+
SOURCE
35+
end
36+
37+
def test_arg_paren_command
38+
assert_format(<<~EXPECTED, <<~SOURCE)
39+
foo(
40+
bar #{"a" * 80}
41+
)
42+
EXPECTED
43+
foo(bar #{"a" * 80})
44+
SOURCE
45+
end
46+
47+
def test_arg_paren_command_call
48+
assert_format(<<~EXPECTED, <<~SOURCE)
49+
foo(
50+
bar.baz #{"a" * 80}
51+
)
52+
EXPECTED
53+
foo(bar.baz #{"a" * 80})
54+
SOURCE
55+
end
56+
57+
def test_array_literal_flat
58+
assert_format("[a]\n")
59+
end
60+
61+
def test_array_literal_break
62+
assert_format(<<~EXPECTED, <<~SOURCE)
63+
[
64+
#{"a" * 80},
65+
]
66+
EXPECTED
67+
[#{"a" * 80}]
68+
SOURCE
69+
end
70+
71+
def test_hash_literal_flat
72+
assert_format("{ a: a }\n")
73+
end
74+
75+
def test_hash_literal_break
76+
assert_format(<<~EXPECTED, <<~SOURCE)
77+
{
78+
a:
79+
#{"a" * 80},
80+
}
81+
EXPECTED
82+
{ a: #{"a" * 80} }
83+
SOURCE
84+
end
85+
86+
private
87+
88+
def assert_format(expected, source = expected)
89+
formatter = TestFormatter.new(source, [])
90+
SyntaxTree.parse(source).format(formatter)
91+
92+
formatter.flush
93+
assert_equal(expected, formatter.output.join)
94+
end
95+
end
96+
end
97+
end

0 commit comments

Comments
 (0)