use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum DependencyUsageSchemaVersion {
#[serde(rename = "1")]
V1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum UsageConfidence {
Syntactic,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DependencyUsage {
pub schema_version: DependencyUsageSchemaVersion,
pub confidence: UsageConfidence,
pub specifiers: Vec<SpecifierUsage>,
pub unresolved: FileLevelUnresolved,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sites: Option<UsageSitePage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub closure: Option<ConsumerClosure>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SpecifierUsage {
pub name: String,
pub file_count: usize,
pub type_only_file_count: usize,
pub call_site_count: usize,
pub unresolved: SpecifierUnresolved,
pub wrappers: Vec<UsageWrapper>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SpecifierUnresolved {
pub value_alias: usize,
pub non_call_reference: usize,
pub jsx_element: usize,
pub re_export: usize,
pub nested_wrapper: usize,
pub binding_without_site: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct FileLevelUnresolved {
pub dynamic_import: usize,
pub require: usize,
pub side_effect_import: usize,
pub star_re_export: usize,
pub unattributed_file: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum WrapperShape {
Call,
Alias,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UsageWrapper {
pub file: String,
pub export: String,
pub shape: WrapperShape,
pub line: u32,
pub consumer_file_count: usize,
pub call_site_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UsageSitePage {
pub items: Vec<UsageSite>,
pub total: usize,
pub limit: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UsageSite {
pub file: String,
pub line: u32,
pub col: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub specifier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub local_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub member: Option<String>,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub kind: UsageSiteKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub via: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageSiteKind {
Call,
WrapperDefinition,
ValueAlias,
NonCallReference,
JsxElement,
ReExport,
NestedWrapper,
DynamicImport,
Require,
SideEffectImport,
StarReExport,
}
impl UsageSiteKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Call => "call",
Self::WrapperDefinition => "wrapper_definition",
Self::ValueAlias => "value_alias",
Self::NonCallReference => "non_call_reference",
Self::JsxElement => "jsx_element",
Self::ReExport => "re_export",
Self::NestedWrapper => "nested_wrapper",
Self::DynamicImport => "dynamic_import",
Self::Require => "require",
Self::SideEffectImport => "side_effect_import",
Self::StarReExport => "star_re_export",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ConsumerClosure {
pub depth: u32,
pub file_count: usize,
pub files: Vec<ClosureFile>,
pub truncated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ClosureFile {
pub file: String,
pub depth: u32,
}
pub const DEFAULT_USAGE_SITE_LIMIT: u16 = 50;
pub const MAX_USAGE_SITE_LIMIT: u16 = 500;
pub const MAX_USAGE_CLOSURE_DEPTH: u32 = 10;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SitePageRequest {
pub limit: u16,
pub cursor: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DependencyUsageQuery {
pub specifiers: Vec<String>,
pub sites: Option<SitePageRequest>,
pub closure_depth: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UsageQueryError {
LimitOutOfRange(u16),
DepthOutOfRange(u32),
}
impl std::fmt::Display for UsageQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LimitOutOfRange(limit) => write!(
f,
"the site limit must be between 1 and {MAX_USAGE_SITE_LIMIT}, got {limit}"
),
Self::DepthOutOfRange(depth) => write!(
f,
"the closure depth must be between 1 and {MAX_USAGE_CLOSURE_DEPTH}, got {depth}"
),
}
}
}
impl std::error::Error for UsageQueryError {}
impl DependencyUsageQuery {
pub fn new(
specifiers: Vec<String>,
sites: Option<SitePageRequest>,
closure_depth: Option<u32>,
) -> Result<Self, UsageQueryError> {
if let Some(page) = &sites
&& !(1..=MAX_USAGE_SITE_LIMIT).contains(&page.limit)
{
return Err(UsageQueryError::LimitOutOfRange(page.limit));
}
if let Some(depth) = closure_depth
&& !(1..=MAX_USAGE_CLOSURE_DEPTH).contains(&depth)
{
return Err(UsageQueryError::DepthOutOfRange(depth));
}
Ok(Self {
specifiers,
sites,
closure_depth,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_rejects_out_of_range_limit_and_depth() {
let page = |limit| {
Some(SitePageRequest {
limit,
cursor: None,
})
};
assert_eq!(
DependencyUsageQuery::new(Vec::new(), page(0), None),
Err(UsageQueryError::LimitOutOfRange(0))
);
assert_eq!(
DependencyUsageQuery::new(Vec::new(), page(501), None),
Err(UsageQueryError::LimitOutOfRange(501))
);
assert_eq!(
DependencyUsageQuery::new(Vec::new(), None, Some(0)),
Err(UsageQueryError::DepthOutOfRange(0))
);
assert_eq!(
DependencyUsageQuery::new(Vec::new(), None, Some(11)),
Err(UsageQueryError::DepthOutOfRange(11))
);
assert!(DependencyUsageQuery::new(Vec::new(), page(500), Some(10)).is_ok());
assert!(DependencyUsageQuery::new(Vec::new(), page(1), Some(1)).is_ok());
}
#[test]
fn site_kind_wire_names_match_serde() {
for kind in [
UsageSiteKind::Call,
UsageSiteKind::WrapperDefinition,
UsageSiteKind::ValueAlias,
UsageSiteKind::NonCallReference,
UsageSiteKind::JsxElement,
UsageSiteKind::ReExport,
UsageSiteKind::NestedWrapper,
UsageSiteKind::DynamicImport,
UsageSiteKind::Require,
UsageSiteKind::SideEffectImport,
UsageSiteKind::StarReExport,
] {
assert_eq!(
serde_json::to_value(kind).expect("a site kind serializes"),
serde_json::Value::String(kind.as_str().to_owned())
);
}
}
}