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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +33 -0
  3. data/CHANGELOG.md +18 -0
  4. data/MEASUREMENTS.md +85 -0
  5. data/README.md +60 -8
  6. data/Rakefile +3 -1
  7. data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +12 -2
  8. data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +5 -1
  9. data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +68 -14
  10. data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +107 -12
  11. data/lib/necropsy/analyzers/static/cha.rb +10 -6
  12. data/lib/necropsy/analyzers/static/name_resolution.rb +29 -20
  13. data/lib/necropsy/analyzers/static/rta.rb +19 -3
  14. data/lib/necropsy/ast_scanner/call_recording.rb +156 -0
  15. data/lib/necropsy/ast_scanner/dsl_macros.rb +142 -0
  16. data/lib/necropsy/ast_scanner/method_definitions.rb +176 -0
  17. data/lib/necropsy/ast_scanner/references.rb +81 -0
  18. data/lib/necropsy/ast_scanner/ruby_semantics.rb +177 -0
  19. data/lib/necropsy/ast_scanner/traversal.rb +184 -0
  20. data/lib/necropsy/ast_scanner/value_definitions.rb +68 -0
  21. data/lib/necropsy/ast_scanner.rb +24 -560
  22. data/lib/necropsy/bench/evaluator.rb +29 -10
  23. data/lib/necropsy/cache/scan_cache.rb +20 -12
  24. data/lib/necropsy/cli.rb +88 -32
  25. data/lib/necropsy/confidence/scorer.rb +97 -19
  26. data/lib/necropsy/configuration.rb +125 -7
  27. data/lib/necropsy/diagnostics.rb +202 -0
  28. data/lib/necropsy/entry_points/plain.rb +19 -2
  29. data/lib/necropsy/entry_points/rails.rb +121 -85
  30. data/lib/necropsy/graph/call_graph.rb +203 -21
  31. data/lib/necropsy/guardrail/baseline.rb +14 -5
  32. data/lib/necropsy/guardrail/diff.rb +5 -2
  33. data/lib/necropsy/guardrail/quarantine.rb +9 -3
  34. data/lib/necropsy/models.rb +19 -11
  35. data/lib/necropsy/project.rb +49 -3
  36. data/lib/necropsy/reachability/engine.rb +31 -7
  37. data/lib/necropsy/report.rb +44 -14
  38. data/lib/necropsy/reporter.rb +11 -5
  39. data/lib/necropsy/runner.rb +33 -4
  40. data/lib/necropsy/trace_point_runtime.rb +19 -0
  41. data/lib/necropsy/version.rb +1 -1
  42. data/lib/necropsy.rb +4 -0
  43. data/script/measure.rb +20 -0
  44. metadata +22 -2
@@ -9,10 +9,10 @@ module Necropsy
9
9
  attr_reader :path, :fingerprints
10
10
 
11
11
  def self.load(path)
12
- return new(path: path, fingerprints: []) unless File.exist?(path)
12
+ return new(path: path, findings: []) unless File.exist?(path)
13
13
 
14
- payload = YAML.load_file(path) || {}
15
- new(path: path, fingerprints: Array(payload['findings']).map { |finding| finding['fingerprint'] })
14
+ payload = YAML.safe_load_file(path, aliases: false) || {}
15
+ new(path: path, findings: Array(payload['findings']))
16
16
  end
17
17
 
18
18
  def self.write(report, path:)
@@ -34,14 +34,23 @@ module Necropsy
34
34
  File.write(path, payload.to_yaml)
35
35
  end
36
36
 
37
- def initialize(path:, fingerprints:)
37
+ def initialize(path:, findings:)
38
38
  @path = path
39
- @fingerprints = fingerprints
39
+ @findings = findings
40
+ @fingerprints = findings.filter_map { |finding| finding['fingerprint'] }.to_set
40
41
  end
41
42
 
42
43
  def include?(finding)
43
44
  fingerprints.include?(finding.fingerprint)
44
45
  end
46
+
47
+ def count_at_least(confidence)
48
+ threshold = CONFIDENCE_LEVELS.fetch(confidence)
49
+ @findings.count do |finding|
50
+ level = finding['confidence']&.to_sym
51
+ level && CONFIDENCE_LEVELS.fetch(level, -1) >= threshold
52
+ end
53
+ end
45
54
  end
46
55
  end
47
56
  end
@@ -6,8 +6,11 @@ module Necropsy
6
6
  module Guardrail
7
7
  class Diff
8
8
  def self.changed_files(root:, diff_base:)
9
- stdout, status = Open3.capture2('git', '-C', root, 'diff', '--name-only', "#{diff_base}...HEAD")
10
- return Set.new unless status.success?
9
+ stdout, stderr, status = Open3.capture3('git', '-C', root, 'diff', '--name-only', "#{diff_base}...HEAD")
10
+ unless status.success?
11
+ detail = stderr.strip.empty? ? 'unknown git error' : stderr.strip
12
+ raise Error, "Could not determine changed files from #{diff_base}: #{detail}"
13
+ end
11
14
 
12
15
  stdout.lines.map(&:strip).reject(&:empty?).to_set
13
16
  end
@@ -26,15 +26,21 @@ module Necropsy
26
26
  def write(min_confidence: :high)
27
27
  grouped = suggestions(min_confidence: min_confidence).group_by { |suggestion| suggestion[:path] }
28
28
  grouped.each do |path, entries|
29
- lines = File.readlines(path, chomp: true)
29
+ source = File.binread(path)
30
+ newline = source[/\r\n|\n/] || "\n"
31
+ trailing_newline = source.end_with?("\r\n", "\n")
32
+ lines = source.split(/\r\n|\n/, -1)
33
+ lines.pop if trailing_newline
30
34
  entries.sort_by { |entry| -entry[:line] }.each do |entry|
31
35
  index = [entry[:line] - 1, 0].max
32
- next if lines[index - 1]&.include?(ANNOTATION_PREFIX)
36
+ next if index.positive? && lines[index - 1]&.include?(ANNOTATION_PREFIX)
33
37
 
34
38
  indent = lines[index][/^\s*/] || ''
35
39
  lines.insert(index, "#{indent}#{entry[:annotation]}")
36
40
  end
37
- File.write(path, "#{lines.join("\n")}\n")
41
+ rewritten = lines.join(newline)
42
+ rewritten << newline if trailing_newline
43
+ File.binwrite(path, rewritten)
38
44
  end
39
45
  end
40
46
 
@@ -3,7 +3,14 @@
3
3
  require 'digest'
4
4
 
5
5
  module Necropsy
6
- Node = Data.define(:id, :kind, :file, :line, :end_line, :defined_via, :owner, :name, :test) do
6
+ CONFIDENCE_LEVELS = {
7
+ low: 0,
8
+ medium: 1,
9
+ high: 2,
10
+ certain: 3
11
+ }.freeze
12
+
13
+ Node = Data.define(:id, :kind, :file, :line, :end_line, :defined_via, :owner, :name, :test, :visibility) do
7
14
  def method?
8
15
  kind != :block_entry
9
16
  end
@@ -22,7 +29,8 @@ module Necropsy
22
29
  'defined_via' => defined_via.to_s,
23
30
  'owner' => owner,
24
31
  'name' => name,
25
- 'test' => test
32
+ 'test' => test,
33
+ 'visibility' => visibility.to_s
26
34
  }
27
35
  end
28
36
  end
@@ -127,22 +135,21 @@ module Necropsy
127
135
  EdgeEvidence = Data.define(:caller_id, :callee_id, :evidence)
128
136
  AliveEvidence = Data.define(:node_id, :evidence)
129
137
 
138
+ ScoreComponent = Data.define(:name, :value, :details) do
139
+ def to_h
140
+ { 'name' => name, 'value' => value, 'details' => details }
141
+ end
142
+ end
143
+
130
144
  AnalyzerResult = Data.define(:edge_evidences, :alive_evidences, :uncertainties, :observation) do
131
145
  def self.empty
132
146
  new(edge_evidences: [], alive_evidences: [], uncertainties: {}, observation: {})
133
147
  end
134
148
  end
135
149
 
136
- Finding = Data.define(:node, :classification, :confidence, :score, :reasons, :evidences) do
137
- LEVELS = {
138
- low: 0,
139
- medium: 1,
140
- high: 2,
141
- certain: 3
142
- }.freeze
143
-
150
+ Finding = Data.define(:node, :classification, :confidence, :score, :score_components, :reasons, :evidences) do
144
151
  def at_least?(level)
145
- LEVELS.fetch(confidence) >= LEVELS.fetch(level)
152
+ CONFIDENCE_LEVELS.fetch(confidence) >= CONFIDENCE_LEVELS.fetch(level)
146
153
  end
147
154
 
148
155
  def fingerprint
@@ -155,6 +162,7 @@ module Necropsy
155
162
  'classification' => classification.to_s,
156
163
  'confidence' => confidence.to_s,
157
164
  'score' => score,
165
+ 'score_components' => score_components.map(&:to_h),
158
166
  'node' => node.to_h,
159
167
  'reasons' => reasons,
160
168
  'evidences' => evidences.map(&:to_h)
@@ -7,7 +7,6 @@ module Necropsy
7
7
  EXCLUDED_DIRECTORIES = %w[
8
8
  .bundle
9
9
  .git
10
- .serena
11
10
  coverage
12
11
  doc
13
12
  node_modules
@@ -36,7 +35,12 @@ module Necropsy
36
35
  rake = Dir.glob(File.join(root, '**', '*.rake'))
37
36
  executables = Dir.glob(File.join(root, '{bin,exe}', '*')).select { |file| File.file?(file) }
38
37
  gemspecs = Dir.glob(File.join(root, '*.gemspec'))
39
- (globbed + special + rake + executables + gemspecs).uniq.select { |file| analyzable_file?(file) }.sort
38
+ candidates = (globbed + special + rake + executables + gemspecs).uniq
39
+ candidates.select! { |file| analyzable_file?(file) }
40
+ warn_excluded_entry_points(candidates) if config.include_paths.any?
41
+ candidates.select! { |file| included_path?(relative_path(file)) } if config.include_paths.any?
42
+ candidates.reject! { |file| excluded_path?(relative_path(file)) }
43
+ candidates.sort
40
44
  end
41
45
  end
42
46
 
@@ -57,9 +61,51 @@ module Necropsy
57
61
 
58
62
  def analyzable_file?(file)
59
63
  relative_parts = relative_path(file).split(File::SEPARATOR)
60
- !relative_parts.intersect?(EXCLUDED_DIRECTORIES)
64
+ return false if EXCLUDED_DIRECTORIES.include?(relative_parts.first)
65
+
66
+ ruby_source?(file)
61
67
  rescue ArgumentError
62
68
  false
63
69
  end
70
+
71
+ def ruby_source?(file)
72
+ return true if file.end_with?('.rb', '.rake', '.gemspec') || File.basename(file) == 'Rakefile'
73
+
74
+ File.open(file, &:readline).match?(/\A#!.*\bruby\b/)
75
+ rescue EOFError, SystemCallError, EncodingError
76
+ false
77
+ end
78
+
79
+ def included_path?(relative)
80
+ config.include_paths.any? { |pattern| path_matches?(pattern, relative) }
81
+ end
82
+
83
+ def excluded_path?(relative)
84
+ config.exclude_paths.any? { |pattern| path_matches?(pattern, relative) }
85
+ end
86
+
87
+ def path_matches?(pattern, relative)
88
+ File.fnmatch?(pattern, relative, File::FNM_PATHNAME | File::FNM_EXTGLOB) ||
89
+ File.fnmatch?(File.join(pattern, '**', '*'), relative, File::FNM_PATHNAME | File::FNM_EXTGLOB)
90
+ end
91
+
92
+ def warn_excluded_entry_points(candidates)
93
+ excluded = candidates.filter_map do |file|
94
+ relative = relative_path(file)
95
+ relative if potential_entry_point_path?(relative) && !included_path?(relative)
96
+ end
97
+ return if excluded.empty?
98
+
99
+ sample = excluded.sort.first(5)
100
+ suffix = excluded.length > sample.length ? " and #{excluded.length - sample.length} more" : ''
101
+ warn "Necropsy paths.include excludes potential entry points: #{sample.join(', ')}#{suffix}. " \
102
+ 'Use report.include to filter findings without narrowing analysis.'
103
+ end
104
+
105
+ def potential_entry_point_path?(relative)
106
+ relative == 'Rakefile' || relative == 'config/routes.rb' ||
107
+ relative.start_with?('bin/', 'exe/', 'spec/', 'test/') ||
108
+ relative.end_with?('.rake', '.gemspec')
109
+ end
64
110
  end
65
111
  end
@@ -2,7 +2,28 @@
2
2
 
3
3
  module Necropsy
4
4
  module Reachability
5
- Result = Data.define(:runtime_alive, :test_alive)
5
+ Result = Data.define(:runtime_paths, :test_paths) do
6
+ def runtime_alive
7
+ runtime_paths.keys
8
+ end
9
+
10
+ def test_alive
11
+ test_paths.keys
12
+ end
13
+
14
+ def witness(node_id, kind: :runtime)
15
+ paths = kind == :runtime ? runtime_paths : test_paths
16
+ return unless paths.key?(node_id)
17
+
18
+ chain = []
19
+ current = node_id
20
+ while current
21
+ chain.unshift(current)
22
+ current = paths[current]
23
+ end
24
+ chain
25
+ end
26
+ end
6
27
 
7
28
  class Engine
8
29
  def initialize(graph)
@@ -14,8 +35,8 @@ module Necropsy
14
35
  test_roots = graph.entry_points.select(&:test?).map(&:node_id)
15
36
 
16
37
  Result.new(
17
- runtime_alive: traverse(runtime_roots),
18
- test_alive: traverse(test_roots)
38
+ runtime_paths: traverse(runtime_roots),
39
+ test_paths: traverse(test_roots)
19
40
  )
20
41
  end
21
42
 
@@ -24,15 +45,18 @@ module Necropsy
24
45
  attr_reader :graph
25
46
 
26
47
  def traverse(roots)
27
- visited = Set.new
48
+ visited = {}
28
49
  queue = roots.compact.uniq
50
+ queue.each { |node_id| visited[node_id] = nil }
29
51
 
30
52
  until queue.empty?
31
53
  node_id = queue.shift
32
- next if visited.include?(node_id)
54
+ graph.edges_from(node_id).each_key do |callee_id|
55
+ next if visited.key?(callee_id)
33
56
 
34
- visited << node_id
35
- graph.edges_from(node_id).each_key { |callee_id| queue << callee_id unless visited.include?(callee_id) }
57
+ visited[callee_id] = node_id
58
+ queue << callee_id
59
+ end
36
60
  end
37
61
 
38
62
  visited
@@ -5,46 +5,76 @@ require 'yaml'
5
5
 
6
6
  module Necropsy
7
7
  class Report
8
- attr_reader :root, :graph, :findings
8
+ attr_reader :root, :graph, :findings, :reachability
9
9
 
10
- def initialize(root:, graph:, findings:)
10
+ def initialize(root:, graph:, findings:, reachability: nil, report_include_paths: [], report_exclude_paths: [])
11
11
  @root = root
12
12
  @graph = graph
13
- @findings = findings
13
+ @findings = findings.sort_by { |finding| [finding.node.file, finding.node.line, finding.node.id] }
14
+ @reachability = reachability
15
+ @report_include_paths = report_include_paths
16
+ @report_exclude_paths = report_exclude_paths
14
17
  end
15
18
 
16
19
  def dead_methods(min_confidence: :low)
17
- findings.select { |finding| finding.at_least?(min_confidence) }
20
+ reported_findings.select { |finding| finding.at_least?(min_confidence) }
18
21
  end
19
22
 
20
- def to_h
21
- {
23
+ def to_h(include_graph: false)
24
+ payload = {
22
25
  'root' => root,
23
26
  'summary' => summary,
24
- 'findings' => findings.map(&:to_h),
25
- 'graph' => graph.to_h
27
+ 'findings' => reported_findings.map(&:to_h)
26
28
  }
29
+ payload['graph'] = graph.to_h if include_graph
30
+ payload
27
31
  end
28
32
 
29
- def to_json(*)
30
- JSON.pretty_generate(to_h, *)
33
+ def to_json(state = nil, include_graph: false)
34
+ payload = to_h(include_graph: include_graph)
35
+ return JSON.pretty_generate(payload) unless state
36
+
37
+ payload.to_json(state)
31
38
  end
32
39
 
33
- def to_yaml
34
- to_h.to_yaml
40
+ def to_yaml(include_graph: false)
41
+ to_h(include_graph: include_graph).to_yaml
35
42
  end
36
43
 
37
44
  def summary
38
- grouped = findings.group_by(&:classification)
45
+ grouped = reported_findings.group_by(&:classification)
39
46
  {
40
47
  'nodes' => graph.nodes.length,
41
48
  'edges' => graph.edges.length,
42
49
  'entry_points' => graph.entry_points.length,
43
- 'findings' => findings.length,
50
+ 'findings' => reported_findings.length,
44
51
  'unreachable' => grouped.fetch(:unreachable, []).length,
45
52
  'unused' => grouped.fetch(:unused, []).length,
46
53
  'test_only_reachable' => grouped.fetch(:test_only_reachable, []).length
47
54
  }
48
55
  end
56
+
57
+ private
58
+
59
+ attr_reader :report_include_paths, :report_exclude_paths
60
+
61
+ def reported_findings
62
+ @reported_findings ||= findings.select do |finding|
63
+ included_in_report?(finding.node.file) && !excluded_from_report?(finding.node.file)
64
+ end
65
+ end
66
+
67
+ def included_in_report?(path)
68
+ report_include_paths.empty? || report_include_paths.any? { |pattern| path_matches?(pattern, path) }
69
+ end
70
+
71
+ def excluded_from_report?(path)
72
+ report_exclude_paths.any? { |pattern| path_matches?(pattern, path) }
73
+ end
74
+
75
+ def path_matches?(pattern, path)
76
+ File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB) ||
77
+ File.fnmatch?(File.join(pattern, '**', '*'), path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
78
+ end
49
79
  end
50
80
  end
@@ -2,21 +2,27 @@
2
2
 
3
3
  module Necropsy
4
4
  class Reporter
5
+ FORMATS = %i[human json yaml yml sarif github annotations].freeze
6
+ DEFAULT_MIN_CONFIDENCE = :medium
7
+
5
8
  def initialize(report)
6
9
  @report = report
7
10
  end
8
11
 
9
- def render(format: :human, min_confidence: :low)
10
- case format.to_sym
12
+ def render(format: :human, min_confidence: DEFAULT_MIN_CONFIDENCE, include_graph: false)
13
+ normalized_format = format.to_sym
14
+ raise Error, "Unknown report format: #{format}" unless FORMATS.include?(normalized_format)
15
+
16
+ case normalized_format
11
17
  when :json
12
- report.to_json
18
+ report.to_json(include_graph: include_graph)
13
19
  when :sarif
14
20
  render_sarif(min_confidence)
15
21
  when :github, :annotations
16
22
  render_github_annotations(min_confidence)
17
23
  when :yaml, :yml
18
- report.to_yaml
19
- else
24
+ report.to_yaml(include_graph: include_graph)
25
+ when :human
20
26
  render_human(min_confidence)
21
27
  end
22
28
  end
@@ -12,17 +12,28 @@ module Necropsy
12
12
 
13
13
  def analyze
14
14
  project = Project.new(root: root, config: config)
15
- graph = CallGraph.new(project.scan_result)
15
+ graph = CallGraph.new(project.scan_result, ambiguity_limit: config.ambiguity_limit)
16
+ rta_results = []
16
17
 
17
18
  apply_entry_points(graph, project)
18
19
  configured_analyzers.each do |analyzer|
19
20
  graph.add_profile(analyzer.profile)
20
- graph.apply_result(analyzer.analyze(graph, project))
21
+ result = analyzer.analyze(graph, project)
22
+ graph.apply_result(result)
23
+ rta_results << result if analyzer.profile.name == :rta
21
24
  end
25
+ rta_results.each { |result| graph.reconcile_rta_result(result) }
22
26
 
23
27
  reachability = Reachability::Engine.new(graph).call
24
28
  findings = Confidence::Scorer.new(graph: graph, reachability: reachability, project: project).findings
25
- Report.new(root: root, graph: graph, findings: findings)
29
+ Report.new(
30
+ root: root,
31
+ graph: graph,
32
+ findings: findings,
33
+ reachability: reachability,
34
+ report_include_paths: config.report_include_paths,
35
+ report_exclude_paths: config.report_exclude_paths
36
+ )
26
37
  end
27
38
 
28
39
  private
@@ -45,6 +56,8 @@ module Necropsy
45
56
  Analyzers::Static::CHA.new
46
57
  when 'rta'
47
58
  Analyzers::Static::RTA.new
59
+ else
60
+ raise Error, "Unknown static analyzer: #{name}"
48
61
  end
49
62
  end
50
63
 
@@ -60,13 +73,29 @@ module Necropsy
60
73
  end
61
74
 
62
75
  def custom_analyzers
63
- config.custom_analyzers.map do |class_name|
76
+ config.custom_analyzers.map do |entry|
77
+ class_name, required_path = custom_analyzer_definition(entry)
78
+ require_custom_analyzer(required_path) if required_path
64
79
  constantize(class_name).new
65
80
  rescue NameError => e
66
81
  raise Error, "Could not load custom analyzer #{class_name}: #{e.message}"
82
+ rescue LoadError => e
83
+ raise Error, "Could not require custom analyzer #{class_name}: #{e.message}"
67
84
  end
68
85
  end
69
86
 
87
+ def custom_analyzer_definition(entry)
88
+ return [entry, nil] unless entry.is_a?(Hash)
89
+
90
+ [entry.fetch('class'), entry['require']]
91
+ end
92
+
93
+ def require_custom_analyzer(path)
94
+ expanded = File.expand_path(path, root)
95
+ expanded = "#{expanded}.rb" if !File.file?(expanded) && File.file?("#{expanded}.rb")
96
+ File.file?(expanded) ? require(expanded) : require(path)
97
+ end
98
+
70
99
  def constantize(class_name)
71
100
  class_name.split('::').reduce(Object) { |constant, name| constant.const_get(name) }
72
101
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'necropsy'
4
+
5
+ root = ENV.fetch('NECROPSY_TRACE_ROOT', nil)
6
+ output = ENV.fetch('NECROPSY_TRACE_OUTPUT', nil)
7
+ sample_rate = ENV.fetch('NECROPSY_TRACE_SAMPLE_RATE', '1.0').to_f
8
+ merge = ENV['NECROPSY_TRACE_MERGE'] == '1'
9
+ run_id = ENV.fetch('NECROPSY_TRACE_RUN_ID', nil)
10
+
11
+ if root && output
12
+ Necropsy::Analyzers::Dynamic::TracePointCollector.install_at_exit(
13
+ root: root,
14
+ output: output,
15
+ sample_rate: sample_rate,
16
+ merge: merge,
17
+ run_id: run_id
18
+ )
19
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Necropsy
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/necropsy.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Set is built in on supported Rubies, but requiring it keeps direct file loading portable.
4
+ require 'set' # rubocop:disable Lint/RedundantRequireStatement
5
+
3
6
  require_relative 'necropsy/version'
4
7
  require_relative 'necropsy/models'
5
8
  require_relative 'necropsy/configuration'
@@ -23,6 +26,7 @@ require_relative 'necropsy/reachability/engine'
23
26
  require_relative 'necropsy/confidence/scorer'
24
27
  require_relative 'necropsy/report'
25
28
  require_relative 'necropsy/reporter'
29
+ require_relative 'necropsy/diagnostics'
26
30
  require_relative 'necropsy/guardrail/baseline'
27
31
  require_relative 'necropsy/guardrail/diff'
28
32
  require_relative 'necropsy/guardrail/quarantine'
data/script/measure.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../lib/necropsy'
4
+
5
+ root = File.expand_path(ARGV[0] || '.', Dir.pwd)
6
+ config_path = ARGV[1]
7
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
8
+ report = Necropsy.analyze(root: root, config_path: config_path)
9
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
10
+ node_count = report.graph.nodes.size
11
+
12
+ puts({
13
+ root: root,
14
+ nodes: node_count,
15
+ edges: report.graph.edges.size,
16
+ findings: report.findings.size,
17
+ ratio: node_count.zero? ? 0.0 : (100.0 * report.findings.size / node_count).round(1),
18
+ by_confidence: report.findings.group_by(&:confidence).transform_values(&:size),
19
+ seconds: elapsed.round(2)
20
+ }.inspect)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: necropsy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '1.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - ">="
24
27
  - !ruby/object:Gem::Version
25
28
  version: '1.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
26
32
  description: Necropsy combines static call-graph reachability, optional dynamic evidence,
27
33
  and CI guardrails for Ruby dead-code detection.
28
34
  email:
@@ -32,7 +38,10 @@ executables:
32
38
  extensions: []
33
39
  extra_rdoc_files: []
34
40
  files:
41
+ - ".rubocop.yml"
42
+ - CHANGELOG.md
35
43
  - LICENSE.txt
44
+ - MEASUREMENTS.md
36
45
  - README.md
37
46
  - Rakefile
38
47
  - exe/necropsy
@@ -47,12 +56,20 @@ files:
47
56
  - lib/necropsy/analyzers/static/name_resolution.rb
48
57
  - lib/necropsy/analyzers/static/rta.rb
49
58
  - lib/necropsy/ast_scanner.rb
59
+ - lib/necropsy/ast_scanner/call_recording.rb
60
+ - lib/necropsy/ast_scanner/dsl_macros.rb
61
+ - lib/necropsy/ast_scanner/method_definitions.rb
62
+ - lib/necropsy/ast_scanner/references.rb
63
+ - lib/necropsy/ast_scanner/ruby_semantics.rb
64
+ - lib/necropsy/ast_scanner/traversal.rb
65
+ - lib/necropsy/ast_scanner/value_definitions.rb
50
66
  - lib/necropsy/bench/evaluator.rb
51
67
  - lib/necropsy/cache/scan_cache.rb
52
68
  - lib/necropsy/cli.rb
53
69
  - lib/necropsy/confidence/scorer.rb
54
70
  - lib/necropsy/configuration.rb
55
71
  - lib/necropsy/coverage_runtime.rb
72
+ - lib/necropsy/diagnostics.rb
56
73
  - lib/necropsy/entry_points/plain.rb
57
74
  - lib/necropsy/entry_points/rails.rb
58
75
  - lib/necropsy/entry_points/test.rb
@@ -66,14 +83,17 @@ files:
66
83
  - lib/necropsy/report.rb
67
84
  - lib/necropsy/reporter.rb
68
85
  - lib/necropsy/runner.rb
86
+ - lib/necropsy/trace_point_runtime.rb
69
87
  - lib/necropsy/version.rb
88
+ - script/measure.rb
70
89
  homepage: https://github.com/ydah/necropsy
71
90
  licenses:
72
91
  - MIT
73
92
  metadata:
74
93
  allowed_push_host: https://rubygems.org
75
94
  homepage_uri: https://github.com/ydah/necropsy
76
- source_code_uri: https://github.com/ydah/necropsy.git
95
+ source_code_uri: https://github.com/ydah/necropsy/tree/main
96
+ changelog_uri: https://github.com/ydah/necropsy/blob/main/CHANGELOG.md
77
97
  rubygems_mfa_required: 'true'
78
98
  rdoc_options: []
79
99
  require_paths: