agent-file-tools 0.45.0

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Component, Path};

const GENERATED_CONTENT_LINES: usize = 5;

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: bool,
) -> bool {
    cached_hint || 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 {
    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)
}

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"
        ));
    }
}