necropsy 0.1.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/.rubocop.yml +33 -0
- data/CHANGELOG.md +44 -0
- data/MEASUREMENTS.md +112 -0
- data/README.md +233 -16
- data/Rakefile +3 -1
- 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 +81 -19
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
- 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 +225 -20
- 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 -69
- data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
- data/lib/necropsy/analyzers/static/rta.rb +249 -25
- data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
- 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 +532 -0
- data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
- data/lib/necropsy/ast_scanner/references.rb +115 -0
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
- data/lib/necropsy/ast_scanner/traversal.rb +347 -0
- data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
- data/lib/necropsy/ast_scanner.rb +89 -564
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +356 -24
- 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 +94 -18
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +296 -53
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +181 -58
- data/lib/necropsy/configuration.rb +339 -18
- 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 +342 -0
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +125 -7
- data/lib/necropsy/entry_points/rails.rb +422 -105
- 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 +1234 -62
- 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 +359 -13
- data/lib/necropsy/guardrail/diff.rb +5 -2
- data/lib/necropsy/guardrail/quarantine.rb +101 -10
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +894 -21
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +361 -13
- data/lib/necropsy/reachability/engine.rb +80 -15
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +153 -14
- data/lib/necropsy/reporter.rb +441 -19
- data/lib/necropsy/runner.rb +258 -14
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -0
- data/lib/necropsy/trace_point_runtime.rb +19 -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 +39 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- data/script/measure.rb +20 -0
- metadata +106 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
5
|
+
module Necropsy
|
|
6
|
+
module EmbeddedRuby
|
|
7
|
+
TAG_PATTERN = /<%(?![%#])[-=]?(.*?)-?%>/m
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def extract(source)
|
|
12
|
+
cursor = 0
|
|
13
|
+
output = +''
|
|
14
|
+
source.to_enum(:scan, TAG_PATTERN).each do
|
|
15
|
+
match = Regexp.last_match
|
|
16
|
+
output << blank(source[cursor...match.begin(0)])
|
|
17
|
+
output << strip_ruby_comments(match[1])
|
|
18
|
+
cursor = match.end(0)
|
|
19
|
+
end
|
|
20
|
+
output << blank(source[cursor..])
|
|
21
|
+
output
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call_names(source)
|
|
25
|
+
result = Prism.parse(extract(source))
|
|
26
|
+
return Set.new if result.failure?
|
|
27
|
+
|
|
28
|
+
names = Set.new
|
|
29
|
+
pending = [result.value]
|
|
30
|
+
until pending.empty?
|
|
31
|
+
node = pending.pop
|
|
32
|
+
names << node.name.to_s if node.is_a?(Prism::CallNode) && node.name
|
|
33
|
+
pending.concat(node.child_nodes.compact)
|
|
34
|
+
end
|
|
35
|
+
names
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def blank(source)
|
|
39
|
+
source.to_s.gsub(/[^\n]/, ' ')
|
|
40
|
+
end
|
|
41
|
+
private_class_method :blank
|
|
42
|
+
|
|
43
|
+
def strip_ruby_comments(source)
|
|
44
|
+
bytes = source.bytes
|
|
45
|
+
Prism.parse(source).comments.each do |comment|
|
|
46
|
+
location = comment.location
|
|
47
|
+
(location.start_offset...location.end_offset).each do |offset|
|
|
48
|
+
bytes[offset] = 32 unless bytes[offset] == 10
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
bytes.pack('C*').force_encoding(source.encoding)
|
|
52
|
+
end
|
|
53
|
+
private_class_method :strip_ruby_comments
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
3
5
|
module Necropsy
|
|
4
6
|
module EntryPoints
|
|
5
7
|
class Plain
|
|
@@ -13,22 +15,138 @@ module Necropsy
|
|
|
13
15
|
|
|
14
16
|
def apply(graph, project)
|
|
15
17
|
graph.nodes.values.select { |node| node.kind == :block_entry && !node.test }.each do |node|
|
|
16
|
-
|
|
18
|
+
pattern = SCRIPT_PATTERNS.find { |candidate| File.fnmatch?(candidate, node.file, File::FNM_PATHNAME) }
|
|
19
|
+
next unless pattern
|
|
17
20
|
|
|
18
|
-
graph.add_entry_point(
|
|
21
|
+
graph.add_entry_point(
|
|
22
|
+
node.graph_id,
|
|
23
|
+
:main_script,
|
|
24
|
+
domain: :runtime,
|
|
25
|
+
evidence: { 'type' => 'path_policy', 'pattern' => pattern, 'file' => node.file }
|
|
26
|
+
)
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
project.config.entry_point_patterns.each do |pattern|
|
|
22
|
-
graph.nodes.
|
|
23
|
-
|
|
30
|
+
graph.nodes.each_value do |node|
|
|
31
|
+
next if node.test
|
|
32
|
+
next unless File.fnmatch?(pattern, node.symbol_id)
|
|
33
|
+
|
|
34
|
+
graph.add_entry_point(
|
|
35
|
+
node.graph_id,
|
|
36
|
+
:public_api_declared,
|
|
37
|
+
domain: project.config.library_world? ? :external : :runtime,
|
|
38
|
+
evidence: { 'type' => 'configured_pattern', 'pattern' => pattern }
|
|
39
|
+
)
|
|
24
40
|
end
|
|
25
41
|
end
|
|
26
42
|
|
|
43
|
+
return if project.config.library_world?
|
|
44
|
+
|
|
45
|
+
api_files = primary_library_files(project)
|
|
27
46
|
graph.method_nodes.each do |node|
|
|
28
|
-
next unless node.file
|
|
29
|
-
next unless node.kind == :singleton_method && node.
|
|
47
|
+
next unless api_files.include?(node.file)
|
|
48
|
+
next unless node.kind == :singleton_method && node.visibility == :public
|
|
49
|
+
|
|
50
|
+
graph.add_entry_point(
|
|
51
|
+
node.graph_id,
|
|
52
|
+
:public_api_declared,
|
|
53
|
+
domain: :runtime,
|
|
54
|
+
evidence: { 'type' => 'gem_entry_file', 'file' => node.file }
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def primary_library_files(project)
|
|
62
|
+
gemspec_metadata(project).flat_map do |metadata|
|
|
63
|
+
metadata.fetch(:require_paths).flat_map do |require_path|
|
|
64
|
+
name = metadata.fetch(:name)
|
|
65
|
+
["#{require_path}/#{name.tr('-', '/')}.rb", "#{require_path}/#{name.tr('-', '_')}.rb"]
|
|
66
|
+
end
|
|
67
|
+
end.uniq.select { |relative| File.file?(File.join(project.root, relative)) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def gemspec_metadata(project)
|
|
71
|
+
project.reference_files.filter_map do |path|
|
|
72
|
+
relative = project.relative_path(path)
|
|
73
|
+
next unless relative.match?(%r{\A[^/]+\.gemspec\z})
|
|
74
|
+
|
|
75
|
+
parse_gemspec(File.read(path))
|
|
76
|
+
rescue SystemCallError, EncodingError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_gemspec(source)
|
|
82
|
+
result = Prism.parse(source)
|
|
83
|
+
return if result.failure?
|
|
84
|
+
|
|
85
|
+
specification = find_specification_call(result.value)
|
|
86
|
+
return unless specification&.block
|
|
87
|
+
|
|
88
|
+
parameter = required_block_parameter(specification.block)
|
|
89
|
+
return unless parameter.respond_to?(:name)
|
|
90
|
+
|
|
91
|
+
assignments = specification_assignments(specification.block.body, parameter.name)
|
|
92
|
+
name_node = first_argument(assignments[:name])
|
|
93
|
+
name = name_node.unescaped.to_s if name_node.is_a?(Prism::StringNode)
|
|
94
|
+
return if name.to_s.empty?
|
|
95
|
+
|
|
96
|
+
paths_node = first_argument(assignments[:require_paths])
|
|
97
|
+
paths = literal_string_array(paths_node)
|
|
98
|
+
{ name: name, require_paths: paths.empty? ? ['lib'] : paths }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def required_block_parameter(block)
|
|
102
|
+
block_parameters = block.parameters
|
|
103
|
+
parameters = block_parameters.parameters if block_parameters
|
|
104
|
+
Array(parameters&.requireds).first
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def first_argument(call)
|
|
108
|
+
arguments = call.arguments if call
|
|
109
|
+
Array(arguments&.arguments).first
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def find_specification_call(root)
|
|
113
|
+
pending = [root]
|
|
114
|
+
until pending.empty?
|
|
115
|
+
node = pending.pop
|
|
116
|
+
return node if node.is_a?(Prism::CallNode) && node.name == :new &&
|
|
117
|
+
constant_name(node.receiver) == 'Gem::Specification'
|
|
118
|
+
|
|
119
|
+
pending.concat(node.child_nodes.compact)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def specification_assignments(root, parameter_name)
|
|
124
|
+
pending = [root]
|
|
125
|
+
assignments = {}
|
|
126
|
+
until pending.empty?
|
|
127
|
+
node = pending.pop
|
|
128
|
+
if node.is_a?(Prism::CallNode) && node.receiver.is_a?(Prism::LocalVariableReadNode) &&
|
|
129
|
+
node.receiver.name == parameter_name
|
|
130
|
+
assignments[:name] = node if node.name == :name=
|
|
131
|
+
assignments[:require_paths] = node if node.name == :require_paths=
|
|
132
|
+
end
|
|
133
|
+
pending.concat(node.child_nodes.compact)
|
|
134
|
+
end
|
|
135
|
+
assignments
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def literal_string_array(node)
|
|
139
|
+
return [] unless node.is_a?(Prism::ArrayNode) && !node.contains_splat?
|
|
140
|
+
return [] unless node.elements.all?(Prism::StringNode)
|
|
141
|
+
|
|
142
|
+
node.elements.map { |element| element.unescaped.to_s }.reject(&:empty?).uniq.sort
|
|
143
|
+
end
|
|
30
144
|
|
|
31
|
-
|
|
145
|
+
def constant_name(node)
|
|
146
|
+
case node
|
|
147
|
+
when Prism::ConstantReadNode then node.name.to_s
|
|
148
|
+
when Prism::ConstantPathNode
|
|
149
|
+
[constant_name(node.parent), node.name.to_s].compact.join('::')
|
|
32
150
|
end
|
|
33
151
|
end
|
|
34
152
|
end
|