Skip to content

[CLEANUP] Rely on PHP to detect access to uninitialized properties#1620

Merged
JakeQZ merged 2 commits into
mainfrom
task/drop-throw/4
May 13, 2026
Merged

[CLEANUP] Rely on PHP to detect access to uninitialized properties#1620
JakeQZ merged 2 commits into
mainfrom
task/drop-throw/4

Conversation

@oliverklee
Copy link
Copy Markdown
Collaborator

In our code, this exception that indicated that the property was access before initialization is never thrown.

So we can simplify our code by removing the check (and the exception) and make use of PHP's own mechanism for throwing an error if the property is accessed before initialization.

@coveralls
Copy link
Copy Markdown

coveralls commented May 3, 2026

Coverage Status

coverage: 98.246% (+0.6%) from 97.669% — task/drop-throw/4 into main

@oliverklee oliverklee requested a review from JakeQZ May 3, 2026 08:58
Copy link
Copy Markdown
Collaborator

@JakeQZ JakeQZ left a comment

Choose a reason for hiding this comment

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

Looks good. I've suggested a couple of assert()s which may be worthwhile additions if you agree.

Coverage Status

coverage: 98.244% (+0.6%) from 97.672% — task/drop-throw/4 into main

Not looked into the reports - do they indicate which lines of code are not exercised in tests? (It's impossible to test code which can never be executed.)

Can we get to 100%? Back-of-an-envelope caclulation suggests there might just be three other impossible assert-throwing branches to eliminate to get us there...

Comment thread src/HtmlProcessor/AbstractHtmlProcessor.php
}

$instance = new static();
$instance->setHtml($unprocessedHtml);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perhaps we should add after this line:

\assert($instance->xPath instanceof \DOMXPath);

This would maybe make it easier to debug any potential breakages in development, pinpointing the point of failure, rather than having to diagnose a less fathomable failure further down the line.

public static function fromDomDocument(\DOMDocument $document): self
{
$instance = new static();
$instance->setDomDocument($document);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ditto re assert().

Comment on lines 47 to 50
@@ -50,9 +50,9 @@
protected $domDocument = null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can similarly remove the nulls from this property - but as a separate PR (and perhaps likewise also add assertions for it being initialized).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@oliverklee
Copy link
Copy Markdown
Collaborator Author

Not looked into the reports - do they indicate which lines of code are not exercised in tests? (It's impossible to test code which can never be executed.)

Can we get to 100%? Back-of-an-envelope caclulation suggests there might just be three other impossible assert-throwing branches to eliminate to get us there...

We'll be able to come very near. I'll create a ticket for raising the code coverage with a list of not-yet-covered parts.

@oliverklee oliverklee force-pushed the task/drop-throw/4 branch from f95dcbb to d82b3bd Compare May 7, 2026 16:10
@JakeQZ
Copy link
Copy Markdown
Collaborator

JakeQZ commented May 7, 2026

Looks good. I've suggested a couple of assert()s which may be worthwhile additions if you agree.

I just checked, and PHPStan would give an error if such assert()s were added. However, we could set treatPhpDocTypesAsCertain to false in phpstan.neon to allow us to assert() complete construction. WDYT?

@oliverklee
Copy link
Copy Markdown
Collaborator Author

oliverklee commented May 9, 2026

However, we could set treatPhpDocTypesAsCertain to false in phpstan.neon to allow us to assert() complete construction. WDYT?

I'd rather not.

In the production code, I'd prefer to keep doing this:

  • add/keep exceptions for error cases that can actually happen, and cover those with tests
  • add assertions for cases where we know the type etc. for sure, but PHPStan cannot deduct it (yet)
  • add PHPDoc type annotations for the same kind of cases where an assert does not suffice (e.g, for generics)

Would you be okay with doing/continuing this?

@JakeQZ
Copy link
Copy Markdown
Collaborator

JakeQZ commented May 9, 2026

  • add/keep exceptions for error cases that can actually happen, and cover those with tests
  • add assertions for cases where we know the type etc. for sure, but PHPStan cannot deduct it (yet)
  • add PHPDoc type annotations for the same kind of cases where an assert does not suffice (e.g, for generics)

I'm fine with all this. But using or not using treatPhpDocTypesAsCertain seems to be independent of the above, although closely related. Disabling it would allow us to catch programming mistakes with assert()s without PHPStan complaining.

E.g. suppose I added this to AbstractHtmlProcessor:

public static function fromNothing(): self
{
    $instance = new static();
    return $instance;
}

This would return an instance with null for xPath and domDocument, despite the PHPDoc saying it's not possible.

@oliverklee
Copy link
Copy Markdown
Collaborator Author

I've given treatPhpDocTypesAsCertain a go at #1622 and MyIntervals/PHP-CSS-Parser#1583. I would have expected the baseline to change a lot more, and now I'm fine with that configuration change. :-)

In our code, this exception that indicated that the property
was access before initialization is never thrown.

So we can simplify our code by removing the check (and the
exception) and make use of PHP's own mechanism for throwing an
error if the property is accessed before initialization.
@oliverklee oliverklee force-pushed the task/drop-throw/4 branch from d82b3bd to 0be5366 Compare May 13, 2026 15:53
@oliverklee
Copy link
Copy Markdown
Collaborator Author

Rebased and added the assertions.

@oliverklee oliverklee force-pushed the task/drop-throw/4 branch from ac6ce85 to e9dd538 Compare May 13, 2026 16:02
@JakeQZ JakeQZ merged commit 41f9ae4 into main May 13, 2026
33 checks passed
@JakeQZ JakeQZ deleted the task/drop-throw/4 branch May 13, 2026 16:08
@JakeQZ
Copy link
Copy Markdown
Collaborator

JakeQZ commented May 14, 2026

I've given treatPhpDocTypesAsCertain a go at #1622 and MyIntervals/PHP-CSS-Parser#1583. I would have expected the baseline to change a lot more, and now I'm fine with that configuration change. :-)

I wonder if there's a way to get the best of both worlds, running PHPStan twice:

  • once with treatPhpDocTypesAsCertain;
  • a second time without, but with some other option (if one exists) to ignore assert()s that cannot fail based on the DocBlocks.

From phpstan/phpstan#12719, it seems this would require creating a PHPStan extension, which seems like quite a bit of effort for something that isn't much of an issue - though we would like to ensure our DocBlock types are correct and consistent with the code (and vice versa) when used internally.

@JakeQZ
Copy link
Copy Markdown
Collaborator

JakeQZ commented May 14, 2026

... it seems this would require creating a PHPStan extension, which seems like quite a bit of effort for something that isn't much of an issue - though we would like to ensure our DocBlock types are correct and consistent with the code (and vice versa) when used internally.

It doesn't look too onerous to create an extension to handle conditionalType.alwaysTrue, then look to see if it's within an assert(). Maybe slightly trickier to also cover an if ($alwaysFalse) { throw Exception() }. But once in place would allow us to use assert()s and guards without needing phpstan-ignore. WDYT?

@oliverklee
Copy link
Copy Markdown
Collaborator Author

It doesn't look too onerous to create an extension to handle conditionalType.alwaysTrue, then look to see if it's within an assert(). Maybe slightly trickier to also cover an if ($alwaysFalse) { throw Exception() }. But once in place would allow us to use assert()s and guards without needing phpstan-ignore. WDYT?

This sounds intriguing to me. I wouldn't want to put time into writing the extension, but I'd be willing to review it and see its benefits. :-)

@JakeQZ
Copy link
Copy Markdown
Collaborator

JakeQZ commented May 15, 2026

This sounds intriguing to me. I wouldn't want to put time into writing the extension, but I'd be willing to review it and see its benefits. :-)

I was easily able to create an extension to potentially disable some warnings, hook it in, but then unable to get context information (to disable the warning only within an assert), and unable to find relevant documentation. I logged phpstan/phpstan#14616 but will give up on this for now.

@JakeQZ
Copy link
Copy Markdown
Collaborator

JakeQZ commented May 15, 2026

I logged phpstan/phpstan#14616 but will give up on this for now.

Got some helpful pointers from some helpful people, so now have #1624...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants