diff --git a/src/Type/Php/DomDocumentCreateElementDynamicReturnTypeExtension.php b/src/Type/Php/DomDocumentCreateElementDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..2495a769e9 --- /dev/null +++ b/src/Type/Php/DomDocumentCreateElementDynamicReturnTypeExtension.php @@ -0,0 +1,63 @@ +getName() === 'createElement'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + if (!isset($args[0])) { + return null; + } + + $argType = $scope->getType($args[0]->value); + + $doc = new DOMDocument(); + + foreach ($argType->getConstantStrings() as $constantString) { + try { + $doc->createElement($constantString->getValue()); + } catch (DOMException) { + return null; + } + + $argType = TypeCombinator::remove($argType, $constantString); + } + + if (!$argType instanceof NeverType) { + return null; + } + + $variant = ParametersAcceptorSelector::selectFromArgs($scope, $args, $methodReflection->getVariants()); + + return TypeCombinator::remove($variant->getReturnType(), new ConstantBooleanType(false)); + } + +} diff --git a/src/Type/Php/DomDocumentCreateElementDynamicThrowTypeExtension.php b/src/Type/Php/DomDocumentCreateElementDynamicThrowTypeExtension.php new file mode 100644 index 0000000000..3d9e125ee2 --- /dev/null +++ b/src/Type/Php/DomDocumentCreateElementDynamicThrowTypeExtension.php @@ -0,0 +1,57 @@ +getDeclaringClass()->getName() === DOMDocument::class + && $methodReflection->getName() === 'createElement'; + } + + public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + if (!isset($args[0])) { + return new ObjectType(DOMException::class); + } + + $argType = $scope->getType($args[0]->value); + + $doc = new DOMDocument(); + + foreach ($argType->getConstantStrings() as $constantString) { + try { + $doc->createElement($constantString->getValue()); + } catch (DOMException) { + return new ObjectType(DOMException::class); + } + + $argType = TypeCombinator::remove($argType, $constantString); + } + + if (!$argType instanceof NeverType) { + return new ObjectType(DOMException::class); + } + + return null; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/dom-document-create-element.php b/tests/PHPStan/Analyser/nsrt/dom-document-create-element.php new file mode 100644 index 0000000000..a97aa48959 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dom-document-create-element.php @@ -0,0 +1,48 @@ +createElement($name)); + } + + public function validConstantNames(DOMDocument $doc): void + { + assertType('DOMElement', $doc->createElement('div')); + assertType('DOMElement', $doc->createElement('my-element')); + assertType('DOMElement', $doc->createElement('ns:tag')); + assertType('DOMElement', $doc->createElement('_private')); + assertType('DOMElement', $doc->createElement('h1')); + } + + public function invalidConstantNames(DOMDocument $doc): void + { + assertType('(DOMElement|false)', $doc->createElement('')); + assertType('(DOMElement|false)', $doc->createElement('123element')); + assertType('(DOMElement|false)', $doc->createElement('my element')); + } + + /** + * @param 'div'|'span' $validUnion + * @param 'div'|'' $mixedUnion + */ + public function unions(DOMDocument $doc, string $validUnion, string $mixedUnion): void + { + assertType('DOMElement', $doc->createElement($validUnion)); + assertType('(DOMElement|false)', $doc->createElement($mixedUnion)); + } + + public function localVariable(DOMDocument $doc): void + { + $name = 'paragraph'; + assertType('DOMElement', $doc->createElement($name)); + } + +} diff --git a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php index aee538052e..8afa426f91 100644 --- a/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/MissingCheckedExceptionInMethodThrowsRuleTest.php @@ -67,6 +67,10 @@ public function testRule(): void 122, ]; } + $errors[] = [ + 'Method MissingExceptionMethodThrows\Foo::domCreateElementDoesThrow() throws checked exception DOMException but it\'s missing from the PHPDoc @throws tag.', + 133, + ]; $this->analyse([__DIR__ . '/data/missing-exception-method-throws.php'], $errors); } diff --git a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php index 21cfdf1072..4052b7daef 100644 --- a/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php +++ b/tests/PHPStan/Rules/Exceptions/data/missing-exception-method-throws.php @@ -122,4 +122,15 @@ public function dateTimeModifyDoesThrows(\DateTime $dt, \DateTimeImmutable $dti, $dti->modify($m); } + public function domCreateElementDoesNotThrow(\DOMDocument $doc): void + { + $doc->createElement('div'); + $doc->createElement('my-element'); + } + + public function domCreateElementDoesThrow(\DOMDocument $doc, string $name): void + { + $doc->createElement($name); + } + }