Skip to content

0.3.0#13

Merged
math3usmartins merged 26 commits into
mainfrom
0.3.x-dev
Jul 20, 2026
Merged

0.3.0#13
math3usmartins merged 26 commits into
mainfrom
0.3.x-dev

Conversation

@math3usmartins

Copy link
Copy Markdown
Member

v0.3.0 — xphp 0.3.0 support

Editor support for xphp v0.3.0, plus on-save whole-project diagnostics,
cross-project navigation, and hover fixes.

  • xphp 0.3.0 language features: out/in variance, Closure(int $x): bool
    signature types, extended turbofish, method-level generics, new diagnostic codes
  • On-save authoritative diagnostics — runs the compiler's own check() in-process
    to catch call-argument closure conformance and cross-file generic errors the
    live pass can't
  • Multi-root navigation — open a file from a sibling project and its symbols
    resolve for go-to-definition, references, completion, and rename
  • Hover fixes — correct generic method signatures (map(…)) and flow-sensitive
    variable types

math3usmartins and others added 26 commits July 15, 2026 09:04
Track the upstream v0.3.0 compiler release (branch 0.3.x) so the language
server can build against the new generic/variance/closure-signature surface.
Pulls symfony/console v8.x transitively.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 replaces the +T/-T variance glyphs with the Kotlin-style `out`/`in`
contextual keywords. The Variance enum is unchanged, so only the rendered
marker text moves: covariant shows `out T`, contravariant shows `in T`.
Migrates the variance hover tests, the behat scenario, and the feature docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 stopped aliasing Integer/Boolean/Double to scalars; they now
resolve as class names. The LSP already treated them as class atoms
(SCALAR_TYPES holds only lowercase canonical types, and isClassAtom's reserved
list never included the capitalized spellings), so no production change is
needed. Pin the behavior so a future re-aliasing can't silently regress it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the eight 0.3.0 generic-validation codes to the DiagnosticCode vocabulary
and triage the ones Registry::recordInstantiation actually throws in-process:
too-many-type-arguments and missing-type-argument (alongside the existing
collision/bound paths). The other codes (bound-unprovable, undeclared-type,
closure-conformance, unspecialized-generic-closure, unresolved-generic-call,
undetermined-receiver) are appended to the compiler's diagnostics collector or
raised on the Compiler path, so they are vocabulary only until that sink is
wired; they are not triaged here. Each triaged phrase is locked by a unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 changed Specializer::substituteTypeRef's second parameter from a
raw name->TypeRef array to a Substitution value object. Wrap the eight
GenericResolver call sites' $paramMap in Substitution::of(), which is the
vendor's documented adapter for an already-built map. Without this every
generic-substitution path threw a TypeError, breaking hover, completion,
inlay hints, and go-to-definition through generics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the fixtures in line with xphp 0.3.0 and lock the new diagnostics:
- completion no longer offers the dropped `integer` scalar alias (only `int`);
- `new Box::<User, Tag>()` now reports xphp.too_many_type_arguments;
- `new Box::<>()` on a required-param template now reports
  xphp.missing_type_argument (was xphp.bound "no default").

Establishes the don't-break baseline: full unit (1091) and behat (104
scenarios / 653 steps) suites are green against 0.3.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 allows a generic method inherited from a base class to be called
with turbofish on a subclass receiver ($child->m::<int>()). Add an
extends-chain walk (findMethodWithContext) that locates the inherited method
and its declaring context, wired into both the return-type inference and the
hover/inlay signature-substitution paths. Parent names resolve against each
ancestor's own use-map/namespace; a fully-qualified parent is used as-is.

Nullsafe turbofish already resolved (returns a nullable specialized type).
First-class-callable value inference is out of scope (see plan DEFERRED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 lets a generic method be named with a PHP keyword
(function list<T>(), $f->list::<int>()). The parser stamps the generic
attributes normally and the byte turbofish scanner is keyword-agnostic, so
resolution and hover already work; no production change is needed. Pin the
behavior with a resolver unit test and a hover behat scenario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 lets a covariant/contravariant class take its type parameter in a
constructor (e.g. Producer<out T> { __construct(private T $item) }). Variance
is orthogonal to substitution, so construction + a covariant getter already
resolve to the concrete type; pin it with a resolver test. static fn<T> value
inference is deferred (closure-value modeling; see plan DEFERRED).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 introduces `Closure(int $x, string $y): bool` signature
types, which erase to a bare `\Closure` in emitted PHP. The parser
stamps the structured `ClosureSignature` on the `Closure` type node,
so the LSP is the only place the params + return survive for display.

Add a stateless `ClosureSignatureView` that renders that tree to its
source shape (`Closure(int, string): bool`), handling all five SigType
members (leaf, union, intersection, nested closure, raw DNF slice),
`&`/`...` param modifiers, whole-type nullability, and FQ-prefixed
class leaves consistent with the rest of the hover card. Wire it into
XphpHoverHandler as an additive, instanceof-guarded case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…letion

Complete the xphp 0.3.0 closure-signature feature beyond hover.

Diagnostics: a fourth WorkspaceAnalyzer pass drives the upstream
ClosureConformanceValidator in collect mode over each open file, so a
factory whose declared return type is `Closure(...)` and that hands back
a closure literal provably violating that target (return not a subtype,
parameter not wider, wrong arity, by-ref mismatch) is surfaced as an
`xphp.closure_conformance` diagnostic. The check is upstream-owned; the
server only maps it. The sibling `xphp.unspecialized_generic_closure`
code is produced solely inside the specialization pipeline the server
never runs and stays vocabulary-only.

Completion: a new ClosureTypePositionDetector spots a cursor at a type
token inside a `Closure(int $x, string $y): bool` signature (first
param, after a comma, or the return type) and offers class + scalar
names, the same unbounded candidate set a bound-free type-arg slot gets.
Conservative by design -- an ambiguous shape offers nothing rather than
firing in the wrong context (guards `new Closure(`, `Closure::`, etc.).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… bounds

A method-level generic (`function contains<U : E>(U $value): bool`) had
no hover: the type-parameter path only inspected class-level generics,
so hovering `U` fell through to plain PHP reflection and showed nothing
useful.

Capture the enclosing function-like scope alongside the class scope when
resolving the Name under the cursor, and add a method-type-parameter
hover case that reads the method's generic params. It renders
`Type parameter U of App\Box::contains bounded by E` -- the bound may
reference the enclosing class parameter `E`, which the bound view
already renders as a bare identifier. The method binding is innermost,
so a method param shadows a same-named class param.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
xphp 0.3.0 introduces an `xphp.json` project manifest (source roots,
transitive includes, build-output + cache dirs). Add an LSP reader that
reuses the compiler's own pure parser (XPHP\Config\ManifestParser) so the
schema stays in lockstep, wrapped so no failure reaches the caller: an
absent, unreadable, or malformed manifest yields null -- the fallback
signal a resolver uses to keep the single initialize rootPath.

The reader locates a manifest (explicit --config path/dir, or auto-detect
by walking up), and resolves the manifest's relative source roots and
output/cache dirs to absolute paths against the manifest directory. Not
yet wired into indexing; that follows in the multi-root resolution step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the xphp 0.3.0 project manifest into the FQN index. When the
workspace declares an xphp.json, the index walks the manifest's source
roots (which may live outside the editor's root) alongside the workspace
root, so declarations in an external source root still resolve for
go-to-definition, hover, and completion. The manifest's build-output and
generated-class-cache directories are pruned from the walk, so the
specialized PHP the compiler emits is never indexed as source. A file
reachable through more than one root is indexed once. An absent or
malformed manifest falls back to the single workspace root.

The FqnIndex constructor gains two optional, backward-compatible params
(extra source roots + excluded dirs); the reflector remains single-root.

Also harden the closure/manifest/scan unit tests (mid-buffer cursors,
index-0 boundaries, path-resolution edges) and record the residual
equivalent boundary/guard mutants in infection.json5, matching the
convention already used for the sibling scanners.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The server now supports xphp 0.3.0 (out/in variance, Closure signature
types, xphp.json multi-root resolution, and the extended turbofish and
diagnostic surface). Report 0.3.0 in the initialize serverInfo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…::check()

Run the xphp compiler's own whole-project validator (Compiler::check())
in-process on save and merge its diagnostics into the published set. This
surfaces the grounded, call-argument closure-conformance and cross-file
generic errors the tolerant per-keystroke tier structurally cannot produce.

- CompilerCheckRunner drives SourceResolver -> Compiler::check(), maps the
  results to per-URI full-line LSP diagnostics, never throws, and skips (with
  a log line) when the resolved set exceeds a file-count safety cap so a large
  unscoped tree can't block the event loop.
- AuthoritativeDiagnosticsStore + a merge in XphpDiagnosticsProvider keep the
  engine the sole publisher of open docs (append-only, deduped by line+code)
  so the two tiers never clobber each other.
- AuthoritativeDiagnosticsListener runs the check on save, publishes non-open
  source files directly, and stale-clears them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the authoritative check's source set by walking up from the saved
file's own directory to the nearest xphp.json, rather than the single
workspace rootPath. A file now resolves to its own project even in a
multi-root or mis-rooted workspace -- the server tracks only a legacy
rootPath and does not read workspaceFolders, so opening a file from project
B while rooted at project A previously scoped the check to A (or skipped it
on the file-count cap). A file with no ancestor manifest falls back to the
workspace-root resolution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The on-save authoritative diagnostics are computed against the on-disk state
and merged into open documents. When a document was edited after a save --
e.g. commenting out or deleting the offending line -- the stale finding kept
being merged onto the changed buffer until the next save, so an error lingered
on a line the user had already fixed.

Evict a file's authoritative diagnostics on TextDocumentUpdated (any edit) so
they stop being merged the instant the buffer diverges from disk; the engine
re-publishes the changed document on the same event (clearing the squiggle),
and the next save re-checks and re-publishes if the error is still present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opening a document now runs the on-save authoritative check for its project,
so a file's deep diagnostics (closure conformance, missing_type_argument,
cross-file generics) appear immediately on open -- from its last-saved state,
which equals the buffer at open time -- without needing a redundant save.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The server tracks a single workspace rootPath and does not read
workspaceFolders, so opening a file from a project OUTSIDE that root left
navigation broken -- its sources were never indexed. On didOpen, discover the
opened file's own project (nearest xphp.json, walking up) and register its
declared source roots on the existing FqnIndex, which already supports multiple
roots and routes duplicate FQNs by proximity. Go-to-definition, references,
completion and rename now resolve symbols declared in the opened project even
when the workspace is rooted elsewhere.

Keys purely off the opened file's path -- no editor-specific signal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registering an opened project's source roots invalidates the FQN index's
filesystem snapshot, so the next query re-walks in-band (~500ms for a large
project). Warm that walk via Amp\asyncCall right after registration, mirroring
FqnIndexWarmer, so the first navigation into a sibling project isn't cold. The
warm is guarded (only when roots were actually added) and wrapped in a
try/catch so a failure warming a less-trusted sibling path can't reach the loop
error handler -- the roots stay registered and the next query re-walks in-band.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add durable committed fixtures (two sibling projects, app + lib, each with its
own xphp.json) and an end-to-end test asserting the full navigation query
surface -- definition (pathFor), go-to-definition (locationForFqn) and
completion/import (fqnsByShortName) -- resolves the sibling's symbols only
after its file is opened. Complements the temp-dir OpenedProjectIndexerTest by
exercising the wider surface against a realistic rooted-at-A / open-from-B layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hovering a variable reassigned to a different generic type showed the LAST
assignment's type regardless of position -- e.g. hovering
`$x = $list->map::<string>(...)` read as `List<int>` because a later
`$x = $list->map::<int>(...)` overwrote the single per-scope binding. (Surfaced
by multi-root: the sibling collection type was only resolvable at all once its
project was indexed.)

Record each assignment's LHS position in a per-scope history and, at query
time, resolve each variable to the binding from the nearest assignment at or
before the queried offset. The flat per-scope map is retained for during-walk
RHS resolution (accumulated state); only the query path becomes flow-sensitive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hovering a generic method showed the generics-ERASED worse-reflection view --
`map(Closure $fn): ImmutableList<R>` -- dropping the `<R>` method-generic clause
and erasing the `Closure(E $x): R` parameter to bare `Closure`, leaving `R`
orphaned in the return type. Recover the original xphp method AST (via
FqnIndex::classLikeAstFor on the DECLARING class, so inherited methods resolve)
and render its `<R>` clause plus the `Closure(...)` signature params (via
ClosureSignatureView). Falls back to the erased worse-reflection view when no
index/declaration is available; non-generic method hovers are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upstream tagged v0.3.0; move off the 0.3.x-dev branch onto the release. Full
unit (1219) + behat (111 scenarios / 689 steps) suites green against the tagged
version, including the Compiler::check()-driven authoritative-diagnostics tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…od hover

Catch the feature reference up to recently-shipped behavior:
- opening a file from a sibling project (outside the workspace root) folds that
  project's xphp.json source roots into the FQN index on didOpen, so its symbols
  become navigable (go-to-definition / references / completion / rename);
- generic method hovers now render the type-parameter clause + Closure(...)
  signature params, resolving inherited methods against the declaring class;
- variable-type hover is flow-sensitive across reassignments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@math3usmartins
math3usmartins requested a review from a team July 20, 2026 12:05
@math3usmartins
math3usmartins merged commit fcb0cb1 into main Jul 20, 2026
3 checks passed
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.

1 participant