Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions src/indexing/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
}
Expand Down
42 changes: 40 additions & 2 deletions src/indexing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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 {
Expand All @@ -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, "<?php").expect("write class file");
let class_path = class_path.canonicalize().expect("canonical class path");

assert_eq!(
classify_class_origin(&class_path, &vendor_paths, &[]),
crate::ClassCompletionOrigin::VendorTransitive
);
}
}
15 changes: 6 additions & 9 deletions src/indexing/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};

use super::classify_class_origin;
use super::{classify_class_origin, path_aliases};
use crate::Backend;
use crate::classmap_scanner::{self, WorkspaceScanResult};
use crate::composer;
Expand Down Expand Up @@ -48,14 +48,10 @@ impl Backend {
// of filesystem calls.
{
let mut paths = self.workspace.vendor_dir_paths.lock();
let mut insert = |path: PathBuf| {
for path in path_aliases(vendor_path) {
if !paths.contains(&path) {
paths.push(path);
}
};
insert(vendor_path.to_path_buf());
if let Ok(canonical) = vendor_path.canonicalize() {
insert(canonical);
}
}
// Store URI prefixes for URI-level skip logic (diagnostics, find
Expand Down Expand Up @@ -97,6 +93,7 @@ impl Backend {
// clone before `composer install`). Register it again now so the
// shared vendor filters cache both raw and canonical spellings.
self.add_vendor_dir(&vendor_path);
let vendor_paths = path_aliases(&vendor_path);

// Rebuild vendor classmap, tracking dependency provenance so
// completion ranking stays accurate after a composer change.
Expand Down Expand Up @@ -131,7 +128,7 @@ impl Backend {
.get(&fqn)
.copied()
.unwrap_or_else(|| {
classify_class_origin(&path, &vendor_path, &vendor_package_roots)
classify_class_origin(&path, &vendor_paths, &vendor_package_roots)
});
origins.insert(fqn.clone(), origin);
idx.insert(fqn, crate::util::path_to_uri(&path));
Expand All @@ -143,7 +140,7 @@ impl Backend {
// Purge functions that pointed into the old vendor tree
// before re-inserting, so symbols removed by a
// `composer update` no longer resolve.
fi.retain(|_, v| !v.starts_with(&vendor_path));
fi.retain(|_, v| !vendor_paths.iter().any(|vendor| v.starts_with(vendor)));
for (fqn, path) in vendor_scan.function_index {
let origin = vendor_scan
.function_origins
Expand All @@ -158,7 +155,7 @@ impl Backend {
let mut ci = self.symbols.autoload_constant_index.write();
let mut origins = self.symbols.autoload_constant_origin_index.write();
// Same for constants from the old vendor tree.
ci.retain(|_, v| !v.starts_with(&vendor_path));
ci.retain(|_, v| !vendor_paths.iter().any(|vendor| v.starts_with(vendor)));
for (name, path) in vendor_scan.constant_index {
let origin = vendor_scan
.constant_origins
Expand Down
Loading