From 9cef7af6611b9d306f43f5b54fa381b61cc29ba4 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 28 Oct 2022 14:11:55 +0200 Subject: [PATCH 01/12] Make the test suite pass on TruffleRuby --- Rakefile | 8 +++++++- lib/syntax_tree/cli.rb | 6 +++--- test/cli_test.rb | 2 ++ test/location_test.rb | 12 ++++-------- test/node_test.rb | 7 +++---- test/test_helper.rb | 22 +++++++++++++--------- 6 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Rakefile b/Rakefile index 4973d45e..6de81bd8 100644 --- a/Rakefile +++ b/Rakefile @@ -7,7 +7,13 @@ require "syntax_tree/rake_tasks" Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" - t.test_files = FileList["test/**/*_test.rb"] + test_files = FileList["test/**/*_test.rb"] + if RUBY_ENGINE == "truffleruby" + # language_server.rb uses pattern matching + test_files -= FileList["test/language_server/*_test.rb"] + test_files -= FileList["test/language_server_test.rb"] + end + t.test_files = test_files end task default: :test diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index b847e059..be0bb793 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -192,9 +192,9 @@ 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 Program === program and expressions = program.statements.body and 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/test/cli_test.rb b/test/cli_test.rb index b4ef0afc..b5316d7f 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 @@ -172,6 +173,7 @@ def test_plugins def test_language_server prev_stdin = $stdin prev_stdout = $stdout + skip unless SUPPORTS_PATTERN_MATCHING request = { method: "shutdown" }.merge(jsonrpc: "2.0").to_json $stdin = 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/test_helper.rb b/test/test_helper.rb index 80e514f0..c421d8ee 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,6 +17,8 @@ require "pp" require "minitest/autorun" +SUPPORTS_PATTERN_MATCHING = RUBY_ENGINE != "truffleruby" + module SyntaxTree module Assertions class Recorder @@ -67,15 +69,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 SUPPORTS_PATTERN_MATCHING + # 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) From 0c8b5e3215d3c2a8f882c13c71f183f125f999db Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 28 Oct 2022 14:11:10 +0200 Subject: [PATCH 02/12] Replace pattern matching in lib/syntax_tree/pattern.rb * For performance and compatibility with Rubies not supporting it yet. --- lib/syntax_tree/pattern.rb | 61 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb index aa558361..15b6a0a9 100644 --- a/lib/syntax_tree/pattern.rb +++ b/lib/syntax_tree/pattern.rb @@ -84,11 +84,11 @@ def combine_or(left, right) end def compile_node(root) - case root - in AryPtn[constant:, requireds:, rest: nil, posts: []] + if AryPtn === root and root.rest.nil? and root.posts.empty? + constant = root.constant compiled_constant = compile_node(constant) if constant - preprocessed = requireds.map { |required| compile_node(required) } + preprocessed = root.requireds.map { |required| compile_node(required) } compiled_requireds = ->(node) do deconstructed = node.deconstruct @@ -104,34 +104,32 @@ def compile_node(root) 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) - clazz = SyntaxTree.const_get(value) + elsif Binary === root and root.operator == :| + combine_or(compile_node(root.left), compile_node(root.right)) + elsif Const === root and SyntaxTree.const_defined?(root.value) + clazz = SyntaxTree.const_get(root.value) ->(node) { node.is_a?(clazz) } - in Const[value:] if Object.const_defined?(value) - clazz = Object.const_get(value) + elsif Const === root and Object.const_defined?(root.value) + clazz = Object.const_get(root.value) ->(node) { node.is_a?(clazz) } - in ConstPathRef[ - parent: VarRef[value: Const[value: "SyntaxTree"]], constant: - ] - compile_node(constant) - in DynaSymbol[parts: []] + elsif ConstPathRef === root and VarRef === root.parent and Const === root.parent.value and root.parent.value.value == "SyntaxTree" + compile_node(root.constant) + elsif DynaSymbol === root and root.parts.empty? symbol = :"" ->(node) { node == symbol } - in DynaSymbol[parts: [TStringContent[value:]]] - symbol = value.to_sym + elsif DynaSymbol === root and parts = root.parts and parts.size == 1 and TStringContent === parts[0] + symbol = parts[0].value.to_sym ->(attribute) { attribute == value } - in HshPtn[constant:, keywords:, keyword_rest: nil] - compiled_constant = compile_node(constant) + elsif HshPtn === root and root.keyword_rest.nil? + compiled_constant = compile_node(root.constant) preprocessed = - keywords.to_h do |keyword, value| - raise NoMatchingPatternError unless keyword.is_a?(Label) + root.keywords.to_h do |keyword, value| + raise CompilationError, PP.pp(root, +"").chomp unless keyword.is_a?(Label) [keyword.value.chomp(":").to_sym, compile_node(value)] end @@ -148,25 +146,26 @@ def compile_node(root) else compiled_keywords end - in RegexpLiteral[parts: [TStringContent[value:]]] - regexp = /#{value}/ + elsif RegexpLiteral === root and parts = root.parts and parts.size == 1 and TStringContent === parts[0] + regexp = /#{parts[0].value}/ ->(attribute) { regexp.match?(attribute) } - in StringLiteral[parts: []] + elsif StringLiteral === root and root.parts.empty? ->(attribute) { attribute == "" } - in StringLiteral[parts: [TStringContent[value:]]] + elsif StringLiteral === root and parts = root.parts and parts.size == 1 and TStringContent === parts[0] + value = parts[0].value ->(attribute) { attribute == value } - in SymbolLiteral[value:] - symbol = value.value.to_sym + elsif SymbolLiteral === root + symbol = root.value.value.to_sym ->(attribute) { attribute == symbol } - in VarRef[value: Const => value] - compile_node(value) - in VarRef[value: Kw[value: "nil"]] + elsif VarRef === root and Const === root.value + compile_node(root.value) + elsif VarRef === root and Kw === root.value and root.value.value.nil? ->(attribute) { attribute.nil? } + else + raise CompilationError, PP.pp(root, +"").chomp end - rescue NoMatchingPatternError - raise CompilationError, PP.pp(root, +"").chomp end end end From 12f3e8691f192a2e20baad867d46f0c2e5832011 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 28 Oct 2022 15:05:27 +0200 Subject: [PATCH 03/12] Fix test for DynaSymbol --- lib/syntax_tree/pattern.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb index 15b6a0a9..f56e8a1b 100644 --- a/lib/syntax_tree/pattern.rb +++ b/lib/syntax_tree/pattern.rb @@ -123,7 +123,7 @@ def compile_node(root) elsif DynaSymbol === root and parts = root.parts and parts.size == 1 and TStringContent === parts[0] symbol = parts[0].value.to_sym - ->(attribute) { attribute == value } + ->(node) { node == symbol } elsif HshPtn === root and root.keyword_rest.nil? compiled_constant = compile_node(root.constant) From 48c659cfa5fe9b4c82939e4196e5aac8bf97f5e9 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 28 Oct 2022 15:06:21 +0200 Subject: [PATCH 04/12] Add TruffleRuby in CI --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d35471fa..7bbdedc7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,7 @@ jobs: - '3.0' - '3.1' - head + - truffleruby-head name: CI runs-on: ubuntu-latest env: From ab6e669635df9cfea52bdf98d8f2d72efb1099c0 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 28 Oct 2022 15:19:35 +0200 Subject: [PATCH 05/12] Run in --verbose mode to see progress in CI --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7bbdedc7..9f95cc9d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,6 +17,7 @@ jobs: runs-on: ubuntu-latest env: CI: true + TESTOPTS: --verbose steps: - uses: actions/checkout@master - uses: ruby/setup-ruby@v1 From 42bb2a44fc81a3e36c0506fc7d842258eae7a641 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 28 Oct 2022 15:21:05 +0200 Subject: [PATCH 06/12] Run bundle exec rake stree:write --- lib/syntax_tree/cli.rb | 3 ++- lib/syntax_tree/pattern.rb | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index be0bb793..518b58a6 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -193,7 +193,8 @@ def run(item) class Expr < Action def run(item) program = item.handler.parse(item.source) - if Program === program and expressions = program.statements.body and expressions.size == 1 + if Program === program and expressions = program.statements.body and + expressions.size == 1 puts expressions.first.construct_keys else warn("The input to `stree expr` must be a single expression.") diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb index f56e8a1b..c612c4ea 100644 --- a/lib/syntax_tree/pattern.rb +++ b/lib/syntax_tree/pattern.rb @@ -114,13 +114,16 @@ def compile_node(root) clazz = Object.const_get(root.value) ->(node) { node.is_a?(clazz) } - elsif ConstPathRef === root and VarRef === root.parent and Const === root.parent.value and root.parent.value.value == "SyntaxTree" + elsif ConstPathRef === root and VarRef === root.parent and + Const === root.parent.value and + root.parent.value.value == "SyntaxTree" compile_node(root.constant) elsif DynaSymbol === root and root.parts.empty? symbol = :"" ->(node) { node == symbol } - elsif DynaSymbol === root and parts = root.parts and parts.size == 1 and TStringContent === parts[0] + elsif DynaSymbol === root and parts = root.parts and parts.size == 1 and + TStringContent === parts[0] symbol = parts[0].value.to_sym ->(node) { node == symbol } @@ -129,7 +132,9 @@ def compile_node(root) preprocessed = root.keywords.to_h do |keyword, value| - raise CompilationError, PP.pp(root, +"").chomp unless keyword.is_a?(Label) + unless keyword.is_a?(Label) + raise CompilationError, PP.pp(root, +"").chomp + end [keyword.value.chomp(":").to_sym, compile_node(value)] end @@ -146,13 +151,15 @@ def compile_node(root) else compiled_keywords end - elsif RegexpLiteral === root and parts = root.parts and parts.size == 1 and TStringContent === parts[0] + elsif RegexpLiteral === root and parts = root.parts and + parts.size == 1 and TStringContent === parts[0] regexp = /#{parts[0].value}/ ->(attribute) { regexp.match?(attribute) } elsif StringLiteral === root and root.parts.empty? ->(attribute) { attribute == "" } - elsif StringLiteral === root and parts = root.parts and parts.size == 1 and TStringContent === parts[0] + elsif StringLiteral === root and parts = root.parts and + parts.size == 1 and TStringContent === parts[0] value = parts[0].value ->(attribute) { attribute == value } elsif SymbolLiteral === root From 570a2d134d74024ca17d557e843d5b6720c1e9dd Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Fri, 28 Oct 2022 15:33:54 +0200 Subject: [PATCH 07/12] Do not run idempotency_test.rb on TruffleRuby * It's too slow as it includes all Ruby files from stdlib, gems and more. --- test/idempotency_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb index 1f560db2..76116572 100644 --- a/test/idempotency_test.rb +++ b/test/idempotency_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -return unless ENV["CI"] +return unless ENV["CI"] and RUBY_ENGINE != "truffleruby" require_relative "test_helper" module SyntaxTree From b4f41326d9e2cac0af6ac4e222755ee8257e24be Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 28 Oct 2022 10:18:45 -0400 Subject: [PATCH 08/12] Fix up style violations --- .rubocop.yml | 3 + lib/syntax_tree/cli.rb | 4 +- lib/syntax_tree/pattern.rb | 271 ++++++++++++++++++++++++++----------- test/idempotency_test.rb | 2 +- 4 files changed, 196 insertions(+), 84 deletions(-) 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/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 518b58a6..62e8ab68 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -193,8 +193,8 @@ def run(item) class Expr < Action def run(item) program = item.handler.parse(item.source) - if Program === program and expressions = program.statements.body and - expressions.size == 1 + + 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.") diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb index c612c4ea..439d573f 100644 --- a/lib/syntax_tree/pattern.rb +++ b/lib/syntax_tree/pattern.rb @@ -75,103 +75,212 @@ 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) - if AryPtn === root and root.rest.nil? and root.posts.empty? - constant = root.constant - 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 - preprocessed = root.requireds.map { |required| compile_node(required) } + # 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 - compiled_requireds = ->(node) do - deconstructed = node.deconstruct + if parts.length == 1 && (part = parts.first) && part.is_a?(TStringContent) + part.value + end + end - deconstructed.length == preprocessed.length && - preprocessed - .zip(deconstructed) - .all? { |(matcher, value)| matcher.call(value) } - end + # in [foo, bar, baz] + def compile_aryptn(node) + compile_error(node) if !node.rest.nil? || node.posts.any? - if compiled_constant - combine_and(compiled_constant, compiled_requireds) - else - compiled_requireds - end - elsif Binary === root and root.operator == :| - combine_or(compile_node(root.left), compile_node(root.right)) - elsif Const === root and SyntaxTree.const_defined?(root.value) - clazz = SyntaxTree.const_get(root.value) - - ->(node) { node.is_a?(clazz) } - elsif Const === root and Object.const_defined?(root.value) - clazz = Object.const_get(root.value) - - ->(node) { node.is_a?(clazz) } - elsif ConstPathRef === root and VarRef === root.parent and - Const === root.parent.value and - root.parent.value.value == "SyntaxTree" - compile_node(root.constant) - elsif DynaSymbol === root and root.parts.empty? + constant = node.constant + compiled_constant = compile_node(constant) if constant + + 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) + + ->(other) { clazz === other } + elsif Object.const_defined?(value) + clazz = Object.const_get(value) + + ->(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 } - elsif DynaSymbol === root and parts = root.parts and parts.size == 1 and - TStringContent === parts[0] - symbol = parts[0].value.to_sym - - ->(node) { node == symbol } - elsif HshPtn === root and root.keyword_rest.nil? - compiled_constant = compile_node(root.constant) - - preprocessed = - root.keywords.to_h do |keyword, value| - unless keyword.is_a?(Label) - raise CompilationError, PP.pp(root, +"").chomp - end - [keyword.value.chomp(":").to_sym, compile_node(value)] - end - - compiled_keywords = ->(node) do - deconstructed = node.deconstruct_keys(preprocessed.keys) - - preprocessed.all? do |keyword, matcher| - matcher.call(deconstructed[keyword]) - end + ->(other) { symbol === other } + elsif (value = extract_string(node)) + symbol = value.to_sym + + ->(other) { symbol === other } + else + compile_error(root) + end + end + + # 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 = + 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 - elsif RegexpLiteral === root and parts = root.parts and - parts.size == 1 and TStringContent === parts[0] - regexp = /#{parts[0].value}/ - - ->(attribute) { regexp.match?(attribute) } - elsif StringLiteral === root and root.parts.empty? - ->(attribute) { attribute == "" } - elsif StringLiteral === root and parts = root.parts and - parts.size == 1 and TStringContent === parts[0] - value = parts[0].value - ->(attribute) { attribute == value } - elsif SymbolLiteral === root - symbol = root.value.value.to_sym - - ->(attribute) { attribute == symbol } - elsif VarRef === root and Const === root.value - compile_node(root.value) - elsif VarRef === root and Kw === root.value and root.value.value.nil? - ->(attribute) { attribute.nil? } + 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 === 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) { symbol === attribute } + end + + # in Foo + # in nil + def compile_var_ref(node) + value = node.value + + if value.is_a?(Const) + compile_node(value) + 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 - raise CompilationError, PP.pp(root, +"").chomp + compile_error(node) end end end diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb index 76116572..32d9d196 100644 --- a/test/idempotency_test.rb +++ b/test/idempotency_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -return unless ENV["CI"] and RUBY_ENGINE != "truffleruby" +return if !ENV["CI"] || RUBY_ENGINE == "truffleruby" require_relative "test_helper" module SyntaxTree From 7405a3a78e54e20b5f5859ed9e0d840c2be826f6 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 28 Oct 2022 10:33:45 -0400 Subject: [PATCH 09/12] Remove pattern matching entirely --- Rakefile | 8 +- lib/syntax_tree/language_server.rb | 81 ++++++++--- test/cli_test.rb | 1 - test/language_server_test.rb | 208 +++++++++++++---------------- test/test_helper.rb | 4 +- 5 files changed, 160 insertions(+), 142 deletions(-) diff --git a/Rakefile b/Rakefile index 6de81bd8..4973d45e 100644 --- a/Rakefile +++ b/Rakefile @@ -7,13 +7,7 @@ require "syntax_tree/rake_tasks" Rake::TestTask.new(:test) do |t| t.libs << "test" t.libs << "lib" - test_files = FileList["test/**/*_test.rb"] - if RUBY_ENGINE == "truffleruby" - # language_server.rb uses pattern matching - test_files -= FileList["test/language_server/*_test.rb"] - test_files -= FileList["test/language_server_test.rb"] - end - t.test_files = test_files + t.test_files = FileList["test/**/*_test.rb"] end task default: :test 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/test/cli_test.rb b/test/cli_test.rb index b5316d7f..c00fb338 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -173,7 +173,6 @@ def test_plugins def test_language_server prev_stdin = $stdin prev_stdout = $stdout - skip unless SUPPORTS_PATTERN_MATCHING request = { method: "shutdown" }.merge(jsonrpc: "2.0").to_json $stdin = 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/test_helper.rb b/test/test_helper.rb index c421d8ee..c46022ae 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -17,8 +17,6 @@ require "pp" require "minitest/autorun" -SUPPORTS_PATTERN_MATCHING = RUBY_ENGINE != "truffleruby" - module SyntaxTree module Assertions class Recorder @@ -69,7 +67,7 @@ def assert_syntax_tree(node) refute_includes(json, "#<") assert_equal(type, JSON.parse(json)["type"]) - if SUPPORTS_PATTERN_MATCHING + if RUBY_ENGINE != "truffleruby" # Get a match expression from the node, then assert that it can in fact # match the node. # rubocop:disable all From 57f5a98d807e261fc945b4c8bbb3ce6fd7f603ad Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 28 Oct 2022 10:43:28 -0400 Subject: [PATCH 10/12] Exit with exit status on rake --- lib/syntax_tree/rake/task.rb | 2 +- test/rake_test.rb | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) 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/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 From 15514e5d9f28d4859466ed61677390353f0d04bf Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 28 Oct 2022 10:52:10 -0400 Subject: [PATCH 11/12] Remove pattern matching from inlay hints --- lib/syntax_tree/language_server/inlay_hints.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 From 331172450c064ccf2bfd80e1bc1936d237e97fd0 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 28 Oct 2022 11:20:02 -0400 Subject: [PATCH 12/12] Bump to v4.3.0 --- CHANGELOG.md | 15 ++++++++++++++- Gemfile.lock | 2 +- lib/syntax_tree/version.rb | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) 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/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