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.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +33 -0
  3. data/CHANGELOG.md +44 -0
  4. data/MEASUREMENTS.md +112 -0
  5. data/README.md +233 -16
  6. data/Rakefile +3 -1
  7. data/bench/README.md +92 -0
  8. data/bench/audit.rb +113 -0
  9. data/bench/audits/0.2.1/audit.json +501 -0
  10. data/bench/audits/0.2.1/audit.md +55 -0
  11. data/bench/audits/0.2.1/baseline_performance.yml +16 -0
  12. data/bench/audits/0.2.1/config.yml +42 -0
  13. data/bench/audits/0.2.1/review.yml +56 -0
  14. data/bench/audits/0.2.1/review_queue.yml +3651 -0
  15. data/bench/corpora/v1/README.md +30 -0
  16. data/bench/corpora/v1/labels.yml +37 -0
  17. data/bench/corpora/v1/manifest.yml +63 -0
  18. data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
  19. data/bench/corpora/v1/self.necropsy.yml +8 -0
  20. data/bench/corpora/v1/tools/debride.yml +42 -0
  21. data/bench/corpora/v1/tools/spoom.yml +12 -0
  22. data/bench/corpora/v1/tools/type_aware.yml +11 -0
  23. data/bench/golden/v1/candidate_union.json +21918 -0
  24. data/bench/golden/v1/metadata.json +12 -0
  25. data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
  26. data/bench/golden/v1/reports/plain_ruby.json +135 -0
  27. data/bench/golden/v1/reports/rails.json +176 -0
  28. data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
  29. data/bench/golden/v1/reports/self.json +3449 -0
  30. data/bench/review_queue.rb +35 -0
  31. data/bench/run.rb +31 -0
  32. data/bench/schema/candidate-union-v1.schema.json +70 -0
  33. data/docs/impv_implementation_matrix.md +179 -0
  34. data/docs/migrations/0.2.1.md +57 -0
  35. data/docs/migrations/0.3.0.md +207 -0
  36. data/docs/migrations/0.4.0.md +13 -0
  37. data/docs/necropsy_performance_adr.md +43 -0
  38. data/docs/necropsy_scope_decisions.md +58 -0
  39. data/docs/necropsy_type_facts_adr.md +22 -0
  40. data/gemfiles/prism_min.gemfile +9 -0
  41. data/gemfiles/prism_min.gemfile.lock +50 -0
  42. data/lib/necropsy/analyzer.rb +121 -2
  43. data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +81 -19
  44. data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
  45. data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
  46. data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
  47. data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
  48. data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
  49. data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
  50. data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
  51. data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
  52. data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
  53. data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +225 -20
  54. data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
  55. data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
  56. data/lib/necropsy/analyzers/static/cha.rb +33 -69
  57. data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
  58. data/lib/necropsy/analyzers/static/rta.rb +249 -25
  59. data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
  60. data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
  61. data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
  62. data/lib/necropsy/ast_scanner/dsl_macros.rb +532 -0
  63. data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
  64. data/lib/necropsy/ast_scanner/references.rb +115 -0
  65. data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
  66. data/lib/necropsy/ast_scanner/traversal.rb +347 -0
  67. data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
  68. data/lib/necropsy/ast_scanner.rb +89 -564
  69. data/lib/necropsy/bench/candidate_union.rb +555 -0
  70. data/lib/necropsy/bench/claim_gate.rb +112 -0
  71. data/lib/necropsy/bench/evaluator.rb +356 -24
  72. data/lib/necropsy/bench/finding_facts.rb +152 -0
  73. data/lib/necropsy/bench/precision_gate.rb +144 -0
  74. data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
  75. data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
  76. data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
  77. data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
  78. data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
  79. data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
  80. data/lib/necropsy/bench/release_audit.rb +360 -0
  81. data/lib/necropsy/bench/report_normalizer.rb +140 -0
  82. data/lib/necropsy/bench/review_queue.rb +154 -0
  83. data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
  84. data/lib/necropsy/bench/seed_runner.rb +408 -0
  85. data/lib/necropsy/bounded_canonicalizer.rb +218 -0
  86. data/lib/necropsy/cache/scan_cache.rb +94 -18
  87. data/lib/necropsy/call_site_identity.rb +54 -0
  88. data/lib/necropsy/cli.rb +296 -53
  89. data/lib/necropsy/clock.rb +40 -0
  90. data/lib/necropsy/confidence/scorer.rb +181 -58
  91. data/lib/necropsy/configuration.rb +339 -18
  92. data/lib/necropsy/convention_rules.rb +138 -0
  93. data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
  94. data/lib/necropsy/definition_identity.rb +37 -0
  95. data/lib/necropsy/diagnostics.rb +342 -0
  96. data/lib/necropsy/embedded_ruby.rb +55 -0
  97. data/lib/necropsy/entry_points/plain.rb +125 -7
  98. data/lib/necropsy/entry_points/rails.rb +422 -105
  99. data/lib/necropsy/entry_points/test.rb +6 -1
  100. data/lib/necropsy/flow_interpreter.rb +460 -0
  101. data/lib/necropsy/graph/blocker_matching.rb +338 -0
  102. data/lib/necropsy/graph/call_graph.rb +1234 -62
  103. data/lib/necropsy/graph/definition_index.rb +149 -0
  104. data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
  105. data/lib/necropsy/graph/evidence_store.rb +213 -0
  106. data/lib/necropsy/graph/resolution_store.rb +497 -0
  107. data/lib/necropsy/graph_self_check.rb +79 -0
  108. data/lib/necropsy/guardrail/baseline.rb +359 -13
  109. data/lib/necropsy/guardrail/diff.rb +5 -2
  110. data/lib/necropsy/guardrail/quarantine.rb +101 -10
  111. data/lib/necropsy/load_graph.rb +206 -0
  112. data/lib/necropsy/models.rb +894 -21
  113. data/lib/necropsy/performance_profiler.rb +108 -0
  114. data/lib/necropsy/project.rb +361 -13
  115. data/lib/necropsy/reachability/engine.rb +80 -15
  116. data/lib/necropsy/reference_barrier.rb +458 -0
  117. data/lib/necropsy/report.rb +153 -14
  118. data/lib/necropsy/reporter.rb +441 -19
  119. data/lib/necropsy/runner.rb +258 -14
  120. data/lib/necropsy/runtime_feedback.rb +136 -0
  121. data/lib/necropsy/semantics_matrix.rb +153 -0
  122. data/lib/necropsy/trace_point_runtime.rb +19 -0
  123. data/lib/necropsy/type_facts.rb +53 -0
  124. data/lib/necropsy/version.rb +1 -1
  125. data/lib/necropsy/why_not_explanation.rb +436 -0
  126. data/lib/necropsy/why_not_renderer.rb +197 -0
  127. data/lib/necropsy/world_policy.rb +90 -0
  128. data/lib/necropsy.rb +39 -2
  129. data/schema/necropsy-report-v2.schema.json +366 -0
  130. data/script/measure.rb +20 -0
  131. 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
- next unless SCRIPT_PATTERNS.any? { |pattern| File.fnmatch?(pattern, node.file, File::FNM_PATHNAME) }
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(node.id, :main_script)
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.each_key do |node_id|
23
- graph.add_entry_point(node_id, :public_api_declared) if File.fnmatch?(pattern, node_id)
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 == 'lib/necropsy.rb'
29
- next unless node.kind == :singleton_method && node.owner == 'Necropsy'
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
- graph.add_entry_point(node.id, :public_api_declared)
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