diff --git a/laravel-lsp/src/main.rs b/laravel-lsp/src/main.rs index 428f2a0b..f8a90ca8 100644 --- a/laravel-lsp/src/main.rs +++ b/laravel-lsp/src/main.rs @@ -18241,10 +18241,25 @@ async fn decl_range_at( /// into the decl walk and never reach its real resolver. When the project root /// is unknown (`None`), default to `false` so the walk never triggers without a /// root to anchor against. +/// +/// Both `path` and the joined `routes/` dir are canonicalized before the +/// component-wise prefix check (issue #122), mirroring the sibling helper +/// `path_within_root`. `root` is stored once (from an earlier `did_open`) while +/// `path` arrives per-request, so the two can resolve through different symlink +/// states — e.g. macOS `/tmp` → `/private/tmp`. A raw textual `starts_with` +/// would then return a false negative and silently skip the decl-fallback walk +/// for a real conventional route file. When either side can't be canonicalized +/// (the file doesn't exist yet, a permission error) we fall back to the textual +/// prefix check rather than guess — no panic, and the same behaviour as before +/// for in-memory paths that aren't on disk. fn is_in_routes_dir(root: Option<&Path>, path: &Path) -> bool { - match root { - Some(r) => path.starts_with(r.join("routes")), - None => false, + let Some(r) = root else { + return false; + }; + let routes_dir = r.join("routes"); + match (path.canonicalize(), routes_dir.canonicalize()) { + (Ok(real_path), Ok(real_routes)) => real_path.starts_with(&real_routes), + _ => path.starts_with(&routes_dir), } } diff --git a/laravel-lsp/src/tests/routes_dir_gate.rs b/laravel-lsp/src/tests/routes_dir_gate.rs index d644afcb..b56e9619 100644 --- a/laravel-lsp/src/tests/routes_dir_gate.rs +++ b/laravel-lsp/src/tests/routes_dir_gate.rs @@ -51,3 +51,68 @@ fn rejects_when_root_unknown() { // No project root to anchor against → never trigger the fallback walk. assert!(!is_in_routes_dir(None, Path::new("/any/routes/file.php"))); } + +#[cfg(unix)] +#[test] +fn matches_through_symlinked_root() { + // `root` and `path` can resolve through different symlink states (issue + // #122) — e.g. a stored root reached via a symlink vs. a per-request file + // addressed by its real path. A raw textual `starts_with` would return a + // false negative; canonicalizing both sides makes the gate survive it. + use std::os::unix::fs::symlink; + + let tmp = tempfile::tempdir().unwrap(); + // Real project with a conventional routes/ dir and a route file on disk. + let real_root = tmp.path().join("real_project"); + std::fs::create_dir_all(real_root.join("routes")).unwrap(); + let route_file = real_root.join("routes").join("web.php"); + std::fs::write(&route_file, "