omena-bridge 0.2.0

CME-coupled bridge crate for Omena semantic graph inputs
Documentation
use omena_parser::ParserByteSpanV0;
use oxc_allocator::Allocator;
use oxc_ast::ast::{ImportDeclaration, ImportDeclarationSpecifier, ImportOrExportKind, Statement};
use oxc_parser::{Parser, ParserReturn};
use serde::Serialize;

use crate::source_language::{project_source_for_language, source_type_for_language};

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceImportDeclarationSummaryV0 {
    pub schema_version: &'static str,
    pub product: &'static str,
    pub import_count: usize,
    pub imports: Vec<SourceImportDeclarationV0>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SourceImportDeclarationV0 {
    pub binding: String,
    pub specifier: String,
    pub specifier_byte_span: ParserByteSpanV0,
}

pub fn summarize_omena_bridge_source_import_declarations(
    source: &str,
) -> SourceImportDeclarationSummaryV0 {
    summarize_omena_bridge_source_import_declarations_for_path("source.tsx", source)
}

pub fn summarize_omena_bridge_source_import_declarations_for_path(
    source_path: &str,
    source: &str,
) -> SourceImportDeclarationSummaryV0 {
    summarize_omena_bridge_source_import_declarations_for_source_language(source_path, source, None)
}

pub fn summarize_omena_bridge_source_import_declarations_for_source_language(
    source_path: &str,
    source: &str,
    source_language: Option<&str>,
) -> SourceImportDeclarationSummaryV0 {
    let allocator = Allocator::default();
    let projected_source = project_source_for_language(source_path, source, source_language);
    let source_type = source_type_for_language(source_path, source_language);
    let ParserReturn {
        program, panicked, ..
    } = Parser::new(&allocator, projected_source.as_ref(), source_type).parse();

    let mut imports = Vec::new();
    if !panicked {
        for statement in &program.body {
            if let Statement::ImportDeclaration(import) = statement {
                push_import_declarations_from_ast(import, &mut imports);
            }
        }
        canonicalize_import_declarations(&mut imports);
    }

    SourceImportDeclarationSummaryV0 {
        schema_version: "0",
        product: "omena-bridge.source-import-declarations",
        import_count: imports.len(),
        imports,
    }
}

fn push_import_declarations_from_ast(
    import: &ImportDeclaration<'_>,
    imports: &mut Vec<SourceImportDeclarationV0>,
) {
    if import.import_kind != ImportOrExportKind::Value {
        return;
    }
    let Some(specifiers) = import.specifiers.as_ref() else {
        return;
    };
    let specifier = import.source.value.as_str();
    let specifier_byte_span = ParserByteSpanV0 {
        start: import.source.span.start as usize,
        end: import.source.span.end as usize,
    };

    for specifier_item in specifiers {
        match specifier_item {
            ImportDeclarationSpecifier::ImportDefaultSpecifier(default_specifier) => {
                imports.push(SourceImportDeclarationV0 {
                    binding: default_specifier.local.name.as_str().to_string(),
                    specifier: specifier.to_string(),
                    specifier_byte_span,
                });
            }
            ImportDeclarationSpecifier::ImportNamespaceSpecifier(namespace_specifier) => {
                imports.push(SourceImportDeclarationV0 {
                    binding: namespace_specifier.local.name.as_str().to_string(),
                    specifier: specifier.to_string(),
                    specifier_byte_span,
                });
            }
            ImportDeclarationSpecifier::ImportSpecifier(_) => {}
        }
    }
}

fn canonicalize_import_declarations(imports: &mut Vec<SourceImportDeclarationV0>) {
    imports.sort_by(|left, right| {
        left.binding
            .cmp(&right.binding)
            .then_with(|| left.specifier.cmp(&right.specifier))
    });
    imports.dedup();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn extracts_default_and_namespace_imports_from_oxc_ast() {
        let summary = summarize_omena_bridge_source_import_declarations_for_path(
            "Component.tsx",
            r#"
import bind from "classnames/bind";
import styles from "./Button.module.scss";
import * as tokens from "./tokens.module.css";
import { type BadgeProps } from "./types";
const lazy = import("./ignored.module.scss");
"#,
        );

        assert_eq!(summary.product, "omena-bridge.source-import-declarations");
        assert_eq!(
            summary
                .imports
                .iter()
                .map(|import| (import.binding.as_str(), import.specifier.as_str()))
                .collect::<Vec<_>>(),
            vec![
                ("bind", "classnames/bind"),
                ("styles", "./Button.module.scss"),
                ("tokens", "./tokens.module.css"),
            ],
        );
    }

    #[test]
    fn ignores_import_like_strings_and_type_only_default_imports() {
        let summary = summarize_omena_bridge_source_import_declarations_for_path(
            "Component.tsx",
            r#"
const text = "import fake from './Fake.module.scss'";
import type styles from "./Typed.module.scss";
import real from "./Real.module.scss";
"#,
        );

        assert_eq!(
            summary
                .imports
                .iter()
                .map(|import| (import.binding.as_str(), import.specifier.as_str()))
                .collect::<Vec<_>>(),
            vec![("real", "./Real.module.scss")],
        );
    }

    #[test]
    fn extracts_imports_from_vue_sfc_script_projection() {
        let source = r#"<template><button /></template>
<script setup lang="ts">
import styles from "./Card.module.scss";
const local = "not a style import";
</script>
<style module>
.root {}
</style>
"#;
        let summary = summarize_omena_bridge_source_import_declarations_for_source_language(
            "Card.vue",
            source,
            Some("vue"),
        );

        assert_eq!(
            summary
                .imports
                .iter()
                .map(|import| (import.binding.as_str(), import.specifier.as_str()))
                .collect::<Vec<_>>(),
            vec![("styles", "./Card.module.scss")],
        );
    }
}