use crate::{
ByteSpan, Comment, CommentKind, Diagnostic, Dialect, Disposition, DispositionExplanation,
Language, Policy, ScanOptions, ScanReport, Severity,
};
use memchr::{memchr, memchr2, memchr3, memmem};
use regex::bytes::RegexSet;
use std::cmp::Ordering;
#[derive(Clone, Debug)]
pub struct PreparedScanner {
pub(crate) options: ScanOptions,
pub(crate) patterns: DispositionPatterns,
pattern_error: Option<String>,
}
impl PreparedScanner {
pub fn new(options: ScanOptions) -> Result<Self, regex::Error> {
let patterns = DispositionPatterns::compile(&options)?;
Ok(Self {
options,
patterns,
pattern_error: None,
})
}
pub fn options(&self) -> &ScanOptions {
&self.options
}
pub fn scan(&self, source: &[u8], language: Language) -> ScanReport {
scan_prepared_internal(source, language, self, 0, false, None).0
}
pub(crate) fn lossy(options: ScanOptions) -> Self {
match DispositionPatterns::compile(&options) {
Ok(patterns) => Self {
options,
patterns,
pattern_error: None,
},
Err(error) => Self {
options,
patterns: DispositionPatterns::empty(),
pattern_error: Some(error.to_string()),
},
}
}
}
pub fn scan(source: &[u8], language: Language, options: ScanOptions) -> ScanReport {
scan_internal(source, language, options, 0, false, None).0
}
#[cfg(test)]
pub(crate) fn scan_with_checkpoints(
source: &[u8],
language: Language,
options: ScanOptions,
offset: usize,
) -> (ScanReport, Vec<usize>) {
let (report, checkpoints, _) = scan_internal(source, language, options, offset, true, None);
(report, checkpoints)
}
pub(crate) fn scan_with_checkpoints_prepared(
source: &[u8],
language: Language,
prepared: &PreparedScanner,
offset: usize,
) -> (ScanReport, Vec<usize>) {
let (report, checkpoints, _) =
scan_prepared_internal(source, language, prepared, offset, true, None);
(report, checkpoints)
}
pub(crate) fn scan_until_checkpoint_prepared(
source: &[u8],
language: Language,
prepared: &PreparedScanner,
offset: usize,
stop: usize,
) -> (ScanReport, Vec<usize>, bool) {
scan_prepared_internal(source, language, prepared, offset, true, Some(stop))
}
#[cfg(test)]
pub(crate) fn scan_checkpoint_watermarks(
source: &[u8],
language: Language,
options: ScanOptions,
) -> Vec<(usize, usize)> {
let mut scanner = Scanner::with_offset(source, language, options, 0, true, None);
scanner.scan_language();
scanner
.safe_checkpoints
.iter()
.copied()
.zip(scanner.checkpoint_watermarks)
.collect()
}
fn scan_prepared_internal(
source: &[u8],
language: Language,
prepared: &PreparedScanner,
offset: usize,
track_checkpoints: bool,
stop: Option<usize>,
) -> (ScanReport, Vec<usize>, bool) {
finish_scan(Scanner::with_prepared(
source,
language,
prepared,
offset,
track_checkpoints,
stop,
))
}
fn scan_internal(
source: &[u8],
language: Language,
options: ScanOptions,
offset: usize,
track_checkpoints: bool,
stop: Option<usize>,
) -> (ScanReport, Vec<usize>, bool) {
finish_scan(Scanner::with_offset(
source,
language,
options,
offset,
track_checkpoints,
stop,
))
}
#[inline]
fn finish_scan(mut scanner: Scanner<'_>) -> (ScanReport, Vec<usize>, bool) {
let language = scanner.language;
scanner.scan_language();
debug_assert!(
scanner.comments.windows(2).all(|comments| {
comments[0].span.start < comments[1].span.start
&& comments[0].span.end <= comments[1].span.start
}),
"{language} scanner returned duplicate, unordered, or overlapping comments: {:?}",
scanner.comments,
);
let valid = !scanner
.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error);
(
ScanReport {
language,
comments: scanner.comments,
diagnostics: scanner.diagnostics,
valid,
},
scanner.safe_checkpoints,
scanner.stopped,
)
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
struct Reach(usize);
impl Reach {
fn through(&mut self, end: usize) {
self.0 = self.0.max(end);
}
fn byte(&mut self, index: usize) {
self.through(index + 1);
}
fn end_of(&mut self, bytes: &[u8]) {
self.through(bytes.len() + 1);
}
}
fn line_bounded_reach(bytes: &[u8], index: usize, width: usize) -> usize {
let limit = (index + width).min(bytes.len());
let window = &bytes[index.min(limit)..limit];
window
.iter()
.position(|byte| is_line_terminator(*byte))
.map_or(limit, |stop| index + stop + 1)
}
struct Scanner<'a> {
source: &'a [u8],
language: Language,
options: ScanOptions,
comments: Vec<Comment>,
diagnostics: Vec<Diagnostic>,
offset: usize,
patterns: DispositionPatterns,
safe_checkpoints: Vec<usize>,
track_checkpoints: bool,
stop: Option<usize>,
stopped: bool,
consulted: usize,
#[cfg(test)]
checkpoint_watermarks: Vec<usize>,
restart_rules: RestartRules,
yaml_blocks: Vec<YamlBlockScalar>,
}
impl<'a> Scanner<'a> {
fn with_offset(
source: &'a [u8],
language: Language,
options: ScanOptions,
offset: usize,
track_checkpoints: bool,
stop: Option<usize>,
) -> Self {
let (patterns, pattern_error) = match DispositionPatterns::compile(&options) {
Ok(patterns) => (patterns, None),
Err(error) => (DispositionPatterns::empty(), Some(error.to_string())),
};
Self::with_owned(
source,
language,
PreparedScanner {
options,
patterns,
pattern_error,
},
offset,
track_checkpoints,
stop,
)
}
fn with_prepared(
source: &'a [u8],
language: Language,
prepared: &PreparedScanner,
offset: usize,
track_checkpoints: bool,
stop: Option<usize>,
) -> Self {
Self::with_owned(
source,
language,
prepared.clone(),
offset,
track_checkpoints,
stop,
)
}
fn with_owned(
source: &'a [u8],
language: Language,
prepared: PreparedScanner,
offset: usize,
track_checkpoints: bool,
stop: Option<usize>,
) -> Self {
let PreparedScanner {
options,
patterns,
pattern_error,
} = prepared;
let mut scanner = Self {
source,
language,
options,
comments: Vec::new(),
diagnostics: Vec::new(),
offset,
patterns,
safe_checkpoints: track_checkpoints
.then_some(vec![offset])
.unwrap_or_default(),
track_checkpoints,
stop,
stopped: false,
consulted: 0,
#[cfg(test)]
checkpoint_watermarks: track_checkpoints
.then_some(vec![offset])
.unwrap_or_default(),
restart_rules: RestartRules::of(source, language),
yaml_blocks: Vec::new(),
};
if let Some(error) = pattern_error.as_deref() {
scanner.error(
"invalid-policy-regex",
&format!("invalid comment policy regex: {error}"),
ByteSpan::new(0, 0),
);
}
scanner
}
fn child(
source: &'a [u8],
language: Language,
options: ScanOptions,
patterns: DispositionPatterns,
offset: usize,
) -> Self {
Self {
source,
language,
options,
comments: Vec::new(),
diagnostics: Vec::new(),
offset,
patterns,
safe_checkpoints: Vec::new(),
track_checkpoints: false,
stop: None,
stopped: false,
consulted: 0,
#[cfg(test)]
checkpoint_watermarks: Vec::new(),
restart_rules: RestartRules::of(source, language),
yaml_blocks: Vec::new(),
}
}
fn scan_language(&mut self) {
match self.language {
Language::Css if self.options.dialect == Dialect::Sass => self.scan_sass(),
Language::Rust
| Language::C
| Language::Cpp
| Language::Go
| Language::Kotlin
| Language::Css
| Language::Jsonc => self.scan_c_family(),
Language::Java => self.scan_java(),
Language::JavaScript | Language::TypeScript => self.scan_javascript(),
Language::Ocaml => self.scan_ocaml(),
Language::Python => self.scan_python(),
Language::Shell => self.scan_shell(),
Language::Html => self.scan_html(),
Language::Sql => self.scan_sql(),
Language::Toml => self.scan_toml(),
Language::Lua => self.scan_lua(),
Language::Yaml => self.scan_yaml(),
Language::Php => self.scan_php(),
Language::Ruby => self.scan_ruby(),
Language::Zig => self.scan_zig(),
Language::R => self.scan_r(),
Language::Dart => self.scan_dart(),
Language::Swift => self.scan_swift(),
Language::CSharp => self.scan_csharp(),
Language::Scala => self.scan_scala(),
Language::Vue => self.scan_vue(),
Language::Svelte => self.scan_svelte(),
Language::Markdown => self.scan_markdown(),
Language::Perl => self.scan_perl(),
Language::Unknown => self.error(
"unknown-language",
"a language is required",
ByteSpan::new(0, 0),
),
}
}
fn consult(&mut self, reach: Reach) {
self.consulted = self.consulted.max(reach.0.min(self.source.len() + 1));
}
fn error(&mut self, code: &str, message: &str, span: ByteSpan) {
let start = span.start.min(self.source.len());
let end = span.end.max(start).min(self.source.len());
self.diagnostics.push(Diagnostic {
code: code.into(),
message: message.into(),
severity: Severity::Error,
span: ByteSpan::new(start + self.offset, end + self.offset),
});
}
fn add_comment(&mut self, start: usize, end: usize, lexical_kind: CommentKind) {
let start = start.min(self.source.len());
let end = end.max(start).min(self.source.len());
let kind = classify_comment(
self.source,
self.language,
lexical_kind,
start,
end,
self.offset,
);
let raw = &self.source[start..end];
let disposition = disposition(kind, &self.options, raw, &self.patterns);
self.comments.push(Comment {
span: ByteSpan::new(start + self.offset, end + self.offset),
kind,
disposition,
});
}
fn merge_child(&mut self, child: Scanner<'_>) {
self.comments.extend(child.comments);
self.diagnostics.extend(child.diagnostics);
}
fn checkpoint_is_restartable(&self, local: usize) -> bool {
local <= self.restart_rules.first_block_scalar
&& (self.offset > 0 || self.restart_rules.permit_restart_at(self.source, local))
&& (self.restart_rules.language != Language::Scala
|| the_scala_xml_boundary_permits_a_restart(self.source, local))
&& (!matches!(
self.restart_rules.language,
Language::Html | Language::Vue | Language::Svelte
) || the_tag_boundary_permits_a_restart(self.source, local))
}
fn add_safe_checkpoint(&mut self, local: usize) {
if !self.track_checkpoints
|| local < self.consulted
|| !self.checkpoint_is_restartable(local)
{
return;
}
let absolute = self.offset + local;
if self.safe_checkpoints.last().copied() != Some(absolute) {
self.safe_checkpoints.push(absolute);
#[cfg(test)]
self.checkpoint_watermarks
.push(self.offset + self.consulted);
}
if self.stop == Some(absolute) {
self.stopped = true;
}
}
fn add_safe_newlines(&mut self, mut start: usize, end: usize) {
if !self.track_checkpoints {
return;
}
while start < end && !self.stopped {
let Some(relative) = memchr2(b'\r', b'\n', &self.source[start..end]) else {
break;
};
let newline = start + relative;
let next = consume_newline(self.source, newline).min(end);
self.add_safe_checkpoint(next);
start = next;
}
}
fn scan_c_family(&mut self) {
if !self.restart_rules.splicing_permits_restarts {
let mapped = MappedBytes::without_c_line_splices(self.source);
let mut child = Scanner::child(
&mapped.bytes,
self.language,
self.options.clone(),
self.patterns.clone(),
0,
);
child.scan_c_family_unmapped();
self.merge_mapped(child, &mapped);
} else {
self.scan_c_family_unmapped();
}
}
fn scan_c_family_unmapped(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() {
let Some(next) =
next_c_family_trigger(bytes, index, self.language, self.options.dialect)
else {
self.add_safe_newlines(index, bytes.len());
break;
};
self.add_safe_newlines(index, next);
if self.stopped {
break;
}
index = next;
if starts(bytes, index, b"//")
&& !(self.language == Language::Css && self.options.dialect != Dialect::Scss)
{
let end = line_end(bytes, index + 2);
self.add_comment(index, end, line_kind(bytes, index));
index = end;
continue;
}
if starts(bytes, index, b"/*") {
let nested = matches!(self.language, Language::Rust | Language::Kotlin);
let (end, closed) = block_end(bytes, index, b"/*", b"*/", nested);
self.add_comment(index, end, block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if self.language == Language::Css && self.options.dialect == Dialect::Scss {
if starts(bytes, index, b"#{") {
index = self.scan_scss_interpolation(index + 2, 0);
continue;
}
if let Some(end) = self.scss_url_end(index, 0) {
index = end;
continue;
}
}
if let Some(end) = self.special_c_string(index) {
index = end;
continue;
}
index += 1;
}
}
fn scan_sass(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"//") {
let end = self.sass_silent_comment_end(index);
self.add_comment(index, end, line_kind(bytes, index));
index = end;
continue;
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
self.add_comment(index, end, block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated Sass block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if starts(bytes, index, b"#{") {
index = self.scan_scss_interpolation(index + 2, 0);
continue;
}
if let Some(end) = self.scss_url_end(index, 0) {
index = end;
continue;
}
if matches!(bytes[index], b'"' | b'\'') {
index = self.scan_scss_string(index, 0);
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn sass_silent_comment_end(&self, start: usize) -> usize {
let bytes = self.source;
let line_start = line_start(bytes, start);
if bytes[line_start..start]
.iter()
.any(|byte| !matches!(byte, b' ' | b'\t'))
{
return line_end(bytes, start + 2);
}
let base_indent = sass_indent_width(&bytes[line_start..start]);
let first_end = line_end(bytes, start + 2);
let mut included_end = first_end;
let mut next = if first_end < bytes.len() {
consume_newline(bytes, first_end)
} else {
return first_end;
};
while next < bytes.len() {
let finish = line_end(bytes, next);
let mut content = next;
while content < finish && matches!(bytes[content], b' ' | b'\t') {
content += 1;
}
let blank = content == finish || matches!(bytes.get(content), Some(b'\r' | b'\n'));
if !blank && sass_indent_width(&bytes[next..content]) <= base_indent {
break;
}
included_end = finish;
if finish >= bytes.len() {
break;
}
next = consume_newline(bytes, finish);
}
included_end
}
fn scss_url_end(&mut self, index: usize, depth: usize) -> Option<usize> {
if !starts_ascii_case(&self.source[index..], b"url(") {
return None;
}
if index > 0 && is_css_identifier_part(self.source[index - 1]) {
return None;
}
let bytes = self.source;
let mut cursor = index + 4;
while bytes.get(cursor).is_some_and(|byte| css_whitespace(*byte)) {
cursor += 1;
}
if matches!(bytes.get(cursor), Some(b'"' | b'\'')) {
cursor = self.scan_scss_string(cursor, depth);
while bytes.get(cursor).is_some_and(|byte| css_whitespace(*byte)) {
cursor += 1;
}
if bytes.get(cursor) == Some(&b')') {
return Some(cursor + 1);
}
}
while cursor < bytes.len() {
if bytes[cursor] == b')' {
return Some(cursor + 1);
}
if bytes[cursor] == b'\\' {
cursor = (cursor + 2).min(bytes.len());
continue;
}
if starts(bytes, cursor, b"#{") {
cursor = self.scan_scss_interpolation(cursor + 2, depth + 1);
continue;
}
cursor += 1;
}
Some(bytes.len())
}
fn scan_scss_interpolation(&mut self, mut index: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"SCSS interpolation nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut braces = 1usize;
while index < bytes.len() {
if starts(bytes, index, b"//") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
self.add_comment(index, end, block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated SCSS block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if let Some(end) = self.scss_url_end(index, depth) {
index = end;
continue;
}
match bytes[index] {
b'"' | b'\'' => index = self.scan_scss_string(index, depth),
b'{' => {
braces += 1;
index += 1;
}
b'}' => {
braces -= 1;
index += 1;
if braces == 0 {
return index;
}
}
_ => index += 1,
}
}
self.error(
"unterminated-interpolation",
"unterminated SCSS interpolation",
ByteSpan::new(index, index),
);
index
}
fn scan_scss_string(&mut self, start: usize, depth: usize) -> usize {
let bytes = self.source;
let quote = bytes[start];
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
continue;
}
if bytes[index] == quote {
return index + 1;
}
if starts(bytes, index, b"#{") {
index = self.scan_scss_interpolation(index + 2, depth + 1);
continue;
}
index += 1;
}
self.error(
"unterminated-string",
"unterminated Sass string",
ByteSpan::new(start, bytes.len()),
);
bytes.len()
}
fn special_c_string(&mut self, index: usize) -> Option<usize> {
let bytes = self.source;
match self.language {
Language::Rust => {
if bytes[index] == b'"'
&& let Some((raw_start, hashes)) = rust_raw_start_at_quote(bytes, index)
{
let content = index + 1;
let mut end_token = Vec::with_capacity(hashes + 1);
end_token.push(b'"');
end_token.extend(std::iter::repeat_n(b'#', hashes));
if let Some(relative) = find_subslice(&bytes[content..], &end_token) {
return Some(content + relative + end_token.len());
}
self.error(
"unterminated-string",
"unterminated Rust raw string",
ByteSpan::new(raw_start, bytes.len()),
);
return Some(bytes.len());
}
if bytes[index] == b'"' || (starts(bytes, index, b"b\"") && index + 1 < bytes.len())
{
let quote = if bytes[index] == b'b' {
index + 1
} else {
index
};
return Some(self.quoted_or_error(quote, true, "string"));
}
if bytes[index] == b'\'' {
let mut reach = Reach::default();
let literal = rust_char_start(bytes, index, &mut reach);
self.consult(reach);
if literal {
return Some(self.quoted_or_error(index, false, "character literal"));
}
}
}
Language::C | Language::Cpp => {
let raw = (self.language == Language::Cpp && bytes[index] == b'"')
.then(|| cpp_raw_start_at_quote(bytes, index))
.flatten()
.and_then(|raw_start| {
let mut reach = Reach::default();
let raw = cpp_raw_string(bytes, raw_start, &mut reach);
self.consult(reach);
raw.map(|(end, closed)| (raw_start, end, closed))
});
if let Some((raw_start, end, closed)) = raw {
if !closed {
self.error(
"unterminated-string",
"unterminated C++ raw string",
ByteSpan::new(raw_start, end),
);
}
return Some(end);
}
if is_c_quote_start(bytes, index) {
let quote_index = if matches!(bytes[index], b'"' | b'\'') {
index
} else {
(index..(index + 3).min(bytes.len()))
.find(|i| matches!(bytes[*i], b'"' | b'\''))
.unwrap_or(index)
};
return Some(self.quoted_or_error(
quote_index,
false,
"string or character literal",
));
}
}
Language::Go => {
if bytes[index] == b'`' {
return Some(self.delimited_or_error(index, b"`", "raw string"));
}
if matches!(bytes[index], b'"' | b'\'') {
return Some(self.quoted_or_error(index, false, "string or rune literal"));
}
}
Language::Kotlin => {
if starts(bytes, index, b"\"\"\"") {
return Some(self.scan_kotlin_string(index, true, 0));
}
if bytes[index] == b'"' {
return Some(self.scan_kotlin_string(index, false, 0));
}
if bytes[index] == b'\'' {
return Some(self.quoted_or_error(index, false, "Kotlin character literal"));
}
}
Language::Jsonc => {
if matches!(bytes[index], b'"' | b'\'') {
return Some(self.quoted_or_error(index, false, "JSON string"));
}
}
Language::Css => {
if matches!(bytes[index], b'"' | b'\'') {
return Some(if self.options.dialect == Dialect::Scss {
self.scan_scss_string(index, 0)
} else {
self.quoted_or_error(index, true, "CSS string")
});
}
}
_ => {}
}
None
}
fn scan_kotlin_string(&mut self, start: usize, triple: bool, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Kotlin string-template nesting limit exceeded",
ByteSpan::new(start, start),
);
return self.source.len();
}
let bytes = self.source;
let delimiter = if triple { b"\"\"\"".as_slice() } else { b"\"" };
let dollars = kotlin_dollar_width(bytes, start);
let mut index = start + delimiter.len();
while index < bytes.len() {
if starts(bytes, index, delimiter) {
return if triple {
index + count_run(bytes, index, b'"')
} else {
index + 1
};
}
if !triple && bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == b'$' {
let run = count_run(bytes, index, b'$');
if run >= dollars && bytes.get(index + run) == Some(&b'{') {
index = self.scan_kotlin_expression(index + run + 1, depth + 1);
} else {
index += run;
}
} else if !triple && matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
"unterminated Kotlin string",
ByteSpan::new(start, index),
);
return index;
} else {
index += 1;
}
}
self.error(
"unterminated-string",
if triple {
"unterminated Kotlin triple-quoted string"
} else {
"unterminated Kotlin string"
},
ByteSpan::new(start, index),
);
index
}
fn scan_kotlin_expression(&mut self, mut index: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Kotlin string-template nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut braces = 1usize;
while index < bytes.len() {
if starts(bytes, index, b"//") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, line_kind(bytes, index));
index = end;
continue;
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
self.add_comment(index, end, block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated Kotlin block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if starts(bytes, index, b"\"\"\"") {
index = self.scan_kotlin_string(index, true, depth + 1);
continue;
}
match bytes[index] {
b'"' => index = self.scan_kotlin_string(index, false, depth + 1),
b'\'' => {
index = self.quoted_or_error(index, false, "Kotlin character literal");
}
b'{' => {
braces += 1;
index += 1;
}
b'}' => {
braces -= 1;
index += 1;
if braces == 0 {
return index;
}
}
_ => index += 1,
}
}
self.error(
"unterminated-template-expression",
"unterminated Kotlin string-template expression",
ByteSpan::new(index, index),
);
index
}
fn quoted_or_error(&mut self, start: usize, multiline: bool, name: &str) -> usize {
let quote = self.source[start];
let mut index = start + 1;
while index < self.source.len() {
if self.source[index] == b'\\' {
if index + 1 < self.source.len() {
index += 2;
} else {
index += 1;
}
} else if self.source[index] == quote {
return index + 1;
} else if !multiline && matches!(self.source[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
&format!("unterminated {name}"),
ByteSpan::new(start, index),
);
return index;
} else {
index += 1;
}
}
self.error(
"unterminated-string",
&format!("unterminated {name}"),
ByteSpan::new(start, index),
);
index
}
fn js_quoted_or_error(&mut self, start: usize) -> usize {
let quote = self.source[start];
let mut index = start + 1;
while index < self.source.len() {
if self.source[index] == b'\\' {
let escaped = index + 1;
if let Some(width) = unicode_line_terminator_width(self.source, escaped) {
index = escaped + width;
} else {
index = (index + 2).min(self.source.len());
}
} else if self.source[index] == quote {
return index + 1;
} else if unicode_line_terminator_width(self.source, index).is_some() {
self.error(
"unterminated-string",
"unterminated JavaScript string",
ByteSpan::new(start, index),
);
return index;
} else {
index += 1;
}
}
self.error(
"unterminated-string",
"unterminated JavaScript string",
ByteSpan::new(start, index),
);
index
}
fn delimited_or_error(&mut self, start: usize, delimiter: &[u8], name: &str) -> usize {
let content = start + delimiter.len();
if let Some(relative) = find_subslice(&self.source[content..], delimiter) {
content + relative + delimiter.len()
} else {
self.error(
"unterminated-string",
&format!("unterminated {name}"),
ByteSpan::new(start, self.source.len()),
);
self.source.len()
}
}
fn scan_java(&mut self) {
let (mapped, invalid_unicode) = MappedBytes::java_unicode(self.source);
for span in invalid_unicode {
self.error(
"invalid-unicode-escape",
"invalid Java Unicode escape",
span,
);
}
let mut child = Scanner::child(
&mapped.bytes,
Language::Java,
self.options.clone(),
self.patterns.clone(),
0,
);
let mut index = 0;
while index < child.source.len() {
if starts(child.source, index, b"//") {
let end = line_end(child.source, index + 2);
child.add_comment(index, end, java_line_kind(child.source, index));
index = end;
continue;
}
if starts(child.source, index, b"/*") {
let (end, closed) = block_end(child.source, index, b"/*", b"*/", false);
child.add_comment(index, end, java_block_kind(child.source, index));
if !closed {
child.error(
"unterminated-comment",
"unterminated block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if starts(child.source, index, b"\"\"\"") {
let (end, closed) = java_text_block_end(child.source, index);
if !closed {
child.error(
"unterminated-string",
"unterminated Java text block",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if matches!(child.source[index], b'"' | b'\'') {
index = child.quoted_or_error(index, false, "Java literal");
continue;
}
index += 1;
}
self.merge_mapped(child, &mapped);
}
fn merge_mapped(&mut self, child: Scanner<'_>, mapped: &MappedBytes) {
for mut comment in child.comments {
comment.span = mapped.original_span(comment.span);
comment.span.start += self.offset;
comment.span.end += self.offset;
self.comments.push(comment);
}
for mut diagnostic in child.diagnostics {
diagnostic.span = mapped.original_span(diagnostic.span);
diagnostic.span.start += self.offset;
diagnostic.span.end += self.offset;
self.diagnostics.push(diagnostic);
}
}
fn scan_ocaml(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"(*") {
let mut reach = Reach::default();
let (end, closed) = ocaml_comment_end(bytes, index, &mut reach);
self.consult(reach);
self.add_comment(
index,
end,
if starts(bytes, index, b"(**") {
CommentKind::DocBlock
} else {
CommentKind::Block
},
);
if !closed {
self.error(
"unterminated-comment",
"unterminated OCaml comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
let mut reach = Reach::default();
let quoted = ocaml_quoted_string(bytes, index, &mut reach);
self.consult(reach);
if let Some((end, closed)) = quoted {
if !closed {
self.error(
"unterminated-string",
"unterminated OCaml quoted string",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if bytes[index] == b'"' {
index = self.quoted_or_error(index, true, "OCaml string");
continue;
}
if bytes[index] == b'\'' {
let mut reach = Reach::default();
let literal = ocaml_char_start(bytes, index, &mut reach);
self.consult(reach);
if literal {
index = self.quoted_or_error(index, false, "OCaml character literal");
continue;
}
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_python(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if bytes[index] == b'#' {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if let Some((quote_start, triple, formatted, raw)) = python_string_start(bytes, index) {
if formatted {
index = self.scan_python_fstring(index, quote_start, triple, raw, 0);
} else {
index = self.scan_python_delimited(index, quote_start, triple);
}
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_python_delimited(
&mut self,
token_start: usize,
quote_start: usize,
triple: bool,
) -> usize {
let bytes = self.source;
let length = if triple { 3 } else { 1 };
let delimiter = &bytes[quote_start..quote_start + length];
let mut index = quote_start + length;
while index < bytes.len() {
if starts(bytes, index, delimiter) {
return index + length;
}
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if !triple && matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
"unterminated Python string",
ByteSpan::new(token_start, index),
);
return index;
} else {
index += 1;
}
}
self.error(
"unterminated-string",
if triple {
"unterminated Python triple-quoted string"
} else {
"unterminated Python string"
},
ByteSpan::new(token_start, index),
);
index
}
fn scan_python_fstring(
&mut self,
token_start: usize,
quote_start: usize,
triple: bool,
_raw: bool,
depth: usize,
) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Python f-string nesting limit exceeded",
ByteSpan::new(token_start, token_start),
);
return self.source.len();
}
let bytes = self.source;
let length = if triple { 3 } else { 1 };
let delimiter = &bytes[quote_start..quote_start + length];
let mut index = quote_start + length;
while index < bytes.len() {
if starts(bytes, index, delimiter) {
return index + length;
}
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if starts(bytes, index, b"{{") || starts(bytes, index, b"}}") {
index += 2;
} else if bytes[index] == b'{' {
index = self.scan_python_expression(index + 1, depth + 1);
} else if !triple && matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
"unterminated Python f-string",
ByteSpan::new(token_start, index),
);
return index;
} else {
index += 1;
}
}
self.error(
"unterminated-string",
"unterminated Python f-string",
ByteSpan::new(token_start, index),
);
index
}
fn scan_python_expression(&mut self, mut index: usize, depth: usize) -> usize {
let bytes = self.source;
let mut braces = 1usize;
while index < bytes.len() {
if bytes[index] == b'#' {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if let Some((quote_start, triple, formatted, raw)) = python_string_start(bytes, index) {
index = if formatted {
self.scan_python_fstring(index, quote_start, triple, raw, depth + 1)
} else if triple {
self.scan_python_delimited(index, quote_start, true)
} else {
self.scan_python_delimited(index, quote_start, false)
};
continue;
}
match bytes[index] {
b'{' => {
braces += 1;
index += 1;
}
b'}' => {
braces -= 1;
index += 1;
if braces == 0 {
return index;
}
}
_ => index += 1,
}
}
self.error(
"unterminated-fstring-expression",
"unterminated Python f-string expression",
ByteSpan::new(index, index),
);
index
}
fn scan_toml(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
match bytes[index] {
b'#' => {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
}
b'"' | b'\'' => index = self.scan_toml_string(index),
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
}
_ => index += 1,
}
}
}
fn scan_toml_string(&mut self, start: usize) -> usize {
let bytes = self.source;
let quote = bytes[start];
let multiline = starts(bytes, start, &[quote, quote, quote]);
let escapes = quote == b'"';
let mut index = start + if multiline { 3 } else { 1 };
while index < bytes.len() {
if escapes && bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] != quote {
if !multiline && matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
"unterminated TOML string",
ByteSpan::new(start, index),
);
return index;
}
index += 1;
} else if !multiline {
return index + 1;
} else {
let run = toml_quote_run(bytes, index, quote);
if run >= 3 {
return index + run.min(5);
}
index += run;
}
}
self.error(
"unterminated-string",
if multiline {
"unterminated TOML multi-line string"
} else {
"unterminated TOML string"
},
ByteSpan::new(start, index),
);
index
}
fn scan_lua(&mut self) {
let bytes = self.source;
let mut index = 0;
let preamble = byte_order_mark_width(bytes);
if self.offset == 0 && bytes.get(preamble) == Some(&b'#') {
let end = line_end(bytes, preamble + 1);
self.add_comment(preamble, end, CommentKind::Line);
index = end;
}
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"--") {
index = self.scan_lua_comment(index);
continue;
}
match bytes[index] {
b'"' | b'\'' => index = self.scan_lua_short_string(index),
b'[' => index = self.scan_lua_long_string(index),
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
}
_ => index += 1,
}
}
}
fn scan_lua_comment(&mut self, start: usize) -> usize {
let bytes = self.source;
if let Some(level) = long_bracket_level(bytes, start + 2) {
let (end, closed) = long_bracket_end(bytes, start + 2 + level + 2, level);
self.add_comment(start, end, CommentKind::Block);
if !closed {
self.error(
"unterminated-comment",
"unterminated Lua long comment",
ByteSpan::new(start, end),
);
}
return end;
}
let end = line_end(bytes, start + 2);
self.add_comment(start, end, lua_line_kind(bytes, start));
end
}
fn scan_lua_long_string(&mut self, start: usize) -> usize {
let bytes = self.source;
let Some(level) = long_bracket_level(bytes, start) else {
return start + 1;
};
let (end, closed) = long_bracket_end(bytes, start + level + 2, level);
if !closed {
self.error(
"unterminated-string",
"unterminated Lua long string",
ByteSpan::new(start, end),
);
}
end
}
fn scan_lua_short_string(&mut self, start: usize) -> usize {
let bytes = self.source;
let quote = bytes[start];
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
let escaped = index + 1;
if bytes.get(escaped) == Some(&b'z') {
index = escaped + 1;
while bytes.get(index).is_some_and(|byte| lua_is_space(*byte)) {
index += 1;
}
} else if let Some(width) = lua_newline_width(bytes, escaped) {
index = escaped + width;
} else {
index = (escaped + 1).min(bytes.len());
}
} else if bytes[index] == quote {
return index + 1;
} else if lua_newline_width(bytes, index).is_some() {
self.error(
"unterminated-string",
"unterminated Lua string",
ByteSpan::new(start, index),
);
return index;
} else {
index += 1;
}
}
self.error(
"unterminated-string",
"unterminated Lua string",
ByteSpan::new(start, index),
);
index
}
fn scan_yaml(&mut self) {
let bytes = self.source;
let mut index = 0;
let mut line_start = 0;
let mut separated = true;
let mut node_start = true;
let mut token_column = None;
let mut owner_column = None;
while index < bytes.len() && !self.stopped {
match bytes[index] {
b'#' if separated => {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
}
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
line_start = index;
separated = true;
if !node_start {
owner_column = None;
}
node_start = true;
token_column = None;
if owner_column.is_none() {
self.add_safe_checkpoint(index);
}
}
b' ' | b'\t' => {
index += 1;
separated = true;
}
b'|' | b'>'
if node_start && separated && yaml_block_header(bytes, index).is_some() =>
{
let (indicator, chomping, comment, header_end) =
yaml_block_header(bytes, index).expect("the guard read the header");
if let Some(start) = comment {
self.add_comment(start, header_end, CommentKind::Line);
}
let base = owner_column.map_or(0, |column| column + 1);
let floor = base + indicator.unwrap_or(1) - 1;
let (end, boundary, detected) = yaml_block_body_end(bytes, header_end, floor);
self.yaml_blocks.push(YamlBlockScalar {
body_end: end + self.offset,
content_indent: indicator.map_or(detected, |_| floor),
chomping,
});
index = end;
line_start = index;
separated = true;
node_start = true;
token_column = None;
owner_column = None;
if boundary {
self.add_safe_checkpoint(index);
}
}
b'!' | b'&' if node_start && separated => {
if token_column.is_none() {
token_column = Some(index - line_start);
}
index = yaml_property_end(bytes, index);
separated = false;
}
b'"' | b'\'' if separated || yaml_flow_opener(bytes, index) => {
if node_start && token_column.is_none() {
token_column = Some(index - line_start);
}
index = self.scan_yaml_quoted(index);
separated = false;
node_start = false;
}
b'-' | b'?'
if node_start
&& bytes
.get(index + 1)
.is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n')) =>
{
owner_column = Some(index - line_start);
token_column = None;
index += 1;
separated = false;
}
b':' if bytes
.get(index + 1)
.is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n')) =>
{
owner_column = token_column.or(owner_column);
token_column = None;
node_start = true;
index += 1;
separated = false;
}
_ => {
if node_start {
if token_column.is_none() {
token_column = Some(index - line_start);
}
node_start = false;
}
index += 1;
separated = false;
}
}
}
if !self.stopped && !self.yaml_blocks.is_empty() {
let keeps = yaml_structural_trail_keeps(
self.source,
self.offset,
&self.yaml_blocks,
&self.comments,
);
for index in keeps {
self.comments[index].disposition = Disposition::Keep {
reason: YAML_STRUCTURAL_TRAIL.to_owned(),
};
}
}
}
fn scan_yaml_quoted(&mut self, start: usize) -> usize {
let bytes = self.source;
let quote = bytes[start];
let mut index = start + 1;
while index < bytes.len() {
if quote == b'"' && bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] != quote {
index += 1;
} else if quote == b'\'' && bytes.get(index + 1) == Some(&b'\'') {
index += 2;
} else {
return index + 1;
}
}
self.error(
"unterminated-string",
if quote == b'"' {
"unterminated YAML double-quoted scalar"
} else {
"unterminated YAML single-quoted scalar"
},
ByteSpan::new(start, index),
);
index
}
fn scan_php(&mut self) {
let bytes = self.source;
let mut index = 0;
if self.offset == 0 && starts(bytes, 0, b"#!") {
let end = line_end(bytes, 2);
self.add_comment(0, end, CommentKind::Line);
index = end;
}
while index < bytes.len() && !self.stopped {
match bytes[index] {
b'<' => match php_open_tag(bytes, index) {
Some(code) => index = self.scan_php_code(code),
None => index += 1,
},
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
}
_ => index += 1,
}
}
}
fn scan_php_code(&mut self, mut index: usize) -> usize {
let bytes = self.source;
while index < bytes.len() {
match bytes[index] {
b'?' if starts(bytes, index, b"?>") => {
let end = index + 2;
if matches!(bytes.get(end), Some(b'\r' | b'\n')) {
let next = consume_newline(bytes, end);
self.add_safe_checkpoint(next);
return next;
}
return end;
}
b'/' if starts(bytes, index, b"//") => {
let end = php_line_comment_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
}
b'/' if starts(bytes, index, b"/*") => {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
self.add_comment(index, end, php_block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated PHP block comment",
ByteSpan::new(index, end),
);
}
index = end;
}
b'#' if bytes.get(index + 1) == Some(&b'[') => index += 1,
b'#' => {
let end = php_line_comment_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
}
b'\'' | b'"' | b'`' => index = self.scan_php_quoted(index),
b'<' if starts(bytes, index, b"<<<") => index = self.scan_php_heredoc(index),
_ => index += 1,
}
}
index
}
fn scan_php_quoted(&mut self, start: usize) -> usize {
let bytes = self.source;
let quote = bytes[start];
let interpolates = quote != b'\'';
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == quote {
return index + 1;
} else if interpolates && bytes[index] == b'{' && bytes.get(index + 1) == Some(&b'$') {
index = php_interpolation_end(bytes, index);
} else if interpolates && bytes[index] == b'$' && bytes.get(index + 1) == Some(&b'{') {
index = php_interpolation_end(bytes, index + 1);
} else {
index += 1;
}
}
self.error(
"unterminated-string",
match quote {
b'\'' => "unterminated PHP single-quoted string",
b'"' => "unterminated PHP double-quoted string",
_ => "unterminated PHP backtick string",
},
ByteSpan::new(start, index),
);
index
}
fn scan_php_heredoc(&mut self, start: usize) -> usize {
let bytes = self.source;
let Some((label, body, nowdoc)) = php_heredoc_header(bytes, start) else {
return start + 1;
};
if let Some(end) = php_heredoc_end(bytes, body, label) {
return end;
}
self.error(
"unterminated-string",
if nowdoc {
"unterminated PHP nowdoc"
} else {
"unterminated PHP heredoc"
},
ByteSpan::new(start, bytes.len()),
);
bytes.len()
}
fn scan_ruby(&mut self) {
let mut pending = Vec::new();
let _ = self.scan_ruby_code(0, false, 0, &mut pending);
}
fn scan_ruby_code(
&mut self,
mut index: usize,
interpolation: bool,
depth: usize,
pending: &mut Vec<RubyHeredoc>,
) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Ruby lexical nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let offset = self.offset;
let mut state = RubyState::Begin;
let mut space_seen = false;
let mut braces = usize::from(interpolation);
let base = pending.len();
while index < bytes.len() && !self.stopped {
match bytes[index] {
b'#' => {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
}
b'=' if ruby_at_line_start(bytes, index, offset)
&& ruby_embedded_document(bytes, index) =>
{
let (end, closed) = ruby_embedded_document_end(bytes, index);
self.add_comment(index, end, CommentKind::Block);
if !closed {
self.error(
"unterminated-comment",
"unterminated Ruby embedded document",
ByteSpan::new(index, end),
);
}
index = end;
state = RubyState::Begin;
space_seen = false;
}
b'_' if ruby_at_line_start(bytes, index, offset)
&& ruby_data_marker(bytes, index) =>
{
index = bytes.len();
}
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
if !pending.is_empty() {
let opened = std::mem::take(pending);
match self.scan_ruby_heredoc_bodies(index, opened, depth + 1, pending) {
Some(end) => index = end,
None => {
index = bytes.len();
continue;
}
}
}
state = RubyState::Begin;
space_seen = false;
if !interpolation && depth == 0 && pending.is_empty() {
self.add_safe_checkpoint(index);
}
}
b'\'' => {
index = self.scan_ruby_string(index, false, depth, pending);
state = RubyState::End;
space_seen = false;
}
b'"' | b'`' => {
index = self.scan_ruby_string(index, true, depth, pending);
state = RubyState::End;
space_seen = false;
}
b':' if starts(bytes, index, b"::") => {
index += 2;
state = RubyState::End;
space_seen = false;
}
b':' if matches!(bytes.get(index + 1), Some(b'\'' | b'"')) => {
index =
self.scan_ruby_string(index + 1, bytes[index + 1] == b'"', depth, pending);
state = RubyState::End;
space_seen = false;
}
b':' if bytes
.get(index + 1)
.is_some_and(|byte| ruby_symbol_head(*byte)) =>
{
index = ruby_symbol_end(bytes, index);
state = RubyState::End;
space_seen = false;
}
b'?' => {
match ruby_character_literal_end(bytes, index) {
Some(end) if !matches!(state, RubyState::End | RubyState::Fname) => {
index = end;
state = RubyState::End;
}
_ => {
index += 1;
state = RubyState::Begin;
}
}
space_seen = false;
}
b'%' => {
match ruby_percent_header(bytes, index) {
Some(literal)
if ruby_percent_opens(
state,
space_seen,
bytes,
index,
literal.form,
) =>
{
let fitem = state == RubyState::Fname && literal.form == b's';
index = self.scan_ruby_percent(index, &literal, depth, pending);
state = if fitem {
RubyState::Fname
} else {
RubyState::End
};
}
_ => {
index += 1;
state = RubyState::Begin;
}
}
space_seen = false;
}
b'/' => {
if ruby_literal_opens(state, space_seen, bytes, index) {
index = self.scan_ruby_regexp(index, depth, pending);
state = RubyState::End;
} else {
index += 1;
state = RubyState::Begin;
}
space_seen = false;
}
b'<' if starts(bytes, index, b"<<") => {
match ruby_heredoc_header(bytes, index) {
Some((heredoc, end)) if ruby_heredoc_may_open(state, space_seen) => {
pending.push(heredoc);
index = end;
state = RubyState::End;
}
_ => {
index += 2;
state = RubyState::Begin;
}
}
space_seen = false;
}
b'$' => {
index = ruby_global_end(bytes, index);
state = RubyState::End;
space_seen = false;
}
b'@' => {
index = ruby_at_variable_end(bytes, index);
state = RubyState::End;
space_seen = false;
}
b'{' => {
braces += 1;
index += 1;
state = RubyState::Begin;
space_seen = false;
}
b'}' => {
index += 1;
if interpolation {
braces -= 1;
if braces == 0 {
return index;
}
}
state = RubyState::End;
space_seen = false;
}
b'(' | b'[' => {
index += 1;
state = RubyState::Begin;
space_seen = false;
}
b')' | b']' => {
index += 1;
state = RubyState::End;
space_seen = false;
}
b'.' => {
index += 1;
state = RubyState::End;
space_seen = false;
}
b'\\' if matches!(bytes.get(index + 1), Some(b'\r' | b'\n')) => {
index = consume_newline(bytes, index + 1);
space_seen = true;
}
b'\\' => {
index = (index + 2).min(bytes.len());
state = RubyState::End;
space_seen = false;
}
byte if byte.is_ascii_digit() => {
index = ruby_number_end(bytes, index);
state = RubyState::End;
space_seen = false;
}
byte if ruby_identifier_start(byte) => {
let start = index;
index = ruby_word_end(bytes, index);
state = ruby_state_after_word(&bytes[start..index]);
space_seen = false;
}
byte if ruby_is_space(byte) => {
index += 1;
space_seen = true;
}
_ => {
index += 1;
state = RubyState::Begin;
space_seen = false;
}
}
}
if let Some(operator) = pending.get(base).map(|heredoc| heredoc.operator) {
self.error(
"unterminated-heredoc",
"unterminated Ruby here document",
ByteSpan::new(operator, bytes.len()),
);
pending.truncate(base);
}
if interpolation {
self.error(
"unterminated-interpolation",
"unterminated Ruby interpolation",
ByteSpan::new(index, index),
);
}
index
}
fn scan_ruby_string(
&mut self,
start: usize,
interpolates: bool,
depth: usize,
pending: &mut Vec<RubyHeredoc>,
) -> usize {
let bytes = self.source;
let quote = bytes[start];
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == quote {
return index + 1;
} else if interpolates && starts(bytes, index, b"#{") {
index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
} else {
index += 1;
}
}
self.error(
"unterminated-string",
match quote {
b'\'' => "unterminated Ruby single-quoted string",
b'"' => "unterminated Ruby double-quoted string",
_ => "unterminated Ruby backtick string",
},
ByteSpan::new(start, index),
);
index
}
fn scan_ruby_regexp(
&mut self,
start: usize,
depth: usize,
pending: &mut Vec<RubyHeredoc>,
) -> usize {
let bytes = self.source;
let mut index = start + 1;
let mut in_class = false;
while index < bytes.len() {
match bytes[index] {
b'\\' => index = (index + 2).min(bytes.len()),
b'[' => {
in_class = true;
index += 1;
}
b']' => {
in_class = false;
index += 1;
}
b'/' if !in_class => return ruby_regexp_flags_end(bytes, index + 1),
b'#' if starts(bytes, index, b"#{") => {
index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
}
_ => index += 1,
}
}
self.error(
"unterminated-string",
"unterminated Ruby regular expression",
ByteSpan::new(start, index),
);
index
}
fn scan_ruby_percent(
&mut self,
start: usize,
literal: &RubyPercent,
depth: usize,
pending: &mut Vec<RubyHeredoc>,
) -> usize {
let bytes = self.source;
let mut index = literal.content;
let mut nesting = 1usize;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
continue;
}
if literal.interpolates && starts(bytes, index, b"#{") {
index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
continue;
}
if literal.open != literal.close && bytes[index] == literal.open {
nesting += 1;
index += 1;
continue;
}
if bytes[index] == literal.close {
nesting -= 1;
index += 1;
if nesting == 0 {
return if literal.form == b'r' {
ruby_regexp_flags_end(bytes, index)
} else {
index
};
}
continue;
}
index += 1;
}
self.error(
"unterminated-string",
"unterminated Ruby percent literal",
ByteSpan::new(start, index),
);
index
}
fn scan_ruby_heredoc_bodies(
&mut self,
mut index: usize,
heredocs: Vec<RubyHeredoc>,
depth: usize,
pending: &mut Vec<RubyHeredoc>,
) -> Option<usize> {
for heredoc in heredocs {
match self.scan_ruby_heredoc_body(index, &heredoc, depth, pending) {
Some(end) => index = end,
None => {
self.error(
"unterminated-heredoc",
"unterminated Ruby here document",
ByteSpan::new(heredoc.operator, self.source.len()),
);
return None;
}
}
}
Some(index)
}
fn scan_ruby_heredoc_body(
&mut self,
mut index: usize,
heredoc: &RubyHeredoc,
depth: usize,
pending: &mut Vec<RubyHeredoc>,
) -> Option<usize> {
let bytes = self.source;
loop {
if index >= bytes.len() {
return None;
}
if ruby_heredoc_terminates(bytes, index, heredoc) {
return Some(consume_newline(bytes, line_end(bytes, index)).min(bytes.len()));
}
while index < bytes.len() && !matches!(bytes[index], b'\r' | b'\n') {
if heredoc.interpolates && bytes[index] == b'\\' {
index = if starts(bytes, index, b"\\\r\n") {
index + 3
} else {
(index + 2).min(bytes.len())
};
} else if heredoc.interpolates && starts(bytes, index, b"#{") {
index = self.scan_ruby_code(index + 2, true, depth + 1, pending);
} else {
index += 1;
}
}
if index >= bytes.len() {
return None;
}
index = consume_newline(bytes, index);
if !pending.is_empty() {
let opened = std::mem::take(pending);
index = self.scan_ruby_heredoc_bodies(index, opened, depth + 1, pending)?;
}
}
}
fn scan_zig(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"//") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, zig_line_kind(bytes, index));
index = end;
continue;
}
if starts(bytes, index, b"\\\\") {
index = line_end(bytes, index + 2);
continue;
}
match bytes[index] {
b'"' | b'\'' => index = self.scan_zig_quoted(index),
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
}
_ => index += 1,
}
}
}
fn scan_zig_quoted(&mut self, start: usize) -> usize {
let bytes = self.source;
let quote = bytes[start];
let message = if quote == b'"' {
"unterminated Zig string"
} else {
"unterminated Zig character literal"
};
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' && !matches!(bytes.get(index + 1), None | Some(b'\r' | b'\n'))
{
index += 2;
} else if bytes[index] == quote {
return index + 1;
} else if matches!(bytes[index], b'\r' | b'\n') {
self.error("unterminated-string", message, ByteSpan::new(start, index));
return index;
} else {
index += 1;
}
}
self.error("unterminated-string", message, ByteSpan::new(start, index));
index
}
fn scan_r(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
match bytes[index] {
b'#' => {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, r_line_kind(bytes, index));
index = end;
}
b'"' | b'\'' => index = self.scan_r_string(index),
b'`' => index = self.scan_r_name(index),
b'%' => index = self.scan_r_operator(index),
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
}
_ => index += 1,
}
}
}
fn scan_r_string(&mut self, start: usize) -> usize {
let bytes = self.source;
if let Some((end, closed)) = r_raw_string(bytes, start) {
if !closed {
self.error(
"unterminated-string",
"unterminated R raw string",
ByteSpan::new(start - 1, end),
);
}
return end;
}
let (end, closed) = r_delimited_end(bytes, start + 1, bytes[start]);
if !closed {
self.error(
"unterminated-string",
"unterminated R string",
ByteSpan::new(start, end),
);
}
end
}
fn scan_r_name(&mut self, start: usize) -> usize {
let bytes = self.source;
let (end, closed) = r_delimited_end(bytes, start + 1, b'`');
if !closed {
self.error(
"unterminated-identifier",
"unterminated R backquoted name",
ByteSpan::new(start, end),
);
}
end
}
fn scan_r_operator(&mut self, start: usize) -> usize {
let bytes = self.source;
let stop = line_end(bytes, start + 1);
match memchr(b'%', &bytes[start + 1..stop]) {
Some(relative) => start + relative + 2,
None => {
self.error(
"unterminated-operator",
"unterminated R special operator",
ByteSpan::new(start, stop),
);
stop
}
}
}
fn scan_dart(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if starts(bytes, index, b"//") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, dart_line_kind(bytes, index));
index = end;
continue;
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
self.add_comment(index, end, dart_block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated Dart block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
match bytes[index] {
b'"' | b'\'' => index = self.scan_dart_string(index, 0),
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
}
_ => index += 1,
}
}
}
fn scan_dart_string(&mut self, quote: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Dart string interpolation nesting limit exceeded",
ByteSpan::new(quote, quote),
);
return self.source.len();
}
let bytes = self.source;
let raw = dart_raw_string_prefix(bytes, quote);
let start = if raw { quote - 1 } else { quote };
let triple = bytes.get(quote + 1) == Some(&bytes[quote])
&& bytes.get(quote + 2) == Some(&bytes[quote]);
let width = if triple { 3 } else { 1 };
let delimiter = &bytes[quote..quote + width];
let mut index = quote + width;
while index < bytes.len() {
if bytes[index..].starts_with(delimiter) {
return index + width;
}
if !raw
&& bytes[index] == b'\\'
&& !matches!(bytes.get(index + 1), None | Some(b'\r' | b'\n'))
{
index += 2;
continue;
}
if !raw && starts(bytes, index, b"${") {
index = self.scan_dart_interpolation(index + 2, depth + 1);
continue;
}
if !triple && matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
dart_unterminated_string(raw, triple),
ByteSpan::new(start, index),
);
return index;
}
index += 1;
}
self.error(
"unterminated-string",
dart_unterminated_string(raw, triple),
ByteSpan::new(start, index),
);
index
}
fn scan_dart_interpolation(&mut self, mut index: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Dart string interpolation nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut braces = 1usize;
while index < bytes.len() {
if starts(bytes, index, b"//") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, dart_line_kind(bytes, index));
index = end;
continue;
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
self.add_comment(index, end, dart_block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated Dart block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
match bytes[index] {
b'"' | b'\'' => index = self.scan_dart_string(index, depth + 1),
b'{' => {
braces += 1;
index += 1;
}
b'}' => {
braces -= 1;
index += 1;
if braces == 0 {
return index;
}
}
_ => index += 1,
}
}
self.error(
"unterminated-template-expression",
"unterminated Dart string interpolation",
ByteSpan::new(index, index),
);
index
}
fn scan_swift(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if let Some(end) = self.scan_swift_lexeme(index, 0) {
index = end;
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_swift_lexeme(&mut self, index: usize, depth: usize) -> Option<usize> {
let bytes = self.source;
if starts(bytes, index, b"//") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, swift_line_kind(bytes, index));
return Some(end);
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
self.add_comment(index, end, swift_block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated Swift block comment",
ByteSpan::new(index, end),
);
}
return Some(end);
}
if bytes[index] == b'/' {
let mut reach = Reach::default();
let regex = swift_bare_regex(bytes, index, &mut reach);
self.consult(reach);
return regex;
}
if bytes[index] == b'#' {
let mut reach = Reach::default();
let hashes = swift_hash_run(bytes, index, &mut reach);
self.consult(reach);
let opener = index + hashes;
if bytes.get(opener) == Some(&b'/') {
return Some(self.scan_swift_extended_regex(index, hashes));
}
if bytes.get(opener) == Some(&b'"') {
return Some(self.scan_swift_string(index, hashes, depth));
}
return Some(opener);
}
if matches!(bytes[index], b'"' | b'\'') {
return Some(self.scan_swift_string(index, 0, depth));
}
None
}
fn scan_swift_string(&mut self, start: usize, hashes: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Swift string interpolation nesting limit exceeded",
ByteSpan::new(start, start),
);
return self.source.len();
}
let bytes = self.source;
let quote = start + hashes;
let delimiter = bytes[quote];
let multiline = delimiter == b'"' && swift_multiline_string(bytes, quote, hashes);
let width = if multiline { 3 } else { 1 };
let mut index = quote + width;
while index < bytes.len() {
if let Some(end) = swift_string_close(bytes, index, delimiter, multiline, hashes) {
return end;
}
if bytes[index] == b'\\' && swift_hashes_at(bytes, index + 1, hashes) {
let escaped = index + 1 + hashes;
if bytes.get(escaped) == Some(&b'(') {
index = self.scan_swift_interpolation(escaped + 1, depth + 1);
continue;
}
if !matches!(bytes.get(escaped), None | Some(b'\r' | b'\n')) {
index = escaped + 1;
continue;
}
}
if !multiline && matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
swift_unterminated_string(delimiter, hashes > 0, multiline),
ByteSpan::new(start, index),
);
return index;
}
index += 1;
}
self.error(
"unterminated-string",
swift_unterminated_string(delimiter, hashes > 0, multiline),
ByteSpan::new(start, index),
);
index
}
fn scan_swift_interpolation(&mut self, mut index: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Swift string interpolation nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut parentheses = 1usize;
while index < bytes.len() {
if let Some(end) = self.scan_swift_lexeme(index, depth) {
index = end;
continue;
}
match bytes[index] {
b'(' => {
parentheses += 1;
index += 1;
}
b')' => {
parentheses -= 1;
index += 1;
if parentheses == 0 {
return index;
}
}
_ => index += 1,
}
}
self.error(
"unterminated-interpolation",
"unterminated Swift string interpolation",
ByteSpan::new(index, index),
);
index
}
fn scan_swift_extended_regex(&mut self, start: usize, hashes: usize) -> usize {
let bytes = self.source;
let mut reach = Reach::default();
let opener = start + hashes + 1;
let mut probe = opener;
while matches!(bytes.get(probe), Some(b' ' | b'\t')) {
probe += 1;
}
reach.byte(probe);
let multiline = bytes
.get(probe)
.is_some_and(|byte| is_line_terminator(*byte));
let mut index = if multiline { probe } else { opener };
while index < bytes.len() {
if !multiline && is_line_terminator(bytes[index]) {
break;
}
if bytes[index] == b'\\' {
let escaped = index + 1;
if !multiline
&& bytes
.get(escaped)
.is_some_and(|byte| is_line_terminator(*byte))
{
index = escaped;
break;
}
index = (escaped + 1).min(bytes.len());
continue;
}
if bytes[index] == b'/' && swift_hashes_at(bytes, index + 1, hashes) {
let mut end = index + 1;
while bytes.get(end) == Some(&b'#') {
end += 1;
}
reach.byte(end);
self.consult(reach);
return end;
}
index += 1;
}
let end = if multiline {
reach.end_of(bytes);
probe
} else {
reach.byte(index);
index
};
self.consult(reach);
self.error(
"unterminated-regex",
"unterminated Swift extended regular expression literal",
ByteSpan::new(start, end),
);
end
}
fn scan_csharp(&mut self) {
let bytes = self.source;
let mut index = if self.offset == 0 {
byte_order_mark_width(bytes)
} else {
0
};
let mut blank_line = true;
while index < bytes.len() && !self.stopped {
if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
let end = csharp_line_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
blank_line = false;
continue;
}
if bytes[index] == b'#' {
index = self.scan_csharp_directive(index, blank_line);
blank_line = false;
continue;
}
if let Some(end) = self.scan_csharp_lexeme(index, 0) {
index = end;
blank_line = false;
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
blank_line = true;
continue;
}
if let Some(width) = csharp_unicode_line_terminator_width(bytes, index) {
index += width;
blank_line = true;
continue;
}
blank_line = blank_line && is_csharp_blank(bytes[index]);
index += 1;
}
}
fn scan_csharp_lexeme(&mut self, index: usize, depth: usize) -> Option<usize> {
let bytes = self.source;
if starts(bytes, index, b"//") {
let end = csharp_line_end(bytes, index + 2);
self.add_comment(index, end, csharp_line_kind(bytes, index));
return Some(end);
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
self.add_comment(index, end, csharp_block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated C# block comment",
ByteSpan::new(index, end),
);
}
return Some(end);
}
if bytes[index] == b'\'' {
return Some(self.scan_csharp_character(index));
}
if !matches!(bytes[index], b'"' | b'@' | b'$') {
return None;
}
let mut reach = Reach::default();
let prefix = csharp_literal_prefix(bytes, index, &mut reach);
self.consult(reach);
match prefix {
Some(prefix) => Some(self.scan_csharp_string(prefix, depth)),
None => Some(csharp_prefix_end(bytes, index)),
}
}
fn scan_csharp_character(&mut self, start: usize) -> usize {
let bytes = self.source;
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
continue;
}
if bytes[index] == b'\'' {
return index + 1;
}
if csharp_line_terminator_width(bytes, index).is_some() {
self.error(
"unterminated-string",
"unterminated C# character literal",
ByteSpan::new(start, index),
);
return index;
}
index += 1;
}
self.error(
"unterminated-string",
"unterminated C# character literal",
ByteSpan::new(start, index),
);
index
}
fn scan_csharp_directive(&mut self, index: usize, line_initial: bool) -> usize {
let bytes = self.source;
let end = csharp_line_end(bytes, index);
if !line_initial {
return end;
}
let mut cursor = index + 1;
while cursor < end && is_csharp_blank(bytes[cursor]) {
cursor += 1;
}
let name = cursor;
while cursor < end && bytes[cursor].is_ascii_alphabetic() {
cursor += 1;
}
if csharp_directive_takes_a_message(&bytes[name..cursor]) {
while cursor < end && is_csharp_blank(bytes[cursor]) {
cursor += 1;
}
if starts(bytes, cursor, b"//") {
self.add_comment(cursor, end, CommentKind::Line);
}
return end;
}
let mut token = index + 1;
while token < end {
if starts(bytes, token, b"//") {
self.add_comment(token, end, CommentKind::Line);
return end;
}
if bytes[token] == b'"' {
token += 1;
while token < end && bytes[token] != b'"' {
token += 1;
}
token = (token + 1).min(end);
continue;
}
token += 1;
}
end
}
fn scan_csharp_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"C# string interpolation nesting limit exceeded",
ByteSpan::new(prefix.start, prefix.start),
);
return self.source.len();
}
match prefix.form {
CsharpStringForm::Raw => self.scan_csharp_raw_string(prefix, depth),
CsharpStringForm::Verbatim => self.scan_csharp_verbatim_string(prefix, depth),
CsharpStringForm::Plain => self.scan_csharp_plain_string(prefix, depth),
}
}
fn scan_csharp_plain_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
let bytes = self.source;
let mut index = prefix.quote + 1;
while index < bytes.len() {
match bytes[index] {
b'\\' => index = (index + 2).min(bytes.len()),
b'"' => return index + 1,
b'{' if prefix.dollars > 0 => {
if bytes.get(index + 1) == Some(&b'{') {
index += 2;
continue;
}
index = self.scan_csharp_hole(index + 1, prefix.dollars, depth + 1);
}
b'}' if prefix.dollars > 0 && bytes.get(index + 1) == Some(&b'}') => index += 2,
_ if csharp_line_terminator_width(bytes, index).is_some() => {
self.error(
"unterminated-string",
csharp_unterminated_string(prefix.form, prefix.dollars > 0),
ByteSpan::new(prefix.start, index),
);
return index;
}
_ => index += 1,
}
}
self.error(
"unterminated-string",
csharp_unterminated_string(prefix.form, prefix.dollars > 0),
ByteSpan::new(prefix.start, index),
);
index
}
fn scan_csharp_verbatim_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
let bytes = self.source;
let mut index = prefix.quote + 1;
while index < bytes.len() {
match bytes[index] {
b'"' if bytes.get(index + 1) == Some(&b'"') => index += 2,
b'"' => return index + 1,
b'{' if prefix.dollars > 0 => {
if bytes.get(index + 1) == Some(&b'{') {
index += 2;
continue;
}
index = self.scan_csharp_hole(index + 1, prefix.dollars, depth + 1);
}
b'}' if prefix.dollars > 0 && bytes.get(index + 1) == Some(&b'}') => index += 2,
_ => index += 1,
}
}
self.error(
"unterminated-string",
csharp_unterminated_string(prefix.form, prefix.dollars > 0),
ByteSpan::new(prefix.start, index),
);
index
}
fn scan_csharp_raw_string(&mut self, prefix: CsharpPrefix, depth: usize) -> usize {
let bytes = self.source;
let multiline = csharp_multiline_raw_string(bytes, prefix.quote + prefix.quotes);
let mut index = prefix.quote + prefix.quotes;
while index < bytes.len() {
match bytes[index] {
b'"' => {
let mut end = index;
while bytes.get(end) == Some(&b'"') {
end += 1;
}
if end - index >= prefix.quotes {
let mut reach = Reach::default();
reach.byte(end);
self.consult(reach);
return end;
}
index = end;
}
b'{' if prefix.dollars > 0 => {
let mut run = index;
while bytes.get(run) == Some(&b'{') {
run += 1;
}
if run - index < prefix.dollars {
index = run;
continue;
}
index = self.scan_csharp_hole(run, prefix.dollars, depth + 1);
}
_ if !multiline && csharp_line_terminator_width(bytes, index).is_some() => {
self.error(
"unterminated-string",
csharp_unterminated_string(prefix.form, prefix.dollars > 0),
ByteSpan::new(prefix.start, index),
);
return index;
}
_ => index += 1,
}
}
self.error(
"unterminated-string",
csharp_unterminated_string(prefix.form, prefix.dollars > 0),
ByteSpan::new(prefix.start, index),
);
index
}
fn scan_csharp_hole(&mut self, mut index: usize, braces: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"C# string interpolation nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut open = 0usize;
while index < bytes.len() {
if let Some(end) = self.scan_csharp_lexeme(index, depth) {
index = end;
continue;
}
match bytes[index] {
b'(' | b'[' | b'{' => {
open += 1;
index += 1;
}
b')' | b']' => {
open = open.saturating_sub(1);
index += 1;
}
b'}' => {
if open > 0 {
open -= 1;
index += 1;
continue;
}
let (next, closed) = csharp_hole_close(bytes, index, braces);
if closed {
return next;
}
index = next;
}
b':' if open == 0 => return self.scan_csharp_format(index, braces),
_ => index += 1,
}
}
self.error(
"unterminated-interpolation",
"unterminated C# string interpolation",
ByteSpan::new(index, index),
);
index
}
fn scan_csharp_format(&mut self, mut index: usize, braces: usize) -> usize {
let bytes = self.source;
while index < bytes.len() {
if bytes[index] == b'}' {
let (next, closed) = csharp_hole_close(bytes, index, braces);
if closed {
return next;
}
index = next;
continue;
}
index += 1;
}
self.error(
"unterminated-interpolation",
"unterminated C# string interpolation",
ByteSpan::new(index, index),
);
index
}
fn scan_scala(&mut self) {
let bytes = self.source;
let mut index = if self.offset == 0 {
byte_order_mark_width(bytes)
} else {
0
};
while index < bytes.len() && !self.stopped {
if self.offset == 0
&& index == byte_order_mark_width(bytes)
&& starts(bytes, index, b"#!")
{
let end = line_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if let Some(end) = self.scan_scala_lexeme(index, 0) {
index = end;
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_scala_lexeme(&mut self, index: usize, depth: usize) -> Option<usize> {
let bytes = self.source;
if starts(bytes, index, b"//") {
let end = line_end(bytes, index + 2);
self.add_comment(index, end, scala_line_kind(bytes, index));
return Some(end);
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", true);
self.add_comment(index, end, scala_block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated Scala block comment",
ByteSpan::new(index, end),
);
}
return Some(end);
}
if bytes[index] == b'"' {
return Some(self.scan_scala_string(index, depth));
}
if bytes[index] == b'\''
&& let Some(end) = scala_character_literal_end(bytes, index)
{
return Some(end);
}
if bytes[index] == b'`' {
return Some(self.scala_backquoted_identifier(index));
}
if bytes[index] == b'<' && scala_is_xml_start(bytes, index) {
return Some(self.scan_scala_xml(index, depth));
}
None
}
fn scan_scala_string(&mut self, start: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Scala string interpolation nesting limit exceeded",
ByteSpan::new(start, start),
);
return self.source.len();
}
let bytes = self.source;
let interpolated = scala_interpolator(bytes, start);
let triple = starts(bytes, start, b"\"\"\"");
let mut index = start + if triple { 3 } else { 1 };
while index < bytes.len() {
if interpolated && bytes[index] == b'$' {
match bytes.get(index + 1) {
Some(b'$' | b'"') => {
index += 2;
continue;
}
Some(b'{') => {
index = self.scan_scala_expression(index + 2, depth + 1);
continue;
}
Some(&byte) if scala_identifier_start(byte) => {
index += 2;
while index < bytes.len() && scala_identifier_part(bytes[index]) {
index += 1;
}
continue;
}
_ => {
index += 1;
continue;
}
}
}
if bytes[index] == b'"' {
if triple {
let run = index + count_run(bytes, index, b'"');
if run - index >= 3 {
return run;
}
} else {
return index + 1;
}
} else if bytes[index] == b'\\' {
if !triple {
index = (index + 2).min(bytes.len());
continue;
}
} else if !triple && matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-string",
"unterminated Scala string",
ByteSpan::new(start, index),
);
return index;
}
index += 1;
}
self.error(
"unterminated-string",
if triple {
"unterminated Scala multi-line string"
} else {
"unterminated Scala string"
},
ByteSpan::new(start, index),
);
index
}
fn scan_scala_expression(&mut self, mut index: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"Scala string interpolation nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut braces = 1usize;
while index < bytes.len() {
if let Some(end) = self.scan_scala_lexeme(index, depth) {
index = end;
continue;
}
match bytes[index] {
b'{' => {
braces += 1;
index += 1;
}
b'}' => {
braces -= 1;
index += 1;
if braces == 0 {
return index;
}
}
_ => index += 1,
}
}
self.error(
"unterminated-template-expression",
"unterminated Scala string-template expression",
ByteSpan::new(index, index),
);
index
}
fn scala_backquoted_identifier(&mut self, start: usize) -> usize {
let bytes = self.source;
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'`' {
return index + 1;
}
if matches!(bytes[index], b'\r' | b'\n') {
self.error(
"unterminated-identifier",
"unclosed quoted identifier",
ByteSpan::new(start, index),
);
return index;
}
index += 1;
}
self.error(
"unterminated-identifier",
"unclosed quoted identifier",
ByteSpan::new(start, index),
);
index
}
fn scan_scala_xml(&mut self, start: usize, depth: usize) -> usize {
let bytes = self.source;
let Some((mut index, self_closing, root_start, root_end)) =
self.scala_xml_tag(start, depth)
else {
return bytes.len();
};
if self_closing {
return index;
}
let mut stack = vec![(root_start, root_end)];
while index < bytes.len() {
if bytes[index] == b'{' {
index = self.scan_scala_expression(index + 1, depth + 1);
continue;
}
if bytes[index] != b'<' {
index += 1;
continue;
}
if starts(bytes, index, b"<!--") {
let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") else {
return bytes.len();
};
let end = index + 4 + relative + 3;
self.add_comment(index, end, CommentKind::HtmlComment);
index = end;
continue;
}
if starts(bytes, index, b"<![CDATA[") {
let Some(relative) = find_subslice(&bytes[index + 9..], b"]]>") else {
return bytes.len();
};
index = index + 9 + relative + 3;
continue;
}
if starts(bytes, index, b"<?") {
let Some(relative) = find_subslice(&bytes[index + 2..], b"?>") else {
return bytes.len();
};
index = index + 2 + relative + 2;
continue;
}
if starts(bytes, index, b"</") {
let name_start = index + 2;
if name_start < bytes.len() && xml_name_start(bytes[name_start]) {
let name_end = name_start + xml_name_len(&bytes[name_start..]);
let Some(close_end) = skip_xml_tag_tail(bytes, name_end) else {
return bytes.len();
};
if bytes[name_start..name_end]
== bytes[stack.last().unwrap().0..stack.last().unwrap().1]
{
stack.pop();
if stack.is_empty() {
return close_end;
}
index = close_end;
continue;
}
}
index += 2;
continue;
}
if starts(bytes, index, b"<!") {
let Some(relative) = find_subslice(&bytes[index + 2..], b">") else {
return bytes.len();
};
index = index + 2 + relative + 1;
continue;
}
if index + 1 < bytes.len() && xml_name_start(bytes[index + 1]) {
let Some((after, self_closing, name_start, name_end)) =
self.scala_xml_tag(index, depth)
else {
return bytes.len();
};
if !self_closing {
stack.push((name_start, name_end));
}
index = after;
continue;
}
index += 1;
}
bytes.len()
}
fn scala_xml_tag(&mut self, start: usize, depth: usize) -> Option<(usize, bool, usize, usize)> {
let bytes = self.source;
let mut index = start + 1;
if index >= bytes.len() || !xml_name_start(bytes[index]) {
return None;
}
let name_start = index;
index += xml_name_len(&bytes[index..]);
let name_end = index;
loop {
if index >= bytes.len() {
return None;
}
match bytes[index] {
b'>' => return Some((index + 1, false, name_start, name_end)),
b'/' if bytes.get(index + 1) == Some(&b'>') => {
return Some((index + 2, true, name_start, name_end));
}
b'"' | b'\'' => {
let quote = bytes[index];
index += 1;
while index < bytes.len() && bytes[index] != quote {
index += 1;
}
if index >= bytes.len() {
return None;
}
index += 1;
}
b'{' => {
index = self.scan_scala_expression(index + 1, depth + 1);
}
_ => index += 1,
}
}
}
fn scan_shell(&mut self) {
let _ = self.scan_shell_region(0, None, 0);
}
fn scan_shell_region(
&mut self,
mut index: usize,
terminator: Option<ShellTerminator>,
depth: usize,
) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"shell lexical nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut heredocs: Vec<Heredoc> = Vec::new();
let backtick_terminator = matches!(terminator, Some(ShellTerminator::Backtick(_)));
let parenthesis_terminator = matches!(terminator, Some(ShellTerminator::Parenthesis(_)));
let mut parentheses = usize::from(parenthesis_terminator);
let mut word_open = false;
let mut command_position = true;
let mut case_states = Vec::new();
while index < bytes.len() && !self.stopped {
match bytes[index] {
b'#' if !word_open => {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
}
b'\'' => {
let start = index;
let (end, closed) = shell_single_quote_end(bytes, index);
index = end;
if !closed {
self.error(
"unterminated-string",
"unterminated shell single quote",
ByteSpan::new(start, index),
);
}
word_open = true;
command_position = false;
}
b'"' => {
index = self.scan_shell_double_quote(index, depth + 1);
word_open = true;
command_position = false;
}
b'`' if backtick_terminator => return index + 1,
b'`' => {
index = self.scan_shell_region(
index + 1,
Some(ShellTerminator::Backtick(index)),
depth + 1,
);
word_open = true;
command_position = false;
}
b'$' if bytes.get(index + 1) == Some(&b'(') => {
index = self.scan_shell_region(
index + 2,
Some(ShellTerminator::Parenthesis(index)),
depth + 1,
);
word_open = true;
command_position = false;
}
b'$' if bytes.get(index + 1) == Some(&b'\'')
&& matches!(self.options.dialect, Dialect::Bash53 | Dialect::Zsh) =>
{
index = self.quoted_or_error(index + 1, true, "shell ANSI-C quoted string");
word_open = true;
command_position = false;
}
b'<' if bytes.get(index + 1) == Some(&b'<') => {
word_open = false;
if bytes.get(index + 2) == Some(&b'<') {
index += 3;
} else {
let mut reach = Reach::default();
let parsed = parse_heredoc(bytes, index, &mut reach);
self.consult(reach);
if let Some((heredoc, end)) = parsed {
heredocs.push(heredoc);
index = end;
word_open = true;
} else {
index += 1;
}
}
}
b'\r' | b'\n' if !heredocs.is_empty() => {
index = consume_newline(bytes, index);
for heredoc in heredocs.drain(..) {
match heredoc_body_end(bytes, index, &heredoc) {
Some(end) => index = end,
None => {
self.error(
"unterminated-heredoc",
"unterminated shell heredoc",
ByteSpan::new(heredoc.operator, bytes.len()),
);
return bytes.len();
}
}
}
word_open = false;
command_position = case_states.last() != Some(&ShellCaseState::Pattern);
if terminator.is_none() && case_states.is_empty() {
self.add_safe_checkpoint(index);
}
}
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
word_open = false;
command_position = case_states.last() != Some(&ShellCaseState::Pattern);
if terminator.is_none() && case_states.is_empty() {
self.add_safe_checkpoint(index);
}
}
b'(' if parenthesis_terminator => {
parentheses += 1;
index += 1;
word_open = false;
command_position = case_states.last() != Some(&ShellCaseState::Pattern);
}
b')' if parenthesis_terminator => {
if parentheses == 1
&& let Some(state @ ShellCaseState::Pattern) = case_states.last_mut()
{
*state = ShellCaseState::Body;
index += 1;
word_open = false;
command_position = true;
continue;
}
parentheses = parentheses.saturating_sub(1);
index += 1;
if parentheses == 0 {
return index;
}
word_open = false;
command_position = true;
}
b')' if case_states.last() == Some(&ShellCaseState::Pattern) => {
*case_states.last_mut().expect("case state exists") = ShellCaseState::Body;
index += 1;
word_open = false;
command_position = true;
}
b';' if case_states.last() == Some(&ShellCaseState::Body)
&& (starts(bytes, index, b";;") || starts(bytes, index, b";&")) =>
{
let width = if starts(bytes, index, b";;&") { 3 } else { 2 };
*case_states.last_mut().expect("case state exists") = ShellCaseState::Pattern;
index += width;
word_open = false;
command_position = false;
}
b';' | b'&' | b'|' | b'(' | b')' => {
index += 1;
word_open = false;
command_position = bytes[index - 1] != b'|'
|| case_states.last() != Some(&ShellCaseState::Pattern);
}
b'<' | b'>' => {
index += 1;
word_open = false;
}
byte if byte.is_ascii_whitespace() => {
index += 1;
word_open = false;
}
b'\\' => {
if bytes.get(index + 1) == Some(&b'\r') && bytes.get(index + 2) == Some(&b'\n')
{
index += 3;
} else if matches!(bytes.get(index + 1), Some(b'\r' | b'\n')) {
index += 2;
} else {
index = (index + 2).min(bytes.len());
word_open = true;
command_position = false;
}
}
byte if !word_open && (byte.is_ascii_alphabetic() || byte == b'_') => {
let start = index;
index += 1;
while index < bytes.len()
&& (bytes[index].is_ascii_alphanumeric() || bytes[index] == b'_')
{
index += 1;
}
let boundary = bytes.get(index).is_none_or(|byte| {
byte.is_ascii_whitespace()
|| matches!(byte, b';' | b'&' | b'|' | b'(' | b')' | b'<' | b'>')
});
let token = &bytes[start..index];
if boundary && token == b"case" && command_position {
case_states.push(ShellCaseState::AwaitIn);
command_position = false;
} else if boundary
&& token == b"in"
&& case_states.last() == Some(&ShellCaseState::AwaitIn)
{
*case_states.last_mut().expect("case state exists") =
ShellCaseState::Pattern;
command_position = false;
} else if boundary
&& token == b"esac"
&& (command_position
|| case_states.last() == Some(&ShellCaseState::Pattern))
{
let _ = case_states.pop();
command_position = false;
} else {
command_position = command_position && bytes.get(index) == Some(&b'=');
}
word_open = true;
}
_ => {
index += 1;
word_open = true;
command_position = false;
}
}
}
if let Some(terminator) = terminator {
let (code, message, start) = match terminator {
ShellTerminator::Parenthesis(start) => (
"unterminated-command-substitution",
"unterminated shell command substitution",
start,
),
ShellTerminator::Backtick(start) => (
"unterminated-string",
"unterminated shell command substitution",
start,
),
};
self.error(code, message, ByteSpan::new(start, index));
}
index
}
fn scan_shell_double_quote(&mut self, start: usize, depth: usize) -> usize {
let bytes = self.source;
let mut index = start + 1;
while index < bytes.len() {
match bytes[index] {
b'\\' => index = (index + 2).min(bytes.len()),
b'"' => return index + 1,
b'$' if bytes.get(index + 1) == Some(&b'(') => {
index = self.scan_shell_region(
index + 2,
Some(ShellTerminator::Parenthesis(index)),
depth + 1,
);
}
b'`' => {
index = self.scan_shell_region(
index + 1,
Some(ShellTerminator::Backtick(index)),
depth + 1,
);
}
_ => index += 1,
}
}
self.error(
"unterminated-string",
"unterminated shell double quote",
ByteSpan::new(start, index),
);
index
}
fn scan_sql(&mut self) {
let bytes = self.source;
let nested = matches!(self.options.dialect, Dialect::PostgreSql | Dialect::TSql);
let mut index = 0;
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"--")
&& (self.options.dialect != Dialect::MySql
|| mysql_dash_comment_boundary(bytes.get(index + 2).copied()))
{
let end = line_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if bytes[index] == b'#' && self.options.dialect == Dialect::MySql {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if starts(bytes, index, b"/*") {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", nested);
self.add_comment(index, end, CommentKind::Block);
if !closed {
self.error(
"unterminated-comment",
"unterminated SQL block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if bytes[index] == b'\'' {
let start = index;
let backslash_escapes = self.options.dialect == Dialect::MySql
|| (self.options.dialect == Dialect::PostgreSql
&& postgres_escape_string_start(bytes, index));
let (end, closed) = sql_quoted_end(bytes, index, b'\'', backslash_escapes);
index = end;
if !closed {
self.error(
"unterminated-string",
"unterminated SQL string",
ByteSpan::new(start, index),
);
}
continue;
}
if matches!(bytes[index], b'"' | b'`') {
let mysql_string = bytes[index] == b'"' && self.options.dialect == Dialect::MySql;
let (end, closed) = if mysql_string {
sql_quoted_end(bytes, index, b'"', true)
} else {
sql_identifier_end(bytes, index, bytes[index])
};
if !closed {
self.error(
if mysql_string {
"unterminated-string"
} else {
"unterminated-identifier"
},
if mysql_string {
"unterminated MySQL quoted string"
} else {
"unterminated SQL quoted identifier"
},
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if bytes[index] == b'[' && self.options.dialect == Dialect::TSql {
let (end, closed) = sql_identifier_end(bytes, index, b']');
if !closed {
self.error(
"unterminated-identifier",
"unterminated T-SQL bracket identifier",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
let dollar = (bytes[index] == b'$' && self.options.dialect == Dialect::PostgreSql)
.then(|| {
let mut reach = Reach::default();
let quoted = sql_dollar_quote_end(bytes, index, &mut reach);
self.consult(reach);
quoted
})
.flatten();
if let Some((end, closed)) = dollar {
if !closed {
self.error(
"unterminated-string",
"unterminated PostgreSQL dollar-quoted string",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
let q_quote = ((bytes[index] == b'q' || bytes[index] == b'Q')
&& self.options.dialect == Dialect::Oracle)
.then(|| {
let mut reach = Reach::default();
let quoted = oracle_q_quote_end(bytes, index, &mut reach);
self.consult(reach);
quoted
})
.flatten();
if let Some((end, closed)) = q_quote {
if !closed {
self.error(
"unterminated-string",
"unterminated Oracle q-quoted string",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_javascript(&mut self) {
let _ = self.scan_js_code(0, None, 0);
}
fn scan_js_code(&mut self, mut index: usize, stop_brace: Option<usize>, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"JavaScript lexical nesting limit exceeded",
ByteSpan::new(index, index),
);
return self.source.len();
}
let bytes = self.source;
let mut brace_depth = stop_brace.unwrap_or(0);
let mut regex_allowed = true;
let mut control_parentheses = Vec::new();
let mut pending_control_parenthesis = false;
let mut brace_blocks = Vec::new();
let mut statement_start = stop_brace.is_none();
let mut pending_block = false;
while index < bytes.len() && !self.stopped {
if index == 0 && self.offset == 0 && starts(bytes, index, b"#!") {
let end = js_line_end(bytes, index + 2);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if bytes[index] == b'/' {
match bytes.get(index + 1) {
Some(b'/') => {
let end = js_line_end(bytes, index + 2);
self.add_comment(index, end, line_kind(bytes, index));
index = end;
continue;
}
Some(b'*') => {
let (end, closed) = block_end(bytes, index, b"/*", b"*/", false);
self.add_comment(index, end, block_kind(bytes, index));
if !closed {
self.error(
"unterminated-comment",
"unterminated JavaScript block comment",
ByteSpan::new(index, end),
);
}
index = end;
continue;
}
_ => {}
}
}
if (bytes[index] == b'<' && starts(bytes, index, b"<!--"))
|| (bytes[index] == b'-' && js_html_close_comment(bytes, index))
{
let end = js_line_end(bytes, index + 3);
self.add_comment(index, end, CommentKind::Line);
index = end;
continue;
}
if matches!(self.options.dialect, Dialect::Jsx | Dialect::Tsx)
&& regex_allowed
&& jsx_open(bytes, index)
{
index = self.scan_jsx_element(index, depth + 1);
regex_allowed = false;
pending_control_parenthesis = false;
statement_start = false;
pending_block = false;
continue;
}
match bytes[index] {
b'\'' | b'"' => {
index = self.js_quoted_or_error(index);
regex_allowed = false;
pending_control_parenthesis = false;
statement_start = false;
pending_block = false;
}
b'`' => {
index = self.scan_js_template(index, depth + 1);
regex_allowed = false;
pending_control_parenthesis = false;
statement_start = false;
pending_block = false;
}
b'/' if regex_allowed => {
if let Some(end) = js_regex_end(bytes, index) {
index = end;
regex_allowed = false;
} else {
index += 1;
regex_allowed = true;
}
pending_control_parenthesis = false;
statement_start = false;
pending_block = false;
}
b'{' => {
let is_block = pending_block || !regex_allowed || statement_start;
brace_blocks.push(is_block);
if stop_brace.is_some() {
brace_depth += 1;
}
index += 1;
regex_allowed = true;
pending_control_parenthesis = false;
statement_start = is_block;
pending_block = false;
}
b'}' => {
if stop_brace.is_some() {
brace_depth = brace_depth.saturating_sub(1);
}
index += 1;
if stop_brace.is_some() && brace_depth == 0 {
return index;
}
let is_block = brace_blocks.pop().unwrap_or(true);
regex_allowed = is_block;
pending_control_parenthesis = false;
statement_start = is_block;
pending_block = false;
}
byte if is_js_identifier_start(byte) || byte.is_ascii_digit() => {
let start = index;
index += 1;
while index < bytes.len() && is_js_identifier_continue(bytes[index]) {
index += 1;
}
let token = &bytes[start..index];
pending_control_parenthesis = is_js_control_keyword(token);
pending_block = matches!(token, b"else" | b"do" | b"try" | b"finally");
regex_allowed = pending_control_parenthesis
|| matches!(
token,
b"return"
| b"throw"
| b"case"
| b"delete"
| b"void"
| b"typeof"
| b"yield"
| b"await"
| b"new"
| b"in"
| b"of"
| b"else"
| b"do"
);
statement_start = false;
}
b'(' => {
control_parentheses.push(pending_control_parenthesis);
pending_control_parenthesis = false;
index += 1;
regex_allowed = true;
statement_start = false;
pending_block = false;
}
b')' => {
let control = control_parentheses.pop().unwrap_or(false);
regex_allowed = control;
pending_control_parenthesis = false;
index += 1;
statement_start = control;
pending_block = control;
}
b']' => {
index += 1;
regex_allowed = false;
pending_control_parenthesis = false;
statement_start = false;
pending_block = false;
}
b'+' | b'-' if bytes.get(index + 1) == Some(&bytes[index]) => {
index += 2;
regex_allowed = false;
pending_control_parenthesis = false;
statement_start = false;
pending_block = false;
}
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
if stop_brace.is_none()
&& depth == 0
&& regex_allowed
&& statement_start
&& !pending_control_parenthesis
&& !pending_block
&& control_parentheses.is_empty()
&& brace_blocks.is_empty()
{
self.add_safe_checkpoint(index);
}
}
byte if js_is_space(byte) => index += 1,
b'=' if bytes.get(index + 1) == Some(&b'>') => {
index += 2;
regex_allowed = true;
pending_control_parenthesis = false;
statement_start = true;
pending_block = true;
}
b';' => {
index += 1;
regex_allowed = true;
pending_control_parenthesis = false;
statement_start = true;
pending_block = false;
}
b':' => {
index += 1;
regex_allowed = true;
pending_control_parenthesis = false;
statement_start = brace_blocks.last().copied().unwrap_or(true);
pending_block = false;
}
_ => {
regex_allowed = true;
pending_control_parenthesis = false;
statement_start = false;
pending_block = false;
index += 1;
}
}
}
if stop_brace.is_some() {
self.error(
"unterminated-template-expression",
"unterminated JavaScript template expression",
ByteSpan::new(index, index),
);
}
index
}
fn scan_jsx_element(&mut self, start: usize, depth: usize) -> usize {
if depth > 256 {
self.error(
"nesting-limit",
"JSX lexical nesting limit exceeded",
ByteSpan::new(start, start),
);
return self.source.len();
}
let bytes = self.source;
let mut index = start;
let mut element_depth = 0usize;
while index < bytes.len() {
if bytes[index] == b'{' {
index = self.scan_js_code(index + 1, Some(1), depth + 1);
continue;
}
if bytes[index] != b'<' {
index += 1;
continue;
}
let closing = bytes.get(index + 1) == Some(&b'/');
let opening = jsx_open(bytes, index);
if !closing && !opening {
index += 1;
continue;
}
let mut cursor = index + if closing { 2 } else { 1 };
let mut quote = None;
let mut self_closing = false;
let mut found_end = false;
while cursor < bytes.len() {
if let Some(active) = quote {
if bytes[cursor] == b'\\' {
cursor = (cursor + 2).min(bytes.len());
} else {
if bytes[cursor] == active {
quote = None;
}
cursor += 1;
}
continue;
}
match bytes[cursor] {
b'\'' | b'"' => {
quote = Some(bytes[cursor]);
cursor += 1;
}
b'{' if !closing => {
cursor = self.scan_js_code(cursor + 1, Some(1), depth + 1);
}
b'>' => {
let mut previous = cursor;
while previous > index && js_is_space(bytes[previous - 1]) {
previous -= 1;
}
self_closing = previous > index && bytes[previous - 1] == b'/';
cursor += 1;
found_end = true;
break;
}
_ => cursor += 1,
}
}
if !found_end {
self.error(
"unterminated-jsx-tag",
"unterminated JSX tag",
ByteSpan::new(index, bytes.len()),
);
return bytes.len();
}
index = cursor;
if closing {
element_depth = element_depth.saturating_sub(1);
if element_depth == 0 {
return index;
}
} else if !self_closing {
element_depth += 1;
} else if element_depth == 0 {
return index;
}
}
self.error(
"unterminated-jsx-element",
"unterminated JSX element",
ByteSpan::new(start, bytes.len()),
);
bytes.len()
}
fn scan_js_template(&mut self, start: usize, depth: usize) -> usize {
let bytes = self.source;
let mut index = start + 1;
while index < bytes.len() {
match bytes[index] {
b'\\' => index = (index + 2).min(bytes.len()),
b'`' => return index + 1,
b'$' if bytes.get(index + 1) == Some(&b'{') => {
index = self.scan_js_code(index + 2, Some(1), depth);
}
_ => index += 1,
}
}
self.error(
"unterminated-string",
"unterminated JavaScript template literal",
ByteSpan::new(start, index),
);
index
}
fn scan_html(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"<!--") {
let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
index + 4 + relative + 3
} else {
self.error(
"unterminated-comment",
"unterminated HTML comment",
ByteSpan::new(index, bytes.len()),
);
bytes.len()
};
self.add_comment(index, end, CommentKind::HtmlComment);
index = end;
continue;
}
if bytes[index] == b'<' {
if let Some((name, language)) = html_embedded_start(bytes, index) {
let Some(content_start) = html_tag_end(bytes, index) else {
self.error(
"unterminated-html-tag",
"unterminated HTML raw-text start tag",
ByteSpan::new(index, bytes.len()),
);
return;
};
let close = find_html_close(bytes, content_start, name);
let content_end = close.unwrap_or(bytes.len());
let slice = &bytes[content_start..content_end];
let mut child = Scanner::child(
slice,
language,
self.options.clone(),
self.patterns.clone(),
self.offset + content_start,
);
if language == Language::JavaScript {
child.scan_javascript();
} else {
child.scan_c_family();
}
self.merge_child(child);
let Some(close) = close else {
self.error(
"unterminated-embedded-language",
"unterminated HTML script or style element",
ByteSpan::new(index, bytes.len()),
);
return;
};
let Some(element_end) = html_tag_end(bytes, close) else {
self.error(
"unterminated-html-tag",
"unterminated HTML raw-text end tag",
ByteSpan::new(close, bytes.len()),
);
return;
};
index = element_end;
continue;
}
if !html_tag_candidate(bytes, index) {
index += 1;
} else if let Some(end) = html_tag_end(bytes, index) {
index = end;
} else {
self.error(
"unterminated-html-tag",
"unterminated HTML tag",
ByteSpan::new(index, bytes.len()),
);
return;
}
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_vue(&mut self) {
self.scan_sfc(true);
}
fn scan_svelte(&mut self) {
self.scan_sfc(false);
}
fn scan_perl(&mut self) {
let bytes = self.source;
let mut index = 0;
let mut regex_allowed: Option<bool> = Some(true);
while index < bytes.len() && !self.stopped {
if perl_at_line_start(bytes, index)
&& (perl_marker_line(bytes, index, b"__DATA__")
|| perl_marker_line(bytes, index, b"__END__"))
{
break;
}
if (index == 0 || matches!(bytes[index - 1], b'\n' | b'\r'))
&& bytes[index] == b'='
&& bytes
.get(index + 1)
.is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
&& !perl_pod_directive(bytes, index, b"cut")
{
index = self.scan_perl_pod(index);
continue;
}
if bytes[index] == b'#' {
let end = line_end(bytes, index + 1);
self.add_comment(index, end, CommentKind::Line);
index = end;
regex_allowed = Some(true);
continue;
}
if matches!(bytes[index], b'\'' | b'"' | b'`') {
index = self.scan_perl_quoted(index);
regex_allowed = Some(false);
continue;
}
if bytes[index] == b'<'
&& bytes.get(index + 1) == Some(&b'<')
&& let Some(end) = self.scan_perl_heredocs(index)
{
index = end;
regex_allowed = Some(false);
continue;
}
if (index == 0 || !is_perl_word_byte(bytes[index - 1]))
&& matches!(bytes[index], b'q' | b'm' | b's' | b't' | b'y')
&& let Some(end) = self.scan_perl_quote_word(index)
{
index = end;
regex_allowed = Some(false);
continue;
}
if bytes[index] == b'/' {
match regex_allowed {
Some(true) => {
let end = self.scan_perl_regex(index);
index = end;
regex_allowed = Some(false);
}
Some(false) => {
index += 1;
regex_allowed = Some(true);
}
None => {
let end = self.scan_perl_regex(index);
self.error(
"lexical-ambiguity",
"ambiguous `/` after a closing delimiter: a regex or a division",
ByteSpan::new(index, end),
);
index = end;
regex_allowed = Some(false);
}
}
continue;
}
if bytes[index] == b')' || bytes[index] == b']' || bytes[index] == b'}' {
index += 1;
regex_allowed = None;
continue;
}
if bytes[index] == b'(' || bytes[index] == b'[' || bytes[index] == b'{' {
index += 1;
regex_allowed = Some(true);
continue;
}
if matches!(bytes[index], b'$' | b'@' | b'%') {
index = perl_variable_end(bytes, index);
regex_allowed = Some(false);
continue;
}
if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' {
let start = index;
index += 1;
while index < bytes.len() && is_perl_word_byte(bytes[index]) {
index += 1;
}
let word = &bytes[start..index];
if word == b"format"
&& bytes[line_start(bytes, start)..start]
.iter()
.all(|byte| matches!(byte, b' ' | b'\t'))
&& let Some(end) = self.scan_perl_format(start)
{
index = end;
regex_allowed = Some(true);
continue;
}
regex_allowed = perl_word_allows_regex(word);
continue;
}
if bytes[index].is_ascii_digit() {
while index < bytes.len()
&& (bytes[index].is_ascii_alphanumeric() || matches!(bytes[index], b'.' | b'_'))
{
index += 1;
}
regex_allowed = Some(false);
continue;
}
match bytes[index] {
b'=' | b'+' | b'-' | b'*' | b'%' | b'!' | b'~' | b'&' | b'|' | b'?' | b':'
| b',' | b';' | b'<' | b'>' => {
index += 1;
regex_allowed = Some(true);
}
b'\r' | b'\n' => {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
regex_allowed = Some(true);
}
_ => index += 1,
}
}
}
fn scan_perl_pod(&mut self, start: usize) -> usize {
let bytes = self.source;
let mut index = start;
while index < bytes.len() {
let line_finish = line_end(bytes, index);
if index != start && perl_pod_directive(bytes, index, b"cut") {
return if line_finish >= bytes.len() {
line_finish
} else {
consume_newline(bytes, line_finish)
};
}
if index == start {
let pod_line = line_end(bytes, start);
if pod_line >= bytes.len() {
return bytes.len();
}
index = consume_newline(bytes, pod_line);
continue;
}
if line_finish >= bytes.len() {
return bytes.len();
}
index = consume_newline(bytes, line_finish);
}
bytes.len()
}
fn scan_perl_quoted(&mut self, start: usize) -> usize {
perl_quoted_end(self.source, start)
}
fn scan_perl_quote_word(&mut self, start: usize) -> Option<usize> {
let bytes = self.source;
let mut reach = Reach::default();
let mut cursor = start;
let mut form = Vec::new();
form.push(bytes[cursor]);
reach.byte(cursor);
if matches!(bytes[cursor], b'q' | b't')
&& let Some(&second) = bytes.get(cursor + 1)
&& ((bytes[cursor] == b'q' && matches!(second, b'q' | b'w' | b'x' | b'r'))
|| (bytes[cursor] == b't' && second == b'r'))
{
form.push(second);
cursor += 1;
reach.byte(cursor);
}
cursor += 1;
while cursor < bytes.len()
&& bytes[cursor].is_ascii_whitespace()
&& !matches!(bytes[cursor], b'\r' | b'\n')
{
reach.byte(cursor);
cursor += 1;
}
let Some(delimiter) = bytes.get(cursor).copied() else {
reach.end_of(bytes);
self.consult(reach);
return None;
};
reach.byte(cursor);
if is_perl_word_byte(delimiter) || delimiter.is_ascii_whitespace() {
self.consult(reach);
return None;
}
let Some(first) = perl_section_end_reach(bytes, cursor, delimiter, &mut reach) else {
self.consult(reach);
return None;
};
if matches!(form[0], b's' | b't' | b'y') {
let paired = matches!(delimiter, b'(' | b'[' | b'{' | b'<');
let second_end = if paired {
let mut second_start = first;
while bytes.get(second_start).is_some_and(|byte| {
byte.is_ascii_whitespace() && !matches!(byte, b'\r' | b'\n')
}) {
reach.byte(second_start);
second_start += 1;
}
let Some(second) = bytes.get(second_start).copied() else {
reach.end_of(bytes);
self.consult(reach);
return None;
};
reach.byte(second_start);
if is_perl_word_byte(second) || second.is_ascii_whitespace() {
self.consult(reach);
return None;
}
let Some(end) = perl_section_end_reach(bytes, second_start, second, &mut reach)
else {
self.consult(reach);
return None;
};
end
} else {
let Some(end) =
perl_unpaired_section_end_reach(bytes, first, delimiter, &mut reach)
else {
self.consult(reach);
return None;
};
end
};
let end = perl_modifiers_end(bytes, second_end);
reach.through(end);
self.consult(reach);
return Some(end);
}
let end = perl_modifiers_end(bytes, first);
reach.through(end);
self.consult(reach);
Some(end)
}
fn scan_perl_heredocs(&mut self, start: usize) -> Option<usize> {
let bytes = self.source;
let header_end = line_end(bytes, start);
let mut declarations = Vec::new();
let mut header_comment = None;
let mut search = start;
while search < header_end {
if matches!(bytes[search], b'\'' | b'"' | b'`') {
search = perl_quoted_end(bytes, search).min(header_end);
} else if bytes[search] == b'#' {
header_comment = Some(search);
break;
} else if matches!(bytes[search], b'$' | b'@' | b'%') {
search = perl_variable_end(bytes, search).min(header_end);
} else if starts(bytes, search, b"<<") {
let Some((declaration, end)) = perl_heredoc_declaration(bytes, search, header_end)
else {
search += 2;
continue;
};
declarations.push(declaration);
search = end;
} else {
search += 1;
}
}
if declarations.is_empty() {
return None;
}
if let Some(comment) = header_comment {
self.add_comment(comment, header_end, CommentKind::Line);
}
let mut body = if header_end >= bytes.len() {
return Some(bytes.len());
} else {
consume_newline(bytes, header_end)
};
for declaration in declarations {
let mut found = false;
while body < bytes.len() {
let line_finish = line_end(bytes, body);
let mut content = body;
if declaration.indented {
while content < line_finish && matches!(bytes[content], b' ' | b'\t') {
content += 1;
}
}
if bytes[content..line_finish] == declaration.terminator[..] {
body = if line_finish >= bytes.len() {
line_finish
} else {
consume_newline(bytes, line_finish)
};
found = true;
break;
}
body = if line_finish >= bytes.len() {
bytes.len()
} else {
consume_newline(bytes, line_finish)
};
}
if !found {
return Some(bytes.len());
}
}
Some(body)
}
fn scan_perl_format(&self, start: usize) -> Option<usize> {
let bytes = self.source;
let header_end = line_end(bytes, start);
if !bytes[start + b"format".len()..header_end].contains(&b'=') {
return None;
}
let mut line = if header_end < bytes.len() {
consume_newline(bytes, header_end)
} else {
return Some(bytes.len());
};
while line < bytes.len() {
let finish = line_end(bytes, line);
if bytes[line..finish].trim_ascii() == b"." {
return Some(if finish < bytes.len() {
consume_newline(bytes, finish)
} else {
finish
});
}
line = if finish < bytes.len() {
consume_newline(bytes, finish)
} else {
bytes.len()
};
}
Some(bytes.len())
}
fn scan_perl_regex(&mut self, start: usize) -> usize {
let bytes = self.source;
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
continue;
}
if bytes[index] == b'[' {
index = (index + 1).min(bytes.len());
while index < bytes.len() && bytes[index] != b']' {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else {
index += 1;
}
}
index += 1;
continue;
}
if bytes[index] == b'/' {
index += 1;
while index < bytes.len() && is_perl_word_byte(bytes[index]) {
index += 1;
}
return index;
}
index += 1;
}
bytes.len()
}
fn scan_markdown(&mut self) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"<!--") {
let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
index + 4 + relative + 3
} else {
self.error(
"unterminated-comment",
"unterminated HTML comment",
ByteSpan::new(index, bytes.len()),
);
bytes.len()
};
self.add_comment(index, end, CommentKind::HtmlComment);
index = end;
continue;
}
if index == 0 || matches!(bytes[index - 1], b'\r' | b'\n') {
let line_finish = line_end(bytes, index);
let (cursor, indent) = markdown_indent(bytes, index, line_finish);
if indent >= 4 {
index = self.scan_markdown_indented(index);
continue;
}
if indent <= 3 && cursor < bytes.len() && matches!(bytes[cursor], b'`' | b'~') {
let marker = bytes[cursor];
let run = count_run(bytes, cursor, marker);
let info_end = line_end(bytes, cursor + run);
let valid_info =
marker != b'`' || !bytes[cursor + run..info_end].contains(&b'`');
if run >= 3 && valid_info {
index = self.scan_markdown_fence(cursor, marker, run);
continue;
}
}
}
if bytes[index] == b'`' {
let mut reach = Reach::default();
index = markdown_inline_code_end(bytes, index, &mut reach);
self.consult(reach);
continue;
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_markdown_fence(&mut self, opener: usize, marker: u8, run: usize) -> usize {
let bytes = self.source;
let info_start = opener + run;
let info_end = line_end(bytes, info_start);
let language = markdown_fence_language(&bytes[info_start..info_end]);
let mut cursor = info_end;
let closer = loop {
if cursor >= bytes.len() {
break None;
}
let mut line = cursor;
let mut spaces = 0;
while line < bytes.len() && bytes[line] == b' ' && spaces < 3 {
line += 1;
spaces += 1;
}
if bytes.get(line) == Some(&marker) {
let closer_run = count_run(bytes, line, marker);
if closer_run >= run {
let mut after = line + closer_run;
while after < bytes.len() && matches!(bytes[after], b' ' | b'\t') {
after += 1;
}
if after >= bytes.len() || matches!(bytes[after], b'\r' | b'\n') {
break Some((line, consume_newline(bytes, after).min(bytes.len())));
}
}
}
let line_finish = line_end(bytes, cursor);
if line_finish >= bytes.len() {
break None;
}
cursor = consume_newline(bytes, line_finish);
};
let content_end = closer.map_or(bytes.len(), |(line, _)| line);
if let Some(language) = language
&& !matches!(
language,
Language::Html | Language::Vue | Language::Svelte | Language::Markdown
)
{
let mut child = Scanner::child(
&bytes[info_end..content_end],
language,
self.options.clone(),
self.patterns.clone(),
self.offset + info_end,
);
child.scan_language();
self.merge_child(child);
}
closer.map_or(bytes.len(), |(_, resume)| resume)
}
fn scan_markdown_indented(&mut self, start: usize) -> usize {
let bytes = self.source;
let mut index = start;
while index < bytes.len() {
let line_finish = line_end(bytes, index);
let (cursor, indent) = markdown_indent(bytes, index, line_finish);
let blank = cursor >= line_finish;
if !blank && indent < 4 {
return index;
}
if line_finish >= bytes.len() {
return bytes.len();
}
index = consume_newline(bytes, line_finish);
}
bytes.len()
}
fn scan_sfc(&mut self, vue: bool) {
let bytes = self.source;
let mut index = 0;
while index < bytes.len() && !self.stopped {
if starts(bytes, index, b"<!--") {
let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
index + 4 + relative + 3
} else {
self.error(
"unterminated-comment",
"unterminated HTML comment",
ByteSpan::new(index, bytes.len()),
);
bytes.len()
};
self.add_comment(index, end, CommentKind::HtmlComment);
index = end;
continue;
}
if bytes[index] == b'{' && !vue {
index = self.scan_js_code(index + 1, Some(1), 0);
continue;
}
if bytes[index] == b'<' {
if let Some(end) = self.scan_sfc_block(index, vue) {
index = end;
continue;
}
if html_tag_candidate(bytes, index) {
let end = if vue {
sfc_tag_end(bytes, index)
} else {
self.scan_svelte_tag_end(index)
};
if let Some(end) = end {
index = end;
continue;
}
}
}
if matches!(bytes[index], b'\r' | b'\n') {
index = consume_newline(bytes, index);
self.add_safe_checkpoint(index);
} else {
index += 1;
}
}
}
fn scan_sfc_block(&mut self, start: usize, vue: bool) -> Option<usize> {
let bytes = self.source;
let rest = &bytes[start..];
let name: &[u8] = if starts_ascii_case(rest, b"<script")
&& tag_boundary(rest.get(7).copied())
{
b"script"
} else if starts_ascii_case(rest, b"<style") && tag_boundary(rest.get(6).copied()) {
b"style"
} else if vue && starts_ascii_case(rest, b"<template") && tag_boundary(rest.get(9).copied())
{
b"template"
} else {
return None;
};
let tag_end = if vue {
sfc_tag_end(bytes, start)
} else {
self.scan_svelte_tag_end(start)
};
let Some(tag_end) = tag_end else {
self.error(
"unterminated-html-tag",
"unterminated single-file component start tag",
ByteSpan::new(start, bytes.len()),
);
return Some(bytes.len());
};
let attrs = &bytes[start + 1 + name.len()..tag_end.saturating_sub(1)];
let lang = tag_attr_value(attrs, b"lang");
let Some(close) = find_html_close(bytes, tag_end, name) else {
self.error(
"unterminated-embedded-language",
"unterminated single-file component element",
ByteSpan::new(start, bytes.len()),
);
return Some(bytes.len());
};
let content_start = tag_end;
let content_end = close;
let resolved = match name {
b"script" => vue_script_language(lang),
b"style" => vue_style_language(lang),
b"template" => {
let html = lang.is_none_or(|value| {
let lower = value.to_ascii_lowercase();
lower == b"html"
});
if html {
Some((Language::Html, Dialect::Standard))
} else {
None
}
}
_ => None,
};
match resolved {
Some((Language::Html, _)) => {
self.scan_sfc_template(vue, content_start, content_end);
}
Some((language, dialect)) => {
let mut child_options = self.options.clone();
child_options.dialect = dialect;
let mut child = Scanner::child(
&bytes[content_start..content_end],
language,
child_options,
self.patterns.clone(),
self.offset + content_start,
);
match language {
Language::JavaScript | Language::TypeScript => child.scan_javascript(),
Language::Css if dialect == Dialect::Sass => child.scan_sass(),
Language::Css => child.scan_c_family(),
_ => {}
}
self.merge_child(child);
}
None => {}
}
Some(content_end)
}
fn scan_sfc_template(&mut self, vue: bool, start: usize, end: usize) {
let bytes = self.source;
let mut index = start;
while index < end {
if starts(bytes, index, b"<!--") {
let end = if let Some(relative) = find_subslice(&bytes[index + 4..], b"-->") {
index + 4 + relative + 3
} else {
self.error(
"unterminated-comment",
"unterminated HTML comment",
ByteSpan::new(index, bytes.len()),
);
bytes.len()
};
self.add_comment(index, end, CommentKind::HtmlComment);
index = end;
continue;
}
if starts(bytes, index, b"{{") && vue {
index = self.scan_js_code(index + 2, Some(2), 0);
continue;
}
if bytes[index] == b'<'
&& html_tag_candidate(bytes, index)
&& let Some(tag_end) = sfc_tag_end(bytes, index)
{
let name_end = html_tag_name_end(bytes, index);
let attrs = &bytes[name_end..tag_end.saturating_sub(1)];
if vue && tag_has_attribute(attrs, b"v-pre") {
let name = &bytes[index + 1..name_end];
if let Some(close) = find_balanced_html_close(bytes, tag_end, name) {
index = html_tag_end(bytes, close).unwrap_or(close);
continue;
}
}
self.scan_sfc_attributes(vue, name_end, tag_end.saturating_sub(1));
index = tag_end;
continue;
}
index += 1;
}
}
fn scan_sfc_attributes(&mut self, vue: bool, start: usize, end: usize) {
let parsed = parse_tag_attributes(&self.source[start..end]);
if vue {
for attribute in parsed {
let name = &self.source[start + attribute.name.start..start + attribute.name.end];
if !vue_directive_attribute(name) {
continue;
}
let Some(value) = attribute.value else {
continue;
};
let value_start = start + value.start;
let value_end = start + value.end;
let mut child = Scanner::child(
&self.source[value_start..value_end],
Language::JavaScript,
self.options.clone(),
self.patterns.clone(),
self.offset + value_start,
);
child.scan_javascript();
self.merge_child(child);
}
return;
}
let mut index = start;
while index < end {
if self.source[index] == b'{' {
let next = self.scan_js_code(index + 1, Some(1), 0);
index = next.max(index + 1).min(end);
} else {
index += 1;
}
}
}
fn scan_svelte_tag_end(&mut self, start: usize) -> Option<usize> {
let comments = self.comments.len();
let diagnostics = self.diagnostics.len();
let mut index = start + 1;
let mut quote = None;
while index < self.source.len() {
if let Some(active) = quote {
if self.source[index] == active {
quote = None;
index += 1;
} else if self.source[index] == b'{' {
index = self.scan_js_code(index + 1, Some(1), 0);
} else {
index += 1;
}
continue;
}
match self.source[index] {
b'\'' | b'"' => {
quote = Some(self.source[index]);
index += 1;
}
b'{' => index = self.scan_js_code(index + 1, Some(1), 0),
b'>' => return Some(index + 1),
_ => index += 1,
}
}
self.comments.truncate(comments);
self.diagnostics.truncate(diagnostics);
None
}
}
#[derive(Clone, Debug)]
pub struct DispositionPatterns {
keep: RegexSet,
remove: RegexSet,
keep_active: bool,
remove_active: bool,
}
impl DispositionPatterns {
pub fn compile(options: &ScanOptions) -> Result<Self, regex::Error> {
Ok(Self {
keep: RegexSet::new(&options.keep_regex)?,
remove: RegexSet::new(&options.remove_regex)?,
keep_active: !options.keep_regex.is_empty(),
remove_active: !options.remove_regex.is_empty(),
})
}
pub fn empty() -> Self {
Self {
keep: RegexSet::empty(),
remove: RegexSet::empty(),
keep_active: false,
remove_active: false,
}
}
}
pub(crate) fn disposition(
kind: CommentKind,
options: &ScanOptions,
raw: &[u8],
patterns: &DispositionPatterns,
) -> Disposition {
if options.keep_kinds.contains(&kind) || (patterns.keep_active && patterns.keep.is_match(raw)) {
return Disposition::Keep {
reason: "kept by kind or regex override".into(),
};
}
let hard = matches!(kind, CommentKind::Shebang | CommentKind::Encoding);
if hard && !options.force_protected {
return Disposition::Keep {
reason: "required source preamble".into(),
};
}
if options.remove_kinds.contains(&kind)
|| (patterns.remove_active && patterns.remove.is_match(raw))
{
return Disposition::Remove;
}
if options.policy == Policy::All {
return Disposition::Remove;
}
if kind == CommentKind::HtmlComment {
return Disposition::Keep {
reason: "HTML comments are DOM-observable".into(),
};
}
if matches!(
kind,
CommentKind::Directive | CommentKind::OptimizerHint | CommentKind::VersionComment
) {
return Disposition::Keep {
reason: "tool or language directive".into(),
};
}
if kind == CommentKind::License && options.policy == Policy::Legal {
return Disposition::Keep {
reason: "legal policy".into(),
};
}
Disposition::Remove
}
fn first_match(set: &RegexSet, raw: &[u8], sources: &[String]) -> Option<(usize, String)> {
let index = set.matches(raw).iter().next()?;
let pattern = sources.get(index).cloned().unwrap_or_default();
Some((index, pattern))
}
fn directive_name_of(raw: &[u8], language: Language) -> Option<&'static str> {
let lower = String::from_utf8_lossy(strip_comment_markers(raw)).to_ascii_lowercase();
directive_name(lower.trim(), language, raw)
}
fn legal_marker_of(raw: &[u8]) -> Option<&'static str> {
let lower = String::from_utf8_lossy(strip_comment_markers(raw)).to_ascii_lowercase();
legal_marker(lower.trim())
}
pub fn explain_disposition(
kind: CommentKind,
raw: &[u8],
language: Language,
options: &ScanOptions,
) -> DispositionExplanation {
let patterns =
DispositionPatterns::compile(options).unwrap_or_else(|_| DispositionPatterns::empty());
explain_disposition_with(&patterns, kind, raw, language, options)
}
pub fn explain_disposition_with(
patterns: &DispositionPatterns,
kind: CommentKind,
raw: &[u8],
language: Language,
options: &ScanOptions,
) -> DispositionExplanation {
if options.keep_kinds.contains(&kind) {
return DispositionExplanation::KeptByKind(kind);
}
if let Some((index, pattern)) = first_match(&patterns.keep, raw, &options.keep_regex) {
return DispositionExplanation::KeptByRegex { index, pattern };
}
let hard = matches!(kind, CommentKind::Shebang | CommentKind::Encoding);
if hard && !options.force_protected {
return DispositionExplanation::ProtectedPreamble;
}
if options.remove_kinds.contains(&kind) {
return DispositionExplanation::RemovedByKind(kind);
}
if let Some((index, pattern)) = first_match(&patterns.remove, raw, &options.remove_regex) {
return DispositionExplanation::RemovedByRegex { index, pattern };
}
if options.policy == Policy::All {
return DispositionExplanation::RemovedByPolicy(options.policy);
}
if kind == CommentKind::HtmlComment {
return DispositionExplanation::KeptHtml;
}
if matches!(
kind,
CommentKind::Directive | CommentKind::OptimizerHint | CommentKind::VersionComment
) {
return DispositionExplanation::KeptDirective {
kind,
name: directive_name_of(raw, language),
};
}
if kind == CommentKind::License && options.policy == Policy::Legal {
return DispositionExplanation::KeptLicense {
marker: legal_marker_of(raw),
};
}
DispositionExplanation::RemovedByDefault(options.policy)
}
pub fn explain_comment(
comment: &Comment,
raw: &[u8],
language: Language,
options: &ScanOptions,
) -> DispositionExplanation {
let patterns =
DispositionPatterns::compile(options).unwrap_or_else(|_| DispositionPatterns::empty());
explain_comment_with(&patterns, comment, raw, language, options)
}
pub fn explain_comment_with(
patterns: &DispositionPatterns,
comment: &Comment,
raw: &[u8],
language: Language,
options: &ScanOptions,
) -> DispositionExplanation {
if is_yaml_structural_trail(&comment.disposition) {
return DispositionExplanation::KeptStructural { language };
}
explain_disposition_with(patterns, comment.kind, raw, language, options)
}
fn java_text_block_end(source: &[u8], start: usize) -> (usize, bool) {
let mut index = start.saturating_add(3);
while index + 2 < source.len() {
if starts(source, index, b"\"\"\"") {
let mut backslashes = 0usize;
let mut cursor = index;
while cursor > start + 3 && source[cursor - 1] == b'\\' {
backslashes += 1;
cursor -= 1;
}
if backslashes.is_multiple_of(2) {
return (index + 3, true);
}
}
index += 1;
}
(source.len(), false)
}
fn byte_order_mark_width(source: &[u8]) -> usize {
if source.starts_with(b"\xef\xbb\xbf") {
3
} else {
0
}
}
fn classify_comment(
source: &[u8],
language: Language,
lexical: CommentKind,
start: usize,
end: usize,
offset: usize,
) -> CommentKind {
let raw = &source[start.min(source.len())..end.min(source.len())];
let body = strip_comment_markers(raw);
let lower = String::from_utf8_lossy(body).to_ascii_lowercase();
let trimmed = lower.trim();
if offset == 0 && start == byte_order_mark_width(source) && raw.starts_with(b"#!") {
return CommentKind::Shebang;
}
if offset == 0
&& matches!(language, Language::Python | Language::Ruby)
&& is_encoding_declaration(source, start, raw)
{
return CommentKind::Encoding;
}
if language == Language::Sql && raw.starts_with(b"/*+") {
return CommentKind::OptimizerHint;
}
if raw.starts_with(b"/*!") && language == Language::Sql {
return CommentKind::VersionComment;
}
if legal_marker(trimmed).is_some() {
return CommentKind::License;
}
if directive_name(trimmed, language, raw).is_some() {
return CommentKind::Directive;
}
lexical
}
fn line_splicing_permits_restarts(source: &[u8], language: Language) -> bool {
!matches!(language, Language::C | Language::Cpp) || !contains_line_splice(source)
}
fn first_yaml_block_scalar(source: &[u8], language: Language) -> usize {
if language != Language::Yaml {
return usize::MAX;
}
let mut index = 0;
while let Some(relative) = memchr2(b'|', b'>', &source[index..]) {
let candidate = index + relative;
if yaml_block_header(source, candidate).is_some() {
return candidate;
}
index = candidate + 1;
}
usize::MAX
}
fn the_line_ending_permits_a_restart(source: &[u8], offset: usize) -> bool {
offset == 0 || source.get(offset - 1) != Some(&b'\r') || source.get(offset) != Some(&b'\n')
}
fn the_preamble_permits_a_restart(source: &[u8], language: Language, offset: usize) -> bool {
offset == 0
|| !matches!(language, Language::Python | Language::Ruby)
|| !is_within_first_two_lines(source, offset)
|| !line_declares_encoding(source, offset)
}
#[derive(Clone, Copy)]
pub(crate) struct RestartRules {
language: Language,
splicing_permits_restarts: bool,
first_block_scalar: usize,
}
impl RestartRules {
pub(crate) fn of(source: &[u8], language: Language) -> Self {
Self {
language,
splicing_permits_restarts: line_splicing_permits_restarts(source, language),
first_block_scalar: first_yaml_block_scalar(source, language),
}
}
pub(crate) fn permit_restart_at(&self, source: &[u8], offset: usize) -> bool {
self.splicing_permits_restarts
&& offset <= self.first_block_scalar
&& the_line_ending_permits_a_restart(source, offset)
&& the_preamble_permits_a_restart(source, self.language, offset)
&& (self.language != Language::Scala
|| the_scala_xml_boundary_permits_a_restart(source, offset))
&& (!matches!(
self.language,
Language::Html | Language::Vue | Language::Svelte
) || the_tag_boundary_permits_a_restart(source, offset))
}
}
fn the_tag_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool {
let mut index = offset;
while index > 0 {
index -= 1;
if source[index] == b'<' {
let mut cursor = index + 1;
let mut quote = None;
let mut closed = false;
while cursor < offset {
if let Some(active) = quote {
if source[cursor] == active {
quote = None;
}
} else if matches!(source[cursor], b'\'' | b'"') {
quote = Some(source[cursor]);
} else if source[cursor] == b'>' {
closed = true;
break;
}
cursor += 1;
}
if !closed {
return false;
}
}
}
true
}
fn the_scala_xml_boundary_permits_a_restart(source: &[u8], offset: usize) -> bool {
if source.get(offset) != Some(&b'<') {
return true;
}
match source.get(offset + 1) {
Some(b'!' | b'?') => false,
Some(&byte) => !(byte.is_ascii_alphabetic() || matches!(byte, b'_') || byte >= 0x80),
None => true,
}
}
pub(crate) fn preamble_is_settled(source: &[u8], offset: usize) -> bool {
!is_within_first_two_lines(source, offset)
}
fn is_within_first_two_lines(source: &[u8], offset: usize) -> bool {
let mut line_breaks = 0;
let mut index = 0;
let end = offset.min(source.len());
while index < end {
if source[index] == b'\r' {
index += usize::from(source.get(index + 1) == Some(&b'\n'));
line_breaks += 1;
if line_breaks >= 2 {
return false;
}
} else if source[index] == b'\n' {
line_breaks += 1;
if line_breaks >= 2 {
return false;
}
}
index += 1;
}
true
}
fn line_declares_encoding(source: &[u8], line_start: usize) -> bool {
let mut index = line_start;
while matches!(source.get(index), Some(b' ' | b'\t' | 0x0c)) {
index += 1;
}
if source.get(index) != Some(&b'#') {
return false;
}
let end = line_end(source, index + 1);
is_encoding_declaration(source, index, &source[index..end])
}
fn is_encoding_declaration(source: &[u8], start: usize, raw: &[u8]) -> bool {
if !is_within_first_two_lines(source, start) || !raw.starts_with(b"#") {
return false;
}
let line_start = source[..start.min(source.len())]
.iter()
.rposition(|byte| matches!(byte, b'\r' | b'\n'))
.map_or(0, |position| position + 1);
let mut prefix = &source[line_start..start.min(source.len())];
if line_start == 0 {
prefix = prefix.strip_prefix(b"\xef\xbb\xbf").unwrap_or(prefix);
}
if !prefix
.iter()
.all(|byte| matches!(byte, b' ' | b'\t' | 0x0c))
{
return false;
}
let body = &raw[1..];
let Some(position) = find_subslice(body, b"coding") else {
return false;
};
let mut cursor = position + b"coding".len();
if !matches!(body.get(cursor), Some(b':' | b'=')) {
return false;
}
cursor += 1;
while matches!(body.get(cursor), Some(b' ' | b'\t')) {
cursor += 1;
}
body.get(cursor)
.is_some_and(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
}
fn strip_comment_markers(raw: &[u8]) -> &[u8] {
let mut start = 0;
let mut end = raw.len();
for marker in [
b"<!--".as_slice(),
b"///",
b"//!",
b"//",
b"/**",
b"/*",
b"(*",
b"--",
b"#",
] {
if raw.starts_with(marker) {
start = marker.len();
break;
}
}
for marker in [b"-->".as_slice(), b"*/", b"*)"] {
if raw.ends_with(marker) {
end = end.saturating_sub(marker.len());
break;
}
}
&raw[start.min(end)..end]
}
fn legal_marker(text: &str) -> Option<&'static str> {
[
"spdx-license-identifier",
"copyright",
"licensed under",
"permission is hereby granted",
"all rights reserved",
]
.into_iter()
.find(|marker| text.contains(marker))
}
fn directive_name(text: &str, language: Language, raw: &[u8]) -> Option<&'static str> {
let compact = text.trim_start_matches(['!', '/', '*', '#', '@', ' ']);
let common = [
"sourcemappingurl=",
"sourceurl=",
"#__pure__",
"@__pure__",
"__pure__",
"#__no_side_effects__",
"__no_side_effects__",
"ts-ignore",
"ts-expect-error",
"ts-nocheck",
"ts-check",
"eslint",
"prettier-ignore",
"stylelint",
"noinspection",
"nolint",
"noqa",
"type: ignore",
"fmt:",
"rustfmt::",
"clang-format",
"spotless:",
"ktlint-disable",
"ktlint-enable",
"detekt:",
"istanbul ignore",
"c8 ignore",
"coverage:",
"ocomment:",
"region",
"endregion",
];
if let Some(name) = common
.into_iter()
.find(|prefix| compact.starts_with(prefix))
{
return Some(name);
}
if opens_with_keyword(compact, "shellcheck") {
return Some("shellcheck");
}
match language {
Language::Go => ["go:", "+build", "line "]
.into_iter()
.find(|prefix| compact.starts_with(prefix)),
Language::TypeScript => {
(raw.starts_with(b"///") && compact.starts_with('<')).then_some("///")
}
Language::C | Language::Cpp => ["pragma", "line "]
.into_iter()
.find(|prefix| compact.starts_with(prefix)),
Language::Python => ["pyright:", "mypy:", "ruff:", "fmt:"]
.into_iter()
.find(|prefix| compact.starts_with(prefix)),
Language::Shell => opens_with_keyword(compact, "hadolint")
.then_some("hadolint")
.or_else(|| compact.starts_with("syntax=").then_some("syntax=")),
Language::Toml => opens_with_keyword(compact, ":schema")
.then_some(":schema")
.or_else(|| compact.starts_with("taplo:").then_some("taplo:")),
Language::Lua => {
if raw.starts_with(b"---@") && text.trim_start_matches('-').starts_with("@diagnostic") {
return Some("---@diagnostic");
}
["luacheck:", "selene:", "stylua:", "luacov:"]
.into_iter()
.find(|prefix| compact.starts_with(prefix))
}
Language::Yaml => {
if opens_with_keyword(text, "@schema") {
return Some("@schema");
}
for keyword in ["yamllint", "nosec", "kics-scan"] {
if opens_with_keyword(compact, keyword) {
return Some(keyword);
}
}
[
"yaml-language-server:",
"renovate:",
"checkov:skip",
"trivy:ignore",
]
.into_iter()
.find(|prefix| compact.starts_with(prefix))
}
Language::Ruby => [
"frozen_string_literal:",
"warn_indent:",
"shareable_constant_value:",
"rubocop:",
"standard:",
"typed:",
]
.into_iter()
.find(|prefix| compact.starts_with(prefix)),
Language::Php => {
if opens_with_keyword(text, "@psalm-suppress") {
return Some("@psalm-suppress");
}
if text.starts_with("@phpstan-ignore") {
return Some("@phpstan-ignore");
}
if text.starts_with("@codecoverageignore") {
return Some("@codeCoverageIgnore");
}
compact.starts_with("phpcs:").then_some("phpcs:")
}
Language::R => {
if opens_with_keyword(compact, "nocov") {
return Some("nocov");
}
compact.starts_with("styler:").then_some("styler:")
}
Language::Zig => {
let opens_a_plain_comment =
raw.starts_with(b"//") && !matches!(raw.get(2), Some(b'/' | b'!'));
(opens_a_plain_comment && matches!(text, "zig fmt: off" | "zig fmt: on"))
.then_some("zig fmt:")
}
Language::Dart => {
if dart_language_version(raw) {
return Some("@dart");
}
let phrase = raw.trim_ascii_end();
if phrase == b"// dart format off" || phrase == b"// dart format on" {
return Some("dart format");
}
["ignore:", "ignore_for_file:"]
.into_iter()
.find(|prefix| compact.starts_with(prefix))
}
Language::Swift => {
if let Some(name) = ["swift-tools-version:", "swiftlint:", "swiftformat:"]
.into_iter()
.find(|prefix| compact.starts_with(prefix))
{
return Some(name);
}
let rest = compact.strip_prefix("swift-format-ignore")?;
let tail = rest.strip_prefix("-file").unwrap_or(rest);
(tail.is_empty()
|| tail.starts_with(':')
|| tail.starts_with(|character: char| {
character.is_ascii_whitespace() || character == '\u{000b}'
}))
.then_some("swift-format-ignore")
}
Language::CSharp => {
if text.contains("<auto-generated") || text.contains("<autogenerated") {
return Some("<auto-generated");
}
if let Some(rest) = compact.strip_prefix("resharper") {
let verb = rest.trim_start_matches([' ', '\t']);
if verb.len() < rest.len()
&& (verb.starts_with("disable") || verb.starts_with("restore"))
{
return Some("ReSharper");
}
}
matches!(
raw,
b"// csharpier-ignore" | b"// csharpier-ignore-start" | b"// csharpier-ignore-end"
)
.then_some("csharpier-ignore")
}
Language::Scala => (compact == "> using"
|| compact.starts_with("> using ")
|| compact.starts_with("> using\t"))
.then_some("//> using"),
_ => None,
}
}
fn opens_with_keyword(text: &str, keyword: &str) -> bool {
text.strip_prefix(keyword).is_some_and(|rest| {
rest.is_empty() || rest.starts_with(|character: char| character.is_ascii_whitespace())
})
}
fn java_line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"///") {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn java_block_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"/**") {
CommentKind::DocBlock
} else {
CommentKind::Block
}
}
fn dart_line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"///") {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn dart_block_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"/**") {
CommentKind::DocBlock
} else {
CommentKind::Block
}
}
fn swift_line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"///") {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn swift_block_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"/**") && !starts(bytes, index, b"/**/") {
CommentKind::DocBlock
} else {
CommentKind::Block
}
}
const fn swift_unterminated_string(delimiter: u8, raw: bool, multiline: bool) -> &'static str {
if delimiter == b'\'' {
return "unterminated Swift single-quoted string";
}
match (raw, multiline) {
(true, true) => "unterminated Swift raw multiline string",
(true, false) => "unterminated Swift raw string",
(false, true) => "unterminated Swift multiline string",
(false, false) => "unterminated Swift string",
}
}
fn swift_hashes_at(bytes: &[u8], index: usize, count: usize) -> bool {
(0..count).all(|offset| bytes.get(index + offset) == Some(&b'#'))
}
fn swift_string_close(
bytes: &[u8],
index: usize,
delimiter: u8,
multiline: bool,
hashes: usize,
) -> Option<usize> {
let width = if multiline { 3 } else { 1 };
let closes = (0..width).all(|offset| bytes.get(index + offset) == Some(&delimiter))
&& swift_hashes_at(bytes, index + width, hashes);
if !closes {
return None;
}
let end = index + width + hashes;
let extra = hashes > 0 && bytes.get(end) == Some(&b'#');
Some(end + usize::from(extra))
}
fn swift_hash_run(bytes: &[u8], index: usize, reach: &mut Reach) -> usize {
let mut end = index;
while bytes.get(end) == Some(&b'#') {
end += 1;
}
reach.byte(end);
end - index
}
fn swift_is_left_bound(bytes: &[u8], index: usize) -> bool {
let Some(previous) = index.checked_sub(1).map(|behind| bytes[behind]) else {
return false;
};
match previous {
b' ' | b'\t' | b'\r' | b'\n' | 0 => false,
b'(' | b'[' | b'{' | b',' | b';' | b':' => false,
b'/' => index < 2 || bytes[index - 2] != b'*',
0xa0 => index < 2 || bytes[index - 2] != 0xc2,
_ => true,
}
}
fn swift_multiline_string(bytes: &[u8], quote: usize, hashes: usize) -> bool {
if !starts(bytes, quote, b"\"\"\"") {
return false;
}
if hashes == 0 {
return true;
}
let mut cursor = quote + 2;
while bytes
.get(cursor)
.is_some_and(|byte| !is_line_terminator(*byte))
{
if bytes[cursor] == b'"' && swift_hashes_at(bytes, cursor + 1, hashes) {
return false;
}
cursor += 1;
}
true
}
fn swift_bare_regex(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<usize> {
if swift_is_left_bound(bytes, index) {
return None;
}
reach.byte(index + 1);
match bytes.get(index + 1) {
None | Some(b' ' | b'\t' | b'\r' | b'\n') => return None,
Some(_) => {}
}
let mut cursor = index + 1;
let mut blank = false;
while cursor < bytes.len() {
match bytes[cursor] {
b'\\' => {
reach.byte(cursor + 1);
if matches!(bytes.get(cursor + 1), None | Some(b'\r' | b'\n')) {
return None;
}
blank = false;
cursor += 2;
}
b'\r' | b'\n' => {
reach.byte(cursor);
return None;
}
b'/' => {
reach.byte(cursor + 1);
if blank || matches!(bytes.get(cursor + 1), Some(b'/' | b'*')) {
return None;
}
return Some(cursor + 1);
}
byte => {
blank = matches!(byte, b' ' | b'\t');
cursor += 1;
}
}
}
reach.end_of(bytes);
None
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CsharpStringForm {
Plain,
Verbatim,
Raw,
}
#[derive(Clone, Copy, Debug)]
struct CsharpPrefix {
start: usize,
quote: usize,
quotes: usize,
dollars: usize,
form: CsharpStringForm,
}
fn csharp_prefix_end(bytes: &[u8], index: usize) -> usize {
let mut end = index;
while matches!(bytes.get(end), Some(b'$' | b'@')) {
end += 1;
}
end
}
fn csharp_literal_prefix(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<CsharpPrefix> {
let mut cursor = index;
let mut dollars = 0;
let mut ats = 0;
while let Some(byte) = bytes.get(cursor) {
match byte {
b'$' => dollars += 1,
b'@' => ats += 1,
_ => break,
}
cursor += 1;
}
if bytes.get(cursor) != Some(&b'"') {
reach.byte(cursor);
return None;
}
let mut quotes = 0;
while bytes.get(cursor + quotes) == Some(&b'"') {
quotes += 1;
}
reach.byte(cursor + quotes);
let form = if dollars >= 2 {
CsharpStringForm::Raw
} else if ats > 0 {
CsharpStringForm::Verbatim
} else if quotes >= 3 {
CsharpStringForm::Raw
} else {
CsharpStringForm::Plain
};
Some(CsharpPrefix {
start: index,
quote: cursor,
quotes,
dollars,
form,
})
}
fn csharp_multiline_raw_string(bytes: &[u8], content: usize) -> bool {
let mut cursor = content;
while cursor < bytes.len() {
if csharp_line_terminator_width(bytes, cursor).is_some() {
return true;
}
if !is_csharp_blank(bytes[cursor]) {
return false;
}
cursor += 1;
}
false
}
fn csharp_hole_close(bytes: &[u8], index: usize, braces: usize) -> (usize, bool) {
let mut run = index;
while run - index < braces && bytes.get(run) == Some(&b'}') {
run += 1;
}
if run - index == braces {
(index + braces, true)
} else {
(run, false)
}
}
fn csharp_line_terminator_width(bytes: &[u8], index: usize) -> Option<usize> {
csharp_unicode_line_terminator_width(bytes, index)
.or_else(|| unicode_line_terminator_width(bytes, index))
}
fn csharp_unicode_line_terminator_width(bytes: &[u8], index: usize) -> Option<usize> {
match bytes.get(index) {
Some(0xc2) if bytes.get(index + 1) == Some(&0x85) => Some(2),
Some(0xe2)
if bytes.get(index + 1) == Some(&0x80)
&& matches!(bytes.get(index + 2), Some(0xa8 | 0xa9)) =>
{
Some(3)
}
_ => None,
}
}
fn csharp_line_end(bytes: &[u8], mut index: usize) -> usize {
while index < bytes.len() && csharp_line_terminator_width(bytes, index).is_none() {
index += 1;
}
index
}
fn scala_line_kind(_bytes: &[u8], _index: usize) -> CommentKind {
CommentKind::Line
}
fn scala_block_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"/**") {
CommentKind::DocBlock
} else {
CommentKind::Block
}
}
fn scala_character_literal_end(bytes: &[u8], start: usize) -> Option<usize> {
let content = start + 1;
let mut end = content;
match *bytes.get(content)? {
b'\r' | b'\n' | b'\'' => return None,
b'\\' => {
end += 1;
if bytes.get(end) == Some(&b'u') {
while bytes.get(end) == Some(&b'u') {
end += 1;
}
let digits = bytes.get(end..end + 4)?;
if !digits.iter().all(u8::is_ascii_hexdigit) {
return None;
}
end += 4;
} else {
bytes.get(end)?;
end += 1;
}
}
byte if byte.is_ascii() => end += 1,
byte => {
let width = match byte {
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => return None,
};
std::str::from_utf8(bytes.get(content..content + width)?).ok()?;
end += width;
}
}
(bytes.get(end) == Some(&b'\'')).then_some(end + 1)
}
fn scala_interpolator(bytes: &[u8], quote: usize) -> bool {
let mut start = quote;
while start > 0 && scala_identifier_part(bytes[start - 1]) {
start -= 1;
}
if start == quote || !scala_identifier_start(bytes[start]) {
return false;
}
!scala_is_keyword(&bytes[start..quote])
}
fn scala_is_keyword(word: &[u8]) -> bool {
matches!(
word,
b"abstract"
| b"case"
| b"catch"
| b"class"
| b"def"
| b"do"
| b"else"
| b"enum"
| b"export"
| b"extends"
| b"final"
| b"finally"
| b"for"
| b"given"
| b"if"
| b"implicit"
| b"import"
| b"lazy"
| b"match"
| b"new"
| b"object"
| b"open"
| b"override"
| b"package"
| b"private"
| b"protected"
| b"return"
| b"sealed"
| b"then"
| b"throw"
| b"trait"
| b"transparent"
| b"try"
| b"type"
| b"val"
| b"var"
| b"while"
| b"with"
| b"yield"
)
}
fn scala_identifier_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
}
fn scala_identifier_part(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')
}
fn scala_is_xml_start(bytes: &[u8], index: usize) -> bool {
let before = if index > 0 { bytes[index - 1] } else { b' ' };
if !matches!(before, b' ' | b'\t' | b'\n' | b'{' | b'(' | b'>') {
return false;
}
match bytes.get(index + 1) {
Some(b'!' | b'?') => true,
Some(&byte) => byte.is_ascii_alphabetic() || matches!(byte, b'_') || byte >= 0x80,
None => false,
}
}
fn xml_name_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || matches!(byte, b'_' | b':') || byte >= 0x80
}
fn xml_name_len(bytes: &[u8]) -> usize {
let mut index = 0;
while index < bytes.len() && xml_name_char(bytes[index]) {
index += 1;
}
index
}
fn xml_name_char(byte: u8) -> bool {
xml_name_start(byte) || byte.is_ascii_digit() || matches!(byte, b'-' | b'.')
}
fn skip_xml_tag_tail(bytes: &[u8], mut index: usize) -> Option<usize> {
while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\r' | b'\n')) {
index += 1;
}
(bytes.get(index) == Some(&b'>')).then_some(index + 1)
}
fn count_run(bytes: &[u8], index: usize, byte: u8) -> usize {
let mut end = index;
while bytes.get(end) == Some(&byte) {
end += 1;
}
end - index
}
const fn is_csharp_blank(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | 0x0b | 0x0c)
}
fn csharp_directive_takes_a_message(name: &[u8]) -> bool {
matches!(name, b"error" | b"warning" | b"region" | b"endregion")
}
const fn csharp_unterminated_string(form: CsharpStringForm, interpolated: bool) -> &'static str {
match (form, interpolated) {
(CsharpStringForm::Plain, false) => "unterminated C# string",
(CsharpStringForm::Plain, true) => "unterminated C# interpolated string",
(CsharpStringForm::Verbatim, false) => "unterminated C# verbatim string",
(CsharpStringForm::Verbatim, true) => "unterminated C# interpolated verbatim string",
(CsharpStringForm::Raw, false) => "unterminated C# raw string",
(CsharpStringForm::Raw, true) => "unterminated C# interpolated raw string",
}
}
fn csharp_line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"///") && bytes.get(index + 3) != Some(&b'/') {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn csharp_block_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"/**") && !matches!(bytes.get(index + 3), Some(b'*' | b'/')) {
CommentKind::DocBlock
} else {
CommentKind::Block
}
}
const fn dart_unterminated_string(raw: bool, triple: bool) -> &'static str {
match (raw, triple) {
(true, true) => "unterminated Dart raw multiline string",
(true, false) => "unterminated Dart raw string",
(false, true) => "unterminated Dart multiline string",
(false, false) => "unterminated Dart string",
}
}
fn dart_raw_string_prefix(bytes: &[u8], quote: usize) -> bool {
if quote == 0 || bytes[quote - 1] != b'r' {
return false;
}
let mut cursor = quote - 1;
while cursor > 0 && is_dart_identifier_continue(bytes[cursor - 1]) {
cursor -= 1;
}
cursor == quote - 1 || bytes[cursor].is_ascii_digit()
}
fn is_dart_identifier_continue(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')
}
fn dart_language_version(raw: &[u8]) -> bool {
fn past_spaces(bytes: &[u8]) -> &[u8] {
let taken = bytes.iter().take_while(|byte| **byte == b' ').count();
&bytes[taken..]
}
fn past_digits(bytes: &[u8]) -> Option<&[u8]> {
let taken = bytes
.iter()
.take_while(|byte| byte.is_ascii_digit())
.count();
(taken > 0).then(|| &bytes[taken..])
}
let Some(rest) = raw.strip_prefix(b"//") else {
return false;
};
if rest.first() == Some(&b'/') {
return false;
}
let Some(rest) = past_spaces(rest).strip_prefix(b"@dart") else {
return false;
};
let Some(rest) = past_spaces(rest).strip_prefix(b"=") else {
return false;
};
let Some(rest) = past_digits(past_spaces(rest)) else {
return false;
};
let Some(rest) = rest.strip_prefix(b".") else {
return false;
};
let Some(rest) = past_digits(rest) else {
return false;
};
past_spaces(rest).is_empty()
}
fn line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"///") || starts(bytes, index, b"//!") {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn block_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"/**") || starts(bytes, index, b"/*!") {
CommentKind::DocBlock
} else {
CommentKind::Block
}
}
fn lua_line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"---") && !starts(bytes, index, b"----") {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn zig_line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"////") {
CommentKind::Line
} else if starts(bytes, index, b"///") || starts(bytes, index, b"//!") {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn r_line_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"#'") {
CommentKind::DocLine
} else {
CommentKind::Line
}
}
fn is_r_name_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_') || byte >= 0x80
}
fn r_raw_string(bytes: &[u8], quote: usize) -> Option<(usize, bool)> {
let prefix = quote.checked_sub(1)?;
if !matches!(bytes[prefix], b'r' | b'R') {
return None;
}
if prefix > 0 && is_r_name_byte(bytes[prefix - 1]) {
return None;
}
let mut bracket = quote + 1;
while bytes.get(bracket) == Some(&b'-') {
bracket += 1;
}
let closing = match bytes.get(bracket) {
Some(b'(') => b')',
Some(b'[') => b']',
Some(b'{') => b'}',
_ => return None,
};
let mut close = Vec::with_capacity(bracket - quote + 1);
close.push(closing);
close.extend_from_slice(&bytes[quote + 1..bracket]);
close.push(bytes[quote]);
Some(match find_subslice(&bytes[bracket + 1..], &close) {
Some(relative) => (bracket + 1 + relative + close.len(), true),
None => (bytes.len(), false),
})
}
fn r_delimited_end(bytes: &[u8], mut index: usize, close: u8) -> (usize, bool) {
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == close {
return (index + 1, true);
} else {
index += 1;
}
}
(bytes.len(), false)
}
fn long_bracket_level(bytes: &[u8], index: usize) -> Option<usize> {
if bytes.get(index) != Some(&b'[') {
return None;
}
let mut cursor = index + 1;
while bytes.get(cursor) == Some(&b'=') {
cursor += 1;
}
(bytes.get(cursor) == Some(&b'[')).then(|| cursor - index - 1)
}
fn long_bracket_end(bytes: &[u8], content: usize, level: usize) -> (usize, bool) {
let mut index = content.min(bytes.len());
while let Some(relative) = memchr(b']', &bytes[index..]) {
let close = index + relative;
let mut cursor = close + 1;
while bytes.get(cursor) == Some(&b'=') {
cursor += 1;
}
if cursor - close - 1 == level && bytes.get(cursor) == Some(&b']') {
return (cursor + 1, true);
}
index = close + 1;
}
(bytes.len(), false)
}
fn js_is_space(byte: u8) -> bool {
matches!(byte, b'\t' | b'\n' | 0x0b | 0x0c | b'\r' | b' ')
}
fn lua_is_space(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
}
fn lua_newline_width(bytes: &[u8], index: usize) -> Option<usize> {
match (bytes.get(index), bytes.get(index + 1)) {
(Some(b'\r'), Some(b'\n')) | (Some(b'\n'), Some(b'\r')) => Some(2),
(Some(b'\r' | b'\n'), _) => Some(1),
_ => None,
}
}
fn toml_quote_run(bytes: &[u8], start: usize, quote: u8) -> usize {
let mut index = start;
while index < bytes.len() && bytes[index] == quote {
index += 1;
}
index - start
}
fn yaml_flow_opener(bytes: &[u8], index: usize) -> bool {
index > 0 && matches!(bytes[index - 1], b',' | b'[' | b'{')
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Chomping {
Strip,
Clip,
Keep,
}
fn yaml_property_end(bytes: &[u8], index: usize) -> usize {
let mut cursor = index + 1;
while cursor < bytes.len()
&& !matches!(
bytes[cursor],
b' ' | b'\t' | b'\r' | b'\n' | b',' | b'[' | b']' | b'{' | b'}'
)
{
cursor += 1;
}
cursor
}
fn yaml_block_header(
bytes: &[u8],
index: usize,
) -> Option<(Option<usize>, Chomping, Option<usize>, usize)> {
let mut cursor = index + 1;
let mut indentation = None;
let mut chomping = None;
while let Some(byte) = bytes.get(cursor).copied() {
match byte {
b'1'..=b'9' if indentation.is_none() => indentation = Some(usize::from(byte - b'0')),
b'+' if chomping.is_none() => chomping = Some(Chomping::Keep),
b'-' if chomping.is_none() => chomping = Some(Chomping::Strip),
_ => break,
}
cursor += 1;
}
let mut spaced = false;
while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
spaced = true;
cursor += 1;
}
let comment = (spaced && bytes.get(cursor) == Some(&b'#')).then_some(cursor);
if comment.is_some() {
cursor = line_end(bytes, cursor);
}
bytes
.get(cursor)
.is_none_or(|byte| matches!(byte, b'\r' | b'\n'))
.then(|| {
(
indentation,
chomping.unwrap_or(Chomping::Clip),
comment,
cursor,
)
})
}
fn yaml_block_body_end(bytes: &[u8], header_end: usize, body_min: usize) -> (usize, bool, usize) {
if header_end >= bytes.len() {
return (bytes.len(), false, body_min);
}
let mut index = consume_newline(bytes, header_end);
let mut content = None;
while index < bytes.len() {
let (indent, blank, end) = yaml_line_shape(bytes, index);
if !blank && (indent < body_min || yaml_document_marker(bytes, index)) {
break;
}
if !blank && content.is_none() {
content = Some(indent);
}
if end >= bytes.len() {
return (bytes.len(), false, content.unwrap_or(body_min));
}
index = consume_newline(bytes, end);
}
(index, true, content.unwrap_or(body_min))
}
fn yaml_line_shape(bytes: &[u8], start: usize) -> (usize, bool, usize) {
let mut index = start;
while bytes.get(index) == Some(&b' ') {
index += 1;
}
let indent = index - start;
let mut cursor = index;
while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
cursor += 1;
}
let blank = bytes
.get(cursor)
.is_none_or(|byte| matches!(byte, b'\r' | b'\n'));
(indent, blank, line_end(bytes, cursor))
}
fn yaml_document_marker(bytes: &[u8], line_start: usize) -> bool {
(starts(bytes, line_start, b"---") || starts(bytes, line_start, b"..."))
&& bytes
.get(line_start + 3)
.is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
}
fn comment_alone_on_line(
source: &[u8],
offset: usize,
comments: &[Comment],
line_start: usize,
line_end: usize,
) -> Option<usize> {
let (start, end) = (line_start + offset, line_end + offset);
let index = comments
.binary_search_by(|comment| {
if comment.span.start < start {
Ordering::Less
} else if comment.span.start >= end {
Ordering::Greater
} else {
Ordering::Equal
}
})
.ok()?;
let span = comments[index].span;
(span.end == end
&& source[line_start..span.start - offset]
.iter()
.all(|byte| matches!(byte, b' ' | b'\t')))
.then_some(index)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct YamlBlockScalar {
body_end: usize,
content_indent: usize,
chomping: Chomping,
}
pub(crate) const YAML_STRUCTURAL_TRAIL: &str = "structural in a YAML block scalar trail";
pub(crate) fn is_yaml_structural_trail(disposition: &Disposition) -> bool {
matches!(disposition, Disposition::Keep { reason } if reason == YAML_STRUCTURAL_TRAIL)
}
fn yaml_structural_trail_keeps(
source: &[u8],
offset: usize,
blocks: &[YamlBlockScalar],
comments: &[Comment],
) -> Vec<usize> {
let mut keeps = Vec::new();
for block in blocks {
let Some(mut probe) = block.body_end.checked_sub(offset) else {
continue;
};
let mut shield = None;
while probe < source.len() {
let (indent, blank, end) = yaml_line_shape(source, probe);
if blank {
probe = past_terminator(source, end);
continue;
}
let Some(found) = comment_alone_on_line(source, offset, comments, probe, end) else {
break;
};
if comments[found].disposition.is_remove() {
if shield.is_none() && indent < block.content_indent {
shield = Some(found);
}
} else if indent < block.content_indent {
break;
} else {
keeps.extend(shield);
break;
}
probe = past_terminator(source, end);
}
}
keeps
}
pub(crate) fn keep_yaml_structural_trails(
source: &[u8],
language: Language,
comments: &mut [Comment],
) {
if language != Language::Yaml || comments.is_empty() || memchr2(b'|', b'>', source).is_none() {
return;
}
if !comments.iter().any(|comment| {
comment.disposition.is_remove() && starts_its_line(source, comment.span.start)
}) {
return;
}
let blocks = yaml_block_scalars(source);
for index in yaml_structural_trail_keeps(source, 0, &blocks, comments) {
comments[index].disposition = Disposition::Keep {
reason: YAML_STRUCTURAL_TRAIL.to_owned(),
};
}
}
fn yaml_block_scalars(source: &[u8]) -> Vec<YamlBlockScalar> {
let mut scanner = Scanner::with_offset(
source,
Language::Yaml,
ScanOptions::default(),
0,
false,
None,
);
scanner.scan_yaml();
scanner.yaml_blocks
}
fn starts_its_line(source: &[u8], start: usize) -> bool {
source[..start]
.iter()
.copied()
.rev()
.find(|byte| !matches!(byte, b' ' | b'\t'))
.is_none_or(|byte| matches!(byte, b'\r' | b'\n'))
}
fn past_terminator(source: &[u8], line_end: usize) -> usize {
if line_end >= source.len() {
line_end
} else {
consume_newline(source, line_end)
}
}
pub(crate) fn lines_a_removal_must_swallow(
source: &[u8],
language: Language,
comments: &[Comment],
) -> Vec<Option<ByteSpan>> {
if language != Language::Yaml || comments.is_empty() {
return Vec::new();
}
if memchr2(b'|', b'>', source).is_none() {
return Vec::new();
}
if !comments.iter().any(|comment| {
comment.disposition.is_remove() && starts_its_line(source, comment.span.start)
}) {
return Vec::new();
}
let blocks = yaml_block_scalars(source);
if blocks.is_empty() {
return Vec::new();
}
let mut answers = vec![None; comments.len()];
for block in blocks {
let mut probe = block.body_end;
while probe < source.len() {
let (_, blank, end) = yaml_line_shape(source, probe);
if blank {
probe = past_terminator(source, end);
continue;
}
let Some(found) = comment_alone_on_line(source, 0, comments, probe, end) else {
break;
};
if comments[found].disposition.is_remove() {
let mut taken = past_terminator(source, end);
if block.chomping == Chomping::Keep {
while taken < source.len() {
let (_, blank, run_end) = yaml_line_shape(source, taken);
if !blank {
break;
}
taken = past_terminator(source, run_end);
}
}
answers[found] = Some(ByteSpan::new(probe, taken));
}
probe = past_terminator(source, end);
}
}
answers
}
fn starts(bytes: &[u8], index: usize, needle: &[u8]) -> bool {
bytes.get(index..index.saturating_add(needle.len())) == Some(needle)
}
fn is_line_terminator(byte: u8) -> bool {
matches!(byte, b'\r' | b'\n')
}
fn line_end(bytes: &[u8], mut index: usize) -> usize {
while index < bytes.len() && !matches!(bytes[index], b'\r' | b'\n') {
index += 1;
}
index
}
pub(crate) fn unicode_line_terminator_width(bytes: &[u8], index: usize) -> Option<usize> {
match bytes.get(index) {
Some(b'\r') if bytes.get(index + 1) == Some(&b'\n') => Some(2),
Some(b'\r' | b'\n') => Some(1),
Some(0xe2)
if bytes.get(index + 1) == Some(&0x80)
&& matches!(bytes.get(index + 2), Some(0xa8 | 0xa9)) =>
{
Some(3)
}
_ => None,
}
}
fn js_line_end(bytes: &[u8], mut index: usize) -> usize {
while index < bytes.len() && unicode_line_terminator_width(bytes, index).is_none() {
index += 1;
}
index
}
fn consume_newline(bytes: &[u8], index: usize) -> usize {
if bytes.get(index) == Some(&b'\r') && bytes.get(index + 1) == Some(&b'\n') {
index + 2
} else {
index + 1
}
}
fn block_end(bytes: &[u8], start: usize, open: &[u8], close: &[u8], nested: bool) -> (usize, bool) {
let mut index = start + open.len();
let mut depth = 1usize;
while index < bytes.len() {
if nested && starts(bytes, index, open) {
depth += 1;
index += open.len();
} else if starts(bytes, index, close) {
depth -= 1;
index += close.len();
if depth == 0 {
return (index, true);
}
} else {
index += 1;
}
}
(bytes.len(), false)
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
memmem::find(haystack, needle)
}
fn rust_raw_start_at_quote(bytes: &[u8], quote: usize) -> Option<(usize, usize)> {
let mut cursor = quote;
while cursor > 0 && bytes[cursor - 1] == b'#' {
cursor -= 1;
}
let hashes = quote - cursor;
if cursor == 0 || bytes[cursor - 1] != b'r' {
return None;
}
let mut start = cursor - 1;
if start > 0 && matches!(bytes[start - 1], b'b' | b'c') {
start -= 1;
}
if start > 0 && is_js_identifier_continue(bytes[start - 1]) {
return None;
}
Some((start, hashes))
}
fn rust_char_start(bytes: &[u8], index: usize, reach: &mut Reach) -> bool {
reach.byte(index + 1);
let Some(next) = bytes.get(index + 1) else {
return false;
};
if is_line_terminator(*next) {
return false;
}
if *next == b'\\' {
reach.through(line_bounded_reach(bytes, index + 2, 2));
return bytes
.get(index + 2)
.is_some_and(|byte| !is_line_terminator(*byte))
&& bytes.get(index + 3..index + 4) == Some(b"'");
}
reach.byte(index + 2);
if bytes.get(index + 2) == Some(&b'\'') {
return true;
}
if *next & 0x80 == 0 {
return false;
}
reach.through(line_bounded_reach(bytes, index + 1, 5));
bytes[index + 1..]
.iter()
.take(5)
.take_while(|byte| !is_line_terminator(**byte))
.any(|byte| *byte == b'\'')
}
fn is_c_quote_start(bytes: &[u8], index: usize) -> bool {
matches!(bytes[index], b'"' | b'\'')
|| (matches!(bytes[index], b'L' | b'u' | b'U')
&& matches!(bytes.get(index + 1), Some(b'"' | b'\'')))
|| (starts(bytes, index, b"u8\"") || starts(bytes, index, b"u8'"))
}
fn cpp_raw_string(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<(usize, bool)> {
let prefixes: [&[u8]; 5] = [b"R\"", b"u8R\"", b"uR\"", b"UR\"", b"LR\""];
let Some(prefix) = prefixes.iter().find(|prefix| starts(bytes, index, prefix)) else {
reach.through((index + 4).min(bytes.len()));
return None;
};
let delimiter_start = index + prefix.len();
let mut open = delimiter_start;
while open < delimiter_start + 16 && bytes.get(open).is_some_and(|byte| is_cpp_d_char(*byte)) {
open += 1;
}
reach.byte(open);
if bytes.get(open) != Some(&b'(') {
return None;
}
let mut close = Vec::with_capacity(open - delimiter_start + 2);
close.push(b')');
close.extend_from_slice(&bytes[delimiter_start..open]);
close.push(b'"');
Some(match find_subslice(&bytes[open + 1..], &close) {
Some(relative) => {
let end = open + 1 + relative + close.len();
reach.through(end);
(end, true)
}
None => (bytes.len(), false),
})
}
const fn is_cpp_d_char(byte: u8) -> bool {
!matches!(
byte,
b' ' | b'(' | b')' | b'\\' | b'\t' | 0x0b | 0x0c | b'\n' | b'\r'
)
}
fn cpp_raw_start_at_quote(bytes: &[u8], quote: usize) -> Option<usize> {
for prefix in [b"R".as_slice(), b"u8R", b"uR", b"UR", b"LR"] {
let Some(start) = quote.checked_sub(prefix.len()) else {
continue;
};
if bytes.get(start..quote) == Some(prefix)
&& (start == 0 || !is_js_identifier_continue(bytes[start - 1]))
{
return Some(start);
}
}
None
}
fn ocaml_comment_end(bytes: &[u8], start: usize, reach: &mut Reach) -> (usize, bool) {
let mut index = start + 2;
let mut depth = 1;
while index < bytes.len() {
if starts(bytes, index, b"(*") {
depth += 1;
index += 2;
} else if starts(bytes, index, b"*)") {
depth -= 1;
index += 2;
if depth == 0 {
return (index, true);
}
} else if bytes[index] == b'"' {
index += 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == b'"' {
index += 1;
break;
} else {
index += 1;
}
}
} else if let Some((end, _)) = ocaml_quoted_string(bytes, index, reach) {
index = end;
} else if bytes[index] == b'\'' && ocaml_char_start(bytes, index, reach) {
let quote = bytes[index];
index += 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == quote {
index += 1;
break;
} else {
index += 1;
}
}
} else {
index += 1;
}
}
(bytes.len(), false)
}
fn ocaml_quoted_string(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<(usize, bool)> {
if bytes.get(index) != Some(&b'{') {
return None;
}
let mut pipe = index + 1;
while bytes
.get(pipe)
.is_some_and(|byte| byte.is_ascii_lowercase() || *byte == b'_')
{
pipe += 1;
}
reach.byte(pipe);
if bytes.get(pipe) != Some(&b'|') {
return None;
}
let mut close = Vec::with_capacity(pipe - index + 1);
close.push(b'|');
close.extend_from_slice(&bytes[index + 1..pipe]);
close.push(b'}');
Some(match find_subslice(&bytes[pipe + 1..], &close) {
Some(relative) => {
let end = pipe + 1 + relative + close.len();
reach.through(end);
(end, true)
}
None => (bytes.len(), false),
})
}
fn ocaml_char_start(bytes: &[u8], index: usize, reach: &mut Reach) -> bool {
reach.byte(index + 1);
let Some(next) = bytes.get(index + 1) else {
return false;
};
if is_line_terminator(*next) {
return false;
}
reach.byte(index + 2);
if bytes.get(index + 2) == Some(&b'\'') {
return true;
}
if *next != b'\\' {
return false;
}
reach.through(line_bounded_reach(bytes, index + 2, 6));
bytes[index + 2..]
.iter()
.take(6)
.take_while(|byte| !is_line_terminator(**byte))
.any(|byte| *byte == b'\'')
}
fn python_string_start(bytes: &[u8], index: usize) -> Option<(usize, bool, bool, bool)> {
if matches!(bytes[index], b'\'' | b'"') {
return Some((
index,
starts(bytes, index, &[bytes[index]; 3]),
false,
false,
));
}
if !matches!(
bytes[index].to_ascii_lowercase(),
b'r' | b'u' | b'b' | b'f' | b't'
) {
return None;
}
if index > 0 && (bytes[index - 1].is_ascii_alphanumeric() || bytes[index - 1] == b'_') {
return None;
}
let mut cursor = index;
while cursor < bytes.len()
&& cursor - index < 3
&& matches!(
bytes[cursor].to_ascii_lowercase(),
b'r' | b'u' | b'b' | b'f' | b't'
)
{
cursor += 1;
}
if cursor < bytes.len() && matches!(bytes[cursor], b'\'' | b'"') {
let prefix = &bytes[index..cursor];
Some((
cursor,
starts(bytes, cursor, &[bytes[cursor]; 3]),
prefix
.iter()
.any(|byte| byte.eq_ignore_ascii_case(&b'f') || byte.eq_ignore_ascii_case(&b't')),
prefix.iter().any(|byte| byte.eq_ignore_ascii_case(&b'r')),
))
} else {
None
}
}
fn shell_single_quote_end(bytes: &[u8], start: usize) -> (usize, bool) {
match bytes[start + 1..].iter().position(|byte| *byte == b'\'') {
Some(relative) => (start + relative + 2, true),
None => (bytes.len(), false),
}
}
#[derive(Clone, Copy)]
enum ShellTerminator {
Parenthesis(usize),
Backtick(usize),
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum ShellCaseState {
AwaitIn,
Pattern,
Body,
}
struct Heredoc {
operator: usize,
delimiter: Vec<u8>,
strip_tabs: bool,
}
fn parse_heredoc(bytes: &[u8], index: usize, reach: &mut Reach) -> Option<(Heredoc, usize)> {
reach.byte(index + 2);
let strip_tabs = bytes.get(index + 2) == Some(&b'-');
let mut cursor = index + if strip_tabs { 3 } else { 2 };
while bytes.get(cursor).is_some_and(u8::is_ascii_whitespace)
&& !matches!(bytes[cursor], b'\r' | b'\n')
{
cursor += 1;
}
reach.byte(cursor);
let mut delimiter = Vec::new();
let mut quote = None;
let mut saw_word = false;
while cursor < bytes.len() {
reach.byte(cursor);
let byte = bytes[cursor];
if let Some(active) = quote {
if byte == active {
quote = None;
cursor += 1;
} else if active == b'"' && byte == b'\\' {
reach.byte(cursor + 1);
let escaped = *bytes.get(cursor + 1)?;
if escaped == b'\r' {
reach.byte(cursor + 2);
}
if escaped == b'\r' && bytes.get(cursor + 2) == Some(&b'\n') {
cursor += 3;
} else if matches!(escaped, b'\r' | b'\n') {
cursor += 2;
} else if matches!(escaped, b'$' | b'`' | b'"' | b'\\') {
delimiter.push(escaped);
cursor += 2;
} else {
delimiter.extend_from_slice(&[b'\\', escaped]);
cursor += 2;
}
} else {
delimiter.push(byte);
cursor += 1;
}
continue;
}
if byte.is_ascii_whitespace()
|| matches!(byte, b';' | b'|' | b'&' | b'(' | b')' | b'<' | b'>')
{
break;
}
match byte {
b'\'' | b'"' => {
saw_word = true;
quote = Some(byte);
cursor += 1;
}
b'\\' => {
saw_word = true;
reach.byte(cursor + 1);
let escaped = *bytes.get(cursor + 1)?;
if escaped == b'\r' {
reach.byte(cursor + 2);
}
if escaped == b'\r' && bytes.get(cursor + 2) == Some(&b'\n') {
cursor += 3;
} else {
if !matches!(escaped, b'\r' | b'\n') {
delimiter.push(escaped);
}
cursor += 2;
}
}
_ => {
saw_word = true;
delimiter.push(byte);
cursor += 1;
}
}
}
if cursor >= bytes.len() {
reach.end_of(bytes);
}
if !saw_word || quote.is_some() {
return None;
}
Some((
Heredoc {
operator: index,
delimiter,
strip_tabs,
},
cursor,
))
}
fn heredoc_body_end(bytes: &[u8], mut index: usize, heredoc: &Heredoc) -> Option<usize> {
while index <= bytes.len() {
let end = line_end(bytes, index);
let mut line = &bytes[index..end];
if heredoc.strip_tabs {
let first = line
.iter()
.position(|byte| *byte != b'\t')
.unwrap_or(line.len());
line = &line[first..];
}
if line == heredoc.delimiter {
return Some(if end < bytes.len() {
consume_newline(bytes, end)
} else {
end
});
}
if end == bytes.len() {
break;
}
index = consume_newline(bytes, end);
}
None
}
fn sql_quoted_end(bytes: &[u8], start: usize, quote: u8, backslash_escapes: bool) -> (usize, bool) {
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == quote && bytes.get(index + 1) == Some("e) {
index += 2;
} else if bytes[index] == quote {
return (index + 1, true);
} else if backslash_escapes && bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else {
index += 1;
}
}
(index, false)
}
fn postgres_escape_string_start(bytes: &[u8], quote: usize) -> bool {
quote > 0
&& matches!(bytes[quote - 1], b'e' | b'E')
&& (quote == 1 || !is_js_identifier_continue(bytes[quote - 2]))
}
fn mysql_dash_comment_boundary(next: Option<u8>) -> bool {
next.is_none_or(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
}
fn sql_identifier_end(bytes: &[u8], start: usize, close: u8) -> (usize, bool) {
let actual_close = if bytes[start] == b'[' { b']' } else { close };
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == actual_close && bytes.get(index + 1) == Some(&actual_close) {
index += 2;
} else if bytes[index] == actual_close {
return (index + 1, true);
} else {
index += 1;
}
}
(index, false)
}
fn sql_dollar_quote_end(bytes: &[u8], start: usize, reach: &mut Reach) -> Option<(usize, bool)> {
let mut second = start + 1;
if bytes
.get(second)
.is_some_and(|byte| matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'_'))
{
second += 1;
while bytes
.get(second)
.is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
{
second += 1;
}
}
reach.byte(second);
if bytes.get(second) != Some(&b'$') {
return None;
}
let delimiter = &bytes[start..=second];
Some(match find_subslice(&bytes[second + 1..], delimiter) {
Some(relative) => {
let end = second + 1 + relative + delimiter.len();
reach.through(end);
(end, true)
}
None => (bytes.len(), false),
})
}
fn oracle_q_quote_end(bytes: &[u8], start: usize, reach: &mut Reach) -> Option<(usize, bool)> {
reach.byte(start + 1);
if bytes.get(start + 1) != Some(&b'\'') {
return None;
}
reach.byte(start + 2);
let open = *bytes.get(start + 2)?;
let close = match open {
b'[' => b']',
b'{' => b'}',
b'(' => b')',
b'<' => b'>',
other => other,
};
let token = [close, b'\''];
Some(match find_subslice(&bytes[start + 3..], &token) {
Some(relative) => {
let end = start + 3 + relative + 2;
reach.through(end);
(end, true)
}
None => (bytes.len(), false),
})
}
fn js_html_close_comment(bytes: &[u8], index: usize) -> bool {
if !starts(bytes, index, b"-->") {
return false;
}
let mut cursor = bytes[..index]
.iter()
.rposition(|byte| matches!(byte, b'\r' | b'\n'))
.map_or(0, |position| position + 1);
while cursor < index {
if starts(bytes, cursor, b"\xef\xbb\xbf") {
cursor += 3;
} else if js_is_space(bytes[cursor]) {
cursor += 1;
} else {
return false;
}
}
true
}
fn js_regex_end(bytes: &[u8], start: usize) -> Option<usize> {
let mut index = start + 1;
let mut class = false;
while index < bytes.len() {
if unicode_line_terminator_width(bytes, index).is_some() {
return None;
}
match bytes[index] {
b'\\' => {
let escaped = index + 1;
if unicode_line_terminator_width(bytes, escaped).is_some() {
return None;
}
index = (index + 2).min(bytes.len());
}
b'[' => {
class = true;
index += 1;
}
b']' => {
class = false;
index += 1;
}
b'/' if !class => {
index += 1;
while index < bytes.len()
&& (bytes[index].is_ascii_alphabetic() || bytes[index] == b'_')
{
index += 1;
}
return Some(index);
}
_ => index += 1,
}
}
None
}
fn is_js_identifier_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$') || byte & 0x80 != 0
}
fn is_js_identifier_continue(byte: u8) -> bool {
is_js_identifier_start(byte) || byte.is_ascii_digit()
}
fn is_js_control_keyword(token: &[u8]) -> bool {
matches!(
token,
b"if" | b"while" | b"for" | b"with" | b"switch" | b"catch"
)
}
fn jsx_open(bytes: &[u8], index: usize) -> bool {
bytes.get(index) == Some(&b'<')
&& bytes
.get(index + 1)
.is_some_and(|byte| byte.is_ascii_alphabetic() || matches!(byte, b'>' | b'_'))
}
fn html_tag_end(bytes: &[u8], start: usize) -> Option<usize> {
let mut index = start + 1;
let mut quote = None;
while index < bytes.len() {
if let Some(active) = quote {
if bytes[index] == active {
quote = None;
}
} else if matches!(bytes[index], b'\'' | b'"') {
quote = Some(bytes[index]);
} else if bytes[index] == b'>' {
return Some(index + 1);
}
index += 1;
}
None
}
fn sfc_tag_end(bytes: &[u8], start: usize) -> Option<usize> {
let mut index = start + 1;
let mut quote = None;
let mut braces = 0usize;
while index < bytes.len() {
if let Some(active) = quote {
if bytes[index] == b'\\' && braces > 0 {
index = (index + 2).min(bytes.len());
continue;
}
if bytes[index] == active {
quote = None;
}
} else {
match bytes[index] {
b'\'' | b'"' | b'`' if braces > 0 => quote = Some(bytes[index]),
b'\'' | b'"' if braces == 0 => quote = Some(bytes[index]),
b'{' => braces += 1,
b'}' if braces > 0 => braces -= 1,
b'>' if braces == 0 => return Some(index + 1),
_ => {}
}
}
index += 1;
}
None
}
fn html_tag_candidate(bytes: &[u8], start: usize) -> bool {
match bytes.get(start + 1).copied() {
Some(byte) if byte.is_ascii_alphabetic() || matches!(byte, b'!' | b'?') => true,
Some(b'/') => bytes
.get(start + 2)
.is_some_and(|byte| byte.is_ascii_alphabetic()),
_ => false,
}
}
fn html_tag_name_end(bytes: &[u8], start: usize) -> usize {
let mut index = start + 1;
if bytes.get(index) == Some(&b'/') {
index += 1;
}
while bytes
.get(index)
.is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'>' | b'/'))
{
index += 1;
}
index
}
fn markdown_inline_code_end(bytes: &[u8], index: usize, reach: &mut Reach) -> usize {
let run = count_run(bytes, index, b'`');
let mut cursor = index + run;
while cursor < bytes.len() {
if bytes[cursor] == b'`' {
let next = count_run(bytes, cursor, b'`');
if next == run {
return cursor + next;
}
cursor += next;
} else {
cursor += 1;
}
}
reach.end_of(bytes);
index + run
}
fn markdown_indent(bytes: &[u8], mut index: usize, end: usize) -> (usize, usize) {
let mut columns = 0usize;
while index < end {
match bytes[index] {
b' ' => {
columns += 1;
index += 1;
}
b'\t' => {
columns += 4 - columns % 4;
index += 1;
}
_ => break,
}
}
(index, columns)
}
fn markdown_fence_language(info: &[u8]) -> Option<Language> {
let trimmed = info.trim_ascii();
let mut word = trimmed;
if word.first() == Some(&b'{') {
let end = word.iter().position(|byte| *byte == b'}')?;
word = &word[1..end];
}
let end = word
.iter()
.position(|byte| byte.is_ascii_whitespace() || *byte == b',')
.unwrap_or(word.len());
let word = &word[..end];
if word.is_empty() {
None
} else {
std::str::from_utf8(word).ok()?.parse().ok()
}
}
fn is_perl_word_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'_')
}
fn perl_at_line_start(bytes: &[u8], index: usize) -> bool {
index == 0 || matches!(bytes.get(index.wrapping_sub(1)), Some(b'\r' | b'\n'))
}
fn perl_marker_line(bytes: &[u8], index: usize, marker: &[u8]) -> bool {
starts(bytes, index, marker)
&& bytes
.get(index + marker.len())
.is_none_or(|byte| byte.is_ascii_whitespace())
}
fn perl_pod_directive(bytes: &[u8], index: usize, name: &[u8]) -> bool {
bytes.get(index) == Some(&b'=')
&& starts(bytes, index + 1, name)
&& bytes
.get(index + 1 + name.len())
.is_none_or(|byte| byte.is_ascii_whitespace())
}
fn perl_variable_end(bytes: &[u8], start: usize) -> usize {
let mut index = start + 1;
if index >= bytes.len() {
return index;
}
if bytes[index] == b'{' {
index += 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == b'}' {
return index + 1;
} else {
index += 1;
}
}
return index;
}
if bytes[index] == b'#'
&& bytes
.get(index + 1)
.is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
{
index += 2;
while index < bytes.len() && is_perl_word_byte(bytes[index]) {
index += 1;
}
return index;
}
if bytes[index] == b'^' {
return (index + 2).min(bytes.len());
}
if bytes[index].is_ascii_digit() {
index += 1;
while index < bytes.len() && bytes[index].is_ascii_digit() {
index += 1;
}
return index;
}
if bytes[index].is_ascii_alphabetic() || bytes[index] == b'_' {
index += 1;
while index < bytes.len() && is_perl_word_byte(bytes[index]) {
index += 1;
}
return index;
}
(index + 1).min(bytes.len())
}
fn perl_quoted_end(bytes: &[u8], start: usize) -> usize {
let quote = bytes[start];
let mut index = start + 1;
while index < bytes.len() {
if bytes[index] == b'\\' {
index = (index + 2).min(bytes.len());
} else if bytes[index] == quote {
return index + 1;
} else {
index += 1;
}
}
bytes.len()
}
fn perl_word_allows_regex(word: &[u8]) -> Option<bool> {
matches!(
word,
b"return"
| b"if"
| b"unless"
| b"while"
| b"until"
| b"for"
| b"foreach"
| b"and"
| b"or"
| b"not"
| b"print"
| b"printf"
| b"say"
| b"split"
| b"grep"
| b"map"
| b"join"
| b"sort"
| b"push"
| b"unshift"
| b"pop"
| b"shift"
| b"splice"
| b"index"
| b"length"
| b"substr"
| b"chomp"
| b"chop"
| b"lc"
| b"uc"
)
.then_some(true)
.or(Some(false))
}
fn perl_section_end_reach(
bytes: &[u8],
start: usize,
delimiter: u8,
reach: &mut Reach,
) -> Option<usize> {
let close = match delimiter {
b'(' => b')',
b'[' => b']',
b'{' => b'}',
b'<' => b'>',
other => other,
};
let mut depth = 1usize;
let mut index = start + 1;
while index < bytes.len() {
reach.byte(index);
if bytes[index] == b'\\' {
reach.byte(index + 1);
index = (index + 2).min(bytes.len());
continue;
}
if delimiter != close && bytes[index] == delimiter {
depth += 1;
} else if bytes[index] == close {
depth -= 1;
index += 1;
if depth == 0 {
return Some(index);
}
continue;
}
index += 1;
}
reach.end_of(bytes);
None
}
fn perl_unpaired_section_end_reach(
bytes: &[u8],
mut index: usize,
delimiter: u8,
reach: &mut Reach,
) -> Option<usize> {
while index < bytes.len() {
reach.byte(index);
if bytes[index] == b'\\' {
reach.byte(index + 1);
index = (index + 2).min(bytes.len());
} else if bytes[index] == delimiter {
return Some(index + 1);
} else {
index += 1;
}
}
reach.end_of(bytes);
None
}
fn perl_modifiers_end(bytes: &[u8], mut index: usize) -> usize {
while bytes
.get(index)
.is_some_and(|byte| byte.is_ascii_alphabetic())
{
index += 1;
}
index
}
struct PerlHeredocDeclaration {
terminator: Vec<u8>,
indented: bool,
}
fn perl_heredoc_declaration(
bytes: &[u8],
operator: usize,
header_end: usize,
) -> Option<(PerlHeredocDeclaration, usize)> {
let mut index = operator + 2;
while index < header_end && matches!(bytes[index], b' ' | b'\t' | 0x0b | 0x0c) {
index += 1;
}
let indented = bytes.get(index) == Some(&b'~');
if indented {
index += 1;
while index < header_end && matches!(bytes[index], b' ' | b'\t' | 0x0b | 0x0c) {
index += 1;
}
}
let (terminator, end) = if let Some("e @ (b'\'' | b'"' | b'`')) = bytes.get(index) {
let content = index + 1;
let relative = bytes[content..header_end]
.iter()
.position(|byte| *byte == quote)?;
let end = content + relative;
(bytes[content..end].to_vec(), end + 1)
} else {
if !bytes
.get(index)
.is_some_and(|byte| byte.is_ascii_alphabetic() || *byte == b'_')
{
return None;
}
let content = index;
index += 1;
while index < header_end && is_perl_word_byte(bytes[index]) {
index += 1;
}
(bytes[content..index].to_vec(), index)
};
(!terminator.is_empty()).then_some((
PerlHeredocDeclaration {
terminator,
indented,
},
end,
))
}
#[derive(Clone, Debug)]
struct ParsedTagAttribute {
name: std::ops::Range<usize>,
value: Option<std::ops::Range<usize>>,
}
fn parse_tag_attributes(attrs: &[u8]) -> Vec<ParsedTagAttribute> {
let mut parsed = Vec::new();
let mut index = 0usize;
while index < attrs.len() {
while index < attrs.len() && attrs[index].is_ascii_whitespace() {
index += 1;
}
if index >= attrs.len() || attrs[index] == b'>' {
break;
}
if attrs[index] == b'/' {
index += 1;
continue;
}
let name_start = index;
while index < attrs.len()
&& !attrs[index].is_ascii_whitespace()
&& !matches!(attrs[index], b'=' | b'>' | b'/')
{
index += 1;
}
if index == name_start {
index += 1;
continue;
}
let name = name_start..index;
while index < attrs.len() && attrs[index].is_ascii_whitespace() {
index += 1;
}
let mut value = None;
if attrs.get(index) == Some(&b'=') {
index += 1;
while index < attrs.len() && attrs[index].is_ascii_whitespace() {
index += 1;
}
if let Some("e @ (b'"' | b'\'')) = attrs.get(index) {
index += 1;
let value_start = index;
while index < attrs.len() && attrs[index] != quote {
index += 1;
}
value = Some(value_start..index);
if index < attrs.len() {
index += 1;
}
} else {
let value_start = index;
if attrs.get(index) == Some(&b'{') {
let mut braces = 0usize;
while index < attrs.len() {
match attrs[index] {
b'{' => braces += 1,
b'}' => {
braces = braces.saturating_sub(1);
index += 1;
if braces == 0 {
break;
}
continue;
}
_ => {}
}
index += 1;
}
} else {
while index < attrs.len()
&& !attrs[index].is_ascii_whitespace()
&& attrs[index] != b'>'
{
index += 1;
}
}
value = Some(value_start..index);
}
}
parsed.push(ParsedTagAttribute { name, value });
}
parsed
}
fn tag_attr_value<'a>(attrs: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
parse_tag_attributes(attrs)
.into_iter()
.find_map(|attribute| {
(attrs[attribute.name.clone()].eq_ignore_ascii_case(name))
.then(|| attribute.value.map(|value| &attrs[value]))
.flatten()
})
}
fn tag_has_attribute(attrs: &[u8], name: &[u8]) -> bool {
parse_tag_attributes(attrs)
.into_iter()
.any(|attribute| attrs[attribute.name].eq_ignore_ascii_case(name))
}
fn vue_directive_attribute(name: &[u8]) -> bool {
name.starts_with(b"v-") || matches!(name.first(), Some(b':' | b'@' | b'#' | b'.'))
}
fn vue_script_language(lang: Option<&[u8]>) -> Option<(Language, Dialect)> {
match lang.map(|value| value.to_ascii_lowercase()).as_deref() {
None | Some(b"js" | b"javascript") => Some((Language::JavaScript, Dialect::Standard)),
Some(b"jsx") => Some((Language::JavaScript, Dialect::Jsx)),
Some(b"ts" | b"typescript") => Some((Language::TypeScript, Dialect::Standard)),
Some(b"tsx") => Some((Language::TypeScript, Dialect::Tsx)),
Some(_) => None,
}
}
fn vue_style_language(lang: Option<&[u8]>) -> Option<(Language, Dialect)> {
match lang.map(|value| value.to_ascii_lowercase()).as_deref() {
None | Some(b"css") => Some((Language::Css, Dialect::Standard)),
Some(b"scss") => Some((Language::Css, Dialect::Scss)),
Some(b"sass") => Some((Language::Css, Dialect::Sass)),
Some(_) => None,
}
}
fn html_embedded_start(bytes: &[u8], start: usize) -> Option<(&'static [u8], Language)> {
let rest = &bytes[start..];
if starts_ascii_case(rest, b"<script") && tag_boundary(rest.get(7).copied()) {
Some((b"script", Language::JavaScript))
} else if starts_ascii_case(rest, b"<style") && tag_boundary(rest.get(6).copied()) {
Some((b"style", Language::Css))
} else {
None
}
}
fn find_html_close(bytes: &[u8], content_start: usize, name: &[u8]) -> Option<usize> {
let mut close = Vec::with_capacity(name.len() + 2);
close.extend_from_slice(b"</");
close.extend_from_slice(name);
let mut cursor = content_start;
while cursor + close.len() <= bytes.len() {
let relative = find_ascii_case(&bytes[cursor..], &close)?;
let candidate = cursor + relative;
if tag_boundary(bytes.get(candidate + close.len()).copied()) {
return Some(candidate);
}
cursor = candidate + close.len();
}
None
}
fn find_balanced_html_close(bytes: &[u8], content_start: usize, name: &[u8]) -> Option<usize> {
let mut index = content_start;
let mut depth = 1usize;
while index < bytes.len() {
let relative = memchr(b'<', &bytes[index..])?;
let tag = index + relative;
if starts(bytes, tag, b"<!--") {
index = find_subslice(&bytes[tag + 4..], b"-->")
.map_or(bytes.len(), |offset| tag + 4 + offset + 3);
continue;
}
let closing = bytes.get(tag + 1) == Some(&b'/');
let name_start = tag + if closing { 2 } else { 1 };
if name_start + name.len() <= bytes.len()
&& bytes[name_start..name_start + name.len()].eq_ignore_ascii_case(name)
&& tag_boundary(bytes.get(name_start + name.len()).copied())
{
let tag_end = html_tag_end(bytes, tag)?;
if closing {
depth -= 1;
if depth == 0 {
return Some(tag);
}
} else {
let mut before = tag_end.saturating_sub(1);
while before > tag && bytes[before - 1].is_ascii_whitespace() {
before -= 1;
}
if before == tag || bytes[before - 1] != b'/' {
depth += 1;
}
}
index = tag_end;
} else {
index = tag + 1;
}
}
None
}
fn php_open_tag(bytes: &[u8], index: usize) -> Option<usize> {
if starts(bytes, index, b"<?=") {
return Some(index + 3);
}
let rest = bytes.get(index..)?;
if starts_ascii_case(rest, b"<?php")
&& rest
.get(5)
.is_none_or(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
{
return Some(index + 5);
}
None
}
fn php_line_comment_end(bytes: &[u8], mut index: usize) -> usize {
while index < bytes.len()
&& !matches!(bytes[index], b'\r' | b'\n')
&& !starts(bytes, index, b"?>")
{
index += 1;
}
index
}
fn php_block_kind(bytes: &[u8], index: usize) -> CommentKind {
if starts(bytes, index, b"/**")
&& bytes
.get(index + 3)
.is_some_and(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
{
CommentKind::DocBlock
} else {
CommentKind::Block
}
}
fn php_interpolation_end(bytes: &[u8], brace: usize) -> usize {
let mut depth = 1usize;
let mut index = brace + 1;
while index < bytes.len() {
match bytes[index] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return index + 1;
}
}
quote @ (b'\'' | b'"' | b'`') => {
index += 1;
while index < bytes.len() && bytes[index] != quote {
index = if bytes[index] == b'\\' {
(index + 2).min(bytes.len())
} else {
index + 1
};
}
}
b'/' if starts(bytes, index, b"/*") => {
index = block_end(bytes, index, b"/*", b"*/", false).0;
continue;
}
b'/' if starts(bytes, index, b"//") => {
index = php_line_comment_end(bytes, index + 2);
continue;
}
b'#' if bytes.get(index + 1) != Some(&b'[') => {
index = php_line_comment_end(bytes, index + 1);
continue;
}
_ => {}
}
index += 1;
}
index.min(bytes.len())
}
fn php_label_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || byte == b'_' || byte >= 0x80
}
fn php_label_continue(byte: u8) -> bool {
php_label_start(byte) || byte.is_ascii_digit()
}
fn php_heredoc_header(bytes: &[u8], start: usize) -> Option<(&[u8], usize, bool)> {
let mut cursor = start + 3;
while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
cursor += 1;
}
let quote = match bytes.get(cursor) {
Some(byte @ (b'\'' | b'"')) => Some(*byte),
_ => None,
};
if quote.is_some() {
cursor += 1;
}
let label_start = cursor;
if !bytes.get(cursor).is_some_and(|byte| php_label_start(*byte)) {
return None;
}
while bytes
.get(cursor)
.is_some_and(|byte| php_label_continue(*byte))
{
cursor += 1;
}
let label = &bytes[label_start..cursor];
if let Some(quote) = quote {
if bytes.get(cursor) != Some("e) {
return None;
}
cursor += 1;
}
if !matches!(bytes.get(cursor), Some(b'\r' | b'\n')) {
return None;
}
Some((label, consume_newline(bytes, cursor), quote == Some(b'\'')))
}
fn php_heredoc_end(bytes: &[u8], mut index: usize, label: &[u8]) -> Option<usize> {
loop {
let mut cursor = index;
while matches!(bytes.get(cursor), Some(b' ' | b'\t')) {
cursor += 1;
}
if starts(bytes, cursor, label)
&& !bytes
.get(cursor + label.len())
.is_some_and(|byte| php_label_continue(*byte))
{
return Some(cursor + label.len());
}
let end = line_end(bytes, index);
if end >= bytes.len() {
return None;
}
index = consume_newline(bytes, end);
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RubyState {
Begin,
Argument,
End,
Fname,
}
#[derive(Clone, Copy)]
struct RubyPercent {
form: u8,
open: u8,
close: u8,
content: usize,
interpolates: bool,
}
struct RubyHeredoc {
operator: usize,
label: Vec<u8>,
indented: bool,
interpolates: bool,
}
fn ruby_identifier_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || byte == b'_' || !byte.is_ascii()
}
fn ruby_identifier_continue(byte: u8) -> bool {
ruby_identifier_start(byte) || byte.is_ascii_digit()
}
fn ruby_is_space(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | 0x0b | 0x0c)
}
fn ruby_identifier_end(bytes: &[u8], mut index: usize) -> usize {
while bytes
.get(index)
.is_some_and(|byte| ruby_identifier_continue(*byte))
{
index += 1;
}
index
}
fn ruby_word_end(bytes: &[u8], index: usize) -> usize {
let end = ruby_identifier_end(bytes, index + 1);
if matches!(bytes.get(end), Some(b'?' | b'!')) && bytes.get(end + 1) != Some(&b'=') {
end + 1
} else {
end
}
}
fn ruby_number_end(bytes: &[u8], mut index: usize) -> usize {
loop {
index = ruby_identifier_end(bytes, index);
if bytes.get(index) == Some(&b'.') && bytes.get(index + 1).is_some_and(u8::is_ascii_digit) {
index += 1;
continue;
}
return index;
}
}
fn ruby_state_after_word(token: &[u8]) -> RubyState {
match token {
b"end" | b"self" | b"nil" | b"true" | b"false" | b"redo" | b"retry" | b"__FILE__"
| b"__LINE__" | b"__ENCODING__" | b"def" | b"class" | b"module" => RubyState::End,
b"alias" | b"undef" => RubyState::Fname,
b"if" | b"unless" | b"while" | b"until" | b"case" | b"when" | b"in" | b"and" | b"or"
| b"return" | b"break" | b"next" | b"then" | b"do" | b"else" | b"elsif" | b"begin"
| b"ensure" | b"rescue" | b"for" => RubyState::Begin,
_ => RubyState::Argument,
}
}
fn ruby_literal_opens(state: RubyState, space_seen: bool, bytes: &[u8], index: usize) -> bool {
match state {
RubyState::Begin => true,
RubyState::End | RubyState::Fname => false,
RubyState::Argument => {
space_seen
&& bytes.get(index + 1).is_some_and(|byte| {
*byte != b'=' && !ruby_is_space(*byte) && !matches!(byte, b'\r' | b'\n')
})
}
}
}
fn ruby_heredoc_may_open(state: RubyState, space_seen: bool) -> bool {
match state {
RubyState::Begin => true,
RubyState::Argument => space_seen,
RubyState::End | RubyState::Fname => false,
}
}
fn ruby_percent_opens(
state: RubyState,
space_seen: bool,
bytes: &[u8],
index: usize,
form: u8,
) -> bool {
(state == RubyState::Fname && form == b's')
|| ruby_literal_opens(state, space_seen, bytes, index)
}
fn ruby_at_line_start(bytes: &[u8], index: usize, offset: usize) -> bool {
if index == 0 || (offset == 0 && index == byte_order_mark_width(bytes)) {
return true;
}
match bytes[index - 1] {
b'\n' => true,
b'\r' => bytes.get(index) != Some(&b'\n'),
_ => false,
}
}
fn ruby_embedded_document(bytes: &[u8], index: usize) -> bool {
starts(bytes, index, b"=begin") && ruby_word_boundary(bytes, index + b"=begin".len())
}
fn ruby_embedded_document_end(bytes: &[u8], start: usize) -> (usize, bool) {
let mut index = line_end(bytes, start);
while index < bytes.len() {
index = consume_newline(bytes, index);
if starts(bytes, index, b"=end") && ruby_word_boundary(bytes, index + b"=end".len()) {
return (line_end(bytes, index + b"=end".len()), true);
}
index = line_end(bytes, index);
}
(bytes.len(), false)
}
fn ruby_word_boundary(bytes: &[u8], index: usize) -> bool {
bytes
.get(index)
.is_none_or(|byte| ruby_is_space(*byte) || matches!(byte, b'\r' | b'\n'))
}
fn ruby_data_marker(bytes: &[u8], index: usize) -> bool {
starts(bytes, index, b"__END__")
&& matches!(
bytes.get(index + b"__END__".len()),
None | Some(b'\r' | b'\n')
)
}
fn ruby_symbol_head(byte: u8) -> bool {
ruby_identifier_start(byte) || matches!(byte, b'@' | b'$') || ruby_symbol_operator(byte)
}
fn ruby_symbol_operator(byte: u8) -> bool {
matches!(
byte,
b'+' | b'-' | b'*' | b'/' | b'%' | b'<' | b'>' | b'=' | b'!' | b'~' | b'^' | b'&' | b'|'
) || matches!(byte, b'[' | b']' | b'@')
}
fn ruby_symbol_end(bytes: &[u8], index: usize) -> usize {
let mut cursor = index + 1;
match bytes.get(cursor) {
Some(b'$') => return ruby_global_end(bytes, cursor),
Some(b'@') => return ruby_at_variable_end(bytes, cursor),
Some(byte) if ruby_identifier_start(*byte) => return ruby_word_end(bytes, cursor),
_ => {}
}
while bytes
.get(cursor)
.is_some_and(|byte| ruby_symbol_operator(*byte))
{
cursor += 1;
}
cursor
}
fn ruby_global_end(bytes: &[u8], index: usize) -> usize {
let Some(byte) = bytes.get(index + 1).copied() else {
return index + 1;
};
if ruby_identifier_start(byte) {
return ruby_identifier_end(bytes, index + 2);
}
if byte.is_ascii_digit() {
let mut end = index + 2;
while bytes.get(end).is_some_and(u8::is_ascii_digit) {
end += 1;
}
return end;
}
if byte == b'-' {
return (index + 3).min(bytes.len());
}
if matches!(
byte,
b'~' | b'*' | b'$' | b'?' | b'!' | b'@' | b'/' | b'\\' | b';' | b',' | b'.' | b'='
) || matches!(byte, b':' | b'<' | b'>' | b'"' | b'&' | b'`' | b'\'' | b'+')
{
return index + 2;
}
index + 1
}
fn ruby_at_variable_end(bytes: &[u8], index: usize) -> usize {
let mut cursor = index + 1;
if bytes.get(cursor) == Some(&b'@') {
cursor += 1;
}
ruby_identifier_end(bytes, cursor)
}
fn ruby_character_literal_end(bytes: &[u8], question: usize) -> Option<usize> {
let index = question + 1;
let byte = *bytes.get(index)?;
if ruby_is_space(byte) || matches!(byte, b'\r' | b'\n') {
return None;
}
if !byte.is_ascii() {
return Some((index + ruby_character_width(byte)).min(bytes.len()));
}
if byte == b'\\' {
if bytes.get(index + 1) == Some(&b'u') && bytes.get(index + 2) == Some(&b'{') {
let mut cursor = index + 3;
while bytes.get(cursor).is_some_and(|byte| *byte != b'}') {
cursor += 1;
}
return Some((cursor + 1).min(bytes.len()));
}
return Some((index + 2).min(bytes.len()));
}
if ruby_identifier_continue(byte)
&& bytes
.get(index + 1)
.is_some_and(|next| ruby_identifier_continue(*next))
{
return None;
}
Some(index + 1)
}
fn ruby_character_width(byte: u8) -> usize {
match byte {
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf7 => 4,
_ => 1,
}
}
fn ruby_regexp_flags_end(bytes: &[u8], mut index: usize) -> usize {
while bytes.get(index).is_some_and(u8::is_ascii_alphabetic) {
index += 1;
}
index
}
fn ruby_percent_header(bytes: &[u8], start: usize) -> Option<RubyPercent> {
let first = *bytes.get(start + 1)?;
let (form, delimiter, content) = if first.is_ascii_alphanumeric() {
if !matches!(
first,
b'q' | b'Q' | b'w' | b'W' | b'i' | b'I' | b's' | b'r' | b'x'
) {
return None;
}
(first, *bytes.get(start + 2)?, start + 3)
} else {
(b'Q', first, start + 2)
};
if delimiter.is_ascii_alphanumeric() || !delimiter.is_ascii() {
return None;
}
let close = match delimiter {
b'(' => b')',
b'[' => b']',
b'{' => b'}',
b'<' => b'>',
_ => delimiter,
};
Some(RubyPercent {
form,
open: delimiter,
close,
content,
interpolates: matches!(form, b'Q' | b'W' | b'I' | b'r' | b'x'),
})
}
fn ruby_heredoc_header(bytes: &[u8], index: usize) -> Option<(RubyHeredoc, usize)> {
let mut cursor = index + 2;
let indented = matches!(bytes.get(cursor), Some(b'-' | b'~'));
if indented {
cursor += 1;
}
let quote = match bytes.get(cursor)? {
b'\'' => Some(b'\''),
b'"' => Some(b'"'),
b'`' => Some(b'`'),
byte if ruby_identifier_continue(*byte) => None,
_ => return None,
};
let (label, end) = match quote {
Some(quote) => {
let start = cursor + 1;
let mut end = start;
loop {
match bytes.get(end) {
Some(byte) if *byte == quote => break,
None | Some(b'\r' | b'\n') => return None,
Some(_) => end += 1,
}
}
(bytes[start..end].to_vec(), end + 1)
}
None => {
let end = ruby_identifier_end(bytes, cursor + 1);
(bytes[cursor..end].to_vec(), end)
}
};
Some((
RubyHeredoc {
operator: index,
label,
indented,
interpolates: quote != Some(b'\''),
},
end,
))
}
fn ruby_heredoc_terminates(bytes: &[u8], index: usize, heredoc: &RubyHeredoc) -> bool {
let mut probe = index;
if heredoc.indented {
while bytes.get(probe).is_some_and(|byte| ruby_is_space(*byte)) {
probe += 1;
}
}
starts(bytes, probe, &heredoc.label)
&& matches!(
bytes.get(probe + heredoc.label.len()),
None | Some(b'\r' | b'\n')
)
}
fn starts_ascii_case(haystack: &[u8], needle: &[u8]) -> bool {
haystack
.get(..needle.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(needle))
}
fn find_ascii_case(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window.eq_ignore_ascii_case(needle))
}
fn tag_boundary(byte: Option<u8>) -> bool {
byte.is_none_or(|byte| byte.is_ascii_whitespace() || matches!(byte, b'>' | b'/'))
}
fn contains_line_splice(bytes: &[u8]) -> bool {
let mut cursor = 0;
while let Some(relative) = memchr(b'\\', &bytes[cursor..]) {
let index = cursor + relative;
if bytes.get(index + 1) == Some(&b'\n')
|| (bytes.get(index + 1) == Some(&b'\r') && bytes.get(index + 2) == Some(&b'\n'))
{
return true;
}
cursor = index + 1;
}
false
}
fn next_c_family_trigger(
bytes: &[u8],
start: usize,
language: Language,
dialect: Dialect,
) -> Option<usize> {
let remaining = bytes.get(start..)?;
let primary = match language {
Language::Go => remaining
.iter()
.position(|byte| matches!(byte, b'/' | b'"' | b'\'' | b'`')),
Language::Css if dialect == Dialect::Scss => remaining
.iter()
.position(|byte| matches!(byte, b'/' | b'"' | b'\'' | b'#' | b'u' | b'U')),
_ => memchr3(b'/', b'"', b'\'', remaining),
}?;
Some(start + primary)
}
fn is_css_identifier_part(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')
}
fn css_whitespace(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | 0x0c)
}
fn line_start(bytes: &[u8], index: usize) -> usize {
bytes[..index.min(bytes.len())]
.iter()
.rposition(|byte| matches!(byte, b'\r' | b'\n'))
.map_or(0, |position| position + 1)
}
fn sass_indent_width(bytes: &[u8]) -> usize {
bytes.iter().fold(0, |column, byte| match byte {
b'\t' => column + (8 - column % 8),
_ => column + 1,
})
}
fn kotlin_dollar_width(bytes: &[u8], start: usize) -> usize {
let mut prefix = start;
while prefix > 0 && bytes[prefix - 1] == b'$' {
prefix -= 1;
}
(start - prefix).max(1)
}
struct MappedBytes {
bytes: Vec<u8>,
origins: Vec<ByteSpan>,
original_len: usize,
}
impl MappedBytes {
fn without_c_line_splices(source: &[u8]) -> Self {
let mut bytes = Vec::with_capacity(source.len());
let mut origins = Vec::with_capacity(source.len());
let mut index = 0;
while index < source.len() {
if starts(source, index, b"\\\r\n") {
index += 3;
continue;
}
if starts(source, index, b"\\\n") {
index += 2;
continue;
}
bytes.push(source[index]);
origins.push(ByteSpan::new(index, index + 1));
index += 1;
}
Self {
bytes,
origins,
original_len: source.len(),
}
}
fn java_unicode(source: &[u8]) -> (Self, Vec<ByteSpan>) {
let mut bytes = Vec::with_capacity(source.len());
let mut origins = Vec::with_capacity(source.len());
let mut invalid = Vec::new();
let mut index = 0;
let mut slash_run = 0usize;
let mut last_was_escape = false;
while index < source.len() {
let eligible = source[index] == b'\\' && (last_was_escape || slash_run & 1 == 0);
if eligible {
let mut cursor = index + 1;
while source.get(cursor) == Some(&b'u') {
cursor += 1;
}
if cursor > index + 1 {
if cursor + 4 <= source.len()
&& let Some(value) = hex4(&source[cursor..cursor + 4])
{
if value <= 0x7f {
bytes.push(value as u8);
origins.push(ByteSpan::new(index, cursor + 4));
if value as u8 == b'\\' {
slash_run += 1;
last_was_escape = true;
} else {
slash_run = 0;
last_was_escape = false;
}
index = cursor + 4;
continue;
}
if let Some(character) = char::from_u32(value as u32) {
let mut encoded = [0; 4];
for byte in character.encode_utf8(&mut encoded).as_bytes() {
bytes.push(*byte);
origins.push(ByteSpan::new(index, cursor + 4));
}
} else {
bytes.push(0x80);
origins.push(ByteSpan::new(index, cursor + 4));
}
slash_run = 0;
last_was_escape = false;
index = cursor + 4;
continue;
}
invalid.push(ByteSpan::new(index, (cursor + 4).min(source.len())));
}
}
bytes.push(source[index]);
origins.push(ByteSpan::new(index, index + 1));
if source[index] == b'\\' {
slash_run += 1;
} else {
slash_run = 0;
}
last_was_escape = false;
index += 1;
}
(
Self {
bytes,
origins,
original_len: source.len(),
},
invalid,
)
}
fn original_span(&self, span: ByteSpan) -> ByteSpan {
if span.is_empty() {
let point = self
.origins
.get(span.start)
.map_or(self.original_len, |origin| origin.start);
return ByteSpan::new(point, point);
}
let start = self
.origins
.get(span.start)
.map_or(self.original_len, |origin| origin.start);
let end = if span.end == self.bytes.len() {
self.original_len
} else {
self.origins
.get(span.end.saturating_sub(1))
.map_or(self.original_len, |origin| origin.end)
};
ByteSpan::new(start, end)
}
}
fn hex4(bytes: &[u8]) -> Option<u16> {
let mut value = 0u16;
for byte in bytes {
value = value.checked_mul(16)?
+ match byte {
b'0'..=b'9' => (byte - b'0') as u16,
b'a'..=b'f' => (byte - b'a' + 10) as u16,
b'A'..=b'F' => (byte - b'A' + 10) as u16,
_ => return None,
};
}
Some(value)
}
#[cfg(test)]
mod tests {
use super::*;
fn comments(source: &[u8], language: Language) -> ScanReport {
scan(source, language, ScanOptions::default())
}
#[test]
fn rust_nested_and_raw() {
let report = comments(
br##"r#"// nope"# /* one /* two */ end */ // yes"##,
Language::Rust,
);
assert!(report.valid);
assert_eq!(report.comments.len(), 2);
}
#[test]
fn javascript_regex_and_template_expression() {
let report = comments(
br#"const x = /\/\/*not/; `text // no ${1 /* yes */}`; // yes"#,
Language::JavaScript,
);
assert_eq!(report.comments.len(), 2);
}
#[test]
fn java_unicode_delimiter() {
let report = comments(br"int x; \u002f\u002f hi\nint y;", Language::Java);
assert_eq!(report.comments.len(), 1);
assert_eq!(
&br"int x; \u002f\u002f hi\nint y;"
[report.comments[0].span.start..report.comments[0].span.end],
br"\u002f\u002f hi\nint y;"
);
}
#[test]
fn a_heredoc_delimiter_parse_reports_every_byte_it_consulted() {
let quoted = b"cat <<\"EO\nF\"\nx\n";
assert_eq!(quoted[9], b'\n');
assert_eq!(quoted[11], b'"');
let mut reach = Reach::default();
let (heredoc, end) =
parse_heredoc(quoted, 4, &mut reach).expect("a quoted delimiter word spanning a line");
assert_eq!(heredoc.delimiter, b"EO\nF");
assert_eq!(end, 12);
assert_eq!(reach, Reach(13));
assert!(
reach.0 > 11,
"{reach:?} does not cover the closing quote at 11"
);
let plain = b"cat <<EOF\nx\n";
assert_eq!(plain[9], b'\n');
let mut reach = Reach::default();
let (heredoc, end) = parse_heredoc(plain, 4, &mut reach).expect("a plain delimiter word");
assert_eq!(heredoc.delimiter, b"EOF");
assert_eq!(end, 9);
assert_eq!(reach, Reach(10));
assert!(
reach.0 <= 10,
"{reach:?} reaches past the line the delimiter word ends on"
);
}
#[test]
fn a_class_bounded_tag_search_keeps_the_checkpoints_under_it() {
let ocaml = b"let x = {aa\n(* c *)\ny\n";
assert_eq!(
scan_checkpoint_watermarks(ocaml, Language::Ocaml, ScanOptions::default()),
[(0, 0), (12, 12), (20, 12), (22, 12)]
);
let cpp = b"char* s = R\"x y\";\n// c\nz\n";
assert_eq!(cpp[13], b' ');
assert_eq!(
scan_checkpoint_watermarks(cpp, Language::Cpp, ScanOptions::default()),
[(0, 0), (18, 14), (23, 14), (25, 14)]
);
let postgres = b"select a$b from t\n-- c\nx\n";
assert_eq!(
scan_checkpoint_watermarks(
postgres,
Language::Sql,
ScanOptions {
dialect: Dialect::PostgreSql,
..Default::default()
}
),
[(0, 0), (18, 11), (23, 11), (25, 11)]
);
let oracle = b"select q from t\n-- c\nx\n";
assert_eq!(
scan_checkpoint_watermarks(
oracle,
Language::Sql,
ScanOptions {
dialect: Dialect::Oracle,
..Default::default()
}
),
[(0, 0), (16, 9), (21, 9), (23, 9)]
);
}
#[test]
fn swift_lookaheads_pay_for_what_they_read() {
let directive = b"let a = #if x\n// c\ny\n";
assert_eq!(
scan_checkpoint_watermarks(directive, Language::Swift, ScanOptions::default()),
[(0, 0), (14, 10), (19, 10), (21, 10)]
);
let division = b"let a = 1 / 2\n// c\nx\n";
assert_eq!(
scan_checkpoint_watermarks(division, Language::Swift, ScanOptions::default()),
[(0, 0), (14, 12), (19, 12), (21, 12)]
);
let candidate = b"let a = (/x y\n// c\nz\n";
assert_eq!(candidate[13], b'\n');
assert_eq!(
scan_checkpoint_watermarks(candidate, Language::Swift, ScanOptions::default()),
[(0, 0), (14, 14), (19, 14), (21, 14)]
);
let unterminated = b"let a = #/\n// c\nz\n";
assert_eq!(
scan_checkpoint_watermarks(unterminated, Language::Swift, ScanOptions::default()),
[(0, 0)]
);
}
#[test]
fn csharp_lookaheads_pay_for_what_they_read() {
let identifier = b"var a = @x;\n// c\ny\n";
assert_eq!(identifier[9], b'x');
assert_eq!(
scan_checkpoint_watermarks(identifier, Language::CSharp, ScanOptions::default()),
[(0, 0), (12, 10), (17, 10), (19, 10)]
);
let raw = b"var a = \"\"\"\n x\n \"\"\";\n// c\ny\n";
assert_eq!(raw[21], b';');
assert_eq!(
scan_checkpoint_watermarks(raw, Language::CSharp, ScanOptions::default()),
[(0, 0), (23, 22), (28, 22), (30, 22)]
);
let directive = b"#if x // c\nvar a = 1;\n";
assert_eq!(
scan_checkpoint_watermarks(directive, Language::CSharp, ScanOptions::default()),
[(0, 0), (11, 0), (22, 0)]
);
let unterminated = b"var a = @\"open\n// c\nz\n";
assert_eq!(
scan_checkpoint_watermarks(unterminated, Language::CSharp, ScanOptions::default()),
[(0, 0)]
);
}
#[test]
fn shell_heredoc_is_opaque() {
let report = comments(b"cat <<EOF\n# data\nEOF\n# comment\n", Language::Shell);
assert_eq!(report.comments.len(), 1);
}
#[test]
fn regex_overrides_apply_to_complete_comment_bytes() {
let source = b"// KEEP this\n// REMOVE this\n// ordinary\n";
let report = scan(
source,
Language::C,
ScanOptions {
policy: Policy::Legal,
keep_regex: vec!["KEEP".into()],
remove_regex: vec!["REMOVE".into()],
..Default::default()
},
);
assert!(matches!(
report.comments[0].disposition,
Disposition::Keep { .. }
));
assert!(report.comments[1].disposition.is_remove());
assert!(report.comments[2].disposition.is_remove());
}
#[test]
fn html_embeds_javascript_but_protects_html() {
let report = comments(
b"<!--keep--><script>let x=1;//remove\n</script>",
Language::Html,
);
assert_eq!(report.comments.len(), 2);
assert!(!report.comments[0].disposition.is_remove());
assert!(report.comments[1].disposition.is_remove());
}
#[test]
fn a_scala_xml_boundary_keeps_its_checkpoint_away_from_the_literal() {
let xml = b"x\n<a>// text</a>\ny\n";
assert_eq!(
scan_checkpoint_watermarks(xml, Language::Scala, ScanOptions::default()),
[(0, 0), (17, 0), (19, 0)]
);
let (_, checkpoints) =
scan_with_checkpoints(xml, Language::Scala, ScanOptions::default(), 0);
assert_eq!(checkpoints, vec![0, 17, 19]);
for point in &checkpoints {
let (suffix, _) = scan_with_checkpoints(
&xml[*point..],
Language::Scala,
ScanOptions::default(),
*point,
);
assert!(suffix.comments.is_empty(), "restarting at {point}");
}
let operator = b"x\n<1 + 2\n// c\ny\n";
assert_eq!(
scan_checkpoint_watermarks(operator, Language::Scala, ScanOptions::default()),
[(0, 0), (2, 0), (9, 0), (14, 0), (16, 0)]
);
let (_, checkpoints) =
scan_with_checkpoints(operator, Language::Scala, ScanOptions::default(), 0);
assert_eq!(checkpoints, vec![0, 2, 9, 14, 16]);
for point in &checkpoints {
let (suffix, _) = scan_with_checkpoints(
&operator[*point..],
Language::Scala,
ScanOptions::default(),
*point,
);
assert!(
suffix
.comments
.iter()
.all(|comment| comment.span.start >= *point),
"restarting at {point}"
);
}
let cr = b"\r<?php //go:build\nz\n";
assert_eq!(
scan_checkpoint_watermarks(cr, Language::Scala, ScanOptions::default()),
[(0, 0), (18, 0), (20, 0)]
);
}
}