diff --git a/codescythe.schema.json b/codescythe.schema.json index 1a6b0ba..c8cc0e2 100644 --- a/codescythe.schema.json +++ b/codescythe.schema.json @@ -28,6 +28,12 @@ "$ref": "#/$defs/stringOrStringArray" } }, + "unresolvedImports": { + "description": "Controls how unresolved imports are handled.", + "type": "string", + "enum": ["report", "ignore", "error"], + "default": "report" + }, "includeEntryExports": { "type": "boolean", "default": false diff --git a/crates/codescythe/analyze.rs b/crates/codescythe/analyze.rs index 21bc2c1..8398e78 100644 --- a/crates/codescythe/analyze.rs +++ b/crates/codescythe/analyze.rs @@ -17,7 +17,7 @@ use rayon::prelude::*; use serde::{Deserialize, Serialize}; use walkdir::{DirEntry, WalkDir}; -use crate::CodescytheConfig; +use crate::{CodescytheConfig, UnresolvedImportsMode}; const PARSE_THREADS_ENV: &str = "CODESCYTHE_PARSE_THREADS"; const RAYON_THREADS_ENV: &str = "RAYON_NUM_THREADS"; @@ -102,6 +102,7 @@ pub fn analyze_path( .map(|(index, file)| (file.path.clone(), index)) .collect::>(); let module_resolver = ModuleResolver::new(&cwd, &files, config); + let unresolved_policy = UnresolvedImportPolicy::new(config); let mut entry_indexes = HashSet::::new(); let mut used_files = UsedFiles::new(); @@ -133,10 +134,7 @@ pub fn analyze_path( } } ImportResolution::Unresolved => { - unresolved - .entry(file.relative.clone()) - .or_default() - .insert(import.source.clone()); + unresolved_policy.record(&mut unresolved, &file.relative, &import.source)?; } ImportResolution::External => {} } @@ -150,10 +148,7 @@ pub fn analyze_path( } } ImportResolution::Unresolved => { - unresolved - .entry(file.relative.clone()) - .or_default() - .insert(source.clone()); + unresolved_policy.record(&mut unresolved, &file.relative, source)?; } ImportResolution::External => {} } @@ -171,6 +166,7 @@ pub fn analyze_path( &mut used_exports, &mut queue, &mut unresolved, + &unresolved_policy, &file.relative, )?; } @@ -191,6 +187,7 @@ pub fn analyze_path( &mut used_exports, &mut queue, &mut unresolved, + &unresolved_policy, &file.relative, )?; } @@ -209,6 +206,7 @@ pub fn analyze_path( &mut used_exports, &mut queue, &mut unresolved, + &unresolved_policy, )?; } for source in &file.reexport_all { @@ -221,6 +219,7 @@ pub fn analyze_path( &mut used_exports, &mut queue, &mut unresolved, + &unresolved_policy, )?; } } @@ -236,6 +235,7 @@ pub fn analyze_path( &mut used_exports, &mut queue, &mut unresolved, + &unresolved_policy, )?; } } @@ -620,6 +620,39 @@ fn is_relative_alias_path(value: &str) -> bool { value == "." || value == ".." || value.starts_with("./") || value.starts_with("../") } +struct UnresolvedImportPolicy { + mode: UnresolvedImportsMode, +} + +impl UnresolvedImportPolicy { + fn new(config: &CodescytheConfig) -> Self { + Self { + mode: config.unresolved_imports, + } + } + + fn record( + &self, + unresolved: &mut UnresolvedImports, + importer: &str, + specifier: &str, + ) -> Result<()> { + match self.mode { + UnresolvedImportsMode::Report => { + unresolved + .entry(importer.to_string()) + .or_default() + .insert(specifier.to_string()); + Ok(()) + } + UnresolvedImportsMode::Ignore => Ok(()), + UnresolvedImportsMode::Error => { + anyhow::bail!("unresolved import {specifier:?} from {importer}") + } + } + } +} + fn is_resolution_miss(error: &ResolveError) -> bool { matches!( error, @@ -654,6 +687,7 @@ fn mark_member_import( used_exports: &mut UsedExports, queue: &mut VecDeque, unresolved: &mut UnresolvedImports, + unresolved_policy: &UnresolvedImportPolicy, importer_relative: &str, ) -> Result<()> { match resolver.resolve(from_file, source)? { @@ -677,16 +711,14 @@ fn mark_member_import( used_exports, queue, unresolved, + unresolved_policy, importer_relative, )?; } } } ImportResolution::Unresolved => { - unresolved - .entry(importer_relative.to_string()) - .or_default() - .insert(source.to_string()); + unresolved_policy.record(unresolved, importer_relative, source)?; } ImportResolution::External => {} } @@ -701,6 +733,7 @@ fn mark_reexport( used_exports: &mut UsedExports, queue: &mut VecDeque, unresolved: &mut UnresolvedImports, + unresolved_policy: &UnresolvedImportPolicy, ) -> Result<()> { if let (Some(source), Some(name)) = (&export.reexport_source, &export.reexport_name) { match resolver.resolve(file, source)? { @@ -711,10 +744,7 @@ fn mark_reexport( used_exports.entry(target).or_default().insert(name.clone()); } ImportResolution::Unresolved => { - unresolved - .entry(file.relative.clone()) - .or_default() - .insert(source.clone()); + unresolved_policy.record(unresolved, &file.relative, source)?; } ImportResolution::External => {} } @@ -728,10 +758,7 @@ fn mark_reexport( } } ImportResolution::Unresolved => { - unresolved - .entry(file.relative.clone()) - .or_default() - .insert(source.clone()); + unresolved_policy.record(unresolved, &file.relative, source)?; } ImportResolution::External => {} } @@ -748,6 +775,7 @@ fn mark_all_exports( used_exports: &mut UsedExports, queue: &mut VecDeque, unresolved: &mut UnresolvedImports, + unresolved_policy: &UnresolvedImportPolicy, ) -> Result<()> { match resolver.resolve(file, source)? { ImportResolution::Project(target) => { @@ -759,10 +787,7 @@ fn mark_all_exports( } } ImportResolution::Unresolved => { - unresolved - .entry(file.relative.clone()) - .or_default() - .insert(source.to_string()); + unresolved_policy.record(unresolved, &file.relative, source)?; } ImportResolution::External => {} } @@ -1440,6 +1465,25 @@ mod tests { ); } + #[test] + fn unresolved_import_modes_control_behavior() { + let report = analyze_missing_import(None).unwrap(); + assert_eq!( + report.issues.unresolved["src/main.ts"], + vec!["./missing".to_string()] + ); + assert_eq!(report.counters.unresolved, 1); + + let ignore = analyze_missing_import(Some("ignore")).unwrap(); + assert!(ignore.issues.unresolved.is_empty()); + assert_eq!(ignore.counters.unresolved, 0); + + let error = analyze_missing_import(Some("error")).unwrap_err(); + let message = format!("{error:#}"); + assert!(message.contains("src/main.ts")); + assert!(message.contains("./missing")); + } + #[test] fn reports_missing_local_imports() { let tempdir = tempfile::tempdir().unwrap(); @@ -1474,6 +1518,29 @@ console.log(missingExternal, missingExternalSubpath); ); } + fn analyze_missing_import(mode: Option<&str>) -> Result { + let tempdir = tempfile::tempdir().unwrap(); + let cwd = tempdir.path(); + let mode_config = mode + .map(|mode| format!(r#", "unresolvedImports": "{mode}""#)) + .unwrap_or_default(); + + write_file( + cwd, + "codescythe.json", + &format!( + r#"{{ + "entry": "src/main.ts", + "project": "src/**/*.ts"{mode_config} + }}"# + ), + ); + write_file(cwd, "src/main.ts", "import './missing';\n"); + + let config = crate::load_config(cwd, None).unwrap(); + analyze_path(cwd, &config, AnalysisOptions::default()) + } + fn write_file(root: &Path, relative: &str, contents: &str) { let path = root.join(relative); if let Some(parent) = path.parent() { diff --git a/crates/codescythe/config.rs b/crates/codescythe/config.rs index 29f1837..b9da1cf 100644 --- a/crates/codescythe/config.rs +++ b/crates/codescythe/config.rs @@ -17,10 +17,20 @@ pub struct CodescytheConfig { pub ignore: Vec, #[serde(deserialize_with = "deserialize_aliases")] pub aliases: BTreeMap>, + pub unresolved_imports: UnresolvedImportsMode, pub include_entry_exports: bool, pub ignore_exports_used_in_file: bool, } +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum UnresolvedImportsMode { + #[default] + Report, + Ignore, + Error, +} + pub fn load_config(cwd: &Path, config_path: Option<&Path>) -> Result { let value = match config_path { Some(path) => Some(read_config_file(path)?), diff --git a/crates/codescythe/lib.rs b/crates/codescythe/lib.rs index f505682..4a5cdb0 100644 --- a/crates/codescythe/lib.rs +++ b/crates/codescythe/lib.rs @@ -5,7 +5,7 @@ mod fix; pub use analyze::{ Analysis, AnalysisOptions, Counters, FileIssue, Issues, SymbolIssue, analyze_path, }; -pub use config::{CodescytheConfig, load_config}; +pub use config::{CodescytheConfig, UnresolvedImportsMode, load_config}; pub use fix::{FixResult, apply_fixes}; use std::path::Path;