Skip to content

Feature: xphp check#19

Closed
math3usmartins wants to merge 16 commits into
0.3.xfrom
feat/xphp-check-phase1
Closed

Feature: xphp check#19
math3usmartins wants to merge 16 commits into
0.3.xfrom
feat/xphp-check-phase1

Conversation

@math3usmartins

Copy link
Copy Markdown
Member

No description provided.

math3usmartins and others added 16 commits June 17, 2026 20:42
Introduce the XPHP\Diagnostics namespace: a string-backed Severity enum
(Error/Warning/Notice with isFailing()), a DiagnosticSource enum (xphp/phpstan),
a SourceLocation (file/line/optional column), the immutable Diagnostic, and a
mutable DiagnosticCollector (add/all/hasErrors/count).

Pure value objects with no pipeline wiring yet — the foundation for the
forthcoming `xphp check` command's structured, collect-all diagnostics. Unit
tests cover severity gating, collector ordering/error detection, and defaults
(100% mutation score over the new files).

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

Thread an optional DiagnosticCollector through the bound-validation path
(Registry ctor -> recordInstantiation -> validateBounds -> checkBounds). When
absent (xphp compile) violations throw exactly as before, byte-identical; when
present each violation is appended as a Diagnostic -- located at the
instantiation site captured from the AST node in RegistryCollector -- and
recording continues so all violations surface in one run.

The user-facing message now comes from a single shared boundViolationMessage()
builder so the throw text and the diagnostic text can never drift. Tests cover
collect-vs-throw, byte-identical and exact message text, multi-violation
collection, and AST-derived source line (100% mutation score over the diff).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Compiler::check(): parse, build the type hierarchy, collect definitions,
validate defaults-against-bounds, collect instantiations (bounds + missing type
arguments), and report instantiations of undefined templates -- gathering every
error into a DiagnosticCollector and halting before specialization/emit, so a
partially-invalid registry never reaches the fixed-point loop.

Extends the optional-collector seam to the padding path (missing required type
argument), validateDefaultsAgainstBounds (per-parameter, continue-on-error), and
a new collectUndefinedTemplates pass. Each reused message is built by a single
shared helper so the throw (compile) and diagnostic (check) text stay
byte-identical. The parse loop is factored into parseAll(), reused by compile()
and check(); compile()'s undefined-template throw now routes through the shared
builder.

Duplicate-definition is intentionally not part of the seam: RegistryCollector's
already-recorded guard makes the class-template path unreachable and surfacing it
would change compile-mode semantics -- deferred. Variance and method-level
generic checks remain fail-fast (not yet part of the check pass).

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

Move the variance-position check out of the parser into a Registry phase
(validateVariancePositions) over collected definitions, wired into compile()
(fail-fast, byte-identical first-violation throw) and check() (collects every
violation across all definitions, each located at the offending member).
VariancePositionValidator now accumulates violations behind a static
assertPositions facade that throws the first when no collector is given or emits
a diagnostic per violation when one is.

The parser-level variance-position tests move to a dedicated
VariancePositionPhaseTest (compile-mode throws via data provider + check-mode
collect/location), and the check integration suite gains a variance_violation
fixture covering the compile-throw and check-collect paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the inner-variance composition walk out of Registry into a dedicated
InnerVarianceValidator (mirroring VariancePositionValidator): it accumulates
violations behind a static assertComposition facade that throws the first when
no collector is given (compile, byte-identical) or emits a diagnostic per
violation (check), each located at the offending member. Registry's
validateInnerVariance is now a thin delegate.

To avoid double-reporting a direct +T/-T misuse, the position check now returns
which definitions it flagged and the inner-variance pass skips them -- matching
compile-mode, where the position check fails fast before inner-variance runs.
Both passes are wired into Compiler::check(); compile() is unchanged.

Adds inner-variance check fixture + collect-mode, gating, and null-file tests
(100% mutation score over the new validator and the diff).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run the variance-position check before the defaults-vs-bounds check so a class
with both surfaces the variance error first in compile-mode — the order it
surfaced when the check lived in the parser. Merge the stacked docblocks on the
two variance delegate methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a DiagnosticRenderer interface and three implementations for `xphp check`
output: TextRenderer (human-readable blocks), JsonRenderer (a stable documented
JSON contract), and GithubRenderer (Actions workflow-command annotations with
proper escaping). Unit tests pin each format exactly, including the JSON shape
and GitHub escaping (100% mutation score over the renderers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire a CheckCommand (`xphp check <source> [--format=text|json|github]`) into the
console alongside compile, sharing one Compiler. It runs the validate-only pass,
renders diagnostics in the chosen format, and exits 0 (clean) / 1 (errors) /
2 (bad source dir or unknown format).

Compiler::check() now parses each file in its own try/catch: a file that fails
to parse (PHP syntax error or an xphp-specific parse rejection) is reported as a
diagnostic and skipped, so the remaining files are still checked. Tests drive the
command via CommandTester across all formats/exit codes, and a parse_error
fixture proves a valid file's bound violation is still reported alongside two
unparseable files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the file read out of check()'s per-file try so an I/O failure surfaces as
itself rather than being mislabeled xphp.parse_error; only parsing is treated as
a recoverable per-file diagnostic. Clarify the parse-error line comment re nikic's
-1 sentinel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an `xphp check` section to the errors reference (formats, exit codes,
per-file parse resilience, and the stable diagnostic codes for the json/github
formats) and a short pointer from the README quick start.

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

Spell out the scope consequence: a clean `check` does not guarantee a clean
`compile`, because method/function/closure-level generic checks (and the
specialization-loop guards, by design) are not run by `check` yet. Advise keeping
`compile` in the build pipeline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run GenericMethodCompiler in a new validate-only mode from Compiler::check():
process(emit: false) walks the call sites for their bound/missing-arg checks and
the duplicate-function / $this-capture / static-closure rejections, threading the
optional DiagnosticCollector + source locations through the (already
collector-aware) Registry::checkBounds/padArgsWithDefaults and the in-process
throws, while suppressing the specialize/strip/finalize side-effects. xphp compile
is unchanged (default emit: true, no collector -> byte-identical fail-fast).

This makes `xphp check` a validation-superset of `xphp compile`: a class-level
and a method-level generic error are now both reported in one run. New diagnostic
codes xphp.duplicate_generic_function / xphp.closure_this_capture /
xphp.static_closure; bound + missing-arg reuse the existing codes.

Fixtures + CheckPassIntegrationTest cover each new collected diagnostic (with
file:line), the both-passes-in-one-run guarantee, and byte-identical-compile
guards. Docs updated: check now covers all generic validation; only the
specialization-loop guards (depth cap, hash collision) remain compile-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a closure_static fixture + check-collect and compile-throws tests for the
generic static-closure rejection (xphp.static_closure), matching the symmetry of
the other method-level checks (the collect path was previously untested). Add the
three new method-level codes to the errors-doc table, and correct the
validate-only comments (the discarded per-file AST may carry in-place call-site
rewrites; templates are deep-cloned so nothing shared is mutated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump phpstan.neon from level 7 to level 9 and make src/ clean at it:
- CompileCommand / CheckCommand: narrow getArgument()/getOption() (typed mixed) to
  string via is_string() instead of a blind (string) cast — the inputs are always
  strings (required argument / option with a string default), so behavior is
  unchanged; level 9 just rejects casting mixed.
- Specializer: annotate the ATTR_GENERIC_ARGS array as list<TypeRef> so array_map
  infers the callback's parameter type (level-9 callable-variance check).

Full suite green; src/ clean at level 9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The check command is unit-tested in-process via CommandTester, but nothing
exercised the real binary: its autoload wiring, the process exit code the shell
sees, or the rendered github/json/text output on stdout. The released PHAR was
only smoke-tested with `list`, never `check`.

Add test/smoke/check.sh — a parameterized POSIX script (XPHP_BIN selects the
binary) that runs `check` against the clean and multi_error fixtures and asserts
the 0/1/2 exit contract plus that every renderer emits and json stays
well-formed. Wire it in:
- Makefile: `test/check` target.
- ci-core.yml: a dedicated `xphp check (self-test)` job running `make test/check`
  against bin/xphp.
- release.yml: a post-build step running the same script against dist/xphp.phar,
  so a packaged binary that can't gate fails the release before upload.

No src/ changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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