use std::fmt;
const NOTE_INDENT: &str = "\n ";
const COMMAND_INDENT: &str = "\n ";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Line {
Note(&'static [&'static str]),
Command(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Guidance(&'static [Line]);
impl Guidance {
#[must_use]
pub const fn new(lines: &'static [Line]) -> Self {
Self(lines)
}
#[must_use]
pub fn lines(self) -> &'static [Line] {
self.0
}
#[must_use]
pub fn defects(self) -> Vec<String> {
let mut defects = Vec::new();
if self.0.is_empty() {
defects.push("the guidance is empty, so it names no way forward".to_owned());
}
for (index, line) in self.0.iter().enumerate() {
match line {
Line::Note(fragments) => {
if fragments.is_empty() {
defects.push(format!("line {index}: a note with no fragments"));
}
for (at, fragment) in fragments.iter().enumerate() {
let where_ = format!("line {index} fragment {at}");
if fragment.trim().is_empty() {
defects.push(format!("{where_}: empty"));
} else if *fragment != fragment.trim() {
defects.push(format!(
"{where_}: has leading or trailing whitespace ({fragment:?}) — \
fragments are joined with one space, so it is never needed"
));
}
if fragment.contains(" ") {
defects.push(format!(
"{where_}: contains a run of spaces ({fragment:?}) — the signature \
of source indentation that leaked into the message"
));
}
if fragment.contains('\n') || fragment.contains('\t') {
defects.push(format!(
"{where_}: contains a newline or tab — a note is one line, and \
more lines are more `Line`s"
));
}
}
}
Line::Command(command) => {
if command.trim().is_empty() {
defects.push(format!("line {index}: an empty command"));
} else if *command != command.trim() {
defects.push(format!(
"line {index}: the command has leading or trailing whitespace \
({command:?}) — indentation is the renderer's"
));
}
if command.contains('\n') {
defects.push(format!(
"line {index}: the command spans lines — one nobody can paste in one \
go is not a way forward"
));
}
if let Some(rest) = command.split("$ ").nth(1)
&& rest.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
{
defects.push(format!(
"line {index}: `$ ` before a name ({command:?}) — the shell will not \
expand it, so the command as printed does not work"
));
}
if let Some(column) = misaligned_run(command) {
defects.push(format!(
"line {index}: a run of spaces at column {column} ({command:?}) that \
does not follow a label — alignment follows a `:` and anything else \
is a wrapped literal, which a command may not be"
));
}
}
}
}
defects
}
}
fn misaligned_run(command: &str) -> Option<usize> {
let bytes = command.as_bytes();
let mut at = 0;
while at < bytes.len() {
if bytes[at] != b' ' {
at += 1;
continue;
}
let start = at;
while at < bytes.len() && bytes[at] == b' ' {
at += 1;
}
if at - start > 1 && start.checked_sub(1).map(|i| bytes[i]) != Some(b':') {
return Some(start);
}
}
None
}
impl fmt::Display for Guidance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
debug_assert!(
self.defects().is_empty(),
"malformed guidance: {}",
self.defects().join("; ")
);
for line in self.0 {
match line {
Line::Note(fragments) => {
f.write_str(NOTE_INDENT)?;
for (at, fragment) in fragments.iter().enumerate() {
if at > 0 {
f.write_str(" ")?;
}
f.write_str(fragment.trim())?;
}
}
Line::Command(command) => {
f.write_str(COMMAND_INDENT)?;
f.write_str(command)?;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{Guidance, Line};
#[test]
fn fragments_join_into_one_sentence_however_the_source_wrapped_them() {
let one = Guidance::new(&[Line::Note(&["asking for isolation and getting execution"])]);
let many = Guidance::new(&[Line::Note(&[
"asking for isolation",
"and getting",
"execution",
])]);
assert_eq!(one.to_string(), many.to_string());
assert_eq!(
one.to_string(),
"\n asking for isolation and getting execution"
);
}
#[test]
fn a_fragment_can_never_leak_source_indentation_into_the_output() {
const PADDED: Guidance = Guidance::new(&[Line::Note(&["getting", " execution"])]);
const EMBEDDED: Guidance = Guidance::new(&[Line::Note(&["getting execution"])]);
let defects = PADDED.defects();
assert!(
defects.iter().any(|d| d.contains("leading or trailing")),
"{defects:?}"
);
let defects = EMBEDDED.defects();
assert_eq!(defects.len(), 1, "{defects:?}");
assert!(defects[0].contains("run of spaces"), "{defects:?}");
}
#[test]
fn rendering_trims_even_though_a_padded_fragment_is_already_a_defect() {
const CLEAN: Guidance = Guidance::new(&[Line::Note(&["getting", "execution"])]);
assert_eq!(CLEAN.to_string(), "\n getting execution");
}
#[test]
fn a_command_keeps_the_alignment_that_is_its_content() {
const ALIGNED: &str = "for this run: roteiro lint <analyzer> --allow-unsandboxed";
const GUIDANCE: Guidance = Guidance::new(&[Line::Command(ALIGNED)]);
assert_eq!(GUIDANCE.to_string(), format!("\n {ALIGNED}"));
assert!(
GUIDANCE.defects().is_empty(),
"internal alignment is content, not a defect: {:?}",
GUIDANCE.defects()
);
}
#[test]
fn a_command_whose_shell_expansion_is_broken_is_a_defect() {
const BROKEN: Guidance = Guidance::new(&[Line::Command(
"roteiro security prefetch --image $ ROTEIRO_TEST_LINT_IMAGE",
)]);
const FIXED: Guidance = Guidance::new(&[Line::Command(
"roteiro security prefetch --image $ROTEIRO_TEST_LINT_IMAGE",
)]);
const NOT_A_VARIABLE: Guidance = Guidance::new(&[Line::Command("cost: $ 5")]);
let defects = BROKEN.defects();
assert_eq!(defects.len(), 1, "{defects:?}");
assert!(defects[0].contains("will not expand"), "{defects:?}");
for fine in [FIXED, NOT_A_VARIABLE] {
assert!(fine.defects().is_empty(), "{:?}", fine.defects());
}
}
#[test]
fn a_command_may_align_after_a_label_and_may_not_wrap() {
const ALIGNED: Guidance = Guidance::new(&[
Line::Command("for this run: roteiro lint <analyzer> --allow-unsandboxed"),
Line::Command(
"standing: add `[lint] allow_unsandboxed = true` to ~/.roteiro/config.toml",
),
Line::Command("cargo fetch --locked"),
]);
const WRAPPED: Guidance = Guidance::new(&[Line::Command(
"roteiro security prefetch --analyzer clippy --allow-download --image $X",
)]);
assert!(ALIGNED.defects().is_empty(), "{:?}", ALIGNED.defects());
let defects = WRAPPED.defects();
assert_eq!(defects.len(), 1, "{defects:?}");
assert!(
defects[0].contains("does not follow a label"),
"{defects:?}"
);
}
#[test]
fn every_rule_names_the_defect_it_prevents() {
const EMPTY: Guidance = Guidance::new(&[]);
const NO_FRAGMENTS: Guidance = Guidance::new(&[Line::Note(&[])]);
const PADDED: Guidance = Guidance::new(&[Line::Note(&[" padded "])]);
const TWO_LINES: Guidance = Guidance::new(&[Line::Note(&["two\nlines"])]);
const MULTI_COMMAND: Guidance = Guidance::new(&[Line::Command("cargo fetch\ncargo build")]);
const INDENTED: Guidance = Guidance::new(&[Line::Command(" indented")]);
for (guidance, expected) in [
(EMPTY, "names no way forward"),
(NO_FRAGMENTS, "no fragments"),
(PADDED, "whitespace"),
(TWO_LINES, "a note is one line"),
(MULTI_COMMAND, "nobody can paste"),
(INDENTED, "whitespace"),
] {
let defects = guidance.defects();
assert!(
defects.iter().any(|d| d.contains(expected)),
"expected a defect mentioning {expected:?}, got {defects:?}"
);
}
}
#[test]
fn a_guidance_appends_to_a_sentence_rather_than_starting_a_document() {
let guidance = Guidance::new(&[Line::Note(&["do this"]), Line::Command("that")]);
assert_eq!(
format!("something is wrong.{guidance}"),
"something is wrong.\n do this\n that"
);
assert!(!guidance.to_string().ends_with('\n'));
}
}