# typed: strict # frozen_string_literal: true module RubyIndexer class DeclarationListener extend T::Sig OBJECT_NESTING = T.let(["Object"].freeze, T::Array[String]) BASIC_OBJECT_NESTING = T.let(["BasicObject"].freeze, T::Array[String]) sig { returns(T::Array[String]) } attr_reader :indexing_errors sig do params( index: Index, dispatcher: Prism::Dispatcher, parse_result: Prism::ParseResult, uri: URI::Generic, collect_comments: T::Boolean, ).void end def initialize(index, dispatcher, parse_result, uri, collect_comments: false) @index = index @uri = uri @enhancements = T.let(Enhancement.all(self), T::Array[Enhancement]) @visibility_stack = T.let([VisibilityScope.public_scope], T::Array[VisibilityScope]) @comments_by_line = T.let( parse_result.comments.to_h do |c| [c.location.start_line, c] end, T::Hash[Integer, Prism::Comment], ) @inside_def = T.let(false, T::Boolean) @code_units_cache = T.let( parse_result.code_units_cache(@index.configuration.encoding), T.any(T.proc.params(arg0: Integer).returns(Integer), Prism::CodeUnitsCache), ) @source_lines = T.let(parse_result.source.lines, T::Array[String]) # The nesting stack we're currently inside. Used to determine the fully qualified name of constants, but only # stored by unresolved aliases which need the original nesting to be lazily resolved @stack = T.let([], T::Array[String]) # A stack of namespace entries that represent where we currently are. Used to properly assign methods to an owner @owner_stack = T.let([], T::Array[Entry::Namespace]) @indexing_errors = T.let([], T::Array[String]) @collect_comments = collect_comments dispatcher.register( self, :on_class_node_enter, :on_class_node_leave, :on_module_node_enter, :on_module_node_leave, :on_singleton_class_node_enter, :on_singleton_class_node_leave, :on_def_node_enter, :on_def_node_leave, :on_call_node_enter, :on_call_node_leave, :on_multi_write_node_enter, :on_constant_path_write_node_enter, :on_constant_path_or_write_node_enter, :on_constant_path_operator_write_node_enter, :on_constant_path_and_write_node_enter, :on_constant_write_node_enter, :on_constant_or_write_node_enter, :on_constant_and_write_node_enter, :on_constant_operator_write_node_enter, :on_global_variable_and_write_node_enter, :on_global_variable_operator_write_node_enter, :on_global_variable_or_write_node_enter, :on_global_variable_target_node_enter, :on_global_variable_write_node_enter, :on_instance_variable_write_node_enter, :on_instance_variable_and_write_node_enter, :on_instance_variable_operator_write_node_enter, :on_instance_variable_or_write_node_enter, :on_instance_variable_target_node_enter, :on_alias_method_node_enter, :on_class_variable_and_write_node_enter, :on_class_variable_operator_write_node_enter, :on_class_variable_or_write_node_enter, :on_class_variable_target_node_enter, :on_class_variable_write_node_enter, ) end sig { params(node: Prism::ClassNode).void } def on_class_node_enter(node) constant_path = node.constant_path superclass = node.superclass nesting = actual_nesting(constant_path.slice) parent_class = case superclass when Prism::ConstantReadNode, Prism::ConstantPathNode superclass.slice else case nesting when OBJECT_NESTING # When Object is reopened, its parent class should still be the top-level BasicObject "::BasicObject" when BASIC_OBJECT_NESTING # When BasicObject is reopened, its parent class should still be nil nil else # Otherwise, the parent class should be the top-level Object "::Object" end end add_class( nesting, node.location, constant_path.location, parent_class_name: parent_class, comments: collect_comments(node), ) end sig { params(node: Prism::ClassNode).void } def on_class_node_leave(node) pop_namespace_stack end sig { params(node: Prism::ModuleNode).void } def on_module_node_enter(node) constant_path = node.constant_path add_module(constant_path.slice, node.location, constant_path.location, comments: collect_comments(node)) end sig { params(node: Prism::ModuleNode).void } def on_module_node_leave(node) pop_namespace_stack end sig { params(node: Prism::SingletonClassNode).void } def on_singleton_class_node_enter(node) @visibility_stack.push(VisibilityScope.public_scope) current_owner = @owner_stack.last if current_owner expression = node.expression name = (expression.is_a?(Prism::SelfNode) ? "" : "") real_nesting = actual_nesting(name) existing_entries = T.cast(@index[real_nesting.join("::")], T.nilable(T::Array[Entry::SingletonClass])) if existing_entries entry = T.must(existing_entries.first) entry.update_singleton_information( Location.from_prism_location(node.location, @code_units_cache), Location.from_prism_location(expression.location, @code_units_cache), collect_comments(node), ) else entry = Entry::SingletonClass.new( real_nesting, @uri, Location.from_prism_location(node.location, @code_units_cache), Location.from_prism_location(expression.location, @code_units_cache), collect_comments(node), nil, ) @index.add(entry, skip_prefix_tree: true) end @owner_stack << entry @stack << name end end sig { params(node: Prism::SingletonClassNode).void } def on_singleton_class_node_leave(node) pop_namespace_stack end sig { params(node: Prism::MultiWriteNode).void } def on_multi_write_node_enter(node) value = node.value values = value.is_a?(Prism::ArrayNode) && value.opening_loc ? value.elements : [] [*node.lefts, *node.rest, *node.rights].each_with_index do |target, i| current_value = values[i] # The moment we find a splat on the right hand side of the assignment, we can no longer figure out which value # gets assigned to what values.clear if current_value.is_a?(Prism::SplatNode) case target when Prism::ConstantTargetNode add_constant(target, fully_qualify_name(target.name.to_s), current_value) when Prism::ConstantPathTargetNode add_constant(target, fully_qualify_name(target.slice), current_value) end end end sig { params(node: Prism::ConstantPathWriteNode).void } def on_constant_path_write_node_enter(node) # ignore variable constants like `var::FOO` or `self.class::FOO` target = node.target return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) name = fully_qualify_name(target.location.slice) add_constant(node, name) end sig { params(node: Prism::ConstantPathOrWriteNode).void } def on_constant_path_or_write_node_enter(node) # ignore variable constants like `var::FOO` or `self.class::FOO` target = node.target return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) name = fully_qualify_name(target.location.slice) add_constant(node, name) end sig { params(node: Prism::ConstantPathOperatorWriteNode).void } def on_constant_path_operator_write_node_enter(node) # ignore variable constants like `var::FOO` or `self.class::FOO` target = node.target return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) name = fully_qualify_name(target.location.slice) add_constant(node, name) end sig { params(node: Prism::ConstantPathAndWriteNode).void } def on_constant_path_and_write_node_enter(node) # ignore variable constants like `var::FOO` or `self.class::FOO` target = node.target return unless target.parent.nil? || target.parent.is_a?(Prism::ConstantReadNode) name = fully_qualify_name(target.location.slice) add_constant(node, name) end sig { params(node: Prism::ConstantWriteNode).void } def on_constant_write_node_enter(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { params(node: Prism::ConstantOrWriteNode).void } def on_constant_or_write_node_enter(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { params(node: Prism::ConstantAndWriteNode).void } def on_constant_and_write_node_enter(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { params(node: Prism::ConstantOperatorWriteNode).void } def on_constant_operator_write_node_enter(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { params(node: Prism::CallNode).void } def on_call_node_enter(node) message = node.name case message when :private_constant handle_private_constant(node) when :attr_reader handle_attribute(node, reader: true, writer: false) when :attr_writer handle_attribute(node, reader: false, writer: true) when :attr_accessor handle_attribute(node, reader: true, writer: true) when :alias_method handle_alias_method(node) when :include, :prepend, :extend handle_module_operation(node, message) when :public @visibility_stack.push(VisibilityScope.public_scope) when :protected @visibility_stack.push(VisibilityScope.new(visibility: Entry::Visibility::PROTECTED)) when :private @visibility_stack.push(VisibilityScope.new(visibility: Entry::Visibility::PRIVATE)) when :module_function handle_module_function(node) when :private_class_method handle_private_class_method(node) end @enhancements.each do |enhancement| enhancement.on_call_node_enter(node) rescue StandardError => e @indexing_errors << <<~MSG Indexing error in #{@uri} with '#{enhancement.class.name}' on call node enter enhancement: #{e.message} MSG end end sig { params(node: Prism::CallNode).void } def on_call_node_leave(node) message = node.name case message when :public, :protected, :private, :private_class_method # We want to restore the visibility stack when we leave a method definition with a visibility modifier # e.g. `private def foo; end` if node.arguments&.arguments&.first&.is_a?(Prism::DefNode) @visibility_stack.pop end end @enhancements.each do |enhancement| enhancement.on_call_node_leave(node) rescue StandardError => e @indexing_errors << <<~MSG Indexing error in #{@uri} with '#{enhancement.class.name}' on call node leave enhancement: #{e.message} MSG end end sig { params(node: Prism::DefNode).void } def on_def_node_enter(node) owner = @owner_stack.last return unless owner @inside_def = true method_name = node.name.to_s comments = collect_comments(node) scope = current_visibility_scope case node.receiver when nil location = Location.from_prism_location(node.location, @code_units_cache) name_location = Location.from_prism_location(node.name_loc, @code_units_cache) signatures = [Entry::Signature.new(list_params(node.parameters))] @index.add(Entry::Method.new( method_name, @uri, location, name_location, comments, signatures, scope.visibility, owner, )) if scope.module_func singleton = @index.existing_or_new_singleton_class(owner.name) @index.add(Entry::Method.new( method_name, @uri, location, name_location, comments, signatures, Entry::Visibility::PUBLIC, singleton, )) end when Prism::SelfNode singleton = @index.existing_or_new_singleton_class(owner.name) @index.add(Entry::Method.new( method_name, @uri, Location.from_prism_location(node.location, @code_units_cache), Location.from_prism_location(node.name_loc, @code_units_cache), comments, [Entry::Signature.new(list_params(node.parameters))], scope.visibility, singleton, )) @owner_stack << singleton end end sig { params(node: Prism::DefNode).void } def on_def_node_leave(node) @inside_def = false if node.receiver.is_a?(Prism::SelfNode) @owner_stack.pop end end sig { params(node: Prism::GlobalVariableAndWriteNode).void } def on_global_variable_and_write_node_enter(node) handle_global_variable(node, node.name_loc) end sig { params(node: Prism::GlobalVariableOperatorWriteNode).void } def on_global_variable_operator_write_node_enter(node) handle_global_variable(node, node.name_loc) end sig { params(node: Prism::GlobalVariableOrWriteNode).void } def on_global_variable_or_write_node_enter(node) handle_global_variable(node, node.name_loc) end sig { params(node: Prism::GlobalVariableTargetNode).void } def on_global_variable_target_node_enter(node) handle_global_variable(node, node.location) end sig { params(node: Prism::GlobalVariableWriteNode).void } def on_global_variable_write_node_enter(node) handle_global_variable(node, node.name_loc) end sig { params(node: Prism::InstanceVariableWriteNode).void } def on_instance_variable_write_node_enter(node) handle_instance_variable(node, node.name_loc) end sig { params(node: Prism::InstanceVariableAndWriteNode).void } def on_instance_variable_and_write_node_enter(node) handle_instance_variable(node, node.name_loc) end sig { params(node: Prism::InstanceVariableOperatorWriteNode).void } def on_instance_variable_operator_write_node_enter(node) handle_instance_variable(node, node.name_loc) end sig { params(node: Prism::InstanceVariableOrWriteNode).void } def on_instance_variable_or_write_node_enter(node) handle_instance_variable(node, node.name_loc) end sig { params(node: Prism::InstanceVariableTargetNode).void } def on_instance_variable_target_node_enter(node) handle_instance_variable(node, node.location) end sig { params(node: Prism::AliasMethodNode).void } def on_alias_method_node_enter(node) method_name = node.new_name.slice comments = collect_comments(node) @index.add( Entry::UnresolvedMethodAlias.new( method_name, node.old_name.slice, @owner_stack.last, @uri, Location.from_prism_location(node.new_name.location, @code_units_cache), comments, ), ) end sig { params(node: Prism::ClassVariableAndWriteNode).void } def on_class_variable_and_write_node_enter(node) handle_class_variable(node, node.name_loc) end sig { params(node: Prism::ClassVariableOperatorWriteNode).void } def on_class_variable_operator_write_node_enter(node) handle_class_variable(node, node.name_loc) end sig { params(node: Prism::ClassVariableOrWriteNode).void } def on_class_variable_or_write_node_enter(node) handle_class_variable(node, node.name_loc) end sig { params(node: Prism::ClassVariableTargetNode).void } def on_class_variable_target_node_enter(node) handle_class_variable(node, node.location) end sig { params(node: Prism::ClassVariableWriteNode).void } def on_class_variable_write_node_enter(node) handle_class_variable(node, node.name_loc) end sig do params( name: String, node_location: Prism::Location, signatures: T::Array[Entry::Signature], visibility: Entry::Visibility, comments: T.nilable(String), ).void end def add_method(name, node_location, signatures, visibility: Entry::Visibility::PUBLIC, comments: nil) location = Location.from_prism_location(node_location, @code_units_cache) @index.add(Entry::Method.new( name, @uri, location, location, comments, signatures, visibility, @owner_stack.last, )) end sig do params( name: String, full_location: Prism::Location, name_location: Prism::Location, comments: T.nilable(String), ).void end def add_module(name, full_location, name_location, comments: nil) location = Location.from_prism_location(full_location, @code_units_cache) name_loc = Location.from_prism_location(name_location, @code_units_cache) entry = Entry::Module.new( actual_nesting(name), @uri, location, name_loc, comments, ) advance_namespace_stack(name, entry) end sig do params( name_or_nesting: T.any(String, T::Array[String]), full_location: Prism::Location, name_location: Prism::Location, parent_class_name: T.nilable(String), comments: T.nilable(String), ).void end def add_class(name_or_nesting, full_location, name_location, parent_class_name: nil, comments: nil) nesting = name_or_nesting.is_a?(Array) ? name_or_nesting : actual_nesting(name_or_nesting) entry = Entry::Class.new( nesting, @uri, Location.from_prism_location(full_location, @code_units_cache), Location.from_prism_location(name_location, @code_units_cache), comments, parent_class_name, ) advance_namespace_stack(T.must(nesting.last), entry) end sig { params(block: T.proc.params(index: Index, base: Entry::Namespace).void).void } def register_included_hook(&block) owner = @owner_stack.last return unless owner @index.register_included_hook(owner.name) do |index, base| block.call(index, base) end end sig { void } def pop_namespace_stack @stack.pop @owner_stack.pop @visibility_stack.pop end sig { returns(T.nilable(Entry::Namespace)) } def current_owner @owner_stack.last end private sig do params( node: T.any( Prism::GlobalVariableAndWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableTargetNode, Prism::GlobalVariableWriteNode, ), loc: Prism::Location, ).void end def handle_global_variable(node, loc) name = node.name.to_s comments = collect_comments(node) @index.add(Entry::GlobalVariable.new( name, @uri, Location.from_prism_location(loc, @code_units_cache), comments, )) end sig do params( node: T.any( Prism::ClassVariableAndWriteNode, Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode, Prism::ClassVariableTargetNode, Prism::ClassVariableWriteNode, ), loc: Prism::Location, ).void end def handle_class_variable(node, loc) name = node.name.to_s # Ignore incomplete class variable names, which aren't valid Ruby syntax. # This could occur if the code is in an incomplete or temporary state. return if name == "@@" comments = collect_comments(node) owner = @owner_stack.last # set the class variable's owner to the attached context when defined within a singleton scope. if owner.is_a?(Entry::SingletonClass) owner = @owner_stack.reverse.find { |entry| !entry.name.include?("