rubocop-rspec_parity 1.6.0 → 2.0.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +53 -0
- data/config/default.yml +1 -0
- data/docs/superpowers/specs/2026-06-15-covers-branch-annotations-design.md +224 -0
- data/exe/rspec-parity-cover +25 -0
- data/lib/rubocop/cop/rspec_parity/private_method_call_graph.rb +19 -9
- data/lib/rubocop/cop/rspec_parity/sufficient_contexts.rb +587 -95
- data/lib/rubocop/rspec_parity/coverage_reporter.rb +105 -0
- data/lib/rubocop/rspec_parity/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bda6248c84f44ce1e039baac11b9e6b3075c3feb13bb4e06a9e804ff26436a6
|
|
4
|
+
data.tar.gz: ebc0e696c427bd033637609d7f1ad4aaa66c603a9ca1ed82adb09d2857c99161
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 102aceedd06b6db82be97c0a45d95a57e22cb1b1ab7427875c6137f98dcfcc87b862cd2cc26dfc14d8f5604a24702166db12b6f94c1835f7e0395fd98082d556
|
|
7
|
+
data.tar.gz: ca83d65bb73322941c49160de66943b2e1c6c93f35db372ef8cb559e5ea2cf3bdf1d3cc31151e654c8d19b620ceb1f4ee2f6dbf5ef8a8213277bd243c7104111
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.0.0] - 2026-06-18
|
|
4
|
+
|
|
5
|
+
Added: `SufficientContexts` now pinpoints which branch is untested — its message names one uncovered branch and the `# rspec_parity:covers <branch>` annotation to add, and the bundled `rspec-parity-cover` executable lists all of a method's gaps as paste-ready context stubs. Annotations are opt-in (new `CoversAnnotations` config key, default `true`).
|
|
6
|
+
|
|
7
|
+
## [1.7.0] - 2026-06-11
|
|
8
|
+
|
|
9
|
+
Fixed: `SufficientContexts` no longer over-counts chains of guard clauses (`return`/`raise`/`next ... if/unless`). A sequence of guards shares a single "all guards pass" fall-through, so that happy path is counted once for the whole method instead of once per guard. Each `&&`/`||` inside a guard condition is still counted as a distinct way for the guard to fire (one scenario per operand).
|
|
10
|
+
|
|
3
11
|
## [1.6.0] - 2026-06-11
|
|
4
12
|
|
|
5
13
|
Fixed: `SufficientContexts` now counts each `it`/`example` within a single context as a separate scenario, so specs that cover branches with multiple examples (instead of separate contexts) no longer trigger false violations
|
data/README.md
CHANGED
|
@@ -230,6 +230,59 @@ end
|
|
|
230
230
|
**Configuration options:**
|
|
231
231
|
|
|
232
232
|
- `IgnoreMemoization` (default: `true`) - When enabled, common memoization patterns like `@var ||=` and `return @var if defined?(@var)` are not counted as branches. Set to `false` if you want to count these as branches.
|
|
233
|
+
- `CoversAnnotations` (default: `true`) - Enables the `# rspec_parity:covers` annotations described below. Set to `false` to revert to the plain numeric message.
|
|
234
|
+
|
|
235
|
+
#### Pinpointing the missing branch with `# rspec_parity:covers`
|
|
236
|
+
|
|
237
|
+
Instead of a bare count, the violation names one uncovered branch and gives the exact context to add:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
Missing coverage for `user.staff?` (line 4) — 2 of 3 branches untested.
|
|
241
|
+
Add `context '...' do # rspec_parity:covers user.staff?` to mark it covered.
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Tag a context with the branch it covers (trailing, or on its own line inside the block):
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
context 'when staff' do # rspec_parity:covers user.staff?
|
|
248
|
+
it { is_expected.to eq('staff') }
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Re-run and the message advances to the next uncovered branch. Annotations are opt-in and only ever raise coverage — they never create a new violation, and each one covers exactly one branch. A mistyped label is reported with a did-you-mean suggestion.
|
|
253
|
+
|
|
254
|
+
Long conditions can push the comment past `Layout/LineLength`; exempt these comments rather than editing the label:
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
Layout/LineLength:
|
|
258
|
+
AllowedPatterns:
|
|
259
|
+
- 'rspec_parity:covers'
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
##### Listing every uncovered branch (`rspec-parity-cover`)
|
|
263
|
+
|
|
264
|
+
To get all the gaps for a method at once as ready-to-paste stubs:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
$ bundle exec rspec-parity-cover app/services/classifier.rb
|
|
268
|
+
|
|
269
|
+
# classify — 2 uncovered branches:
|
|
270
|
+
context '...' do
|
|
271
|
+
# rspec_parity:covers user.staff?
|
|
272
|
+
|
|
273
|
+
it '...' do
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
context '...' do
|
|
278
|
+
# rspec_parity:covers else of user.admin?
|
|
279
|
+
|
|
280
|
+
it '...' do
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
It derives the spec path (or take a second argument), and accepts `app/foo.rb:42` to report only the method at that line. When a method has many untested branches, the cop's message points you straight at this command.
|
|
233
286
|
|
|
234
287
|
## Assumptions
|
|
235
288
|
|
data/config/default.yml
CHANGED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Pinpoint missing branches with `# rspec_parity:covers` annotations
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
`RSpecParity/SufficientContexts` reports a bare numeric gap: "method has 21
|
|
6
|
+
branches but the spec covers only 20 scenarios." When the gap is 1 out of 21,
|
|
7
|
+
there is no way to tell *which* branch is uncovered. Both sides of the
|
|
8
|
+
comparison are pure counts — branches are tallied as integers from the AST, and
|
|
9
|
+
scenarios are tallied as integers from spec text — so there is no axis along
|
|
10
|
+
which a specific branch maps to a specific context.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
|
|
14
|
+
1. Make the offense name the branches it counted, so a human can reconcile the
|
|
15
|
+
gap by eye instead of guessing.
|
|
16
|
+
2. Let authors *opt in* to precise per-branch tracking by tagging contexts with
|
|
17
|
+
a `# rspec_parity:covers <label>` magic comment. Annotations are purely additive —
|
|
18
|
+
they can only ever *help* (raise coverage / narrow the reported gap), never
|
|
19
|
+
create a new violation and never make a passing method fail.
|
|
20
|
+
3. Report annotations that match no known branch (typos / renamed branches),
|
|
21
|
+
with a did-you-mean suggestion.
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
RSpecParity/SufficientContexts:
|
|
27
|
+
CoversAnnotations: true # default
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Set to `false` to restore the prior numeric-only message and skip all
|
|
31
|
+
annotation handling.
|
|
32
|
+
|
|
33
|
+
## Concepts
|
|
34
|
+
|
|
35
|
+
### Branch descriptors
|
|
36
|
+
|
|
37
|
+
`branch_tally` stops returning integer counters and instead collects a list of
|
|
38
|
+
**branch descriptors**, one per counted branch:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Branch = Struct.new(:kind, :label, :line, :origin)
|
|
42
|
+
# kind — :guard or :regular (preserves the guard fall-through logic)
|
|
43
|
+
# label — the normalized condition/operator source (what you annotate with)
|
|
44
|
+
# line — source line of the branch
|
|
45
|
+
# origin — nil for the method's own branches; the helper method name when the
|
|
46
|
+
# branch was inlined from a single-use private method
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`BranchTally` becomes a thin wrapper over an array of `Branch`, still exposing
|
|
50
|
+
`guard`, `regular`, `total`, and `+` so `branches_from` and
|
|
51
|
+
`PrivateMethodCallGraph` keep working unchanged. A new `with_origin(name)`
|
|
52
|
+
returns a copy with each not-yet-tagged descriptor's `origin` set — the call
|
|
53
|
+
graph tags descriptors as it inlines each helper.
|
|
54
|
+
|
|
55
|
+
### Labels
|
|
56
|
+
|
|
57
|
+
A label is the branch's source, whitespace-collapsed (`source.gsub(/\s+/, " ")
|
|
58
|
+
.strip`), case-sensitive. No truncation — the displayed label is exactly the
|
|
59
|
+
string you type into the annotation, so it is always copy-pasteable.
|
|
60
|
+
|
|
61
|
+
Per branch kind:
|
|
62
|
+
|
|
63
|
+
| AST | descriptors |
|
|
64
|
+
|-----|-------------|
|
|
65
|
+
| `if`/`elsif`/`else` (non-guard) | one per condition (`if`, each `elsif`) + `else of <if condition>` |
|
|
66
|
+
| guard `if`/`unless` | one, label = guard condition source, kind `:guard` |
|
|
67
|
+
| `case`/`when` | one per `when` + `else of case <subject>` (only if present) |
|
|
68
|
+
| `&&` / `\|\|` | one, label = operator source; carries `detail` `<rhs> decides` |
|
|
69
|
+
| `\|\|=` / `&&=` | two, label = `<src>`; carry `detail` `already set` / `assigns` |
|
|
70
|
+
| `&` / `\|` send | one, label = node source |
|
|
71
|
+
|
|
72
|
+
**Every branch ends up with a unique token, so one annotation covers exactly one
|
|
73
|
+
branch.** `#disambiguate` enforces this in two passes: branches whose plain token
|
|
74
|
+
still collides get their `detail` applied (`a && b` → `a && b (b decides)`,
|
|
75
|
+
`||=` → `… (assigns)`/`… (already set)`); anything still identical (e.g. two
|
|
76
|
+
`if`s with the same condition) gets a positional `(1)`, `(2)`. Non-colliding
|
|
77
|
+
labels (including standalone `&&`/`||` expressions) keep their plain source, so
|
|
78
|
+
they stay naturally typeable.
|
|
79
|
+
|
|
80
|
+
When a branch was inlined from a private helper, its annotation token is
|
|
81
|
+
prefixed with the origin: `admin_role?: role == 'admin'`. The prefix is
|
|
82
|
+
**optional when unambiguous** — typing just `role == 'admin'` matches when
|
|
83
|
+
exactly one branch has that condition; the prefix is only required to
|
|
84
|
+
disambiguate identical conditions in different helpers.
|
|
85
|
+
|
|
86
|
+
### Annotations
|
|
87
|
+
|
|
88
|
+
A trailing magic comment on a `context`/`describe`/`it`/`example`/`specify`
|
|
89
|
+
line:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
context 'when admin by role' do # rspec_parity:covers admin_role?: role == 'admin'
|
|
93
|
+
it 'returns true'
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- An annotated **context** collapses its inner examples: the whole block counts
|
|
98
|
+
as the branch(es) it declares, *not* as N example scenarios. This overrides
|
|
99
|
+
the "multiple `it`s = multiple scenarios" heuristic for that block — the
|
|
100
|
+
author has explicitly said "this block is one branch."
|
|
101
|
+
- An annotated **example** counts as its own branch.
|
|
102
|
+
- One annotation may list several labels separated by `;` (semicolon — commas
|
|
103
|
+
appear inside conditions). Or repeat the comment.
|
|
104
|
+
- The annotation may also be a **standalone comment line inside the context**
|
|
105
|
+
(handy when a trailing comment would overflow the line length); it is
|
|
106
|
+
attributed to the enclosing context. Place it before the context's examples.
|
|
107
|
+
|
|
108
|
+
## Coverage rule
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
covered = distinct_matched_annotations
|
|
112
|
+
+ numeric_scenarios(from the un-annotated parts only)
|
|
113
|
+
|
|
114
|
+
offense fires when covered < branches # same trigger as before
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Annotations only raise `covered`. With zero annotations this reduces exactly to
|
|
118
|
+
today's `scenario_count` over the whole block.
|
|
119
|
+
|
|
120
|
+
## Messages (single line — RuboCop offense constraint)
|
|
121
|
+
|
|
122
|
+
Rather than listing every branch (unreadable for branchy methods), the message
|
|
123
|
+
names **one** still-uncovered branch and gives the exact context to add. Each
|
|
124
|
+
re-run advances to the next gap as branches get annotated.
|
|
125
|
+
|
|
126
|
+
**With annotations on** (the default):
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Missing coverage for `user.staff?` (line 4) — 2 of 3 branches untested.
|
|
130
|
+
Add `context '...' do # rspec_parity:covers user.staff?` to mark it covered.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The showcased branch is the first uncovered one, preferring a real condition
|
|
134
|
+
over `else`/guard fall-through so the example reads clearly. A branch traced
|
|
135
|
+
from a helper shows its origin in the location (`helper line N`) and token
|
|
136
|
+
(`helper: condition`). The existing `(including branches from: …)` suffix still
|
|
137
|
+
appends when branches were inlined.
|
|
138
|
+
|
|
139
|
+
**With annotations off** — just the count, no annotation guidance:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
Method `classify` is missing coverage for 9 of 10 branches.
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Orphan annotations** (appended when the method offends):
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
… `rspec_parity:covers` annotation `params[:admni]` matches no branch — did you
|
|
149
|
+
mean `params[:admin]`?
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Nearest match via `DidYouMean::SpellChecker`; omit the "did you mean" clause
|
|
153
|
+
when nothing is close. An orphan annotation alone never triggers an offense
|
|
154
|
+
(opt-in): orphans are only reported when the method is already offending.
|
|
155
|
+
|
|
156
|
+
## Architecture
|
|
157
|
+
|
|
158
|
+
### `SufficientContexts`
|
|
159
|
+
|
|
160
|
+
- `initialize`: `@covers_annotations = cop_config.fetch("CoversAnnotations", true)`.
|
|
161
|
+
- `BranchTally` rewritten over `Branch` descriptors (`guard`/`regular`/`total`/
|
|
162
|
+
`+`/`with_origin`/`branches`).
|
|
163
|
+
- `branch_tally`, `count_if_branches`→`if_branch_descriptors`,
|
|
164
|
+
`count_case_branches`→`case_branch_descriptors`, `send_node_branch_count`,
|
|
165
|
+
`or_asgn`/`and_asgn` handling all push descriptors.
|
|
166
|
+
- `branches_from` unchanged (operates on `guard`/`regular`/`total`).
|
|
167
|
+
- Spec parsing (`parse_spec_content`, `count_top_level_contexts`): collect
|
|
168
|
+
trailing `# rspec_parity:covers` labels, suppress example counting inside annotated
|
|
169
|
+
contexts, return labels + un-annotated numeric scenario count.
|
|
170
|
+
- `check_method`: compute `matched`/`unannotated`/`orphans` by intersecting
|
|
171
|
+
annotation labels with the descriptor inventory; `covered = matched.size +
|
|
172
|
+
unannotated_scenarios`; build the enriched message.
|
|
173
|
+
- New helpers: `match_annotations`, `nearest_label` (DidYouMean), message
|
|
174
|
+
builders.
|
|
175
|
+
|
|
176
|
+
### `PrivateMethodCallGraph`
|
|
177
|
+
|
|
178
|
+
`visit_callee` tags the inlined tally: `tally = tally.with_origin(name) if
|
|
179
|
+
tally.respond_to?(:with_origin)`. No other change; the `Result`/traverse logic
|
|
180
|
+
is untouched.
|
|
181
|
+
|
|
182
|
+
### `config/default.yml`
|
|
183
|
+
|
|
184
|
+
Add `CoversAnnotations: true` under `RSpecParity/SufficientContexts`.
|
|
185
|
+
|
|
186
|
+
## Edge cases
|
|
187
|
+
|
|
188
|
+
- **`CoversAnnotations: false`** — message reverts to today's exact
|
|
189
|
+
string; annotation comments are ignored entirely.
|
|
190
|
+
- **No spec annotations** — `matched` is empty, `unannotated_scenarios` equals
|
|
191
|
+
today's `scenario_count`; coverage and trigger are identical to today. Only
|
|
192
|
+
the message gains the inventory suffix.
|
|
193
|
+
- **Duplicate identical labels** (two branches, same source, same origin) — a
|
|
194
|
+
single annotation covers both; acceptable for "good enough" precision and
|
|
195
|
+
logged via the inventory line.
|
|
196
|
+
- **Annotated context with nested contexts** — the whole subtree collapses to
|
|
197
|
+
the declared label(s).
|
|
198
|
+
- **Multi-line / very long condition** — collapsed to one line; long but exact
|
|
199
|
+
and matchable.
|
|
200
|
+
|
|
201
|
+
## Testing
|
|
202
|
+
|
|
203
|
+
Extend `spec/rubocop/cop/rspec_parity/sufficient_contexts_spec.rb`:
|
|
204
|
+
|
|
205
|
+
- inventory appears in the message when no annotations exist
|
|
206
|
+
- a matching annotation removes a branch from the uncovered list / satisfies the
|
|
207
|
+
count
|
|
208
|
+
- annotated context with several `it`s counts as one branch (collapse)
|
|
209
|
+
- optional origin prefix: bare condition matches when unambiguous
|
|
210
|
+
- orphan annotation reported with did-you-mean
|
|
211
|
+
- `CoversAnnotations: false` reproduces the legacy message
|
|
212
|
+
- interaction with `TraceSingleUsePrivateMethods` (origin-prefixed labels)
|
|
213
|
+
|
|
214
|
+
Existing message assertions are updated to the new output (captured from an
|
|
215
|
+
actual run).
|
|
216
|
+
|
|
217
|
+
## Open questions resolved
|
|
218
|
+
|
|
219
|
+
- **Default** — `true` (opt-out), consistent with `TraceSingleUsePrivateMethods`.
|
|
220
|
+
The richer message is strictly more informative; annotations are additive.
|
|
221
|
+
- **Message is single-line** — RuboCop offense messages are single-line; the
|
|
222
|
+
inventory is joined with `; ` rather than rendered as a list.
|
|
223
|
+
- **Annotations never trigger** — opt-in means an annotation can only narrow or
|
|
224
|
+
satisfy; it never creates an offense.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rubocop/rspec_parity/coverage_reporter"
|
|
5
|
+
|
|
6
|
+
target = ARGV[0]
|
|
7
|
+
|
|
8
|
+
if target.nil? || %w[-h --help].include?(target)
|
|
9
|
+
warn "Usage: rspec-parity-cover SOURCE_FILE[:LINE] [SPEC_FILE]"
|
|
10
|
+
warn ""
|
|
11
|
+
warn "Lists a method's uncovered branches as paste-ready"
|
|
12
|
+
warn "`context '...' do # rspec_parity:covers <branch>` stubs."
|
|
13
|
+
warn "Append :LINE (e.g. app/foo.rb:20) to report only the method at that line."
|
|
14
|
+
warn "The spec file is derived from SOURCE_FILE (app/ -> spec/, *.rb -> *_spec.rb)"
|
|
15
|
+
warn "unless SPEC_FILE is given."
|
|
16
|
+
exit(target.nil? ? 1 : 0)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
source, line = target.match(/\A(?<path>.+):(?<line>\d+)\z/)&.named_captures&.values_at("path", "line") || [target, nil]
|
|
20
|
+
|
|
21
|
+
abort "rspec-parity-cover: no such file: #{source}" unless File.file?(source)
|
|
22
|
+
|
|
23
|
+
reporter = RuboCop::RSpecParity::CoverageReporter.new(source, spec_path: ARGV[1], line: line&.to_i)
|
|
24
|
+
output = reporter.render
|
|
25
|
+
puts output.empty? ? "# No uncovered branches found for #{target}." : output
|
|
@@ -8,7 +8,9 @@ module RuboCop
|
|
|
8
8
|
# private/protected helpers that are called from exactly one place in
|
|
9
9
|
# the same container.
|
|
10
10
|
class PrivateMethodCallGraph # rubocop:disable Metrics/ClassLength
|
|
11
|
-
|
|
11
|
+
# `branch_tally` is whatever the branch_counter returns (a BranchTally),
|
|
12
|
+
# or nil when nothing was inlined. It must respond to `+` and `total`.
|
|
13
|
+
Result = Struct.new(:branch_tally, :traced_methods)
|
|
12
14
|
|
|
13
15
|
DYNAMIC_DISPATCH_SENDS = %i[send public_send __send__].freeze
|
|
14
16
|
EVAL_METHODS = %i[class_eval instance_eval module_eval].freeze
|
|
@@ -28,13 +30,13 @@ module RuboCop
|
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def inlinable_from(method_node, branch_counter)
|
|
31
|
-
return Result.new(
|
|
32
|
-
return Result.new(
|
|
33
|
+
return Result.new(nil, []) unless @container
|
|
34
|
+
return Result.new(nil, []) if dynamic_dispatch?
|
|
33
35
|
|
|
34
36
|
build! unless @built
|
|
35
37
|
|
|
36
38
|
key = key_for(method_node)
|
|
37
|
-
return Result.new(
|
|
39
|
+
return Result.new(nil, []) unless @methods.key?(key)
|
|
38
40
|
|
|
39
41
|
traverse(key, branch_counter)
|
|
40
42
|
end
|
|
@@ -42,7 +44,7 @@ module RuboCop
|
|
|
42
44
|
private
|
|
43
45
|
|
|
44
46
|
def traverse(start_key, branch_counter)
|
|
45
|
-
state = { visited: Set.new([start_key]),
|
|
47
|
+
state = { visited: Set.new([start_key]), tally: nil, traced: [] }
|
|
46
48
|
stack = callees_of(start_key)
|
|
47
49
|
until stack.empty?
|
|
48
50
|
key = stack.shift
|
|
@@ -51,7 +53,7 @@ module RuboCop
|
|
|
51
53
|
visit_callee(key, branch_counter, state)
|
|
52
54
|
stack.concat(callees_of(key))
|
|
53
55
|
end
|
|
54
|
-
Result.new(state[:
|
|
56
|
+
Result.new(state[:tally], sorted_names(state[:traced]))
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
def callees_of(key)
|
|
@@ -60,13 +62,21 @@ module RuboCop
|
|
|
60
62
|
|
|
61
63
|
def visit_callee(key, branch_counter, state)
|
|
62
64
|
state[:visited] << key
|
|
63
|
-
|
|
64
|
-
return unless
|
|
65
|
+
tally = branch_counter.call(@methods[key][:node])
|
|
66
|
+
return unless tally.total.positive?
|
|
65
67
|
|
|
66
|
-
state[:
|
|
68
|
+
state[:tally] = combine_tally(state[:tally], tally, @methods[key][:name])
|
|
67
69
|
state[:traced] << key
|
|
68
70
|
end
|
|
69
71
|
|
|
72
|
+
# Attribute the inlined branches to the helper they came from so the cop
|
|
73
|
+
# can show "label (helper, line N)" and accept origin-prefixed
|
|
74
|
+
# `# rspec_parity:covers` annotations. Duck-typed: a plain count would skip.
|
|
75
|
+
def combine_tally(existing, tally, name)
|
|
76
|
+
tally = tally.with_origin(name) if tally.respond_to?(:with_origin)
|
|
77
|
+
existing ? existing + tally : tally
|
|
78
|
+
end
|
|
79
|
+
|
|
70
80
|
def inlinable?(key, visited)
|
|
71
81
|
return false if visited.include?(key)
|
|
72
82
|
return false unless @methods.key?(key)
|