use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use skif_core::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ImportKind {
Static,
TypeOnly,
SideEffect,
Dynamic,
Require,
ImportMetaGlob,
SourceGlob,
ExportFrom,
Mock,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ImportedKind {
Default,
Namespace,
Named(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ImportedName {
pub kind: ImportedKind,
pub local: Option<String>,
pub is_type: bool,
pub accessed_members: Vec<String>,
pub escapes: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Import {
pub specifier: String,
pub kind: ImportKind,
pub names: Vec<ImportedName>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExportKind {
Value,
Type,
Enum,
Namespace,
Class,
Function,
Default,
ReExport,
StarReExport,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Member {
pub name: String,
pub span: Span,
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub removal: Option<(u32, u32)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RemovalKind {
ExportKeyword,
SharedExportKeyword,
Specifier,
Statement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Removal {
pub start: u32,
pub end: u32,
pub kind: RemovalKind,
pub statement: (u32, u32),
pub siblings: u32,
pub default_expression: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Export {
pub name: String,
pub kind: ExportKind,
pub is_type: bool,
pub local_name: Option<String>,
pub from: Option<String>,
pub imported_name: Option<String>,
pub tags: Vec<String>,
pub span: Span,
pub enum_members: Vec<Member>,
pub namespace_members: Vec<Member>,
pub removal: Option<Removal>,
pub referenced_in_file: bool,
#[serde(default)]
pub referenced_in: Vec<String>,
#[serde(default)]
pub members_read_in_file: Vec<String>,
#[serde(default)]
pub alias_of: Option<String>,
#[serde(default)]
pub registrations: Vec<Registration>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Registration {
Define,
Decorator {
module: String,
name: String,
},
BaseClassDefine {
module: String,
name: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ParseIssue {
pub message: String,
pub span: Span,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ModuleInfo {
pub imports: Vec<Import>,
pub exports: Vec<Export>,
pub in_file_references: Vec<String>,
pub has_cjs_exports: bool,
pub has_jsx: bool,
pub jsx_import_source: Option<String>,
pub jsx_import_source_span: Option<Span>,
pub directives: Vec<String>,
pub unresolved_dynamic: Vec<Span>,
#[serde(default)]
pub ambient_modules: Vec<String>,
#[serde(default)]
pub augmented_modules: Vec<String>,
pub parse_diagnostics: Vec<ParseIssue>,
pub fatal: bool,
}
impl ModuleInfo {
pub fn removal_ranges(&self) -> impl Iterator<Item = &Removal> {
self.exports.iter().filter_map(|e| e.removal.as_ref())
}
#[must_use]
pub fn is_client_boundary(&self) -> bool {
self.directives.iter().any(|d| d == "use client")
}
#[must_use]
pub fn is_server_actions(&self) -> bool {
self.directives.iter().any(|d| d == "use server")
}
pub fn removal_edits(&self, is_unused: impl Fn(&Export) -> bool) -> Vec<(u32, u32)> {
let mut selected: HashMap<(u32, u32), u32> = HashMap::new();
for export in &self.exports {
if let Some(removal) = &export.removal
&& is_unused(export)
{
*selected.entry(removal.statement).or_default() += 1;
}
}
let mut ranges = Vec::new();
let mut whole_statements = HashSet::new();
let mut clauses: HashMap<(u32, u32), Vec<(Span, bool)>> = HashMap::new();
for export in &self.exports {
if let Some(removal) = &export.removal
&& removal.kind == RemovalKind::Specifier
{
clauses
.entry(removal.statement)
.or_default()
.push((export.span, is_unused(export)));
}
}
for export in &self.exports {
let Some(removal) = &export.removal else { continue };
if !is_unused(export) {
continue;
}
let all_selected = selected.get(&removal.statement).copied().unwrap_or(0) >= removal.siblings;
match removal.kind {
RemovalKind::ExportKeyword | RemovalKind::Statement => ranges.push((removal.start, removal.end)),
RemovalKind::SharedExportKeyword if all_selected => ranges.push((removal.start, removal.end)),
RemovalKind::Specifier if all_selected => {
whole_statements.insert(removal.statement);
}
RemovalKind::SharedExportKeyword | RemovalKind::Specifier => {}
}
}
for (statement, specifiers) in &mut clauses {
if whole_statements.contains(statement) {
continue;
}
specifiers.sort_by_key(|(span, _)| span.start);
ranges.extend(specifier_runs(specifiers));
}
ranges.extend(whole_statements);
coalesce_ranges(ranges)
}
pub(crate) fn merge(&mut self, other: Self) {
self.imports.extend(other.imports);
self.exports.extend(other.exports);
self.in_file_references.extend(other.in_file_references);
self.in_file_references.sort();
self.in_file_references.dedup();
self.has_cjs_exports |= other.has_cjs_exports;
self.has_jsx |= other.has_jsx;
if self.jsx_import_source.is_none() {
self.jsx_import_source = other.jsx_import_source;
self.jsx_import_source_span = other.jsx_import_source_span;
}
self.directives.extend(other.directives);
self.unresolved_dynamic.extend(other.unresolved_dynamic);
self.ambient_modules.extend(other.ambient_modules);
self.augmented_modules.extend(other.augmented_modules);
self.parse_diagnostics.extend(other.parse_diagnostics);
self.fatal |= other.fatal;
}
}
fn specifier_runs(specifiers: &[(Span, bool)]) -> Vec<(u32, u32)> {
let mut out = Vec::new();
let mut i = 0;
while i < specifiers.len() {
if !specifiers[i].1 {
i += 1;
continue;
}
let first = i;
while i < specifiers.len() && specifiers[i].1 {
i += 1;
}
let last = i - 1;
if let Some(next) = specifiers.get(i) {
out.push((specifiers[first].0.start, next.0.start));
} else if let Some(before) = first.checked_sub(1).and_then(|k| specifiers.get(k)) {
out.push((before.0.end, specifiers[last].0.end));
}
}
out
}
#[must_use]
pub fn coalesce_ranges(ranges: impl IntoIterator<Item = (u32, u32)>) -> Vec<(u32, u32)> {
let mut sorted: Vec<(u32, u32)> = ranges.into_iter().filter(|(s, e)| s < e).collect();
sorted.sort_unstable();
let mut out: Vec<(u32, u32)> = Vec::with_capacity(sorted.len());
for (start, end) in sorted {
match out.last_mut() {
Some(last) if start <= last.1 => last.1 = last.1.max(end),
_ => out.push((start, end)),
}
}
out
}
#[must_use]
pub fn is_source_query(query: &str) -> bool {
query
.strip_prefix('?')
.unwrap_or(query)
.split('&')
.any(|parameter| parameter == "raw")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseOptions {
pub in_file_references: bool,
pub member_accesses: bool,
}
impl Default for ParseOptions {
fn default() -> Self {
Self {
in_file_references: true,
member_accesses: true,
}
}
}
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ModuleInfo>();
assert_send_sync::<ParseOptions>();
};