Skip to content

0.2.0#6

Merged
math3usmartins merged 22 commits into
mainfrom
0.2.x
Jun 12, 2026
Merged

0.2.0#6
math3usmartins merged 22 commits into
mainfrom
0.2.x

Conversation

@math3usmartins

Copy link
Copy Markdown
Member

No description provided.

math3usmartins and others added 21 commits June 7, 2026 09:43
Bump the xphp-lang/xphp dependency from ^0.1.0 to the 0.2.x line
(0.2.x-dev), which introduces the turbofish call-site syntax, variance
markers, default type arguments, and composite intersection/union bounds.

The vendor public surface the server binds to is unchanged: the
generic AST attribute constants, the parse*/strip methods, and
ByteOffsetMap are all present, and strip() still blanks generic clauses
to equal-length whitespace so byte offsets remain 1:1.

README documents the targeted xphp version and the new constructs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The parser now models a type parameter's upper bound as a small
expression tree (a single leaf, or an intersection/union of leaves)
instead of a single bound FQN string. Introduce BoundExprView, a
stateless helper that renders a bound for display, flattens its leaf
FQNs, and answers whether a candidate type satisfies it (all leaves for
an intersection, any leaf for a union).

Route the existing bound read sites through it:
- hover renders the bound via the view's display string,
- the workspace analyzer detects violations and filters swap candidates
  via the satisfaction check,
- the FQN index keeps exposing the first leaf FQN for the completion
  bound filter.

Single-leaf bound behavior is unchanged; this is the groundwork for
composite-bound intelligence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0.2.x requires the turbofish `Name::<Args>(...)` at expression-context
generic calls and rejects the old bare-angle form (`new Box<T>()`).
Centralise the duplicated `<…>`-scanning in a new TurbofishScanner and
teach the self-scanning handlers the `::<` opener:

- the type-arg position detector (completion / go-to-definition) and the
  hover handler's angle-clause finder now delegate to the scanner, which
  requires `::` before the opening `<` and reads the receiver name to its
  left (handling `Foo::<`, the empty `Foo::<>`, FQN receivers, and nested
  bare type-args inside an outer turbofish);
- the semantic-token pass keeps the bare-`<` opener for declaration
  clauses (`class Box<T>`, `function f<T>`, which are unchanged in 0.2.x)
  and additionally opens a clause on a `<` preceded by `::`, covering
  static, instance, `static::<…>`, and FQN call sites;
- `$a < $b` and other comparisons still never open a clause.

Docs migrate the call-site generic examples to the turbofish form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 0.2.x parser rejects the old bare-angle call form, so every
call-site generic in the feature specs and unit fixtures moves to the
turbofish (`new Box::<T>()`, `Class::method::<T>(...)`, free-function
`name::<T>(...)`). Declaration clauses (`class Box<T>`, `function f<T>`,
property/return type positions) keep the bare `<` as the language does.

The completion specs that drive an incomplete `new Box::<` cursor have
their step needles bumped to `Box::<` accordingly.

Also route the analyzer's type-argument range finder through the shared
turbofish scanner so the bound-violation "change type argument" quick-fix
resolves its edit range for `::<` call sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signature help already works for turbofish calls without a code change:
strip() blanks the whole `::<…>` clause to equal-length whitespace, so
the cursor offset inside the arg list still maps 1:1 to the stripped
source the AST is built on. Add unit coverage for a turbofish
constructor, a turbofish static call, and active-parameter advance, plus
a behat signature-help scenario over a turbofish constructor.

Strengthen the turbofish scanner, type-arg detector, semantic-token, and
bound-fix tests to pin the exact clause ranges and reject the negatives
(lowercase after `::<`, bare `::`, a closed clause's trailing name), and
record the genuinely-equivalent mutants (defensive byte-range bounds
guards, sealed-instanceof fallthroughs, candidate ordering/cap) as
ignore rules with per-mutant rationale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generic type parameters can now carry composite upper bounds:
intersection (`T : A & B`), union (`T : A | B`), and F-bounded
(`T : Comparable<T>`). Surface them across the editor intelligence:

- the FQN index exposes the full bound expression per slot (alongside
  the existing first-leaf string contract);
- type-argument completion filters candidates against the whole bound --
  a candidate must satisfy every leaf of an intersection and any leaf of
  a union;
- hover renders the full bound, including the recursive F-bounded form.

Docs describe composite-bound completion and hover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework the bound-violation fix data and code actions for composite
bounds. The diagnostic payload now carries the full bound display string
plus the flat leaf list, and one "add implements" insert per leaf the
concrete class is missing:

- "Change type argument to <Candidate>" lists workspace types satisfying
  the whole bound (every leaf of an intersection, any leaf of a union);
- "Add implements \Leaf to <Concrete>" is offered once per missing leaf
  for intersection and single-leaf bounds, and suppressed for union
  bounds where implementing any one leaf would suffice but choosing one
  is ambiguous.

The diagnostic triage is unchanged: composite-bound violations share the
"Generic bound violated" prefix and route to the bound-violation code, as
a new regression test confirms.

Docs describe the composite bound-violation fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A generic with trailing defaults (`class Box<T = X>`,
`class Pair<A, B = A>`) may now be instantiated with the defaulted
arguments omitted (`new Box::<>()`, `new Pair::<Dog>(...)`). The
argument-type checker accepts a call that supplies no more args than
params, pads the missing trailing slots from each parameter's default
(resolving left-to-right so a default that references an earlier
parameter picks up the supplied argument), and substitutes the effective
type into method-parameter checks -- without ever reporting a false
"missing type argument".

A method parameter typed by a type parameter the call site leaves
unresolved (omitted with no default) is skipped rather than resolved to
a non-existent class, removing a latent false positive on under-supplied
generic calls.

Docs describe default type arguments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hovering a type parameter now surfaces its variance: a covariant `+T`
and a contravariant `-T` render with their marker and a "(covariant)" /
"(contravariant)" label, while an invariant parameter shows the bare
name. The variance line composes with the existing bound line.

Docs note variance display in hover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `self` / `static` / `parent` pseudo-type turbofish forms
(`new self::<T>()`, `self::method::<T>(...)`, `static::<T>()`) are
already handled by the call-site `::<` recognition: the clause strips to
equal-length whitespace, the semantic-token pass opens the clause on the
`::`-preceded `<` while keeping the pseudo-type keyword classification,
and go-to-definition on a type argument resolves through the existing
path.

Add semantic-token coverage for self/static/parent turbofish and a
go-to-definition behat scenario over a `new self::<T>()` type argument.
Docs note the pseudo-type turbofish navigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verify the argument-type checker handles the turbofish method-call
shapes. An instance-method turbofish (`$obj->m::<T>(...)`) carries its
type argument on the call node, so the checker binds T and flags a
mismatched argument; a variable turbofish (`$f::<T>(...)`) over an
unknown callee is conservatively skipped to avoid false positives.

Add unit + behat coverage for both shapes. Docs note the behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generic closures and arrows (`fn<T>(…)`, `function<T>(…)`) now
highlight their type parameter. The semantic-token pass opens a
declaration clause on a `<` that follows the `fn` / `function` keyword,
and the type-parameter scope stack — previously tracked only for class
templates — now also pushes a frame for a closure or arrow carrying
type parameters, so body-level `T` references inside the closure
re-classify as type parameters (and a `T` outside it does not).

Anonymous-closure call-argument checking remains a deliberate skip
(anonymous closures aren't in the function index), avoiding false
positives.

Docs note generic-closure semantic-token support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Four correctness fixes for the 0.2.0 turbofish/generics support:

- Semantic tokens: open a call-site turbofish clause on `::<` unconditionally
  so a lowercase scalar first arg (`Box::<int>`, `Map::<int, User>`) no longer
  suppresses highlighting of the whole clause. The `::` already disambiguates
  from the `<` comparison operator, so no uppercase look-ahead is needed.

- Diagnostics: validate an empty turbofish `new Box::<>()`. An empty type-arg
  list was treated as "not a generic instantiation" and skipped; it now reaches
  the registry, which reports the arity error when a parameter has no default.
  The instantiation guard is extracted into a named genericInstantiation()
  helper.

- Argument checking: stop reporting a bogus "expects T" mismatch when a call
  supplies more type arguments than the template declares; every parameter binds
  to an unresolved sentinel so generic-typed method params are skipped rather
  than resolved to a fictional class. Concrete-typed params are still checked.

- Bound quick-fixes: only offer "add implements <Leaf>" for the leaves of a
  composite bound the concrete type does not already satisfy via a parent class
  or a transitively-implemented interface.

Adds unit and behat coverage pinning each fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…declaration

The declaration-clause opener painted any name before `<` followed by an
uppercase identifier as a generic type parameter, so a comparison whose left
operand ends in a bareword -- `Foo::CONST < Bar`, `MY_CONST < Other` -- wrongly
highlighted the compared constant.

A real generic declaration's name is always preceded by a `class` / `interface`
/ `trait` / `function` keyword; require that keyword before opening the clause.
Anonymous closures (`fn<T>` / `function<T>`) and call-site turbofish (`::<`) are
handled by separate branches and are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…und leaf

Add end-to-end behat coverage for two cases previously exercised only by unit
tests:

- An over-supplied turbofish (`new Box::<User, Tag>()`) must not raise a false
  argument-type mismatch on a method typed by the type parameter.
- A composite intersection bound where the concrete satisfies one leaf via a
  parent class must offer an "implement" fix only for the genuinely-missing
  leaf, not the one already satisfied through the hierarchy.

Both scenarios fail against the pre-fix behavior and pass now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Validate section listed "five diagnostic codes" but six are emitted; enumerate
  them and give xphp.arg-mismatch its own section (method/static/free-function
  argument check) instead of folding it into the constructor one. Note that
  default type arguments are checker behavior, not a separate code.
- Fix the --lint command path (bin/xphp-lsp, not tools/lsp/bin/xphp-lsp).
- Update the roadmap's exploratory examples to the turbofish call syntax.
- Point CONTRIBUTING at the real executeCommand name (editor.action.showReferences).
- Strip trailing whitespace in README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The server advertised version 0.1.0 in its initialize response while it targets
xphp 0.2.x, so every client (and the IDE logs) showed a stale version. Align it
with the supported language generation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PositionMap (LSP line/char <-> byte, UTF-16 aware) was rebuilt on every request
at the hot handler sites even when the document was unchanged; its constructor
scans the whole source to index line offsets. Cache it alongside the
version-keyed parse result in ParsedDocumentCache and reuse it from the
open-document handlers that already hold the (uri, version, text) triple.

Behavior is unchanged: a PositionMap is a pure function of its source, the cache
reuses the same (uri, version) invalidation contract as the AST cache, and the
filesystem/aggregated-source sites keep constructing directly. Verified by unit
(incl. byte-parity vs a freshly built map over multibyte/emoji input) and behat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the lens command from the VS Code-internal
`editor.action.showReferences` to a neutral `xphp.showReferences`, and
advertise it in `executeCommandProvider` only when the client wants it.

PhpStorm's LSP API renders a CodeLens as clickable only when its command
is advertised; VS Code's languageclient registers a forwarding command
for every advertised command, which shadows the extension's own
`xphp.showReferences` handler and round-trips clicks to the server. So
advertise by default (PhpStorm, Helix, ...) and let the VS Code extension
opt out via `initializationOptions.advertiseCodeLensCommand: false`.

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

- Bind a generic method's own turbofish args at the call site
  (`$u->identity::<int>(99)`) so the result infers `int` instead of the
  bare type parameter `T`. Mirrors the existing static-call path.
- Track non-generic `new` receivers (empty-paramMap binding) so instance
  calls can resolve their receiver, kept invisible to hover/completion so
  plain objects still defer to worse-reflection.
- Resolve relative return types (`fresh(): static` / `self`) to the
  receiver's concrete type (`Builder<int>`).
- Add Behat coverage (hover + inlay hints) for both behaviors end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move off the `0.2.x-dev` branch dependency now that xphp v0.2.0 is
tagged: constraint `0.2.x-dev` -> `^v0.2.0`, lock to the v0.2.0 tag, and
drop the no-longer-needed dev stability flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@math3usmartins math3usmartins requested a review from a team June 12, 2026 10:29
The fs-skip regression test hardcoded that Other.xphp is walked before
Tag.xphp (`assertSame($unwarmedPath, $walked[0])`). That holds on
tmpfs-alphabetical Linux but not on the CI runner, where readdir returned
Tag first and the assertion failed.

Discover the actual iteration order with a throwaway index, then write the
dropped dummy class into the first-iterated file and Tag into the second
(content defines the FQN, not the filename). This preserves the
break-vs-continue mutant detection (cache-missing path must precede Tag)
regardless of the platform's directory order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@math3usmartins math3usmartins merged commit 11f4064 into main Jun 12, 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