use std::path::Path;
use oxc_allocator::Allocator;
use oxc_ast::ast::{Comment, Program};
use oxc_ast_visit::Visit;
use oxc_parser::Parser;
use oxc_span::SourceType;
use crate::ExportInfo;
use crate::ModuleInfo;
use crate::astro::{is_astro_file, parse_astro_to_module};
use crate::css::{is_css_file, parse_css_to_module};
use crate::mdx::{is_mdx_file, parse_mdx_to_module};
use crate::sfc::{is_sfc_file, parse_sfc_to_module};
use crate::visitor::ModuleInfoExtractor;
use fallow_types::discover::FileId;
use fallow_types::extract::ImportInfo;
pub fn parse_source_to_module(
file_id: FileId,
path: &Path,
source: &str,
content_hash: u64,
) -> ModuleInfo {
if is_sfc_file(path) {
return parse_sfc_to_module(file_id, source, content_hash);
}
if is_astro_file(path) {
return parse_astro_to_module(file_id, source, content_hash);
}
if is_mdx_file(path) {
return parse_mdx_to_module(file_id, source, content_hash);
}
if is_css_file(path) {
return parse_css_to_module(file_id, path, source, content_hash);
}
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, source, source_type).parse();
let mut suppressions =
crate::suppress::parse_suppressions(&parser_return.program.comments, source);
let mut extractor = ModuleInfoExtractor::new();
extractor.visit_program(&parser_return.program);
let mut unused_bindings =
compute_unused_import_bindings(&parser_return.program, &extractor.imports);
let line_offsets = fallow_types::extract::compute_line_offsets(source);
let mut complexity =
crate::complexity::compute_complexity(&parser_return.program, line_offsets.clone());
let total_extracted =
extractor.exports.len() + extractor.imports.len() + extractor.re_exports.len();
let mut used_retry = false;
if total_extracted == 0 && source.len() > 100 && !source_type.is_jsx() {
let jsx_type = if source_type.is_typescript() {
SourceType::tsx()
} else {
SourceType::jsx()
};
let allocator2 = Allocator::default();
let retry_return = Parser::new(&allocator2, source, jsx_type).parse();
let mut retry_extractor = ModuleInfoExtractor::new();
retry_extractor.visit_program(&retry_return.program);
let retry_total = retry_extractor.exports.len()
+ retry_extractor.imports.len()
+ retry_extractor.re_exports.len();
if retry_total > total_extracted {
unused_bindings =
compute_unused_import_bindings(&retry_return.program, &retry_extractor.imports);
complexity =
crate::complexity::compute_complexity(&retry_return.program, line_offsets.clone());
suppressions =
crate::suppress::parse_suppressions(&retry_return.program.comments, source);
apply_jsdoc_public_tags(
&mut retry_extractor.exports,
&retry_return.program.comments,
source,
);
extractor = retry_extractor;
used_retry = true;
}
}
if !used_retry {
apply_jsdoc_public_tags(
&mut extractor.exports,
&parser_return.program.comments,
source,
);
}
let mut info = extractor.into_module_info(file_id, content_hash, suppressions);
info.unused_import_bindings = unused_bindings;
info.line_offsets = line_offsets;
info.complexity = complexity;
info
}
fn apply_jsdoc_public_tags(exports: &mut [ExportInfo], comments: &[Comment], source: &str) {
if exports.is_empty() || comments.is_empty() {
return;
}
let mut public_offsets: Vec<u32> = Vec::new();
for comment in comments {
if comment.is_jsdoc() {
let content_span = comment.content_span();
let start = content_span.start as usize;
let end = (content_span.end as usize).min(source.len());
if start < end && has_public_tag(&source[start..end]) {
public_offsets.push(comment.attached_to);
}
}
}
if public_offsets.is_empty() {
return;
}
public_offsets.sort_unstable();
for export in exports.iter_mut() {
if export.span.start == 0 && export.span.end == 0 {
continue;
}
if public_offsets.binary_search(&export.span.start).is_ok() {
export.is_public = true;
continue;
}
let idx = public_offsets.partition_point(|&o| o <= export.span.start);
if idx > 0 {
let offset = public_offsets[idx - 1] as usize;
let export_start = export.span.start as usize;
if offset < export_start && export_start <= source.len() {
let between = &source[offset..export_start];
if between.starts_with("export") && !between.contains(';') && !between.contains('}')
{
export.is_public = true;
}
}
}
}
}
const fn is_ident_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn has_public_tag(comment_text: &str) -> bool {
for (i, _) in comment_text.match_indices("@public") {
let after = i + "@public".len();
if after >= comment_text.len() || !is_ident_char(comment_text.as_bytes()[after]) {
return true;
}
}
for (i, _) in comment_text.match_indices("@api") {
let after = i + "@api".len();
if after < comment_text.len() && !is_ident_char(comment_text.as_bytes()[after]) {
let rest = comment_text[after..].trim_start();
if rest.starts_with("public") {
let after_public = "public".len();
if after_public >= rest.len() || !is_ident_char(rest.as_bytes()[after_public]) {
return true;
}
}
}
}
false
}
fn compute_unused_import_bindings(program: &Program<'_>, imports: &[ImportInfo]) -> Vec<String> {
use oxc_semantic::SemanticBuilder;
if imports.is_empty() {
return Vec::new();
}
let semantic_ret = SemanticBuilder::new().build(program);
let semantic = semantic_ret.semantic;
let scoping = semantic.scoping();
let root_scope = scoping.root_scope_id();
let mut unused = Vec::new();
for import in imports {
if import.local_name.is_empty() {
continue;
}
let name = oxc_span::Ident::from(import.local_name.as_str());
if let Some(symbol_id) = scoping.get_binding(root_scope, name)
&& scoping.get_resolved_references(symbol_id).count() == 0
{
unused.push(import.local_name.clone());
}
}
unused
}