diff --git a/.rubocop.yml b/.rubocop.yml index 8c1bc99e..8cf5f209 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -78,3 +78,6 @@ Style/PerlBackrefs: Style/SpecialGlobalVars: Enabled: false + +Style/StructInheritance: + Enabled: false diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index 5d362129..56de6a4a 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -4,6 +4,18 @@ module SyntaxTree # A slightly enhanced PP that knows how to format recursively including # comments. class Formatter < PrettierPrint + # We want to minimize as much as possible the number of options that are + # available in syntax tree. For the most part, if users want non-default + # formatting, they should override the format methods on the specific nodes + # themselves. However, because of some history with prettier and the fact + # that folks have become entrenched in their ways, we decided to provide a + # small amount of configurability. + # + # Note that we're keeping this in a global-ish hash instead of just + # overriding methods on classes so that other plugins can reference this if + # necessary. For example, the RBS plugin references the quote style. + OPTIONS = { quote: "\"", trailing_comma: false } + COMMENT_PRIORITY = 1 HEREDOC_PRIORITY = 2 @@ -14,13 +26,20 @@ class Formatter < PrettierPrint attr_reader :quote, :trailing_comma alias trailing_comma? trailing_comma - def initialize(source, ...) - super(...) + def initialize( + source, + *args, + quote: OPTIONS[:quote], + trailing_comma: OPTIONS[:trailing_comma] + ) + super(*args) @source = source @stack = [] - @quote = "\"" - @trailing_comma = false + + # Memoizing these values per formatter to make access faster. + @quote = quote + @trailing_comma = trailing_comma end def self.format(source, node) diff --git a/lib/syntax_tree/formatter/single_quotes.rb b/lib/syntax_tree/formatter/single_quotes.rb deleted file mode 100644 index 4d1f41b3..00000000 --- a/lib/syntax_tree/formatter/single_quotes.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Formatter - # This module overrides the quote method on the formatter to use single - # quotes for everything instead of double quotes. - module SingleQuotes - def quote - "'" - end - end - end -end diff --git a/lib/syntax_tree/formatter/trailing_comma.rb b/lib/syntax_tree/formatter/trailing_comma.rb deleted file mode 100644 index 63fe2e9a..00000000 --- a/lib/syntax_tree/formatter/trailing_comma.rb +++ /dev/null @@ -1,13 +0,0 @@ -# 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/language_server.rb b/lib/syntax_tree/language_server.rb index 1e305cca..3853ee18 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -70,13 +70,11 @@ def run id:, params: { textDocument: { uri: } } } - output = [] - PP.pp(SyntaxTree.parse(store[uri]), output) - write(id: id, result: output.join) + write(id: id, result: PP.pp(SyntaxTree.parse(store[uri]), +"")) in method: %r{\$/.+} # ignored else - raise "Unhandled: #{request}" + raise ArgumentError, "Unhandled: #{request}" end end end @@ -109,10 +107,6 @@ def format(source) } end - def log(message) - write(method: "window/logMessage", params: { type: 4, message: message }) - end - def inlay_hints(source) inlay_hints = InlayHints.find(SyntaxTree.parse(source)) serialize = ->(position, text) { { position: position, text: text } } diff --git a/lib/syntax_tree/language_server/inlay_hints.rb b/lib/syntax_tree/language_server/inlay_hints.rb index 69fc5ce4..089355a7 100644 --- a/lib/syntax_tree/language_server/inlay_hints.rb +++ b/lib/syntax_tree/language_server/inlay_hints.rb @@ -38,6 +38,7 @@ def visit(node) # def visit_assign(node) parentheses(node.location) if stack[-2].is_a?(Params) + super end # Adds parentheses around binary expressions to make it clear which @@ -57,6 +58,8 @@ def visit_binary(node) parentheses(node.location) else end + + super end # Adds parentheses around ternary operators contained within certain @@ -70,9 +73,13 @@ def visit_binary(node) # a ? b : ₍c ? d : e₎ # def visit_if_op(node) - if stack[-2] in Assign | Binary | IfOp | OpAssign + case stack[-2] + in Assign | Binary | IfOp | OpAssign parentheses(node.location) + else end + + super end # Adds the implicitly rescued StandardError into a bare rescue clause. For @@ -92,6 +99,8 @@ def visit_rescue(node) if node.exception.nil? after[node.location.start_char + "rescue".length] << " StandardError" end + + super end # Adds parentheses around unary statements using the - operator that are @@ -107,6 +116,8 @@ def visit_unary(node) if stack[-2].is_a?(Binary) && (node.operator == "-") parentheses(node.location) end + + super end def self.find(program) diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 92947735..85956c8c 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -2129,11 +2129,13 @@ def format(q) # # break # - in [Paren[ - contents: { - body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array] - } - ]] + in [ + Paren[ + contents: { + body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array] + } + ] + ] # Here we have a single argument that is a set of parentheses wrapping # an array literal that has at least 2 elements. We're going to print # the contents of the array directly. This would be like if we had: @@ -5472,12 +5474,14 @@ def format_flat(q) q.format(predicate) q.text(" ?") - q.breakable - q.format(truthy) - q.text(" :") + q.indent do + q.breakable + q.format(truthy) + q.text(" :") - q.breakable - q.format(falsy) + q.breakable + q.format(falsy) + end end end diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index 6bff0838..fdffbeb9 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -548,13 +548,6 @@ def on_aryptn(constant, requireds, rest, posts) parts[0].location.to(parts[-1].location) end - # If there's the optional then keyword, then we'll delete that and use it - # as the end bounds of the location. - if (token = find_token(Kw, "then", consume: false)) - tokens.delete(token) - location = location.to(token.location) - end - # If there is a plain *, then we're going to fix up the location of it # here because it currently doesn't have anything to use for its precise # location. If we hit a comma, then we've gone too far. @@ -1698,12 +1691,6 @@ def on_hshptn(constant, keywords, keyword_rest) end end - # Delete the optional then keyword - if (token = find_token(Kw, "then", consume: false)) - parts << token - tokens.delete(token) - end - HshPtn.new( constant: constant, keywords: keywords || [], @@ -3013,6 +3000,11 @@ def on_stmts_new # (StringEmbExpr | StringDVar | TStringContent) part # ) -> StringContent def on_string_add(string, part) + # Due to some eccentricities in how ripper works, you need this here in + # case you have a syntax error with an embedded expression that doesn't + # finish, as in: "#{" + return string if part.is_a?(String) + location = string.parts.any? ? string.location.to(part.location) : part.location diff --git a/lib/syntax_tree/plugin/single_quotes.rb b/lib/syntax_tree/plugin/single_quotes.rb index d8034084..c6e829e0 100644 --- a/lib/syntax_tree/plugin/single_quotes.rb +++ b/lib/syntax_tree/plugin/single_quotes.rb @@ -1,4 +1,3 @@ # frozen_string_literal: true -require "syntax_tree/formatter/single_quotes" -SyntaxTree::Formatter.prepend(SyntaxTree::Formatter::SingleQuotes) +SyntaxTree::Formatter::OPTIONS[:quote] = "'" diff --git a/lib/syntax_tree/plugin/trailing_comma.rb b/lib/syntax_tree/plugin/trailing_comma.rb index eaa8cb6a..878703c3 100644 --- a/lib/syntax_tree/plugin/trailing_comma.rb +++ b/lib/syntax_tree/plugin/trailing_comma.rb @@ -1,4 +1,3 @@ # frozen_string_literal: true -require "syntax_tree/formatter/trailing_comma" -SyntaxTree::Formatter.prepend(SyntaxTree::Formatter::TrailingComma) +SyntaxTree::Formatter::OPTIONS[:trailing_comma] = true diff --git a/test/cli_test.rb b/test/cli_test.rb index ade1485c..7f2bcd26 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -142,11 +142,41 @@ def test_generic_error end end + def test_plugins + Dir.mktmpdir do |directory| + Dir.mkdir(File.join(directory, "syntax_tree")) + $:.unshift(directory) + + File.write( + File.join(directory, "syntax_tree", "plugin.rb"), + "puts 'Hello, world!'" + ) + result = run_cli("format", "--plugins=plugin") + + assert_equal("Hello, world!\ntest\n", result.stdio) + end + end + + def test_language_server + prev_stdin = $stdin + prev_stdout = $stdout + + request = { method: "shutdown" }.merge(jsonrpc: "2.0").to_json + $stdin = + StringIO.new("Content-Length: #{request.bytesize}\r\n\r\n#{request}") + $stdout = StringIO.new + + assert_equal(0, SyntaxTree::CLI.run(["lsp"])) + ensure + $stdin = prev_stdin + $stdout = prev_stdout + end + private Result = Struct.new(:status, :stdio, :stderr, keyword_init: true) - def run_cli(command, file: nil) + def run_cli(command, *args, file: nil) if file.nil? file = Tempfile.new(%w[test- .rb]) file.puts("test") @@ -156,7 +186,7 @@ def run_cli(command, file: nil) status = nil stdio, stderr = - capture_io { status = SyntaxTree::CLI.run([command, file.path]) } + capture_io { status = SyntaxTree::CLI.run([command, *args, file.path]) } Result.new(status: status, stdio: stdio, stderr: stderr) ensure diff --git a/test/fixtures/aryptn.rb b/test/fixtures/aryptn.rb index eddd8e3f..64d5d9d0 100644 --- a/test/fixtures/aryptn.rb +++ b/test/fixtures/aryptn.rb @@ -4,6 +4,22 @@ end % case foo +in [] then +end +- +case foo +in [] +end +% +case foo +in * then +end +- +case foo +in [*] +end +% +case foo in _, _ end - diff --git a/test/fixtures/call.rb b/test/fixtures/call.rb index f3333276..c41ee4ac 100644 --- a/test/fixtures/call.rb +++ b/test/fixtures/call.rb @@ -1,6 +1,8 @@ % foo.bar % +foo.bar(baz) +% foo.() % foo::() @@ -21,3 +23,40 @@ .barrrrrrrrrrrrrrrrrrr {} .bazzzzzzzzzzzzzzzzzzzzzzzzzz .quxxxxxxxxx +% +foo. # comment + bar +% +foo + .bar + .baz # comment + .qux + .quux +% +foo + .bar + .baz. + # comment + qux + .quux +% +{ a: 1, b: 2 }.fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr.bazzzzzzzzzzzz.quxxxxxxxxxxxx +- +{ a: 1, b: 2 }.fooooooooooooooooo + .barrrrrrrrrrrrrrrrrrr + .bazzzzzzzzzzzz + .quxxxxxxxxxxxx +% +fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr.bazzzzzzzzzzzz.quxxxxxxxxxxxx.each { block } +- +fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr.bazzzzzzzzzzzz.quxxxxxxxxxxxx.each do + block +end +% +foo.bar.baz.each do + block1 + block2 +end +% +a b do +end.c d diff --git a/test/fixtures/command_call.rb b/test/fixtures/command_call.rb index fb0d084a..4a0f60f0 100644 --- a/test/fixtures/command_call.rb +++ b/test/fixtures/command_call.rb @@ -32,3 +32,5 @@ foo. # comment bar baz +% +foo.bar baz ? qux : qaz diff --git a/test/fixtures/do_block.rb b/test/fixtures/do_block.rb index 016f27b2..8ea4f75f 100644 --- a/test/fixtures/do_block.rb +++ b/test/fixtures/do_block.rb @@ -14,3 +14,15 @@ foo :bar do baz end +% +sig do + override.params(contacts: Contact::ActiveRecord_Relation).returns( + Customer::ActiveRecord_Relation + ) +end +- +sig do + override + .params(contacts: Contact::ActiveRecord_Relation) + .returns(Customer::ActiveRecord_Relation) +end diff --git a/test/fixtures/hshptn.rb b/test/fixtures/hshptn.rb index f8733170..505336b8 100644 --- a/test/fixtures/hshptn.rb +++ b/test/fixtures/hshptn.rb @@ -64,6 +64,14 @@ end % case foo +in {} then +end +- +case foo +in {} +end +% +case foo in **nil end % diff --git a/test/fixtures/if.rb b/test/fixtures/if.rb index 9045e5bf..e5e88103 100644 --- a/test/fixtures/if.rb +++ b/test/fixtures/if.rb @@ -49,3 +49,13 @@ end - not(a) ? b : c +% +(if foo then bar else baz end) +- +( + if foo + bar + else + baz + end +) diff --git a/test/fixtures/ifop.rb b/test/fixtures/ifop.rb index 541e667e..e56eb987 100644 --- a/test/fixtures/ifop.rb +++ b/test/fixtures/ifop.rb @@ -10,3 +10,9 @@ end % foo bar ? 1 : 2 +% +foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ? break : baz +- +foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ? + break : + baz diff --git a/test/formatter/single_quotes_test.rb b/test/formatter/single_quotes_test.rb deleted file mode 100644 index 78f9ae3d..00000000 --- a/test/formatter/single_quotes_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require_relative "../test_helper" -require "syntax_tree/formatter/single_quotes" - -module SyntaxTree - class Formatter - 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_single_quote_in_string - assert_format("\"str'ing\"\n") - 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 deleted file mode 100644 index f6585772..00000000 --- a/test/formatter/trailing_comma_test.rb +++ /dev/null @@ -1,97 +0,0 @@ -# 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/language_server/inlay_hints_test.rb b/test/language_server/inlay_hints_test.rb new file mode 100644 index 00000000..f652f6d8 --- /dev/null +++ b/test/language_server/inlay_hints_test.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative "../test_helper" +require "syntax_tree/language_server" + +module SyntaxTree + class LanguageServer + class InlayHintsTest < Minitest::Test + def test_assignments_in_parameters + hints = find("def foo(a = b = c); end") + + assert_equal(1, hints.before.length) + assert_equal(1, hints.after.length) + end + + def test_operators_in_binaries + hints = find("1 + 2 * 3") + + assert_equal(1, hints.before.length) + assert_equal(1, hints.after.length) + end + + def test_binaries_in_assignments + hints = find("a = 1 + 2") + + assert_equal(1, hints.before.length) + assert_equal(1, hints.after.length) + end + + def test_nested_ternaries + hints = find("a ? b : c ? d : e") + + assert_equal(1, hints.before.length) + assert_equal(1, hints.after.length) + end + + def test_bare_rescue + hints = find("begin; rescue; end") + + assert_equal(1, hints.after.length) + end + + def test_unary_in_binary + hints = find("-a + b") + + assert_equal(1, hints.before.length) + assert_equal(1, hints.after.length) + end + + private + + def find(source) + InlayHints.find(SyntaxTree.parse(source)) + end + end + end +end diff --git a/test/language_server_test.rb b/test/language_server_test.rb new file mode 100644 index 00000000..f8a61003 --- /dev/null +++ b/test/language_server_test.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require_relative "test_helper" +require "syntax_tree/language_server" + +module SyntaxTree + class LanguageServerTest < Minitest::Test + class Initialize < Struct.new(:id) + def to_hash + { method: "initialize", id: id } + end + end + + class Shutdown + def to_hash + { method: "shutdown" } + end + end + + class TextDocumentDidOpen < Struct.new(:uri, :text) + def to_hash + { + method: "textDocument/didOpen", + params: { + textDocument: { + uri: uri, + text: text + } + } + } + end + end + + class TextDocumentDidChange < Struct.new(:uri, :text) + def to_hash + { + method: "textDocument/didChange", + params: { + textDocument: { + uri: uri + }, + contentChanges: [{ text: text }] + } + } + end + end + + class TextDocumentDidClose < Struct.new(:uri) + def to_hash + { + method: "textDocument/didClose", + params: { + textDocument: { + uri: uri + } + } + } + end + end + + class TextDocumentFormatting < Struct.new(:id, :uri) + def to_hash + { + method: "textDocument/formatting", + id: id, + params: { + textDocument: { + uri: uri + } + } + } + end + end + + class TextDocumentInlayHints < Struct.new(:id, :uri) + def to_hash + { + method: "textDocument/inlayHints", + id: id, + params: { + textDocument: { + uri: uri + } + } + } + end + end + + class SyntaxTreeVisualizing < Struct.new(:id, :uri) + def to_hash + { + method: "syntaxTree/visualizing", + id: id, + params: { + textDocument: { + uri: uri + } + } + } + end + end + + def test_formatting + messages = [ + Initialize.new(1), + TextDocumentDidOpen.new("file:///path/to/file.rb", "class Foo; end"), + TextDocumentDidChange.new("file:///path/to/file.rb", "class Bar; end"), + TextDocumentFormatting.new(2, "file:///path/to/file.rb"), + TextDocumentDidClose.new("file:///path/to/file.rb"), + Shutdown.new + ] + + case run_server(messages) + in [ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: [{ newText: new_text }] } + ] + assert_equal("class Bar\nend\n", new_text) + end + end + + def test_inlay_hints + messages = [ + Initialize.new(1), + TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY), + begin + 1 + 2 * 3 + rescue + end + RUBY + TextDocumentInlayHints.new(2, "file:///path/to/file.rb"), + Shutdown.new + ] + + case run_server(messages) + in [ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: { before:, after: } } + ] + assert_equal(1, before.length) + assert_equal(2, after.length) + end + end + + def test_visualizing + messages = [ + Initialize.new(1), + TextDocumentDidOpen.new("file:///path/to/file.rb", "1 + 2"), + SyntaxTreeVisualizing.new(2, "file:///path/to/file.rb"), + Shutdown.new + ] + + case run_server(messages) + in [{ id: 1, result: { capabilities: Hash } }, { id: 2, result: }] + assert_equal( + "(program (statements ((binary (int \"1\") + (int \"2\")))))\n", + result + ) + end + end + + def test_reading_file + Tempfile.open(%w[test- .rb]) do |file| + file.write("class Foo; end") + file.rewind + + messages = [ + Initialize.new(1), + TextDocumentFormatting.new(2, "file://#{file.path}"), + Shutdown.new + ] + + case run_server(messages) + in [ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: [{ newText: new_text }] } + ] + assert_equal("class Foo\nend\n", new_text) + end + end + end + + def test_bogus_request + assert_raises(ArgumentError) do + run_server([{ method: "textDocument/bogus" }]) + end + end + + private + + def write(content) + request = content.to_hash.merge(jsonrpc: "2.0").to_json + "Content-Length: #{request.bytesize}\r\n\r\n#{request}" + end + + def read(content) + [].tap do |messages| + while (headers = content.gets("\r\n\r\n")) + source = content.read(headers[/Content-Length: (\d+)/i, 1].to_i) + messages << JSON.parse(source, symbolize_names: true) + end + end + end + + def run_server(messages) + input = StringIO.new(messages.map { |message| write(message) }.join) + output = StringIO.new + + LanguageServer.new(input: input, output: output).run + read(output.tap(&:rewind)) + end + end +end diff --git a/test/location_test.rb b/test/location_test.rb new file mode 100644 index 00000000..2a697281 --- /dev/null +++ b/test/location_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class LocationTest < Minitest::Test + def test_lines + location = Location.fixed(line: 1, char: 0, column: 0) + location = location.to(Location.fixed(line: 3, char: 3, column: 3)) + + assert_equal(1..3, location.lines) + end + + def test_deconstruct + location = Location.fixed(line: 1, char: 0, column: 0) + + case location + in [start_line, 0, 0, *] + assert_equal(1, start_line) + end + end + + def test_deconstruct_keys + location = Location.fixed(line: 1, char: 0, column: 0) + + case location + in start_line: + assert_equal(1, start_line) + end + end + end +end diff --git a/test/node_test.rb b/test/node_test.rb index 6bde39bc..ffd00fa5 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -1032,6 +1032,20 @@ def test_multibyte_column_positions assert_node(Command, source, at: at) end + def test_root_class_raises_not_implemented_errors + { + accept: [nil], + child_nodes: [], + deconstruct: [], + deconstruct_keys: [[]], + format: [nil] + }.each do |method, arguments| + assert_raises(NotImplementedError) do + Node.new.public_send(method, *arguments) + end + end + end + private def location(lines: 1..1, chars: 0..0, columns: 0..0) diff --git a/test/parser_test.rb b/test/parser_test.rb index 8aadbfc2..b36c1a5f 100644 --- a/test/parser_test.rb +++ b/test/parser_test.rb @@ -30,5 +30,17 @@ def test_parses_ripper_methods # Finally, assert that we have no remaining events. assert_empty(events) end + + def test_errors_on_missing_token_with_location + assert_raises(Parser::ParseError) { SyntaxTree.parse("\"foo") } + end + + def test_errors_on_missing_token_without_location + assert_raises(Parser::ParseError) { SyntaxTree.parse(":\"foo") } + end + + def test_handles_strings_with_non_terminated_embedded_expressions + assert_raises(Parser::ParseError) { SyntaxTree.parse('"#{"') } + end end end diff --git a/test/plugin/single_quotes_test.rb b/test/plugin/single_quotes_test.rb new file mode 100644 index 00000000..719f33c1 --- /dev/null +++ b/test/plugin/single_quotes_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module SyntaxTree + class SingleQuotesTest < Minitest::Test + OPTIONS = Plugin.options("syntax_tree/plugin/single_quotes") + + 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_single_quote_in_string + assert_format("\"str'ing\"\n") + end + + def test_label + assert_format( + "{ foo => foo, :'bar' => bar }\n", + "{ foo => foo, \"bar\": bar }" + ) + end + + private + + def assert_format(expected, source = expected) + formatter = Formatter.new(source, [], **OPTIONS) + SyntaxTree.parse(source).format(formatter) + + formatter.flush + assert_equal(expected, formatter.output.join) + end + end +end diff --git a/test/plugin/trailing_comma_test.rb b/test/plugin/trailing_comma_test.rb new file mode 100644 index 00000000..ba9ad846 --- /dev/null +++ b/test/plugin/trailing_comma_test.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require_relative "../test_helper" + +module SyntaxTree + class TrailingCommaTest < Minitest::Test + OPTIONS = Plugin.options("syntax_tree/plugin/trailing_comma") + + 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 = Formatter.new(source, [], **OPTIONS) + SyntaxTree.parse(source).format(formatter) + + formatter.flush + assert_equal(expected, formatter.output.join) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index bb3ea67f..895fbc82 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -77,10 +77,30 @@ def assert_syntax_tree(node) end RUBY end + + Minitest::Test.include(self) end end -Minitest::Test.include(SyntaxTree::Assertions) +module SyntaxTree + module Plugin + # A couple of plugins modify the options hash on the formatter. They're + # modeled as files that should be required so that it's simple for the CLI + # and the library to use the same code path. In this case we're going to + # require the file for the plugin but ensure it doesn't make any lasting + # changes. + def self.options(path) + previous_options = SyntaxTree::Formatter::OPTIONS.dup + + begin + require path + SyntaxTree::Formatter::OPTIONS.dup + ensure + SyntaxTree::Formatter::OPTIONS.merge!(previous_options) + end + end + end +end # There are a bunch of fixtures defined in test/fixtures. They exercise every # possible combination of syntax that leads to variations in the types of nodes.