- a cast refused for the wrong shapeshift form names it, "Must be in Cat Form", a two-form spell joined the reference's way, and the combat log reads its own buffer rather than the red line (2280, 2285) - three refusals that never had a combat-log line, totems, reagents and equipped item class, get one; a pet's form refusal names one form (2285) - the game's plugins are one group and the whole client builds headless in 0.12 s, so the schedule is a test subject: three one-shot consumers caught firing into the login drain are gated (2279) - the net drain's parameters are named bundles, not nested tuples (2279) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
132 lines
4.5 KiB
Rust
132 lines
4.5 KiB
Rust
//! Helpers for the crate's **structural tests** — the ones that read this crate's own source
|
|
//! and insist on a shape (decisions 1290, 2220, 2279). A structural test exists where the
|
|
//! failure is silent at runtime, so the check has to happen at the line; these are the readers
|
|
//! they share.
|
|
|
|
/// Every `.rs` file under `root`, recursively.
|
|
pub(crate) fn rust_files(root: &std::path::Path) -> Vec<std::path::PathBuf> {
|
|
let mut out = Vec::new();
|
|
let mut stack = vec![root.to_path_buf()];
|
|
while let Some(dir) = stack.pop() {
|
|
let Ok(entries) = std::fs::read_dir(&dir) else {
|
|
continue;
|
|
};
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.is_dir() {
|
|
stack.push(path);
|
|
} else if path.extension().is_some_and(|e| e == "rs") {
|
|
out.push(path);
|
|
}
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// This crate's `src/` directory.
|
|
pub(crate) fn src_dir() -> std::path::PathBuf {
|
|
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")
|
|
}
|
|
|
|
/// A file's path under `src/`, with forward slashes — the key every `EXEMPT` table uses.
|
|
pub(crate) fn rel_path(file: &std::path::Path) -> String {
|
|
let src = src_dir();
|
|
file.strip_prefix(&src)
|
|
.unwrap_or(file)
|
|
.to_string_lossy()
|
|
.replace('\\', "/")
|
|
}
|
|
|
|
/// One `fn` item as the scanners see it: its name, its parenthesised parameter list, and its
|
|
/// body (the text between the outermost braces), each found by bracket matching so a scan
|
|
/// sees a signature or a body rather than whatever text happens to follow.
|
|
pub(crate) struct FnItem<'a> {
|
|
pub name: &'a str,
|
|
pub params: &'a str,
|
|
pub body: &'a str,
|
|
}
|
|
|
|
/// Every `fn` in `text`. A `fn` with no body (a trait method signature) comes back with an
|
|
/// empty body; a generic parameter list before the `(` is tolerated in the shapes this
|
|
/// codebase writes.
|
|
pub(crate) fn fn_items(text: &str) -> Vec<FnItem<'_>> {
|
|
let bytes = text.as_bytes();
|
|
let mut out = Vec::new();
|
|
for (i, _) in text.match_indices("fn ") {
|
|
// `fn` must start a word: `…_fn (` and `Fn(` are not declarations.
|
|
if i > 0 && (bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_') {
|
|
continue;
|
|
}
|
|
let Some(open) = text[i..].find('(').map(|o| i + o) else {
|
|
continue;
|
|
};
|
|
let name = text[i + 3..open].trim();
|
|
let name = name.split('<').next().unwrap_or(name).trim();
|
|
let Some(close) = matching(text, open, b'(', b')') else {
|
|
continue;
|
|
};
|
|
let params = &text[open + 1..close];
|
|
// The body: the first `{` after the parameter list, unless a `;` (a bodiless
|
|
// signature) or another `fn` comes first.
|
|
let tail = &text[close + 1..];
|
|
let body = match (tail.find('{'), tail.find(';')) {
|
|
(Some(b), Some(s)) if s < b => "",
|
|
(Some(b), _) => {
|
|
let bopen = close + 1 + b;
|
|
match matching(text, bopen, b'{', b'}') {
|
|
Some(bclose) => &text[bopen + 1..bclose],
|
|
None => "",
|
|
}
|
|
}
|
|
(None, _) => "",
|
|
};
|
|
out.push(FnItem { name, params, body });
|
|
}
|
|
out
|
|
}
|
|
|
|
/// The parenthesised parameter list of every `fn` in `text` — see [`fn_items`].
|
|
pub(crate) fn fn_parameter_lists(text: &str) -> Vec<&str> {
|
|
fn_items(text).into_iter().map(|f| f.params).collect()
|
|
}
|
|
|
|
/// The index of the bracket closing the one at `open`, or `None` if it never closes.
|
|
fn matching(text: &str, open: usize, lhs: u8, rhs: u8) -> Option<usize> {
|
|
let mut depth = 0usize;
|
|
for (j, &c) in text.as_bytes()[open..].iter().enumerate() {
|
|
if c == lhs {
|
|
depth += 1;
|
|
} else if c == rhs {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
return Some(open + j);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_fn_comes_back_with_its_name_its_params_and_its_body() {
|
|
let text = "fn a(x: u32) -> u32 { x + 1 }\nfn b();\nfn c<T>(t: T) { if t { { } } }";
|
|
let items = fn_items(text);
|
|
assert_eq!(items.len(), 3);
|
|
assert_eq!(
|
|
(items[0].name, items[0].params, items[0].body),
|
|
("a", "x: u32", " x + 1 ")
|
|
);
|
|
assert_eq!(
|
|
(items[1].name, items[1].params, items[1].body),
|
|
("b", "", "")
|
|
);
|
|
assert_eq!(
|
|
(items[2].name, items[2].params, items[2].body),
|
|
("c", "t: T", " if t { { } } ")
|
|
);
|
|
}
|
|
}
|