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.
- checksums.yaml +4 -4
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +18 -0
- data/MEASUREMENTS.md +85 -0
- data/README.md +60 -8
- data/Rakefile +3 -1
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +12 -2
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +5 -1
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +68 -14
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +107 -12
- data/lib/necropsy/analyzers/static/cha.rb +10 -6
- data/lib/necropsy/analyzers/static/name_resolution.rb +29 -20
- data/lib/necropsy/analyzers/static/rta.rb +19 -3
- data/lib/necropsy/ast_scanner/call_recording.rb +156 -0
- data/lib/necropsy/ast_scanner/dsl_macros.rb +142 -0
- data/lib/necropsy/ast_scanner/method_definitions.rb +176 -0
- data/lib/necropsy/ast_scanner/references.rb +81 -0
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +177 -0
- data/lib/necropsy/ast_scanner/traversal.rb +184 -0
- data/lib/necropsy/ast_scanner/value_definitions.rb +68 -0
- data/lib/necropsy/ast_scanner.rb +24 -560
- data/lib/necropsy/bench/evaluator.rb +29 -10
- data/lib/necropsy/cache/scan_cache.rb +20 -12
- data/lib/necropsy/cli.rb +88 -32
- data/lib/necropsy/confidence/scorer.rb +97 -19
- data/lib/necropsy/configuration.rb +125 -7
- data/lib/necropsy/diagnostics.rb +202 -0
- data/lib/necropsy/entry_points/plain.rb +19 -2
- data/lib/necropsy/entry_points/rails.rb +121 -85
- data/lib/necropsy/graph/call_graph.rb +203 -21
- data/lib/necropsy/guardrail/baseline.rb +14 -5
- data/lib/necropsy/guardrail/diff.rb +5 -2
- data/lib/necropsy/guardrail/quarantine.rb +9 -3
- data/lib/necropsy/models.rb +19 -11
- data/lib/necropsy/project.rb +49 -3
- data/lib/necropsy/reachability/engine.rb +31 -7
- data/lib/necropsy/report.rb +44 -14
- data/lib/necropsy/reporter.rb +11 -5
- data/lib/necropsy/runner.rb +33 -4
- data/lib/necropsy/trace_point_runtime.rb +19 -0
- data/lib/necropsy/version.rb +1 -1
- data/lib/necropsy.rb +4 -0
- data/script/measure.rb +20 -0
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|