lib/ivar/targeted_prism_analysis.rb
# frozen_string_literal: true require "prism" module Ivar # Analyzes a class to find instance variable references in specific instance methods # Unlike PrismAnalysis, this targets only the class's own methods (not inherited) # and precisely locates instance variable references within each method definition class TargetedPrismAnalysis attr_reader :ivars, :references def initialize(klass) @klass = klass @references = [] @method_locations = {} collect_method_locations analyze_methods @ivars = unique_ivar_names end # Returns a list of hashes each representing a code reference to an ivar # Each hash includes var name, path, line number, and column number def ivar_references @references end private def unique_ivar_names @references.map { |ref| ref[:name] }.uniq.sort end def collect_method_locations # Get all instance methods defined directly on this class (not inherited) instance_methods = @klass.instance_methods(false) | @klass.private_instance_methods(false) instance_methods.each do |method_name| # Try to get the method from the stash first, then fall back to the current method method_obj = Ivar.get_stashed_method(@klass, method_name) || @klass.instance_method(method_name) next unless method_obj.source_location file_path, line_number = method_obj.source_location @method_locations[method_name] = {path: file_path, line: line_number} end end def analyze_methods # Group methods by file to avoid parsing the same file multiple times methods_by_file = @method_locations.group_by { |_, location| location[:path] } methods_by_file.each do |file_path, methods_in_file| code = File.read(file_path) result = Prism.parse(code) methods_in_file.each do |method_name, location| visitor = MethodTargetedInstanceVariableReferenceVisitor.new( file_path, method_name, location[:line] ) result.value.accept(visitor) @references.concat(visitor.references) end end end end # Visitor that collects instance variable references within a specific method definition class MethodTargetedInstanceVariableReferenceVisitor < Prism::Visitor attr_reader :references def initialize(file_path, target_method_name, target_line) super() @file_path = file_path @target_method_name = target_method_name @target_line = target_line @references = [] @in_target_method = false end # Only visit the method definition we're targeting def visit_def_node(node) # Check if this is our target method if node.name.to_sym == @target_method_name && node.location.start_line == @target_line # Found our target method, now collect all instance variable references within it collector = IvarCollector.new(@file_path, @target_method_name) node.body&.accept(collector) @references = collector.references false else # Sometimes methods are found inside other methods... node.body&.accept(self) true end end end # Helper visitor that collects all instance variable references class IvarCollector < Prism::Visitor attr_reader :references def initialize(file_path, method_name) super() @file_path = file_path @method_name = method_name @references = [] end def visit_instance_variable_read_node(node) add_reference(node) true end def visit_instance_variable_write_node(node) add_reference(node) true end def visit_instance_variable_operator_write_node(node) add_reference(node) true end def visit_instance_variable_target_node(node) add_reference(node) true end private def add_reference(node) location = node.location reference = { name: node.name.to_sym, path: @file_path, line: location.start_line, column: location.start_column, method: @method_name } @references << reference end end end