diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fb39f163b..abb6a44b0 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Vendor directories remain excluded through filesystem path aliases.** Project analysis no longer scans dependencies when the workspace or vendor path resolves through an alias such as macOS `/var` to `/private/var`. Contributed by @sidux. - **`phpantom_lsp fix` runs its workers on the same stack as `analyze`.** The parse and fix workers were spawned with the 2 MB default a thread gets, where `analyze` gives its workers the 8 MB the recursive parser and type walker need, so a project holding a deeply nested file could crash `fix` outright where `analyze` completed. Both commands now share one parse phase, and `fix` also runs the Laravel discovery `analyze` does before fixing, so the two see the same project. - **A formatting edit measures the last line in UTF-16 units.** The whole-document replacement a formatter produces ended at a column counted in bytes, so a file whose unterminated last line held multibyte text was sent an end position past that line. - **Pint reads the project's `pint.json`.** Pint looks for its configuration in the directory it is started from, and it was started in the language server's own directory, so an editor that launches the server from a subdirectory or a multi-root workspace had Blade and PHP files formatted with Pint's default `laravel` preset rather than the project's. Pint, php-cs-fixer, and phpcbf now run with the workspace root as their working directory. diff --git a/src/indexing/init.rs b/src/indexing/init.rs index 766ebf4f2..4fcf0cb73 100644 --- a/src/indexing/init.rs +++ b/src/indexing/init.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use tower_lsp::lsp_types::*; -use super::classify_class_origin; +use super::{classify_class_origin, path_aliases}; use crate::Backend; use crate::classmap_scanner; use crate::composer; @@ -136,6 +136,7 @@ impl Backend { .set_runtime_permission_package(runtime_permissions); let (vendor_dir, vendor_path) = self.init_autoload_paths(root, composer_json.as_ref()); + let vendor_paths = path_aliases(&vendor_path); // ── Build the classmap ────────────────────────────────────── let strategy = self.config().indexing.strategy(); @@ -282,7 +283,7 @@ impl Backend { let origin = class_origins .get(&fqn) .copied() - .unwrap_or_else(|| classify_class_origin(&path, &vendor_path, &package_roots)); + .unwrap_or_else(|| classify_class_origin(&path, &vendor_paths, &package_roots)); origins.insert(fqn.clone(), origin); idx.or_insert_with(fqn, || crate::util::path_to_uri(&path)); } diff --git a/src/indexing/mod.rs b/src/indexing/mod.rs index 7edf8d3c5..d591d2f1c 100644 --- a/src/indexing/mod.rs +++ b/src/indexing/mod.rs @@ -22,14 +22,26 @@ mod reconcile; mod scan; mod watch; +/// Return the path as supplied plus its canonical spelling when the +/// filesystem exposes the same location through an alias. +pub(crate) fn path_aliases(path: &Path) -> Vec { + let mut paths = vec![path.to_path_buf()]; + if let Ok(canonical) = path.canonicalize() + && canonical != path + { + paths.push(canonical); + } + paths +} + /// Classify where a class file originates (project source, a direct vendor /// dependency, or a transitive vendor dependency) for completion ranking. pub(crate) fn classify_class_origin( path: &Path, - vendor_path: &Path, + vendor_paths: &[PathBuf], vendor_package_roots: &[(PathBuf, crate::ClassCompletionOrigin, String)], ) -> crate::ClassCompletionOrigin { - if !path.starts_with(vendor_path) { + if !vendor_paths.iter().any(|vendor| path.starts_with(vendor)) { return crate::ClassCompletionOrigin::Project; } for (root, origin, _pkg_name) in vendor_package_roots { @@ -39,3 +51,29 @@ pub(crate) fn classify_class_origin( } crate::ClassCompletionOrigin::VendorTransitive } + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::os::unix::fs::symlink; + + #[test] + fn canonical_vendor_files_stay_vendor_through_an_aliased_path() { + let dir = tempfile::tempdir().expect("tempdir"); + let canonical_vendor = dir.path().join("packages"); + let package_src = canonical_vendor.join("acme/package/src"); + std::fs::create_dir_all(&package_src).expect("create package directory"); + let aliased_vendor = dir.path().join("vendor"); + symlink(&canonical_vendor, &aliased_vendor).expect("create vendor alias"); + + let vendor_paths = path_aliases(&aliased_vendor); + let class_path = package_src.join("Service.php"); + std::fs::write(&class_path, "