Skip to content

v0.1.1#5

Merged
math3usmartins merged 15 commits into
mainfrom
0.1.x
Jun 3, 2026
Merged

v0.1.1#5
math3usmartins merged 15 commits into
mainfrom
0.1.x

Conversation

@math3usmartins

Copy link
Copy Markdown
Member

No description provided.

math3usmartins and others added 15 commits June 3, 2026 08:55
A duplicate template declaration was only flagged on the second-seen file, and
the pull provider forces the edited file first -- so it was treated as canonical
and the duplicate never surfaced on the file the editor was looking at.

Flag the duplicate on ALL colliding open declarations via a deterministic
cross-file pass (each diagnostic naming the others), independent of iteration
order. Registry recording is split out (recordDefinitions) and keeps swallowing
the duplicate throw for its first-wins state. Only open files are considered, so
an open file is never flagged against its own on-disk copy.

Flips the @todo duplicate-template Behat scenario green; updates the
WorkspaceAnalyzer test (both files now carry it) and adds a provider test that
pulling either file returns the duplicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GTD on `$users->first()` where `$users: Collection<User>` returned null:
worse-reflection (Path 3) can't reflect the receiver's generic body (`T[]`,
reified `T`), so the method lookup failed.

Resolve it xphp-natively instead, reusing the inlay machinery: GenericResolver
infers the receiver class (resolveMethodDeclarationAt) and a new
FqnIndex::methodLocation locates the ClassMethod name span (open-doc and
filesystem, mapped through ByteOffsetMap). Wired as a new definition path before
the worse-reflection fallback; plain receivers still use Path 3.

Flips the @todo generic-method-jump Behat scenario green; adds a handler unit
test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update features/README -- no @todo scenarios remain; every scenario runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the post-monomorphization argument-type checker (V1, constructors
only) to the remaining call shapes -- instance method calls `$obj->m(…)`,
static calls `C::m(…)`, and free-function calls `fn(…)` -- under a new
`xphp.arg-mismatch` diagnostic code. The constructor surface keeps its
`xphp.ctor-arg-mismatch` code and behaviour byte-for-byte.

Inference stays conservative and adds "simple locals": a `$var` assigned
exactly once, before use, in the same function scope from a literal or
`new` flows into both argument typing and method-call receiver typing
(`$users = new Collection<User>(); $users->add(42);`). Reassigned,
forward-assigned, or non-inferable variables are skipped -- no false
positives. A known scalar argument against a class-typed parameter is now
a definite mismatch (a PHP scalar never satisfies a class hint).

ConstructorArgumentChecker is renamed/generalised to CallArgumentChecker,
reusing its existing renderType/isSatisfied/substitution machinery over
the in-pass `$files` ASTs (no per-URI resolver), so the workspace pass
stays self-contained and unit-testable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single workspace pass already computes diagnostics for every open
document, but the provider previously returned only the linted file's --
so editing `Box.xphp` left a stale `Use.xphp` until the user touched it.

The provider now folds the per-file syntax + cross-file passes into one
`computeAllOpenDiagnostics()` that yields a per-URI map, and on the push
path pushes `textDocument/publishDiagnostics` for every OTHER open
document whose diagnostics changed since it was last published. A per-URI
signature guard avoids re-publishing unchanged dependents on edit storms;
the linted document itself is left to the engine. No dependency index is
needed -- the pass already visits the whole open set. The ClientApi is
injected (nullable) so pull-mode / unit contexts skip the broadcast.

Behat drives the real push path deterministically: the harness builds the
tester with a 0ms debounce, starts the diagnostics engine service, edits
a dependency, and pumps the cooperative loop until the dependent's
re-publish lands in the transmitter. Unit tests pin the precise push
accounting (broadcast fires, unchanged dependents aren't re-published,
the linted doc isn't self-broadcast) against a real ClientApi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`xphp.bound` diagnostics were dead-ends -- no fix offered. They now carry
a structured `data` payload (added to the neutral Diagnostic + plumbed
through DiagnosticTranslator into LSP `Diagnostic.data`, which clients
round-trip on codeAction). The WorkspaceAnalyzer computes it at the point
of the violation, where the full context is available: which type-param /
bound was violated (via Registry::definition + the hierarchy), the
offending concrete type, the source range of the offending type-argument
(generic clauses strip to equal-length whitespace, so offsets are 1:1
with the original text), workspace types that satisfy the bound, and --
for an editable open class -- where to add an `implements` clause.

A new data-driven BoundErrorCodeActionProvider turns those facts into two
quick-fixes: "Change type argument to <Candidate>" (one per bound-
satisfying workspace type, works for scalar concretes too) and "Add
implements \Bound to <Concrete>" (a cross-file edit, when the concrete is
an editable open class). Registered alongside the existing code-action
providers.

Behat pulls the real diagnostic and feeds it back through codeAction so
the data round-trip is exercised end-to-end; unit tests drive the whole
analyzer -> translator -> provider chain incl. the multi-type-param case
(the fix targets the correct argument).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two long-standing gaps in the semantic-token emitter, called out in the
class docblock:

Non-ASCII (the real fix): emit() reported token lengths in BYTES, but LSP
measures them in UTF-16 code units. A token covering a multi-byte char
(e.g. a "café" string literal) came out one unit too long, so the
highlight over-ran. emit() now converts the covered byte span to its
UTF-16 length via the same conversion offsetToPosition already uses
(exposed as PositionMap::lengthInUtf16). For the all-ASCII common case
this is a no-op, so existing behaviour is unchanged.

Interpolation: already handled -- PHP's tokenizer decomposes a
double-quoted "… $x …" into literal T_ENCAPSED_AND_WHITESPACE slabs and
the inner T_VARIABLE, which the token pass classifies as `string` and
`variable` respectively, so the variable highlights inside the string.
The class docblock claimed it was emitted "as a single span"; that was
stale. This corrects the docblock and locks the behaviour with a test +
acceptance scenario.

Unit tests pin the UTF-16 length (a `"café"` literal is 6 units not 7
bytes; a 4-byte emoji counts as 2 surrogate units) and the interpolation
decomposition; a Behat scenario covers the interpolated-variable case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
documentHighlight reported every occurrence as DocumentHighlightKind::TEXT,
so clients that colour a symbol's definition / writes differently from its
reads (e.g. VS Code) couldn't. Each occurrence is now classified via a
parent-aware AST walk (DocumentHighlightKindResolver): a declaration name
or an lvalue (assignment LHS, foreach target, ++/--, property/static-
property write) is WRITE; every use site is READ.

Location ranges are in original-source coordinates while AST offsets are
in the xphp-stripped source, so the resolver maps stripped->original via
the document's ByteOffsetMap to line the keys up; unresolved occurrences
default to READ so a highlight is never dropped.

Unit tests cover the lvalue rules (variable read/write, foreach, inc,
property write vs read, declaration vs use); the handler test asserts the
class declaration is a WRITE and its two uses are READs; a Behat scenario
covers the same end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an authoritative FQN -> list-of-declaring-records map
(`filesystemDecls`) alongside the existing single-value maps, plus a
`nearestDecl()` selector that picks the declaration nearest a requesting
document (longest shared directory prefix; ties and a null origin fall
back to shortest-path-then-alphabetical).

Resolution methods (pathFor, classLikeFor, functionFor, locationForFqn,
methodLocation, boundsForGenericClass, locationByShortName) gain an
optional `?string $origin = null`. With origin null they reduce to the
prior global tiebreak, so this is behavior-identical until callers start
threading the requesting URI (next phase) -- the full suite (914) and all
FqnIndex tests stay green. New tests lock the proximity selection:
duplicate FQNs resolve to the requester's own package; null origin keeps
shortest-path; locationForFqn returns the near copy's position; open docs
still win over any on-disk copy.

This is the groundwork for indexing fixture trees without reintroducing
the wrong-file go-to-definition the test/fixture skip prevents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a per-request origin anchor on FqnIndex (withOrigin/currentOrigin)
and an OriginTrackingMiddleware that sets it from each request's
textDocument.uri (clearing it for workspace-wide requests). selectDecl
consults the anchor when no explicit origin is passed.

This is how proximity reaches the layers that have no document context:
the static GenericResolver inference chain and the worse-reflection
SourceCodeLocator both call origin-less pathFor/classLikeFor, which now
fall back to the anchor -- so duplicate-FQN resolution becomes
proximity-aware end-to-end without threading an origin parameter through
every resolver and call site. The explicit ?origin params from the prior
commit remain as direct overrides.

Safe because requests dispatch one-at-a-time on the loop and the xphp
handlers resolve synchronously; the async diagnostics path doesn't use
anchor-based resolution. Middleware unit tests cover set-from-request,
clear-on-workspace-request, notifications, and chain delegation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bound-check hierarchy previously absorbed every warmed filesystem
file. Once fixture trees are indexed, the same FQN is declared in many
packages, and TypeHierarchy::fromAstPerFile keys ancestry by FQN -- so
duplicate declarations collide and a class's ancestry could come from the
wrong copy.

A filesystem file now enters the hierarchy only when it is the NEAREST
declarer (to the document being linted) of a class it defines --
`pathFor(fqn, currentUri)` is the proximity oracle, and open buffers win.
Pure usage sites (no class declaration) contribute no ancestry and are
skipped. Each FQN is therefore single-sourced from its nearest definition.

A new test pins both directions: a `Box<Tag>` instantiation in pkgA
(whose Tag implements \Stringable) is clean, while the identical
instantiation in pkgB (whose same-FQN Tag does not) is flagged -- the
hierarchy used each file's local Tag. Exposes WorkspaceAnalyzer::classFqnsIn
for the provider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With proximity-aware resolution in place, the blunt `test/fixture` /
`tests/fixtures` walk exclusion is no longer needed -- duplicate FQNs are
now disambiguated by nearness to the requesting document instead of by
hiding whole subtrees. Remove SKIP_NESTED and its iterator() filter
branch (SKIP_DIRS for .git/vendor/node_modules/var/build stays).

This is what fixes the reported prod gap: editing a file inside a fixture
tree (e.g. the xphp compiler's own test/fixture/compile/**) now sees its
closed sibling declarations -- go-to-definition resolves Box/Tag without
opening them, and the bound check no longer fires the spurious
"not in the source set" error.

The two tests that locked the skip now assert the inverse: fixture-tree
declarations are indexed and resolve to the copy nearest the requester.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spell out what the acceptance suite intentionally does NOT cover and why:
the harness runs with no rootUri (100% in-memory, no disk, parallel-safe),
so the filesystem layer -- the FQN index, proximity-aware resolution + the
origin middleware, closed-file GTD/hover, the warmers, watcher
invalidation -- plus non-ASCII semantic-token length are unit-tested
rather than driven here. Also notes the in-memory-drivable gaps left
unscripted (lifecycle notifications, codeAction/resolve round-trip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`new X(...)` is a call to the constructor, but the source text is the
class name, never `__construct` -- so the reference finder's member-name
match missed every instantiation. Find Usages, the "Show references" code
lens, and document-highlight on a `__construct` declaration therefore
reported no instantiation sites.

The method-target collector now also matches `New_` nodes whose
instantiated class is (or inherits the constructor from) the target
class, yielding the class-name token as a constructor reference. The
filesystem pre-filter learns the class short name for `__construct`
targets so files that say `new X()` (and never `__construct`) aren't
skipped.

Rename is unaffected: RenameProvider already drops any reference whose
covered text differs from the member name, so renaming `__construct`
never touches the `X` in `new X()`. (Go-to-definition on `new X()` still
lands on the class, matching PhpStorm's native behavior.)

Covered by unit tests (instantiations counted across files; the reference
spans the class-name token) and Behat scenarios for both Find Usages and
the constructor code-lens count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three roadmap items shipped on this branch but were still listed as
pending: argument-type checker V2 and cross-file diagnostic broadcast
(Planned) and bound-error fix-its (Exploratory). Move them out of those
sections, refresh the Shipped lane, and add a "Recently shipped" record
that also notes the refinements that landed (proximity-aware FQN
resolution + fixture indexing, constructor usages of `new X()`,
semantic-token interpolation/non-ASCII, document-highlight read/write).
Mirror the same changes in the README overview mindmap.

Planned now holds exactly the still-pending small capabilities
(prepareRename, selectionRange, documentLink).

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