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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Necropsy
|
|
4
|
+
class AstScanner
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def callback_names(node)
|
|
8
|
+
names = symbol_arguments(node)
|
|
9
|
+
with = keyword_value(node, 'with')
|
|
10
|
+
names << with if with.is_a?(String)
|
|
11
|
+
names.uniq
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def custom_validator_names(node)
|
|
15
|
+
keyword_keys(node).reject { |name| RAILS_BUILTIN_VALIDATORS.include?(name) }.map do |name|
|
|
16
|
+
name.split('_').map(&:capitalize).join
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def delegated_method_name(name, target, prefix)
|
|
21
|
+
return name unless prefix
|
|
22
|
+
|
|
23
|
+
prefix_name = prefix == true ? target.to_s.delete_prefix('@') : prefix.to_s
|
|
24
|
+
"#{prefix_name}_#{name}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def record_delegate_target(caller_id, target, node, context)
|
|
28
|
+
record_reference_call(caller_id, target.to_s.delete_prefix('@'), node, context, receiver_kind: :self)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def record_delegated_message(caller_id, message, node, context)
|
|
32
|
+
record_reference_call(caller_id, message, node, context, receiver_kind: :unknown)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def record_symbol_reference(node, context)
|
|
36
|
+
return unless SYMBOL_REFERENCE_CALLS.include?(node.name)
|
|
37
|
+
|
|
38
|
+
message = first_symbol_argument(node) || first_string_argument(node)
|
|
39
|
+
return unless message
|
|
40
|
+
|
|
41
|
+
receiver = classify_receiver(node.receiver, context)
|
|
42
|
+
record_reference_call(
|
|
43
|
+
context.current_caller_id,
|
|
44
|
+
message,
|
|
45
|
+
node,
|
|
46
|
+
context,
|
|
47
|
+
receiver_kind: receiver[:kind],
|
|
48
|
+
receiver_name: receiver[:name],
|
|
49
|
+
candidates: receiver[:candidates]
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def record_symbol_to_proc(node, context)
|
|
54
|
+
block = node.block
|
|
55
|
+
return unless block.is_a?(Prism::BlockArgumentNode)
|
|
56
|
+
return unless block.expression.is_a?(Prism::SymbolNode)
|
|
57
|
+
|
|
58
|
+
record_reference_call(
|
|
59
|
+
context.current_caller_id,
|
|
60
|
+
block.expression.unescaped.to_s,
|
|
61
|
+
node,
|
|
62
|
+
context,
|
|
63
|
+
receiver_kind: :unknown
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def record_reference_call(caller_id, message, node, context, receiver_kind:, receiver_name: nil, candidates: [])
|
|
68
|
+
call_sites << CallSite.new(
|
|
69
|
+
caller_id: caller_id,
|
|
70
|
+
message: message.to_s,
|
|
71
|
+
receiver_kind: receiver_kind,
|
|
72
|
+
receiver_name: receiver_name,
|
|
73
|
+
file: context.relative_file,
|
|
74
|
+
line: node.location.start_line,
|
|
75
|
+
test: context.test,
|
|
76
|
+
dynamic: false,
|
|
77
|
+
metadata: { 'symbol_reference' => true, 'receiver_candidates' => Array(candidates) }
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Necropsy
|
|
4
|
+
class AstScanner
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def classify_receiver(receiver, context)
|
|
8
|
+
return { kind: :implicit, name: nil } unless receiver
|
|
9
|
+
return { kind: :self, name: context.owner } if receiver.is_a?(Prism::SelfNode)
|
|
10
|
+
|
|
11
|
+
constant = constant_name(receiver)
|
|
12
|
+
if constant
|
|
13
|
+
candidates = constant_candidates(constant, context.namespace)
|
|
14
|
+
return { kind: :constant, name: candidates.first, candidates: candidates }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if receiver.is_a?(Prism::CallNode) && receiver.name == :new
|
|
18
|
+
receiver_constant = constant_name(receiver.receiver)
|
|
19
|
+
if receiver_constant
|
|
20
|
+
candidates = constant_candidates(receiver_constant, context.namespace)
|
|
21
|
+
return { kind: :instance, name: candidates.first, candidates: candidates }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
{ kind: :unknown, name: nil, candidates: [] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def first_symbol_argument(node)
|
|
29
|
+
symbol_arguments(node).first
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def first_string_argument(node)
|
|
33
|
+
arguments(node).find { |argument| argument.is_a?(Prism::StringNode) }&.unescaped
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def symbol_arguments(node)
|
|
37
|
+
arguments(node).filter_map do |arg|
|
|
38
|
+
next unless arg.is_a?(Prism::SymbolNode)
|
|
39
|
+
|
|
40
|
+
arg.unescaped.to_s
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def arguments(node)
|
|
45
|
+
node.arguments&.arguments || []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def keyword_value(node, key)
|
|
49
|
+
hash = arguments(node).find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
50
|
+
pair = hash&.elements&.find do |element|
|
|
51
|
+
element.is_a?(Prism::AssocNode) && literal_value(element.key).to_s == key
|
|
52
|
+
end
|
|
53
|
+
literal_value(pair&.value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def keyword_keys(node)
|
|
57
|
+
hash = arguments(node).find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
58
|
+
Array(hash&.elements).filter_map do |element|
|
|
59
|
+
literal_value(element.key).to_s if element.is_a?(Prism::AssocNode)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def literal_value(node)
|
|
64
|
+
case node
|
|
65
|
+
when Prism::SymbolNode, Prism::StringNode
|
|
66
|
+
node.unescaped.to_s
|
|
67
|
+
when Prism::TrueNode
|
|
68
|
+
true
|
|
69
|
+
when Prism::FalseNode
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def constant_name(node)
|
|
75
|
+
return nil unless node
|
|
76
|
+
|
|
77
|
+
case node
|
|
78
|
+
when Prism::ConstantReadNode
|
|
79
|
+
node.name.to_s
|
|
80
|
+
when Prism::ConstantPathNode
|
|
81
|
+
prefix = node.parent ? constant_name(node.parent) : ''
|
|
82
|
+
[prefix, node.name.to_s].join('::')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def qualify_constant(name, namespace)
|
|
87
|
+
return nil unless name
|
|
88
|
+
return name.delete_prefix('::') if name.start_with?('::')
|
|
89
|
+
return name if namespace.nil? || namespace.empty? || name.include?('::')
|
|
90
|
+
|
|
91
|
+
"#{namespace}::#{name}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def record_class_info(node, namespace, context)
|
|
95
|
+
return unless namespace
|
|
96
|
+
|
|
97
|
+
data = class_record(namespace)
|
|
98
|
+
data[:kind] = node.is_a?(Prism::ModuleNode) ? :module : :class
|
|
99
|
+
data[:file] = context.relative_file
|
|
100
|
+
data[:line] = node.location.start_line
|
|
101
|
+
return unless node.respond_to?(:superclass)
|
|
102
|
+
|
|
103
|
+
superclass = constant_name(node.superclass)
|
|
104
|
+
data[:superclass_candidates] = constant_candidates(superclass, context.namespace) if superclass
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def class_record(namespace)
|
|
108
|
+
class_data[namespace] ||= {
|
|
109
|
+
id: namespace,
|
|
110
|
+
kind: :class,
|
|
111
|
+
file: nil,
|
|
112
|
+
line: nil,
|
|
113
|
+
superclass: nil,
|
|
114
|
+
superclass_candidates: [],
|
|
115
|
+
includes: [],
|
|
116
|
+
prepends: [],
|
|
117
|
+
extends: [],
|
|
118
|
+
dynamic: false
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def class_infos
|
|
123
|
+
class_data.values.map do |data|
|
|
124
|
+
superclass_candidates = data.fetch(:superclass_candidates)
|
|
125
|
+
ClassInfo.new(
|
|
126
|
+
id: data.fetch(:id),
|
|
127
|
+
kind: data.fetch(:kind),
|
|
128
|
+
file: data[:file],
|
|
129
|
+
line: data[:line],
|
|
130
|
+
superclass: resolve_candidate_group(superclass_candidates),
|
|
131
|
+
superclass_candidates: superclass_candidates,
|
|
132
|
+
includes: resolve_candidate_groups(data.fetch(:includes)),
|
|
133
|
+
prepends: resolve_candidate_groups(data.fetch(:prepends)),
|
|
134
|
+
extends: resolve_candidate_groups(data.fetch(:extends)),
|
|
135
|
+
dynamic: data.fetch(:dynamic)
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resolve_candidate_groups(groups)
|
|
141
|
+
Array(groups).filter_map { |group| resolve_candidate_group(Array(group)) }.uniq
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def resolve_candidate_group(group)
|
|
145
|
+
group.find { |candidate| class_data.key?(candidate) } || group.first
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def constant_candidates(name, namespace)
|
|
149
|
+
return [name.delete_prefix('::')] if name.start_with?('::')
|
|
150
|
+
return [name] if namespace.nil? || namespace.empty?
|
|
151
|
+
|
|
152
|
+
head, *rest = name.split('::')
|
|
153
|
+
suffix = rest.empty? ? '' : "::#{rest.join('::')}"
|
|
154
|
+
parts = namespace.split('::')
|
|
155
|
+
candidates = []
|
|
156
|
+
parts.length.downto(1) do |length|
|
|
157
|
+
candidates << "#{parts.first(length).join('::')}::#{head}#{suffix}"
|
|
158
|
+
end
|
|
159
|
+
inherited_namespaces(namespace).each do |ancestor|
|
|
160
|
+
candidates << "#{ancestor}::#{head}#{suffix}"
|
|
161
|
+
end
|
|
162
|
+
candidates << name
|
|
163
|
+
candidates.uniq
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def inherited_namespaces(namespace, seen = Set.new)
|
|
167
|
+
return [] unless namespace && seen.add?(namespace)
|
|
168
|
+
|
|
169
|
+
data = class_data[namespace]
|
|
170
|
+
return [] unless data
|
|
171
|
+
|
|
172
|
+
Array(data[:superclass_candidates]).flat_map do |superclass|
|
|
173
|
+
[superclass, *inherited_namespaces(superclass, seen)]
|
|
174
|
+
end.uniq
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Necropsy
|
|
4
|
+
class AstScanner
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def copy_module_function_call_sites
|
|
8
|
+
module_functions = nodes.select { |node| node.defined_via == :module_function }
|
|
9
|
+
copies = module_functions.flat_map do |node|
|
|
10
|
+
instance_id = "#{node.owner}##{node.name}"
|
|
11
|
+
call_sites.select { |site| site.caller_id == instance_id }.map do |site|
|
|
12
|
+
site.with(caller_id: node.id, metadata: site.metadata.merge('module_function' => true))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
call_sites.concat(copies)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def scan_file(file)
|
|
19
|
+
relative = project.relative_path(file)
|
|
20
|
+
root_id = "file:#{relative}"
|
|
21
|
+
test = project.test_file?(file)
|
|
22
|
+
nodes << Node.new(
|
|
23
|
+
id: root_id,
|
|
24
|
+
kind: :block_entry,
|
|
25
|
+
file: relative,
|
|
26
|
+
line: 1,
|
|
27
|
+
end_line: 1,
|
|
28
|
+
defined_via: :file,
|
|
29
|
+
owner: nil,
|
|
30
|
+
name: relative,
|
|
31
|
+
test: test,
|
|
32
|
+
visibility: :public
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
result = Prism.parse(File.read(file))
|
|
36
|
+
record_parse_errors(root_id, result) if result.failure?
|
|
37
|
+
|
|
38
|
+
visit(
|
|
39
|
+
result.value,
|
|
40
|
+
Context.new(
|
|
41
|
+
namespace: nil,
|
|
42
|
+
owner: nil,
|
|
43
|
+
current_caller_id: root_id,
|
|
44
|
+
current_kind: :block_entry,
|
|
45
|
+
root_id: root_id,
|
|
46
|
+
file: file,
|
|
47
|
+
relative_file: relative,
|
|
48
|
+
test: test,
|
|
49
|
+
singleton_scope: false,
|
|
50
|
+
visibility: :public,
|
|
51
|
+
module_function: false
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
rescue SystemCallError, EncodingError => e
|
|
55
|
+
uncertainties[root_id] << "Could not parse #{relative}: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def visit(node, context)
|
|
59
|
+
return unless node.respond_to?(:child_nodes)
|
|
60
|
+
|
|
61
|
+
case node
|
|
62
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
63
|
+
visit_namespace(node, context)
|
|
64
|
+
when Prism::DefNode
|
|
65
|
+
visit_def(node, context)
|
|
66
|
+
when Prism::CallNode
|
|
67
|
+
visit_call(node, context)
|
|
68
|
+
when Prism::AliasMethodNode
|
|
69
|
+
visit_alias_method_node(node, context)
|
|
70
|
+
when Prism::SuperNode, Prism::ForwardingSuperNode
|
|
71
|
+
visit_super(node, context)
|
|
72
|
+
when Prism::SingletonClassNode
|
|
73
|
+
visit_singleton_class(node, context)
|
|
74
|
+
when Prism::ConstantWriteNode
|
|
75
|
+
visit_constant_write(node, context)
|
|
76
|
+
else
|
|
77
|
+
node.child_nodes.compact.each { |child| visit(child, context) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visit_namespace(node, context)
|
|
82
|
+
namespace = qualify_constant(constant_name(node.constant_path), context.namespace)
|
|
83
|
+
record_class_info(node, namespace, context)
|
|
84
|
+
child_context = context.dup
|
|
85
|
+
child_context.namespace = namespace
|
|
86
|
+
child_context.owner = namespace
|
|
87
|
+
child_context.singleton_scope = false
|
|
88
|
+
child_context.visibility = :public
|
|
89
|
+
child_context.module_function = false
|
|
90
|
+
visit(node.body, child_context)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def visit_def(node, context)
|
|
94
|
+
owner = definition_owner(node, context)
|
|
95
|
+
return visit_children(node, context) unless owner
|
|
96
|
+
|
|
97
|
+
kind = node.receiver || context.singleton_scope ? :singleton_method : :instance_method
|
|
98
|
+
separator = kind == :singleton_method ? '.' : '#'
|
|
99
|
+
id = "#{owner}#{separator}#{node.name}"
|
|
100
|
+
nodes << Node.new(
|
|
101
|
+
id: id,
|
|
102
|
+
kind: kind,
|
|
103
|
+
file: context.relative_file,
|
|
104
|
+
line: node.location.start_line,
|
|
105
|
+
end_line: node.location.end_line,
|
|
106
|
+
defined_via: :def,
|
|
107
|
+
owner: owner,
|
|
108
|
+
name: node.name.to_s,
|
|
109
|
+
test: context.test,
|
|
110
|
+
visibility: node.receiver ? :public : context.visibility
|
|
111
|
+
)
|
|
112
|
+
record_module_function_copy(node, context, owner) if kind == :instance_method && context.module_function
|
|
113
|
+
if node.name == :method_missing
|
|
114
|
+
uncertainties[id] << "#{owner} defines method_missing"
|
|
115
|
+
class_record(owner)[:dynamic] = true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
method_context = context.dup
|
|
119
|
+
method_context.owner = owner
|
|
120
|
+
method_context.current_caller_id = id
|
|
121
|
+
method_context.current_kind = kind
|
|
122
|
+
method_context.singleton_scope = false
|
|
123
|
+
method_context.visibility = :public
|
|
124
|
+
method_context.module_function = false
|
|
125
|
+
visit(node.body, method_context)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def visit_call(node, context)
|
|
129
|
+
return if handle_visibility(node, context)
|
|
130
|
+
return if handle_module_function(node, context)
|
|
131
|
+
return if handle_eval(node, context)
|
|
132
|
+
return if handle_define_singleton_method(node, context)
|
|
133
|
+
return if handle_define_method(node, context)
|
|
134
|
+
return if handle_attr_macro(node, context)
|
|
135
|
+
return if handle_delegate(node, context)
|
|
136
|
+
return if handle_forwardable(node, context)
|
|
137
|
+
return if handle_alias_method(node, context)
|
|
138
|
+
|
|
139
|
+
handle_module_relation(node, context)
|
|
140
|
+
handle_rails_callback(node, context)
|
|
141
|
+
|
|
142
|
+
record_instantiation(node, context)
|
|
143
|
+
record_symbol_reference(node, context)
|
|
144
|
+
record_symbol_to_proc(node, context)
|
|
145
|
+
site = build_call_site(node, context)
|
|
146
|
+
call_sites << site if site
|
|
147
|
+
if site&.dynamic
|
|
148
|
+
record_uncertainty(site)
|
|
149
|
+
elsif unresolved_dynamic_dispatch?(node)
|
|
150
|
+
record_uncertainty_at(node, context)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
visit_children(node, context)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def visit_super(node, context)
|
|
157
|
+
method_name = context.current_caller_id&.split(/[.#]/)&.last
|
|
158
|
+
if method_name
|
|
159
|
+
call_sites << CallSite.new(
|
|
160
|
+
caller_id: context.current_caller_id,
|
|
161
|
+
message: method_name,
|
|
162
|
+
receiver_kind: :super,
|
|
163
|
+
receiver_name: context.owner,
|
|
164
|
+
file: context.relative_file,
|
|
165
|
+
line: node.location.start_line,
|
|
166
|
+
test: context.test,
|
|
167
|
+
dynamic: false,
|
|
168
|
+
metadata: { 'super' => true }
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
visit_children(node, context)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def visit_singleton_class(node, context)
|
|
175
|
+
return visit_children(node, context) unless node.expression.is_a?(Prism::SelfNode) && context.owner
|
|
176
|
+
|
|
177
|
+
singleton_context = context.dup
|
|
178
|
+
singleton_context.singleton_scope = true
|
|
179
|
+
singleton_context.visibility = :public
|
|
180
|
+
singleton_context.module_function = false
|
|
181
|
+
visit(node.body, singleton_context)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Necropsy
|
|
4
|
+
class AstScanner
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def visit_constant_write(node, context)
|
|
8
|
+
return visit_children(node, context) unless struct_or_data_definition?(node.value)
|
|
9
|
+
|
|
10
|
+
owner = qualify_constant(node.name.to_s, context.namespace)
|
|
11
|
+
data = class_record(owner)
|
|
12
|
+
data[:kind] = :class
|
|
13
|
+
data[:file] = context.relative_file
|
|
14
|
+
data[:line] = node.location.start_line
|
|
15
|
+
instantiated_classes << owner
|
|
16
|
+
|
|
17
|
+
symbol_arguments(node.value).each do |name|
|
|
18
|
+
nodes << Node.new(
|
|
19
|
+
id: "#{owner}##{name}",
|
|
20
|
+
kind: :instance_method,
|
|
21
|
+
file: context.relative_file,
|
|
22
|
+
line: node.location.start_line,
|
|
23
|
+
end_line: node.location.end_line,
|
|
24
|
+
defined_via: node.value.name == :define ? :data_define : :struct_new,
|
|
25
|
+
owner: owner,
|
|
26
|
+
name: name,
|
|
27
|
+
test: context.test,
|
|
28
|
+
visibility: :public
|
|
29
|
+
)
|
|
30
|
+
next if node.value.name == :define
|
|
31
|
+
|
|
32
|
+
nodes << Node.new(
|
|
33
|
+
id: "#{owner}##{name}=",
|
|
34
|
+
kind: :instance_method,
|
|
35
|
+
file: context.relative_file,
|
|
36
|
+
line: node.location.start_line,
|
|
37
|
+
end_line: node.location.end_line,
|
|
38
|
+
defined_via: :struct_new,
|
|
39
|
+
owner: owner,
|
|
40
|
+
name: "#{name}=",
|
|
41
|
+
test: context.test,
|
|
42
|
+
visibility: :public
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return unless node.value.block
|
|
47
|
+
|
|
48
|
+
block_context = context.dup
|
|
49
|
+
block_context.namespace = owner
|
|
50
|
+
block_context.owner = owner
|
|
51
|
+
block_context.singleton_scope = false
|
|
52
|
+
block_context.visibility = :public
|
|
53
|
+
block_context.module_function = false
|
|
54
|
+
visit(node.value.block.body, block_context)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def struct_or_data_definition?(value)
|
|
58
|
+
return false unless value.is_a?(Prism::CallNode)
|
|
59
|
+
|
|
60
|
+
receiver_name = constant_name(value.receiver)
|
|
61
|
+
(receiver_name == 'Struct' && value.name == :new) || (receiver_name == 'Data' && value.name == :define)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def visit_children(node, context)
|
|
65
|
+
node.child_nodes.compact.each { |child| visit(child, context) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|