agent-file-tools 0.49.1

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
#[cfg(test)]
use std::collections::BTreeMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
#[cfg(test)]
use std::path::PathBuf;
use std::path::{Component, Path};
#[cfg(test)]
use std::sync::{Mutex, OnceLock};

const GENERATED_CONTENT_LINES: usize = 5;

#[cfg(test)]
static FILE_PROBES: OnceLock<Mutex<BTreeMap<PathBuf, usize>>> = OnceLock::new();

pub(crate) fn is_generated_file(project_root: &Path, path: &Path) -> bool {
    if path_has_generated_shape(path) {
        return true;
    }

    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        project_root.join(path)
    };
    if path_has_generated_shape(&absolute) {
        return true;
    }

    first_lines_have_generated_marker(&absolute)
}

pub(crate) fn is_generated_file_from_source(path: &Path, source: &str) -> bool {
    path_has_generated_shape(path) || source_has_generated_marker(source)
}

pub(crate) fn is_generated_file_with_cached_hint(
    project_root: &Path,
    relative_file: &str,
    cached_hint: Option<bool>,
) -> bool {
    cached_hint.unwrap_or_else(|| is_generated_file(project_root, Path::new(relative_file)))
}

pub(crate) fn path_has_generated_shape(path: &Path) -> bool {
    if path.components().any(is_generated_segment) {
        return true;
    }

    let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
        return false;
    };
    let file_name = file_name.to_ascii_lowercase();
    file_name.ends_with("_pb.ts")
        || file_name.ends_with("_pb.js")
        || file_name.ends_with("_pb.d.ts")
        || file_name.contains(".generated.")
}

fn is_generated_segment(component: Component<'_>) -> bool {
    let Component::Normal(segment) = component else {
        return false;
    };
    matches!(
        segment.to_str().map(str::to_ascii_lowercase).as_deref(),
        Some("gen" | "generated" | "__generated__")
    )
}

fn first_lines_have_generated_marker(path: &Path) -> bool {
    #[cfg(test)]
    bump_file_probe_count(path);

    let Ok(file) = File::open(path) else {
        return false;
    };
    let reader = BufReader::new(file);
    let mut prefix = String::new();
    for line in reader.lines().take(GENERATED_CONTENT_LINES) {
        let Ok(line) = line else {
            break;
        };
        prefix.push_str(&line);
        prefix.push('\n');
    }
    source_has_generated_marker(&prefix)
}

#[cfg(test)]
fn debug_file_probes() -> &'static Mutex<BTreeMap<PathBuf, usize>> {
    FILE_PROBES.get_or_init(|| Mutex::new(BTreeMap::new()))
}

#[cfg(test)]
fn bump_file_probe_count(path: &Path) {
    if let Ok(mut probes) = debug_file_probes().lock() {
        *probes.entry(path.to_path_buf()).or_default() += 1;
    }
}

#[cfg(test)]
#[doc(hidden)]
pub fn reset_file_probe_count_for_debug(project_root: &Path) {
    let project_root =
        std::fs::canonicalize(project_root).unwrap_or_else(|_| project_root.to_path_buf());
    if let Ok(mut probes) = debug_file_probes().lock() {
        probes.retain(|path, _| !path.starts_with(&project_root));
    }
}

#[cfg(test)]
#[doc(hidden)]
pub fn file_probe_count_for_debug(project_root: &Path) -> usize {
    let project_root =
        std::fs::canonicalize(project_root).unwrap_or_else(|_| project_root.to_path_buf());
    debug_file_probes()
        .lock()
        .map(|probes| {
            probes
                .iter()
                .filter(|(path, _)| path.starts_with(&project_root))
                .map(|(_, count)| *count)
                .sum()
        })
        .unwrap_or_default()
}

fn source_has_generated_marker(source: &str) -> bool {
    let prefix = source
        .lines()
        .take(GENERATED_CONTENT_LINES)
        .collect::<Vec<_>>()
        .join("\n")
        .to_ascii_lowercase();

    if prefix.contains("@generated")
        || prefix.contains("code generated by")
        || prefix.contains("do not edit")
    {
        return true;
    }

    prefix.contains("eslint-disable")
        && (prefix.contains("codegen")
            || prefix.contains("generated")
            || prefix.contains("auto-generated")
            || prefix.contains("automatically generated"))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn path_shape_catches_conservative_generated_names() {
        assert!(path_has_generated_shape(Path::new("src/gen/types.ts")));
        assert!(path_has_generated_shape(Path::new(
            "src/__generated__/api.ts"
        )));
        assert!(path_has_generated_shape(Path::new("src/foo_pb.ts")));
        assert!(path_has_generated_shape(Path::new("src/foo_pb.d.ts")));
        assert!(path_has_generated_shape(Path::new("src/foo.generated.ts")));
        assert!(!path_has_generated_shape(Path::new("src/general.ts")));
    }

    #[test]
    fn content_marker_only_checks_file_prefix() {
        assert!(source_has_generated_marker(
            "// Code generated by protoc-gen-es. DO NOT EDIT.\nexport const x = 1;"
        ));
        assert!(source_has_generated_marker(
            "/* eslint-disable */\n// emitted by codegen\nexport const x = 1;"
        ));
        assert!(!source_has_generated_marker(
            "export const handwritten = true;\n// 2\n// 3\n// 4\n// 5\n// DO NOT EDIT"
        ));
    }
}