agent-file-tools 0.35.4

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
use super::{
    import_byte_range, ImportBlock, ImportForm, ImportGroup, ImportKind, ImportRequest,
    ImportStatement, ImportSyntax,
};
use tree_sitter::{Node, Tree};

pub(crate) fn classify_group_java(module_path: &str) -> ImportGroup {
    if module_path.starts_with("java.") || module_path.starts_with("javax.") {
        ImportGroup::Stdlib
    } else {
        ImportGroup::External
    }
}

pub(crate) fn parse_java_imports(source: &str, tree: &Tree) -> ImportBlock {
    let root = tree.root_node();
    let mut imports = Vec::new();
    let mut cursor = root.walk();
    if cursor.goto_first_child() {
        loop {
            let node = cursor.node();
            if node.kind() == "import_declaration" {
                if let Some(imp) = parse_java_import_declaration(source, &node) {
                    imports.push(imp);
                }
            }
            if !cursor.goto_next_sibling() {
                break;
            }
        }
    }

    let byte_range = import_byte_range(&imports);
    ImportBlock {
        imports,
        byte_range,
    }
}

fn parse_java_import_declaration(source: &str, node: &Node) -> Option<ImportStatement> {
    let raw_text = source[node.byte_range()].to_string();
    let byte_range = node.byte_range();
    let mut module_path: Option<String> = None;
    let mut modifiers = Vec::new();

    let mut cursor = node.walk();
    if cursor.goto_first_child() {
        loop {
            let child = cursor.node();
            match child.kind() {
                "scoped_identifier" => {
                    module_path = Some(source[child.byte_range()].to_string());
                }
                "static" => modifiers.push("static".to_string()),
                "asterisk" => modifiers.push("wildcard".to_string()),
                _ => {}
            }
            if !cursor.goto_next_sibling() {
                break;
            }
        }
    }

    let module_path = module_path?;
    if module_path.is_empty() {
        return None;
    }

    let group = classify_group_java(&module_path);

    Some(ImportStatement {
        module_path,
        names: vec![],
        default_import: None,
        namespace_import: None,
        kind: ImportKind::Value,
        group,
        byte_range,
        raw_text,
        form: ImportForm::Structured {
            named: vec![],
            namespace: None,
            alias: None,
            modifiers,
            import_kind: None,
        },
    })
}

pub(crate) fn generate_java_import_line(req: &ImportRequest) -> String {
    let is_static = req.modifiers.iter().any(|m| m == "static");
    let static_prefix = if is_static { "static " } else { "" };
    let wildcard_suffix = if req.modifiers.iter().any(|m| m == "wildcard") {
        ".*"
    } else {
        ""
    };

    if is_static && wildcard_suffix.is_empty() && req.names.len() == 1 {
        return format!("import static {}.{};", req.module_path, req.names[0]);
    }

    format!(
        "import {static_prefix}{}{wildcard_suffix};",
        req.module_path
    )
}

pub struct JavaSyntax;

impl ImportSyntax for JavaSyntax {
    fn parse(&self, source: &str, tree: &Tree) -> ImportBlock {
        parse_java_imports(source, tree)
    }

    fn generate_line(&self, req: &ImportRequest) -> String {
        generate_java_import_line(req)
    }

    fn classify_group(&self, module_path: &str) -> ImportGroup {
        classify_group_java(module_path)
    }
}

pub static JAVA_SYNTAX: JavaSyntax = JavaSyntax;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parser::{grammar_for, LangId};
    use std::collections::BTreeSet;
    use tree_sitter::Parser;

    fn parse_java(source: &str) -> (Tree, ImportBlock) {
        let grammar = grammar_for(LangId::Java);
        let mut parser = Parser::new();
        parser.set_language(&grammar).unwrap();
        let tree = parser.parse(source, None).unwrap();
        let block = crate::imports::parse_imports(source, &tree, LangId::Java);
        (tree, block)
    }

    fn structured_modifiers(import: &ImportStatement) -> &[String] {
        match &import.form {
            ImportForm::Structured {
                named,
                namespace,
                alias,
                modifiers,
                import_kind,
            } => {
                assert!(named.is_empty());
                assert!(namespace.is_none());
                assert!(alias.is_none());
                assert!(import_kind.is_none());
                modifiers
            }
            other => panic!("expected Structured form, got {other:?}"),
        }
    }

    /// Grammar fixture: lock the tree-sitter-java node kinds the parser depends
    /// on. If the grammar updates and renames these, this test fails loudly.
    #[test]
    fn java_grammar_node_kinds_are_stable() {
        let grammar = grammar_for(LangId::Java);
        let mut parser = Parser::new();
        parser.set_language(&grammar).unwrap();
        let src = "package com.example;\n\nimport java.util.List;\nimport static java.util.Collections.emptyList;\nimport java.util.*;\nimport static java.util.Arrays.*;\n\nclass C {}\n";
        let tree = parser.parse(src, None).unwrap();
        let mut kinds: BTreeSet<String> = BTreeSet::new();
        fn walk(node: tree_sitter::Node, kinds: &mut BTreeSet<String>) {
            kinds.insert(node.kind().to_string());
            let mut c = node.walk();
            if c.goto_first_child() {
                loop {
                    walk(c.node(), kinds);
                    if !c.goto_next_sibling() {
                        break;
                    }
                }
            }
        }
        walk(tree.root_node(), &mut kinds);
        for required in [
            "import_declaration",
            "scoped_identifier",
            "static",
            "asterisk",
            "import",
            ";",
        ] {
            assert!(
                kinds.contains(required),
                "java grammar missing node kind {required:?}; present: {kinds:?}"
            );
        }
    }

    #[test]
    fn parse_java_all_four_forms() {
        let (_, block) = parse_java(
            "package com.example;\n\nimport java.util.List;\nimport static java.util.Collections.emptyList;\nimport java.util.*;\nimport static java.util.Arrays.*;\n\nclass C {}\n",
        );
        assert_eq!(block.imports.len(), 4);

        assert_eq!(block.imports[0].module_path, "java.util.List");
        assert_eq!(block.imports[0].names, Vec::<String>::new());
        assert_eq!(block.imports[0].default_import, None);
        assert_eq!(block.imports[0].namespace_import, None);
        assert_eq!(block.imports[0].kind, ImportKind::Value);
        assert_eq!(block.imports[0].group, ImportGroup::Stdlib);
        assert!(structured_modifiers(&block.imports[0]).is_empty());

        assert_eq!(
            block.imports[1].module_path,
            "java.util.Collections.emptyList"
        );
        assert_eq!(
            structured_modifiers(&block.imports[1]),
            &["static".to_string()]
        );

        assert_eq!(block.imports[2].module_path, "java.util");
        assert_eq!(
            structured_modifiers(&block.imports[2]),
            &["wildcard".to_string()]
        );

        assert_eq!(block.imports[3].module_path, "java.util.Arrays");
        assert_eq!(
            structured_modifiers(&block.imports[3]),
            &["static".to_string(), "wildcard".to_string()]
        );
    }

    #[test]
    fn generate_java_all_forms() {
        assert_eq!(
            crate::imports::generate_import(
                LangId::Java,
                &ImportRequest::legacy("java.util.List", &[], None, None, false),
            ),
            "import java.util.List;"
        );

        let static_modifier = vec!["static".to_string()];
        assert_eq!(
            crate::imports::generate_import(
                LangId::Java,
                &ImportRequest {
                    module_path: "java.util.Collections.emptyList",
                    names: &[],
                    default_import: None,
                    namespace: None,
                    alias: None,
                    type_only: false,
                    modifiers: &static_modifier,
                    import_kind: None,
                },
            ),
            "import static java.util.Collections.emptyList;"
        );

        let wildcard_modifier = vec!["wildcard".to_string()];
        assert_eq!(
            crate::imports::generate_import(
                LangId::Java,
                &ImportRequest {
                    module_path: "java.util",
                    names: &[],
                    default_import: None,
                    namespace: None,
                    alias: None,
                    type_only: false,
                    modifiers: &wildcard_modifier,
                    import_kind: None,
                },
            ),
            "import java.util.*;"
        );

        let static_wildcard_modifiers = vec!["static".to_string(), "wildcard".to_string()];
        assert_eq!(
            crate::imports::generate_import(
                LangId::Java,
                &ImportRequest {
                    module_path: "java.util.Arrays",
                    names: &[],
                    default_import: None,
                    namespace: None,
                    alias: None,
                    type_only: false,
                    modifiers: &static_wildcard_modifiers,
                    import_kind: None,
                },
            ),
            "import static java.util.Arrays.*;"
        );
    }

    #[test]
    fn classify_java_groups() {
        assert_eq!(classify_group_java("java.util.List"), ImportGroup::Stdlib);
        assert_eq!(
            classify_group_java("javax.sql.DataSource"),
            ImportGroup::Stdlib
        );
        assert_eq!(
            classify_group_java("org.example.project.Foo"),
            ImportGroup::External
        );
        assert_eq!(
            classify_group_java("com.example.Foo"),
            ImportGroup::External
        );
    }

    #[test]
    fn java_round_trips_through_parse_generate() {
        for src in [
            "import java.util.List;",
            "import static java.util.Collections.emptyList;",
            "import java.util.*;",
            "import static java.util.Arrays.*;",
        ] {
            let (_, block) = parse_java(src);
            assert_eq!(block.imports.len(), 1, "parse {src:?}");
            let imp = &block.imports[0];
            let modifiers = structured_modifiers(imp);
            let regenerated = crate::imports::generate_import(
                LangId::Java,
                &ImportRequest {
                    module_path: &imp.module_path,
                    names: &imp.names,
                    default_import: None,
                    namespace: None,
                    alias: None,
                    type_only: false,
                    modifiers,
                    import_kind: None,
                },
            );
            assert_eq!(regenerated, src, "round-trip mismatch for {src:?}");
        }
    }
}