blob: f86d37174ccc7a022046a8b77afbc14a68a1a1c2 [file]
//! The binary's own messages start with `amem:`, the command's name, and none starts
//! with `memory:`, the name it had before. The demo selects the lines of `amem work` by
//! that prefix, so a message under the old name is a line the demo drops without a word.
//! This reads the sources, since no run reaches every message. It checks that the old
//! name is gone, not that every line carries the new one: `relay:`, `error:` and the
//! like are prefixes of their own.
use std::path::Path;
fn sources(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
for entry in std::fs::read_dir(dir).expect("crates/ reads") {
let path = entry.expect("an entry").path();
if path.is_dir() {
if path.file_name().is_some_and(|n| n != "target") {
sources(&path, out);
}
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
/// Where a message under the old name starts, each as it is spelled in a source line.
fn old_spellings(name: &str) -> Vec<String> {
// after a quote, a backtick, a byte string, or a newline escape; and what follows
// the colon is a space, a tab, a placeholder, a line continuation, or the string's end
let starts = ["\"", "'", "`", "b\"", "\\n"];
let follows = [" ", "\\t", "{", "\\", "\""];
let mut old = Vec::new();
for s in starts {
for f in follows {
old.push(format!("{s}{name}:{f}"));
}
old.push(format!("{s}{name} hook:"));
}
old
}
/// `text` with every comment blanked and every line break kept, so a line number still
/// points at its source line. A literal is kept whole: `//` or `/*` inside a string or
/// a character opens no comment.
fn without_comments(text: &str) -> String {
let chars: Vec<char> = text.chars().collect();
let mut out = String::with_capacity(text.len());
let mut i = 0;
while i < chars.len() {
let rest = &chars[i..];
if let Some(n) = comment_len(rest) {
out.extend(rest[..n].iter().map(|&c| if c == '\n' { c } else { ' ' }));
i += n;
} else {
let n = literal_len(rest).unwrap_or(1);
out.extend(&rest[..n]);
i += n;
}
}
out
}
/// The length of the comment that `rest` starts with, if it starts with one. Block
/// comments nest.
fn comment_len(rest: &[char]) -> Option<usize> {
if rest.starts_with(&['/', '/']) {
return Some(rest.iter().position(|&c| c == '\n').unwrap_or(rest.len()));
}
if !rest.starts_with(&['/', '*']) {
return None;
}
let mut depth = 0;
let mut i = 0;
while i + 1 < rest.len() {
match (rest[i], rest[i + 1]) {
('/', '*') => depth += 1,
('*', '/') => depth -= 1,
_ => {
i += 1;
continue;
}
}
i += 2;
if depth == 0 {
return Some(i);
}
}
Some(rest.len())
}
/// The length of the string or character literal that `rest` starts with, if it starts
/// with one. A quote that no character closes is a lifetime.
fn literal_len(rest: &[char]) -> Option<usize> {
let up_to = |close: &[char], from: usize| {
rest[from..]
.windows(close.len())
.position(|w| w == close)
.map_or(rest.len(), |p| from + p + close.len())
};
match rest {
['"', ..] => {
let mut i = 1;
while i < rest.len() && rest[i] != '"' {
i += if rest[i] == '\\' { 2 } else { 1 };
}
Some((i + 1).min(rest.len()))
}
['r', tail @ ..] => {
let hashes = tail.iter().take_while(|&&c| c == '#').count();
let close: Vec<char> = std::iter::once('"')
.chain(std::iter::repeat_n('#', hashes))
.collect();
(tail.get(hashes) == Some(&'"')).then(|| up_to(&close, hashes + 2))
}
['\'', '\\', ..] => Some(up_to(&['\''], 3)),
['\'', _, '\'', ..] => Some(3),
_ => None,
}
}
/// The lines of `text` that start a message under the old name. A comment is not a
/// message, and a line that says it holds the old name on purpose is let through.
fn under_the_old_name(text: &str) -> Vec<(usize, String)> {
// assembled, so this file does not hold what it looks for
let name = ["mem", "ory"].concat();
let allowed = ["old name", " allowed"].concat();
let old = old_spellings(&name);
let code = without_comments(text);
let mut found = Vec::new();
let mut continues = false;
for (n, (line, source)) in code.lines().zip(text.lines()).enumerate() {
let trimmed = line.trim_start();
// a continuation skips blank lines as it skips indentation
if trimmed.is_empty() {
continue;
}
let held = old.iter().any(|o| line.contains(o.as_str()))
|| (continues && trimmed.starts_with(&format!("{name}:")));
if held && !source.contains(&allowed) {
found.push((n + 1, source.trim_start().to_string()));
}
continues = line.ends_with('\\');
}
found
}
#[test]
fn every_spelling_of_the_old_name_is_found_and_only_those() {
let name = ["mem", "ory"].concat();
let caught = [
format!("eprintln!(\"{name}: x\");"),
format!("eprintln!(\"{name}:{{e}}\");"),
format!("eprintln!(\"{name}:\\tx\");"),
format!("eprintln!(\"\\n{name}: after a blank line\");"),
format!("let js = r#\"console.warn('{name}: ' + e);\"#;"),
format!("let b = b\"{name}: sandbox: \";"),
format!("eprintln!(\"{name} hook: x\");"),
// what opens a comment inside a literal opens none
format!("let s = \"//\"; eprintln!(\"{name}: x\");"),
format!("let s = \"/*\"; eprintln!(\"{name}: x\"); // */"),
format!("let s = \"a \\\" // b\"; eprintln!(\"{name}: x\");"),
format!("let s = r#\"a \" // \"#; eprintln!(\"{name}: x\");"),
format!("let q = '\"'; let s = \"//\"; eprintln!(\"{name}: x\");"),
format!("let q = '\\''; let s = \"//\"; eprintln!(\"{name}: x\");"),
format!("fn f<'a>(s: &'a str) {{ eprintln!(\"{name}: x\"); }}"),
format!("/* a note */ eprintln!(\"{name}: x\");"),
// a comment ends at its line, and an escaped quote in a char opens no string
format!("// a note\neprintln!(\"{name}: x\");"),
format!("let c = '\\\"'; let s = \"//\"; eprintln!(\"{name}: x\");"),
];
for line in &caught {
assert_eq!(under_the_old_name(line).len(), 1, "missed: {line}");
}
let continued = format!("eprintln!(\"first line\\n\\\n {name}: second line\");");
assert_eq!(
under_the_old_name(&continued),
vec![(2, format!("{name}: second line\");"))]
);
// a continuation skips every blank line and all indentation that follows it
for blank in ["", " "] {
let continued =
format!("eprintln!(\"first line\\n\\\n{blank}\n {name}: second line\");");
assert_eq!(
under_the_old_name(&continued),
vec![(3, format!("{name}: second line\");"))],
"after a line of {blank:?}"
);
}
let after_a_block = format!("/* a note\n on two lines */ eprintln!(\"{name}: x\");");
assert_eq!(under_the_old_name(&after_a_block).len(), 1);
let passed = [
format!("/// the `{name}: ` key"),
format!("// \"{name}: x\""),
format!("let x = 1; // formerly \"{name}: x\""),
format!("/* formerly \"{name}: x\" */"),
format!("let x = 1; /* \"{name}: x\" */ let y = 2;"),
format!("/* /* nested */ \"{name}: x\" */"),
format!("/*\n * formerly \"{name}: x\"\n */"),
format!("/**\n * eprintln!(\"first\\n\\\n {name}: x\");\n */"),
format!("let limits = \"{name}: 1Gi\"; // old name allowed: a pod limit"),
format!("{name}: Arc<Memory>,"),
"eprintln!(\"amem: x\");".to_string(),
];
for line in &passed {
assert!(under_the_old_name(line).is_empty(), "flagged: {line}");
}
}
#[test]
fn no_message_starts_with_the_old_name() {
let crates = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let mut files = Vec::new();
sources(&crates, &mut files);
assert!(
files.len() > 50,
"found only {} sources under {}",
files.len(),
crates.display()
);
let mut found = Vec::new();
let mut read = 0;
for file in &files {
let text = std::fs::read_to_string(file).expect("a source reads");
read += without_comments(&text).matches("\"amem: ").count();
for (n, line) in under_the_old_name(&text) {
found.push(format!("{}:{n}: {line}", file.display()));
}
}
// The binary has about 80 messages; far fewer seen means comments hid code.
assert!(read > 70, "only {read} messages seen outside comments");
assert!(
found.is_empty(),
"messages under the old name:\n{}",
found.join("\n")
);
}