necropsy 0.1.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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +125 -0
  4. data/Rakefile +8 -0
  5. data/exe/necropsy +6 -0
  6. data/lib/necropsy/analyzer.rb +19 -0
  7. data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +196 -0
  8. data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +69 -0
  9. data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +376 -0
  10. data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +96 -0
  11. data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +18 -0
  12. data/lib/necropsy/analyzers/static/cha.rb +106 -0
  13. data/lib/necropsy/analyzers/static/name_resolution.rb +53 -0
  14. data/lib/necropsy/analyzers/static/rta.rb +79 -0
  15. data/lib/necropsy/ast_scanner.rb +616 -0
  16. data/lib/necropsy/bench/evaluator.rb +127 -0
  17. data/lib/necropsy/cache/scan_cache.rb +158 -0
  18. data/lib/necropsy/cli.rb +287 -0
  19. data/lib/necropsy/confidence/scorer.rb +158 -0
  20. data/lib/necropsy/configuration.rb +116 -0
  21. data/lib/necropsy/coverage_runtime.rb +13 -0
  22. data/lib/necropsy/entry_points/plain.rb +36 -0
  23. data/lib/necropsy/entry_points/rails.rb +335 -0
  24. data/lib/necropsy/entry_points/test.rb +13 -0
  25. data/lib/necropsy/graph/call_graph.rb +199 -0
  26. data/lib/necropsy/guardrail/baseline.rb +47 -0
  27. data/lib/necropsy/guardrail/diff.rb +16 -0
  28. data/lib/necropsy/guardrail/quarantine.rb +46 -0
  29. data/lib/necropsy/models.rb +164 -0
  30. data/lib/necropsy/project.rb +65 -0
  31. data/lib/necropsy/reachability/engine.rb +42 -0
  32. data/lib/necropsy/report.rb +50 -0
  33. data/lib/necropsy/reporter.rb +112 -0
  34. data/lib/necropsy/runner.rb +74 -0
  35. data/lib/necropsy/version.rb +5 -0
  36. data/lib/necropsy.rb +46 -0
  37. metadata +95 -0
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ class Reporter
5
+ def initialize(report)
6
+ @report = report
7
+ end
8
+
9
+ def render(format: :human, min_confidence: :low)
10
+ case format.to_sym
11
+ when :json
12
+ report.to_json
13
+ when :sarif
14
+ render_sarif(min_confidence)
15
+ when :github, :annotations
16
+ render_github_annotations(min_confidence)
17
+ when :yaml, :yml
18
+ report.to_yaml
19
+ else
20
+ render_human(min_confidence)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :report
27
+
28
+ def render_human(min_confidence)
29
+ findings = report.dead_methods(min_confidence: min_confidence)
30
+ lines = [
31
+ 'Necropsy report',
32
+ "Root: #{report.root}",
33
+ "Nodes: #{report.summary['nodes']}, Edges: #{report.summary['edges']}, Entry points: #{report.summary['entry_points']}",
34
+ "Findings: #{findings.length}"
35
+ ]
36
+
37
+ findings.group_by(&:classification).sort_by do |classification, _|
38
+ classification.to_s
39
+ end.each do |classification, group|
40
+ lines << ''
41
+ lines << "#{classification} (#{group.length})"
42
+ group.sort_by { |finding| [finding.node.file, finding.node.line, finding.node.id] }.each do |finding|
43
+ lines << " [#{finding.confidence}] #{finding.node.id} #{finding.node.file}:#{finding.node.line}"
44
+ end
45
+ end
46
+
47
+ lines.join("\n")
48
+ end
49
+
50
+ def render_github_annotations(min_confidence)
51
+ report.dead_methods(min_confidence: min_confidence).map do |finding|
52
+ message = "#{finding.classification} #{finding.node.id} confidence=#{finding.confidence}"
53
+ escaped = message.gsub('%', '%25').gsub("\n", '%0A').gsub("\r", '%0D')
54
+ "::warning file=#{finding.node.file},line=#{finding.node.line},title=Necropsy #{finding.confidence}::#{escaped}"
55
+ end.join("\n")
56
+ end
57
+
58
+ def render_sarif(min_confidence)
59
+ findings = report.dead_methods(min_confidence: min_confidence)
60
+ {
61
+ 'version' => '2.1.0',
62
+ '$schema' => 'https://json.schemastore.org/sarif-2.1.0.json',
63
+ 'runs' => [
64
+ {
65
+ 'tool' => {
66
+ 'driver' => {
67
+ 'name' => 'Necropsy',
68
+ 'informationUri' => 'https://github.com/ydah/necropsy',
69
+ 'rules' => sarif_rules(findings)
70
+ }
71
+ },
72
+ 'results' => findings.map { |finding| sarif_result(finding) }
73
+ }
74
+ ]
75
+ }.to_json
76
+ end
77
+
78
+ def sarif_rules(findings)
79
+ findings.map(&:classification).uniq.map do |classification|
80
+ {
81
+ 'id' => classification.to_s,
82
+ 'name' => classification.to_s,
83
+ 'shortDescription' => { 'text' => "Necropsy #{classification}" }
84
+ }
85
+ end
86
+ end
87
+
88
+ def sarif_result(finding)
89
+ {
90
+ 'ruleId' => finding.classification.to_s,
91
+ 'level' => sarif_level(finding),
92
+ 'message' => { 'text' => "#{finding.node.id} is #{finding.classification} (#{finding.confidence})" },
93
+ 'locations' => [
94
+ {
95
+ 'physicalLocation' => {
96
+ 'artifactLocation' => { 'uri' => finding.node.file },
97
+ 'region' => { 'startLine' => finding.node.line }
98
+ }
99
+ }
100
+ ],
101
+ 'partialFingerprints' => { 'necropsy' => finding.fingerprint }
102
+ }
103
+ end
104
+
105
+ def sarif_level(finding)
106
+ return 'error' if %i[certain high].include?(finding.confidence)
107
+ return 'warning' if finding.confidence == :medium
108
+
109
+ 'note'
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ class Runner
5
+ attr_reader :root, :config, :analyzers
6
+
7
+ def initialize(root:, config_path: nil, analyzers: nil)
8
+ @root = File.expand_path(root)
9
+ @config = Configuration.load(root: @root, path: config_path)
10
+ @analyzers = analyzers
11
+ end
12
+
13
+ def analyze
14
+ project = Project.new(root: root, config: config)
15
+ graph = CallGraph.new(project.scan_result)
16
+
17
+ apply_entry_points(graph, project)
18
+ configured_analyzers.each do |analyzer|
19
+ graph.add_profile(analyzer.profile)
20
+ graph.apply_result(analyzer.analyze(graph, project))
21
+ end
22
+
23
+ reachability = Reachability::Engine.new(graph).call
24
+ findings = Confidence::Scorer.new(graph: graph, reachability: reachability, project: project).findings
25
+ Report.new(root: root, graph: graph, findings: findings)
26
+ end
27
+
28
+ private
29
+
30
+ def apply_entry_points(graph, project)
31
+ EntryPoints::Plain.new.apply(graph, project)
32
+ EntryPoints::Rails.new.apply(graph, project)
33
+ EntryPoints::Test.new.apply(graph, project)
34
+ graph.entrypoint_hints.each { |entry| graph.add_entry_point(entry.node_id, entry.reason) }
35
+ end
36
+
37
+ def configured_analyzers
38
+ return analyzers if analyzers
39
+
40
+ static = config.static_analyzers.filter_map do |name|
41
+ case name
42
+ when 'name_resolution'
43
+ Analyzers::Static::NameResolution.new
44
+ when 'cha'
45
+ Analyzers::Static::CHA.new
46
+ when 'rta'
47
+ Analyzers::Static::RTA.new
48
+ end
49
+ end
50
+
51
+ dynamic = []
52
+ coverage_config = config.dynamic_config(:coverage)
53
+ dynamic << Analyzers::Dynamic::CoverageImporter.new(coverage_config) if coverage_config['source']
54
+ coverband_config = config.dynamic_config(:coverband)
55
+ dynamic << Analyzers::Dynamic::CoverbandImporter.new(coverband_config) if coverband_config['source']
56
+ trace_point_config = config.dynamic_config(:trace_point)
57
+ dynamic << Analyzers::Dynamic::TracePointImporter.new(trace_point_config) if trace_point_config['source']
58
+
59
+ static + dynamic + custom_analyzers
60
+ end
61
+
62
+ def custom_analyzers
63
+ config.custom_analyzers.map do |class_name|
64
+ constantize(class_name).new
65
+ rescue NameError => e
66
+ raise Error, "Could not load custom analyzer #{class_name}: #{e.message}"
67
+ end
68
+ end
69
+
70
+ def constantize(class_name)
71
+ class_name.split('::').reduce(Object) { |constant, name| constant.const_get(name) }
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ VERSION = '0.1.0'
5
+ end
data/lib/necropsy.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'necropsy/version'
4
+ require_relative 'necropsy/models'
5
+ require_relative 'necropsy/configuration'
6
+ require_relative 'necropsy/ast_scanner'
7
+ require_relative 'necropsy/cache/scan_cache'
8
+ require_relative 'necropsy/project'
9
+ require_relative 'necropsy/graph/call_graph'
10
+ require_relative 'necropsy/analyzer'
11
+ require_relative 'necropsy/analyzers/static/name_resolution'
12
+ require_relative 'necropsy/analyzers/static/cha'
13
+ require_relative 'necropsy/analyzers/static/rta'
14
+ require_relative 'necropsy/analyzers/dynamic/coverage_importer'
15
+ require_relative 'necropsy/analyzers/dynamic/coverage_collector'
16
+ require_relative 'necropsy/analyzers/dynamic/coverband_importer'
17
+ require_relative 'necropsy/analyzers/dynamic/trace_point_importer'
18
+ require_relative 'necropsy/analyzers/dynamic/trace_point_collector'
19
+ require_relative 'necropsy/entry_points/plain'
20
+ require_relative 'necropsy/entry_points/rails'
21
+ require_relative 'necropsy/entry_points/test'
22
+ require_relative 'necropsy/reachability/engine'
23
+ require_relative 'necropsy/confidence/scorer'
24
+ require_relative 'necropsy/report'
25
+ require_relative 'necropsy/reporter'
26
+ require_relative 'necropsy/guardrail/baseline'
27
+ require_relative 'necropsy/guardrail/diff'
28
+ require_relative 'necropsy/guardrail/quarantine'
29
+ require_relative 'necropsy/bench/evaluator'
30
+ require_relative 'necropsy/runner'
31
+
32
+ module Necropsy
33
+ class Error < StandardError; end
34
+
35
+ def self.analyze(root: '.', config_path: nil, analyzers: nil)
36
+ Runner.new(root: root, config_path: config_path, analyzers: analyzers).analyze
37
+ end
38
+
39
+ def self.default_analyzers
40
+ [
41
+ Analyzers::Static::NameResolution.new,
42
+ Analyzers::Static::CHA.new,
43
+ Analyzers::Static::RTA.new
44
+ ]
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: necropsy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: prism
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ description: Necropsy combines static call-graph reachability, optional dynamic evidence,
27
+ and CI guardrails for Ruby dead-code detection.
28
+ email:
29
+ - t.yudai92@gmail.com
30
+ executables:
31
+ - necropsy
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE.txt
36
+ - README.md
37
+ - Rakefile
38
+ - exe/necropsy
39
+ - lib/necropsy.rb
40
+ - lib/necropsy/analyzer.rb
41
+ - lib/necropsy/analyzers/dynamic/coverage_collector.rb
42
+ - lib/necropsy/analyzers/dynamic/coverage_importer.rb
43
+ - lib/necropsy/analyzers/dynamic/coverband_importer.rb
44
+ - lib/necropsy/analyzers/dynamic/trace_point_collector.rb
45
+ - lib/necropsy/analyzers/dynamic/trace_point_importer.rb
46
+ - lib/necropsy/analyzers/static/cha.rb
47
+ - lib/necropsy/analyzers/static/name_resolution.rb
48
+ - lib/necropsy/analyzers/static/rta.rb
49
+ - lib/necropsy/ast_scanner.rb
50
+ - lib/necropsy/bench/evaluator.rb
51
+ - lib/necropsy/cache/scan_cache.rb
52
+ - lib/necropsy/cli.rb
53
+ - lib/necropsy/confidence/scorer.rb
54
+ - lib/necropsy/configuration.rb
55
+ - lib/necropsy/coverage_runtime.rb
56
+ - lib/necropsy/entry_points/plain.rb
57
+ - lib/necropsy/entry_points/rails.rb
58
+ - lib/necropsy/entry_points/test.rb
59
+ - lib/necropsy/graph/call_graph.rb
60
+ - lib/necropsy/guardrail/baseline.rb
61
+ - lib/necropsy/guardrail/diff.rb
62
+ - lib/necropsy/guardrail/quarantine.rb
63
+ - lib/necropsy/models.rb
64
+ - lib/necropsy/project.rb
65
+ - lib/necropsy/reachability/engine.rb
66
+ - lib/necropsy/report.rb
67
+ - lib/necropsy/reporter.rb
68
+ - lib/necropsy/runner.rb
69
+ - lib/necropsy/version.rb
70
+ homepage: https://github.com/ydah/necropsy
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ allowed_push_host: https://rubygems.org
75
+ homepage_uri: https://github.com/ydah/necropsy
76
+ source_code_uri: https://github.com/ydah/necropsy.git
77
+ rubygems_mfa_required: 'true'
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 3.2.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 4.0.6
93
+ specification_version: 4
94
+ summary: Hybrid dead-code detection for Ruby projects.
95
+ test_files: []