mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
Critical bug fixes and recall improvements (#68)
This commit is contained in:
parent
7d0e7320e2
commit
55247b7fcd
352 changed files with 60069 additions and 900 deletions
1042
src/resolve/mod.rs
Normal file
1042
src/resolve/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
380
src/resolve/tests.rs
Normal file
380
src/resolve/tests.rs
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
//! Phase-04 resolver tests.
|
||||
//!
|
||||
//! Six specifier shapes (relative, parent-relative, scoped package,
|
||||
//! tsconfig path alias, node builtin, missing) plus a memory-ceiling
|
||||
//! guard. Each test sets up a synthetic tree under
|
||||
//! `tests/fixtures/resolver/` (or a `tempfile::TempDir` for the cheap
|
||||
//! ceiling test), constructs a [`ModuleGraph`] via [`build_module_graph`],
|
||||
//! and asserts the resolver verdict.
|
||||
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn fixture_root() -> PathBuf {
|
||||
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
p.push("tests/fixtures/resolver");
|
||||
p
|
||||
}
|
||||
|
||||
fn root() -> PathBuf {
|
||||
let r = fixture_root();
|
||||
if r.exists() {
|
||||
r.canonicalize().unwrap_or(r)
|
||||
} else {
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_relative_specifier() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "./foo")
|
||||
.expect("relative spec must classify");
|
||||
let file = resolved.file.expect("./foo must resolve");
|
||||
assert!(
|
||||
file.ends_with("apps/web/src/foo.ts"),
|
||||
"unexpected resolution: {}",
|
||||
file.display()
|
||||
);
|
||||
assert!(!resolved.is_builtin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_parent_relative_specifier() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "../bar/baz")
|
||||
.expect("../bar/baz must classify");
|
||||
let file = resolved.file.expect("../bar/baz must resolve");
|
||||
assert!(
|
||||
file.ends_with("apps/web/bar/baz.ts"),
|
||||
"unexpected resolution: {}",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_scoped_package_import() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "@scope/util")
|
||||
.expect("@scope/util must classify");
|
||||
assert_eq!(resolved.package.as_deref(), Some("@scope/util"));
|
||||
let file = resolved.file.expect("@scope/util must resolve to a file");
|
||||
assert!(
|
||||
file.ends_with("packages/util/src/index.ts") || file.ends_with("packages/util/index.ts"),
|
||||
"unexpected resolution: {}",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_tsconfig_path_alias() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "@/lib/x")
|
||||
.expect("@/lib/x must classify");
|
||||
let file = resolved.file.expect("@/lib/x must resolve");
|
||||
assert!(
|
||||
file.ends_with("apps/web/src/lib/x.ts"),
|
||||
"unexpected resolution: {}",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_node_builtin_specifier() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "node:fs/promises")
|
||||
.expect("node:fs/promises must classify");
|
||||
assert!(resolved.is_builtin);
|
||||
assert!(resolved.file.is_none());
|
||||
assert!(resolved.package.is_none());
|
||||
|
||||
let bare = graph
|
||||
.resolve_specifier(&importer, "fs")
|
||||
.expect("bare 'fs' must classify");
|
||||
assert!(bare.is_builtin);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_module_returns_none_resolved_file() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "no-such-package")
|
||||
.expect("non-empty spec must classify");
|
||||
assert!(!resolved.is_builtin);
|
||||
assert!(resolved.file.is_none(), "missing module must not resolve");
|
||||
assert!(resolved.package.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_for_returns_innermost_match() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let inner = r.join("packages/util/src/index.ts");
|
||||
let outer_pkg = graph
|
||||
.package_for(&inner)
|
||||
.expect("file under packages/util belongs to a package");
|
||||
assert_eq!(outer_pkg.name, "@scope/util");
|
||||
|
||||
let app_file = r.join("apps/web/src/index.ts");
|
||||
let web_pkg = graph
|
||||
.package_for(&app_file)
|
||||
.expect("file under apps/web belongs to a package");
|
||||
assert_eq!(web_pkg.name, "web-app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_namespace_prefixes_when_in_package() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let in_pkg = r.join("packages/util/src/index.ts");
|
||||
let ns = graph.project_namespace_for(&in_pkg, &r);
|
||||
assert!(
|
||||
ns.starts_with("@scope/util::"),
|
||||
"expected package-prefixed namespace, got {ns}"
|
||||
);
|
||||
|
||||
let outside = std::env::temp_dir().join("nyx-resolver-outside.ts");
|
||||
let plain = graph.project_namespace_for(&outside, &r);
|
||||
assert!(
|
||||
!plain.contains("::"),
|
||||
"outside-package namespace must be plain: {plain}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `"exports"."."` conditional map: `import` branch wins over `default`,
|
||||
/// and the legacy `main` field is shadowed when exports resolve.
|
||||
#[test]
|
||||
fn resolves_exports_root_conditional() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "@scope/exports-pkg")
|
||||
.expect("@scope/exports-pkg must classify");
|
||||
assert_eq!(resolved.package.as_deref(), Some("@scope/exports-pkg"));
|
||||
let file = resolved.file.expect("@scope/exports-pkg must resolve");
|
||||
assert!(
|
||||
file.ends_with("exports-pkg/src/main.ts"),
|
||||
"expected import-branch main.ts, got {}",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
|
||||
/// Exact subpath key (`"./sub": "./src/sub.ts"`) resolves before any
|
||||
/// pattern fallback would fire.
|
||||
#[test]
|
||||
fn resolves_exports_exact_subpath() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "@scope/exports-pkg/sub")
|
||||
.expect("subpath spec must classify");
|
||||
let file = resolved.file.expect("./sub must resolve");
|
||||
assert!(
|
||||
file.ends_with("exports-pkg/src/sub.ts"),
|
||||
"unexpected resolution: {}",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
|
||||
/// Wildcard pattern (`"./feat/*": "./src/feat/*.ts"`) substitutes the
|
||||
/// matched tail into the target.
|
||||
#[test]
|
||||
fn resolves_exports_wildcard_subpath() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "@scope/exports-pkg/feat/widget")
|
||||
.expect("wildcard subpath must classify");
|
||||
let file = resolved.file.expect("./feat/widget must resolve");
|
||||
assert!(
|
||||
file.ends_with("exports-pkg/src/feat/widget.ts"),
|
||||
"unexpected resolution: {}",
|
||||
file.display()
|
||||
);
|
||||
}
|
||||
|
||||
/// `null` value blocks the subpath: resolver returns no file rather than
|
||||
/// falling back to a direct path join.
|
||||
#[test]
|
||||
fn exports_null_blocks_subpath() {
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let resolved = graph
|
||||
.resolve_specifier(&importer, "@scope/exports-pkg/blocked")
|
||||
.expect("blocked spec must classify");
|
||||
assert!(
|
||||
resolved.file.is_none(),
|
||||
"null exports value must not resolve, got {:?}",
|
||||
resolved.file
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_graph_is_cheap() {
|
||||
use std::time::Instant;
|
||||
|
||||
let r = root();
|
||||
let bytes_before = approximate_rss_kib();
|
||||
let start = Instant::now();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let elapsed = start.elapsed();
|
||||
let bytes_after = approximate_rss_kib();
|
||||
|
||||
assert!(
|
||||
elapsed.as_millis() < 50,
|
||||
"build_module_graph took {}ms (>50ms ceiling)",
|
||||
elapsed.as_millis()
|
||||
);
|
||||
|
||||
let delta_kib = bytes_after.saturating_sub(bytes_before);
|
||||
assert!(
|
||||
delta_kib < 10 * 1024,
|
||||
"build_module_graph added {delta_kib} KiB RSS (>10 MiB ceiling)"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!graph.packages().is_empty(),
|
||||
"fixture tree must have packages"
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse a TypeScript file with tree-sitter and run
|
||||
/// [`extract_resolved_imports`] against it. Tests pull this through to
|
||||
/// keep the parsing setup in one place.
|
||||
fn extract_imports_for(file: &std::path::Path, graph: &ModuleGraph) -> Vec<ImportBinding> {
|
||||
let bytes = std::fs::read(file).expect("read fixture file");
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser
|
||||
.set_language(&tree_sitter::Language::from(
|
||||
tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
|
||||
))
|
||||
.expect("load TS grammar");
|
||||
let tree = parser.parse(&bytes, None).expect("parse fixture");
|
||||
extract_resolved_imports(&tree, &bytes, file, graph, "typescript")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_imports_from_fixture_file() {
|
||||
// Verify `extract_resolved_imports` lifts the same four binding shapes
|
||||
// that `tests/fixtures/resolver/apps/web/src/index.ts` exercises:
|
||||
// relative, parent-relative, scoped package, tsconfig path alias, plus
|
||||
// the `node:fs/promises` builtin. Phases 09/10 thread these bindings
|
||||
// through cross-file taint, so the parsed-file integration path must
|
||||
// produce the rows the resolver tests already cover via
|
||||
// `resolve_specifier`.
|
||||
let r = root();
|
||||
let graph = build_module_graph(std::slice::from_ref(&r));
|
||||
let importer = r.join("apps/web/src/index.ts");
|
||||
let bindings = extract_imports_for(&importer, &graph);
|
||||
|
||||
let by_local: std::collections::HashMap<&str, &ImportBinding> = bindings
|
||||
.iter()
|
||||
.map(|b| (b.local_name.as_str(), b))
|
||||
.collect();
|
||||
|
||||
// `import { foo } from "./foo"` — relative.
|
||||
let foo = by_local.get("foo").expect("foo binding present");
|
||||
assert_eq!(foo.source_module, "./foo");
|
||||
assert_eq!(foo.exported_name.as_deref(), Some("foo"));
|
||||
let foo_file = foo.resolved_file.as_ref().expect("./foo resolves");
|
||||
assert!(
|
||||
foo_file.ends_with("apps/web/src/foo.ts"),
|
||||
"foo unexpected: {}",
|
||||
foo_file.display()
|
||||
);
|
||||
|
||||
// `import { baz } from "../bar/baz"` — parent-relative.
|
||||
let baz = by_local.get("baz").expect("baz binding present");
|
||||
assert_eq!(baz.source_module, "../bar/baz");
|
||||
let baz_file = baz.resolved_file.as_ref().expect("../bar/baz resolves");
|
||||
assert!(
|
||||
baz_file.ends_with("apps/web/bar/baz.ts"),
|
||||
"baz unexpected: {}",
|
||||
baz_file.display()
|
||||
);
|
||||
|
||||
// `import { util } from "@scope/util"` — scoped package.
|
||||
let util = by_local.get("util").expect("util binding present");
|
||||
assert_eq!(util.source_module, "@scope/util");
|
||||
assert!(
|
||||
util.resolved_file.is_some(),
|
||||
"@scope/util must resolve to a file"
|
||||
);
|
||||
|
||||
// `import { x } from "@/lib/x"` — tsconfig path alias.
|
||||
let x = by_local.get("x").expect("x binding present");
|
||||
assert_eq!(x.source_module, "@/lib/x");
|
||||
let x_file = x.resolved_file.as_ref().expect("@/lib/x resolves");
|
||||
assert!(
|
||||
x_file.ends_with("apps/web/src/lib/x.ts"),
|
||||
"x unexpected: {}",
|
||||
x_file.display()
|
||||
);
|
||||
|
||||
// `import { promises as fs } from "node:fs/promises"` — node builtin.
|
||||
// Local-name binding must use the alias `fs`, not the original `promises`.
|
||||
let fs = by_local.get("fs").expect("fs alias binding present");
|
||||
assert_eq!(fs.source_module, "node:fs/promises");
|
||||
assert_eq!(fs.exported_name.as_deref(), Some("promises"));
|
||||
assert!(
|
||||
fs.resolved_file.is_none(),
|
||||
"node:* builtin must not carry a resolved file"
|
||||
);
|
||||
}
|
||||
|
||||
/// Best-effort RSS reader. Returns 0 on any failure, the test only uses
|
||||
/// the delta and treats "0 → 0" as "below ceiling".
|
||||
fn approximate_rss_kib() -> u64 {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
std::fs::read_to_string("/proc/self/status")
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
s.lines().find(|l| l.starts_with("VmRSS:")).and_then(|l| {
|
||||
l.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|n| n.parse::<u64>().ok())
|
||||
})
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let output = std::process::Command::new("ps")
|
||||
.args(["-o", "rss=", "-p", &std::process::id().to_string()])
|
||||
.output()
|
||||
.ok();
|
||||
output
|
||||
.and_then(|o| {
|
||||
String::from_utf8(o.stdout)
|
||||
.ok()
|
||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
0
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue