diff --git a/README.md b/README.md index c238620e..2c86c053 100644 --- a/README.md +++ b/README.md @@ -711,6 +711,7 @@ To register plugins, define a file somewhere in your load path named `syntax_tre * `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. * `plugin/disable_auto_ternary` - This will prevent the automatic conversion of `if ... else` to ternary expressions. +* `plugin/compact_empty_hash` - This will prevent the splitting of empty hashes `{}` over multiple lines. If you're using Syntax Tree as a library, you can require those files directly or manually pass those options to the formatter initializer through the `SyntaxTree::Formatter::Options` class. diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index 2b229885..d4eadfe2 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -24,12 +24,14 @@ class Options attr_reader :quote, :trailing_comma, :disable_auto_ternary, + :compact_empty_hash, :target_ruby_version def initialize( quote: :default, trailing_comma: :default, disable_auto_ternary: :default, + compact_empty_hash: :default, target_ruby_version: :default ) @quote = @@ -65,6 +67,17 @@ def initialize( disable_auto_ternary end + @compact_empty_hash = + if compact_empty_hash == :default + # We ship with a compact empty hash plugin that will define this + # constant. That constant is responsible for determining the default + # compact empty hash value. If it's defined, then we default to true. + # Otherwise we default to false. + defined?(COMPACT_EMPTY_HASH) + else + compact_empty_hash + end + @target_ruby_version = if target_ruby_version == :default # The default target Ruby version is the current version of Ruby. @@ -87,10 +100,12 @@ def initialize( attr_reader :quote, :trailing_comma, :disable_auto_ternary, + :compact_empty_hash, :target_ruby_version alias trailing_comma? trailing_comma alias disable_auto_ternary? disable_auto_ternary + alias compact_empty_hash? compact_empty_hash def initialize(source, *args, options: Options.new) super(*args) @@ -102,6 +117,7 @@ def initialize(source, *args, options: Options.new) @quote = options.quote @trailing_comma = options.trailing_comma @disable_auto_ternary = options.disable_auto_ternary + @compact_empty_hash = options.compact_empty_hash @target_ruby_version = options.target_ruby_version end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 96241bb1..b05adc5e 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -5766,7 +5766,7 @@ def format_contents(q) q.format(lbrace) if assocs.empty? - q.breakable_empty + q.breakable_empty unless q.compact_empty_hash? else q.indent do q.breakable_space diff --git a/lib/syntax_tree/plugin/compact_empty_hash.rb b/lib/syntax_tree/plugin/compact_empty_hash.rb new file mode 100644 index 00000000..85f946ce --- /dev/null +++ b/lib/syntax_tree/plugin/compact_empty_hash.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module SyntaxTree + class Formatter + COMPACT_EMPTY_HASH = true + end +end diff --git a/test/plugin/compact_empty_hash_test.rb b/test/plugin/compact_empty_hash_test.rb new file mode 100644 index 00000000..b2458d90 --- /dev/null +++ b/test/plugin/compact_empty_hash_test.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module SyntaxTree + class CompactEmptyHashTest < Minitest::Test + def test_empty_hash + assert_format("{}\n", "{}") + end + + def test_empty_hash_with_spaces + assert_format("{}\n", "{ }") + end + + def test_empty_hash_with_newlines + assert_format("{}\n", "{\n}") + end + + def test_empty_hash_in_assignment + assert_format("x = {}\n", "x = {}") + end + + def test_empty_hash_in_method_call + assert_format("method({})\n", "method({})") + end + + def test_empty_hash_in_array + assert_format("[{}]\n", "[{}]") + end + + def test_long_assignment_with_empty_hash + source = "this_is_a_very_long_variable_name_that_might_cause_line_breaks_when_assigned_an_empty_hash = {}" + expected = "this_is_a_very_long_variable_name_that_might_cause_line_breaks_when_assigned_an_empty_hash = {}\n" + assert_format(expected, source) + end + + def test_empty_hash_values_in_multiline_hash + source = "{ very_long_key_name_that_might_cause_issues: {}, another_very_long_key_name: {}, yet_another_key: {} }" + expected = <<~RUBY + { + very_long_key_name_that_might_cause_issues: {}, + another_very_long_key_name: {}, + yet_another_key: {} + } + RUBY + assert_format(expected, source) + end + + def test_non_empty_hash_still_works + source = "{ key: value }" + expected = "{ key: value }\n" + assert_format(expected, source) + end + + def test_without_plugin_allows_multiline_empty_hash + source = "this_is_a_very_long_variable_name_that_might_cause_line_breaks_when_assigned_an_empty_hash = {}" + + # Format without the compact_empty_hash option + options = Formatter::Options.new(compact_empty_hash: false) + formatter = Formatter.new(source, [], options: options) + SyntaxTree.parse(source).format(formatter) + formatter.flush + result = formatter.output.join + + # Should allow the hash to break across lines + assert(result.include?("= {\n}"), "Expected empty hash to break across lines when plugin is disabled") + end + + private + + def assert_format(expected, source = expected.chomp) + options = Formatter::Options.new(compact_empty_hash: true) + formatter = Formatter.new(source, [], options: options) + SyntaxTree.parse(source).format(formatter) + formatter.flush + assert_equal(expected, formatter.output.join) + end + end +end