Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions codescythe.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 92 additions & 25 deletions crates/codescythe/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -102,6 +102,7 @@ pub fn analyze_path(
.map(|(index, file)| (file.path.clone(), index))
.collect::<HashMap<_, _>>();
let module_resolver = ModuleResolver::new(&cwd, &files, config);
let unresolved_policy = UnresolvedImportPolicy::new(config);

let mut entry_indexes = HashSet::<usize>::new();
let mut used_files = UsedFiles::new();
Expand Down Expand Up @@ -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 => {}
}
Expand All @@ -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 => {}
}
Expand All @@ -171,6 +166,7 @@ pub fn analyze_path(
&mut used_exports,
&mut queue,
&mut unresolved,
&unresolved_policy,
&file.relative,
)?;
}
Expand All @@ -191,6 +187,7 @@ pub fn analyze_path(
&mut used_exports,
&mut queue,
&mut unresolved,
&unresolved_policy,
&file.relative,
)?;
}
Expand All @@ -209,6 +206,7 @@ pub fn analyze_path(
&mut used_exports,
&mut queue,
&mut unresolved,
&unresolved_policy,
)?;
}
for source in &file.reexport_all {
Expand All @@ -221,6 +219,7 @@ pub fn analyze_path(
&mut used_exports,
&mut queue,
&mut unresolved,
&unresolved_policy,
)?;
}
}
Expand All @@ -236,6 +235,7 @@ pub fn analyze_path(
&mut used_exports,
&mut queue,
&mut unresolved,
&unresolved_policy,
)?;
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -654,6 +687,7 @@ fn mark_member_import(
used_exports: &mut UsedExports,
queue: &mut VecDeque<usize>,
unresolved: &mut UnresolvedImports,
unresolved_policy: &UnresolvedImportPolicy,
importer_relative: &str,
) -> Result<()> {
match resolver.resolve(from_file, source)? {
Expand All @@ -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 => {}
}
Expand All @@ -701,6 +733,7 @@ fn mark_reexport(
used_exports: &mut UsedExports,
queue: &mut VecDeque<usize>,
unresolved: &mut UnresolvedImports,
unresolved_policy: &UnresolvedImportPolicy,
) -> Result<()> {
if let (Some(source), Some(name)) = (&export.reexport_source, &export.reexport_name) {
match resolver.resolve(file, source)? {
Expand All @@ -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 => {}
}
Expand All @@ -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 => {}
}
Expand All @@ -748,6 +775,7 @@ fn mark_all_exports(
used_exports: &mut UsedExports,
queue: &mut VecDeque<usize>,
unresolved: &mut UnresolvedImports,
unresolved_policy: &UnresolvedImportPolicy,
) -> Result<()> {
match resolver.resolve(file, source)? {
ImportResolution::Project(target) => {
Expand All @@ -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 => {}
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1474,6 +1518,29 @@ console.log(missingExternal, missingExternalSubpath);
);
}

fn analyze_missing_import(mode: Option<&str>) -> Result<Analysis> {
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() {
Expand Down
10 changes: 10 additions & 0 deletions crates/codescythe/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@ pub struct CodescytheConfig {
pub ignore: Vec<String>,
#[serde(deserialize_with = "deserialize_aliases")]
pub aliases: BTreeMap<String, Vec<String>>,
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<CodescytheConfig> {
let value = match config_path {
Some(path) => Some(read_config_file(path)?),
Expand Down
2 changes: 1 addition & 1 deletion crates/codescythe/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading