use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::parse::{ImportFact, ModuleFacts};
use crate::resolve::Resolution;
use super::ResolutionCache;
pub(super) fn count_unresolved_imports(
modules: &BTreeMap<PathBuf, ModuleFacts>,
resolution_cache: &mut ResolutionCache<'_>,
ignore: &[String],
) -> usize {
let mut count = 0;
for module in modules.values() {
for import in &module.imports {
if !should_count_unresolved_import(import, ignore) {
continue;
}
if !resolution_cache.is_internal_specifier(&module.file, &import.source) {
continue;
}
if matches!(
resolution_cache.resolve(&module.file, &import.source),
Resolution::Unresolved
) {
count += 1;
}
}
}
count
}
const NON_CODE_IMPORT_EXTENSIONS: &[&str] = &[
".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".css", ".scss", ".sass", ".less",
".json", ".yaml", ".yml", ".md", ".mdx",
];
fn should_count_unresolved_import(import: &ImportFact, ignore: &[String]) -> bool {
if import.type_only {
return false;
}
let source = import.source.as_str();
let path = source.split(['?', '#']).next().unwrap_or(source);
if NON_CODE_IMPORT_EXTENSIONS
.iter()
.any(|extension| path.ends_with(extension))
{
return false;
}
if ignore.iter().any(|pattern| source.contains(pattern)) {
return false;
}
true
}
#[cfg(test)]
mod tests {
use crate::parse::{ImportKind, ImportTarget};
use super::*;
fn import(source: &str, type_only: bool) -> ImportFact {
ImportFact {
source: source.to_string(),
local: "local".to_string(),
imported: ImportTarget::Default,
kind: ImportKind::Esm,
type_only,
}
}
#[test]
fn skips_asset_imports_without_any_config() {
for source in [
"./package.json",
"./logo.svg",
"../styles/theme.css",
"./tokens.json",
"./content.mdx",
] {
assert!(!should_count_unresolved_import(&import(source, false), &[]));
}
}
#[test]
fn skips_asset_imports_with_query_or_hash_suffixes() {
for source in [
"./logo.svg?react",
"./style.css?inline",
"./data.json?raw",
"./icon.svg#symbol",
"./image.png?url",
] {
assert!(!should_count_unresolved_import(&import(source, false), &[]));
}
}
#[test]
fn skips_repo_configured_ignore_patterns() {
let ignore = vec![
".velite".to_string(),
"/+types/".to_string(),
"styled-system/css".to_string(),
"/dist/esm/".to_string(),
];
for source in [
"./.velite/generated",
"./+types/root",
"./routes/+types/page",
"./styled-system/css",
"./pkg/dist/esm/index",
] {
assert!(!should_count_unresolved_import(&import(source, false), &ignore));
}
}
#[test]
fn skips_type_only_unresolved_imports() {
assert!(!should_count_unresolved_import(&import("./types", true), &[]));
}
#[test]
fn counts_regular_runtime_imports() {
assert!(should_count_unresolved_import(&import("./missing", false), &[]));
assert!(should_count_unresolved_import(
&import("./missing", false),
&["styled-system/css".to_string()]
));
}
}