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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +8 -0
- data/exe/necropsy +6 -0
- data/lib/necropsy/analyzer.rb +19 -0
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +196 -0
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +69 -0
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +376 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +18 -0
- data/lib/necropsy/analyzers/static/cha.rb +106 -0
- data/lib/necropsy/analyzers/static/name_resolution.rb +53 -0
- data/lib/necropsy/analyzers/static/rta.rb +79 -0
- data/lib/necropsy/ast_scanner.rb +616 -0
- data/lib/necropsy/bench/evaluator.rb +127 -0
- data/lib/necropsy/cache/scan_cache.rb +158 -0
- data/lib/necropsy/cli.rb +287 -0
- data/lib/necropsy/confidence/scorer.rb +158 -0
- data/lib/necropsy/configuration.rb +116 -0
- data/lib/necropsy/coverage_runtime.rb +13 -0
- data/lib/necropsy/entry_points/plain.rb +36 -0
- data/lib/necropsy/entry_points/rails.rb +335 -0
- data/lib/necropsy/entry_points/test.rb +13 -0
- data/lib/necropsy/graph/call_graph.rb +199 -0
- data/lib/necropsy/guardrail/baseline.rb +47 -0
- data/lib/necropsy/guardrail/diff.rb +16 -0
- data/lib/necropsy/guardrail/quarantine.rb +46 -0
- data/lib/necropsy/models.rb +164 -0
- data/lib/necropsy/project.rb +65 -0
- data/lib/necropsy/reachability/engine.rb +42 -0
- data/lib/necropsy/report.rb +50 -0
- data/lib/necropsy/reporter.rb +112 -0
- data/lib/necropsy/runner.rb +74 -0
- data/lib/necropsy/version.rb +5 -0
- data/lib/necropsy.rb +46 -0
- metadata +95 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: acc21c7bf0937bb810decd0a6df03911149a32fc08146b15a37e74d133b397eb
|
|
4
|
+
data.tar.gz: 781f39ee1fb14299dbd9bedb6cad792785269517494b1d6c336fdb52674c10c2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cef485cc0bb21d6e684107bb08f7882655ddf24600c9f9d53c744b5ce8a4cf78a460540c6bc9d981e652c6679f12641d34950761ceb1f78d17e8708436afc4d8
|
|
7
|
+
data.tar.gz: 787d658f3eb89506530b4467d3b7071ed9eccab5a9288b4e023e6b9dbcdd97f6a5b5a7127927d060f698deca80510d0ccd81e059a33990bb0531238d85c95ed4
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Necropsy
|
|
2
|
+
|
|
3
|
+
Necropsy is a Ruby dead-code detector built around a unified call graph. It
|
|
4
|
+
collects method definitions with Prism, adds call-edge evidence from static and
|
|
5
|
+
optional dynamic analyzers, then runs reachability from framework and configured
|
|
6
|
+
entry points.
|
|
7
|
+
|
|
8
|
+
The first implementation includes:
|
|
9
|
+
|
|
10
|
+
- Prism-based method collection for `def`, `define_method`, `attr_*`, and `delegate`
|
|
11
|
+
- static name resolution, CHA, and RTA-style filtering over instantiated classes
|
|
12
|
+
- Rails route/callback/view-helper/component, plain Ruby, and test-suite entry point providers
|
|
13
|
+
- `unreachable`, `unused`, and `test_only_reachable` classifications
|
|
14
|
+
- confidence levels, JSON/YAML/human/SARIF/GitHub reports, baseline/check guardrails, quarantine suggestions, TracePoint recording, and a bench evaluator
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add the gem to the development/test group:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "necropsy", require: false
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Create a baseline:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bundle exec necropsy baseline --root .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run a report:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bundle exec necropsy analyze --root . --format human
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Fail CI only for new high-confidence findings:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bundle exec necropsy check --root . --fail-on high
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Record dynamic evidence from a Ruby script:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bundle exec necropsy record --root . --output tmp/necropsy_trace_point.yml -- script/runner.rb
|
|
48
|
+
bundle exec necropsy coverage --root . --output tmp/necropsy_coverage.yml -- script/runner.rb
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Record Ruby Coverage while running an external command:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bundle exec necropsy coverage --root . --output tmp/necropsy_coverage.yml -- bundle exec rspec
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Child Ruby processes inherit the collector and are merged into the same output
|
|
58
|
+
for that run.
|
|
59
|
+
|
|
60
|
+
Evaluate against a gold standard:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bundle exec necropsy bench --root . --gold-standard gold.yml --ablation
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Example configuration:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
analyzers:
|
|
70
|
+
static: [name_resolution, cha, rta]
|
|
71
|
+
dynamic:
|
|
72
|
+
coverage:
|
|
73
|
+
source: tmp/necropsy_coverage.yml
|
|
74
|
+
min_observation_days: 30
|
|
75
|
+
coverband:
|
|
76
|
+
source: redis://prod-redis:6379/2?key=coverband
|
|
77
|
+
trace_point:
|
|
78
|
+
source: tmp/necropsy_trace_point.yml
|
|
79
|
+
custom:
|
|
80
|
+
- "MyCompany::GraphqlEntryAnalyzer"
|
|
81
|
+
cache:
|
|
82
|
+
enabled: true
|
|
83
|
+
path: .necropsy_cache/scan.yml
|
|
84
|
+
entry_points:
|
|
85
|
+
extra:
|
|
86
|
+
- "PublicApi::*"
|
|
87
|
+
ci:
|
|
88
|
+
fail_on: high
|
|
89
|
+
baseline: .necropsy_baseline.yml
|
|
90
|
+
quarantine:
|
|
91
|
+
days: 30
|
|
92
|
+
bench:
|
|
93
|
+
precision_threshold: 0.85
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The scan cache is invalidated when scanned Ruby files or configuration values
|
|
97
|
+
change.
|
|
98
|
+
|
|
99
|
+
Dynamic inputs may provide `executed` or `nodes` entries with method IDs,
|
|
100
|
+
`edges` with `caller_id`/`callee_id`, and an `observation` hash. SARIF and
|
|
101
|
+
GitHub Actions annotations are available via `--format sarif` and
|
|
102
|
+
`--format github`.
|
|
103
|
+
|
|
104
|
+
Coverband file, Redis string, and Redis hash exports are supported. Rails route
|
|
105
|
+
entry point detection covers common `resources`, `resource`, `namespace`,
|
|
106
|
+
`scope`, `controller`, `concerns`, `draw`, `mount`, `root`, and verb route
|
|
107
|
+
forms.
|
|
108
|
+
|
|
109
|
+
`necropsy quarantine --write` adds `# necropsy:quarantine since=YYYY-MM-DD`.
|
|
110
|
+
When the configured quarantine window expires and no alive evidence appears,
|
|
111
|
+
the finding is raised to `certain`.
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
116
|
+
|
|
117
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
118
|
+
|
|
119
|
+
## Contributing
|
|
120
|
+
|
|
121
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/exe/necropsy
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Necropsy
|
|
4
|
+
class Analyzer
|
|
5
|
+
def analyze(_graph, _project)
|
|
6
|
+
raise NotImplementedError, "#{self.class} must implement #analyze"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def profile
|
|
10
|
+
raise NotImplementedError, "#{self.class} must implement #profile"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def evidence(kind:, details:, analyzer: profile.name, weight: 1.0, metadata: {})
|
|
16
|
+
Evidence.new(analyzer: analyzer, kind: kind, weight: weight, details: details, metadata: metadata)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'coverage'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'yaml'
|
|
7
|
+
|
|
8
|
+
module Necropsy
|
|
9
|
+
module Analyzers
|
|
10
|
+
module Dynamic
|
|
11
|
+
class CoverageCollector
|
|
12
|
+
def self.record(root:, output:, &)
|
|
13
|
+
new(root: root, output: output).record(&)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.install_at_exit(root:, output:, merge: false, run_id: nil)
|
|
17
|
+
new(root: root, output: output, merge: merge, run_id: run_id).install_at_exit
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(root:, output:, merge: false, run_id: nil)
|
|
21
|
+
@root = File.expand_path(root)
|
|
22
|
+
@output = output
|
|
23
|
+
@merge = merge
|
|
24
|
+
@run_id = run_id
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def record
|
|
28
|
+
started_at = Time.now.utc
|
|
29
|
+
Coverage.start(methods: true)
|
|
30
|
+
yield
|
|
31
|
+
write_payload(result: Coverage.result, started_at: started_at, finished_at: Time.now.utc)
|
|
32
|
+
ensure
|
|
33
|
+
Coverage.result(stop: true, clear: true) if Coverage.running?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def install_at_exit
|
|
37
|
+
started_at = Time.now.utc
|
|
38
|
+
started = start_coverage
|
|
39
|
+
|
|
40
|
+
at_exit do
|
|
41
|
+
finished_at = Time.now.utc
|
|
42
|
+
result = coverage_result(started: started)
|
|
43
|
+
write_payload(result: result, started_at: started_at, finished_at: finished_at)
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
warn "Necropsy coverage collector failed: #{e.message}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :root, :output, :run_id
|
|
52
|
+
|
|
53
|
+
def merge?
|
|
54
|
+
@merge
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def start_coverage
|
|
58
|
+
return false if Coverage.running?
|
|
59
|
+
|
|
60
|
+
Coverage.start(methods: true)
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def coverage_result(started:)
|
|
65
|
+
return Coverage.result(stop: true, clear: true) if started && Coverage.running?
|
|
66
|
+
return Coverage.peek_result if Coverage.respond_to?(:peek_result) && Coverage.running?
|
|
67
|
+
|
|
68
|
+
{}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_payload(result:, started_at:, finished_at:)
|
|
72
|
+
payload = {
|
|
73
|
+
'nodes' => executed_nodes(result).sort,
|
|
74
|
+
'observation' => {
|
|
75
|
+
'started_at' => started_at.iso8601,
|
|
76
|
+
'finished_at' => finished_at.iso8601,
|
|
77
|
+
'days' => [((finished_at - started_at) / 86_400.0).ceil, 1].max,
|
|
78
|
+
'collector' => 'coverage'
|
|
79
|
+
}.tap { |observation| observation['run_id'] = run_id if run_id }
|
|
80
|
+
}
|
|
81
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
82
|
+
merge? ? write_merged_payload(payload) : File.write(output, payload.to_yaml)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def write_merged_payload(payload)
|
|
86
|
+
File.open(output, File::RDWR | File::CREAT, 0o644) do |file|
|
|
87
|
+
file.flock(File::LOCK_EX)
|
|
88
|
+
existing = payload_for_current_run(read_payload(file), payload)
|
|
89
|
+
merged = merge_payload(existing, payload)
|
|
90
|
+
file.rewind
|
|
91
|
+
file.truncate(0)
|
|
92
|
+
file.write(merged.to_yaml)
|
|
93
|
+
file.flush
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def read_payload(file)
|
|
98
|
+
file.rewind
|
|
99
|
+
content = file.read
|
|
100
|
+
return {} if content.empty?
|
|
101
|
+
|
|
102
|
+
YAML.load(content) || {}
|
|
103
|
+
rescue Psych::Exception
|
|
104
|
+
{}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def payload_for_current_run(existing, payload)
|
|
108
|
+
current_run = payload.dig('observation', 'run_id')
|
|
109
|
+
return existing unless current_run
|
|
110
|
+
return existing if empty_payload?(existing)
|
|
111
|
+
return existing if existing.dig('observation', 'run_id') == current_run
|
|
112
|
+
|
|
113
|
+
{}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def empty_payload?(payload)
|
|
117
|
+
Array(payload['nodes']).empty? && payload.fetch('observation', {}).empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def merge_payload(left, right)
|
|
121
|
+
observation = merge_observation(left.fetch('observation', {}), right.fetch('observation', {}))
|
|
122
|
+
{
|
|
123
|
+
'nodes' => (Array(left['nodes']) + Array(right['nodes'])).uniq.sort,
|
|
124
|
+
'observation' => observation
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def merge_observation(left, right)
|
|
129
|
+
started_at = [left['started_at'], right['started_at']].compact.min
|
|
130
|
+
finished_at = [left['finished_at'], right['finished_at']].compact.max
|
|
131
|
+
observation = left.merge(right)
|
|
132
|
+
observation['started_at'] = started_at if started_at
|
|
133
|
+
observation['finished_at'] = finished_at if finished_at
|
|
134
|
+
observation['days'] = merged_days(left, right, started_at, finished_at)
|
|
135
|
+
observation['collector'] = 'coverage'
|
|
136
|
+
observation['processes'] = process_count(left) + process_count(right)
|
|
137
|
+
observation
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def merged_days(left, right, started_at, finished_at)
|
|
141
|
+
observed_days = [left['days'].to_i, right['days'].to_i, days_between(started_at, finished_at)].max
|
|
142
|
+
[observed_days, 1].max
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def days_between(started_at, finished_at)
|
|
146
|
+
return 0 unless started_at && finished_at
|
|
147
|
+
|
|
148
|
+
[((Time.parse(finished_at) - Time.parse(started_at)) / 86_400.0).ceil, 1].max
|
|
149
|
+
rescue ArgumentError
|
|
150
|
+
0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def process_count(observation)
|
|
154
|
+
return observation['processes'].to_i if observation['processes']
|
|
155
|
+
return 0 if observation.empty?
|
|
156
|
+
|
|
157
|
+
1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def executed_nodes(result)
|
|
161
|
+
result.flat_map do |path, coverage|
|
|
162
|
+
next [] unless project_path?(path)
|
|
163
|
+
|
|
164
|
+
coverage.fetch(:methods, {}).filter_map do |method_key, count|
|
|
165
|
+
next unless count.to_i.positive?
|
|
166
|
+
|
|
167
|
+
node_id_for(method_key)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def project_path?(path)
|
|
173
|
+
File.expand_path(path).start_with?("#{root}/")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def node_id_for(method_key)
|
|
177
|
+
owner = method_key[0]
|
|
178
|
+
method_name = method_key[1]
|
|
179
|
+
owner_name, separator = owner_and_separator(owner)
|
|
180
|
+
return nil unless owner_name
|
|
181
|
+
|
|
182
|
+
"#{owner_name}#{separator}#{method_name}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def owner_and_separator(owner)
|
|
186
|
+
return [owner.name, '#'] if owner.respond_to?(:name) && owner.name
|
|
187
|
+
|
|
188
|
+
singleton_owner = owner.inspect[/\A#<Class:(.+)>\z/, 1]
|
|
189
|
+
return [singleton_owner, '.'] if singleton_owner
|
|
190
|
+
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module Necropsy
|
|
7
|
+
module Analyzers
|
|
8
|
+
module Dynamic
|
|
9
|
+
class CoverageImporter < Analyzer
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config || {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def analyze(_graph, project)
|
|
15
|
+
source = config['source']
|
|
16
|
+
return AnalyzerResult.empty unless source
|
|
17
|
+
|
|
18
|
+
payload = load_payload(File.expand_path(source, project.root))
|
|
19
|
+
alive = Array(payload['executed'] || payload['nodes']).map do |node_id|
|
|
20
|
+
AliveEvidence.new(
|
|
21
|
+
node_id: node_id,
|
|
22
|
+
evidence: evidence(kind: :alive, details: "Coverage marked #{node_id} as executed",
|
|
23
|
+
metadata: payload['observation'] || {})
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
edge_evidences = Array(payload['edges']).map do |edge|
|
|
28
|
+
caller_id = edge['caller_id'] || edge[:caller_id]
|
|
29
|
+
callee_id = edge['callee_id'] || edge[:callee_id]
|
|
30
|
+
EdgeEvidence.new(
|
|
31
|
+
caller_id: caller_id,
|
|
32
|
+
callee_id: callee_id,
|
|
33
|
+
evidence: evidence(kind: :call_edge, details: "Coverage observed #{caller_id} -> #{callee_id}")
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
AnalyzerResult.new(
|
|
38
|
+
edge_evidences: edge_evidences,
|
|
39
|
+
alive_evidences: alive,
|
|
40
|
+
uncertainties: {},
|
|
41
|
+
observation: { 'coverage' => payload['observation'] || {} }
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def profile
|
|
46
|
+
AnalyzerProfile.new(
|
|
47
|
+
name: :coverage,
|
|
48
|
+
kind: :dynamic,
|
|
49
|
+
soundness: :observational,
|
|
50
|
+
description: 'Imports method execution and observed edges from Ruby Coverage output.'
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
attr_reader :config
|
|
57
|
+
|
|
58
|
+
def load_payload(path)
|
|
59
|
+
case File.extname(path)
|
|
60
|
+
when '.json'
|
|
61
|
+
JSON.parse(File.read(path))
|
|
62
|
+
else
|
|
63
|
+
YAML.load_file(path) || {}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|