From db4aba0504e088d637e645b162279263b571f6b4 Mon Sep 17 00:00:00 2001 From: Anatoly Pashin Date: Mon, 23 Nov 2020 22:28:06 +1000 Subject: [PATCH 1/2] Check datetime instantiation --- conf/bleedingEdge.neon | 1 + conf/config.level5.neon | 4 ++ conf/config.neon | 2 + src/Rules/DateTimeInstantiationRule.php | 70 +++++++++++++++++++ .../Rules/DateTimeInstantiationRuleTest.php | 49 +++++++++++++ .../Rules/data/datetime-instantiation.php | 20 ++++++ 6 files changed, 146 insertions(+) create mode 100644 src/Rules/DateTimeInstantiationRule.php create mode 100644 tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php create mode 100644 tests/PHPStan/Rules/data/datetime-instantiation.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 3206e9d330..b0abe75c56 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -7,5 +7,6 @@ parameters: fileWhitespace: true unusedClassElements: true readComposerPhpVersion: true + dateTimeInstantiation: true stubFiles: - ../stubs/SplObjectStorage.stub diff --git a/conf/config.level5.neon b/conf/config.level5.neon index a80147786e..57dc11d4a0 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -4,6 +4,8 @@ includes: conditionalTags: PHPStan\Rules\Functions\RandomIntParametersRule: phpstan.rules.rule: %featureToggles.randomIntParameters% + PHPStan\Rules\DateTimeInstantiationRule: + phpstan.rules.rule: %featureToggles.dateTimeInstantiation% parameters: checkFunctionArgumentTypes: true @@ -15,3 +17,5 @@ services: class: PHPStan\Rules\Functions\RandomIntParametersRule arguments: reportMaybes: %reportMaybes% + - + class: PHPStan\Rules\DateTimeInstantiationRule diff --git a/conf/config.neon b/conf/config.neon index d0cc9d7f7b..af1c6c18f2 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -19,6 +19,7 @@ parameters: fileWhitespace: false unusedClassElements: false readComposerPhpVersion: false + dateTimeInstantiation: false fileExtensions: - php checkAlwaysTrueCheckTypeFunctionCall: false @@ -151,6 +152,7 @@ parametersSchema: fileWhitespace: bool(), unusedClassElements: bool(), readComposerPhpVersion: bool() + dateTimeInstantiation: bool() ]) fileExtensions: listOf(string()) checkAlwaysTrueCheckTypeFunctionCall: bool() diff --git a/src/Rules/DateTimeInstantiationRule.php b/src/Rules/DateTimeInstantiationRule.php new file mode 100644 index 0000000000..03c3c634fb --- /dev/null +++ b/src/Rules/DateTimeInstantiationRule.php @@ -0,0 +1,70 @@ + + */ +class DateTimeInstantiationRule implements \PHPStan\Rules\Rule +{ + + public function getNodeType(): string + { + return New_::class; + } + + /** + * @param New_ $node + */ + public function processNode(Node $node, Scope $scope): array + { + if ( + !($node->class instanceof \PhpParser\Node\Name) + || \count($node->args) === 0 + || !\in_array(strtolower((string) $node->class), ['datetime', 'datetimeimmutable'], true) + ) { + return []; + } + + $arg = $scope->getType($node->args[0]->value); + if (!($arg instanceof ConstantStringType)) { + return []; + } + + $errors = []; + $dateString = $arg->getValue(); + try { + new DateTime($dateString); + } catch (\Throwable $e) { + // an exception is thrown for errors only but we want to catch warnings too + } + $lastErrors = DateTime::getLastErrors(); + if ($lastErrors !== false) { + foreach ($lastErrors['warnings'] as $warning) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Instantiating %s with %s produces a warning: %s', + (string) $node->class, + $dateString, + $warning + ))->build(); + } + foreach ($lastErrors['errors'] as $error) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Instantiating %s with %s produces an error: %s', + (string) $node->class, + $dateString, + $error + ))->build(); + } + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php new file mode 100644 index 0000000000..12e611a0cb --- /dev/null +++ b/tests/PHPStan/Rules/DateTimeInstantiationRuleTest.php @@ -0,0 +1,49 @@ + + */ +class DateTimeInstantiationRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + return new DateTimeInstantiationRule(); + } + + public function test(): void + { + $this->analyse( + [__DIR__ . '/data/datetime-instantiation.php'], + [ + [ + 'Instantiating DateTime with 2020.11.17 produces an error: Double time specification', + 3, + ], + [ + 'Instantiating DateTimeImmutable with asdfasdf produces a warning: Double timezone specification', + 5, + ], + [ + 'Instantiating DateTimeImmutable with asdfasdf produces an error: The timezone could not be found in the database', + 5, + ], + [ + 'Instantiating DateTimeImmutable with 2020.11.17 produces an error: Double time specification', + 10, + ], + [ + 'Instantiating DateTimeImmutable with 2020.11.18 produces an error: Double time specification', + 17, + ], + [ + 'Instantiating DateTime with 2020-04-31 produces a warning: The parsed date was invalid', + 20, + ], + ] + ); + } + +} diff --git a/tests/PHPStan/Rules/data/datetime-instantiation.php b/tests/PHPStan/Rules/data/datetime-instantiation.php new file mode 100644 index 0000000000..c529c265de --- /dev/null +++ b/tests/PHPStan/Rules/data/datetime-instantiation.php @@ -0,0 +1,20 @@ + Date: Tue, 24 Nov 2020 19:48:29 +1000 Subject: [PATCH 2/2] Fix DateTime/DateTimeImmutable::getLastErrors() return types --- resources/functionMap.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 7e31ce42a7..8ee69ec33a 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -1601,7 +1601,7 @@ 'DateTime::createFromImmutable' => ['static', 'object'=>'DateTimeImmutable'], 'DateTime::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string', 'format'=>'string'], -'DateTime::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}'], +'DateTime::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int'], 'DateTime::getTimezone' => ['DateTimeZone'], @@ -1620,7 +1620,7 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::format' => ['string', 'format'=>'string'], -'DateTimeImmutable::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}'], +'DateTimeImmutable::getLastErrors' => ['array{warning_count: int, warnings: array, error_count: int, errors: array}|false'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int'], 'DateTimeImmutable::getTimezone' => ['DateTimeZone'],