omena_bridge/
source_imports.rs1use 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}