Generalize the class locator to a kind-agnostic SymbolLocator - #388
Open
Firehed wants to merge 31 commits into
Open
Generalize the class locator to a kind-agnostic SymbolLocator#388Firehed wants to merge 31 commits into
Firehed wants to merge 31 commits into
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #388 +/- ##
============================================
+ Coverage 98.55% 98.61% +0.05%
- Complexity 1753 1794 +41
============================================
Files 115 120 +5
Lines 4434 4534 +100
============================================
+ Hits 4370 4471 +101
+ Misses 64 63 -1 ☔ View full report in Codecov by Harness. |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PHP matches namespace names case-insensitively for every kind; only a constant's final segment is case-sensitive. Keying the whole FQN case-sensitively made \FIXTURES\HELPERS\HELPER_LIMIT unresolvable even though PHP resolves it to the declared \Fixtures\Helpers\HELPER_LIMIT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both invalidation tests passed under mutation. "At least one more parse" could not tell re-reading the changed file from rescanning the whole set, and a rebuild from wholly cached scans costs no parse either, so the parse counter could not observe the autoload.files guard at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deleting the instanceof Invalidatable branch left the suite green. It is the only wiring between a watched-file event and the autoload.files index, and it has no production consumer until S3.8, so no integration test could have caught its loss indirectly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flipping ??= to = left the suite green: every autoload.files fixture declares disjoint names, so nothing distinguished first-wins from last-wins. The new fixture is read as data only and stays out of the files autoload, since PHP could not load both entries anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The fixture project's autoload.files all sit outside vendor/, so warming only the first warmable left the suite green. Partitioning at AutoloadFiles/ puts the sole entry in the vendor half. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-message budgets subtract startupParseCount(), which measures the same path, so any startup regression cancels and is invisible there. Unwiring KnowledgeWarmer entirely left the suite green, as would a warm-up that walked vendor/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Asserting the absence of a runtime-concatenated name can never fail, so recording a placeholder for the computed site left both named tests green. The constant count catches it; the locator-level test says where its force comes from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The superseded ComposerClassLocatorTest asserted which file the classmap resolved to; its successor kept only assertNotNull, so a QualifiedName conversion that mangled the namespace could still pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A guarded polyfill is the shape most real autoload.files entries take, and its declaration is nested rather than top-level. Nothing covered it, so walking only top-level statements would have silently dropped every one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The recorded reason — that the project root is unknown until initialize — is not true of this server: forProject takes the root as an argument and builds the whole stack before reading the transport. Nothing in src/ reads rootUri or workspaceFolders. The real reason is that initialized is the first point with no client request outstanding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docs enumerated only the computed-define carve-out, so an entry that merely requires a sibling read as covered. It is not: this repository's own symfony/polyfill-deepclone entry declares nothing and requires bootstrap81, where every symbol lives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ternary filed every non-function kind under 'constant'. Unreachable today, but a restructuring of locate()'s early return would have indexed functions as constants silently rather than failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This slice made file:// conversion one seam and migrated the two internal call sites, leaving the one whose output reaches the client. A definition response for a workspace under a path with a space emitted an unencoded, invalid URI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An intersection mock satisfies PHPUnit at runtime but is a bare MockObject to static analysis, so the backendWithLocator() argument failed level max. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Firehed
added a commit
that referenced
this pull request
Aug 3, 2026
Re-scopes the remaining Step 3b slices so each is reviewable as one unit, and records two decisions found while sizing them. ## Why S3.7 was built as a single slice (#388: 622 src lines over 17 files, 30 commits) and is too large to review as one unit. The seam it missed is already in the code: a class-like lookup is arithmetic on the name (`findFile` — five lines, the old `ComposerClassLocator` verbatim), while a function or constant lookup has no name→file map and must derive one by parsing the `autoload.files` set. Those are two different reviews. Sizing the rest of Step 3b against the merged slices (S3.2 +58/−40, S3.4 +171/−28, S3.1 +305/−47) showed S3.8 and S3.9 heading the same way. ## What changes - **S3.7 → S3.7a–d.** `autoload.files` into the autoload map; the per-file declaration scan; the kind-agnostic `SymbolLocator` with its class-like branch (behavior-preserving, proven by the existing golden); the derived function/constant index (new behavior, new fixtures). S3.7a and S3.7b are independent inputs to S3.7d. - **S3.8 → S3.8a–c, cut by symbol namespace rather than by layer.** A layer cut would land `SymbolBackend` methods no backend implements. S3.8c carries the `SymbolResolver` edit, so S4.2 now serializes against it rather than against S3.8 as a whole. - **S3.9 → S3.9a–b, cut on provability.** S3.9a widens `searchClassLikes` to a kind parameter with every Step P golden frozen; S3.9b makes the backends answer function search and rewrites only the function-surface golden. - **SC.4 added.** `file://` conversion is hand-rolled in four live places, each differing in how it handles the scheme and percent-encoding. Pre-existing duplication owned by no step, which is what the SC.* block is for. S3.7c is gated on it rather than adding a fifth copy. ## Decisions recorded - **The `autoload.files` warm-up is declined**, with numbers (0002 §3). It was never a correctness requirement — an unwarmed locator answers identically — so it only moves which message pays a one-off cost. At §8.1's measured ~4 MB/s that cost is ~35 ms here (~7 ms excluding PHPUnit's `Assert/Functions.php`, which is 111 KB of the 139 KB). Not worth ~105 lines, a second `InitializedListener`, and a non-obvious ordering rule. A reopen condition is named, and `warm()` changes no interface, so re-adding it later is free. - **`ConstantName` is already taken** by `Domain\ConstantName` (class constants), which collides with §5.3's global-constant FQN type. Flagged to be decided before S3.8b rather than inside it. Also corrects the name-type JIT note: `NameKind` already exists, and `QualifiedName` lands in S3.7c rather than S3.8. Docs only — no `src/` or `tests/` changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice S3.7 (build manifest) — plan step 3b (
docs/architecture/0002-execution-plan.md§3, §3b, §5.3), RFC 1 §5.1, §5.2, §5.3.Generalizes
ClassLocatorinto a kind-agnosticSymbolLocatorand folds inautoload.files, so S3.8 can addlookupFunction/lookupConstantover a finished interface.What's here
SymbolLocator::locate(QualifiedName, NameKind): ?string(src/Knowledge/) — one entry for all three of PHP's symbol namespaces. The kind is a parameter rather than a method per kind, keeping the backends × kinds cross-product closed (§5.6). It is not redundant with the name:QualifiedNameis deliberately kind-neutral, andFoo\barnames a different symbol as a function than as a constant.ComposerSymbolLocator(src/Index/) — the two halves work differently because Composer's data does. Class-likes have a name→file map (PSR-4/PSR-0/classmap), so a lookup is arithmetic on the name and reads no file. Functions and constants have no such map and never can, so a name→file index is derived by parsing theautoload.filesset — explicit and usually tiny, which is what makes it affordable (§3).DeclarationScanner/FileDeclarations— reads the functions and constants a parsed file declares, using thenamespacedNamethatNameResolveralready sets inParserService, rather than tracking namespace statements by hand. Constant reach coversconstdeclarations and literal-namedefine(); a computed-namedefine()is a runtime call invisible to a static parse and is out of scope (§3b), which a test asserts rather than leaving to prose.QualifiedName(src/Domain/) — the kind-neutral base FQN value type from §5.3.Lazy index, warmed at
initialized— the index builds on the first function/constant lookup, so nothing depends on startup ordering and §1's lazy-first principle holds.KnowledgeWarmer(anInitializedListener) also triggers the build so the cost lands before the first keystroke;Cache\Warmablestates that this is latency only and an unwarmed implementation must answer identically. Per-file scans sit behind the PSR-16 seam keyed by path, andFilesystemBackend::invalidate()forwards to the locator, so an external change to anautoload.filesentry re-reads exactly that file (§5.2, §5.3).ClassLocator/ComposerClassLocatorare removed; all eight of their test cases were ported toComposerSymbolLocatorTestfirst.Acceptance criteria
ClassLocatoris generalized to a kind-agnosticSymbolLocatorautoload.filesis folded in — read byComposerAutoloadMap, split bypartitionByVendorDirectory, and indexed by the locatorlocateanswers definitively for all three kinds; absence is a barenull(§5.3)constand literal-namedefine(); computed-namedefine()is out of scope and tested as suchFilesystemBackendandKnowledgeStackresolve through the new seam; the superseded locator is goneNotes
build-manifest.md: it scheduledQualifiedNamefor S3.8, but the kind-agnostic signature makes S3.7 its first caller, so it lands here.NameKindalready existed insrc/Resolution/.FunctionName/ConstantNamestill have no caller and remain S3.8's.SymbolSource::locate(QualifiedName, NameKind): ?SymbolDefinitionfor the workspace scope. That is a different layer (aSymbolDefinitionon the read interface) from this one (a file path below the backend). Followed the manifest'sSymbolLocatornaming verbatim; worth a look if the collision reads badly.ServerTestcounted from zero, so they now measure a startup baseline instead of hard-coding one — a change to the fixture project'sfilesautoload no longer silently moves a budget it has nothing to do with.autoload.filesset is 12 files / 139 KB, ~35 ms at §8.1's measured 4 MB/s.tests/Fixtures/AutoloadFiles/{globals,helpers}.phpare outsidesrc/and reached only through thefilesautoload, so they are absent from the PSR-4 corpus and move no golden.tests/Fixturesneeds acomposer dump-autoload(orinstall) to pick them up.Candidate closes (pending review verification): the manifest lists #239 / #181 / #317 as landing somewhere in S3.7–S3.10; #181 (
autoload.filesreach) is the plausible one here, but I have not read the issues, so none are wired.🤖 Generated with Claude Code