crabmap 0.1.1

Rust code satellite map — index, query, and navigate your entire codebase
use crate::model::Location;
use quote::ToTokens;
use std::collections::BTreeMap;
use syn::visit::Visit;

pub fn module_name(
    crate_name: &str,
    target_root: &std::path::Path,
    file: &std::path::Path,
) -> String {
    let root = target_root.parent().unwrap_or(target_root);
    let relative = file.strip_prefix(root).unwrap_or(file);
    let mut parts = vec![crate_name.replace('-', "_")];
    for part in relative.components() {
        let value = part.as_os_str().to_string_lossy();
        if value == "src" || value == "lib.rs" || value == "main.rs" || value == "mod.rs" {
            continue;
        }
        parts.push(value.trim_end_matches(".rs").to_string());
    }
    parts.join("::")
}

pub fn file_metrics(source: &str) -> BTreeMap<String, usize> {
    let mut metrics = BTreeMap::new();
    metrics.insert("lines".to_string(), source.lines().count());
    metrics.insert(
        "non_empty_lines".to_string(),
        source
            .lines()
            .filter(|line| !line.trim().is_empty())
            .count(),
    );
    metrics.insert(
        "comment_lines".to_string(),
        source
            .lines()
            .filter(|line| line.trim_start().starts_with("//"))
            .count(),
    );
    metrics
}

pub fn visibility(vis: &syn::Visibility) -> Option<String> {
    match vis {
        syn::Visibility::Public(_) => Some("pub".to_string()),
        syn::Visibility::Restricted(value) => Some(value.to_token_stream().to_string()),
        syn::Visibility::Inherited => None,
    }
}

pub fn docs(attrs: &[syn::Attribute]) -> Vec<String> {
    attrs
        .iter()
        .filter(|attr| attr.path().is_ident("doc"))
        .filter_map(|attr| match &attr.meta {
            syn::Meta::NameValue(value) => {
                if let syn::Expr::Lit(lit) = &value.value {
                    if let syn::Lit::Str(value) = &lit.lit {
                        return Some(value.value().trim().to_string());
                    }
                }
                None
            }
            _ => None,
        })
        .collect()
}

pub fn flatten_use_tree(tree: &syn::UseTree, prefix: Option<String>) -> Vec<String> {
    match tree {
        syn::UseTree::Path(path) => flatten_use_tree(
            &path.tree,
            Some(match prefix {
                Some(prefix) => format!("{prefix}::{}", path.ident),
                None => path.ident.to_string(),
            }),
        ),
        syn::UseTree::Name(name) => vec![match prefix {
            Some(prefix) => format!("{prefix}::{}", name.ident),
            None => name.ident.to_string(),
        }],
        syn::UseTree::Rename(name) => vec![match prefix {
            Some(prefix) => format!("{prefix}::{}", name.ident),
            None => name.ident.to_string(),
        }],
        syn::UseTree::Glob(_) => prefix.into_iter().collect(),
        syn::UseTree::Group(group) => group
            .items
            .iter()
            .flat_map(|item| flatten_use_tree(item, prefix.clone()))
            .collect(),
    }
}

pub fn type_names(ty: &syn::Type) -> Vec<String> {
    let mut visitor = TypeCollector::default();
    visitor.visit_type(ty);
    visitor.names
}

#[derive(Default)]
pub struct TypeCollector {
    pub names: Vec<String>,
}

impl<'ast> syn::visit::Visit<'ast> for TypeCollector {
    fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
        self.names.push(path_name(&node.path));
        syn::visit::visit_type_path(self, node);
    }
}

pub fn type_name(ty: &syn::Type) -> String {
    match ty {
        syn::Type::Path(path) => path_name(&path.path),
        _ => compact(ty.to_token_stream().to_string()),
    }
}

pub fn path_name(path: &syn::Path) -> String {
    path.segments
        .iter()
        .map(|segment| segment.ident.to_string())
        .collect::<Vec<_>>()
        .join("::")
}

pub fn expr_name(expr: &syn::Expr) -> String {
    match expr {
        syn::Expr::Path(path) => path_name(&path.path),
        syn::Expr::Field(field) => field.member.to_token_stream().to_string(),
        _ => compact(expr.to_token_stream().to_string()),
    }
}

pub fn location(file: &str, source: &str, needle: &str) -> Location {
    Location {
        file: file.to_string(),
        line: find_line(source, needle),
    }
}

pub fn find_line(source: &str, needle: &str) -> usize {
    source
        .lines()
        .position(|line| line.contains(needle))
        .map(|line| line + 1)
        .unwrap_or(1)
}

pub fn find_line_after(source: &str, needle: &str, start_line: usize) -> usize {
    source
        .lines()
        .enumerate()
        .skip(start_line.saturating_sub(1))
        .find(|(_, line)| line.contains(needle))
        .map(|(index, _)| index + 1)
        .unwrap_or(start_line)
}

pub fn compact(value: String) -> String {
    value.split_whitespace().collect::<Vec<_>>().join(" ")
}