df-derive 0.3.0

Derive fast conversions from Rust structs into Polars DataFrames.
Documentation
//! Safety net for generated external crate paths.
//!
//! `df-derive-macros/src/codegen/external_paths.rs` is the only place codegen should spell raw
//! external crate roots such as `::polars`, `::polars_arrow`, or `::chrono`.
//! Everywhere else should go through resolver helpers so downstream dependency
//! renames keep working.

use std::fs;
use std::path::{Path, PathBuf};

fn strip_comments(src: &str) -> String {
    let bytes = src.as_bytes();
    let mut out = Vec::with_capacity(bytes.len());
    let mut i = 0;
    while i < bytes.len() {
        if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
            out.push(b' ');
            out.push(b' ');
            i += 2;
            while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
                out.push(if bytes[i] == b'\n' { b'\n' } else { b' ' });
                i += 1;
            }
            if i + 1 < bytes.len() {
                out.push(b' ');
                out.push(b' ');
                i += 2;
            }
            continue;
        }
        if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'/' {
            while i < bytes.len() && bytes[i] != b'\n' {
                out.push(b' ');
                i += 1;
            }
            continue;
        }
        out.push(bytes[i]);
        i += 1;
    }
    String::from_utf8(out).expect("comment stripper preserves UTF-8")
}

fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
    let entries = fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()));
    for entry in entries {
        let entry = entry.expect("read_dir entry");
        let path = entry.path();
        let file_type = entry.file_type().expect("file_type");
        if file_type.is_dir() {
            collect_rs_files(&path, out);
        } else if file_type.is_file() && path.extension().is_some_and(|e| e == "rs") {
            out.push(path);
        }
    }
}

#[test]
fn generated_external_roots_are_centralized() {
    let manifest =
        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by cargo test");
    let repo_root = PathBuf::from(&manifest)
        .parent()
        .expect("facade crate lives under the workspace root")
        .to_path_buf();
    let codegen_root = repo_root
        .join("df-derive-macros")
        .join("src")
        .join("codegen");
    let allowed = fs::canonicalize(codegen_root.join("external_paths.rs"))
        .expect("external_paths.rs should exist");

    let mut files = Vec::new();
    collect_rs_files(&codegen_root, &mut files);
    files.sort();

    let raw_external_roots = ["::polars::", "::polars_arrow::", "::chrono::"];
    let mut violations: Vec<String> = Vec::new();
    for path in &files {
        let path_canon = fs::canonicalize(path).unwrap_or_else(|_| path.clone());
        if path_canon == allowed {
            continue;
        }
        let src =
            fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
        let stripped = strip_comments(&src);
        for (lineno, line) in stripped.lines().enumerate() {
            if raw_external_roots
                .iter()
                .any(|needle| line.contains(needle))
            {
                let original = src.lines().nth(lineno).unwrap_or("<line out of range>");
                let display_path = path
                    .strip_prefix(&repo_root)
                    .map_or_else(|_| path.clone(), Path::to_path_buf);
                violations.push(format!(
                    "  {}:{}: {}",
                    display_path.display(),
                    lineno + 1,
                    original.trim_end(),
                ));
            }
        }
    }

    assert!(
        violations.is_empty(),
        "generated external crate roots must go through df-derive-macros/src/codegen/external_paths.rs \
         so dependency renames are honored.\n\nviolations:\n{}",
        violations.join("\n"),
    );
}