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
7 changes: 7 additions & 0 deletions codescythe.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"description": "Glob patterns to exclude from analysis.",
"$ref": "#/$defs/stringOrStringArray"
},
"aliases": {
"description": "Import specifier aliases. These override package.json imports and may use wildcard keys such as #generated/*.",
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/stringOrStringArray"
}
},
"includeEntryExports": {
"type": "boolean",
"default": false
Expand Down
88 changes: 85 additions & 3 deletions crates/codescythe/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use oxc::ast_visit::{Visit, walk};
use oxc_allocator::Allocator;
use oxc_ast::ast::*;
use oxc_parser::{Parser, ParserReturn};
use oxc_resolver::{ResolveError, ResolveOptions, Resolver, TsconfigDiscovery};
use oxc_resolver::{AliasValue, ResolveError, ResolveOptions, Resolver, TsconfigDiscovery};
use oxc_span::{GetSpan, SourceType, Span};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -101,7 +101,7 @@ pub fn analyze_path(
.enumerate()
.map(|(index, file)| (file.path.clone(), index))
.collect::<HashMap<_, _>>();
let module_resolver = ModuleResolver::new(&cwd, &files);
let module_resolver = ModuleResolver::new(&cwd, &files, config);

let mut entry_indexes = HashSet::<usize>::new();
let mut used_files = UsedFiles::new();
Expand Down Expand Up @@ -517,10 +517,11 @@ enum ImportResolution {
}

impl ModuleResolver {
fn new(cwd: &Path, files: &[FileData]) -> Self {
fn new(cwd: &Path, files: &[FileData], config: &CodescytheConfig) -> Self {
let resolver = Resolver::new(ResolveOptions {
cwd: Some(cwd.to_path_buf()),
tsconfig: Some(TsconfigDiscovery::Auto),
alias: config_aliases(cwd, config),
condition_names: vec!["node".into(), "import".into()],
extensions: vec![
".ts".into(),
Expand Down Expand Up @@ -590,6 +591,35 @@ impl ModuleResolver {
}
}

fn config_aliases(cwd: &Path, config: &CodescytheConfig) -> Vec<(String, Vec<AliasValue>)> {
config
.aliases
.iter()
.map(|(key, values)| {
(
key.clone(),
values
.iter()
.map(|value| AliasValue::Path(config_alias_value(cwd, value)))
.collect(),
)
})
.collect()
}

fn config_alias_value(cwd: &Path, value: &str) -> String {
if is_relative_alias_path(value) {
return normalize_path(&cwd.join(value))
.to_string_lossy()
.replace('\\', "/");
}
value.to_string()
}

fn is_relative_alias_path(value: &str) -> bool {
value == "." || value == ".." || value.starts_with("./") || value.starts_with("../")
}

fn is_resolution_miss(error: &ResolveError) -> bool {
matches!(
error,
Expand Down Expand Up @@ -1358,6 +1388,58 @@ mod tests {
);
}

#[test]
fn explicit_aliases_override_package_json_imports() {
let tempdir = tempfile::tempdir().unwrap();
let cwd = tempdir.path();

write_file(
cwd,
"codescythe.json",
r##"{
"entry": "src/main.ts",
"project": [
"src/**/*.ts",
"generated/**/*.ts",
"wrong/**/*.ts"
],
"aliases": {
"#generated/*": "./generated/*.ts"
}
}"##,
);
write_file(
cwd,
"package.json",
r##"{
"imports": {
"#generated/*": "./wrong/*.ts"
}
}"##,
);
write_file(
cwd,
"src/main.ts",
"import { used } from '#generated/used';\nconsole.log(used);\n",
);
write_file(cwd, "generated/used.ts", "export const used = 1;\n");
write_file(cwd, "wrong/used.ts", "export const used = 1;\n");

let config = crate::load_config(cwd, None).unwrap();
let analysis = analyze_path(cwd, &config, AnalysisOptions::default()).unwrap();

assert!(analysis.issues.unresolved.is_empty());
assert!(!analysis.issues.files.contains_key("generated/used.ts"));
assert!(analysis.issues.files.contains_key("wrong/used.ts"));
assert!(
!analysis
.issues
.exports
.get("generated/used.ts")
.is_some_and(|exports| exports.contains_key("used"))
);
}

#[test]
fn reports_missing_local_imports() {
let tempdir = tempfile::tempdir().unwrap();
Expand Down
22 changes: 21 additions & 1 deletion crates/codescythe/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{fs, path::Path};
use std::{collections::BTreeMap, fs, path::Path};

use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
Expand All @@ -15,6 +15,8 @@ pub struct CodescytheConfig {
pub project: Vec<String>,
#[serde(deserialize_with = "deserialize_patterns")]
pub ignore: Vec<String>,
#[serde(deserialize_with = "deserialize_aliases")]
pub aliases: BTreeMap<String, Vec<String>>,
pub include_entry_exports: bool,
pub ignore_exports_used_in_file: bool,
}
Expand Down Expand Up @@ -109,6 +111,24 @@ where
})
}

fn deserialize_aliases<'de, D>(deserializer: D) -> Result<BTreeMap<String, Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<BTreeMap<String, StringOrVec>>::deserialize(deserializer)?;
Ok(value
.unwrap_or_default()
.into_iter()
.map(|(key, value)| {
let values = match value {
StringOrVec::String(value) => vec![value],
StringOrVec::Vec(values) => values,
};
(key, values)
})
.collect())
}

#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
Expand Down
Loading