From 528662658ec016c41fd4e510d3f180cf22b81783 Mon Sep 17 00:00:00 2001 From: Nolan Date: Sat, 4 Mar 2023 21:21:49 -0800 Subject: [PATCH 01/23] List editor support for Emacs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 03942d46..aa57eabf 100644 --- a/README.md +++ b/README.md @@ -788,6 +788,7 @@ inherit_gem: * [Neovim](https://neovim.io/) - [neovim/nvim-lspconfig](https://github.com/neovim/nvim-lspconfig). * [Vim](https://www.vim.org/) - [dense-analysis/ale](https://github.com/dense-analysis/ale). * [VSCode](https://code.visualstudio.com/) - [ruby-syntax-tree/vscode-syntax-tree](https://github.com/ruby-syntax-tree/vscode-syntax-tree). +* [Emacs](https://www.gnu.org/software/emacs/) - [emacs-format-all-the-code](https://github.com/lassik/emacs-format-all-the-code). ## Contributing From a5a071091ccfd7cbd045b4998d1321fc6389d996 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 5 Mar 2023 13:06:38 -0500 Subject: [PATCH 02/23] Capture alias methods in index --- lib/syntax_tree/index.rb | 47 +++++++++++++++++++++++++++++++++++++++- test/index_test.rb | 7 ++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index c6973847..4e84ab2a 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -68,6 +68,19 @@ def initialize(nesting, name, location, comments) end end + # This entry represents a method definition that was created using the alias + # keyword. + class AliasMethodDefinition + attr_reader :nesting, :name, :location, :comments + + def initialize(nesting, name, location, comments) + @nesting = nesting + @name = name + @location = location + @comments = comments + end + end + # When you're using the instruction sequence backend, this class is used to # lazily parse comments out of the source code. class FileComments @@ -297,7 +310,7 @@ def index_iseq(iseq, file_comments) EntryComments.new(file_comments, location) ) when :definesmethod - if current_iseq[13][index - 1] != [:putself] + if insns[index - 1] != [:putself] raise NotImplementedError, "singleton method with non-self receiver" end @@ -309,6 +322,24 @@ def index_iseq(iseq, file_comments) location, EntryComments.new(file_comments, location) ) + when :opt_send_without_block, :send + if insn[1][:mid] == :"core#set_method_alias" + # Now we have to validate that the alias is happening with a + # non-interpolated value. To do this we'll match the specific + # pattern we're expecting. + values = insns[(index - 4)...index].map { |insn| insn.is_a?(Array) ? insn[0] : insn } + next if values != %i[putspecialobject putspecialobject putobject putobject] + + # Now that we know it's in the structure we want it, we can use + # the values of the putobject to determine the alias. + location = Location.new(line, 0) + results << AliasMethodDefinition.new( + current_nesting, + insns[index - 2][1], + location, + EntryComments.new(file_comments, location) + ) + end end end end @@ -331,6 +362,20 @@ def initialize end visit_methods do + def visit_alias(node) + if node.left.is_a?(SymbolLiteral) && node.right.is_a?(SymbolLiteral) + location = + Location.new(node.location.start_line, node.location.start_column) + + results << AliasMethodDefinition.new( + nesting.dup, + node.left.value.value.to_sym, + location, + comments_for(node) + ) + end + end + def visit_class(node) names = visit(node.constant) nesting << names diff --git a/test/index_test.rb b/test/index_test.rb index 60c51d9d..0813dc02 100644 --- a/test/index_test.rb +++ b/test/index_test.rb @@ -139,6 +139,13 @@ def test_singleton_method_comments end end + def test_alias_method + index_each("alias foo bar") do |entry| + assert_equal :foo, entry.name + assert_empty entry.nesting + end + end + def test_this_file entries = Index.index_file(__FILE__, backend: Index::ParserBackend.new) From 31e4a4724c495017f65d674a5211ddb9cb7349e9 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 5 Mar 2023 16:35:03 -0500 Subject: [PATCH 03/23] Index attr_readers --- lib/syntax_tree/index.rb | 83 +++++++++++++++++++++++++++++++++------- test/index_test.rb | 7 ++++ 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index 4e84ab2a..f0788619 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -323,7 +323,34 @@ def index_iseq(iseq, file_comments) EntryComments.new(file_comments, location) ) when :opt_send_without_block, :send - if insn[1][:mid] == :"core#set_method_alias" + case insn[1][:mid] + when :attr_reader + # We're going to scan backward finding symbols until we hit a + # different instruction. We'll then use that to determine the + # receiver. It needs to be self if we're going to understand it. + names = [] + current = index - 1 + + while current >= 0 && names.length < insn[1][:orig_argc] + if insns[current].is_a?(Array) && insns[current][0] == :putobject + names.unshift(insns[current][1]) + end + + current -= 1 + end + + next if insns[current] != [:putself] + + location = Location.new(line, 0) + names.each do |name| + results << MethodDefinition.new( + current_nesting, + name, + location, + EntryComments.new(file_comments, location) + ) + end + when :"core#set_method_alias" # Now we have to validate that the alias is happening with a # non-interpolated value. To do this we'll match the specific # pattern we're expecting. @@ -352,6 +379,20 @@ def index_iseq(iseq, file_comments) # It is not as fast as using the instruction sequences directly, but is # supported on all runtimes. class ParserBackend + class ConstantNameVisitor < Visitor + def visit_const_ref(node) + [node.constant.value.to_sym] + end + + def visit_const_path_ref(node) + visit(node.parent) << node.constant.value.to_sym + end + + def visit_var_ref(node) + [node.value.value.to_sym] + end + end + class IndexVisitor < Visitor attr_reader :results, :nesting, :statements @@ -374,10 +415,12 @@ def visit_alias(node) comments_for(node) ) end + + super end def visit_class(node) - names = visit(node.constant) + names = node.constant.accept(ConstantNameVisitor.new) nesting << names location = @@ -385,7 +428,7 @@ def visit_class(node) superclass = if node.superclass - visited = visit(node.superclass) + visited = node.superclass.accept(ConstantNameVisitor.new) if visited == [[]] raise NotImplementedError, "superclass with non constant path" @@ -408,12 +451,24 @@ def visit_class(node) nesting.pop end - def visit_const_ref(node) - [node.constant.value.to_sym] - end + def visit_command(node) + if node.message.value == "attr_reader" + location = + Location.new(node.location.start_line, node.location.start_column) + + node.arguments.parts.each do |argument| + next unless argument.is_a?(SymbolLiteral) + + results << MethodDefinition.new( + nesting.dup, + argument.value.value.to_sym, + location, + comments_for(node) + ) + end + end - def visit_const_path_ref(node) - visit(node.parent) << node.constant.value.to_sym + super end def visit_def(node) @@ -436,10 +491,12 @@ def visit_def(node) comments_for(node) ) end + + super end def visit_module(node) - names = visit(node.constant) + names = node.constant.accept(ConstantNameVisitor.new) nesting << names location = @@ -465,10 +522,6 @@ def visit_statements(node) @statements = node super end - - def visit_var_ref(node) - [node.value.value.to_sym] - end end private @@ -478,8 +531,10 @@ def comments_for(node) body = statements.body line = node.location.start_line - 1 - index = body.index(node) - 1 + index = body.index(node) + return comments if index.nil? + index -= 1 while index >= 0 && body[index].is_a?(Comment) && (line - body[index].location.start_line < 2) comments.unshift(body[index].value) diff --git a/test/index_test.rb b/test/index_test.rb index 0813dc02..41c9495f 100644 --- a/test/index_test.rb +++ b/test/index_test.rb @@ -146,6 +146,13 @@ def test_alias_method end end + def test_attr_reader + index_each("attr_reader :foo") do |entry| + assert_equal :foo, entry.name + assert_empty entry.nesting + end + end + def test_this_file entries = Index.index_file(__FILE__, backend: Index::ParserBackend.new) From ee2db3ff99a68756d10fc7eb522a11a7c7dfe5bf Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 5 Mar 2023 17:07:37 -0500 Subject: [PATCH 04/23] attr_writer and attr_accessor --- lib/syntax_tree/index.rb | 76 +++++++++++++++++++++++++--------------- test/index_test.rb | 14 ++++++++ 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index f0788619..ad090f95 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -220,6 +220,22 @@ def find_constant_path(insns, index) end end + def find_attr_arguments(insns, index) + orig_argc = insns[index][1][:orig_argc] + names = [] + + current = index - 1 + while current >= 0 && names.length < orig_argc + if insns[current].is_a?(Array) && insns[current][0] == :putobject + names.unshift(insns[current][1]) + end + + current -= 1 + end + + names if insns[current] == [:putself] && names.length == orig_argc + end + def index_iseq(iseq, file_comments) results = [] queue = [[iseq, []]] @@ -324,31 +340,29 @@ def index_iseq(iseq, file_comments) ) when :opt_send_without_block, :send case insn[1][:mid] - when :attr_reader - # We're going to scan backward finding symbols until we hit a - # different instruction. We'll then use that to determine the - # receiver. It needs to be self if we're going to understand it. - names = [] - current = index - 1 - - while current >= 0 && names.length < insn[1][:orig_argc] - if insns[current].is_a?(Array) && insns[current][0] == :putobject - names.unshift(insns[current][1]) - end - - current -= 1 - end - - next if insns[current] != [:putself] + when :attr_reader, :attr_writer, :attr_accessor + names = find_attr_arguments(insns, index) + next unless names location = Location.new(line, 0) names.each do |name| - results << MethodDefinition.new( - current_nesting, - name, - location, - EntryComments.new(file_comments, location) - ) + if insn[1][:mid] != :attr_writer + results << MethodDefinition.new( + current_nesting, + name, + location, + EntryComments.new(file_comments, location) + ) + end + + if insn[1][:mid] != :attr_reader + results << MethodDefinition.new( + current_nesting, + :"#{name}=", + location, + EntryComments.new(file_comments, location) + ) + end end when :"core#set_method_alias" # Now we have to validate that the alias is happening with a @@ -452,19 +466,23 @@ def visit_class(node) end def visit_command(node) - if node.message.value == "attr_reader" + case node.message.value + when "attr_reader", "attr_writer", "attr_accessor" + comments = comments_for(node) location = Location.new(node.location.start_line, node.location.start_column) node.arguments.parts.each do |argument| next unless argument.is_a?(SymbolLiteral) + name = argument.value.value.to_sym - results << MethodDefinition.new( - nesting.dup, - argument.value.value.to_sym, - location, - comments_for(node) - ) + if node.message.value != "attr_writer" + results << MethodDefinition.new(nesting.dup, name, location, comments) + end + + if node.message.value != "attr_reader" + results << MethodDefinition.new(nesting.dup, :"#{name}=", location, comments) + end end end diff --git a/test/index_test.rb b/test/index_test.rb index 41c9495f..42da9704 100644 --- a/test/index_test.rb +++ b/test/index_test.rb @@ -153,6 +153,20 @@ def test_attr_reader end end + def test_attr_writer + index_each("attr_writer :foo") do |entry| + assert_equal :foo=, entry.name + assert_empty entry.nesting + end + end + + def test_attr_accessor + index_each("attr_accessor :foo") do |entry| + assert_equal :foo=, entry.name + assert_empty entry.nesting + end + end + def test_this_file entries = Index.index_file(__FILE__, backend: Index::ParserBackend.new) From f712366084241bd7c0ab38da768f3d56f5705399 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 5 Mar 2023 17:30:51 -0500 Subject: [PATCH 05/23] Constant definitions --- lib/syntax_tree/index.rb | 44 +++++++++++++++++++++++++++++++++++++++- test/index_test.rb | 7 +++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index ad090f95..35dbb898 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -31,6 +31,18 @@ def initialize(nesting, name, superclass, location, comments) end end + # This entry represents a constant assignment. + class ConstantDefinition + attr_reader :nesting, :name, :location, :comments + + def initialize(nesting, name, location, comments) + @nesting = nesting + @name = name + @location = location + @comments = comments + end + end + # This entry represents a module definition using the module keyword. class ModuleDefinition attr_reader :nesting, :name, :location, :comments @@ -191,7 +203,7 @@ def location_for(iseq) end def find_constant_path(insns, index) - index -= 1 while insns[index].is_a?(Integer) + index -= 1 while index >= 0 && (insns[index].is_a?(Integer) || (insns[index].is_a?(Array) && %i[swap topn].include?(insns[index][0]))) insn = insns[index] if insn.is_a?(Array) && insn[0] == :opt_getconstant_path @@ -338,6 +350,20 @@ def index_iseq(iseq, file_comments) location, EntryComments.new(file_comments, location) ) + when :setconstant + next_nesting = current_nesting.dup + name = insn[1] + + _, nesting = find_constant_path(insns, index - 1) + next_nesting << nesting if nesting.any? + + location = Location.new(line, 0) + results << ConstantDefinition.new( + next_nesting, + name, + location, + EntryComments.new(file_comments, location) + ) when :opt_send_without_block, :send case insn[1][:mid] when :attr_reader, :attr_writer, :attr_accessor @@ -433,6 +459,22 @@ def visit_alias(node) super end + def visit_assign(node) + if node.target.is_a?(VarField) && node.target.value.is_a?(Const) + location = + Location.new(node.location.start_line, node.location.start_column) + + results << ConstantDefinition.new( + nesting.dup, + node.target.value.value.to_sym, + location, + comments_for(node) + ) + end + + super + end + def visit_class(node) names = node.constant.accept(ConstantNameVisitor.new) nesting << names diff --git a/test/index_test.rb b/test/index_test.rb index 42da9704..855e36ec 100644 --- a/test/index_test.rb +++ b/test/index_test.rb @@ -167,6 +167,13 @@ def test_attr_accessor end end + def test_constant + index_each("FOO = 1") do |entry| + assert_equal :FOO, entry.name + assert_empty entry.nesting + end + end + def test_this_file entries = Index.index_file(__FILE__, backend: Index::ParserBackend.new) From 474931b89d847f17b40f9df8e942c26fb927b539 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 5 Mar 2023 19:39:20 -0500 Subject: [PATCH 06/23] Correctly set singleton method status --- lib/syntax_tree/index.rb | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index 35dbb898..c5945470 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -248,6 +248,16 @@ def find_attr_arguments(insns, index) names if insns[current] == [:putself] && names.length == orig_argc end + def method_definition(nesting, name, location, file_comments) + comments = EntryComments.new(file_comments, location) + + if nesting.last == [:singletonclass] + SingletonMethodDefinition.new(nesting[0...-1], name, location, comments) + else + MethodDefinition.new(nesting, name, location, comments) + end + end + def index_iseq(iseq, file_comments) results = [] queue = [[iseq, []]] @@ -331,12 +341,7 @@ def index_iseq(iseq, file_comments) queue << [class_iseq, next_nesting] when :definemethod location = location_for(insn[2]) - results << MethodDefinition.new( - current_nesting, - insn[1], - location, - EntryComments.new(file_comments, location) - ) + results << method_definition(current_nesting, insn[1], location, file_comments) when :definesmethod if insns[index - 1] != [:putself] raise NotImplementedError, @@ -373,21 +378,11 @@ def index_iseq(iseq, file_comments) location = Location.new(line, 0) names.each do |name| if insn[1][:mid] != :attr_writer - results << MethodDefinition.new( - current_nesting, - name, - location, - EntryComments.new(file_comments, location) - ) + results << method_definition(current_nesting, name, location, file_comments) end if insn[1][:mid] != :attr_reader - results << MethodDefinition.new( - current_nesting, - :"#{name}=", - location, - EntryComments.new(file_comments, location) - ) + results << method_definition(current_nesting, :"#{name}=", location, file_comments) end end when :"core#set_method_alias" From eeea72003fd36ad4e72f3fc6339995b1186ffb86 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Sun, 5 Mar 2023 19:46:47 -0500 Subject: [PATCH 07/23] Explicitly specify that locations can have :unknown columns --- lib/syntax_tree/index.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index c5945470..7865a949 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -297,8 +297,8 @@ def index_iseq(iseq, file_comments) find_constant_path(insns, index - 1) if superclass.empty? - raise NotImplementedError, - "superclass with non constant path on line #{line}" + warn("superclass with non constant path on line #{line}") + next end end @@ -316,8 +316,8 @@ def index_iseq(iseq, file_comments) # defined on self. We could, but it would require more # emulation. if insns[index - 2] != [:putself] - raise NotImplementedError, - "singleton class with non-self receiver" + warn("singleton class with non-self receiver") + next end elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0 location = location_for(class_iseq) @@ -344,8 +344,8 @@ def index_iseq(iseq, file_comments) results << method_definition(current_nesting, insn[1], location, file_comments) when :definesmethod if insns[index - 1] != [:putself] - raise NotImplementedError, - "singleton method with non-self receiver" + warn("singleton method with non-self receiver") + next end location = location_for(insn[2]) @@ -362,7 +362,7 @@ def index_iseq(iseq, file_comments) _, nesting = find_constant_path(insns, index - 1) next_nesting << nesting if nesting.any? - location = Location.new(line, 0) + location = Location.new(line, :unknown) results << ConstantDefinition.new( next_nesting, name, @@ -375,7 +375,7 @@ def index_iseq(iseq, file_comments) names = find_attr_arguments(insns, index) next unless names - location = Location.new(line, 0) + location = Location.new(line, :unknown) names.each do |name| if insn[1][:mid] != :attr_writer results << method_definition(current_nesting, name, location, file_comments) @@ -394,7 +394,7 @@ def index_iseq(iseq, file_comments) # Now that we know it's in the structure we want it, we can use # the values of the putobject to determine the alias. - location = Location.new(line, 0) + location = Location.new(line, :unknown) results << AliasMethodDefinition.new( current_nesting, insns[index - 2][1], From c187683d70e70c8ea4a5377b4d8e407690f5dc9f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 6 Mar 2023 10:52:47 -0500 Subject: [PATCH 08/23] CTags CLI action --- lib/syntax_tree/cli.rb | 84 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index cbe10446..9243d3bf 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -154,6 +154,88 @@ def failure end end + # An action of the CLI that generates ctags for the given source. + class CTags < Action + attr_reader :entries + + def initialize(options) + super(options) + @entries = [] + end + + def run(item) + lines = item.source.lines(chomp: true) + + SyntaxTree.index(item.source).each do |entry| + line = lines[entry.location.line - 1] + pattern = "/^#{line.gsub("\\", "\\\\\\\\").gsub("/", "\\/")}$/;\"" + + entries << + case entry + when SyntaxTree::Index::ModuleDefinition + parts = [entry.name, item.filepath, pattern, "m"] + + if entry.nesting != [[entry.name]] + parts << "class:#{entry.nesting.flatten.tap(&:pop).join(".")}" + end + + parts.join("\t") + when SyntaxTree::Index::ClassDefinition + parts = [entry.name, item.filepath, pattern, "c"] + + if entry.nesting != [[entry.name]] + parts << "class:#{entry.nesting.flatten.tap(&:pop).join(".")}" + end + + unless entry.superclass.empty? + inherits = entry.superclass.join(".").delete_prefix(".") + parts << "inherits:#{inherits}" + end + + parts.join("\t") + when SyntaxTree::Index::MethodDefinition + parts = [entry.name, item.filepath, pattern, "f"] + + unless entry.nesting.empty? + parts << "class:#{entry.nesting.flatten.join(".")}" + end + + parts.join("\t") + when SyntaxTree::Index::SingletonMethodDefinition + parts = [entry.name, item.filepath, pattern, "F"] + + unless entry.nesting.empty? + parts << "class:#{entry.nesting.flatten.join(".")}" + end + + parts.join("\t") + when SyntaxTree::Index::AliasMethodDefinition + parts = [entry.name, item.filepath, pattern, "a"] + + unless entry.nesting.empty? + parts << "class:#{entry.nesting.flatten.join(".")}" + end + + parts.join("\t") + when SyntaxTree::Index::ConstantDefinition + parts = [entry.name, item.filepath, pattern, "C"] + + unless entry.nesting.empty? + parts << "class:#{entry.nesting.flatten.join(".")}" + end + + parts.join("\t") + end + end + end + + def success + puts("!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/") + puts("!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/") + entries.sort.each { |entry| puts(entry) } + end + end + # An action of the CLI that formats the source twice to check if the first # format is not idempotent. class Debug < Action @@ -488,6 +570,8 @@ def run(argv) AST.new(options) when "c", "check" Check.new(options) + when "ctags" + CTags.new(options) when "debug" Debug.new(options) when "doc" From dea5da2527bc8d23500ee517cb9700226cdf7c60 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 6 Mar 2023 10:53:45 -0500 Subject: [PATCH 09/23] Reformat --- lib/syntax_tree/cli.rb | 15 ++++---- lib/syntax_tree/index.rb | 76 +++++++++++++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 9243d3bf..02f8f55d 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -166,12 +166,13 @@ def initialize(options) def run(item) lines = item.source.lines(chomp: true) - SyntaxTree.index(item.source).each do |entry| - line = lines[entry.location.line - 1] - pattern = "/^#{line.gsub("\\", "\\\\\\\\").gsub("/", "\\/")}$/;\"" + SyntaxTree + .index(item.source) + .each do |entry| + line = lines[entry.location.line - 1] + pattern = "/^#{line.gsub("\\", "\\\\\\\\").gsub("/", "\\/")}$/;\"" - entries << - case entry + entries << case entry when SyntaxTree::Index::ModuleDefinition parts = [entry.name, item.filepath, pattern, "m"] @@ -230,7 +231,9 @@ def run(item) end def success - puts("!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/") + puts( + "!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/" + ) puts("!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/") entries.sort.each { |entry| puts(entry) } end diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index 7865a949..fef97be4 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -203,7 +203,14 @@ def location_for(iseq) end def find_constant_path(insns, index) - index -= 1 while index >= 0 && (insns[index].is_a?(Integer) || (insns[index].is_a?(Array) && %i[swap topn].include?(insns[index][0]))) + index -= 1 while index >= 0 && + ( + insns[index].is_a?(Integer) || + ( + insns[index].is_a?(Array) && + %i[swap topn].include?(insns[index][0]) + ) + ) insn = insns[index] if insn.is_a?(Array) && insn[0] == :opt_getconstant_path @@ -252,7 +259,12 @@ def method_definition(nesting, name, location, file_comments) comments = EntryComments.new(file_comments, location) if nesting.last == [:singletonclass] - SingletonMethodDefinition.new(nesting[0...-1], name, location, comments) + SingletonMethodDefinition.new( + nesting[0...-1], + name, + location, + comments + ) else MethodDefinition.new(nesting, name, location, comments) end @@ -341,7 +353,12 @@ def index_iseq(iseq, file_comments) queue << [class_iseq, next_nesting] when :definemethod location = location_for(insn[2]) - results << method_definition(current_nesting, insn[1], location, file_comments) + results << method_definition( + current_nesting, + insn[1], + location, + file_comments + ) when :definesmethod if insns[index - 1] != [:putself] warn("singleton method with non-self receiver") @@ -378,19 +395,35 @@ def index_iseq(iseq, file_comments) location = Location.new(line, :unknown) names.each do |name| if insn[1][:mid] != :attr_writer - results << method_definition(current_nesting, name, location, file_comments) + results << method_definition( + current_nesting, + name, + location, + file_comments + ) end if insn[1][:mid] != :attr_reader - results << method_definition(current_nesting, :"#{name}=", location, file_comments) + results << method_definition( + current_nesting, + :"#{name}=", + location, + file_comments + ) end end when :"core#set_method_alias" # Now we have to validate that the alias is happening with a # non-interpolated value. To do this we'll match the specific # pattern we're expecting. - values = insns[(index - 4)...index].map { |insn| insn.is_a?(Array) ? insn[0] : insn } - next if values != %i[putspecialobject putspecialobject putobject putobject] + values = + insns[(index - 4)...index].map do |insn| + insn.is_a?(Array) ? insn[0] : insn + end + if values != + %i[putspecialobject putspecialobject putobject putobject] + next + end # Now that we know it's in the structure we want it, we can use # the values of the putobject to determine the alias. @@ -441,7 +474,10 @@ def initialize def visit_alias(node) if node.left.is_a?(SymbolLiteral) && node.right.is_a?(SymbolLiteral) location = - Location.new(node.location.start_line, node.location.start_column) + Location.new( + node.location.start_line, + node.location.start_column + ) results << AliasMethodDefinition.new( nesting.dup, @@ -457,7 +493,10 @@ def visit_alias(node) def visit_assign(node) if node.target.is_a?(VarField) && node.target.value.is_a?(Const) location = - Location.new(node.location.start_line, node.location.start_column) + Location.new( + node.location.start_line, + node.location.start_column + ) results << ConstantDefinition.new( nesting.dup, @@ -507,18 +546,31 @@ def visit_command(node) when "attr_reader", "attr_writer", "attr_accessor" comments = comments_for(node) location = - Location.new(node.location.start_line, node.location.start_column) + Location.new( + node.location.start_line, + node.location.start_column + ) node.arguments.parts.each do |argument| next unless argument.is_a?(SymbolLiteral) name = argument.value.value.to_sym if node.message.value != "attr_writer" - results << MethodDefinition.new(nesting.dup, name, location, comments) + results << MethodDefinition.new( + nesting.dup, + name, + location, + comments + ) end if node.message.value != "attr_reader" - results << MethodDefinition.new(nesting.dup, :"#{name}=", location, comments) + results << MethodDefinition.new( + nesting.dup, + :"#{name}=", + location, + comments + ) end end end From 600d94c262cb951e1fa212c18d2fa01c46e8801e Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 6 Mar 2023 11:05:01 -0500 Subject: [PATCH 10/23] Fix up index test --- lib/syntax_tree/cli.rb | 9 +++++---- lib/syntax_tree/index.rb | 23 +++++++++++++---------- test/index_test.rb | 14 -------------- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 02f8f55d..43265c2b 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -231,10 +231,11 @@ def run(item) end def success - puts( - "!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;\" to lines/" - ) - puts("!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/") + puts(<<~HEADER) + !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ + !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ + HEADER + entries.sort.each { |entry| puts(entry) } end end diff --git a/lib/syntax_tree/index.rb b/lib/syntax_tree/index.rb index fef97be4..0280749f 100644 --- a/lib/syntax_tree/index.rb +++ b/lib/syntax_tree/index.rb @@ -275,6 +275,7 @@ def index_iseq(iseq, file_comments) queue = [[iseq, []]] while (current_iseq, current_nesting = queue.shift) + file = current_iseq[5] line = current_iseq[8] insns = current_iseq[13] @@ -309,7 +310,7 @@ def index_iseq(iseq, file_comments) find_constant_path(insns, index - 1) if superclass.empty? - warn("superclass with non constant path on line #{line}") + warn("#{file}:#{line}: superclass with non constant path") next end end @@ -328,7 +329,9 @@ def index_iseq(iseq, file_comments) # defined on self. We could, but it would require more # emulation. if insns[index - 2] != [:putself] - warn("singleton class with non-self receiver") + warn( + "#{file}:#{line}: singleton class with non-self receiver" + ) next end elsif flags & VM_DEFINECLASS_TYPE_MODULE > 0 @@ -361,7 +364,7 @@ def index_iseq(iseq, file_comments) ) when :definesmethod if insns[index - 1] != [:putself] - warn("singleton method with non-self receiver") + warn("#{file}:#{line}: singleton method with non-self receiver") next end @@ -389,15 +392,15 @@ def index_iseq(iseq, file_comments) when :opt_send_without_block, :send case insn[1][:mid] when :attr_reader, :attr_writer, :attr_accessor - names = find_attr_arguments(insns, index) - next unless names + attr_names = find_attr_arguments(insns, index) + next unless attr_names location = Location.new(line, :unknown) - names.each do |name| + attr_names.each do |attr_name| if insn[1][:mid] != :attr_writer results << method_definition( current_nesting, - name, + attr_name, location, file_comments ) @@ -406,7 +409,7 @@ def index_iseq(iseq, file_comments) if insn[1][:mid] != :attr_reader results << method_definition( current_nesting, - :"#{name}=", + :"#{attr_name}=", location, file_comments ) @@ -417,8 +420,8 @@ def index_iseq(iseq, file_comments) # non-interpolated value. To do this we'll match the specific # pattern we're expecting. values = - insns[(index - 4)...index].map do |insn| - insn.is_a?(Array) ? insn[0] : insn + insns[(index - 4)...index].map do |previous| + previous.is_a?(Array) ? previous[0] : previous end if values != %i[putspecialobject putspecialobject putobject putobject] diff --git a/test/index_test.rb b/test/index_test.rb index 855e36ec..1e2a7fc7 100644 --- a/test/index_test.rb +++ b/test/index_test.rb @@ -76,20 +76,6 @@ def test_class_path_superclass end end - def test_class_path_superclass_unknown - source = "class Foo < bar; end" - - assert_raises NotImplementedError do - Index.index(source, backend: Index::ParserBackend.new) - end - - if defined?(RubyVM::InstructionSequence) - assert_raises NotImplementedError do - Index.index(source, backend: Index::ISeqBackend.new) - end - end - end - def test_class_comments index_each("# comment1\n# comment2\nclass Foo; end") do |entry| assert_equal :Foo, entry.name From d9b21ee7393cc02647ce9f29c75f45924f336c03 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 6 Mar 2023 11:25:26 -0500 Subject: [PATCH 11/23] Document CTags --- README.md | 28 ++++++++++++++++++++++++++++ lib/syntax_tree/cli.rb | 3 +++ 2 files changed, 31 insertions(+) diff --git a/README.md b/README.md index 03942d46..d15bb5f1 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ It is built with only standard library dependencies. It additionally ships with - [CLI](#cli) - [ast](#ast) - [check](#check) + - [ctags](#ctags) - [expr](#expr) - [format](#format) - [json](#json) @@ -139,6 +140,33 @@ To change the print width that you are checking against, specify the `--print-wi stree check --print-width=100 path/to/file.rb ``` +### ctags + +This command will output to stdout a set of tags suitable for usage with [ctags](https://github.com/universal-ctags/ctags). + +```sh +stree ctags path/to/file.rb +``` + +For a file containing the following Ruby code: + +```ruby +class Foo +end + +class Bar < Foo +end +``` + +you will receive: + +``` +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +Bar test.rb /^class Bar < Foo$/;" c inherits:Foo +Foo test.rb /^class Foo$/;" c +``` + ### expr This command will output a Ruby case-match expression that would match correctly against the first expression of the input. diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 43265c2b..f2616c87 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -413,6 +413,9 @@ def run(item) #{Color.bold("stree check [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Check that the given files are formatted as syntax tree would format them + #{Color.bold("stree ctags [-e SCRIPT] FILE")} + Print out a ctags-compatible index of the given files + #{Color.bold("stree debug [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")} Check that the given files can be formatted idempotently From 9a10b4e84d0314afbf3abb364d363c5cda12e850 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 18:10:26 +0000 Subject: [PATCH 12/23] Bump rubocop from 1.47.0 to 1.48.0 Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.47.0 to 1.48.0. - [Release notes](https://github.com/rubocop/rubocop/releases) - [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop/compare/v1.47.0...v1.48.0) --- updated-dependencies: - dependency-name: rubocop dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 735a5025..5bb2d3bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,7 +19,7 @@ GEM rake (13.0.6) regexp_parser (2.7.0) rexml (3.2.5) - rubocop (1.47.0) + rubocop (1.48.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) @@ -31,7 +31,7 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.27.0) parser (>= 3.2.1.0) - ruby-progressbar (1.12.0) + ruby-progressbar (1.13.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) From 989c5d83a579f102b52864f7e2deda60b7ab9f9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 18:10:27 +0000 Subject: [PATCH 13/23] Bump minitest from 5.17.0 to 5.18.0 Bumps [minitest](https://github.com/seattlerb/minitest) from 5.17.0 to 5.18.0. - [Release notes](https://github.com/seattlerb/minitest/releases) - [Changelog](https://github.com/minitest/minitest/blob/master/History.rdoc) - [Commits](https://github.com/seattlerb/minitest/compare/v5.17.0...v5.18.0) --- updated-dependencies: - dependency-name: minitest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 735a5025..f14f4912 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GEM ast (2.4.2) docile (1.4.0) json (2.6.3) - minitest (5.17.0) + minitest (5.18.0) parallel (1.22.1) parser (3.2.1.0) ast (~> 2.4.1) From 22bea0cbe3d3d19f321c1ffe5c256f35ae74f2d9 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 8 Mar 2023 10:51:31 -0500 Subject: [PATCH 14/23] Fix Ruby build on HEAD This fixes two bugs. The first is that in an `in` clause, you need to use the `then` keyword or parentheses if the pattern you are matching is an endless range. The second is that we are associating the `then` keyword with the wrong `in` clauses because they come in in reverse order and we're deleting them from the parent clauses incorrectly. --- lib/syntax_tree/node.rb | 1 + lib/syntax_tree/parser.rb | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index c4bc1495..63a5d466 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -6767,6 +6767,7 @@ def format(q) q.group do q.text(keyword) q.nest(keyword.length) { q.format(pattern) } + q.text(" then") if pattern.is_a?(RangeNode) && pattern.right.nil? unless statements.empty? q.indent do diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index ed0de408..825cd90e 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -2132,13 +2132,20 @@ def on_in(pattern, statements, consequent) ending = consequent || consume_keyword(:end) statements_start = pattern - if (token = find_keyword(:then)) + if (token = find_keyword_between(:then, pattern, statements)) tokens.delete(token) statements_start = token end start_char = find_next_statement_start((token || statements_start).location.end_char) + + # Ripper ignores parentheses on patterns, so we need to do the same in + # order to attach comments correctly to the pattern. + if source[start_char] == ")" + start_char = find_next_statement_start(start_char + 1) + end + statements.bind( self, start_char, From dcae7057a46ad62df742d38ab6eff621847ab6c0 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 8 Mar 2023 11:38:46 -0500 Subject: [PATCH 15/23] A little bit of Sorbet --- .gitignore | 1 + .rubocop.yml | 2 +- .ruby-version | 1 + lib/syntax_tree/node.rb | 96 +++++++++++++------- lib/syntax_tree/reflection.rb | 5 +- lib/syntax_tree/yarv/instruction_sequence.rb | 2 +- tasks/sorbet.rake | 33 +++++++ test/language_server_test.rb | 69 ++++++++++++-- 8 files changed, 165 insertions(+), 44 deletions(-) create mode 100644 .ruby-version diff --git a/.gitignore b/.gitignore index 69755243..3ce1e327 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /coverage/ /pkg/ /rdocs/ +/sorbet/ /spec/reports/ /tmp/ /vendor/ diff --git a/.rubocop.yml b/.rubocop.yml index e74cdc1b..c1c17001 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,7 +7,7 @@ AllCops: SuggestExtensions: false TargetRubyVersion: 2.7 Exclude: - - '{.git,.github,bin,coverage,pkg,spec,test/fixtures,vendor,tmp}/**/*' + - '{.git,.github,bin,coverage,pkg,sorbet,spec,test/fixtures,vendor,tmp}/**/*' - test.rb Gemspec/DevelopmentDependencies: diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..944880fa --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.0 diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 63a5d466..3f013b31 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -792,9 +792,10 @@ def arity private def trailing_comma? + arguments = self.arguments return false unless arguments.is_a?(Args) - parts = arguments.parts + parts = arguments.parts if parts.last.is_a?(ArgBlock) # If the last argument is a block, then we can't put a trailing comma # after it without resulting in a syntax error. @@ -1188,8 +1189,11 @@ def deconstruct_keys(_keys) end def format(q) - if lbracket.comments.empty? && contents && contents.comments.empty? && - contents.parts.length > 1 + lbracket = self.lbracket + contents = self.contents + + if lbracket.is_a?(LBracket) && lbracket.comments.empty? && contents && + contents.comments.empty? && contents.parts.length > 1 if qwords? QWordsFormatter.new(contents).format(q) return @@ -2091,6 +2095,7 @@ def deconstruct_keys(_keys) end def format(q) + left = self.left power = operator == :** q.group do @@ -2307,6 +2312,8 @@ def initialize( end def bind(parser, start_char, start_column, end_char, end_column) + rescue_clause = self.rescue_clause + @location = Location.new( start_line: location.start_line, @@ -2330,6 +2337,7 @@ def bind(parser, start_char, start_column, end_char, end_column) # Next we're going to determine the rescue clause if there is one if rescue_clause consequent = else_clause || ensure_clause + rescue_clause.bind_end( consequent ? consequent.location.start_char : end_char, consequent ? consequent.location.start_column : end_column @@ -2735,7 +2743,7 @@ def format(q) children << receiver end when MethodAddBlock - if receiver.call.is_a?(CallNode) && !receiver.call.receiver.nil? + if (call = receiver.call).is_a?(CallNode) && !call.receiver.nil? children << receiver else break @@ -2744,8 +2752,8 @@ def format(q) break end when MethodAddBlock - if child.call.is_a?(CallNode) && !child.call.receiver.nil? - children << child.call + if (call = child.call).is_a?(CallNode) && !call.receiver.nil? + children << call else break end @@ -2767,8 +2775,8 @@ def format(q) # of just Statements nodes. parent = parents[3] if parent.is_a?(BlockNode) && parent.keywords? - if parent.is_a?(MethodAddBlock) && parent.call.is_a?(CallNode) && - parent.call.message.value == "sig" + if parent.is_a?(MethodAddBlock) && + (call = parent.call).is_a?(CallNode) && call.message.value == "sig" threshold = 2 end end @@ -2813,10 +2821,10 @@ def format_chain(q, children) while (child = children.pop) if child.is_a?(CallNode) - if child.receiver.is_a?(CallNode) && - (child.receiver.message != :call) && - (child.receiver.message.value == "where") && - (child.message.value == "not") + if (receiver = child.receiver).is_a?(CallNode) && + (receiver.message != :call) && + (receiver.message.value == "where") && + (message.value == "not") # This is very specialized behavior wherein we group # .where.not calls together because it looks better. For more # information, see @@ -2872,7 +2880,8 @@ def self.chained?(node) when CallNode !node.receiver.nil? when MethodAddBlock - node.call.is_a?(CallNode) && !node.call.receiver.nil? + call = node.call + call.is_a?(CallNode) && !call.receiver.nil? else false end @@ -3629,6 +3638,10 @@ def deconstruct_keys(_keys) end def format(q) + message = self.message + arguments = self.arguments + block = self.block + q.group do doc = q.nest(0) do @@ -3637,7 +3650,7 @@ def format(q) # If there are leading comments on the message then we know we have # a newline in the source that is forcing these things apart. In # this case we will have to use a trailing operator. - if message.comments.any?(&:leading?) + if message != :call && message.comments.any?(&:leading?) q.format(CallOperatorFormatter.new(operator), stackable: false) q.indent do q.breakable_empty @@ -4153,6 +4166,9 @@ def deconstruct_keys(_keys) end def format(q) + params = self.params + bodystmt = self.bodystmt + q.group do q.group do q.text("def") @@ -4209,6 +4225,8 @@ def endless? end def arity + params = self.params + case params when Params params.arity @@ -5293,6 +5311,7 @@ def accept(visitor) end def child_nodes + operator = self.operator [parent, (operator if operator != :"::"), name] end @@ -5674,7 +5693,7 @@ def accept(visitor) end def child_nodes - [lbrace] + assocs + [lbrace].concat(assocs) end def copy(lbrace: nil, assocs: nil, location: nil) @@ -5766,7 +5785,7 @@ class Heredoc < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(beginning:, ending: nil, dedent: 0, parts: [], location:) + def initialize(beginning:, location:, ending: nil, dedent: 0, parts: []) @beginning = beginning @ending = ending @dedent = dedent @@ -6134,6 +6153,8 @@ def ===(other) private def format_contents(q, parts, nested) + keyword_rest = self.keyword_rest + q.group { q.seplist(parts) { |part| q.format(part, stackable: false) } } # If there isn't a constant, and there's a blank keyword_rest, then we @@ -6763,6 +6784,8 @@ def deconstruct_keys(_keys) def format(q) keyword = "in " + pattern = self.pattern + consequent = self.consequent q.group do q.text(keyword) @@ -7165,6 +7188,8 @@ def deconstruct_keys(_keys) end def format(q) + params = self.params + q.text("->") q.group do if params.is_a?(Paren) @@ -7643,7 +7668,7 @@ class MLHS < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, comma: false, location:) + def initialize(parts:, location:, comma: false) @parts = parts @comma = comma @location = location @@ -7704,7 +7729,7 @@ class MLHSParen < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(contents:, comma: false, location:) + def initialize(contents:, location:, comma: false) @contents = contents @comma = comma @location = location @@ -8287,14 +8312,14 @@ def format(q) attr_reader :comments def initialize( + location:, requireds: [], optionals: [], rest: nil, posts: [], keywords: [], keyword_rest: nil, - block: nil, - location: + block: nil ) @requireds = requireds @optionals = optionals @@ -8321,6 +8346,8 @@ def accept(visitor) end def child_nodes + keyword_rest = self.keyword_rest + [ *requireds, *optionals.flatten(1), @@ -8375,16 +8402,19 @@ def deconstruct_keys(_keys) end def format(q) + rest = self.rest + keyword_rest = self.keyword_rest + parts = [ *requireds, *optionals.map { |(name, value)| OptionalFormatter.new(name, value) } ] parts << rest if rest && !rest.is_a?(ExcessedComma) - parts += [ - *posts, - *keywords.map { |(name, value)| KeywordFormatter.new(name, value) } - ] + parts.concat(posts) + parts.concat( + keywords.map { |(name, value)| KeywordFormatter.new(name, value) } + ) parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest parts << block if block @@ -8511,6 +8541,8 @@ def deconstruct_keys(_keys) end def format(q) + contents = self.contents + q.group do q.format(lparen) @@ -9425,11 +9457,11 @@ def bind_end(end_char, end_column) end_column: end_column ) - if consequent - consequent.bind_end(end_char, end_column) + if (next_node = consequent) + next_node.bind_end(end_char, end_column) statements.bind_end( - consequent.location.start_char, - consequent.location.start_column + next_node.location.start_char, + next_node.location.start_column ) else statements.bind_end(end_char, end_column) @@ -9872,8 +9904,8 @@ def bind(parser, start_char, start_column, end_char, end_column) end_column: end_column ) - if body[0].is_a?(VoidStmt) - location = body[0].location + if (void_stmt = body[0]).is_a?(VoidStmt) + location = void_stmt.location location = Location.new( start_line: location.start_line, @@ -10352,7 +10384,7 @@ def format(q) opening_quote, closing_quote = if !Quotes.locked?(self, q.quote) [q.quote, q.quote] - elsif quote.start_with?("%") + elsif quote&.start_with?("%") [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])] else [quote, quote] @@ -11521,7 +11553,7 @@ def accept(visitor) end def child_nodes - [value] + value == :nil ? [] : [value] end def copy(value: nil, location: nil) diff --git a/lib/syntax_tree/reflection.rb b/lib/syntax_tree/reflection.rb index b2ffec6d..a27593ee 100644 --- a/lib/syntax_tree/reflection.rb +++ b/lib/syntax_tree/reflection.rb @@ -183,10 +183,11 @@ def parse_comments(statements, index) next unless main_statement.is_a?(SyntaxTree::ClassDeclaration) # Ensure we're looking at class declarations with superclasses. - next unless main_statement.superclass.is_a?(SyntaxTree::VarRef) + superclass = main_statement.superclass + next unless superclass.is_a?(SyntaxTree::VarRef) # Ensure we're looking at class declarations that inherit from Node. - next unless main_statement.superclass.value.value == "Node" + next unless superclass.value.value == "Node" # All child nodes inherit the location attr_reader from Node, so we'll add # that to the list of attributes first. diff --git a/lib/syntax_tree/yarv/instruction_sequence.rb b/lib/syntax_tree/yarv/instruction_sequence.rb index 45b543e6..5aaaef44 100644 --- a/lib/syntax_tree/yarv/instruction_sequence.rb +++ b/lib/syntax_tree/yarv/instruction_sequence.rb @@ -50,7 +50,7 @@ def initialize @tail_node = nil end - def each + def each(&_blk) return to_enum(__method__) unless block_given? each_node { |node| yield node.value } end diff --git a/tasks/sorbet.rake b/tasks/sorbet.rake index e4152664..c80ec91d 100644 --- a/tasks/sorbet.rake +++ b/tasks/sorbet.rake @@ -122,8 +122,41 @@ module SyntaxTree @line += 1 node_body << generate_def_node("child_nodes", nil) + @line += 2 + + node_body << sig_block do + CallNode( + sig_params do + BareAssocHash( + [ + Assoc( + Label("other:"), + CallNode( + VarRef(Const("T")), + Period("."), + Ident("untyped"), + nil + ) + ) + ] + ) + end, + Period("."), + sig_returns { ConstPathRef(VarRef(Const("T")), Const("Boolean")) }, + nil + ) + end @line += 1 + node_body << generate_def_node( + "==", + Paren( + LParen("("), + Params.new(location: location, requireds: [Ident("other")]) + ) + ) + @line += 2 + node_body end diff --git a/test/language_server_test.rb b/test/language_server_test.rb index 2fe4e60a..f5a6ca57 100644 --- a/test/language_server_test.rb +++ b/test/language_server_test.rb @@ -6,19 +6,38 @@ module SyntaxTree # stree-ignore class LanguageServerTest < Minitest::Test - class Initialize < Struct.new(:id) + class Initialize + attr_reader :id + + def initialize(id) + @id = id + end + def to_hash { method: "initialize", id: id } end end - class Shutdown < Struct.new(:id) + class Shutdown + attr_reader :id + + def initialize(id) + @id = id + end + def to_hash { method: "shutdown", id: id } end end - class TextDocumentDidOpen < Struct.new(:uri, :text) + class TextDocumentDidOpen + attr_reader :uri, :text + + def initialize(uri, text) + @uri = uri + @text = text + end + def to_hash { method: "textDocument/didOpen", @@ -27,7 +46,14 @@ def to_hash end end - class TextDocumentDidChange < Struct.new(:uri, :text) + class TextDocumentDidChange + attr_reader :uri, :text + + def initialize(uri, text) + @uri = uri + @text = text + end + def to_hash { method: "textDocument/didChange", @@ -39,7 +65,13 @@ def to_hash end end - class TextDocumentDidClose < Struct.new(:uri) + class TextDocumentDidClose + attr_reader :uri + + def initialize(uri) + @uri = uri + end + def to_hash { method: "textDocument/didClose", @@ -48,7 +80,14 @@ def to_hash end end - class TextDocumentFormatting < Struct.new(:id, :uri) + class TextDocumentFormatting + attr_reader :id, :uri + + def initialize(id, uri) + @id = id + @uri = uri + end + def to_hash { method: "textDocument/formatting", @@ -58,7 +97,14 @@ def to_hash end end - class TextDocumentInlayHint < Struct.new(:id, :uri) + class TextDocumentInlayHint + attr_reader :id, :uri + + def initialize(id, uri) + @id = id + @uri = uri + end + def to_hash { method: "textDocument/inlayHint", @@ -68,7 +114,14 @@ def to_hash end end - class SyntaxTreeVisualizing < Struct.new(:id, :uri) + class SyntaxTreeVisualizing + attr_reader :id, :uri + + def initialize(id, uri) + @id = id + @uri = uri + end + def to_hash { method: "syntaxTree/visualizing", From ec0396d235ac5a9a0408707c40dd7713eec624d8 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 8 Mar 2023 14:16:47 -0500 Subject: [PATCH 16/23] Require the pp gem, necessary for pretty printing --- lib/syntax_tree.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 4e183383..24d8426f 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "prettier_print" +require "pp" require "ripper" require_relative "syntax_tree/node" From acd8238b47ee2f831554645c181f609c7be90ceb Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 9 Mar 2023 09:36:48 -0500 Subject: [PATCH 17/23] Add the visitor method on reflection --- lib/syntax_tree/reflection.rb | 20 ++++++++++++-- tasks/sorbet.rake | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/lib/syntax_tree/reflection.rb b/lib/syntax_tree/reflection.rb index a27593ee..aa7b85b6 100644 --- a/lib/syntax_tree/reflection.rb +++ b/lib/syntax_tree/reflection.rb @@ -138,12 +138,13 @@ def initialize(name, comment) # as a placeholder for collecting all of the various places that nodes are # used. class Node - attr_reader :name, :comment, :attributes + attr_reader :name, :comment, :attributes, :visitor_method - def initialize(name, comment, attributes) + def initialize(name, comment, attributes, visitor_method) @name = name @comment = comment @attributes = attributes + @visitor_method = visitor_method end end @@ -196,6 +197,10 @@ def parse_comments(statements, index) Attribute.new(:location, "[Location] the location of this node") } + # This is the name of the method tha gets called on the given visitor when + # the accept method is called on this node. + visitor_method = nil + statements = main_statement.bodystmt.statements.body statements.each_with_index do |statement, statement_index| case statement @@ -225,16 +230,25 @@ def parse_comments(statements, index) end attributes[attribute.name] = attribute + when SyntaxTree::DefNode + if statement.name.value == "accept" + call_node = statement.bodystmt.statements.body.first + visitor_method = call_node.message.value.to_sym + end end end + # If we never found a visitor method, then we have an error. + raise if visitor_method.nil? + # Finally, set it up in the hash of nodes so that we can use it later. comments = parse_comments(main_statements, main_statement_index) node = Node.new( main_statement.constant.constant.value.to_sym, "#{comments.join("\n")}\n", - attributes + attributes, + visitor_method ) @nodes[node.name] = node diff --git a/tasks/sorbet.rake b/tasks/sorbet.rake index c80ec91d..134b6011 100644 --- a/tasks/sorbet.rake +++ b/tasks/sorbet.rake @@ -20,6 +20,22 @@ module SyntaxTree generate_parent Reflection.nodes.sort.each { |(_, node)| generate_node(node) } + body << ClassDeclaration( + ConstPathRef(VarRef(Const("SyntaxTree")), Const("BasicVisitor")), + nil, + BodyStmt( + Statements(generate_visitor("overridable")), nil, nil, nil, nil + ), + location + ) + + body << ClassDeclaration( + ConstPathRef(VarRef(Const("SyntaxTree")), Const("Visitor")), + ConstPathRef(VarRef(Const("SyntaxTree")), Const("BasicVisitor")), + BodyStmt(Statements(generate_visitor("override")), nil, nil, nil, nil), + location + ) + Formatter.format(nil, Program(Statements(body))) end @@ -228,6 +244,42 @@ module SyntaxTree ) end + def generate_visitor(override) + body = [] + + Reflection.nodes.each do |name, node| + body << sig_block do + CallNode( + CallNode( + Ident(override), + Period("."), + sig_params do + BareAssocHash([ + Assoc(Label("node:"), + sig_type_for(SyntaxTree.const_get(name))) + ]) + end, + nil + ), + Period("."), + sig_returns do + CallNode(VarRef(Const("T")), Period("."), Ident("untyped"), nil) + end, + nil + ) + end + + body << generate_def_node(node.visitor_method, Paren( + LParen("("), + Params.new(requireds: [Ident("node")], location: location) + )) + + @line += 2 + end + + body + end + def sig_block MethodAddBlock( CallNode(nil, nil, Ident("sig"), nil), From 57caa25b945a48b20dafe7d454bdf9f99ff2caae Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 9 Mar 2023 10:03:25 -0500 Subject: [PATCH 18/23] Fix up formatting --- tasks/sorbet.rake | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tasks/sorbet.rake b/tasks/sorbet.rake index 134b6011..05f48874 100644 --- a/tasks/sorbet.rake +++ b/tasks/sorbet.rake @@ -24,7 +24,11 @@ module SyntaxTree ConstPathRef(VarRef(Const("SyntaxTree")), Const("BasicVisitor")), nil, BodyStmt( - Statements(generate_visitor("overridable")), nil, nil, nil, nil + Statements(generate_visitor("overridable")), + nil, + nil, + nil, + nil ), location ) @@ -254,10 +258,14 @@ module SyntaxTree Ident(override), Period("."), sig_params do - BareAssocHash([ - Assoc(Label("node:"), - sig_type_for(SyntaxTree.const_get(name))) - ]) + BareAssocHash( + [ + Assoc( + Label("node:"), + sig_type_for(SyntaxTree.const_get(name)) + ) + ] + ) end, nil ), @@ -269,10 +277,13 @@ module SyntaxTree ) end - body << generate_def_node(node.visitor_method, Paren( - LParen("("), - Params.new(requireds: [Ident("node")], location: location) - )) + body << generate_def_node( + node.visitor_method, + Paren( + LParen("("), + Params.new(requireds: [Ident("node")], location: location) + ) + ) @line += 2 end From e348f8fb75b2c532b35311f43703ad1d646eb9f7 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 9 Mar 2023 10:18:19 -0500 Subject: [PATCH 19/23] defined_ivar instruction --- lib/syntax_tree/yarv/compiler.rb | 3 +- lib/syntax_tree/yarv/instruction_sequence.rb | 15 ++++- lib/syntax_tree/yarv/instructions.rb | 58 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/lib/syntax_tree/yarv/compiler.rb b/lib/syntax_tree/yarv/compiler.rb index bd20bc19..b0afcc99 100644 --- a/lib/syntax_tree/yarv/compiler.rb +++ b/lib/syntax_tree/yarv/compiler.rb @@ -875,8 +875,7 @@ def visit_defined(node) when Ident iseq.putobject("local-variable") when IVar - iseq.putnil - iseq.defined(Defined::TYPE_IVAR, name, "instance-variable") + iseq.defined_ivar(name, iseq.inline_storage, "instance-variable") when Kw case name when :false diff --git a/lib/syntax_tree/yarv/instruction_sequence.rb b/lib/syntax_tree/yarv/instruction_sequence.rb index 5aaaef44..2d89e052 100644 --- a/lib/syntax_tree/yarv/instruction_sequence.rb +++ b/lib/syntax_tree/yarv/instruction_sequence.rb @@ -673,12 +673,21 @@ def concatstrings(number) push(ConcatStrings.new(number)) end + def defineclass(name, class_iseq, flags) + push(DefineClass.new(name, class_iseq, flags)) + end + def defined(type, name, message) push(Defined.new(type, name, message)) end - def defineclass(name, class_iseq, flags) - push(DefineClass.new(name, class_iseq, flags)) + def defined_ivar(name, cache, message) + if RUBY_VERSION < "3.3" + push(PutNil.new) + push(Defined.new(Defined::TYPE_IVAR, name, message)) + else + push(DefinedIVar.new(name, cache, message)) + end end def definemethod(name, method_iseq) @@ -1058,6 +1067,8 @@ def self.from(source, options = Compiler::Options.new, parent_iseq = nil) iseq.defineclass(opnds[0], from(opnds[1], options, iseq), opnds[2]) when :defined iseq.defined(opnds[0], opnds[1], opnds[2]) + when :defined_ivar + iseq.defined_ivar(opnds[0], opnds[1], opnds[2]) when :definemethod iseq.definemethod(opnds[0], from(opnds[1], options, iseq)) when :definesmethod diff --git a/lib/syntax_tree/yarv/instructions.rb b/lib/syntax_tree/yarv/instructions.rb index 38c80fde..cf83ddeb 100644 --- a/lib/syntax_tree/yarv/instructions.rb +++ b/lib/syntax_tree/yarv/instructions.rb @@ -994,6 +994,64 @@ def call(vm) end end + # ### Summary + # + # `defined_ivar` checks if an instance variable is defined. It is a + # specialization of the `defined` instruction. It accepts three arguments: + # the name of the instance variable, an inline cache, and the string that + # should be pushed onto the stack in the event that the instance variable + # is defined. + # + # ### Usage + # + # ~~~ruby + # defined?(@value) + # ~~~ + # + class DefinedIVar < Instruction + attr_reader :name, :cache, :message + + def initialize(name, cache, message) + @name = name + @cache = cache + @message = message + end + + def disasm(fmt) + fmt.instruction( + "defined_ivar", + [fmt.object(name), fmt.inline_storage(cache), fmt.object(message)] + ) + end + + def to_a(_iseq) + [:defined_ivar, name, cache, message] + end + + def deconstruct_keys(_keys) + { name: name, cache: cache, message: message } + end + + def ==(other) + other.is_a?(DefinedIVar) && other.name == name && + other.cache == cache && other.message == message + end + + def length + 4 + end + + def pushes + 1 + end + + def call(vm) + result = (message if vm.frame._self.instance_variable_defined?(name)) + + vm.push(result) + end + end + # ### Summary # # `definemethod` defines a method on the class of the current value of From 3e4fcd533ab983645da555ce1ad02e673ab80ab9 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 9 Mar 2023 10:42:33 -0500 Subject: [PATCH 20/23] definedivar --- lib/syntax_tree/yarv/compiler.rb | 2 +- lib/syntax_tree/yarv/instruction_sequence.rb | 6 +++--- lib/syntax_tree/yarv/instructions.rb | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/syntax_tree/yarv/compiler.rb b/lib/syntax_tree/yarv/compiler.rb index b0afcc99..0f7e7372 100644 --- a/lib/syntax_tree/yarv/compiler.rb +++ b/lib/syntax_tree/yarv/compiler.rb @@ -875,7 +875,7 @@ def visit_defined(node) when Ident iseq.putobject("local-variable") when IVar - iseq.defined_ivar(name, iseq.inline_storage, "instance-variable") + iseq.definedivar(name, iseq.inline_storage, "instance-variable") when Kw case name when :false diff --git a/lib/syntax_tree/yarv/instruction_sequence.rb b/lib/syntax_tree/yarv/instruction_sequence.rb index 2d89e052..7ce7bcdd 100644 --- a/lib/syntax_tree/yarv/instruction_sequence.rb +++ b/lib/syntax_tree/yarv/instruction_sequence.rb @@ -681,7 +681,7 @@ def defined(type, name, message) push(Defined.new(type, name, message)) end - def defined_ivar(name, cache, message) + def definedivar(name, cache, message) if RUBY_VERSION < "3.3" push(PutNil.new) push(Defined.new(Defined::TYPE_IVAR, name, message)) @@ -1067,8 +1067,8 @@ def self.from(source, options = Compiler::Options.new, parent_iseq = nil) iseq.defineclass(opnds[0], from(opnds[1], options, iseq), opnds[2]) when :defined iseq.defined(opnds[0], opnds[1], opnds[2]) - when :defined_ivar - iseq.defined_ivar(opnds[0], opnds[1], opnds[2]) + when :definedivar + iseq.definedivar(opnds[0], opnds[1], opnds[2]) when :definemethod iseq.definemethod(opnds[0], from(opnds[1], options, iseq)) when :definesmethod diff --git a/lib/syntax_tree/yarv/instructions.rb b/lib/syntax_tree/yarv/instructions.rb index cf83ddeb..ceb237dc 100644 --- a/lib/syntax_tree/yarv/instructions.rb +++ b/lib/syntax_tree/yarv/instructions.rb @@ -996,7 +996,7 @@ def call(vm) # ### Summary # - # `defined_ivar` checks if an instance variable is defined. It is a + # `definedivar` checks if an instance variable is defined. It is a # specialization of the `defined` instruction. It accepts three arguments: # the name of the instance variable, an inline cache, and the string that # should be pushed onto the stack in the event that the instance variable @@ -1019,13 +1019,13 @@ def initialize(name, cache, message) def disasm(fmt) fmt.instruction( - "defined_ivar", + "definedivar", [fmt.object(name), fmt.inline_storage(cache), fmt.object(message)] ) end def to_a(_iseq) - [:defined_ivar, name, cache, message] + [:definedivar, name, cache, message] end def deconstruct_keys(_keys) From ed033e49603be8fb1a1b9a523aa4669e384c6df1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Mar 2023 17:15:01 +0000 Subject: [PATCH 21/23] Bump prettier_print from 1.2.0 to 1.2.1 Bumps [prettier_print](https://github.com/ruby-syntax-tree/prettier_print) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/ruby-syntax-tree/prettier_print/releases) - [Changelog](https://github.com/ruby-syntax-tree/prettier_print/blob/main/CHANGELOG.md) - [Commits](https://github.com/ruby-syntax-tree/prettier_print/compare/v1.2.0...v1.2.1) --- updated-dependencies: - dependency-name: prettier_print dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9ba5adf8..cd726fb4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,7 +14,7 @@ GEM parallel (1.22.1) parser (3.2.1.0) ast (~> 2.4.1) - prettier_print (1.2.0) + prettier_print (1.2.1) rainbow (3.1.1) rake (13.0.6) regexp_parser (2.7.0) From 662b9f273b3c8e37749c07b0dd0033d36d8c9ddc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 18:08:50 +0000 Subject: [PATCH 22/23] Bump rubocop from 1.48.0 to 1.48.1 Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.48.0 to 1.48.1. - [Release notes](https://github.com/rubocop/rubocop/releases) - [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop/compare/v1.48.0...v1.48.1) --- updated-dependencies: - dependency-name: rubocop dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cd726fb4..565fb7ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,14 +12,14 @@ GEM json (2.6.3) minitest (5.18.0) parallel (1.22.1) - parser (3.2.1.0) + parser (3.2.1.1) ast (~> 2.4.1) prettier_print (1.2.1) rainbow (3.1.1) rake (13.0.6) regexp_parser (2.7.0) rexml (3.2.5) - rubocop (1.48.0) + rubocop (1.48.1) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) From 1e13c69fb65f0ddbc3818dceb3845fcb00430c41 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 20 Mar 2023 09:53:02 -0400 Subject: [PATCH 23/23] Bump to version 6.1.0 --- CHANGELOG.md | 13 +++++++++++++ Gemfile.lock | 2 +- lib/syntax_tree/version.rb | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 960bb0e9..2d3daa58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [6.1.0] - 2023-03-20 + +### Added + +- The `stree ctags` command for generating ctags like `universal-ctags` or `ripper-tags` would. +- The `definedivar` YARV instruction has been added to match CRuby's implementation. +- We now generate better Sorbet RBI files for the nodes in the tree and the visitors. +- `SyntaxTree::Reflection.nodes` now includes the visitor method. + +### Changed + +- We now explicitly require `pp` in environments that need it. + ## [6.0.2] - 2023-03-03 ### Added diff --git a/Gemfile.lock b/Gemfile.lock index 565fb7ad..f69c40d1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (6.0.2) + syntax_tree (6.1.0) prettier_print (>= 1.2.0) GEM diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index ff3db370..3ed889e4 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "6.0.2" + VERSION = "6.1.0" end