Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions .docker/php.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM php:8.4-cli-alpine

# Composer: copied from the official multi-arch image instead of curl-
# piped install. Reproducible across rebuilds and gets composer 2.x
# without an interactive installer. Both Makefile entry points
# (`make test` and `make test/mutation`) shell out to `composer
# install`, so this needs to be in PATH.
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

RUN apk add --update --no-cache linux-headers
RUN apk add --no-cache \
php84-dev \
build-base \
git \
unzip

# Both coverage drivers are installed. Xdebug runs as the dev-time
# debugger; PCOV is used exclusively for Infection's coverage pass
# (orders of magnitude less memory than Xdebug, which avoids the
# host OOM-killer firing during the initial test run on tight
# containers). Infection auto-detects PCOV when it's loaded and
# Xdebug is disabled via XDEBUG_MODE=off; the mutation Makefile
# target does both.
RUN pecl install \
xdebug \
pcov \
&& docker-php-ext-enable \
xdebug \
pcov
90 changes: 90 additions & 0 deletions .github/workflows/ci-lsp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: LSP CI

# PHPUnit runs on every PR + push to main as a hard gate.
# Infection (mutation testing) is opt-in via workflow_dispatch -- its
# resource envelope outgrew GitHub-hosted `ubuntu-latest` (~7 GB RAM)
# once the suite passed ~150 tests, and the initial coverage run now
# OOMs as SIGTERM 143 mid-stream. Re-enable as a PR gate once the
# job is reshaped to filter / batch the test set (see follow-up).
on:
pull_request:
push:
branches: [main]
workflow_dispatch:

# Per-workflow concurrency. See ci-core.yml for the rationale.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
phpunit-lsp:
name: PHPUnit (LSP)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup PHP 8.4
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: none
tools: composer:v2

# The LSP package has its own composer.json under tools/lsp/ with a
# path-repo pointing back at the repo root. Cache its deps separately
# from the core install so a core-only change doesn't bust the LSP cache.
- name: Install LSP dependencies
uses: ramsey/composer-install@v3

- name: Run PHPUnit (LSP)
run: make test/unit

infection-lsp:
name: Mutation testing (LSP)
# Gate: only run when explicitly requested via the Actions tab. The
# 424-test suite plus per-mutation reruns overruns the GitHub-hosted
# runner's RAM, killing the initial coverage subprocess with SIGTERM
# 143. PRs and merges to main get fast unambiguous signal from the
# PHPUnit job above; Infection scoring is opt-in until the job is
# reshaped to filter / batch the test set.
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
needs: phpunit-lsp
steps:
- uses: actions/checkout@v4

- name: Setup PHP 8.4 (with PCOV for coverage)
# PCOV is preferred over Xdebug for Infection's initial run: an
# order of magnitude less memory under the same coverage scope,
# which keeps the runner well clear of the OOM-killer. The
# Makefile target sets pcov.directory + XDEBUG_MODE=off so the
# driver choice is deterministic regardless of what else is loaded.
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
extensions: dom, json, mbstring, tokenizer
coverage: pcov
tools: composer:v2

- name: Install LSP dependencies
uses: ramsey/composer-install@v3

- name: Run Infection (LSP)
# `make test/mutation` lazily downloads infection.phar
# 0.33.1 into tools/lsp/var/ (PHAR sidesteps the composer dep conflict
# with phpactor/language-server documented in tools/lsp/README.md).
# Runs with --min-covered-msi=93; current baseline is 94%.
run: make test/mutation

- name: Upload Infection report (LSP)
if: always()
uses: actions/upload-artifact@v4
with:
name: infection-lsp-report
path: |
tools/lsp/var/infection.log
tools/lsp/var/infection.html
if-no-files-found: ignore
retention-days: 14
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/vendor/
/var/
.phpunit.result.cache
.infection.json5.tmp.log
/docker-compose.override.yml
53 changes: 53 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Contributing

## Test

```bash
make test/unit
make test/mutation
```

`test/mutation` downloads `infection.phar` lazily into `var/` and runs against
the same source + test set. The PHAR distribution sidesteps the
`thecodingmachine/safe` / `psr/log` conflicts that prevent a composer-installed
Infection from coexisting with `phpactor/language-server`. Curated
equivalent-mutation ignores live in `infection.json5` with per-mutator `ignore`
rules and inline rationale.

## How it works

PHP-semantic GTD / hover / completion is backed by
[`phpactor/worse-reflection`](https://github.com/phpactor/worse-reflection)
and [`jetbrains/phpstorm-stubs`](https://github.com/JetBrains/phpstorm-stubs).
xphp-specific paths run FIRST (template instantiation, type-args inside `<...>`
clauses); when those don't apply we fall through to the worse-reflection path so
behaviour on `.xphp` files matches PhpStorm's PHP intelligence on regular `.php`
files. The same `PhpHoverResolver` / `PhpDefinitionResolver` /
`PhpCompletionResolver` triad also drives `signatureHelp`, `inlayHint`, and
`callHierarchy` so all five features agree on receiver / member resolution.

## LSP capabilities advertised at `initialize`

For LSP-client developers wiring this server into a non-bundled editor:

- `textDocumentSync: 1` (Full)
- `hoverProvider`, `definitionProvider`, `typeDefinitionProvider`,
`referencesProvider`, `implementationProvider`
- `documentHighlightProvider`, `documentSymbolProvider`,
`workspaceSymbolProvider`
- `renameProvider`
- `foldingRangeProvider`
- `completionProvider` with `triggerCharacters: ["<", ",", ">", ":"]`
and `resolveProvider: true`
- `signatureHelpProvider` with `triggerCharacters: ["(", ","]`
- `inlayHintProvider`
- `codeActionProvider` with `resolveProvider: true`
- `codeLensProvider` with `resolveProvider: true`
- `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`
- `workspace.fileOperations.willRename` (LSP 3.17)
- `workspace.didChangeWatchedFiles` (dynamic registration)
89 changes: 89 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Makefile for the xphp LSP package.
#
# Run from this directory directly (`make test`) or from the repo root with
# `make -C tools/lsp <target>`. There is intentionally no root-level
# delegator — the per-package Makefile is the single source of truth.

.PHONY: test
test/unit:
composer install --quiet && \
php -d error_reporting='E_ALL & ~E_DEPRECATED' vendor/bin/phpunit

# Infection runs against this package via its PHAR distribution rather than a
# composer require: phpactor/language-server pins psr/log ^1.0 which composer
# can't reconcile with infection 0.33's ^2||^3, AND sharing the root vendor's
# infection with tools/lsp would collide on thecodingmachine/safe's global
# functions (both vendor trees install it). The PHAR ships its internal deps
# under PHP-Scoper-prefixed namespaces, so neither problem applies. Downloaded
# lazily into var/infection.phar (gitignored).
INFECTION_VERSION := 0.33.1
INFECTION_PHAR := var/infection.phar

$(INFECTION_PHAR):
@mkdir -p $(dir $(INFECTION_PHAR))
@echo "==> Downloading infection.phar $(INFECTION_VERSION)"
@curl -fsSL -o $@ \
https://github.com/infection/infection/releases/download/$(INFECTION_VERSION)/infection.phar
@chmod +x $@

.PHONY: test/mutation
# Coverage driver: PCOV, not Xdebug. PCOV uses orders of magnitude
# less memory for line-coverage tracking (no full execution-context
# capture). Xdebug on the initial 424-test run was triggering the
# host OOM-killer in tight container envelopes (SIGTERM 143);
# PCOV reliably stays under 512M total RSS for the same workload.
#
# `XDEBUG_MODE=off` ensures Xdebug doesn't load even though the
# extension is enabled at the docker layer -- otherwise Infection
# picks Xdebug by precedence regardless of PCOV's presence.
# `pcov.directory` scopes PCOV to our source tree (without it PCOV
# tracks every file it sees, vendor/ included, ballooning memory).
test/mutation: $(INFECTION_PHAR)
composer install --quiet && \
XDEBUG_MODE=off php \
-d error_reporting='E_ALL & ~E_DEPRECATED' \
-d memory_limit=-1 \
-d pcov.enabled=1 \
-d pcov.directory=src \
var/infection.phar \
--threads=max --min-covered-msi=93 --show-mutations --no-progress \
--initial-tests-php-options='-d error_reporting=E_ALL\&~E_DEPRECATED -d pcov.enabled=1 -d pcov.directory=src'

# The xphp-lsp PHAR is built via Humbug Box, distributed as a PHAR itself
# (same lazy-download pattern as infection.phar so we don't fight phpactor's
# psr/log ^1.0 pin in composer-resolved Box). The output at var/xphp-lsp.phar
# is what the PHPStorm plugin bundles for zero-config install.
BOX_VERSION := 4.6.6
BOX_PHAR := var/box.phar

$(BOX_PHAR):
@mkdir -p $(dir $(BOX_PHAR))
@echo "==> Downloading box.phar $(BOX_VERSION)"
@curl -fsSL -o $@ \
https://github.com/box-project/box/releases/download/$(BOX_VERSION)/box.phar
@chmod +x $@

.PHONY: build/phar
build/phar: $(BOX_PHAR)
# Build sequence:
# 1) --no-dev + --classmap-authoritative trims phpunit and noisy dev classes.
# 2) The path-repo entry in composer.json pins "symlink": true (great for
# dev: parent edits show up live). That same setting is fatal for the
# PHAR -- PHARs can't follow symlinks, so Box would embed a dangling
# pointer and the runtime can't resolve XPHP\Transpiler\* classes.
# Replace the symlinked package with a real copy of just src/ +
# composer.json (the only paths xphp's PSR-4 autoload reaches)
# and re-dump the autoloader so the classmap targets the copy.
# 3) Restore the symlinked dev install for ongoing `make test` runs.
composer install --no-dev --classmap-authoritative --quiet --no-interaction
@if [ -L vendor/xphp-lang/xphp ]; then \
parser_src="$$(readlink -f vendor/xphp-lang/xphp)"; \
rm vendor/xphp-lang/xphp; \
mkdir -p vendor/xphp-lang/xphp/src; \
cp -RL "$$parser_src/src/." vendor/xphp-lang/xphp/src/; \
cp -L "$$parser_src/composer.json" vendor/xphp-lang/xphp/composer.json; \
composer dump-autoload --classmap-authoritative --no-dev --quiet --no-interaction; \
fi
php -d phar.readonly=0 var/box.phar compile --no-interaction
composer install --quiet --no-interaction
@echo "==> Built $$(ls -lh var/xphp-lsp.phar | awk '{print $$5, $$9}')"
77 changes: 76 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,79 @@ The server reuses the parent `xphp` package's AST, generic-instantiation
language semantics.

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

---

## Install

```bash
composer require xphp-lang/language-server
```

---

## Build

```bash
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.

---

## Overview

```mermaid
---
config:
layout: tidy-tree
---
mindmap
root((LSP))
Navigate
definition
typeDefinition
references
implementation
callHierarchy
typeHierarchy
documentSymbol
workspaceSymbol
documentHighlight
Edit
rename
willRenameFiles
codeAction
codeLens
Understand
hover
signatureHelp
inlayHint
foldingRange
semanticTokens
Validate
parse
bound
duplicate-template
undefined-bareword
constructor-arg-mismatch
Find
completion
completionItem/resolve
Performance
AST cache
stub cache
tolerant-parse
UTF-16 columns
short-name tie-break
lint mode
```

## See also

- [detailed list of features](docs/features/index.md)
- [roadmap](./docs/roadmap.md)
Loading
Loading