From 93621660c22c3e302bf6414ec3f11f352204dd87 Mon Sep 17 00:00:00 2001 From: Redddy Date: Sat, 7 Mar 2026 09:34:57 +0000 Subject: [PATCH] Add TODO checker --- .github/workflows/main.yml | 12 +++++- build_system/main.rs | 13 +++++- build_system/todo.rs | 87 ++++++++++++++++++++++++++++++++++++++ build_system/usage.txt | 1 + 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 build_system/todo.rs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 07d9af4a9b..8acaa42d68 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,6 +19,16 @@ env: RUSTFLAGS: "-Dwarnings" jobs: + todo_check: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Check todo + run: ./y.sh check-todo + rustfmt: runs-on: ubuntu-latest timeout-minutes: 10 @@ -232,7 +242,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 if: ${{ github.ref == 'refs/heads/main' }} - needs: [rustfmt, test, bench, dist] + needs: [todo_check, rustfmt, test, bench, dist] permissions: contents: write # for creating the dev tag and release diff --git a/build_system/main.rs b/build_system/main.rs index 6251687bab..971e9453da 100644 --- a/build_system/main.rs +++ b/build_system/main.rs @@ -17,6 +17,7 @@ mod prepare; mod rustc_info; mod shared_utils; mod tests; +mod todo; mod utils; fn usage() { @@ -38,6 +39,7 @@ enum Command { Test, AbiCafe, Bench, + CheckTodo, } #[derive(Copy, Clone, Debug)] @@ -66,6 +68,7 @@ fn main() { Some("test") => Command::Test, Some("abi-cafe") => Command::AbiCafe, Some("bench") => Command::Bench, + Some("check-todo") => Command::CheckTodo, Some(flag) if flag.starts_with('-') => arg_error!("Expected command found flag {}", flag), Some(command) => arg_error!("Unknown command {}", command), None => { @@ -139,6 +142,14 @@ fn main() { process::exit(0); } + if command == Command::CheckTodo { + if let Err(err) = todo::run() { + eprintln!("{err}"); + process::exit(1); + } + process::exit(0); + } + let rustup_toolchain_name = match (env::var("CARGO"), env::var("RUSTC"), env::var("RUSTDOC")) { (Ok(_), Ok(_), Ok(_)) => None, (_, Err(_), Err(_)) => Some(rustc_info::get_toolchain_name()), @@ -202,7 +213,7 @@ fn main() { )) }; match command { - Command::Prepare => { + Command::Prepare | Command::CheckTodo => { // Handled above } Command::Test => { diff --git a/build_system/todo.rs b/build_system/todo.rs new file mode 100644 index 0000000000..31f0996b13 --- /dev/null +++ b/build_system/todo.rs @@ -0,0 +1,87 @@ +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const EXTENSIONS: &[&str] = + &["rs", "py", "js", "sh", "c", "cpp", "h", "md", "css", "ftl", "toml", "yml", "yaml"]; +const SKIP_FILES: &[&str] = &["build_system/todo.rs", ".github/workflows/main.yml"]; + +fn has_supported_extension(path: &Path) -> bool { + path.extension().is_some_and(|ext| EXTENSIONS.iter().any(|e| ext == OsStr::new(e))) +} + +fn is_editor_temp(path: &Path) -> bool { + path.file_name().is_some_and(|name| name.to_string_lossy().starts_with(".#")) +} + +fn list_tracked_files() -> Result, String> { + let output = Command::new("git") + .args(["ls-files", "-z"]) + .output() + .map_err(|e| format!("Failed to run `git ls-files`: {e}"))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("`git ls-files` failed: {stderr}")); + } + + let mut files = Vec::new(); + for entry in output.stdout.split(|b| *b == 0) { + if entry.is_empty() { + continue; + } + let path = std::str::from_utf8(entry) + .map_err(|_| "Non-utf8 path returned by `git ls-files`".to_string())?; + files.push(PathBuf::from(path)); + } + + Ok(files) +} + +pub(crate) fn run() -> Result<(), String> { + let files = list_tracked_files()?; + let mut errors = Vec::new(); + // Avoid embedding the task marker in source so greps only find real occurrences. + let todo_marker = "todo".to_ascii_uppercase(); + + for file in files { + if is_editor_temp(&file) { + continue; + } + if SKIP_FILES.iter().any(|skip| file.ends_with(Path::new(skip))) { + continue; + } + if !has_supported_extension(&file) { + continue; + } + + let bytes = + fs::read(&file).map_err(|e| format!("Failed to read `{}`: {e}", file.display()))?; + let Ok(contents) = std::str::from_utf8(&bytes) else { + continue; + }; + + for (i, line) in contents.split('\n').enumerate() { + let trimmed = line.trim(); + if trimmed.contains(&todo_marker) { + errors.push(format!( + "{}:{}: {} is used for tasks that should be done before merging a PR; if you want to leave a message in the codebase use FIXME", + file.display(), + i + 1, + todo_marker + )); + } + } + } + + if errors.is_empty() { + return Ok(()); + } + + for err in &errors { + eprintln!("{err}"); + } + + Err(format!("found {} {}(s)", errors.len(), todo_marker)) +} diff --git a/build_system/usage.txt b/build_system/usage.txt index 6c98087e52..572fe78058 100644 --- a/build_system/usage.txt +++ b/build_system/usage.txt @@ -6,6 +6,7 @@ USAGE: ./y.sh test [--sysroot none|clif|llvm] [--out-dir DIR] [--download-dir DIR] [--no-unstable-features] [--frozen] [--skip-test TESTNAME] ./y.sh abi-cafe [--sysroot none|clif|llvm] [--out-dir DIR] [--download-dir DIR] [--no-unstable-features] [--frozen] ./y.sh bench [--sysroot none|clif|llvm] [--out-dir DIR] [--download-dir DIR] [--no-unstable-features] [--frozen] + ./y.sh check-todo OPTIONS: --sysroot none|clif|llvm