From 40fac28b4ea3f4f175e3c75a56476cf8150d7f6a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Thu, 17 Aug 2023 14:53:21 +0100 Subject: [PATCH] Index constants (#893) * Support indexing constants * Extract RubyIndexer::TestCase --- lib/ruby_indexer/lib/ruby_indexer/index.rb | 3 + lib/ruby_indexer/lib/ruby_indexer/visitor.rb | 30 +++++ .../test/classes_and_modules_test.rb | 34 +----- lib/ruby_indexer/test/constant_test.rb | 108 ++++++++++++++++++ lib/ruby_indexer/test/index_test.rb | 8 +- lib/ruby_indexer/test/test_case.rb | 42 +++++++ 6 files changed, 187 insertions(+), 38 deletions(-) create mode 100644 lib/ruby_indexer/test/constant_test.rb create mode 100644 lib/ruby_indexer/test/test_case.rb diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb index aea685803..b5b2fa21b 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/index.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb @@ -114,6 +114,9 @@ class Module < Namespace class Class < Namespace end + + class Constant < Entry + end end end end diff --git a/lib/ruby_indexer/lib/ruby_indexer/visitor.rb b/lib/ruby_indexer/lib/ruby_indexer/visitor.rb index f82b52706..22998af8d 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/visitor.rb @@ -35,6 +35,10 @@ def visit(node) add_index_entry(node, Index::Entry::Class) when YARP::ModuleNode add_index_entry(node, Index::Entry::Module) + when YARP::ConstantWriteNode, YARP::ConstantOperatorOrWriteNode + add_constant(node) + when YARP::ConstantPathWriteNode, YARP::ConstantPathOperatorOrWriteNode + add_constant_with_path(node) end end @@ -46,6 +50,32 @@ def visit_all(nodes) private + sig do + params( + node: T.any(YARP::ConstantWriteNode, YARP::ConstantOperatorOrWriteNode), + ).void + end + def add_constant(node) + comments = collect_comments(node) + @index << Index::Entry::Constant.new(fully_qualify_name(node.name), @file_path, node.location, comments) + end + + sig do + params( + node: T.any(YARP::ConstantPathWriteNode, YARP::ConstantPathOperatorOrWriteNode), + ).void + end + def add_constant_with_path(node) + # ignore variable constants like `var::FOO` or `self.class::FOO` + return unless node.target.parent.nil? || node.target.parent.is_a?(YARP::ConstantReadNode) + + name = node.target.location.slice + fully_qualified_name = name.start_with?("::") ? name.delete_prefix("::") : fully_qualify_name(name) + + comments = collect_comments(node) + @index << Index::Entry::Constant.new(fully_qualified_name, @file_path, node.location, comments) + end + sig { params(node: T.any(YARP::ClassNode, YARP::ModuleNode), klass: T.class_of(Index::Entry)).void } def add_index_entry(node, klass) name = node.constant_path.location.slice diff --git a/lib/ruby_indexer/test/classes_and_modules_test.rb b/lib/ruby_indexer/test/classes_and_modules_test.rb index fcf322c94..122c95643 100644 --- a/lib/ruby_indexer/test/classes_and_modules_test.rb +++ b/lib/ruby_indexer/test/classes_and_modules_test.rb @@ -1,14 +1,10 @@ # typed: true # frozen_string_literal: true -require "test_helper" +require_relative "test_case" module RubyIndexer - class ClassesAndModulesTest < Minitest::Test - def setup - @index = Index.new - end - + class ClassesAndModulesTest < TestCase def test_empty_statements_class index(<<~RUBY) class Foo @@ -204,31 +200,5 @@ class Foo; end second_foo_entry = @index["Foo"][1] assert_equal("# This is another Foo comment\n", second_foo_entry.comments.join) end - - private - - def index(source) - @index.index_single("/fake/path/foo.rb", source) - end - - def assert_entry(expected_name, type, expected_location) - entries = @index[expected_name] - refute_empty(entries, "Expected #{expected_name} to be indexed") - - entry = entries.first - assert_instance_of(type, entry, "Expected #{expected_name} to be a #{type}") - - location = entry.location - location_string = - "#{entry.file_path}:#{location.start_line - 1}-#{location.start_column}" \ - ":#{location.end_line - 1}-#{location.end_column}" - - assert_equal(expected_location, location_string) - end - - def refute_entry(expected_name) - entries = @index[expected_name] - assert_nil(entries, "Expected #{expected_name} to not be indexed") - end end end diff --git a/lib/ruby_indexer/test/constant_test.rb b/lib/ruby_indexer/test/constant_test.rb new file mode 100644 index 000000000..9bc590497 --- /dev/null +++ b/lib/ruby_indexer/test/constant_test.rb @@ -0,0 +1,108 @@ +# typed: true +# frozen_string_literal: true + +require_relative "test_case" + +module RubyIndexer + class ConstantTest < TestCase + def test_constant_writes + index(<<~RUBY) + FOO = 1 + + class ::Bar + FOO = 2 + end + RUBY + + assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-6") + assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-8") + end + + def test_constant_or_writes + index(<<~RUBY) + FOO ||= 1 + + class ::Bar + FOO ||= 2 + end + RUBY + + assert_entry("FOO", Index::Entry::Constant, "/fake/path/foo.rb:0-0:0-8") + assert_entry("Bar::FOO", Index::Entry::Constant, "/fake/path/foo.rb:3-2:3-10") + end + + def test_constant_path_writes + index(<<~RUBY) + class A + FOO = 1 + ::BAR = 1 + + module B + FOO = 1 + end + end + + A::BAZ = 1 + RUBY + + assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-8") + assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-10") + assert_entry("A::B::FOO", Index::Entry::Constant, "/fake/path/foo.rb:5-4:5-10") + assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:9-0:9-9") + end + + def test_constant_path_or_writes + index(<<~RUBY) + class A + FOO ||= 1 + ::BAR ||= 1 + end + + A::BAZ ||= 1 + RUBY + + assert_entry("A::FOO", Index::Entry::Constant, "/fake/path/foo.rb:1-2:1-10") + assert_entry("BAR", Index::Entry::Constant, "/fake/path/foo.rb:2-2:2-12") + assert_entry("A::BAZ", Index::Entry::Constant, "/fake/path/foo.rb:5-0:5-11") + end + + def test_comments_for_constants + index(<<~RUBY) + # FOO comment + FOO = 1 + + class A + # A::FOO comment + FOO = 1 + + # ::BAR comment + ::BAR = 1 + end + + # A::BAZ comment + A::BAZ = 1 + RUBY + + foo_comment = @index["FOO"].first.comments.join("\n") + assert_equal("# FOO comment\n", foo_comment) + + a_foo_comment = @index["A::FOO"].first.comments.join("\n") + assert_equal("# A::FOO comment\n", a_foo_comment) + + bar_comment = @index["BAR"].first.comments.join("\n") + assert_equal("# ::BAR comment\n", bar_comment) + + a_baz_comment = @index["A::BAZ"].first.comments.join("\n") + assert_equal("# A::BAZ comment\n", a_baz_comment) + end + + def test_variable_path_constants_are_ignored + index(<<~RUBY) + var::FOO = 1 + self.class::FOO = 1 + RUBY + + assert_no_entry + end + end +end diff --git a/lib/ruby_indexer/test/index_test.rb b/lib/ruby_indexer/test/index_test.rb index 00c10d98a..d3b35bd11 100644 --- a/lib/ruby_indexer/test/index_test.rb +++ b/lib/ruby_indexer/test/index_test.rb @@ -1,14 +1,10 @@ # typed: true # frozen_string_literal: true -require "test_helper" +require_relative "test_case" module RubyIndexer - class IndexTest < Minitest::Test - def setup - @index = Index.new - end - + class IndexTest < TestCase def test_deleting_one_entry_for_a_class @index.index_single("/fake/path/foo.rb", <<~RUBY) class Foo diff --git a/lib/ruby_indexer/test/test_case.rb b/lib/ruby_indexer/test/test_case.rb new file mode 100644 index 000000000..58978dfcd --- /dev/null +++ b/lib/ruby_indexer/test/test_case.rb @@ -0,0 +1,42 @@ +# typed: true +# frozen_string_literal: true + +require "test_helper" + +module RubyIndexer + class TestCase < Minitest::Test + def setup + @index = Index.new + end + + private + + def index(source) + @index.index_single("/fake/path/foo.rb", source) + end + + def assert_entry(expected_name, type, expected_location) + entries = @index[expected_name] + refute_empty(entries, "Expected #{expected_name} to be indexed") + + entry = entries.first + assert_instance_of(type, entry, "Expected #{expected_name} to be a #{type}") + + location = entry.location + location_string = + "#{entry.file_path}:#{location.start_line - 1}-#{location.start_column}" \ + ":#{location.end_line - 1}-#{location.end_column}" + + assert_equal(expected_location, location_string) + end + + def refute_entry(expected_name) + entries = @index[expected_name] + assert_nil(entries, "Expected #{expected_name} to not be indexed") + end + + def assert_no_entry + assert_empty(@index.instance_variable_get(:@entries), "Expected nothing to be indexed") + end + end +end