From afe3af4d32850d68dd6c97884a2fff57e3a013ab Mon Sep 17 00:00:00 2001 From: Mike Bronner Date: Tue, 16 Jun 2026 06:09:50 -0700 Subject: [PATCH 1/2] chore: start work on #183 From ca965d4125a6097b5b694e079460f3dcea8b4678 Mon Sep 17 00:00:00 2001 From: Mike Bronner Date: Tue, 16 Jun 2026 06:14:05 -0700 Subject: [PATCH 2/2] =?UTF-8?q?test:=20=E2=9C=85=20cover=20the=20alternate?= =?UTF-8?q?=20Flux=20registration=20forms=20at=20the=20goto-def=20handler?= =?UTF-8?q?=20level?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two `#[tokio::test]`s to `flux_goto_def_handler.rs` covering the resolution paths #108's tests left untested at the handler level: - the `anonymous_component_paths` registration form (`Blade::anonymousComponentPath`), where the registered directory IS the components dir — distinct from the `anonymous_component_namespaces` form the existing tests drive; and - the directory-self `{base}/{last}.blade.php` candidate, the third file shape `push_component_file_candidates` emits. Extract `backend_with_config` so the path-form test can prime a distinct config without duplicating the `LspService` harness; `flux_backend` now delegates to it (existing call sites unchanged). Add a `flux_paths_config` helper mirroring the `anonymous_component_paths` arm in `flux_component_hover.rs`. Both drive the real `create_component_location_from_salsa` → `resolve_component_existing_file` → `LocationLink` chain. Fixes #183 --- .../src/tests/flux_goto_def_handler.rs | 126 ++++++++++++++++-- 1 file changed, 117 insertions(+), 9 deletions(-) diff --git a/laravel-lsp/src/tests/flux_goto_def_handler.rs b/laravel-lsp/src/tests/flux_goto_def_handler.rs index 9dacbe39..046fe0cd 100644 --- a/laravel-lsp/src/tests/flux_goto_def_handler.rs +++ b/laravel-lsp/src/tests/flux_goto_def_handler.rs @@ -21,6 +21,15 @@ //! `get_cached_config` short-circuits the (inert) Salsa actor and the chain runs //! purely against a tempdir: `file_exists_cached` reads real files off disk //! (the document cache is empty), exactly as it does for the live server. +//! +//! A follow-up (#183) extends the same handler-level coverage to the other +//! resolution paths the chain walks for a `flux:*` tag: +//! - the **anonymous-component-path** registration form +//! (`Blade::anonymousComponentPath`), where the registered directory IS the +//! components directory — contrast the namespace form the first tests use; and +//! - the **directory-self** candidate `{base}/{last}.blade.php` — the third +//! file shape `push_component_file_candidates` emits (`salsa_impl.rs`), which +//! no handler-level test exercised until now. use crate::LaravelLanguageServer; use laravel_lsp::salsa_impl::{ComponentReferenceData, LaravelConfigData}; @@ -60,21 +69,54 @@ fn flux_config(root: PathBuf) -> LaravelConfigData { } } -/// Build a backend for handler-level goto-def tests: `LspService::new` wires up a -/// real `Client`, `inner().clone()` hands back the `LaravelLanguageServer`, and -/// we prime the two pieces of state `resolve_component_existing_file` reads — the -/// cached config and the project root. With `cached_config` primed, -/// `get_cached_config` never touches the Salsa actor the harness spawns, so it -/// stays inert. The document cache is left empty, so `file_exists_cached` falls -/// through to a real `metadata` check against the tempdir. -async fn flux_backend(root: &Path) -> LaravelLanguageServer { +/// A `LaravelConfigData` rooted at `root` that registers the `flux` prefix as an +/// *anonymous component path* (`Blade::anonymousComponentPath`) pointing at +/// `flux_dir`. Unlike the namespace form `flux_config` builds, the registered +/// directory IS the components directory, so `` resolves to +/// `{flux_dir}/button.blade.php` directly — no `components/` segment. Mirrors the +/// `anonymous_component_paths` arm of `flux_config` in `flux_component_hover.rs`; +/// every other field is empty. +fn flux_paths_config(root: PathBuf, flux_dir: PathBuf) -> LaravelConfigData { + let mut anonymous_component_paths = HashMap::new(); + anonymous_component_paths.insert("flux".to_string(), flux_dir); + LaravelConfigData { + root, + view_paths: vec![PathBuf::from("resources/views")], + component_paths: Vec::new(), + livewire_path: None, + has_livewire: false, + view_namespaces: HashMap::new(), + component_namespaces: HashMap::new(), + anonymous_component_paths, + anonymous_component_namespaces: HashMap::new(), + component_aliases: HashMap::new(), + icon_aliases: HashMap::new(), + class_component_files: HashMap::new(), + } +} + +/// Build a backend for handler-level goto-def tests from an explicit `config`: +/// `LspService::new` wires up a real `Client`, `inner().clone()` hands back the +/// `LaravelLanguageServer`, and we prime the two pieces of state +/// `resolve_component_existing_file` reads — the cached config and the project +/// root. With `cached_config` primed, `get_cached_config` never touches the Salsa +/// actor the harness spawns, so it stays inert. The document cache is left empty, +/// so `file_exists_cached` falls through to a real `metadata` check against the +/// tempdir. +async fn backend_with_config(root: &Path, config: LaravelConfigData) -> LaravelLanguageServer { let (service, _socket) = LspService::new(LaravelLanguageServer::new); let backend = service.inner().clone(); *backend.root_path.write().await = Some(root.to_path_buf()); - *backend.cached_config.write().await = Some(flux_config(root.to_path_buf())); + *backend.cached_config.write().await = Some(config); backend } +/// Build a backend primed with the namespace-form `flux_config` — the +/// registration the first tests in this module exercise. +async fn flux_backend(root: &Path) -> LaravelLanguageServer { + backend_with_config(root, flux_config(root.to_path_buf())).await +} + /// Write `body` to `relpath` under `dir`, creating parent directories, and /// return the absolute path written. fn write_file(dir: &Path, relpath: &str, body: &str) -> PathBuf { @@ -213,3 +255,69 @@ async fn unresolvable_flux_tag_returns_none() { "an unresolvable Flux tag yields no LocationLink, got {resp:?}" ); } + +#[tokio::test] +async fn flux_tag_resolves_via_anonymous_component_path() { + // The *other* registration form: `Blade::anonymousComponentPath` registers + // the `flux` prefix against a directory that IS the components dir, so + // `` resolves to `{flux_dir}/button.blade.php` directly — no + // `components/` segment (contrast `flux_tag_resolves_to_location_link`, which + // drives the namespace form). The registered directory sits *outside* Flux's + // conventional fallback locations (`resources/views/flux`, + // `vendor/livewire/flux…`), so only the anonymous-component-path branch can + // produce this candidate — the assertion can't pass by coincidence. + let dir = TempDir::new().unwrap(); + let expected = write_file( + dir.path(), + "packages/flux/resources/views/button.blade.php", + FLUX_BLADE, + ); + let flux_dir = dir.path().join("packages/flux/resources/views"); + + let backend = backend_with_config( + dir.path(), + flux_paths_config(dir.path().to_path_buf(), flux_dir), + ) + .await; + let resp = backend + .create_component_location_from_salsa(&flux_ref("flux:button")) + .await + .expect("a Flux tag backed by an anonymous-component-path registration resolves"); + + assert_eq!( + single_link_target(resp), + expected, + "goto-def lands on the file under the registered anonymous-component path" + ); +} + +#[tokio::test] +async fn flux_tag_resolves_via_self_named_candidate() { + // The third file shape `push_component_file_candidates` emits is the + // directory-self convention `{base}/{last}.blade.php` (`salsa_impl.rs`): + // `` → `…/components/flux/button/button.blade.php`. With neither + // the direct file (`…/flux/button.blade.php`) nor the directory index + // (`…/flux/button/index.blade.php`) present, the handler must fall through to + // this self-named file. Uses the namespace registration form + // (`flux_config`/`flux_backend`), same as the first three tests. + let dir = TempDir::new().unwrap(); + let expected = write_file( + dir.path(), + "resources/views/components/flux/button/button.blade.php", + FLUX_BLADE, + ); + + let backend = flux_backend(dir.path()).await; + let resp = backend + .create_component_location_from_salsa(&flux_ref("flux:button")) + .await + .expect( + "the directory-self convention backs a Flux tag when no direct or index file exists", + ); + + assert_eq!( + single_link_target(resp), + expected, + "the self-named {{base}}/{{last}}.blade.php candidate resolves when the direct and index files are absent" + ); +}