Skip to content

Commit 961f780

Browse files
authored
[dx] show type coverage only if full paths are analysed, to avoid spamming output (#51)
* Return always RuleError objects * [dx] show type coverage only if full paths are analysed, to avoid spamming output * skip in tests * cache
1 parent 8f5752b commit 961f780

9 files changed

+171
-43
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,22 @@ parameters:
111111
112112
<br>
113113
114+
## Full Paths only
115+
116+
If you run PHPStan only on some subpaths that are different from your setup in `phpstan.neon`, e.g.:
117+
118+
```bash
119+
vendor/bin/phpstan analyze src/Controller
120+
```
121+
122+
This package could show false positives, as classes in the `src/Controller` could be slightly less typed. This would be spamming whole PHPStan output and make hard to see any other errors you look for.
123+
124+
That's why this package only triggers if there are full paths, e.g.:
125+
126+
```bash
127+
vendor/bin/phpstan
128+
````
129+
130+
<br>
131+
114132
Happy coding!

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"keywords": ["static analysis", "phpstan-extension"],
77
"require": {
88
"php": "^8.2",
9-
"phpstan/phpstan": "^2.0.3"
9+
"phpstan/phpstan": "^2.1.1"
1010
},
1111
"require-dev": {
1212
"phpstan/extension-installer": "^1.4",
1313
"phpunit/phpunit": "^11.5",
14-
"symplify/easy-coding-standard": "^12.4",
15-
"rector/rector": "^2.0",
14+
"symplify/easy-coding-standard": "^12.5",
15+
"rector/rector": "^2.0.6",
1616
"tracy/tracy": "^2.10",
1717
"tomasvotruba/unused-public": "^2.0"
1818
},

phpstan.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ parameters:
2727
# used in tests
2828
- message: '#Public constant "TomasVotruba\\TypeCoverage\\(.*?)::ERROR_MESSAGE" is never#'
2929

30+
# needed to access metadata about paths
31+
-
32+
message: '#PHPStan\\Analyser\\LazyInternalScopeFactory#'
33+
identifier: phpstanApi.class
34+
35+
-
36+
message: '#PHPStan\\DependencyInjection\\Nette\\NetteContainer#'
37+
identifier: phpstanApi.method
38+
39+
-
40+
message: '#PHPStan\\DependencyInjection\\MemoizingContainer#'
41+
identifier: phpstanApi.class
42+
3043
excludePaths:
3144
- "*/Fixture/*"
3245
- "*/Source/*"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TomasVotruba\TypeCoverage\Configuration;
6+
7+
use PHPStan\Analyser\LazyInternalScopeFactory;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\DependencyInjection\MemoizingContainer;
10+
use PHPStan\DependencyInjection\Nette\NetteContainer;
11+
use ReflectionProperty;
12+
13+
/**
14+
* The easiest way to reach project configuration from Scope object
15+
*/
16+
final class ScopeConfigurationResolver
17+
{
18+
private static ?bool $areFullPathsAnalysed = null;
19+
20+
public static function areFullPathsAnalysed(Scope $scope): bool
21+
{
22+
// cache for speed
23+
if (self::$areFullPathsAnalysed !== null) {
24+
return self::$areFullPathsAnalysed;
25+
}
26+
27+
$scopeFactory = self::getPrivateProperty($scope, 'scopeFactory');
28+
29+
// different types are used in tests, there we want to always analyse everything
30+
if (! $scopeFactory instanceof LazyInternalScopeFactory) {
31+
return true;
32+
}
33+
34+
$scopeFactoryContainer = self::getPrivateProperty($scopeFactory, 'container');
35+
if (! $scopeFactoryContainer instanceof MemoizingContainer) {
36+
// edge case, unable to analyse
37+
return true;
38+
}
39+
40+
/** @var NetteContainer $originalContainer */
41+
$originalContainer = self::getPrivateProperty($scopeFactoryContainer, 'originalContainer');
42+
43+
$analysedPaths = $originalContainer->getParameter('analysedPaths');
44+
$analysedPathsFromConfig = $originalContainer->getParameter('analysedPathsFromConfig');
45+
46+
self::$areFullPathsAnalysed = $analysedPathsFromConfig === $analysedPaths;
47+
48+
return self::$areFullPathsAnalysed;
49+
}
50+
51+
private static function getPrivateProperty(object $object, string $propertyName): object
52+
{
53+
$reflectionProperty = new ReflectionProperty($object, $propertyName);
54+
55+
return $reflectionProperty->getValue($object);
56+
}
57+
}

src/Rules/ConstantTypeCoverageRule.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
use PHPStan\Analyser\Scope;
99
use PHPStan\Node\CollectedDataNode;
1010
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
12+
use PHPStan\Rules\RuleErrorBuilder;
1113
use TomasVotruba\TypeCoverage\CollectorDataNormalizer;
1214
use TomasVotruba\TypeCoverage\Collectors\ConstantTypeDeclarationCollector;
1315
use TomasVotruba\TypeCoverage\Configuration;
16+
use TomasVotruba\TypeCoverage\Configuration\ScopeConfigurationResolver;
1417
use TomasVotruba\TypeCoverage\Formatter\TypeCoverageFormatter;
1518

1619
/**
@@ -47,21 +50,26 @@ public function getNodeType(): string
4750

4851
/**
4952
* @param CollectedDataNode $node
50-
* @return mixed[]
53+
* @return RuleError[]
5154
*/
5255
public function processNode(Node $node, Scope $scope): array
5356
{
57+
// if only subpaths are analysed, skip as data will be false positive
58+
if (! ScopeConfigurationResolver::areFullPathsAnalysed($scope)) {
59+
return [];
60+
}
61+
5462
$constantTypeDeclarationCollector = $node->get(ConstantTypeDeclarationCollector::class);
5563
$typeCountAndMissingTypes = $this->collectorDataNormalizer->normalize($constantTypeDeclarationCollector);
5664

5765
if ($this->configuration->showOnlyMeasure()) {
58-
return [
59-
sprintf(
60-
'Class constant type coverage is %.1f %% out of %d possible',
61-
$typeCountAndMissingTypes->getCoveragePercentage(),
62-
$typeCountAndMissingTypes->getTotalCount()
63-
),
64-
];
66+
$errorMessage = sprintf(
67+
'Class constant type coverage is %.1f %% out of %d possible',
68+
$typeCountAndMissingTypes->getCoveragePercentage(),
69+
$typeCountAndMissingTypes->getTotalCount()
70+
);
71+
72+
return [RuleErrorBuilder::message($errorMessage)->build()];
6573
}
6674

6775
if (! $this->configuration->isConstantTypeCoverageEnabled()) {

src/Rules/DeclareCoverageRule.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
use PHPStan\Analyser\Scope;
99
use PHPStan\Node\CollectedDataNode;
1010
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
1112
use PHPStan\Rules\RuleErrorBuilder;
1213
use TomasVotruba\TypeCoverage\Collectors\DeclareCollector;
1314
use TomasVotruba\TypeCoverage\Configuration;
15+
use TomasVotruba\TypeCoverage\Configuration\ScopeConfigurationResolver;
1416

1517
/**
1618
* @see \TomasVotruba\TypeCoverage\Tests\Rules\DeclareCoverageRule\DeclareCoverageRuleTest
@@ -39,10 +41,15 @@ public function getNodeType(): string
3941

4042
/**
4143
* @param CollectedDataNode $node
42-
* @return mixed[]
44+
* @return RuleError[]
4345
*/
4446
public function processNode(Node $node, Scope $scope): array
4547
{
48+
// if only subpaths are analysed, skip as data will be false positive
49+
if (! ScopeConfigurationResolver::areFullPathsAnalysed($scope)) {
50+
return [];
51+
}
52+
4653
$requiredDeclareLevel = $this->configuration->getRequiredDeclareLevel();
4754

4855
$declareCollector = $node->get(DeclareCollector::class);
@@ -63,13 +70,12 @@ public function processNode(Node $node, Scope $scope): array
6370
$declareCoverage = ($coveredDeclares / $totalPossibleDeclares) * 100;
6471

6572
if ($this->configuration->showOnlyMeasure()) {
66-
return [
67-
sprintf(
68-
'Strict declares coverage is %.1f %% out of %d possible',
69-
$declareCoverage,
70-
$totalPossibleDeclares
71-
),
72-
];
73+
$errorMessage = sprintf(
74+
'Strict declares coverage is %.1f %% out of %d possible',
75+
$declareCoverage,
76+
$totalPossibleDeclares
77+
);
78+
return [RuleErrorBuilder::message($errorMessage)->build()];
7379
}
7480

7581
// not enabled

src/Rules/ParamTypeCoverageRule.php

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
use PHPStan\Analyser\Scope;
99
use PHPStan\Node\CollectedDataNode;
1010
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
12+
use PHPStan\Rules\RuleErrorBuilder;
1113
use TomasVotruba\TypeCoverage\CollectorDataNormalizer;
1214
use TomasVotruba\TypeCoverage\Collectors\ParamTypeDeclarationCollector;
1315
use TomasVotruba\TypeCoverage\Configuration;
16+
use TomasVotruba\TypeCoverage\Configuration\ScopeConfigurationResolver;
1417
use TomasVotruba\TypeCoverage\Formatter\TypeCoverageFormatter;
1518

1619
/**
@@ -47,22 +50,30 @@ public function getNodeType(): string
4750

4851
/**
4952
* @param CollectedDataNode $node
50-
* @return mixed[]
53+
* @return RuleError[]
5154
*/
5255
public function processNode(Node $node, Scope $scope): array
5356
{
57+
// if only subpaths are analysed, skip as data will be false positive
58+
if (! ScopeConfigurationResolver::areFullPathsAnalysed($scope)) {
59+
return [];
60+
}
61+
5462
$paramTypeDeclarationCollector = $node->get(ParamTypeDeclarationCollector::class);
5563

5664
$typeCountAndMissingTypes = $this->collectorDataNormalizer->normalize($paramTypeDeclarationCollector);
5765

5866
if ($this->configuration->showOnlyMeasure()) {
59-
return [
60-
sprintf(
61-
'Param type coverage is %.1f %% out of %d possible',
62-
$typeCountAndMissingTypes->getCoveragePercentage(),
63-
$typeCountAndMissingTypes->getTotalCount()
64-
),
65-
];
67+
$errorMessage = sprintf(
68+
'Param type coverage is %.1f %% out of %d possible',
69+
$typeCountAndMissingTypes->getCoveragePercentage(),
70+
$typeCountAndMissingTypes->getTotalCount()
71+
);
72+
73+
$ruleError = RuleErrorBuilder::message($errorMessage)
74+
->build();
75+
76+
return [$ruleError];
6677
}
6778

6879
if ($this->configuration->getRequiredParamTypeLevel() === 0) {

src/Rules/PropertyTypeCoverageRule.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
use PHPStan\Analyser\Scope;
99
use PHPStan\Node\CollectedDataNode;
1010
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
12+
use PHPStan\Rules\RuleErrorBuilder;
1113
use TomasVotruba\TypeCoverage\CollectorDataNormalizer;
1214
use TomasVotruba\TypeCoverage\Collectors\PropertyTypeDeclarationCollector;
1315
use TomasVotruba\TypeCoverage\Configuration;
16+
use TomasVotruba\TypeCoverage\Configuration\ScopeConfigurationResolver;
1417
use TomasVotruba\TypeCoverage\Formatter\TypeCoverageFormatter;
1518

1619
/**
@@ -47,21 +50,26 @@ public function getNodeType(): string
4750

4851
/**
4952
* @param CollectedDataNode $node
50-
* @return mixed[]
53+
* @return RuleError[]
5154
*/
5255
public function processNode(Node $node, Scope $scope): array
5356
{
57+
// if only subpaths are analysed, skip as data will be false positive
58+
if (! ScopeConfigurationResolver::areFullPathsAnalysed($scope)) {
59+
return [];
60+
}
61+
5462
$propertyTypeDeclarationCollector = $node->get(PropertyTypeDeclarationCollector::class);
5563
$typeCountAndMissingTypes = $this->collectorDataNormalizer->normalize($propertyTypeDeclarationCollector);
5664

5765
if ($this->configuration->showOnlyMeasure()) {
58-
return [
59-
sprintf(
60-
'Property type coverage is %.1f %% out of %d possible',
61-
$typeCountAndMissingTypes->getCoveragePercentage(),
62-
$typeCountAndMissingTypes->getTotalCount()
63-
),
64-
];
66+
$errorMessage = sprintf(
67+
'Property type coverage is %.1f %% out of %d possible',
68+
$typeCountAndMissingTypes->getCoveragePercentage(),
69+
$typeCountAndMissingTypes->getTotalCount()
70+
);
71+
72+
return [RuleErrorBuilder::message($errorMessage)->build()];
6573
}
6674

6775
if ($this->configuration->getRequiredPropertyTypeLevel() === 0) {

src/Rules/ReturnTypeCoverageRule.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
use PHPStan\Analyser\Scope;
99
use PHPStan\Node\CollectedDataNode;
1010
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
12+
use PHPStan\Rules\RuleErrorBuilder;
1113
use TomasVotruba\TypeCoverage\CollectorDataNormalizer;
1214
use TomasVotruba\TypeCoverage\Collectors\ReturnTypeDeclarationCollector;
1315
use TomasVotruba\TypeCoverage\Configuration;
16+
use TomasVotruba\TypeCoverage\Configuration\ScopeConfigurationResolver;
1417
use TomasVotruba\TypeCoverage\Formatter\TypeCoverageFormatter;
1518

1619
/**
@@ -47,21 +50,25 @@ public function getNodeType(): string
4750

4851
/**
4952
* @param CollectedDataNode $node
50-
* @return mixed[]
53+
* @return RuleError[]
5154
*/
5255
public function processNode(Node $node, Scope $scope): array
5356
{
57+
// if only subpaths are analysed, skip as data will be false positive
58+
if (! ScopeConfigurationResolver::areFullPathsAnalysed($scope)) {
59+
return [];
60+
}
61+
5462
$returnSeaLevelDataByFilePath = $node->get(ReturnTypeDeclarationCollector::class);
5563
$typeCountAndMissingTypes = $this->collectorDataNormalizer->normalize($returnSeaLevelDataByFilePath);
5664

5765
if ($this->configuration->showOnlyMeasure()) {
58-
return [
59-
sprintf(
60-
'Return type coverage is %.1f %% out of %d possible',
61-
$typeCountAndMissingTypes->getCoveragePercentage(),
62-
$typeCountAndMissingTypes->getTotalCount()
63-
),
64-
];
66+
$errorMessage = sprintf(
67+
'Return type coverage is %.1f %% out of %d possible',
68+
$typeCountAndMissingTypes->getCoveragePercentage(),
69+
$typeCountAndMissingTypes->getTotalCount()
70+
);
71+
return [RuleErrorBuilder::message($errorMessage)->build()];
6572
}
6673

6774
if ($this->configuration->getRequiredReturnTypeLevel() === 0) {

0 commit comments

Comments
 (0)