Skip to content
Merged
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
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ parameters:
reportNestedTooWideType: false # tmp
assignToByRefForeachExpr: true
curlSetOptArrayTypes: true
checkDateIntervalConstructor: true
4 changes: 4 additions & 0 deletions conf/config.level5.neon
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions%
PHPStan\Rules\Functions\PrintfParameterTypeRule:
phpstan.rules.rule: %featureToggles.checkPrintfParameterTypes%
PHPStan\Rules\DateIntervalInstantiationRule:
phpstan.rules.rule: %featureToggles.checkDateIntervalConstructor%

autowiredAttributeServices:
# registers rules with #[RegisteredRule] attribute
Expand All @@ -22,3 +24,5 @@ services:
class: PHPStan\Rules\Functions\PrintfParameterTypeRule
arguments:
checkStrictPrintfPlaceholderTypes: %checkStrictPrintfPlaceholderTypes%
-
class: PHPStan\Rules\DateIntervalInstantiationRule
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ parameters:
reportNestedTooWideType: false
assignToByRefForeachExpr: false
curlSetOptArrayTypes: false
checkDateIntervalConstructor: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ parametersSchema:
reportNestedTooWideType: bool()
assignToByRefForeachExpr: bool()
curlSetOptArrayTypes: bool()
checkDateIntervalConstructor: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
60 changes: 60 additions & 0 deletions src/Rules/DateIntervalInstantiationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use DateInterval;
use PhpParser\Node;
use PhpParser\Node\Expr\New_;
use PHPStan\Analyser\Scope;
use Throwable;
use function count;
use function sprintf;
use function strtolower;

/**
* @implements Rule<Node\Expr\New_>
*/
final class DateIntervalInstantiationRule implements Rule
{

public function getNodeType(): string
{
return New_::class;
}

/**
* @param New_ $node
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->class instanceof Node\Name) {
return [];
}

if (
count($node->getArgs()) === 0
|| strtolower((string) $node->class) !== 'dateinterval'
) {
return [];
}

$arg = $scope->getType($node->getArgs()[0]->value);
$errors = [];

foreach ($arg->getConstantStrings() as $constantString) {
$dateIntervalString = $constantString->getValue();
try {
new DateInterval($dateIntervalString);
} catch (Throwable $e) {
$errors[] = RuleErrorBuilder::message(sprintf(
'Instantiating DateInterval with %s produces an error: %s',
$dateIntervalString,
$e->getMessage(),
))->identifier('new.dateInterval')->build();
}
}

return $errors;
}

}
54 changes: 54 additions & 0 deletions tests/PHPStan/Rules/DateIntervalInstantiationRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use PHPStan\Testing\RuleTestCase;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<DateIntervalInstantiationRule>
*/
class DateIntervalInstantiationRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new DateIntervalInstantiationRule();
}

public function test(): void
Comment thread
staabm marked this conversation as resolved.
{
if (PHP_VERSION_ID < 80100) {
$prefix = 'DateInterval::__construct(): ';
} else {
$prefix = '';
}

$this->analyse(
[__DIR__ . '/data/date-interval-instantiation.php'],
[
[
'Instantiating DateInterval with 1M produces an error: ' . $prefix . 'Unknown or bad format (1M)',
5,
],
[
'Instantiating DateInterval with asdfasdf produces an error: ' . $prefix . 'Unknown or bad format (asdfasdf)',
18,
],
[
'Instantiating DateInterval with produces an error: ' . $prefix . 'Unknown or bad format ()',
21,
],
[
'Instantiating DateInterval with 1M produces an error: ' . $prefix . 'Unknown or bad format (1M)',
30,
],
[
'Instantiating DateInterval with invalid produces an error: ' . $prefix . 'Unknown or bad format (invalid)',
37,
],
],
);
}

}
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/data/date-interval-instantiation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
namespace DateIntervalInstantiation;

// invalid - missing P prefix
new \DateInterval('1M');

// valid durations
new \DateInterval('P1Y');
new \DateInterval('P1M');
new \DateInterval('P1D');
new \DateInterval('PT1H');
new \DateInterval('PT1M');
new \DateInterval('PT1S');
new \DateInterval('P1Y2M3DT4H5M6S');
new \DateInterval('P7D');

// invalid
new \DateInterval('asdfasdf');

// empty string is invalid
new \DateInterval('');

// non-constant string - should not report
function foo(string $duration): void {
new \DateInterval($duration);
}

// constant string via variable
$test = '1M';
new \DateInterval($test);

/**
* @param 'invalid' $duration2
*/
function bar(string $duration, string $duration2): void {
new \DateInterval($duration);
new \DateInterval($duration2);
}
Loading