Skip to content

Commit 801f28f

Browse files
authored
Narrow DOMDocument::createElement() return type and throw type for valid constant names (#5192)
1 parent 5c0e37f commit 801f28f

File tree

5 files changed

+224
-0
lines changed

5 files changed

+224
-0
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use DOMDocument;
6+
use DOMException;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\DependencyInjection\AutowiredService;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\Type\Constant\ConstantBooleanType;
13+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
14+
use PHPStan\Type\NeverType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
use function extension_loaded;
18+
19+
#[AutowiredService]
20+
final class DomDocumentCreateElementDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
21+
{
22+
23+
public function getClass(): string
24+
{
25+
return DOMDocument::class;
26+
}
27+
28+
public function isMethodSupported(MethodReflection $methodReflection): bool
29+
{
30+
return extension_loaded('dom') && $methodReflection->getName() === 'createElement';
31+
}
32+
33+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
34+
{
35+
$args = $methodCall->getArgs();
36+
if (!isset($args[0])) {
37+
return null;
38+
}
39+
40+
$argType = $scope->getType($args[0]->value);
41+
42+
$doc = new DOMDocument();
43+
44+
foreach ($argType->getConstantStrings() as $constantString) {
45+
try {
46+
$doc->createElement($constantString->getValue());
47+
} catch (DOMException) {
48+
return null;
49+
}
50+
51+
$argType = TypeCombinator::remove($argType, $constantString);
52+
}
53+
54+
if (!$argType instanceof NeverType) {
55+
return null;
56+
}
57+
58+
$variant = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants());
59+
60+
return TypeCombinator::remove($variant->getReturnType(), new ConstantBooleanType(false));
61+
}
62+
63+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use DOMDocument;
6+
use DOMException;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\DependencyInjection\AutowiredService;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Type\DynamicMethodThrowTypeExtension;
12+
use PHPStan\Type\NeverType;
13+
use PHPStan\Type\ObjectType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use function extension_loaded;
17+
18+
#[AutowiredService]
19+
final class DomDocumentCreateElementDynamicThrowTypeExtension implements DynamicMethodThrowTypeExtension
20+
{
21+
22+
public function isMethodSupported(MethodReflection $methodReflection): bool
23+
{
24+
return extension_loaded('dom')
25+
&& $methodReflection->getDeclaringClass()->getName() === DOMDocument::class
26+
&& $methodReflection->getName() === 'createElement';
27+
}
28+
29+
public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
30+
{
31+
$args = $methodCall->getArgs();
32+
if (!isset($args[0])) {
33+
return new ObjectType(DOMException::class);
34+
}
35+
36+
$argType = $scope->getType($args[0]->value);
37+
38+
$doc = new DOMDocument();
39+
40+
foreach ($argType->getConstantStrings() as $constantString) {
41+
try {
42+
$doc->createElement($constantString->getValue());
43+
} catch (DOMException) {
44+
return new ObjectType(DOMException::class);
45+
}
46+
47+
$argType = TypeCombinator::remove($argType, $constantString);
48+
}
49+
50+
if (!$argType instanceof NeverType) {
51+
return new ObjectType(DOMException::class);
52+
}
53+
54+
return null;
55+
}
56+
57+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace DomDocumentCreateElement;
4+
5+
use DOMDocument;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
11+
public function dynamicName(DOMDocument $doc, string $name): void
12+
{
13+
assertType('(DOMElement|false)', $doc->createElement($name));
14+
}
15+
16+
public function validConstantNames(DOMDocument $doc): void
17+
{
18+
assertType('DOMElement', $doc->createElement('div'));
19+
assertType('DOMElement', $doc->createElement('my-element'));
20+
assertType('DOMElement', $doc->createElement('ns:tag'));
21+
assertType('DOMElement', $doc->createElement('_private'));
22+
assertType('DOMElement', $doc->createElement('h1'));
23+
}
24+
25+
public function invalidConstantNames(DOMDocument $doc): void
26+
{
27+
assertType('(DOMElement|false)', $doc->createElement(''));
28+
assertType('(DOMElement|false)', $doc->createElement('123element'));
29+
assertType('(DOMElement|false)', $doc->createElement('my element'));
30+
}
31+
32+
/**
33+
* @param 'div'|'span' $validUnion
34+
* @param 'div'|'' $mixedUnion
35+
*/
36+
public function unions(DOMDocument $doc, string $validUnion, string $mixedUnion): void
37+
{
38+
assertType('DOMElement', $doc->createElement($validUnion));
39+
assertType('(DOMElement|false)', $doc->createElement($mixedUnion));
40+
}
41+
42+
public function localVariable(DOMDocument $doc): void
43+
{
44+
$name = 'paragraph';
45+
assertType('DOMElement', $doc->createElement($name));
46+
}
47+
48+
}

tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,22 @@ public function testBugArrayOffset(): void
9292
]);
9393
}
9494

95+
public function testBug13792(): void
96+
{
97+
$this->analyse([__DIR__ . '/data/bug-13792.php'], [
98+
[
99+
'Method Bug13792\Foo::dynamicName() throws checked exception DOMException but it\'s missing from the PHPDoc @throws tag.',
100+
20,
101+
],
102+
[
103+
'Method Bug13792\Foo::invalidConstantName() throws checked exception DOMException but it\'s missing from the PHPDoc @throws tag.',
104+
25,
105+
],
106+
[
107+
'Method Bug13792\Foo::unions() throws checked exception DOMException but it\'s missing from the PHPDoc @throws tag.',
108+
35,
109+
],
110+
]);
111+
}
112+
95113
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13792;
4+
5+
use DOMDocument;
6+
7+
class Foo
8+
{
9+
10+
public function validConstantNames(DOMDocument $doc): void
11+
{
12+
$doc->createElement('div');
13+
$doc->createElement('my-element');
14+
$doc->createElement('ns:tag');
15+
$doc->createElement('_private');
16+
}
17+
18+
public function dynamicName(DOMDocument $doc, string $name): void
19+
{
20+
$doc->createElement($name); // error
21+
}
22+
23+
public function invalidConstantName(DOMDocument $doc): void
24+
{
25+
$doc->createElement(''); // error
26+
}
27+
28+
/**
29+
* @param 'div'|'span' $validUnion
30+
* @param 'div'|'' $mixedUnion
31+
*/
32+
public function unions(DOMDocument $doc, string $validUnion, string $mixedUnion): void
33+
{
34+
$doc->createElement($validUnion);
35+
$doc->createElement($mixedUnion); // error
36+
}
37+
38+
}

0 commit comments

Comments
 (0)