Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions build/datetime-php-83.neon

This file was deleted.

4 changes: 0 additions & 4 deletions build/ignore-by-php-version.neon.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@
$includes[] = __DIR__ . '/spl-autoload-functions-php-8.neon';
}

if (PHP_VERSION_ID >= 80300) {
$includes[] = __DIR__ . '/datetime-php-83.neon';
}

if (PHP_VERSION_ID >= 80400) {
$includes[] = __DIR__ . '/deprecated-8.4.neon';
}
Expand Down
1 change: 1 addition & 0 deletions resources/functionMap_php83delta.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
return [
'new' => [
'DateInterval::createFromDateString' => ['static', 'modify'=>'string'],
'DateTime::modify' => ['static', 'modify'=>'string'],
'DateTimeImmutable::modify' => ['static', 'modify'=>'string'],
'str_decrement' => ['non-empty-string', 'string'=>'non-empty-string'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use DateInterval;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;

#[AutowiredService]
final class DateIntervalCreateFromDateStringThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension
{

public function __construct(private PhpVersion $phpVersion)
{
}

public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'createFromDateString'
&& $methodReflection->getDeclaringClass()->getName() === DateInterval::class;
}

public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
{
if (count($methodCall->getArgs()) === 0) {
return null;
}

if (!$this->phpVersion->hasDateTimeExceptions()) {
return null;
}

$valueType = $scope->getType($methodCall->getArgs()[0]->value);
$constantStrings = $valueType->getConstantStrings();

foreach ($constantStrings as $constantString) {
try {
DateInterval::createFromDateString($constantString->getValue());
} catch (\Exception) { // phpcs:ignore
return $methodReflection->getThrowType();
}

$valueType = TypeCombinator::remove($valueType, $constantString);
}

if (!$valueType instanceof NeverType) {
return $methodReflection->getThrowType();
}

return null;
}

}
44 changes: 27 additions & 17 deletions src/Type/Php/DateIntervalDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Throwable;
use function count;
use function in_array;

#[AutowiredService]
final class DateIntervalDynamicReturnTypeExtension
implements DynamicStaticMethodReturnTypeExtension
final class DateIntervalDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{

public function __construct(private PhpVersion $phpVersion)
{
}

public function getClass(): string
{
return DateInterval::class;
Expand All @@ -40,31 +43,38 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,

$strings = $scope->getType($arguments[0]->value)->getConstantStrings();

$possibleReturnTypes = [];
$hasFalse = false;
$hasDateInterval = false;
foreach ($strings as $string) {
try {
$result = @DateInterval::createFromDateString($string->getValue());
} catch (Throwable) {
$possibleReturnTypes[] = false;
continue;
$result = false;
}
$possibleReturnTypes[] = $result instanceof DateInterval ? DateInterval::class : false;
}

// the error case, when wrong types are passed
if (count($possibleReturnTypes) === 0) {
return null;
if ($result === false) {
$hasFalse = true;
} else {
$hasDateInterval = true;
}
}

if (in_array(false, $possibleReturnTypes, true) && in_array(DateInterval::class, $possibleReturnTypes, true)) {
if ($hasFalse) {
if (!$hasDateInterval) {
if ($this->phpVersion->hasDateTimeExceptions()) {
return new NeverType();
}

return new ConstantBooleanType(false);
}

return null;
}

if (in_array(false, $possibleReturnTypes, true)) {
return new ConstantBooleanType(false);
if ($hasDateInterval) {
return new ObjectType(DateInterval::class);
}

return new ObjectType(DateInterval::class);
return null;
}

}
14 changes: 7 additions & 7 deletions src/Type/Php/DateTimeModifyReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
try {
$result = @(new DateTime())->modify($constantString->getValue());
} catch (Throwable) {
$valueType = TypeCombinator::remove($valueType, $constantString);
continue;
$result = false;
}

if ($result === false) {
Expand All @@ -76,11 +75,16 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method

if ($hasFalse) {
if (!$hasDateTime) {
if ($this->phpVersion->hasDateTimeExceptions()) {
return new NeverType();
}

return new ConstantBooleanType(false);
}

return null;
} elseif ($hasDateTime) {
}
if ($hasDateTime) {
$callerType = $scope->getType($methodCall->var);

$dateTimeInterfaceType = new ObjectType(DateTimeInterface::class);
Expand All @@ -102,10 +106,6 @@ static function (Type $type, callable $traverse) use ($dateTimeInterfaceType): T
);
}

if ($this->phpVersion->hasDateTimeExceptions()) {
return new NeverType();
}

return null;
}

Expand Down
22 changes: 22 additions & 0 deletions tests/PHPStan/Analyser/data/bug-14479-82.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php // lint < 8.3

namespace Bug14479Php82;

use function PHPStan\Testing\assertType;

function test(string $input) {
assertType('DateInterval|false', \DateInterval::createFromDateString($input));
}

function testValid() {
assertType('DateInterval', \DateInterval::createFromDateString('P1D'));
}

function testInvalid() {
assertType('false', \DateInterval::createFromDateString('foo'));
}

/** @param 'P1D'|'foo' $input */
function testUnion(string $input) {
assertType('DateInterval|false', \DateInterval::createFromDateString($input));
}
22 changes: 22 additions & 0 deletions tests/PHPStan/Analyser/data/bug-14479-83.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php // lint >= 8.3

namespace Bug14479Php83;

use function PHPStan\Testing\assertType;

function test(string $input) {
assertType('DateInterval', \DateInterval::createFromDateString($input));
}

function testValid() {
assertType('DateInterval', \DateInterval::createFromDateString('P1D'));
}

function testInvalid() {
assertType('*NEVER*', \DateInterval::createFromDateString('foo'));
}

/** @param 'P1D'|'foo' $input */
function testUnion(string $input) {
assertType('DateInterval', \DateInterval::createFromDateString($input));
}
40 changes: 40 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-8442-83.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php // lint >= 8.3

namespace Bug8442Php83;

use stdClass;
use function PHPStan\Testing\assertType;
use DateInterval;

function () {
assertType('*NEVER*', DateInterval::createFromDateString('foo'));
assertType('DateInterval', DateInterval::createFromDateString('1 Day'));

if (rand(0,1)) {
$interval = '1 day';
} else {
$interval = '2 day';
}

assertType('DateInterval', DateInterval::createFromDateString($interval));

if (rand(0,1)) {
$interval = 'foo';
} else {
$interval = '2 day';
}

assertType('DateInterval', DateInterval::createFromDateString($interval));

if (rand(0,1)) {
$interval = 'foo';
} else {
$interval = 'foo';
}

assertType('*NEVER*', DateInterval::createFromDateString($interval));

assertType('DateInterval',DateInterval::createFromDateString(str_shuffle('1 day')));
assertType('DateInterval',DateInterval::createFromDateString());
assertType('DateInterval',DateInterval::createFromDateString(new stdClass()));
};
3 changes: 1 addition & 2 deletions tests/PHPStan/Analyser/nsrt/bug-8442.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php declare(strict_types = 1);
<?php // lint < 8.3

namespace Bug8442;

Expand Down Expand Up @@ -38,4 +38,3 @@ function () {
assertType('DateInterval|false',DateInterval::createFromDateString());
assertType('DateInterval|false',DateInterval::createFromDateString(new stdClass()));
};

Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,12 @@ public function testBug13806(): void
]);
}

#[RequiresPhp('>= 8.3.0')]
public function testBug14479(): void
{
$this->analyse([__DIR__ . '/data/bug-14479.php'], []);
}

public function testBug5952(): void
{
$this->analyse([__DIR__ . '/data/bug-5952.php'], [
Expand Down
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/bug-14479.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php // lint >= 8.3

namespace Bug14479;


function test(string $input) {
try {
\DateInterval::createFromDateString($input);
} catch (\Exception $e) {
}
}

function testValid() {
try {
\DateInterval::createFromDateString('P1D');
} catch (\Exception $e) {
}
}

function testInvalid() {
try {
\DateInterval::createFromDateString('foo');
} catch (\Exception $e) {
}
}

/** @param 'P1D'|'foo' $input */
function testUnion(string $input) {
try {
\DateInterval::createFromDateString($input);
} catch (\Exception $e) {
}
}
Loading