use oxc_allocator::Allocator;
use oxc_ast::ast::{ImportDeclaration, ImportDeclarationSpecifier, ImportOrExportKind, Statement};
use oxc_parser::{Parser, ParserReturn};
use oxc_span::SourceType;
use serde::Serialize;
#[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 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 {
let allocator = Allocator::default();
let source_type = match SourceType::from_path(source_path) {
Ok(source_type) => source_type,
Err(_) => SourceType::tsx(),
};
let ParserReturn {
program, panicked, ..
} = Parser::new(&allocator, source, 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();
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(),
});
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(namespace_specifier) => {
imports.push(SourceImportDeclarationV0 {
binding: namespace_specifier.local.name.as_str().to_string(),
specifier: specifier.to_string(),
});
}
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")],
);
}
}