# typed: strict # frozen_string_literal: true module RubyIndexer class Collector extend T::Sig LEAVE_EVENT = T.let(Object.new.freeze, Object) sig { params(index: Index, parse_result: Prism::ParseResult, file_path: String).void } def initialize(index, parse_result, file_path) @index = index @file_path = file_path @stack = T.let([], T::Array[String]) @comments_by_line = T.let( parse_result.comments.to_h do |c| [c.location.start_line, c] end, T::Hash[Integer, Prism::Comment], ) @queue = T.let([], T::Array[Object]) @current_owner = T.let(nil, T.nilable(Entry::Namespace)) super() end sig { params(node: Prism::Node).void } def collect(node) @queue = [node] until @queue.empty? node_or_event = @queue.shift case node_or_event when Prism::ProgramNode @queue << node_or_event.statements when Prism::StatementsNode T.unsafe(@queue).prepend(*node_or_event.body) when Prism::ClassNode add_class_entry(node_or_event) when Prism::ModuleNode add_module_entry(node_or_event) when Prism::MultiWriteNode handle_multi_write_node(node_or_event) when Prism::ConstantPathWriteNode handle_constant_path_write_node(node_or_event) when Prism::ConstantPathOrWriteNode handle_constant_path_or_write_node(node_or_event) when Prism::ConstantPathOperatorWriteNode handle_constant_path_operator_write_node(node_or_event) when Prism::ConstantPathAndWriteNode handle_constant_path_and_write_node(node_or_event) when Prism::ConstantWriteNode handle_constant_write_node(node_or_event) when Prism::ConstantOrWriteNode name = fully_qualify_name(node_or_event.name.to_s) add_constant(node_or_event, name) when Prism::ConstantAndWriteNode name = fully_qualify_name(node_or_event.name.to_s) add_constant(node_or_event, name) when Prism::ConstantOperatorWriteNode name = fully_qualify_name(node_or_event.name.to_s) add_constant(node_or_event, name) when Prism::CallNode handle_call_node(node_or_event) when Prism::DefNode handle_def_node(node_or_event) when LEAVE_EVENT @stack.pop end end end private sig { params(node: Prism::MultiWriteNode).void } def handle_multi_write_node(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 handle_constant_path_write_node(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 handle_constant_path_or_write_node(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 handle_constant_path_operator_write_node(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 handle_constant_path_and_write_node(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 handle_constant_write_node(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { params(node: Prism::CallNode).void } def handle_call_node(node) message = node.message handle_private_constant(node) if message == "private_constant" end sig { params(node: Prism::DefNode).void } def handle_def_node(node) method_name = node.name.to_s comments = collect_comments(node) case node.receiver when nil @index << Entry::InstanceMethod.new( method_name, @file_path, node.location, comments, node.parameters, @current_owner, ) when Prism::SelfNode @index << Entry::SingletonMethod.new( method_name, @file_path, node.location, comments, node.parameters, @current_owner, ) end end sig { params(node: Prism::CallNode).void } def handle_private_constant(node) arguments = node.arguments&.arguments return unless arguments first_argument = arguments.first name = case first_argument when Prism::StringNode first_argument.content when Prism::SymbolNode first_argument.value end return unless name receiver = node.receiver name = "#{receiver.slice}::#{name}" if receiver # The private_constant method does not resolve the constant name. It always points to a constant that needs to # exist in the current namespace entries = @index[fully_qualify_name(name)] entries&.each { |entry| entry.visibility = :private } end sig do params( node: T.any( Prism::ConstantWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOperatorWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode, Prism::ConstantTargetNode, Prism::ConstantPathTargetNode, ), name: String, value: T.nilable(Prism::Node), ).void end def add_constant(node, name, value = nil) value = node.value unless node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode) comments = collect_comments(node) @index << case value when Prism::ConstantReadNode, Prism::ConstantPathNode Entry::UnresolvedAlias.new(value.slice, @stack.dup, name, @file_path, node.location, comments) when Prism::ConstantWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantOperatorWriteNode # If the right hand side is another constant assignment, we need to visit it because that constant has to be # indexed too @queue.prepend(value) Entry::UnresolvedAlias.new(value.name.to_s, @stack.dup, name, @file_path, node.location, comments) when Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathAndWriteNode @queue.prepend(value) Entry::UnresolvedAlias.new(value.target.slice, @stack.dup, name, @file_path, node.location, comments) else Entry::Constant.new(name, @file_path, node.location, comments) end end sig { params(node: Prism::ModuleNode).void } def add_module_entry(node) name = node.constant_path.location.slice unless /^[A-Z:]/.match?(name) @queue << node.body return end comments = collect_comments(node) @current_owner = Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments) @index << @current_owner @stack << name @queue.prepend(node.body, LEAVE_EVENT) end sig { params(node: Prism::ClassNode).void } def add_class_entry(node) name = node.constant_path.location.slice unless /^[A-Z:]/.match?(name) @queue << node.body return end comments = collect_comments(node) superclass = node.superclass parent_class = case superclass when Prism::ConstantReadNode, Prism::ConstantPathNode superclass.slice end @current_owner = Entry::Class.new( fully_qualify_name(name), @file_path, node.location, comments, parent_class, ) @index << @current_owner @stack << name @queue.prepend(node.body, LEAVE_EVENT) end sig { params(node: Prism::Node).returns(T::Array[String]) } def collect_comments(node) comments = [] start_line = node.location.start_line - 1 start_line -= 1 unless @comments_by_line.key?(start_line) start_line.downto(1) do |line| comment = @comments_by_line[line] break unless comment comment_content = comment.location.slice.chomp next if comment_content.match?(RubyIndexer.configuration.magic_comment_regex) comment_content.delete_prefix!("#") comment_content.delete_prefix!(" ") comments.prepend(comment_content) end comments end sig { params(name: String).returns(String) } def fully_qualify_name(name) if @stack.empty? || name.start_with?("::") name else "#{@stack.join("::")}::#{name}" end.delete_prefix("::") end end end