necropsy 0.2.0 → 0.3.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/CHANGELOG.md +26 -0
- data/MEASUREMENTS.md +27 -0
- data/README.md +182 -17
- data/bench/README.md +92 -0
- data/bench/audit.rb +113 -0
- data/bench/audits/0.2.1/audit.json +501 -0
- data/bench/audits/0.2.1/audit.md +55 -0
- data/bench/audits/0.2.1/baseline_performance.yml +16 -0
- data/bench/audits/0.2.1/config.yml +42 -0
- data/bench/audits/0.2.1/review.yml +56 -0
- data/bench/audits/0.2.1/review_queue.yml +3651 -0
- data/bench/corpora/v1/README.md +30 -0
- data/bench/corpora/v1/labels.yml +37 -0
- data/bench/corpora/v1/manifest.yml +63 -0
- data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
- data/bench/corpora/v1/self.necropsy.yml +8 -0
- data/bench/corpora/v1/tools/debride.yml +42 -0
- data/bench/corpora/v1/tools/spoom.yml +12 -0
- data/bench/corpora/v1/tools/type_aware.yml +11 -0
- data/bench/golden/v1/candidate_union.json +21918 -0
- data/bench/golden/v1/metadata.json +12 -0
- data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
- data/bench/golden/v1/reports/plain_ruby.json +135 -0
- data/bench/golden/v1/reports/rails.json +176 -0
- data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
- data/bench/golden/v1/reports/self.json +3449 -0
- data/bench/review_queue.rb +35 -0
- data/bench/run.rb +31 -0
- data/bench/schema/candidate-union-v1.schema.json +70 -0
- data/docs/impv_implementation_matrix.md +179 -0
- data/docs/migrations/0.2.1.md +57 -0
- data/docs/migrations/0.3.0.md +207 -0
- data/docs/migrations/0.4.0.md +13 -0
- data/docs/necropsy_performance_adr.md +43 -0
- data/docs/necropsy_scope_decisions.md +58 -0
- data/docs/necropsy_type_facts_adr.md +22 -0
- data/gemfiles/prism_min.gemfile +9 -0
- data/gemfiles/prism_min.gemfile.lock +50 -0
- data/lib/necropsy/analyzer.rb +121 -2
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +69 -17
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +99 -9
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +69 -299
- data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
- data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
- data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
- data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
- data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
- data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +133 -23
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
- data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
- data/lib/necropsy/analyzers/static/cha.rb +33 -73
- data/lib/necropsy/analyzers/static/name_resolution.rb +133 -12
- data/lib/necropsy/analyzers/static/rta.rb +237 -29
- data/lib/necropsy/ast_scanner/call_recording.rb +149 -30
- data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
- data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
- data/lib/necropsy/ast_scanner/dsl_macros.rb +431 -41
- data/lib/necropsy/ast_scanner/method_definitions.rb +232 -50
- data/lib/necropsy/ast_scanner/references.rb +43 -9
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +101 -15
- data/lib/necropsy/ast_scanner/traversal.rb +234 -71
- data/lib/necropsy/ast_scanner/value_definitions.rb +23 -13
- data/lib/necropsy/ast_scanner.rb +67 -6
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +329 -16
- data/lib/necropsy/bench/finding_facts.rb +152 -0
- data/lib/necropsy/bench/precision_gate.rb +144 -0
- data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
- data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
- data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
- data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
- data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
- data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
- data/lib/necropsy/bench/release_audit.rb +360 -0
- data/lib/necropsy/bench/report_normalizer.rb +140 -0
- data/lib/necropsy/bench/review_queue.rb +154 -0
- data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
- data/lib/necropsy/bench/seed_runner.rb +408 -0
- data/lib/necropsy/bounded_canonicalizer.rb +218 -0
- data/lib/necropsy/cache/scan_cache.rb +85 -17
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +220 -33
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +103 -58
- data/lib/necropsy/configuration.rb +224 -21
- data/lib/necropsy/convention_rules.rb +138 -0
- data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
- data/lib/necropsy/definition_identity.rb +37 -0
- data/lib/necropsy/diagnostics.rb +176 -36
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +111 -10
- data/lib/necropsy/entry_points/rails.rb +322 -41
- data/lib/necropsy/entry_points/test.rb +6 -1
- data/lib/necropsy/flow_interpreter.rb +460 -0
- data/lib/necropsy/graph/blocker_matching.rb +338 -0
- data/lib/necropsy/graph/call_graph.rb +1099 -109
- data/lib/necropsy/graph/definition_index.rb +149 -0
- data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
- data/lib/necropsy/graph/evidence_store.rb +213 -0
- data/lib/necropsy/graph/resolution_store.rb +497 -0
- data/lib/necropsy/graph_self_check.rb +79 -0
- data/lib/necropsy/guardrail/baseline.rb +350 -13
- data/lib/necropsy/guardrail/quarantine.rb +94 -9
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +878 -13
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +327 -25
- data/lib/necropsy/reachability/engine.rb +54 -13
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +113 -4
- data/lib/necropsy/reporter.rb +431 -15
- data/lib/necropsy/runner.rb +233 -18
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -0
- data/lib/necropsy/type_facts.rb +53 -0
- data/lib/necropsy/version.rb +1 -1
- data/lib/necropsy/why_not_explanation.rb +436 -0
- data/lib/necropsy/why_not_renderer.rb +197 -0
- data/lib/necropsy/world_policy.rb +90 -0
- data/lib/necropsy.rb +35 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- metadata +85 -1
|
@@ -8,73 +8,446 @@ module Necropsy
|
|
|
8
8
|
return unless MODULE_RELATION_MACROS.include?(node.name)
|
|
9
9
|
return unless context.owner
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
unless static_module_relation?(node, context)
|
|
12
|
+
constants = arguments(node).filter_map { |argument| constant_name(argument) }
|
|
13
|
+
class_record(context.owner)[:dynamic] = true
|
|
14
|
+
record_semantic_blocker(
|
|
15
|
+
:dynamic_ancestry,
|
|
16
|
+
node,
|
|
17
|
+
context,
|
|
18
|
+
"#{node.name} is not an unconditional load-time ancestry mutation",
|
|
19
|
+
suggested_action: :make_ancestry_static,
|
|
20
|
+
scope_owner: [context.owner, *constants].uniq,
|
|
21
|
+
force_global: %w[BasicObject Kernel Object].include?(context.owner)
|
|
22
|
+
)
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if node.name == :extend && arguments(node).any?(Prism::SelfNode)
|
|
27
|
+
class_record(context.owner)[:extends] << [context.owner]
|
|
28
|
+
class_record(context.owner)[:singleton_includes] << [context.owner]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
dynamic_arguments = arguments(node).reject do |argument|
|
|
32
|
+
constant_name(argument) || (node.name == :extend && argument.is_a?(Prism::SelfNode))
|
|
33
|
+
end
|
|
34
|
+
unless dynamic_arguments.empty?
|
|
35
|
+
class_record(context.owner)[:dynamic] = true
|
|
36
|
+
record_semantic_blocker(
|
|
37
|
+
:dynamic_ancestry,
|
|
38
|
+
node,
|
|
39
|
+
context,
|
|
40
|
+
"#{node.name} has a runtime-computed module and lookup cannot be closed",
|
|
41
|
+
suggested_action: :make_ancestry_static
|
|
42
|
+
)
|
|
43
|
+
end
|
|
12
44
|
|
|
13
45
|
constants = arguments(node).filter_map { |argument| constant_name(argument) }
|
|
14
46
|
return if constants.empty?
|
|
15
47
|
|
|
16
48
|
data = class_record(context.owner)
|
|
17
|
-
key =
|
|
18
|
-
constants.
|
|
49
|
+
key = module_relation_key(node.name, context)
|
|
50
|
+
constants.reverse_each do |constant|
|
|
51
|
+
candidates = constant_candidates(constant, context.lexical_nesting)
|
|
52
|
+
data[key] << candidates
|
|
53
|
+
data[:extends] << candidates if key == :singleton_includes && node.name == :extend
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def static_module_relation?(node, context)
|
|
58
|
+
context.static_ancestry && node.receiver.nil?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def module_relation_key(name, context)
|
|
62
|
+
return :singleton_includes if context.singleton_scope && name == :include
|
|
63
|
+
return :singleton_prepends if context.singleton_scope && name == :prepend
|
|
64
|
+
return :singleton_includes if name == :extend
|
|
65
|
+
|
|
66
|
+
:"#{name}s"
|
|
19
67
|
end
|
|
20
68
|
|
|
21
69
|
def handle_rails_callback(node, context)
|
|
22
|
-
return unless context.owner
|
|
23
|
-
return if context.test
|
|
70
|
+
return false unless context.owner
|
|
71
|
+
return false if context.test
|
|
72
|
+
|
|
73
|
+
if runtime_block_macro_enabled?(node)
|
|
74
|
+
callback = keyword_value(node, 'callback')
|
|
75
|
+
if callback.is_a?(String)
|
|
76
|
+
entrypoint_hints << EntryPoint.new(
|
|
77
|
+
node_id: "#{context.owner}##{callback}", reason: :callback_registered,
|
|
78
|
+
evidence: { 'type' => 'runtime_callback', 'macro' => node.name.to_s }
|
|
79
|
+
)
|
|
80
|
+
elsif keyword_keys(node).include?('callback')
|
|
81
|
+
record_semantic_blocker(
|
|
82
|
+
:framework_runtime_callback,
|
|
83
|
+
node,
|
|
84
|
+
context,
|
|
85
|
+
"#{node.name} callback is not statically bounded",
|
|
86
|
+
suggested_action: :review_callback
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
if node.block
|
|
90
|
+
synthetic = visit_synthetic_body(node.block.body, context, :runtime_callback_block)
|
|
91
|
+
entrypoint_hints << EntryPoint.new(
|
|
92
|
+
node_id: synthetic.graph_id,
|
|
93
|
+
reason: :callback_registered,
|
|
94
|
+
evidence: { 'type' => 'runtime_callback_block', 'macro' => node.name.to_s }
|
|
95
|
+
)
|
|
96
|
+
return true
|
|
97
|
+
end
|
|
98
|
+
return false
|
|
99
|
+
end
|
|
24
100
|
|
|
25
101
|
if RAILS_CALLBACK_MACROS.include?(node.name)
|
|
102
|
+
return false if callback_disabled?(node)
|
|
103
|
+
|
|
104
|
+
record_callback_condition_blocker(node, context) if callback_condition_unknown?(node)
|
|
26
105
|
callback_names(node).each do |method_name|
|
|
27
106
|
entrypoint_hints << EntryPoint.new(node_id: "#{context.owner}##{method_name}", reason: :callback_registered)
|
|
28
107
|
end
|
|
108
|
+
callback_condition_names(node).each do |method_name|
|
|
109
|
+
entrypoint_hints << EntryPoint.new(
|
|
110
|
+
node_id: "#{context.owner}##{method_name}",
|
|
111
|
+
reason: :callback_registered,
|
|
112
|
+
evidence: { 'type' => 'callback_condition', 'macro' => node.name.to_s }
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
if node.block
|
|
116
|
+
synthetic = visit_synthetic_body(node.block.body, context, :callback_block)
|
|
117
|
+
entrypoint_hints << EntryPoint.new(
|
|
118
|
+
node_id: synthetic.graph_id,
|
|
119
|
+
reason: :callback_registered,
|
|
120
|
+
evidence: { 'type' => 'callback_block', 'macro' => node.name.to_s }
|
|
121
|
+
)
|
|
122
|
+
return true
|
|
123
|
+
end
|
|
29
124
|
elsif node.name == :validates
|
|
30
125
|
custom_validator_names(node).each do |validator|
|
|
31
|
-
constant_candidates("#{validator}Validator", context.
|
|
126
|
+
constant_candidates("#{validator}Validator", context.lexical_nesting).each do |owner|
|
|
32
127
|
entrypoint_hints << EntryPoint.new(node_id: "#{owner}#validate_each", reason: :callback_registered)
|
|
33
128
|
end
|
|
34
129
|
end
|
|
35
130
|
elsif node.name == :validates_with
|
|
36
131
|
arguments(node).filter_map { |argument| constant_name(argument) }.each do |validator|
|
|
37
|
-
constant_candidates(validator, context.
|
|
132
|
+
constant_candidates(validator, context.lexical_nesting).each do |owner|
|
|
38
133
|
entrypoint_hints << EntryPoint.new(node_id: "#{owner}#validate", reason: :callback_registered)
|
|
39
134
|
end
|
|
40
135
|
end
|
|
41
136
|
end
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def runtime_block_macro_enabled?(node)
|
|
141
|
+
return framework_enabled?('rails') if RAILS_RUNTIME_BLOCK_MACROS.include?(node.name)
|
|
142
|
+
return framework_enabled?('sidekiq') if SIDEKIQ_RUNTIME_BLOCK_MACROS.include?(node.name)
|
|
143
|
+
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def handle_graphql_field(node, context)
|
|
148
|
+
return unless node.name == :field && context.owner && !context.test
|
|
149
|
+
return unless framework_enabled?('graphql')
|
|
150
|
+
return unless graphql_owner?(context.owner)
|
|
151
|
+
|
|
152
|
+
method_name = if keyword_keys(node).include?('method')
|
|
153
|
+
keyword_value(node, 'method')
|
|
154
|
+
else
|
|
155
|
+
literal_argument(node, index: 0)
|
|
156
|
+
end
|
|
157
|
+
if method_name.is_a?(String)
|
|
158
|
+
entrypoint_hints << EntryPoint.new(
|
|
159
|
+
node_id: "#{context.owner}##{method_name}",
|
|
160
|
+
reason: :graphql_field,
|
|
161
|
+
evidence: { 'type' => 'graphql_field', 'field' => literal_argument(node, index: 0) }
|
|
162
|
+
)
|
|
163
|
+
else
|
|
164
|
+
record_semantic_blocker(
|
|
165
|
+
:graphql_field,
|
|
166
|
+
node,
|
|
167
|
+
context,
|
|
168
|
+
'GraphQL field resolver method is not statically bounded',
|
|
169
|
+
suggested_action: :review_graphql_field
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def graphql_owner?(owner)
|
|
175
|
+
owner.end_with?('Type', 'Resolver', 'Mutation', 'Subscription') ||
|
|
176
|
+
convention_ancestors(owner).any? { |ancestor| ancestor.start_with?('GraphQL::') }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def framework_enabled?(name)
|
|
180
|
+
project.config.frameworks(reference_files: project.reference_files).include?(name)
|
|
181
|
+
rescue SystemCallError, EncodingError
|
|
182
|
+
project.config.frameworks.include?(name)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def callback_condition_names(node)
|
|
186
|
+
%w[if unless].filter_map do |key|
|
|
187
|
+
value = keyword_value(node, key)
|
|
188
|
+
value if value.is_a?(String)
|
|
189
|
+
end.uniq
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def callback_disabled?(node)
|
|
193
|
+
(keyword_keys(node).include?('if') && keyword_value(node, 'if') == false) ||
|
|
194
|
+
(keyword_keys(node).include?('unless') && keyword_value(node, 'unless') == true)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def callback_condition_unknown?(node)
|
|
198
|
+
keyword_keys(node).intersect?(%w[if unless]) &&
|
|
199
|
+
keyword_value(node, 'if').nil? && keyword_value(node, 'unless').nil?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def record_callback_condition_blocker(node, context)
|
|
203
|
+
record_semantic_blocker(
|
|
204
|
+
:rails_callback_condition,
|
|
205
|
+
node,
|
|
206
|
+
context,
|
|
207
|
+
'callback condition is not statically known',
|
|
208
|
+
suggested_action: :review_callback_condition
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def handle_generated_rails_methods(node, context)
|
|
213
|
+
return false unless RAILS_GENERATED_METHOD_MACROS.include?(node.name)
|
|
214
|
+
return false unless context.owner && !context.test && rails_framework_enabled?
|
|
215
|
+
|
|
216
|
+
return handle_rails_enum(node, context) if node.name == :enum
|
|
217
|
+
return handle_rails_scope(node, context) if node.name == :scope
|
|
218
|
+
|
|
219
|
+
names = literal_positional_names(node)
|
|
220
|
+
if names.empty?
|
|
221
|
+
record_semantic_blocker(
|
|
222
|
+
:rails_generated_methods,
|
|
223
|
+
node,
|
|
224
|
+
context,
|
|
225
|
+
"#{node.name} has dynamic generated method names",
|
|
226
|
+
suggested_action: :review_generated_methods
|
|
227
|
+
)
|
|
228
|
+
return handled_call
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
case node.name
|
|
232
|
+
when :store_accessor
|
|
233
|
+
add_generated_methods(context, names.drop(1).flat_map { |name| [name, "#{name}="] }, node)
|
|
234
|
+
when :store
|
|
235
|
+
accessors = array_keyword_values(node, 'accessors')
|
|
236
|
+
add_generated_methods(context, [names.first, "#{names.first}=", *accessors.flat_map { |name| [name, "#{name}="] }], node)
|
|
237
|
+
when :attribute
|
|
238
|
+
add_generated_methods(context, [names.first, "#{names.first}="], node)
|
|
239
|
+
when :class_attribute, :mattr_reader, :mattr_writer, :mattr_accessor,
|
|
240
|
+
:cattr_reader, :cattr_writer, :cattr_accessor
|
|
241
|
+
reader = !node.name.to_s.end_with?('_writer')
|
|
242
|
+
writer = !node.name.to_s.end_with?('_reader')
|
|
243
|
+
generated = names.flat_map { |name| [reader && name, writer && "#{name}="].compact }
|
|
244
|
+
add_generated_methods(context, generated, node)
|
|
245
|
+
add_generated_methods(context, generated, node, kind: :singleton_method)
|
|
246
|
+
when :belongs_to, :has_one
|
|
247
|
+
names.each do |name|
|
|
248
|
+
add_generated_methods(
|
|
249
|
+
context,
|
|
250
|
+
[name, "#{name}=", "build_#{name}", "create_#{name}", "create_#{name}!", "reload_#{name}", "reset_#{name}"],
|
|
251
|
+
node
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
when :has_many
|
|
255
|
+
names.each { |name| add_generated_methods(context, [name, "#{name}="], node) }
|
|
256
|
+
end
|
|
257
|
+
handled_call
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def handle_rails_enum(node, context)
|
|
261
|
+
declarations = enum_declarations(node)
|
|
262
|
+
if declarations.empty?
|
|
263
|
+
record_semantic_blocker(
|
|
264
|
+
:rails_generated_methods,
|
|
265
|
+
node,
|
|
266
|
+
context,
|
|
267
|
+
'enum has a dynamic name or value set',
|
|
268
|
+
suggested_action: :review_generated_methods
|
|
269
|
+
)
|
|
270
|
+
return handled_call
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
declarations.each do |enum_name, values|
|
|
274
|
+
add_generated_methods(context, [enum_name, "#{enum_name}="], node)
|
|
275
|
+
next if keyword_value(node, 'instance_methods') == false
|
|
276
|
+
|
|
277
|
+
decorated = values.map { |value| decorate_enum_value(enum_name, value, node) }
|
|
278
|
+
add_generated_methods(context, decorated.flat_map { |value| ["#{value}?", "#{value}!"] }, node)
|
|
279
|
+
next if keyword_value(node, 'scopes') == false
|
|
280
|
+
|
|
281
|
+
scopes = decorated.flat_map { |value| [value, "not_#{value}"] }
|
|
282
|
+
add_generated_methods(context, scopes, node, kind: :singleton_method)
|
|
283
|
+
end
|
|
284
|
+
handled_call
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def enum_declarations(node)
|
|
288
|
+
positional = arguments(node).grep_v(Prism::KeywordHashNode)
|
|
289
|
+
if literal_name?(positional.first)
|
|
290
|
+
values = literal_hash_keys(positional[1])
|
|
291
|
+
values = literal_array_values(positional[1]) if values.empty?
|
|
292
|
+
if values.empty?
|
|
293
|
+
options = %w[prefix suffix _prefix _suffix scopes instance_methods validate]
|
|
294
|
+
values = keyword_keys(node) - options
|
|
295
|
+
end
|
|
296
|
+
return [[literal_value(positional.first), values]] unless values.empty?
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
keyword_hash = arguments(node).find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
300
|
+
Array(keyword_hash&.elements).filter_map do |element|
|
|
301
|
+
next unless element.is_a?(Prism::AssocNode)
|
|
302
|
+
|
|
303
|
+
enum_name = literal_value(element.key)
|
|
304
|
+
values = literal_hash_keys(element.value)
|
|
305
|
+
values = literal_array_values(element.value) if values.empty?
|
|
306
|
+
[enum_name, values] if enum_name && !values.empty?
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def decorate_enum_value(enum_name, value, node)
|
|
311
|
+
prefix = keyword_value(node, 'prefix') || keyword_value(node, '_prefix')
|
|
312
|
+
suffix = keyword_value(node, 'suffix') || keyword_value(node, '_suffix')
|
|
313
|
+
value = "#{prefix == true ? enum_name : prefix}_#{value}" if prefix
|
|
314
|
+
value = "#{value}_#{suffix == true ? enum_name : suffix}" if suffix
|
|
315
|
+
value
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def handle_rails_scope(node, context)
|
|
319
|
+
name = literal_argument(node, index: 0)
|
|
320
|
+
unless name
|
|
321
|
+
record_semantic_blocker(
|
|
322
|
+
:rails_generated_methods,
|
|
323
|
+
node,
|
|
324
|
+
context,
|
|
325
|
+
'scope has a runtime-computed name',
|
|
326
|
+
suggested_action: :review_generated_methods
|
|
327
|
+
)
|
|
328
|
+
return handled_call
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
definition = add_generated_methods(context, [name], node, kind: :singleton_method).first
|
|
332
|
+
callable = arguments(node)[1]
|
|
333
|
+
body = callable.body if callable.is_a?(Prism::LambdaNode)
|
|
334
|
+
body ||= node.block&.body
|
|
335
|
+
if body
|
|
336
|
+
scope_context = context.dup
|
|
337
|
+
scope_context.current_caller_id = definition.graph_id
|
|
338
|
+
scope_context.current_method_name = name
|
|
339
|
+
scope_context.current_kind = :singleton_method
|
|
340
|
+
scope_context.static_ancestry = false
|
|
341
|
+
scope_context.flow_result = nil
|
|
342
|
+
visit(body, scope_context)
|
|
343
|
+
return handled_call(arguments: false)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
record_semantic_blocker(
|
|
347
|
+
:rails_generated_methods,
|
|
348
|
+
node,
|
|
349
|
+
context,
|
|
350
|
+
'scope body is not a literal block or lambda',
|
|
351
|
+
suggested_action: :review_generated_methods
|
|
352
|
+
)
|
|
353
|
+
handled_call
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def literal_positional_names(node)
|
|
357
|
+
arguments(node).grep_v(Prism::KeywordHashNode).filter_map do |argument|
|
|
358
|
+
literal_value(argument) if literal_name?(argument)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def array_keyword_values(node, key)
|
|
363
|
+
hash = arguments(node).find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
364
|
+
pair = Array(hash&.elements).find do |element|
|
|
365
|
+
element.is_a?(Prism::AssocNode) && literal_value(element.key).to_s == key
|
|
366
|
+
end
|
|
367
|
+
literal_array_values(pair&.value)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def literal_hash_keys(node)
|
|
371
|
+
return [] unless node.is_a?(Prism::HashNode) || node.is_a?(Prism::KeywordHashNode)
|
|
372
|
+
|
|
373
|
+
Array(node.elements).filter_map do |element|
|
|
374
|
+
literal_value(element.key) if element.is_a?(Prism::AssocNode) && literal_name?(element.key)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def literal_array_values(node)
|
|
379
|
+
return [] unless node.is_a?(Prism::ArrayNode) && !node.contains_splat?
|
|
380
|
+
|
|
381
|
+
node.elements.filter_map { |element| literal_value(element) if literal_name?(element) }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def rails_framework_enabled?
|
|
385
|
+
project.config.frameworks.include?('rails') || project.config.rails_enabled?(reference_files: project.reference_files)
|
|
386
|
+
rescue SystemCallError, EncodingError
|
|
387
|
+
project.config.frameworks.include?('rails')
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def add_generated_methods(context, names, source_node, kind: nil)
|
|
391
|
+
names.uniq.map do |name|
|
|
392
|
+
generated_kind, separator = if kind
|
|
393
|
+
[kind, kind == :singleton_method ? '.' : '#']
|
|
394
|
+
else
|
|
395
|
+
method_kind_and_separator(context)
|
|
396
|
+
end
|
|
397
|
+
add_definition(
|
|
398
|
+
symbol_id: "#{context.owner}#{separator}#{name}",
|
|
399
|
+
kind: generated_kind,
|
|
400
|
+
source_node: source_node,
|
|
401
|
+
context: context,
|
|
402
|
+
defined_via: :"rails_#{source_node.name}",
|
|
403
|
+
owner: context.owner,
|
|
404
|
+
name: name,
|
|
405
|
+
visibility: :public
|
|
406
|
+
)
|
|
407
|
+
end
|
|
42
408
|
end
|
|
43
409
|
|
|
44
410
|
def handle_attr_macro(node, context)
|
|
45
411
|
return false unless ATTR_MACROS.include?(node.name)
|
|
46
412
|
return false unless context.owner
|
|
47
413
|
|
|
48
|
-
|
|
414
|
+
names = arguments(node).filter_map { |argument| literal_value(argument) if literal_name?(argument) }
|
|
415
|
+
if names.length != arguments(node).length
|
|
416
|
+
record_semantic_blocker(
|
|
417
|
+
:dynamic_generated_methods,
|
|
418
|
+
node,
|
|
419
|
+
context,
|
|
420
|
+
"#{node.name} has a runtime-computed method name",
|
|
421
|
+
suggested_action: :make_method_name_literal
|
|
422
|
+
)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
names.each do |name|
|
|
49
426
|
kind, separator = method_kind_and_separator(context)
|
|
50
|
-
|
|
51
|
-
|
|
427
|
+
add_definition(
|
|
428
|
+
symbol_id: "#{context.owner}#{separator}#{name}",
|
|
52
429
|
kind: kind,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end_line: node.location.end_line,
|
|
430
|
+
source_node: node,
|
|
431
|
+
context: context,
|
|
56
432
|
defined_via: node.name,
|
|
57
433
|
owner: context.owner,
|
|
58
434
|
name: name,
|
|
59
|
-
test: context.test,
|
|
60
435
|
visibility: context.visibility
|
|
61
436
|
)
|
|
62
437
|
next unless %i[attr_writer attr_accessor].include?(node.name)
|
|
63
438
|
|
|
64
|
-
|
|
65
|
-
|
|
439
|
+
add_definition(
|
|
440
|
+
symbol_id: "#{context.owner}#{separator}#{name}=",
|
|
66
441
|
kind: kind,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
end_line: node.location.end_line,
|
|
442
|
+
source_node: node,
|
|
443
|
+
context: context,
|
|
70
444
|
defined_via: node.name,
|
|
71
445
|
owner: context.owner,
|
|
72
446
|
name: "#{name}=",
|
|
73
|
-
test: context.test,
|
|
74
447
|
visibility: context.visibility
|
|
75
448
|
)
|
|
76
449
|
end
|
|
77
|
-
|
|
450
|
+
handled_call
|
|
78
451
|
end
|
|
79
452
|
|
|
80
453
|
def handle_delegate(node, context)
|
|
@@ -83,26 +456,36 @@ module Necropsy
|
|
|
83
456
|
|
|
84
457
|
target = keyword_value(node, 'to')
|
|
85
458
|
prefix = keyword_value(node, 'prefix')
|
|
86
|
-
|
|
459
|
+
positional = arguments(node).grep_v(Prism::KeywordHashNode)
|
|
460
|
+
names = positional.filter_map { |argument| literal_value(argument) if literal_name?(argument) }
|
|
461
|
+
if names.length != positional.length || target.nil?
|
|
462
|
+
record_semantic_blocker(
|
|
463
|
+
:dynamic_generated_methods,
|
|
464
|
+
node,
|
|
465
|
+
context,
|
|
466
|
+
'delegate name or target is not statically bounded',
|
|
467
|
+
suggested_action: :make_delegate_literal
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
names.each do |name|
|
|
87
472
|
generated_name = delegated_method_name(name, target, prefix)
|
|
88
473
|
kind, separator = method_kind_and_separator(context)
|
|
89
474
|
id = "#{context.owner}#{separator}#{generated_name}"
|
|
90
|
-
|
|
91
|
-
|
|
475
|
+
definition = add_definition(
|
|
476
|
+
symbol_id: id,
|
|
92
477
|
kind: kind,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
end_line: node.location.end_line,
|
|
478
|
+
source_node: node,
|
|
479
|
+
context: context,
|
|
96
480
|
defined_via: :delegate,
|
|
97
481
|
owner: context.owner,
|
|
98
482
|
name: generated_name,
|
|
99
|
-
test: context.test,
|
|
100
483
|
visibility: context.visibility
|
|
101
484
|
)
|
|
102
|
-
record_delegate_target(
|
|
103
|
-
record_delegated_message(
|
|
485
|
+
record_delegate_target(definition.graph_id, target, node, context) if target
|
|
486
|
+
record_delegated_message(definition.graph_id, name, node, context)
|
|
104
487
|
end
|
|
105
|
-
|
|
488
|
+
handled_call
|
|
106
489
|
end
|
|
107
490
|
|
|
108
491
|
def handle_forwardable(node, context)
|
|
@@ -110,7 +493,16 @@ module Necropsy
|
|
|
110
493
|
return false unless context.owner
|
|
111
494
|
|
|
112
495
|
symbols = symbol_arguments(node)
|
|
113
|
-
|
|
496
|
+
if symbols.length < 2 || symbols.length != arguments(node).length
|
|
497
|
+
record_semantic_blocker(
|
|
498
|
+
:dynamic_generated_methods,
|
|
499
|
+
node,
|
|
500
|
+
context,
|
|
501
|
+
"#{node.name} has runtime-computed delegation names",
|
|
502
|
+
suggested_action: :make_delegate_literal
|
|
503
|
+
)
|
|
504
|
+
return handled_call
|
|
505
|
+
end
|
|
114
506
|
|
|
115
507
|
target = symbols.shift
|
|
116
508
|
definitions = if node.name == :def_delegator
|
|
@@ -121,22 +513,20 @@ module Necropsy
|
|
|
121
513
|
definitions.each do |generated_name, delegated_name|
|
|
122
514
|
kind, separator = method_kind_and_separator(context)
|
|
123
515
|
id = "#{context.owner}#{separator}#{generated_name}"
|
|
124
|
-
|
|
125
|
-
|
|
516
|
+
definition = add_definition(
|
|
517
|
+
symbol_id: id,
|
|
126
518
|
kind: kind,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
end_line: node.location.end_line,
|
|
519
|
+
source_node: node,
|
|
520
|
+
context: context,
|
|
130
521
|
defined_via: node.name,
|
|
131
522
|
owner: context.owner,
|
|
132
523
|
name: generated_name,
|
|
133
|
-
test: context.test,
|
|
134
524
|
visibility: context.visibility
|
|
135
525
|
)
|
|
136
|
-
record_delegate_target(
|
|
137
|
-
record_delegated_message(
|
|
526
|
+
record_delegate_target(definition.graph_id, target, node, context)
|
|
527
|
+
record_delegated_message(definition.graph_id, delegated_name, node, context)
|
|
138
528
|
end
|
|
139
|
-
|
|
529
|
+
handled_call
|
|
140
530
|
end
|
|
141
531
|
end
|
|
142
532
|
end
|