syft-semantic 0.2.1

Rust-first semantic indexing and diffing for syft
Documentation
use std::path::Path;

use quote::ToTokens;
use syft_types::{EdgeKind, Language, SemanticEdge, SpanRef, SymbolDescriptor, SymbolId, SymbolRef, SymbolSource, SymbolTarget, Visibility};
use syn::spanned::Spanned;

pub(crate) fn link_parent(edges: &mut Vec<SemanticEdge>, parent: Option<SymbolId>, child: &SymbolId) {
    if let Some(parent) = parent {
        edges.push(SemanticEdge {
            from: parent,
            to: SymbolTarget::Symbol(child.clone()),
            kind: EdgeKind::Contains,
        });
    }
}

pub(crate) fn module_path_from_file(path: &Path) -> String {
    let mut parts = path
        .components()
        .map(|component| component.as_os_str().to_string_lossy().to_string())
        .collect::<Vec<_>>();
    if parts.first().is_some_and(|part| part == "src") {
        parts.remove(0);
    }

    let mut normalized = Vec::new();
    for part in parts {
        if let Some(stem) = part.strip_suffix(".rs") {
            if stem == "lib" || stem == "main" || stem == "mod" {
                continue;
            }
            normalized.push(stem.to_string());
        } else {
            normalized.push(part);
        }
    }

    normalized.join("::")
}

pub(crate) fn join_module_path(module_path: &str, local_name: &str) -> String {
    if module_path.is_empty() {
        local_name.to_string()
    } else {
        format!("{module_path}::{local_name}")
    }
}

pub(crate) fn normalize_path(path: &Path) -> String {
    path.components()
        .map(|component| component.as_os_str().to_string_lossy().to_string())
        .collect::<Vec<_>>()
        .join("/")
}

pub(crate) fn span_ref<T: Spanned>(value: &T) -> SpanRef {
    let span = value.span();
    let start = span.start();
    let end = span.end();
    SpanRef {
        start_line: start.line as u32,
        start_col: (start.column + 1) as u32,
        end_line: end.line as u32,
        end_col: (end.column + 1) as u32,
    }
}

pub(crate) fn normalize_signature<T: ToTokens>(value: &T) -> String {
    normalize_signature_text(&value.to_token_stream().to_string())
}

pub(crate) fn normalize_signature_text(raw: &str) -> String {
    let mut normalized = String::new();
    let mut pending_space = false;
    let punctuation = [
        '(', ')', '{', '}', '[', ']', ',', ':', ';', '<', '>', '&', '=', '-', '+', '!', '|', '?',
    ];

    for ch in raw.chars() {
        if ch.is_whitespace() {
            pending_space = true;
            continue;
        }

        if punctuation.contains(&ch) {
            if normalized.ends_with(' ') {
                normalized.pop();
            }
            normalized.push(ch);
            pending_space = false;
            continue;
        }

        if pending_space && !normalized.is_empty() && !normalized.ends_with(' ') {
            normalized.push(' ');
        }
        normalized.push(ch);
        pending_space = false;
    }

    normalized.trim().to_string()
}

pub(crate) fn descriptor(
    file_path: &str,
    module_path: &str,
    local_name: String,
    span: SpanRef,
    visibility: Visibility,
    category: syft_types::SymbolCategory,
    tags: Vec<String>,
    attributes: std::collections::BTreeMap<String, serde_json::Value>,
) -> SymbolDescriptor {
    let qualified = join_module_path(module_path, &local_name);
    SymbolDescriptor {
        symbol: SymbolRef {
            id: SymbolId {
                language: Language::Rust,
                namespace: module_path.to_string(),
                path: qualified.clone(),
                local_name: local_name.clone(),
                disambiguator: None,
            },
            display_name: qualified,
            source: SymbolSource {
                file_path: file_path.to_string(),
                span,
                visibility,
            },
        },
        category,
        tags,
        attributes,
    }
}