Fix two follow-up issues: reject nonexistent roots, single-source extensions

- pipeline: fail index create/update with a clear error when the project root
  is not a directory instead of building a misleading zero-file index
- config: single LANGUAGE_BY_EXT table as source of truth; language_for and
  resolver code_extensions both derive from it (no more duplicated ext list)
- imports: resolver probes code_extensions() instead of a local RESOLVE_EXTS
- tests: regression for nonexistent-root rejection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
endergebnisandCopilot committed 2026-09-04 20:45:17 +02:00
1 parent 4338e28ce0
commit a416a9fe52
4 files changed
+69 -22

No files matched your search

+43 -12
View File
@@ -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<Item = &'static str> {
LANGUAGE_BY_EXT
.iter()
.filter(|(_, lang)| *lang != "html" && *lang != "css")
.map(|(ext, _)| *ext)
}
/// Base cache directory for index databases.
+4 -10
View File
@@ -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<String> {
}
/// 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<i64> {
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);
}
+4
View File
@@ -44,6 +44,10 @@ fn read_crate_name(project: &Path) -> Option<String> {
/// (Re)index a project. `recreate` drops any existing index first.
pub fn run(project: &Path, db_path: &Path, recreate: bool) -> Result<IndexStats> {
// 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")?;
}