Skip to content

Commit a69e3bc

Browse files
committed
Bleeding edge - check var tag type against native type
1 parent ce24c83 commit a69e3bc

18 files changed

+160
-20
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ parameters:
2727
invarianceComposition: true
2828
alwaysTrueAlwaysReported: true
2929
disableUnreachableBranchesRules: true
30+
varTagType: true

conf/config.level2.neon

+6-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ rules:
4242
- PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule
4343
- PHPStan\Rules\PhpDoc\InvalidPHPStanDocTagRule
4444
- PHPStan\Rules\PhpDoc\InvalidThrowsPhpDocValueRule
45-
- PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule
4645
- PHPStan\Rules\Properties\AccessPrivatePropertyThroughStaticRule
4746

4847
conditionalTags:
@@ -75,3 +74,9 @@ services:
7574
checkMissingVarTagTypehint: %checkMissingVarTagTypehint%
7675
tags:
7776
- phpstan.rules.rule
77+
-
78+
class: PHPStan\Rules\PhpDoc\WrongVariableNameInVarTagRule
79+
arguments:
80+
checkTypeAgainstNativeType: %featureToggles.varTagType%
81+
tags:
82+
- phpstan.rules.rule

conf/config.neon

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ parameters:
5757
invarianceComposition: false
5858
alwaysTrueAlwaysReported: false
5959
disableUnreachableBranchesRules: false
60+
varTagType: false
6061
fileExtensions:
6162
- php
6263
checkAdvancedIsset: false
@@ -280,6 +281,7 @@ parametersSchema:
280281
invarianceComposition: bool()
281282
alwaysTrueAlwaysReported: bool()
282283
disableUnreachableBranchesRules: bool()
284+
varTagType: bool()
283285
])
284286
fileExtensions: listOf(string())
285287
checkAdvancedIsset: bool()

phpstan-baseline.neon

+20
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ parameters:
7676
count: 1
7777
path: src/Command/CommandHelper.php
7878

79+
-
80+
message: "#^PHPDoc tag @var with type array\\<callable\\>\\|false is not subtype of native type array\\.$#"
81+
count: 2
82+
path: src/Command/CommandHelper.php
83+
7984
-
8085
message: "#^Parameter \\#1 \\$path of function dirname expects string, string\\|false given\\.$#"
8186
count: 1
@@ -335,6 +340,11 @@ parameters:
335340
count: 1
336341
path: src/Reflection/InitializerExprTypeResolver.php
337342

343+
-
344+
message: "#^PHPDoc tag @var with type float\\|int is not subtype of native type int\\.$#"
345+
count: 1
346+
path: src/Reflection/InitializerExprTypeResolver.php
347+
338348
-
339349
message: "#^Creating new PHPStan\\\\Php8StubsMap is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
340350
count: 1
@@ -403,6 +413,16 @@ parameters:
403413
count: 1
404414
path: src/Testing/PHPStanTestCase.php
405415

416+
-
417+
message: "#^PHPDoc tag @var with type float\\|int is not subtype of native type int\\.$#"
418+
count: 3
419+
path: src/Type/Constant/ConstantArrayTypeBuilder.php
420+
421+
-
422+
message: "#^PHPDoc tag @var with type int\\|string is not subtype of native type string\\.$#"
423+
count: 1
424+
path: src/Type/Constant/ConstantStringType.php
425+
406426
-
407427
message: """
408428
#^Call to deprecated method getInstance\\(\\) of class PHPStan\\\\Broker\\\\Broker\\:

src/Command/CommandHelper.php

-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace PHPStan\Command;
44

5-
use Closure;
65
use Composer\XdebugHandler\XdebugHandler;
76
use Nette\DI\Helpers;
87
use Nette\DI\InvalidConfigurationException;
@@ -91,7 +90,6 @@ public static function begin(
9190
{
9291
$stdOutput = new SymfonyOutput($output, new SymfonyStyle(new ErrorsConsoleStyle($input, $output)));
9392

94-
/** @var Output $errorOutput */
9593
$errorOutput = (static function () use ($input, $output): Output {
9694
$symfonyErrorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
9795
return new SymfonyOutput($symfonyErrorOutput, new SymfonyStyle(new ErrorsConsoleStyle($input, $symfonyErrorOutput)));
@@ -474,7 +472,6 @@ public static function begin(
474472
/** @var StubFilesProvider $stubFilesProvider */
475473
$stubFilesProvider = $container->getByType(StubFilesProvider::class);
476474

477-
/** @var Closure(): array{string[], bool} $filesCallback */
478475
$filesCallback = static function () use ($currentWorkingDirectoryFileHelper, $stubFilesProvider, $fileFinder, $pathRoutingParser, $paths): array {
479476
$fileFinderResult = $fileFinder->findFiles($paths);
480477
$files = $fileFinderResult->getFiles();

src/Command/FixerApplication.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public function run(
120120
/** @var string $serverAddress */
121121
$serverAddress = $server->getAddress();
122122

123-
/** @var int $serverPort */
123+
/** @var int<0, 65535> $serverPort */
124124
$serverPort = parse_url($serverAddress, PHP_URL_PORT);
125125

126126
$reanalyseProcessQueue = new RunnableQueue(

src/File/FuzzyRelativePathHelper.php

-2
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,11 @@ public function __construct(
6161
) {
6262
[$pathBeginning, $currentWorkingDirectory] = $trimBeginning($currentWorkingDirectory);
6363

64-
/** @var string[] $pathToTrimArray */
6564
$pathToTrimArray = explode($directorySeparator, $currentWorkingDirectory);
6665
}
6766
foreach ($analysedPaths as $pathNumber => $path) {
6867
[$tempPathBeginning, $path] = $trimBeginning($path);
6968

70-
/** @var string[] $pathArray */
7169
$pathArray = explode($directorySeparator, $path);
7270
$pathTempParts = [];
7371
$pathArraySize = count($pathArray);

src/Parallel/ParallelAnalyser.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public function analyse(
9999
/** @var string $serverAddress */
100100
$serverAddress = $server->getAddress();
101101

102-
/** @var int $serverPort */
102+
/** @var int<0, 65535> $serverPort */
103103
$serverPort = parse_url($serverAddress, PHP_URL_PORT);
104104

105105
$internalErrorsCount = 0;

src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php

-1
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,6 @@ private function locateClassByName(string $className): ?array
334334
$this->silenceErrors();
335335

336336
try {
337-
/** @var array{string[], string, null}|null */
338337
$result = FileReadTrapStreamWrapper::withStreamWrapperOverride(
339338
static function () use ($className): ?array {
340339
$functions = spl_autoload_functions();

src/Reflection/FunctionVariantWithPhpDocs.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function __construct(
3737
*/
3838
public function getParameters(): array
3939
{
40-
/** @var ParameterReflectionWithPhpDocs[] $parameters */
40+
/** @var array<int, ParameterReflectionWithPhpDocs> $parameters */
4141
$parameters = parent::getParameters();
4242

4343
return $parameters;

src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php

+32-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
use PHPStan\Rules\RuleError;
1616
use PHPStan\Rules\RuleErrorBuilder;
1717
use PHPStan\ShouldNotHappenException;
18+
use PHPStan\Type\ConstantType;
1819
use PHPStan\Type\FileTypeMapper;
20+
use PHPStan\Type\Generic\GenericObjectType;
21+
use PHPStan\Type\ObjectType;
22+
use PHPStan\Type\VerbosityLevel;
1923
use function array_keys;
2024
use function array_map;
2125
use function array_merge;
@@ -34,6 +38,7 @@ class WrongVariableNameInVarTagRule implements Rule
3438

3539
public function __construct(
3640
private FileTypeMapper $fileTypeMapper,
41+
private bool $checkTypeAgainstNativeType,
3742
)
3843
{
3944
}
@@ -127,12 +132,36 @@ public function processNode(Node $node, Scope $scope): array
127132
* @param VarTag[] $varTags
128133
* @return RuleError[]
129134
*/
130-
private function processAssign(Scope $scope, Node\Expr $var, array $varTags): array
135+
private function processAssign(Scope $scope, Node\Expr $var, Node\Expr $expr, array $varTags): array
131136
{
132137
$errors = [];
133138
$hasMultipleMessage = false;
134139
$assignedVariables = $this->getAssignedVariables($var);
135-
foreach (array_keys($varTags) as $key) {
140+
foreach ($varTags as $key => $varTag) {
141+
if ($this->checkTypeAgainstNativeType) {
142+
$exprNativeType = $scope->getNativeType($expr);
143+
if ($expr instanceof Expr\New_) {
144+
if ($exprNativeType instanceof GenericObjectType) {
145+
$exprNativeType = new ObjectType($exprNativeType->getClassName());
146+
}
147+
}
148+
149+
if ($exprNativeType instanceof ConstantType) {
150+
$report = $exprNativeType->isSuperTypeOf($varTag->getType())->no();
151+
} else {
152+
$report = !$exprNativeType->isSuperTypeOf($varTag->getType())->yes();
153+
}
154+
155+
if ($report) {
156+
$verbosity = VerbosityLevel::getRecommendedLevelByType($exprNativeType, $varTag->getType());
157+
$errors[] = RuleErrorBuilder::message(sprintf(
158+
'PHPDoc tag @var with type %s is not subtype of native type %s.',
159+
$varTag->getType()->describe($verbosity),
160+
$exprNativeType->describe($verbosity),
161+
))->build();
162+
}
163+
}
164+
136165
if (is_int($key)) {
137166
if (count($varTags) !== 1) {
138167
if (!$hasMultipleMessage) {
@@ -289,7 +318,7 @@ private function processStatic(array $vars, array $varTags): array
289318
private function processExpression(Scope $scope, Expr $expr, array $varTags): array
290319
{
291320
if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) {
292-
return $this->processAssign($scope, $expr->var, $varTags);
321+
return $this->processAssign($scope, $expr->var, $expr->expr, $varTags);
293322
}
294323

295324
return $this->processStmt($scope, $varTags, null);

src/Type/ClosureTypeFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public function fromClosureObject(Closure $closure): ClosureType
5454
throw new ShouldNotHappenException('Closure reflection not found.');
5555
}
5656

57-
/** @var \PHPStan\BetterReflection\Reflection\ReflectionFunction[] $reflections */
57+
/** @var list<\PHPStan\BetterReflection\Reflection\ReflectionFunction> $reflections */
5858
$reflections = $find($this->reflector, $ast, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), $locatedSource);
5959
if (count($reflections) !== 1) {
6060
throw new ShouldNotHappenException('Closure reflection not found.');

src/Type/Constant/ConstantStringType.php

-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,6 @@ public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope)
245245
public function toNumber(): Type
246246
{
247247
if (is_numeric($this->value)) {
248-
/** @var mixed $value */
249248
$value = $this->value;
250249
$value = +$value;
251250
if (is_float($value)) {

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

+12-4
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,9 @@ public function testBug6375(): void
545545
public function testBug6501(): void
546546
{
547547
$errors = $this->runAnalyse(__DIR__ . '/data/bug-6501.php');
548-
$this->assertNoErrors($errors);
548+
$this->assertCount(1, $errors);
549+
$this->assertSame('PHPDoc tag @var with type R of Exception|stdClass is not subtype of native type stdClass.', $errors[0]->getMessage());
550+
$this->assertSame(24, $errors[0]->getLine());
549551
}
550552

551553
public function testBug6114(): void
@@ -591,9 +593,15 @@ public function testBug6649(): void
591593
public function testBug6842(): void
592594
{
593595
$errors = $this->runAnalyse(__DIR__ . '/data/bug-6842.php');
594-
$this->assertCount(1, $errors);
595-
$this->assertSame('Generator expects value type T of DateTimeInterface, DateTime|DateTimeImmutable|T of DateTimeInterface given.', $errors[0]->getMessage());
596-
$this->assertSame(28, $errors[0]->getLine());
596+
$this->assertCount(3, $errors);
597+
$this->assertSame('PHPDoc tag @var with type Iterator<mixed, T of DateTimeInterface> is not subtype of native type DatePeriod.', $errors[0]->getMessage());
598+
$this->assertSame(22, $errors[0]->getLine());
599+
600+
$this->assertSame('Generator expects value type T of DateTimeInterface, DateTime|DateTimeImmutable|T of DateTimeInterface given.', $errors[1]->getMessage());
601+
$this->assertSame(28, $errors[1]->getLine());
602+
603+
$this->assertSame('Generator expects value type T of DateTimeInterface, DateTime|DateTimeImmutable|T of DateTimeInterface given.', $errors[2]->getMessage());
604+
$this->assertSame(54, $errors[2]->getLine());
597605
}
598606

599607
public function testBug6896(): void

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ public function testClassMethodScope(): void
6565

6666
private function getFileScope(string $filename): Scope
6767
{
68-
/** @var Scope $testScope */
6968
$testScope = null;
7069
$this->processFile($filename, static function (Node $node, Scope $scope) use (&$testScope): void {
7170
if (!($node instanceof Exit_)) {
@@ -75,6 +74,7 @@ private function getFileScope(string $filename): Scope
7574
$testScope = $scope;
7675
});
7776

77+
/** @var Scope */
7878
return $testScope;
7979
}
8080

tests/PHPStan/Analyser/data/bug-6842.php

+26
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,32 @@ public function getScheduledEvents(
3030
}
3131
}
3232

33+
/**
34+
* @template T of \DateTimeInterface|\DateTime|\DateTimeImmutable
35+
*
36+
* @param T $startDate
37+
* @param T $endDate
38+
*
39+
* @return \Iterator<T>
40+
*/
41+
public function getScheduledEvents2(
42+
\DateTimeInterface $startDate,
43+
\DateTimeInterface $endDate
44+
): \Iterator {
45+
$interval = \DateInterval::createFromDateString('1 day');
46+
47+
/** @var \DatePeriod<\DateTimeInterface, \DateTimeInterface, null>&iterable<T> $datePeriod */
48+
$datePeriod = new \DatePeriod($startDate, $interval, $endDate);
49+
50+
foreach ($datePeriod as $dateTime) {
51+
$scheduledEvent = $this->createScheduledEventFromSchedule($dateTime);
52+
53+
if ($scheduledEvent >= $startDate) {
54+
yield $scheduledEvent;
55+
}
56+
}
57+
}
58+
3359
/**
3460
* @template T of \DateTimeInterface
3561
*

tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php

+22
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
class WrongVariableNameInVarTagRuleTest extends RuleTestCase
1414
{
1515

16+
private bool $checkTypeAgainstNativeType = false;
17+
1618
protected function getRule(): Rule
1719
{
1820
return new WrongVariableNameInVarTagRule(
1921
self::getContainer()->getByType(FileTypeMapper::class),
22+
$this->checkTypeAgainstNativeType,
2023
);
2124
}
2225

@@ -188,4 +191,23 @@ public function testEnums(): void
188191
]);
189192
}
190193

194+
public function testReportWrongType(): void
195+
{
196+
$this->checkTypeAgainstNativeType = true;
197+
$this->analyse([__DIR__ . '/data/wrong-var-native-type.php'], [
198+
[
199+
'PHPDoc tag @var with type string|null is not subtype of native type string.',
200+
14,
201+
],
202+
[
203+
'PHPDoc tag @var with type stdClass is not subtype of native type SplObjectStorage.',
204+
23,
205+
],
206+
[
207+
'PHPDoc tag @var with type int is not subtype of native type \'foo\'.',
208+
26,
209+
],
210+
]);
211+
}
212+
191213
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace WrongVarNativeType;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(): void
9+
{
10+
/** @var 'a' $a */
11+
$a = $this->doBar();
12+
13+
/** @var string|null $stringOrNull */
14+
$stringOrNull = $this->doBar();
15+
16+
/** @var string|null $null */
17+
$null = null;
18+
19+
/** @var \SplObjectStorage<\stdClass, array{int, string}> $running */
20+
$running = new \SplObjectStorage();
21+
22+
/** @var \stdClass $running2 */
23+
$running2 = new \SplObjectStorage();
24+
25+
/** @var int $int */
26+
$int = 'foo';
27+
}
28+
29+
public function doBar(): string
30+
{
31+
32+
}
33+
34+
}

0 commit comments

Comments
 (0)