Skip to main content

omena_bridge/
source_imports.rs

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