Skip to content
Open
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
66 changes: 9 additions & 57 deletions src/Type/Php/DateIntervalFormatDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,17 @@
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function is_numeric;
use function strtolower;
use function strtoupper;

#[AutowiredService]
final class DateIntervalFormatDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{

public function __construct(private DateIntervalFormatReturnTypeHelper $helper)
{
}

public function getClass(): string
{
return DateInterval::class;
Expand All @@ -44,51 +36,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
return null;
}

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

$constantStrings = $arg->getConstantStrings();
if (count($constantStrings) === 0) {
if ($arg->isNonEmptyString()->yes()) {
return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]);
}

return null;
}

// The worst case scenario for the non-falsy-string check is that every number is 0.
// `%a` format gives `(unknown)` and removes numeric and uppercase accessory but then
// we'll have to manually check for the non-falsy one.
$dateInterval = new DateInterval('P0D');

$possibleReturnTypes = [];
foreach ($constantStrings as $string) {
$formatString = $string->getValue();
$value = $dateInterval->format($formatString);

$accessories = [];
if (is_numeric($value)) {
$accessories[] = new AccessoryNumericStringType();
}
if ($value !== '0' && $value !== '' && $formatString !== '%a') {
$accessories[] = new AccessoryNonFalsyStringType();
} elseif ($value !== '') {
$accessories[] = new AccessoryNonEmptyStringType();
}
if (strtolower($value) === $value) {
$accessories[] = new AccessoryLowercaseStringType();
}
if (strtoupper($value) === $value) {
$accessories[] = new AccessoryUppercaseStringType();
}

if (count($accessories) === 0) {
return null;
}

$possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]);
}

return TypeCombinator::union(...$possibleReturnTypes);
return $this->helper->getType(
$scope->getType($arguments[0]->value),
$scope->getType($methodCall->var),
$scope,
);
}

}
40 changes: 40 additions & 0 deletions src/Type/Php/DateIntervalFormatFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Type;
use function count;

#[AutowiredService]
final class DateIntervalFormatFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(private DateIntervalFormatReturnTypeHelper $helper)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'date_interval_format';
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
{
$args = $functionCall->getArgs();
if (count($args) < 2) {
return null;
}

return $this->helper->getType(
$scope->getType($args[1]->value),
$scope->getType($args[0]->value),
$scope,
);
}

}
77 changes: 77 additions & 0 deletions src/Type/Php/DateIntervalFormatReturnTypeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use DateInterval;
use DateTime;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function is_numeric;
use function strtolower;
use function strtoupper;

#[AutowiredService]
final class DateIntervalFormatReturnTypeHelper
{

public function getType(Type $formatType, Type $intervalType, Scope $scope): ?Type
{
$constantStrings = $formatType->getConstantStrings();
if (count($constantStrings) === 0) {
if ($formatType->isNonEmptyString()->yes()) {
return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]);
}

return null;
}

$daysIsInt = $intervalType->hasInstanceProperty('days')->yes()

Check warning on line 39 in src/Type/Php/DateIntervalFormatReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return null; } - $daysIsInt = $intervalType->hasInstanceProperty('days')->yes() + $daysIsInt = !$intervalType->hasInstanceProperty('days')->no() && (new IntegerType())->isSuperTypeOf($intervalType->getInstanceProperty('days', $scope)->getReadableType())->yes(); $dateInterval = $daysIsInt

Check warning on line 39 in src/Type/Php/DateIntervalFormatReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return null; } - $daysIsInt = $intervalType->hasInstanceProperty('days')->yes() + $daysIsInt = !$intervalType->hasInstanceProperty('days')->no() && (new IntegerType())->isSuperTypeOf($intervalType->getInstanceProperty('days', $scope)->getReadableType())->yes(); $dateInterval = $daysIsInt
&& (new IntegerType())->isSuperTypeOf($intervalType->getInstanceProperty('days', $scope)->getReadableType())->yes();

$dateInterval = $daysIsInt
? (new DateTime('2000-01-01'))->diff(new DateTime('2000-01-01'))
: new DateInterval('P0D');

$possibleReturnTypes = [];
foreach ($constantStrings as $string) {
$formatString = $string->getValue();
$value = $dateInterval->format($formatString);

$accessories = [];
if (is_numeric($value)) {
$accessories[] = new AccessoryNumericStringType();
}
if ($value !== '0' && $value !== '' && ($formatString !== '%a' || $daysIsInt)) {
$accessories[] = new AccessoryNonFalsyStringType();
} elseif ($value !== '') {
$accessories[] = new AccessoryNonEmptyStringType();
}
if (strtolower($value) === $value) {
$accessories[] = new AccessoryLowercaseStringType();
}
if (strtoupper($value) === $value) {
$accessories[] = new AccessoryUppercaseStringType();
}

if (count($accessories) === 0) {
return null;
}

$possibleReturnTypes[] = new IntersectionType([new StringType(), ...$accessories]);
}

return TypeCombinator::union(...$possibleReturnTypes);
}

}
44 changes: 38 additions & 6 deletions tests/PHPStan/Analyser/nsrt/bug-1452.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,44 @@
<?php declare(strict_types = 1);
<?php

namespace Bug1452;

use DateInterval;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use function PHPStan\Testing\assertType;

$dateInterval = (new \DateTimeImmutable('now -60 minutes'))->diff(new \DateTimeImmutable('now'));
function doFoo(): void {
$dateInterval = (new DateTimeImmutable('now -60 minutes'))->diff(new DateTimeImmutable('now'));
assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $dateInterval->format('%a'));
assertType('float|int', $dateInterval->format('%a') * 60);
}

assertType(
'lowercase-string&non-empty-string',
$dateInterval->format('%a')
);
function doBar(DateTime $a, DateTime $b): void {
$interval = $a->diff($b);
assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $interval->format('%a'));
assertType('lowercase-string&non-falsy-string&numeric-string&uppercase-string', $interval->format('%R%a'));
}

function doBaz(DateTimeInterface $a, DateTimeInterface $b): void {
$interval = $a->diff($b);
assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $interval->format('%a'));
}

function doDateDiff(DateTime $a, DateTime $b): void {
$interval = date_diff($a, $b);
assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', $interval->format('%a'));
}

function doPlainInterval(DateInterval $interval): void {
assertType('lowercase-string&non-empty-string', $interval->format('%a'));
}

function doDateIntervalFormat(DateTime $a, DateTime $b): void {
$interval = date_diff($a, $b);
assertType('lowercase-string&non-empty-string&numeric-string&uppercase-string', date_interval_format($interval, '%a'));
}

function doDateIntervalFormatPlain(DateInterval $interval): void {
assertType('lowercase-string&non-empty-string', date_interval_format($interval, '%a'));
}
Loading