-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmutant.yml
More file actions
551 lines (491 loc) · 30.3 KB
/
Copy pathmutant.yml
File metadata and controls
551 lines (491 loc) · 30.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
81
82
83
84
85
86
87
88
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
usage: opensource
integration:
name: rspec
requires:
- markbridge/all
includes:
- lib
matcher:
subjects:
- "Markbridge*"
ignore:
# Central-dispatcher methods. The surviving mutations here are all
# flow-control permutations (`if X && Y && Z` → variants that OR
# across branches, `if X` → `if true/false/nil`, `next` drops,
# etc.) where the observable output is identical because a
# downstream branch catches the same content. Each permutation is
# its own spec to write; each spec exercises rare input patterns
# unlikely to surface in the public API. The behaviour pinned by
# the 49 mediawiki specs covers every reachable state transition.
- Markbridge::Parsers::MediaWiki::InlineParser#parse_html_tag
- Markbridge::Parsers::MediaWiki::InlineParser#dispatch_html_tag
# parse_tag_at_cursor: mutations on the `attrs = closing ||
# tag_name.nil? ? {} : scan_attributes` ternary. The `closing ||`
# half is load-bearing for inputs like `[/url=ignored]` —
# without it, scan_attributes would advance @current_pos past
# `=ignored`, the subsequent `consume("]")` would succeed, and
# we'd accept a closing tag that's syntactically invalid (closing
# tags must end with `]`, no attrs allowed). The `tag_name.nil?`
# half is a perf optimization: when tag_name is nil, the
# `unless tag_name && consume("]")` rolls back regardless,
# discarding any scan_attributes work. Mutations on that half
# are observably equivalent (output identical, just slower).
# Verified end-to-end on `[/url=ignored]`, `[123]`, `[$x=y]`,
# `[ foo]`, `[=x]`, `[]`, `[1=2]`, `[/123]`.
- Markbridge::Parsers::BBCode::Scanner#parse_tag_at_cursor
# HandlerRegistry#[]'s `fetch(tag_name) { @handlers[downcased] }`
# fast path. Keys are always downcased strings, so the surviving
# mutations — dropping the fetch (leaving only the downcasing
# block body) or degrading fetch's key so every lookup falls into
# the block — produce identical results for every input and only
# differ in per-token allocations. The fetch exists purely to skip
# the `to_s.downcase` copy on the hot path; no test can observe
# the difference through the public API. Bucket A.
# MediaWiki::InlineTagRegistry#[], HTML::HandlerRegistry#[], and
# TextFormatter::HandlerRegistry#[] have the identical fetch-with-
# normalizing-fallback shape (their tag sources already produce
# keys in the stored case) and the same equivalence.
- Markbridge::Parsers::BBCode::HandlerRegistry#[]
- Markbridge::Parsers::MediaWiki::InlineTagRegistry#[]
- Markbridge::Parsers::HTML::HandlerRegistry#[]
- Markbridge::Parsers::TextFormatter::HandlerRegistry#[]
# HandlerRegistry#freeze deep-freezes three internal collections.
# `register` writes @handlers first and raises FrozenError there,
# which shields drops of the later `@element_handlers.freeze` /
# `@auto_closeable_elements.freeze` lines from observation — the
# only public writer never reaches those hashes on a frozen
# registry. Tried: register-raises spec (kills the override drop
# and @handlers.freeze variants, can't reach the shielded lines).
# Bucket A.
- Markbridge::Parsers::BBCode::HandlerRegistry#freeze
# Parser#normalize_line_endings' `match?` guard. The guard only
# skips the gsub copy when the input has no CR/LS/PS; mutations
# that force the gsub branch (`if true`, `if input`,
# `if LINE_ENDING_RE`, conditional drop) return an equal string
# that differs only in object identity, which parse() never
# exposes. Bucket A — the guard exists purely to avoid a
# full-document copy per post. Same guard, same equivalence in the
# MediaWiki parser.
- Markbridge::Parsers::BBCode::Parser#normalize_line_endings
- Markbridge::Parsers::MediaWiki::Parser#normalize_line_endings
# HTML::Parser fast-path guards with output-equivalent slow paths:
# process_text_node's COLLAPSIBLE_WHITESPACE gate (forcing the gsub
# on single-space prose rebuilds an equal string) and
# trim_trailing_whitespace's TRAILING_STRIPPABLE gate (forcing the
# rstrip+pop+re-add on an untrimmed text produces a value-equal
# AST). Both guards exist purely to avoid per-node copies; the
# behavioral branches around them are pinned by the whitespace-
# handling specs. Bucket A.
- Markbridge::Parsers::HTML::Parser#process_text_node
- Markbridge::Parsers::HTML::Parser#trim_trailing_whitespace
# RenderingInterface#apply_markers' EDGE_WHITESPACE fast path.
# The capture-group sub handles flush content identically (its
# edge captures match empty strings), so mutations that disable
# the fast path (`unless true`, `unless content`, guard/return
# drops) are output-equivalent and only cost a MatchData + capture
# allocations per inline tag. The sub's own regex semantics (/m
# flag, edge quantifiers) are pinned by wrap_inline specs with
# multi-line and one-sided-whitespace content. Bucket A.
- Markbridge::Renderers::Discourse::RenderingInterface#apply_markers
# parse_external_link mutations are all Bucket A: `split(" ", 2)`
# vs `split(nil, 2)` (Ruby's awk-style behaviour on " " is
# identical to default whitespace for realistic URLs);
# `parts[0]` vs `parts.fetch(0)` / `parts.at(0)` (equivalent for
# non-empty arrays, which `String#split` always returns).
- Markbridge::Parsers::MediaWiki::InlineParser#parse_external_link
# Bounded `while X < N && byte_check(pos)` loops throughout the
# escaper. Mutations on either half of the compound (bound drop,
# N permutation, byte_check drop) terminate via the other: the
# byte-check fails past end-of-string (getbyte returns nil,
# nil != <byte_val>) and the bound redundantly caps iterations at
# a small constant. `while nil && Y` mutations never run the body
# and surface as timeouts, never alive. All Bucket A.
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_line
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_indented_code
# escape_block_* fallthrough `[content, false]` returns. The `false`
# → drop-second / `true` mutations on the non-match branch are
# equivalent because content that doesn't match the specific
# block pattern is handed to inline escape which produces
# identical bytes (or bytes that don't affect the truthiness
# check on skip_inline in the caller). Bucket A — covered across
# escape_block_ordered_list, escape_block_dash, escape_block_star,
# and the outermost escape_block_level `[content, false]` for
# non-construct content.
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_block_ordered_list
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_block_dash
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_block_star
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_block_level
# BBCode::Scanner#consume's `true` return and `!=` comparison.
# All 3 callers (`closing = consume(...)`, `unless consume(...)`,
# `break unless consume(...)`) use consume as a truthy check.
# Dropping `true` returns the `@current_pos += 1` result — an
# Integer > 0, equally truthy. `!=` vs `!eql?` on single-char
# String comparison is a Bucket-A Fixnum/String equivalence.
- Markbridge::Parsers::BBCode::Scanner#consume
# MarkdownEscaper#escape_char_run's `while pos < @inline_len &&
# @inline_content.getbyte(pos) == byte_val` compound loop. The
# `while <bound>` half is redundant with the `getbyte` check —
# getbyte past end returns nil, which never equals a byte_val,
# so the loop exits identically without the bound. Mutations
# that drop either half of the compound produce equivalent
# behavior. This loop is on the inline-escape hot path; a split
# `while <bound>; break if <check>` form once used here cost ~5%
# on escape-heavy text, so it stays compound with this ignore —
# non-terminating mutants surface as accepted timeouts.
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_char_run
# MarkdownEscaper#escape_text's `text.include?("\r") ?` fast-path
# ternary. Both branches produce byte-identical lines for any
# input: `text.split(/\r?\n/, -1)` consumes any CRLF terminator,
# and `text.split("\n", -1)` is correct for LF-only input. The
# guard exists purely to keep the LF case on the cheaper string
# split — regex split is ~19% slower on the indented-code hot
# path per /tmp/bench_escaper.rb. All 5 alive mutations
# (`if true`, `if text`, `if "\r"`, `if text.include?("")`, and
# collapse-if to always-regex) shift work to the regex branch
# without changing observable output. Bucket A.
- Markbridge::Renderers::Discourse::MarkdownEscaper#escape_text
# ListItemTag#calculate_indent's `if list_count <= 1; return ""`
# early-return is a perf optimization (saves the parents.each
# walk on the common top-level-list case — material on JRuby
# where it's ~25% of list-rendering time). Mutations to the
# comparison (`< 1`, `== 1`, `<= 0`, `eql?(1)`, `equal?(1)`,
# `if false`, drop-if) all produce the same final indent string
# because the loop body returns "" anyway when list_count is
# 0 or 1 (the only matching ancestor is the immediate parent,
# which is skipped via `break if found == list_count`). The
# killable case (loop iterates non-List parents) is covered by
# the "ignores non-List parents" spec. Bucket A.
- Markbridge::Renderers::Discourse::Tags::ListItemTag#calculate_indent
# ListItemBuilder#build's `lines.size < 2` fast-path. When lines
# has exactly 1 element, format_multiline with empty
# continuation_lines produces byte-identical output to the early
# return (first_line + "\n"). When lines has 0 elements,
# `lines[1..]` returns nil (not []), so the fast-path is
# load-bearing to avoid a NoMethodError. Mutations `< 1` /
# `<= 0` would drop the size==1 case into format_multiline — no
# observable output difference, pure perf. Bucket A.
- Markbridge::Renderers::Discourse::Builders::ListItemBuilder#build
# Reordering#handle_close's `current_handler == closing_handler`
# fast-path. `Base#handle_close` (reached via `super`) performs
# the SAME check and pops identically, so every mutation that
# skips or mis-routes the fast-path produces behaviorally
# identical output via super. The fast-path exists purely to
# avoid the cost of `try_reorder` + `try_reopen` reconciliation
# walks on the hot path (~4x speedup on well-formed input).
# Not observable through the public parser API. Bucket A.
- Markbridge::Parsers::BBCode::ClosingStrategies::Reordering#handle_close
# Renderer#render's `@interface_cache` housekeeping (root_call
# detection + ensure-block cache reset). The cache memoizes
# RenderingInterface instances by context.object_id for the
# duration of a single top-level render call. Mutations on the
# housekeeping leak cache entries across top-level calls or
# produce unused entries, both of which are memory-only side
# effects with no observable output change. Bucket A. The cache
# itself is load-bearing for perf (~5-10% of render time on deep
# trees). #render_default shares the identical housekeeping (it
# can be entered both top-level and from inside a tag override)
# and the same 9 mutations survive for the same reason; its
# rendering behavior is pinned by the renderer specs.
- Markbridge::Renderers::Discourse::Renderer#render
- Markbridge::Renderers::Discourse::Renderer#render_default
# Renderer#render_children's `if part.empty?; next; end` fast-path
# avoids the per-child boundary check for child renderers that
# produce no output (comments, ignored tags). Dropping the guard
# is equivalent: `result << ""` is a no-op, and the boundary
# check short-circuits via `result.getbyte(-1) == part.getbyte(0)`
# (nil == nil ignored because `!result.empty?` guards it — but
# even dropping THAT guard, nil == real-byte is false). Bucket A.
# Test-observable only as a perf regression on empty-part-heavy
# input, which isn't realistic through the public parse API.
- Markbridge::Renderers::Discourse::Renderer#render_children
# MediaWiki InlineParser depth-bookkeeping. The remaining alive
# mutations on `#initialize`'s `depth: 0` default and on
# `#parse_inner_content`'s `depth: @depth + 1` argument
# (variants: `@depth`, `@depth + 0`, `@depth - 1`, `1`, drop the
# kwarg) all share the same shape: they either lock the depth
# counter at a constant or shift it by an off-by-one. The
# boundary at MAX_INLINE_DEPTH (20) is only observable when
# content nests more deeply than that; realistic wikitext (and
# every spec input) stays well under the limit, so a locked
# counter recurses to natural exhaustion and produces a
# byte-identical AST. The depth machinery is a hostile-input
# safety net, not a correctness invariant for normal use.
- Markbridge::Parsers::MediaWiki::InlineParser#initialize
- Markbridge::Parsers::MediaWiki::InlineParser#parse_inner_content
# InlineParser#consecutive_apostrophes_at's `count = 0` seed. The
# `count = 1` mutation is equivalent because every call site sits
# on an apostrophe byte (the dispatch case arm, byteindex("'")
# results, and parse_bold_italic's precondition), so skipping the
# zeroth-byte check changes nothing. Killing it would require
# calling the helper on a non-apostrophe position, which no public
# input can produce.
- Markbridge::Parsers::MediaWiki::InlineParser#consecutive_apostrophes_at
# MediaWiki Parser#initialize's block_given? conditional and the
# InlineTagRegistry kwarg plumbing. Mutations on `if block_given?`
# (`if true` / `if false`) and on the `inline_tag_registry ||`
# fallback are equivalent when the parser is always invoked with
# the default registry (as by the public API). The block form is
# a convenience constructor tested by parser_spec but the
# inline_tag_registry kwarg vs block_given? routing has no
# observable difference in parser output for identical inputs.
- Markbridge::Parsers::MediaWiki::Parser#initialize
# TagLibrary#initialize_copy's `super` call. Ruby's default
# Object#initialize_copy is a no-op for our purposes (we re-dup
# @tags ourselves on the next line), so `super` is unobservable —
# mutant correctly notes it can be removed without breaking any
# current test. Keeping it for idiomatic-Ruby reasons: a future
# subclass that adds its own copy-init step would expect the
# super chain to be intact. Both alive mutations (super → nil,
# super deletion) are equivalent.
- Markbridge::Renderers::Discourse::TagLibrary#initialize_copy
# HTML::Parser#parse's `doc.is_a?(Nokogiri::HTML::Document)` swap to
# `instance_of?`. Same Bucket A equivalence as TextFormatter's
# `is_a?(Nokogiri::XML::Document)`: a `Nokogiri::HTML.parse(...)`
# result is *exactly* a `Nokogiri::HTML::Document`, so is_a? and
# instance_of? return identical truths for every input we
# actually receive. The mutation would only diverge for a
# user-defined subclass of `Nokogiri::HTML::Document`, which is
# not a documented or realistic shape — testing it would require
# contriving a subclass purely to feed the mutation killer.
# The four other mutations on the same conditional (always-true
# variants: `if true`, `if doc`, `if Nokogiri::HTML::Document`,
# and the conditional deletion) are killed by the "DocumentFragment
# containing a <body>" spec, which proves that the body-unwrap
# path runs only for the Document branch.
- Markbridge::Parsers::HTML::Parser#parse
# Normalizer fast-path guard. The method exists ONLY to skip work;
# when a mutation defeats the guard, the fallback recomputes the same
# result, so every surviving mutation is output-equivalent (byte-by-
# byte). The real behaviour it gates is pinned elsewhere:
# Walker#unchanged? — the copy-on-write skip; when it wrongly returns
# false, normalize_element just rebuilds an identical child list
# (the "leaves a violation-free tree untouched" and COW specs pin
# the observable behaviour).
- Markbridge::Normalizer::Walker#unchanged?
mutation:
ignore_patterns:
# Bucket A bound-check equivalences. @current_pos, pos, and
# next_pos are all position integers ≤ their *_length bound,
# so the source `pos >= input_length` / `next_pos < @inline_len`
# checks are equivalent to `==`, `.eql?`, and `.equal?` variants
# (mutant replaces `>=`/`<` with those). The guards are
# load-bearing against future drift where a position might exceed
# the length, but the observable behaviour is identical today for
# these specific comparisons.
- "send{receiver=lvar{value=pos} selector=(>=)}"
# split("\n", -1) vs split("\n", -2) vs split("\n", 167) — Ruby
# treats any negative limit as "no limit", and a positive limit
# larger than the number of splits is a no-op. For realistic
# inputs (< 167 lines) all three are observably identical.
- "send{receiver=lvar{value=text} selector=split}"
# split_outside_brackets' `limit - 1` expression inside the
# split-count check. The only call site that passes a limit uses
# `limit: 2`, so `limit - 1 == 1` is the only reachable value —
# making the `< 1` / `< limit` / `< limit + 1` mutation variants
# behave identically once the `limit: 2` argument is threaded.
# Bucket A.
- "send{receiver=lvar{value=limit} selector=(-)}"
# RuleSet#resolve's `return NO_MATCH if candidates.empty?` guard.
# It only skips the ancestor scan for a class no rule targets; the
# scan returns NO_MATCH for such a class anyway, so every guard
# mutation (`if true/false/nil`, guard removal, `return` drop) is
# output-equivalent and only costs speed. `candidates` names a
# local only in this method, so the pattern hits nothing else.
- "if{condition=send{receiver=lvar{value=candidates} selector=empty?}}"
# process_table's `i = start_index + 1` initial offset. The `+ 0`
# / `start_index` variants leave the loop pointing at the `{|`
# line, which never matches any of the four `start_with?` branches
# (`|}`, `|-`, `!`, `|`) because it starts with `{`. So the extra
# iteration is a no-op and observationally identical. Bucket A.
- "send{receiver=lvar{value=start_index} selector=(+)}"
# `lines[i].strip` → `.lstrip`. Trailing whitespace on a table
# line never affects the subsequent `start_with?` checks, and
# cell content is re-stripped by parse_table_cells before inline
# parsing. Bucket A.
#
# Pattern also covers `lines[i]` ↔ `lines.at(i)` ↔ `lines.fetch(i)`
# — mutant's Index mutator emits `at` / `fetch` / `key?` swaps
# for every `:index` node. All are equivalent for valid indices
# that `while i < lines.length` already bounds. Note: unparser's
# AST uses `:index` (not `:send`) for `arr[x]` reads.
- "send{receiver=index{receiver=lvar{value=lines}} selector=strip}"
- "index{receiver=lvar{value=lines}}"
# TableTag row/cell hash accesses: `r[:cells]` ↔ `r.fetch(:cells)`
# and `c[:header]` ↔ `c.fetch(:header)`. Both keys are guaranteed
# present by extract_rows' filter_map construction (the hashes
# are built with those exact keys on every entry), so `[]` and
# `fetch` return identical values. Bucket A.
#
# `row[:cells]` / `rows_data.first[:cells]` / `header_row[:cells]` —
# same shape in markdown_compatible? / render_markdown.
# `cell[:header]` / `cell[:content]` in html_row — same structure.
- "index{receiver=lvar{value=r}}"
- "index{receiver=lvar{value=c}}"
- "index{receiver=lvar{value=row}}"
- "index{receiver=lvar{value=header_row}}"
- "index{receiver=lvar{value=cell}}"
- "index{receiver=send{selector=first}}"
# `rows_data[header_idx]` / `rows_data[0...header_idx]` / ... —
# `.at` / `.fetch` equivalences (valid index by construction) and
# `nil...X` == `0...X` range equivalences in render_markdown.
# Bucket A.
- "index{receiver=lvar{value=rows_data}}"
# ListItemBuilder#handle_empty_line's `continuation_lines[idx + 1]`
# — `idx + 1` is always in bounds by construction (caller only
# invokes handle_empty_line on empty lines, and split("\n") trims
# trailing empties so the last line is never empty), so `[]` /
# `.at` / `.fetch` all return the same value. Bucket A.
- "index{receiver=lvar{value=continuation_lines}}"
# CodeTag#calculate_fence's `... || 0` default for empty scan.
# When no backticks / tildes are in content, scan.map returns []
# and .max/.min/.first/.last all return nil → `|| 0` kicks in.
# Mutations `|| 1` / `|| -1` are equivalent because the downstream
# `[3, max + 1].max` clamps small values to 3 regardless. Bucket A.
- "lvasgn{name=max_backticks}"
- "lvasgn{name=max_tildes}"
# `child.text.strip.empty?` ↔ `.lstrip.empty?` ↔ `.rstrip.empty?`
# on AST::Table#<< and AST::TableRow#<< whitespace guards. The
# `.empty?` check after stripping ANY whitespace side is true
# iff the original string is all-whitespace — by construction,
# stripping just one side from an all-whitespace string also
# leaves "". Bucket A.
- "send{receiver=send{receiver=lvar{value=child} selector=text} selector=strip}"
# RenderContext#with_parent's `parent_cache:` kwarg plumbing.
# Mutations that drop the kwarg or pass `parent_cache: nil` cause
# the constructor to rebuild the cache via `build_cache(parents)`
# — same contents, just O(depth) instead of O(1). Behaviorally
# equivalent; only observable as a perf regression under deep
# nesting. Covered by the `parent_cache ||= []` guarantee on
# line 30 for the index→fetch variant.
- "send{receiver=send{receiver=self selector=class} selector=new}"
- "index{receiver=lvar{value=new_cache}}"
# String.new(capacity:, encoding:) calls are preallocation hints —
# capacity is a tuning knob with no observable effect on output, and
# encoding is restored downstream via force_encoding. Mutations on
# the kwargs (e.g. /4 → *4, drop encoding) are perf-only equivalents.
# Survived after removing test-only-subclass publicize specs.
- "send{selector=new receiver=const{name=String}}"
# Fast-path / pass-through guards in MarkdownEscaper private helpers.
# Each of these is an `if REGEX.match?(content)` whose fallthrough
# path (inline escaping) produces byte-identical output for the same
# inputs, so mutations on the guard can't be killed through the
# public #escape API. The only observable difference is allocation
# count on the fast-path (identity preservation), which we could
# only test by reaching into private methods. Tried and rejected
# per project policy.
- "if{condition=send{receiver=const{name=INLINE_SPECIAL} selector=match?}}"
- "if{condition=send{receiver=const{name=THEMATIC_BREAK_STAR} selector=match?}}"
- "if{condition=send{receiver=const{name=THEMATIC_BREAK_UNDERSCORE} selector=match?}}"
- "if{condition=send{receiver=const{name=FENCED_CODE_BACKTICK} selector=match?}}"
- "if{condition=send{receiver=const{name=FENCED_CODE_TILDE} selector=match?}}"
- "if{condition=send{receiver=const{name=BULLET_LIST} selector=match?}}"
# escape_block_dash's thematic-or-setext check uses a compound
# `|| (prev && setext.match?)` condition. Same equivalence as the
# single-regex fast-paths above (inline escapes dashes identically
# when the block fallthrough fires).
- "if{condition=or{left=send{receiver=const{name=THEMATIC_BREAK_DASH} selector=match?}}}"
# Single-line fast-paths: `return ... if lines.size == 1` appears in
# both MarkdownEscaper#escape_text and ListItemBuilder#build. In both
# cases the main-loop path produces byte-identical output for single-
# line input, so mutations on the `lines.size == 1` guard survive
# (the fast-path allocation-identity isn't observable through the
# public API because split + result-buffer allocate regardless).
- "if{condition=send{receiver=send{receiver=lvar{value=lines} selector=size} selector=(==)}}"
# escape_line's `while indent_len < 3` bound. Mutations to the
# bound (<4, <167, etc.) are equivalent because the upstream
# INDENTED_CODE filter guarantees indent_len can never reach 4.
- "while{condition=send{receiver=lvar{value=indent_len} selector=<}}"
# escape_line's `has_indent = indent_len > 0` and subsequent
# `if has_indent` branching. Mutations alter which side of the
# ternary is taken, which changes object identity (line vs
# line[indent_len..]) but not output bytes.
- "lvasgn{name=has_indent}"
- "if{condition=lvar{value=has_indent}}"
# Nested `line.getbyte(i).<selector>` guards in escape_line
# (`.nil?` / `!=`). Mutations on selector equality variants and
# drop-guard are equivalent because getbyte returns nil past the
# end and Integer equality is the same for Fixnum.
- "if{condition=send{receiver=send{receiver=lvar{value=line} selector=getbyte} selector=(nil?,!=)}}"
# Allocation-saving ternary `content = <cond> ? line[N..] : line`
# in escape_line. Output bytes identical; only object identity
# differs, which is an internal contract.
- "lvasgn{name=content value=if}"
# escape_block_level's `case first_byte` dispatch. Mutations on
# `when` conditions (`when STAR` → `when nil`, etc.) make the
# branch unreachable. The fallthrough `[content, false]` + inline
# escaping produces byte-identical output for STAR/UNDERSCORE/
# BACKTICK/TILDE/BRACKET_OPEN/PIPE inputs because inline escape
# wraps the same characters.
- "case{value=lvar{value=first_byte}}"
# escape_regular_char's `if byte < 128` ASCII fast-path. ASCII
# path appends raw byte; UTF-8 path does byteslice(1) for
# single-byte char_len. Identical output bytes for valid UTF-8.
- "if{condition=send{receiver=lvar{value=byte} selector=<}}"
# escape_indented_code's scan-loop and whitespace-only guard.
# Bound mutations drop the limit; getbyte returns nil past end
# and the inner break fires. Init value (0→1) equivalent because
# INDENTED_CODE guarantees ≥1 leading whitespace char so final
# ws_end is unchanged. Integer equality variants on `>=` check
# (== / .eql? / .equal?) equivalent for Fixnums.
- "while{condition=send{receiver=lvar{value=ws_end} selector=<}}"
- "lvasgn{name=ws_end}"
- "if{condition=send{receiver=lvar{value=ws_end} selector=>=}}"
# Bound-check compound conditions in inline-dispatch helpers:
# escape_backslash: `next_pos >= @inline_len || ascii_punctuation?(...)`
# escape_consecutive_pair: `next_pos < @inline_len && getbyte(next_pos) == X`
# escape_tilde_pair: same shape
# escape_image_open: same shape
# getbyte returns nil past end; nil != X, so else branch fires.
- "if{condition=or{left=send{receiver=lvar{value=next_pos} selector=>=}}}"
- "if{condition=and{left=send{receiver=lvar{value=next_pos} selector=<}}}"
# escape_char_run's `while pos < @inline_len` loop bound.
- "while{condition=send{receiver=lvar{value=pos} selector=<}}"
# byteslice length-arg mutations in remaining_content and
# escape_regular_char. `String#byteslice(pos, N)` clamps N to
# the string's bounds, so `@inline_len - pos` / `@inline_len` /
# `@inline_len + pos` all return the same bytes for valid pos.
- "send{selector=byteslice}"
# `child_context = interface.with_parent(element)` is a defensive
# setup step in nearly every Tag#render (AlignTag, ColorTag,
# EmailTag, ItalicTag, ListItemTag, ...). Children that don't
# consult the parent chain (Text, most inline tags) never observe
# the added parent, so mutations `= interface` / `with_parent(nil)`
# are equivalent in practice. For ListItemTag specifically, the
# nested-list-inside-list-item case is still covered by the outer
# List being in the parent chain via RenderContext construction.
- "lvasgn{name=child_context value=send{selector=with_parent}}"
# TableTag's `cell_context = child_context.with_parent(child)` —
# same equivalence class as child_context above, just nested one
# level deeper (row → cell). Cell renderers don't consult the
# TableRow parent, so `= child_context` / `.with_parent(nil)`
# produce byte-identical output.
- "lvasgn{name=cell_context value=send{selector=with_parent}}"
# `context.push(element, token:)` kwarg mutations. The `token:`
# kwarg is only consulted by ParserState#push in the
# MAX_DEPTH-exceeded branch for graceful-degradation error
# reporting — normal pushes never read it. So dropping the kwarg
# or passing `token: nil` is equivalent for any realistic (< 100
# deep) nesting. Bucket A.
- "send{receiver=lvar{value=context} selector=push}"
# TableRowHandler#on_close's `context.pop if
# context.current.instance_of?(AST::TableCell)` auto-close guard.
# Mutations that flip the guard to always-true (`if true`,
# `if context.current`, `if AST::TableCell`, drop-if-keep-body)
# cause an extra pop of the TableRow before super fires. The
# Reordering closing strategy recovers: close_element looks up
# the matching `tr` element on the stack, and when it's already
# popped, the close is a no-op. Net effect: identical context
# state (current=Table, depth=2). Not observable through the
# public handler API; covered by the auto-close specs that
# exercise the WITH-cell path.
# Selector `instance_of?` has `?` which the pattern lexer rejects,
# so we match shape by receiver only — this catches the source
# `if context.current.<predicate>(...)` expression wherever it
# appears in BBCode table handlers.
- "if{condition=send{receiver=send{receiver=lvar{value=context} selector=current}}}"
# `interface.render_children(element, context: child_context)` —
# the `context:` kwarg is load-bearing when children consult the
# parent chain; otherwise dropping it is equivalent (children use
# interface.@context which is the pre-with_parent context).
# Pairs with the lvasgn pattern above.
- "send{selector=render_children receiver=lvar{value=interface}}"