Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
94e0570
lsp(slice 1): semanticTokens/full skeleton + capability + empty handler
math3usmartins May 26, 2026
0f2c6e9
lsp(slice 2): visitor emits PHP-shaped tokens (keywords / vars / stri…
math3usmartins May 26, 2026
d6a7874
lsp(slice 3): typeParameter classification for the 12 xphp generic forms
math3usmartins May 26, 2026
15fd9f8
lsp(slice 3 fix): handler signature matches phpactor's positional-arg…
math3usmartins May 26, 2026
6db6382
lsp(fix 1/5): classify reserved-word identifiers (null/true/false/etc…
math3usmartins May 26, 2026
aeb5073
lsp(fix 2/5): single spec per parameter (no more variable+parameter o…
math3usmartins May 26, 2026
f20614c
lsp(fix 5/5): handlers respect cancellation tokens
math3usmartins May 26, 2026
fa35f1f
lsp(fix 4/5): skip filesystem-index invalidation when changed file is…
math3usmartins May 26, 2026
47a37fa
lsp(fix 3/5): warn on undefined bareword constants (catches `nul`-sty…
math3usmartins May 26, 2026
47a5655
lsp(fix H): per-session hit cache + miss-log dedupe in FilesystemSour…
math3usmartins May 26, 2026
83ab162
lsp(fix D): mid-resolver-chain cancellation polling
math3usmartins May 26, 2026
18d3b3b
lsp(fix I): eager FQN-index warm-up on `initialized` event
math3usmartins May 26, 2026
0ab0ad9
lsp(fix L): short-circuit type-param FQNs before workspace walk
math3usmartins May 26, 2026
c1e7476
lsp: route diagnostic stderr through a test-time mute (unblocks Infec…
math3usmartins May 27, 2026
5921d2e
lsp: kill new-code mutants from fixes H/D/I/L (MSI 78% -> 79%)
math3usmartins May 27, 2026
45ad918
lsp: kill FqnIndex deep-collector mutants (Phase 3, MSI 79% -> 81%)
math3usmartins May 27, 2026
fadd067
lsp: tighten hover-resolver markdown assertions (Phase 4a, MSI 81% ->…
math3usmartins May 27, 2026
83ed1ff
lsp: kill file:// URI mutants in RenameProvider (Phase 4b)
math3usmartins May 27, 2026
6b8c725
lsp: Reflection test for clientSupportsRenameFileOp (Phase 5, MSI 82%…
math3usmartins May 27, 2026
2cfe5e9
lsp: cancel-poll handler tests (Bucket A)
math3usmartins May 27, 2026
d47c67d
lsp: equivalent-mutant ignore sweep (Bucket B, MSI 82% -> 84%)
math3usmartins May 27, 2026
75f634c
lsp: PhpHoverResolver match-arm coverage (Bucket H)
math3usmartins May 27, 2026
c89eee4
lsp: tighten hover-resolver substitution tests (Bucket C)
math3usmartins May 27, 2026
ecbb6f1
lsp: defensive-guard ignores for ReferenceFinder (Buckets D+F)
math3usmartins May 27, 2026
e4b3a81
lsp: FqnIndex empty-needle ReturnRemoval ignores (Bucket G)
math3usmartins May 27, 2026
c91215c
lsp: FqnIndex dedup-set TrueValue ignores (Bucket I)
math3usmartins May 27, 2026
e7f0235
lsp: textDocument/foldingRange (feature 1/7)
math3usmartins May 27, 2026
1ff4759
lsp: textDocument/typeDefinition (feature 2/7)
math3usmartins May 27, 2026
f792763
lsp: textDocument/documentHighlight (feature 3/7)
math3usmartins May 27, 2026
39bdf6d
lsp: completionItem/resolve (feature 4/7)
math3usmartins May 27, 2026
77b955e
lsp: textDocument/signatureHelp (feature 5/7)
math3usmartins May 27, 2026
c382a44
lsp: textDocument/inlayHint (feature 6/7) -- xphp generics demo
math3usmartins May 27, 2026
8f80175
lsp: textDocument/codeAction + codeAction/resolve scaffolding (featur…
math3usmartins May 27, 2026
8b9a4f6
lsp(fix): LspObjectArgumentResolver for non-Params LSP request payloads
math3usmartins May 27, 2026
4f22c4a
lsp(fix): pre-filter non-class type strings before locator
math3usmartins May 27, 2026
04c22ad
lsp(fix): restrict documentHighlight scan to the requesting URI
math3usmartins May 27, 2026
e2c3c40
lsp(fix): suppress locator miss-log for namespaced builtin function refs
math3usmartins May 27, 2026
ae1e292
plugin(fix): claim stub-extraction cache files in isSupportedFile
math3usmartins May 27, 2026
b0b4d65
lsp(fix): handle global constants & interface receivers in def/hover/…
math3usmartins May 28, 2026
d68e05d
lsp: fix wrong SourceNotFound import + drop stale params docblock
math3usmartins May 28, 2026
8cbac16
lsp(refactor): promote isClassFqn and gate every reflectClassLike caller
math3usmartins May 28, 2026
edd6902
lsp(feat): union/intersection receiver navigation (Cycle K, V1)
math3usmartins May 28, 2026
4d0a51f
lsp(feat): union/intersection receiver completion + find-usages (Cycl…
math3usmartins May 28, 2026
0e891f6
lsp(fix): tolerant-parse fallback so in-memory locator survives trail…
math3usmartins May 28, 2026
97bb92b
lsp(feat): codeAction Sprint A -- Import class + Simplify FQN (Cycle B)
math3usmartins May 28, 2026
2512b7a
lsp(feat): durable per-user stub cache root (Cycle D)
math3usmartins May 28, 2026
de5d582
lsp(feat): codeAction Sprint B -- per-diagnostic typo fixes (Cycle E)
math3usmartins May 29, 2026
ec948b6
lsp(feat): codeAction Sprint C -- Optimize Imports (Cycle F)
math3usmartins May 29, 2026
29a1426
lsp(feat): textDocument/codeLens for declarations (Cycle G)
math3usmartins May 29, 2026
e69a0f2
lsp(feat): textDocument/prepareCallHierarchy + incoming/outgoing call…
math3usmartins May 29, 2026
74b5399
lsp(fix): tolerate NameResolver alias conflicts + register xphp.showR…
math3usmartins May 29, 2026
e08b3a6
lsp(fix): restore interface-implementation walks dropped by Cycle C r…
math3usmartins May 29, 2026
9f23273
lsp(feat): post-monomorphization constructor argument-type check (V1)
math3usmartins May 29, 2026
a2ce375
lsp(fix): ctor-arg checker resolves FQNs without NameResolver (prod gap)
math3usmartins May 29, 2026
9920e51
lsp(feat): substitute generic type-args through property fetches
math3usmartins May 29, 2026
9769d22
lsp(fix): preserve the `$` when accepting a variable completion
math3usmartins May 29, 2026
575d33b
lsp(fix): swallow nikic Invalid-position-information from parse-error…
math3usmartins May 29, 2026
a7ce975
lsp(fix): surface static properties on bare `Cls::|` / `$obj::|` comp…
math3usmartins May 29, 2026
2206e92
docs: split roadmap per project + scrub stale internal details
math3usmartins May 29, 2026
f997088
lsp(fix): hover inside `<...>` resolves the type-arg, not the outer c…
math3usmartins May 29, 2026
ce92503
lsp(feat): textDocument/diagnostic (LSP 3.17 pull-mode diagnostics)
math3usmartins May 30, 2026
1446c0d
lsp(feat): textDocument/prepareTypeHierarchy + super/subtypes
math3usmartins May 30, 2026
a879381
lsp(feat): textDocument/implementation (interface implementers / subc…
math3usmartins May 30, 2026
540e93f
lsp(fix): align prepareTypeHierarchy + super/subtypes + diagnostic pa…
math3usmartins May 30, 2026
7429e42
lsp(fix): align prepareCallHierarchy + incoming/outgoingCalls signatu…
math3usmartins May 30, 2026
03dd972
lsp(fix): callHierarchy walker surfaces top-level (script-mode) call …
math3usmartins May 30, 2026
626c9e9
lsp(fix): callHierarchy walks filesystem-indexed paths, not just open…
math3usmartins May 30, 2026
284dd58
lsp(fix): codeLens "Show references" actually opens the references panel
math3usmartins May 30, 2026
977dd36
phpstorm-plugin: handle editor.action.showReferences command client-side
math3usmartins May 30, 2026
4aca03d
phpstorm-plugin: codeLens chooser popup for multi-location usages
math3usmartins May 30, 2026
2693778
lsp(perf): lazy codeLens via codeLens/resolve, cold emission ~10ms
math3usmartins May 30, 2026
57e37d1
lsp(fix): codeLens unresolved-emission shape works for PhpStorm/LSP4IJ
math3usmartins May 30, 2026
45c894a
phpstorm-plugin(fix): parse uri arg through Gson, not raw String cast
math3usmartins May 30, 2026
1df99af
lsp(perf): warm ParsedDocumentCache + short-name pre-filter for cold …
math3usmartins May 30, 2026
3d05562
phpstorm-plugin(fix): anchor "Show references" popup at lens position
math3usmartins May 30, 2026
c7c1852
lsp+plugin(feat): PSR-4 class <-> filename rename sync (Cycle L)
math3usmartins May 30, 2026
ba3f52e
lsp+plugin(fix): Cycle L prod-test fallout — drop RenameFile op, fix …
math3usmartins May 30, 2026
4c7ae16
phpstorm-plugin(fix): restore XphpFileRenameListener — PhpStorm doesn…
math3usmartins May 30, 2026
61f536f
phpstorm-plugin(fix): apply willRenameFiles edits on EDT via invokeLater
math3usmartins May 30, 2026
699c14c
phpstorm-plugin(feat): Cycle L Half A file rename — class rename now …
math3usmartins May 30, 2026
ef0c54b
docs: catch up public docs with the LSP + plugin features shipped rec…
math3usmartins May 30, 2026
8ff4dd9
lsp+plugin(feat): PSR-4 namespace move on cross-directory file move (…
math3usmartins May 30, 2026
c7e24f2
phpstorm-plugin(fix): compute VFileMoveEvent newUri from newParent, n…
math3usmartins May 30, 2026
371f075
lsp(fix): NamespaceMoveProvider emits source edit against the URI tha…
math3usmartins May 30, 2026
816c7a0
lsp(fix): sourceFor retries through the file-rename race window
math3usmartins May 30, 2026
8788843
lsp+plugin(refactor): pre-seed workspace via synthetic didOpen, drop …
math3usmartins May 30, 2026
5edc682
playground(fix): use imported short name in Phase3BoundAware type-arg
math3usmartins May 30, 2026
01de607
lsp(diagnostics): use warmed cache to enrich bound-check hierarchy
math3usmartins May 30, 2026
eae56e3
lsp(diagnostics): register filesystem-only template definitions for b…
math3usmartins May 30, 2026
80c96f2
lsp(completion): scope-aware insertText for class-name completion
math3usmartins May 30, 2026
b7745e5
docs: several changes as prep for MVP
math3usmartins May 30, 2026
d6e9790
phpstorm-plugin(ci): narrow pluginVerification matrix to PhpStorm-only
math3usmartins May 31, 2026
50b6d1d
docs: several changes as prep for MVP
math3usmartins May 31, 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
151 changes: 79 additions & 72 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,45 @@

## Monorepo layout

The repository hosts the xphp language plus the tooling that grows around it.
Every shippable artifact lives in its own sub-project with its own build
system, lockfile, tests, and CI workflow. The PHP core compiler lives under
`core/`; satellites (LSP, PhpStorm plugin, etc.) live under `tools/<name>/`.
The repository hosts the `xphp` language plus the tooling that grows around it.

Every shippable artifact lives in its own sub-project with its own build system,
lockfile, tests, and CI workflow. The xphp compiler lives under `core/`;
satellites (LSP, PhpStorm plugin, etc.) live under `tools/<name>/`.

```
xphp-lang/
+-- core/ # the PHP compiler package
| +-- src/, test/, bin/, composer.json
| `-- Makefile, infection.json5, phpunit.xml.dist
+-- docs/ # language-level documentation
+-- playground/ # demo workspace that depends on the core
+-- core/ # the xphp compiler
+-- docs/ # language-level documentation
+-- playground/ # demo workspace that depends on the core
+-- tools/
| +-- lsp/ # Language Server (PHP, phpactor/language-server)
| +-- phpstorm-plugin/ # JetBrains plugin (Kotlin + Gradle)
| `-- vscode-extension/ # VS Code client (TypeScript)
| +-- lsp/ # Language Server (PHP, phpactor/language-server)
| +-- phpstorm-plugin/ # JetBrains plugin (Kotlin + Gradle)
| `-- vscode-extension/ # VS Code client (TypeScript)
+-- .github/workflows/
| +-- ci-core.yml # phpunit + infection for core/
| `-- ci-<package>.yml # one file per package under tools/
+-- docker-compose.yml # shared dev stack
`-- README.md, CONTRIBUTING.md # repo-level docs
| +-- ci-core.yml # phpunit + infection for core/
` -- ci-<package>.yml # one file per package under tools/
```

### What goes where

| Concern | Lives at |
|------------------------------------------|---------------|
| Compiler source + tests | `core/src/`, `core/test/` |
| Language documentation (generics, roadmap, comparison) | `docs/` (root) |
| Demo / acceptance harness | `playground/` |
| Anything that *uses* the compiler externally | `tools/<name>/` |
| Per-tool documentation | `tools/<name>/README.md` |
| Shared dev tooling (`.docker/`, `.github/`, compose files) | root |

The split is a single principle in disguise: **the core compiler is the
product; everything else is a way to consume it**. A package that depends on
the core's published behavior (LSP analyzing `.xphp` source, an editor plugin
spawning the LSP, a CI lint tool calling `bin/xphp`) is a tool. The core
itself never depends on tools.
itself never depends on external `xphp` tools.

### Adding a new package

The current shape was settled when we added `tools/lsp/` and validated when
we added `tools/phpstorm-plugin/` (Kotlin + Gradle, an entirely different
toolchain than the LSP's PHP + Composer -- both fit the same per-package
shape, which is the proof the convention generalises). To add a fourth
sibling:
The current shape was settled when `tools/lsp/` was a dded and validated when
through `tools/phpstorm-plugin/`.

To add a tool:

1. **Create the directory** under `tools/<name>/`. Pick a name that names the
thing concretely (`lsp`, `phpstorm-plugin`) rather than abstractly
(`server`, `editor-integration`).

2. **Self-contained build**: each package owns its build system. The LSP has
`tools/lsp/composer.json`; the JetBrains plugin has
`tools/lsp/composer.json`; the PhpStorm plugin has
`tools/phpstorm-plugin/build.gradle.kts` + `gradle.properties` (every
version pin lives there so a single edit propagates to since-build,
IDE target, Kotlin runtime, JVM toolchain). The root never collects
Expand Down Expand Up @@ -108,8 +93,8 @@ We deliberately **do not** path-filter workflows at the `on:` level. GitHub's
branch protection requires named status checks to actually run — a
path-filtered workflow that doesn't trigger on an unrelated change reports
"expected, never received" and blocks the merge. Running every workflow
every time costs a small amount of CI minutes (the jobs are parallel and each
finishes in ~30s); the alternative is a coordination tax with sharp edges.
every time costs a small amount of CI minutes (the jobs are parallel); the
alternative is a coordination tax with sharp edges.

If CI minutes ever become a real concern, the fix is to add job-level `if:`
guards using `paths-filter` action results, NOT to add `paths:` at the
Expand All @@ -120,65 +105,87 @@ workflow level. Leave required status checks intact.
### Unit tests

```bash
make -C core test/unit
make test/unit
```

Most tests are pure unit tests against `XPHP\Transpiler\Monomorphize\*`. The handful of
**integration tests** compile a fixture under `core/test/fixture/compile/<name>/` end-to-end and
either assert on the emitted text or autoload the result and call into it at runtime. Those
Most tests are pure unit tests against `XPHP\Transpiler\Monomorphize\*`. The
handful of **integration tests** compile a fixture under
`core/test/fixture/compile/<name>/` end-to-end and either assert on the emitted
text or autoload the result and call into it at runtime. Those
runtime tests have one isolation gotcha worth knowing before you add a new one.

#### Cross-fixture class-table collisions

`Registry::generatedFqn` (`core/src/Transpiler/Monomorphize/Registry.php`) names every specialized
class as:
`Registry::generatedFqn` (`core/src/Transpiler/Monomorphize/Registry.php`) names
every specialized class as:

```
XPHP\Generated\<template-FQN> \ T_<sha256(canonical-args)>
└── namespace, mirrors template ─┘ └── hash of args ONLY ──┘
```

The hash covers the **argument list**, not the template's own FQN — the template's FQN is
already encoded in the namespace path. That's deliberate and right for a single compile: it
collapses identical instantiations into one class file.

It bites in tests because **multiple fixtures redeclare the same template path**. Five
fixtures define an `App\Containers\Box<T>` (with subtly different bodies — some have a
constructor, the `generic_interface` one implements `Container<T>`, etc.) and all of them
instantiate it with `App\Models\Plastic`. Same template path + same arg means same generated
FQN: `XPHP\Generated\App\Containers\Box\T_de1e0eaa…`. The on-disk files live in different
per-test work directories, but **PHP's class table is process-wide** — once any integration
test `new`s up that FQN, PHP caches *that* body for the rest of the phpunit run. A
later test loading from a different fixture's work dir gets the stale class.

The failure mode is shape-dependent and the test ordering is randomized, so this surfaces as
an intermittent flake. Two known-good patterns to avoid it:

1. **Reflection-only assertions** when you're verifying the structural contract (an interface
chain, an `implements` link, the type of a property) rather than runtime behavior.
The hash covers the **argument list**, not the template's own FQN — the
template's FQN is already encoded in the namespace path. That's deliberate and
right for a single compile: it collapses identical instantiations into one class
file.

It bites in tests because **multiple fixtures redeclare the same template path
**.
Five fixtures define an `App\Containers\Box<T>` (with subtly different bodies —
some
have a constructor, the `generic_interface` one implements `Container<T>`, etc.)
and
all of them instantiate it with `App\Models\Plastic`. Same template path + same
arg means
same generated FQN: `XPHP\Generated\App\Containers\Box\T_de1e0eaa…`. The on-disk
files live in
different per-test work directories, but **PHP's class table is process-wide** —
once any
integration test `new`s up that FQN, PHP caches *that* body for the rest of the
phpunit run.
A later test loading from a different fixture's work dir gets the stale class.

The failure mode is shape-dependent and the test ordering is randomized, so this
surfaces as an intermittent flake. Two known-good patterns to avoid it:

1. **Reflection-only assertions** when you're verifying the structural
contract (an interface chain, an `implements` link, the type of a property)
rather than runtime behavior.
`ReflectionClass::implementsInterface($marker)`, `::getParentClass()`,
`(new ReflectionProperty($fqn, 'item'))->getType()->getName()` all read the class metadata
without depending on which body PHP's class table happens to hold. **Example:**
`(new ReflectionProperty($fqn, 'item'))->getType()->getName()` all read the
class metadata without depending on which body PHP's class table happens to
hold.
**Example**
`test/Transpiler/Monomorphize/CompilerIntegrationTest.php::testInstanceofAgainstOriginalTemplateMatchesAllSpecializations`.

2. **Fixture-unique concrete types** when you legitimately need `new $fqn(...)` at runtime.
2. **Fixture-unique concrete types** when you legitimately need `new $fqn(...)`
at runtime.
Add a model class that's only declared inside your fixture (e.g.
`test/fixture/compile/generic_interface/source/Models/Polymer.xphp`) and instantiate
against `Box<Polymer>`. Other fixtures don't redefine `Box<Polymer>`, so the hash is
`test/fixture/compile/generic_interface/source/Models/Polymer.xphp`) and
instantiate
against `Box<Polymer>`. Other fixtures don't redefine `Box<Polymer>`, so the
hash is
unique and PHP's class table can't cache a competing body. **Example:**
`test/Transpiler/Monomorphize/GenericInterfaceIntegrationTest.php::testSpecializedClassIsInstanceOfOriginalInterfaceMarker`.

Rule of thumb: if your test does `new $generatedFqn(...)` and the concrete type is `Plastic`,
`Metal`, `User`, `Food`, or any other name that already lives in another fixture, switch to
one of the two patterns above before pushing — random-order CI will catch it eventually, and
the diagnostic (`ArgumentCountError`, `instanceof returns false`) won't point at the root
Rule of thumb: if your test does `new $generatedFqn(...)` and the concrete type
is `Plastic`,
`Metal`, `User`, `Food`, or any other name that already lives in another
fixture, switch to
one of the two patterns above before pushing — random-order CI will catch it
eventually, and
the diagnostic (`ArgumentCountError`, `instanceof returns false`) won't point at
the root
cause.

### Mutation tests

Mutation testing is the headline quality signal -- the test suite isn't just covering lines, it's surviving deliberate
Mutation testing is the headline quality signal -- the test suite isn't just
covering lines, it's surviving deliberate
code perturbations. Run via [Infection](https://infection.github.io/):

The CI workflow runs Infection on every PR and every push to `main`, failing the build if MSI drops below **95%**.
`infection.json5` carries a curated set of per-mutator `ignore` rules for equivalent / cosmetic cases so the report only
The CI workflow runs Infection on every PR and every push to `main`, failing the
build if MSI drops below **95%**.
`infection.json5` carries a curated set of per-mutator `ignore` rules for
equivalent / cosmetic cases so the report only
surfaces genuine test gaps when they appear.
Loading
Loading