Skip to content

Commit ab0b1ae

Browse files
phpstan-botstaabmclaude
authored
Report invalid DateInterval constructor arguments at analysis time (#5587)
Co-authored-by: staabm <120441+staabm@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Markus Staab <maggus.staab@googlemail.com>
1 parent 37e8fe0 commit ab0b1ae

7 files changed

Lines changed: 159 additions & 0 deletions

File tree

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ parameters:
1616
reportNestedTooWideType: false # tmp
1717
assignToByRefForeachExpr: true
1818
curlSetOptArrayTypes: true
19+
checkDateIntervalConstructor: true

conf/config.level5.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ conditionalTags:
1010
phpstan.rules.rule: %featureToggles.checkParameterCastableToNumberFunctions%
1111
PHPStan\Rules\Functions\PrintfParameterTypeRule:
1212
phpstan.rules.rule: %featureToggles.checkPrintfParameterTypes%
13+
PHPStan\Rules\DateIntervalInstantiationRule:
14+
phpstan.rules.rule: %featureToggles.checkDateIntervalConstructor%
1315

1416
autowiredAttributeServices:
1517
# registers rules with #[RegisteredRule] attribute
@@ -22,3 +24,5 @@ services:
2224
class: PHPStan\Rules\Functions\PrintfParameterTypeRule
2325
arguments:
2426
checkStrictPrintfPlaceholderTypes: %checkStrictPrintfPlaceholderTypes%
27+
-
28+
class: PHPStan\Rules\DateIntervalInstantiationRule

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ parameters:
4343
reportNestedTooWideType: false
4444
assignToByRefForeachExpr: false
4545
curlSetOptArrayTypes: false
46+
checkDateIntervalConstructor: false
4647
fileExtensions:
4748
- php
4849
checkAdvancedIsset: false

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ parametersSchema:
4545
reportNestedTooWideType: bool()
4646
assignToByRefForeachExpr: bool()
4747
curlSetOptArrayTypes: bool()
48+
checkDateIntervalConstructor: bool()
4849
])
4950
fileExtensions: listOf(string())
5051
checkAdvancedIsset: bool()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules;
4+
5+
use DateInterval;
6+
use PhpParser\Node;
7+
use PhpParser\Node\Expr\New_;
8+
use PHPStan\Analyser\Scope;
9+
use Throwable;
10+
use function count;
11+
use function sprintf;
12+
use function strtolower;
13+
14+
/**
15+
* @implements Rule<Node\Expr\New_>
16+
*/
17+
final class DateIntervalInstantiationRule implements Rule
18+
{
19+
20+
public function getNodeType(): string
21+
{
22+
return New_::class;
23+
}
24+
25+
/**
26+
* @param New_ $node
27+
*/
28+
public function processNode(Node $node, Scope $scope): array
29+
{
30+
if (!$node->class instanceof Node\Name) {
31+
return [];
32+
}
33+
34+
if (
35+
count($node->getArgs()) === 0
36+
|| strtolower((string) $node->class) !== 'dateinterval'
37+
) {
38+
return [];
39+
}
40+
41+
$arg = $scope->getType($node->getArgs()[0]->value);
42+
$errors = [];
43+
44+
foreach ($arg->getConstantStrings() as $constantString) {
45+
$dateIntervalString = $constantString->getValue();
46+
try {
47+
new DateInterval($dateIntervalString);
48+
} catch (Throwable $e) {
49+
$errors[] = RuleErrorBuilder::message(sprintf(
50+
'Instantiating DateInterval with %s produces an error: %s',
51+
$dateIntervalString,
52+
$e->getMessage(),
53+
))->identifier('new.dateInterval')->build();
54+
}
55+
}
56+
57+
return $errors;
58+
}
59+
60+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules;
4+
5+
use PHPStan\Testing\RuleTestCase;
6+
use const PHP_VERSION_ID;
7+
8+
/**
9+
* @extends RuleTestCase<DateIntervalInstantiationRule>
10+
*/
11+
class DateIntervalInstantiationRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new DateIntervalInstantiationRule();
17+
}
18+
19+
public function test(): void
20+
{
21+
if (PHP_VERSION_ID < 80100) {
22+
$prefix = 'DateInterval::__construct(): ';
23+
} else {
24+
$prefix = '';
25+
}
26+
27+
$this->analyse(
28+
[__DIR__ . '/data/date-interval-instantiation.php'],
29+
[
30+
[
31+
'Instantiating DateInterval with 1M produces an error: ' . $prefix . 'Unknown or bad format (1M)',
32+
5,
33+
],
34+
[
35+
'Instantiating DateInterval with asdfasdf produces an error: ' . $prefix . 'Unknown or bad format (asdfasdf)',
36+
18,
37+
],
38+
[
39+
'Instantiating DateInterval with produces an error: ' . $prefix . 'Unknown or bad format ()',
40+
21,
41+
],
42+
[
43+
'Instantiating DateInterval with 1M produces an error: ' . $prefix . 'Unknown or bad format (1M)',
44+
30,
45+
],
46+
[
47+
'Instantiating DateInterval with invalid produces an error: ' . $prefix . 'Unknown or bad format (invalid)',
48+
37,
49+
],
50+
],
51+
);
52+
}
53+
54+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
namespace DateIntervalInstantiation;
3+
4+
// invalid - missing P prefix
5+
new \DateInterval('1M');
6+
7+
// valid durations
8+
new \DateInterval('P1Y');
9+
new \DateInterval('P1M');
10+
new \DateInterval('P1D');
11+
new \DateInterval('PT1H');
12+
new \DateInterval('PT1M');
13+
new \DateInterval('PT1S');
14+
new \DateInterval('P1Y2M3DT4H5M6S');
15+
new \DateInterval('P7D');
16+
17+
// invalid
18+
new \DateInterval('asdfasdf');
19+
20+
// empty string is invalid
21+
new \DateInterval('');
22+
23+
// non-constant string - should not report
24+
function foo(string $duration): void {
25+
new \DateInterval($duration);
26+
}
27+
28+
// constant string via variable
29+
$test = '1M';
30+
new \DateInterval($test);
31+
32+
/**
33+
* @param 'invalid' $duration2
34+
*/
35+
function bar(string $duration, string $duration2): void {
36+
new \DateInterval($duration);
37+
new \DateInterval($duration2);
38+
}

0 commit comments

Comments
 (0)