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:
1 parent
4338e28ce0
commit
a416a9fe52
4 files changed
+69
-22
No files matched your search
+43
-12
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")?;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user