Skip to main content

ocomment_core/
scanner.rs

1use crate::{
2    ByteSpan, Comment, CommentKind, Diagnostic, Dialect, Disposition, DispositionExplanation,
3    Language, Policy, ScanOptions, ScanReport, Severity,
4};
5use memchr::{memchr, memchr2, memchr3, memmem};
6use regex::bytes::RegexSet;
7use std::cmp::Ordering;
8
9/// Scan options with their policy regular expressions compiled once.
10///
11/// A scanner can be shared between threads. Embedded language scanners (HTML
12/// script/style bodies, Markdown fences, Vue and Svelte regions) reuse the
13/// same compiled pattern sets as their parent.
14#[derive(Clone, Debug)]
15pub struct PreparedScanner {
16    pub(crate) options: ScanOptions,
17    pub(crate) patterns: DispositionPatterns,
18    pattern_error: Option<String>,
19}
20
21impl PreparedScanner {
22    /// Compile the policy patterns in `options` for reuse across any number of
23    /// source files.
24    pub fn new(options: ScanOptions) -> Result<Self, regex::Error> {
25        let patterns = DispositionPatterns::compile(&options)?;
26        Ok(Self {
27            options,
28            patterns,
29            pattern_error: None,
30        })
31    }
32
33    /// The effective options this scanner was prepared from.
34    pub fn options(&self) -> &ScanOptions {
35        &self.options
36    }
37
38    /// Scan one source without recompiling its policy regular expressions.
39    pub fn scan(&self, source: &[u8], language: Language) -> ScanReport {
40        scan_prepared_internal(source, language, self, 0, false, None).0
41    }
42
43    pub(crate) fn lossy(options: ScanOptions) -> Self {
44        match DispositionPatterns::compile(&options) {
45            Ok(patterns) => Self {
46                options,
47                patterns,
48                pattern_error: None,
49            },
50            Err(error) => Self {
51                options,
52                patterns: DispositionPatterns::empty(),
53                pattern_error: Some(error.to_string()),
54            },
55        }
56    }
57}
58
59/// Find every comment in `source` and decide what happens to each.
60///
61/// One pass over the bytes, with the source never decoded as a whole: the
62/// spans that come back are byte offsets into `source`, which does not have
63/// to be valid UTF-8. Nothing is written and no output is built — that is
64/// [`transform`](crate::transform).
65///
66/// A source that will not lex, such as one with an unterminated comment or
67/// string, comes back with a [`Severity::Error`](crate::Severity::Error)
68/// diagnostic and [`ScanReport::valid`] false.
69///
70/// # Examples
71///
72/// ```
73/// use ocomment_core::{CommentKind, Language, ScanOptions, scan};
74///
75/// let report = scan(b"let x = 1; // note\n", Language::Rust, ScanOptions::default());
76/// assert!(report.valid);
77/// assert_eq!(report.comments.len(), 1);
78/// assert_eq!(report.comments[0].kind, CommentKind::Line);
79/// assert!(report.comments[0].disposition.is_remove());
80///
81/// // A build tag is a directive, and the default policy keeps one.
82/// let tagged = scan(b"//go:build linux\n", Language::Go, ScanOptions::default());
83/// assert_eq!(tagged.comments[0].kind, CommentKind::Directive);
84/// assert!(!tagged.comments[0].disposition.is_remove());
85/// ```
86pub fn scan(source: &[u8], language: Language, options: ScanOptions) -> ScanReport {
87    scan_internal(source, language, options, 0, false, None).0
88}
89
90#[cfg(test)]
91pub(crate) fn scan_with_checkpoints(
92    source: &[u8],
93    language: Language,
94    options: ScanOptions,
95    offset: usize,
96) -> (ScanReport, Vec<usize>) {
97    let (report, checkpoints, _) = scan_internal(source, language, options, offset, true, None);
98    (report, checkpoints)
99}
100
101pub(crate) fn scan_with_checkpoints_prepared(
102    source: &[u8],
103    language: Language,
104    prepared: &PreparedScanner,
105    offset: usize,
106) -> (ScanReport, Vec<usize>) {
107    let (report, checkpoints, _) =
108        scan_prepared_internal(source, language, prepared, offset, true, None);
109    (report, checkpoints)
110}
111
112/// Scan `source` — which must be the *whole* remaining document suffix, so that
113/// every lexical lookahead sees the bytes a full scan would — but stop as soon
114/// as the scanner reaches the clean top-level state at absolute offset `stop`.
115///
116/// Returns whether that state was actually reached. When it was not, the scan
117/// ran to the end of `source` and the report covers the entire suffix. Handing
118/// the scanner a slice that had been truncated at `stop` instead would let a
119/// bounded lookahead straddling the cut decide differently than it does in the
120/// real document.
121pub(crate) fn scan_until_checkpoint_prepared(
122    source: &[u8],
123    language: Language,
124    prepared: &PreparedScanner,
125    offset: usize,
126    stop: usize,
127) -> (ScanReport, Vec<usize>, bool) {
128    scan_prepared_internal(source, language, prepared, offset, true, Some(stop))
129}
130
131/// Every safe checkpoint a scan of `source` offers, paired with the watermark
132/// standing when it was offered — one past the furthest byte any decision made
133/// before it had read.
134///
135/// Test-only, and the whole of what [`Reach`] is for: the checkpoint-soundness
136/// property asserts the mechanism directly rather than inferring it from a
137/// rescan that may re-lex the same bytes by luck.
138#[cfg(test)]
139pub(crate) fn scan_checkpoint_watermarks(
140    source: &[u8],
141    language: Language,
142    options: ScanOptions,
143) -> Vec<(usize, usize)> {
144    let mut scanner = Scanner::with_offset(source, language, options, 0, true, None);
145    scanner.scan_language();
146    scanner
147        .safe_checkpoints
148        .iter()
149        .copied()
150        .zip(scanner.checkpoint_watermarks)
151        .collect()
152}
153
154fn scan_prepared_internal(
155    source: &[u8],
156    language: Language,
157    prepared: &PreparedScanner,
158    offset: usize,
159    track_checkpoints: bool,
160    stop: Option<usize>,
161) -> (ScanReport, Vec<usize>, bool) {
162    finish_scan(Scanner::with_prepared(
163        source,
164        language,
165        prepared,
166        offset,
167        track_checkpoints,
168        stop,
169    ))
170}
171
172fn scan_internal(
173    source: &[u8],
174    language: Language,
175    options: ScanOptions,
176    offset: usize,
177    track_checkpoints: bool,
178    stop: Option<usize>,
179) -> (ScanReport, Vec<usize>, bool) {
180    finish_scan(Scanner::with_offset(
181        source,
182        language,
183        options,
184        offset,
185        track_checkpoints,
186        stop,
187    ))
188}
189
190#[inline]
191fn finish_scan(mut scanner: Scanner<'_>) -> (ScanReport, Vec<usize>, bool) {
192    let language = scanner.language;
193    scanner.scan_language();
194    debug_assert!(
195        scanner.comments.windows(2).all(|comments| {
196            comments[0].span.start < comments[1].span.start
197                && comments[0].span.end <= comments[1].span.start
198        }),
199        "{language} scanner returned duplicate, unordered, or overlapping comments: {:?}",
200        scanner.comments,
201    );
202    let valid = !scanner
203        .diagnostics
204        .iter()
205        .any(|diagnostic| diagnostic.severity == Severity::Error);
206    (
207        ScanReport {
208            language,
209            comments: scanner.comments,
210            diagnostics: scanner.diagnostics,
211            valid,
212        },
213        scanner.safe_checkpoints,
214        scanner.stopped,
215    )
216}
217
218/// One past the furthest byte a lookahead read, in the coordinates of the
219/// slice it was handed.
220///
221/// INVARIANT: a safe checkpoint promises that nothing decided before it depends
222/// on bytes at or after it, and [`Scanner::add_safe_checkpoint`] keeps that
223/// promise by refusing any position an earlier decision already read through.
224/// That is a mechanism rather than an audit only while every *lookahead*
225/// reports how far it went: a helper takes `&mut Reach`, records each byte as
226/// it consults it — a `get` that came back `None` included, because deciding
227/// that the document ends there is a decision about that byte just the same —
228/// and its caller folds the result into the scan with [`Scanner::consult`].
229///
230/// NOTE: a plain forward scan is not a lookahead and reports nothing: the index
231/// consumes every byte it reads, so a restart inside the region reaches the
232/// same answer for what is left of it. What has to be reported is the read that
233/// the scan then *rewinds* behind — a delimiter parse that gives up and lexes
234/// the same bytes again, an unbounded search for a closing token that fails —
235/// because the bytes past the resume point have already decided something no
236/// rescan from a later checkpoint would revisit.
237#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
238struct Reach(usize);
239
240impl Reach {
241    /// Record a read of every byte below `end`.
242    fn through(&mut self, end: usize) {
243        self.0 = self.0.max(end);
244    }
245
246    /// Record a read of the byte at `index`.
247    fn byte(&mut self, index: usize) {
248        self.through(index + 1);
249    }
250
251    /// Record a search that ran off the end of `bytes`.
252    ///
253    /// One byte further than the document holds, because what such a search
254    /// decided out of is not only every byte it crossed but the end itself —
255    /// and an append is an edit exactly there. A checkpoint at the end of the
256    /// document is the one an append reuses the whole prefix from, so it is
257    /// the one this has to withdraw.
258    fn end_of(&mut self, bytes: &[u8]) {
259        self.through(bytes.len() + 1);
260    }
261}
262
263/// One past the last byte a bounded window of `width` bytes at `index` can
264/// consult: the windows that tell a character literal from something else stop
265/// at the first line terminator, and read that terminator to know they must.
266fn line_bounded_reach(bytes: &[u8], index: usize, width: usize) -> usize {
267    let limit = (index + width).min(bytes.len());
268    let window = &bytes[index.min(limit)..limit];
269    window
270        .iter()
271        .position(|byte| is_line_terminator(*byte))
272        .map_or(limit, |stop| index + stop + 1)
273}
274
275struct Scanner<'a> {
276    source: &'a [u8],
277    language: Language,
278    options: ScanOptions,
279    comments: Vec<Comment>,
280    diagnostics: Vec<Diagnostic>,
281    offset: usize,
282    patterns: DispositionPatterns,
283    safe_checkpoints: Vec<usize>,
284    track_checkpoints: bool,
285    stop: Option<usize>,
286    stopped: bool,
287    /// One past the furthest byte any decision so far consulted, in the local
288    /// coordinates every index here is in. See [`Reach`].
289    consulted: usize,
290    /// The watermark as each safe checkpoint was offered, in the order
291    /// `safe_checkpoints` holds them. Test-only: it is what lets the
292    /// checkpoint-soundness property assert the mechanism itself, instead of
293    /// trusting that a rescan happens to re-lex whatever a lookahead read.
294    #[cfg(test)]
295    checkpoint_watermarks: Vec<usize>,
296    restart_rules: RestartRules,
297    /// Every YAML block scalar the scan walked over, in source order. Empty
298    /// for every other language, and for the YAML documents — nearly all of
299    /// them — that hold no block scalar at all.
300    yaml_blocks: Vec<YamlBlockScalar>,
301}
302
303impl<'a> Scanner<'a> {
304    fn with_offset(
305        source: &'a [u8],
306        language: Language,
307        options: ScanOptions,
308        offset: usize,
309        track_checkpoints: bool,
310        stop: Option<usize>,
311    ) -> Self {
312        let (patterns, pattern_error) = match DispositionPatterns::compile(&options) {
313            Ok(patterns) => (patterns, None),
314            Err(error) => (DispositionPatterns::empty(), Some(error.to_string())),
315        };
316        Self::with_owned(
317            source,
318            language,
319            PreparedScanner {
320                options,
321                patterns,
322                pattern_error,
323            },
324            offset,
325            track_checkpoints,
326            stop,
327        )
328    }
329
330    fn with_prepared(
331        source: &'a [u8],
332        language: Language,
333        prepared: &PreparedScanner,
334        offset: usize,
335        track_checkpoints: bool,
336        stop: Option<usize>,
337    ) -> Self {
338        Self::with_owned(
339            source,
340            language,
341            prepared.clone(),
342            offset,
343            track_checkpoints,
344            stop,
345        )
346    }
347
348    fn with_owned(
349        source: &'a [u8],
350        language: Language,
351        prepared: PreparedScanner,
352        offset: usize,
353        track_checkpoints: bool,
354        stop: Option<usize>,
355    ) -> Self {
356        let PreparedScanner {
357            options,
358            patterns,
359            pattern_error,
360        } = prepared;
361        let mut scanner = Self {
362            source,
363            language,
364            options,
365            comments: Vec::new(),
366            diagnostics: Vec::new(),
367            offset,
368            patterns,
369            safe_checkpoints: track_checkpoints
370                .then_some(vec![offset])
371                .unwrap_or_default(),
372            track_checkpoints,
373            stop,
374            stopped: false,
375            consulted: 0,
376            #[cfg(test)]
377            checkpoint_watermarks: track_checkpoints
378                .then_some(vec![offset])
379                .unwrap_or_default(),
380            restart_rules: RestartRules::of(source, language),
381            yaml_blocks: Vec::new(),
382        };
383        if let Some(error) = pattern_error.as_deref() {
384            scanner.error(
385                "invalid-policy-regex",
386                &format!("invalid comment policy regex: {error}"),
387                ByteSpan::new(0, 0),
388            );
389        }
390        scanner
391    }
392
393    fn child(
394        source: &'a [u8],
395        language: Language,
396        options: ScanOptions,
397        patterns: DispositionPatterns,
398        offset: usize,
399    ) -> Self {
400        Self {
401            source,
402            language,
403            options,
404            comments: Vec::new(),
405            diagnostics: Vec::new(),
406            offset,
407            patterns,
408            safe_checkpoints: Vec::new(),
409            track_checkpoints: false,
410            stop: None,
411            stopped: false,
412            consulted: 0,
413            #[cfg(test)]
414            checkpoint_watermarks: Vec::new(),
415            restart_rules: RestartRules::of(source, language),
416            yaml_blocks: Vec::new(),
417        }
418    }
419
420    /// Run the scanner for its own language.
421    fn scan_language(&mut self) {
422        match self.language {
423            Language::Css if self.options.dialect == Dialect::Sass => self.scan_sass(),
424            Language::Rust
425            | Language::C
426            | Language::Cpp
427            | Language::Go
428            | Language::Kotlin
429            | Language::Css
430            | Language::Jsonc => self.scan_c_family(),
431            Language::Java => self.scan_java(),
432            Language::JavaScript | Language::TypeScript => self.scan_javascript(),
433            Language::Ocaml => self.scan_ocaml(),
434            Language::Python => self.scan_python(),
435            Language::Shell => self.scan_shell(),
436            Language::Html => self.scan_html(),
437            Language::Sql => self.scan_sql(),
438            Language::Toml => self.scan_toml(),
439            Language::Lua => self.scan_lua(),
440            Language::Yaml => self.scan_yaml(),
441            Language::Php => self.scan_php(),
442            Language::Ruby => self.scan_ruby(),
443            Language::Zig => self.scan_zig(),
444            Language::R => self.scan_r(),
445            Language::Dart => self.scan_dart(),
446            Language::Swift => self.scan_swift(),
447            Language::CSharp => self.scan_csharp(),
448            Language::Scala => self.scan_scala(),
449            Language::Vue => self.scan_vue(),
450            Language::Svelte => self.scan_svelte(),
451            Language::Markdown => self.scan_markdown(),
452            Language::Perl => self.scan_perl(),
453            Language::Unknown => self.error(
454                "unknown-language",
455                "a language is required",
456                ByteSpan::new(0, 0),
457            ),
458        }
459    }
460
461    /// Fold what a lookahead read into the scan's watermark.
462    ///
463    /// Clamped to the source, so that a `get` past the last byte does not
464    /// withdraw the checkpoint a trailing line terminator earns.
465    fn consult(&mut self, reach: Reach) {
466        self.consulted = self.consulted.max(reach.0.min(self.source.len() + 1));
467    }
468
469    fn error(&mut self, code: &str, message: &str, span: ByteSpan) {
470        let start = span.start.min(self.source.len());
471        let end = span.end.max(start).min(self.source.len());
472        self.diagnostics.push(Diagnostic {
473            code: code.into(),
474            message: message.into(),
475            severity: Severity::Error,
476            span: ByteSpan::new(start + self.offset, end + self.offset),
477        });
478    }
479
480    fn add_comment(&mut self, start: usize, end: usize, lexical_kind: CommentKind) {
481        let start = start.min(self.source.len());
482        let end = end.max(start).min(self.source.len());
483        let kind = classify_comment(
484            self.source,
485            self.language,
486            lexical_kind,
487            start,
488            end,
489            self.offset,
490        );
491        let raw = &self.source[start..end];
492        let disposition = disposition(kind, &self.options, raw, &self.patterns);
493        self.comments.push(Comment {
494            span: ByteSpan::new(start + self.offset, end + self.offset),
495            kind,
496            disposition,
497        });
498    }
499
500    fn merge_child(&mut self, child: Scanner<'_>) {
501        self.comments.extend(child.comments);
502        self.diagnostics.extend(child.diagnostics);
503    }
504
505    /// Whether a checkpoint the scanner is about to emit at `local` is a
506    /// restart point, asked of the same [`RestartRules`] the incremental engine
507    /// consults before reusing one. A suffix scan is past the preamble by
508    /// construction — its source starts mid-document, so the offset-sensitive
509    /// rules cannot fire for it, and the engine validates the offset it
510    /// restarts *from* against the whole edited document instead.
511    ///
512    /// The block scalar rule is asked of a suffix scan as well, because it is
513    /// not about where the offset sits in a document: it is about what the
514    /// bytes it is being asked of hold, and the suffix holds its own.
515    fn checkpoint_is_restartable(&self, local: usize) -> bool {
516        local <= self.restart_rules.first_block_scalar
517            && (self.offset > 0 || self.restart_rules.permit_restart_at(self.source, local))
518            && (self.restart_rules.language != Language::Scala
519                || the_scala_xml_boundary_permits_a_restart(self.source, local))
520            && (!matches!(
521                self.restart_rules.language,
522                Language::Html | Language::Vue | Language::Svelte
523            ) || the_tag_boundary_permits_a_restart(self.source, local))
524    }
525
526    /// Offer `local` as a restart point, if it may stand as one.
527    ///
528    /// INVARIANT: a checkpoint promises that nothing decided before it depends
529    /// on bytes at or after it, so it may only stand where no earlier decision
530    /// read past it. That is what the watermark is for: a position below what
531    /// [`Reach`] has already recorded is refused outright, rather than left to
532    /// an audit of which lookahead reaches how far. Refusing one costs a
533    /// rescan the chance to start there; keeping an unsound one corrupts the
534    /// rescan silently.
535    fn add_safe_checkpoint(&mut self, local: usize) {
536        if !self.track_checkpoints
537            || local < self.consulted
538            || !self.checkpoint_is_restartable(local)
539        {
540            return;
541        }
542        let absolute = self.offset + local;
543        if self.safe_checkpoints.last().copied() != Some(absolute) {
544            self.safe_checkpoints.push(absolute);
545            #[cfg(test)]
546            self.checkpoint_watermarks
547                .push(self.offset + self.consulted);
548        }
549        if self.stop == Some(absolute) {
550            self.stopped = true;
551        }
552    }
553
554    fn add_safe_newlines(&mut self, mut start: usize, end: usize) {
555        if !self.track_checkpoints {
556            return;
557        }
558        while start < end && !self.stopped {
559            let Some(relative) = memchr2(b'\r', b'\n', &self.source[start..end]) else {
560                break;
561            };
562            let newline = start + relative;
563            let next = consume_newline(self.source, newline).min(end);
564            self.add_safe_checkpoint(next);
565            start = next;
566        }
567    }
568
569    fn scan_c_family(&mut self) {
570        /* INVARIANT: Translation phase 2 line splicing is significant to C-family lexical
571         * input. The remapped copy is scanned by a child, which tracks no
572         * checkpoints — that is the document-wide half of the restart rules, and
573         * it is the same answer the incremental engine gets from them. */
574        if !self.restart_rules.splicing_permits_restarts {
575            let mapped = MappedBytes::without_c_line_splices(self.source);
576            let mut child = Scanner::child(
577                &mapped.bytes,
578                self.language,
579                self.options.clone(),
580                self.patterns.clone(),
581                0,
582            );
583            child.scan_c_family_unmapped();
584            self.merge_mapped(child, &mapped);
585        } else {
586            self.scan_c_family_unmapped();
587        }
588    }
589
590    fn scan_c_family_unmapped(&mut self) {
591        let bytes = self.source;
592        let mut index = 0;
593        while index < bytes.len() {
594            let Some(next) =
595                next_c_family_trigger(bytes, index, self.language, self.options.dialect)
596            else {
597                self.add_safe_newlines(index, bytes.len());
598                break;
599            };
600            self.add_safe_newlines(index, next);
601            if self.stopped {
602                break;
603            }
604            index = next;
605            if starts(bytes, index, b"//")
606                && !(self.language == Language::Css && self.options.dialect != Dialect::Scss)
607            {
608                let end = line_end(bytes, index + 2);
609                self.add_comment(index, end, line_kind(bytes, index));
610                index = end;
611                continue;
612            }
613            if starts(bytes, index, b"/*") {
614                let nested = matches!(self.language, Language::Rust | Language::Kotlin);
615                let (end, closed) = block_end(bytes, index, b"/*", b"*/", nested);
616                self.add_comment(index, end, block_kind(bytes, index));
617                if !closed {
618                    self.error(
619                        "unterminated-comment",
620                        "unterminated block comment",
621                        ByteSpan::new(index, end),
622                    );
623                }
624                index = end;
625                continue;
626            }
627            if self.language == Language::Css && self.options.dialect == Dialect::Scss {
628                if starts(bytes, index, b"#{") {
629                    index = self.scan_scss_interpolation(index + 2, 0);
630                    continue;
631                }
632                if let Some(end) = self.scss_url_end(index, 0) {
633                    index = end;
634                    continue;
635                }
636            }
637            if let Some(end) = self.special_c_string(index) {
638                index = end;
639                continue;
640            }
641            index += 1;
642        }
643    }
644
645    /// The indentation-based Sass syntax.
646    ///
647    /// Its strings, URLs, interpolation and explicit block comments use the
648    /// same lexical rules as SCSS. A silent `//` comment on an otherwise blank
649    /// prefix additionally owns every following non-blank line indented more
650    /// deeply than the comment line. Keeping that body in one span is
651    /// important: treating a `#` or `/*` in a picture line as source would
652    /// make a removal rewrite bytes the Sass parser never exposes as code.
653    fn scan_sass(&mut self) {
654        let bytes = self.source;
655        let mut index = 0;
656        while index < bytes.len() && !self.stopped {
657            if starts(bytes, index, b"//") {
658                let end = self.sass_silent_comment_end(index);
659                self.add_comment(index, end, line_kind(bytes, index));
660                index = end;
661                continue;
662            }
663            if starts(bytes, index, b"/*") {
664                let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
665                self.add_comment(index, end, block_kind(bytes, index));
666                if !closed {
667                    self.error(
668                        "unterminated-comment",
669                        "unterminated Sass block comment",
670                        ByteSpan::new(index, end),
671                    );
672                }
673                index = end;
674                continue;
675            }
676            if starts(bytes, index, b"#{") {
677                index = self.scan_scss_interpolation(index + 2, 0);
678                continue;
679            }
680            if let Some(end) = self.scss_url_end(index, 0) {
681                index = end;
682                continue;
683            }
684            if matches!(bytes[index], b'"' | b'\'') {
685                index = self.scan_scss_string(index, 0);
686                continue;
687            }
688            if matches!(bytes[index], b'\r' | b'\n') {
689                index = consume_newline(bytes, index);
690                self.add_safe_checkpoint(index);
691            } else {
692                index += 1;
693            }
694        }
695    }
696
697    fn sass_silent_comment_end(&self, start: usize) -> usize {
698        let bytes = self.source;
699        let line_start = line_start(bytes, start);
700        if bytes[line_start..start]
701            .iter()
702            .any(|byte| !matches!(byte, b' ' | b'\t'))
703        {
704            return line_end(bytes, start + 2);
705        }
706        let base_indent = sass_indent_width(&bytes[line_start..start]);
707        let first_end = line_end(bytes, start + 2);
708        let mut included_end = first_end;
709        let mut next = if first_end < bytes.len() {
710            consume_newline(bytes, first_end)
711        } else {
712            return first_end;
713        };
714        while next < bytes.len() {
715            let finish = line_end(bytes, next);
716            let mut content = next;
717            while content < finish && matches!(bytes[content], b' ' | b'\t') {
718                content += 1;
719            }
720            let blank = content == finish || matches!(bytes.get(content), Some(b'\r' | b'\n'));
721            if !blank && sass_indent_width(&bytes[next..content]) <= base_indent {
722                break;
723            }
724            included_end = finish;
725            if finish >= bytes.len() {
726                break;
727            }
728            next = consume_newline(bytes, finish);
729        }
730        included_end
731    }
732
733    /// The byte after an SCSS/Sass `url(...)`. CSS white space around the
734    /// value, quoted values and escapes are part of the function token; an
735    /// interpolation inside either value form is code.
736    fn scss_url_end(&mut self, index: usize, depth: usize) -> Option<usize> {
737        if !starts_ascii_case(&self.source[index..], b"url(") {
738            return None;
739        }
740        if index > 0 && is_css_identifier_part(self.source[index - 1]) {
741            return None;
742        }
743        let bytes = self.source;
744        let mut cursor = index + 4;
745        while bytes.get(cursor).is_some_and(|byte| css_whitespace(*byte)) {
746            cursor += 1;
747        }
748        if matches!(bytes.get(cursor), Some(b'"' | b'\'')) {
749            cursor = self.scan_scss_string(cursor, depth);
750            while bytes.get(cursor).is_some_and(|byte| css_whitespace(*byte)) {
751                cursor += 1;
752            }
753            if bytes.get(cursor) == Some(&b')') {
754                return Some(cursor + 1);
755            }
756        }
757        while cursor < bytes.len() {
758            if bytes[cursor] == b')' {
759                return Some(cursor + 1);
760            }
761            if bytes[cursor] == b'\\' {
762                cursor = (cursor + 2).min(bytes.len());
763                continue;
764            }
765            if starts(bytes, cursor, b"#{") {
766                cursor = self.scan_scss_interpolation(cursor + 2, depth + 1);
767                continue;
768            }
769            cursor += 1;
770        }
771        /* NOTE: the URL never closes, which is a file dart-sass rejects. The
772         * scan reads the rest of it as URL bytes rather than rewinding: the
773         * interpolation inside has already reported its comments, and a
774         * rewind would read them a second time. */
775        Some(bytes.len())
776    }
777
778    /// One SCSS `#{ ... }` interpolation, beginning past its opening brace.
779    ///
780    /// The braces are counted rather than searched for, because the
781    /// expression is code: a comment written there is a comment, and a string
782    /// or an unquoted URL written there is scanned as such. A line comment
783    /// runs to the end of its line while the interpolation carries on below.
784    fn scan_scss_interpolation(&mut self, mut index: usize, depth: usize) -> usize {
785        if depth > 256 {
786            self.error(
787                "nesting-limit",
788                "SCSS interpolation nesting limit exceeded",
789                ByteSpan::new(index, index),
790            );
791            return self.source.len();
792        }
793        let bytes = self.source;
794        let mut braces = 1usize;
795        while index < bytes.len() {
796            if starts(bytes, index, b"//") {
797                let end = line_end(bytes, index + 2);
798                self.add_comment(index, end, CommentKind::Line);
799                index = end;
800                continue;
801            }
802            if starts(bytes, index, b"/*") {
803                let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
804                self.add_comment(index, end, block_kind(bytes, index));
805                if !closed {
806                    self.error(
807                        "unterminated-comment",
808                        "unterminated SCSS block comment",
809                        ByteSpan::new(index, end),
810                    );
811                }
812                index = end;
813                continue;
814            }
815            if let Some(end) = self.scss_url_end(index, depth) {
816                index = end;
817                continue;
818            }
819            match bytes[index] {
820                b'"' | b'\'' => index = self.scan_scss_string(index, depth),
821                b'{' => {
822                    braces += 1;
823                    index += 1;
824                }
825                b'}' => {
826                    braces -= 1;
827                    index += 1;
828                    if braces == 0 {
829                        return index;
830                    }
831                }
832                _ => index += 1,
833            }
834        }
835        self.error(
836            "unterminated-interpolation",
837            "unterminated SCSS interpolation",
838            ByteSpan::new(index, index),
839        );
840        index
841    }
842
843    /// A Sass-family quoted string. Unlike a CSS string treated as an opaque
844    /// token, interpolation re-enters Sass code and may therefore contain real
845    /// comments. A backslash carries the following byte, including a line
846    /// terminator, exactly as the Sass tokenizer does.
847    fn scan_scss_string(&mut self, start: usize, depth: usize) -> usize {
848        let bytes = self.source;
849        let quote = bytes[start];
850        let mut index = start + 1;
851        while index < bytes.len() {
852            if bytes[index] == b'\\' {
853                index = (index + 2).min(bytes.len());
854                continue;
855            }
856            if bytes[index] == quote {
857                return index + 1;
858            }
859            if starts(bytes, index, b"#{") {
860                index = self.scan_scss_interpolation(index + 2, depth + 1);
861                continue;
862            }
863            index += 1;
864        }
865        self.error(
866            "unterminated-string",
867            "unterminated Sass string",
868            ByteSpan::new(start, bytes.len()),
869        );
870        bytes.len()
871    }
872
873    fn special_c_string(&mut self, index: usize) -> Option<usize> {
874        let bytes = self.source;
875        match self.language {
876            Language::Rust => {
877                if bytes[index] == b'"'
878                    && let Some((raw_start, hashes)) = rust_raw_start_at_quote(bytes, index)
879                {
880                    let content = index + 1;
881                    let mut end_token = Vec::with_capacity(hashes + 1);
882                    end_token.push(b'"');
883                    end_token.extend(std::iter::repeat_n(b'#', hashes));
884                    if let Some(relative) = find_subslice(&bytes[content..], &end_token) {
885                        return Some(content + relative + end_token.len());
886                    }
887                    self.error(
888                        "unterminated-string",
889                        "unterminated Rust raw string",
890                        ByteSpan::new(raw_start, bytes.len()),
891                    );
892                    return Some(bytes.len());
893                }
894                if bytes[index] == b'"' || (starts(bytes, index, b"b\"") && index + 1 < bytes.len())
895                {
896                    let quote = if bytes[index] == b'b' {
897                        index + 1
898                    } else {
899                        index
900                    };
901                    /* INVARIANT: a Rust string or byte-string literal carries a
902                     * bare newline as content, unlike its C, Go, and Java
903                     * cousins, so only the closing quote or the end of the file
904                     * ends one. A character literal below still ends at the
905                     * line, which is what keeps a lifetime from swallowing the
906                     * rest of the source. */
907                    return Some(self.quoted_or_error(quote, true, "string"));
908                }
909                if bytes[index] == b'\'' {
910                    let mut reach = Reach::default();
911                    let literal = rust_char_start(bytes, index, &mut reach);
912                    self.consult(reach);
913                    if literal {
914                        return Some(self.quoted_or_error(index, false, "character literal"));
915                    }
916                    /* NOTE: What is left is an apostrophe this window read as
917                     * no literal, and nothing on its line says whether it
918                     * opens one. A Rust identifier is `XID_Start
919                     * XID_Continue*` (Rust Reference, Identifiers) and has
920                     * been since 1.53, so `'ä` is as good a lifetime or loop
921                     * label as `'a` -- `fn f<'ä>() {}` and `'ä: loop {}` both
922                     * compile -- and an unterminated non-ASCII character
923                     * literal is spelled the same way within one line. `rustc`
924                     * tells the two apart in the parser, which is where E0762
925                     * is raised; this scanner is a lexer with a line-bounded
926                     * window and cannot. So it reports neither: over-keeping a
927                     * comment is the safe direction, calling a valid file
928                     * invalid is not. */
929                }
930            }
931            Language::C | Language::Cpp => {
932                let raw = (self.language == Language::Cpp && bytes[index] == b'"')
933                    .then(|| cpp_raw_start_at_quote(bytes, index))
934                    .flatten()
935                    .and_then(|raw_start| {
936                        let mut reach = Reach::default();
937                        let raw = cpp_raw_string(bytes, raw_start, &mut reach);
938                        self.consult(reach);
939                        raw.map(|(end, closed)| (raw_start, end, closed))
940                    });
941                if let Some((raw_start, end, closed)) = raw {
942                    if !closed {
943                        self.error(
944                            "unterminated-string",
945                            "unterminated C++ raw string",
946                            ByteSpan::new(raw_start, end),
947                        );
948                    }
949                    return Some(end);
950                }
951                if is_c_quote_start(bytes, index) {
952                    let quote_index = if matches!(bytes[index], b'"' | b'\'') {
953                        index
954                    } else {
955                        (index..(index + 3).min(bytes.len()))
956                            .find(|i| matches!(bytes[*i], b'"' | b'\''))
957                            .unwrap_or(index)
958                    };
959                    return Some(self.quoted_or_error(
960                        quote_index,
961                        false,
962                        "string or character literal",
963                    ));
964                }
965            }
966            Language::Go => {
967                if bytes[index] == b'`' {
968                    return Some(self.delimited_or_error(index, b"`", "raw string"));
969                }
970                if matches!(bytes[index], b'"' | b'\'') {
971                    return Some(self.quoted_or_error(index, false, "string or rune literal"));
972                }
973            }
974            Language::Kotlin => {
975                if starts(bytes, index, b"\"\"\"") {
976                    return Some(self.scan_kotlin_string(index, true, 0));
977                }
978                if bytes[index] == b'"' {
979                    return Some(self.scan_kotlin_string(index, false, 0));
980                }
981                if bytes[index] == b'\'' {
982                    return Some(self.quoted_or_error(index, false, "Kotlin character literal"));
983                }
984            }
985            Language::Jsonc => {
986                /* NOTE: JSON5 4.4 writes a string with either quote, and this
987                 * language is `JSON with comments, including JSON5` — it owns
988                 * `.json5` as well as `.jsonc`. An apostrophe is already
989                 * invalid in the stricter dialect, so reading one as a string
990                 * only hides a `//` that the dialect could not have meant as a
991                 * comment. Both quotes report the one construct a reader
992                 * recognises, a JSON string. */
993                if matches!(bytes[index], b'"' | b'\'') {
994                    return Some(self.quoted_or_error(index, false, "JSON string"));
995                }
996            }
997            Language::Css => {
998                if matches!(bytes[index], b'"' | b'\'') {
999                    return Some(if self.options.dialect == Dialect::Scss {
1000                        self.scan_scss_string(index, 0)
1001                    } else {
1002                        self.quoted_or_error(index, true, "CSS string")
1003                    });
1004                }
1005            }
1006            _ => {}
1007        }
1008        None
1009    }
1010
1011    fn scan_kotlin_string(&mut self, start: usize, triple: bool, depth: usize) -> usize {
1012        if depth > 256 {
1013            self.error(
1014                "nesting-limit",
1015                "Kotlin string-template nesting limit exceeded",
1016                ByteSpan::new(start, start),
1017            );
1018            return self.source.len();
1019        }
1020        let bytes = self.source;
1021        let delimiter = if triple { b"\"\"\"".as_slice() } else { b"\"" };
1022        let dollars = kotlin_dollar_width(bytes, start);
1023        let mut index = start + delimiter.len();
1024        while index < bytes.len() {
1025            if starts(bytes, index, delimiter) {
1026                return if triple {
1027                    index + count_run(bytes, index, b'"')
1028                } else {
1029                    index + 1
1030                };
1031            }
1032            if !triple && bytes[index] == b'\\' {
1033                index = (index + 2).min(bytes.len());
1034            } else if bytes[index] == b'$' {
1035                let run = count_run(bytes, index, b'$');
1036                if run >= dollars && bytes.get(index + run) == Some(&b'{') {
1037                    index = self.scan_kotlin_expression(index + run + 1, depth + 1);
1038                } else {
1039                    index += run;
1040                }
1041            } else if !triple && matches!(bytes[index], b'\r' | b'\n') {
1042                self.error(
1043                    "unterminated-string",
1044                    "unterminated Kotlin string",
1045                    ByteSpan::new(start, index),
1046                );
1047                return index;
1048            } else {
1049                index += 1;
1050            }
1051        }
1052        self.error(
1053            "unterminated-string",
1054            if triple {
1055                "unterminated Kotlin triple-quoted string"
1056            } else {
1057                "unterminated Kotlin string"
1058            },
1059            ByteSpan::new(start, index),
1060        );
1061        index
1062    }
1063
1064    fn scan_kotlin_expression(&mut self, mut index: usize, depth: usize) -> usize {
1065        if depth > 256 {
1066            self.error(
1067                "nesting-limit",
1068                "Kotlin string-template nesting limit exceeded",
1069                ByteSpan::new(index, index),
1070            );
1071            return self.source.len();
1072        }
1073        let bytes = self.source;
1074        let mut braces = 1usize;
1075        while index < bytes.len() {
1076            if starts(bytes, index, b"//") {
1077                let end = line_end(bytes, index + 2);
1078                self.add_comment(index, end, line_kind(bytes, index));
1079                index = end;
1080                continue;
1081            }
1082            if starts(bytes, index, b"/*") {
1083                let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
1084                self.add_comment(index, end, block_kind(bytes, index));
1085                if !closed {
1086                    self.error(
1087                        "unterminated-comment",
1088                        "unterminated Kotlin block comment",
1089                        ByteSpan::new(index, end),
1090                    );
1091                }
1092                index = end;
1093                continue;
1094            }
1095            if starts(bytes, index, b"\"\"\"") {
1096                index = self.scan_kotlin_string(index, true, depth + 1);
1097                continue;
1098            }
1099            match bytes[index] {
1100                b'"' => index = self.scan_kotlin_string(index, false, depth + 1),
1101                b'\'' => {
1102                    index = self.quoted_or_error(index, false, "Kotlin character literal");
1103                }
1104                b'{' => {
1105                    braces += 1;
1106                    index += 1;
1107                }
1108                b'}' => {
1109                    braces -= 1;
1110                    index += 1;
1111                    if braces == 0 {
1112                        return index;
1113                    }
1114                }
1115                _ => index += 1,
1116            }
1117        }
1118        self.error(
1119            "unterminated-template-expression",
1120            "unterminated Kotlin string-template expression",
1121            ByteSpan::new(index, index),
1122        );
1123        index
1124    }
1125
1126    fn quoted_or_error(&mut self, start: usize, multiline: bool, name: &str) -> usize {
1127        let quote = self.source[start];
1128        let mut index = start + 1;
1129        while index < self.source.len() {
1130            if self.source[index] == b'\\' {
1131                if index + 1 < self.source.len() {
1132                    index += 2;
1133                } else {
1134                    index += 1;
1135                }
1136            } else if self.source[index] == quote {
1137                return index + 1;
1138            } else if !multiline && matches!(self.source[index], b'\r' | b'\n') {
1139                self.error(
1140                    "unterminated-string",
1141                    &format!("unterminated {name}"),
1142                    ByteSpan::new(start, index),
1143                );
1144                return index;
1145            } else {
1146                index += 1;
1147            }
1148        }
1149        self.error(
1150            "unterminated-string",
1151            &format!("unterminated {name}"),
1152            ByteSpan::new(start, index),
1153        );
1154        index
1155    }
1156
1157    fn js_quoted_or_error(&mut self, start: usize) -> usize {
1158        let quote = self.source[start];
1159        let mut index = start + 1;
1160        while index < self.source.len() {
1161            if self.source[index] == b'\\' {
1162                let escaped = index + 1;
1163                if let Some(width) = unicode_line_terminator_width(self.source, escaped) {
1164                    index = escaped + width;
1165                } else {
1166                    index = (index + 2).min(self.source.len());
1167                }
1168            } else if self.source[index] == quote {
1169                return index + 1;
1170            } else if unicode_line_terminator_width(self.source, index).is_some() {
1171                self.error(
1172                    "unterminated-string",
1173                    "unterminated JavaScript string",
1174                    ByteSpan::new(start, index),
1175                );
1176                return index;
1177            } else {
1178                index += 1;
1179            }
1180        }
1181        self.error(
1182            "unterminated-string",
1183            "unterminated JavaScript string",
1184            ByteSpan::new(start, index),
1185        );
1186        index
1187    }
1188
1189    fn delimited_or_error(&mut self, start: usize, delimiter: &[u8], name: &str) -> usize {
1190        let content = start + delimiter.len();
1191        if let Some(relative) = find_subslice(&self.source[content..], delimiter) {
1192            content + relative + delimiter.len()
1193        } else {
1194            self.error(
1195                "unterminated-string",
1196                &format!("unterminated {name}"),
1197                ByteSpan::new(start, self.source.len()),
1198            );
1199            self.source.len()
1200        }
1201    }
1202
1203    fn scan_java(&mut self) {
1204        let (mapped, invalid_unicode) = MappedBytes::java_unicode(self.source);
1205        for span in invalid_unicode {
1206            self.error(
1207                "invalid-unicode-escape",
1208                "invalid Java Unicode escape",
1209                span,
1210            );
1211        }
1212        let mut child = Scanner::child(
1213            &mapped.bytes,
1214            Language::Java,
1215            self.options.clone(),
1216            self.patterns.clone(),
1217            0,
1218        );
1219        let mut index = 0;
1220        while index < child.source.len() {
1221            if starts(child.source, index, b"//") {
1222                let end = line_end(child.source, index + 2);
1223                child.add_comment(index, end, java_line_kind(child.source, index));
1224                index = end;
1225                continue;
1226            }
1227            if starts(child.source, index, b"/*") {
1228                let (end, closed) = block_end(child.source, index, b"/*", b"*/", false);
1229                child.add_comment(index, end, java_block_kind(child.source, index));
1230                if !closed {
1231                    child.error(
1232                        "unterminated-comment",
1233                        "unterminated block comment",
1234                        ByteSpan::new(index, end),
1235                    );
1236                }
1237                index = end;
1238                continue;
1239            }
1240            if starts(child.source, index, b"\"\"\"") {
1241                let (end, closed) = java_text_block_end(child.source, index);
1242                if !closed {
1243                    child.error(
1244                        "unterminated-string",
1245                        "unterminated Java text block",
1246                        ByteSpan::new(index, end),
1247                    );
1248                }
1249                index = end;
1250                continue;
1251            }
1252            if matches!(child.source[index], b'"' | b'\'') {
1253                index = child.quoted_or_error(index, false, "Java literal");
1254                continue;
1255            }
1256            index += 1;
1257        }
1258        self.merge_mapped(child, &mapped);
1259    }
1260
1261    fn merge_mapped(&mut self, child: Scanner<'_>, mapped: &MappedBytes) {
1262        for mut comment in child.comments {
1263            comment.span = mapped.original_span(comment.span);
1264            comment.span.start += self.offset;
1265            comment.span.end += self.offset;
1266            self.comments.push(comment);
1267        }
1268        for mut diagnostic in child.diagnostics {
1269            diagnostic.span = mapped.original_span(diagnostic.span);
1270            diagnostic.span.start += self.offset;
1271            diagnostic.span.end += self.offset;
1272            self.diagnostics.push(diagnostic);
1273        }
1274    }
1275
1276    fn scan_ocaml(&mut self) {
1277        let bytes = self.source;
1278        let mut index = 0;
1279        while index < bytes.len() && !self.stopped {
1280            if starts(bytes, index, b"(*") {
1281                let mut reach = Reach::default();
1282                let (end, closed) = ocaml_comment_end(bytes, index, &mut reach);
1283                self.consult(reach);
1284                self.add_comment(
1285                    index,
1286                    end,
1287                    if starts(bytes, index, b"(**") {
1288                        CommentKind::DocBlock
1289                    } else {
1290                        CommentKind::Block
1291                    },
1292                );
1293                if !closed {
1294                    self.error(
1295                        "unterminated-comment",
1296                        "unterminated OCaml comment",
1297                        ByteSpan::new(index, end),
1298                    );
1299                }
1300                index = end;
1301                continue;
1302            }
1303            let mut reach = Reach::default();
1304            let quoted = ocaml_quoted_string(bytes, index, &mut reach);
1305            self.consult(reach);
1306            if let Some((end, closed)) = quoted {
1307                if !closed {
1308                    self.error(
1309                        "unterminated-string",
1310                        "unterminated OCaml quoted string",
1311                        ByteSpan::new(index, end),
1312                    );
1313                }
1314                index = end;
1315                continue;
1316            }
1317            if bytes[index] == b'"' {
1318                index = self.quoted_or_error(index, true, "OCaml string");
1319                continue;
1320            }
1321            if bytes[index] == b'\'' {
1322                let mut reach = Reach::default();
1323                let literal = ocaml_char_start(bytes, index, &mut reach);
1324                self.consult(reach);
1325                if literal {
1326                    index = self.quoted_or_error(index, false, "OCaml character literal");
1327                    continue;
1328                }
1329            }
1330            if matches!(bytes[index], b'\r' | b'\n') {
1331                index = consume_newline(bytes, index);
1332                self.add_safe_checkpoint(index);
1333            } else {
1334                index += 1;
1335            }
1336        }
1337    }
1338
1339    fn scan_python(&mut self) {
1340        let bytes = self.source;
1341        let mut index = 0;
1342        while index < bytes.len() && !self.stopped {
1343            if bytes[index] == b'#' {
1344                let end = line_end(bytes, index + 1);
1345                self.add_comment(index, end, CommentKind::Line);
1346                index = end;
1347                continue;
1348            }
1349            if let Some((quote_start, triple, formatted, raw)) = python_string_start(bytes, index) {
1350                if formatted {
1351                    index = self.scan_python_fstring(index, quote_start, triple, raw, 0);
1352                } else {
1353                    /* NOTE: `index` rather than `quote_start`: a prefix and the quote
1354                     * after it are one token (Python reference 2.4.1), so an
1355                     * unterminated `r"` is reported from the `r`, the way the
1356                     * triple-quoted and f-string paths already report theirs.
1357                     * The single-quoted case used the generic string reader,
1358                     * which knows only where the quote was. */
1359                    index = self.scan_python_delimited(index, quote_start, triple);
1360                }
1361                continue;
1362            }
1363            if matches!(bytes[index], b'\r' | b'\n') {
1364                index = consume_newline(bytes, index);
1365                self.add_safe_checkpoint(index);
1366            } else {
1367                index += 1;
1368            }
1369        }
1370    }
1371
1372    fn scan_python_delimited(
1373        &mut self,
1374        token_start: usize,
1375        quote_start: usize,
1376        triple: bool,
1377    ) -> usize {
1378        let bytes = self.source;
1379        let length = if triple { 3 } else { 1 };
1380        let delimiter = &bytes[quote_start..quote_start + length];
1381        let mut index = quote_start + length;
1382        while index < bytes.len() {
1383            if starts(bytes, index, delimiter) {
1384                return index + length;
1385            }
1386            if bytes[index] == b'\\' {
1387                index = (index + 2).min(bytes.len());
1388            } else if !triple && matches!(bytes[index], b'\r' | b'\n') {
1389                self.error(
1390                    "unterminated-string",
1391                    "unterminated Python string",
1392                    ByteSpan::new(token_start, index),
1393                );
1394                return index;
1395            } else {
1396                index += 1;
1397            }
1398        }
1399        self.error(
1400            "unterminated-string",
1401            if triple {
1402                "unterminated Python triple-quoted string"
1403            } else {
1404                "unterminated Python string"
1405            },
1406            ByteSpan::new(token_start, index),
1407        );
1408        index
1409    }
1410
1411    fn scan_python_fstring(
1412        &mut self,
1413        token_start: usize,
1414        quote_start: usize,
1415        triple: bool,
1416        _raw: bool,
1417        depth: usize,
1418    ) -> usize {
1419        if depth > 256 {
1420            self.error(
1421                "nesting-limit",
1422                "Python f-string nesting limit exceeded",
1423                ByteSpan::new(token_start, token_start),
1424            );
1425            return self.source.len();
1426        }
1427        let bytes = self.source;
1428        let length = if triple { 3 } else { 1 };
1429        let delimiter = &bytes[quote_start..quote_start + length];
1430        let mut index = quote_start + length;
1431        while index < bytes.len() {
1432            if starts(bytes, index, delimiter) {
1433                return index + length;
1434            }
1435            if bytes[index] == b'\\' {
1436                index = (index + 2).min(bytes.len());
1437            } else if starts(bytes, index, b"{{") || starts(bytes, index, b"}}") {
1438                index += 2;
1439            } else if bytes[index] == b'{' {
1440                index = self.scan_python_expression(index + 1, depth + 1);
1441            } else if !triple && matches!(bytes[index], b'\r' | b'\n') {
1442                self.error(
1443                    "unterminated-string",
1444                    "unterminated Python f-string",
1445                    ByteSpan::new(token_start, index),
1446                );
1447                return index;
1448            } else {
1449                index += 1;
1450            }
1451        }
1452        self.error(
1453            "unterminated-string",
1454            "unterminated Python f-string",
1455            ByteSpan::new(token_start, index),
1456        );
1457        index
1458    }
1459
1460    fn scan_python_expression(&mut self, mut index: usize, depth: usize) -> usize {
1461        let bytes = self.source;
1462        let mut braces = 1usize;
1463        while index < bytes.len() {
1464            if bytes[index] == b'#' {
1465                let end = line_end(bytes, index + 1);
1466                self.add_comment(index, end, CommentKind::Line);
1467                index = end;
1468                continue;
1469            }
1470            if let Some((quote_start, triple, formatted, raw)) = python_string_start(bytes, index) {
1471                index = if formatted {
1472                    self.scan_python_fstring(index, quote_start, triple, raw, depth + 1)
1473                } else if triple {
1474                    self.scan_python_delimited(index, quote_start, true)
1475                } else {
1476                    /* NOTE: `index` rather than `quote_start`: a prefix and the
1477                     * quote after it are one token (Python reference 2.4.1), so
1478                     * an unterminated `r"` inside a replacement field is
1479                     * reported from the `r`, the way the delimited reader
1480                     * reports every other string and the OCaml reference
1481                     * reports this one. */
1482                    self.scan_python_delimited(index, quote_start, false)
1483                };
1484                continue;
1485            }
1486            match bytes[index] {
1487                b'{' => {
1488                    braces += 1;
1489                    index += 1;
1490                }
1491                b'}' => {
1492                    braces -= 1;
1493                    index += 1;
1494                    if braces == 0 {
1495                        return index;
1496                    }
1497                }
1498                _ => index += 1,
1499            }
1500        }
1501        self.error(
1502            "unterminated-fstring-expression",
1503            "unterminated Python f-string expression",
1504            ByteSpan::new(index, index),
1505        );
1506        index
1507    }
1508
1509    fn scan_toml(&mut self) {
1510        let bytes = self.source;
1511        let mut index = 0;
1512        while index < bytes.len() && !self.stopped {
1513            match bytes[index] {
1514                b'#' => {
1515                    let end = line_end(bytes, index + 1);
1516                    self.add_comment(index, end, CommentKind::Line);
1517                    index = end;
1518                }
1519                b'"' | b'\'' => index = self.scan_toml_string(index),
1520                b'\r' | b'\n' => {
1521                    index = consume_newline(bytes, index);
1522                    self.add_safe_checkpoint(index);
1523                }
1524                _ => index += 1,
1525            }
1526        }
1527    }
1528
1529    /// One TOML string beginning at its opening quote, whether it is a value
1530    /// or a quoted key.
1531    ///
1532    /// `"` opens a basic string, which takes `\` escapes, and `'` a literal
1533    /// string, which takes none, so a `\` in a literal string is a byte of it
1534    /// (TOML v1.0.0, String). Three of either quote open the multi-line form,
1535    /// where a newline is content instead of the end of an unterminated
1536    /// string.
1537    fn scan_toml_string(&mut self, start: usize) -> usize {
1538        let bytes = self.source;
1539        let quote = bytes[start];
1540        let multiline = starts(bytes, start, &[quote, quote, quote]);
1541        let escapes = quote == b'"';
1542        let mut index = start + if multiline { 3 } else { 1 };
1543        while index < bytes.len() {
1544            if escapes && bytes[index] == b'\\' {
1545                /* NOTE: This also carries the line-ending backslash of a multi-line
1546                 * basic string, which continues the string across the newline
1547                 * it swallows: the newline is content either way, so the two
1548                 * need no separate rules. */
1549                index = (index + 2).min(bytes.len());
1550            } else if bytes[index] != quote {
1551                if !multiline && matches!(bytes[index], b'\r' | b'\n') {
1552                    self.error(
1553                        "unterminated-string",
1554                        "unterminated TOML string",
1555                        ByteSpan::new(start, index),
1556                    );
1557                    return index;
1558                }
1559                index += 1;
1560            } else if !multiline {
1561                return index + 1;
1562            } else {
1563                /* NOTE: The delimiter is three quotes, and up to two more may sit
1564                 * in front of it as content, so a run of three or more ends the
1565                 * string on its last three — after at most five of them. A
1566                 * sixth quote is past what the grammar lets the delimiter
1567                 * absorb and belongs to whatever follows the string. */
1568                let run = toml_quote_run(bytes, index, quote);
1569                if run >= 3 {
1570                    return index + run.min(5);
1571                }
1572                index += run;
1573            }
1574        }
1575        self.error(
1576            "unterminated-string",
1577            if multiline {
1578                "unterminated TOML multi-line string"
1579            } else {
1580                "unterminated TOML string"
1581            },
1582            ByteSpan::new(start, index),
1583        );
1584        index
1585    }
1586
1587    /// One Lua chunk (Lua 5.4 reference manual, 3.1 Lexical Conventions).
1588    fn scan_lua(&mut self) {
1589        let bytes = self.source;
1590        let mut index = 0;
1591        /* NOTE: The loader skips a first line that opens with `#` before it lexes
1592         * anything (`lauxlib.c`, `skipcomment`), which is what lets a chunk
1593         * carry a `#!` line. It is that one byte at that one offset: `#` is the
1594         * length operator everywhere else. `skipcomment` calls `skipBOM` first,
1595         * so the offset is behind a UTF-8 byte order mark when the file carries
1596         * one. The `self.offset` test is what keeps a suffix scan out of the
1597         * rule, and no checkpoint of a full scan falls inside the first line, so
1598         * the two answers cannot disagree. */
1599        let preamble = byte_order_mark_width(bytes);
1600        if self.offset == 0 && bytes.get(preamble) == Some(&b'#') {
1601            let end = line_end(bytes, preamble + 1);
1602            self.add_comment(preamble, end, CommentKind::Line);
1603            index = end;
1604        }
1605        while index < bytes.len() && !self.stopped {
1606            if starts(bytes, index, b"--") {
1607                index = self.scan_lua_comment(index);
1608                continue;
1609            }
1610            match bytes[index] {
1611                b'"' | b'\'' => index = self.scan_lua_short_string(index),
1612                b'[' => index = self.scan_lua_long_string(index),
1613                b'\r' | b'\n' => {
1614                    index = consume_newline(bytes, index);
1615                    self.add_safe_checkpoint(index);
1616                }
1617                _ => index += 1,
1618            }
1619        }
1620    }
1621
1622    /// One Lua comment beginning at its `--`.
1623    ///
1624    /// A long bracket immediately after the `--` opens a long comment, which
1625    /// runs to the closing bracket of its own level; anything else is a short
1626    /// comment to the end of the line. The two differ in the byte that
1627    /// completes the bracket, so `--[=` is a short comment and `--[=[` is not.
1628    fn scan_lua_comment(&mut self, start: usize) -> usize {
1629        let bytes = self.source;
1630        if let Some(level) = long_bracket_level(bytes, start + 2) {
1631            let (end, closed) = long_bracket_end(bytes, start + 2 + level + 2, level);
1632            self.add_comment(start, end, CommentKind::Block);
1633            if !closed {
1634                self.error(
1635                    "unterminated-comment",
1636                    "unterminated Lua long comment",
1637                    ByteSpan::new(start, end),
1638                );
1639            }
1640            return end;
1641        }
1642        let end = line_end(bytes, start + 2);
1643        self.add_comment(start, end, lua_line_kind(bytes, start));
1644        end
1645    }
1646
1647    /// One Lua long string beginning at `start`, or `start + 1` when no long
1648    /// bracket opens there: `a[b[1]]` indexes twice and opens nothing.
1649    fn scan_lua_long_string(&mut self, start: usize) -> usize {
1650        let bytes = self.source;
1651        let Some(level) = long_bracket_level(bytes, start) else {
1652            return start + 1;
1653        };
1654        let (end, closed) = long_bracket_end(bytes, start + level + 2, level);
1655        if !closed {
1656            self.error(
1657                "unterminated-string",
1658                "unterminated Lua long string",
1659                ByteSpan::new(start, end),
1660            );
1661        }
1662        end
1663    }
1664
1665    /// One Lua short string beginning at its quote.
1666    ///
1667    /// `\z` skips the whitespace that follows it, newlines included, and a
1668    /// backslash before a real line terminator carries that terminator into the
1669    /// string. Any other unescaped line terminator ends a string that was never
1670    /// closed. The remaining escapes — `\ddd`, `\xXX`, `\u{XXX}` and the
1671    /// single-character ones — carry no quote and no newline, so skipping the
1672    /// byte after the backslash finds the same closing quote the whole rule
1673    /// would.
1674    fn scan_lua_short_string(&mut self, start: usize) -> usize {
1675        let bytes = self.source;
1676        let quote = bytes[start];
1677        let mut index = start + 1;
1678        while index < bytes.len() {
1679            if bytes[index] == b'\\' {
1680                let escaped = index + 1;
1681                if bytes.get(escaped) == Some(&b'z') {
1682                    index = escaped + 1;
1683                    while bytes.get(index).is_some_and(|byte| lua_is_space(*byte)) {
1684                        index += 1;
1685                    }
1686                } else if let Some(width) = lua_newline_width(bytes, escaped) {
1687                    index = escaped + width;
1688                } else {
1689                    index = (escaped + 1).min(bytes.len());
1690                }
1691            } else if bytes[index] == quote {
1692                return index + 1;
1693            } else if lua_newline_width(bytes, index).is_some() {
1694                self.error(
1695                    "unterminated-string",
1696                    "unterminated Lua string",
1697                    ByteSpan::new(start, index),
1698                );
1699                return index;
1700            } else {
1701                index += 1;
1702            }
1703        }
1704        self.error(
1705            "unterminated-string",
1706            "unterminated Lua string",
1707            ByteSpan::new(start, index),
1708        );
1709        index
1710    }
1711
1712    /// One YAML stream (YAML 1.2.2 specification).
1713    ///
1714    /// The scanner is line-local but for one answer: `#` opens a comment only
1715    /// where white space separates it from the token in front of it (6.6), the
1716    /// two quoted styles (7.3.1, 7.3.2) may run over a line break and carry
1717    /// every `#` inside them as content, and a block scalar (8.1) swallows
1718    /// every following line that is more indented than the node it hangs off.
1719    /// That last depth is the exception: a `|` may sit on the line under the
1720    /// `key:` or the `-` that owns it, or behind node properties (6.9), so the
1721    /// owner's indentation is carried across the line break rather than read
1722    /// off the header's own column.
1723    ///
1724    /// That is what makes the start of a line a restart point — but only a
1725    /// line outside a quoted scalar, outside a block scalar body, and with no
1726    /// owner carried into it, because in each of those the same bytes mean
1727    /// something else. None of the three emits a checkpoint: a quoted scalar
1728    /// consumes its line breaks without offering one, the body of a block
1729    /// scalar is consumed by [`yaml_block_body_end`], which reports where it
1730    /// ended and whether that offset is the start of a line at all, and a line
1731    /// under a live carry is skipped outright. Where a body ends is decided by
1732    /// the lines *below* it, which an edit can move, so
1733    /// [`first_yaml_block_scalar`] withdraws every checkpoint past the first
1734    /// header a document opens: what this function offers is what those rules
1735    /// then hold it to.
1736    ///
1737    /// `valid` is a lexical answer here and nothing more. YAML has shapes a
1738    /// lexer cannot rule out and a parser rejects, and removing a comment can
1739    /// walk a file from one to the other: a comment line inside a multi-line
1740    /// plain scalar is a parse error while it is there, and taking it away
1741    /// leaves a scalar that parses and folds the two halves into one value.
1742    /// A comment line under a block scalar body is the same hazard read the
1743    /// other way — see [`lines_a_removal_must_swallow`], which is what stops
1744    /// the hole a removal leaves from being read back as content of the body
1745    /// above it. Every block scalar this walks over is recorded in
1746    /// [`Self::yaml_blocks`] for exactly that question.
1747    fn scan_yaml(&mut self) {
1748        let bytes = self.source;
1749        let mut index = 0;
1750        let mut line_start = 0;
1751        /* INVARIANT: `separated` is whether a `#` at `index` would be separated from
1752         * what precedes it, which is the whole of the comment rule; `node_start`
1753         * is whether a node may begin here, which is what tells the block scalar
1754         * indicator `key: >` from the `>` inside the plain scalar `key: a > b`;
1755         * `token_column` is where the token being read began, so that a `: `
1756         * behind it can name the column the value hangs off; and `owner_column`
1757         * is that column once one is known. The first three are reset by the
1758         * line break; `owner_column` survives it while the node it names is
1759         * still owed one, which is the only state a restart at a line start
1760         * cannot reproduce — so no checkpoint is offered while it is set. */
1761        let mut separated = true;
1762        let mut node_start = true;
1763        let mut token_column = None;
1764        let mut owner_column = None;
1765        while index < bytes.len() && !self.stopped {
1766            match bytes[index] {
1767                b'#' if separated => {
1768                    let end = line_end(bytes, index + 1);
1769                    self.add_comment(index, end, CommentKind::Line);
1770                    index = end;
1771                }
1772                b'\r' | b'\n' => {
1773                    index = consume_newline(bytes, index);
1774                    line_start = index;
1775                    separated = true;
1776                    /* NOTE: A line that ends while a node is still owed — `key:`,
1777                     * a bare `-`, a property whose node has not come yet, and
1778                     * the blank and comment lines a separation may hold (6.9,
1779                     * 8.2.2) — hands the owner's indentation to the line below,
1780                     * because the `|` of the block scalar it introduces may be
1781                     * down there. A line that put a node on itself hands over
1782                     * nothing. */
1783                    if !node_start {
1784                        owner_column = None;
1785                    }
1786                    node_start = true;
1787                    token_column = None;
1788                    if owner_column.is_none() {
1789                        self.add_safe_checkpoint(index);
1790                    }
1791                }
1792                b' ' | b'\t' => {
1793                    index += 1;
1794                    separated = true;
1795                }
1796                b'|' | b'>'
1797                    if node_start && separated && yaml_block_header(bytes, index).is_some() =>
1798                {
1799                    let (indicator, chomping, comment, header_end) =
1800                        yaml_block_header(bytes, index).expect("the guard read the header");
1801                    if let Some(start) = comment {
1802                        self.add_comment(start, header_end, CommentKind::Line);
1803                    }
1804                    /* NOTE: The body is indented past the node the scalar hangs off
1805                     * (8.1.1.1). For `key: |` that node is the mapping, whose
1806                     * indentation is the column of the key; for `- |` it is the
1807                     * sequence, whose indentation is the column of the `-`. The
1808                     * header itself may sit anywhere past that owner — on a
1809                     * line of its own, or behind an anchor or a tag — so its
1810                     * own column says nothing about how deep a body line has to
1811                     * be, and reading it as the floor would take a body
1812                     * indented less than the header for the end of the scalar
1813                     * and its `#` lines for comments. With no owner at all the
1814                     * scalar is the whole document, whose indentation is one
1815                     * short of column zero, which leaves every line under it
1816                     * body. An explicit indentation indicator counts from that
1817                     * same owner, which is why it replaces the detected depth
1818                     * rather than adding to it. Detection proper reads the
1819                     * first non-empty line instead, and a line shallower than
1820                     * that but still past the owner is content of neither
1821                     * reading; taking it for body is the one that leaves bytes
1822                     * alone. */
1823                    let base = owner_column.map_or(0, |column| column + 1);
1824                    let floor = base + indicator.unwrap_or(1) - 1;
1825                    let (end, boundary, detected) = yaml_block_body_end(bytes, header_end, floor);
1826                    /* NOTE: Where this body stopped, on what terms it keeps its
1827                     * trailing empty lines, and how deep its content is are the
1828                     * whole of what `lines_a_removal_must_swallow` and
1829                     * `yaml_structural_trail_keeps` need from a scan: the lines
1830                     * under a body are the only place in YAML where the hole a
1831                     * removal leaves carries meaning. Recorded here rather than
1832                     * re-derived, because only the scan knows the column of the
1833                     * node the header hangs off. An explicit indicator *is* the
1834                     * content depth (8.1.1.1); without one the depth is
1835                     * detected from the first non-empty line, and a body with
1836                     * no non-empty line at all has none to detect, so the floor
1837                     * stands in for it — which is the depth the next line the
1838                     * scalar could take would set. */
1839                    self.yaml_blocks.push(YamlBlockScalar {
1840                        body_end: end + self.offset,
1841                        content_indent: indicator.map_or(detected, |_| floor),
1842                        chomping,
1843                    });
1844                    index = end;
1845                    line_start = index;
1846                    separated = true;
1847                    node_start = true;
1848                    token_column = None;
1849                    owner_column = None;
1850                    if boundary {
1851                        self.add_safe_checkpoint(index);
1852                    }
1853                }
1854                /* NOTE: An anchor `&name` and a tag `!tag` are node properties
1855                 * (6.9): they stand in front of the node they decorate rather
1856                 * than being one, so a node may still begin after them. That is
1857                 * what leaves the `|` of `key: !!str |` a block scalar header
1858                 * instead of a byte of a plain scalar. A property belongs to
1859                 * the node it decorates, so it is that node's first token and
1860                 * names the column a `: ` behind it hangs off. */
1861                b'!' | b'&' if node_start && separated => {
1862                    if token_column.is_none() {
1863                        token_column = Some(index - line_start);
1864                    }
1865                    index = yaml_property_end(bytes, index);
1866                    separated = false;
1867                }
1868                b'"' | b'\'' if separated || yaml_flow_opener(bytes, index) => {
1869                    if node_start && token_column.is_none() {
1870                        token_column = Some(index - line_start);
1871                    }
1872                    index = self.scan_yaml_quoted(index);
1873                    separated = false;
1874                    node_start = false;
1875                }
1876                /* NOTE: `-` is a sequence entry and `?` an explicit key only when
1877                 * white space or the line ends them (6.9 and 8.2): `-x` is a
1878                 * plain scalar, and so is `?x`. Either leaves the position a
1879                 * node may begin at, one column further in. */
1880                b'-' | b'?'
1881                    if node_start
1882                        && bytes
1883                            .get(index + 1)
1884                            .is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n')) =>
1885                {
1886                    owner_column = Some(index - line_start);
1887                    token_column = None;
1888                    index += 1;
1889                    separated = false;
1890                }
1891                /* NOTE: A `:` ends a key only where white space or the line follows
1892                 * it (7.2), which is what leaves the `:` of `http://x` inside
1893                 * the plain scalar it belongs to. */
1894                b':' if bytes
1895                    .get(index + 1)
1896                    .is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n')) =>
1897                {
1898                    owner_column = token_column.or(owner_column);
1899                    token_column = None;
1900                    node_start = true;
1901                    index += 1;
1902                    separated = false;
1903                }
1904                _ => {
1905                    if node_start {
1906                        if token_column.is_none() {
1907                            token_column = Some(index - line_start);
1908                        }
1909                        node_start = false;
1910                    }
1911                    index += 1;
1912                    separated = false;
1913                }
1914            }
1915        }
1916        /* NOTE: One rule needs the whole file rather than the byte in front of
1917         * it, so it runs once the trails are all there to read: a comment that
1918         * is the only thing holding a block scalar out of the kept comment
1919         * under it is not commentary and is kept. A scan that stopped early is
1920         * a partial answer the incremental engine completes from the previous
1921         * revision's tail, and the trail it would read is truncated, so it is
1922         * left alone — no checkpoint a YAML scan offers sits past the first
1923         * block scalar, which is what leaves the tail's own answer intact. */
1924        if !self.stopped && !self.yaml_blocks.is_empty() {
1925            let keeps = yaml_structural_trail_keeps(
1926                self.source,
1927                self.offset,
1928                &self.yaml_blocks,
1929                &self.comments,
1930            );
1931            for index in keeps {
1932                self.comments[index].disposition = Disposition::Keep {
1933                    reason: YAML_STRUCTURAL_TRAIL.to_owned(),
1934                };
1935            }
1936        }
1937    }
1938
1939    /// One quoted scalar beginning at its own quote.
1940    ///
1941    /// A double-quoted scalar takes `\` escapes (YAML 1.2.2, 7.3.1) and a
1942    /// single-quoted one takes none, where `''` is the one way to write a
1943    /// quote of its own (7.3.2), so a backslash inside the second is a byte of
1944    /// it. Both fold over a line break, which makes the end of the file the
1945    /// only thing that leaves one unterminated.
1946    fn scan_yaml_quoted(&mut self, start: usize) -> usize {
1947        let bytes = self.source;
1948        let quote = bytes[start];
1949        let mut index = start + 1;
1950        while index < bytes.len() {
1951            if quote == b'"' && bytes[index] == b'\\' {
1952                index = (index + 2).min(bytes.len());
1953            } else if bytes[index] != quote {
1954                index += 1;
1955            } else if quote == b'\'' && bytes.get(index + 1) == Some(&b'\'') {
1956                index += 2;
1957            } else {
1958                return index + 1;
1959            }
1960        }
1961        self.error(
1962            "unterminated-string",
1963            if quote == b'"' {
1964                "unterminated YAML double-quoted scalar"
1965            } else {
1966                "unterminated YAML single-quoted scalar"
1967            },
1968            ByteSpan::new(start, index),
1969        );
1970        index
1971    }
1972
1973    /// One PHP file (PHP manual, Language Reference: Basic syntax, Comments,
1974    /// Strings, and Heredoc text).
1975    ///
1976    /// A PHP file is two languages at once. It opens in inline-HTML mode,
1977    /// where every byte is output verbatim and nothing is a comment; `<?php`
1978    /// with white space or the end of the file behind it, and the short echo
1979    /// tag `<?=`, enter PHP mode, and `?>` returns to inline HTML. With the
1980    /// default `short_open_tag=Off` a bare `<?` opens nothing at all, which is
1981    /// what leaves an XML declaration inline text.
1982    ///
1983    /// Inline HTML is opaque in v1: an HTML `<!-- ... -->` comment in a PHP
1984    /// file is not reported. Reading it would mean scanning the inline halves
1985    /// as HTML, which is a change of what the language *is* rather than a
1986    /// missing arm here, so v1 leaves those bytes alone — the direction that
1987    /// can only keep a comment, never remove one.
1988    ///
1989    /// Which mode a byte sits in is decided entirely by the bytes in front of
1990    /// it, and no lexical state PHP opens is ended by anything except its own
1991    /// closer, so a restart in inline HTML reproduces the rest of a full scan.
1992    /// That is the only place a checkpoint is offered: a line break met in PHP
1993    /// mode is not a restart point, because the same line at the same offset
1994    /// means something else with an unclosed `<?php` above it. A file that is
1995    /// all PHP therefore rescans from the top.
1996    fn scan_php(&mut self) {
1997        let bytes = self.source;
1998        let mut index = 0;
1999        /* NOTE: The CLI strips a `#!` line from the first line of a script before
2000         * the engine sees it (`php_cli.c`, which tests the first two bytes), so
2001         * that line is a preamble rather than the inline HTML the rest of the
2002         * file opens as. Unlike CPython and Lua, PHP skips no byte order mark
2003         * first, and neither does the kernel, so a mark in front of the `#!`
2004         * leaves it ordinary inline HTML — the same reason a shell script has.
2005         * The `self.offset` test is what keeps a suffix scan out of the rule,
2006         * and no checkpoint of a full scan falls inside the first line, so the
2007         * two answers cannot disagree. */
2008        if self.offset == 0 && starts(bytes, 0, b"#!") {
2009            let end = line_end(bytes, 2);
2010            self.add_comment(0, end, CommentKind::Line);
2011            index = end;
2012        }
2013        while index < bytes.len() && !self.stopped {
2014            match bytes[index] {
2015                b'<' => match php_open_tag(bytes, index) {
2016                    Some(code) => index = self.scan_php_code(code),
2017                    None => index += 1,
2018                },
2019                b'\r' | b'\n' => {
2020                    index = consume_newline(bytes, index);
2021                    self.add_safe_checkpoint(index);
2022                }
2023                _ => index += 1,
2024            }
2025        }
2026    }
2027
2028    /// PHP mode, from the byte after the opening tag that entered it.
2029    ///
2030    /// Returns where inline HTML resumes: past a `?>` and the one line break
2031    /// it carries away with it, or the end of the file.
2032    fn scan_php_code(&mut self, mut index: usize) -> usize {
2033        let bytes = self.source;
2034        while index < bytes.len() {
2035            match bytes[index] {
2036                b'?' if starts(bytes, index, b"?>") => {
2037                    let end = index + 2;
2038                    /* NOTE: The closing tag token carries one line break with it
2039                     * (`zend_language_scanner.l`: `"?>"{NEWLINE}?`), which is
2040                     * what keeps a template from emitting a blank line for
2041                     * every block of code it holds. A CRLF pair is that one
2042                     * break. The byte after it starts a line of inline HTML,
2043                     * so it is a restart point like any other line start. */
2044                    if matches!(bytes.get(end), Some(b'\r' | b'\n')) {
2045                        let next = consume_newline(bytes, end);
2046                        self.add_safe_checkpoint(next);
2047                        return next;
2048                    }
2049                    return end;
2050                }
2051                b'/' if starts(bytes, index, b"//") => {
2052                    let end = php_line_comment_end(bytes, index + 2);
2053                    self.add_comment(index, end, CommentKind::Line);
2054                    index = end;
2055                }
2056                b'/' if starts(bytes, index, b"/*") => {
2057                    let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
2058                    self.add_comment(index, end, php_block_kind(bytes, index));
2059                    if !closed {
2060                        self.error(
2061                            "unterminated-comment",
2062                            "unterminated PHP block comment",
2063                            ByteSpan::new(index, end),
2064                        );
2065                    }
2066                    index = end;
2067                }
2068                /* NOTE: PHP 8.0 gave `#[` to attributes (Attributes, Attribute
2069                 * syntax), so a `#` with a bracket behind it opens no comment
2070                 * and what follows is ordinary code. */
2071                b'#' if bytes.get(index + 1) == Some(&b'[') => index += 1,
2072                b'#' => {
2073                    let end = php_line_comment_end(bytes, index + 1);
2074                    self.add_comment(index, end, CommentKind::Line);
2075                    index = end;
2076                }
2077                b'\'' | b'"' | b'`' => index = self.scan_php_quoted(index),
2078                b'<' if starts(bytes, index, b"<<<") => index = self.scan_php_heredoc(index),
2079                _ => index += 1,
2080            }
2081        }
2082        index
2083    }
2084
2085    /// One PHP string beginning at its delimiter: `'`, `"`, or the backtick of
2086    /// the execution operator.
2087    ///
2088    /// A single-quoted string escapes only `\'` and `\\`, and every other
2089    /// backslash is a byte of it — but the byte after a backslash can never be
2090    /// the closing quote unless the pair *is* the `\'` escape, so skipping two
2091    /// finds the same closer either way. A double-quoted or backtick string
2092    /// takes the full escape set and interpolates: `{$...}` and `${...}` hold
2093    /// an expression, which [`php_interpolation_end`] skips by balancing
2094    /// braces, so a comment written inside one stays content. None of the
2095    /// three ends at a line break, so only the end of the file leaves one
2096    /// unterminated.
2097    fn scan_php_quoted(&mut self, start: usize) -> usize {
2098        let bytes = self.source;
2099        let quote = bytes[start];
2100        let interpolates = quote != b'\'';
2101        let mut index = start + 1;
2102        while index < bytes.len() {
2103            if bytes[index] == b'\\' {
2104                index = (index + 2).min(bytes.len());
2105            } else if bytes[index] == quote {
2106                return index + 1;
2107            } else if interpolates && bytes[index] == b'{' && bytes.get(index + 1) == Some(&b'$') {
2108                index = php_interpolation_end(bytes, index);
2109            } else if interpolates && bytes[index] == b'$' && bytes.get(index + 1) == Some(&b'{') {
2110                index = php_interpolation_end(bytes, index + 1);
2111            } else {
2112                index += 1;
2113            }
2114        }
2115        self.error(
2116            "unterminated-string",
2117            match quote {
2118                b'\'' => "unterminated PHP single-quoted string",
2119                b'"' => "unterminated PHP double-quoted string",
2120                _ => "unterminated PHP backtick string",
2121            },
2122            ByteSpan::new(start, index),
2123        );
2124        index
2125    }
2126
2127    /// One heredoc or nowdoc beginning at its `<<<`, or `start + 1` when those
2128    /// three bytes head no header at all — `$a <<< 1` is two shift operators
2129    /// and a number, and the conservative reading of anything the header
2130    /// grammar refuses is that it opened nothing.
2131    fn scan_php_heredoc(&mut self, start: usize) -> usize {
2132        let bytes = self.source;
2133        let Some((label, body, nowdoc)) = php_heredoc_header(bytes, start) else {
2134            return start + 1;
2135        };
2136        if let Some(end) = php_heredoc_end(bytes, body, label) {
2137            return end;
2138        }
2139        self.error(
2140            "unterminated-string",
2141            if nowdoc {
2142                "unterminated PHP nowdoc"
2143            } else {
2144                "unterminated PHP heredoc"
2145            },
2146            ByteSpan::new(start, bytes.len()),
2147        );
2148        bytes.len()
2149    }
2150
2151    /// Ruby, from the first byte of the file.
2152    ///
2153    /// The whole scanner is one state machine over three states — see
2154    /// [`RubyState`] — because four of Ruby's tokens are spelled with a byte
2155    /// that is also an operator, and only where the token stands decides
2156    /// which: `/` is a regular expression or a division, `%` a literal or a
2157    /// modulo, `?` a character literal or a ternary, and `<<` a here document
2158    /// or a shift. Ruby's own lexer answers those four questions from its
2159    /// `lex_state`, and these three states are that variable folded down to
2160    /// what the four questions actually read out of it.
2161    fn scan_ruby(&mut self) {
2162        let mut pending = Vec::new();
2163        let _ = self.scan_ruby_code(0, false, 0, &mut pending);
2164    }
2165
2166    /// Ruby code from `index`, to the end of the file — or, when
2167    /// `interpolation` is set, to the `}` that balances the `#{` the caller
2168    /// has just consumed. Returns where it stopped.
2169    ///
2170    /// `pending` is the here documents opened on the physical line being read
2171    /// and not yet given a body, in the order Ruby will consume them. It is one
2172    /// list for the whole line rather than one per nested scan because a header
2173    /// may stand inside an interpolation — `puts "#{ <<EOS }"` opens a here
2174    /// document whose body is the line *under* that one — and because Ruby
2175    /// takes the bodies in header order across the whole line, so an opener
2176    /// written before an interpolation and one written inside it queue
2177    /// together. The list is drained by whichever scan reaches the line break
2178    /// first, which is why it has to outlive the `}` this call returns from.
2179    fn scan_ruby_code(
2180        &mut self,
2181        mut index: usize,
2182        interpolation: bool,
2183        depth: usize,
2184        pending: &mut Vec<RubyHeredoc>,
2185    ) -> usize {
2186        if depth > 256 {
2187            self.error(
2188                "nesting-limit",
2189                "Ruby lexical nesting limit exceeded",
2190                ByteSpan::new(index, index),
2191            );
2192            return self.source.len();
2193        }
2194        let bytes = self.source;
2195        let offset = self.offset;
2196        let mut state = RubyState::Begin;
2197        let mut space_seen = false;
2198        let mut braces = usize::from(interpolation);
2199        /* NOTE: Where this call's own openers begin in the shared list. An
2200         * enclosing scan's entries sit in front of them and are that scan's to
2201         * report, which is what keeps one unterminated here document to one
2202         * diagnostic. */
2203        let base = pending.len();
2204        while index < bytes.len() && !self.stopped {
2205            match bytes[index] {
2206                b'#' => {
2207                    let end = line_end(bytes, index + 1);
2208                    self.add_comment(index, end, CommentKind::Line);
2209                    index = end;
2210                }
2211                b'=' if ruby_at_line_start(bytes, index, offset)
2212                    && ruby_embedded_document(bytes, index) =>
2213                {
2214                    let (end, closed) = ruby_embedded_document_end(bytes, index);
2215                    self.add_comment(index, end, CommentKind::Block);
2216                    if !closed {
2217                        self.error(
2218                            "unterminated-comment",
2219                            "unterminated Ruby embedded document",
2220                            ByteSpan::new(index, end),
2221                        );
2222                    }
2223                    index = end;
2224                    state = RubyState::Begin;
2225                    space_seen = false;
2226                }
2227                /* NOTE: Everything past the marker is the DATA section, which is
2228                 * not source and holds no comments. */
2229                b'_' if ruby_at_line_start(bytes, index, offset)
2230                    && ruby_data_marker(bytes, index) =>
2231                {
2232                    index = bytes.len();
2233                }
2234                b'\r' | b'\n' => {
2235                    index = consume_newline(bytes, index);
2236                    if !pending.is_empty() {
2237                        let opened = std::mem::take(pending);
2238                        match self.scan_ruby_heredoc_bodies(index, opened, depth + 1, pending) {
2239                            Some(end) => index = end,
2240                            None => {
2241                                index = bytes.len();
2242                                continue;
2243                            }
2244                        }
2245                    }
2246                    state = RubyState::Begin;
2247                    space_seen = false;
2248                    /* NOTE: The queue is drained above before a checkpoint is
2249                     * offered, so a restart here never lands inside a body a
2250                     * header on an earlier line asked for. The emptiness test
2251                     * says so locally rather than leaving it to that argument. */
2252                    if !interpolation && depth == 0 && pending.is_empty() {
2253                        self.add_safe_checkpoint(index);
2254                    }
2255                }
2256                b'\'' => {
2257                    index = self.scan_ruby_string(index, false, depth, pending);
2258                    state = RubyState::End;
2259                    space_seen = false;
2260                }
2261                b'"' | b'`' => {
2262                    index = self.scan_ruby_string(index, true, depth, pending);
2263                    state = RubyState::End;
2264                    space_seen = false;
2265                }
2266                b':' if starts(bytes, index, b"::") => {
2267                    index += 2;
2268                    state = RubyState::End;
2269                    space_seen = false;
2270                }
2271                b':' if matches!(bytes.get(index + 1), Some(b'\'' | b'"')) => {
2272                    index =
2273                        self.scan_ruby_string(index + 1, bytes[index + 1] == b'"', depth, pending);
2274                    state = RubyState::End;
2275                    space_seen = false;
2276                }
2277                b':' if bytes
2278                    .get(index + 1)
2279                    .is_some_and(|byte| ruby_symbol_head(*byte)) =>
2280                {
2281                    index = ruby_symbol_end(bytes, index);
2282                    state = RubyState::End;
2283                    space_seen = false;
2284                }
2285                b'?' => {
2286                    match ruby_character_literal_end(bytes, index) {
2287                        Some(end) if !matches!(state, RubyState::End | RubyState::Fname) => {
2288                            index = end;
2289                            state = RubyState::End;
2290                        }
2291                        _ => {
2292                            index += 1;
2293                            state = RubyState::Begin;
2294                        }
2295                    }
2296                    space_seen = false;
2297                }
2298                b'%' => {
2299                    match ruby_percent_header(bytes, index) {
2300                        Some(literal)
2301                            if ruby_percent_opens(
2302                                state,
2303                                space_seen,
2304                                bytes,
2305                                index,
2306                                literal.form,
2307                            ) =>
2308                        {
2309                            /* NOTE: `alias` and `undef` hold `EXPR_FNAME|EXPR_FITEM`
2310                             * across the whole statement rather than only up to
2311                             * the first name: `Ripper.lex` under Ruby 3.3.12
2312                             * reports that state again after the `)` of the
2313                             * first symbol, which is what makes
2314                             * `alias%s(a)%s(b # c)` two symbols and not one
2315                             * symbol and a modulo. */
2316                            let fitem = state == RubyState::Fname && literal.form == b's';
2317                            index = self.scan_ruby_percent(index, &literal, depth, pending);
2318                            state = if fitem {
2319                                RubyState::Fname
2320                            } else {
2321                                RubyState::End
2322                            };
2323                        }
2324                        _ => {
2325                            index += 1;
2326                            state = RubyState::Begin;
2327                        }
2328                    }
2329                    space_seen = false;
2330                }
2331                b'/' => {
2332                    if ruby_literal_opens(state, space_seen, bytes, index) {
2333                        index = self.scan_ruby_regexp(index, depth, pending);
2334                        state = RubyState::End;
2335                    } else {
2336                        index += 1;
2337                        state = RubyState::Begin;
2338                    }
2339                    space_seen = false;
2340                }
2341                b'<' if starts(bytes, index, b"<<") => {
2342                    match ruby_heredoc_header(bytes, index) {
2343                        Some((heredoc, end)) if ruby_heredoc_may_open(state, space_seen) => {
2344                            pending.push(heredoc);
2345                            index = end;
2346                            state = RubyState::End;
2347                        }
2348                        _ => {
2349                            index += 2;
2350                            state = RubyState::Begin;
2351                        }
2352                    }
2353                    space_seen = false;
2354                }
2355                b'$' => {
2356                    index = ruby_global_end(bytes, index);
2357                    state = RubyState::End;
2358                    space_seen = false;
2359                }
2360                b'@' => {
2361                    index = ruby_at_variable_end(bytes, index);
2362                    state = RubyState::End;
2363                    space_seen = false;
2364                }
2365                b'{' => {
2366                    braces += 1;
2367                    index += 1;
2368                    state = RubyState::Begin;
2369                    space_seen = false;
2370                }
2371                b'}' => {
2372                    index += 1;
2373                    if interpolation {
2374                        braces -= 1;
2375                        if braces == 0 {
2376                            return index;
2377                        }
2378                    }
2379                    state = RubyState::End;
2380                    space_seen = false;
2381                }
2382                b'(' | b'[' => {
2383                    index += 1;
2384                    state = RubyState::Begin;
2385                    space_seen = false;
2386                }
2387                b')' | b']' => {
2388                    index += 1;
2389                    state = RubyState::End;
2390                    space_seen = false;
2391                }
2392                /* NOTE: The method-call dot, which is also the two range
2393                 * operators. All three want the byte after them read as a name
2394                 * rather than as a literal delimiter, which is what `End` says. */
2395                b'.' => {
2396                    index += 1;
2397                    state = RubyState::End;
2398                    space_seen = false;
2399                }
2400                /* NOTE: Outside a literal a backslash only continues the line,
2401                 * which is white space to the grammar. No checkpoint is emitted
2402                 * for the break it swallows, because the statement runs on past
2403                 * it and a scan restarted there would not know that. */
2404                b'\\' if matches!(bytes.get(index + 1), Some(b'\r' | b'\n')) => {
2405                    index = consume_newline(bytes, index + 1);
2406                    space_seen = true;
2407                }
2408                b'\\' => {
2409                    index = (index + 2).min(bytes.len());
2410                    state = RubyState::End;
2411                    space_seen = false;
2412                }
2413                byte if byte.is_ascii_digit() => {
2414                    index = ruby_number_end(bytes, index);
2415                    state = RubyState::End;
2416                    space_seen = false;
2417                }
2418                byte if ruby_identifier_start(byte) => {
2419                    let start = index;
2420                    index = ruby_word_end(bytes, index);
2421                    state = ruby_state_after_word(&bytes[start..index]);
2422                    space_seen = false;
2423                }
2424                byte if ruby_is_space(byte) => {
2425                    index += 1;
2426                    space_seen = true;
2427                }
2428                _ => {
2429                    index += 1;
2430                    state = RubyState::Begin;
2431                    space_seen = false;
2432                }
2433            }
2434        }
2435        /* NOTE: A here document opened on a last line that has no break of its
2436         * own never reaches [`Self::scan_ruby_heredoc_bodies`], because that is
2437         * driven from the break. It is unterminated all the same, and is
2438         * reported from its own `<<` with the span that call would have given
2439         * it. Only this call's own openers are reported here — the ones from
2440         * `base` on — because an enclosing scan reports its own, and the list is
2441         * cut back to `base` so that it reports them once. Nothing is left to
2442         * report whenever the loop stopped at a checkpoint instead, because a
2443         * break empties the list before one is offered. */
2444        if let Some(operator) = pending.get(base).map(|heredoc| heredoc.operator) {
2445            self.error(
2446                "unterminated-heredoc",
2447                "unterminated Ruby here document",
2448                ByteSpan::new(operator, bytes.len()),
2449            );
2450            pending.truncate(base);
2451        }
2452        if interpolation {
2453            self.error(
2454                "unterminated-interpolation",
2455                "unterminated Ruby interpolation",
2456                ByteSpan::new(index, index),
2457            );
2458        }
2459        index
2460    }
2461
2462    /// One Ruby string, from its delimiter: `'`, `"`, or the backtick of a
2463    /// command literal.
2464    ///
2465    /// A single-quoted string escapes only `\'` and `\\`, and every other
2466    /// backslash is a byte of it — but the byte after a backslash can never be
2467    /// the closing quote unless the pair *is* the `\'` escape, so skipping two
2468    /// finds the same closer either way. The other two take the full escape set
2469    /// and interpolate, and `#{ ... }` holds an expression, so a comment
2470    /// written inside one is a comment. None of the three ends at a line break,
2471    /// so only the end of the file leaves one unterminated.
2472    fn scan_ruby_string(
2473        &mut self,
2474        start: usize,
2475        interpolates: bool,
2476        depth: usize,
2477        pending: &mut Vec<RubyHeredoc>,
2478    ) -> usize {
2479        let bytes = self.source;
2480        let quote = bytes[start];
2481        let mut index = start + 1;
2482        while index < bytes.len() {
2483            if bytes[index] == b'\\' {
2484                index = (index + 2).min(bytes.len());
2485            } else if bytes[index] == quote {
2486                return index + 1;
2487            } else if interpolates && starts(bytes, index, b"#{") {
2488                index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
2489            } else {
2490                index += 1;
2491            }
2492        }
2493        self.error(
2494            "unterminated-string",
2495            match quote {
2496                b'\'' => "unterminated Ruby single-quoted string",
2497                b'"' => "unterminated Ruby double-quoted string",
2498                _ => "unterminated Ruby backtick string",
2499            },
2500            ByteSpan::new(start, index),
2501        );
2502        index
2503    }
2504
2505    /// One `/ ... /` regular expression, from its opening slash.
2506    ///
2507    /// A `[` opens a character class, where the delimiter is one of the
2508    /// pattern's own bytes. That is deliberately more forgiving than Ruby's own
2509    /// `tokadd_string`, which ends the literal at the first unescaped `/`
2510    /// wherever it stands: reading `/[/]/` as one literal keeps the rest of the
2511    /// line inside it, which hides bytes from a removal rather than exposing
2512    /// them, and it is the reading a person writing the pattern meant. A
2513    /// pattern interpolates and may span lines, so only the end of the file
2514    /// leaves one unterminated.
2515    fn scan_ruby_regexp(
2516        &mut self,
2517        start: usize,
2518        depth: usize,
2519        pending: &mut Vec<RubyHeredoc>,
2520    ) -> usize {
2521        let bytes = self.source;
2522        let mut index = start + 1;
2523        let mut in_class = false;
2524        while index < bytes.len() {
2525            match bytes[index] {
2526                b'\\' => index = (index + 2).min(bytes.len()),
2527                b'[' => {
2528                    in_class = true;
2529                    index += 1;
2530                }
2531                b']' => {
2532                    in_class = false;
2533                    index += 1;
2534                }
2535                b'/' if !in_class => return ruby_regexp_flags_end(bytes, index + 1),
2536                b'#' if starts(bytes, index, b"#{") => {
2537                    index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
2538                }
2539                _ => index += 1,
2540            }
2541        }
2542        self.error(
2543            "unterminated-string",
2544            "unterminated Ruby regular expression",
2545            ByteSpan::new(start, index),
2546        );
2547        index
2548    }
2549
2550    /// One `%` literal, from the `%` itself, with the header
2551    /// [`ruby_percent_header`] read out of it.
2552    ///
2553    /// A paired delimiter nests, which is what lets `%w[a [b] c]` hold a
2554    /// bracket; every other delimiter closes with itself and cannot. The
2555    /// interpolating forms read `#{ ... }` as an expression before either
2556    /// delimiter is considered, so the braces of one never count towards a
2557    /// `%Q{...}` nesting depth.
2558    fn scan_ruby_percent(
2559        &mut self,
2560        start: usize,
2561        literal: &RubyPercent,
2562        depth: usize,
2563        pending: &mut Vec<RubyHeredoc>,
2564    ) -> usize {
2565        let bytes = self.source;
2566        let mut index = literal.content;
2567        let mut nesting = 1usize;
2568        while index < bytes.len() {
2569            if bytes[index] == b'\\' {
2570                index = (index + 2).min(bytes.len());
2571                continue;
2572            }
2573            if literal.interpolates && starts(bytes, index, b"#{") {
2574                index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
2575                continue;
2576            }
2577            if literal.open != literal.close && bytes[index] == literal.open {
2578                nesting += 1;
2579                index += 1;
2580                continue;
2581            }
2582            if bytes[index] == literal.close {
2583                nesting -= 1;
2584                index += 1;
2585                if nesting == 0 {
2586                    return if literal.form == b'r' {
2587                        ruby_regexp_flags_end(bytes, index)
2588                    } else {
2589                        index
2590                    };
2591                }
2592                continue;
2593            }
2594            index += 1;
2595        }
2596        self.error(
2597            "unterminated-string",
2598            "unterminated Ruby percent literal",
2599            ByteSpan::new(start, index),
2600        );
2601        index
2602    }
2603
2604    /// Every here document opened on the line that has just ended, in the order
2605    /// they were opened, from the first byte of the line under it.
2606    ///
2607    /// `None` once one of them runs out of file, which is reported from the
2608    /// `<<` that opened it rather than from the line it swallowed. `pending` is
2609    /// the shared queue the caller has just emptied into `heredocs`; a body line
2610    /// that opens another here document fills it again, and this call reads that
2611    /// one from under that body line before the body around it resumes.
2612    fn scan_ruby_heredoc_bodies(
2613        &mut self,
2614        mut index: usize,
2615        heredocs: Vec<RubyHeredoc>,
2616        depth: usize,
2617        pending: &mut Vec<RubyHeredoc>,
2618    ) -> Option<usize> {
2619        for heredoc in heredocs {
2620            match self.scan_ruby_heredoc_body(index, &heredoc, depth, pending) {
2621                Some(end) => index = end,
2622                None => {
2623                    self.error(
2624                        "unterminated-heredoc",
2625                        "unterminated Ruby here document",
2626                        ByteSpan::new(heredoc.operator, self.source.len()),
2627                    );
2628                    return None;
2629                }
2630            }
2631        }
2632        Some(index)
2633    }
2634
2635    /// The body of one here document, from the first byte of a line of it.
2636    ///
2637    /// Returns the offset just past the terminator line. The body is opaque:
2638    /// only `#{ ... }` in an interpolating form is read as code, and the
2639    /// terminator is looked for at the start of each of the body's own lines,
2640    /// so one written inside an interpolation is content like the rest of it.
2641    ///
2642    /// A body line is a physical line, so a here document header reached
2643    /// through one of those interpolations queues a body for the line beneath
2644    /// *it* — Ruby 3.3.12 reads `puts <<A` / `x #{<<B}` / `A` / `B` with `A` as
2645    /// B's body and not as A's terminator — and this loop drains the queue at
2646    /// each line break before looking for its own terminator again.
2647    fn scan_ruby_heredoc_body(
2648        &mut self,
2649        mut index: usize,
2650        heredoc: &RubyHeredoc,
2651        depth: usize,
2652        pending: &mut Vec<RubyHeredoc>,
2653    ) -> Option<usize> {
2654        let bytes = self.source;
2655        loop {
2656            if index >= bytes.len() {
2657                return None;
2658            }
2659            if ruby_heredoc_terminates(bytes, index, heredoc) {
2660                return Some(consume_newline(bytes, line_end(bytes, index)).min(bytes.len()));
2661            }
2662            while index < bytes.len() && !matches!(bytes[index], b'\r' | b'\n') {
2663                if heredoc.interpolates && bytes[index] == b'\\' {
2664                    index = if starts(bytes, index, b"\\\r\n") {
2665                        index + 3
2666                    } else {
2667                        (index + 2).min(bytes.len())
2668                    };
2669                } else if heredoc.interpolates && starts(bytes, index, b"#{") {
2670                    index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
2671                } else {
2672                    index += 1;
2673                }
2674            }
2675            if index >= bytes.len() {
2676                return None;
2677            }
2678            index = consume_newline(bytes, index);
2679            /* NOTE: A body line is a physical line like any other, so a header
2680             * reached through an interpolation on it queues for the line under
2681             * it and is read there — before this body resumes. */
2682            if !pending.is_empty() {
2683                let opened = std::mem::take(pending);
2684                index = self.scan_ruby_heredoc_bodies(index, opened, depth + 1, pending)?;
2685            }
2686        }
2687    }
2688
2689    /// One Zig source file (Zig Language Reference: Comments, Doc comments,
2690    /// String Literals).
2691    ///
2692    /// Zig has no block comment at all — `/*` is the division operator and
2693    /// then multiplication, which `std.zig.Tokenizer` reports as `slash` and
2694    /// `asterisk` — so this is its own small lexer rather than a
2695    /// [`Self::scan_c_family`] with one delimiter taken away. Everything it has
2696    /// to know ends at a line break: a comment runs to the end of its line, a
2697    /// quoted literal may not cross one, and a multiline string literal is one
2698    /// line of content at a time. That is what makes every line start a
2699    /// restart point with nothing to carry across it.
2700    fn scan_zig(&mut self) {
2701        let bytes = self.source;
2702        let mut index = 0;
2703        while index < bytes.len() && !self.stopped {
2704            if starts(bytes, index, b"//") {
2705                let end = line_end(bytes, index + 2);
2706                self.add_comment(index, end, zig_line_kind(bytes, index));
2707                index = end;
2708                continue;
2709            }
2710            /* NOTE: `\\` is the whole opener of a multiline string literal line,
2711             * and the tokenizer takes it wherever a token may begin rather
2712             * than only as the first thing on a line: `const b = \\text` is
2713             * one `multiline_string_literal_line` to `std.zig.Tokenizer` just
2714             * as an indented `\\` is. Everything to the end of the line is
2715             * content, and the next line starts in code again, so consecutive
2716             * lines are separate tokens that the parser joins. A single `\` is
2717             * an invalid token to Zig and an ordinary byte here: nothing it
2718             * could open is a state a comment can hide in. */
2719            if starts(bytes, index, b"\\\\") {
2720                index = line_end(bytes, index + 2);
2721                continue;
2722            }
2723            match bytes[index] {
2724                b'"' | b'\'' => index = self.scan_zig_quoted(index),
2725                b'\r' | b'\n' => {
2726                    index = consume_newline(bytes, index);
2727                    self.add_safe_checkpoint(index);
2728                }
2729                _ => index += 1,
2730            }
2731        }
2732    }
2733
2734    /// One Zig string or character literal beginning at its quote.
2735    ///
2736    /// The two are one rule: `.string_literal` and `.char_literal` of
2737    /// `std.zig.Tokenizer` differ only in the quote that closes them. A `\`
2738    /// carries the next byte into the literal, and a real line terminator ends
2739    /// neither — the tokenizer marks the token `invalid` at it — so a quote
2740    /// that never closes is reported at the line break rather than swallowing
2741    /// the lines below it. A `\` in front of that terminator does not carry it
2742    /// either, for the same reason.
2743    ///
2744    /// `@"quoted identifier"` needs no rule of its own: the `@` is an ordinary
2745    /// byte and the identifier that follows it is lexed as the string literal
2746    /// it is spelled as, which is what hides a `//` written inside one.
2747    fn scan_zig_quoted(&mut self, start: usize) -> usize {
2748        let bytes = self.source;
2749        let quote = bytes[start];
2750        let message = if quote == b'"' {
2751            "unterminated Zig string"
2752        } else {
2753            "unterminated Zig character literal"
2754        };
2755        let mut index = start + 1;
2756        while index < bytes.len() {
2757            if bytes[index] == b'\\' && !matches!(bytes.get(index + 1), None | Some(b'\r' | b'\n'))
2758            {
2759                index += 2;
2760            } else if bytes[index] == quote {
2761                return index + 1;
2762            } else if matches!(bytes[index], b'\r' | b'\n') {
2763                self.error("unterminated-string", message, ByteSpan::new(start, index));
2764                return index;
2765            } else {
2766                index += 1;
2767            }
2768        }
2769        self.error("unterminated-string", message, ByteSpan::new(start, index));
2770        index
2771    }
2772
2773    /// One R script (R Language Definition, 10 Parser; `?Quotes`).
2774    ///
2775    /// `#` opens a comment that runs to the end of the line and that is the
2776    /// whole comment grammar — there is no block form and no nesting. What
2777    /// makes the scanner more than a search for `#` is the four literals that
2778    /// carry one as content: a quoted string, a raw string, a backquoted name,
2779    /// and the `%...%` operator.
2780    ///
2781    /// Three of the four may cross a line break, so the start of a line is a
2782    /// restart point only when the scan reaches it here, at the top level. The
2783    /// fourth may not: `SpecialValue` in `gram.y` returns `ERROR` at a newline,
2784    /// which is why an unterminated `%` is reported where it is rather than
2785    /// swallowing the rest of the file.
2786    ///
2787    /// Measured against the interpreter over the 42 `.R` files the R 4.3.3
2788    /// distribution ships — its `demo/`, `doc/` and `share/R/` scripts — every
2789    /// one of the 1,330 comments `utils::getParseData` reports as a `COMMENT`
2790    /// token comes back here with the same byte span, and no file is called
2791    /// invalid.
2792    fn scan_r(&mut self) {
2793        let bytes = self.source;
2794        let mut index = 0;
2795        while index < bytes.len() && !self.stopped {
2796            match bytes[index] {
2797                b'#' => {
2798                    let end = line_end(bytes, index + 1);
2799                    self.add_comment(index, end, r_line_kind(bytes, index));
2800                    index = end;
2801                }
2802                b'"' | b'\'' => index = self.scan_r_string(index),
2803                b'`' => index = self.scan_r_name(index),
2804                b'%' => index = self.scan_r_operator(index),
2805                b'\r' | b'\n' => {
2806                    index = consume_newline(bytes, index);
2807                    self.add_safe_checkpoint(index);
2808                }
2809                _ => index += 1,
2810            }
2811        }
2812    }
2813
2814    /// One R string beginning at its quote, raw or not.
2815    ///
2816    /// A raw string is the quote behind an `r` or an `R` that begins a token
2817    /// ([`r_raw_string`]); everything else is the ordinary form, which takes
2818    /// `\` escapes and carries a line break as content, so only the matching
2819    /// quote or the end of the file ends one.
2820    ///
2821    /// Where the bytes look like a raw string and are not one — `r"<a>"`, whose
2822    /// delimiter is not a bracket — R refuses the file outright with `malformed
2823    /// raw string literal`. Falling back to the ordinary reading is what this
2824    /// does instead: it is the same fallback [`cpp_raw_string`] takes, and it
2825    /// hides the `#` behind a quote rather than exposing it in a file no
2826    /// interpreter would run.
2827    fn scan_r_string(&mut self, start: usize) -> usize {
2828        let bytes = self.source;
2829        if let Some((end, closed)) = r_raw_string(bytes, start) {
2830            if !closed {
2831                self.error(
2832                    "unterminated-string",
2833                    "unterminated R raw string",
2834                    ByteSpan::new(start - 1, end),
2835                );
2836            }
2837            return end;
2838        }
2839        let (end, closed) = r_delimited_end(bytes, start + 1, bytes[start]);
2840        if !closed {
2841            self.error(
2842                "unterminated-string",
2843                "unterminated R string",
2844                ByteSpan::new(start, end),
2845            );
2846        }
2847        end
2848    }
2849
2850    /// One backquoted name beginning at its backquote.
2851    ///
2852    /// It is a quoted string in every lexical respect: `\` carries the next
2853    /// byte into it, a line break is content, and only the closing backquote
2854    /// ends it. What it is not is a string constant, so an unterminated one is
2855    /// reported as the identifier it was going to be.
2856    fn scan_r_name(&mut self, start: usize) -> usize {
2857        let bytes = self.source;
2858        let (end, closed) = r_delimited_end(bytes, start + 1, b'`');
2859        if !closed {
2860            self.error(
2861                "unterminated-identifier",
2862                "unterminated R backquoted name",
2863                ByteSpan::new(start, end),
2864            );
2865        }
2866        end
2867    }
2868
2869    /// One `%...%` operator beginning at its first `%`.
2870    ///
2871    /// `SpecialValue` (`gram.y`) pushes every byte up to the next `%` into the
2872    /// operator's name and returns `ERROR` when a line break arrives first, so
2873    /// the name may hold a `#`, a quote or a backquote, takes no escapes, and
2874    /// cannot cross a line. `%%` and `%in%` are the same rule with nothing
2875    /// interesting inside them.
2876    fn scan_r_operator(&mut self, start: usize) -> usize {
2877        let bytes = self.source;
2878        let stop = line_end(bytes, start + 1);
2879        match memchr(b'%', &bytes[start + 1..stop]) {
2880            Some(relative) => start + relative + 2,
2881            None => {
2882                self.error(
2883                    "unterminated-operator",
2884                    "unterminated R special operator",
2885                    ByteSpan::new(start, stop),
2886                );
2887                stop
2888            }
2889        }
2890    }
2891
2892    /// One Dart compilation unit (Dart Language Specification, 17.1 Comments
2893    /// and 17.6 Strings).
2894    ///
2895    /// Dart is a C-family syntax with three departures that decide the shape of
2896    /// this scanner rather than of [`Self::scan_c_family`]:
2897    ///
2898    /// * its block comment *nests*, so `/* /* */ */` is one comment;
2899    /// * `//!` and `/*!` document nothing — Dart's only markers are `///` and
2900    ///   `/**` — while `////` still does ([`dart_line_kind`]); and
2901    /// * `#!` at the very first byte is the script tag, and `#` is the
2902    ///   symbol-literal operator everywhere else.
2903    ///
2904    /// A string is the one construct that hides a comment opener, and Dart
2905    /// writes six of them: either quote, single-line or triple-quoted, raw or
2906    /// not. A raw one is the quote behind an `r`, and only where that `r`
2907    /// begins a token ([`dart_raw_string_prefix`]).
2908    ///
2909    /// The start of a line is a restart point only when the scan reaches it
2910    /// here, at the top level: a triple-quoted string, a nested block comment,
2911    /// and an interpolation with a comment inside it all carry a line break.
2912    ///
2913    /// Ground truth for every rule below is `scanString` of
2914    /// `package:_fe_analyzer_shared` as the Dart SDK 3.13.2 ships it, read for
2915    /// token kinds and offsets, with `dart analyze` for acceptance.
2916    ///
2917    /// Measured against that scanner over the 3,143 `.dart` files of the SDK's
2918    /// own `lib/` and of the packages `dart pub get` fetched beside it: all
2919    /// 147,988 comments it reports — its comment stream plus the `SCRIPT_TAG`
2920    /// token, with its UTF-16 offsets mapped back to bytes — come back here
2921    /// with the same byte span, and no file is called invalid.
2922    fn scan_dart(&mut self) {
2923        let bytes = self.source;
2924        let mut index = 0;
2925        while index < bytes.len() && !self.stopped {
2926            /* NOTE: `tokenizeTag` reads a `#!` line only when `scanOffset == 0`, and
2927             * a comment on the line above one is enough to take that away, so
2928             * this is the first byte of the document and no other. `#`
2929             * elsewhere is the operator that opens a symbol literal — `#foo`,
2930             * `#+` — which needs no rule of its own because nothing it can be
2931             * followed by hides a comment. */
2932            if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
2933                let end = line_end(bytes, index + 2);
2934                self.add_comment(index, end, CommentKind::Line);
2935                index = end;
2936                continue;
2937            }
2938            if starts(bytes, index, b"//") {
2939                let end = line_end(bytes, index + 2);
2940                self.add_comment(index, end, dart_line_kind(bytes, index));
2941                index = end;
2942                continue;
2943            }
2944            if starts(bytes, index, b"/*") {
2945                let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
2946                self.add_comment(index, end, dart_block_kind(bytes, index));
2947                if !closed {
2948                    self.error(
2949                        "unterminated-comment",
2950                        "unterminated Dart block comment",
2951                        ByteSpan::new(index, end),
2952                    );
2953                }
2954                index = end;
2955                continue;
2956            }
2957            match bytes[index] {
2958                b'"' | b'\'' => index = self.scan_dart_string(index, 0),
2959                b'\r' | b'\n' => {
2960                    index = consume_newline(bytes, index);
2961                    self.add_safe_checkpoint(index);
2962                }
2963                _ => index += 1,
2964            }
2965        }
2966    }
2967
2968    /// One Dart string beginning at its opening quote.
2969    ///
2970    /// The six forms are one rule with two switches. `triple` is three of the
2971    /// same quote, which makes a line break content instead of the end of the
2972    /// literal; `raw` is the `r` in front of it, which takes away both the `\`
2973    /// escape and `${` interpolation. A `\` in an ordinary string carries the
2974    /// next byte in — that is what hides a `\'` — but it does not carry a line
2975    /// terminator: `tokenizeSingleLineString` leaves `.string_literal` at the
2976    /// break either way, so `'x\<newline>y'` is an unterminated string and not
2977    /// a continuation.
2978    ///
2979    /// The closing delimiter is the first unescaped run of it, not the last:
2980    /// `''''x''''` is `'''` + `'x` + `'''` and then one quote left over, which
2981    /// is what the Dart scanner reports for those bytes.
2982    fn scan_dart_string(&mut self, quote: usize, depth: usize) -> usize {
2983        if depth > 256 {
2984            self.error(
2985                "nesting-limit",
2986                "Dart string interpolation nesting limit exceeded",
2987                ByteSpan::new(quote, quote),
2988            );
2989            return self.source.len();
2990        }
2991        let bytes = self.source;
2992        let raw = dart_raw_string_prefix(bytes, quote);
2993        let start = if raw { quote - 1 } else { quote };
2994        let triple = bytes.get(quote + 1) == Some(&bytes[quote])
2995            && bytes.get(quote + 2) == Some(&bytes[quote]);
2996        let width = if triple { 3 } else { 1 };
2997        let delimiter = &bytes[quote..quote + width];
2998        let mut index = quote + width;
2999        while index < bytes.len() {
3000            if bytes[index..].starts_with(delimiter) {
3001                return index + width;
3002            }
3003            if !raw
3004                && bytes[index] == b'\\'
3005                && !matches!(bytes.get(index + 1), None | Some(b'\r' | b'\n'))
3006            {
3007                index += 2;
3008                continue;
3009            }
3010            if !raw && starts(bytes, index, b"${") {
3011                index = self.scan_dart_interpolation(index + 2, depth + 1);
3012                continue;
3013            }
3014            if !triple && matches!(bytes[index], b'\r' | b'\n') {
3015                self.error(
3016                    "unterminated-string",
3017                    dart_unterminated_string(raw, triple),
3018                    ByteSpan::new(start, index),
3019                );
3020                return index;
3021            }
3022            index += 1;
3023        }
3024        self.error(
3025            "unterminated-string",
3026            dart_unterminated_string(raw, triple),
3027            ByteSpan::new(start, index),
3028        );
3029        index
3030    }
3031
3032    /// One `${ ... }` interpolation, beginning past its `${`.
3033    ///
3034    /// The braces of the expression are counted rather than searched for,
3035    /// because the expression is code: a nested string, a map literal, and a
3036    /// comment may all stand inside one. A comment written there is a comment —
3037    /// the Dart scanner attaches it to the token that follows, exactly as it
3038    /// does outside a string — and a `//` one runs to the end of its line while
3039    /// the string it sits inside carries on below.
3040    fn scan_dart_interpolation(&mut self, mut index: usize, depth: usize) -> usize {
3041        if depth > 256 {
3042            self.error(
3043                "nesting-limit",
3044                "Dart string interpolation nesting limit exceeded",
3045                ByteSpan::new(index, index),
3046            );
3047            return self.source.len();
3048        }
3049        let bytes = self.source;
3050        let mut braces = 1usize;
3051        while index < bytes.len() {
3052            if starts(bytes, index, b"//") {
3053                let end = line_end(bytes, index + 2);
3054                self.add_comment(index, end, dart_line_kind(bytes, index));
3055                index = end;
3056                continue;
3057            }
3058            if starts(bytes, index, b"/*") {
3059                let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
3060                self.add_comment(index, end, dart_block_kind(bytes, index));
3061                if !closed {
3062                    self.error(
3063                        "unterminated-comment",
3064                        "unterminated Dart block comment",
3065                        ByteSpan::new(index, end),
3066                    );
3067                }
3068                index = end;
3069                continue;
3070            }
3071            match bytes[index] {
3072                b'"' | b'\'' => index = self.scan_dart_string(index, depth + 1),
3073                b'{' => {
3074                    braces += 1;
3075                    index += 1;
3076                }
3077                b'}' => {
3078                    braces -= 1;
3079                    index += 1;
3080                    if braces == 0 {
3081                        return index;
3082                    }
3083                }
3084                _ => index += 1,
3085            }
3086        }
3087        self.error(
3088            "unterminated-template-expression",
3089            "unterminated Dart string interpolation",
3090            ByteSpan::new(index, index),
3091        );
3092        index
3093    }
3094
3095    /// One Swift source file (The Swift Programming Language, Lexical
3096    /// Structure: Comments, String Literals, Regular Expression Literals).
3097    ///
3098    /// Swift is a C-family syntax with four departures that decide the shape of
3099    /// this scanner rather than of [`Self::scan_c_family`]:
3100    ///
3101    /// * its block comment *nests*, so `/* /* */ */` is one comment;
3102    /// * `//!` and `/*!` document nothing — Swift's markers are `///` and
3103    ///   `/**` — while `////` still documents ([`swift_line_kind`]) and the
3104    ///   empty `/**/` does not ([`swift_block_kind`]);
3105    /// * `'` is not a delimiter at all, so an apostrophe hides nothing; and
3106    /// * a `/` may open a regular expression literal, which is the one
3107    ///   construct here that can carry a `//` without a quote in front of it.
3108    ///
3109    /// A string is the other construct that hides a comment opener, and Swift
3110    /// writes it four ways: single-line or multi-line, each raw or not, where
3111    /// raw is a run of `#` in front of the quote that also has to come back
3112    /// behind it ([`Self::scan_swift_string`]).
3113    ///
3114    /// The start of a line is a restart point only when the scan reaches it
3115    /// here, at the top level: a multi-line string, a nested block comment, an
3116    /// extended regular expression literal and an interpolation with a comment
3117    /// inside it all carry a line break.
3118    ///
3119    /// Ground truth for every rule below is the SwiftSyntax parser the Swift
3120    /// 6.3.3 toolchain ships (`SwiftParser.Parser.parse`, read for token kinds
3121    /// and the `lineComment`/`blockComment`/`docLineComment`/`docBlockComment`
3122    /// trivia and their UTF-8 offsets), with `swift-frontend -dump-parse
3123    /// -swift-version 6` for what the compiler makes of the same bytes.
3124    ///
3125    /// Measured against that parser over the 3,962 Swift files of swift-syntax,
3126    /// swift-format, swift-nio, swift-algorithms, swift-collections,
3127    /// swift-experimental-string-processing, swift-protobuf,
3128    /// swift-composable-architecture and the toolchain's own module interfaces:
3129    /// all 207,959 comments it reports come back here with the same byte span
3130    /// and the same kind, and no file is called invalid.
3131    fn scan_swift(&mut self) {
3132        let bytes = self.source;
3133        let mut index = 0;
3134        while index < bytes.len() && !self.stopped {
3135            /* NOTE: SwiftSyntax reports a `shebang` token only at offset 0; the
3136             * same `#!` on line 2 comes back as `pound`, `exclamationMark` and
3137             * two operators. `#` elsewhere opens a compiler directive, a raw
3138             * string, or an extended regular expression literal, and the last
3139             * two are the ones that hide anything. */
3140            if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
3141                let end = line_end(bytes, index + 2);
3142                self.add_comment(index, end, CommentKind::Line);
3143                index = end;
3144                continue;
3145            }
3146            if let Some(end) = self.scan_swift_lexeme(index, 0) {
3147                index = end;
3148                continue;
3149            }
3150            if matches!(bytes[index], b'\r' | b'\n') {
3151                index = consume_newline(bytes, index);
3152                self.add_safe_checkpoint(index);
3153            } else {
3154                index += 1;
3155            }
3156        }
3157    }
3158
3159    /// One Swift comment, string, or regular expression literal beginning at
3160    /// `index`, or `None` when the byte there opens none of them.
3161    ///
3162    /// This is the whole of Swift's lexical surface that a comment can hide
3163    /// behind or be, which is why the top level and the inside of an
3164    /// interpolation ask one function for it: an interpolation is code, and a
3165    /// comment written there is a comment exactly as it is outside one.
3166    fn scan_swift_lexeme(&mut self, index: usize, depth: usize) -> Option<usize> {
3167        let bytes = self.source;
3168        if starts(bytes, index, b"//") {
3169            let end = line_end(bytes, index + 2);
3170            self.add_comment(index, end, swift_line_kind(bytes, index));
3171            return Some(end);
3172        }
3173        if starts(bytes, index, b"/*") {
3174            let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
3175            self.add_comment(index, end, swift_block_kind(bytes, index));
3176            if !closed {
3177                self.error(
3178                    "unterminated-comment",
3179                    "unterminated Swift block comment",
3180                    ByteSpan::new(index, end),
3181                );
3182            }
3183            return Some(end);
3184        }
3185        if bytes[index] == b'/' {
3186            let mut reach = Reach::default();
3187            let regex = swift_bare_regex(bytes, index, &mut reach);
3188            self.consult(reach);
3189            return regex;
3190        }
3191        if bytes[index] == b'#' {
3192            let mut reach = Reach::default();
3193            let hashes = swift_hash_run(bytes, index, &mut reach);
3194            self.consult(reach);
3195            let opener = index + hashes;
3196            if bytes.get(opener) == Some(&b'/') {
3197                return Some(self.scan_swift_extended_regex(index, hashes));
3198            }
3199            if bytes.get(opener) == Some(&b'"') {
3200                return Some(self.scan_swift_string(index, hashes, depth));
3201            }
3202            /* NOTE: the run opens neither, and the whole of it is taken rather
3203             * than one byte of it: a shorter run inside this one ends at the
3204             * same byte, so if that byte is neither a quote nor a slash then no
3205             * suffix of the run opens a literal either. Taking it whole also
3206             * keeps a line of `#` from being re-read once per byte. */
3207            return Some(opener);
3208        }
3209        /* NOTE: `'` is no delimiter in the language — the Swift book's Lexical
3210         * Structure has no single-quoted literal and no character literal at
3211         * all — but it is one in the compiler, which lexes `'...'` as a
3212         * `singleQuote` string so that it can offer the fix-it that turns it
3213         * into a `"..."` one (`Lexer.Cursor.lexStringQuote`). A file holding
3214         * one is rejected by `swiftc` either way, and following the lexer
3215         * rather than the grammar is what keeps a `//` inside such a literal
3216         * from being read as a comment and removed out of a file that is
3217         * already broken. No `'` can stand in Swift code outside a string, a
3218         * comment or a regular expression literal, all three of which are
3219         * settled above, so this costs a valid file nothing. */
3220        if matches!(bytes[index], b'"' | b'\'') {
3221            return Some(self.scan_swift_string(index, 0, depth));
3222        }
3223        None
3224    }
3225
3226    /// One Swift string literal, beginning at the first `#` of a raw one and at
3227    /// the quote of any other.
3228    ///
3229    /// The four forms are one rule with two switches. `multiline` is three
3230    /// quotes, which makes a line break content instead of the end of the
3231    /// literal; `hashes` is the run of `#` in front of the quote, which is what
3232    /// makes a string raw. A raw string does not take away the escape and the
3233    /// interpolation — it *renames* them: with `n` hashes the escape is `\` and
3234    /// `n` hashes, so `\#n` is a newline and `\#(` opens an interpolation in a
3235    /// `#"..."#` literal while a bare `\(` is two characters of content. The
3236    /// closing delimiter is renamed with it, and needs the same `n` hashes
3237    /// behind the quote, which is what lets a raw string carry a quote.
3238    ///
3239    /// A `\` in an ordinary string carries the next byte in — that is what
3240    /// hides a `\"` — but it does not carry a line terminator, so `"x\` and a
3241    /// line break is an unterminated string rather than a continuation.
3242    fn scan_swift_string(&mut self, start: usize, hashes: usize, depth: usize) -> usize {
3243        if depth > 256 {
3244            self.error(
3245                "nesting-limit",
3246                "Swift string interpolation nesting limit exceeded",
3247                ByteSpan::new(start, start),
3248            );
3249            return self.source.len();
3250        }
3251        let bytes = self.source;
3252        let quote = start + hashes;
3253        let delimiter = bytes[quote];
3254        let multiline = delimiter == b'"' && swift_multiline_string(bytes, quote, hashes);
3255        let width = if multiline { 3 } else { 1 };
3256        let mut index = quote + width;
3257        while index < bytes.len() {
3258            if let Some(end) = swift_string_close(bytes, index, delimiter, multiline, hashes) {
3259                return end;
3260            }
3261            if bytes[index] == b'\\' && swift_hashes_at(bytes, index + 1, hashes) {
3262                let escaped = index + 1 + hashes;
3263                if bytes.get(escaped) == Some(&b'(') {
3264                    index = self.scan_swift_interpolation(escaped + 1, depth + 1);
3265                    continue;
3266                }
3267                if !matches!(bytes.get(escaped), None | Some(b'\r' | b'\n')) {
3268                    index = escaped + 1;
3269                    continue;
3270                }
3271            }
3272            if !multiline && matches!(bytes[index], b'\r' | b'\n') {
3273                self.error(
3274                    "unterminated-string",
3275                    swift_unterminated_string(delimiter, hashes > 0, multiline),
3276                    ByteSpan::new(start, index),
3277                );
3278                return index;
3279            }
3280            index += 1;
3281        }
3282        self.error(
3283            "unterminated-string",
3284            swift_unterminated_string(delimiter, hashes > 0, multiline),
3285            ByteSpan::new(start, index),
3286        );
3287        index
3288    }
3289
3290    /// One `\( ... )` interpolation, beginning past its opening parenthesis.
3291    ///
3292    /// The parentheses of the expression are counted rather than searched for,
3293    /// because the expression is code: a nested string, a tuple, a call, and a
3294    /// comment may all stand inside one. SwiftSyntax reports a comment written
3295    /// there as `lineComment` or `blockComment` trivia exactly as it does
3296    /// outside a string, and a `//` one runs to the end of its line while the
3297    /// multi-line string it sits inside carries on below.
3298    fn scan_swift_interpolation(&mut self, mut index: usize, depth: usize) -> usize {
3299        if depth > 256 {
3300            self.error(
3301                "nesting-limit",
3302                "Swift string interpolation nesting limit exceeded",
3303                ByteSpan::new(index, index),
3304            );
3305            return self.source.len();
3306        }
3307        let bytes = self.source;
3308        let mut parentheses = 1usize;
3309        while index < bytes.len() {
3310            if let Some(end) = self.scan_swift_lexeme(index, depth) {
3311                index = end;
3312                continue;
3313            }
3314            match bytes[index] {
3315                b'(' => {
3316                    parentheses += 1;
3317                    index += 1;
3318                }
3319                b')' => {
3320                    parentheses -= 1;
3321                    index += 1;
3322                    if parentheses == 0 {
3323                        return index;
3324                    }
3325                }
3326                _ => index += 1,
3327            }
3328        }
3329        self.error(
3330            "unterminated-interpolation",
3331            "unterminated Swift string interpolation",
3332            ByteSpan::new(index, index),
3333        );
3334        index
3335    }
3336
3337    /// One extended regular expression literal `#/ ... /#`, beginning at its
3338    /// first `#`.
3339    ///
3340    /// This is the form that may carry an unescaped `/`, which is exactly why
3341    /// it exists: `#/https://x/#` is a regular expression with a `//` in the
3342    /// middle of it, and a scanner that read that `//` as a comment would
3343    /// delete the rest of the line. The closing delimiter is a `/` and at least
3344    /// the run of `#` the opener carried — extra ones are taken with it — so a
3345    /// `/#` inside a `##/ ... /##` literal is content, and a `\` carries the
3346    /// byte behind it in: `#/a\/#b/#` closes at the *second* `/#`.
3347    ///
3348    /// It is also the only form that may span lines, and only when it opens
3349    /// one: SwiftSyntax's `RegexLiteralLexer` enters multi-line mode when the
3350    /// opener is followed by blanks and then a line terminator, and in every
3351    /// other case a line terminator inside the pattern ends the literal
3352    /// unterminated. A multi-line literal that never closes gives its lines
3353    /// back rather than swallowing the file — the lexer resumes at the first
3354    /// newline, "so we don't want to skip over what is likely otherwise valid
3355    /// Swift code" — and that is the one read here the scan rewinds behind, so
3356    /// it is the one this reports.
3357    fn scan_swift_extended_regex(&mut self, start: usize, hashes: usize) -> usize {
3358        let bytes = self.source;
3359        let mut reach = Reach::default();
3360        let opener = start + hashes + 1;
3361        let mut probe = opener;
3362        while matches!(bytes.get(probe), Some(b' ' | b'\t')) {
3363            probe += 1;
3364        }
3365        reach.byte(probe);
3366        let multiline = bytes
3367            .get(probe)
3368            .is_some_and(|byte| is_line_terminator(*byte));
3369        let mut index = if multiline { probe } else { opener };
3370        while index < bytes.len() {
3371            if !multiline && is_line_terminator(bytes[index]) {
3372                break;
3373            }
3374            if bytes[index] == b'\\' {
3375                /* NOTE: `lexPatternCharacter` reads the escaped byte through the
3376                 * same switch as an unescaped one, and its line-terminator arm
3377                 * does not ask whether an escape carried it there, so a `\`
3378                 * before a line break ends a single-line literal rather than
3379                 * joining the line under it on. */
3380                let escaped = index + 1;
3381                if !multiline
3382                    && bytes
3383                        .get(escaped)
3384                        .is_some_and(|byte| is_line_terminator(*byte))
3385                {
3386                    index = escaped;
3387                    break;
3388                }
3389                index = (escaped + 1).min(bytes.len());
3390                continue;
3391            }
3392            if bytes[index] == b'/' && swift_hashes_at(bytes, index + 1, hashes) {
3393                let mut end = index + 1;
3394                while bytes.get(end) == Some(&b'#') {
3395                    end += 1;
3396                }
3397                /* NOTE: the run of `#` is taken whole — `tryEatEnding` consumes
3398                 * every one of them — so the byte that ended it is read one past
3399                 * where the scan resumes, and an append at the end of the
3400                 * document is exactly the edit that would put another `#` there. */
3401                reach.byte(end);
3402                self.consult(reach);
3403                return end;
3404            }
3405            index += 1;
3406        }
3407        let end = if multiline {
3408            /* NOTE: the search crossed every line under the opener and the scan
3409             * takes them back, so an append at the end of the document — which
3410             * could close this literal — has to withdraw the checkpoint it
3411             * would otherwise reuse the whole prefix from. */
3412            reach.end_of(bytes);
3413            probe
3414        } else {
3415            reach.byte(index);
3416            index
3417        };
3418        self.consult(reach);
3419        self.error(
3420            "unterminated-regex",
3421            "unterminated Swift extended regular expression literal",
3422            ByteSpan::new(start, end),
3423        );
3424        end
3425    }
3426
3427    /// One C# source file (ECMA-334 6.3 Lexical analysis: comments, literals;
3428    /// 6.5 Pre-processing directives).
3429    ///
3430    /// C# is a C-family syntax with three departures that decide the shape of
3431    /// this scanner rather than of [`Self::scan_c_family`]:
3432    ///
3433    /// * a line whose first non-blank byte is `#` is a *pre-processing
3434    ///   directive*, and the rest of it is not ordinary code:
3435    ///   ([`Self::scan_csharp_directive`]);
3436    /// * a string is written eight ways — plain, verbatim, raw, and each of
3437    ///   those interpolated — and only the plain one takes a `\` escape
3438    ///   ([`Self::scan_csharp_string`]); and
3439    /// * an interpolation hole is code, so a comment written in one is a
3440    ///   comment ([`Self::scan_csharp_hole`]), while the format clause behind
3441    ///   its `:` is text again.
3442    ///
3443    /// The start of a line is a restart point only when the scan reaches it
3444    /// here, at the top level: a verbatim string, a multi-line raw string, a
3445    /// block comment and an interpolation hole all carry a line break.
3446    ///
3447    /// NOTE: a conditional section is scanned as ordinary code rather than
3448    /// skipped. Roslyn reports the body of an `#if` whose symbol is undefined
3449    /// as one `DisabledTextTrivia` blob and finds no comment in it, but which
3450    /// symbols a build defines is not in the file, and a comment inside `#if
3451    /// DEBUG` is a comment in every build that defines it. C and C++ read `#if
3452    /// 0` the same way here for the same reason.
3453    ///
3454    /// Ground truth for every rule below is the Roslyn lexer the .NET SDK
3455    /// 10.0.400 ships (`CSharpSyntaxTree.ParseText` with
3456    /// `LanguageVersion.Preview`, read for the comment, disabled-text and
3457    /// pre-processing-message trivia and their UTF-8 offsets, and for the token
3458    /// each literal was lexed as).
3459    ///
3460    /// Measured against that lexer over the 70,630 C# files of dotnet/runtime,
3461    /// dotnet/roslyn, dotnet/aspnetcore, dotnet/efcore, Newtonsoft.Json,
3462    /// Serilog and ImageSharp: all 1,946,012 comments it reports come back here
3463    /// with the same byte span and the same kind. Four of those files are
3464    /// called invalid — two are not C# at all, and two are the conditional
3465    /// section a `'` in prose costs, which
3466    /// `spec/fixtures/v1/hazards.json` records as
3467    /// `csharp-conditional-section-limitation`. 23,510 of the files were
3468    /// stripped of every comment and handed back to Roslyn, and all 650,360
3469    /// removals left a file that still parses with no error.
3470    fn scan_csharp(&mut self) {
3471        let bytes = self.source;
3472        /* NOTE: a byte order mark is consumed before the first line is read, so
3473         * the `#` behind one is still the first non-blank byte of its line and
3474         * still opens a directive — Roslyn reports the comment at the end of
3475         * `<BOM>#pragma warning disable 1591 // c` and this has to as well.
3476         * Only a full scan skips it: a suffix that happens to open with those
3477         * three bytes carries no mark, and the full scan of that document read
3478         * them as three ordinary bytes too. */
3479        let mut index = if self.offset == 0 {
3480            byte_order_mark_width(bytes)
3481        } else {
3482            0
3483        };
3484        /* NOTE: whether every byte of this line so far is blank, which is what
3485         * makes a `#` a directive rather than the bad token Roslyn reports at
3486         * CS1040. It is carried rather than searched for backwards because a
3487         * line of `#` would otherwise cost one scan of the line per byte, and
3488         * a restart begins at a line start, where `true` is the answer. */
3489        let mut blank_line = true;
3490        while index < bytes.len() && !self.stopped {
3491            /* NOTE: Roslyn reports a `ShebangDirectiveTrivia` for a `#!` at any
3492             * directive position and then raises CS9378 unless it stands at the
3493             * very first byte, which is also the only place `dotnet-script`
3494             * reads one. Only that first byte is the preamble here; the same
3495             * bytes lower down are a directive line whose message is opaque. */
3496            if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
3497                let end = csharp_line_end(bytes, index + 2);
3498                self.add_comment(index, end, CommentKind::Line);
3499                index = end;
3500                blank_line = false;
3501                continue;
3502            }
3503            if bytes[index] == b'#' {
3504                index = self.scan_csharp_directive(index, blank_line);
3505                blank_line = false;
3506                continue;
3507            }
3508            if let Some(end) = self.scan_csharp_lexeme(index, 0) {
3509                index = end;
3510                blank_line = false;
3511                continue;
3512            }
3513            if matches!(bytes[index], b'\r' | b'\n') {
3514                index = consume_newline(bytes, index);
3515                self.add_safe_checkpoint(index);
3516                blank_line = true;
3517                continue;
3518            }
3519            /* NOTE: a restart is offered after a `\r` or a `\n` alone, though
3520             * three more characters end a line to this lexer. The incremental
3521             * engine's line rules — the CRLF pair, the preamble window — are
3522             * written in those two bytes, and a checkpoint after a U+2028 would
3523             * be a line start only one of the two agreed on. Refusing it costs
3524             * a rescan the chance to begin there and nothing else. */
3525            if let Some(width) = csharp_unicode_line_terminator_width(bytes, index) {
3526                index += width;
3527                blank_line = true;
3528                continue;
3529            }
3530            blank_line = blank_line && is_csharp_blank(bytes[index]);
3531            index += 1;
3532        }
3533    }
3534
3535    /// One C# comment, string, or character literal beginning at `index`, or
3536    /// `None` when the byte there opens none of them.
3537    ///
3538    /// This is the whole of C#'s lexical surface that a comment can hide behind
3539    /// or be, which is why the top level and the inside of an interpolation
3540    /// hole ask one function for it: a hole is code, and a comment written
3541    /// there is a comment exactly as it is outside one.
3542    fn scan_csharp_lexeme(&mut self, index: usize, depth: usize) -> Option<usize> {
3543        let bytes = self.source;
3544        if starts(bytes, index, b"//") {
3545            let end = csharp_line_end(bytes, index + 2);
3546            self.add_comment(index, end, csharp_line_kind(bytes, index));
3547            return Some(end);
3548        }
3549        if starts(bytes, index, b"/*") {
3550            /* NOTE: ECMA-334 6.3.3: `/*` has no special meaning inside a
3551             * delimited comment, so the first `*/` closes it however many
3552             * openers stand in front. */
3553            let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
3554            self.add_comment(index, end, csharp_block_kind(bytes, index));
3555            if !closed {
3556                self.error(
3557                    "unterminated-comment",
3558                    "unterminated C# block comment",
3559                    ByteSpan::new(index, end),
3560                );
3561            }
3562            return Some(end);
3563        }
3564        if bytes[index] == b'\'' {
3565            return Some(self.scan_csharp_character(index));
3566        }
3567        if !matches!(bytes[index], b'"' | b'@' | b'$') {
3568            return None;
3569        }
3570        let mut reach = Reach::default();
3571        let prefix = csharp_literal_prefix(bytes, index, &mut reach);
3572        self.consult(reach);
3573        match prefix {
3574            Some(prefix) => Some(self.scan_csharp_string(prefix, depth)),
3575            /* NOTE: a run of `$` and `@` that no quote follows opens no literal,
3576             * and the whole of it is taken rather than one byte of it: a shorter
3577             * run inside this one ends at the same byte, so if that byte is no
3578             * quote then no suffix of the run opens a string either. `@x` is the
3579             * verbatim identifier of ECMA-334 6.4.3 and `$` outside a literal is
3580             * no C# token at all. */
3581            None => Some(csharp_prefix_end(bytes, index)),
3582        }
3583    }
3584
3585    /// One `'x'` character literal, beginning at its opening quote.
3586    ///
3587    /// A `\` carries the byte behind it in — that is what hides a `'\''`, and
3588    /// Roslyn 10.0.400 lexes `'\` and a line feed and a `'` as one
3589    /// `CharacterLiteralToken` spanning the break — while an unescaped line
3590    /// terminator ends the literal unterminated, which it reports as CS1010,
3591    /// `Newline in constant`.
3592    fn scan_csharp_character(&mut self, start: usize) -> usize {
3593        let bytes = self.source;
3594        let mut index = start + 1;
3595        while index < bytes.len() {
3596            if bytes[index] == b'\\' {
3597                index = (index + 2).min(bytes.len());
3598                continue;
3599            }
3600            if bytes[index] == b'\'' {
3601                return index + 1;
3602            }
3603            if csharp_line_terminator_width(bytes, index).is_some() {
3604                self.error(
3605                    "unterminated-string",
3606                    "unterminated C# character literal",
3607                    ByteSpan::new(start, index),
3608                );
3609                return index;
3610            }
3611            index += 1;
3612        }
3613        self.error(
3614            "unterminated-string",
3615            "unterminated C# character literal",
3616            ByteSpan::new(start, index),
3617        );
3618        index
3619    }
3620
3621    /// One pre-processing directive line, beginning at its `#`.
3622    ///
3623    /// ECMA-334 6.5.1 ends every directive with
3624    /// `PP_New_Line : PP_Whitespace? SINGLE_LINE_COMMENT? New_Line`, so a `//`
3625    /// is the one comment a directive line can carry: `/*` opens nothing there
3626    /// and neither does `'`. A `"` still opens a string — that is the file name
3627    /// of `#line` and the three arguments of `#pragma checksum` — and it takes
3628    /// no `\` escape and ends at the line either way, which is what keeps the
3629    /// `//` inside `#line 1 "a//b.cs"` out of reach.
3630    ///
3631    /// Four directives take the rest of their line as a message instead:
3632    /// `#error` and `#warning` carry the text a diagnostic quotes, and
3633    /// `#region` and `#endregion` the label an editor folds under. Roslyn lexes
3634    /// a `//` there as a comment only when it opens the message, so
3635    /// `#region // x` carries a comment and `#region x // y` does not.
3636    ///
3637    /// `line_initial` is false for a `#` that some other byte on its line came
3638    /// before. Roslyn raises CS1040 for that one and makes the whole rest of
3639    /// the line a single bad token, with no comment in it, so nothing is
3640    /// reported here either.
3641    fn scan_csharp_directive(&mut self, index: usize, line_initial: bool) -> usize {
3642        let bytes = self.source;
3643        let end = csharp_line_end(bytes, index);
3644        if !line_initial {
3645            return end;
3646        }
3647        /* NOTE: `#` and its keyword may stand apart — `PP_Kind` opens with
3648         * `PP_Whitespace?` — so `#  region` is the directive `#region`. */
3649        let mut cursor = index + 1;
3650        while cursor < end && is_csharp_blank(bytes[cursor]) {
3651            cursor += 1;
3652        }
3653        let name = cursor;
3654        while cursor < end && bytes[cursor].is_ascii_alphabetic() {
3655            cursor += 1;
3656        }
3657        if csharp_directive_takes_a_message(&bytes[name..cursor]) {
3658            while cursor < end && is_csharp_blank(bytes[cursor]) {
3659                cursor += 1;
3660            }
3661            if starts(bytes, cursor, b"//") {
3662                /* NOTE: the directive lexer has no documentation trivia, so a
3663                 * `///` on this line is the ordinary line comment Roslyn reports
3664                 * it as. */
3665                self.add_comment(cursor, end, CommentKind::Line);
3666            }
3667            return end;
3668        }
3669        /* NOTE: every other directive is lexed as tokens, so the line is walked
3670         * for the two that matter. A `//` cannot straddle the end of the line:
3671         * what ends one is never a `/`. */
3672        let mut token = index + 1;
3673        while token < end {
3674            if starts(bytes, token, b"//") {
3675                self.add_comment(token, end, CommentKind::Line);
3676                return end;
3677            }
3678            if bytes[token] == b'"' {
3679                token += 1;
3680                while token < end && bytes[token] != b'"' {
3681                    token += 1;
3682                }
3683                /* NOTE: Roslyn's `ScanStringLiteral` takes an `inDirective`
3684                 * flag that turns the `\` escape off, so `"a\"` closes at its
3685                 * second quote and the `b` behind it is a token of its own.
3686                 * A string left open ends at the line, with CS1010. */
3687                token = (token + 1).min(end);
3688                continue;
3689            }
3690            token += 1;
3691        }
3692        end
3693    }
3694
3695    /// One C# string literal, beginning at the first byte of its `$` and `@`
3696    /// prefix and at the quote of a plain one.
3697    ///
3698    /// The eight forms are three rules and one switch. A *plain* string takes
3699    /// the `\` escape and ends at its line; a *verbatim* one spells its quote
3700    /// `""`, takes no escape, and carries line breaks; a *raw* one is opaque
3701    /// until a run of at least as many quotes as its opener carried comes back,
3702    /// and carries line breaks only when its opener ends a line. The switch is
3703    /// the `$` run: with none the braces are content, with `n` of them a run of
3704    /// `n` braces opens a hole.
3705    ///
3706    /// A plain interpolated string is the one form whose *text* stops at a line
3707    /// terminator while its *holes* do not: C# 11 let an expression inside one
3708    /// span lines, which is why a `//` comment written in a hole runs to the
3709    /// end of its line and the string carries on below.
3710    fn scan_csharp_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
3711        if depth > 256 {
3712            self.error(
3713                "nesting-limit",
3714                "C# string interpolation nesting limit exceeded",
3715                ByteSpan::new(prefix.start, prefix.start),
3716            );
3717            return self.source.len();
3718        }
3719        match prefix.form {
3720            CsharpStringForm::Raw => self.scan_csharp_raw_string(prefix, depth),
3721            CsharpStringForm::Verbatim => self.scan_csharp_verbatim_string(prefix, depth),
3722            CsharpStringForm::Plain => self.scan_csharp_plain_string(prefix, depth),
3723        }
3724    }
3725
3726    /// One plain `"..."` string, interpolated when a `$` stands in front of it.
3727    fn scan_csharp_plain_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
3728        let bytes = self.source;
3729        let mut index = prefix.quote + 1;
3730        while index < bytes.len() {
3731            match bytes[index] {
3732                /* NOTE: a `\` carries the byte behind it in whatever it is, a
3733                 * line terminator included: `ScanEscapeSequence` takes one
3734                 * character and raises CS1009 when it spells no escape, so
3735                 * `"p\` and a line feed is a string that carries on below and
3736                 * not one left open. A CRLF pair still ends it — only the `\r`
3737                 * is carried — which is why this counts bytes rather than
3738                 * characters. */
3739                b'\\' => index = (index + 2).min(bytes.len()),
3740                b'"' => return index + 1,
3741                b'{' if prefix.dollars > 0 => {
3742                    if bytes.get(index + 1) == Some(&b'{') {
3743                        index += 2;
3744                        continue;
3745                    }
3746                    index = self.scan_csharp_hole(index + 1, prefix.dollars, depth + 1);
3747                }
3748                b'}' if prefix.dollars > 0 && bytes.get(index + 1) == Some(&b'}') => index += 2,
3749                _ if csharp_line_terminator_width(bytes, index).is_some() => {
3750                    self.error(
3751                        "unterminated-string",
3752                        csharp_unterminated_string(prefix.form, prefix.dollars > 0),
3753                        ByteSpan::new(prefix.start, index),
3754                    );
3755                    return index;
3756                }
3757                _ => index += 1,
3758            }
3759        }
3760        self.error(
3761            "unterminated-string",
3762            csharp_unterminated_string(prefix.form, prefix.dollars > 0),
3763            ByteSpan::new(prefix.start, index),
3764        );
3765        index
3766    }
3767
3768    /// One verbatim `@"..."` string, interpolated when a `$` stands with it.
3769    ///
3770    /// `""` is how such a literal spells a quote, so the closing delimiter is
3771    /// the first quote no second quote follows, and a `\` is content.
3772    fn scan_csharp_verbatim_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
3773        let bytes = self.source;
3774        let mut index = prefix.quote + 1;
3775        while index < bytes.len() {
3776            match bytes[index] {
3777                b'"' if bytes.get(index + 1) == Some(&b'"') => index += 2,
3778                b'"' => return index + 1,
3779                b'{' if prefix.dollars > 0 => {
3780                    if bytes.get(index + 1) == Some(&b'{') {
3781                        index += 2;
3782                        continue;
3783                    }
3784                    index = self.scan_csharp_hole(index + 1, prefix.dollars, depth + 1);
3785                }
3786                b'}' if prefix.dollars > 0 && bytes.get(index + 1) == Some(&b'}') => index += 2,
3787                _ => index += 1,
3788            }
3789        }
3790        self.error(
3791            "unterminated-string",
3792            csharp_unterminated_string(prefix.form, prefix.dollars > 0),
3793            ByteSpan::new(prefix.start, index),
3794        );
3795        index
3796    }
3797
3798    /// One raw `"""..."""` string, interpolated when a `$` run stands with it.
3799    ///
3800    /// The closing delimiter is the first run of at least as many quotes as the
3801    /// opener carried, and the whole run is taken: Roslyn ends
3802    /// `"""abc""""` at its fourth closing quote and raises CS8998 rather than
3803    /// leaving one behind. A run shorter than the opener's is content, which is
3804    /// what lets a raw string carry `"""`.
3805    ///
3806    /// The opener decides whether line breaks are content: when only blanks
3807    /// stand between it and the end of its line the literal is the multi-line
3808    /// form, and otherwise a line terminator ends it unterminated with CS8997.
3809    /// Roslyn ends a multi-line one at the first run all the same, raising
3810    /// CS9000 when that run does not stand on a line of its own, so the search
3811    /// here is the same one either way.
3812    fn scan_csharp_raw_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
3813        let bytes = self.source;
3814        let multiline = csharp_multiline_raw_string(bytes, prefix.quote + prefix.quotes);
3815        let mut index = prefix.quote + prefix.quotes;
3816        while index < bytes.len() {
3817            match bytes[index] {
3818                b'"' => {
3819                    let mut end = index;
3820                    while bytes.get(end) == Some(&b'"') {
3821                        end += 1;
3822                    }
3823                    if end - index >= prefix.quotes {
3824                        /* NOTE: the run is taken whole, so the byte that ended
3825                         * it is read one past where the scan resumes — and an
3826                         * append at the end of the document is exactly the edit
3827                         * that would put another quote there. */
3828                        let mut reach = Reach::default();
3829                        reach.byte(end);
3830                        self.consult(reach);
3831                        return end;
3832                    }
3833                    index = end;
3834                }
3835                b'{' if prefix.dollars > 0 => {
3836                    let mut run = index;
3837                    while bytes.get(run) == Some(&b'{') {
3838                        run += 1;
3839                    }
3840                    if run - index < prefix.dollars {
3841                        index = run;
3842                        continue;
3843                    }
3844                    /* NOTE: a run longer than the `$` run spends its first
3845                     * braces as content and its last `dollars` on the hole,
3846                     * which is how `$"""{{x}}"""` writes a literal `{` in front
3847                     * of one — Roslyn reports the first brace as
3848                     * `InterpolatedStringTextToken` and the second as
3849                     * `OpenBraceToken`. */
3850                    index = self.scan_csharp_hole(run, prefix.dollars, depth + 1);
3851                }
3852                _ if !multiline && csharp_line_terminator_width(bytes, index).is_some() => {
3853                    self.error(
3854                        "unterminated-string",
3855                        csharp_unterminated_string(prefix.form, prefix.dollars > 0),
3856                        ByteSpan::new(prefix.start, index),
3857                    );
3858                    return index;
3859                }
3860                _ => index += 1,
3861            }
3862        }
3863        self.error(
3864            "unterminated-string",
3865            csharp_unterminated_string(prefix.form, prefix.dollars > 0),
3866            ByteSpan::new(prefix.start, index),
3867        );
3868        index
3869    }
3870
3871    /// One interpolation hole, beginning past the `braces` opening braces.
3872    ///
3873    /// The brackets of the expression are counted rather than searched for,
3874    /// because the expression is code: a nested string, a collection
3875    /// expression, a lambda, and a comment may all stand inside one. Roslyn
3876    /// reports a comment written there as `SingleLineCommentTrivia` or
3877    /// `MultiLineCommentTrivia` exactly as it does outside a string.
3878    ///
3879    /// A `:` where no bracket is open ends the expression and opens the format
3880    /// clause, which is text to the end of the hole: `$"{x:D4 // n}"` holds no
3881    /// comment. That is Roslyn's rule down to the case that surprises a reader,
3882    /// `$"{global::X}"`, whose first `:` opens a format clause of its own.
3883    fn scan_csharp_hole(&mut self, mut index: usize, braces: usize, depth: usize) -> usize {
3884        if depth > 256 {
3885            self.error(
3886                "nesting-limit",
3887                "C# string interpolation nesting limit exceeded",
3888                ByteSpan::new(index, index),
3889            );
3890            return self.source.len();
3891        }
3892        let bytes = self.source;
3893        let mut open = 0usize;
3894        while index < bytes.len() {
3895            if let Some(end) = self.scan_csharp_lexeme(index, depth) {
3896                index = end;
3897                continue;
3898            }
3899            match bytes[index] {
3900                b'(' | b'[' | b'{' => {
3901                    open += 1;
3902                    index += 1;
3903                }
3904                b')' | b']' => {
3905                    open = open.saturating_sub(1);
3906                    index += 1;
3907                }
3908                b'}' => {
3909                    if open > 0 {
3910                        open -= 1;
3911                        index += 1;
3912                        continue;
3913                    }
3914                    let (next, closed) = csharp_hole_close(bytes, index, braces);
3915                    if closed {
3916                        return next;
3917                    }
3918                    index = next;
3919                }
3920                b':' if open == 0 => return self.scan_csharp_format(index, braces),
3921                _ => index += 1,
3922            }
3923        }
3924        self.error(
3925            "unterminated-interpolation",
3926            "unterminated C# string interpolation",
3927            ByteSpan::new(index, index),
3928        );
3929        index
3930    }
3931
3932    /// The format clause of an interpolation hole, beginning at its `:`.
3933    ///
3934    /// Everything to the closing braces of the hole is text — Roslyn lexes it
3935    /// as one `InterpolatedStringTextToken` — so no comment is reported here
3936    /// and no nested literal is lexed.
3937    fn scan_csharp_format(&mut self, mut index: usize, braces: usize) -> usize {
3938        let bytes = self.source;
3939        while index < bytes.len() {
3940            if bytes[index] == b'}' {
3941                let (next, closed) = csharp_hole_close(bytes, index, braces);
3942                if closed {
3943                    return next;
3944                }
3945                index = next;
3946                continue;
3947            }
3948            index += 1;
3949        }
3950        self.error(
3951            "unterminated-interpolation",
3952            "unterminated C# string interpolation",
3953            ByteSpan::new(index, index),
3954        );
3955        index
3956    }
3957
3958    fn scan_scala(&mut self) {
3959        let bytes = self.source;
3960        let mut index = if self.offset == 0 {
3961            byte_order_mark_width(bytes)
3962        } else {
3963            0
3964        };
3965        while index < bytes.len() && !self.stopped {
3966            /* NOTE: Scala has no `#` comment of its own, so a `#!` line is the
3967             * only shape a `#` is part of, and it is a preamble only at the
3968             * very first byte — a byte order mark permitting — which is also
3969             * the only place scala-cli reads one. The same bytes lower down are
3970             * ordinary code. */
3971            if self.offset == 0
3972                && index == byte_order_mark_width(bytes)
3973                && starts(bytes, index, b"#!")
3974            {
3975                let end = line_end(bytes, index + 2);
3976                self.add_comment(index, end, CommentKind::Line);
3977                index = end;
3978                continue;
3979            }
3980            if let Some(end) = self.scan_scala_lexeme(index, 0) {
3981                index = end;
3982                continue;
3983            }
3984            if matches!(bytes[index], b'\r' | b'\n') {
3985                index = consume_newline(bytes, index);
3986                self.add_safe_checkpoint(index);
3987            } else {
3988                index += 1;
3989            }
3990        }
3991    }
3992
3993    /// One Scala comment, string, backquoted identifier, or XML literal
3994    /// beginning at `index`, or `None` when the byte there opens none of them.
3995    ///
3996    /// This is the whole of Scala's lexical surface that a comment can hide
3997    /// behind or be, which is why the top level and the inside of a `${ ... }`
3998    /// interpolation or of an XML literal's braces ask one function for it:
3999    /// both are code, and a comment written there is a comment exactly as it is
4000    /// outside one.
4001    fn scan_scala_lexeme(&mut self, index: usize, depth: usize) -> Option<usize> {
4002        let bytes = self.source;
4003        if starts(bytes, index, b"//") {
4004            let end = line_end(bytes, index + 2);
4005            self.add_comment(index, end, scala_line_kind(bytes, index));
4006            return Some(end);
4007        }
4008        if starts(bytes, index, b"/*") {
4009            let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
4010            self.add_comment(index, end, scala_block_kind(bytes, index));
4011            if !closed {
4012                self.error(
4013                    "unterminated-comment",
4014                    "unterminated Scala block comment",
4015                    ByteSpan::new(index, end),
4016                );
4017            }
4018            return Some(end);
4019        }
4020        if bytes[index] == b'"' {
4021            return Some(self.scan_scala_string(index, depth));
4022        }
4023        if bytes[index] == b'\''
4024            && let Some(end) = scala_character_literal_end(bytes, index)
4025        {
4026            return Some(end);
4027        }
4028        /* NOTE: a backquoted identifier may hold any bytes but a backtick,
4029         * `//` included — `` val `a//b` = 1 `` is one identifier — and ends at
4030         * its line, which is what keeps it from swallowing the source below. */
4031        if bytes[index] == b'`' {
4032            return Some(self.scala_backquoted_identifier(index));
4033        }
4034        if bytes[index] == b'<' && scala_is_xml_start(bytes, index) {
4035            return Some(self.scan_scala_xml(index, depth));
4036        }
4037        None
4038    }
4039
4040    /// One Scala string literal, beginning at its opening quote.
4041    ///
4042    /// A string is interpolated exactly when an identifier stands directly
4043    /// before its quote — the compiler's lexer turns that identifier into
4044    /// `INTERPOLATIONID` — so `s"..."`, `raw"..."`, a custom interpolator such
4045    /// as `xml"..."`, and each of them triple-quoted are the interpolated
4046    /// forms, and every other quote opens a plain string whose `$` is content.
4047    /// Inside an interpolated string `$$` and `$"` write a literal `$` and `"`,
4048    /// `${ ... }` opens an expression that is code, and `$` followed by an
4049    /// identifier starts another expression; the compiler rejects any other
4050    /// `$`, and this scanner reads it as content, which costs a comment nothing
4051    /// and keeps a broken file scannable. A `\` carries the byte behind it in
4052    /// a single-line string — that is what hides a `\"` — and means nothing in
4053    /// a triple-quoted one.
4054    ///
4055    /// A triple-quoted string closes on the first three quotes of a run, and
4056    /// any further quotes of the run are part of the string's value: the
4057    /// lexer's `isTripleQuote` consumes the first three and `putChar`s the
4058    /// rest, so `"""a""""` is the string `a"` and ends after the run of four.
4059    /// That is where Scala parts company with Kotlin's existing scanner, which
4060    /// closes on the first three of a run and leaves a stray quote behind.
4061    fn scan_scala_string(&mut self, start: usize, depth: usize) -> usize {
4062        if depth > 256 {
4063            self.error(
4064                "nesting-limit",
4065                "Scala string interpolation nesting limit exceeded",
4066                ByteSpan::new(start, start),
4067            );
4068            return self.source.len();
4069        }
4070        let bytes = self.source;
4071        let interpolated = scala_interpolator(bytes, start);
4072        let triple = starts(bytes, start, b"\"\"\"");
4073        let mut index = start + if triple { 3 } else { 1 };
4074        while index < bytes.len() {
4075            if interpolated && bytes[index] == b'$' {
4076                match bytes.get(index + 1) {
4077                    Some(b'$' | b'"') => {
4078                        index += 2;
4079                        continue;
4080                    }
4081                    Some(b'{') => {
4082                        index = self.scan_scala_expression(index + 2, depth + 1);
4083                        continue;
4084                    }
4085                    Some(&byte) if scala_identifier_start(byte) => {
4086                        index += 2;
4087                        while index < bytes.len() && scala_identifier_part(bytes[index]) {
4088                            index += 1;
4089                        }
4090                        continue;
4091                    }
4092                    _ => {
4093                        index += 1;
4094                        continue;
4095                    }
4096                }
4097            }
4098            if bytes[index] == b'"' {
4099                if triple {
4100                    let run = index + count_run(bytes, index, b'"');
4101                    if run - index >= 3 {
4102                        return run;
4103                    }
4104                } else {
4105                    return index + 1;
4106                }
4107            } else if bytes[index] == b'\\' {
4108                if !triple {
4109                    index = (index + 2).min(bytes.len());
4110                    continue;
4111                }
4112            } else if !triple && matches!(bytes[index], b'\r' | b'\n') {
4113                self.error(
4114                    "unterminated-string",
4115                    "unterminated Scala string",
4116                    ByteSpan::new(start, index),
4117                );
4118                return index;
4119            }
4120            index += 1;
4121        }
4122        self.error(
4123            "unterminated-string",
4124            if triple {
4125                "unterminated Scala multi-line string"
4126            } else {
4127                "unterminated Scala string"
4128            },
4129            ByteSpan::new(start, index),
4130        );
4131        index
4132    }
4133
4134    /// One `${ ... }` expression or the inside of an XML literal's braces,
4135    /// beginning past the opening brace.
4136    ///
4137    /// The braces are counted rather than searched for, because the expression
4138    /// is code: a nested string, a tuple, a call, and a comment may all stand
4139    /// inside one, and a string or an XML literal written there is scanned as
4140    /// such. The compiler reports a comment written inside an interpolation as
4141    /// a comment exactly as it does outside a string, and a `//` one runs to
4142    /// the end of its line while a multi-line string carries on below.
4143    fn scan_scala_expression(&mut self, mut index: usize, depth: usize) -> usize {
4144        if depth > 256 {
4145            self.error(
4146                "nesting-limit",
4147                "Scala string interpolation nesting limit exceeded",
4148                ByteSpan::new(index, index),
4149            );
4150            return self.source.len();
4151        }
4152        let bytes = self.source;
4153        let mut braces = 1usize;
4154        while index < bytes.len() {
4155            if let Some(end) = self.scan_scala_lexeme(index, depth) {
4156                index = end;
4157                continue;
4158            }
4159            match bytes[index] {
4160                b'{' => {
4161                    braces += 1;
4162                    index += 1;
4163                }
4164                b'}' => {
4165                    braces -= 1;
4166                    index += 1;
4167                    if braces == 0 {
4168                        return index;
4169                    }
4170                }
4171                _ => index += 1,
4172            }
4173        }
4174        self.error(
4175            "unterminated-template-expression",
4176            "unterminated Scala string-template expression",
4177            ByteSpan::new(index, index),
4178        );
4179        index
4180    }
4181
4182    /// One backquoted identifier, beginning at its opening backtick.
4183    ///
4184    /// The compiler's `getBackquotedIdent` reads to the next backtick and ends
4185    /// at a line terminator, which it reports as `unclosed quoted identifier`;
4186    /// this scanner follows both.
4187    fn scala_backquoted_identifier(&mut self, start: usize) -> usize {
4188        let bytes = self.source;
4189        let mut index = start + 1;
4190        while index < bytes.len() {
4191            if bytes[index] == b'`' {
4192                return index + 1;
4193            }
4194            if matches!(bytes[index], b'\r' | b'\n') {
4195                self.error(
4196                    "unterminated-identifier",
4197                    "unclosed quoted identifier",
4198                    ByteSpan::new(start, index),
4199                );
4200                return index;
4201            }
4202            index += 1;
4203        }
4204        self.error(
4205            "unterminated-identifier",
4206            "unclosed quoted identifier",
4207            ByteSpan::new(start, index),
4208        );
4209        index
4210    }
4211
4212    /// One XML literal, beginning at the `<` that opened it.
4213    ///
4214    /// The compiler's *lexer* emits an `XMLSTART` token for the `<` and then
4215    /// hands the literal to the parser, which re-reads it with an XML scanner:
4216    /// element text, CDATA and processing instructions are *not* code, and a
4217    /// `//` in them is a byte the lexer alone would call a comment. This
4218    /// scanner follows the parser, so text and the construct bodies are
4219    /// opaque, `{ ... }` in text or an attribute is code, `<!-- ... -->` is an
4220    /// XML comment, and the literal ends at the close tag matching its root or
4221    /// at a self-closing `/>`. A literal whose root never closes is a file the
4222    /// parser rejects, and the rest of it is read as opaque content, which is
4223    /// the safe half of being wrong: a comment below a broken literal is
4224    /// over-kept rather than removed out of one that parses.
4225    fn scan_scala_xml(&mut self, start: usize, depth: usize) -> usize {
4226        let bytes = self.source;
4227        let Some((mut index, self_closing, root_start, root_end)) =
4228            self.scala_xml_tag(start, depth)
4229        else {
4230            return bytes.len();
4231        };
4232        if self_closing {
4233            return index;
4234        }
4235        let mut stack = vec![(root_start, root_end)];
4236        while index < bytes.len() {
4237            if bytes[index] == b'{' {
4238                index = self.scan_scala_expression(index + 1, depth + 1);
4239                continue;
4240            }
4241            if bytes[index] != b'<' {
4242                index += 1;
4243                continue;
4244            }
4245            if starts(bytes, index, b"<!--") {
4246                let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") else {
4247                    return bytes.len();
4248                };
4249                let end = index + 4 + relative + 3;
4250                self.add_comment(index, end, CommentKind::HtmlComment);
4251                index = end;
4252                continue;
4253            }
4254            if starts(bytes, index, b"<![CDATA[") {
4255                let Some(relative) = find_subslice(&bytes[index + 9..], b"]]>") else {
4256                    return bytes.len();
4257                };
4258                index = index + 9 + relative + 3;
4259                continue;
4260            }
4261            if starts(bytes, index, b"<?") {
4262                let Some(relative) = find_subslice(&bytes[index + 2..], b"?>") else {
4263                    return bytes.len();
4264                };
4265                index = index + 2 + relative + 2;
4266                continue;
4267            }
4268            if starts(bytes, index, b"</") {
4269                let name_start = index + 2;
4270                if name_start < bytes.len() && xml_name_start(bytes[name_start]) {
4271                    let name_end = name_start + xml_name_len(&bytes[name_start..]);
4272                    let Some(close_end) = skip_xml_tag_tail(bytes, name_end) else {
4273                        return bytes.len();
4274                    };
4275                    if bytes[name_start..name_end]
4276                        == bytes[stack.last().unwrap().0..stack.last().unwrap().1]
4277                    {
4278                        stack.pop();
4279                        if stack.is_empty() {
4280                            return close_end;
4281                        }
4282                        index = close_end;
4283                        continue;
4284                    }
4285                }
4286                index += 2;
4287                continue;
4288            }
4289            if starts(bytes, index, b"<!") {
4290                let Some(relative) = find_subslice(&bytes[index + 2..], b">") else {
4291                    return bytes.len();
4292                };
4293                index = index + 2 + relative + 1;
4294                continue;
4295            }
4296            if index + 1 < bytes.len() && xml_name_start(bytes[index + 1]) {
4297                let Some((after, self_closing, name_start, name_end)) =
4298                    self.scala_xml_tag(index, depth)
4299                else {
4300                    return bytes.len();
4301                };
4302                if !self_closing {
4303                    stack.push((name_start, name_end));
4304                }
4305                index = after;
4306                continue;
4307            }
4308            index += 1;
4309        }
4310        bytes.len()
4311    }
4312
4313    /// One XML tag beginning at its `<`, returning where it ends, whether it
4314    /// is self-closing, and the range of its name.
4315    fn scala_xml_tag(&mut self, start: usize, depth: usize) -> Option<(usize, bool, usize, usize)> {
4316        let bytes = self.source;
4317        let mut index = start + 1;
4318        if index >= bytes.len() || !xml_name_start(bytes[index]) {
4319            return None;
4320        }
4321        let name_start = index;
4322        index += xml_name_len(&bytes[index..]);
4323        let name_end = index;
4324        loop {
4325            if index >= bytes.len() {
4326                return None;
4327            }
4328            match bytes[index] {
4329                b'>' => return Some((index + 1, false, name_start, name_end)),
4330                b'/' if bytes.get(index + 1) == Some(&b'>') => {
4331                    return Some((index + 2, true, name_start, name_end));
4332                }
4333                b'"' | b'\'' => {
4334                    let quote = bytes[index];
4335                    index += 1;
4336                    while index < bytes.len() && bytes[index] != quote {
4337                        index += 1;
4338                    }
4339                    if index >= bytes.len() {
4340                        return None;
4341                    }
4342                    index += 1;
4343                }
4344                b'{' => {
4345                    index = self.scan_scala_expression(index + 1, depth + 1);
4346                }
4347                _ => index += 1,
4348            }
4349        }
4350    }
4351
4352    fn scan_shell(&mut self) {
4353        let _ = self.scan_shell_region(0, None, 0);
4354    }
4355
4356    fn scan_shell_region(
4357        &mut self,
4358        mut index: usize,
4359        terminator: Option<ShellTerminator>,
4360        depth: usize,
4361    ) -> usize {
4362        if depth > 256 {
4363            self.error(
4364                "nesting-limit",
4365                "shell lexical nesting limit exceeded",
4366                ByteSpan::new(index, index),
4367            );
4368            return self.source.len();
4369        }
4370        let bytes = self.source;
4371        let mut heredocs: Vec<Heredoc> = Vec::new();
4372        let backtick_terminator = matches!(terminator, Some(ShellTerminator::Backtick(_)));
4373        let parenthesis_terminator = matches!(terminator, Some(ShellTerminator::Parenthesis(_)));
4374        let mut parentheses = usize::from(parenthesis_terminator);
4375        let mut word_open = false;
4376        let mut command_position = true;
4377        let mut case_states = Vec::new();
4378        while index < bytes.len() && !self.stopped {
4379            match bytes[index] {
4380                b'#' if !word_open => {
4381                    let end = line_end(bytes, index + 1);
4382                    self.add_comment(index, end, CommentKind::Line);
4383                    index = end;
4384                }
4385                b'\'' => {
4386                    let start = index;
4387                    let (end, closed) = shell_single_quote_end(bytes, index);
4388                    index = end;
4389                    if !closed {
4390                        self.error(
4391                            "unterminated-string",
4392                            "unterminated shell single quote",
4393                            ByteSpan::new(start, index),
4394                        );
4395                    }
4396                    word_open = true;
4397                    command_position = false;
4398                }
4399                b'"' => {
4400                    index = self.scan_shell_double_quote(index, depth + 1);
4401                    word_open = true;
4402                    command_position = false;
4403                }
4404                b'`' if backtick_terminator => return index + 1,
4405                b'`' => {
4406                    index = self.scan_shell_region(
4407                        index + 1,
4408                        Some(ShellTerminator::Backtick(index)),
4409                        depth + 1,
4410                    );
4411                    word_open = true;
4412                    command_position = false;
4413                }
4414                b'$' if bytes.get(index + 1) == Some(&b'(') => {
4415                    index = self.scan_shell_region(
4416                        index + 2,
4417                        Some(ShellTerminator::Parenthesis(index)),
4418                        depth + 1,
4419                    );
4420                    word_open = true;
4421                    command_position = false;
4422                }
4423                b'$' if bytes.get(index + 1) == Some(&b'\'')
4424                    && matches!(self.options.dialect, Dialect::Bash53 | Dialect::Zsh) =>
4425                {
4426                    index = self.quoted_or_error(index + 1, true, "shell ANSI-C quoted string");
4427                    word_open = true;
4428                    command_position = false;
4429                }
4430                b'<' if bytes.get(index + 1) == Some(&b'<') => {
4431                    word_open = false;
4432                    if bytes.get(index + 2) == Some(&b'<') {
4433                        index += 3;
4434                    } else {
4435                        let mut reach = Reach::default();
4436                        let parsed = parse_heredoc(bytes, index, &mut reach);
4437                        self.consult(reach);
4438                        if let Some((heredoc, end)) = parsed {
4439                            heredocs.push(heredoc);
4440                            index = end;
4441                            word_open = true;
4442                        } else {
4443                            index += 1;
4444                        }
4445                    }
4446                }
4447                b'\r' | b'\n' if !heredocs.is_empty() => {
4448                    index = consume_newline(bytes, index);
4449                    for heredoc in heredocs.drain(..) {
4450                        match heredoc_body_end(bytes, index, &heredoc) {
4451                            Some(end) => index = end,
4452                            None => {
4453                                self.error(
4454                                    "unterminated-heredoc",
4455                                    "unterminated shell heredoc",
4456                                    ByteSpan::new(heredoc.operator, bytes.len()),
4457                                );
4458                                return bytes.len();
4459                            }
4460                        }
4461                    }
4462                    word_open = false;
4463                    command_position = case_states.last() != Some(&ShellCaseState::Pattern);
4464                    if terminator.is_none() && case_states.is_empty() {
4465                        self.add_safe_checkpoint(index);
4466                    }
4467                }
4468                b'\r' | b'\n' => {
4469                    index = consume_newline(bytes, index);
4470                    word_open = false;
4471                    command_position = case_states.last() != Some(&ShellCaseState::Pattern);
4472                    if terminator.is_none() && case_states.is_empty() {
4473                        self.add_safe_checkpoint(index);
4474                    }
4475                }
4476                b'(' if parenthesis_terminator => {
4477                    parentheses += 1;
4478                    index += 1;
4479                    word_open = false;
4480                    command_position = case_states.last() != Some(&ShellCaseState::Pattern);
4481                }
4482                b')' if parenthesis_terminator => {
4483                    if parentheses == 1
4484                        && let Some(state @ ShellCaseState::Pattern) = case_states.last_mut()
4485                    {
4486                        *state = ShellCaseState::Body;
4487                        index += 1;
4488                        word_open = false;
4489                        command_position = true;
4490                        continue;
4491                    }
4492                    parentheses = parentheses.saturating_sub(1);
4493                    index += 1;
4494                    if parentheses == 0 {
4495                        return index;
4496                    }
4497                    word_open = false;
4498                    command_position = true;
4499                }
4500                b')' if case_states.last() == Some(&ShellCaseState::Pattern) => {
4501                    *case_states.last_mut().expect("case state exists") = ShellCaseState::Body;
4502                    index += 1;
4503                    word_open = false;
4504                    command_position = true;
4505                }
4506                b';' if case_states.last() == Some(&ShellCaseState::Body)
4507                    && (starts(bytes, index, b";;") || starts(bytes, index, b";&")) =>
4508                {
4509                    let width = if starts(bytes, index, b";;&") { 3 } else { 2 };
4510                    *case_states.last_mut().expect("case state exists") = ShellCaseState::Pattern;
4511                    index += width;
4512                    word_open = false;
4513                    command_position = false;
4514                }
4515                b';' | b'&' | b'|' | b'(' | b')' => {
4516                    index += 1;
4517                    word_open = false;
4518                    command_position = bytes[index - 1] != b'|'
4519                        || case_states.last() != Some(&ShellCaseState::Pattern);
4520                }
4521                b'<' | b'>' => {
4522                    index += 1;
4523                    word_open = false;
4524                }
4525                byte if byte.is_ascii_whitespace() => {
4526                    index += 1;
4527                    word_open = false;
4528                }
4529                b'\\' => {
4530                    if bytes.get(index + 1) == Some(&b'\r') && bytes.get(index + 2) == Some(&b'\n')
4531                    {
4532                        index += 3;
4533                    } else if matches!(bytes.get(index + 1), Some(b'\r' | b'\n')) {
4534                        index += 2;
4535                    } else {
4536                        index = (index + 2).min(bytes.len());
4537                        word_open = true;
4538                        command_position = false;
4539                    }
4540                }
4541                byte if !word_open && (byte.is_ascii_alphabetic() || byte == b'_') => {
4542                    let start = index;
4543                    index += 1;
4544                    while index < bytes.len()
4545                        && (bytes[index].is_ascii_alphanumeric() || bytes[index] == b'_')
4546                    {
4547                        index += 1;
4548                    }
4549                    let boundary = bytes.get(index).is_none_or(|byte| {
4550                        byte.is_ascii_whitespace()
4551                            || matches!(byte, b';' | b'&' | b'|' | b'(' | b')' | b'<' | b'>')
4552                    });
4553                    let token = &bytes[start..index];
4554                    if boundary && token == b"case" && command_position {
4555                        case_states.push(ShellCaseState::AwaitIn);
4556                        command_position = false;
4557                    } else if boundary
4558                        && token == b"in"
4559                        && case_states.last() == Some(&ShellCaseState::AwaitIn)
4560                    {
4561                        *case_states.last_mut().expect("case state exists") =
4562                            ShellCaseState::Pattern;
4563                        command_position = false;
4564                    } else if boundary
4565                        && token == b"esac"
4566                        && (command_position
4567                            || case_states.last() == Some(&ShellCaseState::Pattern))
4568                    {
4569                        let _ = case_states.pop();
4570                        command_position = false;
4571                    } else {
4572                        command_position = command_position && bytes.get(index) == Some(&b'=');
4573                    }
4574                    word_open = true;
4575                }
4576                _ => {
4577                    index += 1;
4578                    word_open = true;
4579                    command_position = false;
4580                }
4581            }
4582        }
4583        if let Some(terminator) = terminator {
4584            let (code, message, start) = match terminator {
4585                ShellTerminator::Parenthesis(start) => (
4586                    "unterminated-command-substitution",
4587                    "unterminated shell command substitution",
4588                    start,
4589                ),
4590                ShellTerminator::Backtick(start) => (
4591                    "unterminated-string",
4592                    "unterminated shell command substitution",
4593                    start,
4594                ),
4595            };
4596            self.error(code, message, ByteSpan::new(start, index));
4597        }
4598        index
4599    }
4600
4601    fn scan_shell_double_quote(&mut self, start: usize, depth: usize) -> usize {
4602        let bytes = self.source;
4603        let mut index = start + 1;
4604        while index < bytes.len() {
4605            match bytes[index] {
4606                b'\\' => index = (index + 2).min(bytes.len()),
4607                b'"' => return index + 1,
4608                b'$' if bytes.get(index + 1) == Some(&b'(') => {
4609                    index = self.scan_shell_region(
4610                        index + 2,
4611                        Some(ShellTerminator::Parenthesis(index)),
4612                        depth + 1,
4613                    );
4614                }
4615                b'`' => {
4616                    index = self.scan_shell_region(
4617                        index + 1,
4618                        Some(ShellTerminator::Backtick(index)),
4619                        depth + 1,
4620                    );
4621                }
4622                _ => index += 1,
4623            }
4624        }
4625        self.error(
4626            "unterminated-string",
4627            "unterminated shell double quote",
4628            ByteSpan::new(start, index),
4629        );
4630        index
4631    }
4632
4633    fn scan_sql(&mut self) {
4634        let bytes = self.source;
4635        let nested = matches!(self.options.dialect, Dialect::PostgreSql | Dialect::TSql);
4636        let mut index = 0;
4637        while index < bytes.len() && !self.stopped {
4638            if starts(bytes, index, b"--")
4639                && (self.options.dialect != Dialect::MySql
4640                    || mysql_dash_comment_boundary(bytes.get(index + 2).copied()))
4641            {
4642                let end = line_end(bytes, index + 2);
4643                self.add_comment(index, end, CommentKind::Line);
4644                index = end;
4645                continue;
4646            }
4647            if bytes[index] == b'#' && self.options.dialect == Dialect::MySql {
4648                let end = line_end(bytes, index + 1);
4649                self.add_comment(index, end, CommentKind::Line);
4650                index = end;
4651                continue;
4652            }
4653            if starts(bytes, index, b"/*") {
4654                let (end, closed) = block_end(bytes, index, b"/*", b"*/", nested);
4655                self.add_comment(index, end, CommentKind::Block);
4656                if !closed {
4657                    self.error(
4658                        "unterminated-comment",
4659                        "unterminated SQL block comment",
4660                        ByteSpan::new(index, end),
4661                    );
4662                }
4663                index = end;
4664                continue;
4665            }
4666            if bytes[index] == b'\'' {
4667                let start = index;
4668                let backslash_escapes = self.options.dialect == Dialect::MySql
4669                    || (self.options.dialect == Dialect::PostgreSql
4670                        && postgres_escape_string_start(bytes, index));
4671                let (end, closed) = sql_quoted_end(bytes, index, b'\'', backslash_escapes);
4672                index = end;
4673                if !closed {
4674                    self.error(
4675                        "unterminated-string",
4676                        "unterminated SQL string",
4677                        ByteSpan::new(start, index),
4678                    );
4679                }
4680                continue;
4681            }
4682            if matches!(bytes[index], b'"' | b'`') {
4683                let mysql_string = bytes[index] == b'"' && self.options.dialect == Dialect::MySql;
4684                let (end, closed) = if mysql_string {
4685                    sql_quoted_end(bytes, index, b'"', true)
4686                } else {
4687                    sql_identifier_end(bytes, index, bytes[index])
4688                };
4689                if !closed {
4690                    self.error(
4691                        if mysql_string {
4692                            "unterminated-string"
4693                        } else {
4694                            "unterminated-identifier"
4695                        },
4696                        if mysql_string {
4697                            "unterminated MySQL quoted string"
4698                        } else {
4699                            "unterminated SQL quoted identifier"
4700                        },
4701                        ByteSpan::new(index, end),
4702                    );
4703                }
4704                index = end;
4705                continue;
4706            }
4707            if bytes[index] == b'[' && self.options.dialect == Dialect::TSql {
4708                let (end, closed) = sql_identifier_end(bytes, index, b']');
4709                if !closed {
4710                    self.error(
4711                        "unterminated-identifier",
4712                        "unterminated T-SQL bracket identifier",
4713                        ByteSpan::new(index, end),
4714                    );
4715                }
4716                index = end;
4717                continue;
4718            }
4719            let dollar = (bytes[index] == b'$' && self.options.dialect == Dialect::PostgreSql)
4720                .then(|| {
4721                    let mut reach = Reach::default();
4722                    let quoted = sql_dollar_quote_end(bytes, index, &mut reach);
4723                    self.consult(reach);
4724                    quoted
4725                })
4726                .flatten();
4727            if let Some((end, closed)) = dollar {
4728                if !closed {
4729                    self.error(
4730                        "unterminated-string",
4731                        "unterminated PostgreSQL dollar-quoted string",
4732                        ByteSpan::new(index, end),
4733                    );
4734                }
4735                index = end;
4736                continue;
4737            }
4738            let q_quote = ((bytes[index] == b'q' || bytes[index] == b'Q')
4739                && self.options.dialect == Dialect::Oracle)
4740                .then(|| {
4741                    let mut reach = Reach::default();
4742                    let quoted = oracle_q_quote_end(bytes, index, &mut reach);
4743                    self.consult(reach);
4744                    quoted
4745                })
4746                .flatten();
4747            if let Some((end, closed)) = q_quote {
4748                if !closed {
4749                    self.error(
4750                        "unterminated-string",
4751                        "unterminated Oracle q-quoted string",
4752                        ByteSpan::new(index, end),
4753                    );
4754                }
4755                index = end;
4756                continue;
4757            }
4758            if matches!(bytes[index], b'\r' | b'\n') {
4759                index = consume_newline(bytes, index);
4760                self.add_safe_checkpoint(index);
4761            } else {
4762                index += 1;
4763            }
4764        }
4765    }
4766
4767    fn scan_javascript(&mut self) {
4768        let _ = self.scan_js_code(0, None, 0);
4769    }
4770
4771    fn scan_js_code(&mut self, mut index: usize, stop_brace: Option<usize>, depth: usize) -> usize {
4772        if depth > 256 {
4773            self.error(
4774                "nesting-limit",
4775                "JavaScript lexical nesting limit exceeded",
4776                ByteSpan::new(index, index),
4777            );
4778            return self.source.len();
4779        }
4780        let bytes = self.source;
4781        let mut brace_depth = stop_brace.unwrap_or(0);
4782        let mut regex_allowed = true;
4783        let mut control_parentheses = Vec::new();
4784        let mut pending_control_parenthesis = false;
4785        let mut brace_blocks = Vec::new();
4786        let mut statement_start = stop_brace.is_none();
4787        let mut pending_block = false;
4788        while index < bytes.len() && !self.stopped {
4789            if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
4790                let end = js_line_end(bytes, index + 2);
4791                self.add_comment(index, end, CommentKind::Line);
4792                index = end;
4793                continue;
4794            }
4795            if bytes[index] == b'/' {
4796                match bytes.get(index + 1) {
4797                    Some(b'/') => {
4798                        let end = js_line_end(bytes, index + 2);
4799                        self.add_comment(index, end, line_kind(bytes, index));
4800                        index = end;
4801                        continue;
4802                    }
4803                    Some(b'*') => {
4804                        let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
4805                        self.add_comment(index, end, block_kind(bytes, index));
4806                        if !closed {
4807                            self.error(
4808                                "unterminated-comment",
4809                                "unterminated JavaScript block comment",
4810                                ByteSpan::new(index, end),
4811                            );
4812                        }
4813                        index = end;
4814                        continue;
4815                    }
4816                    _ => {}
4817                }
4818            }
4819            /* PERF: Annex B HTML-like comments are uncommon.  Guard both delimiter
4820             * checks by their first byte so the ordinary JavaScript hot path
4821             * does not perform two slice comparisons for every source byte. */
4822            if (bytes[index] == b'<' && starts(bytes, index, b"<!--"))
4823                || (bytes[index] == b'-' && js_html_close_comment(bytes, index))
4824            {
4825                let end = js_line_end(bytes, index + 3);
4826                self.add_comment(index, end, CommentKind::Line);
4827                index = end;
4828                continue;
4829            }
4830            if matches!(self.options.dialect, Dialect::Jsx | Dialect::Tsx)
4831                && regex_allowed
4832                && jsx_open(bytes, index)
4833            {
4834                index = self.scan_jsx_element(index, depth + 1);
4835                regex_allowed = false;
4836                pending_control_parenthesis = false;
4837                statement_start = false;
4838                pending_block = false;
4839                continue;
4840            }
4841            match bytes[index] {
4842                b'\'' | b'"' => {
4843                    index = self.js_quoted_or_error(index);
4844                    regex_allowed = false;
4845                    pending_control_parenthesis = false;
4846                    statement_start = false;
4847                    pending_block = false;
4848                }
4849                b'`' => {
4850                    index = self.scan_js_template(index, depth + 1);
4851                    regex_allowed = false;
4852                    pending_control_parenthesis = false;
4853                    statement_start = false;
4854                    pending_block = false;
4855                }
4856                b'/' if regex_allowed => {
4857                    if let Some(end) = js_regex_end(bytes, index) {
4858                        index = end;
4859                        regex_allowed = false;
4860                    } else {
4861                        index += 1;
4862                        regex_allowed = true;
4863                    }
4864                    pending_control_parenthesis = false;
4865                    statement_start = false;
4866                    pending_block = false;
4867                }
4868                b'{' => {
4869                    let is_block = pending_block || !regex_allowed || statement_start;
4870                    brace_blocks.push(is_block);
4871                    if stop_brace.is_some() {
4872                        brace_depth += 1;
4873                    }
4874                    index += 1;
4875                    regex_allowed = true;
4876                    pending_control_parenthesis = false;
4877                    statement_start = is_block;
4878                    pending_block = false;
4879                }
4880                b'}' => {
4881                    if stop_brace.is_some() {
4882                        brace_depth = brace_depth.saturating_sub(1);
4883                    }
4884                    index += 1;
4885                    if stop_brace.is_some() && brace_depth == 0 {
4886                        return index;
4887                    }
4888                    let is_block = brace_blocks.pop().unwrap_or(true);
4889                    regex_allowed = is_block;
4890                    pending_control_parenthesis = false;
4891                    statement_start = is_block;
4892                    pending_block = false;
4893                }
4894                byte if is_js_identifier_start(byte) || byte.is_ascii_digit() => {
4895                    let start = index;
4896                    index += 1;
4897                    while index < bytes.len() && is_js_identifier_continue(bytes[index]) {
4898                        index += 1;
4899                    }
4900                    let token = &bytes[start..index];
4901                    pending_control_parenthesis = is_js_control_keyword(token);
4902                    pending_block = matches!(token, b"else" | b"do" | b"try" | b"finally");
4903                    regex_allowed = pending_control_parenthesis
4904                        || matches!(
4905                            token,
4906                            b"return"
4907                                | b"throw"
4908                                | b"case"
4909                                | b"delete"
4910                                | b"void"
4911                                | b"typeof"
4912                                | b"yield"
4913                                | b"await"
4914                                | b"new"
4915                                | b"in"
4916                                | b"of"
4917                                | b"else"
4918                                | b"do"
4919                        );
4920                    statement_start = false;
4921                }
4922                b'(' => {
4923                    control_parentheses.push(pending_control_parenthesis);
4924                    pending_control_parenthesis = false;
4925                    index += 1;
4926                    regex_allowed = true;
4927                    statement_start = false;
4928                    pending_block = false;
4929                }
4930                b')' => {
4931                    let control = control_parentheses.pop().unwrap_or(false);
4932                    regex_allowed = control;
4933                    pending_control_parenthesis = false;
4934                    index += 1;
4935                    statement_start = control;
4936                    pending_block = control;
4937                }
4938                b']' => {
4939                    index += 1;
4940                    regex_allowed = false;
4941                    pending_control_parenthesis = false;
4942                    statement_start = false;
4943                    pending_block = false;
4944                }
4945                b'+' | b'-' if bytes.get(index + 1) == Some(&bytes[index]) => {
4946                    index += 2;
4947                    regex_allowed = false;
4948                    pending_control_parenthesis = false;
4949                    statement_start = false;
4950                    pending_block = false;
4951                }
4952                b'\r' | b'\n' => {
4953                    index = consume_newline(bytes, index);
4954                    if stop_brace.is_none()
4955                        && depth == 0
4956                        && regex_allowed
4957                        && statement_start
4958                        && !pending_control_parenthesis
4959                        && !pending_block
4960                        && control_parentheses.is_empty()
4961                        && brace_blocks.is_empty()
4962                    {
4963                        self.add_safe_checkpoint(index);
4964                    }
4965                }
4966                byte if js_is_space(byte) => index += 1,
4967                b'=' if bytes.get(index + 1) == Some(&b'>') => {
4968                    index += 2;
4969                    regex_allowed = true;
4970                    pending_control_parenthesis = false;
4971                    statement_start = true;
4972                    pending_block = true;
4973                }
4974                b';' => {
4975                    index += 1;
4976                    regex_allowed = true;
4977                    pending_control_parenthesis = false;
4978                    statement_start = true;
4979                    pending_block = false;
4980                }
4981                b':' => {
4982                    index += 1;
4983                    regex_allowed = true;
4984                    pending_control_parenthesis = false;
4985                    statement_start = brace_blocks.last().copied().unwrap_or(true);
4986                    pending_block = false;
4987                }
4988                _ => {
4989                    regex_allowed = true;
4990                    pending_control_parenthesis = false;
4991                    statement_start = false;
4992                    pending_block = false;
4993                    index += 1;
4994                }
4995            }
4996        }
4997        if stop_brace.is_some() {
4998            self.error(
4999                "unterminated-template-expression",
5000                "unterminated JavaScript template expression",
5001                ByteSpan::new(index, index),
5002            );
5003        }
5004        index
5005    }
5006
5007    fn scan_jsx_element(&mut self, start: usize, depth: usize) -> usize {
5008        if depth > 256 {
5009            self.error(
5010                "nesting-limit",
5011                "JSX lexical nesting limit exceeded",
5012                ByteSpan::new(start, start),
5013            );
5014            return self.source.len();
5015        }
5016        let bytes = self.source;
5017        let mut index = start;
5018        let mut element_depth = 0usize;
5019        while index < bytes.len() {
5020            if bytes[index] == b'{' {
5021                index = self.scan_js_code(index + 1, Some(1), depth + 1);
5022                continue;
5023            }
5024            if bytes[index] != b'<' {
5025                index += 1;
5026                continue;
5027            }
5028            let closing = bytes.get(index + 1) == Some(&b'/');
5029            let opening = jsx_open(bytes, index);
5030            if !closing && !opening {
5031                index += 1;
5032                continue;
5033            }
5034            let mut cursor = index + if closing { 2 } else { 1 };
5035            let mut quote = None;
5036            let mut self_closing = false;
5037            let mut found_end = false;
5038            while cursor < bytes.len() {
5039                if let Some(active) = quote {
5040                    if bytes[cursor] == b'\\' {
5041                        cursor = (cursor + 2).min(bytes.len());
5042                    } else {
5043                        if bytes[cursor] == active {
5044                            quote = None;
5045                        }
5046                        cursor += 1;
5047                    }
5048                    continue;
5049                }
5050                match bytes[cursor] {
5051                    b'\'' | b'"' => {
5052                        quote = Some(bytes[cursor]);
5053                        cursor += 1;
5054                    }
5055                    b'{' if !closing => {
5056                        cursor = self.scan_js_code(cursor + 1, Some(1), depth + 1);
5057                    }
5058                    b'>' => {
5059                        let mut previous = cursor;
5060                        while previous > index && js_is_space(bytes[previous - 1]) {
5061                            previous -= 1;
5062                        }
5063                        self_closing = previous > index && bytes[previous - 1] == b'/';
5064                        cursor += 1;
5065                        found_end = true;
5066                        break;
5067                    }
5068                    _ => cursor += 1,
5069                }
5070            }
5071            if !found_end {
5072                self.error(
5073                    "unterminated-jsx-tag",
5074                    "unterminated JSX tag",
5075                    ByteSpan::new(index, bytes.len()),
5076                );
5077                return bytes.len();
5078            }
5079            index = cursor;
5080            if closing {
5081                element_depth = element_depth.saturating_sub(1);
5082                if element_depth == 0 {
5083                    return index;
5084                }
5085            } else if !self_closing {
5086                element_depth += 1;
5087            } else if element_depth == 0 {
5088                return index;
5089            }
5090        }
5091        self.error(
5092            "unterminated-jsx-element",
5093            "unterminated JSX element",
5094            ByteSpan::new(start, bytes.len()),
5095        );
5096        bytes.len()
5097    }
5098
5099    fn scan_js_template(&mut self, start: usize, depth: usize) -> usize {
5100        let bytes = self.source;
5101        let mut index = start + 1;
5102        while index < bytes.len() {
5103            match bytes[index] {
5104                b'\\' => index = (index + 2).min(bytes.len()),
5105                b'`' => return index + 1,
5106                b'$' if bytes.get(index + 1) == Some(&b'{') => {
5107                    index = self.scan_js_code(index + 2, Some(1), depth);
5108                }
5109                _ => index += 1,
5110            }
5111        }
5112        self.error(
5113            "unterminated-string",
5114            "unterminated JavaScript template literal",
5115            ByteSpan::new(start, index),
5116        );
5117        index
5118    }
5119
5120    fn scan_html(&mut self) {
5121        let bytes = self.source;
5122        let mut index = 0;
5123        while index < bytes.len() && !self.stopped {
5124            if starts(bytes, index, b"<!--") {
5125                let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
5126                    index + 4 + relative + 3
5127                } else {
5128                    self.error(
5129                        "unterminated-comment",
5130                        "unterminated HTML comment",
5131                        ByteSpan::new(index, bytes.len()),
5132                    );
5133                    bytes.len()
5134                };
5135                self.add_comment(index, end, CommentKind::HtmlComment);
5136                index = end;
5137                continue;
5138            }
5139            if bytes[index] == b'<' {
5140                if let Some((name, language)) = html_embedded_start(bytes, index) {
5141                    let Some(content_start) = html_tag_end(bytes, index) else {
5142                        self.error(
5143                            "unterminated-html-tag",
5144                            "unterminated HTML raw-text start tag",
5145                            ByteSpan::new(index, bytes.len()),
5146                        );
5147                        return;
5148                    };
5149                    let close = find_html_close(bytes, content_start, name);
5150                    let content_end = close.unwrap_or(bytes.len());
5151                    let slice = &bytes[content_start..content_end];
5152                    let mut child = Scanner::child(
5153                        slice,
5154                        language,
5155                        self.options.clone(),
5156                        self.patterns.clone(),
5157                        self.offset + content_start,
5158                    );
5159                    if language == Language::JavaScript {
5160                        child.scan_javascript();
5161                    } else {
5162                        child.scan_c_family();
5163                    }
5164                    self.merge_child(child);
5165                    let Some(close) = close else {
5166                        self.error(
5167                            "unterminated-embedded-language",
5168                            "unterminated HTML script or style element",
5169                            ByteSpan::new(index, bytes.len()),
5170                        );
5171                        return;
5172                    };
5173                    let Some(element_end) = html_tag_end(bytes, close) else {
5174                        self.error(
5175                            "unterminated-html-tag",
5176                            "unterminated HTML raw-text end tag",
5177                            ByteSpan::new(close, bytes.len()),
5178                        );
5179                        return;
5180                    };
5181                    index = element_end;
5182                    continue;
5183                }
5184                if !html_tag_candidate(bytes, index) {
5185                    index += 1;
5186                } else if let Some(end) = html_tag_end(bytes, index) {
5187                    index = end;
5188                } else {
5189                    self.error(
5190                        "unterminated-html-tag",
5191                        "unterminated HTML tag",
5192                        ByteSpan::new(index, bytes.len()),
5193                    );
5194                    return;
5195                }
5196                continue;
5197            }
5198            if matches!(bytes[index], b'\r' | b'\n') {
5199                index = consume_newline(bytes, index);
5200                self.add_safe_checkpoint(index);
5201            } else {
5202                index += 1;
5203            }
5204        }
5205    }
5206
5207    fn scan_vue(&mut self) {
5208        self.scan_sfc(true);
5209    }
5210
5211    fn scan_svelte(&mut self) {
5212        self.scan_sfc(false);
5213    }
5214
5215    /// One Perl document.
5216    ///
5217    /// Perl's lexical surface is almost all quote words: the single and
5218    /// double quotes and backticks, the `q`, `qq`, `qw` and `qx` forms, the
5219    /// `m`, `s`, `tr` and `y` operators with delimiters of their own, and the
5220    /// here-documents, all hide a `#` written inside them, and a POD block is
5221    /// opaque. The one place the bytes do not settle the reading is a `/`
5222    /// directly after a closing parenthesis, bracket or brace: perl reads
5223    /// `f() /a#b/` as a regular expression and `(2) / 2` as a division, and
5224    /// only the parse context tells which, so this scanner reports that `/`
5225    /// as lexically ambiguous and refuses to edit the file, keeping the `#`
5226    /// that would decide the other way out of reach.
5227    fn scan_perl(&mut self) {
5228        let bytes = self.source;
5229        let mut index = 0;
5230        let mut regex_allowed: Option<bool> = Some(true);
5231        while index < bytes.len() && !self.stopped {
5232            if perl_at_line_start(bytes, index)
5233                && (perl_marker_line(bytes, index, b"__DATA__")
5234                    || perl_marker_line(bytes, index, b"__END__"))
5235            {
5236                break;
5237            }
5238            if (index == 0 || matches!(bytes[index - 1], b'\n' | b'\r'))
5239                && bytes[index] == b'='
5240                && bytes
5241                    .get(index + 1)
5242                    .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
5243                && !perl_pod_directive(bytes, index, b"cut")
5244            {
5245                index = self.scan_perl_pod(index);
5246                continue;
5247            }
5248            if bytes[index] == b'#' {
5249                let end = line_end(bytes, index + 1);
5250                self.add_comment(index, end, CommentKind::Line);
5251                index = end;
5252                regex_allowed = Some(true);
5253                continue;
5254            }
5255            if matches!(bytes[index], b'\'' | b'"' | b'`') {
5256                index = self.scan_perl_quoted(index);
5257                regex_allowed = Some(false);
5258                continue;
5259            }
5260            if bytes[index] == b'<'
5261                && bytes.get(index + 1) == Some(&b'<')
5262                && let Some(end) = self.scan_perl_heredocs(index)
5263            {
5264                index = end;
5265                regex_allowed = Some(false);
5266                continue;
5267            }
5268            if (index == 0 || !is_perl_word_byte(bytes[index - 1]))
5269                && matches!(bytes[index], b'q' | b'm' | b's' | b't' | b'y')
5270                && let Some(end) = self.scan_perl_quote_word(index)
5271            {
5272                index = end;
5273                regex_allowed = Some(false);
5274                continue;
5275            }
5276            if bytes[index] == b'/' {
5277                match regex_allowed {
5278                    Some(true) => {
5279                        let end = self.scan_perl_regex(index);
5280                        index = end;
5281                        regex_allowed = Some(false);
5282                    }
5283                    Some(false) => {
5284                        index += 1;
5285                        regex_allowed = Some(true);
5286                    }
5287                    None => {
5288                        let end = self.scan_perl_regex(index);
5289                        self.error(
5290                            "lexical-ambiguity",
5291                            "ambiguous `/` after a closing delimiter: a regex or a division",
5292                            ByteSpan::new(index, end),
5293                        );
5294                        index = end;
5295                        regex_allowed = Some(false);
5296                    }
5297                }
5298                continue;
5299            }
5300            if bytes[index] == b')' || bytes[index] == b']' || bytes[index] == b'}' {
5301                index += 1;
5302                regex_allowed = None;
5303                continue;
5304            }
5305            if bytes[index] == b'(' || bytes[index] == b'[' || bytes[index] == b'{' {
5306                index += 1;
5307                regex_allowed = Some(true);
5308                continue;
5309            }
5310            if matches!(bytes[index], b'$' | b'@' | b'%') {
5311                index = perl_variable_end(bytes, index);
5312                regex_allowed = Some(false);
5313                continue;
5314            }
5315            if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' {
5316                let start = index;
5317                index += 1;
5318                while index < bytes.len() && is_perl_word_byte(bytes[index]) {
5319                    index += 1;
5320                }
5321                let word = &bytes[start..index];
5322                if word == b"format"
5323                    && bytes[line_start(bytes, start)..start]
5324                        .iter()
5325                        .all(|byte| matches!(byte, b' ' | b'\t'))
5326                    && let Some(end) = self.scan_perl_format(start)
5327                {
5328                    index = end;
5329                    regex_allowed = Some(true);
5330                    continue;
5331                }
5332                regex_allowed = perl_word_allows_regex(word);
5333                continue;
5334            }
5335            if bytes[index].is_ascii_digit() {
5336                while index < bytes.len()
5337                    && (bytes[index].is_ascii_alphanumeric() || matches!(bytes[index], b'.' | b'_'))
5338                {
5339                    index += 1;
5340                }
5341                regex_allowed = Some(false);
5342                continue;
5343            }
5344            match bytes[index] {
5345                b'=' | b'+' | b'-' | b'*' | b'%' | b'!' | b'~' | b'&' | b'|' | b'?' | b':'
5346                | b',' | b';' | b'<' | b'>' => {
5347                    index += 1;
5348                    regex_allowed = Some(true);
5349                }
5350                b'\r' | b'\n' => {
5351                    index = consume_newline(bytes, index);
5352                    self.add_safe_checkpoint(index);
5353                    regex_allowed = Some(true);
5354                }
5355                _ => index += 1,
5356            }
5357        }
5358    }
5359
5360    /// One POD block, beginning at the `=` of its marker. The block is opaque
5361    /// until a complete `=cut` directive; `=cutlery` is POD text, not a
5362    /// boundary.
5363    fn scan_perl_pod(&mut self, start: usize) -> usize {
5364        let bytes = self.source;
5365        let mut index = start;
5366        while index < bytes.len() {
5367            let line_finish = line_end(bytes, index);
5368            if index != start && perl_pod_directive(bytes, index, b"cut") {
5369                return if line_finish >= bytes.len() {
5370                    line_finish
5371                } else {
5372                    consume_newline(bytes, line_finish)
5373                };
5374            }
5375            if index == start {
5376                let pod_line = line_end(bytes, start);
5377                if pod_line >= bytes.len() {
5378                    return bytes.len();
5379                }
5380                index = consume_newline(bytes, pod_line);
5381                continue;
5382            }
5383            if line_finish >= bytes.len() {
5384                return bytes.len();
5385            }
5386            index = consume_newline(bytes, line_finish);
5387        }
5388        bytes.len()
5389    }
5390
5391    /// One Perl string or command substitution, beginning at its quote.
5392    fn scan_perl_quoted(&mut self, start: usize) -> usize {
5393        perl_quoted_end(self.source, start)
5394    }
5395
5396    /// One Perl quote word, here-document or the `m`, `s`, `tr` and `y`
5397    /// operators, beginning at the letter that names it, or `None` when the
5398    /// letter is a bareword rather than a quote.
5399    fn scan_perl_quote_word(&mut self, start: usize) -> Option<usize> {
5400        let bytes = self.source;
5401        let mut reach = Reach::default();
5402        let mut cursor = start;
5403        let mut form = Vec::new();
5404        form.push(bytes[cursor]);
5405        reach.byte(cursor);
5406        if matches!(bytes[cursor], b'q' | b't')
5407            && let Some(&second) = bytes.get(cursor + 1)
5408            && ((bytes[cursor] == b'q' && matches!(second, b'q' | b'w' | b'x' | b'r'))
5409                || (bytes[cursor] == b't' && second == b'r'))
5410        {
5411            form.push(second);
5412            cursor += 1;
5413            reach.byte(cursor);
5414        }
5415        cursor += 1;
5416        while cursor < bytes.len()
5417            && bytes[cursor].is_ascii_whitespace()
5418            && !matches!(bytes[cursor], b'\r' | b'\n')
5419        {
5420            reach.byte(cursor);
5421            cursor += 1;
5422        }
5423        let Some(delimiter) = bytes.get(cursor).copied() else {
5424            reach.end_of(bytes);
5425            self.consult(reach);
5426            return None;
5427        };
5428        reach.byte(cursor);
5429        if is_perl_word_byte(delimiter) || delimiter.is_ascii_whitespace() {
5430            self.consult(reach);
5431            return None;
5432        }
5433        let Some(first) = perl_section_end_reach(bytes, cursor, delimiter, &mut reach) else {
5434            self.consult(reach);
5435            return None;
5436        };
5437        if matches!(form[0], b's' | b't' | b'y') {
5438            let paired = matches!(delimiter, b'(' | b'[' | b'{' | b'<');
5439            let second_end = if paired {
5440                let mut second_start = first;
5441                while bytes.get(second_start).is_some_and(|byte| {
5442                    byte.is_ascii_whitespace() && !matches!(byte, b'\r' | b'\n')
5443                }) {
5444                    reach.byte(second_start);
5445                    second_start += 1;
5446                }
5447                let Some(second) = bytes.get(second_start).copied() else {
5448                    reach.end_of(bytes);
5449                    self.consult(reach);
5450                    return None;
5451                };
5452                reach.byte(second_start);
5453                if is_perl_word_byte(second) || second.is_ascii_whitespace() {
5454                    self.consult(reach);
5455                    return None;
5456                }
5457                let Some(end) = perl_section_end_reach(bytes, second_start, second, &mut reach)
5458                else {
5459                    self.consult(reach);
5460                    return None;
5461                };
5462                end
5463            } else {
5464                let Some(end) =
5465                    perl_unpaired_section_end_reach(bytes, first, delimiter, &mut reach)
5466                else {
5467                    self.consult(reach);
5468                    return None;
5469                };
5470                end
5471            };
5472            let end = perl_modifiers_end(bytes, second_end);
5473            reach.through(end);
5474            self.consult(reach);
5475            return Some(end);
5476        }
5477        let end = perl_modifiers_end(bytes, first);
5478        reach.through(end);
5479        self.consult(reach);
5480        Some(end)
5481    }
5482
5483    /// Every Perl here-document queued by the header line beginning at this
5484    /// `<<`, in declaration order. Horizontal white space is permitted around
5485    /// `~` and the delimiter.
5486    fn scan_perl_heredocs(&mut self, start: usize) -> Option<usize> {
5487        let bytes = self.source;
5488        let header_end = line_end(bytes, start);
5489        let mut declarations = Vec::new();
5490        let mut header_comment = None;
5491        let mut search = start;
5492        while search < header_end {
5493            if matches!(bytes[search], b'\'' | b'"' | b'`') {
5494                search = perl_quoted_end(bytes, search).min(header_end);
5495            } else if bytes[search] == b'#' {
5496                header_comment = Some(search);
5497                break;
5498            } else if matches!(bytes[search], b'$' | b'@' | b'%') {
5499                search = perl_variable_end(bytes, search).min(header_end);
5500            } else if starts(bytes, search, b"<<") {
5501                let Some((declaration, end)) = perl_heredoc_declaration(bytes, search, header_end)
5502                else {
5503                    search += 2;
5504                    continue;
5505                };
5506                declarations.push(declaration);
5507                search = end;
5508            } else {
5509                search += 1;
5510            }
5511        }
5512        if declarations.is_empty() {
5513            return None;
5514        }
5515        if let Some(comment) = header_comment {
5516            self.add_comment(comment, header_end, CommentKind::Line);
5517        }
5518        let mut body = if header_end >= bytes.len() {
5519            return Some(bytes.len());
5520        } else {
5521            consume_newline(bytes, header_end)
5522        };
5523        for declaration in declarations {
5524            let mut found = false;
5525            while body < bytes.len() {
5526                let line_finish = line_end(bytes, body);
5527                let mut content = body;
5528                if declaration.indented {
5529                    while content < line_finish && matches!(bytes[content], b' ' | b'\t') {
5530                        content += 1;
5531                    }
5532                }
5533                if bytes[content..line_finish] == declaration.terminator[..] {
5534                    body = if line_finish >= bytes.len() {
5535                        line_finish
5536                    } else {
5537                        consume_newline(bytes, line_finish)
5538                    };
5539                    found = true;
5540                    break;
5541                }
5542                body = if line_finish >= bytes.len() {
5543                    bytes.len()
5544                } else {
5545                    consume_newline(bytes, line_finish)
5546                };
5547            }
5548            if !found {
5549                return Some(bytes.len());
5550            }
5551        }
5552        Some(body)
5553    }
5554
5555    /// A format declaration and its picture body, through the line containing
5556    /// only `.`. Picture lines are a mini-language and are opaque to Perl's
5557    /// ordinary `#` comment token.
5558    fn scan_perl_format(&self, start: usize) -> Option<usize> {
5559        let bytes = self.source;
5560        let header_end = line_end(bytes, start);
5561        if !bytes[start + b"format".len()..header_end].contains(&b'=') {
5562            return None;
5563        }
5564        let mut line = if header_end < bytes.len() {
5565            consume_newline(bytes, header_end)
5566        } else {
5567            return Some(bytes.len());
5568        };
5569        while line < bytes.len() {
5570            let finish = line_end(bytes, line);
5571            if bytes[line..finish].trim_ascii() == b"." {
5572                return Some(if finish < bytes.len() {
5573                    consume_newline(bytes, finish)
5574                } else {
5575                    finish
5576                });
5577            }
5578            line = if finish < bytes.len() {
5579                consume_newline(bytes, finish)
5580            } else {
5581                bytes.len()
5582            };
5583        }
5584        Some(bytes.len())
5585    }
5586
5587    /// One Perl regular expression, beginning at its `/`, to the unescaped
5588    /// `/` that closes it — a `/` inside a character class is content — and
5589    /// past the modifiers that follow.
5590    fn scan_perl_regex(&mut self, start: usize) -> usize {
5591        let bytes = self.source;
5592        let mut index = start + 1;
5593        while index < bytes.len() {
5594            if bytes[index] == b'\\' {
5595                index = (index + 2).min(bytes.len());
5596                continue;
5597            }
5598            if bytes[index] == b'[' {
5599                index = (index + 1).min(bytes.len());
5600                while index < bytes.len() && bytes[index] != b']' {
5601                    if bytes[index] == b'\\' {
5602                        index = (index + 2).min(bytes.len());
5603                    } else {
5604                        index += 1;
5605                    }
5606                }
5607                index += 1;
5608                continue;
5609            }
5610            if bytes[index] == b'/' {
5611                index += 1;
5612                while index < bytes.len() && is_perl_word_byte(bytes[index]) {
5613                    index += 1;
5614                }
5615                return index;
5616            }
5617            index += 1;
5618        }
5619        bytes.len()
5620    }
5621
5622    /// One Markdown document.
5623    ///
5624    /// An HTML comment is a comment, a fenced code block is scanned as the
5625    /// language its info string names — an unknown or absent language leaves
5626    /// the block opaque — and an inline code span or an indented code block
5627    /// is opaque: a `//` or a `/*` in one is code text, not a comment. Every
5628    /// construct is recognised at its own start and read forward, so no
5629    /// decision depends on a byte behind a checkpoint.
5630    fn scan_markdown(&mut self) {
5631        let bytes = self.source;
5632        let mut index = 0;
5633        while index < bytes.len() && !self.stopped {
5634            if starts(bytes, index, b"<!--") {
5635                let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
5636                    index + 4 + relative + 3
5637                } else {
5638                    self.error(
5639                        "unterminated-comment",
5640                        "unterminated HTML comment",
5641                        ByteSpan::new(index, bytes.len()),
5642                    );
5643                    bytes.len()
5644                };
5645                self.add_comment(index, end, CommentKind::HtmlComment);
5646                index = end;
5647                continue;
5648            }
5649            if index == 0 || matches!(bytes[index - 1], b'\r' | b'\n') {
5650                let line_finish = line_end(bytes, index);
5651                let (cursor, indent) = markdown_indent(bytes, index, line_finish);
5652                if indent >= 4 {
5653                    index = self.scan_markdown_indented(index);
5654                    continue;
5655                }
5656                if indent <= 3 && cursor < bytes.len() && matches!(bytes[cursor], b'`' | b'~') {
5657                    let marker = bytes[cursor];
5658                    let run = count_run(bytes, cursor, marker);
5659                    let info_end = line_end(bytes, cursor + run);
5660                    let valid_info =
5661                        marker != b'`' || !bytes[cursor + run..info_end].contains(&b'`');
5662                    if run >= 3 && valid_info {
5663                        index = self.scan_markdown_fence(cursor, marker, run);
5664                        continue;
5665                    }
5666                }
5667            }
5668            if bytes[index] == b'`' {
5669                let mut reach = Reach::default();
5670                index = markdown_inline_code_end(bytes, index, &mut reach);
5671                self.consult(reach);
5672                continue;
5673            }
5674            if matches!(bytes[index], b'\r' | b'\n') {
5675                index = consume_newline(bytes, index);
5676                self.add_safe_checkpoint(index);
5677            } else {
5678                index += 1;
5679            }
5680        }
5681    }
5682
5683    /// One fenced code block, beginning at its opening run of backticks or
5684    /// tildes. The body is scanned as the language its info string names, or
5685    /// as nothing when the string names none; the block ends at a line of the
5686    /// same marker with a run at least as long as the opener's, and a block
5687    /// that never closes is a file CommonMark reads to its end, so the rest
5688    /// of the document is opaque.
5689    fn scan_markdown_fence(&mut self, opener: usize, marker: u8, run: usize) -> usize {
5690        let bytes = self.source;
5691        let info_start = opener + run;
5692        let info_end = line_end(bytes, info_start);
5693        let language = markdown_fence_language(&bytes[info_start..info_end]);
5694        let mut cursor = info_end;
5695        let closer = loop {
5696            if cursor >= bytes.len() {
5697                break None;
5698            }
5699            let mut line = cursor;
5700            let mut spaces = 0;
5701            while line < bytes.len() && bytes[line] == b' ' && spaces < 3 {
5702                line += 1;
5703                spaces += 1;
5704            }
5705            if bytes.get(line) == Some(&marker) {
5706                let closer_run = count_run(bytes, line, marker);
5707                if closer_run >= run {
5708                    let mut after = line + closer_run;
5709                    while after < bytes.len() && matches!(bytes[after], b' ' | b'\t') {
5710                        after += 1;
5711                    }
5712                    if after >= bytes.len() || matches!(bytes[after], b'\r' | b'\n') {
5713                        break Some((line, consume_newline(bytes, after).min(bytes.len())));
5714                    }
5715                }
5716            }
5717            let line_finish = line_end(bytes, cursor);
5718            if line_finish >= bytes.len() {
5719                break None;
5720            }
5721            cursor = consume_newline(bytes, line_finish);
5722        };
5723        let content_end = closer.map_or(bytes.len(), |(line, _)| line);
5724        if let Some(language) = language
5725            && !matches!(
5726                language,
5727                Language::Html | Language::Vue | Language::Svelte | Language::Markdown
5728            )
5729        {
5730            let mut child = Scanner::child(
5731                &bytes[info_end..content_end],
5732                language,
5733                self.options.clone(),
5734                self.patterns.clone(),
5735                self.offset + info_end,
5736            );
5737            child.scan_language();
5738            self.merge_child(child);
5739        }
5740        closer.map_or(bytes.len(), |(_, resume)| resume)
5741    }
5742
5743    /// One indented code block, beginning at its first non-space byte. The
5744    /// block holds every following line that is blank or indented at least
5745    /// four spaces, and all of it is opaque.
5746    fn scan_markdown_indented(&mut self, start: usize) -> usize {
5747        let bytes = self.source;
5748        let mut index = start;
5749        while index < bytes.len() {
5750            let line_finish = line_end(bytes, index);
5751            let (cursor, indent) = markdown_indent(bytes, index, line_finish);
5752            let blank = cursor >= line_finish;
5753            if !blank && indent < 4 {
5754                return index;
5755            }
5756            if line_finish >= bytes.len() {
5757                return bytes.len();
5758            }
5759            index = consume_newline(bytes, line_finish);
5760        }
5761        bytes.len()
5762    }
5763
5764    /// One Vue or Svelte single-file component.
5765    ///
5766    /// The top level holds the `<script>` and `<style>` blocks and — for Vue —
5767    /// the `<template>` block, and the body of each is scanned as its own
5768    /// language, the `lang` attribute choosing which. For Svelte the text
5769    /// between the blocks is the template itself, whose every `{ ... }` opens
5770    /// an expression; for Vue the template is the body of its `<template>`
5771    /// element. A `lang` this scanner has no rules for makes the whole block
5772    /// opaque, and a top-level `<!-- ... -->` is an HTML comment.
5773    fn scan_sfc(&mut self, vue: bool) {
5774        let bytes = self.source;
5775        let mut index = 0;
5776        while index < bytes.len() && !self.stopped {
5777            if starts(bytes, index, b"<!--") {
5778                let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
5779                    index + 4 + relative + 3
5780                } else {
5781                    self.error(
5782                        "unterminated-comment",
5783                        "unterminated HTML comment",
5784                        ByteSpan::new(index, bytes.len()),
5785                    );
5786                    bytes.len()
5787                };
5788                self.add_comment(index, end, CommentKind::HtmlComment);
5789                index = end;
5790                continue;
5791            }
5792            if bytes[index] == b'{' && !vue {
5793                index = self.scan_js_code(index + 1, Some(1), 0);
5794                continue;
5795            }
5796            if bytes[index] == b'<' {
5797                if let Some(end) = self.scan_sfc_block(index, vue) {
5798                    index = end;
5799                    continue;
5800                }
5801                if html_tag_candidate(bytes, index) {
5802                    let end = if vue {
5803                        sfc_tag_end(bytes, index)
5804                    } else {
5805                        self.scan_svelte_tag_end(index)
5806                    };
5807                    if let Some(end) = end {
5808                        index = end;
5809                        continue;
5810                    }
5811                }
5812            }
5813            if matches!(bytes[index], b'\r' | b'\n') {
5814                index = consume_newline(bytes, index);
5815                self.add_safe_checkpoint(index);
5816            } else {
5817                index += 1;
5818            }
5819        }
5820    }
5821
5822    /// One `<script>`, `<style>` or — for Vue — `<template>` block beginning
5823    /// at its start tag, scanned for its embedded language, or `None` when
5824    /// the tag there opens none of them.
5825    fn scan_sfc_block(&mut self, start: usize, vue: bool) -> Option<usize> {
5826        let bytes = self.source;
5827        let rest = &bytes[start..];
5828        let name: &[u8] = if starts_ascii_case(rest, b"<script")
5829            && tag_boundary(rest.get(7).copied())
5830        {
5831            b"script"
5832        } else if starts_ascii_case(rest, b"<style") && tag_boundary(rest.get(6).copied()) {
5833            b"style"
5834        } else if vue && starts_ascii_case(rest, b"<template") && tag_boundary(rest.get(9).copied())
5835        {
5836            b"template"
5837        } else {
5838            return None;
5839        };
5840        let tag_end = if vue {
5841            sfc_tag_end(bytes, start)
5842        } else {
5843            self.scan_svelte_tag_end(start)
5844        };
5845        let Some(tag_end) = tag_end else {
5846            self.error(
5847                "unterminated-html-tag",
5848                "unterminated single-file component start tag",
5849                ByteSpan::new(start, bytes.len()),
5850            );
5851            return Some(bytes.len());
5852        };
5853        let attrs = &bytes[start + 1 + name.len()..tag_end.saturating_sub(1)];
5854        let lang = tag_attr_value(attrs, b"lang");
5855        let Some(close) = find_html_close(bytes, tag_end, name) else {
5856            self.error(
5857                "unterminated-embedded-language",
5858                "unterminated single-file component element",
5859                ByteSpan::new(start, bytes.len()),
5860            );
5861            return Some(bytes.len());
5862        };
5863        let content_start = tag_end;
5864        let content_end = close;
5865        let resolved = match name {
5866            b"script" => vue_script_language(lang),
5867            b"style" => vue_style_language(lang),
5868            b"template" => {
5869                let html = lang.is_none_or(|value| {
5870                    let lower = value.to_ascii_lowercase();
5871                    lower == b"html"
5872                });
5873                if html {
5874                    Some((Language::Html, Dialect::Standard))
5875                } else {
5876                    None
5877                }
5878            }
5879            _ => None,
5880        };
5881        match resolved {
5882            Some((Language::Html, _)) => {
5883                /* NOTE: the template is scanned on the scanner itself, because
5884                 * it is the same language — Vue or Svelte — and the mustache
5885                 * code it holds is JavaScript. */
5886                self.scan_sfc_template(vue, content_start, content_end);
5887            }
5888            Some((language, dialect)) => {
5889                let mut child_options = self.options.clone();
5890                child_options.dialect = dialect;
5891                let mut child = Scanner::child(
5892                    &bytes[content_start..content_end],
5893                    language,
5894                    child_options,
5895                    self.patterns.clone(),
5896                    self.offset + content_start,
5897                );
5898                match language {
5899                    Language::JavaScript | Language::TypeScript => child.scan_javascript(),
5900                    Language::Css if dialect == Dialect::Sass => child.scan_sass(),
5901                    Language::Css => child.scan_c_family(),
5902                    _ => {}
5903                }
5904                self.merge_child(child);
5905            }
5906            None => {}
5907        }
5908        Some(content_end)
5909    }
5910
5911    /// The body of a Vue `<template>`: HTML with `{{ ... }}` mustaches that
5912    /// are code, and `v-pre` elements whose whole content is raw text.
5913    fn scan_sfc_template(&mut self, vue: bool, start: usize, end: usize) {
5914        let bytes = self.source;
5915        let mut index = start;
5916        while index < end {
5917            if starts(bytes, index, b"<!--") {
5918                let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
5919                    index + 4 + relative + 3
5920                } else {
5921                    self.error(
5922                        "unterminated-comment",
5923                        "unterminated HTML comment",
5924                        ByteSpan::new(index, bytes.len()),
5925                    );
5926                    bytes.len()
5927                };
5928                self.add_comment(index, end, CommentKind::HtmlComment);
5929                index = end;
5930                continue;
5931            }
5932            if starts(bytes, index, b"{{") && vue {
5933                index = self.scan_js_code(index + 2, Some(2), 0);
5934                continue;
5935            }
5936            if bytes[index] == b'<'
5937                && html_tag_candidate(bytes, index)
5938                && let Some(tag_end) = sfc_tag_end(bytes, index)
5939            {
5940                let name_end = html_tag_name_end(bytes, index);
5941                let attrs = &bytes[name_end..tag_end.saturating_sub(1)];
5942                if vue && tag_has_attribute(attrs, b"v-pre") {
5943                    let name = &bytes[index + 1..name_end];
5944                    if let Some(close) = find_balanced_html_close(bytes, tag_end, name) {
5945                        index = html_tag_end(bytes, close).unwrap_or(close);
5946                        continue;
5947                    }
5948                }
5949                self.scan_sfc_attributes(vue, name_end, tag_end.saturating_sub(1));
5950                index = tag_end;
5951                continue;
5952            }
5953            index += 1;
5954        }
5955    }
5956
5957    /// Scan precisely the attribute forms whose host framework defines as
5958    /// JavaScript. Ordinary HTML attribute text stays opaque.
5959    fn scan_sfc_attributes(&mut self, vue: bool, start: usize, end: usize) {
5960        let parsed = parse_tag_attributes(&self.source[start..end]);
5961        if vue {
5962            for attribute in parsed {
5963                let name = &self.source[start + attribute.name.start..start + attribute.name.end];
5964                if !vue_directive_attribute(name) {
5965                    continue;
5966                }
5967                let Some(value) = attribute.value else {
5968                    continue;
5969                };
5970                let value_start = start + value.start;
5971                let value_end = start + value.end;
5972                let mut child = Scanner::child(
5973                    &self.source[value_start..value_end],
5974                    Language::JavaScript,
5975                    self.options.clone(),
5976                    self.patterns.clone(),
5977                    self.offset + value_start,
5978                );
5979                child.scan_javascript();
5980                self.merge_child(child);
5981            }
5982            return;
5983        }
5984
5985        let mut index = start;
5986        while index < end {
5987            if self.source[index] == b'{' {
5988                let next = self.scan_js_code(index + 1, Some(1), 0);
5989                index = next.max(index + 1).min(end);
5990            } else {
5991                index += 1;
5992            }
5993        }
5994    }
5995
5996    /// End of a Svelte tag, using the JavaScript scanner itself for every
5997    /// braced attribute expression. This matters for a regex such as `/}>/`:
5998    /// its `}` and `>` are pattern bytes, not the end of the expression and
5999    /// tag. The same pass records expression comments, so its caller must not
6000    /// scan the attributes a second time.
6001    fn scan_svelte_tag_end(&mut self, start: usize) -> Option<usize> {
6002        /* INVARIANT: This is a probe until a closing `>` is found. A `<` in
6003         * template text can look like the beginning of a tag and still be
6004         * ordinary text; in that case any braced JavaScript the probe visited
6005         * will be scanned again by `scan_sfc`. Keep the probe transactional so
6006         * its comments and diagnostics cannot leak into that pass. */
6007        let comments = self.comments.len();
6008        let diagnostics = self.diagnostics.len();
6009        let mut index = start + 1;
6010        let mut quote = None;
6011        while index < self.source.len() {
6012            if let Some(active) = quote {
6013                if self.source[index] == active {
6014                    quote = None;
6015                    index += 1;
6016                } else if self.source[index] == b'{' {
6017                    index = self.scan_js_code(index + 1, Some(1), 0);
6018                } else {
6019                    index += 1;
6020                }
6021                continue;
6022            }
6023            match self.source[index] {
6024                b'\'' | b'"' => {
6025                    quote = Some(self.source[index]);
6026                    index += 1;
6027                }
6028                b'{' => index = self.scan_js_code(index + 1, Some(1), 0),
6029                b'>' => return Some(index + 1),
6030                _ => index += 1,
6031            }
6032        }
6033        self.comments.truncate(comments);
6034        self.diagnostics.truncate(diagnostics);
6035        None
6036    }
6037}
6038///
6039/// Compiling a regex set is far more expensive than matching against it, and
6040/// every comment scanned under one set of options is matched against the very
6041/// same two sets. A caller explaining a whole file compiles them once here and
6042/// hands them to [`explain_disposition_with`] for each of its comments.
6043#[derive(Clone, Debug)]
6044pub struct DispositionPatterns {
6045    keep: RegexSet,
6046    remove: RegexSet,
6047    keep_active: bool,
6048    remove_active: bool,
6049}
6050
6051impl DispositionPatterns {
6052    /// The two sets `options` asks for, or the error the first pattern that
6053    /// would not compile raised.
6054    pub fn compile(options: &ScanOptions) -> Result<Self, regex::Error> {
6055        Ok(Self {
6056            keep: RegexSet::new(&options.keep_regex)?,
6057            remove: RegexSet::new(&options.remove_regex)?,
6058            keep_active: !options.keep_regex.is_empty(),
6059            remove_active: !options.remove_regex.is_empty(),
6060        })
6061    }
6062
6063    /// Sets that match nothing: what a pattern list that will not compile falls
6064    /// back to. The scanner reports such a list as a diagnostic and then scans
6065    /// as though it were empty, so an explanation has to ignore it the same way
6066    /// to stay in step with the verdict it is accounting for.
6067    pub fn empty() -> Self {
6068        Self {
6069            keep: RegexSet::empty(),
6070            remove: RegexSet::empty(),
6071            keep_active: false,
6072            remove_active: false,
6073        }
6074    }
6075}
6076
6077pub(crate) fn disposition(
6078    kind: CommentKind,
6079    options: &ScanOptions,
6080    raw: &[u8],
6081    patterns: &DispositionPatterns,
6082) -> Disposition {
6083    if options.keep_kinds.contains(&kind) || (patterns.keep_active && patterns.keep.is_match(raw)) {
6084        return Disposition::Keep {
6085            reason: "kept by kind or regex override".into(),
6086        };
6087    }
6088    let hard = matches!(kind, CommentKind::Shebang | CommentKind::Encoding);
6089    if hard && !options.force_protected {
6090        return Disposition::Keep {
6091            reason: "required source preamble".into(),
6092        };
6093    }
6094    if options.remove_kinds.contains(&kind)
6095        || (patterns.remove_active && patterns.remove.is_match(raw))
6096    {
6097        return Disposition::Remove;
6098    }
6099    if options.policy == Policy::All {
6100        return Disposition::Remove;
6101    }
6102    if kind == CommentKind::HtmlComment {
6103        return Disposition::Keep {
6104            reason: "HTML comments are DOM-observable".into(),
6105        };
6106    }
6107    if matches!(
6108        kind,
6109        CommentKind::Directive | CommentKind::OptimizerHint | CommentKind::VersionComment
6110    ) {
6111        return Disposition::Keep {
6112            reason: "tool or language directive".into(),
6113        };
6114    }
6115    if kind == CommentKind::License && options.policy == Policy::Legal {
6116        return Disposition::Keep {
6117            reason: "legal policy".into(),
6118        };
6119    }
6120    Disposition::Remove
6121}
6122
6123/// The index and text of the first pattern in `set` that matches `raw`.
6124///
6125/// `set` was compiled from `sources` in order, so the index addresses both.
6126fn first_match(set: &RegexSet, raw: &[u8], sources: &[String]) -> Option<(usize, String)> {
6127    let index = set.matches(raw).iter().next()?;
6128    let pattern = sources.get(index).cloned().unwrap_or_default();
6129    Some((index, pattern))
6130}
6131
6132/// Recover the directive name of an already-classified comment from its bytes,
6133/// exactly as [`classify_comment`] found it.
6134fn directive_name_of(raw: &[u8], language: Language) -> Option<&'static str> {
6135    let lower = String::from_utf8_lossy(strip_comment_markers(raw)).to_ascii_lowercase();
6136    directive_name(lower.trim(), language, raw)
6137}
6138
6139/// Recover the legal marker of an already-classified comment from its bytes,
6140/// exactly as [`classify_comment`] found it.
6141fn legal_marker_of(raw: &[u8]) -> Option<&'static str> {
6142    let lower = String::from_utf8_lossy(strip_comment_markers(raw)).to_ascii_lowercase();
6143    legal_marker(lower.trim())
6144}
6145
6146/// Name the rule that decides this comment's fate.
6147///
6148/// The branches below are the branches of `disposition()` in the same order,
6149/// so `explain_disposition(..).action().is_remove()` always equals
6150/// `disposition(..).is_remove()` for the same comment and options. An
6151/// unparseable pattern list is ignored here as the scanner ignores it, which
6152/// keeps the two in step even on input the scanner has already flagged.
6153///
6154/// That agreement is with the bytes-only rule table and with nothing else. A
6155/// scan applies one rule no reading of `raw` can reach —
6156/// [`DispositionExplanation::KeptStructural`], where a YAML block scalar leans
6157/// on the comment that ends it — and a comment kept by *where it sits* is
6158/// reported here as the table alone would have it. That verdict comes only from
6159/// [`explain_comment`], which is handed the [`Comment`] a scan produced.
6160///
6161/// `raw` is the comment's complete bytes, delimiters included, exactly as
6162/// [`Comment::span`](crate::Comment::span) delimits them.
6163///
6164/// # Examples
6165///
6166/// ```
6167/// use ocomment_core::{
6168///     Action, CommentKind, DispositionExplanation, Language, Policy, ScanOptions,
6169///     explain_disposition,
6170/// };
6171///
6172/// let mut options = ScanOptions::default();
6173/// let why = explain_disposition(CommentKind::Line, b"// note", Language::Rust, &options);
6174/// assert_eq!(why.action(), Action::Remove);
6175/// assert!(matches!(why, DispositionExplanation::RemovedByDefault(Policy::Safe)));
6176///
6177/// options.keep_regex.push(r"^//\s*NOTE\b".into());
6178/// let kept = explain_disposition(CommentKind::Line, b"// NOTE: why", Language::Rust, &options);
6179/// assert_eq!(kept.action(), Action::Keep);
6180/// assert!(matches!(kept, DispositionExplanation::KeptByRegex { index: 0, .. }));
6181/// assert_eq!(kept.to_string(), r"kept: matched keep_regex #0 `^//\s*NOTE\b`");
6182/// ```
6183pub fn explain_disposition(
6184    kind: CommentKind,
6185    raw: &[u8],
6186    language: Language,
6187    options: &ScanOptions,
6188) -> DispositionExplanation {
6189    let patterns =
6190        DispositionPatterns::compile(options).unwrap_or_else(|_| DispositionPatterns::empty());
6191    explain_disposition_with(&patterns, kind, raw, language, options)
6192}
6193
6194/// The same answer, against pattern sets the caller already compiled.
6195///
6196/// [`explain_disposition`] compiles `options.keep_regex` and
6197/// `options.remove_regex` on every call, which is once per comment for a caller
6198/// explaining a file. `patterns` must be [`DispositionPatterns::compile`] of the
6199/// same `options` — or [`DispositionPatterns::empty`] where that compile failed,
6200/// which is what the wrapper falls back to — and the two functions then return
6201/// the identical explanation.
6202pub fn explain_disposition_with(
6203    patterns: &DispositionPatterns,
6204    kind: CommentKind,
6205    raw: &[u8],
6206    language: Language,
6207    options: &ScanOptions,
6208) -> DispositionExplanation {
6209    if options.keep_kinds.contains(&kind) {
6210        return DispositionExplanation::KeptByKind(kind);
6211    }
6212    if let Some((index, pattern)) = first_match(&patterns.keep, raw, &options.keep_regex) {
6213        return DispositionExplanation::KeptByRegex { index, pattern };
6214    }
6215    let hard = matches!(kind, CommentKind::Shebang | CommentKind::Encoding);
6216    if hard && !options.force_protected {
6217        return DispositionExplanation::ProtectedPreamble;
6218    }
6219    if options.remove_kinds.contains(&kind) {
6220        return DispositionExplanation::RemovedByKind(kind);
6221    }
6222    if let Some((index, pattern)) = first_match(&patterns.remove, raw, &options.remove_regex) {
6223        return DispositionExplanation::RemovedByRegex { index, pattern };
6224    }
6225    if options.policy == Policy::All {
6226        return DispositionExplanation::RemovedByPolicy(options.policy);
6227    }
6228    if kind == CommentKind::HtmlComment {
6229        return DispositionExplanation::KeptHtml;
6230    }
6231    if matches!(
6232        kind,
6233        CommentKind::Directive | CommentKind::OptimizerHint | CommentKind::VersionComment
6234    ) {
6235        return DispositionExplanation::KeptDirective {
6236            kind,
6237            name: directive_name_of(raw, language),
6238        };
6239    }
6240    if kind == CommentKind::License && options.policy == Policy::Legal {
6241        return DispositionExplanation::KeptLicense {
6242            marker: legal_marker_of(raw),
6243        };
6244    }
6245    DispositionExplanation::RemovedByDefault(options.policy)
6246}
6247
6248/// Name the rule that decided the fate of a comment a scan actually found.
6249///
6250/// [`explain_disposition`] accounts for every rule a comment's own bytes can
6251/// trigger. One rule is not one of those: a YAML block scalar leaning on the
6252/// comment that ends it keeps that comment because of where it sits, and no
6253/// amount of reading its bytes could say so. This is that answer, and for every
6254/// other comment it is exactly [`explain_disposition`].
6255///
6256/// `comment` must be one the scan of `raw`'s file produced, and `raw` its
6257/// complete bytes as [`Comment::span`](crate::Comment::span) delimits them.
6258///
6259/// # Examples
6260///
6261/// ```
6262/// use ocomment_core::{
6263///     Action, DispositionExplanation, Language, ScanOptions, explain_comment, scan,
6264/// };
6265///
6266/// let source = b"k: |\n  a\n# ends the block\n  # yamllint disable\nz: 1\n";
6267/// let report = scan(source, Language::Yaml, ScanOptions::default());
6268/// let comment = &report.comments[0];
6269/// let why = explain_comment(
6270///     comment,
6271///     &source[comment.span.start..comment.span.end],
6272///     Language::Yaml,
6273///     &ScanOptions::default(),
6274/// );
6275/// assert_eq!(why.action(), Action::Keep);
6276/// assert!(matches!(
6277///     why,
6278///     DispositionExplanation::KeptStructural { language: Language::Yaml }
6279/// ));
6280/// ```
6281pub fn explain_comment(
6282    comment: &Comment,
6283    raw: &[u8],
6284    language: Language,
6285    options: &ScanOptions,
6286) -> DispositionExplanation {
6287    let patterns =
6288        DispositionPatterns::compile(options).unwrap_or_else(|_| DispositionPatterns::empty());
6289    explain_comment_with(&patterns, comment, raw, language, options)
6290}
6291
6292/// The same answer, against pattern sets the caller already compiled, as
6293/// [`explain_disposition_with`] is to [`explain_disposition`].
6294pub fn explain_comment_with(
6295    patterns: &DispositionPatterns,
6296    comment: &Comment,
6297    raw: &[u8],
6298    language: Language,
6299    options: &ScanOptions,
6300) -> DispositionExplanation {
6301    if is_yaml_structural_trail(&comment.disposition) {
6302        return DispositionExplanation::KeptStructural { language };
6303    }
6304    explain_disposition_with(patterns, comment.kind, raw, language, options)
6305}
6306
6307fn java_text_block_end(source: &[u8], start: usize) -> (usize, bool) {
6308    let mut index = start.saturating_add(3);
6309    while index + 2 < source.len() {
6310        if starts(source, index, b"\"\"\"") {
6311            let mut backslashes = 0usize;
6312            let mut cursor = index;
6313            while cursor > start + 3 && source[cursor - 1] == b'\\' {
6314                backslashes += 1;
6315                cursor -= 1;
6316            }
6317            if backslashes.is_multiple_of(2) {
6318                return (index + 3, true);
6319            }
6320        }
6321        index += 1;
6322    }
6323    (source.len(), false)
6324}
6325
6326/// How many bytes of UTF-8 byte order mark `source` opens with: three, or none.
6327///
6328/// A BOM is consumed before the first line is read — CPython's `check_bom`,
6329/// Lua's `skipBOM` — so the line behind one is still the first line, and a
6330/// preamble rule that asked for byte 0 alone would miss it. The bytes stay
6331/// where they are; only the question `is this the first line?` skips them.
6332/// [`is_encoding_declaration`] has always skipped the same three.
6333fn byte_order_mark_width(source: &[u8]) -> usize {
6334    if source.starts_with(b"\xef\xbb\xbf") {
6335        3
6336    } else {
6337        0
6338    }
6339}
6340
6341fn classify_comment(
6342    source: &[u8],
6343    language: Language,
6344    lexical: CommentKind,
6345    start: usize,
6346    end: usize,
6347    offset: usize,
6348) -> CommentKind {
6349    let raw = &source[start.min(source.len())..end.min(source.len())];
6350    let body = strip_comment_markers(raw);
6351    let lower = String::from_utf8_lossy(body).to_ascii_lowercase();
6352    let trimmed = lower.trim();
6353    if offset == 0 && start == byte_order_mark_width(source) && raw.starts_with(b"#!") {
6354        return CommentKind::Shebang;
6355    }
6356    if offset == 0
6357        && matches!(language, Language::Python | Language::Ruby)
6358        && is_encoding_declaration(source, start, raw)
6359    {
6360        return CommentKind::Encoding;
6361    }
6362    if language == Language::Sql && raw.starts_with(b"/*+") {
6363        return CommentKind::OptimizerHint;
6364    }
6365    if raw.starts_with(b"/*!") && language == Language::Sql {
6366        return CommentKind::VersionComment;
6367    }
6368    if legal_marker(trimmed).is_some() {
6369        return CommentKind::License;
6370    }
6371    if directive_name(trimmed, language, raw).is_some() {
6372        return CommentKind::Directive;
6373    }
6374    lexical
6375}
6376
6377/// The document-wide half of the restart rules: C and C++ splice
6378/// `\<newline>` out of the input before lexing, and the remapped copy that
6379/// results is scanned without tracking checkpoints, so a full scan of a spliced
6380/// document offers no restart point beyond offset 0.
6381fn line_splicing_permits_restarts(source: &[u8], language: Language) -> bool {
6382    !matches!(language, Language::C | Language::Cpp) || !contains_line_splice(source)
6383}
6384
6385/// The offset of the first byte in `source` that could head a YAML block
6386/// scalar, or [`usize::MAX`] when there is none — and for every other language,
6387/// which has no such construct.
6388///
6389/// This is the one lexical state whose *end* is decided by the bytes that come
6390/// after it: a body runs while the lines below stay indented past the node it
6391/// hangs off, so an edit that indents the line under a body, or appends one to
6392/// a document that ended with it, swallows an offset a previous revision
6393/// recorded as a line start. Restarting there would read the content of a
6394/// scalar as YAML and remove a `#` that is one of its bytes. No body can begin
6395/// before its own header, so every line start up to the first one is safe from
6396/// that whatever an edit does below it — and past it, nothing is.
6397///
6398/// The test is deliberately looser than [`Scanner::scan_yaml`]'s: any `|` or
6399/// `>` with the shape of a header counts, whether or not a node may begin
6400/// there. Refusing a restart costs a rescan; permitting a wrong one loses a
6401/// user's bytes.
6402///
6403/// One pass, not one per candidate: [`yaml_block_header`] reads the comment on
6404/// a header line to its end, but a comment is enough to make the bytes a
6405/// header, so that read happens at most once before this returns.
6406fn first_yaml_block_scalar(source: &[u8], language: Language) -> usize {
6407    if language != Language::Yaml {
6408        return usize::MAX;
6409    }
6410    let mut index = 0;
6411    while let Some(relative) = memchr2(b'|', b'>', &source[index..]) {
6412        let candidate = index + relative;
6413        if yaml_block_header(source, candidate).is_some() {
6414            return candidate;
6415        }
6416        index = candidate + 1;
6417    }
6418    usize::MAX
6419}
6420
6421/// A checkpoint sits immediately after a line terminator, and a CRLF pair is a
6422/// single terminator. An edit that supplies the LF after an existing CR moves
6423/// the boundary one byte on, leaving the offset a previous revision recorded
6424/// inside the pair, where no scan of these bytes would ever resume.
6425fn the_line_ending_permits_a_restart(source: &[u8], offset: usize) -> bool {
6426    offset == 0 || source.get(offset - 1) != Some(&b'\r') || source.get(offset) != Some(&b'\n')
6427}
6428
6429/// Preamble classification depends on the absolute offset, and the two
6430/// languages that declare a source encoding in a comment — Python and Ruby —
6431/// only recognise one while scanning from offset 0, which makes the start of
6432/// line 2 a restart point exactly when no encoding declaration follows. Offset
6433/// 0 always passes — restarting a scan there *is* the full scan.
6434fn the_preamble_permits_a_restart(source: &[u8], language: Language, offset: usize) -> bool {
6435    offset == 0
6436        || !matches!(language, Language::Python | Language::Ruby)
6437        || !is_within_first_two_lines(source, offset)
6438        || !line_declares_encoding(source, offset)
6439}
6440
6441/// The restart rules for one revision of a document: a safe checkpoint promises
6442/// that restarting the scan there reproduces the rest of a full scan byte for
6443/// byte, and both halves of that promise are conditions on the bytes *around*
6444/// the checkpoint. The document-wide half is answered once here, when the rules
6445/// are built, because answering it costs a scan of the source.
6446///
6447/// The scanner consults these rules before emitting a checkpoint; the
6448/// incremental engine builds them for the *edited* bytes and consults them
6449/// again before restarting at a checkpoint the previous revision recorded.
6450/// Emitting a checkpoint and reusing one therefore ask one function and cannot
6451/// drift apart.
6452#[derive(Clone, Copy)]
6453pub(crate) struct RestartRules {
6454    language: Language,
6455    splicing_permits_restarts: bool,
6456    first_block_scalar: usize,
6457}
6458
6459impl RestartRules {
6460    pub(crate) fn of(source: &[u8], language: Language) -> Self {
6461        Self {
6462            language,
6463            splicing_permits_restarts: line_splicing_permits_restarts(source, language),
6464            first_block_scalar: first_yaml_block_scalar(source, language),
6465        }
6466    }
6467
6468    /// Whether restarting a scan of `source` — the bytes these rules were built
6469    /// from — at `offset` reproduces the rest of a full scan of it.
6470    pub(crate) fn permit_restart_at(&self, source: &[u8], offset: usize) -> bool {
6471        self.splicing_permits_restarts
6472            && offset <= self.first_block_scalar
6473            && the_line_ending_permits_a_restart(source, offset)
6474            && the_preamble_permits_a_restart(source, self.language, offset)
6475            && (self.language != Language::Scala
6476                || the_scala_xml_boundary_permits_a_restart(source, offset))
6477            && (!matches!(
6478                self.language,
6479                Language::Html | Language::Vue | Language::Svelte
6480            ) || the_tag_boundary_permits_a_restart(source, offset))
6481    }
6482}
6483
6484/// Whether a restart at `offset` may stand next to an HTML-style tag.
6485///
6486/// A `<` ... `>` tag is read forward from its `<` — with its quoted attribute
6487/// values — and the whole of it is consumed, so no checkpoint stands inside
6488/// one in a full scan. But a checkpoint recorded by an earlier revision can
6489/// sit where an edit grew a tag across it, and a rescan that begins there
6490/// would read the rest of the tag as ordinary text and find a comment in it
6491/// the full scan protected.
6492///
6493/// The test walks back from `offset` and reads every `<` it meets forward,
6494/// exactly as the scan would: the quotes pair and the first unquoted `>`
6495/// ends the tag. The offset is refused when any of those tags is still open
6496/// there — its `>` lies beyond it or never comes. Every `<` rather than only
6497/// the nearest one, because a tag's attributes may hold further `<` signs of
6498/// their own: the tag that decided the scan's reading of this stretch can
6499/// open far behind the closest sign, and an edit that grows the tag's `>`
6500/// across the offset can withdraw a checkpoint the nearest sign alone would
6501/// still permit. A suffix scan — whose own source begins at the checkpoint —
6502/// gives the same answer, because a checkpoint that is itself sound, as every
6503/// recorded one is, sits inside no tag, so every `<` its walk meets lies
6504/// within its bytes.
6505fn the_tag_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool {
6506    let mut index = offset;
6507    while index > 0 {
6508        index -= 1;
6509        if source[index] == b'<' {
6510            let mut cursor = index + 1;
6511            let mut quote = None;
6512            let mut closed = false;
6513            while cursor < offset {
6514                if let Some(active) = quote {
6515                    if source[cursor] == active {
6516                        quote = None;
6517                    }
6518                } else if matches!(source[cursor], b'\'' | b'"') {
6519                    quote = Some(source[cursor]);
6520                } else if source[cursor] == b'>' {
6521                    closed = true;
6522                    break;
6523                }
6524                cursor += 1;
6525            }
6526            if !closed {
6527                return false;
6528            }
6529        }
6530    }
6531    true
6532}
6533
6534fn the_scala_xml_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool {
6535    if source.get(offset) != Some(&b'<') {
6536        return true;
6537    }
6538    match source.get(offset + 1) {
6539        Some(b'!' | b'?') => false,
6540        Some(&byte) => !(byte.is_ascii_alphabetic() || matches!(byte, b'_') || byte >= 0x80),
6541        None => true,
6542    }
6543}
6544
6545/// Whether classification from `offset` onwards is independent of where those
6546/// bytes sit in the document. The preamble rules are the only position
6547/// sensitive ones — a `#!` line is a shebang only at offset 0, and a Python
6548/// encoding declaration only inside the first two lines — so anything past the
6549/// first two lines classifies the same wherever an edit moves it to.
6550///
6551/// The incremental engine reuses the previous revision's report for the tail it
6552/// converges on, shifted by the edit's length delta. That reuse keeps the old
6553/// classification, so it is sound exactly while the tail is settled at both its
6554/// old and its new position.
6555pub(crate) fn preamble_is_settled(source: &[u8], offset: usize) -> bool {
6556    !is_within_first_two_lines(source, offset)
6557}
6558
6559fn is_within_first_two_lines(source: &[u8], offset: usize) -> bool {
6560    let mut line_breaks = 0;
6561    let mut index = 0;
6562    let end = offset.min(source.len());
6563    while index < end {
6564        if source[index] == b'\r' {
6565            index += usize::from(source.get(index + 1) == Some(&b'\n'));
6566            line_breaks += 1;
6567            if line_breaks >= 2 {
6568                return false;
6569            }
6570        } else if source[index] == b'\n' {
6571            line_breaks += 1;
6572            if line_breaks >= 2 {
6573                return false;
6574            }
6575        }
6576        index += 1;
6577    }
6578    true
6579}
6580
6581/// Whether the line beginning at `line_start` carries a source-encoding
6582/// declaration, and therefore a comment whose classification depends on the
6583/// scan starting at offset 0.
6584fn line_declares_encoding(source: &[u8], line_start: usize) -> bool {
6585    let mut index = line_start;
6586    while matches!(source.get(index), Some(b' ' | b'\t' | 0x0c)) {
6587        index += 1;
6588    }
6589    if source.get(index) != Some(&b'#') {
6590        return false;
6591    }
6592    let end = line_end(source, index + 1);
6593    is_encoding_declaration(source, index, &source[index..end])
6594}
6595
6596/// Whether the comment beginning at `start` is a source-encoding declaration.
6597///
6598/// Python and Ruby share the phrase, down to the spelling: PEP 263 asks for
6599/// `coding[:=]\s*([-\w.]+)` in one of the first two lines, and Ruby's
6600/// `magic_comment` reads the same phrase out of the same two lines. The Emacs
6601/// form `# -*- coding: utf-8 -*-` satisfies both, which is why both languages
6602/// are written with it.
6603///
6604/// What the two do *not* share is which second line counts, and the rule here
6605/// is neither of theirs: any `coding:` comment on either of the first two lines
6606/// is a declaration, whatever stands on the line above it. Ruby reads the
6607/// second line only behind a `#!` line, and Python only behind a line that is
6608/// itself a comment or blank — so `x = 1\n# coding: us-ascii\n` names an
6609/// encoding to neither of them (Ruby 3.3.12 reports `__ENCODING__` as UTF-8,
6610/// and `tokenize.detect_encoding` reports utf-8), and this function calls it a
6611/// declaration all the same.
6612///
6613/// That is deliberate. Saying yes here only ever *keeps* a comment that `safe`
6614/// would otherwise remove, so the two ways to be wrong are not the same size:
6615/// a missed declaration removes the line a file's encoding is written on, and
6616/// an invented one leaves an ordinary comment in place. One rule for both
6617/// languages is also one rule that cannot drift apart between them, which is
6618/// what [`preamble_is_settled`] leans on when it names the first two lines the
6619/// only position-sensitive bytes in any document.
6620fn is_encoding_declaration(source: &[u8], start: usize, raw: &[u8]) -> bool {
6621    if !is_within_first_two_lines(source, start) || !raw.starts_with(b"#") {
6622        return false;
6623    }
6624    let line_start = source[..start.min(source.len())]
6625        .iter()
6626        .rposition(|byte| matches!(byte, b'\r' | b'\n'))
6627        .map_or(0, |position| position + 1);
6628    let mut prefix = &source[line_start..start.min(source.len())];
6629    if line_start == 0 {
6630        prefix = prefix.strip_prefix(b"\xef\xbb\xbf").unwrap_or(prefix);
6631    }
6632    if !prefix
6633        .iter()
6634        .all(|byte| matches!(byte, b' ' | b'\t' | 0x0c))
6635    {
6636        return false;
6637    }
6638    let body = &raw[1..];
6639    let Some(position) = find_subslice(body, b"coding") else {
6640        return false;
6641    };
6642    let mut cursor = position + b"coding".len();
6643    if !matches!(body.get(cursor), Some(b':' | b'=')) {
6644        return false;
6645    }
6646    cursor += 1;
6647    while matches!(body.get(cursor), Some(b' ' | b'\t')) {
6648        cursor += 1;
6649    }
6650    body.get(cursor)
6651        .is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
6652}
6653
6654fn strip_comment_markers(raw: &[u8]) -> &[u8] {
6655    let mut start = 0;
6656    let mut end = raw.len();
6657    for marker in [
6658        b"<!--".as_slice(),
6659        b"///",
6660        b"//!",
6661        b"//",
6662        b"/**",
6663        b"/*",
6664        b"(*",
6665        b"--",
6666        b"#",
6667    ] {
6668        if raw.starts_with(marker) {
6669            start = marker.len();
6670            break;
6671        }
6672    }
6673    for marker in [b"-->".as_slice(), b"*/", b"*)"] {
6674        if raw.ends_with(marker) {
6675            end = end.saturating_sub(marker.len());
6676            break;
6677        }
6678    }
6679    &raw[start.min(end)..end]
6680}
6681
6682/// The legal marker `text` carries, or `None` when it carries none. The marker
6683/// is the phrase that matched, which is what an explanation has to quote to
6684/// justify calling a comment a license.
6685fn legal_marker(text: &str) -> Option<&'static str> {
6686    [
6687        "spdx-license-identifier",
6688        "copyright",
6689        "licensed under",
6690        "permission is hereby granted",
6691        "all rights reserved",
6692    ]
6693    .into_iter()
6694    .find(|marker| text.contains(marker))
6695}
6696
6697/// The tool or language directive `text` opens, or `None` when it opens none.
6698/// The name is the prefix that matched, so an explanation can point at the
6699/// directive a reader recognises instead of at the whole comment.
6700fn directive_name(text: &str, language: Language, raw: &[u8]) -> Option<&'static str> {
6701    let compact = text.trim_start_matches(['!', '/', '*', '#', '@', ' ']);
6702    let common = [
6703        "sourcemappingurl=",
6704        "sourceurl=",
6705        "#__pure__",
6706        "@__pure__",
6707        "__pure__",
6708        "#__no_side_effects__",
6709        "__no_side_effects__",
6710        "ts-ignore",
6711        "ts-expect-error",
6712        "ts-nocheck",
6713        "ts-check",
6714        "eslint",
6715        "prettier-ignore",
6716        "stylelint",
6717        "noinspection",
6718        "nolint",
6719        "noqa",
6720        "type: ignore",
6721        "fmt:",
6722        "rustfmt::",
6723        "clang-format",
6724        "spotless:",
6725        "ktlint-disable",
6726        "ktlint-enable",
6727        "detekt:",
6728        "istanbul ignore",
6729        "c8 ignore",
6730        "coverage:",
6731        "ocomment:",
6732        "region",
6733        "endregion",
6734    ];
6735    if let Some(name) = common
6736        .into_iter()
6737        .find(|prefix| compact.starts_with(prefix))
6738    {
6739        return Some(name);
6740    }
6741    /* NOTE: `shellcheck` is out of the list above for one reason: it is the whole
6742     * word the tool answers to rather than the head of a longer one, so it
6743     * ends at a boundary instead of at a byte. Every language is still asked
6744     * about it, because a shell fragment is embedded in more than one of
6745     * them. */
6746    if opens_with_keyword(compact, "shellcheck") {
6747        return Some("shellcheck");
6748    }
6749    match language {
6750        Language::Go => ["go:", "+build", "line "]
6751            .into_iter()
6752            .find(|prefix| compact.starts_with(prefix)),
6753        Language::TypeScript => {
6754            (raw.starts_with(b"///") && compact.starts_with('<')).then_some("///")
6755        }
6756        Language::C | Language::Cpp => ["pragma", "line "]
6757            .into_iter()
6758            .find(|prefix| compact.starts_with(prefix)),
6759        Language::Python => ["pyright:", "mypy:", "ruff:", "fmt:"]
6760            .into_iter()
6761            .find(|prefix| compact.starts_with(prefix)),
6762        /* NOTE: A Dockerfile is detected as shell, and two of its comment lines are
6763         * addressed to a tool: `# syntax=` is the parser directive BuildKit
6764         * reads before it reads the file, and `# hadolint ignore=` turns one
6765         * rule of the Dockerfile linter off for the instruction below it.
6766         * `hadolint` is a whole word and `syntax=` is not — the frontend
6767         * reference follows the `=` with no space at all — so only the first
6768         * of the two is matched with a boundary after it. */
6769        Language::Shell => opens_with_keyword(compact, "hadolint")
6770            .then_some("hadolint")
6771            .or_else(|| compact.starts_with("syntax=").then_some("syntax=")),
6772        /* NOTE: Taplo reads two instructions out of a TOML comment: `#:schema`
6773         * names the JSON schema the file is validated against, and `# taplo:`
6774         * opens a formatter option. The first is followed by the URL after
6775         * whitespace, so it ends at a boundary; the second carries its own in
6776         * the colon. */
6777        Language::Toml => opens_with_keyword(compact, ":schema")
6778            .then_some(":schema")
6779            .or_else(|| compact.starts_with("taplo:").then_some("taplo:")),
6780        /* NOTE: `strip_comment_markers` takes the `--` off a Lua comment and
6781         * leaves the third dash of `---@diagnostic` behind, so the annotation
6782         * is read with that dash removed. `raw` is what tells it from prose:
6783         * the language server's annotations are the only comments that open
6784         * with `---@`, and `diagnostic` is the only one of them that instructs
6785         * a tool rather than describing a type. The four checkers below are
6786         * addressed as `-- <tool>:`, which carries its own boundary in the
6787         * colon. */
6788        Language::Lua => {
6789            if raw.starts_with(b"---@") && text.trim_start_matches('-').starts_with("@diagnostic") {
6790                return Some("---@diagnostic");
6791            }
6792            ["luacheck:", "selene:", "stylua:", "luacov:"]
6793                .into_iter()
6794                .find(|prefix| compact.starts_with(prefix))
6795        }
6796        /* NOTE: `@schema` is asked of `text` rather than of `compact`, because
6797         * `compact` is what takes the `@` off: the annotation the Helm schema
6798         * generator reads is spelled with it, and `schema` on its own is a
6799         * word any comment about a schema opens with. The three keywords below
6800         * it are the whole word their tool answers to and end at a boundary;
6801         * the four prefixes carry their own in a colon. */
6802        Language::Yaml => {
6803            if opens_with_keyword(text, "@schema") {
6804                return Some("@schema");
6805            }
6806            for keyword in ["yamllint", "nosec", "kics-scan"] {
6807                if opens_with_keyword(compact, keyword) {
6808                    return Some(keyword);
6809                }
6810            }
6811            [
6812                "yaml-language-server:",
6813                "renovate:",
6814                "checkov:skip",
6815                "trivy:ignore",
6816            ]
6817            .into_iter()
6818            .find(|prefix| compact.starts_with(prefix))
6819        }
6820        /* NOTE: Three of the four are asked of `text` rather than of `compact`,
6821         * because `compact` is what takes the `@` off, and the `@` is what
6822         * tells the annotation from prose about it. `@psalm-suppress` is
6823         * followed by the issue it silences after whitespace, so it ends at a
6824         * boundary; `@phpstan-ignore` and `@codeCoverageIgnore` are namespaces
6825         * whose members differ only in what runs on past them —
6826         * `-next-line`, `Start`, `End` — so a prefix is the whole rule there.
6827         * `phpcs:` carries its own boundary in the colon and covers `ignore`,
6828         * `disable`, `enable`, and `ignoreFile` alike. */
6829        /* NOTE: Three of these six are Ruby's own magic comments, which the
6830         * interpreter reads out of the head of a file: `frozen_string_literal`
6831         * decides whether every literal string in it is frozen,
6832         * `shareable_constant_value` what Ractor may share, and `warn_indent`
6833         * whether the parser complains about the indentation. The other three
6834         * are the tools every Ruby project runs — RuboCop, StandardRB, and
6835         * Sorbet's `# typed:` sigil. Each carries its own boundary in the
6836         * colon and covers the whole namespace behind it: `rubocop:disable`,
6837         * `:enable` and `:todo` alike. The encoding declaration is deliberately
6838         * absent: it is a kind of its own, classified before this runs.
6839         *
6840         * A magic comment is honoured only at the head of a file, and this is
6841         * asked of every comment in it. Reading one further down as an
6842         * instruction keeps a comment a removal would otherwise take, which is
6843         * the direction to be wrong in, and it is what keeps the answer
6844         * independent of where in the document the scan began. */
6845        Language::Ruby => [
6846            "frozen_string_literal:",
6847            "warn_indent:",
6848            "shareable_constant_value:",
6849            "rubocop:",
6850            "standard:",
6851            "typed:",
6852        ]
6853        .into_iter()
6854        .find(|prefix| compact.starts_with(prefix)),
6855        Language::Php => {
6856            if opens_with_keyword(text, "@psalm-suppress") {
6857                return Some("@psalm-suppress");
6858            }
6859            if text.starts_with("@phpstan-ignore") {
6860                return Some("@phpstan-ignore");
6861            }
6862            if text.starts_with("@codecoverageignore") {
6863                return Some("@codeCoverageIgnore");
6864            }
6865            compact.starts_with("phpcs:").then_some("phpcs:")
6866        }
6867        /* NOTE: `zig fmt` reads one instruction out of a comment, and it reads it
6868         * by equality rather than by prefix: `Render.zig` takes `"//".len()`
6869         * bytes off the trimmed comment, trims the white space that follows,
6870         * and compares the remainder with `zig fmt: off` and `zig fmt: on`.
6871         * So `// zig fmt: off please` turns nothing off, and neither does
6872         * `/// zig fmt: off` or `//// zig fmt: off` — the first leaves a `/`
6873         * in front of the phrase and the second two. `raw` is what tells those
6874         * apart, because `strip_comment_markers` takes a `///` off whole; the
6875         * comparison itself is against the trimmed text, which is folded to
6876         * lower case here where `zig fmt` is case-sensitive. Folding can only
6877         * keep a comment a removal would otherwise take, which is the
6878         * direction to be wrong in. */
6879        /* NOTE: The two comments an R tool reads rather than a reader. styler
6880         * turns its formatter off between `# styler: off` and `# styler: on`,
6881         * and the colon carries the marker's own boundary; covr excludes the
6882         * lines between `# nocov start` and `# nocov end`, and `nocov` is the
6883         * whole word it looks for — `start`, `end` and nothing at all all
6884         * follow it — so that one ends at a boundary instead. lintr's
6885         * `# nolint` is protected for every language already and is deliberately
6886         * absent here. */
6887        Language::R => {
6888            if opens_with_keyword(compact, "nocov") {
6889                return Some("nocov");
6890            }
6891            compact.starts_with("styler:").then_some("styler:")
6892        }
6893        Language::Zig => {
6894            let opens_a_plain_comment =
6895                raw.starts_with(b"//") && !matches!(raw.get(2), Some(b'/' | b'!'));
6896            (opens_a_plain_comment && matches!(text, "zig fmt: off" | "zig fmt: on"))
6897                .then_some("zig fmt:")
6898        }
6899        /* NOTE: Four instructions, and only one of them is addressed to a tool.
6900         * `// @dart = 2.12` is read by the Dart scanner itself, and it decides
6901         * which version of the language the file is written in, so a removal
6902         * that took it would change what the remaining code means
6903         * ([`dart_language_version`] follows that grammar). `dart format` is
6904         * matched by equality on the whole comment rather than by prefix,
6905         * because that is how `dart_style` matches it: `piece_writer.dart`
6906         * switches on `comment.text` against `// dart format off` and
6907         * `// dart format on`, so `//   dart format off` with a second space
6908         * and `/// dart format off` with a third slash turn nothing off —
6909         * measured on `dart format` from SDK 3.13.2, which reformatted both.
6910         * `comment.text` is trimmed at the end and not at the front, and this
6911         * is asked of `raw` for the reason Zig's is: `strip_comment_markers`
6912         * takes a `///` off whole and would leave the two spellings
6913         * indistinguishable. The analyzer's two ignore comments each carry
6914         * their own boundary in the colon and cover the whole namespace behind
6915         * it (`ignore_info.dart`). */
6916        Language::Dart => {
6917            if dart_language_version(raw) {
6918                return Some("@dart");
6919            }
6920            let phrase = raw.trim_ascii_end();
6921            if phrase == b"// dart format off" || phrase == b"// dart format on" {
6922                return Some("dart format");
6923            }
6924            ["ignore:", "ignore_for_file:"]
6925                .into_iter()
6926                .find(|prefix| compact.starts_with(prefix))
6927        }
6928        /* NOTE: Four instructions, and only one of them is addressed to a
6929         * formatter. `// swift-tools-version:` is the first line of a
6930         * `Package.swift`, and SwiftPM reads it before it reads any of the
6931         * manifest: it decides which version of the package description the
6932         * file is written against, so a removal that took it would leave a
6933         * package that no longer builds. The other three name the tool that
6934         * reads them and carry their own boundary — a colon for `swiftlint:`
6935         * and `swiftformat:`, and for `swift-format-ignore` the end of the
6936         * comment, a colon, or the `-file` that widens it to the whole file.
6937         * Measured on `swift-format` 6.3.3: `// swift-format-ignore` and
6938         * `// swift-format-ignore-file` both leave `let    a     = 1` alone,
6939         * and `// swift-format-ignoreish note` reformats it. `// MARK:` is
6940         * deliberately absent: Xcode reads it to build a jump bar, so it is
6941         * addressed to a reader rather than to a build, and a project that
6942         * wants it kept says so with a `keep_regex`. */
6943        Language::Swift => {
6944            if let Some(name) = ["swift-tools-version:", "swiftlint:", "swiftformat:"]
6945                .into_iter()
6946                .find(|prefix| compact.starts_with(prefix))
6947            {
6948                return Some(name);
6949            }
6950            let rest = compact.strip_prefix("swift-format-ignore")?;
6951            let tail = rest.strip_prefix("-file").unwrap_or(rest);
6952            (tail.is_empty()
6953                || tail.starts_with(':')
6954                || tail.starts_with(|character: char| {
6955                    character.is_ascii_whitespace() || character == '\u{000b}'
6956                }))
6957            .then_some("swift-format-ignore")
6958        }
6959        /* NOTE: Three instructions, and the first is the one a *compiler* reads.
6960         * Roslyn's `GeneratedCodeUtilities.BeginsWithAutoGeneratedComment`
6961         * searches the `//` and `/* */` comments in front of a file's first
6962         * token for `<auto-generated` — the legacy `<autogenerated` with it —
6963         * and a file it finds one in is exempt from every analyzer that opts
6964         * out of generated code, so a removal that took it would turn a
6965         * generated file into a hand-written one and light up the diagnostics
6966         * it was written to escape. That search is `contains` rather than a
6967         * prefix, and it is followed here: the `<` is the marker's own
6968         * boundary, and prose about generated code carries none. Roslyn asks
6969         * for it in the leading trivia alone and asks case-sensitively; both
6970         * are widened here, because a comment that merely *reads* like the
6971         * marker is a comment a reader meant as one.
6972         *
6973         * `// ReSharper disable` and `// ReSharper restore` bound the region an
6974         * inspection is turned off over, and only those two verbs are
6975         * instructions — the whitespace between the tool and its verb is what
6976         * tells them from prose that opens with the same letters.
6977         * `// csharpier-ignore` is matched on the whole comment rather than by
6978         * prefix, because that is how CSharpier matches it: measured on
6979         * `csharpier` 1.3.0, `// csharpier-ignore`, `// csharpier-ignore-start`
6980         * and `// csharpier-ignore-end` each left `int    a     =    1;`
6981         * unformatted, while `//  csharpier-ignore` with a second space,
6982         * `// csharpier-ignore some text`, `/* csharpier-ignore */` and
6983         * `/// csharpier-ignore` all reformatted it. */
6984        Language::CSharp => {
6985            if text.contains("<auto-generated") || text.contains("<autogenerated") {
6986                return Some("<auto-generated");
6987            }
6988            if let Some(rest) = compact.strip_prefix("resharper") {
6989                let verb = rest.trim_start_matches([' ', '\t']);
6990                if verb.len() < rest.len()
6991                    && (verb.starts_with("disable") || verb.starts_with("restore"))
6992                {
6993                    return Some("ReSharper");
6994                }
6995            }
6996            matches!(
6997                raw,
6998                b"// csharpier-ignore" | b"// csharpier-ignore-start" | b"// csharpier-ignore-end"
6999            )
7000            .then_some("csharpier-ignore")
7001        }
7002        /* NOTE: scala-cli reads a directive line before it reads the manifest
7003         * at all, and the directive is `//>` followed by a space and a name,
7004         * of which `using` is the one that configures the build. `compact` is
7005         * the comment with its markers stripped, so `//> using` is `> using`,
7006         * and the boundary is what keeps a comment that only opens with the
7007         * same letters — `//> usingless`, or `//>> using` with one `>` more —
7008         * from being kept as one. */
7009        Language::Scala => (compact == "> using"
7010            || compact.starts_with("> using ")
7011            || compact.starts_with("> using\t"))
7012        .then_some("//> using"),
7013        _ => None,
7014    }
7015}
7016
7017/// Whether `text` opens with `keyword` and then ends it.
7018///
7019/// A directive named after the tool that reads it — `shellcheck`, `hadolint` —
7020/// is followed by the argument that tool takes, and what separates the two is
7021/// whitespace of the writer's choosing rather than one particular byte:
7022/// `# hadolint\tignore=DL3018` is the same instruction as the one written with
7023/// a space. Matching the bare prefix instead would read prose that merely opens
7024/// with those letters — `# shellcheckish note` — as an instruction as well, and
7025/// protect a comment that is only *about* the tool.
7026///
7027/// The end of the comment ends the keyword too. `#:schema` with its URL still
7028/// to be typed is the directive it is about to be, and `text` arrives trimmed,
7029/// so `#:schema ` reaches here as the bare word in any case: refusing the empty
7030/// remainder would protect the directive or not depending on a trailing space.
7031fn opens_with_keyword(text: &str, keyword: &str) -> bool {
7032    text.strip_prefix(keyword).is_some_and(|rest| {
7033        rest.is_empty() || rest.starts_with(|character: char| character.is_ascii_whitespace())
7034    })
7035}
7036
7037/// The kind of a Java line comment.
7038///
7039/// Java has exactly one line-comment documentation marker: `///`, the Markdown
7040/// documentation comment JEP 467 added in JDK 23. `//!` is Rust's inner-doc
7041/// marker and means nothing here, so a comment opening with it is an ordinary
7042/// line comment — reading it as documentation would hide it from
7043/// [`crate::Policy::Safe`] in a language that never wrote it as one.
7044fn java_line_kind(bytes: &[u8], index: usize) -> CommentKind {
7045    if starts(bytes, index, b"///") {
7046        CommentKind::DocLine
7047    } else {
7048        CommentKind::Line
7049    }
7050}
7051
7052/// The kind of a Java block comment: `/** ... */` is the documentation comment
7053/// of JLS 3.7, and `/*!` — Doxygen's marker, which C and C++ do honour — is
7054/// not one, for the same reason [`java_line_kind`] gives.
7055fn java_block_kind(bytes: &[u8], index: usize) -> CommentKind {
7056    if starts(bytes, index, b"/**") {
7057        CommentKind::DocBlock
7058    } else {
7059        CommentKind::Block
7060    }
7061}
7062
7063/// The kind of a Dart line comment.
7064///
7065/// `tokenizeSingleLineComment` reads the byte behind `//` and sets `dartdoc`
7066/// when it is a third slash, then reads no further: a fourth slash leaves
7067/// `////` a `DartDocToken` just as `///` is one, which is where Dart parts
7068/// company with Lua's `----` and Zig's `////`. `//!` is Rust's inner-doc
7069/// marker and means nothing here, so a comment opening with it is an ordinary
7070/// line comment — reading it as documentation would hide it from
7071/// [`crate::Policy::Safe`] in a language that never wrote it as one.
7072fn dart_line_kind(bytes: &[u8], index: usize) -> CommentKind {
7073    if starts(bytes, index, b"///") {
7074        CommentKind::DocLine
7075    } else {
7076        CommentKind::Line
7077    }
7078}
7079
7080/// The kind of a Dart block comment.
7081///
7082/// `tokenizeMultiLineComment` sets `dartdoc` from the single byte behind `/*`,
7083/// so `/**` opens the documentation comment `dart doc` reads and `/**/` is an
7084/// empty one. `/*!` is Doxygen's marker, which C and C++ honour and Dart does
7085/// not, for the reason [`dart_line_kind`] gives.
7086fn dart_block_kind(bytes: &[u8], index: usize) -> CommentKind {
7087    if starts(bytes, index, b"/**") {
7088        CommentKind::DocBlock
7089    } else {
7090        CommentKind::Block
7091    }
7092}
7093
7094/// The kind of a Swift line comment.
7095///
7096/// Swift's documentation marker is `///`, and a fourth slash does not take it
7097/// away: SwiftSyntax reports `////` as `docLineComment` exactly as it reports
7098/// `///`, which is where Swift keeps company with Dart and parts company with
7099/// Lua's `----` and Zig's `////`. `//!` is Rust's inner-doc marker and means
7100/// nothing here, so a comment opening with it is an ordinary line comment —
7101/// reading it as documentation would hide it from [`crate::Policy::Safe`] in a
7102/// language that never wrote it as one.
7103fn swift_line_kind(bytes: &[u8], index: usize) -> CommentKind {
7104    if starts(bytes, index, b"///") {
7105        CommentKind::DocLine
7106    } else {
7107        CommentKind::Line
7108    }
7109}
7110
7111/// The kind of a Swift block comment.
7112///
7113/// `/**` opens the documentation comment DocC reads, with one exception that
7114/// the third `*` is not there to open anything: `/**/` is the *empty* block
7115/// comment, whose second `*` is the first byte of its own terminator.
7116/// SwiftSyntax reports `/**/` as `blockComment` and `/***/` as
7117/// `docBlockComment`, and this follows it. `/*!` is Doxygen's marker, which C
7118/// and C++ honour and Swift does not, for the reason [`swift_line_kind`] gives.
7119fn swift_block_kind(bytes: &[u8], index: usize) -> CommentKind {
7120    if starts(bytes, index, b"/**") && !starts(bytes, index, b"/**/") {
7121        CommentKind::DocBlock
7122    } else {
7123        CommentKind::Block
7124    }
7125}
7126
7127/// The name of the Swift string form that was left open, so the diagnostic says
7128/// which of the five a reader has to go and close. The fifth is the
7129/// single-quoted literal the compiler lexes and then rejects, which carries
7130/// neither a hash nor a multi-line spelling.
7131const fn swift_unterminated_string(delimiter: u8, raw: bool, multiline: bool) -> &'static str {
7132    if delimiter == b'\'' {
7133        return "unterminated Swift single-quoted string";
7134    }
7135    match (raw, multiline) {
7136        (true, true) => "unterminated Swift raw multiline string",
7137        (true, false) => "unterminated Swift raw string",
7138        (false, true) => "unterminated Swift multiline string",
7139        (false, false) => "unterminated Swift string",
7140    }
7141}
7142
7143/// Whether `count` `#` bytes stand at `index`, and the byte behind them is not
7144/// a `#` as well.
7145///
7146/// The run has to be *exactly* `count` long for a delimiter to close: a `"#`
7147/// inside a `##"..."##` literal is content, and only `"##` ends it. A longer run
7148/// still closes it, because the extra hashes are the content that follows the
7149/// delimiter rather than part of it, which is why the byte behind is not
7150/// examined.
7151fn swift_hashes_at(bytes: &[u8], index: usize, count: usize) -> bool {
7152    (0..count).all(|offset| bytes.get(index + offset) == Some(&b'#'))
7153}
7154
7155/// The end of a string with `hashes` hashes if its delimiter closes at
7156/// `index`, or `None` when it does not.
7157///
7158/// NOTE: a *raw* delimiter takes one `#` more than it needs when one is there.
7159/// `Lexer.Cursor.advanceIfStringDelimiter` counts pounds with the advance in
7160/// the loop *condition* and the count test in its body, so it consumes a
7161/// `hashes + 1`-th before it stops — and then `swiftc` calls the literal an
7162/// error, `too many '#' characters in closing delimiter`. Taking that byte too
7163/// is what keeps this scan standing where the lexer stands on a file that is
7164/// already broken; a file that closes its raw strings correctly never reaches
7165/// the case at all. An ordinary string takes none: the same function returns on
7166/// `delimiterLength == 0` before it looks at a byte, so the `#` behind the
7167/// closing quote of `"x"#/y/#` opens the regular expression literal that
7168/// follows rather than belonging to the string — which is the difference
7169/// between reading the `//` inside that literal as pattern and reading it as a
7170/// comment.
7171fn swift_string_close(
7172    bytes: &[u8],
7173    index: usize,
7174    delimiter: u8,
7175    multiline: bool,
7176    hashes: usize,
7177) -> Option<usize> {
7178    let width = if multiline { 3 } else { 1 };
7179    let closes = (0..width).all(|offset| bytes.get(index + offset) == Some(&delimiter))
7180        && swift_hashes_at(bytes, index + width, hashes);
7181    if !closes {
7182        return None;
7183    }
7184    let end = index + width + hashes;
7185    let extra = hashes > 0 && bytes.get(end) == Some(&b'#');
7186    Some(end + usize::from(extra))
7187}
7188
7189/// The length of the run of `#` beginning at `index`.
7190///
7191/// INVARIANT: a `#` in Swift opens a compiler directive, a macro, a raw string,
7192/// or an extended regular expression literal, and only the last two carry a run
7193/// of them. What decides which is the byte behind the run, so this reads the
7194/// class and one byte more rather than searching for a quote that may never
7195/// come — the bound is what the reach is for. A `#` run holds no line
7196/// terminator, so the watermark it leaves never crosses the line the run stands
7197/// on, and the lines under it keep their checkpoints.
7198fn swift_hash_run(bytes: &[u8], index: usize, reach: &mut Reach) -> usize {
7199    let mut end = index;
7200    while bytes.get(end) == Some(&b'#') {
7201        end += 1;
7202    }
7203    /* NOTE: the byte that ended the run decided this, and a `get` that came
7204     * back `None` at the end of the document decided it just the same. */
7205    reach.byte(end);
7206    end - index
7207}
7208
7209/// Whether the `/` at `index` stands where a *binary* operator does, and can
7210/// therefore open no regular expression literal.
7211///
7212/// The Swift book (Lexical Structure, Operators) decides this from the white
7213/// space around an operator: one with white space on both sides or on neither
7214/// is binary, and one with white space on the left alone is prefix. For that
7215/// rule `(`, `[` and `{` before an operator count as white space, and so do
7216/// `,`, `;` and `:`. A regular expression literal may only stand where a prefix
7217/// operator may, so `a /b/ c` opens one and `a/b/c` is three divisions —
7218/// measured with `swift-frontend -dump-parse -swift-version 6`, which reports a
7219/// `regex_literal_expr` for the first and none for the second.
7220///
7221/// The end of a block comment is white space as well: `1 /* c *//a/` opens a
7222/// literal, because the `/` that closes the comment is not a token the `/`
7223/// behind it could bind to.
7224fn swift_is_left_bound(bytes: &[u8], index: usize) -> bool {
7225    let Some(previous) = index.checked_sub(1).map(|behind| bytes[behind]) else {
7226        return false;
7227    };
7228    /* NOTE: the set is SwiftSyntax's `Lexer.Cursor.isLeftBound` as the Swift
7229     * 6.3.3 toolchain ships it, which is the book's rule with two more bytes
7230     * that are white space in fact: a NUL, and the second byte of a U+00A0
7231     * no-break space. */
7232    match previous {
7233        b' ' | b'\t' | b'\r' | b'\n' | 0 => false,
7234        b'(' | b'[' | b'{' | b',' | b';' | b':' => false,
7235        b'/' => index < 2 || bytes[index - 2] != b'*',
7236        0xa0 => index < 2 || bytes[index - 2] != 0xc2,
7237        _ => true,
7238    }
7239}
7240
7241/// Whether the quotes at `quote` open a *multi-line* string literal.
7242///
7243/// Three quotes are the multi-line delimiter, with one exception that only a
7244/// raw string can spell: `#"""#` is the single-line raw string whose one
7245/// character of content is a quote, and not a multi-line literal left open.
7246/// SwiftSyntax decides it in `Lexer.Cursor.advanceIfMultilineStringDelimiter`,
7247/// and only for a raw string: from the *third* quote it reads the rest of the
7248/// line for a `"` carrying the opening run of `#` behind it, and a literal that
7249/// closes on its own line that way is a single-line one.
7250///
7251/// This is a read of the line the literal opens on and no further, and the scan
7252/// takes every byte of it either way — a multi-line literal reaches past the
7253/// line and a single-line one ends inside it, but neither hands those bytes
7254/// back — so there is nothing here for a checkpoint to lose.
7255fn swift_multiline_string(bytes: &[u8], quote: usize, hashes: usize) -> bool {
7256    if !starts(bytes, quote, b"\"\"\"") {
7257        return false;
7258    }
7259    if hashes == 0 {
7260        return true;
7261    }
7262    let mut cursor = quote + 2;
7263    while bytes
7264        .get(cursor)
7265        .is_some_and(|byte| !is_line_terminator(*byte))
7266    {
7267        if bytes[cursor] == b'"' && swift_hashes_at(bytes, cursor + 1, hashes) {
7268            return false;
7269        }
7270        cursor += 1;
7271    }
7272    true
7273}
7274
7275/// The end of the bare regular expression literal `/ ... /` opening at `index`,
7276/// or `None` when those bytes open none.
7277///
7278/// The Swift book (Lexical Structure, Regular Expression Literals) states the
7279/// rule this follows: a literal *can't begin with an unescaped tab or space*,
7280/// and it *can't contain an unescaped forward slash, a carriage return, or a
7281/// line feed*. So the first unescaped `/` is the closing delimiter, the search
7282/// for it never leaves the line the literal opened on, and a backslash carries
7283/// the byte behind it in — `/a\//` is a literal whose content is `a\/`, which
7284/// is exactly the shape that makes this rule worth having: its last two bytes
7285/// spell `//`, and a scanner that read them as a comment would delete the rest
7286/// of the line.
7287///
7288/// Two more conditions come from the compiler rather than from the book, and
7289/// both were measured with `swift-frontend -dump-parse -swift-version 6`. The
7290/// closing delimiter may not be *preceded* by an unescaped space or tab, so
7291/// `/b /` opens nothing while `/a\ /` does. And when the closing delimiter
7292/// would be the first byte of a comment the comment wins outright, rather than
7293/// the literal ending one byte earlier: `/a//b/` is `/a` and then the line
7294/// comment `//b/`, and `/a/*b*/` is `/a` and then the block comment `/*b*/`.
7295///
7296/// What is left after all of that is a lookahead the scan rewinds behind, so it
7297/// reports every byte it read. It stops at the first line terminator, so the
7298/// reach it leaves reaches no further than the line start under it — except
7299/// when the document simply ends, which is a decision an append would change
7300/// and is recorded as such.
7301fn swift_bare_regex(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<usize> {
7302    if swift_is_left_bound(bytes, index) {
7303        return None;
7304    }
7305    reach.byte(index + 1);
7306    match bytes.get(index + 1) {
7307        None | Some(b' ' | b'\t' | b'\r' | b'\n') => return None,
7308        Some(_) => {}
7309    }
7310    let mut cursor = index + 1;
7311    let mut blank = false;
7312    while cursor < bytes.len() {
7313        match bytes[cursor] {
7314            b'\\' => {
7315                reach.byte(cursor + 1);
7316                if matches!(bytes.get(cursor + 1), None | Some(b'\r' | b'\n')) {
7317                    return None;
7318                }
7319                blank = false;
7320                cursor += 2;
7321            }
7322            b'\r' | b'\n' => {
7323                reach.byte(cursor);
7324                return None;
7325            }
7326            b'/' => {
7327                reach.byte(cursor + 1);
7328                if blank || matches!(bytes.get(cursor + 1), Some(b'/' | b'*')) {
7329                    return None;
7330                }
7331                return Some(cursor + 1);
7332            }
7333            byte => {
7334                blank = matches!(byte, b' ' | b'\t');
7335                cursor += 1;
7336            }
7337        }
7338    }
7339    /* NOTE: no line terminator and no closing delimiter, so this read to the end
7340     * of the document and the scan takes those bytes back. An append is an edit
7341     * exactly at the end, and appending a `/` would close this literal, so the
7342     * checkpoint an append reuses the whole prefix from is the one to withdraw. */
7343    reach.end_of(bytes);
7344    None
7345}
7346
7347/// Which of C#'s three string rules a literal follows.
7348///
7349/// The three differ in what closes them and in what an escape is: a plain
7350/// string takes `\` and ends at its line, a verbatim one spells its quote `""`
7351/// and carries line breaks, and a raw one is opaque until a run of at least as
7352/// many quotes as its opener carried comes back. Interpolation is a switch on
7353/// top of all three rather than a fourth rule.
7354#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7355enum CsharpStringForm {
7356    /// `"..."`, and `$"..."` with one `$`.
7357    Plain,
7358    /// `@"..."`, `$@"..."` and `@$"..."`.
7359    Verbatim,
7360    /// `"""..."""`, `$"""..."""` and every literal a run of two `$` or more
7361    /// opens, which Roslyn lexes as a raw one whatever its quote run holds.
7362    Raw,
7363}
7364
7365/// The opener of one C# string literal: where it begins, where its quote run
7366/// begins and how long that run is, how many `$` stood in front of it, and
7367/// which of the three rules it follows.
7368#[derive(Clone, Copy, Debug)]
7369struct CsharpPrefix {
7370    /// The first byte of the literal, `$` and `@` included.
7371    start: usize,
7372    /// The first `"`.
7373    quote: usize,
7374    /// The length of the opening run of `"`, which is the length a closing run
7375    /// of a raw literal must match or beat.
7376    quotes: usize,
7377    /// How many `$` opened it, which is how many braces open one of its holes.
7378    /// Zero for a literal that interpolates nothing.
7379    dollars: usize,
7380    form: CsharpStringForm,
7381}
7382
7383/// One past the run of `$` and `@` at `index`, which is where a scan that found
7384/// no literal behind them resumes.
7385fn csharp_prefix_end(bytes: &[u8], index: usize) -> usize {
7386    let mut end = index;
7387    while matches!(bytes.get(end), Some(b'$' | b'@')) {
7388        end += 1;
7389    }
7390    end
7391}
7392
7393/// The literal a run of `$` and `@` and then a run of `"` opens at `index`, or
7394/// `None` when no quote follows the prefix and those bytes open none.
7395///
7396/// Which rule applies is decided the way Roslyn's lexer decides it, and the
7397/// order of the three tests is the part that is not obvious. Two `$` or more
7398/// make a literal raw whatever else it carries — `$$"a"` is lexed as a raw
7399/// interpolated string with a one-quote delimiter and CS9004 rather than as a
7400/// plain one — while a single `@` makes it verbatim even in front of three
7401/// quotes: Roslyn reports `@"""x"""` as one `StringLiteralToken` whose content
7402/// is `"x"`, two escaped quotes around a letter.
7403///
7404/// INVARIANT: the quote run is read whole and the byte behind it with it, which
7405/// is a bounded read of a run that can hold no line terminator, so the
7406/// watermark it leaves never reaches past the line the literal opens on.
7407fn csharp_literal_prefix(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<CsharpPrefix> {
7408    let mut cursor = index;
7409    let mut dollars = 0;
7410    let mut ats = 0;
7411    while let Some(byte) = bytes.get(cursor) {
7412        match byte {
7413            b'$' => dollars += 1,
7414            b'@' => ats += 1,
7415            _ => break,
7416        }
7417        cursor += 1;
7418    }
7419    if bytes.get(cursor) != Some(&b'"') {
7420        /* NOTE: the byte that ended the run decided this, and a `get` that came
7421         * back `None` at the end of the document decided it just the same. */
7422        reach.byte(cursor);
7423        return None;
7424    }
7425    let mut quotes = 0;
7426    while bytes.get(cursor + quotes) == Some(&b'"') {
7427        quotes += 1;
7428    }
7429    reach.byte(cursor + quotes);
7430    let form = if dollars >= 2 {
7431        CsharpStringForm::Raw
7432    } else if ats > 0 {
7433        CsharpStringForm::Verbatim
7434    } else if quotes >= 3 {
7435        CsharpStringForm::Raw
7436    } else {
7437        CsharpStringForm::Plain
7438    };
7439    Some(CsharpPrefix {
7440        start: index,
7441        quote: cursor,
7442        quotes,
7443        dollars,
7444        form,
7445    })
7446}
7447
7448/// Whether the raw string whose content begins at `content` carries its line
7449/// breaks, which it does when only blanks stand between its opening quote run
7450/// and the end of that line.
7451///
7452/// This is a read of the line the literal opens on and no further, and the scan
7453/// takes every byte of it either way — the blanks are content of a multi-line
7454/// literal and content of a single-line one alike, and the byte that ends them
7455/// is the closing quote run, a brace, or the line terminator the literal ends
7456/// at — so there is nothing here for a checkpoint to lose.
7457fn csharp_multiline_raw_string(bytes: &[u8], content: usize) -> bool {
7458    let mut cursor = content;
7459    while cursor < bytes.len() {
7460        if csharp_line_terminator_width(bytes, cursor).is_some() {
7461            return true;
7462        }
7463        if !is_csharp_blank(bytes[cursor]) {
7464            return false;
7465        }
7466        cursor += 1;
7467    }
7468    false
7469}
7470
7471/// Where a `}` at `index` leaves an interpolation hole that `braces` braces
7472/// close: the end of the hole and `true` when the run is long enough, and one
7473/// past the run it did hold with `false` when it is not.
7474///
7475/// The run is read no further than `braces`, so the close consumes exactly what
7476/// it read and a run too short is consumed whole; neither hands a byte back.
7477fn csharp_hole_close(bytes: &[u8], index: usize, braces: usize) -> (usize, bool) {
7478    let mut run = index;
7479    while run - index < braces && bytes.get(run) == Some(&b'}') {
7480        run += 1;
7481    }
7482    if run - index == braces {
7483        (index + braces, true)
7484    } else {
7485        (run, false)
7486    }
7487}
7488
7489/// The width of the line terminator at `index`, or `None` when there is none.
7490///
7491/// ECMA-334 6.3.1 writes `New_Line_Character` as five characters rather than
7492/// two: the carriage return and line feed every language here has, and U+0085,
7493/// U+2028 and U+2029 besides. Roslyn's lexer ends a `//` comment, a string and
7494/// a character literal at all five, which is exactly why this is not
7495/// [`line_end`]: a comment read on past a U+2028 would swallow the code behind
7496/// it and a removal would take that code with it.
7497fn csharp_line_terminator_width(bytes: &[u8], index: usize) -> Option<usize> {
7498    csharp_unicode_line_terminator_width(bytes, index)
7499        .or_else(|| unicode_line_terminator_width(bytes, index))
7500}
7501
7502/// The width of the *non-ASCII* line terminator at `index`, or `None`.
7503///
7504/// U+2028 and U+2029 are the two JavaScript shares, and U+0085 the one it does
7505/// not: a C# source encoded in UTF-8 spells the next-line character `\xc2\x85`.
7506fn csharp_unicode_line_terminator_width(bytes: &[u8], index: usize) -> Option<usize> {
7507    match bytes.get(index) {
7508        Some(0xc2) if bytes.get(index + 1) == Some(&0x85) => Some(2),
7509        Some(0xe2)
7510            if bytes.get(index + 1) == Some(&0x80)
7511                && matches!(bytes.get(index + 2), Some(0xa8 | 0xa9)) =>
7512        {
7513            Some(3)
7514        }
7515        _ => None,
7516    }
7517}
7518
7519/// One past the end of the line `index` stands on, counting every one of C#'s
7520/// five line terminators. See [`csharp_line_terminator_width`].
7521fn csharp_line_end(bytes: &[u8], mut index: usize) -> usize {
7522    while index < bytes.len() && csharp_line_terminator_width(bytes, index).is_none() {
7523        index += 1;
7524    }
7525    index
7526}
7527
7528/// The kind of a Scala line comment.
7529///
7530/// The comment reader of the Scala 3 compiler classifies a comment as
7531/// documentation when its raw text starts with `/**` (`Comment.isDocComment`),
7532/// and a line comment cannot, so `///` — which scaladoc does not read — and
7533/// `//!` are ordinary line comments, unlike Dart's and Swift's third slash.
7534fn scala_line_kind(_bytes: &[u8], _index: usize) -> CommentKind {
7535    CommentKind::Line
7536}
7537
7538/// The kind of a Scala block comment: documentation exactly when its raw text
7539/// starts with `/**`, which is what `Comment.isDocComment` answers, so `/**/`
7540/// and `/***/` are documentation comments — their second `*` is content — and
7541/// `/*!` is Doxygen's marker, which Scala does not honour.
7542fn scala_block_kind(bytes: &[u8], index: usize) -> CommentKind {
7543    if starts(bytes, index, b"/**") {
7544        CommentKind::DocBlock
7545    } else {
7546        CommentKind::Block
7547    }
7548}
7549
7550/// End of a Scala character literal, or `None` for the Scala 2 symbol form
7551/// `'name`. Requiring the closing apostrophe immediately after exactly one
7552/// character (or one escape) is what prevents a symbol from becoming a
7553/// line-spanning string, while still protecting `'\u0022'` from being mistaken
7554/// for the opening quote of a Scala string.
7555fn scala_character_literal_end(bytes: &[u8], start: usize) -> Option<usize> {
7556    let content = start + 1;
7557    let mut end = content;
7558    match *bytes.get(content)? {
7559        b'\r' | b'\n' | b'\'' => return None,
7560        b'\\' => {
7561            end += 1;
7562            if bytes.get(end) == Some(&b'u') {
7563                while bytes.get(end) == Some(&b'u') {
7564                    end += 1;
7565                }
7566                let digits = bytes.get(end..end + 4)?;
7567                if !digits.iter().all(u8::is_ascii_hexdigit) {
7568                    return None;
7569                }
7570                end += 4;
7571            } else {
7572                bytes.get(end)?;
7573                end += 1;
7574            }
7575        }
7576        byte if byte.is_ascii() => end += 1,
7577        byte => {
7578            let width = match byte {
7579                0xc2..=0xdf => 2,
7580                0xe0..=0xef => 3,
7581                0xf0..=0xf4 => 4,
7582                _ => return None,
7583            };
7584            std::str::from_utf8(bytes.get(content..content + width)?).ok()?;
7585            end += width;
7586        }
7587    }
7588    (bytes.get(end) == Some(&b'\'')).then_some(end + 1)
7589}
7590
7591/// Whether the quote at `quote` opens an *interpolated* string.
7592///
7593/// The compiler's lexer turns an identifier standing directly before a quote
7594/// into `INTERPOLATIONID` (`fetchToken` checks `ch == '"' && token ==
7595/// IDENTIFIER` after reading an identifier), and a keyword is its own token,
7596/// so `s"..."` and `raw"..."` and a custom interpolator interpolate and
7597/// `return"..."` does not. A number is not an identifier either, so the run
7598/// of identifier characters is read back from the quote and refused when it
7599/// begins with a digit.
7600fn scala_interpolator(bytes: &[u8], quote: usize) -> bool {
7601    let mut start = quote;
7602    while start > 0 && scala_identifier_part(bytes[start - 1]) {
7603        start -= 1;
7604    }
7605    if start == quote || !scala_identifier_start(bytes[start]) {
7606        return false;
7607    }
7608    !scala_is_keyword(&bytes[start..quote])
7609}
7610
7611/// Whether `word` is a hard Scala keyword, which is its own token and does not
7612/// interpolate a string after it. Soft keywords — `as`, `derives`, `end`,
7613/// `extension`, `infix`, `inline`, `opaque`, `using` — are identifiers in the
7614/// lexer, so they are interpolators like any other.
7615fn scala_is_keyword(word: &[u8]) -> bool {
7616    matches!(
7617        word,
7618        b"abstract"
7619            | b"case"
7620            | b"catch"
7621            | b"class"
7622            | b"def"
7623            | b"do"
7624            | b"else"
7625            | b"enum"
7626            | b"export"
7627            | b"extends"
7628            | b"final"
7629            | b"finally"
7630            | b"for"
7631            | b"given"
7632            | b"if"
7633            | b"implicit"
7634            | b"import"
7635            | b"lazy"
7636            | b"match"
7637            | b"new"
7638            | b"object"
7639            | b"open"
7640            | b"override"
7641            | b"package"
7642            | b"private"
7643            | b"protected"
7644            | b"return"
7645            | b"sealed"
7646            | b"then"
7647            | b"throw"
7648            | b"trait"
7649            | b"transparent"
7650            | b"try"
7651            | b"type"
7652            | b"val"
7653            | b"var"
7654            | b"while"
7655            | b"with"
7656            | b"yield"
7657    )
7658}
7659
7660/// The first byte of a Scala identifier; `$` is one, because `$anonfun` is.
7661fn scala_identifier_start(byte: u8) -> bool {
7662    byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
7663}
7664
7665/// A byte an identifier may carry after its first.
7666fn scala_identifier_part(byte: u8) -> bool {
7667    byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')
7668}
7669
7670/// Whether the `<` at `index` opens an XML literal, which is exactly where the
7671/// compiler's lexer emits `XMLSTART`: the byte before it is space, tab, line
7672/// feed, `{`, `(` or `>` — a token boundary, and not the `x` of `x<a>` — and
7673/// the byte after it is an XML name start, `!` or `?`. The name start is the
7674/// lexer's own `xml.Utility.isNameStart`, which answers to letters and `_`
7675/// and *not* to `:` — that is what keeps `_ <: Suite` a type bound and `x<a>`
7676/// a literal — and a byte with the high bit is read as a letter too, which
7677/// costs a comparison nothing and gives a literal a chance to protect text. A
7678/// byte order is read from the start of the file, so a `<` at its very
7679/// beginning is preceded by the space the lexer defaults to. A bare `\r` is
7680/// not a boundary: `<a>` after one is a comparison, exactly as the lexer
7681/// reads it.
7682fn scala_is_xml_start(bytes: &[u8], index: usize) -> bool {
7683    let before = if index > 0 { bytes[index - 1] } else { b' ' };
7684    if !matches!(before, b' ' | b'\t' | b'\n' | b'{' | b'(' | b'>') {
7685        return false;
7686    }
7687    match bytes.get(index + 1) {
7688        Some(b'!' | b'?') => true,
7689        Some(&byte) => byte.is_ascii_alphabetic() || matches!(byte, b'_') || byte >= 0x80,
7690        None => false,
7691    }
7692}
7693
7694/// The first byte of an XML name: XML 1.0 `NameStartChar`, of which the ASCII
7695/// alphabet, `_` and `:` are the bytes a source file actually writes. A byte
7696/// with the high bit is read as one too — it may be a non-ASCII letter — which
7697/// costs a comparison nothing and gives a literal a chance to protect text.
7698fn xml_name_start(byte: u8) -> bool {
7699    byte.is_ascii_alphabetic() || matches!(byte, b'_' | b':') || byte >= 0x80
7700}
7701
7702/// The length of the XML name beginning at `index`, in bytes.
7703fn xml_name_len(bytes: &[u8]) -> usize {
7704    let mut index = 0;
7705    while index < bytes.len() && xml_name_char(bytes[index]) {
7706        index += 1;
7707    }
7708    index
7709}
7710
7711/// A byte an XML name may carry after its first.
7712fn xml_name_char(byte: u8) -> bool {
7713    xml_name_start(byte) || byte.is_ascii_digit() || matches!(byte, b'-' | b'.')
7714}
7715
7716/// The byte after the tail of a closing XML tag: the name already read, and
7717/// whatever white space and `>` end the tag, if the `>` is there at all.
7718fn skip_xml_tag_tail(bytes: &[u8], mut index: usize) -> Option<usize> {
7719    while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\r' | b'\n')) {
7720        index += 1;
7721    }
7722    (bytes.get(index) == Some(&b'>')).then_some(index + 1)
7723}
7724
7725/// The length of the run of `byte` beginning at `index`.
7726fn count_run(bytes: &[u8], index: usize, byte: u8) -> usize {
7727    let mut end = index;
7728    while bytes.get(end) == Some(&byte) {
7729        end += 1;
7730    }
7731    end - index
7732}
7733
7734/// Whether `byte` is white space that a pre-processing directive may stand
7735/// behind, and that leaves the line it is on still blank.
7736///
7737/// ECMA-334 6.5.1 writes `PP_Whitespace` as `Whitespace_Character+`, and a
7738/// vertical tab and a form feed are two of those: Roslyn's `IsWhitespace`
7739/// answers to both, and to the space and tab a file is actually indented with.
7740///
7741/// NOTE: the `Zs` category is deliberately not here. Roslyn does count a
7742/// no-break space as white space in front of a directive, so a line indented
7743/// with one is a directive line there and an ordinary line here — which costs
7744/// that line the one comment a directive may carry and nothing else, where
7745/// reading a `#` no compiler would as a directive would cost the rest of the
7746/// line. No C# file is indented that way; being wrong in the direction that
7747/// keeps a comment is what this picks.
7748const fn is_csharp_blank(byte: u8) -> bool {
7749    matches!(byte, b' ' | b'\t' | 0x0b | 0x0c)
7750}
7751
7752/// Whether the directive named `name` takes the rest of its line as a message.
7753///
7754/// `#error` and `#warning` carry the text their diagnostic quotes and `#region`
7755/// and `#endregion` the label an editor folds under, and ECMA-334 6.5.1 writes
7756/// all four as `Input_Character*` — every byte to the end of the line,
7757/// whatever it spells. Roslyn still lexes a `//` that *opens* the message as a
7758/// comment, which is the one case this distinction has to keep.
7759fn csharp_directive_takes_a_message(name: &[u8]) -> bool {
7760    matches!(name, b"error" | b"warning" | b"region" | b"endregion")
7761}
7762
7763/// The name of the C# string form that was left open, so the diagnostic says
7764/// which of the six a reader has to go and close.
7765const fn csharp_unterminated_string(form: CsharpStringForm, interpolated: bool) -> &'static str {
7766    match (form, interpolated) {
7767        (CsharpStringForm::Plain, false) => "unterminated C# string",
7768        (CsharpStringForm::Plain, true) => "unterminated C# interpolated string",
7769        (CsharpStringForm::Verbatim, false) => "unterminated C# verbatim string",
7770        (CsharpStringForm::Verbatim, true) => "unterminated C# interpolated verbatim string",
7771        (CsharpStringForm::Raw, false) => "unterminated C# raw string",
7772        (CsharpStringForm::Raw, true) => "unterminated C# interpolated raw string",
7773    }
7774}
7775
7776/// The kind of a C# line comment.
7777///
7778/// `///` is the XML documentation comment of ECMA-334 6.3.3, and a fourth slash
7779/// takes it back: Roslyn's lexer asks for `///` and then that the byte behind
7780/// is no slash, so `////` is the ordinary comment a reader rules a section off
7781/// with — which is where C# parts company with Dart and Swift and keeps company
7782/// with Java. `//!` is Rust's inner-doc marker and means nothing here.
7783fn csharp_line_kind(bytes: &[u8], index: usize) -> CommentKind {
7784    if starts(bytes, index, b"///") && bytes.get(index + 3) != Some(&b'/') {
7785        CommentKind::DocLine
7786    } else {
7787        CommentKind::Line
7788    }
7789}
7790
7791/// The kind of a C# block comment.
7792///
7793/// `/**` opens the delimited documentation comment, with two spellings that are
7794/// not there to open anything: `/**/` is the *empty* block comment, whose
7795/// second `*` is the first byte of its own terminator, and `/***` is a rule of
7796/// stars. Roslyn asks for `/**` and then that the byte behind is neither `*`
7797/// nor `/`, and reports `/**/`, `/***/` and `/*** x */` as `MultiLineComment`
7798/// where `/** x */` is `MultiLineDocumentationComment`. `/*!` is Doxygen's
7799/// marker, which C and C++ honour and C# does not.
7800fn csharp_block_kind(bytes: &[u8], index: usize) -> CommentKind {
7801    if starts(bytes, index, b"/**") && !matches!(bytes.get(index + 3), Some(b'*' | b'/')) {
7802        CommentKind::DocBlock
7803    } else {
7804        CommentKind::Block
7805    }
7806}
7807
7808/// The name of the Dart string form that was left open, so the diagnostic says
7809/// which of the six a reader has to go and close.
7810const fn dart_unterminated_string(raw: bool, triple: bool) -> &'static str {
7811    match (raw, triple) {
7812        (true, true) => "unterminated Dart raw multiline string",
7813        (true, false) => "unterminated Dart raw string",
7814        (false, true) => "unterminated Dart multiline string",
7815        (false, false) => "unterminated Dart string",
7816    }
7817}
7818
7819/// Whether the quote at `quote` is opened by a raw-string `r`.
7820///
7821/// `tokenizeRawStringKeywordOrIdentifier` is reached from the scanner's main
7822/// switch, which means the `r` has to *begin a token*: an `r` that continues an
7823/// identifier is a letter of that identifier, and the quote behind it opens an
7824/// ordinary string. Only a lower-case `r` does it — `R'x'` is the identifier
7825/// `R` and then a string.
7826///
7827/// What decides it is therefore the run of identifier bytes ending just before
7828/// the `r`. An empty run means nothing precedes it and the `r` begins a token.
7829/// A run that begins with a letter, `_` or `$` is an identifier the `r`
7830/// continues. A run that begins with a digit is a number, and a number token
7831/// always ends before an `r` — `r` is not a digit, a hex digit, `x`, `e`, `.`
7832/// or `_` — so the `r` begins a token there too.
7833///
7834/// Measured on Dart SDK 3.13.2: `1r'x'` and `0x1r'x'` are `INT`/`HEXADECIMAL`
7835/// and then a raw `STRING`, while `xr'x'`, `_r'x'` and `$r'x'` are one
7836/// `IDENTIFIER` and then an ordinary `STRING`.
7837fn dart_raw_string_prefix(bytes: &[u8], quote: usize) -> bool {
7838    if quote == 0 || bytes[quote - 1] != b'r' {
7839        return false;
7840    }
7841    let mut cursor = quote - 1;
7842    while cursor > 0 && is_dart_identifier_continue(bytes[cursor - 1]) {
7843        cursor -= 1;
7844    }
7845    cursor == quote - 1 || bytes[cursor].is_ascii_digit()
7846}
7847
7848/// Whether `byte` may stand inside a Dart identifier.
7849///
7850/// The grammar spells one `IDENTIFIER_START_NO_DOLLAR ::= LETTER | '_'` with
7851/// `'$'` allowed as well, and `IDENTIFIER_PART` adds the digits (Dart Language
7852/// Specification, 17.4 Identifier Reference). `LETTER` is an ASCII letter
7853/// there and nothing wider, so this is deliberately ASCII-only.
7854fn is_dart_identifier_continue(byte: u8) -> bool {
7855    byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')
7856}
7857
7858/// Whether `raw` is Dart's language version comment, which the scanner itself
7859/// reads rather than a tool.
7860///
7861/// `tokenizeLanguageVersionOrSingleLineComment` accepts exactly two slashes —
7862/// a third one sends it to `tokenizeSingleLineComment` instead — then spaces,
7863/// `@dart` in lower case, spaces, `=`, spaces, a run of digits, `.`, a second
7864/// run of digits, spaces, and the end of the line. Anything else falls back to
7865/// an ordinary comment, so this follows the same grammar byte for byte. Only
7866/// the space is skipped, not the tab: the scanner compares against `$SPACE`.
7867///
7868/// The comment is honoured only ahead of the first real token of a file, and
7869/// this is asked of every comment in one. Reading a later one as an instruction
7870/// keeps a comment a removal would otherwise take, which is the direction to be
7871/// wrong in, and it is what keeps the answer independent of where in the
7872/// document the scan began.
7873fn dart_language_version(raw: &[u8]) -> bool {
7874    fn past_spaces(bytes: &[u8]) -> &[u8] {
7875        let taken = bytes.iter().take_while(|byte| **byte == b' ').count();
7876        &bytes[taken..]
7877    }
7878    fn past_digits(bytes: &[u8]) -> Option<&[u8]> {
7879        let taken = bytes
7880            .iter()
7881            .take_while(|byte| byte.is_ascii_digit())
7882            .count();
7883        (taken > 0).then(|| &bytes[taken..])
7884    }
7885    let Some(rest) = raw.strip_prefix(b"//") else {
7886        return false;
7887    };
7888    if rest.first() == Some(&b'/') {
7889        return false;
7890    }
7891    let Some(rest) = past_spaces(rest).strip_prefix(b"@dart") else {
7892        return false;
7893    };
7894    let Some(rest) = past_spaces(rest).strip_prefix(b"=") else {
7895        return false;
7896    };
7897    let Some(rest) = past_digits(past_spaces(rest)) else {
7898        return false;
7899    };
7900    let Some(rest) = rest.strip_prefix(b".") else {
7901        return false;
7902    };
7903    let Some(rest) = past_digits(rest) else {
7904        return false;
7905    };
7906    past_spaces(rest).is_empty()
7907}
7908
7909fn line_kind(bytes: &[u8], index: usize) -> CommentKind {
7910    if starts(bytes, index, b"///") || starts(bytes, index, b"//!") {
7911        CommentKind::DocLine
7912    } else {
7913        CommentKind::Line
7914    }
7915}
7916
7917fn block_kind(bytes: &[u8], index: usize) -> CommentKind {
7918    if starts(bytes, index, b"/**") || starts(bytes, index, b"/*!") {
7919        CommentKind::DocBlock
7920    } else {
7921        CommentKind::Block
7922    }
7923}
7924
7925/// The kind of a Lua short comment.
7926///
7927/// `---` opens the documentation comment LDoc and the Lua language server
7928/// read; a fourth dash makes the divider that separates one section of a file
7929/// from the next, which documents nothing and is an ordinary comment.
7930fn lua_line_kind(bytes: &[u8], index: usize) -> CommentKind {
7931    if starts(bytes, index, b"---") && !starts(bytes, index, b"----") {
7932        CommentKind::DocLine
7933    } else {
7934        CommentKind::Line
7935    }
7936}
7937
7938/// The kind of a Zig comment.
7939///
7940/// `///` documents the declaration under it and `//!` the container the file
7941/// is (Zig Language Reference, Doc comments), and `std.zig.Tokenizer` tags
7942/// them `doc_comment` and `container_doc_comment`. A fourth slash takes the
7943/// first back: `.doc_comment_start` falls to `.line_comment` when it meets
7944/// one, so `////` is an ordinary comment and only exactly three slashes
7945/// document anything. `//!!` stays a top-level doc comment, because the
7946/// tokenizer decides that one at the `!` and reads no further.
7947fn zig_line_kind(bytes: &[u8], index: usize) -> CommentKind {
7948    if starts(bytes, index, b"////") {
7949        CommentKind::Line
7950    } else if starts(bytes, index, b"///") || starts(bytes, index, b"//!") {
7951        CommentKind::DocLine
7952    } else {
7953        CommentKind::Line
7954    }
7955}
7956
7957/// The kind of an R comment.
7958///
7959/// R's parser has one comment token and calls every `#` line a `COMMENT`
7960/// (measured on R 4.3.3: `utils::getParseData` gives `#' doc` and `# line` the
7961/// same token name). `#'` is roxygen2's marker for the prose it turns into a
7962/// manual page, so it is documentation here for the reason Lua's `---` and
7963/// Zig's `///` are: the tool that reads it is what makes it one. Nothing takes
7964/// the marker back the way a fourth slash does in Zig — roxygen2 reads `#''`
7965/// and `#'#` as its own too — so the test is the two bytes and no more.
7966fn r_line_kind(bytes: &[u8], index: usize) -> CommentKind {
7967    if starts(bytes, index, b"#'") {
7968        CommentKind::DocLine
7969    } else {
7970        CommentKind::Line
7971    }
7972}
7973
7974/// Whether `byte` may continue an R name, and so cannot be followed by the `r`
7975/// that opens a raw string.
7976///
7977/// `SymbolValue` (`gram.y`) reads a name while the bytes are alphanumeric, `.`
7978/// or `_`, and it is entered on a multi-byte character as well, so every byte
7979/// with the high bit set counts here. Counting one that does not only refuses
7980/// the raw reading, which falls back to an ordinary string and hides more
7981/// rather than less.
7982fn is_r_name_byte(byte: u8) -> bool {
7983    byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_') || byte >= 0x80
7984}
7985
7986/// The end of the R raw string whose quote is at `quote`, and whether it closed
7987/// — or `None` when no raw string opens there at all.
7988///
7989/// The literal is `r` or `R`, the quote, a run of dashes, and one of `(`, `[`
7990/// or `{`; it closes on the matching bracket, the same run of dashes, and the
7991/// same quote (`?Quotes`; R 4.0.0 and later). The dash run is what lets the
7992/// closing bracket appear as content, so it is copied out of the source rather
7993/// than counted twice, and R puts no limit on its length — 100 dashes were
7994/// measured accepted on R 4.3.3.
7995///
7996/// The `r` opens the literal only where it begins a token: `xr"(a)"` is the
7997/// name `xr` and then an ordinary string, which is what R's lexer reads there
7998/// too.
7999fn r_raw_string(bytes: &[u8], quote: usize) -> Option<(usize, bool)> {
8000    let prefix = quote.checked_sub(1)?;
8001    if !matches!(bytes[prefix], b'r' | b'R') {
8002        return None;
8003    }
8004    if prefix > 0 && is_r_name_byte(bytes[prefix - 1]) {
8005        return None;
8006    }
8007    let mut bracket = quote + 1;
8008    while bytes.get(bracket) == Some(&b'-') {
8009        bracket += 1;
8010    }
8011    let closing = match bytes.get(bracket) {
8012        Some(b'(') => b')',
8013        Some(b'[') => b']',
8014        Some(b'{') => b'}',
8015        _ => return None,
8016    };
8017    let mut close = Vec::with_capacity(bracket - quote + 1);
8018    close.push(closing);
8019    close.extend_from_slice(&bytes[quote + 1..bracket]);
8020    close.push(bytes[quote]);
8021    Some(match find_subslice(&bytes[bracket + 1..], &close) {
8022        Some(relative) => (bracket + 1 + relative + close.len(), true),
8023        None => (bytes.len(), false),
8024    })
8025}
8026
8027/// The end of the R literal that runs from `index` to the next unescaped
8028/// `close`, and whether that delimiter was there at all.
8029///
8030/// One function for the two quoted strings and the backquoted name, because R
8031/// lexes all three the same way: `\` carries the next byte in — a line break
8032/// included, which is why a literal that never closes runs to the end of the
8033/// file rather than to the end of its line — and nothing but the delimiter ends
8034/// them.
8035fn r_delimited_end(bytes: &[u8], mut index: usize, close: u8) -> (usize, bool) {
8036    while index < bytes.len() {
8037        if bytes[index] == b'\\' {
8038            index = (index + 2).min(bytes.len());
8039        } else if bytes[index] == close {
8040            return (index + 1, true);
8041        } else {
8042            index += 1;
8043        }
8044    }
8045    (bytes.len(), false)
8046}
8047
8048/// The level of the long bracket that opens at `index`, or `None` when none
8049/// does.
8050///
8051/// An opening long bracket is `[`, then a run of `=`, then `[`, and the length
8052/// of that run is its level (Lua 5.4 reference manual, 3.1). The second `[` is
8053/// what tells `[[` from the two brackets of `a[b[1]]`, so a bracket that never
8054/// reaches it opens nothing at all.
8055///
8056/// The closing form is the same run between `]` and `]`, which is why
8057/// [`long_bracket_end`] takes the level rather than a delimiter: a level-two
8058/// bracket carries `]]` and `]=]` as content and ends only at `]==]`.
8059fn long_bracket_level(bytes: &[u8], index: usize) -> Option<usize> {
8060    if bytes.get(index) != Some(&b'[') {
8061        return None;
8062    }
8063    let mut cursor = index + 1;
8064    while bytes.get(cursor) == Some(&b'=') {
8065        cursor += 1;
8066    }
8067    (bytes.get(cursor) == Some(&b'[')).then(|| cursor - index - 1)
8068}
8069
8070/// The end of a long bracket whose content starts at `content`, and whether the
8071/// closing bracket of `level` was there at all.
8072///
8073/// Long brackets do not nest, so the first close at the right level ends it and
8074/// a run of the wrong length is content. A `]` that opens a run of the wrong
8075/// length is passed over rather than skipped: the bytes it ran through are `=`,
8076/// which can start no close of their own.
8077fn long_bracket_end(bytes: &[u8], content: usize, level: usize) -> (usize, bool) {
8078    let mut index = content.min(bytes.len());
8079    while let Some(relative) = memchr(b']', &bytes[index..]) {
8080        let close = index + relative;
8081        let mut cursor = close + 1;
8082        while bytes.get(cursor) == Some(&b'=') {
8083            cursor += 1;
8084        }
8085        if cursor - close - 1 == level && bytes.get(cursor) == Some(&b']') {
8086            return (cursor + 1, true);
8087        }
8088        index = close + 1;
8089    }
8090    (bytes.len(), false)
8091}
8092
8093/// Whether `byte` is ECMAScript `WhiteSpace` or a `LineTerminator`, as far as
8094/// one byte can say (ECMA-262 12.2, 12.3). <VT> is whitespace to JavaScript and
8095/// is exactly what [`u8::is_ascii_whitespace`] leaves out, so asking that
8096/// instead reads `a\u{b}<div>` as a JSX element where the language sees a
8097/// comparison. The non-ASCII members — U+00A0, U+FEFF, and the `Zs` category —
8098/// take more than one byte and are not decided here.
8099fn js_is_space(byte: u8) -> bool {
8100    matches!(byte, b'\t' | b'\n' | 0x0b | 0x0c | b'\r' | b' ')
8101}
8102
8103/// Whether `byte` is whitespace to Lua's lexer, which is what `\z` skips. It is
8104/// C's `isspace` in the default locale, and so takes the vertical tab that
8105/// [`u8::is_ascii_whitespace`] leaves out.
8106fn lua_is_space(byte: u8) -> bool {
8107    matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
8108}
8109
8110/// The width of the line terminator at `index`, or `None` when there is none.
8111///
8112/// Lua counts `\r\n` and `\n\r` alike as one line (`llex.c`,
8113/// `inclinenumber`), so a backslash in front of either escapes the whole pair.
8114fn lua_newline_width(bytes: &[u8], index: usize) -> Option<usize> {
8115    match (bytes.get(index), bytes.get(index + 1)) {
8116        (Some(b'\r'), Some(b'\n')) | (Some(b'\n'), Some(b'\r')) => Some(2),
8117        (Some(b'\r' | b'\n'), _) => Some(1),
8118        _ => None,
8119    }
8120}
8121
8122/// How many `quote` bytes run on from `start`, which is one of them.
8123fn toml_quote_run(bytes: &[u8], start: usize, quote: u8) -> usize {
8124    let mut index = start;
8125    while index < bytes.len() && bytes[index] == quote {
8126        index += 1;
8127    }
8128    index - start
8129}
8130
8131/// Whether a quote at `index` follows a flow indicator, which is the other
8132/// place a scalar may begin: the quote of `[a,"b # c"]` opens one although no
8133/// white space precedes it (YAML 1.2.2, 7.4). Everywhere else an apostrophe or
8134/// a quote behind an ordinary byte is content of the plain scalar it sits in,
8135/// which is what keeps the one in `note: it's fine` from opening a literal
8136/// that would swallow the rest of the file.
8137fn yaml_flow_opener(bytes: &[u8], index: usize) -> bool {
8138    index > 0 && matches!(bytes[index - 1], b',' | b'[' | b'{')
8139}
8140
8141/// Which trailing line breaks a block scalar keeps (YAML 1.2.2, 8.1.1.2).
8142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8143enum Chomping {
8144    /// `-`: the final line break and every empty line behind it are dropped.
8145    Strip,
8146    /// No indicator, and the default: the final line break stays and the empty
8147    /// lines behind it are dropped.
8148    Clip,
8149    /// `+`: the final line break and every empty line behind it are content,
8150    /// which is what makes a blank line under such a body change its value.
8151    Keep,
8152}
8153
8154/// Where the node property beginning at `index` ends.
8155///
8156/// An anchor `&name` and a tag `!tag` run to the white space, the line, or the
8157/// flow indicator that ends them (YAML 1.2.2, 6.9 and 7.4); nothing else may
8158/// close one, which is what keeps `!!str` a single token.
8159fn yaml_property_end(bytes: &[u8], index: usize) -> usize {
8160    let mut cursor = index + 1;
8161    while cursor < bytes.len()
8162        && !matches!(
8163            bytes[cursor],
8164            b' ' | b'\t' | b'\r' | b'\n' | b',' | b'[' | b']' | b'{' | b'}'
8165        )
8166    {
8167        cursor += 1;
8168    }
8169    cursor
8170}
8171
8172/// The block scalar header at `index`, which is its `|` or `>`: the explicit
8173/// indentation indicator, `None` where the header spells none out and the body
8174/// detects its own; the chomping indicator; where a comment on the header line
8175/// begins; and where that line ends.
8176///
8177/// The two readings of a missing indicator are not the same answer. The floor a
8178/// body line has to clear is the owner's column either way — an absent
8179/// indicator behaves as `1` for that — but the depth the body's *content* sits
8180/// at is written on the header only when the indicator is, and is otherwise
8181/// whatever the first non-empty line turns out to be.
8182///
8183/// `None` means the bytes are not a header at all and the indicator is content
8184/// of a plain scalar. That is the whole of what tells `key: >` from `key: a >
8185/// b`: a header is followed by its indicators, then white space, then at most
8186/// a comment, and then the line ends (YAML 1.2.2, 8.1.1). The comment needs
8187/// that white space in front of it like any other (6.6), so `key: |#c` is no
8188/// header either.
8189fn yaml_block_header(
8190    bytes: &[u8],
8191    index: usize,
8192) -> Option<(Option<usize>, Chomping, Option<usize>, usize)> {
8193    let mut cursor = index + 1;
8194    let mut indentation = None;
8195    let mut chomping = None;
8196    while let Some(byte) = bytes.get(cursor).copied() {
8197        match byte {
8198            b'1'..=b'9' if indentation.is_none() => indentation = Some(usize::from(byte - b'0')),
8199            b'+' if chomping.is_none() => chomping = Some(Chomping::Keep),
8200            b'-' if chomping.is_none() => chomping = Some(Chomping::Strip),
8201            _ => break,
8202        }
8203        cursor += 1;
8204    }
8205    let mut spaced = false;
8206    while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
8207        spaced = true;
8208        cursor += 1;
8209    }
8210    let comment = (spaced && bytes.get(cursor) == Some(&b'#')).then_some(cursor);
8211    if comment.is_some() {
8212        cursor = line_end(bytes, cursor);
8213    }
8214    bytes
8215        .get(cursor)
8216        .is_none_or(|byte| matches!(byte, b'\r' | b'\n'))
8217        .then(|| {
8218            (
8219                indentation,
8220                chomping.unwrap_or(Chomping::Clip),
8221                comment,
8222                cursor,
8223            )
8224        })
8225}
8226
8227/// Where the body of the block scalar whose header line ends at `header_end`
8228/// ends, whether that offset is the start of a line, and how deep its content
8229/// turned out to sit.
8230///
8231/// A line belongs to the body while it is empty — an empty line is content of
8232/// the scalar (YAML 1.2.2, 8.1.1.2) whatever its indentation — or indented to
8233/// at least `body_min`. The first line that is neither ends it, and so does a
8234/// document marker in column zero (9.1.2, 9.1.3), which is what ends the body
8235/// of a scalar that is the whole document and therefore has no indentation to
8236/// fall short of.
8237///
8238/// The second of the three answers is what the caller turns into a checkpoint:
8239/// a body that ran out of file in the middle of a line ends nowhere a scan
8240/// could resume.
8241///
8242/// The third is the *detected* content indentation (8.1.1.1): the indentation
8243/// of the first non-empty line, which is what a parser measures every later
8244/// line against, and `body_min` when the body holds no non-empty line to
8245/// measure. It is never less than `body_min`, because a line shallower than
8246/// that would have ended the body instead of opening it.
8247fn yaml_block_body_end(bytes: &[u8], header_end: usize, body_min: usize) -> (usize, bool, usize) {
8248    if header_end >= bytes.len() {
8249        return (bytes.len(), false, body_min);
8250    }
8251    let mut index = consume_newline(bytes, header_end);
8252    let mut content = None;
8253    while index < bytes.len() {
8254        let (indent, blank, end) = yaml_line_shape(bytes, index);
8255        if !blank && (indent < body_min || yaml_document_marker(bytes, index)) {
8256            break;
8257        }
8258        if !blank && content.is_none() {
8259            content = Some(indent);
8260        }
8261        if end >= bytes.len() {
8262            return (bytes.len(), false, content.unwrap_or(body_min));
8263        }
8264        index = consume_newline(bytes, end);
8265    }
8266    (index, true, content.unwrap_or(body_min))
8267}
8268
8269/// The indentation of the line beginning at `start`, whether it is empty, and
8270/// where it ends.
8271///
8272/// Indentation is spaces alone: a tab may not indent a line (YAML 1.2.2, 6.1),
8273/// so the first one ends the indentation and is content of whatever follows
8274/// it. A line of nothing but white space is empty even so, which is what keeps
8275/// a blank line inside a block scalar body from ending it.
8276fn yaml_line_shape(bytes: &[u8], start: usize) -> (usize, bool, usize) {
8277    let mut index = start;
8278    while bytes.get(index) == Some(&b' ') {
8279        index += 1;
8280    }
8281    let indent = index - start;
8282    let mut cursor = index;
8283    while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
8284        cursor += 1;
8285    }
8286    let blank = bytes
8287        .get(cursor)
8288        .is_none_or(|byte| matches!(byte, b'\r' | b'\n'));
8289    (indent, blank, line_end(bytes, cursor))
8290}
8291
8292/// Whether the line beginning at `line_start` is a document marker: `---` or
8293/// `...` with the line or white space behind it. Both are read in column zero
8294/// alone, which is what `line_start` carries — a line with any indentation at
8295/// all begins with a space and matches neither.
8296fn yaml_document_marker(bytes: &[u8], line_start: usize) -> bool {
8297    (starts(bytes, line_start, b"---") || starts(bytes, line_start, b"..."))
8298        && bytes
8299            .get(line_start + 3)
8300            .is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
8301}
8302
8303/// The one comment that is all its line holds, as an index into `comments`.
8304///
8305/// `None` when the line holds none, holds one with something else on it, or
8306/// holds a comment that does not run to the end of the line — in each of those
8307/// the line survives a removal whatever else is decided about it.
8308fn comment_alone_on_line(
8309    source: &[u8],
8310    offset: usize,
8311    comments: &[Comment],
8312    line_start: usize,
8313    line_end: usize,
8314) -> Option<usize> {
8315    /* NOTE: `source` may be a suffix the scan was handed, so its indices run
8316     * `offset` behind the absolute spans a comment carries. Everything below
8317     * compares in the absolute frame and slices in the local one. */
8318    let (start, end) = (line_start + offset, line_end + offset);
8319    let index = comments
8320        .binary_search_by(|comment| {
8321            if comment.span.start < start {
8322                Ordering::Less
8323            } else if comment.span.start >= end {
8324                Ordering::Greater
8325            } else {
8326                Ordering::Equal
8327            }
8328        })
8329        .ok()?;
8330    let span = comments[index].span;
8331    (span.end == end
8332        && source[line_start..span.start - offset]
8333            .iter()
8334            .all(|byte| matches!(byte, b' ' | b'\t')))
8335    .then_some(index)
8336}
8337
8338/// One YAML block scalar, as the two things the lines below it depend on.
8339///
8340/// Where a body ends is decided by the column of the node the header hangs
8341/// off, which is not written on the header's own line — `key:` on one line and
8342/// `|` on the next is the same scalar as `key: |`. Only a scan knows that
8343/// column, so this is recorded while one runs rather than re-derived from the
8344/// bytes afterwards.
8345#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8346pub(crate) struct YamlBlockScalar {
8347    /// The first byte past the body: the start of the first line that is not
8348    /// part of it, or the end of the source.
8349    body_end: usize,
8350    /// How deep the body's content sits: the explicit indentation indicator
8351    /// counted from the owner, or the indentation of the first non-empty line
8352    /// where the header spelled none out (YAML 1.2.2, 8.1.1.1). A line under
8353    /// the body that reaches this depth is content of it; one that does not is
8354    /// outside it whatever else is true, which is the difference between a
8355    /// trail comment a removal may take and one it may not.
8356    content_indent: usize,
8357    /// Which trailing line breaks the header asked to keep.
8358    chomping: Chomping,
8359}
8360
8361/// The keep reason the scanner writes for a comment a YAML block scalar leans
8362/// on, and the one keep no option can overrule.
8363///
8364/// Frozen: the differential protocol compares this string byte for byte, and
8365/// `--explain` recognises the rule by it.
8366pub(crate) const YAML_STRUCTURAL_TRAIL: &str = "structural in a YAML block scalar trail";
8367
8368/// Whether `disposition` is the keep [`YAML_STRUCTURAL_TRAIL`] names.
8369pub(crate) fn is_yaml_structural_trail(disposition: &Disposition) -> bool {
8370    matches!(disposition, Disposition::Keep { reason } if reason == YAML_STRUCTURAL_TRAIL)
8371}
8372
8373/// Which comments in the trails of `blocks` no removal may take, as indices
8374/// into `comments`.
8375///
8376/// A block scalar body ends at the first line under it that is shallower than
8377/// its content (YAML 1.2.2, 8.1.1), and in a trail of whole-line comments that
8378/// line is a comment. Removing it — and a removal there takes the whole line,
8379/// which is the least a removal can leave — hands the lines under it back to
8380/// the body, and a line that reaches the content depth is content again. When
8381/// what comes back up is a comment the run keeps, no removal preserves the
8382/// value: the comment above it is not commentary but the thing that closes the
8383/// scalar, and it is kept.
8384///
8385/// Only the *first* comment of a trail can do that work, and it always can.
8386/// The line a body ended at is shallower than the floor and so shallower than
8387/// the content, so keeping it closes the scalar there and leaves everything
8388/// below outside — which is why one keep per block is both necessary and
8389/// enough, and why the deeper comments of the trail stay removable. Keeping a
8390/// deeper one instead would be no fix at all: it reaches the content depth
8391/// itself, so the body would swallow the survivor.
8392///
8393/// A trail whose every comment is removable needs none of this: with nothing
8394/// left standing under the body there is nothing for it to take back.
8395fn yaml_structural_trail_keeps(
8396    source: &[u8],
8397    offset: usize,
8398    blocks: &[YamlBlockScalar],
8399    comments: &[Comment],
8400) -> Vec<usize> {
8401    let mut keeps = Vec::new();
8402    for block in blocks {
8403        let Some(mut probe) = block.body_end.checked_sub(offset) else {
8404            continue;
8405        };
8406        /* INVARIANT: `shield` is the trail's first removable comment shallower
8407         * than the content — the one keep that would close the body — and is
8408         * set before any deeper line can be reached, because the line a body
8409         * ends at is shallower than the content by construction. */
8410        let mut shield = None;
8411        while probe < source.len() {
8412            let (indent, blank, end) = yaml_line_shape(source, probe);
8413            if blank {
8414                /* NOTE: An empty line is content of the body above whatever its
8415                 * indentation (8.1.1.2), so it neither ends the trail nor
8416                 * shields anything under it. */
8417                probe = past_terminator(source, end);
8418                continue;
8419            }
8420            let Some(found) = comment_alone_on_line(source, offset, comments, probe, end) else {
8421                /* NOTE: The first line with anything else on it is the next
8422                 * node, and it is not a line any removal here can move. */
8423                break;
8424            };
8425            if comments[found].disposition.is_remove() {
8426                if shield.is_none() && indent < block.content_indent {
8427                    shield = Some(found);
8428                }
8429            } else if indent < block.content_indent {
8430                /* NOTE: A surviving line shallower than the content closes the
8431                 * body on its own, so nothing above it is load-bearing. */
8432                break;
8433            } else {
8434                keeps.extend(shield);
8435                break;
8436            }
8437            probe = past_terminator(source, end);
8438        }
8439    }
8440    keeps
8441}
8442
8443/// Apply [`yaml_structural_trail_keeps`] to comments that did not come from a
8444/// scan of this crate's own, which is the external hand-off of
8445/// [`transform_spans`](crate::transform_spans).
8446///
8447/// A scan reaches the same answer from the blocks it already walked over; this
8448/// is the same answer re-derived from the bytes, so the two paths cannot
8449/// disagree about a value.
8450pub(crate) fn keep_yaml_structural_trails(
8451    source: &[u8],
8452    language: Language,
8453    comments: &mut [Comment],
8454) {
8455    /* PERF: The same two answers `lines_a_removal_must_swallow` opens with: no
8456     * `|` and no `>` is no block scalar, and a file whose comments all trail
8457     * something has no whole-line comment to weigh. */
8458    if language != Language::Yaml || comments.is_empty() || memchr2(b'|', b'>', source).is_none() {
8459        return;
8460    }
8461    if !comments.iter().any(|comment| {
8462        comment.disposition.is_remove() && starts_its_line(source, comment.span.start)
8463    }) {
8464        return;
8465    }
8466    let blocks = yaml_block_scalars(source);
8467    for index in yaml_structural_trail_keeps(source, 0, &blocks, comments) {
8468        comments[index].disposition = Disposition::Keep {
8469            reason: YAML_STRUCTURAL_TRAIL.to_owned(),
8470        };
8471    }
8472}
8473
8474/// Every block scalar in a YAML source, in order.
8475///
8476/// A scan of its own, so that the answer stays a function of the bytes alone
8477/// and an incremental rescan or an external hand-off reaches the same one with
8478/// no state to carry. It is the *scanner's* reading of a header, not the loose
8479/// one [`first_yaml_block_scalar`] uses: `key: a |+` ends a plain scalar with
8480/// two characters that look like a header, and reading it as one would hang a
8481/// phantom keep-chomped tail off a line that has no body at all.
8482fn yaml_block_scalars(source: &[u8]) -> Vec<YamlBlockScalar> {
8483    let mut scanner = Scanner::with_offset(
8484        source,
8485        Language::Yaml,
8486        ScanOptions::default(),
8487        0,
8488        false,
8489        None,
8490    );
8491    scanner.scan_yaml();
8492    scanner.yaml_blocks
8493}
8494
8495/// Whether nothing but indentation stands between `start` and the beginning of
8496/// its line, which is the whole of what makes a comment a candidate for being
8497/// swallowed whole.
8498fn starts_its_line(source: &[u8], start: usize) -> bool {
8499    source[..start]
8500        .iter()
8501        .copied()
8502        .rev()
8503        .find(|byte| !matches!(byte, b' ' | b'\t'))
8504        .is_none_or(|byte| matches!(byte, b'\r' | b'\n'))
8505}
8506
8507/// Where a line under a block scalar ends once its terminator is taken with it.
8508fn past_terminator(source: &[u8], line_end: usize) -> usize {
8509    if line_end >= source.len() {
8510        line_end
8511    } else {
8512        consume_newline(source, line_end)
8513    }
8514}
8515
8516/// For each comment, the line a removal has to take whole — its terminator
8517/// included — instead of leaving the ordinary hole on it, or `None` where the
8518/// ordinary hole is right. An empty answer stands for all-`None`, which is
8519/// every language but YAML and nearly every YAML file.
8520///
8521/// YAML is the one language where the hole itself carries meaning, and the
8522/// reason is that a block scalar decides where its body ends from the lines
8523/// *below* it (YAML 1.2.2, 8.1.1). A whole-line comment under a body is
8524/// `l-trail-comments` and is not part of the value, but the hole left in its
8525/// place is read as one of two things:
8526///
8527/// * a line of spaces as wide as the comment, which `columns` writes, is
8528///   indented at least as deep as the body whenever the comment was wide
8529///   enough — and a line indented that deep *is* body content, so the scalar
8530///   silently grows a line;
8531/// * an empty line, which `lines` writes, is content under `|+` and `>+`,
8532///   where every empty line trailing a body is kept (8.1.1.2).
8533///
8534/// So every whole-line comment whose own line sits in the run of empty and
8535/// comment lines under a body is removed by taking the line, terminator and
8536/// all, under every layout — which is the line `compact` takes already. That
8537/// costs those lines their numbering under `lines` and their columns under
8538/// `columns`; the alternative costs the reader's value, and no indentation a
8539/// padded line could be given is safe, because the depth that would put it
8540/// outside one body is the depth that puts it inside the mapping the body
8541/// belongs to.
8542///
8543/// Under `|+` and `>+` the line is not enough on its own. The empty lines
8544/// *between* a removed comment and the next line are `l-comment` while the
8545/// comment shelters them and `l-keep-empty` once it is gone (8.1.1.2), so the
8546/// swallow runs on through them. The empty lines *above* the first comment are
8547/// already content and are left exactly where they are: a removal takes what
8548/// the comment was sheltering and nothing else.
8549///
8550/// The answer is a function of the source and the comments alone, so an
8551/// incremental rescan and an external hand-off reach the same one with no
8552/// state to carry.
8553pub(crate) fn lines_a_removal_must_swallow(
8554    source: &[u8],
8555    language: Language,
8556    comments: &[Comment],
8557) -> Vec<Option<ByteSpan>> {
8558    if language != Language::Yaml || comments.is_empty() {
8559        return Vec::new();
8560    }
8561    /* PERF: Two answers that cost almost nothing, in front of a scan of the
8562     * whole source. A file with no `|` and no `>` in it has no block scalar at
8563     * all; and a comment a body could swallow is one that is alone on its
8564     * line, which is a walk back over that line's indentation and no further —
8565     * the `# note` of `key: value # note` stops on the byte behind it. A YAML
8566     * file whose comments all trail something therefore never pays for the
8567     * scan below. */
8568    if memchr2(b'|', b'>', source).is_none() {
8569        return Vec::new();
8570    }
8571    if !comments.iter().any(|comment| {
8572        comment.disposition.is_remove() && starts_its_line(source, comment.span.start)
8573    }) {
8574        return Vec::new();
8575    }
8576    let blocks = yaml_block_scalars(source);
8577    if blocks.is_empty() {
8578        return Vec::new();
8579    }
8580    let mut answers = vec![None; comments.len()];
8581    for block in blocks {
8582        let mut probe = block.body_end;
8583        while probe < source.len() {
8584            let (_, blank, end) = yaml_line_shape(source, probe);
8585            if blank {
8586                /* NOTE: An empty line neither ends the run nor is taken on its
8587                 * own: it is content of the body above until a comment below
8588                 * it is removed, and only that removal may take it. */
8589                probe = past_terminator(source, end);
8590                continue;
8591            }
8592            let Some(found) = comment_alone_on_line(source, 0, comments, probe, end) else {
8593                /* NOTE: The first line with anything else on it is the next
8594                 * node, and the comments under *it* are that node's. */
8595                break;
8596            };
8597            if comments[found].disposition.is_remove() {
8598                let mut taken = past_terminator(source, end);
8599                if block.chomping == Chomping::Keep {
8600                    while taken < source.len() {
8601                        let (_, blank, run_end) = yaml_line_shape(source, taken);
8602                        if !blank {
8603                            break;
8604                        }
8605                        taken = past_terminator(source, run_end);
8606                    }
8607                }
8608                answers[found] = Some(ByteSpan::new(probe, taken));
8609            }
8610            probe = past_terminator(source, end);
8611        }
8612    }
8613    answers
8614}
8615
8616fn starts(bytes: &[u8], index: usize, needle: &[u8]) -> bool {
8617    bytes.get(index..index.saturating_add(needle.len())) == Some(needle)
8618}
8619
8620/// The two bytes that end a line everywhere a checkpoint may be offered.
8621///
8622/// INVARIANT: a bounded lookahead that decides a token consults this before it
8623/// reads one byte further: a checkpoint sits at the line start behind a
8624/// terminator, so a decision that crossed one would depend on bytes the
8625/// incremental engine is entitled to rescan on their own.
8626fn is_line_terminator(byte: u8) -> bool {
8627    matches!(byte, b'\r' | b'\n')
8628}
8629
8630fn line_end(bytes: &[u8], mut index: usize) -> usize {
8631    while index < bytes.len() && !matches!(bytes[index], b'\r' | b'\n') {
8632        index += 1;
8633    }
8634    index
8635}
8636
8637pub(crate) fn unicode_line_terminator_width(bytes: &[u8], index: usize) -> Option<usize> {
8638    match bytes.get(index) {
8639        Some(b'\r') if bytes.get(index + 1) == Some(&b'\n') => Some(2),
8640        Some(b'\r' | b'\n') => Some(1),
8641        Some(0xe2)
8642            if bytes.get(index + 1) == Some(&0x80)
8643                && matches!(bytes.get(index + 2), Some(0xa8 | 0xa9)) =>
8644        {
8645            Some(3)
8646        }
8647        _ => None,
8648    }
8649}
8650
8651fn js_line_end(bytes: &[u8], mut index: usize) -> usize {
8652    while index < bytes.len() && unicode_line_terminator_width(bytes, index).is_none() {
8653        index += 1;
8654    }
8655    index
8656}
8657
8658fn consume_newline(bytes: &[u8], index: usize) -> usize {
8659    if bytes.get(index) == Some(&b'\r') && bytes.get(index + 1) == Some(&b'\n') {
8660        index + 2
8661    } else {
8662        index + 1
8663    }
8664}
8665
8666fn block_end(bytes: &[u8], start: usize, open: &[u8], close: &[u8], nested: bool) -> (usize, bool) {
8667    let mut index = start + open.len();
8668    let mut depth = 1usize;
8669    while index < bytes.len() {
8670        if nested && starts(bytes, index, open) {
8671            depth += 1;
8672            index += open.len();
8673        } else if starts(bytes, index, close) {
8674            depth -= 1;
8675            index += close.len();
8676            if depth == 0 {
8677                return (index, true);
8678            }
8679        } else {
8680            index += 1;
8681        }
8682    }
8683    (bytes.len(), false)
8684}
8685
8686fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
8687    memmem::find(haystack, needle)
8688}
8689
8690fn rust_raw_start_at_quote(bytes: &[u8], quote: usize) -> Option<(usize, usize)> {
8691    let mut cursor = quote;
8692    while cursor > 0 && bytes[cursor - 1] == b'#' {
8693        cursor -= 1;
8694    }
8695    let hashes = quote - cursor;
8696    if cursor == 0 || bytes[cursor - 1] != b'r' {
8697        return None;
8698    }
8699    let mut start = cursor - 1;
8700    if start > 0 && matches!(bytes[start - 1], b'b' | b'c') {
8701        start -= 1;
8702    }
8703    if start > 0 && is_js_identifier_continue(bytes[start - 1]) {
8704        return None;
8705    }
8706    Some((start, hashes))
8707}
8708
8709/// Whether the apostrophe at `index` opens a character literal rather than a
8710/// lifetime, told apart by a bounded lookahead.
8711///
8712/// INVARIANT: no window this reads may run past a line terminator. `scan_c_family`
8713/// offers a checkpoint at the line start behind every terminator, and a
8714/// checkpoint promises that nothing decided before it depends on bytes after
8715/// it — so a lookahead that read across one would let an edit on the next line
8716/// rewrite a token on this one while the incremental engine reused it
8717/// unchanged. Nothing is lost by stopping there: a Rust character literal ends
8718/// at the line (Rust Reference, Tokens), and `\` before a line terminator is a
8719/// string continuation, never a character escape, so every shape the window
8720/// would have reached across is already invalid Rust.
8721fn rust_char_start(bytes: &[u8], index: usize, reach: &mut Reach) -> bool {
8722    reach.byte(index + 1);
8723    let Some(next) = bytes.get(index + 1) else {
8724        return false;
8725    };
8726    if is_line_terminator(*next) {
8727        return false;
8728    }
8729    if *next == b'\\' {
8730        reach.through(line_bounded_reach(bytes, index + 2, 2));
8731        return bytes
8732            .get(index + 2)
8733            .is_some_and(|byte| !is_line_terminator(*byte))
8734            && bytes.get(index + 3..index + 4) == Some(b"'");
8735    }
8736    reach.byte(index + 2);
8737    if bytes.get(index + 2) == Some(&b'\'') {
8738        return true;
8739    }
8740    if *next & 0x80 == 0 {
8741        return false;
8742    }
8743    reach.through(line_bounded_reach(bytes, index + 1, 5));
8744    bytes[index + 1..]
8745        .iter()
8746        .take(5)
8747        .take_while(|byte| !is_line_terminator(**byte))
8748        .any(|byte| *byte == b'\'')
8749}
8750
8751fn is_c_quote_start(bytes: &[u8], index: usize) -> bool {
8752    matches!(bytes[index], b'"' | b'\'')
8753        || (matches!(bytes[index], b'L' | b'u' | b'U')
8754            && matches!(bytes.get(index + 1), Some(b'"' | b'\'')))
8755        || (starts(bytes, index, b"u8\"") || starts(bytes, index, b"u8'"))
8756}
8757
8758fn cpp_raw_string(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<(usize, bool)> {
8759    let prefixes: [&[u8]; 5] = [b"R\"", b"u8R\"", b"uR\"", b"UR\"", b"LR\""];
8760    let Some(prefix) = prefixes.iter().find(|prefix| starts(bytes, index, prefix)) else {
8761        reach.through((index + 4).min(bytes.len()));
8762        return None;
8763    };
8764    let delimiter_start = index + prefix.len();
8765    /* INVARIANT: as in `ocaml_quoted_string`, and for the same reason. The
8766     * delimiter of a raw string literal is a run of at most 16 d-chars and the
8767     * `(` stands directly behind it ([lex.string]), so this reads that class
8768     * and one byte more rather than searching the document for a `(` that may
8769     * never come. An ordinary `R"` in the code then costs the lines under it
8770     * nothing. */
8771    let mut open = delimiter_start;
8772    while open < delimiter_start + 16 && bytes.get(open).is_some_and(|byte| is_cpp_d_char(*byte)) {
8773        open += 1;
8774    }
8775    /* NOTE: the byte that ended the class decided this, and a `get` that came
8776     * back `None` at the end of the document decided it just the same. */
8777    reach.byte(open);
8778    if bytes.get(open) != Some(&b'(') {
8779        return None;
8780    }
8781    let mut close = Vec::with_capacity(open - delimiter_start + 2);
8782    close.push(b')');
8783    close.extend_from_slice(&bytes[delimiter_start..open]);
8784    close.push(b'"');
8785    Some(match find_subslice(&bytes[open + 1..], &close) {
8786        Some(relative) => {
8787            let end = open + 1 + relative + close.len();
8788            reach.through(end);
8789            (end, true)
8790        }
8791        /* NOTE: nothing is recorded here, and nothing is read past either: the
8792         * scan takes every byte this search crossed, so a rescan from a later
8793         * checkpoint would meet the same open literal and reach the same
8794         * answer. `parse_heredoc` is the one give-up path whose search the scan
8795         * then rewinds behind, which is why the `end_of` there is the one that
8796         * carries weight. */
8797        None => (bytes.len(), false),
8798    })
8799}
8800
8801/// Whether `byte` may stand in the delimiter of a C++ raw string literal.
8802///
8803/// [lex.string]: a d-char is any member of the basic source character set
8804/// except space, `(`, `)`, `\`, and the control characters horizontal tab,
8805/// vertical tab, form feed and new-line. The vertical tab is in that list and
8806/// is not in `u8::is_ascii_whitespace`, so it is named here.
8807const fn is_cpp_d_char(byte: u8) -> bool {
8808    !matches!(
8809        byte,
8810        b' ' | b'(' | b')' | b'\\' | b'\t' | 0x0b | 0x0c | b'\n' | b'\r'
8811    )
8812}
8813
8814fn cpp_raw_start_at_quote(bytes: &[u8], quote: usize) -> Option<usize> {
8815    for prefix in [b"R".as_slice(), b"u8R", b"uR", b"UR", b"LR"] {
8816        let Some(start) = quote.checked_sub(prefix.len()) else {
8817            continue;
8818        };
8819        if bytes.get(start..quote) == Some(prefix)
8820            && (start == 0 || !is_js_identifier_continue(bytes[start - 1]))
8821        {
8822            return Some(start);
8823        }
8824    }
8825    None
8826}
8827
8828/// The end of the OCaml comment opening at `start`, and whether it closed.
8829///
8830/// A comment lexes the string and character literals inside it, so the
8831/// lookaheads that decide those are this one's as well: `reach` carries theirs
8832/// out, because a quoted-string tag search inside a comment can read past the
8833/// comment's own end.
8834fn ocaml_comment_end(bytes: &[u8], start: usize, reach: &mut Reach) -> (usize, bool) {
8835    let mut index = start + 2;
8836    let mut depth = 1;
8837    while index < bytes.len() {
8838        if starts(bytes, index, b"(*") {
8839            depth += 1;
8840            index += 2;
8841        } else if starts(bytes, index, b"*)") {
8842            depth -= 1;
8843            index += 2;
8844            if depth == 0 {
8845                return (index, true);
8846            }
8847        } else if bytes[index] == b'"' {
8848            index += 1;
8849            while index < bytes.len() {
8850                if bytes[index] == b'\\' {
8851                    index = (index + 2).min(bytes.len());
8852                } else if bytes[index] == b'"' {
8853                    index += 1;
8854                    break;
8855                } else {
8856                    index += 1;
8857                }
8858            }
8859        } else if let Some((end, _)) = ocaml_quoted_string(bytes, index, reach) {
8860            index = end;
8861        } else if bytes[index] == b'\'' && ocaml_char_start(bytes, index, reach) {
8862            let quote = bytes[index];
8863            index += 1;
8864            while index < bytes.len() {
8865                if bytes[index] == b'\\' {
8866                    index = (index + 2).min(bytes.len());
8867                } else if bytes[index] == quote {
8868                    index += 1;
8869                    break;
8870                } else {
8871                    index += 1;
8872                }
8873            }
8874        } else {
8875            index += 1;
8876        }
8877    }
8878    (bytes.len(), false)
8879}
8880
8881fn ocaml_quoted_string(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<(usize, bool)> {
8882    /* NOTE: the byte at `index` is the one the scan stands on and consumes
8883     * whichever way this goes, so reading it is no lookahead and nothing is
8884     * recorded for it. `scan_ocaml` asks this of every byte of a document, and
8885     * recording each one would push the watermark along in front of the scan
8886     * and take away every checkpoint the language has. */
8887    if bytes.get(index) != Some(&b'{') {
8888        return None;
8889    }
8890    /* INVARIANT: the tag of a quoted string literal is `[a-z_]*` and the `|`
8891     * stands directly behind it (OCaml manual, Lexical conventions), so this
8892     * reads the class and one byte more rather than searching the document for
8893     * a `|` that may never come. The bound is what the reach is for: an
8894     * ordinary `{` in OCaml code gives up at the first byte outside the class,
8895     * and the lines under it keep their checkpoints instead of losing them to
8896     * a search that crossed the whole file to say no. */
8897    let mut pipe = index + 1;
8898    while bytes
8899        .get(pipe)
8900        .is_some_and(|byte| byte.is_ascii_lowercase() || *byte == b'_')
8901    {
8902        pipe += 1;
8903    }
8904    /* NOTE: the byte that ended the class decided this, and a `get` that came
8905     * back `None` at the end of the document decided it just the same. */
8906    reach.byte(pipe);
8907    if bytes.get(pipe) != Some(&b'|') {
8908        return None;
8909    }
8910    let mut close = Vec::with_capacity(pipe - index + 1);
8911    close.push(b'|');
8912    close.extend_from_slice(&bytes[index + 1..pipe]);
8913    close.push(b'}');
8914    Some(match find_subslice(&bytes[pipe + 1..], &close) {
8915        Some(relative) => {
8916            let end = pipe + 1 + relative + close.len();
8917            reach.through(end);
8918            (end, true)
8919        }
8920        /* NOTE: nothing is recorded here, for the reason `cpp_raw_string`
8921         * gives: the scan consumes every byte this search crossed. */
8922        None => (bytes.len(), false),
8923    })
8924}
8925
8926/// Whether the apostrophe at `index` opens an OCaml character literal.
8927///
8928/// INVARIANT: the same rule [`rust_char_start`] states — neither the two-byte
8929/// window for a bare character nor the eight-byte one for an escape may run
8930/// past a line terminator, because `scan_ocaml` offers a checkpoint at the line
8931/// start behind it. `'\` followed by a line terminator is an illegal backslash
8932/// escape (OCaml manual, Lexical conventions; `ocamlc` 5.5.0 rejects it), so
8933/// the escaped window gives up nothing valid by stopping. The bare window
8934/// crossing costs the one shape OCaml does accept — an apostrophe, a literal
8935/// newline, an apostrophe — which the scanner never read as a literal anyway:
8936/// it ends a character literal at the line, so that shape used to be reported
8937/// as an unterminated literal and is now simply not one.
8938fn ocaml_char_start(bytes: &[u8], index: usize, reach: &mut Reach) -> bool {
8939    reach.byte(index + 1);
8940    let Some(next) = bytes.get(index + 1) else {
8941        return false;
8942    };
8943    if is_line_terminator(*next) {
8944        return false;
8945    }
8946    reach.byte(index + 2);
8947    if bytes.get(index + 2) == Some(&b'\'') {
8948        return true;
8949    }
8950    if *next != b'\\' {
8951        return false;
8952    }
8953    reach.through(line_bounded_reach(bytes, index + 2, 6));
8954    bytes[index + 2..]
8955        .iter()
8956        .take(6)
8957        .take_while(|byte| !is_line_terminator(**byte))
8958        .any(|byte| *byte == b'\'')
8959}
8960
8961fn python_string_start(bytes: &[u8], index: usize) -> Option<(usize, bool, bool, bool)> {
8962    if matches!(bytes[index], b'\'' | b'"') {
8963        return Some((
8964            index,
8965            starts(bytes, index, &[bytes[index]; 3]),
8966            false,
8967            false,
8968        ));
8969    }
8970    if !matches!(
8971        bytes[index].to_ascii_lowercase(),
8972        b'r' | b'u' | b'b' | b'f' | b't'
8973    ) {
8974        return None;
8975    }
8976    if index > 0 && (bytes[index - 1].is_ascii_alphanumeric() || bytes[index - 1] == b'_') {
8977        return None;
8978    }
8979    let mut cursor = index;
8980    while cursor < bytes.len()
8981        && cursor - index < 3
8982        && matches!(
8983            bytes[cursor].to_ascii_lowercase(),
8984            b'r' | b'u' | b'b' | b'f' | b't'
8985        )
8986    {
8987        cursor += 1;
8988    }
8989    if cursor < bytes.len() && matches!(bytes[cursor], b'\'' | b'"') {
8990        let prefix = &bytes[index..cursor];
8991        Some((
8992            cursor,
8993            starts(bytes, cursor, &[bytes[cursor]; 3]),
8994            prefix
8995                .iter()
8996                .any(|byte| byte.eq_ignore_ascii_case(&b'f') || byte.eq_ignore_ascii_case(&b't')),
8997            prefix.iter().any(|byte| byte.eq_ignore_ascii_case(&b'r')),
8998        ))
8999    } else {
9000        None
9001    }
9002}
9003
9004fn shell_single_quote_end(bytes: &[u8], start: usize) -> (usize, bool) {
9005    match bytes[start + 1..].iter().position(|byte| *byte == b'\'') {
9006        Some(relative) => (start + relative + 2, true),
9007        None => (bytes.len(), false),
9008    }
9009}
9010
9011#[derive(Clone, Copy)]
9012enum ShellTerminator {
9013    Parenthesis(usize),
9014    Backtick(usize),
9015}
9016
9017#[derive(Clone, Copy, Eq, PartialEq)]
9018enum ShellCaseState {
9019    AwaitIn,
9020    Pattern,
9021    Body,
9022}
9023
9024struct Heredoc {
9025    operator: usize,
9026    delimiter: Vec<u8>,
9027    strip_tabs: bool,
9028}
9029
9030/// The here-document the `<<` at `index` opens, and the byte after its
9031/// delimiter word.
9032///
9033/// INVARIANT: a quoted delimiter word may legitimately span lines — `<<"EO`,
9034/// a line break, `F"` names the delimiter `EO\nF` — and so may an unquoted one
9035/// carrying a backslash-newline continuation, so this is a lookahead with no
9036/// line bound at all. `reach` carries out how far it read, because the paths
9037/// that give up (an unterminated quote, a backslash at the end of the
9038/// document) rewind the scan to the byte after the operator and lex those
9039/// bytes again from a state this parse already decided out of them. That the
9040/// re-lex happens to reach the same end today is a property of two lexers
9041/// agreeing, not a guarantee the checkpoints may rest on.
9042fn parse_heredoc(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<(Heredoc, usize)> {
9043    reach.byte(index + 2);
9044    let strip_tabs = bytes.get(index + 2) == Some(&b'-');
9045    let mut cursor = index + if strip_tabs { 3 } else { 2 };
9046    while bytes.get(cursor).is_some_and(u8::is_ascii_whitespace)
9047        && !matches!(bytes[cursor], b'\r' | b'\n')
9048    {
9049        cursor += 1;
9050    }
9051    reach.byte(cursor);
9052    let mut delimiter = Vec::new();
9053    let mut quote = None;
9054    let mut saw_word = false;
9055    while cursor < bytes.len() {
9056        reach.byte(cursor);
9057        let byte = bytes[cursor];
9058        if let Some(active) = quote {
9059            if byte == active {
9060                quote = None;
9061                cursor += 1;
9062            } else if active == b'"' && byte == b'\\' {
9063                reach.byte(cursor + 1);
9064                let escaped = *bytes.get(cursor + 1)?;
9065                if escaped == b'\r' {
9066                    reach.byte(cursor + 2);
9067                }
9068                if escaped == b'\r' && bytes.get(cursor + 2) == Some(&b'\n') {
9069                    cursor += 3;
9070                } else if matches!(escaped, b'\r' | b'\n') {
9071                    cursor += 2;
9072                } else if matches!(escaped, b'$' | b'`' | b'"' | b'\\') {
9073                    delimiter.push(escaped);
9074                    cursor += 2;
9075                } else {
9076                    delimiter.extend_from_slice(&[b'\\', escaped]);
9077                    cursor += 2;
9078                }
9079            } else {
9080                delimiter.push(byte);
9081                cursor += 1;
9082            }
9083            continue;
9084        }
9085        /* NOTE: The delimiter is a word (POSIX Shell Command Language, 2.7.4),
9086         * and a word ends at an unquoted operator character. `>` is one:
9087         * `cat <<EOF>out` is a here-document named `EOF` and a redirection,
9088         * not a here-document named `EOF>out`. */
9089        if byte.is_ascii_whitespace()
9090            || matches!(byte, b';' | b'|' | b'&' | b'(' | b')' | b'<' | b'>')
9091        {
9092            break;
9093        }
9094        match byte {
9095            b'\'' | b'"' => {
9096                saw_word = true;
9097                quote = Some(byte);
9098                cursor += 1;
9099            }
9100            b'\\' => {
9101                saw_word = true;
9102                reach.byte(cursor + 1);
9103                let escaped = *bytes.get(cursor + 1)?;
9104                if escaped == b'\r' {
9105                    reach.byte(cursor + 2);
9106                }
9107                if escaped == b'\r' && bytes.get(cursor + 2) == Some(&b'\n') {
9108                    cursor += 3;
9109                } else {
9110                    if !matches!(escaped, b'\r' | b'\n') {
9111                        delimiter.push(escaped);
9112                    }
9113                    cursor += 2;
9114                }
9115            }
9116            _ => {
9117                saw_word = true;
9118                delimiter.push(byte);
9119                cursor += 1;
9120            }
9121        }
9122    }
9123    if cursor >= bytes.len() {
9124        reach.end_of(bytes);
9125    }
9126    if !saw_word || quote.is_some() {
9127        return None;
9128    }
9129    Some((
9130        Heredoc {
9131            operator: index,
9132            delimiter,
9133            strip_tabs,
9134        },
9135        cursor,
9136    ))
9137}
9138
9139fn heredoc_body_end(bytes: &[u8], mut index: usize, heredoc: &Heredoc) -> Option<usize> {
9140    while index <= bytes.len() {
9141        let end = line_end(bytes, index);
9142        let mut line = &bytes[index..end];
9143        if heredoc.strip_tabs {
9144            let first = line
9145                .iter()
9146                .position(|byte| *byte != b'\t')
9147                .unwrap_or(line.len());
9148            line = &line[first..];
9149        }
9150        if line == heredoc.delimiter {
9151            return Some(if end < bytes.len() {
9152                consume_newline(bytes, end)
9153            } else {
9154                end
9155            });
9156        }
9157        if end == bytes.len() {
9158            break;
9159        }
9160        index = consume_newline(bytes, end);
9161    }
9162    None
9163}
9164
9165fn sql_quoted_end(bytes: &[u8], start: usize, quote: u8, backslash_escapes: bool) -> (usize, bool) {
9166    let mut index = start + 1;
9167    while index < bytes.len() {
9168        if bytes[index] == quote && bytes.get(index + 1) == Some(&quote) {
9169            index += 2;
9170        } else if bytes[index] == quote {
9171            return (index + 1, true);
9172        } else if backslash_escapes && bytes[index] == b'\\' {
9173            index = (index + 2).min(bytes.len());
9174        } else {
9175            index += 1;
9176        }
9177    }
9178    (index, false)
9179}
9180
9181fn postgres_escape_string_start(bytes: &[u8], quote: usize) -> bool {
9182    quote > 0
9183        && matches!(bytes[quote - 1], b'e' | b'E')
9184        && (quote == 1 || !is_js_identifier_continue(bytes[quote - 2]))
9185}
9186
9187fn mysql_dash_comment_boundary(next: Option<u8>) -> bool {
9188    next.is_none_or(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
9189}
9190
9191fn sql_identifier_end(bytes: &[u8], start: usize, close: u8) -> (usize, bool) {
9192    let actual_close = if bytes[start] == b'[' { b']' } else { close };
9193    let mut index = start + 1;
9194    while index < bytes.len() {
9195        if bytes[index] == actual_close && bytes.get(index + 1) == Some(&actual_close) {
9196            index += 2;
9197        } else if bytes[index] == actual_close {
9198            return (index + 1, true);
9199        } else {
9200            index += 1;
9201        }
9202    }
9203    (index, false)
9204}
9205
9206fn sql_dollar_quote_end(bytes: &[u8], start: usize, reach: &mut Reach) -> Option<(usize, bool)> {
9207    /* INVARIANT: as in `ocaml_quoted_string`, and for the same reason. The tag
9208     * of a dollar-quoted string is empty or an identifier —
9209     * `[A-Za-z_][A-Za-z0-9_]*` (PostgreSQL 4.1.2.4) — and the second `$`
9210     * stands directly behind it, so this reads that class and one byte more.
9211     * An ordinary `$` in a query then costs the lines under it nothing. */
9212    let mut second = start + 1;
9213    if bytes
9214        .get(second)
9215        .is_some_and(|byte| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'_'))
9216    {
9217        second += 1;
9218        while bytes
9219            .get(second)
9220            .is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
9221        {
9222            second += 1;
9223        }
9224    }
9225    reach.byte(second);
9226    if bytes.get(second) != Some(&b'$') {
9227        return None;
9228    }
9229    let delimiter = &bytes[start..=second];
9230    Some(match find_subslice(&bytes[second + 1..], delimiter) {
9231        Some(relative) => {
9232            let end = second + 1 + relative + delimiter.len();
9233            reach.through(end);
9234            (end, true)
9235        }
9236        /* NOTE: nothing is recorded here, for the reason `cpp_raw_string`
9237         * gives: the scan consumes every byte this search crossed. */
9238        None => (bytes.len(), false),
9239    })
9240}
9241
9242fn oracle_q_quote_end(bytes: &[u8], start: usize, reach: &mut Reach) -> Option<(usize, bool)> {
9243    reach.byte(start + 1);
9244    if bytes.get(start + 1) != Some(&b'\'') {
9245        return None;
9246    }
9247    reach.byte(start + 2);
9248    let open = *bytes.get(start + 2)?;
9249    let close = match open {
9250        b'[' => b']',
9251        b'{' => b'}',
9252        b'(' => b')',
9253        b'<' => b'>',
9254        other => other,
9255    };
9256    let token = [close, b'\''];
9257    Some(match find_subslice(&bytes[start + 3..], &token) {
9258        Some(relative) => {
9259            let end = start + 3 + relative + 2;
9260            reach.through(end);
9261            (end, true)
9262        }
9263        /* NOTE: nothing is recorded here, for the reason `cpp_raw_string`
9264         * gives: the scan consumes every byte this search crossed. */
9265        None => (bytes.len(), false),
9266    })
9267}
9268
9269/// Whether the `-->` at `index` closes an HTML-like comment: ECMA-262 12.5
9270/// makes one of a `-->` that nothing but white space precedes on its line.
9271///
9272/// U+FEFF is `<ZWNBSP>`, which 12.2 lists among `WhiteSpace` wherever it sits
9273/// and however many of it there are — the start of a file is only the most
9274/// common place to meet one. It takes three bytes, which is why the prefix is
9275/// walked rather than handed to [`js_is_space`] byte by byte.
9276fn js_html_close_comment(bytes: &[u8], index: usize) -> bool {
9277    if !starts(bytes, index, b"-->") {
9278        return false;
9279    }
9280    let mut cursor = bytes[..index]
9281        .iter()
9282        .rposition(|byte| matches!(byte, b'\r' | b'\n'))
9283        .map_or(0, |position| position + 1);
9284    while cursor < index {
9285        if starts(bytes, cursor, b"\xef\xbb\xbf") {
9286            cursor += 3;
9287        } else if js_is_space(bytes[cursor]) {
9288            cursor += 1;
9289        } else {
9290            return false;
9291        }
9292    }
9293    true
9294}
9295
9296fn js_regex_end(bytes: &[u8], start: usize) -> Option<usize> {
9297    let mut index = start + 1;
9298    let mut class = false;
9299    while index < bytes.len() {
9300        if unicode_line_terminator_width(bytes, index).is_some() {
9301            return None;
9302        }
9303        match bytes[index] {
9304            b'\\' => {
9305                let escaped = index + 1;
9306                if unicode_line_terminator_width(bytes, escaped).is_some() {
9307                    return None;
9308                }
9309                index = (index + 2).min(bytes.len());
9310            }
9311            b'[' => {
9312                class = true;
9313                index += 1;
9314            }
9315            b']' => {
9316                class = false;
9317                index += 1;
9318            }
9319            b'/' if !class => {
9320                index += 1;
9321                while index < bytes.len()
9322                    && (bytes[index].is_ascii_alphabetic() || bytes[index] == b'_')
9323                {
9324                    index += 1;
9325                }
9326                return Some(index);
9327            }
9328            _ => index += 1,
9329        }
9330    }
9331    None
9332}
9333
9334fn is_js_identifier_start(byte: u8) -> bool {
9335    byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$') || byte & 0x80 != 0
9336}
9337fn is_js_identifier_continue(byte: u8) -> bool {
9338    is_js_identifier_start(byte) || byte.is_ascii_digit()
9339}
9340
9341fn is_js_control_keyword(token: &[u8]) -> bool {
9342    matches!(
9343        token,
9344        b"if" | b"while" | b"for" | b"with" | b"switch" | b"catch"
9345    )
9346}
9347
9348fn jsx_open(bytes: &[u8], index: usize) -> bool {
9349    bytes.get(index) == Some(&b'<')
9350        && bytes
9351            .get(index + 1)
9352            .is_some_and(|byte| byte.is_ascii_alphabetic() || matches!(byte, b'>' | b'_'))
9353}
9354
9355fn html_tag_end(bytes: &[u8], start: usize) -> Option<usize> {
9356    let mut index = start + 1;
9357    let mut quote = None;
9358    while index < bytes.len() {
9359        if let Some(active) = quote {
9360            if bytes[index] == active {
9361                quote = None;
9362            }
9363        } else if matches!(bytes[index], b'\'' | b'"') {
9364            quote = Some(bytes[index]);
9365        } else if bytes[index] == b'>' {
9366            return Some(index + 1);
9367        }
9368        index += 1;
9369    }
9370    None
9371}
9372
9373/// End of a Vue/Svelte tag. Braced attribute expressions may contain the `>`
9374/// of an arrow or comparison, so only one outside balanced braces closes the
9375/// tag. Quotes inside and outside an expression protect their contents.
9376fn sfc_tag_end(bytes: &[u8], start: usize) -> Option<usize> {
9377    let mut index = start + 1;
9378    let mut quote = None;
9379    let mut braces = 0usize;
9380    while index < bytes.len() {
9381        if let Some(active) = quote {
9382            if bytes[index] == b'\\' && braces > 0 {
9383                index = (index + 2).min(bytes.len());
9384                continue;
9385            }
9386            if bytes[index] == active {
9387                quote = None;
9388            }
9389        } else {
9390            match bytes[index] {
9391                b'\'' | b'"' | b'`' if braces > 0 => quote = Some(bytes[index]),
9392                b'\'' | b'"' if braces == 0 => quote = Some(bytes[index]),
9393                b'{' => braces += 1,
9394                b'}' if braces > 0 => braces -= 1,
9395                b'>' if braces == 0 => return Some(index + 1),
9396                _ => {}
9397            }
9398        }
9399        index += 1;
9400    }
9401    None
9402}
9403
9404fn html_tag_candidate(bytes: &[u8], start: usize) -> bool {
9405    match bytes.get(start + 1).copied() {
9406        Some(byte) if byte.is_ascii_alphabetic() || matches!(byte, b'!' | b'?') => true,
9407        Some(b'/') => bytes
9408            .get(start + 2)
9409            .is_some_and(|byte| byte.is_ascii_alphabetic()),
9410        _ => false,
9411    }
9412}
9413
9414fn html_tag_name_end(bytes: &[u8], start: usize) -> usize {
9415    let mut index = start + 1;
9416    if bytes.get(index) == Some(&b'/') {
9417        index += 1;
9418    }
9419    while bytes
9420        .get(index)
9421        .is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'>' | b'/'))
9422    {
9423        index += 1;
9424    }
9425    index
9426}
9427
9428/// The value of the attribute `name` in an attribute list, without its
9429/// quotes, or `None` when the list does not carry the attribute with a value.
9430///
9431/// Attributes are separated by white space and their values are quoted or a
9432/// bare word; the name is matched as a word, so `langx` is not `lang`.
9433/// The end of an inline code span beginning at a run of backticks: the span
9434/// closes at the next run of exactly the same length, per CommonMark 6.1, so
9435/// a shorter or longer run is code text. With no matching run the opener is
9436/// literal text, not a code span, and only its own bytes are consumed.
9437fn markdown_inline_code_end(bytes: &[u8], index: usize, reach: &mut Reach) -> usize {
9438    let run = count_run(bytes, index, b'`');
9439    let mut cursor = index + run;
9440    while cursor < bytes.len() {
9441        if bytes[cursor] == b'`' {
9442            let next = count_run(bytes, cursor, b'`');
9443            if next == run {
9444                return cursor + next;
9445            }
9446            cursor += next;
9447        } else {
9448            cursor += 1;
9449        }
9450    }
9451    /* INVARIANT: The failed probe rewinds to just after the opener. A later
9452     * edit can turn a backtick run beyond an intervening line into the closer,
9453     * so those line starts are not safe incremental restart points yet. */
9454    reach.end_of(bytes);
9455    index + run
9456}
9457
9458/// Leading CommonMark indentation in display columns. Tabs advance to the
9459/// next four-column stop; callers only need to distinguish at most three
9460/// columns from an indented code block, but returning the full width keeps the
9461/// helper useful for continuation lines as well.
9462fn markdown_indent(bytes: &[u8], mut index: usize, end: usize) -> (usize, usize) {
9463    let mut columns = 0usize;
9464    while index < end {
9465        match bytes[index] {
9466            b' ' => {
9467                columns += 1;
9468                index += 1;
9469            }
9470            b'\t' => {
9471                columns += 4 - columns % 4;
9472                index += 1;
9473            }
9474            _ => break,
9475        }
9476    }
9477    (index, columns)
9478}
9479
9480/// The language a fenced code block's info string names: the first word of
9481/// the string, with the braces of an R Markdown chunk header taken off —
9482/// `` ```{r} `` is R — resolved through the same spellings the CLI accepts.
9483fn markdown_fence_language(info: &[u8]) -> Option<Language> {
9484    let trimmed = info.trim_ascii();
9485    let mut word = trimmed;
9486    if word.first() == Some(&b'{') {
9487        let end = word.iter().position(|byte| *byte == b'}')?;
9488        word = &word[1..end];
9489    }
9490    let end = word
9491        .iter()
9492        .position(|byte| byte.is_ascii_whitespace() || *byte == b',')
9493        .unwrap_or(word.len());
9494    let word = &word[..end];
9495    if word.is_empty() {
9496        None
9497    } else {
9498        std::str::from_utf8(word).ok()?.parse().ok()
9499    }
9500}
9501
9502/// A byte a Perl word may carry.
9503fn is_perl_word_byte(byte: u8) -> bool {
9504    byte.is_ascii_alphanumeric() || matches!(byte, b'_')
9505}
9506
9507fn perl_at_line_start(bytes: &[u8], index: usize) -> bool {
9508    index == 0 || matches!(bytes.get(index.wrapping_sub(1)), Some(b'\r' | b'\n'))
9509}
9510
9511fn perl_marker_line(bytes: &[u8], index: usize, marker: &[u8]) -> bool {
9512    starts(bytes, index, marker)
9513        && bytes
9514            .get(index + marker.len())
9515            .is_none_or(|byte| byte.is_ascii_whitespace())
9516}
9517
9518fn perl_pod_directive(bytes: &[u8], index: usize, name: &[u8]) -> bool {
9519    bytes.get(index) == Some(&b'=')
9520        && starts(bytes, index + 1, name)
9521        && bytes
9522            .get(index + 1 + name.len())
9523            .is_none_or(|byte| byte.is_ascii_whitespace())
9524}
9525
9526/// Past a Perl scalar, array or hash variable, including the punctuation that
9527/// names special variables and the `#` in `$#array`.
9528fn perl_variable_end(bytes: &[u8], start: usize) -> usize {
9529    let mut index = start + 1;
9530    if index >= bytes.len() {
9531        return index;
9532    }
9533    if bytes[index] == b'{' {
9534        index += 1;
9535        while index < bytes.len() {
9536            if bytes[index] == b'\\' {
9537                index = (index + 2).min(bytes.len());
9538            } else if bytes[index] == b'}' {
9539                return index + 1;
9540            } else {
9541                index += 1;
9542            }
9543        }
9544        return index;
9545    }
9546    if bytes[index] == b'#'
9547        && bytes
9548            .get(index + 1)
9549            .is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
9550    {
9551        index += 2;
9552        while index < bytes.len() && is_perl_word_byte(bytes[index]) {
9553            index += 1;
9554        }
9555        return index;
9556    }
9557    if bytes[index] == b'^' {
9558        return (index + 2).min(bytes.len());
9559    }
9560    if bytes[index].is_ascii_digit() {
9561        index += 1;
9562        while index < bytes.len() && bytes[index].is_ascii_digit() {
9563            index += 1;
9564        }
9565        return index;
9566    }
9567    if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' {
9568        index += 1;
9569        while index < bytes.len() && is_perl_word_byte(bytes[index]) {
9570            index += 1;
9571        }
9572        return index;
9573    }
9574    /* INVARIANT: Perl's one-byte special names include quotes and comment-looking
9575     * punctuation (`$"`, `$'`, `$#` in its punctuation form). */
9576    (index + 1).min(bytes.len())
9577}
9578
9579fn perl_quoted_end(bytes: &[u8], start: usize) -> usize {
9580    let quote = bytes[start];
9581    let mut index = start + 1;
9582    while index < bytes.len() {
9583        if bytes[index] == b'\\' {
9584            index = (index + 2).min(bytes.len());
9585        } else if bytes[index] == quote {
9586            return index + 1;
9587        } else {
9588            index += 1;
9589        }
9590    }
9591    bytes.len()
9592}
9593
9594/// Whether the word at the front of a token leaves the next `/` a regex
9595/// opener: the functions and operators perl reads a term after.
9596fn perl_word_allows_regex(word: &[u8]) -> Option<bool> {
9597    matches!(
9598        word,
9599        b"return"
9600            | b"if"
9601            | b"unless"
9602            | b"while"
9603            | b"until"
9604            | b"for"
9605            | b"foreach"
9606            | b"and"
9607            | b"or"
9608            | b"not"
9609            | b"print"
9610            | b"printf"
9611            | b"say"
9612            | b"split"
9613            | b"grep"
9614            | b"map"
9615            | b"join"
9616            | b"sort"
9617            | b"push"
9618            | b"unshift"
9619            | b"pop"
9620            | b"shift"
9621            | b"splice"
9622            | b"index"
9623            | b"length"
9624            | b"substr"
9625            | b"chomp"
9626            | b"chop"
9627            | b"lc"
9628            | b"uc"
9629    )
9630    .then_some(true)
9631    .or(Some(false))
9632}
9633
9634fn perl_section_end_reach(
9635    bytes: &[u8],
9636    start: usize,
9637    delimiter: u8,
9638    reach: &mut Reach,
9639) -> Option<usize> {
9640    let close = match delimiter {
9641        b'(' => b')',
9642        b'[' => b']',
9643        b'{' => b'}',
9644        b'<' => b'>',
9645        other => other,
9646    };
9647    let mut depth = 1usize;
9648    let mut index = start + 1;
9649    while index < bytes.len() {
9650        reach.byte(index);
9651        if bytes[index] == b'\\' {
9652            reach.byte(index + 1);
9653            index = (index + 2).min(bytes.len());
9654            continue;
9655        }
9656        if delimiter != close && bytes[index] == delimiter {
9657            depth += 1;
9658        } else if bytes[index] == close {
9659            depth -= 1;
9660            index += 1;
9661            if depth == 0 {
9662                return Some(index);
9663            }
9664            continue;
9665        }
9666        index += 1;
9667    }
9668    reach.end_of(bytes);
9669    None
9670}
9671
9672/// The replacement part of `s///`, `tr///` or `y///` when both sections use
9673/// one unpaired delimiter: the first section's closing byte is also the
9674/// implicit opening boundary of the second section.
9675fn perl_unpaired_section_end_reach(
9676    bytes: &[u8],
9677    mut index: usize,
9678    delimiter: u8,
9679    reach: &mut Reach,
9680) -> Option<usize> {
9681    while index < bytes.len() {
9682        reach.byte(index);
9683        if bytes[index] == b'\\' {
9684            reach.byte(index + 1);
9685            index = (index + 2).min(bytes.len());
9686        } else if bytes[index] == delimiter {
9687            return Some(index + 1);
9688        } else {
9689            index += 1;
9690        }
9691    }
9692    reach.end_of(bytes);
9693    None
9694}
9695
9696fn perl_modifiers_end(bytes: &[u8], mut index: usize) -> usize {
9697    while bytes
9698        .get(index)
9699        .is_some_and(|byte| byte.is_ascii_alphabetic())
9700    {
9701        index += 1;
9702    }
9703    index
9704}
9705
9706struct PerlHeredocDeclaration {
9707    terminator: Vec<u8>,
9708    indented: bool,
9709}
9710
9711fn perl_heredoc_declaration(
9712    bytes: &[u8],
9713    operator: usize,
9714    header_end: usize,
9715) -> Option<(PerlHeredocDeclaration, usize)> {
9716    let mut index = operator + 2;
9717    while index < header_end && matches!(bytes[index], b' ' | b'\t' | 0x0b | 0x0c) {
9718        index += 1;
9719    }
9720    let indented = bytes.get(index) == Some(&b'~');
9721    if indented {
9722        index += 1;
9723        while index < header_end && matches!(bytes[index], b' ' | b'\t' | 0x0b | 0x0c) {
9724            index += 1;
9725        }
9726    }
9727    let (terminator, end) = if let Some(&quote @ (b'\'' | b'"' | b'`')) = bytes.get(index) {
9728        let content = index + 1;
9729        let relative = bytes[content..header_end]
9730            .iter()
9731            .position(|byte| *byte == quote)?;
9732        let end = content + relative;
9733        (bytes[content..end].to_vec(), end + 1)
9734    } else {
9735        if !bytes
9736            .get(index)
9737            .is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
9738        {
9739            return None;
9740        }
9741        let content = index;
9742        index += 1;
9743        while index < header_end && is_perl_word_byte(bytes[index]) {
9744            index += 1;
9745        }
9746        (bytes[content..index].to_vec(), index)
9747    };
9748    (!terminator.is_empty()).then_some((
9749        PerlHeredocDeclaration {
9750            terminator,
9751            indented,
9752        },
9753        end,
9754    ))
9755}
9756
9757#[derive(Clone, Debug)]
9758struct ParsedTagAttribute {
9759    name: std::ops::Range<usize>,
9760    value: Option<std::ops::Range<usize>>,
9761}
9762
9763/// Tokenize an HTML-like attribute list. In particular, a suffix of
9764/// `data-lang` is never a second attribute named `lang`.
9765fn parse_tag_attributes(attrs: &[u8]) -> Vec<ParsedTagAttribute> {
9766    let mut parsed = Vec::new();
9767    let mut index = 0usize;
9768    while index < attrs.len() {
9769        while index < attrs.len() && attrs[index].is_ascii_whitespace() {
9770            index += 1;
9771        }
9772        if index >= attrs.len() || attrs[index] == b'>' {
9773            break;
9774        }
9775        if attrs[index] == b'/' {
9776            index += 1;
9777            continue;
9778        }
9779        let name_start = index;
9780        while index < attrs.len()
9781            && !attrs[index].is_ascii_whitespace()
9782            && !matches!(attrs[index], b'=' | b'>' | b'/')
9783        {
9784            index += 1;
9785        }
9786        if index == name_start {
9787            index += 1;
9788            continue;
9789        }
9790        let name = name_start..index;
9791        while index < attrs.len() && attrs[index].is_ascii_whitespace() {
9792            index += 1;
9793        }
9794        let mut value = None;
9795        if attrs.get(index) == Some(&b'=') {
9796            index += 1;
9797            while index < attrs.len() && attrs[index].is_ascii_whitespace() {
9798                index += 1;
9799            }
9800            if let Some(&quote @ (b'"' | b'\'')) = attrs.get(index) {
9801                index += 1;
9802                let value_start = index;
9803                while index < attrs.len() && attrs[index] != quote {
9804                    index += 1;
9805                }
9806                value = Some(value_start..index);
9807                if index < attrs.len() {
9808                    index += 1;
9809                }
9810            } else {
9811                let value_start = index;
9812                if attrs.get(index) == Some(&b'{') {
9813                    let mut braces = 0usize;
9814                    while index < attrs.len() {
9815                        match attrs[index] {
9816                            b'{' => braces += 1,
9817                            b'}' => {
9818                                braces = braces.saturating_sub(1);
9819                                index += 1;
9820                                if braces == 0 {
9821                                    break;
9822                                }
9823                                continue;
9824                            }
9825                            _ => {}
9826                        }
9827                        index += 1;
9828                    }
9829                } else {
9830                    while index < attrs.len()
9831                        && !attrs[index].is_ascii_whitespace()
9832                        && attrs[index] != b'>'
9833                    {
9834                        index += 1;
9835                    }
9836                }
9837                value = Some(value_start..index);
9838            }
9839        }
9840        parsed.push(ParsedTagAttribute { name, value });
9841    }
9842    parsed
9843}
9844
9845fn tag_attr_value<'a>(attrs: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
9846    parse_tag_attributes(attrs)
9847        .into_iter()
9848        .find_map(|attribute| {
9849            (attrs[attribute.name.clone()].eq_ignore_ascii_case(name))
9850                .then(|| attribute.value.map(|value| &attrs[value]))
9851                .flatten()
9852        })
9853}
9854
9855/// Whether an attribute list carries `name` as a bare attribute, which is how
9856/// Vue's `v-pre` directive is written.
9857fn tag_has_attribute(attrs: &[u8], name: &[u8]) -> bool {
9858    parse_tag_attributes(attrs)
9859        .into_iter()
9860        .any(|attribute| attrs[attribute.name].eq_ignore_ascii_case(name))
9861}
9862
9863fn vue_directive_attribute(name: &[u8]) -> bool {
9864    name.starts_with(b"v-") || matches!(name.first(), Some(b':' | b'@' | b'#' | b'.'))
9865}
9866
9867/// The language a Vue `<script>` body is written in, from its `lang`
9868/// attribute; `None` for a `lang` this scanner has no rules for, which makes
9869/// the block opaque.
9870fn vue_script_language(lang: Option<&[u8]>) -> Option<(Language, Dialect)> {
9871    match lang.map(|value| value.to_ascii_lowercase()).as_deref() {
9872        None | Some(b"js" | b"javascript") => Some((Language::JavaScript, Dialect::Standard)),
9873        Some(b"jsx") => Some((Language::JavaScript, Dialect::Jsx)),
9874        Some(b"ts" | b"typescript") => Some((Language::TypeScript, Dialect::Standard)),
9875        Some(b"tsx") => Some((Language::TypeScript, Dialect::Tsx)),
9876        Some(_) => None,
9877    }
9878}
9879
9880/// The language a Vue or Svelte `<style>` body is written in, from its `lang`
9881/// attribute; the default is CSS, `scss` and the indented `sass` select the
9882/// SCSS dialect, and any other `lang` makes the block opaque.
9883fn vue_style_language(lang: Option<&[u8]>) -> Option<(Language, Dialect)> {
9884    match lang.map(|value| value.to_ascii_lowercase()).as_deref() {
9885        None | Some(b"css") => Some((Language::Css, Dialect::Standard)),
9886        Some(b"scss") => Some((Language::Css, Dialect::Scss)),
9887        Some(b"sass") => Some((Language::Css, Dialect::Sass)),
9888        Some(_) => None,
9889    }
9890}
9891
9892fn html_embedded_start(bytes: &[u8], start: usize) -> Option<(&'static [u8], Language)> {
9893    let rest = &bytes[start..];
9894    if starts_ascii_case(rest, b"<script") && tag_boundary(rest.get(7).copied()) {
9895        Some((b"script", Language::JavaScript))
9896    } else if starts_ascii_case(rest, b"<style") && tag_boundary(rest.get(6).copied()) {
9897        Some((b"style", Language::Css))
9898    } else {
9899        None
9900    }
9901}
9902
9903fn find_html_close(bytes: &[u8], content_start: usize, name: &[u8]) -> Option<usize> {
9904    let mut close = Vec::with_capacity(name.len() + 2);
9905    close.extend_from_slice(b"</");
9906    close.extend_from_slice(name);
9907    let mut cursor = content_start;
9908    while cursor + close.len() <= bytes.len() {
9909        let relative = find_ascii_case(&bytes[cursor..], &close)?;
9910        let candidate = cursor + relative;
9911        if tag_boundary(bytes.get(candidate + close.len()).copied()) {
9912            return Some(candidate);
9913        }
9914        cursor = candidate + close.len();
9915    }
9916    None
9917}
9918
9919/// The closing tag paired with an element whose body begins at
9920/// `content_start`, counting nested elements with the same name. Vue's
9921/// `v-pre` makes the complete subtree raw text, so the first textual `</name>`
9922/// is not sufficient when another `<name>` is nested inside it.
9923fn find_balanced_html_close(bytes: &[u8], content_start: usize, name: &[u8]) -> Option<usize> {
9924    let mut index = content_start;
9925    let mut depth = 1usize;
9926    while index < bytes.len() {
9927        let relative = memchr(b'<', &bytes[index..])?;
9928        let tag = index + relative;
9929        if starts(bytes, tag, b"<!--") {
9930            index = find_subslice(&bytes[tag + 4..], b"-->")
9931                .map_or(bytes.len(), |offset| tag + 4 + offset + 3);
9932            continue;
9933        }
9934        let closing = bytes.get(tag + 1) == Some(&b'/');
9935        let name_start = tag + if closing { 2 } else { 1 };
9936        if name_start + name.len() <= bytes.len()
9937            && bytes[name_start..name_start + name.len()].eq_ignore_ascii_case(name)
9938            && tag_boundary(bytes.get(name_start + name.len()).copied())
9939        {
9940            let tag_end = html_tag_end(bytes, tag)?;
9941            if closing {
9942                depth -= 1;
9943                if depth == 0 {
9944                    return Some(tag);
9945                }
9946            } else {
9947                let mut before = tag_end.saturating_sub(1);
9948                while before > tag && bytes[before - 1].is_ascii_whitespace() {
9949                    before -= 1;
9950                }
9951                if before == tag || bytes[before - 1] != b'/' {
9952                    depth += 1;
9953                }
9954            }
9955            index = tag_end;
9956        } else {
9957            index = tag + 1;
9958        }
9959    }
9960    None
9961}
9962
9963/// The offset PHP mode begins at when an opening tag starts at `index`, or
9964/// `None` when none does.
9965///
9966/// `<?php` is matched without regard to case and has to be followed by white
9967/// space or the end of the file (`zend_language_scanner.l`:
9968/// `"<?php"([ \t]|{NEWLINE})`), so `<?phpinfo()` is inline text. `<?=` is the
9969/// short echo tag and needs nothing behind it. A bare `<?` opens nothing,
9970/// because `short_open_tag` is off by default — which is what leaves `<?xml`
9971/// an XML declaration in the output rather than the start of a program.
9972fn php_open_tag(bytes: &[u8], index: usize) -> Option<usize> {
9973    if starts(bytes, index, b"<?=") {
9974        return Some(index + 3);
9975    }
9976    let rest = bytes.get(index..)?;
9977    if starts_ascii_case(rest, b"<?php")
9978        && rest
9979            .get(5)
9980            .is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
9981    {
9982        return Some(index + 5);
9983    }
9984    None
9985}
9986
9987/// Where a PHP `//` or `#` comment ends: at the line break, or at a closing
9988/// tag, whichever comes first (PHP manual, Comments — "the closing tag breaks
9989/// out of PHP mode"). The `?>` is not part of the comment.
9990fn php_line_comment_end(bytes: &[u8], mut index: usize) -> usize {
9991    while index < bytes.len()
9992        && !matches!(bytes[index], b'\r' | b'\n')
9993        && !starts(bytes, index, b"?>")
9994    {
9995        index += 1;
9996    }
9997    index
9998}
9999
10000/// The kind of a PHP block comment.
10001///
10002/// The tokenizer makes a documentation comment of `/**` only when white space
10003/// follows it — its rule is `"/*"|"/**"{WHITESPACE}`, and the longer
10004/// alternative is what sets `T_DOC_COMMENT` — so `/**/` and `/**text*/` are
10005/// ordinary block comments. `/*!` is Doxygen's marker and means nothing to
10006/// PHP's own tooling, so it is an ordinary comment too.
10007fn php_block_kind(bytes: &[u8], index: usize) -> CommentKind {
10008    if starts(bytes, index, b"/**")
10009        && bytes
10010            .get(index + 3)
10011            .is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
10012    {
10013        CommentKind::DocBlock
10014    } else {
10015        CommentKind::Block
10016    }
10017}
10018
10019/// The offset just past the `}` that closes the interpolation opening at
10020/// `brace`, or the end of the file when none does.
10021///
10022/// The complex syntax `{$...}` holds a PHP expression, which the engine lexes
10023/// as ordinary code. This balances its braces instead, skipping over the two
10024/// things inside one that can carry a brace of their own — a nested string and
10025/// a comment — so `"{$a['}']}"` ends where PHP ends it. Nothing else in an
10026/// expression can, which is what makes the count right rather than merely
10027/// close.
10028///
10029/// What it does *not* do is report the comment it skipped: reading one out of a
10030/// string would mean running the whole lexer inside one, and v1 leaves those
10031/// bytes alone instead.
10032fn php_interpolation_end(bytes: &[u8], brace: usize) -> usize {
10033    let mut depth = 1usize;
10034    let mut index = brace + 1;
10035    while index < bytes.len() {
10036        match bytes[index] {
10037            b'{' => depth += 1,
10038            b'}' => {
10039                depth -= 1;
10040                if depth == 0 {
10041                    return index + 1;
10042                }
10043            }
10044            quote @ (b'\'' | b'"' | b'`') => {
10045                index += 1;
10046                while index < bytes.len() && bytes[index] != quote {
10047                    index = if bytes[index] == b'\\' {
10048                        (index + 2).min(bytes.len())
10049                    } else {
10050                        index + 1
10051                    };
10052                }
10053            }
10054            b'/' if starts(bytes, index, b"/*") => {
10055                index = block_end(bytes, index, b"/*", b"*/", false).0;
10056                continue;
10057            }
10058            b'/' if starts(bytes, index, b"//") => {
10059                index = php_line_comment_end(bytes, index + 2);
10060                continue;
10061            }
10062            b'#' if bytes.get(index + 1) != Some(&b'[') => {
10063                index = php_line_comment_end(bytes, index + 1);
10064                continue;
10065            }
10066            _ => {}
10067        }
10068        index += 1;
10069    }
10070    index.min(bytes.len())
10071}
10072
10073/// Whether `byte` may open a PHP label: a letter, `_`, or any byte from `0x80`
10074/// up (PHP manual, Variables — the label grammar is byte-oriented and takes
10075/// the whole upper half of the range).
10076fn php_label_start(byte: u8) -> bool {
10077    byte.is_ascii_alphabetic() || byte == b'_' || byte >= 0x80
10078}
10079
10080/// Whether `byte` may continue a PHP label: [`php_label_start`] and the
10081/// digits.
10082fn php_label_continue(byte: u8) -> bool {
10083    php_label_start(byte) || byte.is_ascii_digit()
10084}
10085
10086/// The label, the offset its body starts at, and whether it is a nowdoc, for
10087/// the heredoc header opening at `start`; `None` when those three bytes head
10088/// no header.
10089///
10090/// The header is `<<<`, blanks, the label — bare, or quoted with `'` for a
10091/// nowdoc or `"` for a heredoc — and then the line break, with nothing else
10092/// allowed in between (`zend_language_scanner.l`:
10093/// `"<<<"{TABS_AND_SPACES}({LABEL}|(['"]{LABEL}['"])){NEWLINE}`). The body
10094/// begins on the next line.
10095fn php_heredoc_header(bytes: &[u8], start: usize) -> Option<(&[u8], usize, bool)> {
10096    let mut cursor = start + 3;
10097    while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
10098        cursor += 1;
10099    }
10100    let quote = match bytes.get(cursor) {
10101        Some(byte @ (b'\'' | b'"')) => Some(*byte),
10102        _ => None,
10103    };
10104    if quote.is_some() {
10105        cursor += 1;
10106    }
10107    let label_start = cursor;
10108    if !bytes.get(cursor).is_some_and(|byte| php_label_start(*byte)) {
10109        return None;
10110    }
10111    while bytes
10112        .get(cursor)
10113        .is_some_and(|byte| php_label_continue(*byte))
10114    {
10115        cursor += 1;
10116    }
10117    let label = &bytes[label_start..cursor];
10118    if let Some(quote) = quote {
10119        if bytes.get(cursor) != Some(&quote) {
10120            return None;
10121        }
10122        cursor += 1;
10123    }
10124    if !matches!(bytes.get(cursor), Some(b'\r' | b'\n')) {
10125        return None;
10126    }
10127    Some((label, consume_newline(bytes, cursor), quote == Some(b'\'')))
10128}
10129
10130/// The offset just past the closing label of the body starting at `index`, or
10131/// `None` when no line closes it.
10132///
10133/// Since PHP 7.3 the closing label may be indented by blanks and may be
10134/// followed by anything that cannot continue a label — `;`, `,`, `)`, an
10135/// operator, the line break, or the end of the file (PHP manual, Heredoc
10136/// text). A byte that *can* continue one leaves the line ordinary body, which
10137/// is what keeps `EOTX` from ending an `EOT`.
10138fn php_heredoc_end(bytes: &[u8], mut index: usize, label: &[u8]) -> Option<usize> {
10139    loop {
10140        let mut cursor = index;
10141        while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
10142            cursor += 1;
10143        }
10144        if starts(bytes, cursor, label)
10145            && !bytes
10146                .get(cursor + label.len())
10147                .is_some_and(|byte| php_label_continue(*byte))
10148        {
10149            return Some(cursor + label.len());
10150        }
10151        let end = line_end(bytes, index);
10152        if end >= bytes.len() {
10153            return None;
10154        }
10155        index = consume_newline(bytes, end);
10156    }
10157}
10158
10159/// Where a Ruby token may begin, which is what decides whether `/`, `%`, `?`
10160/// and `<<` open a literal or are the operator spelled with the same byte.
10161///
10162/// This is Ruby's own `lex_state` folded onto the three answers those four
10163/// questions read out of it: `IS_BEG()`, `IS_END()`, and the `IS_ARG()` in
10164/// between, where a bare word may be a method about to take a command argument
10165/// and only the spacing around the byte says which. Ruby's lexer tells a local
10166/// variable from a method name by the symbol table it is building, which a
10167/// scanner has not got, so every bare word lands in [`Self::Argument`]: `a /b/`
10168/// is read as a regular expression where Ruby, knowing `a` to be a variable,
10169/// reads a division. That reading keeps more bytes inside a literal than the
10170/// parser would, which loses no comment that is one.
10171#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10172enum RubyState {
10173    /// A value is expected: the start of a file, or just past an operator, a
10174    /// comma, an opening bracket, a keyword that opens an expression, or a
10175    /// line break.
10176    Begin,
10177    /// Just past a bare word, which may be a method taking a command argument.
10178    Argument,
10179    /// Just past an operand: a literal, a `)`, `]` or `}`, a variable, or one
10180    /// of the keywords that finishes an expression.
10181    End,
10182    /// Just past `alias` or `undef`, where Ruby stands in
10183    /// `EXPR_FNAME|EXPR_FITEM`. It answers every question [`Self::End`]
10184    /// answers, and one differently: `parse_percent` opens a symbol literal on
10185    /// `%s` there, spacing or none.
10186    Fname,
10187}
10188
10189/// The header of one Ruby `%` literal: what closes it, whether it nests, and
10190/// whether it interpolates.
10191#[derive(Clone, Copy)]
10192struct RubyPercent {
10193    /// The letter naming the form, or `Q` for the bare `%(...)`.
10194    form: u8,
10195    /// The opening delimiter, equal to `close` where the delimiter does not
10196    /// pair and so does not nest.
10197    open: u8,
10198    /// The delimiter that ends the literal.
10199    close: u8,
10200    /// The offset of the first byte of the content.
10201    content: usize,
10202    /// Whether a `#{ ... }` inside it is an expression.
10203    interpolates: bool,
10204}
10205
10206/// One Ruby here document, as the lines under the one that opened it need it.
10207struct RubyHeredoc {
10208    /// Where the `<<` sits, which is what an unterminated body is reported
10209    /// from.
10210    operator: usize,
10211    /// The terminator word, without the quotes that may have written it.
10212    label: Vec<u8>,
10213    /// `<<-` and `<<~` let the terminator line be indented; a bare `<<` wants
10214    /// it at column zero.
10215    indented: bool,
10216    /// A single-quoted terminator turns interpolation off; every other form
10217    /// leaves it on.
10218    interpolates: bool,
10219}
10220
10221/// Ruby's `is_identchar` for the first byte of a name: a letter, `_`, or the
10222/// lead byte of a character outside ASCII, which Ruby takes as a name byte
10223/// wholesale.
10224fn ruby_identifier_start(byte: u8) -> bool {
10225    byte.is_ascii_alphabetic() || byte == b'_' || !byte.is_ascii()
10226}
10227
10228/// The same for every byte after the first, which a digit may also be.
10229fn ruby_identifier_continue(byte: u8) -> bool {
10230    ruby_identifier_start(byte) || byte.is_ascii_digit()
10231}
10232
10233/// White space that separates Ruby tokens without ending a line. The vertical
10234/// tab and the form feed are in it, as `rb_isspace` has them; the two line
10235/// terminators are handled on their own, because they finish a statement.
10236fn ruby_is_space(byte: u8) -> bool {
10237    matches!(byte, b' ' | b'\t' | 0x0b | 0x0c)
10238}
10239
10240/// Past the name at `index`.
10241fn ruby_identifier_end(bytes: &[u8], mut index: usize) -> usize {
10242    while bytes
10243        .get(index)
10244        .is_some_and(|byte| ruby_identifier_continue(*byte))
10245    {
10246        index += 1;
10247    }
10248    index
10249}
10250
10251/// Past the bare word at `index`, including the `?` or `!` that may end a
10252/// method name.
10253///
10254/// Ruby's lexer takes a trailing `?` or `!` into the name unless a `=` follows
10255/// it, which is what tells `x.empty?` from the ternary `x ? y : z` — and, in
10256/// the other direction, keeps `a != b` a comparison rather than a call of a
10257/// method named `a!`.
10258fn ruby_word_end(bytes: &[u8], index: usize) -> usize {
10259    let end = ruby_identifier_end(bytes, index + 1);
10260    if matches!(bytes.get(end), Some(b'?' | b'!')) && bytes.get(end + 1) != Some(&b'=') {
10261        end + 1
10262    } else {
10263        end
10264    }
10265}
10266
10267/// Past the numeric literal at `index`.
10268///
10269/// The digits, the `_` separators, the radix letters and the `r` and `i`
10270/// suffixes are one run of name bytes; a `.` joins the run only when a digit
10271/// follows it, which is what keeps `1.times` a method call.
10272fn ruby_number_end(bytes: &[u8], mut index: usize) -> usize {
10273    loop {
10274        index = ruby_identifier_end(bytes, index);
10275        if bytes.get(index) == Some(&b'.') && bytes.get(index + 1).is_some_and(u8::is_ascii_digit) {
10276            index += 1;
10277            continue;
10278        }
10279        return index;
10280    }
10281}
10282
10283/// The state a bare word leaves the lexer in.
10284///
10285/// The three lists are Ruby's own keyword table folded onto [`RubyState`].
10286/// `def`, `alias` and `undef` refuse a literal because the name that follows
10287/// one may be spelled `/` or `%` — `def /(other)` defines division; `class`
10288/// and `module` are in the first list for the mirror-image reason, that `class
10289/// <<self` is a singleton class and never a here document. `alias` and `undef`
10290/// take [`RubyState::Fname`] rather than [`RubyState::End`] because they leave
10291/// Ruby in `EXPR_FNAME|EXPR_FITEM`, which is one answer wider. `super`,
10292/// `yield`, `not` and `defined?` are in none of the three, which leaves them
10293/// where Ruby has them: a command that may take an argument.
10294///
10295/// [`RubyState::End`] is a coarser answer than either reason asked for, and it
10296/// is worth naming what the coarseness costs. What is measured is this table
10297/// against `Ripper.lex`, keyword by keyword: `End` answers all four of the
10298/// state machine's questions at once, so it also decides `/`, `%` and `?`, and
10299/// the four readings below are the ones where the answer it gives is not
10300/// Ruby's.
10301///
10302/// * After `def`, `End` is Ruby's own answer for `/` and for every percent
10303///   literal, which `EXPR_FNAME` reads as the method names they are. `def` has
10304///   no exception: `def%s(foo)` is `on_op "%"` then `on_ident "s"` to Ruby
10305///   3.3.12, the modulo this table reads as well.
10306/// * After `class` and `module`, `End` is not Ruby's answer either:
10307///   `EXPR_CLASS` expects a value, so Ruby opens a literal there. `class
10308///   /x # c/` is one regular expression to Ruby 3.3.12 (`Ripper.lex` gives
10309///   `on_regexp_beg`) and a division with a comment behind it here.
10310/// * `?` diverges after all five — `class ?# x` and `def ?# x` are `on_CHAR
10311///   "?#"` to Ruby and a comment opener here.
10312///
10313/// Both of those are spellings Ruby itself refuses: `class` and `module` take a
10314/// constant, a `::` or a `<<` in any program that parses, and `def ?# x` is a
10315/// syntax error, so both are reachable only where the scan is already reading a
10316/// broken file, and buying them back with a state of their own — one that keeps
10317/// the literal readings and refuses only the here document — would add a state
10318/// to the machine for no program that runs.
10319///
10320/// [`RubyState::Fname`] is the state that *is* worth its keep, and `%s` after
10321/// `alias` or `undef` is what buys it: `alias%s(baz # x) %s(bar)` is a file
10322/// Ruby runs, and `Ripper.lex` under Ruby 3.3.12 gives `on_symbeg "%s("` in
10323/// state `FNAME|FITEM` for both names with `baz # x` an `on_tstring_content`.
10324/// Reading that `#` as a comment would remove bytes Ruby has inside a symbol,
10325/// which is the one direction this scanner may not take. The rule is about the
10326/// delimiter and not only the state — `alias` goes on refusing `/`, `<<`, `%w`
10327/// and `%q` in the same breath it accepts `%s` (`alias%w[a]` is `on_op "%"` to
10328/// Ripper) — so [`ruby_percent_opens`] asks it rather than
10329/// [`ruby_literal_opens`] alone.
10330///
10331/// `<<` itself is not an entry on that list, and what keeps it off is not
10332/// this function. A header [`RubyState::End`] refuses is a shift, which reads
10333/// no fewer bytes into a literal than Ruby does; a header it allows queues a
10334/// body, and
10335/// [`Scanner::scan_ruby_code`] queues it for the physical line the header
10336/// stands on wherever that header was written — before an interpolation, inside
10337/// one, inside a nested one, or inside an interpolation on another here
10338/// document's body line. A queue that stopped at an interpolation boundary
10339/// would read a whole here document body as code, which would be one more
10340/// reading that takes fewer bytes into a literal than Ruby does, and it is the
10341/// one the corpus cases named `ruby-heredoc-*-interpolation` hold shut.
10342///
10343/// NOTE: `End` is as far as that first half reaches, and `def`, `alias` and
10344/// `undef` are where it stops. MRI's `parser_yylex` tries `heredoc_identifier`
10345/// on `<<` unless the lexer state is `EXPR_DOT|EXPR_CLASS`, unless `IS_END()`,
10346/// or unless it is an `IS_ARG()` with no white space in front — and
10347/// `EXPR_FNAME` is in none of those three, so the state `def` leaves Ruby in,
10348/// and the `EXPR_FNAME|EXPR_FITEM` that `alias` and `undef` do, still reach a
10349/// here document. This table answers `End` for `def` and [`RubyState::Fname`]
10350/// for the other two, and both refuse the header: `def <<EOS` is a shift here
10351/// and a here-document header to MRI, which is the direction — fewer bytes into
10352/// a literal than Ruby takes — that the rest of this file refuses. What bounds
10353/// it is what bounds the `class` and `module` readings above: no program that
10354/// runs is written that way, because `def` is followed by a method name and
10355/// `<<EOS` is not one. `class` and `module` are not part of this exception at
10356/// all — `EXPR_CLASS` is named in that guard, which is what makes `class
10357/// <<self` a singleton class rather than a here document. Unlike every other
10358/// reading in this comment, the `def <<EOS` one is argued from MRI's `parse.y`
10359/// alone and has not been put to `Ripper.lex`: no Ruby 3.3 was available where
10360/// it was written.
10361fn ruby_state_after_word(token: &[u8]) -> RubyState {
10362    match token {
10363        b"end" | b"self" | b"nil" | b"true" | b"false" | b"redo" | b"retry" | b"__FILE__"
10364        | b"__LINE__" | b"__ENCODING__" | b"def" | b"class" | b"module" => RubyState::End,
10365        b"alias" | b"undef" => RubyState::Fname,
10366        b"if" | b"unless" | b"while" | b"until" | b"case" | b"when" | b"in" | b"and" | b"or"
10367        | b"return" | b"break" | b"next" | b"then" | b"do" | b"else" | b"elsif" | b"begin"
10368        | b"ensure" | b"rescue" | b"for" => RubyState::Begin,
10369        _ => RubyState::Argument,
10370    }
10371}
10372
10373/// Whether the delimiter at `index` opens a literal rather than being the
10374/// operator spelled with the same byte.
10375///
10376/// Ruby's rule for both `/` and `%` (`parse_slash`, `parse_percent`) is one
10377/// rule: where a value is expected the byte always opens a literal; after an
10378/// operand it never does; and in between it opens one exactly when white space
10379/// stands before it and none behind it, which is what tells the command
10380/// argument of `puts /x/` from the division in `a / b`. `/=` and `%=` are
10381/// recognised before that last test, so an assignment operator is never read as
10382/// a literal outside value position.
10383fn ruby_literal_opens(state: RubyState, space_seen: bool, bytes: &[u8], index: usize) -> bool {
10384    match state {
10385        RubyState::Begin => true,
10386        RubyState::End | RubyState::Fname => false,
10387        RubyState::Argument => {
10388            space_seen
10389                && bytes.get(index + 1).is_some_and(|byte| {
10390                    *byte != b'=' && !ruby_is_space(*byte) && !matches!(byte, b'\r' | b'\n')
10391                })
10392        }
10393    }
10394}
10395
10396/// Whether a `<<` where the lexer stands may open a here document.
10397///
10398/// Ruby's `parser_yylex`: never after an operand, and after a bare word only
10399/// when white space stands in front of it — which is why `a << b` is a shift
10400/// and `a <<b` is the here document that spacing exists to avoid.
10401fn ruby_heredoc_may_open(state: RubyState, space_seen: bool) -> bool {
10402    match state {
10403        RubyState::Begin => true,
10404        RubyState::Argument => space_seen,
10405        RubyState::End | RubyState::Fname => false,
10406    }
10407}
10408
10409/// Whether the `%` at `index` opens the percent literal `form` names.
10410///
10411/// [`ruby_literal_opens`] answers it everywhere but one: `parse_percent` tests
10412/// `IS_lex_state(EXPR_FNAME | EXPR_FITEM)` before it reaches the spacing rule
10413/// and opens a symbol literal on `%s` there, so `alias%s(a)` and `alias %s(a)`
10414/// open one alike. Only `s` does; `%w`, `%q` and the rest fall through to the
10415/// ordinary answer, which is `false` in that state.
10416fn ruby_percent_opens(
10417    state: RubyState,
10418    space_seen: bool,
10419    bytes: &[u8],
10420    index: usize,
10421    form: u8,
10422) -> bool {
10423    (state == RubyState::Fname && form == b's')
10424        || ruby_literal_opens(state, space_seen, bytes, index)
10425}
10426
10427/// Whether `index` is the first byte of a line, which is where Ruby's two
10428/// column-zero markers — `=begin` and `__END__` — are recognised.
10429///
10430/// A byte order mark is consumed before the first line is read, so the byte
10431/// behind one still opens the first line. That clause is asked only of a scan
10432/// starting at offset zero, because the first byte a suffix scan is handed
10433/// opens a line whatever it is, and a mark cannot stand there in the document
10434/// the suffix came from.
10435fn ruby_at_line_start(bytes: &[u8], index: usize, offset: usize) -> bool {
10436    if index == 0 || (offset == 0 && index == byte_order_mark_width(bytes)) {
10437        return true;
10438    }
10439    match bytes[index - 1] {
10440        b'\n' => true,
10441        b'\r' => bytes.get(index) != Some(&b'\n'),
10442        _ => false,
10443    }
10444}
10445
10446/// Whether a `=begin` at `index` opens an embedded document.
10447///
10448/// Ruby's `word_match_p`: the word ends at white space or at the end of the
10449/// file, so `=beginner` is the `=` operator and a name.
10450fn ruby_embedded_document(bytes: &[u8], index: usize) -> bool {
10451    starts(bytes, index, b"=begin") && ruby_word_boundary(bytes, index + b"=begin".len())
10452}
10453
10454/// Where the embedded document opened at `start` ends, and whether its `=end`
10455/// was there at all.
10456///
10457/// The document runs to the end of the `=end` line, whose remaining bytes Ruby
10458/// skips along with the rest of it, and both markers stand at column zero.
10459fn ruby_embedded_document_end(bytes: &[u8], start: usize) -> (usize, bool) {
10460    let mut index = line_end(bytes, start);
10461    while index < bytes.len() {
10462        index = consume_newline(bytes, index);
10463        if starts(bytes, index, b"=end") && ruby_word_boundary(bytes, index + b"=end".len()) {
10464            return (line_end(bytes, index + b"=end".len()), true);
10465        }
10466        index = line_end(bytes, index);
10467    }
10468    (bytes.len(), false)
10469}
10470
10471/// Whether `index` is past the end of a word: white space, a line break, or the
10472/// end of the file.
10473fn ruby_word_boundary(bytes: &[u8], index: usize) -> bool {
10474    bytes
10475        .get(index)
10476        .is_none_or(|byte| ruby_is_space(*byte) || matches!(byte, b'\r' | b'\n'))
10477}
10478
10479/// Whether a `__END__` alone on its line begins the DATA section at `index`.
10480///
10481/// Ruby's `whole_match_p`: the marker is the whole line, so `__END__ x` is an
10482/// ordinary name and the source runs on past it.
10483fn ruby_data_marker(bytes: &[u8], index: usize) -> bool {
10484    starts(bytes, index, b"__END__")
10485        && matches!(
10486            bytes.get(index + b"__END__".len()),
10487            None | Some(b'\r' | b'\n')
10488        )
10489}
10490
10491/// Whether the byte behind a `:` makes it the head of a symbol rather than the
10492/// operator of a ternary or the colon of a hash label.
10493fn ruby_symbol_head(byte: u8) -> bool {
10494    ruby_identifier_start(byte) || matches!(byte, b'@' | b'$') || ruby_symbol_operator(byte)
10495}
10496
10497/// The characters an operator method is spelled with, which is how a symbol
10498/// naming one — `:<=>`, `:[]=`, `:+@` — is written.
10499fn ruby_symbol_operator(byte: u8) -> bool {
10500    matches!(
10501        byte,
10502        b'+' | b'-' | b'*' | b'/' | b'%' | b'<' | b'>' | b'=' | b'!' | b'~' | b'^' | b'&' | b'|'
10503    ) || matches!(byte, b'[' | b']' | b'@')
10504}
10505
10506/// Past the symbol a `:` at `index` opens.
10507///
10508/// A symbol is a name — with the `@`, `@@` or `$` of a variable in front of it
10509/// where one is meant — or one of the operator methods, which is read here as
10510/// the run of characters those are spelled with rather than as a table of
10511/// them: a run that names no method is a syntax error either way, and reading
10512/// it as one symbol keeps the byte after it out of the literal path.
10513fn ruby_symbol_end(bytes: &[u8], index: usize) -> usize {
10514    let mut cursor = index + 1;
10515    match bytes.get(cursor) {
10516        Some(b'$') => return ruby_global_end(bytes, cursor),
10517        Some(b'@') => return ruby_at_variable_end(bytes, cursor),
10518        Some(byte) if ruby_identifier_start(*byte) => return ruby_word_end(bytes, cursor),
10519        _ => {}
10520    }
10521    while bytes
10522        .get(cursor)
10523        .is_some_and(|byte| ruby_symbol_operator(*byte))
10524    {
10525        cursor += 1;
10526    }
10527    cursor
10528}
10529
10530/// Past the global variable a `$` at `index` opens, or past the `$` alone.
10531///
10532/// Ruby's `parse_gvar`: a name, a digit run, `-` and one character, or one of
10533/// the punctuation names. `$"` and `$'` are two of those names, which is what
10534/// keeps the quote in either from opening a string, and `$/` and `$\` two more.
10535/// `#` is not one of them — the reference refuses that spelling outright — so
10536/// `$#` is a `$` on its own, and the byte behind it opens the comment it opens
10537/// everywhere else in the language.
10538fn ruby_global_end(bytes: &[u8], index: usize) -> usize {
10539    let Some(byte) = bytes.get(index + 1).copied() else {
10540        return index + 1;
10541    };
10542    if ruby_identifier_start(byte) {
10543        return ruby_identifier_end(bytes, index + 2);
10544    }
10545    if byte.is_ascii_digit() {
10546        let mut end = index + 2;
10547        while bytes.get(end).is_some_and(u8::is_ascii_digit) {
10548            end += 1;
10549        }
10550        return end;
10551    }
10552    if byte == b'-' {
10553        return (index + 3).min(bytes.len());
10554    }
10555    if matches!(
10556        byte,
10557        b'~' | b'*' | b'$' | b'?' | b'!' | b'@' | b'/' | b'\\' | b';' | b',' | b'.' | b'='
10558    ) || matches!(byte, b':' | b'<' | b'>' | b'"' | b'&' | b'`' | b'\'' | b'+')
10559    {
10560        return index + 2;
10561    }
10562    index + 1
10563}
10564
10565/// Past the instance or class variable a `@` at `index` opens, or past the `@`
10566/// alone where no name follows it.
10567fn ruby_at_variable_end(bytes: &[u8], index: usize) -> usize {
10568    let mut cursor = index + 1;
10569    if bytes.get(cursor) == Some(&b'@') {
10570        cursor += 1;
10571    }
10572    ruby_identifier_end(bytes, cursor)
10573}
10574
10575/// The end of the character literal a `?` at `question` opens, or `None` when
10576/// those bytes are the ternary operator.
10577///
10578/// Ruby's `parse_qmark`: white space behind the `?` makes it the operator; a
10579/// character outside ASCII is a literal whole; an ASCII letter, digit or `_`
10580/// with another name byte behind it is the operator again, which is what keeps
10581/// `a ?bc : d` a ternary; and everything else — an escape, or one punctuation
10582/// byte — is a literal.
10583fn ruby_character_literal_end(bytes: &[u8], question: usize) -> Option<usize> {
10584    let index = question + 1;
10585    let byte = *bytes.get(index)?;
10586    if ruby_is_space(byte) || matches!(byte, b'\r' | b'\n') {
10587        return None;
10588    }
10589    if !byte.is_ascii() {
10590        return Some((index + ruby_character_width(byte)).min(bytes.len()));
10591    }
10592    if byte == b'\\' {
10593        /* NOTE: `\u{...}` is the one escape whose length the bytes after it
10594         * decide; every other one ends within a byte or two of name bytes,
10595         * which are read as the name they look like and cannot open anything. */
10596        if bytes.get(index + 1) == Some(&b'u') && bytes.get(index + 2) == Some(&b'{') {
10597            let mut cursor = index + 3;
10598            while bytes.get(cursor).is_some_and(|byte| *byte != b'}') {
10599                cursor += 1;
10600            }
10601            return Some((cursor + 1).min(bytes.len()));
10602        }
10603        return Some((index + 2).min(bytes.len()));
10604    }
10605    if ruby_identifier_continue(byte)
10606        && bytes
10607            .get(index + 1)
10608            .is_some_and(|next| ruby_identifier_continue(*next))
10609    {
10610        return None;
10611    }
10612    Some(index + 1)
10613}
10614
10615/// How many bytes the UTF-8 sequence headed by `byte` takes, or one where it
10616/// heads none. A trailing byte read on its own is a name byte here, which opens
10617/// nothing, so a miscount costs nothing but a token boundary.
10618fn ruby_character_width(byte: u8) -> usize {
10619    match byte {
10620        0xc0..=0xdf => 2,
10621        0xe0..=0xef => 3,
10622        0xf0..=0xf7 => 4,
10623        _ => 1,
10624    }
10625}
10626
10627/// Past the option letters that may follow a regular expression — `i`, `m`,
10628/// `x`, `o`, `n`, `e`, `s`, `u`.
10629///
10630/// They are read as a run of ASCII letters rather than as that set: a letter
10631/// that is not an option is a syntax error either way, and taking it here
10632/// leaves the lexer where the letters ended rather than in the middle of them.
10633fn ruby_regexp_flags_end(bytes: &[u8], mut index: usize) -> usize {
10634    while bytes.get(index).is_some_and(u8::is_ascii_alphabetic) {
10635        index += 1;
10636    }
10637    index
10638}
10639
10640/// The header of the `%` literal at `start`, or `None` when those bytes head
10641/// none and the `%` is the modulo operator.
10642///
10643/// Ruby's `parse_percent`: the byte after the `%` is the delimiter unless it is
10644/// alphanumeric, in which case it names the form and the byte after *that* is
10645/// the delimiter. A delimiter is any ASCII byte that is not alphanumeric, the
10646/// space of `% a ` included. `(`, `[`, `{` and `<` pair with their closer and
10647/// nest; every other delimiter closes with itself.
10648fn ruby_percent_header(bytes: &[u8], start: usize) -> Option<RubyPercent> {
10649    let first = *bytes.get(start + 1)?;
10650    let (form, delimiter, content) = if first.is_ascii_alphanumeric() {
10651        if !matches!(
10652            first,
10653            b'q' | b'Q' | b'w' | b'W' | b'i' | b'I' | b's' | b'r' | b'x'
10654        ) {
10655            return None;
10656        }
10657        (first, *bytes.get(start + 2)?, start + 3)
10658    } else {
10659        (b'Q', first, start + 2)
10660    };
10661    if delimiter.is_ascii_alphanumeric() || !delimiter.is_ascii() {
10662        return None;
10663    }
10664    let close = match delimiter {
10665        b'(' => b')',
10666        b'[' => b']',
10667        b'{' => b'}',
10668        b'<' => b'>',
10669        _ => delimiter,
10670    };
10671    Some(RubyPercent {
10672        form,
10673        open: delimiter,
10674        close,
10675        content,
10676        interpolates: matches!(form, b'Q' | b'W' | b'I' | b'r' | b'x'),
10677    })
10678}
10679
10680/// The here document a `<<` at `index` opens, and where its header ends, or
10681/// `None` when those two bytes open none.
10682///
10683/// Ruby's `heredoc_identifier`: an optional `-` or `~`, then a quoted
10684/// terminator or a bare word. The bare word is a run of `is_identchar` bytes
10685/// from its very first one, which is a wider set than a name may start with: a
10686/// digit is an identchar, so `<<2` is a here document terminated by a line
10687/// reading `2` and `<<9x` one terminated by `9x`. Refusing digits would read
10688/// the body as code, which is the one direction that invents a comment out of
10689/// bytes Ruby has inside a string, so they are taken. Whether the `<<` stands
10690/// where one may open at all is [`ruby_heredoc_may_open`]'s question, and it is
10691/// what still leaves `a[0] <<2` and `p 1 <<2` the shift they are. A quoted
10692/// terminator that runs past the end of its line opens nothing.
10693fn ruby_heredoc_header(bytes: &[u8], index: usize) -> Option<(RubyHeredoc, usize)> {
10694    let mut cursor = index + 2;
10695    let indented = matches!(bytes.get(cursor), Some(b'-' | b'~'));
10696    if indented {
10697        cursor += 1;
10698    }
10699    let quote = match bytes.get(cursor)? {
10700        b'\'' => Some(b'\''),
10701        b'"' => Some(b'"'),
10702        b'`' => Some(b'`'),
10703        byte if ruby_identifier_continue(*byte) => None,
10704        _ => return None,
10705    };
10706    let (label, end) = match quote {
10707        Some(quote) => {
10708            let start = cursor + 1;
10709            let mut end = start;
10710            loop {
10711                match bytes.get(end) {
10712                    Some(byte) if *byte == quote => break,
10713                    None | Some(b'\r' | b'\n') => return None,
10714                    Some(_) => end += 1,
10715                }
10716            }
10717            (bytes[start..end].to_vec(), end + 1)
10718        }
10719        None => {
10720            let end = ruby_identifier_end(bytes, cursor + 1);
10721            (bytes[cursor..end].to_vec(), end)
10722        }
10723    };
10724    Some((
10725        RubyHeredoc {
10726            operator: index,
10727            label,
10728            indented,
10729            interpolates: quote != Some(b'\''),
10730        },
10731        end,
10732    ))
10733}
10734
10735/// Whether the line beginning at `index` is `heredoc`'s terminator.
10736///
10737/// Ruby's `whole_match_p`: the terminator is the whole line, with leading white
10738/// space skipped only for the `<<-` and `<<~` forms.
10739fn ruby_heredoc_terminates(bytes: &[u8], index: usize, heredoc: &RubyHeredoc) -> bool {
10740    let mut probe = index;
10741    if heredoc.indented {
10742        while bytes.get(probe).is_some_and(|byte| ruby_is_space(*byte)) {
10743            probe += 1;
10744        }
10745    }
10746    starts(bytes, probe, &heredoc.label)
10747        && matches!(
10748            bytes.get(probe + heredoc.label.len()),
10749            None | Some(b'\r' | b'\n')
10750        )
10751}
10752
10753fn starts_ascii_case(haystack: &[u8], needle: &[u8]) -> bool {
10754    haystack
10755        .get(..needle.len())
10756        .is_some_and(|prefix| prefix.eq_ignore_ascii_case(needle))
10757}
10758fn find_ascii_case(haystack: &[u8], needle: &[u8]) -> Option<usize> {
10759    haystack
10760        .windows(needle.len())
10761        .position(|window| window.eq_ignore_ascii_case(needle))
10762}
10763fn tag_boundary(byte: Option<u8>) -> bool {
10764    byte.is_none_or(|byte| byte.is_ascii_whitespace() || matches!(byte, b'>' | b'/'))
10765}
10766
10767fn contains_line_splice(bytes: &[u8]) -> bool {
10768    let mut cursor = 0;
10769    while let Some(relative) = memchr(b'\\', &bytes[cursor..]) {
10770        let index = cursor + relative;
10771        if bytes.get(index + 1) == Some(&b'\n')
10772            || (bytes.get(index + 1) == Some(&b'\r') && bytes.get(index + 2) == Some(&b'\n'))
10773        {
10774            return true;
10775        }
10776        cursor = index + 1;
10777    }
10778    false
10779}
10780
10781fn next_c_family_trigger(
10782    bytes: &[u8],
10783    start: usize,
10784    language: Language,
10785    dialect: Dialect,
10786) -> Option<usize> {
10787    let remaining = bytes.get(start..)?;
10788    let primary = match language {
10789        Language::Go => remaining
10790            .iter()
10791            .position(|byte| matches!(byte, b'/' | b'"' | b'\'' | b'`')),
10792        /* NOTE: SCSS adds the `#` of its `#{ ... }` interpolation and the `u`
10793         * of its unquoted `url( ... )` to the bytes the scan jumps to, because
10794         * both open a region whose `//` is not a comment. */
10795        Language::Css if dialect == Dialect::Scss => remaining
10796            .iter()
10797            .position(|byte| matches!(byte, b'/' | b'"' | b'\'' | b'#' | b'u' | b'U')),
10798        _ => memchr3(b'/', b'"', b'\'', remaining),
10799    }?;
10800    Some(start + primary)
10801}
10802
10803/// A byte a CSS identifier may carry, which is what keeps `myurl(` from
10804/// reading as the `url(` function.
10805fn is_css_identifier_part(byte: u8) -> bool {
10806    byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')
10807}
10808
10809fn css_whitespace(byte: u8) -> bool {
10810    matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | 0x0c)
10811}
10812
10813fn line_start(bytes: &[u8], index: usize) -> usize {
10814    bytes[..index.min(bytes.len())]
10815        .iter()
10816        .rposition(|byte| matches!(byte, b'\r' | b'\n'))
10817        .map_or(0, |position| position + 1)
10818}
10819
10820fn sass_indent_width(bytes: &[u8]) -> usize {
10821    bytes.iter().fold(0, |column, byte| match byte {
10822        b'\t' => column + (8 - column % 8),
10823        _ => column + 1,
10824    })
10825}
10826
10827/// The interpolation threshold of a Kotlin string whose quote begins at
10828/// `start`. With no prefix the historic single-dollar rule applies; a run of
10829/// dollars immediately before the quote opts into multi-dollar interpolation.
10830fn kotlin_dollar_width(bytes: &[u8], start: usize) -> usize {
10831    let mut prefix = start;
10832    while prefix > 0 && bytes[prefix - 1] == b'$' {
10833        prefix -= 1;
10834    }
10835    (start - prefix).max(1)
10836}
10837
10838struct MappedBytes {
10839    bytes: Vec<u8>,
10840    origins: Vec<ByteSpan>,
10841    original_len: usize,
10842}
10843
10844impl MappedBytes {
10845    fn without_c_line_splices(source: &[u8]) -> Self {
10846        let mut bytes = Vec::with_capacity(source.len());
10847        let mut origins = Vec::with_capacity(source.len());
10848        let mut index = 0;
10849        while index < source.len() {
10850            if starts(source, index, b"\\\r\n") {
10851                index += 3;
10852                continue;
10853            }
10854            if starts(source, index, b"\\\n") {
10855                index += 2;
10856                continue;
10857            }
10858            bytes.push(source[index]);
10859            origins.push(ByteSpan::new(index, index + 1));
10860            index += 1;
10861        }
10862        Self {
10863            bytes,
10864            origins,
10865            original_len: source.len(),
10866        }
10867    }
10868
10869    fn java_unicode(source: &[u8]) -> (Self, Vec<ByteSpan>) {
10870        let mut bytes = Vec::with_capacity(source.len());
10871        let mut origins = Vec::with_capacity(source.len());
10872        let mut invalid = Vec::new();
10873        let mut index = 0;
10874        let mut slash_run = 0usize;
10875        let mut last_was_escape = false;
10876        while index < source.len() {
10877            let eligible = source[index] == b'\\' && (last_was_escape || slash_run & 1 == 0);
10878            if eligible {
10879                let mut cursor = index + 1;
10880                while source.get(cursor) == Some(&b'u') {
10881                    cursor += 1;
10882                }
10883                if cursor > index + 1 {
10884                    if cursor + 4 <= source.len()
10885                        && let Some(value) = hex4(&source[cursor..cursor + 4])
10886                    {
10887                        if value <= 0x7f {
10888                            bytes.push(value as u8);
10889                            origins.push(ByteSpan::new(index, cursor + 4));
10890                            if value as u8 == b'\\' {
10891                                slash_run += 1;
10892                                last_was_escape = true;
10893                            } else {
10894                                slash_run = 0;
10895                                last_was_escape = false;
10896                            }
10897                            index = cursor + 4;
10898                            continue;
10899                        }
10900                        if let Some(character) = char::from_u32(value as u32) {
10901                            let mut encoded = [0; 4];
10902                            for byte in character.encode_utf8(&mut encoded).as_bytes() {
10903                                bytes.push(*byte);
10904                                origins.push(ByteSpan::new(index, cursor + 4));
10905                            }
10906                        } else {
10907                            /* NOTE: Java Unicode escapes are UTF-16 code units, so a
10908                             * lone surrogate is lexically valid even though it
10909                             * has no standalone UTF-8 representation. */
10910                            bytes.push(0x80);
10911                            origins.push(ByteSpan::new(index, cursor + 4));
10912                        }
10913                        slash_run = 0;
10914                        last_was_escape = false;
10915                        index = cursor + 4;
10916                        continue;
10917                    }
10918                    invalid.push(ByteSpan::new(index, (cursor + 4).min(source.len())));
10919                }
10920            }
10921            bytes.push(source[index]);
10922            origins.push(ByteSpan::new(index, index + 1));
10923            if source[index] == b'\\' {
10924                slash_run += 1;
10925            } else {
10926                slash_run = 0;
10927            }
10928            last_was_escape = false;
10929            index += 1;
10930        }
10931        (
10932            Self {
10933                bytes,
10934                origins,
10935                original_len: source.len(),
10936            },
10937            invalid,
10938        )
10939    }
10940
10941    fn original_span(&self, span: ByteSpan) -> ByteSpan {
10942        if span.is_empty() {
10943            let point = self
10944                .origins
10945                .get(span.start)
10946                .map_or(self.original_len, |origin| origin.start);
10947            return ByteSpan::new(point, point);
10948        }
10949        let start = self
10950            .origins
10951            .get(span.start)
10952            .map_or(self.original_len, |origin| origin.start);
10953        let end = if span.end == self.bytes.len() {
10954            self.original_len
10955        } else {
10956            self.origins
10957                .get(span.end.saturating_sub(1))
10958                .map_or(self.original_len, |origin| origin.end)
10959        };
10960        ByteSpan::new(start, end)
10961    }
10962}
10963
10964fn hex4(bytes: &[u8]) -> Option<u16> {
10965    let mut value = 0u16;
10966    for byte in bytes {
10967        value = value.checked_mul(16)?
10968            + match byte {
10969                b'0'..=b'9' => (byte - b'0') as u16,
10970                b'a'..=b'f' => (byte - b'a' + 10) as u16,
10971                b'A'..=b'F' => (byte - b'A' + 10) as u16,
10972                _ => return None,
10973            };
10974    }
10975    Some(value)
10976}
10977
10978#[cfg(test)]
10979mod tests {
10980    use super::*;
10981
10982    fn comments(source: &[u8], language: Language) -> ScanReport {
10983        scan(source, language, ScanOptions::default())
10984    }
10985
10986    #[test]
10987    fn rust_nested_and_raw() {
10988        let report = comments(
10989            br##"r#"// nope"# /* one /* two */ end */ // yes"##,
10990            Language::Rust,
10991        );
10992        assert!(report.valid);
10993        assert_eq!(report.comments.len(), 2);
10994    }
10995
10996    #[test]
10997    fn javascript_regex_and_template_expression() {
10998        let report = comments(
10999            br#"const x = /\/\/*not/; `text // no ${1 /* yes */}`; // yes"#,
11000            Language::JavaScript,
11001        );
11002        assert_eq!(report.comments.len(), 2);
11003    }
11004
11005    #[test]
11006    fn java_unicode_delimiter() {
11007        let report = comments(br"int x; \u002f\u002f hi\nint y;", Language::Java);
11008        assert_eq!(report.comments.len(), 1);
11009        assert_eq!(
11010            &br"int x; \u002f\u002f hi\nint y;"
11011                [report.comments[0].span.start..report.comments[0].span.end],
11012            br"\u002f\u002f hi\nint y;"
11013        );
11014    }
11015
11016    /// [`parse_heredoc`] is a lookahead with no line bound — a quoted
11017    /// delimiter word may carry a line terminator as content, so `<<"EO`, a
11018    /// break, `F"` names the delimiter `EO\nF` — and every path that gives up
11019    /// rewinds the scan to the byte after the operator and lexes those bytes
11020    /// again from a state this parse already decided out of them. The reach it
11021    /// reports is what withdraws the checkpoints in between, so it is asserted
11022    /// here against the parse itself rather than only through a document whose
11023    /// checkpoints might move for some other reason.
11024    #[test]
11025    fn a_heredoc_delimiter_parse_reports_every_byte_it_consulted() {
11026        let quoted = b"cat <<\"EO\nF\"\nx\n";
11027        assert_eq!(quoted[9], b'\n');
11028        assert_eq!(quoted[11], b'"');
11029        let mut reach = Reach::default();
11030        let (heredoc, end) =
11031            parse_heredoc(quoted, 4, &mut reach).expect("a quoted delimiter word spanning a line");
11032        assert_eq!(heredoc.delimiter, b"EO\nF");
11033        assert_eq!(end, 12);
11034        /* NOTE: one past the line terminator that ended the word, and so past
11035         * the closing quote on line 2 the parse had to cross to reach it. A
11036         * checkpoint at the line start at 10 would sit inside that reading. */
11037        assert_eq!(reach, Reach(13));
11038        assert!(
11039            reach.0 > 11,
11040            "{reach:?} does not cover the closing quote at 11"
11041        );
11042
11043        /* NOTE: A plain delimiter word ends at its line, and the parse says so:
11044         * the terminator that ended the word is the last byte it consulted, and
11045         * the line start behind it is left standing. */
11046        let plain = b"cat <<EOF\nx\n";
11047        assert_eq!(plain[9], b'\n');
11048        let mut reach = Reach::default();
11049        let (heredoc, end) = parse_heredoc(plain, 4, &mut reach).expect("a plain delimiter word");
11050        assert_eq!(heredoc.delimiter, b"EOF");
11051        assert_eq!(end, 9);
11052        assert_eq!(reach, Reach(10));
11053        assert!(
11054            reach.0 <= 10,
11055            "{reach:?} reaches past the line the delimiter word ends on"
11056        );
11057    }
11058
11059    /// A tag search a character class bounds reads that class and one byte
11060    /// more, never the whole document.
11061    ///
11062    /// INVARIANT: the reach a lookahead reports withdraws every checkpoint at
11063    /// or under it, so a search that runs to the end of the file on an
11064    /// ordinary byte costs the rest of the document its restart points. An
11065    /// OCaml quoted-string tag is `[a-z_]*` and is followed by `|` (OCaml
11066    /// manual, Lexical conventions); a PostgreSQL dollar-quote tag is an
11067    /// identifier or nothing and is followed by `$` (PostgreSQL 4.1.2.4); a C++
11068    /// raw-string delimiter is at most 16 d-chars and is followed by `(`
11069    /// ([lex.string]). Each search therefore gives up at the first byte outside
11070    /// its class, and an ordinary `{`, `$` or `R"` in the code leaves the lines
11071    /// under it their checkpoints.
11072    #[test]
11073    fn a_class_bounded_tag_search_keeps_the_checkpoints_under_it() {
11074        // NOTE: `{aa` opens no quoted string: the tag class stops at the line
11075        // NOTE: terminator, which is not the `|` a tag needs behind it. The
11076        // NOTE: byte the scan stands on is not recorded, so asking this of
11077        // NOTE: every byte of the document does not drag the watermark along
11078        // NOTE: behind the scan: the `{` on line 1 is the only thing that reads
11079        // NOTE: ahead here, and the lines under it keep the watermark it left.
11080        let ocaml = b"let x = {aa\n(* c *)\ny\n";
11081        assert_eq!(
11082            scan_checkpoint_watermarks(ocaml, Language::Ocaml, ScanOptions::default()),
11083            [(0, 0), (12, 12), (20, 12), (22, 12)]
11084        );
11085
11086        // NOTE: `R"x y"` opens no raw string: the d-char class stops at the
11087        // NOTE: space, which is not the `(` a delimiter needs behind it. The
11088        // NOTE: search that used to answer that read the whole document looking
11089        // NOTE: for a `(`, which took every checkpoint below it away.
11090        let cpp = b"char* s = R\"x y\";\n// c\nz\n";
11091        assert_eq!(cpp[13], b' ');
11092        assert_eq!(
11093            scan_checkpoint_watermarks(cpp, Language::Cpp, ScanOptions::default()),
11094            [(0, 0), (18, 14), (23, 14), (25, 14)]
11095        );
11096
11097        // NOTE: `a$b` opens no dollar-quoted string: the tag class stops at the
11098        // NOTE: space, which is not the `$` a tag needs behind it.
11099        let postgres = b"select a$b from t\n-- c\nx\n";
11100        assert_eq!(
11101            scan_checkpoint_watermarks(
11102                postgres,
11103                Language::Sql,
11104                ScanOptions {
11105                    dialect: Dialect::PostgreSql,
11106                    ..Default::default()
11107                }
11108            ),
11109            [(0, 0), (18, 11), (23, 11), (25, 11)]
11110        );
11111
11112        // NOTE: An Oracle q-quote reads one delimiter byte and no more, so a
11113        // NOTE: bare `q` in the code costs nothing either.
11114        let oracle = b"select q from t\n-- c\nx\n";
11115        assert_eq!(
11116            scan_checkpoint_watermarks(
11117                oracle,
11118                Language::Sql,
11119                ScanOptions {
11120                    dialect: Dialect::Oracle,
11121                    ..Default::default()
11122                }
11123            ),
11124            [(0, 0), (16, 9), (21, 9), (23, 9)]
11125        );
11126    }
11127
11128    /// What Swift's three lookaheads cost the lines under them.
11129    ///
11130    /// INVARIANT: two of them are bounded and one is not, and the difference is
11131    /// whether the scan rewinds behind what they read. A `#` run stops at the
11132    /// first byte outside its class, and a bare `/ ... /` may hold no line
11133    /// terminator (The Swift Programming Language, Lexical Structure), so
11134    /// neither can reach past the line it stands on and the lines under it keep
11135    /// their checkpoints. An extended `#/ ... /#` that opens a multi-line
11136    /// literal and never closes is the one that cannot be bounded: it reads to
11137    /// the end of the document, hands the lines back, and lexes them again as
11138    /// code — so every checkpoint under it is withdrawn, the end of the
11139    /// document included, because appending a `/#` would close it.
11140    #[test]
11141    fn swift_lookaheads_pay_for_what_they_read() {
11142        // NOTE: `#if` opens no literal: the run of `#` stops at the `i`, which
11143        // NOTE: is neither the quote of a raw string nor the slash of a regular
11144        // NOTE: expression literal.
11145        let directive = b"let a = #if x\n// c\ny\n";
11146        assert_eq!(
11147            scan_checkpoint_watermarks(directive, Language::Swift, ScanOptions::default()),
11148            [(0, 0), (14, 10), (19, 10), (21, 10)]
11149        );
11150
11151        // NOTE: `1 / 2` opens no literal either, and gives up at the space one
11152        // NOTE: byte behind the slash rather than reading the line at all.
11153        let division = b"let a = 1 / 2\n// c\nx\n";
11154        assert_eq!(
11155            scan_checkpoint_watermarks(division, Language::Swift, ScanOptions::default()),
11156            [(0, 0), (14, 12), (19, 12), (21, 12)]
11157        );
11158
11159        // NOTE: `(/x y` opens none: the closing delimiter would be preceded by
11160        // NOTE: an unescaped space, and there is none on the line in any case.
11161        // NOTE: The search reads the terminator to know it must stop there,
11162        // NOTE: which leaves the line start behind it standing.
11163        let candidate = b"let a = (/x y\n// c\nz\n";
11164        assert_eq!(candidate[13], b'\n');
11165        assert_eq!(
11166            scan_checkpoint_watermarks(candidate, Language::Swift, ScanOptions::default()),
11167            [(0, 0), (14, 14), (19, 14), (21, 14)]
11168        );
11169
11170        // NOTE: the unbounded one, and the only checkpoint left is the offset a
11171        // NOTE: restart there *is* the full scan from.
11172        let unterminated = b"let a = #/\n// c\nz\n";
11173        assert_eq!(
11174            scan_checkpoint_watermarks(unterminated, Language::Swift, ScanOptions::default()),
11175            [(0, 0)]
11176        );
11177    }
11178
11179    /// Every C# lookahead is bounded by the run it is reading, so the line
11180    /// starts under one keep their restart points.
11181    #[test]
11182    fn csharp_lookaheads_pay_for_what_they_read() {
11183        // NOTE: `@x` opens no literal: the run of `$` and `@` stops at the `x`,
11184        // NOTE: which is not the quote every string form needs. The byte that
11185        // NOTE: ended the run is the one byte read past where the scan resumes.
11186        let identifier = b"var a = @x;\n// c\ny\n";
11187        assert_eq!(identifier[9], b'x');
11188        assert_eq!(
11189            scan_checkpoint_watermarks(identifier, Language::CSharp, ScanOptions::default()),
11190            [(0, 0), (12, 10), (17, 10), (19, 10)]
11191        );
11192
11193        // NOTE: a raw string's closing run is taken whole, so the byte that
11194        // NOTE: ended it is read one past the resume point — and the read that
11195        // NOTE: decided the literal spans lines costs nothing on top, because
11196        // NOTE: the scan takes every byte of that line either way.
11197        let raw = b"var a = \"\"\"\n  x\n  \"\"\";\n// c\ny\n";
11198        assert_eq!(raw[21], b';');
11199        assert_eq!(
11200            scan_checkpoint_watermarks(raw, Language::CSharp, ScanOptions::default()),
11201            [(0, 0), (23, 22), (28, 22), (30, 22)]
11202        );
11203
11204        // NOTE: a directive line is a plain forward scan of one line, so it
11205        // NOTE: reports nothing and the line under it keeps its restart point.
11206        let directive = b"#if x // c\nvar a = 1;\n";
11207        assert_eq!(
11208            scan_checkpoint_watermarks(directive, Language::CSharp, ScanOptions::default()),
11209            [(0, 0), (11, 0), (22, 0)]
11210        );
11211
11212        // NOTE: a verbatim string that never closes swallows the file, and the
11213        // NOTE: only checkpoint left is the offset a restart there *is* the full
11214        // NOTE: scan from.
11215        let unterminated = b"var a = @\"open\n// c\nz\n";
11216        assert_eq!(
11217            scan_checkpoint_watermarks(unterminated, Language::CSharp, ScanOptions::default()),
11218            [(0, 0)]
11219        );
11220    }
11221
11222    #[test]
11223    fn shell_heredoc_is_opaque() {
11224        let report = comments(b"cat <<EOF\n# data\nEOF\n# comment\n", Language::Shell);
11225        assert_eq!(report.comments.len(), 1);
11226    }
11227
11228    #[test]
11229    fn regex_overrides_apply_to_complete_comment_bytes() {
11230        let source = b"// KEEP this\n// REMOVE this\n// ordinary\n";
11231        let report = scan(
11232            source,
11233            Language::C,
11234            ScanOptions {
11235                policy: Policy::Legal,
11236                keep_regex: vec!["KEEP".into()],
11237                remove_regex: vec!["REMOVE".into()],
11238                ..Default::default()
11239            },
11240        );
11241        assert!(matches!(
11242            report.comments[0].disposition,
11243            Disposition::Keep { .. }
11244        ));
11245        assert!(report.comments[1].disposition.is_remove());
11246        assert!(report.comments[2].disposition.is_remove());
11247    }
11248
11249    #[test]
11250    fn html_embeds_javascript_but_protects_html() {
11251        let report = comments(
11252            b"<!--keep--><script>let x=1;//remove\n</script>",
11253            Language::Html,
11254        );
11255        assert_eq!(report.comments.len(), 2);
11256        assert!(!report.comments[0].disposition.is_remove());
11257        assert!(report.comments[1].disposition.is_remove());
11258    }
11259
11260    /// A Scala checkpoint may not stand directly before a `<` that could open
11261    /// an XML literal: the literal's trigger reads the byte *behind* its `<`,
11262    /// and a rescan that begins there has no byte behind it to read. A line
11263    /// that opens with one therefore offers no restart point, while a line
11264    /// that opens with a `<` no literal could begin at keeps its checkpoint.
11265    #[test]
11266    fn a_scala_xml_boundary_keeps_its_checkpoint_away_from_the_literal() {
11267        // NOTE: `<a>` after a newline is an XML literal: the checkpoint after
11268        // NOTE: the newline is refused, and the text of the literal is opaque
11269        // NOTE: in a full scan and in a rescan from any earlier checkpoint.
11270        let xml = b"x\n<a>// text</a>\ny\n";
11271        assert_eq!(
11272            scan_checkpoint_watermarks(xml, Language::Scala, ScanOptions::default()),
11273            [(0, 0), (17, 0), (19, 0)]
11274        );
11275        let (_, checkpoints) =
11276            scan_with_checkpoints(xml, Language::Scala, ScanOptions::default(), 0);
11277        assert_eq!(checkpoints, vec![0, 17, 19]);
11278        for point in &checkpoints {
11279            let (suffix, _) = scan_with_checkpoints(
11280                &xml[*point..],
11281                Language::Scala,
11282                ScanOptions::default(),
11283                *point,
11284            );
11285            assert!(suffix.comments.is_empty(), "restarting at {point}");
11286        }
11287
11288        // NOTE: `<1` cannot begin a literal (a digit is no XML name start), so
11289        // NOTE: the line keeps its checkpoint and the `//` under it is a
11290        // NOTE: comment in a full scan and in a rescan from it alike.
11291        let operator = b"x\n<1 + 2\n// c\ny\n";
11292        assert_eq!(
11293            scan_checkpoint_watermarks(operator, Language::Scala, ScanOptions::default()),
11294            [(0, 0), (2, 0), (9, 0), (14, 0), (16, 0)]
11295        );
11296        let (_, checkpoints) =
11297            scan_with_checkpoints(operator, Language::Scala, ScanOptions::default(), 0);
11298        assert_eq!(checkpoints, vec![0, 2, 9, 14, 16]);
11299        for point in &checkpoints {
11300            let (suffix, _) = scan_with_checkpoints(
11301                &operator[*point..],
11302                Language::Scala,
11303                ScanOptions::default(),
11304                *point,
11305            );
11306            assert!(
11307                suffix
11308                    .comments
11309                    .iter()
11310                    .all(|comment| comment.span.start >= *point),
11311                "restarting at {point}"
11312            );
11313        }
11314
11315        // NOTE: `<` after a bare `\r` is no literal (a `\r` is not a trigger
11316        // NOTE: byte), so the checkpoint the `\r` earns is refused all the
11317        // NOTE: same: a rescan from it would not know the `\r` was there.
11318        let cr = b"\r<?php //go:build\nz\n";
11319        assert_eq!(
11320            scan_checkpoint_watermarks(cr, Language::Scala, ScanOptions::default()),
11321            [(0, 0), (18, 0), (20, 0)]
11322        );
11323    }
11324}