Skip to content

Type DOMDocument load/save/validate string arguments as non-empty-string on PHP 8 - #6075

Merged
VincentLanglet merged 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-i07zcbb
Jul 21, 2026
Merged

Type DOMDocument load/save/validate string arguments as non-empty-string on PHP 8#6075
VincentLanglet merged 3 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-i07zcbb

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Since PHP 8.0, DOMDocument::loadHTML() (and its sibling load/save/validate
methods) throw a ValueError when passed an empty string
(Argument #1 ($source) must not be empty). PHPStan previously typed these
arguments as plain string, so calling e.g. $doc->loadHTML('') was not
reported. This change types those arguments as non-empty-string on PHP 8+
so the mistake is caught.

Changes

  • Added src/PhpDoc/DomDocumentStubFilesExtension.php, a version-gated
    StubFilesExtension that loads stubs/DOMDocument_php8.stub on PHP >= 8.0
    and stubs/DOMDocument.stub on older versions (same pattern as
    SocketSelectStubFilesExtension).
  • Moved the DOMDocument class definition out of stubs/dom.stub (which keeps
    the other DOM classes) into the two new stub files. A stub class may only be
    declared in one loaded stub file, so the class had to be extracted to be
    version-gated.
  • stubs/DOMDocument_php8.stub types the string arguments of the following
    methods as non-empty-string:
    load, loadXML, loadHTML, loadHTMLFile, save, saveHTMLFile,
    schemaValidate, schemaValidateSource, relaxNGValidate,
    relaxNGValidateSource.
  • stubs/DOMDocument.stub preserves the original pre-PHP-8 signatures
    (arguments stay plain string), so no false positives for code targeting
    PHP 7.
  • Updated tests/PHPStan/Command/ErrorFormatter/JunitErrorFormatterTest.php
    to assert the captured output is non-empty before passing it to
    DOMDocument::loadXML(), since the stub is now stricter.

Root cause

The DOM stub described the load/save/validate arguments as string, but on
PHP 8+ these methods reject the empty string with a ValueError. The reported
loadHTML bug is one instance of a whole family: every DOMDocument method that
consumes a source string or a file path got the same "must not be empty"
ValueError in the PHP 8.0 ext/dom cleanup. All ten were verified against the
runtime and fixed together. The fix is version-gated because on PHP 7 these
methods return false for an empty string instead of throwing, so
non-empty-string must not be enforced there.

Test

  • tests/PHPStan/Rules/Methods/data/bug-14979.php +
    CallMethodsRuleTest::testBug14979() (guarded with
    #[RequiresPhp('>= 8.0.0')]) assert that passing '' to all ten DOMDocument
    methods is reported as expects non-empty-string, '' given., while non-empty
    literals (loadHTML('<html></html>'), loadXML('<root/>')) are accepted.
    The test fails without the stub change (no errors reported) and passes with it.

Fixes phpstan/phpstan#14979

…string` on PHP 8

- Since PHP 8.0 the `$source`/`$filename` arguments of DOMDocument's
  load/save/validate methods throw a `ValueError` ("must not be empty")
  when given an empty string; the stub now types them as `non-empty-string`
  so passing `''` is reported.
- Moved the `DOMDocument` class out of `stubs/dom.stub` into two version-gated
  stub files selected by a new `DomDocumentStubFilesExtension`:
  `stubs/DOMDocument.stub` (< PHP 8, original behaviour) and
  `stubs/DOMDocument_php8.stub` (>= PHP 8, `non-empty-string` arguments),
  mirroring the existing `socket_select` version-gated stub pattern.
- Swept the whole sibling family, not just the reported `loadHTML`:
  `load`, `loadXML`, `loadHTML`, `loadHTMLFile`, `save`, `saveHTMLFile`,
  `schemaValidate`, `schemaValidateSource`, `relaxNGValidate`,
  `relaxNGValidateSource` — all verified to throw `ValueError` on empty input.
- Adjusted `JunitErrorFormatterTest` to assert non-empty output before
  `DOMDocument::loadXML()`, matching the stricter stub.

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use functionMap_php80delta instead ?

Replace the version-gated stub-file approach with entries in
functionMap_php80delta.php, the idiomatic mechanism for version-specific
signatures. The base functionMap.php keeps the pre-PHP-8 plain-string
signatures; the php80delta 'new' section overrides the ten load/save/validate
methods with non-empty-string arguments on PHP 8.0+.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

I've addressed VincentLanglet's review. Here's what I did:

Response to the review

Yes — functionMap_php80delta.php is the right mechanism, and it's much cleaner than the version-gated stub approach. I've switched to it.

Changes

Reverted the stub-file approach:

  • Deleted src/PhpDoc/DomDocumentStubFilesExtension.php
  • Deleted stubs/DOMDocument.stub and stubs/DOMDocument_php8.stub
  • Restored the DOMDocument class back into stubs/dom.stub (unchanged from before the PR)

Used the function map delta instead:

  • Added the ten load/save/validate methods to resources/functionMap_php80delta.php:
    • The new section (applied on PHP ≥ 8.0) types the source/filename arguments as non-empty-string.
    • The old section documents the pre-8.0 plain-string signatures that the delta unsets.
  • The base functionMap.php already carries the plain-string signatures, so PHP 7 code is unaffected — no false positives — while PHP 8.0+ gets the stricter non-empty-string.

The parameter names in functionMap (filename/source) already match the expected error messages, so the existing testBug14979 and the JunitErrorFormatterTest change work unchanged.

Verification

  • Confirmed testBug14979 fails without the delta change (no errors reported) and passes with it — the same right-reason failure the stub version had.
  • make phpstan → no errors.
  • make tests → 17606 tests pass (97 skipped, all pre-existing).

Note: because DomDocumentStubFilesExtension used #[AutowiredService], I regenerated the attribute-collector cache (vendor/attributes.php, gitignored) via composer dump-autoload so the container no longer references the deleted class.

The commit is pushed to the PR branch.

@VincentLanglet
VincentLanglet requested a review from staabm July 21, 2026 09:45

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move the new signatures into functionMap_php80delta_bleedingEdge because narrowing of parameter types is a BC break

…a_bleedingEdge

Narrowing the load/save/validate string arguments from string to
non-empty-string is a BC break, so gate it behind the stricter function map
(bleeding edge) rather than applying it unconditionally on PHP 8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

I've addressed staabm's review.

What I did

Moved the ten DOMDocument non-empty-string signatures out of functionMap_php80delta.php and into functionMap_php80delta_bleedingEdge.php, since narrowing a parameter from string to non-empty-string is a BC break and should only take effect under the stricter function map (bleeding edge).

Changes

  • resources/functionMap_php80delta.php — removed the DOMDocument entries from both the new and old sections.
  • resources/functionMap_php80delta_bleedingEdge.php — added the DOMDocument entries: the new section types filename/source as non-empty-string, and the old section documents the plain-string signatures being replaced (matching the existing array_rand pattern in that file).

This file is only loaded when stricterFunctionMap is true (FunctionSignatureMapProvider line 280–282, gated on %featureToggles.stricterFunctionMap%, enabled by conf/bleedingEdge.neon). So on stable PHP 8 the arguments stay plain string (no BC break), and only bleeding edge gets the stricter type.

Why no test/other changes were needed

  • RuleTestCase::getAdditionalConfigFiles() already includes conf/bleedingEdge.neon by default, so CallMethodsRuleTest::testBug14979 still sees the non-empty-string signatures and passes unchanged.
  • PHPStan's own self-analysis config (build/phpstan.neon) includes bleedingEdge.neon, so the JunitErrorFormatterTest assertNotSame('', …) guard is still required and correct.

Verification

  • testBug14979 → passes.
  • JunitErrorFormatterTest → 6 tests pass.
  • CallMethodsRuleTest + signature-map tests → 3031 tests, 0 failures.
  • make phpstan → no errors.
  • make tests → 17606 tests pass (97 skipped, all pre-existing).

Committed as 9251e73f6 and pushed to the PR branch.

@staabm
staabm requested a review from VincentLanglet July 21, 2026 10:28
@VincentLanglet
VincentLanglet merged commit e3cf399 into phpstan:2.2.x Jul 21, 2026
734 of 737 checks passed
@VincentLanglet
VincentLanglet deleted the create-pull-request/patch-i07zcbb branch July 21, 2026 11:20
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.

DomDocument::loadHtml does not accept empty string since PHP 8

3 participants