Module: SpecGuard::RSpec::AnnotationScanner
- Defined in:
- lib/specguard/rspec/annotation_scanner.rb
Overview
Finds @intent: annotations in test source and captures the object
literal that follows each one.
This is a syntactic pass only: it hands back the payload exactly as written (still in PROTOCOL.md §1's permissive syntax — see PayloadNormalizer) and never inspects its contents. Schema validation is a later stage entirely.
The scan is string-aware: quoted content is skipped over wholesale, so
a behavior sentence containing an apostrophe or an unbalanced { cannot
terminate the capture early. Plain brace-counting gets that case wrong.
Ported from open-test-intent's bin/validate-intent
(extract_intents / _scan_object / _scan_string), which is the
reference implementation of this algorithm.
Constant Summary collapse
- INTENT_TOKEN =
"@intent:"- OPENERS =
Closing bracket => the opener it must match.
{ "}" => "{", "]" => "[" }.freeze
- UNTERMINATED_OBJECT =
The
{atstartwas never closed on this line. "unterminated object literal (an annotation must fit on one line)"- UNBALANCED_BRACKETS =
"unbalanced brackets in the annotation payload"- NO_PAYLOAD =
"no '{...}' object literal follows the @intent: token"
Class Method Summary collapse
-
.each_intent(text) {|line_no, raw_payload, problem| ... } ⇒ Enumerator
Yields
[line_no, raw_payload, problem]for every@intent:token intext, in source order. -
.scan_object(text, start) ⇒ Object
Returns the index just past the
}matching the{attext[start]. -
.scan_string(text, start, quote) ⇒ Object
Returns the index just past the closing
quoteof a string literal.
Class Method Details
.each_intent(text) {|line_no, raw_payload, problem| ... } ⇒ Enumerator
Yields [line_no, raw_payload, problem] for every @intent: token in
text, in source order.
raw_payload is the object literal as written; it is nil when
problem explains why the annotation could not be captured.
An @intent: token carrying no extractable payload is reported, not
skipped — a typo'd annotation must fail loudly rather than silently
counting as "this example is unannotated".
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/specguard/rspec/annotation_scanner.rb', line 48 def each_intent(text) return enum_for(:each_intent, text) unless block_given? text.each_line.with_index(1) do |raw_line, line_no| line = raw_line.chomp pos = 0 loop do token_at = line.index(INTENT_TOKEN, pos) break if token_at.nil? brace_at = line.index("{", token_at + INTENT_TOKEN.length) if brace_at.nil? yield line_no, nil, NO_PAYLOAD break end begin finish = scan_object(line, brace_at) rescue ScanError => e yield line_no, nil, e. break end yield line_no, line[brace_at...finish], nil # Resume *after* the captured payload so trailing prose containing # another `@intent:` is still seen, but the payload's own contents # are never rescanned. pos = finish end end end |
.scan_object(text, start) ⇒ Object
Returns the index just past the } matching the { at text[start].
Bracket-balanced and string-aware, so a brace inside a quoted value does
not end the payload early. Annotations are single-line per PROTOCOL.md
§1, so text is one line.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/specguard/rspec/annotation_scanner.rb', line 89 def scan_object(text, start) stack = [] i = start length = text.length while i < length char = text[i] if char == '"' || char == "'" i = scan_string(text, i, char) next end if char == "{" || char == "[" stack.push(char) elsif char == "}" || char == "]" raise ScanError, UNBALANCED_BRACKETS if stack.empty? || stack.last != OPENERS[char] stack.pop return i + 1 if stack.empty? end i += 1 end raise ScanError, UNTERMINATED_OBJECT end |
.scan_string(text, start, quote) ⇒ Object
Returns the index just past the closing quote of a string literal.
text[start] must be the opening quote. Backslash escapes are honoured,
so a quote or brace inside the string never terminates the scan.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/specguard/rspec/annotation_scanner.rb', line 123 def scan_string(text, start, quote) i = start + 1 length = text.length while i < length char = text[i] if char == "\\" i += 2 # skip the escape and whatever it escapes next end return i + 1 if char == quote i += 1 end raise ScanError, "unterminated #{quote}-quoted string" end |