Llogiq on stuff

Write small Rust scripts

Recently I was working on a Rust PR to reduce unreachable_code lint churn after todo!() calls, that basically removes lint messages from unreachable_code after todo!() and instead adds a todo_macro_uses lint which can be turned off while the code is still being worked on. However, once that change was done, I ran into a number of failing tests, because while they had a #![allow(unused)] or some such, this didn’t cover the todo_macro_uses lint.

Brief digression: rustc itself is tested by a tool called compiletest. That tool runs the compiler on code snippets, captures the output and compares it with known-good golden master output it stores alongside the snippets. In this case, there were a good number of tests that had todo!() but didn’t #![allow(todo_macro_uses)]. More tests than I’d care to change manually.

In this year of the lord, many of us would ask some agent to do it for them, but I didn’t like the fact that I would have to review the output (I have seen too many needless formatting changes to be comfortable with investing time and tokens into that). Also I had a code snippet to find all rust files lying around that only used standard library functions and could easily be pasted into a throwaway project.

use std::io;
use std::path::Path;

fn check_files(path: &Path) -> io::Result<()> {
    for e in std::fs::read_dir(path)? {
        let Ok(d) = e else { continue; };
        if d.file_type().is_ok_and(|ft| ft.is_dir()) {
            check_files(&d.path())?;
        } else {
            let path = d.path();
            if path.extension().is_some_and(|ext| ext == "rs") {
                check_file(&path)?;
            }
        }
    }
    Ok(())
}

This can be called on a Path and walks it recursively, calling check_file on all Rust files. I also had done a few read-modify-write functions in Rust (notably in my twirer tool I use for my weekly This Week in Rust contributions). They look like this:

fn check_file(path: &Path) -> io::Result<()> {
    let orig_text = std::fs::read_to_string(path)?;

    let text = todo!(); // put the changed `orig_text` into `text`

    std::fs::write(path, text)
}

There was some slight complication in that a) I wanted to amend any #![allow(..)] annotation I would find instead of adding another, and b) to add one, I would have to find the first position after the initial comments (which are interpreted by compiletest, which would be foiled by putting them below a non-comment line). Also I didn’t want to needlessly add empty lines, so I had to check whether to insert a newline. All in all this came out to less than 50 lines of Rust code, which I’m reproducing here; perhaps someone can use them to copy into their own code to have their own one-off Rust scripts.

use std::fs::{read_dir, read_to_string, write};
use std::io;
use std::path::Path;

fn check_file(path: &Path) -> io::Result<()> {
    let orig_text = read_to_string(path)?;
    if !orig_text.contains("todo!(") || orig_text.contains("todo_macro_uses") {
        return Ok(());
    }
    let text = if let Some(pos) = orig_text.find("#![allow(") {
       // we have an `#[allow(..)]` we can extend
       let Some(insert_pos) = orig_text[pos..].find(")]") else {
           panic!("unclosed #![allow()]");
       };
       let (before, after) = orig_text.split_at(pos + insert_pos);
       format!("{before}, todo_macro_uses{after}")
    } else {
        // find the first line after all // comments
        let mut pos = 0;
        while orig_text[pos..].starts_with("//") {
            let Some(nl) = orig_text[pos..].find("\n") else {
                pos = orig_text.len();
                break;
            };
            pos += nl + 1;
        }
        let (before, after) = orig_text.split_at(pos);
        // insert a newline unless at beginning or we already have one
        let nl = if pos == 0 || before.ends_with('\n') {
            ""
        } else {
            "\n"
        };
        format!("{before}{nl}#![allow(todo_macro_uses)]\n{after}")
    };
    write(path, text)
}

fn check_files(path: &Path) -> io::Result<()> {
    for e in read_dir(path)? {
        let Ok(d) = e else { continue; };
        if d.file_type().is_ok_and(|ft| ft.is_dir()) {
            check_files(&d.path())?;
        } else {
            let path = d.path();
            if path.extension().is_some_and(|ext| ext == "rs") {
                check_file(&path)?;
            }
        }
    }
    Ok(())
}

fn main() -> io::Result<()> {
    check_files(&Path::new("../rust/tests/ui"))
}

The script ran flawlessly, I didn’t need to check the output for errors, and I can reuse parts of it whenever I feel like it.

Conclusion: It’s easy and quick to write small Rust scripts to transform code. And since you know what the code does, you don’t need any time to review the output. And Rust’s standard library, while missing pieces that might simplify some tasks, is certainly servicable for work like this. Even if I had the need for, say, regexes, those would’ve been a mere cargo add regex away. So next time you need to mechanically transform some code, don’t reach for AI, simply rust it.