Skip to content

feat(indexing): add [indexing] follow-links to walk symlinked directories inside the workspace - #386

Open
liudashuang wants to merge 1 commit into
PHPantom-dev:mainfrom
liudashuang:feat/follow-links-indexing
Open

feat(indexing): add [indexing] follow-links to walk symlinked directories inside the workspace#386
liudashuang wants to merge 1 commit into
PHPantom-dev:mainfrom
liudashuang:feat/follow-links-indexing

Conversation

@liudashuang

@liudashuang liudashuang commented Aug 20, 2026

Copy link
Copy Markdown

Fixes #383

Problem

A directory symlink found inside the workspace root (e.g. kdhelp -> ../kdhelp, pointing at a framework tree kept outside the repo) is never descended into: the ignore crate yields the symlink entry itself and stops, so classes, functions and constants under the linked tree are missing from the class index, Find References, workspace diagnostics, and everything else built on the walks. Go-to-definition, hover and completion all fail for those namespaces.

Change

Add an opt-in [indexing] follow-links setting (default false, matching the project's off-by-default posture for anything that widens the scan — the same stance as workspace-external and [diagnostics] workspace). When enabled, the workspace walks follow interior directory symlinks; the symlink spelling is preserved in every indexed path and returned URI, so the index, didOpen keys, and references all share one path representation.

All four workspace walkers are switched together — a half-wired feature would index classes but still miss references:

  • classmap_scanner::discovery::walk_roots (self-scan / PSR-4 / vendor / no-composer classmaps)
  • references::collect_php_files_gitignore (Find References, Rename, preload Phase 2, Laravel config trees)
  • analyse::discover_user_files (CLI analyze / fix)
  • util::collect_php_files (Go-to-implementation Phase 5)

The [indexing] section is the natural home: its only existing key is strategy, the walker behaviours are all scheduled by indexing::init, and the off-by-default style already has precedent in the same section's peers. The composer pipeline (PSR-4 + vendor walks via installed.json) also receives the config value, so a composer project with an interior symlink is not left half-broken.

Deliberately not changed:

  • scan_drupal_directories — Drupal's gitignore-less web-root scanner stays independent.
  • database_schema.rs migration walk — its comment explicitly refuses to follow links to avoid walking the whole project through a cycle.
  • composer::discover_subproject_roots — a no-op without a composer.json; following would let an external framework's own composer.json be mistaken for a monorepo subproject.

Cycle safety

Both walker shapes are safe against symlink cycles:

  • The parallel walk_roots path has ignore's check_symlink_loop (dev+inode handles), and the worker callback skips Err entries (Error::Loop lands there) without unwrapping.
  • The three serial walkers (collect_php_files_gitignore, discover_user_files, collect_php_files) share the same loop detection in ignore's Walk::next(); they consume entries via flatten(), which silently drops the Err a cycle produces instead of panicking.

Both are covered by tests: walk_roots_follows_symlink_cycle_safely (parallel, self-cycle) and collect_php_files_gitignore_follows_symlink_cycle_safely (serial, self-cycle).

Watcher blind spot (documented, not fixed)

The client's watchers only cover the workspace root, so changes on disk inside a linked target (a git pull into the framework) do not trigger a re-index. A file open in the editor re-parses on didOpen/didChange; everything else needs a window reload or server restart. This is documented in the CHANGELOG, docs/configuration.md, and the config doc comment.

Multi-root workspaces

Not addressed, matching the server's existing single-root limitation: server.rs reads root_uri only and ignores workspace_folders. With follow-links enabled the behaviour is exactly the same as the single-root case.

Windows CI note

CI has no Windows test job (release.yml builds Windows but doesn't run cargo test), so all new symlink tests use the #[cfg(unix)] / #[cfg(windows)] dual-branch pattern (see scan_directories_follows_a_symlinked_root, discovery_tests.rs). The author verified the tests on macOS; Windows runs are not exercised by CI.

Performance measurement (CLI walk/analyze scope)

  • Walk file count on the motivating project: +44.5% (2808 → 4057 PHP files, of which +1249 come from the linked framework).
  • Precise fixture numbers (real linked tree of 1249 PHP files): analyze wall time +8.7%, peak RSS +27.8%, error count 4 → 2 (no new errors).
  • Full-project analyze on the real workspace is in the "up to ~2×" range, but that is the CLI's heaviest one-shot path; interactive LSP use goes through incremental indexing (existing_uris filter in preload Phase 2), where the +44% file count adds sub-second disk-walk cost.
  • No online-session latency measurement was done; the numbers above are CLI walk/analyze scope only.
  • symbol_maps is pure in-memory (Arc<RwLock<HashMap>>) with no persistence layer, so follow-links affects memory only, not cold-start disk I/O.

Tests

14 new tests, all passing (cargo +1.97.0 test --lib → 5893 passed, 0 failed):

  • walk_roots: default-false, interior-follow (symlink-spelling contract), nested two-level links, self-cycle, skip_dirs-literal-match interaction
  • collect_php_files_gitignore: interior-follow / default-false / serial self-cycle
  • collect_php_files: interior-follow / default-false
  • discover_user_files: interior-follow / default-false
  • config deserialization: follow-links = true parses; absent defaults to false

Checklist

  • CHANGELOG updated (docs/CHANGELOG.md, Unreleased → Added, off-by-default phrasing)
  • docs/ updated (docs/configuration.md [indexing] table)
  • config-schema.json updated
  • Tests added and passing
  • clippy clean (verified locally with rustc 1.97.0)

…ries

Workspace walks now follow interior directory symlinks when
`follow-links = true` is set in .phpantom.toml (off by default).
A project that keeps its framework behind a symlink (e.g.
`kdhelp -> ../kdhelp`) gets go-to-definition, hover, completion,
Find References, and workspace diagnostics for the linked tree,
with the symlink spelling preserved in every indexed path and
returned URI.

All four workspace walkers are switched together so the feature
is never half-wired (classes indexed but references missed):

- classmap_scanner::discovery::walk_roots (self-scan / PSR-4 /
  vendor / no-composer classmaps)
- references::collect_php_files_gitignore (Find References,
  Rename, preload Phase 2, Laravel config trees)
- analyse::discover_user_files (CLI analyze / fix)
- util::collect_php_files (Go-to-implementation)

Deliberately unchanged: the Drupal scanner, the Laravel migration
walk (its comment refuses to follow links to avoid walking the
whole project through a cycle), and composer::discover_subproject_roots.

Both walker shapes are cycle-safe: the parallel path uses ignore's
check_symlink_loop and skips Err entries, the serial walkers share
the same loop detection and consume via flatten(), which drops the
loop error instead of panicking. Each is covered by a test.

Watcher blind spot documented: the client's watchers only cover the
workspace root, so changes on disk inside a linked target (a git
pull into the framework) do not trigger a re-index; reload the
window after such changes.

Closes PHPantom-dev#383
@AJenbo AJenbo added this to the Sprint 7 milestone Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support indexing classes under symlinked directories (e.g. framework source linked into the project)

2 participants