Skip to main content

omena_bridge/
source_imports.rs

1use omena_parser::ParserByteSpanV0;
2use oxc_allocator::Allocator;
3use oxc_ast::ast::{ImportDeclaration, ImportDeclarationSpecifier, ImportOrExportKind, Statement};
4use oxc_parser::{Parser, ParserReturn};
5use serde::Serialize;
6
7use crate::source_language::{project_source_for_language, source_type_for_language};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "camelCase")]
11pub struct SourceImportDeclarationSummaryV0 {
12    pub schema_version: &'static str,
13    pub product: &'static str,
14    pub import_count: usize,
15    pub imports: Vec<SourceImportDeclarationV0>,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SourceImportDeclarationV0 {
21    pub binding: String,
22    pub specifier: String,
23    pub specifier_byte_span: ParserByteSpanV0,
24}
25
26pub fn summarize_omena_bridge_source_import_declarations(
27    source: &str,
28) -> SourceImportDeclarationSummaryV0 {
29    summarize_omena_bridge_source_import_declarations_for_path("source.tsx", source)
30}
31
32pub fn summarize_omena_bridge_source_import_declarations_for_path(
33    source_path: &str,
34    source: &str,
35) -> SourceImportDeclarationSummaryV0 {
36    summarize_omena_bridge_source_import_declarations_for_source_language(source_path, source, None)
37}
38
39pub fn summarize_omena_bridge_source_import_declarations_for_source_language(
40    source_path: &str,
41    source: &str,
42    source_language: Option<&str>,
43) -> SourceImportDeclarationSummaryV0 {
44    let allocator = Allocator::default();
45    let projected_source = project_source_for_language(source_path, source, source_language);
46    let source_type = source_type_for_language(source_path, source_language);
47    let ParserReturn {
48        program, panicked, ..
49    } = Parser::new(&allocator, projected_source.as_ref(), source_type).parse();
50
51    let mut imports = Vec::new();
52    if !panicked {
53        for statement in &program.body {
54            if let Statement::ImportDeclaration(import) = statement {
55                push_import_declarations_from_ast(import, &mut imports);
56            }
57        }
58        canonicalize_import_declarations(&mut imports);
59    }
60
61    SourceImportDeclarationSummaryV0 {
62        schema_version: "0",
63        product: "omena-bridge.source-import-declarations",
64        import_count: imports.len(),
65        imports,
66    }
67}
68
69fn push_import_declarations_from_ast(
70    import: &ImportDeclaration<'_>,
71    imports: &mut Vec<SourceImportDeclarationV0>,
72) {
73    if import.import_kind != ImportOrExportKind::Value {
74        return;
75    }
76    let Some(specifiers) = import.specifiers.as_ref() else {
77        return;
78    };
79    let specifier = import.source.value.as_str();
80    let specifier_byte_span = ParserByteSpanV0 {
81        start: import.source.span.start as usize,
82        end: import.source.span.end as usize,
83    };
84
85    for specifier_item in specifiers {
86        match specifier_item {
87            ImportDeclarationSpecifier::ImportDefaultSpecifier(default_specifier) => {
88                imports.push(SourceImportDeclarationV0 {
89                    binding: default_specifier.local.name.as_str().to_string(),
90                    specifier: specifier.to_string(),
91                    specifier_byte_span,
92                });
93            }
94            ImportDeclarationSpecifier::ImportNamespaceSpecifier(namespace_specifier) => {
95                imports.push(SourceImportDeclarationV0 {
96                    binding: namespace_specifier.local.name.as_str().to_string(),
97                    specifier: specifier.to_string(),
98                    specifier_byte_span,
99                });
100            }
101            ImportDeclarationSpecifier::ImportSpecifier(_) => {}
102        }
103    }
104}
105
106fn canonicalize_import_declarations(imports: &mut Vec<SourceImportDeclarationV0>) {
107    imports.sort_by(|left, right| {
108        left.binding
109            .cmp(&right.binding)
110            .then_with(|| left.specifier.cmp(&right.specifier))
111    });
112    imports.dedup();
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn extracts_default_and_namespace_imports_from_oxc_ast() {
121        let summary = summarize_omena_bridge_source_import_declarations_for_path(
122            "Component.tsx",
123            r#"
124import bind from "classnames/bind";
125import styles from "./Button.module.scss";
126import * as tokens from "./tokens.module.css";
127import { type BadgeProps } from "./types";
128const lazy = import("./ignored.module.scss");
129"#,
130        );
131
132        assert_eq!(summary.product, "omena-bridge.source-import-declarations");
133        assert_eq!(
134            summary
135                .imports
136                .iter()
137                .map(|import| (import.binding.as_str(), import.specifier.as_str()))
138                .collect::<Vec<_>>(),
139            vec![
140                ("bind", "classnames/bind"),
141                ("styles", "./Button.module.scss"),
142                ("tokens", "./tokens.module.css"),
143            ],
144        );
145    }
146
147    #[test]
148    fn ignores_import_like_strings_and_type_only_default_imports() {
149        let summary = summarize_omena_bridge_source_import_declarations_for_path(
150            "Component.tsx",
151            r#"
152const text = "import fake from './Fake.module.scss'";
153import type styles from "./Typed.module.scss";
154import real from "./Real.module.scss";
155"#,
156        );
157
158        assert_eq!(
159            summary
160                .imports
161                .iter()
162                .map(|import| (import.binding.as_str(), import.specifier.as_str()))
163                .collect::<Vec<_>>(),
164            vec![("real", "./Real.module.scss")],
165        );
166    }
167
168    #[test]
169    fn extracts_imports_from_vue_sfc_script_projection() {
170        let source = r#"<template><button /></template>
171<script setup lang="ts">
172import styles from "./Card.module.scss";
173const local = "not a style import";
174</script>
175<style module>
176.root {}
177</style>
178"#;
179        let summary = summarize_omena_bridge_source_import_declarations_for_source_language(
180            "Card.vue",
181            source,
182            Some("vue"),
183        );
184
185        assert_eq!(
186            summary
187                .imports
188                .iter()
189                .map(|import| (import.binding.as_str(), import.specifier.as_str()))
190                .collect::<Vec<_>>(),
191            vec![("styles", "./Card.module.scss")],
192        );
193    }
194}