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