specguard-ruby 0.3.28 → 0.3.29

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58f8188803b68968be3289f01834d0f128f239b10d31162d34eeaaf55b6daefe
4
- data.tar.gz: 273ce5b5eb675b12b302e23c3b24e025d81e60e77b77476ba9c7401fd7742600
3
+ metadata.gz: 887fc6d71732b1947cc48c34f27c68ac5b9b71c729f604cf95c41e508cc8d6af
4
+ data.tar.gz: e2f20dbc68a7324cc34381e204bc8ac0f5db262002d86ffc3bd24c8c2cfb3663
5
5
  SHA512:
6
- metadata.gz: fcc5a85ecf595eeb62aae5446387ff74802579f97991c71b9b7a61014499c954d6fcc546171440dd3fde330b06ad11cc6fb2717b5f8f569aee884d51534c1f61
7
- data.tar.gz: 52ed4889c3074cb474b9a192603414ba11644c753ad6dc2a3d946579663bf0432bcd533702f36568d9c81259a4eaf3e7735102a5374379fbabe0a6b4f24b76e5
6
+ metadata.gz: e3d18c2b2d19c8d25b0c5a9277847767996ff27be06f7f5802b052c7a4dab7f20e7244214a339fc0a4bc0343103572ad842bd381f95ccd6efa670422da445edc
7
+ data.tar.gz: 9a2b14e04e71e6e957134e29ad38f11fdaad0aede17893907a048d9e6414d93affb7844f4a99e630194f5095c0bc945e271800871f39f71f325ed8b4805492ae
data/README.md CHANGED
@@ -49,8 +49,9 @@ bundle exec ruby -rminitest/specguard_plugin -e 'Minitest.extensions << "specgua
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
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
52
+ from its `it` by one blank or ordinary comment line, which puts it out of the lookback's reach;
53
+ directly above an `it` that carries its own trailing `@intent:`, which extraction prefers, so the
54
+ comment is shadowed; or trailing on a `describe`/`context` group line, which no example's extraction can ever reach) — and
54
55
  **never** fails on a *missing* one (adoption is opt-in and gradual).
55
56
 
56
57
  ```bash
@@ -336,8 +337,8 @@ Every way the backend can fail — the binary is missing, will not execute, exit
336
337
  is not a verdict, or emits output that is not a report — is **exit 2**, the linter's "could not do
337
338
  my job" code. It never becomes exit 1, which means "an annotation is malformed" — or well-formed
338
339
  but unreachable: stacked above another comment-form `@intent:` line, so the one-line lookback
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
+ never claims it, separated from its `it` by one blank or ordinary comment line, shadowed by its
341
+ `it`'s own trailing `@intent:`, or trailing on a `describe`/`context` group line, which no example's extraction can ever reach — and nothing else.
341
342
 
342
343
  ## The formatter — `SpecGuard::RSpecFormatter`
343
344
 
@@ -126,9 +126,10 @@ 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/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
129
+ # unreachable (SPGD-900/SPGD-1510/SPGD-1554/SPGD-1560: stacked above
130
+ # another comment-form `@intent:` line so the one-line lookback never
131
+ # claims it, separated from its example by one blank/comment line,
132
+ # shadowed by its example's own trailing `@intent:`, or trailing on
132
133
  # an example-group line no example's extraction can reach — a dead
133
134
  # contract in every case, and still a failure). The only code produced by
134
135
  # inspecting content, and the only path that reaches it is a failed
@@ -168,13 +169,15 @@ module SpecGuard
168
169
 
169
170
  results = backend.check(selection.files)
170
171
 
171
- # The structural pass (SPGD-900, SPGD-1510, SPGD-1554): annotations
172
+ # The structural pass (SPGD-900, SPGD-1510, SPGD-1554, SPGD-1560): annotations
172
173
  # that are valid in isolation but can never be extracted — stacked
173
174
  # consecutive comment-form `@intent:` lines, where the one-line
174
175
  # lookback (SPGD-12 §2) claims only the line just above the example,
175
176
  # 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
177
+ # line, comment-form lines shadowed by their example's own trailing
178
+ # `@intent:` (extraction is own-line-first), and `@intent:` tokens
179
+ # trailing on an example-group line, which no extraction rule can ever
180
+ # reach. These are Linter::Results like any
178
181
  # other, so both renderers and the exit code pick them up with no
179
182
  # second path to keep in step.
180
183
  results += unreachable_results(selection.files)
@@ -482,15 +485,15 @@ module SpecGuard
482
485
  #
483
486
  # The boundary is the set-difference the partition above already
484
487
  # supports: a file counts as covered when it yielded any line-scoped
485
- # result (an annotation, valid or not — the SPGD-900/SPGD-1510/SPGD-1554
488
+ # result (an annotation, valid or not — the SPGD-900/SPGD-1510/SPGD-1554/SPGD-1560
486
489
  # unreachable findings are line-scoped too and require an annotation to
487
490
  # exist, so they cover their file) or when it could not be read at all
488
491
  # (KIND_READ → `unread`). An unread file is NOT a zero-annotation file:
489
492
  # it already gets its own summary clause and FAIL line, and calling it
490
493
  # annotation-free would be the same overstatement `summary_line` refuses
491
494
  # to make. Unreachable findings need an annotation to exist — a stacked
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
495
+ # comment-form pair, a separated comment-form run, a shadowed comment-form
496
+ # run, or an `@intent:` token on a group line — impossible in a file with none, so `annotations ∪ unread` already covers every
494
497
  # non-bare file and no third subtraction is needed.
495
498
  #
496
499
  # 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
- # Three shapes, all dead the moment they are written:
52
+ # Four 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 —
@@ -61,7 +61,10 @@ module SpecGuard
61
61
  # it, and the example beneath it silently ingests unannotated;
62
62
  # * a comment-form `@intent:` run separated from its example by ONE
63
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.
64
+ # reads only that interleave, so the whole run is out of reach;
65
+ # * a comment-form `@intent:` directly above an example whose OWN line
66
+ # also carries a trailing `@intent:` (SPGD-1560): extraction is
67
+ # own-line-first, so the example's own annotation shadows the comment.
65
68
  #
66
69
  # Whichever shape, the contract is dead metadata: counted by the linter,
67
70
  # discarded by extraction. Produced by {Scanner}'s structural pass, not
@@ -40,15 +40,17 @@ 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. Three shapes reach this kind: a comment-form
43
+ # therefore also exits 1. Four shapes reach this kind: a comment-form
44
44
  # `@intent:` stacked above another comment-form `@intent:` line (SPGD-900),
45
45
  # an `@intent:` trailing on an example-group line, which is no
46
46
  # example's own and not comment-only (SPGD-1510), and a comment-form
47
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
48
+ # comment line (SPGD-1554), and a comment-form `@intent:` directly above an
49
+ # example whose own line carries a trailing `@intent:`, which own-line-first
50
+ # extraction prefers (SPGD-1560). Nothing about any of them is malformed; the
49
51
  # contract is dead metadata, counted by the linter and discarded by
50
52
  # extraction, and the structural pass that flags them (SPGD-900,
51
- # SPGD-1510, SPGD-1554) reports it loudly rather than letting it pass as
53
+ # SPGD-1510, SPGD-1554, SPGD-1560) reports it loudly rather than letting it pass as
52
54
  # clean.
53
55
  module Linter
54
56
  # 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. Three shapes fall outside both and are dead the
264
+ # IMMEDIATELY ABOVE it. Four 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
  #
@@ -283,7 +283,15 @@ module SpecGuard
283
283
  # the example, which is the interleave — so the annotation is out of
284
284
  # reach and every line of the run is dead.
285
285
  #
286
- # All three are reported loudly, not skipped — the same stance
286
+ # * the SHADOWED annotation (SPGD-1560, ported from specguard-ts
287
+ # SPGD-1556): a comment-form `@intent:` run directly above an example
288
+ # whose OWN line also carries a trailing `@intent: {`. Extraction is
289
+ # own-line-first ({AnnotationLookup::Index#intent_for} returns the
290
+ # example's own annotation before it ever consults the line above),
291
+ # so the comment is shadowed and its LAST line is dead. (Any earlier
292
+ # lines of the run are the stacked arm's.)
293
+ #
294
+ # All four are reported loudly, not skipped — the same stance
287
295
  # {AnnotationScanner} takes for NO_PAYLOAD.
288
296
  #
289
297
  # A comment-form annotation line is a comment-only line
@@ -336,6 +344,12 @@ module SpecGuard
336
344
  "discarded at extraction. Move it directly above the example (or merge it into " \
337
345
  "the line that is)"
338
346
 
347
+ UNREACHABLE_SHADOWED_ANNOTATION =
348
+ "unreachable annotation: the example directly below carries its own trailing @intent:, " \
349
+ "and extraction is own-line-first (SPGD-12 §2), so this comment-form annotation is " \
350
+ "shadowed and silently discarded. Delete this comment, or merge it into the trailing " \
351
+ "payload"
352
+
339
353
  # A comment-only line carrying an `@intent:` token — the only form an
340
354
  # example on the NEXT line may claim ({AnnotationLookup::COMMENT_LINE}).
341
355
  COMMENT_INTENT_LINE = /\A\s*#.*@intent:/
@@ -370,14 +384,18 @@ module SpecGuard
370
384
  # @return [Array<Finding>] the structural findings for one file, in line
371
385
  # order: one per comment-form `@intent:` line in a stacked run (see
372
386
  # {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.
387
+ # {separated_findings_in_text}), one per run whose example carries its own
388
+ # trailing annotation (see {own_line_shadow_findings_in_text}) plus one
389
+ # per group line carrying the trailing form (see
390
+ # {group_line_findings_in_text}). The four passes are disjoint by
391
+ # construction: the stacked pass flags all but the LAST line of a run
392
+ # when an example directly follows, the shadow pass flags ONLY that last
393
+ # line (and only when the example also carries `@intent: {`), the
394
+ # separated pass needs a blank or comment-only line after the run (one
395
+ # slot cannot be both that and an example), and the group pass reads
396
+ # only lines that are not comment-only — so the merge cannot double-flag
397
+ # a line, and the sort restores the file-then-line order
398
+ # {unreachable_findings} promises.
381
399
  def unreachable_findings_in_text(text, file:)
382
400
  # A file that is not valid UTF-8 is reported once, loudly, by the
383
401
  # backend as a read failure; nothing positional can be said about it,
@@ -386,6 +404,7 @@ module SpecGuard
386
404
 
387
405
  (stacked_findings_in_text(text, file: file) +
388
406
  separated_findings_in_text(text, file: file) +
407
+ own_line_shadow_findings_in_text(text, file: file) +
389
408
  group_line_findings_in_text(text, file: file)).sort_by(&:line)
390
409
  end
391
410
 
@@ -473,6 +492,44 @@ module SpecGuard
473
492
  findings
474
493
  end
475
494
 
495
+ # @param text [String] source of one file
496
+ # @param file [String] path to record on each Finding
497
+ # @return [Array<Finding>] one per maximal run of comment-form
498
+ # `@intent:` lines whose following line is an example
499
+ # ({EXAMPLE_LINE}) that ALSO carries a trailing `@intent: {`
500
+ # ({INTENT_WITH_PAYLOAD}) — reported on the run's LAST line, the one
501
+ # the lookback would otherwise claim. Extraction is own-line-first
502
+ # (SPGD-12 §2), so the example's own annotation always wins and that
503
+ # comment is dead. Earlier lines of a longer run are the stacked arm's
504
+ # ({stacked_findings_in_text}); each dead line is flagged exactly once.
505
+ def own_line_shadow_findings_in_text(text, file:)
506
+ # Same UTF-8 contract as the stacked pass above.
507
+ return [] unless text.valid_encoding?
508
+
509
+ lines = text.lines
510
+ i = 0
511
+ findings = []
512
+
513
+ while i < lines.length
514
+ unless COMMENT_INTENT_LINE.match?(lines[i])
515
+ i += 1
516
+ next
517
+ end
518
+
519
+ i += 1 while i < lines.length && COMMENT_INTENT_LINE.match?(lines[i])
520
+ run_end = i # exclusive; lines[run_end] is the line after the run
521
+
522
+ example = lines[run_end]
523
+ next if example.nil?
524
+ next unless EXAMPLE_LINE.match?(example) && INTENT_WITH_PAYLOAD.match?(example)
525
+
526
+ findings << Finding.new(file: file, line: run_end, problem: UNREACHABLE_SHADOWED_ANNOTATION,
527
+ kind: Finding::KIND_UNREACHABLE)
528
+ end
529
+
530
+ findings
531
+ end
532
+
476
533
  # RSpec's example-group keywords, at the start of a line and optionally
477
534
  # `RSpec.`-prefixed. A line matching this hosts a GROUP, not an example
478
535
  # — and {AnnotationLookup}'s extraction can claim an annotation only
@@ -7,5 +7,5 @@
7
7
  # framework-scoped version constant a lie, so the number moved here and the
8
8
  # rspec module now points at it for back-compat.
9
9
  module SpecGuard
10
- VERSION = "0.3.28"
10
+ VERSION = "0.3.29"
11
11
  end
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.28
4
+ version: 0.3.29
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-30 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json