# typed: strict # frozen_string_literal: true module RubyIndexer class IndexVisitor < Prism::Visitor extend T::Sig 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], ) super() end sig { override.params(node: Prism::ClassNode).void } def visit_class_node(node) add_class_entry(node) end sig { override.params(node: Prism::ModuleNode).void } def visit_module_node(node) add_module_entry(node) end sig { override.params(node: Prism::MultiWriteNode).void } def visit_multi_write_node(node) value = node.value values = value.is_a?(Prism::ArrayNode) && value.opening_loc ? value.elements : [] node.targets.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 { override.params(node: Prism::ConstantPathWriteNode).void } def visit_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 { override.params(node: Prism::ConstantPathOrWriteNode).void } def visit_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 { override.params(node: Prism::ConstantPathOperatorWriteNode).void } def visit_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 { override.params(node: Prism::ConstantPathAndWriteNode).void } def visit_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 { override.params(node: Prism::ConstantWriteNode).void } def visit_constant_write_node(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { override.params(node: Prism::ConstantOrWriteNode).void } def visit_constant_or_write_node(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { override.params(node: Prism::ConstantAndWriteNode).void } def visit_constant_and_write_node(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { override.params(node: Prism::ConstantOperatorWriteNode).void } def visit_constant_operator_write_node(node) name = fully_qualify_name(node.name.to_s) add_constant(node, name) end sig { override.params(node: Prism::CallNode).void } def visit_call_node(node) message = node.message handle_private_constant(node) if message == "private_constant" end sig { override.params(node: Prism::DefNode).void } def visit_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) when Prism::SelfNode @index << Entry::SingletonMethod.new(method_name, @file_path, node.location, comments, node.parameters) end end private 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 visit(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 visit(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 return visit_child_nodes(node) unless /^[A-Z:]/.match?(name) comments = collect_comments(node) @index << Entry::Module.new(fully_qualify_name(name), @file_path, node.location, comments) @stack << name visit_child_nodes(node) @stack.pop end sig { params(node: Prism::ClassNode).void } def add_class_entry(node) name = node.constant_path.location.slice return visit_child_nodes(node) unless /^[A-Z:]/.match?(name) comments = collect_comments(node) superclass = node.superclass parent_class = case superclass when Prism::ConstantReadNode, Prism::ConstantPathNode superclass.slice end @index << Entry::Class.new(fully_qualify_name(name), @file_path, node.location, comments, parent_class) @stack << name visit(node.body) @stack.pop 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.unshift(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