diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d35471fa..9f95cc9d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,10 +12,12 @@ jobs: - '3.0' - '3.1' - head + - truffleruby-head name: CI runs-on: ubuntu-latest env: CI: true + TESTOPTS: --verbose steps: - uses: actions/checkout@master - uses: ruby/setup-ruby@v1 diff --git a/.rubocop.yml b/.rubocop.yml index c0892d8a..27efc39a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -46,6 +46,9 @@ Naming/MethodParameterName: Naming/RescuedExceptionsVariableName: PreferredName: error +Style/CaseEquality: + Enabled: false + Style/ExplicitBlockArgument: Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index bbaf044e..45a06c13 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] +## [4.3.0] - 2022-10-28 + +### Added + +- [#183](https://github.com/ruby-syntax-tree/syntax_tree/pull/183) - Support TruffleRuby by eliminating internal pattern matching in some places and stopping some tests from running in other places. +- [#184](https://github.com/ruby-syntax-tree/syntax_tree/pull/184) - Remove internal pattern matching entirely. + +### Changed + +- [#183](https://github.com/ruby-syntax-tree/syntax_tree/pull/183) - Pattern matching works against dynamic symbols now. +- [#184](https://github.com/ruby-syntax-tree/syntax_tree/pull/184) - Exit with the correct exit status within the rake tasks. + ## [4.2.0] - 2022-10-25 ### Added @@ -414,7 +426,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/v4.2.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.3.0...HEAD +[4.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.2.0...v4.3.0 [4.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.1.0...v4.2.0 [4.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.2...v4.1.0 [4.0.2]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.1...v4.0.2 diff --git a/Gemfile.lock b/Gemfile.lock index 339de160..25f461c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (4.2.0) + syntax_tree (4.3.0) prettier_print (>= 1.0.2) GEM diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index b847e059..62e8ab68 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -192,9 +192,10 @@ def run(item) # would match the first expression of the input given. class Expr < Action def run(item) - case item.handler.parse(item.source) - in Program[statements: Statements[body: [expression]]] - puts expression.construct_keys + program = item.handler.parse(item.source) + + if (expressions = program.statements.body) && expressions.size == 1 + puts expressions.first.construct_keys else warn("The input to `stree expr` must be a single expression.") exit(1) diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index d2714b5c..c2265c32 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -13,6 +13,50 @@ module SyntaxTree # stree lsp # class LanguageServer + # This is a small module that effectively mirrors pattern matching. We're + # using it so that we can support truffleruby without having to ignore the + # language server. + module Request + # Represents a hash pattern. + class Shape + attr_reader :values + + def initialize(values) + @values = values + end + + def ===(other) + values.all? do |key, value| + value == :any ? other.key?(key) : value === other[key] + end + end + end + + # Represents an array pattern. + class Tuple + attr_reader :values + + def initialize(values) + @values = values + end + + def ===(other) + values.each_with_index.all? { |value, index| value === other[index] } + end + end + + def self.[](value) + case value + when Array + Tuple.new(value.map { |child| self[child] }) + when Hash + Shape.new(value.transform_values { |child| self[child] }) + else + value + end + end + end + attr_reader :input, :output, :print_width def initialize( @@ -39,30 +83,33 @@ def run # stree-ignore case request - in { method: "initialize", id: } + when Request[method: "initialize", id: :any] store.clear - write(id: id, result: { capabilities: capabilities }) - in { method: "initialized" } + write(id: request[:id], result: { capabilities: capabilities }) + when Request[method: "initialized"] # ignored - in { method: "shutdown" } # tolerate missing ID to be a good citizen + when Request[method: "shutdown"] # tolerate missing ID to be a good citizen store.clear write(id: request[:id], result: {}) return - in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } } - store[uri] = text - in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } } - store[uri] = text - in { method: "textDocument/didClose", params: { textDocument: { uri: } } } - store.delete(uri) - in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } } + when Request[method: "textDocument/didChange", params: { textDocument: { uri: :any }, contentChanges: [{ text: :any }] }] + store[request.dig(:params, :textDocument, :uri)] = request.dig(:params, :contentChanges, 0, :text) + when Request[method: "textDocument/didOpen", params: { textDocument: { uri: :any, text: :any } }] + store[request.dig(:params, :textDocument, :uri)] = request.dig(:params, :textDocument, :text) + when Request[method: "textDocument/didClose", params: { textDocument: { uri: :any } }] + store.delete(request.dig(:params, :textDocument, :uri)) + when Request[method: "textDocument/formatting", id: :any, params: { textDocument: { uri: :any } }] + uri = request.dig(:params, :textDocument, :uri) contents = store[uri] - write(id: id, result: contents ? format(contents, uri.split(".").last) : nil) - in { method: "textDocument/inlayHint", id:, params: { textDocument: { uri: } } } + write(id: request[:id], result: contents ? format(contents, uri.split(".").last) : nil) + when Request[method: "textDocument/inlayHint", id: :any, params: { textDocument: { uri: :any } }] + uri = request.dig(:params, :textDocument, :uri) contents = store[uri] - write(id: id, result: contents ? inlay_hints(contents) : nil) - in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } } - write(id: id, result: PP.pp(SyntaxTree.parse(store[uri]), +"")) - in { method: %r{\$/.+} } + write(id: request[:id], result: contents ? inlay_hints(contents) : nil) + when Request[method: "syntaxTree/visualizing", id: :any, params: { textDocument: { uri: :any } }] + uri = request.dig(:params, :textDocument, :uri) + write(id: request[:id], result: PP.pp(SyntaxTree.parse(store[uri]), +"")) + when Request[method: %r{\$/.+}] # ignored else raise ArgumentError, "Unhandled: #{request}" diff --git a/lib/syntax_tree/language_server/inlay_hints.rb b/lib/syntax_tree/language_server/inlay_hints.rb index 12c10230..dfd63b8d 100644 --- a/lib/syntax_tree/language_server/inlay_hints.rb +++ b/lib/syntax_tree/language_server/inlay_hints.rb @@ -69,11 +69,10 @@ def visit_assign(node) # def visit_binary(node) case stack[-2] - in Assign | OpAssign + when Assign, OpAssign parentheses(node.location) - in Binary[operator: operator] if operator != node.operator - parentheses(node.location) - else + when Binary + parentheses(node.location) if stack[-2].operator != node.operator end super @@ -91,9 +90,8 @@ def visit_binary(node) # def visit_if_op(node) case stack[-2] - in Assign | Binary | IfOp | OpAssign + when Assign, Binary, IfOp, OpAssign parentheses(node.location) - else end super diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb index aa558361..439d573f 100644 --- a/lib/syntax_tree/pattern.rb +++ b/lib/syntax_tree/pattern.rb @@ -75,98 +75,213 @@ def compile private + # Shortcut for combining two procs into one that returns true if both return + # true. def combine_and(left, right) - ->(node) { left.call(node) && right.call(node) } + ->(other) { left.call(other) && right.call(other) } end + # Shortcut for combining two procs into one that returns true if either + # returns true. def combine_or(left, right) - ->(node) { left.call(node) || right.call(node) } + ->(other) { left.call(other) || right.call(other) } end - def compile_node(root) - case root - in AryPtn[constant:, requireds:, rest: nil, posts: []] - compiled_constant = compile_node(constant) if constant + # Raise an error because the given node is not supported. + def compile_error(node) + raise CompilationError, PP.pp(node, +"").chomp + end + + # There are a couple of nodes (string literals, dynamic symbols, and regexp) + # that contain list of parts. This can include plain string content, + # interpolated expressions, and interpolated variables. We only support + # plain string content, so this method will extract out the plain string + # content if it is the only element in the list. + def extract_string(node) + parts = node.parts - preprocessed = requireds.map { |required| compile_node(required) } + if parts.length == 1 && (part = parts.first) && part.is_a?(TStringContent) + part.value + end + end - compiled_requireds = ->(node) do - deconstructed = node.deconstruct + # in [foo, bar, baz] + def compile_aryptn(node) + compile_error(node) if !node.rest.nil? || node.posts.any? - deconstructed.length == preprocessed.length && - preprocessed - .zip(deconstructed) - .all? { |(matcher, value)| matcher.call(value) } - end + constant = node.constant + compiled_constant = compile_node(constant) if constant - if compiled_constant - combine_and(compiled_constant, compiled_requireds) - else - compiled_requireds - end - in Binary[left:, operator: :|, right:] - combine_or(compile_node(left), compile_node(right)) - in Const[value:] if SyntaxTree.const_defined?(value) + preprocessed = node.requireds.map { |required| compile_node(required) } + + compiled_requireds = ->(other) do + deconstructed = other.deconstruct + + deconstructed.length == preprocessed.length && + preprocessed + .zip(deconstructed) + .all? { |(matcher, value)| matcher.call(value) } + end + + if compiled_constant + combine_and(compiled_constant, compiled_requireds) + else + compiled_requireds + end + end + + # in foo | bar + def compile_binary(node) + compile_error(node) if node.operator != :| + + combine_or(compile_node(node.left), compile_node(node.right)) + end + + # in Ident + # in String + def compile_const(node) + value = node.value + + if SyntaxTree.const_defined?(value) clazz = SyntaxTree.const_get(value) - ->(node) { node.is_a?(clazz) } - in Const[value:] if Object.const_defined?(value) + ->(other) { clazz === other } + elsif Object.const_defined?(value) clazz = Object.const_get(value) - ->(node) { node.is_a?(clazz) } - in ConstPathRef[ - parent: VarRef[value: Const[value: "SyntaxTree"]], constant: - ] - compile_node(constant) - in DynaSymbol[parts: []] + ->(other) { clazz === other } + else + compile_error(node) + end + end + + # in SyntaxTree::Ident + def compile_const_path_ref(node) + parent = node.parent + compile_error(node) if !parent.is_a?(VarRef) || !parent.value.is_a?(Const) + + if parent.value.value == "SyntaxTree" + compile_node(node.constant) + else + compile_error(node) + end + end + + # in :"" + # in :"foo" + def compile_dyna_symbol(node) + if node.parts.empty? symbol = :"" - ->(node) { node == symbol } - in DynaSymbol[parts: [TStringContent[value:]]] + ->(other) { symbol === other } + elsif (value = extract_string(node)) symbol = value.to_sym - ->(attribute) { attribute == value } - in HshPtn[constant:, keywords:, keyword_rest: nil] - compiled_constant = compile_node(constant) - - preprocessed = - keywords.to_h do |keyword, value| - raise NoMatchingPatternError unless keyword.is_a?(Label) - [keyword.value.chomp(":").to_sym, compile_node(value)] - end + ->(other) { symbol === other } + else + compile_error(root) + end + end - compiled_keywords = ->(node) do - deconstructed = node.deconstruct_keys(preprocessed.keys) + # in Ident[value: String] + # in { value: String } + def compile_hshptn(node) + compile_error(node) unless node.keyword_rest.nil? + compiled_constant = compile_node(node.constant) if node.constant - preprocessed.all? do |keyword, matcher| - matcher.call(deconstructed[keyword]) - end + preprocessed = + node.keywords.to_h do |keyword, value| + compile_error(node) unless keyword.is_a?(Label) + [keyword.value.chomp(":").to_sym, compile_node(value)] end - if compiled_constant - combine_and(compiled_constant, compiled_keywords) - else - compiled_keywords + compiled_keywords = ->(other) do + deconstructed = other.deconstruct_keys(preprocessed.keys) + + preprocessed.all? do |keyword, matcher| + matcher.call(deconstructed[keyword]) end - in RegexpLiteral[parts: [TStringContent[value:]]] + end + + if compiled_constant + combine_and(compiled_constant, compiled_keywords) + else + compiled_keywords + end + end + + # in /foo/ + def compile_regexp_literal(node) + if (value = extract_string(node)) regexp = /#{value}/ - ->(attribute) { regexp.match?(attribute) } - in StringLiteral[parts: []] - ->(attribute) { attribute == "" } - in StringLiteral[parts: [TStringContent[value:]]] - ->(attribute) { attribute == value } - in SymbolLiteral[value:] - symbol = value.value.to_sym + ->(attribute) { regexp === attribute } + else + compile_error(node) + end + end + + # in "" + # in "foo" + def compile_string_literal(node) + if node.parts.empty? + ->(attribute) { "" === attribute } + elsif (value = extract_string(node)) + ->(attribute) { value === attribute } + else + compile_error(node) + end + end + + # in :+ + # in :foo + def compile_symbol_literal(node) + symbol = node.value.value.to_sym - ->(attribute) { attribute == symbol } - in VarRef[value: Const => value] + ->(attribute) { symbol === attribute } + end + + # in Foo + # in nil + def compile_var_ref(node) + value = node.value + + if value.is_a?(Const) compile_node(value) - in VarRef[value: Kw[value: "nil"]] - ->(attribute) { attribute.nil? } + elsif value.is_a?(Kw) && value.value.nil? + ->(attribute) { nil === attribute } + else + compile_error(node) + end + end + + # Compile any kind of node. Dispatch out to the individual compilation + # methods based on the type of node. + def compile_node(node) + case node + when AryPtn + compile_aryptn(node) + when Binary + compile_binary(node) + when Const + compile_const(node) + when ConstPathRef + compile_const_path_ref(node) + when DynaSymbol + compile_dyna_symbol(node) + when HshPtn + compile_hshptn(node) + when RegexpLiteral + compile_regexp_literal(node) + when StringLiteral + compile_string_literal(node) + when SymbolLiteral + compile_symbol_literal(node) + when VarRef + compile_var_ref(node) + else + compile_error(node) end - rescue NoMatchingPatternError - raise CompilationError, PP.pp(root, +"").chomp end end end diff --git a/lib/syntax_tree/rake/task.rb b/lib/syntax_tree/rake/task.rb index ea228e8f..e9a20433 100644 --- a/lib/syntax_tree/rake/task.rb +++ b/lib/syntax_tree/rake/task.rb @@ -78,7 +78,7 @@ def run_task arguments << "--ignore-files=#{ignore_files}" if ignore_files != "" - SyntaxTree::CLI.run(arguments + Array(source_files)) + abort if SyntaxTree::CLI.run(arguments + Array(source_files)) != 0 end end end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index 0b68a850..a12c472d 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "4.2.0" + VERSION = "4.3.0" end diff --git a/test/cli_test.rb b/test/cli_test.rb index b4ef0afc..c00fb338 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -148,6 +148,7 @@ def test_inline_script end def test_multiple_inline_scripts + skip if RUBY_ENGINE == "truffleruby" # Relies on a thread-safe StringIO stdio, = capture_io { SyntaxTree::CLI.run(%w[format -e 1+1 -e 2+2]) } assert_equal(["1 + 1", "2 + 2"], stdio.split("\n").sort) end diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb index 1f560db2..32d9d196 100644 --- a/test/idempotency_test.rb +++ b/test/idempotency_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -return unless ENV["CI"] +return if !ENV["CI"] || RUBY_ENGINE == "truffleruby" require_relative "test_helper" module SyntaxTree diff --git a/test/language_server_test.rb b/test/language_server_test.rb index 466bf737..8e1ed9a7 100644 --- a/test/language_server_test.rb +++ b/test/language_server_test.rb @@ -4,6 +4,7 @@ require "syntax_tree/language_server" module SyntaxTree + # stree-ignore class LanguageServerTest < Minitest::Test class Initialize < Struct.new(:id) def to_hash @@ -21,12 +22,7 @@ class TextDocumentDidOpen < Struct.new(:uri, :text) def to_hash { method: "textDocument/didOpen", - params: { - textDocument: { - uri: uri, - text: text - } - } + params: { textDocument: { uri: uri, text: text } } } end end @@ -36,9 +32,7 @@ def to_hash { method: "textDocument/didChange", params: { - textDocument: { - uri: uri - }, + textDocument: { uri: uri }, contentChanges: [{ text: text }] } } @@ -49,11 +43,7 @@ class TextDocumentDidClose < Struct.new(:uri) def to_hash { method: "textDocument/didClose", - params: { - textDocument: { - uri: uri - } - } + params: { textDocument: { uri: uri } } } end end @@ -63,11 +53,7 @@ def to_hash { method: "textDocument/formatting", id: id, - params: { - textDocument: { - uri: uri - } - } + params: { textDocument: { uri: uri } } } end end @@ -77,11 +63,7 @@ def to_hash { method: "textDocument/inlayHint", id: id, - params: { - textDocument: { - uri: uri - } - } + params: { textDocument: { uri: uri } } } end end @@ -91,75 +73,71 @@ def to_hash { method: "syntaxTree/visualizing", id: id, - params: { - textDocument: { - uri: uri - } - } + params: { textDocument: { uri: uri } } } end end def test_formatting - messages = [ + responses = run_server([ 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(3) - ] - - case run_server(messages) - in [ - { id: 1, result: { capabilities: Hash } }, - { id: 2, result: [{ newText: new_text }] }, - { id: 3, result: {} } - ] - assert_equal("class Bar\nend\n", new_text) - end + ]) + + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: [{ newText: :any }] }, + { id: 3, result: {} } + ]] + + assert_operator(shape, :===, responses) + assert_equal("class Bar\nend\n", responses.dig(1, :result, 0, :newText)) end def test_formatting_failure - messages = [ + responses = run_server([ Initialize.new(1), TextDocumentDidOpen.new("file:///path/to/file.rb", "<>"), TextDocumentFormatting.new(2, "file:///path/to/file.rb"), Shutdown.new(3) - ] - - case run_server(messages) - in [ - { id: 1, result: { capabilities: Hash } }, - { id: 2, result: }, - { id: 3, result: {} } - ] - assert_nil(result) - end + ]) + + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: :any }, + { id: 3, result: {} } + ]] + + assert_operator(shape, :===, responses) + assert_nil(responses.dig(1, :result)) end def test_formatting_print_width contents = "#{"a" * 40} + #{"b" * 40}\n" - messages = [ + responses = run_server([ Initialize.new(1), TextDocumentDidOpen.new("file:///path/to/file.rb", contents), TextDocumentFormatting.new(2, "file:///path/to/file.rb"), TextDocumentDidClose.new("file:///path/to/file.rb"), Shutdown.new(3) - ] - - case run_server(messages, print_width: 100) - in [ - { id: 1, result: { capabilities: Hash } }, - { id: 2, result: [{ newText: new_text }] }, - { id: 3, result: {} } - ] - assert_equal(contents, new_text) - end + ], print_width: 100) + + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: [{ newText: :any }] }, + { id: 3, result: {} } + ]] + + assert_operator(shape, :===, responses) + assert_equal(contents, responses.dig(1, :result, 0, :newText)) end def test_inlay_hint - messages = [ + responses = run_server([ Initialize.new(1), TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY), begin @@ -169,37 +147,37 @@ def test_inlay_hint RUBY TextDocumentInlayHint.new(2, "file:///path/to/file.rb"), Shutdown.new(3) - ] - - case run_server(messages) - in [ - { id: 1, result: { capabilities: Hash } }, - { id: 2, result: hints }, - { id: 3, result: {} } - ] - assert_equal(3, hints.length) - end + ]) + + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: :any }, + { id: 3, result: {} } + ]] + + assert_operator(shape, :===, responses) + assert_equal(3, responses.dig(1, :result).size) end def test_visualizing - messages = [ + responses = run_server([ Initialize.new(1), TextDocumentDidOpen.new("file:///path/to/file.rb", "1 + 2"), SyntaxTreeVisualizing.new(2, "file:///path/to/file.rb"), Shutdown.new(3) - ] - - case run_server(messages) - in [ - { id: 1, result: { capabilities: Hash } }, - { id: 2, result: }, - { id: 3, result: {} } - ] - assert_equal( - "(program (statements ((binary (int \"1\") + (int \"2\")))))\n", - result - ) - end + ]) + + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: :any }, + { id: 3, result: {} } + ]] + + assert_operator(shape, :===, responses) + assert_equal( + "(program (statements ((binary (int \"1\") + (int \"2\")))))\n", + responses.dig(1, :result) + ) end def test_reading_file @@ -207,20 +185,20 @@ def test_reading_file file.write("class Foo; end") file.rewind - messages = [ + responses = run_server([ Initialize.new(1), TextDocumentFormatting.new(2, "file://#{file.path}"), Shutdown.new(3) - ] - - case run_server(messages) - in [ - { id: 1, result: { capabilities: Hash } }, - { id: 2, result: [{ newText: new_text }] }, - { id: 3, result: {} } - ] - assert_equal("class Foo\nend\n", new_text) - end + ]) + + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: [{ newText: :any }] }, + { id: 3, result: {} } + ]] + + assert_operator(shape, :===, responses) + assert_equal("class Foo\nend\n", responses.dig(1, :result, 0, :newText)) end end @@ -231,29 +209,30 @@ def test_bogus_request end def test_clean_shutdown - messages = [Initialize.new(1), Shutdown.new(2)] + responses = run_server([Initialize.new(1), Shutdown.new(2)]) - case run_server(messages) - in [{ id: 1, result: { capabilities: Hash } }, { id: 2, result: {} }] - assert_equal(true, true) - end + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: {} } + ]] + + assert_operator(shape, :===, responses) end def test_file_that_does_not_exist - messages = [ + responses = run_server([ Initialize.new(1), TextDocumentFormatting.new(2, "file:///path/to/file.rb"), Shutdown.new(3) - ] - - case run_server(messages) - in [ - { id: 1, result: { capabilities: Hash } }, - { id: 2, result: nil }, - { id: 3, result: {} } - ] - assert_equal(true, true) - end + ]) + + shape = LanguageServer::Request[[ + { id: 1, result: { capabilities: Hash } }, + { id: 2, result: :any }, + { id: 3, result: {} } + ]] + + assert_operator(shape, :===, responses) end private @@ -281,6 +260,7 @@ def run_server(messages, print_width: DEFAULT_PRINT_WIDTH) output: output, print_width: print_width ).run + read(output.tap(&:rewind)) end end diff --git a/test/location_test.rb b/test/location_test.rb index 2a697281..26831fb1 100644 --- a/test/location_test.rb +++ b/test/location_test.rb @@ -14,19 +14,15 @@ def test_lines 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 + assert_equal(1, location.start_line) + assert_equal(0, location.start_char) + assert_equal(0, location.start_column) 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 + assert_equal(1, location.start_line) end end end diff --git a/test/node_test.rb b/test/node_test.rb index 1a5af125..ce26f9ea 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -759,10 +759,9 @@ def test_program program = parser.parse refute(parser.error?) - case program - in statements: { body: [statement] } - assert_kind_of(VCall, statement) - end + statements = program.statements.body + assert_equal 1, statements.size + assert_kind_of(VCall, statements.first) json = JSON.parse(program.to_json) io = StringIO.new diff --git a/test/rake_test.rb b/test/rake_test.rb index 57364859..bd315cc6 100644 --- a/test/rake_test.rb +++ b/test/rake_test.rb @@ -6,30 +6,36 @@ module SyntaxTree module Rake class CheckTaskTest < Minitest::Test - Invoke = Struct.new(:args) + Invocation = 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) + invocation = invoke("stree:check") + assert_equal(["check", source_files], invocation.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 + invocation = invoke("stree:write") + assert_equal(["write", source_files], invocation.args) + end - assert_equal(["write", source_files], invoke.args) + private + + def invoke(task_name) + invocation = nil + stub = ->(args) { invocation = Invocation.new(args) } + + begin + SyntaxTree::CLI.stub(:run, stub) { ::Rake::Task[task_name].invoke } + flunk + rescue SystemExit + invocation + end end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 80e514f0..c46022ae 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -67,15 +67,17 @@ def assert_syntax_tree(node) refute_includes(json, "#<") assert_equal(type, JSON.parse(json)["type"]) - # Get a match expression from the node, then assert that it can in fact - # match the node. - # rubocop:disable all - assert(eval(<<~RUBY)) - case node - in #{node.construct_keys} - true - end - RUBY + if RUBY_ENGINE != "truffleruby" + # Get a match expression from the node, then assert that it can in fact + # match the node. + # rubocop:disable all + assert(eval(<<~RUBY)) + case node + in #{node.construct_keys} + true + end + RUBY + end end Minitest::Test.include(self)