necropsy 0.1.0 → 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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +33 -0
  3. data/CHANGELOG.md +18 -0
  4. data/MEASUREMENTS.md +85 -0
  5. data/README.md +60 -8
  6. data/Rakefile +3 -1
  7. data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +12 -2
  8. data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +5 -1
  9. data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +68 -14
  10. data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +107 -12
  11. data/lib/necropsy/analyzers/static/cha.rb +10 -6
  12. data/lib/necropsy/analyzers/static/name_resolution.rb +29 -20
  13. data/lib/necropsy/analyzers/static/rta.rb +19 -3
  14. data/lib/necropsy/ast_scanner/call_recording.rb +156 -0
  15. data/lib/necropsy/ast_scanner/dsl_macros.rb +142 -0
  16. data/lib/necropsy/ast_scanner/method_definitions.rb +176 -0
  17. data/lib/necropsy/ast_scanner/references.rb +81 -0
  18. data/lib/necropsy/ast_scanner/ruby_semantics.rb +177 -0
  19. data/lib/necropsy/ast_scanner/traversal.rb +184 -0
  20. data/lib/necropsy/ast_scanner/value_definitions.rb +68 -0
  21. data/lib/necropsy/ast_scanner.rb +24 -560
  22. data/lib/necropsy/bench/evaluator.rb +29 -10
  23. data/lib/necropsy/cache/scan_cache.rb +20 -12
  24. data/lib/necropsy/cli.rb +88 -32
  25. data/lib/necropsy/confidence/scorer.rb +97 -19
  26. data/lib/necropsy/configuration.rb +125 -7
  27. data/lib/necropsy/diagnostics.rb +202 -0
  28. data/lib/necropsy/entry_points/plain.rb +19 -2
  29. data/lib/necropsy/entry_points/rails.rb +121 -85
  30. data/lib/necropsy/graph/call_graph.rb +203 -21
  31. data/lib/necropsy/guardrail/baseline.rb +14 -5
  32. data/lib/necropsy/guardrail/diff.rb +5 -2
  33. data/lib/necropsy/guardrail/quarantine.rb +9 -3
  34. data/lib/necropsy/models.rb +19 -11
  35. data/lib/necropsy/project.rb +49 -3
  36. data/lib/necropsy/reachability/engine.rb +31 -7
  37. data/lib/necropsy/report.rb +44 -14
  38. data/lib/necropsy/reporter.rb +11 -5
  39. data/lib/necropsy/runner.rb +33 -4
  40. data/lib/necropsy/trace_point_runtime.rb +19 -0
  41. data/lib/necropsy/version.rb +1 -1
  42. data/lib/necropsy.rb +4 -0
  43. data/script/measure.rb +20 -0
  44. metadata +22 -2
@@ -6,7 +6,7 @@ module Necropsy
6
6
  class CHA < Analyzer
7
7
  def analyze(graph, _project)
8
8
  edge_evidences = graph.call_sites.flat_map do |site|
9
- resolve(graph, site).map do |candidate|
9
+ candidates(graph, site).map do |candidate|
10
10
  EdgeEvidence.new(
11
11
  caller_id: site.caller_id,
12
12
  callee_id: candidate.id,
@@ -36,9 +36,7 @@ module Necropsy
36
36
  )
37
37
  end
38
38
 
39
- private
40
-
41
- def resolve(graph, site)
39
+ def candidates(graph, site)
42
40
  case site.receiver_kind
43
41
  when :constant
44
42
  constant_targets(graph, site)
@@ -47,15 +45,21 @@ module Necropsy
47
45
  when :self, :implicit
48
46
  self_targets(graph, site)
49
47
  else
50
- graph.candidate_nodes(site.message)
48
+ graph.resolve_call_site(site)
51
49
  end.uniq(&:id)
52
50
  end
53
51
 
52
+ private
53
+
54
54
  def constant_targets(graph, site)
55
55
  receiver_candidates(site).flat_map do |owner|
56
- owners_for_lookup(graph, owner, include_descendants: false).filter_map do |candidate_owner|
56
+ targets = owners_for_lookup(graph, owner, include_descendants: false).filter_map do |candidate_owner|
57
57
  graph.nodes["#{candidate_owner}.#{site.message}"]
58
58
  end
59
+ extended = Array(graph.class_info(owner)&.extends).filter_map do |mod|
60
+ graph.nodes["#{mod}##{site.message}"]
61
+ end
62
+ targets + extended
59
63
  end
60
64
  end
61
65
 
@@ -5,24 +5,19 @@ module Necropsy
5
5
  module Static
6
6
  class NameResolution < Analyzer
7
7
  def analyze(graph, _project)
8
- edge_evidences = graph.call_sites.flat_map do |site|
9
- graph.resolve_call_site(site).map do |candidate|
10
- EdgeEvidence.new(
11
- caller_id: site.caller_id,
12
- callee_id: candidate.id,
13
- evidence: evidence(
14
- kind: :call_edge,
15
- details: "Name resolution at #{site.file}:#{site.line}",
16
- metadata: site.to_h
17
- )
18
- )
19
- end
8
+ edge_evidences = []
9
+ uncertainties = {}
10
+ graph.call_sites.each do |site|
11
+ candidates = graph.resolve_call_site(site)
12
+ fallback = graph.fallback_resolution?(site, resolved: candidates)
13
+ edge_evidences.concat(edge_evidences_for(site, candidates, fallback))
14
+ record_unresolved_uncertainty(graph, site, candidates, uncertainties)
20
15
  end
21
16
 
22
17
  AnalyzerResult.new(
23
18
  edge_evidences: edge_evidences,
24
19
  alive_evidences: [],
25
- uncertainties: unresolved_uncertainties(graph),
20
+ uncertainties: uncertainties,
26
21
  observation: {}
27
22
  )
28
23
  end
@@ -38,15 +33,29 @@ module Necropsy
38
33
 
39
34
  private
40
35
 
41
- def unresolved_uncertainties(graph)
42
- graph.call_sites.each_with_object({}) do |site, memo|
43
- next if site.dynamic
44
- next unless graph.resolve_call_site(site).empty? && site.receiver_kind == :unknown
45
-
46
- memo[site.caller_id] ||= []
47
- memo[site.caller_id] << "Unknown receiver for #{site.message} at #{site.file}:#{site.line}"
36
+ def edge_evidences_for(site, candidates, fallback)
37
+ candidates.map do |candidate|
38
+ EdgeEvidence.new(
39
+ caller_id: site.caller_id,
40
+ callee_id: candidate.id,
41
+ evidence: evidence(
42
+ kind: :call_edge,
43
+ details: "Name resolution#{' fallback' if fallback} at #{site.file}:#{site.line}",
44
+ weight: fallback ? 0.35 : 1.0,
45
+ metadata: site.to_h
46
+ )
47
+ )
48
48
  end
49
49
  end
50
+
51
+ def record_unresolved_uncertainty(graph, site, candidates, uncertainties)
52
+ return if site.dynamic || candidates.any?
53
+ return if graph.candidate_nodes(site.message).empty? && site.receiver_kind != :unknown
54
+
55
+ uncertainties[site.caller_id] ||= []
56
+ uncertainties[site.caller_id] <<
57
+ "Unknown receiver for #{site.message} at #{site.file}:#{site.line}; dispatch may be ambiguous"
58
+ end
50
59
  end
51
60
  end
52
61
  end
@@ -13,8 +13,9 @@ module Necropsy
13
13
  COMPARISON_MESSAGES = %w[< <= > >= between? clamp sort sort_by max min].freeze
14
14
 
15
15
  def analyze(graph, _project)
16
- edge_evidences = expanded_call_sites(graph).flat_map do |site|
17
- graph.resolve_call_site(site, rta: true).map do |candidate|
16
+ sites = expanded_call_sites(graph)
17
+ edge_evidences = sites.flat_map do |site|
18
+ rta_candidates(graph, site).map do |candidate|
18
19
  EdgeEvidence.new(
19
20
  caller_id: site.caller_id,
20
21
  callee_id: candidate.id,
@@ -31,7 +32,7 @@ module Necropsy
31
32
  edge_evidences: edge_evidences,
32
33
  alive_evidences: [],
33
34
  uncertainties: {},
34
- observation: {}
35
+ observation: { 'rta' => { 'analyzed_sites' => sites.map(&:to_h) } }
35
36
  )
36
37
  end
37
38
 
@@ -73,6 +74,21 @@ module Necropsy
73
74
  messages << 'to_s' if %w[puts print warn].include?(message)
74
75
  messages
75
76
  end
77
+
78
+ private
79
+
80
+ def rta_candidates(graph, site)
81
+ candidates = Analyzers::Static::CHA.new.candidates(graph, site)
82
+ candidates = fallback_candidates(graph, site) if candidates.empty?
83
+ graph.retain_rta_candidates(candidates, site)
84
+ end
85
+
86
+ def fallback_candidates(graph, site)
87
+ return graph.candidate_nodes(site.message) if site.receiver_kind == :unknown
88
+ return [] unless graph.ambiguous_resolution?
89
+
90
+ graph.ambiguous_fallback_candidates(site.message)
91
+ end
76
92
  end
77
93
  end
78
94
  end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ class AstScanner
5
+ private
6
+
7
+ def build_call_site(node, context)
8
+ message = node.name&.to_s
9
+ dynamic = false
10
+ receiver = classify_receiver(node.receiver, context)
11
+ metadata = { 'original_message' => message, 'receiver_candidates' => receiver[:candidates] }
12
+
13
+ if DYNAMIC_SENDS.include?(node.name)
14
+ literal = first_symbol_argument(node) || first_string_argument(node)
15
+ dynamic = literal.nil?
16
+ message = literal
17
+ metadata['dynamic_dispatch'] = true
18
+ end
19
+
20
+ return nil unless message
21
+
22
+ CallSite.new(
23
+ caller_id: context.current_caller_id,
24
+ message: message,
25
+ receiver_kind: receiver.fetch(:kind),
26
+ receiver_name: receiver[:name],
27
+ file: context.relative_file,
28
+ line: node.location.start_line,
29
+ test: context.test,
30
+ dynamic: dynamic,
31
+ metadata: metadata
32
+ )
33
+ end
34
+
35
+ def record_instantiation(node, context)
36
+ return record_factory_instantiation(node, context) unless %i[new []].include?(node.name)
37
+
38
+ receiver = classify_receiver(node.receiver, context)
39
+ receiver = implicit_self_constructor(context) if implicit_constructor?(receiver, context)
40
+ return unless receiver && receiver[:kind] == :constant
41
+
42
+ Array(receiver[:candidates] || receiver[:name]).each { |name| instantiated_classes << name }
43
+ record_initialize_call(node, context, receiver)
44
+ end
45
+
46
+ def implicit_constructor?(receiver, context)
47
+ return false unless %i[implicit self].include?(receiver[:kind])
48
+ return false unless context.owner
49
+
50
+ context.current_kind == :singleton_method || context.current_caller_id == context.root_id
51
+ end
52
+
53
+ def implicit_self_constructor(context)
54
+ { kind: :constant, name: context.owner, candidates: [context.owner] }
55
+ end
56
+
57
+ def record_initialize_call(node, context, receiver)
58
+ return unless node.name == :new
59
+
60
+ call_sites << CallSite.new(
61
+ caller_id: context.current_caller_id,
62
+ message: 'initialize',
63
+ receiver_kind: :instance,
64
+ receiver_name: receiver[:name],
65
+ file: context.relative_file,
66
+ line: node.location.start_line,
67
+ test: context.test,
68
+ dynamic: false,
69
+ metadata: { 'original_message' => 'new', 'receiver_candidates' => receiver[:candidates],
70
+ 'implicit_from' => 'new' }
71
+ )
72
+ end
73
+
74
+ def record_factory_instantiation(node, context)
75
+ return unless @factory_methods.include?(node.name.to_s)
76
+
77
+ receiver = classify_receiver(node.receiver, context)
78
+ return unless receiver[:kind] == :constant
79
+
80
+ Array(receiver[:candidates] || receiver[:name]).each do |name|
81
+ instantiated_classes << name
82
+ end
83
+ end
84
+
85
+ def record_uncertainty(site)
86
+ uncertainties[site.caller_id] << "Dynamic dispatch at #{site.file}:#{site.line}"
87
+ end
88
+
89
+ def record_uncertainty_at(node, context)
90
+ uncertainties[context.current_caller_id] << "Dynamic dispatch at #{context.relative_file}:#{node.location.start_line}"
91
+ end
92
+
93
+ def unresolved_dynamic_dispatch?(node)
94
+ DYNAMIC_SENDS.include?(node.name) && first_symbol_argument(node).nil? && first_string_argument(node).nil?
95
+ end
96
+
97
+ def record_parse_errors(root_id, result)
98
+ result.errors.each do |error|
99
+ uncertainties[root_id] << "Parse warning at line #{error.location.start_line}: #{error.message}"
100
+ end
101
+ end
102
+
103
+ def definition_owner(node, context)
104
+ return context.owner unless node.receiver
105
+ return context.owner if node.receiver.is_a?(Prism::SelfNode)
106
+
107
+ receiver = classify_receiver(node.receiver, context)
108
+ receiver[:name]
109
+ end
110
+
111
+ def definition_owner_for_call(node, context)
112
+ return context.owner unless node.receiver
113
+ return context.owner if node.receiver.is_a?(Prism::SelfNode)
114
+
115
+ classify_receiver(node.receiver, context)[:name]
116
+ end
117
+
118
+ def eval_owner(node, context)
119
+ return context.owner unless node.receiver
120
+
121
+ definition_owner_for_call(node, context)
122
+ end
123
+
124
+ def method_kind_and_separator(context)
125
+ context.singleton_scope ? [:singleton_method, '.'] : [:instance_method, '#']
126
+ end
127
+
128
+ def update_method_visibility(context, name, visibility, singleton: false)
129
+ separator = singleton ? '.' : method_kind_and_separator(context).last
130
+ id = "#{context.owner}#{separator}#{name}"
131
+ index = nodes.rindex { |candidate| candidate.id == id }
132
+ nodes[index] = nodes[index].with(visibility: visibility) if index
133
+ end
134
+
135
+ def promote_module_function(context, name, location)
136
+ instance_id = "#{context.owner}##{name}"
137
+ index = nodes.rindex { |candidate| candidate.id == instance_id }
138
+ return unless index
139
+
140
+ instance_node = nodes[index]
141
+ nodes[index] = instance_node.with(visibility: :private)
142
+ nodes << Node.new(
143
+ id: "#{context.owner}.#{name}",
144
+ kind: :singleton_method,
145
+ file: context.relative_file,
146
+ line: location.start_line,
147
+ end_line: location.end_line,
148
+ defined_via: :module_function,
149
+ owner: context.owner,
150
+ name: name,
151
+ test: context.test,
152
+ visibility: :public
153
+ )
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ class AstScanner
5
+ private
6
+
7
+ def handle_module_relation(node, context)
8
+ return unless MODULE_RELATION_MACROS.include?(node.name)
9
+ return unless context.owner
10
+
11
+ class_record(context.owner)[:extends] << [context.owner] if node.name == :extend && arguments(node).any?(Prism::SelfNode)
12
+
13
+ constants = arguments(node).filter_map { |argument| constant_name(argument) }
14
+ return if constants.empty?
15
+
16
+ data = class_record(context.owner)
17
+ key = :"#{node.name}s"
18
+ constants.each { |constant| data[key] << constant_candidates(constant, context.namespace) }
19
+ end
20
+
21
+ def handle_rails_callback(node, context)
22
+ return unless context.owner
23
+ return if context.test
24
+
25
+ if RAILS_CALLBACK_MACROS.include?(node.name)
26
+ callback_names(node).each do |method_name|
27
+ entrypoint_hints << EntryPoint.new(node_id: "#{context.owner}##{method_name}", reason: :callback_registered)
28
+ end
29
+ elsif node.name == :validates
30
+ custom_validator_names(node).each do |validator|
31
+ constant_candidates("#{validator}Validator", context.namespace).each do |owner|
32
+ entrypoint_hints << EntryPoint.new(node_id: "#{owner}#validate_each", reason: :callback_registered)
33
+ end
34
+ end
35
+ elsif node.name == :validates_with
36
+ arguments(node).filter_map { |argument| constant_name(argument) }.each do |validator|
37
+ constant_candidates(validator, context.namespace).each do |owner|
38
+ entrypoint_hints << EntryPoint.new(node_id: "#{owner}#validate", reason: :callback_registered)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def handle_attr_macro(node, context)
45
+ return false unless ATTR_MACROS.include?(node.name)
46
+ return false unless context.owner
47
+
48
+ symbol_arguments(node).each do |name|
49
+ kind, separator = method_kind_and_separator(context)
50
+ nodes << Node.new(
51
+ id: "#{context.owner}#{separator}#{name}",
52
+ kind: kind,
53
+ file: context.relative_file,
54
+ line: node.location.start_line,
55
+ end_line: node.location.end_line,
56
+ defined_via: node.name,
57
+ owner: context.owner,
58
+ name: name,
59
+ test: context.test,
60
+ visibility: context.visibility
61
+ )
62
+ next unless %i[attr_writer attr_accessor].include?(node.name)
63
+
64
+ nodes << Node.new(
65
+ id: "#{context.owner}#{separator}#{name}=",
66
+ kind: kind,
67
+ file: context.relative_file,
68
+ line: node.location.start_line,
69
+ end_line: node.location.end_line,
70
+ defined_via: node.name,
71
+ owner: context.owner,
72
+ name: "#{name}=",
73
+ test: context.test,
74
+ visibility: context.visibility
75
+ )
76
+ end
77
+ true
78
+ end
79
+
80
+ def handle_delegate(node, context)
81
+ return false unless node.name == :delegate
82
+ return false unless context.owner
83
+
84
+ target = keyword_value(node, 'to')
85
+ prefix = keyword_value(node, 'prefix')
86
+ symbol_arguments(node).each do |name|
87
+ generated_name = delegated_method_name(name, target, prefix)
88
+ kind, separator = method_kind_and_separator(context)
89
+ id = "#{context.owner}#{separator}#{generated_name}"
90
+ nodes << Node.new(
91
+ id: id,
92
+ kind: kind,
93
+ file: context.relative_file,
94
+ line: node.location.start_line,
95
+ end_line: node.location.end_line,
96
+ defined_via: :delegate,
97
+ owner: context.owner,
98
+ name: generated_name,
99
+ test: context.test,
100
+ visibility: context.visibility
101
+ )
102
+ record_delegate_target(id, target, node, context) if target
103
+ record_delegated_message(id, name, node, context)
104
+ end
105
+ true
106
+ end
107
+
108
+ def handle_forwardable(node, context)
109
+ return false unless %i[def_delegator def_delegators].include?(node.name)
110
+ return false unless context.owner
111
+
112
+ symbols = symbol_arguments(node)
113
+ return false if symbols.length < 2
114
+
115
+ target = symbols.shift
116
+ definitions = if node.name == :def_delegator
117
+ [[symbols[1] || symbols[0], symbols[0]]]
118
+ else
119
+ symbols.map { |name| [name, name] }
120
+ end
121
+ definitions.each do |generated_name, delegated_name|
122
+ kind, separator = method_kind_and_separator(context)
123
+ id = "#{context.owner}#{separator}#{generated_name}"
124
+ nodes << Node.new(
125
+ id: id,
126
+ kind: kind,
127
+ file: context.relative_file,
128
+ line: node.location.start_line,
129
+ end_line: node.location.end_line,
130
+ defined_via: node.name,
131
+ owner: context.owner,
132
+ name: generated_name,
133
+ test: context.test,
134
+ visibility: context.visibility
135
+ )
136
+ record_delegate_target(id, target, node, context)
137
+ record_delegated_message(id, delegated_name, node, context)
138
+ end
139
+ true
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ class AstScanner
5
+ private
6
+
7
+ def handle_define_method(node, context)
8
+ return false unless node.name == :define_method
9
+ return false unless context.owner
10
+
11
+ method_name = first_symbol_argument(node)
12
+ return false unless method_name
13
+
14
+ kind, separator = method_kind_and_separator(context)
15
+ id = "#{context.owner}#{separator}#{method_name}"
16
+ nodes << Node.new(
17
+ id: id,
18
+ kind: kind,
19
+ file: context.relative_file,
20
+ line: node.location.start_line,
21
+ end_line: node.location.end_line,
22
+ defined_via: :define_method,
23
+ owner: context.owner,
24
+ name: method_name,
25
+ test: context.test,
26
+ visibility: context.visibility
27
+ )
28
+ record_module_function_copy(node, context, context.owner, method_name) if context.module_function
29
+
30
+ if node.block
31
+ block_context = context.dup
32
+ block_context.current_caller_id = id
33
+ block_context.current_kind = kind
34
+ block_context.singleton_scope = false
35
+ block_context.visibility = :public
36
+ block_context.module_function = false
37
+ visit(node.block.body, block_context)
38
+ end
39
+ true
40
+ end
41
+
42
+ def handle_define_singleton_method(node, context)
43
+ return false unless node.name == :define_singleton_method
44
+
45
+ owner = definition_owner_for_call(node, context)
46
+ method_name = first_symbol_argument(node) || first_string_argument(node)
47
+ return false unless owner && method_name
48
+
49
+ id = "#{owner}.#{method_name}"
50
+ nodes << Node.new(
51
+ id: id,
52
+ kind: :singleton_method,
53
+ file: context.relative_file,
54
+ line: node.location.start_line,
55
+ end_line: node.location.end_line,
56
+ defined_via: :define_singleton_method,
57
+ owner: owner,
58
+ name: method_name,
59
+ test: context.test,
60
+ visibility: :public
61
+ )
62
+ if node.block
63
+ block_context = context.dup
64
+ block_context.owner = owner
65
+ block_context.current_caller_id = id
66
+ block_context.current_kind = :singleton_method
67
+ block_context.singleton_scope = false
68
+ block_context.visibility = :public
69
+ block_context.module_function = false
70
+ visit(node.block.body, block_context)
71
+ end
72
+ true
73
+ end
74
+
75
+ def handle_eval(node, context)
76
+ return false unless %i[class_eval module_eval].include?(node.name)
77
+ return false unless node.block
78
+
79
+ owner = eval_owner(node, context)
80
+ return false unless owner
81
+
82
+ block_context = context.dup
83
+ block_context.namespace = owner
84
+ block_context.owner = owner
85
+ block_context.singleton_scope = false
86
+ block_context.visibility = :public
87
+ block_context.module_function = false
88
+ visit(node.block.body, block_context)
89
+ true
90
+ end
91
+
92
+ def handle_visibility(node, context)
93
+ return false unless VISIBILITY_MACROS.include?(node.name)
94
+ return false unless context.owner
95
+
96
+ names = symbol_arguments(node)
97
+ class_method = node.name.to_s.end_with?('_class_method')
98
+ visibility = node.name.to_s.delete_suffix('_class_method').to_sym
99
+ if names.empty?
100
+ unless class_method
101
+ context.visibility = visibility
102
+ context.module_function = false
103
+ end
104
+ else
105
+ names.each { |name| update_method_visibility(context, name, visibility, singleton: class_method) }
106
+ end
107
+ true
108
+ end
109
+
110
+ def handle_module_function(node, context)
111
+ return false unless node.name == :module_function
112
+ return false unless context.owner
113
+
114
+ names = symbol_arguments(node)
115
+ if names.empty?
116
+ context.module_function = true
117
+ context.visibility = :private
118
+ else
119
+ names.each { |name| promote_module_function(context, name, node.location) }
120
+ end
121
+ true
122
+ end
123
+
124
+ def record_module_function_copy(node, context, _owner, method_name = node.name.to_s)
125
+ promote_module_function(context, method_name, node.location)
126
+ end
127
+
128
+ def handle_alias_method(node, context)
129
+ return false unless node.name == :alias_method
130
+ return false unless context.owner
131
+
132
+ new_name, old_name = symbol_arguments(node)
133
+ return false unless new_name && old_name
134
+
135
+ record_alias_method(context, node.location, new_name, old_name)
136
+ true
137
+ end
138
+
139
+ def visit_alias_method_node(node, context)
140
+ return visit_children(node, context) unless context.owner
141
+
142
+ new_name = node.new_name.unescaped.to_s
143
+ old_name = node.old_name.unescaped.to_s
144
+ record_alias_method(context, node.location, new_name, old_name)
145
+ end
146
+
147
+ def record_alias_method(context, location, new_name, old_name)
148
+ kind = context.singleton_scope ? :singleton_method : :instance_method
149
+ separator = kind == :singleton_method ? '.' : '#'
150
+ id = "#{context.owner}#{separator}#{new_name}"
151
+ nodes << Node.new(
152
+ id: id,
153
+ kind: kind,
154
+ file: context.relative_file,
155
+ line: location.start_line,
156
+ end_line: location.end_line,
157
+ defined_via: :alias_method,
158
+ owner: context.owner,
159
+ name: new_name,
160
+ test: context.test,
161
+ visibility: context.visibility
162
+ )
163
+ call_sites << CallSite.new(
164
+ caller_id: id,
165
+ message: old_name,
166
+ receiver_kind: :self,
167
+ receiver_name: context.owner,
168
+ file: context.relative_file,
169
+ line: location.start_line,
170
+ test: context.test,
171
+ dynamic: false,
172
+ metadata: { 'original_message' => old_name, 'alias_method' => true }
173
+ )
174
+ end
175
+ end
176
+ end