fc8d5cb164
Mit `use std::{fs, process};` und anschließendem `fs::remove_file(…)`
kam bisher beides durch: Der Textwächter suchte `std::fs` wörtlich, und
Clippy kennt nur die aufgelisteten Funktionen. Globaler veränderlicher
Zustand über `static` mit Mutex, RwLock, OnceLock oder LazyLock wurde
auch nicht erkannt.
Neu in FORBIDDEN: `std::{` (gruppierte Importe verbergen dem Text die
Modulnamen) sowie Mutex, RwLock, OnceLock und LazyLock. Der Kern in
core/src nutzt nichts davon. Der Kommentar in core/clippy.toml behauptet
jetzt nur noch, was Clippy leistet, und CLAUDE.md nennt die Regel, dass
jeder std-Pfad in einer eigenen use-Zeile steht.
Der bestehende Selbsttest every_forbidden_term_is_detected deckt die
neuen Einträge ab.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
131 lines
4.3 KiB
Rust
131 lines
4.3 KiB
Rust
//! Guards the lockstep rules: the core must not use anything whose result
|
|
//! can differ between machines or between runs.
|
|
//!
|
|
//! Clippy checks the same rules on resolved paths (see `core/clippy.toml`),
|
|
//! which also catches aliases and grouped imports. This text search adds
|
|
//! what Clippy cannot express, such as floating point types and global
|
|
//! mutable state, and it runs even when Clippy does not.
|
|
|
|
// Tests may read files. The rules of core/clippy.toml apply to core/src.
|
|
#![allow(clippy::disallowed_methods)]
|
|
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Text that must not appear anywhere in the source of the core, and why.
|
|
const FORBIDDEN: &[(&str, &str)] = &[
|
|
("f32", "floating point results can differ between machines"),
|
|
("f64", "floating point results can differ between machines"),
|
|
(
|
|
"HashMap",
|
|
"iteration order differs between runs; use BTreeMap or Vec",
|
|
),
|
|
(
|
|
"HashSet",
|
|
"iteration order differs between runs; use BTreeSet or Vec",
|
|
),
|
|
("std::time", "the core must not read the clock"),
|
|
("Instant", "the core must not read the clock"),
|
|
("SystemTime", "the core must not read the clock"),
|
|
("std::thread", "the core must stay single-threaded"),
|
|
("std::env", "the core must not depend on the environment"),
|
|
(
|
|
"std::fs",
|
|
"the core must not touch files; the shell does that",
|
|
),
|
|
(
|
|
"std::net",
|
|
"the core must not open connections; the shell does that",
|
|
),
|
|
(
|
|
"std::io",
|
|
"the core must not read or write streams; the shell does that",
|
|
),
|
|
("std::process", "the core must not start or end processes"),
|
|
(
|
|
"std::{",
|
|
"grouped std imports hide forbidden modules from this search; import each path on its own line",
|
|
),
|
|
("static mut", "global mutable state can differ between runs"),
|
|
(
|
|
"thread_local",
|
|
"global mutable state can differ between runs",
|
|
),
|
|
("Atomic", "global mutable state can differ between runs"),
|
|
("Mutex", "global mutable state can differ between runs"),
|
|
("RwLock", "global mutable state can differ between runs"),
|
|
("OnceLock", "global mutable state can differ between runs"),
|
|
("LazyLock", "global mutable state can differ between runs"),
|
|
("unsafe", "the core forbids unsafe code"),
|
|
];
|
|
|
|
/// Every forbidden term in `text`, as (line number, term, reason).
|
|
fn violations(text: &str) -> Vec<(usize, &'static str, &'static str)> {
|
|
let mut found = Vec::new();
|
|
for (index, line) in text.lines().enumerate() {
|
|
for &(needle, reason) in FORBIDDEN {
|
|
if line.contains(needle) {
|
|
found.push((index + 1, needle, reason));
|
|
}
|
|
}
|
|
}
|
|
found
|
|
}
|
|
|
|
fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
|
|
for entry in fs::read_dir(dir).expect("source directory is readable") {
|
|
let path = entry.expect("directory entry is readable").path();
|
|
if path.is_dir() {
|
|
rust_files(&path, out);
|
|
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
|
out.push(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn core_source_uses_no_forbidden_apis() {
|
|
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
|
let mut files = Vec::new();
|
|
rust_files(&src, &mut files);
|
|
files.sort();
|
|
assert!(
|
|
!files.is_empty(),
|
|
"no source files found in {}",
|
|
src.display()
|
|
);
|
|
|
|
let mut problems = Vec::new();
|
|
for file in &files {
|
|
let text = fs::read_to_string(file).expect("source file is readable");
|
|
for (line, needle, reason) in violations(&text) {
|
|
problems.push(format!("{}:{line}: `{needle}`: {reason}", file.display()));
|
|
}
|
|
}
|
|
assert!(
|
|
problems.is_empty(),
|
|
"forbidden APIs in the core:\n{}",
|
|
problems.join("\n")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn every_forbidden_term_is_detected() {
|
|
for &(needle, _) in FORBIDDEN {
|
|
let text = format!("pub fn fine() {{}}\nlet x = {needle};\n");
|
|
let found = violations(&text);
|
|
assert!(
|
|
found
|
|
.iter()
|
|
.any(|&(line, term, _)| line == 2 && term == needle),
|
|
"`{needle}` was not detected: {found:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn clean_code_has_no_violations() {
|
|
let text = "pub fn add(a: u32, b: u32) -> u32 {\n a.wrapping_add(b)\n}\n";
|
|
assert!(violations(text).is_empty());
|
|
}
|