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",
|
"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.
|
/// Map a file extension to a language label, or `None` if unsupported.
|
||||||
pub fn language_for(path: &Path) -> Option<&'static str> {
|
pub fn language_for(path: &Path) -> Option<&'static str> {
|
||||||
let ext = path.extension()?.to_str()?;
|
let ext = path.extension()?.to_str()?;
|
||||||
Some(match ext {
|
LANGUAGE_BY_EXT
|
||||||
"rs" => "rust",
|
.iter()
|
||||||
"java" => "java",
|
.find(|(e, _)| *e == ext)
|
||||||
"py" => "python",
|
.map(|(_, lang)| *lang)
|
||||||
"c" | "h" => "c",
|
}
|
||||||
"cc" | "cpp" | "cxx" | "hpp" | "hh" | "hxx" => "cpp",
|
|
||||||
"ts" | "tsx" | "mts" | "cts" => "typescript",
|
/// Code extensions probed when resolving extensionless import targets.
|
||||||
"js" | "jsx" | "mjs" | "cjs" => "javascript",
|
/// Markup/asset languages (html/css) are always imported with an explicit
|
||||||
"html" | "htm" => "html",
|
/// extension, so they are excluded from probing.
|
||||||
"css" => "css",
|
pub fn code_extensions() -> impl Iterator<Item = &'static str> {
|
||||||
_ => return None,
|
LANGUAGE_BY_EXT
|
||||||
})
|
.iter()
|
||||||
|
.filter(|(_, lang)| *lang != "html" && *lang != "css")
|
||||||
|
.map(|(ext, _)| *ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Base cache directory for index databases.
|
/// Base cache directory for index databases.
|
||||||
|
|||||||
+4
-10
@@ -5,6 +5,7 @@ use std::sync::OnceLock;
|
|||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::config;
|
||||||
use crate::discovery::SourceFile;
|
use crate::discovery::SourceFile;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -306,14 +307,7 @@ fn clean(target: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Code file extensions probed when resolving extensionless import targets.
|
/// Code file extensions probed when resolving extensionless import targets.
|
||||||
/// Mirrors `config::language_for` for code languages (html/css assets are
|
/// Derived from `config::language_for` so the two lists can't drift.
|
||||||
/// 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.
|
|
||||||
const MODULE_ENTRIES: &[&str] = &["mod.rs", "__init__.py", "index.ts", "index.js", "index.tsx"];
|
const MODULE_ENTRIES: &[&str] = &["mod.rs", "__init__.py", "index.ts", "index.js", "index.tsx"];
|
||||||
|
|
||||||
impl Resolver {
|
impl Resolver {
|
||||||
@@ -403,7 +397,7 @@ impl Resolver {
|
|||||||
if let Some(id) = self.by_path.get(&format!("{base}{t}")) {
|
if let Some(id) = self.by_path.get(&format!("{base}{t}")) {
|
||||||
return Some(*id);
|
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}")) {
|
if let Some(id) = self.by_path.get(&format!("{base}{t}.{ext}")) {
|
||||||
return Some(*id);
|
return Some(*id);
|
||||||
}
|
}
|
||||||
@@ -417,7 +411,7 @@ impl Resolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn try_module(&self, base: &str) -> Option<i64> {
|
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}")) {
|
if let Some(id) = self.by_path.get(&format!("{base}.{ext}")) {
|
||||||
return Some(*id);
|
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.
|
/// (Re)index a project. `recreate` drops any existing index first.
|
||||||
pub fn run(project: &Path, db_path: &Path, recreate: bool) -> Result<IndexStats> {
|
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() {
|
if recreate && db_path.exists() {
|
||||||
std::fs::remove_file(db_path).context("remove old index")?;
|
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");
|
.expect("parse error response");
|
||||||
assert_eq!(parse["error"]["message"], "parse error");
|
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