Class: Necropsy::Report

Inherits:
Object
  • Object
show all
Defined in:
lib/necropsy/report.rb

Constant Summary collapse

SCHEMA_VERSION =
2
SCHEMA_PATH =
File.expand_path("../../schema/necropsy-report-v#{SCHEMA_VERSION}.schema.json", __dir__).freeze
ACTIONABLE_CLASSIFICATIONS =
%i[unreachable unused].freeze
FINGERPRINT_COMPATIBILITY =
{
  'fingerprint' => 'legacy logical symbol fingerprint retained for compatibility',
  'physical_fingerprint' => 'physical definition fingerprint for baselines and definition-level matching'
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:, graph:, findings:, reachability: nil, report_include_paths: [], report_exclude_paths: [], project: nil, source_snapshot: nil, performance_profile: nil, analysis_health: nil) ⇒ Report

Returns a new instance of Report.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/necropsy/report.rb', line 24

def initialize(root:, graph:, findings:, reachability: nil, report_include_paths: [], report_exclude_paths: [],
               project: nil, source_snapshot: nil, performance_profile: nil, analysis_health: nil)
  @root = root
  @graph = graph
  @findings = findings.sort_by do |finding|
    [finding.node.file, finding.node.line, finding.node.id, finding.node.definition_id]
  end
  @reachability = reachability
  @project = project
  @source_snapshot = source_snapshot
  @performance_profile = performance_profile
  @analysis_health = analysis_health || AnalysisHealth.complete
  @report_include_paths = report_include_paths
  @report_exclude_paths = report_exclude_paths
end

Instance Attribute Details

#analysis_healthObject (readonly)

Returns the value of attribute analysis_health.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def analysis_health
  @analysis_health
end

#findingsObject (readonly)

Returns the value of attribute findings.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def findings
  @findings
end

#graphObject (readonly)

Returns the value of attribute graph.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def graph
  @graph
end

#performance_profileObject (readonly)

Returns the value of attribute performance_profile.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def performance_profile
  @performance_profile
end

#projectObject (readonly)

Returns the value of attribute project.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def project
  @project
end

#reachabilityObject (readonly)

Returns the value of attribute reachability.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def reachability
  @reachability
end

#rootObject (readonly)

Returns the value of attribute root.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def root
  @root
end

#source_snapshotObject (readonly)

Returns the value of attribute source_snapshot.



17
18
19
# File 'lib/necropsy/report.rb', line 17

def source_snapshot
  @source_snapshot
end

Class Method Details

.schema_pathObject



20
21
22
# File 'lib/necropsy/report.rb', line 20

def self.schema_path
  SCHEMA_PATH
end

Instance Method Details

#actionable_candidates(min_confidence: :low) ⇒ Object

Unlike the legacy dead_methods API, this excludes findings that exist to explain uncertainty or test-only reachability. Benchmarks and precision gates must measure only definitions that a user can actually review as a removal candidate.



48
49
50
51
52
# File 'lib/necropsy/report.rb', line 48

def actionable_candidates(min_confidence: :low)
  reported_findings.select do |finding|
    ACTIONABLE_CLASSIFICATIONS.include?(finding.classification) && finding.at_least?(min_confidence)
  end
end

#blocked_methodsObject



71
72
73
# File 'lib/necropsy/report.rb', line 71

def blocked_methods
  reported_findings.select { |finding| finding.classification == :blocked }
end

#dead_methods(min_confidence: :low) ⇒ Object



40
41
42
# File 'lib/necropsy/report.rb', line 40

def dead_methods(min_confidence: :low)
  reported_findings.select { |finding| finding.at_least?(min_confidence) }
end

#diagnostic_findingsObject



54
55
56
# File 'lib/necropsy/report.rb', line 54

def diagnostic_findings
  reported_findings.reject { |finding| ACTIONABLE_CLASSIFICATIONS.include?(finding.classification) }
end

#diagnosticsObject



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/necropsy/report.rb', line 122

def diagnostics
  result = {}
  dynamic = graph.dynamic_evidence_diagnostic
  result['dynamic_evidence'] = dynamic if dynamic
  definition_resolution = graph.observation['definition_resolution']
  result['definition_resolution'] = definition_resolution if definition_resolution
  reference_barrier = graph.observation['non_ruby_reference_barrier']
  result['non_ruby_reference_barrier'] = reference_barrier if reference_barrier
  result['source_incompleteness'] = graph.source_incompleteness if graph.incomplete_files.any?
  result['analysis_scope'] = graph.scope_diagnostics unless graph.scope_diagnostics.empty?
  unrooted = graph.observation['unrooted_load_units']
  result['unrooted_load_units'] = unrooted if unrooted && unrooted['count'].positive?
  generated_macros = graph.observation['rails_generated_macros']
  result['rails_generated_macros'] = generated_macros if generated_macros
  result['performance'] = performance_profile if performance_profile
  result
end

#finding_for_definition(definition_id) ⇒ Object



62
63
64
65
# File 'lib/necropsy/report.rb', line 62

def finding_for_definition(definition_id)
  @findings_by_definition ||= findings.to_h { |finding| [finding.node.graph_id, finding] }.freeze
  @findings_by_definition[definition_id.to_s]
end

#report_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


67
68
69
# File 'lib/necropsy/report.rb', line 67

def report_path?(path)
  included_in_report?(path) && !excluded_from_report?(path)
end

#reportable_findingsObject



58
59
60
# File 'lib/necropsy/report.rb', line 58

def reportable_findings
  reported_findings.dup
end

#summaryObject



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/necropsy/report.rb', line 102

def summary
  grouped = reported_findings.group_by(&:classification)
  actionable = reported_findings.count { |finding| ACTIONABLE_CLASSIFICATIONS.include?(finding.classification) }
  blocked = grouped.fetch(:blocked, []).length
  {
    'nodes' => graph.nodes.length,
    'edges' => graph.edges.length,
    'entry_points' => graph.entry_points.length,
    'incomplete_files' => graph.incomplete_files.length,
    'findings' => reported_findings.length,
    'actionable' => actionable,
    'diagnostic' => reported_findings.length - actionable - blocked,
    'health_failures' => analysis_health.reasons.length,
    'unreachable' => grouped.fetch(:unreachable, []).length,
    'unused' => grouped.fetch(:unused, []).length,
    'blocked' => blocked,
    'test_only_reachable' => grouped.fetch(:test_only_reachable, []).length
  }
end

#to_h(include_graph: false) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/necropsy/report.rb', line 75

def to_h(include_graph: false)
  payload = {
    'schema_version' => SCHEMA_VERSION,
    'artifact_provenance' => artifact_provenance,
    'compatibility' => { 'finding_fingerprints' => FINGERPRINT_COMPATIBILITY },
    'root' => root,
    'analysis_health' => analysis_health.to_h,
    'summary' => summary,
    'findings' => reported_findings.map(&:to_h)
  }
  payload['diagnostics'] = diagnostics unless diagnostics.empty?
  payload['source_snapshot'] = source_snapshot if source_snapshot
  payload['graph'] = graph.to_h if include_graph
  payload
end

#to_json(state = nil, include_graph: false) ⇒ Object



91
92
93
94
95
96
# File 'lib/necropsy/report.rb', line 91

def to_json(state = nil, include_graph: false)
  payload = to_h(include_graph: include_graph)
  return JSON.pretty_generate(payload) unless state

  payload.to_json(state)
end

#to_yaml(include_graph: false) ⇒ Object



98
99
100
# File 'lib/necropsy/report.rb', line 98

def to_yaml(include_graph: false)
  to_h(include_graph: include_graph).to_yaml
end