lib/ruby_indexer/test/instance_variables_test.rb



# typed: true
# frozen_string_literal: true

require_relative "test_case"

module RubyIndexer
  class InstanceVariableTest < TestCase
    def test_instance_variable_write
      index(<<~RUBY)
        module Foo
          class Bar
            def initialize
              # Hello
              @a = 1
            end
          end
        end
      RUBY

      assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::Class, owner)
      assert_equal("Foo::Bar", owner&.name)
    end

    def test_instance_variable_with_multibyte_characters
      index(<<~RUBY)
        class Foo
          def initialize
            @あ = 1
          end
        end
      RUBY

      assert_entry("@あ", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6")
    end

    def test_instance_variable_and_write
      index(<<~RUBY)
        module Foo
          class Bar
            def initialize
              # Hello
              @a &&= value
            end
          end
        end
      RUBY

      assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::Class, owner)
      assert_equal("Foo::Bar", owner&.name)
    end

    def test_instance_variable_operator_write
      index(<<~RUBY)
        module Foo
          class Bar
            def initialize
              # Hello
              @a += value
            end
          end
        end
      RUBY

      assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::Class, owner)
      assert_equal("Foo::Bar", owner&.name)
    end

    def test_instance_variable_or_write
      index(<<~RUBY)
        module Foo
          class Bar
            def initialize
              # Hello
              @a ||= value
            end
          end
        end
      RUBY

      assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::Class, owner)
      assert_equal("Foo::Bar", owner&.name)
    end

    def test_instance_variable_target
      index(<<~RUBY)
        module Foo
          class Bar
            def initialize
              # Hello
              @a, @b = [1, 2]
            end
          end
        end
      RUBY

      assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:4-6:4-8")
      assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:4-10:4-12")

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::Class, owner)
      assert_equal("Foo::Bar", owner&.name)

      entry = @index["@b"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::Class, owner)
      assert_equal("Foo::Bar", owner&.name)
    end

    def test_empty_name_instance_variables
      index(<<~RUBY)
        module Foo
          class Bar
            def initialize
              @ = 123
            end
          end
        end
      RUBY

      refute_entry("@")
    end

    def test_class_instance_variables
      index(<<~RUBY)
        module Foo
          class Bar
            @a = 123

            class << self
              def hello
                @b = 123
              end

              @c = 123
            end
          end
        end
      RUBY

      assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6")

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::SingletonClass, owner)
      assert_equal("Foo::Bar::<Class:Bar>", owner&.name)

      assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:6-8:6-10")

      entry = @index["@b"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::SingletonClass, owner)
      assert_equal("Foo::Bar::<Class:Bar>", owner&.name)

      assert_entry("@c", Entry::InstanceVariable, "/fake/path/foo.rb:9-6:9-8")

      entry = @index["@c"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::SingletonClass, owner)
      assert_equal("Foo::Bar::<Class:Bar>::<Class:<Class:Bar>>", owner&.name)
    end

    def test_top_level_instance_variables
      index(<<~RUBY)
        @a = 123
      RUBY

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      assert_nil(entry.owner)
    end

    def test_class_instance_variables_inside_self_method
      index(<<~RUBY)
        class Foo
          def self.bar
            @a = 123
          end
        end
      RUBY

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::SingletonClass, owner)
      assert_equal("Foo::<Class:Foo>", owner&.name)
    end

    def test_instance_variable_inside_dynamic_method_declaration
      index(<<~RUBY)
        class Foo
          def something.bar
            @a = 123
          end
        end
      RUBY

      # If the surrounding method is being defined on any dynamic value that isn't `self`, then we attribute the
      # instance variable to the wrong owner since there's no way to understand that statically
      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::Class, owner)
      assert_equal("Foo", owner&.name)
    end

    def test_module_function_does_not_impact_instance_variables
      # One possible way of implementing `module_function` would be to push a fake singleton class to the stack, so that
      # methods are inserted into it. However, that would be incorrect because it would then bind instance variables to
      # the wrong type. This test is here to prevent that from happening.
      index(<<~RUBY)
        module Foo
          module_function

          def something; end

          @a = 123
        end
      RUBY

      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      owner = entry.owner
      assert_instance_of(Entry::SingletonClass, owner)
      assert_equal("Foo::<Class:Foo>", owner&.name)
    end

    def test_class_instance_variable_comments
      index(<<~RUBY)
          class Foo
            # Documentation for @a
            @a = "Hello" #: String
            @b = "World" # trailing comment
            @c = "!"
          end
        end
      RUBY

      assert_entry("@a", Entry::InstanceVariable, "/fake/path/foo.rb:2-4:2-6")
      entry = @index["@a"]&.first #: as Entry::InstanceVariable
      assert_equal("Documentation for @a", entry.comments)

      assert_entry("@b", Entry::InstanceVariable, "/fake/path/foo.rb:3-4:3-6")
      entry = @index["@b"]&.first #: as Entry::InstanceVariable
      assert_empty(entry.comments)

      assert_entry("@c", Entry::InstanceVariable, "/fake/path/foo.rb:4-4:4-6")
      entry = @index["@c"]&.first #: as Entry::InstanceVariable
      assert_empty(entry.comments)
    end
  end
end