# typed: true # frozen_string_literal: true require_relative "test_case" module RubyIndexer class EnhancementTest < TestCase def teardown super Enhancement.clear end def test_enhancing_indexing_included_hook Class.new(Enhancement) do def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod owner = @listener.current_owner return unless owner return unless call_node.name == :extend arguments = call_node.arguments&.arguments return unless arguments arguments.each do |node| next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) module_name = node.full_name next unless module_name == "ActiveSupport::Concern" @listener.register_included_hook do |index, base| class_methods_name = "#{owner.name}::ClassMethods" if index.indexed?(class_methods_name) singleton = index.existing_or_new_singleton_class(base.name) singleton.mixin_operations << Entry::Include.new(class_methods_name) end end @listener.add_method( "new_method", call_node.location, [Entry::Signature.new([Entry::RequiredParameter.new(name: :a)])], ) rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError, Prism::ConstantPathNode::MissingNodesInConstantPathError # Do nothing end end end index(<<~RUBY) module ActiveSupport module Concern def self.extended(base) base.class_eval("def new_method(a); end") end end end module ActiveRecord module Associations extend ActiveSupport::Concern module ClassMethods def belongs_to(something); end end end class Base include Associations end end class User < ActiveRecord::Base end RUBY assert_equal( [ "User::", "ActiveRecord::Base::", "ActiveRecord::Associations::ClassMethods", "Object::", "BasicObject::", "Class", "Module", "Object", "Kernel", "BasicObject", ], @index.linearized_ancestors_of("User::"), ) assert_entry("new_method", Entry::Method, "/fake/path/foo.rb:10-4:10-33") end def test_enhancing_indexing_configuration_dsl Class.new(Enhancement) do def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod return unless @listener.current_owner name = node.name return unless name == :has_many arguments = node.arguments&.arguments return unless arguments association_name = arguments.first return unless association_name.is_a?(Prism::SymbolNode) @listener.add_method( T.must(association_name.value), association_name.location, [], ) end end index(<<~RUBY) module ActiveSupport module Concern def self.extended(base) base.class_eval("def new_method(a); end") end end end module ActiveRecord module Associations extend ActiveSupport::Concern module ClassMethods def belongs_to(something); end end end class Base include Associations end end class User < ActiveRecord::Base has_many :posts end RUBY assert_entry("posts", Entry::Method, "/fake/path/foo.rb:23-11:23-17") end def test_error_handling_in_on_call_node_enter_enhancement Class.new(Enhancement) do def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod raise "Error" end class << self def name "TestEnhancement" end end end _stdout, stderr = capture_io do index(<<~RUBY) module ActiveSupport module Concern def self.extended(base) base.class_eval("def new_method(a); end") end end end RUBY end assert_match( %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node enter enhancement}, stderr, ) # The module should still be indexed assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5") end def test_error_handling_in_on_call_node_leave_enhancement Class.new(Enhancement) do def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod raise "Error" end class << self def name "TestEnhancement" end end end _stdout, stderr = capture_io do index(<<~RUBY) module ActiveSupport module Concern def self.extended(base) base.class_eval("def new_method(a); end") end end end RUBY end assert_match( %r{Indexing error in /fake/path/foo\.rb with 'TestEnhancement' on call node leave enhancement}, stderr, ) # The module should still be indexed assert_entry("ActiveSupport::Concern", Entry::Module, "/fake/path/foo.rb:1-2:5-5") end def test_advancing_namespace_stack_from_enhancement Class.new(Enhancement) do def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod owner = @listener.current_owner return unless owner case call_node.name when :class_methods @listener.add_module("ClassMethods", call_node.location, call_node.location) when :extend arguments = call_node.arguments&.arguments return unless arguments arguments.each do |node| next unless node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode) module_name = node.full_name next unless module_name == "ActiveSupport::Concern" @listener.register_included_hook do |index, base| class_methods_name = "#{owner.name}::ClassMethods" if index.indexed?(class_methods_name) singleton = index.existing_or_new_singleton_class(base.name) singleton.mixin_operations << Entry::Include.new(class_methods_name) end end end end end def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod return unless call_node.name == :class_methods @listener.pop_namespace_stack end end index(<<~RUBY) module ActiveSupport module Concern end end module MyConcern extend ActiveSupport::Concern class_methods do def foo; end end end class User include MyConcern end RUBY assert_equal( [ "User::", "MyConcern::ClassMethods", "Object::", "BasicObject::", "Class", "Module", "Object", "Kernel", "BasicObject", ], @index.linearized_ancestors_of("User::"), ) refute_nil(@index.resolve_method("foo", "User::")) end def test_creating_anonymous_classes_from_enhancement Class.new(Enhancement) do def on_call_node_enter(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod case call_node.name when :context arguments = call_node.arguments&.arguments first_argument = arguments&.first return unless first_argument.is_a?(Prism::StringNode) @listener.add_class( "", call_node.location, first_argument.location, ) when :subject @listener.add_method("subject", call_node.location, []) end end def on_call_node_leave(call_node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod return unless call_node.name == :context @listener.pop_namespace_stack end end index(<<~RUBY) context "does something" do subject { call_whatever } end RUBY refute_nil(@index.resolve_method("subject", "")) end end end