Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9cb96c3
build(deps): target xphp 0.2.x
math3usmartins Jun 7, 2026
7c4fdce
refactor(bounds): read type-param bounds through a BoundExpr view
math3usmartins Jun 7, 2026
f9da2be
feat(generics): recognise the turbofish call-site syntax
math3usmartins Jun 7, 2026
4205413
test(generics): migrate fixtures to the turbofish call syntax
math3usmartins Jun 7, 2026
e076851
test(generics): cover signature help and harden mutation for turbofish
math3usmartins Jun 7, 2026
ef8986c
feat(bounds): understand composite type-parameter bounds
math3usmartins Jun 7, 2026
57cdbf8
feat(bounds): composite-aware bound-violation quick-fixes
math3usmartins Jun 7, 2026
6f7289a
feat(generics): tolerate omitted default type arguments
math3usmartins Jun 7, 2026
8e92ff2
feat(generics): show type-parameter variance in hover
math3usmartins Jun 7, 2026
920ba0f
test(generics): cover self/static/parent turbofish
math3usmartins Jun 7, 2026
6892f69
test(generics): cover instance and variable turbofish argument checks
math3usmartins Jun 7, 2026
89c5110
feat(generics): highlight generic closures and arrow functions
math3usmartins Jun 7, 2026
c7424ef
fix(generics): correct turbofish highlighting, arity, and bound fix-its
math3usmartins Jun 7, 2026
20ec673
fix(semantic-tokens): don't treat a bareword comparison as a generic …
math3usmartins Jun 7, 2026
cd3815b
test(generics): cover over-supplied turbofish and parent-satisfied bo…
math3usmartins Jun 7, 2026
90dfdec
docs: correct diagnostics inventory, lint path, and turbofish examples
math3usmartins Jun 7, 2026
e43ab8f
fix(lsp): report serverInfo version 0.2.0
math3usmartins Jun 7, 2026
befb012
perf(positionmap): memoize PositionMap per (uri, version)
math3usmartins Jun 7, 2026
4042452
fix(codelens): make "Show references" lens work in VS Code and PhpStorm
math3usmartins Jun 11, 2026
42b8e9a
fix(generics): resolve method turbofish and static/self returns at in…
math3usmartins Jun 11, 2026
c1002c3
build(deps): pin xphp-lang/xphp to released v0.2.0
math3usmartins Jun 12, 2026
a9fe0d1
test(diagnostics): don't assume alphabetical filesystem iteration order
math3usmartins Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ For LSP-client developers wiring this server into a non-bundled editor:
- `inlayHintProvider`
- `codeActionProvider` with `resolveProvider: true`
- `codeLensProvider` with `resolveProvider: true`
- `executeCommandProvider` advertising `xphp.showReferences` (the
"Show references" CodeLens command) -- advertised by default so
PhpStorm renders the lens as clickable; suppressed when the client
sends `initializationOptions: {advertiseCodeLensCommand: false}`
(VS Code does, to avoid its forwarder shadowing the client handler)
- `callHierarchyProvider`, `typeHierarchyProvider`
- `executeCommandProvider` for `xphp.showReferences` (no-op server-
side; both clients dispatch `editor.action.showReferences` directly)
- `semanticTokensProvider` (full file; standard LSP-spec token
legend including `typeParameter`)
- Pull-mode `diagnosticProvider`
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ The server reuses the parent `xphp` package's AST, generic-instantiation
`Registry`, and `TypeHierarchy` directly -- no second parser, no duplicated
language semantics.

Targets **xphp 0.2.x**, including the turbofish call-site syntax
(`new Box::<T>()`, `Foo::method::<T>(...)`), variance markers, default type
arguments, and composite (intersection / union) bounds.

For the public-facing feature inventory plus what's planned next, see
[roadmap](docs/roadmap.md).

Expand All @@ -37,7 +41,7 @@ make build/phar # → var/xphp-lsp.phar

The PHAR is the distribution format for editor integrations bundle --
zero-config install for editors that can't reasonably depend on a
Composer-managed working tree.
Composer-managed working tree.

---

Expand Down Expand Up @@ -94,5 +98,5 @@ mindmap

## See also

- [detailed list of features](docs/features/index.md)
- [detailed list of features](docs/features/index.md)
- [roadmap](./docs/roadmap.md)
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"phpactor/language-server": "^6.0",
"phpactor/language-server-protocol": "^3.5",
"phpactor/worse-reflection": "^0.6.0",
"xphp-lang/xphp": "^0.1.0"
"xphp-lang/xphp": "^v0.2.0"
},
"require-dev": {
"phpunit/phpunit": "^13.0"
Expand Down
13 changes: 7 additions & 6 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 72 additions & 14 deletions docs/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ through xphp generics: if `$users` is declared as `Collection<User>`
and the cursor sits on `$users->first()`, the jump lands on the
correct `User` method, not on the template's placeholder `T`. Union
and intersection receivers fan out to a per-constituent picker so
each branch is reachable individually.
each branch is reachable individually. The turbofish forms of the
`self` / `static` / `parent` pseudo-types (`new self::<T>()`,
`self::method::<T>(...)`) navigate and highlight like any other call
site.

### Go to Type Definition

Expand Down Expand Up @@ -151,17 +154,32 @@ round-trip so cursor movement stays responsive. Currently offered:
- **"Did you mean `null` / `true` / `false`?"** typo fixes attached
to `UndefinedName` diagnostics, using Levenshtein distance against
the small set of constants frequently misspelled as a bareword.
- **Bound-violation fixes** -- on a `Generic bound violated`
diagnostic: "Change type argument to `<Candidate>`" (one per
workspace type that satisfies the whole bound) and, for an
intersection or single-leaf bound, "Add implements `\Leaf` to
`<Concrete>`" once per leaf the concrete class is missing. Union
bounds offer only the swap (implementing any one leaf is ambiguous).

### Code Lens

LSP methods: `textDocument/codeLens`, `codeLens/resolve`.

"Show references" lens above every class / interface / trait / enum
/ function / method declaration. The resolve step fills in a lazy
reference count; clicking the lens opens a chooser popup
(`editor.action.showReferences`) -- natively in VS Code, dispatched
client-side by the PhpStorm plugin so the popup anchors at the lens
position rather than the caret.
reference count; the lens carries a namespaced `xphp.showReferences`
command (with the locations baked in) that each client handles
client-side -- VS Code via a wrapper command that forwards to its
built-in references peek, the PhpStorm plugin via a usage chooser
anchored at the lens position rather than the caret.

The command is advertised in `executeCommandProvider` by default --
PhpStorm's LSP API only renders a CodeLens as clickable when its
command is advertised. VS Code instead auto-registers a forwarding
command for every advertised command (which would shadow its own
client-side handler), so the VS Code extension opts out via
`initializationOptions: {advertiseCodeLensCommand: false}` and the
server then omits it.

---

Expand All @@ -180,14 +198,20 @@ property / native function info, hover renders:
- Generic `T` resolved to the concrete type, including through
property fetches (`$item = $box->item` where `$box: Box<Tag>`
shows `Tag`, not `T`).
- A type parameter's full upper bound, including composite forms --
intersection (`A & B`), union (`A | B`), and F-bounded
(`Comparable<T>`).
- A type parameter's variance: `+T` (covariant) / `-T` (contravariant)
are shown with their marker and a label; invariant params show the
bare name.

### Signature Help

LSP method: `textDocument/signatureHelp`.

Inline parameter list with the active argument highlighted. Type-arg
substitution is baked into the rendered signature: a call to
`new Box<Tag>(...)` shows `Tag` rather than `T` in the parameter
`new Box::<Tag>(...)` shows `Tag` rather than `T` in the parameter
hint. Works at static, instance, and free-function call sites.

### Inlay Hints
Expand All @@ -214,15 +238,20 @@ LSP method: `textDocument/semanticTokens/full`.
AST-driven syntax highlighting using the standard LSP token-type
legend. Type-parameter `T` references render with the
`typeParameter` color in generic-syntax positions, distinguishing
them visually from regular class references.
them visually from regular class references. This extends to generic
closures and arrows (`fn<T>(…)`, `function<T>(…)`): the declaration
clause and body-level `T` references inside the closure are coloured
as type parameters.

---

## Validate

Diagnostics surface in both push (`textDocument/publishDiagnostics`)
and pull (`textDocument/diagnostic`, LSP 3.17) modes. Five
diagnostic codes are emitted today:
and pull (`textDocument/diagnostic`, LSP 3.17) modes. Six diagnostic
codes are emitted today: `xphp.parse`, `xphp.bound`, `xphp.definition`
(duplicate template), `xphp.undefined-name`, `xphp.ctor-arg-mismatch`,
and `xphp.arg-mismatch`.

### Parse errors

Expand All @@ -236,11 +265,25 @@ file.

Compile-time validation of `T: Bound` against each concrete
type-arg. The hierarchy spans the whole project on disk (not just
open buffers), so `new Box<Tag>(...)` resolves correctly even when
open buffers), so `new Box::<Tag>(...)` resolves correctly even when
`Tag.xphp` isn't currently open in the editor. Error messages
reference the source-level instantiation (e.g. `Box<int>`) rather
than the hashed specialization name.

### Default type arguments (no false missing-arg)

Not a diagnostic code of its own -- this is how the bound and
argument-type checks treat omitted defaults. A generic with trailing
defaults (`class Box<T = \stdClass>`, `class Pair<A, B = A>`) may be
instantiated with the defaulted args omitted (`new Box::<>()`,
`new Pair::<Dog>(...)`). The argument-type checker resolves the
effective type for each omitted slot left-to-right (so `B = A` picks up
the supplied `A`) and never reports a false "missing type argument",
while still substituting the effective type into method parameter
checks. (An empty turbofish on a template with a non-defaulted
parameter is still reported -- as `xphp.bound` -- since the
instantiation is genuinely incomplete.)

### Duplicate template declarations

Fires when two files declare the same generic class / interface /
Expand All @@ -257,7 +300,7 @@ are fixable in one keystroke.

### Constructor argument-type mismatch (`xphp.ctor-arg-mismatch`)

Post-monomorphization check on `new C(...)` and `new C<T>(...)`
Post-monomorphization check on `new C(...)` and `new C::<T>(...)`
call sites. Catches the case where the supplied argument's
statically-known type can't satisfy the constructor parameter's
declared type -- a runtime `TypeError` waiting to happen, surfaced
Expand All @@ -266,6 +309,18 @@ at compile time. Inference is intentionally narrow (literals,
avoid false positives on arguments whose type would require flow
analysis to know.

### Argument-type mismatch (`xphp.arg-mismatch`)

The same narrow-inference check, extended beyond constructors to
method calls (`$obj->m(...)`), static calls (`Cls::m(...)`), and free
functions (`freeFn(...)`). Type-argument turbofish is honoured: an
instance-method turbofish (`$obj->m::<T>(...)`) binds its type
argument for the check. Cases that would require flow analysis are
conservatively skipped rather than guessed -- a variable turbofish
(`$f::<T>(...)`) over an unknown callee, and an over-supplied
type-argument list (more args than the template declares), produce no
mismatch.

---

## Find
Expand All @@ -276,9 +331,12 @@ LSP method: `textDocument/completion`.

Context-aware completion in every meaningful position:

- **Type-arg position** (`new Box<|>(...)`) -- bound-aware
- **Type-arg position** (`new Box::<|>(...)`) -- bound-aware
filtering hides candidates that don't satisfy the slot's declared
upper bound; scalars are dropped when the bound is class-like.
Composite bounds are respected: a candidate must satisfy **every**
leaf of an intersection (`T : A & B`) and **any** leaf of a union
(`T : A | B`).
- **Member access** (`$obj->`) and **static access** (`Cls::`) --
methods, properties, and constants from the receiver.
- **Static property access** (`Cls::$`) -- a distinct context kind
Expand Down Expand Up @@ -342,7 +400,7 @@ is paid once per machine, not once per session.
### Tolerant-parse fallback

In-memory locators recover from trailing parse errors so mid-edit
source (`$x->|`, `new Foo<|`) still returns useful completion /
source (`$x->|`, `new Foo::<|`) still returns useful completion /
hover / GTD results. Without this fallback, every incomplete
keystroke would temporarily break the editor's intelligence and
force the developer to wait for the source to be syntactically
Expand Down Expand Up @@ -370,7 +428,7 @@ navigation lands on the production declaration by default.
CI-friendly entry point that doesn't require an LSP client:

```bash
tools/lsp/bin/xphp-lsp --lint path/to/file.xphp [more.xphp ...]
bin/xphp-lsp --lint path/to/file.xphp [more.xphp ...]
```

Output format is `<file>:<line>:<col>: <severity>: [<code>] <message>`
Expand Down
19 changes: 17 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ timeline
Moved out of Planned / Exploratory since the last revision (exercised by the
test suite; full descriptions to fold into [`README.md`](../README.md#features)):

- **xphp 0.2.x generics** -- the turbofish call-site syntax
(`new Box::<T>()`, `Foo::method::<T>(...)`) is understood across completion,
hover, signature help, semantic tokens, and diagnostics. Composite bounds
(intersection `T : A & B`, union `T : A | B`, and F-bounded `T :
Comparable<T>`) are rendered in hover and respected by type-argument
completion (a candidate must satisfy every leaf of an intersection, any leaf
of a union). Default type arguments (`class Box<T = X>`,
`class Pair<A, B = A>`) may be omitted at a call site without a false
"missing type argument", with the effective type substituted into parameter
checks. Variance markers (`+T` covariant, `-T` contravariant) are shown in
hover. Instance-method turbofish (`$obj->m::<T>(...)`) binds its type
argument for argument checking; variable turbofish over an unknown callee is
conservatively skipped. Generic closures and arrows (`fn<T>(…)`,
`function<T>(…)`) highlight their declaration clause and body-level `T`
references as type parameters.
- **Argument-type checker V2** -- a new `xphp.arg-mismatch` diagnostic extends
the constructor check to `$obj->m(...)`, `Cls::m(...)`, and `freeFn(...)`, with
conservative "simple-locals" inference for `$var` arguments assigned from a
Expand Down Expand Up @@ -121,7 +136,7 @@ settling those is a prerequisite to any implementation work.

### Lowering preview -- "show me the generated PHP"

**What it'd do.** A code lens or peek-window above any `new Foo<X>(...)` site
**What it'd do.** A code lens or peek-window above any `new Foo::<X>(...)` site
that opens the generated PHP for that specialization, side-by-side with the
source. Same affordance for generic method calls.

Expand Down Expand Up @@ -171,7 +186,7 @@ unifying.
### Instantiation inlay hints -- show the specialized FQN inline

**What it'd do.** Render `// → Box_T_d59a1...` (or a shortened
hash) as an inlay hint at every `new Box<X>(...)` site so the
hash) as an inlay hint at every `new Box::<X>(...)` site so the
specialization a given call resolves to is visible without leaving
the editor.

Expand Down
Loading
Loading