ivar 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithIvarBlock
6
+ include Ivar::Checked
7
+
8
+ # Pre-declare instance variables with a block that runs before initialization
9
+ ivar :@side do
10
+ @pickles = true
11
+ @condiments = []
12
+ end
13
+
14
+ def initialize
15
+ @bread = "wheat"
16
+ @cheese = "muenster"
17
+ # Note: @pickles is already set to true by the ivar block
18
+ # Note: @condiments is already initialized to an empty array by the ivar block
19
+ @condiments << "mayo" if !@pickles
20
+ @condiments << "mustard"
21
+ # Note: @side is not set here, but it's pre-initialized to nil
22
+ end
23
+
24
+ def to_s
25
+ result = "A #{@bread} sandwich with #{@cheese}"
26
+ result += " and #{@condiments.join(", ")}" unless @condiments.empty?
27
+ result += " with pickles" if @pickles
28
+ result += " and a side of #{@side}" if @side
29
+ result
30
+ end
31
+
32
+ def add_side(side)
33
+ @side = side
34
+ end
35
+ end
36
+
37
+ # Create a sandwich - this will automatically check instance variables
38
+ sandwich = SandwichWithIvarBlock.new
39
+ puts sandwich
40
+
41
+ # Add a side
42
+ sandwich.add_side("chips")
43
+ puts sandwich
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithIvarMacro
6
+ include Ivar::Checked
7
+
8
+ # Pre-declare only instance variables that might be referenced before being set
9
+ # You don't need to include variables that are always set in initialize
10
+ ivar :@side
11
+
12
+ def initialize
13
+ @bread = "wheat"
14
+ @cheese = "muenster"
15
+ @condiments = %w[mayo mustard]
16
+ # NOTE: @side is not set here, but it's pre-initialized to nil
17
+ end
18
+
19
+ def to_s
20
+ result = "A #{@bread} sandwich with #{@cheese} and #{@condiments.join(", ")}"
21
+ # This won't trigger a warning because @side is pre-initialized
22
+ result += " and a side of #{@side}" if @side
23
+ result
24
+ end
25
+
26
+ def add_side(side)
27
+ @side = side
28
+ end
29
+ end
30
+
31
+ # Create a sandwich - this will automatically check instance variables
32
+ sandwich = SandwichWithIvarMacro.new
33
+ puts sandwich
34
+
35
+ # Add a side and print again
36
+ sandwich.add_side("chips")
37
+ puts sandwich
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ivar"
4
+
5
+ class SandwichWithKwarg
6
+ include Ivar::Checked
7
+
8
+ # Pre-declare instance variables to be initialized from keyword arguments
9
+ ivar kwarg: [:@bread, :@cheese, :@condiments]
10
+
11
+ def initialize(pickles: false, side: nil)
12
+ # Note: @bread, @cheese, and @condiments are already set from keyword arguments
13
+ # We only need to handle the remaining keyword arguments
14
+ @pickles = pickles
15
+ @side = side
16
+ end
17
+
18
+ def to_s
19
+ result = "A #{@bread} sandwich with #{@cheese}"
20
+ result += " and #{@condiments.join(", ")}" unless @condiments.empty?
21
+ result += " with pickles" if @pickles
22
+ result += " and a side of #{@side}" if @side
23
+ result
24
+ end
25
+ end
26
+
27
+ # Create a sandwich with keyword arguments
28
+ sandwich = SandwichWithKwarg.new(
29
+ bread: "wheat",
30
+ cheese: "muenster",
31
+ condiments: ["mayo", "mustard"],
32
+ side: "chips"
33
+ )
34
+
35
+ puts sandwich # Outputs: A wheat sandwich with muenster and mayo, mustard and a side of chips
36
+
37
+ # Create another sandwich with different keyword arguments
38
+ sandwich2 = SandwichWithKwarg.new(
39
+ bread: "rye",
40
+ cheese: "swiss",
41
+ condiments: ["mustard"],
42
+ pickles: true
43
+ )
44
+
45
+ puts sandwich2 # Outputs: A rye sandwich with swiss and mustard with pickles
data/ivar.gemspec ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/ivar/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ivar"
7
+ spec.version = Ivar::VERSION
8
+ spec.authors = ["Avdi Grimm"]
9
+ spec.email = ["avdi@avdi.codes"]
10
+
11
+ spec.summary = "Automatically check instance variables for typos."
12
+ spec.description = <<~EOF
13
+ Ruby instance variables are so convenient - you don't even need to declare them!
14
+ But... they are also dangerous, because a mispelled variable name results in `nil`
15
+ instead of an error.
16
+
17
+ Why not have the best of both worlds? Ivar lets you use plain-old instance variables,
18
+ and automatically checks for typos.
19
+
20
+ Ivar waits until an instance is created to do the checking, then uses Prism to look
21
+ for variables that don't match what was set in initialization. So it's a little bit
22
+ dynamic, a little bit static. It doesn't encumber your instance variable reads and
23
+ writes with any extra checking. And with the `:warn_once` policy, it won't overwhelm
24
+ you with output.
25
+ EOF
26
+
27
+ spec.homepage = "https://github.com/avdi/ivar"
28
+ spec.license = "MIT"
29
+ spec.required_ruby_version = ">= 3.4.0"
30
+
31
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
32
+
33
+ spec.metadata["homepage_uri"] = spec.homepage
34
+ spec.metadata["source_code_uri"] = "https://github.com/avdi/ivar"
35
+ spec.metadata["changelog_uri"] = "https://github.com/avdi/ivar/blob/main/CHANGELOG.md"
36
+
37
+ # Specify which files should be added to the gem when it is released.
38
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
39
+ spec.files = Dir.chdir(__dir__) do
40
+ `git ls-files -z`.split("\x0").reject do |f|
41
+ (File.expand_path(f) == __FILE__) ||
42
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
43
+ end
44
+ end
45
+ spec.require_paths = ["lib"]
46
+
47
+ # Dependencies
48
+ spec.add_dependency "prism", "~> 1.2"
49
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validation"
4
+ require_relative "macros"
5
+
6
+ module Ivar
7
+ # Module for adding ivar_check_policy to classes
8
+ module CheckPolicy
9
+ # Set the check policy for this class
10
+ # @param policy [Symbol, Policy] The check policy
11
+ # @return [Symbol, Policy] The check policy
12
+ def ivar_check_policy(policy = nil, **options)
13
+ if policy.nil?
14
+ # Getter - return the current policy
15
+ @__ivar_check_policy || Ivar.check_policy
16
+ else
17
+ # Setter - set the policy
18
+ @__ivar_check_policy = options.empty? ? policy : [policy, options]
19
+ end
20
+ end
21
+
22
+ # Hook method called when the module is included
23
+ def inherited(subclass)
24
+ super
25
+ # Copy the check policy to the subclass
26
+ subclass.instance_variable_set(:@__ivar_check_policy, @__ivar_check_policy)
27
+ end
28
+ end
29
+
30
+ # Provides automatic validation for instance variables
31
+ # When included, automatically calls check_ivars after initialization
32
+ module Checked
33
+ # When this module is included in a class, it extends the class
34
+ # with ClassMethods and includes the Validation module
35
+ def self.included(base)
36
+ base.include(Validation)
37
+ base.include(PreInitializeIvars)
38
+ base.extend(ClassMethods)
39
+ base.extend(CheckPolicy)
40
+ base.extend(Macros)
41
+ base.prepend(InstanceMethods)
42
+
43
+ # Set default policy for Checked to :warn
44
+ base.ivar_check_policy(:warn)
45
+ end
46
+
47
+ # Class methods added to the including class
48
+ module ClassMethods
49
+ # Hook method called when the module is included
50
+ def inherited(subclass)
51
+ super
52
+ # Ensure subclasses also get the initialize wrapper
53
+ subclass.prepend(Ivar::Checked::InstanceMethods)
54
+ end
55
+ end
56
+
57
+ # Instance methods that will be prepended to the including class
58
+ module InstanceMethods
59
+ # Wrap the initialize method to automatically call check_ivars
60
+ def initialize(*args, **kwargs, &block)
61
+ # Initialize pre-declared instance variables
62
+ initialize_pre_declared_ivars
63
+ # Execute the initialization block if provided
64
+ execute_ivar_init_block
65
+
66
+ # Process keyword arguments
67
+ remaining_kwargs = initialize_from_kwargs(kwargs)
68
+
69
+ # Call the original initialize method with remaining arguments
70
+ super(*args, **remaining_kwargs, &block)
71
+
72
+ # Automatically check instance variables
73
+ check_ivars
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validation"
4
+
5
+ module Ivar
6
+ # Provides automatic validation for instance variables
7
+ # When included, automatically checks instance variables after initialization
8
+ module Checked
9
+ # When this module is included in a class, it extends the class
10
+ # with ClassMethods and includes the Validation module
11
+ def self.included(base)
12
+ base.include(Validation)
13
+ base.extend(ClassMethods)
14
+ base.prepend(InstanceMethods)
15
+ end
16
+
17
+ # Class methods added to the including class
18
+ module ClassMethods
19
+ # Hook method called when the module is included
20
+ def inherited(subclass)
21
+ super
22
+ # Ensure subclasses also get the initialize wrapper
23
+ subclass.prepend(InstanceMethods)
24
+ end
25
+ end
26
+
27
+ # Instance methods that will be prepended to the including class
28
+ module InstanceMethods
29
+ # Wrap the initialize method to automatically call check_ivars
30
+ def initialize(*args, **kwargs, &block)
31
+ # Call the original initialize method
32
+ super
33
+ # Automatically check instance variables
34
+ # We need to collect all instance variables from the current object
35
+ # and pass them to check_ivars_once to ensure they're all recognized
36
+ check_ivars_once
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ivar
4
+ # Provides macros for working with instance variables
5
+ module Macros
6
+ # When this module is extended, it adds class methods to the extending class
7
+ def self.extended(base)
8
+ # Store pre-declared instance variables for this class
9
+ base.instance_variable_set(:@__ivar_pre_declared_ivars, [])
10
+ # Store initialization block
11
+ base.instance_variable_set(:@__ivar_init_block, nil)
12
+ # Store keyword argument mappings
13
+ base.instance_variable_set(:@__ivar_kwarg_mappings, [])
14
+ end
15
+
16
+ # Declares instance variables that should be pre-initialized to nil
17
+ # before the initializer is called
18
+ # @param ivars [Array<Symbol>] Instance variables to pre-initialize
19
+ # @param kwarg [Array<Symbol>] Instance variables to initialize from keyword arguments
20
+ # @yield Optional block to execute in the context of the instance before initialization
21
+ def ivar(*ivars, kwarg: [], &block)
22
+ # Store the pre-declared instance variables
23
+ pre_declared = instance_variable_get(:@__ivar_pre_declared_ivars) || []
24
+ instance_variable_set(:@__ivar_pre_declared_ivars, pre_declared + ivars)
25
+
26
+ # Store the keyword argument mappings
27
+ kwarg_mappings = instance_variable_get(:@__ivar_kwarg_mappings) || []
28
+ instance_variable_set(:@__ivar_kwarg_mappings, kwarg_mappings + Array(kwarg))
29
+
30
+ # Store the initialization block if provided
31
+ instance_variable_set(:@__ivar_init_block, block) if block
32
+ end
33
+
34
+ # Hook method called when the module is included
35
+ def inherited(subclass)
36
+ super
37
+ # Copy pre-declared instance variables to subclass
38
+ parent_ivars = instance_variable_get(:@__ivar_pre_declared_ivars) || []
39
+ subclass.instance_variable_set(:@__ivar_pre_declared_ivars, parent_ivars.dup)
40
+
41
+ # Copy keyword argument mappings to subclass
42
+ parent_kwarg_mappings = instance_variable_get(:@__ivar_kwarg_mappings) || []
43
+ subclass.instance_variable_set(:@__ivar_kwarg_mappings, parent_kwarg_mappings.dup)
44
+
45
+ # Copy initialization block to subclass
46
+ parent_block = instance_variable_get(:@__ivar_init_block)
47
+ subclass.instance_variable_set(:@__ivar_init_block, parent_block) if parent_block
48
+ end
49
+
50
+ # Get the pre-declared instance variables for this class
51
+ # @return [Array<Symbol>] Pre-declared instance variables
52
+ def ivar_pre_declared
53
+ instance_variable_get(:@__ivar_pre_declared_ivars) || []
54
+ end
55
+
56
+ # Get the keyword argument mappings for this class
57
+ # @return [Array<Symbol>] Keyword argument mappings
58
+ def ivar_kwarg_mappings
59
+ instance_variable_get(:@__ivar_kwarg_mappings) || []
60
+ end
61
+
62
+ # Get the initialization block for this class
63
+ # @return [Proc, nil] The initialization block or nil if none was provided
64
+ def ivar_init_block
65
+ instance_variable_get(:@__ivar_init_block)
66
+ end
67
+ end
68
+
69
+ # Module to pre-initialize instance variables
70
+ module PreInitializeIvars
71
+ # Initialize pre-declared instance variables to nil
72
+ def initialize_pre_declared_ivars
73
+ klass = self.class
74
+ while klass.respond_to?(:ivar_pre_declared)
75
+ klass.ivar_pre_declared.each do |ivar|
76
+ instance_variable_set(ivar, nil) unless instance_variable_defined?(ivar)
77
+ end
78
+ klass = klass.superclass
79
+ end
80
+ end
81
+
82
+ # Execute the initialization block in the context of the instance
83
+ def execute_ivar_init_block
84
+ klass = self.class
85
+ while klass.respond_to?(:ivar_init_block)
86
+ block = klass.ivar_init_block
87
+ instance_eval(&block) if block
88
+ klass = klass.superclass
89
+ end
90
+ end
91
+
92
+ # Get all keyword argument mappings from the class hierarchy
93
+ # @return [Array<Symbol>] All keyword argument mappings
94
+ def all_kwarg_mappings
95
+ mappings = []
96
+ klass = self.class
97
+ while klass.respond_to?(:ivar_kwarg_mappings)
98
+ mappings.concat(klass.ivar_kwarg_mappings)
99
+ klass = klass.superclass
100
+ end
101
+ mappings
102
+ end
103
+
104
+ # Initialize instance variables from keyword arguments
105
+ # @param kwargs [Hash] Keyword arguments
106
+ # @return [Hash] Remaining keyword arguments
107
+ def initialize_from_kwargs(kwargs)
108
+ remaining_kwargs = kwargs.dup
109
+
110
+ all_kwarg_mappings.each do |ivar|
111
+ # Convert @ivar_name to ivar_name for keyword lookup
112
+ key = ivar.to_s.delete_prefix("@").to_sym
113
+
114
+ if remaining_kwargs.key?(key)
115
+ instance_variable_set(ivar, remaining_kwargs[key])
116
+ remaining_kwargs.delete(key)
117
+ end
118
+ end
119
+
120
+ remaining_kwargs
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Ivar
6
+ # Base class for all ivar checking policies
7
+ class Policy
8
+ # Handle unknown instance variables
9
+ # @param unknown_refs [Array<Hash>] References to unknown instance variables
10
+ # @param klass [Class] The class being checked
11
+ # @param allowed_ivars [Array<Symbol>] List of allowed instance variables
12
+ def handle_unknown_ivars(unknown_refs, klass, allowed_ivars)
13
+ raise NotImplementedError, "Subclasses must implement handle_unknown_ivars"
14
+ end
15
+
16
+ # Find the closest match for a variable name
17
+ # @param ivar [Symbol] The variable to find a match for
18
+ # @param known_ivars [Array<Symbol>] List of known variables
19
+ # @return [Symbol, nil] The closest match or nil if none found
20
+ def find_closest_match(ivar, known_ivars)
21
+ finder = DidYouMean::SpellChecker.new(dictionary: known_ivars)
22
+ suggestions = finder.correct(ivar.to_s)
23
+ suggestions.first&.to_sym if suggestions.any?
24
+ end
25
+
26
+ # Format a warning message for an unknown instance variable
27
+ # @param ref [Hash] Reference to an unknown instance variable
28
+ # @param suggestion [Symbol, nil] Suggested correction or nil
29
+ # @return [String] Formatted warning message
30
+ def format_warning(ref, suggestion)
31
+ ivar = ref[:name]
32
+ suggestion_text = suggestion ? "Did you mean: #{suggestion}?" : ""
33
+ "#{ref[:path]}:#{ref[:line]}: warning: unknown instance variable #{ivar}. #{suggestion_text}\n"
34
+ end
35
+ end
36
+
37
+ # Policy that warns about unknown instance variables
38
+ class WarnPolicy < Policy
39
+ # Handle unknown instance variables by emitting warnings
40
+ # @param unknown_refs [Array<Hash>] References to unknown instance variables
41
+ # @param klass [Class] The class being checked
42
+ # @param allowed_ivars [Array<Symbol>] List of allowed instance variables
43
+ def handle_unknown_ivars(unknown_refs, _klass, allowed_ivars)
44
+ unknown_refs.each do |ref|
45
+ ivar = ref[:name]
46
+ suggestion = find_closest_match(ivar, allowed_ivars)
47
+ $stderr.write(format_warning(ref, suggestion))
48
+ end
49
+ end
50
+ end
51
+
52
+ # Policy that warns about unknown instance variables only once per class
53
+ class WarnOncePolicy < Policy
54
+ # Handle unknown instance variables by emitting warnings once per class
55
+ # @param unknown_refs [Array<Hash>] References to unknown instance variables
56
+ # @param klass [Class] The class being checked
57
+ # @param allowed_ivars [Array<Symbol>] List of allowed instance variables
58
+ def handle_unknown_ivars(unknown_refs, klass, allowed_ivars)
59
+ # Skip if this class has already been checked
60
+ return if Ivar.class_checked?(klass)
61
+
62
+ # Emit warnings
63
+ unknown_refs.each do |ref|
64
+ ivar = ref[:name]
65
+ suggestion = find_closest_match(ivar, allowed_ivars)
66
+ $stderr.write(format_warning(ref, suggestion))
67
+ end
68
+
69
+ # Mark this class as having been checked
70
+ Ivar.mark_class_checked(klass)
71
+ end
72
+ end
73
+
74
+ # Policy that raises an exception for unknown instance variables
75
+ class RaisePolicy < Policy
76
+ # Handle unknown instance variables by raising an exception
77
+ # @param unknown_refs [Array<Hash>] References to unknown instance variables
78
+ # @param klass [Class] The class being checked
79
+ # @param allowed_ivars [Array<Symbol>] List of allowed instance variables
80
+ def handle_unknown_ivars(unknown_refs, _klass, allowed_ivars)
81
+ return if unknown_refs.empty?
82
+
83
+ # Get the first unknown reference
84
+ ref = unknown_refs.first
85
+ ivar = ref[:name]
86
+ suggestion = find_closest_match(ivar, allowed_ivars)
87
+ suggestion_text = suggestion ? " Did you mean: #{suggestion}?" : ""
88
+
89
+ # Raise an exception with location information
90
+ message = "#{ref[:path]}:#{ref[:line]}: unknown instance variable #{ivar}.#{suggestion_text}"
91
+ raise NameError, message
92
+ end
93
+ end
94
+
95
+ # Policy that logs unknown instance variables to a logger
96
+ class LogPolicy < Policy
97
+ # Initialize with a logger
98
+ # @param logger [Logger] The logger to use
99
+ def initialize(logger: Logger.new($stderr))
100
+ @logger = logger
101
+ end
102
+
103
+ # Handle unknown instance variables by logging them
104
+ # @param unknown_refs [Array<Hash>] References to unknown instance variables
105
+ # @param klass [Class] The class being checked
106
+ # @param allowed_ivars [Array<Symbol>] List of allowed instance variables
107
+ def handle_unknown_ivars(unknown_refs, _klass, allowed_ivars)
108
+ unknown_refs.each do |ref|
109
+ ivar = ref[:name]
110
+ suggestion = find_closest_match(ivar, allowed_ivars)
111
+ suggestion_text = suggestion ? " Did you mean: #{suggestion}?" : ""
112
+ message = "#{ref[:path]}:#{ref[:line]}: unknown instance variable #{ivar}.#{suggestion_text}"
113
+ @logger.warn(message)
114
+ end
115
+ end
116
+ end
117
+
118
+ # Map of policy symbols to policy classes
119
+ POLICY_CLASSES = {
120
+ warn: WarnPolicy,
121
+ warn_once: WarnOncePolicy,
122
+ raise: RaisePolicy,
123
+ log: LogPolicy
124
+ }.freeze
125
+
126
+ # Get a policy instance from a symbol or policy object
127
+ # @param policy [Symbol, Policy, Array] The policy to get
128
+ # @param options [Hash] Options to pass to the policy constructor
129
+ # @return [Policy] The policy instance
130
+ def self.get_policy(policy, **options)
131
+ return policy if policy.is_a?(Policy)
132
+
133
+ # Handle the case where policy is an array with [policy_name, options]
134
+ if policy.is_a?(Array) && policy.size == 2 && policy[1].is_a?(Hash)
135
+ policy_name, policy_options = policy
136
+ policy_class = POLICY_CLASSES[policy_name]
137
+ raise ArgumentError, "Unknown policy: #{policy_name}" unless policy_class
138
+
139
+ return policy_class.new(**policy_options)
140
+ end
141
+
142
+ policy_class = POLICY_CLASSES[policy]
143
+ raise ArgumentError, "Unknown policy: #{policy}" unless policy_class
144
+
145
+ policy_class.new(**options)
146
+ end
147
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Ivar
6
+ # Analyzes a class to find all instance variables using Prism
7
+ class PrismAnalysis
8
+ attr_reader :ivars
9
+
10
+ def initialize(klass)
11
+ @klass = klass
12
+ @references = nil
13
+ collect_references
14
+ @ivars = unique_ivar_names
15
+ end
16
+
17
+ # Returns a list of hashes each representing a code reference to an ivar
18
+ # Each hash includes var name, path, line number, and column number
19
+ def ivar_references
20
+ @references
21
+ end
22
+
23
+ private
24
+
25
+ def collect_references
26
+ source_files = collect_source_files
27
+ @references = []
28
+
29
+ source_files.each do |file_path|
30
+ code = File.read(file_path)
31
+ result = Prism.parse(code)
32
+ visitor = IvarReferenceVisitor.new(file_path)
33
+ result.value.accept(visitor)
34
+ @references.concat(visitor.references)
35
+ end
36
+ end
37
+
38
+ def unique_ivar_names
39
+ @references.map { |ref| ref[:name] }.uniq.sort
40
+ end
41
+
42
+ def collect_source_files
43
+ # Get all instance methods
44
+ instance_methods = @klass.instance_methods(false) | @klass.private_instance_methods(false)
45
+
46
+ # Collect source files for all methods
47
+ source_files = Set.new
48
+ instance_methods.each do |method_name|
49
+ next unless @klass.instance_method(method_name).source_location
50
+
51
+ source_files << @klass.instance_method(method_name).source_location.first
52
+ end
53
+
54
+ source_files
55
+ end
56
+
57
+ # Visitor that collects instance variable references with location information
58
+ class IvarReferenceVisitor < Prism::Visitor
59
+ attr_reader :references
60
+
61
+ def initialize(file_path)
62
+ super()
63
+ @file_path = file_path
64
+ @references = []
65
+ end
66
+
67
+ def visit_instance_variable_read_node(node)
68
+ add_reference(node)
69
+ true
70
+ end
71
+
72
+ def visit_instance_variable_write_node(node)
73
+ add_reference(node)
74
+ true
75
+ end
76
+
77
+ def visit_instance_variable_operator_write_node(node)
78
+ add_reference(node)
79
+ true
80
+ end
81
+
82
+ def visit_instance_variable_target_node(node)
83
+ add_reference(node)
84
+ true
85
+ end
86
+
87
+ private
88
+
89
+ def add_reference(node)
90
+ location = node.location
91
+ reference = {
92
+ name: node.name.to_sym,
93
+ path: @file_path,
94
+ line: location.start_line,
95
+ column: location.start_column
96
+ }
97
+
98
+ @references << reference
99
+ end
100
+ end
101
+ end
102
+ end