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("e) {
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("e @ (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("e @ (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("e) {
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}