specguard-ruby 0.3.26 → 0.3.28
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/README.md +5 -4
- data/lib/specguard/rspec/cli.rb +15 -13
- data/lib/specguard/rspec/finding.rb +6 -3
- data/lib/specguard/rspec/linter.rb +8 -6
- data/lib/specguard/rspec/scanner.rb +88 -14
- data/lib/specguard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58f8188803b68968be3289f01834d0f128f239b10d31162d34eeaaf55b6daefe
|
|
4
|
+
data.tar.gz: 273ce5b5eb675b12b302e23c3b24e025d81e60e77b77476ba9c7401fd7742600
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fcc5a85ecf595eeb62aae5446387ff74802579f97991c71b9b7a61014499c954d6fcc546171440dd3fde330b06ad11cc6fb2717b5f8f569aee884d51534c1f61
|
|
7
|
+
data.tar.gz: 52ed4889c3074cb474b9a192603414ba11644c753ad6dc2a3d946579663bf0432bcd533702f36568d9c81259a4eaf3e7735102a5374379fbabe0a6b4f24b76e5
|
data/README.md
CHANGED
|
@@ -48,8 +48,9 @@ bundle exec ruby -rminitest/specguard_plugin -e 'Minitest.extensions << "specgua
|
|
|
48
48
|
|
|
49
49
|
Validates `# @intent:` annotations in changed (or all) `*_spec.rb` files against the OpenTestIntent
|
|
50
50
|
JSON Schema. Exits `1` on a malformed annotation — or a well-formed but unreachable one (stacked
|
|
51
|
-
above another comment-form `@intent:` line, so the one-line lookback never claims it
|
|
52
|
-
|
|
51
|
+
above another comment-form `@intent:` line, so the one-line lookback never claims it; separated
|
|
52
|
+
from its `it` by one blank or ordinary comment line, which puts it out of the lookback's reach; or
|
|
53
|
+
trailing on a `describe`/`context` group line, which no example's extraction can ever reach) — and
|
|
53
54
|
**never** fails on a *missing* one (adoption is opt-in and gradual).
|
|
54
55
|
|
|
55
56
|
```bash
|
|
@@ -335,8 +336,8 @@ Every way the backend can fail — the binary is missing, will not execute, exit
|
|
|
335
336
|
is not a verdict, or emits output that is not a report — is **exit 2**, the linter's "could not do
|
|
336
337
|
my job" code. It never becomes exit 1, which means "an annotation is malformed" — or well-formed
|
|
337
338
|
but unreachable: stacked above another comment-form `@intent:` line, so the one-line lookback
|
|
338
|
-
never claims it,
|
|
339
|
-
can ever reach — and nothing else.
|
|
339
|
+
never claims it, separated from its `it` by one blank or ordinary comment line, or trailing on a
|
|
340
|
+
`describe`/`context` group line, which no example's extraction can ever reach — and nothing else.
|
|
340
341
|
|
|
341
342
|
## The formatter — `SpecGuard::RSpecFormatter`
|
|
342
343
|
|
data/lib/specguard/rspec/cli.rb
CHANGED
|
@@ -126,10 +126,11 @@ module SpecGuard
|
|
|
126
126
|
# "Lint, don't require": a missing annotation is never an error.
|
|
127
127
|
EXIT_OK = 0
|
|
128
128
|
# One or more annotations are malformed — or well-formed but
|
|
129
|
-
# unreachable (SPGD-900/SPGD-1510: stacked above another
|
|
130
|
-
# `@intent:` line so the one-line lookback never claims it,
|
|
131
|
-
#
|
|
132
|
-
#
|
|
129
|
+
# unreachable (SPGD-900/SPGD-1510/SPGD-1554: stacked above another
|
|
130
|
+
# comment-form `@intent:` line so the one-line lookback never claims it,
|
|
131
|
+
# separated from its example by one blank/comment line, or trailing on
|
|
132
|
+
# an example-group line no example's extraction can reach — a dead
|
|
133
|
+
# contract in every case, and still a failure). The only code produced by
|
|
133
134
|
# inspecting content, and the only path that reaches it is a failed
|
|
134
135
|
# {Linter::Result}.
|
|
135
136
|
EXIT_MALFORMED = 1
|
|
@@ -167,12 +168,13 @@ module SpecGuard
|
|
|
167
168
|
|
|
168
169
|
results = backend.check(selection.files)
|
|
169
170
|
|
|
170
|
-
# The structural pass (SPGD-900, SPGD-1510): annotations
|
|
171
|
-
# valid in isolation but can never be extracted — stacked
|
|
172
|
-
# comment-form `@intent:` lines, where the one-line
|
|
173
|
-
# (SPGD-12 §2) claims only the line just above the example,
|
|
174
|
-
#
|
|
175
|
-
#
|
|
171
|
+
# The structural pass (SPGD-900, SPGD-1510, SPGD-1554): annotations
|
|
172
|
+
# that are valid in isolation but can never be extracted — stacked
|
|
173
|
+
# consecutive comment-form `@intent:` lines, where the one-line
|
|
174
|
+
# lookback (SPGD-12 §2) claims only the line just above the example,
|
|
175
|
+
# comment-form runs separated from their example by one blank/comment
|
|
176
|
+
# line, and `@intent:` tokens trailing on an example-group line, which
|
|
177
|
+
# no extraction rule can ever reach. These are Linter::Results like any
|
|
176
178
|
# other, so both renderers and the exit code pick them up with no
|
|
177
179
|
# second path to keep in step.
|
|
178
180
|
results += unreachable_results(selection.files)
|
|
@@ -480,15 +482,15 @@ module SpecGuard
|
|
|
480
482
|
#
|
|
481
483
|
# The boundary is the set-difference the partition above already
|
|
482
484
|
# supports: a file counts as covered when it yielded any line-scoped
|
|
483
|
-
# result (an annotation, valid or not — the SPGD-900/SPGD-1510
|
|
485
|
+
# result (an annotation, valid or not — the SPGD-900/SPGD-1510/SPGD-1554
|
|
484
486
|
# unreachable findings are line-scoped too and require an annotation to
|
|
485
487
|
# exist, so they cover their file) or when it could not be read at all
|
|
486
488
|
# (KIND_READ → `unread`). An unread file is NOT a zero-annotation file:
|
|
487
489
|
# it already gets its own summary clause and FAIL line, and calling it
|
|
488
490
|
# annotation-free would be the same overstatement `summary_line` refuses
|
|
489
491
|
# to make. Unreachable findings need an annotation to exist — a stacked
|
|
490
|
-
# comment-form pair, or an `@intent:`
|
|
491
|
-
# in a file with none, so `annotations ∪ unread` already covers every
|
|
492
|
+
# comment-form pair, a separated comment-form run, or an `@intent:`
|
|
493
|
+
# token on a group line — impossible in a file with none, so `annotations ∪ unread` already covers every
|
|
492
494
|
# non-bare file and no third subtraction is needed.
|
|
493
495
|
#
|
|
494
496
|
# It is a NOTE on stderr, never a warning and never an exit code: "lint,
|
|
@@ -49,7 +49,7 @@ module SpecGuard
|
|
|
49
49
|
KIND_SCHEMA = :schema
|
|
50
50
|
|
|
51
51
|
# The annotation is well-formed in isolation but can never be EXTRACTED.
|
|
52
|
-
#
|
|
52
|
+
# Three shapes, all dead the moment they are written:
|
|
53
53
|
#
|
|
54
54
|
# * a comment-form `@intent:` stacked above another comment-form
|
|
55
55
|
# `@intent:` line, so the one-line lookback (SPGD-12 §2 —
|
|
@@ -58,9 +58,12 @@ module SpecGuard
|
|
|
58
58
|
# * an `@intent:` trailing on an example-group line
|
|
59
59
|
# ({Scanner::GROUP_LINE} — SPGD-1510): a group line is no example's
|
|
60
60
|
# own and is not comment-only, so no extraction rule can ever reach
|
|
61
|
-
# it, and the example beneath it silently ingests unannotated
|
|
61
|
+
# it, and the example beneath it silently ingests unannotated;
|
|
62
|
+
# * a comment-form `@intent:` run separated from its example by ONE
|
|
63
|
+
# blank or ordinary comment line (SPGD-1554): the one-line lookback
|
|
64
|
+
# reads only that interleave, so the whole run is out of reach.
|
|
62
65
|
#
|
|
63
|
-
#
|
|
66
|
+
# Whichever shape, the contract is dead metadata: counted by the linter,
|
|
64
67
|
# discarded by extraction. Produced by {Scanner}'s structural pass, not
|
|
65
68
|
# by {AnnotationScanner} — each annotation is scanned in isolation
|
|
66
69
|
# there, which is exactly why these defects are invisible to it.
|
|
@@ -40,13 +40,15 @@ module SpecGuard
|
|
|
40
40
|
# `Finding::KIND_UNREACHABLE` — an annotation that is well-formed in
|
|
41
41
|
# isolation but can never be extracted, so the one-line lookback
|
|
42
42
|
# (SPGD-12 §2) never claims it — is also reported as a failure, and
|
|
43
|
-
# therefore also exits 1.
|
|
43
|
+
# therefore also exits 1. Three shapes reach this kind: a comment-form
|
|
44
44
|
# `@intent:` stacked above another comment-form `@intent:` line (SPGD-900),
|
|
45
|
-
#
|
|
46
|
-
# example's own and not comment-only (SPGD-1510)
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
45
|
+
# an `@intent:` trailing on an example-group line, which is no
|
|
46
|
+
# example's own and not comment-only (SPGD-1510), and a comment-form
|
|
47
|
+
# `@intent:` run separated from its example by one blank or ordinary
|
|
48
|
+
# comment line (SPGD-1554). Nothing about any of them is malformed; the
|
|
49
|
+
# contract is dead metadata, counted by the linter and discarded by
|
|
50
|
+
# extraction, and the structural pass that flags them (SPGD-900,
|
|
51
|
+
# SPGD-1510, SPGD-1554) reports it loudly rather than letting it pass as
|
|
50
52
|
# clean.
|
|
51
53
|
module Linter
|
|
52
54
|
# One annotation's verdict. `problem` is set when discovery could not
|
|
@@ -261,7 +261,7 @@ module SpecGuard
|
|
|
261
261
|
# ({AnnotationLookup}, SPGD-12 §2) is positional: an annotation reaches
|
|
262
262
|
# an example only through its own line (the trailing form, and only when
|
|
263
263
|
# that line IS the example's) or through the comment-form line
|
|
264
|
-
# IMMEDIATELY ABOVE it.
|
|
264
|
+
# IMMEDIATELY ABOVE it. Three shapes fall outside both and are dead the
|
|
265
265
|
# moment they are written — silently discarded by extraction while this
|
|
266
266
|
# pipeline counts them as valid annotations and the run exits 0:
|
|
267
267
|
#
|
|
@@ -276,8 +276,14 @@ module SpecGuard
|
|
|
276
276
|
# ever claim it — the example beneath it silently ingests as
|
|
277
277
|
# unannotated. A describe insertion that swallowed a newline is how
|
|
278
278
|
# the shape is born.
|
|
279
|
-
#
|
|
280
|
-
#
|
|
279
|
+
# * the SEPARATED annotation (SPGD-1554, ported from specguard-ts
|
|
280
|
+
# SPGD-1550): a comment-form `@intent:` run followed by exactly ONE
|
|
281
|
+
# blank line or ordinary (non-`@intent:`) comment line and THEN an
|
|
282
|
+
# example. The one-line lookback reads only the line directly above
|
|
283
|
+
# the example, which is the interleave — so the annotation is out of
|
|
284
|
+
# reach and every line of the run is dead.
|
|
285
|
+
#
|
|
286
|
+
# All three are reported loudly, not skipped — the same stance
|
|
281
287
|
# {AnnotationScanner} takes for NO_PAYLOAD.
|
|
282
288
|
#
|
|
283
289
|
# A comment-form annotation line is a comment-only line
|
|
@@ -323,6 +329,13 @@ module SpecGuard
|
|
|
323
329
|
"claim it. Move it onto its example's `it` line, or to the comment line directly " \
|
|
324
330
|
"above that `it`"
|
|
325
331
|
|
|
332
|
+
UNREACHABLE_SEPARATED_ANNOTATION =
|
|
333
|
+
"unreachable annotation: this @intent: is separated from its example by an " \
|
|
334
|
+
"intervening comment or blank line, and the one-line lookback (SPGD-12 §2) claims " \
|
|
335
|
+
"only the line directly above the example — so this annotation is silently " \
|
|
336
|
+
"discarded at extraction. Move it directly above the example (or merge it into " \
|
|
337
|
+
"the line that is)"
|
|
338
|
+
|
|
326
339
|
# A comment-only line carrying an `@intent:` token — the only form an
|
|
327
340
|
# example on the NEXT line may claim ({AnnotationLookup::COMMENT_LINE}).
|
|
328
341
|
COMMENT_INTENT_LINE = /\A\s*#.*@intent:/
|
|
@@ -337,20 +350,34 @@ module SpecGuard
|
|
|
337
350
|
# spec/fixtures/): there is no lookback there to silently discard
|
|
338
351
|
# anything, so there is nothing to report. The defects this pass exists
|
|
339
352
|
# for are annotations an author believed were attached to a real
|
|
340
|
-
# example: stacked lines above it (SPGD-897),
|
|
341
|
-
# a group line directly above it (SPGD-1510)
|
|
353
|
+
# example: stacked lines above it (SPGD-897), the trailing form on
|
|
354
|
+
# a group line directly above it (SPGD-1510), and a comment-form run
|
|
355
|
+
# separated from it by one blank/comment line (SPGD-1554).
|
|
342
356
|
EXAMPLE_LINE = /\A\s*(?:it|specify)\b/
|
|
343
357
|
|
|
358
|
+
# A blank (whitespace-only) line — one of the two interleaves the
|
|
359
|
+
# separated pass reads. `\n` is whitespace, so a `text.lines` element
|
|
360
|
+
# that is only its newline matches.
|
|
361
|
+
BLANK_LINE = /\A\s*\z/
|
|
362
|
+
|
|
363
|
+
# A comment-only line (`#`-leading). The separated pass reads it as the
|
|
364
|
+
# interleave only when it carries no `@intent:` token (a comment-form
|
|
365
|
+
# `@intent:` line is part of the run itself, never an interleave).
|
|
366
|
+
COMMENT_ONLY_LINE = /\A\s*#/
|
|
367
|
+
|
|
344
368
|
# @param text [String] source of one file
|
|
345
369
|
# @param file [String] path to record on each Finding
|
|
346
370
|
# @return [Array<Finding>] the structural findings for one file, in line
|
|
347
371
|
# order: one per comment-form `@intent:` line in a stacked run (see
|
|
348
|
-
# {stacked_findings_in_text})
|
|
349
|
-
#
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
#
|
|
353
|
-
#
|
|
372
|
+
# {stacked_findings_in_text}), one per line of a separated run (see
|
|
373
|
+
# {separated_findings_in_text}) plus one per group line carrying the
|
|
374
|
+
# trailing form (see {group_line_findings_in_text}). The three passes
|
|
375
|
+
# are disjoint by construction: the stacked pass needs an example
|
|
376
|
+
# line directly after the run, the separated pass needs a blank or
|
|
377
|
+
# comment-only line there (one slot cannot be both), and the group
|
|
378
|
+
# pass reads only lines that are not comment-only — so the merge
|
|
379
|
+
# cannot double-flag a line, and the sort restores the file-then-line
|
|
380
|
+
# order {unreachable_findings} promises.
|
|
354
381
|
def unreachable_findings_in_text(text, file:)
|
|
355
382
|
# A file that is not valid UTF-8 is reported once, loudly, by the
|
|
356
383
|
# backend as a read failure; nothing positional can be said about it,
|
|
@@ -358,6 +385,7 @@ module SpecGuard
|
|
|
358
385
|
return [] unless text.valid_encoding?
|
|
359
386
|
|
|
360
387
|
(stacked_findings_in_text(text, file: file) +
|
|
388
|
+
separated_findings_in_text(text, file: file) +
|
|
361
389
|
group_line_findings_in_text(text, file: file)).sort_by(&:line)
|
|
362
390
|
end
|
|
363
391
|
|
|
@@ -399,6 +427,52 @@ module SpecGuard
|
|
|
399
427
|
findings
|
|
400
428
|
end
|
|
401
429
|
|
|
430
|
+
# @param text [String] source of one file
|
|
431
|
+
# @param file [String] path to record on each Finding
|
|
432
|
+
# @return [Array<Finding>] one per comment-form `@intent:` line of a
|
|
433
|
+
# SEPARATED run — a maximal run of consecutive comment-form `@intent:`
|
|
434
|
+
# lines whose following line (the interleave) is blank or a comment
|
|
435
|
+
# without an `@intent:` token AND whose line after that is an example
|
|
436
|
+
# ({EXAMPLE_LINE}). The one-line lookback reads only the interleave,
|
|
437
|
+
# so NOTHING in the run is claimed: every line is flagged, the run's
|
|
438
|
+
# last line included — the discriminating difference from
|
|
439
|
+
# {stacked_findings_in_text}, which spares the last line. The window
|
|
440
|
+
# is exactly one line: a second interleave, a code interleave, a
|
|
441
|
+
# `describe` target or a run at end of file is not flagged.
|
|
442
|
+
def separated_findings_in_text(text, file:)
|
|
443
|
+
# Same UTF-8 contract as the stacked pass above.
|
|
444
|
+
return [] unless text.valid_encoding?
|
|
445
|
+
|
|
446
|
+
lines = text.lines
|
|
447
|
+
i = 0
|
|
448
|
+
findings = []
|
|
449
|
+
|
|
450
|
+
while i < lines.length
|
|
451
|
+
unless COMMENT_INTENT_LINE.match?(lines[i])
|
|
452
|
+
i += 1
|
|
453
|
+
next
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
run_start = i
|
|
457
|
+
i += 1 while i < lines.length && COMMENT_INTENT_LINE.match?(lines[i])
|
|
458
|
+
run_end = i # exclusive; lines[run_end] is the interleave candidate
|
|
459
|
+
|
|
460
|
+
between = lines[run_end]
|
|
461
|
+
example = lines[run_end + 1]
|
|
462
|
+
next if between.nil? || example.nil?
|
|
463
|
+
next unless BLANK_LINE.match?(between) ||
|
|
464
|
+
(COMMENT_ONLY_LINE.match?(between) && !COMMENT_INTENT_LINE.match?(between))
|
|
465
|
+
next unless EXAMPLE_LINE.match?(example)
|
|
466
|
+
|
|
467
|
+
(run_start...run_end).each do |j|
|
|
468
|
+
findings << Finding.new(file: file, line: j + 1, problem: UNREACHABLE_SEPARATED_ANNOTATION,
|
|
469
|
+
kind: Finding::KIND_UNREACHABLE)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
findings
|
|
474
|
+
end
|
|
475
|
+
|
|
402
476
|
# RSpec's example-group keywords, at the start of a line and optionally
|
|
403
477
|
# `RSpec.`-prefixed. A line matching this hosts a GROUP, not an example
|
|
404
478
|
# — and {AnnotationLookup}'s extraction can claim an annotation only
|
|
@@ -450,9 +524,9 @@ module SpecGuard
|
|
|
450
524
|
# @return [Array<Finding>] one per example-group line carrying an
|
|
451
525
|
# `@intent:` token, for lines that define no example of their own
|
|
452
526
|
# ({EXAMPLE_ON_LINE}). Comment-only lines are left to
|
|
453
|
-
# {stacked_findings_in_text}
|
|
454
|
-
#
|
|
455
|
-
# line ITSELF).
|
|
527
|
+
# {stacked_findings_in_text} and {separated_findings_in_text}: a
|
|
528
|
+
# comment above a group is a different shape, deliberately out of
|
|
529
|
+
# scope here (SPGD-1510 flags the group line ITSELF).
|
|
456
530
|
def group_line_findings_in_text(text, file:)
|
|
457
531
|
# Same UTF-8 contract as the stacked pass above.
|
|
458
532
|
return [] unless text.valid_encoding?
|
data/lib/specguard/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: specguard-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- specguard Agent
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|