gruff-rs 0.3.0

Rust static analyzer and quality linter for CI: dead-code, complexity, security, secrets, and architecture rules with deterministic SARIF/JSON output and baseline support.
use super::*;

pub(crate) struct ProjectIndexBuilders<'a> {
    pub(crate) modules: &'a mut Vec<ModuleSummary>,
    pub(crate) items: &'a mut Vec<ItemSummary>,
    pub(crate) call_names: &'a mut Vec<CallNameSummary>,
}

pub(crate) fn collect_project_rust_index(
    file: &SourceFile,
    source: &str,
    ast: &syn::File,
    module_path: &str,
    builders: ProjectIndexBuilders<'_>,
) {
    let scope = ProjectItemScope {
        file,
        module_path,
        cfg_context: false,
        test_context: false,
    };
    collect_project_items(scope, &ast.items, builders.modules, builders.items);
    collect_call_names(file, source, builders.call_names);
}

pub(crate) fn collect_project_items(
    scope: ProjectItemScope<'_>,
    syn_items: &[Item],
    modules: &mut Vec<ModuleSummary>,
    items: &mut Vec<ItemSummary>,
) {
    for item in syn_items {
        collect_project_item(scope, item, modules, items);
    }
}

#[derive(Clone, Copy)]
pub(crate) struct ProjectItemScope<'a> {
    pub(crate) file: &'a SourceFile,
    pub(crate) module_path: &'a str,
    pub(crate) cfg_context: bool,
    pub(crate) test_context: bool,
}

pub(crate) fn collect_project_item(
    scope: ProjectItemScope<'_>,
    item: &Item,
    modules: &mut Vec<ModuleSummary>,
    items: &mut Vec<ItemSummary>,
) {
    match item {
        Item::Fn(item_fn) => collect_project_function(scope, item_fn, items),
        Item::Struct(item_struct) => collect_project_struct(scope, item_struct, items),
        Item::Enum(item_enum) => collect_project_enum(scope, item_enum, items),
        Item::Trait(item_trait) => collect_project_trait(scope, item_trait, items),
        Item::Const(item_const) => collect_project_const(scope, item_const, items),
        Item::Static(item_static) => collect_project_static(scope, item_static, items),
        Item::Type(item_type) => collect_project_type_alias(scope, item_type, items),
        Item::Impl(item_impl) => collect_project_impl(scope, item_impl, items),
        Item::Mod(item_mod) => collect_project_module(scope, item_mod, modules, items),
        _ => {}
    }
}

pub(crate) fn collect_project_function(
    scope: ProjectItemScope<'_>,
    item_fn: &syn::ItemFn,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        item_fn.sig.ident.to_string(),
        "function",
        line_from_span(item_fn.sig.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&item_fn.vis),
            externally_public: visibility_is_externally_public(&item_fn.vis),
            cfg_gated: scope.cfg_context || has_cfg_attr(&item_fn.attrs),
            test_context: scope.test_context
                || has_test_attr(&item_fn.attrs)
                || has_cfg_test_attr(&item_fn.attrs),
            container: None,
            trait_impl: false,
        },
    ));
}

pub(crate) fn collect_project_struct(
    scope: ProjectItemScope<'_>,
    item_struct: &syn::ItemStruct,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        item_struct.ident.to_string(),
        "struct",
        line_from_span(item_struct.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&item_struct.vis),
            externally_public: visibility_is_externally_public(&item_struct.vis),
            cfg_gated: scope.cfg_context || has_cfg_attr(&item_struct.attrs),
            test_context: scope.test_context || has_cfg_test_attr(&item_struct.attrs),
            container: None,
            trait_impl: false,
        },
    ));
}

pub(crate) fn collect_project_enum(
    scope: ProjectItemScope<'_>,
    item_enum: &syn::ItemEnum,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        item_enum.ident.to_string(),
        "enum",
        line_from_span(item_enum.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&item_enum.vis),
            externally_public: visibility_is_externally_public(&item_enum.vis),
            cfg_gated: scope.cfg_context || has_cfg_attr(&item_enum.attrs),
            test_context: scope.test_context || has_cfg_test_attr(&item_enum.attrs),
            container: None,
            trait_impl: false,
        },
    ));
}

pub(crate) fn collect_project_trait(
    scope: ProjectItemScope<'_>,
    item_trait: &syn::ItemTrait,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        item_trait.ident.to_string(),
        "trait",
        line_from_span(item_trait.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&item_trait.vis),
            externally_public: visibility_is_externally_public(&item_trait.vis),
            cfg_gated: scope.cfg_context || has_cfg_attr(&item_trait.attrs),
            test_context: scope.test_context || has_cfg_test_attr(&item_trait.attrs),
            container: None,
            trait_impl: false,
        },
    ));
}

pub(crate) fn collect_project_const(
    scope: ProjectItemScope<'_>,
    item_const: &syn::ItemConst,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        item_const.ident.to_string(),
        "const",
        line_from_span(item_const.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&item_const.vis),
            externally_public: visibility_is_externally_public(&item_const.vis),
            cfg_gated: scope.cfg_context || has_cfg_attr(&item_const.attrs),
            test_context: scope.test_context || has_cfg_test_attr(&item_const.attrs),
            container: None,
            trait_impl: false,
        },
    ));
}

pub(crate) fn collect_project_static(
    scope: ProjectItemScope<'_>,
    item_static: &syn::ItemStatic,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        item_static.ident.to_string(),
        "static",
        line_from_span(item_static.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&item_static.vis),
            externally_public: visibility_is_externally_public(&item_static.vis),
            cfg_gated: scope.cfg_context || has_cfg_attr(&item_static.attrs),
            test_context: scope.test_context || has_cfg_test_attr(&item_static.attrs),
            container: None,
            trait_impl: false,
        },
    ));
}

pub(crate) fn collect_project_type_alias(
    scope: ProjectItemScope<'_>,
    item_type: &syn::ItemType,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        item_type.ident.to_string(),
        "type alias",
        line_from_span(item_type.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&item_type.vis),
            externally_public: visibility_is_externally_public(&item_type.vis),
            cfg_gated: scope.cfg_context || has_cfg_attr(&item_type.attrs),
            test_context: scope.test_context || has_cfg_test_attr(&item_type.attrs),
            container: None,
            trait_impl: false,
        },
    ));
}

pub(crate) fn collect_project_impl(
    scope: ProjectItemScope<'_>,
    item_impl: &syn::ItemImpl,
    items: &mut Vec<ItemSummary>,
) {
    for impl_item in &item_impl.items {
        if let ImplItem::Fn(method) = impl_item {
            collect_project_method(scope, item_impl, method, items);
        }
    }
}

pub(crate) fn collect_project_method(
    scope: ProjectItemScope<'_>,
    item_impl: &syn::ItemImpl,
    method: &syn::ImplItemFn,
    items: &mut Vec<ItemSummary>,
) {
    items.push(project_item(
        scope,
        method.sig.ident.to_string(),
        "method",
        line_from_span(method.sig.ident.span().start()),
        ProjectItemContext {
            public: visibility_is_public(&method.vis),
            externally_public: visibility_is_externally_public(&method.vis),
            cfg_gated: scope.cfg_context
                || has_cfg_attr(&item_impl.attrs)
                || has_cfg_attr(&method.attrs),
            test_context: scope.test_context
                || has_test_attr(&item_impl.attrs)
                || has_cfg_test_attr(&item_impl.attrs)
                || has_test_attr(&method.attrs)
                || has_cfg_test_attr(&method.attrs),
            container: impl_self_type_name(item_impl),
            trait_impl: item_impl.trait_.is_some(),
        },
    ));
}

pub(crate) fn collect_project_module(
    scope: ProjectItemScope<'_>,
    item_mod: &syn::ItemMod,
    modules: &mut Vec<ModuleSummary>,
    items: &mut Vec<ItemSummary>,
) {
    let current_module = module_name(scope.module_path, &item_mod.ident.to_string());
    let module_cfg_gated = scope.cfg_context || has_cfg_attr(&item_mod.attrs);
    let module_test_context = scope.test_context || is_test_module(item_mod);
    modules.push(ModuleSummary {
        file_path: scope.file.display_path.clone(),
        module_path: current_module.clone(),
        line: line_from_span(item_mod.ident.span().start()),
        public: visibility_is_public(&item_mod.vis),
        inline: item_mod.content.is_some(),
        cfg_gated: module_cfg_gated,
    });
    if let Some((_, nested)) = &item_mod.content {
        let nested_scope = ProjectItemScope {
            file: scope.file,
            module_path: &current_module,
            cfg_context: module_cfg_gated,
            test_context: module_test_context,
        };
        collect_project_items(nested_scope, nested, modules, items);
    }
}

pub(crate) fn project_item(
    scope: ProjectItemScope<'_>,
    name: String,
    kind: &str,
    line: usize,
    context: ProjectItemContext,
) -> ItemSummary {
    ItemSummary {
        file_path: scope.file.display_path.clone(),
        module_path: scope.module_path.to_string(),
        name,
        kind: kind.to_string(),
        container: context.container,
        line,
        public: context.public,
        externally_public: context.externally_public,
        cfg_gated: context.cfg_gated,
        test_context: context.test_context,
        trait_impl: context.trait_impl,
    }
}

fn impl_self_type_name(item_impl: &syn::ItemImpl) -> Option<String> {
    let syn::Type::Path(path) = item_impl.self_ty.as_ref() else {
        return None;
    };
    path.path
        .segments
        .last()
        .map(|segment| segment.ident.to_string())
}

pub(crate) fn module_name(parent: &str, name: &str) -> String {
    if parent.is_empty() {
        name.to_string()
    } else {
        format!("{parent}::{name}")
    }
}

pub(crate) fn inferred_file_module_path(file: &SourceFile) -> String {
    let Some(path) = file.display_path.strip_prefix("src/") else {
        return String::new();
    };
    if matches!(path, "lib.rs" | "main.rs") {
        return String::new();
    }

    let without_extension = path
        .strip_suffix("/mod.rs")
        .or_else(|| path.strip_suffix(".rs"))
        .unwrap_or(path);
    without_extension.replace('/', "::")
}