diff --git a/src/config.rs b/src/config.rs index d840e37..f3f45fb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,21 +28,52 @@ pub const SKIP_DIRS: &[&str] = &[ "out", ]; +/// Extension → language for every supported source extension. Single source of +/// truth: discovery (`language_for`) and import resolution (`code_extensions`) +/// both derive from this table, so a new extension can never drift between the +/// two. Code extensions come first (resolver probe order), markup/assets last. +const LANGUAGE_BY_EXT: &[(&str, &str)] = &[ + ("rs", "rust"), + ("py", "python"), + ("ts", "typescript"), + ("tsx", "typescript"), + ("mts", "typescript"), + ("cts", "typescript"), + ("js", "javascript"), + ("jsx", "javascript"), + ("mjs", "javascript"), + ("cjs", "javascript"), + ("c", "c"), + ("h", "c"), + ("cc", "cpp"), + ("cpp", "cpp"), + ("cxx", "cpp"), + ("hpp", "cpp"), + ("hh", "cpp"), + ("hxx", "cpp"), + ("java", "java"), + ("html", "html"), + ("htm", "html"), + ("css", "css"), +]; + /// Map a file extension to a language label, or `None` if unsupported. pub fn language_for(path: &Path) -> Option<&'static str> { let ext = path.extension()?.to_str()?; - Some(match ext { - "rs" => "rust", - "java" => "java", - "py" => "python", - "c" | "h" => "c", - "cc" | "cpp" | "cxx" | "hpp" | "hh" | "hxx" => "cpp", - "ts" | "tsx" | "mts" | "cts" => "typescript", - "js" | "jsx" | "mjs" | "cjs" => "javascript", - "html" | "htm" => "html", - "css" => "css", - _ => return None, - }) + LANGUAGE_BY_EXT + .iter() + .find(|(e, _)| *e == ext) + .map(|(_, lang)| *lang) +} + +/// Code extensions probed when resolving extensionless import targets. +/// Markup/asset languages (html/css) are always imported with an explicit +/// extension, so they are excluded from probing. +pub fn code_extensions() -> impl Iterator { + LANGUAGE_BY_EXT + .iter() + .filter(|(_, lang)| *lang != "html" && *lang != "css") + .map(|(ext, _)| *ext) } /// Base cache directory for index databases. diff --git a/src/index/imports.rs b/src/index/imports.rs index 20f2670..d92227a 100644 --- a/src/index/imports.rs +++ b/src/index/imports.rs @@ -5,6 +5,7 @@ use std::sync::OnceLock; use regex::Regex; +use crate::config; use crate::discovery::SourceFile; #[derive(Debug, Clone)] @@ -306,14 +307,7 @@ fn clean(target: &str) -> Option { } /// Code file extensions probed when resolving extensionless import targets. -/// Mirrors `config::language_for` for code languages (html/css assets are -/// always imported with an explicit extension, so they need no probing). -const RESOLVE_EXTS: &[&str] = &[ - "rs", "py", "ts", "tsx", "mts", "cts", "js", "jsx", "mjs", "cjs", "c", "h", "cc", "cpp", "cxx", - "hpp", "hh", "hxx", "java", -]; - -/// Module-directory entry files tried for `dir/mod` style resolution. +/// Derived from `config::language_for` so the two lists can't drift. const MODULE_ENTRIES: &[&str] = &["mod.rs", "__init__.py", "index.ts", "index.js", "index.tsx"]; impl Resolver { @@ -403,7 +397,7 @@ impl Resolver { if let Some(id) = self.by_path.get(&format!("{base}{t}")) { return Some(*id); } - for ext in RESOLVE_EXTS { + for ext in config::code_extensions() { if let Some(id) = self.by_path.get(&format!("{base}{t}.{ext}")) { return Some(*id); } @@ -417,7 +411,7 @@ impl Resolver { } fn try_module(&self, base: &str) -> Option { - for ext in RESOLVE_EXTS { + for ext in config::code_extensions() { if let Some(id) = self.by_path.get(&format!("{base}.{ext}")) { return Some(*id); } diff --git a/src/index/pipeline.rs b/src/index/pipeline.rs index 12ec7c5..2b43277 100644 --- a/src/index/pipeline.rs +++ b/src/index/pipeline.rs @@ -44,6 +44,10 @@ fn read_crate_name(project: &Path) -> Option { /// (Re)index a project. `recreate` drops any existing index first. pub fn run(project: &Path, db_path: &Path, recreate: bool) -> Result { + // A typo'd path must fail loudly, not create a misleading empty index. + if !project.is_dir() { + anyhow::bail!("project root is not a directory: {}", project.display()); + } if recreate && db_path.exists() { std::fs::remove_file(db_path).context("remove old index")?; } diff --git a/tests/cli.rs b/tests/cli.rs index b8c2e8b..bd07869 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -626,3 +626,21 @@ fn mcp_reports_invalid_params_and_parse_errors() { .expect("parse error response"); assert_eq!(parse["error"]["message"], "parse error"); } + +#[test] +fn index_create_rejects_nonexistent_root() { + let out = Command::new(bin()) + .args(["index", "create", "/definitely/not/a/real/rustytokens/path"]) + .env("XDG_CACHE_HOME", cache_dir()) + .output() + .expect("run rustytokens"); + assert!( + !out.status.success(), + "index create must fail on a nonexistent root" + ); + assert!( + String::from_utf8_lossy(&out.stderr).contains("not a directory"), + "stderr explains the failure: {}", + String::from_utf8_lossy(&out.stderr) + ); +}