weavatrix-rust 2.1.1

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use proc_macro2::{LineColumn, Span};
use syn::UseTree;
use weavatrix_graph::{SourcePosition, SourceSpan};

pub(super) fn attributes_mark_test(attributes: &[syn::Attribute]) -> bool {
    attributes.iter().any(|attribute| {
        let attribute_name = attribute
            .path()
            .segments
            .last()
            .map(|part| part.ident.to_string());
        if attribute_name.as_deref().is_some_and(|name| {
            matches!(
                name,
                "test" | "rstest" | "proptest" | "wasm_bindgen_test" | "test_case"
            )
        }) {
            return true;
        }
        if !attribute.path().is_ident("cfg") {
            return false;
        }
        let syn::Meta::List(meta_list) = &attribute.meta else {
            return false;
        };
        cfg_list_marks_test(meta_list)
    })
}

fn cfg_list_marks_test(list: &syn::MetaList) -> bool {
    cfg_list_marks_test_with_negation(list, false)
}

fn cfg_list_marks_test_with_negation(list: &syn::MetaList, negated: bool) -> bool {
    let nested_negated = if list.path.is_ident("not") {
        !negated
    } else {
        negated
    };
    list.parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated)
        .is_ok_and(|items| {
            items
                .iter()
                .any(|meta| cfg_meta_marks_test(meta, nested_negated))
        })
}

fn cfg_meta_marks_test(meta: &syn::Meta, negated: bool) -> bool {
    match meta {
        syn::Meta::Path(path) => path.is_ident("test") && !negated,
        syn::Meta::List(list) => cfg_list_marks_test_with_negation(list, negated),
        syn::Meta::NameValue(_) => false,
    }
}

pub(super) fn impl_owner(ty: &syn::Type) -> Option<String> {
    let syn::Type::Path(path) = ty else {
        return None;
    };
    path.path
        .segments
        .last()
        .map(|segment| segment.ident.to_string())
}

pub(super) fn use_tree_targets(tree: &UseTree) -> Vec<String> {
    use_tree_targets_with_prefix(tree, "")
}

/// Makes `self` and `super` paths relative to the source file. The file-level
/// resolver knows the module represented by that file, but cannot infer the
/// lexical path of modules whose bodies live inside it.
pub(super) fn scoped_use_target(target: &str, inline_modules: &[String]) -> String {
    if inline_modules.is_empty() {
        return target.to_owned();
    }
    let segments = target.split("::").collect::<Vec<_>>();
    if segments.first() == Some(&"self") {
        return std::iter::once("self")
            .chain(inline_modules.iter().map(String::as_str))
            .chain(segments[1..].iter().copied())
            .collect::<Vec<_>>()
            .join("::");
    }
    let super_count = segments
        .iter()
        .take_while(|segment| **segment == "super")
        .count();
    if super_count == 0 {
        return target.to_owned();
    }
    let remaining_supers = super_count.saturating_sub(inline_modules.len());
    let mut resolved = if remaining_supers == 0 {
        std::iter::once("self")
            .chain(
                inline_modules[..inline_modules.len() - super_count]
                    .iter()
                    .map(String::as_str),
            )
            .collect::<Vec<_>>()
    } else {
        vec!["super"; remaining_supers]
    };
    resolved.extend_from_slice(&segments[super_count..]);
    resolved.join("::")
}

fn use_tree_targets_with_prefix(tree: &UseTree, prefix: &str) -> Vec<String> {
    match tree {
        UseTree::Path(path) => {
            let prefix = join_use_path(prefix, &path.ident.to_string());
            use_tree_targets_with_prefix(&path.tree, &prefix)
        }
        UseTree::Name(name) => vec![join_use_path(prefix, &name.ident.to_string())],
        UseTree::Rename(rename) => vec![format!(
            "{} as {}",
            join_use_path(prefix, &rename.ident.to_string()),
            rename.rename
        )],
        UseTree::Glob(_) => vec![join_use_path(prefix, "*")],
        UseTree::Group(group) => group
            .items
            .iter()
            .flat_map(|item| use_tree_targets_with_prefix(item, prefix))
            .collect(),
    }
}

fn join_use_path(prefix: &str, item: &str) -> String {
    if prefix.is_empty() {
        item.to_owned()
    } else {
        format!("{prefix}::{item}")
    }
}

pub(super) fn source_span(path: &str, span: Span) -> SourceSpan {
    SourceSpan {
        file: path.to_owned(),
        start: position(span.start()),
        end: position(span.end()),
    }
}

fn position(point: LineColumn) -> SourcePosition {
    SourcePosition {
        line: u32::try_from(point.line).unwrap_or(u32::MAX),
        column: u32::try_from(point.column)
            .unwrap_or(u32::MAX)
            .saturating_add(1),
    }
}