use rowan::TextRange;
use crate::parser::core::SyntaxError;
use crate::parser::grammar::is_def_prefix_command;
use crate::parser::grammar::{BEGIN_CMD, END_CMD, reads_definition_body};
use crate::parser::lexer::reads_following_text;
use crate::semantic::define::is_definition_command;
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
use super::Edit;
#[derive(Clone, Copy)]
pub(super) struct Context {
pub(super) in_math: bool,
}
pub(super) fn context_admits(leaf: &SyntaxToken, relex_from: &SyntaxToken) -> Option<Context> {
let mut in_math = false;
for node in leaf.parent_ancestors() {
match node.kind() {
SyntaxKind::NAME_GROUP | SyntaxKind::BEGIN | SyntaxKind::END => return None,
SyntaxKind::MATH => in_math = true,
SyntaxKind::COMMAND => {
let head = command_head(&node)?;
if head_reads_following_text(&head) || is_expl3_name(&head) {
return None;
}
}
_ => {}
}
}
let mut prev = relex_from.prev_token();
while let Some(token) = prev {
if token.kind() == SyntaxKind::NEWLINE {
break;
}
if token.kind() == SyntaxKind::CONTROL_WORD && head_reads_following_text(token.text()) {
return None;
}
prev = token.prev_token();
}
if leaf
.prev_token()
.is_some_and(|t| t.kind() == SyntaxKind::HASH)
{
return None;
}
Some(Context { in_math })
}
fn is_expl3_name(text: &str) -> bool {
text.contains(':')
}
fn command_head(command: &SyntaxNode) -> Option<String> {
command
.children_with_tokens()
.filter_map(|e| e.into_token())
.find(|t| {
matches!(
t.kind(),
SyntaxKind::CONTROL_WORD | SyntaxKind::CONTROL_SYMBOL
)
})
.map(|t| t.text().to_owned())
}
fn head_reads_following_text(text: &str) -> bool {
if text == BEGIN_CMD || text == END_CMD {
return true;
}
if reads_definition_body(text) || is_def_prefix_command(text) {
return true;
}
if text.strip_prefix('\\').is_some_and(is_definition_command) {
return true;
}
reads_following_text(text)
}
pub(super) fn text_reads_are_inert(kind: SyntaxKind, old: &str, new: &str, ctx: Context) -> bool {
match kind {
SyntaxKind::WHITESPACE | SyntaxKind::COMMENT => true,
SyntaxKind::WORD => word_reads_are_inert(old, new, ctx),
SyntaxKind::VERB | SyntaxKind::VERBATIM_BODY => raw_capture_reads_are_inert(old, new),
_ => false,
}
}
fn word_reads_are_inert(old: &str, new: &str, ctx: Context) -> bool {
if old.contains(';') != new.contains(';') {
return false;
}
if old == "*" || new == "*" {
return false;
}
if ctx.in_math {
return false;
}
if old.starts_with('\\') || new.starts_with('\\') {
return false;
}
true
}
fn raw_capture_reads_are_inert(old: &str, new: &str) -> bool {
old.starts_with('\\') == new.starts_with('\\')
}
pub(super) fn shifted_errors(
errors: &[SyntaxError],
leaf: TextRange,
edit: &Edit,
) -> Option<Vec<SyntaxError>> {
let start = usize::from(leaf.start());
let end = usize::from(leaf.end());
let delta = edit.delta();
let mut out = Vec::with_capacity(errors.len());
for error in errors {
if error.end <= start {
out.push(error.clone());
} else if error.start >= end {
out.push(SyntaxError {
message: error.message.clone(),
start: error.start.checked_add_signed(delta)?,
end: error.end.checked_add_signed(delta)?,
});
} else {
return None;
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use crate::syntax::SyntaxKind;
use super::text_reads_are_inert;
fn ctx() -> super::Context {
super::Context { in_math: false }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Verdict {
ControlSequence,
Accessor,
Offsets,
Guarded(&'static str),
Context(&'static str),
}
use Verdict::*;
const TEXT_READS: &[(&str, Verdict)] = &[
(
"&& P::MATH_ANCHOR.anchors(t.text.as_str()) =>",
ControlSequence,
),
("&& super::is_def_prefix_command(&t.text)", ControlSequence),
(
"let end = self.tokens[idx].text.len();",
Guarded("math WORD slicing, guarded by the token tier's structural proof"),
),
(
"let last = self.tokens[idx].text[start..end]",
Guarded("math WORD slicing, guarded by the token tier's structural proof"),
),
(
"let text = &self.tokens[idx].text;",
Guarded("math WORD slicing, guarded by the token tier's structural proof"),
),
("&& self.tokens[i].text == END_CMD", ControlSequence),
(".map(|t| t.text.as_str())", Accessor),
(".text", ControlSequence),
(r".then(|| t.text.strip_prefix('\\'))", ControlSequence),
("0 => definition_name_slots(&t.text),", ControlSequence),
(
r#"Some(SyntaxKind::CONTROL_SYMBOL) => matches!(self.text(), "\\]" | "\\)"),"#,
ControlSequence,
),
(
r#"Some(SyntaxKind::CONTROL_SYMBOL) if matches!(self.text(), "\\]" | "\\)") => {"#,
ControlSequence,
),
(
r#"Some(SyntaxKind::CONTROL_SYMBOL) if self.text() == "\\\\" => self.line_break(),"#,
ControlSequence,
),
(
"Some(SyntaxKind::CONTROL_SYMBOL) if self.text() == closer => {",
ControlSequence,
),
(
"SyntaxKind::CONTROL_SYMBOL => match t.text.as_str() {",
ControlSequence,
),
(
"SyntaxKind::WORD if is_param_digit_text(&t.text) => {",
Context("a `#` immediately before the leaf"),
),
(
"SyntaxKind::WORD if t.text.chars().count() == 1 => {",
Context("an expl3 (colon-carrying) head on an ancestor `COMMAND`"),
),
(
"[t] => Cow::Borrowed(t.text.trim()),",
Context("a `NAME_GROUP`/`BEGIN`/`END` ancestor, or a `\\begin` head"),
),
(
"if let Some(toggle) = expl_toggle(&t.text) {",
ControlSequence,
),
(
"if self.kind() == Some(SyntaxKind::CONTROL_SYMBOL) && self.text() == closer {",
ControlSequence,
),
(
r#"if self.kind() == Some(SyntaxKind::WORD) && self.text() == "*" {"#,
Guarded("the lone-`*` ban, itself gated on `WORD`"),
),
(
"if self.tokens.get(idx).is_some_and(|t| t.text == BEGIN_CMD) {",
ControlSequence,
),
(
"if self.tokens.get(self.pos).map(|t| (t.kind, t.text.as_str()))",
Guarded("the lone-`*` ban (`at_star_variant_marker`), gated on `WORD`"),
),
("if t.text.as_str() == RIGHT_CMD {", ControlSequence),
(
"let bracket = if is_big_delimiter_command(self.text()) {",
ControlSequence,
),
(
"let builtin_args = builtin_command_args(self.text());",
ControlSequence,
),
(
"is_def_prefix_command(self.text()) || is_command_definition_command(self.text());",
ControlSequence,
),
(
"let kind = match self.tokens[s.next].text.as_str() {",
ControlSequence,
),
(r"let name = t.text.strip_prefix('\\')?;", ControlSequence),
("let sym = self.text().to_owned();", ControlSequence),
(
"let text = self.p.tokens[idx].text.as_str();",
ControlSequence,
),
("match self.p.tokens[idx].text.as_str() {", ControlSequence),
(
"name.push_str(&t.text);",
Context("a `NAME_GROUP`/`BEGIN`/`END` ancestor, or a `\\begin` head"),
),
(
"name.push_str(self.text());",
Context("a `NAME_GROUP`/`BEGIN`/`END` ancestor, or a `\\begin` head"),
),
("off += t.text.len();", Offsets),
(
"return Some(t.text.as_str());",
Guarded(
"the leading-backslash ban (`peek_meaningful_text`) — a `WORD` may not \
gain one, and a raw capture may not gain or lose one",
),
),
(
"self.in_def_body = saved || is_definition_body_command(self.text());",
ControlSequence,
),
(
"self.kind() == Some(SyntaxKind::CONTROL_WORD) && self.text() == name",
ControlSequence,
),
(
r#"self.kind() == Some(SyntaxKind::WORD) && self.text().contains(';');"#,
Guarded("the `;` presence ban, gated on `WORD`"),
),
(
"self.tokens[idx].text == BEGIN_CMD && self.env_name_follows(idx)",
ControlSequence,
),
(
"self.tokens[idx].text == END_CMD && self.env_name_follows(idx)",
ControlSequence,
),
(
"t.kind == SyntaxKind::CONTROL_SYMBOL && t.text.as_str() == self.closer",
ControlSequence,
),
(
"t.kind == SyntaxKind::CONTROL_WORD && t.text.as_str() == LEFT_CMD",
ControlSequence,
),
(
"t.kind == SyntaxKind::CONTROL_WORD && t.text.as_str() == RIGHT_CMD",
ControlSequence,
),
(
r"t.text.strip_prefix('\\').and_then(conditional::flow_word)",
ControlSequence,
),
(
r"} else if t.text.strip_prefix('\\').and_then(conditional::flow_word)",
ControlSequence,
),
];
const GRAMMAR_SOURCES: &[(&str, &str)] = &[
("grammar.rs", include_str!("../grammar.rs")),
("grammar/prescan.rs", include_str!("../grammar/prescan.rs")),
("grammar/expl3.rs", include_str!("../grammar/expl3.rs")),
("grammar/trivia.rs", include_str!("../grammar/trivia.rs")),
("grammar/facts.rs", include_str!("../grammar/facts.rs")),
("conditional.rs", include_str!("../conditional.rs")),
];
fn text_reads(src: &str) -> Vec<&str> {
let body = src.split("\n#[cfg(test)]").next().unwrap_or(src);
body.lines()
.map(str::trim)
.filter(|line| !line.starts_with("//"))
.filter(|line| {
line.match_indices(".text").any(|(i, _)| {
!line[i + 5..]
.chars()
.next()
.is_some_and(|c| c == '_' || c.is_alphanumeric())
})
})
.collect()
}
#[test]
fn the_text_read_survey_is_complete() {
let mut found: Vec<&str> = GRAMMAR_SOURCES
.iter()
.flat_map(|(_, src)| text_reads(src))
.collect();
found.sort_unstable();
found.dedup();
let mut classified: Vec<&str> = TEXT_READS.iter().map(|(line, _)| *line).collect();
classified.sort_unstable();
classified.dedup();
assert_eq!(
classified.len(),
TEXT_READS.len(),
"the survey table has duplicate entries",
);
let unclassified: Vec<&&str> = found.iter().filter(|l| !classified.contains(l)).collect();
assert!(
unclassified.is_empty(),
"the grammar grew text reads nobody classified. Each one is a decision \
that could differ between a splice and a full parse, so it must be \
added to `TEXT_READS` with a verdict — and, if it can see a leaf a tier \
splices, a guard in `text_reads_are_inert` or `context_admits` \
besides:\n{unclassified:#?}",
);
let stale: Vec<&&str> = classified.iter().filter(|l| !found.contains(l)).collect();
assert!(
stale.is_empty(),
"the survey table names lines the grammar no longer has. Drop them — a \
table that outlives its sites stops describing anything:\n{stale:#?}",
);
}
#[test]
fn the_text_read_scanner_finds_the_sites_it_claims_to() {
for (name, src) in GRAMMAR_SOURCES {
assert!(
!src.is_empty(),
"{name} is empty: include_str! resolved wrong"
);
}
let found = GRAMMAR_SOURCES
.iter()
.flat_map(|(_, src)| text_reads(src))
.count();
assert!(
found >= 40,
"the scanner found only {found} text reads; it has stopped matching",
);
assert!(text_reads("let x = node.text_range().start();").is_empty());
assert!(text_reads("self.text_bracket_batch.clear();").is_empty());
assert!(text_reads("// t.text == BEGIN_CMD").is_empty());
assert_eq!(text_reads("if t.text == BEGIN_CMD {").len(), 1);
}
#[test]
fn an_unclassified_kind_is_refused() {
let ctx = ctx();
assert!(!text_reads_are_inert(
SyntaxKind::CONTROL_WORD,
"\\a",
"\\ab",
ctx
));
assert!(!text_reads_are_inert(SyntaxKind::L_BRACE, "{", "{", ctx));
}
#[test]
fn a_raw_capture_may_not_gain_or_lose_its_leading_backslash() {
let ctx = ctx();
for kind in [SyntaxKind::VERB, SyntaxKind::VERBATIM_BODY] {
assert!(text_reads_are_inert(kind, "|a|", "|ab|", ctx));
assert!(text_reads_are_inert(kind, "\\verb|a|", "\\verb|ab|", ctx));
assert!(!text_reads_are_inert(kind, "|a|", "\\a|", ctx));
assert!(!text_reads_are_inert(kind, "\\verb|a|", "verb|a|", ctx));
}
}
#[test]
fn the_word_guards_did_not_follow_the_raw_captures() {
let ctx = ctx();
assert!(!text_reads_are_inert(SyntaxKind::WORD, "a", "a;", ctx));
assert!(!text_reads_are_inert(SyntaxKind::WORD, "*", "**", ctx));
assert!(text_reads_are_inert(
SyntaxKind::VERBATIM_BODY,
"a",
"a; *",
ctx
));
}
}