Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/sed/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ pub enum RegexMode {
pub struct ReplacementTemplate {
pub parts: Vec<ReplacementPart>,
pub max_group_number: usize, // Highest used group number (e.g. 8 for \8)
/// Where the first reference to `max_group_number` appears in the script.
pub max_group_location: Option<ScriptLocation>,
}

impl Default for ReplacementTemplate {
Expand All @@ -161,6 +163,7 @@ impl ReplacementTemplate {
Self {
parts,
max_group_number: max_group_number.try_into().unwrap(),
max_group_location: None,
}
}

Expand Down
19 changes: 15 additions & 4 deletions src/sed/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ pub fn compile_replacement(
) -> UResult<ReplacementTemplate> {
let mut parts = Vec::new();
let mut literal = Vec::new();
let mut max_group: Option<(u32, ScriptLocation)> = None;

let delimiter = line.current();
line.advance();
Expand Down Expand Up @@ -787,6 +788,12 @@ pub fn compile_replacement(
if ref_num == 0 {
parts.push(ReplacementPart::WholeMatch);
} else {
if max_group.as_ref().is_none_or(|(n, _)| ref_num > *n) {
// Point at the backslash, one byte back.
let mut location = ScriptLocation::at_position(lines, line);
location.column_number -= 1;
max_group = Some((ref_num, location));
}
parts.push(ReplacementPart::Group(ref_num));
}
line.advance();
Expand Down Expand Up @@ -837,7 +844,9 @@ pub fn compile_replacement(
if !literal.is_empty() {
parts.push(ReplacementPart::Literal(literal));
}
return Ok(ReplacementTemplate::new(parts));
let mut template = ReplacementTemplate::new(parts);
template.max_group_location = max_group.map(|(_, location)| location);
return Ok(template);
}

_ => {
Expand Down Expand Up @@ -915,12 +924,14 @@ fn compile_subst_command(
.map(Rc::new);

// Catch invalid group references at compile time, if possible.
// `compile_replacement` records a location for every `\N` it reads,
// so the location is only absent when there is no group reference.
if let Some(regex) = &subst.regex
&& let Some(location) = &subst.replacement.max_group_location
&& subst.replacement.max_group_number > regex.captures_len() - 1
{
return compilation_error(
lines,
line,
return semantic_error(
location,
format!(
"invalid reference \\{} on `s' command's RHS",
subst.replacement.max_group_number
Expand Down
39 changes: 39 additions & 0 deletions tests/by-util/test_sed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,45 @@ fn diagnostics_reach_errors_raised_after_the_line_was_read() {
));
}

#[test]
fn diagnostics_underline_the_invalid_back_reference() {
// The message is raised after the flags are read; the caret still
// points at the first `\3`, not past the end of the command.
new_ucmd!()
.env("UUTILS_DIAG", "always")
.args(&[r"s/\(a\)/x\3\1\3/"])
.fails()
.code_is(1)
.stderr_is(concat!(
"sed: <script argument 1>:1:10: error: invalid reference \\3 on `s' command's RHS\n",
" ╭─[ <script argument 1>:1:10 ]\n",
" │\n",
r" 1 │ s/\(a\)/x\3\1\3/",
"\n",
" │ ─\n",
"───╯\n",
));
}

#[test]
fn diagnostics_underline_the_invalid_back_reference_on_a_later_line() {
// The replacement continues on line 2: the caret follows it there.
new_ucmd!()
.env("UUTILS_DIAG", "always")
.args(&["-e", "s/\\(a\\)/x\\\ny\\2/"])
.fails()
.code_is(1)
.stderr_is(concat!(
"sed: <script argument 1>:2:2: error: invalid reference \\2 on `s' command's RHS\n",
" ╭─[ <script argument 1>:2:2 ]\n",
" │\n",
r" 2 │ y\2/",
"\n",
" │ ─\n",
"───╯\n",
));
}

#[test]
fn diagnostics_line_up_under_wide_characters() {
// `日本` takes four columns: the underline must sit under `q`.
Expand Down
Loading