rspec-covers 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 +202 -0
- data/data/evaluation/ground_truth.example.json +26 -0
- data/lib/minitest/covers.rb +133 -0
- data/lib/rspec/covers/checked_coverage.rb +27 -0
- data/lib/rspec/covers/code_location.rb +49 -0
- data/lib/rspec/covers/configuration.rb +135 -0
- data/lib/rspec/covers/declaration.rb +199 -0
- data/lib/rspec/covers/declaration_validation.rb +65 -0
- data/lib/rspec/covers/evaluation.rb +197 -0
- data/lib/rspec/covers/formatter.rb +70 -0
- data/lib/rspec/covers/integration.rb +54 -0
- data/lib/rspec/covers/metadata_reader.rb +34 -0
- data/lib/rspec/covers/method_entry.rb +19 -0
- data/lib/rspec/covers/method_label.rb +37 -0
- data/lib/rspec/covers/probe/call_log_probe.rb +132 -0
- data/lib/rspec/covers/probe/coverage_probe.rb +57 -0
- data/lib/rspec/covers/production_inventory.rb +90 -0
- data/lib/rspec/covers/rake_task.rb +63 -0
- data/lib/rspec/covers/reporter.rb +162 -0
- data/lib/rspec/covers/source_range.rb +37 -0
- data/lib/rspec/covers/static_method_scanner.rb +184 -0
- data/lib/rspec/covers/strict_verdict.rb +107 -0
- data/lib/rspec/covers/tracer/composite.rb +73 -0
- data/lib/rspec/covers/tracer/dynamic.rb +27 -0
- data/lib/rspec/covers/tracer/dynamic_corpus.rb +43 -0
- data/lib/rspec/covers/tracer/lcba.rb +17 -0
- data/lib/rspec/covers/tracer/nc.rb +36 -0
- data/lib/rspec/covers/tracer/ncc.rb +23 -0
- data/lib/rspec/covers/tracer/suggestion.rb +17 -0
- data/lib/rspec/covers/tracer/tokenizer.rb +18 -0
- data/lib/rspec/covers/version.rb +11 -0
- data/lib/rspec/covers.rb +225 -0
- data/sig/minitest/covers.rbs +6 -0
- data/sig/rspec/covers.rbs +27 -0
- metadata +134 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
require_relative "../method_label"
|
|
6
|
+
|
|
7
|
+
module RSpec
|
|
8
|
+
module Covers
|
|
9
|
+
module Probe
|
|
10
|
+
Call = Struct.new(:label, :file, :line, :event, keyword_init: true) do
|
|
11
|
+
def to_h
|
|
12
|
+
{ label: label, file: file, line: line, event: event }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Expectation = Struct.new(:actual_object_id, :call_index, keyword_init: true) do
|
|
17
|
+
def to_h
|
|
18
|
+
{ actual_object_id: actual_object_id, call_index: call_index }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class CallLog
|
|
23
|
+
attr_reader :calls, :returns_by_object_id, :expectations
|
|
24
|
+
|
|
25
|
+
def initialize(calls:, returns_by_object_id:, expectations: [])
|
|
26
|
+
@calls = calls
|
|
27
|
+
@returns_by_object_id = returns_by_object_id
|
|
28
|
+
@expectations = expectations
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def last_call
|
|
32
|
+
calls.last
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def calls_before_expectations
|
|
36
|
+
expectations.filter_map do |expectation|
|
|
37
|
+
next if expectation.call_index.to_i <= 0
|
|
38
|
+
|
|
39
|
+
calls[expectation.call_index - 1]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def labels_before_expectations
|
|
44
|
+
calls_before_expectations.map(&:label).uniq
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def labels
|
|
48
|
+
calls.map(&:label)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def expectation_object_ids
|
|
52
|
+
expectations.map(&:actual_object_id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def return_labels_for(object_ids)
|
|
56
|
+
object_ids.flat_map { |object_id| returns_by_object_id.fetch(object_id, []) }.uniq
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class CallLogProbe
|
|
61
|
+
def initialize(config)
|
|
62
|
+
@config = config
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def start_example
|
|
66
|
+
@calls = []
|
|
67
|
+
@call_stack = []
|
|
68
|
+
@expectations = []
|
|
69
|
+
@returns_by_object_id = Hash.new { |hash, key| hash[key] = Set.new }
|
|
70
|
+
@trace_point = TracePoint.new(:call, :return) { |trace_point| record(trace_point) }
|
|
71
|
+
@trace_point.enable
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def active?
|
|
75
|
+
!@trace_point.nil?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def record_expectation(actual)
|
|
79
|
+
return unless active?
|
|
80
|
+
|
|
81
|
+
@expectations << Expectation.new(
|
|
82
|
+
actual_object_id: actual.__id__,
|
|
83
|
+
call_index: @calls.length
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stop_example
|
|
88
|
+
@trace_point&.disable
|
|
89
|
+
|
|
90
|
+
CallLog.new(
|
|
91
|
+
calls: @calls || [],
|
|
92
|
+
returns_by_object_id: @returns_by_object_id || {},
|
|
93
|
+
expectations: @expectations || []
|
|
94
|
+
)
|
|
95
|
+
ensure
|
|
96
|
+
@trace_point = nil
|
|
97
|
+
@calls = nil
|
|
98
|
+
@call_stack = nil
|
|
99
|
+
@expectations = nil
|
|
100
|
+
@returns_by_object_id = nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def record(trace_point)
|
|
106
|
+
return unless @config.production_file?(trace_point.path)
|
|
107
|
+
|
|
108
|
+
case trace_point.event
|
|
109
|
+
when :call
|
|
110
|
+
call = Call.new(
|
|
111
|
+
label: MethodLabel.from_trace_point(trace_point),
|
|
112
|
+
file: File.expand_path(trace_point.path),
|
|
113
|
+
line: trace_point.lineno,
|
|
114
|
+
event: :call
|
|
115
|
+
)
|
|
116
|
+
@calls << call
|
|
117
|
+
@call_stack << call
|
|
118
|
+
when :return
|
|
119
|
+
record_return(trace_point)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def record_return(trace_point)
|
|
124
|
+
call = @call_stack.pop
|
|
125
|
+
return unless call
|
|
126
|
+
|
|
127
|
+
@returns_by_object_id[trace_point.return_value.__id__] << call.label
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "coverage"
|
|
4
|
+
|
|
5
|
+
require_relative "../code_location"
|
|
6
|
+
|
|
7
|
+
module RSpec
|
|
8
|
+
module Covers
|
|
9
|
+
module Probe
|
|
10
|
+
class CoverageProbe
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@config = config
|
|
13
|
+
@started = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start
|
|
17
|
+
return if @started
|
|
18
|
+
|
|
19
|
+
Coverage.start(lines: true) unless Coverage.running?
|
|
20
|
+
@started = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def snapshot
|
|
24
|
+
start
|
|
25
|
+
normalize(Coverage.peek_result)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def difference(before_snapshot, after_snapshot)
|
|
29
|
+
after_snapshot.each_with_object(Set.new) do |(file, lines), executed|
|
|
30
|
+
next unless @config.production_file?(file)
|
|
31
|
+
|
|
32
|
+
previous = before_snapshot.fetch(file, [])
|
|
33
|
+
lines.each_with_index do |count, index|
|
|
34
|
+
next unless count && count > previous.fetch(index, 0).to_i
|
|
35
|
+
|
|
36
|
+
executed << CodeLocation.new(file: file, line: index + 1)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def normalize(raw_result)
|
|
44
|
+
raw_result.each_with_object({}) do |(file, payload), normalized|
|
|
45
|
+
normalized[File.expand_path(file)] = lines_for(payload)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def lines_for(payload)
|
|
50
|
+
return payload if payload.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
payload[:lines] || payload["lines"] || []
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
require_relative "code_location"
|
|
6
|
+
require_relative "method_entry"
|
|
7
|
+
require_relative "method_label"
|
|
8
|
+
require_relative "source_range"
|
|
9
|
+
require_relative "static_method_scanner"
|
|
10
|
+
|
|
11
|
+
module RSpec
|
|
12
|
+
module Covers
|
|
13
|
+
class ProductionInventory
|
|
14
|
+
def initialize(config)
|
|
15
|
+
@config = config
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def loaded_methods
|
|
19
|
+
@loaded_methods ||= ObjectSpace.each_object(Module).flat_map do |mod|
|
|
20
|
+
method_entries_for(mod)
|
|
21
|
+
end.uniq(&:label)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def methods
|
|
25
|
+
@methods ||= (loaded_methods + StaticMethodScanner.new(@config).scan).uniq(&:label)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def methods_for_locations(locations)
|
|
29
|
+
locations.flat_map do |location|
|
|
30
|
+
methods.select { |entry| entry.include?(location) }
|
|
31
|
+
end.uniq(&:label)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def methods_by_label(labels)
|
|
35
|
+
wanted = labels.to_set
|
|
36
|
+
methods.select { |entry| wanted.include?(entry.label) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def orphan_methods(declared_regions)
|
|
40
|
+
declared_labels = declared_regions.map(&:label).to_set
|
|
41
|
+
methods.reject { |entry| declared_labels.include?(entry.label) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def method_entries_for(mod)
|
|
47
|
+
instance_method_entries_for(mod) + singleton_method_entries_for(mod)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def instance_method_entries_for(mod)
|
|
51
|
+
own_instance_methods(mod).filter_map do |method_name|
|
|
52
|
+
callable = mod.instance_method(method_name)
|
|
53
|
+
method_entry(callable, MethodLabel.instance(mod, method_name))
|
|
54
|
+
rescue NameError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def singleton_method_entries_for(mod)
|
|
60
|
+
mod.singleton_methods(false).filter_map do |method_name|
|
|
61
|
+
method_entry(mod.method(method_name), MethodLabel.singleton(mod, method_name))
|
|
62
|
+
rescue NameError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def method_entry(callable, label)
|
|
68
|
+
source = callable.source_location
|
|
69
|
+
return unless source
|
|
70
|
+
|
|
71
|
+
file, line = source
|
|
72
|
+
return unless @config.production_file?(file)
|
|
73
|
+
|
|
74
|
+
last_line = SourceRange.last_line(callable) || line
|
|
75
|
+
region = CodeRegion.new(file: file, lines: line..last_line, label: label)
|
|
76
|
+
|
|
77
|
+
MethodEntry.new(label: label, file: File.expand_path(file), line: line, region: region)
|
|
78
|
+
rescue ArgumentError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def own_instance_methods(mod)
|
|
83
|
+
mod.instance_methods(false) +
|
|
84
|
+
mod.private_instance_methods(false) +
|
|
85
|
+
mod.protected_instance_methods(false)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rake"
|
|
5
|
+
|
|
6
|
+
require_relative "evaluation"
|
|
7
|
+
|
|
8
|
+
module RSpec
|
|
9
|
+
module Covers
|
|
10
|
+
class RakeTask
|
|
11
|
+
include ::Rake::DSL
|
|
12
|
+
|
|
13
|
+
def self.install
|
|
14
|
+
new.install
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def install
|
|
18
|
+
namespace :covers do
|
|
19
|
+
desc "Print an rspec-covers JSON report summary"
|
|
20
|
+
task :report, [:path] do |_task, args|
|
|
21
|
+
path = args[:path] || RSpec::Covers.configuration.report_path || ".rspec-covers/result.json"
|
|
22
|
+
report = JSON.parse(File.read(path))
|
|
23
|
+
summary = report.fetch("summary")
|
|
24
|
+
|
|
25
|
+
puts "examples: #{summary.fetch("examples")}"
|
|
26
|
+
puts "risky: #{summary.fetch("risky")}"
|
|
27
|
+
puts "suggestions: #{summary.fetch("suggestions")}"
|
|
28
|
+
puts "declaration_warnings: #{summary.fetch("declaration_warnings", 0)}"
|
|
29
|
+
puts "orphan_methods: #{summary.fetch("orphan_methods", 0)}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
desc "Print covers metadata suggestions from an rspec-covers JSON report"
|
|
33
|
+
task :suggest, [:path] do |_task, args|
|
|
34
|
+
path = args[:path] || RSpec::Covers.configuration.report_path || ".rspec-covers/result.json"
|
|
35
|
+
report = JSON.parse(File.read(path))
|
|
36
|
+
|
|
37
|
+
report.fetch("examples").each do |example|
|
|
38
|
+
example.fetch("suggestions").each do |suggestion|
|
|
39
|
+
puts "#{example.fetch("file")}:#{example.fetch("line")}"
|
|
40
|
+
puts " suggestion: covers: #{suggestion.fetch("label").inspect} " \
|
|
41
|
+
"(score #{suggestion.fetch("score")}: #{suggestion.fetch("reasons").join(" + ")})"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc "Evaluate suggestion precision/recall/F1 against ground truth JSON"
|
|
47
|
+
task :evaluate, [:ground_truth, :predictions] do |_task, args|
|
|
48
|
+
unless args[:ground_truth] && args[:predictions]
|
|
49
|
+
raise ArgumentError, "usage: rake covers:evaluate[ground_truth.json,predictions.json]"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result = Evaluation.evaluate(
|
|
53
|
+
ground_truth_path: args[:ground_truth],
|
|
54
|
+
predictions_path: args[:predictions]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
puts JSON.pretty_generate(result)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "declaration_validation"
|
|
7
|
+
require_relative "production_inventory"
|
|
8
|
+
require_relative "tracer/composite"
|
|
9
|
+
require_relative "tracer/dynamic_corpus"
|
|
10
|
+
|
|
11
|
+
module RSpec
|
|
12
|
+
module Covers
|
|
13
|
+
ExampleResult = Struct.new(
|
|
14
|
+
:id,
|
|
15
|
+
:file,
|
|
16
|
+
:line,
|
|
17
|
+
:description,
|
|
18
|
+
:verdict,
|
|
19
|
+
:suggestions,
|
|
20
|
+
:declaration_validation,
|
|
21
|
+
:declaration,
|
|
22
|
+
:example,
|
|
23
|
+
:call_log,
|
|
24
|
+
:executed_locations,
|
|
25
|
+
:measured_locations,
|
|
26
|
+
:unchecked_locations,
|
|
27
|
+
keyword_init: true
|
|
28
|
+
) do
|
|
29
|
+
def risky?
|
|
30
|
+
verdict.risky?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
id: id,
|
|
36
|
+
file: file,
|
|
37
|
+
line: line,
|
|
38
|
+
description: description,
|
|
39
|
+
verdict: verdict.to_h,
|
|
40
|
+
suggestions: suggestions.map(&:to_h),
|
|
41
|
+
declaration_validation: declaration_validation&.to_h,
|
|
42
|
+
measured_locations: Array(measured_locations).map(&:to_h),
|
|
43
|
+
unchecked_locations: Array(unchecked_locations).map(&:to_h),
|
|
44
|
+
declaration: {
|
|
45
|
+
covers: declaration.covers.map(&:to_h),
|
|
46
|
+
uses: declaration.uses.map(&:to_h),
|
|
47
|
+
unresolved: declaration.unresolved.map(&:to_s)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class Reporter
|
|
54
|
+
attr_reader :results
|
|
55
|
+
|
|
56
|
+
def initialize
|
|
57
|
+
@results = []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def record(result)
|
|
61
|
+
results << result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def risky_results
|
|
65
|
+
results.select(&:risky?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def suggestions
|
|
69
|
+
results.flat_map do |result|
|
|
70
|
+
result.suggestions.map { |suggestion| [result, suggestion] }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def declared_regions
|
|
75
|
+
results.flat_map { |result| result.declaration.covers }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def finalize_suggestions(config)
|
|
79
|
+
return unless config.suggest || config.validate_declarations
|
|
80
|
+
|
|
81
|
+
corpus = Tracer::DynamicCorpus.new
|
|
82
|
+
results.each do |result|
|
|
83
|
+
corpus.record(result.id, result.call_log.labels) if result.call_log
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
finalize_declaration_validations(config, corpus)
|
|
87
|
+
return unless config.suggest
|
|
88
|
+
|
|
89
|
+
results.each do |result|
|
|
90
|
+
next unless result.example
|
|
91
|
+
next if result.declaration.declared?
|
|
92
|
+
|
|
93
|
+
result.suggestions = Tracer::Composite.new(config, corpus: corpus).suggest(
|
|
94
|
+
example: result.example,
|
|
95
|
+
call_log: result.call_log,
|
|
96
|
+
executed_locations: result.executed_locations || []
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def validation_warnings
|
|
102
|
+
results.select do |result|
|
|
103
|
+
validation = result.declaration_validation
|
|
104
|
+
validation && !validation.valid?
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def orphan_methods
|
|
109
|
+
ProductionInventory.new(RSpec::Covers.configuration).orphan_methods(declared_regions)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_h
|
|
113
|
+
orphans = orphan_methods
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
version: VERSION,
|
|
117
|
+
summary: {
|
|
118
|
+
examples: results.length,
|
|
119
|
+
risky: risky_results.length,
|
|
120
|
+
suggestions: suggestions.length,
|
|
121
|
+
declaration_warnings: validation_warnings.length,
|
|
122
|
+
orphan_methods: orphans.length
|
|
123
|
+
},
|
|
124
|
+
orphan_methods: orphans.map(&:to_h),
|
|
125
|
+
examples: results.map(&:to_h)
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def write_json(path)
|
|
130
|
+
return unless path
|
|
131
|
+
|
|
132
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
133
|
+
File.write(path, JSON.pretty_generate(to_h))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def finalize_declaration_validations(config, corpus)
|
|
139
|
+
results.each do |result|
|
|
140
|
+
next unless result.example
|
|
141
|
+
next unless result.declaration.declared?
|
|
142
|
+
|
|
143
|
+
result.declaration_validation = validation_for(result, config, corpus)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def validation_for(result, config, corpus)
|
|
148
|
+
suggestions = Tracer::Composite.new(config, corpus: corpus).suggest(
|
|
149
|
+
example: result.example,
|
|
150
|
+
call_log: result.call_log,
|
|
151
|
+
executed_locations: result.executed_locations || [],
|
|
152
|
+
limit: nil
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
DeclarationValidator.new(config).call(
|
|
156
|
+
declaration: result.declaration,
|
|
157
|
+
suggestions: suggestions
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Covers
|
|
5
|
+
module SourceRange
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def last_line(callable)
|
|
9
|
+
ast_last_line(callable) || method_source_last_line(callable)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def ast_last_line(callable)
|
|
13
|
+
return unless defined?(RubyVM::AbstractSyntaxTree)
|
|
14
|
+
|
|
15
|
+
RubyVM::AbstractSyntaxTree.of(callable).last_lineno
|
|
16
|
+
rescue ArgumentError, RuntimeError, TypeError
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def method_source_last_line(callable)
|
|
21
|
+
source_location = callable.source_location
|
|
22
|
+
return unless source_location
|
|
23
|
+
|
|
24
|
+
require "method_source"
|
|
25
|
+
return unless callable.respond_to?(:source)
|
|
26
|
+
|
|
27
|
+
source_location.last + callable.source.lines.length - 1
|
|
28
|
+
rescue LoadError, ArgumentError
|
|
29
|
+
nil
|
|
30
|
+
rescue StandardError => error
|
|
31
|
+
raise unless defined?(MethodSource::SourceNotFoundError) && error.is_a?(MethodSource::SourceNotFoundError)
|
|
32
|
+
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|