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
18 changes: 18 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1668,6 +1668,24 @@ jobs:
../bashunit assert contains 'Result cache not used because extension file' "$OUTPUT"
../bashunit assert contains 'the rule says another thing' "$OUTPUT"
../bashunit assert not_contains 'the rule says one thing' "$OUTPUT"
- script: |
cd e2e/result-cache-extension-attribute-services
composer install
# The rule is registered through the attributeServicesDirectories section, not in services:.
# Editing it has to invalidate the cache, editing another class in the same directory does not.
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit assert contains 'the rule says one thing' "$OUTPUT"
patch -p0 < foo.patch
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit assert contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
patch -p0 < rule.patch
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit assert contains 'Result cache not used because extension file' "$OUTPUT"
../bashunit assert contains 'the rule says another thing' "$OUTPUT"
../bashunit assert not_contains 'the rule says one thing' "$OUTPUT"
- script: |
cd e2e/result-cache-extension-copied-package
composer install
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ lint:
--exclude tests/PHPStan/Rules/Operators/data/invalid-assign-var.php \
--exclude tests/PHPStan/Rules/Functions/data/arrow-function-nullsafe-by-ref.php \
--exclude tests/PHPStan/Levels/data/namedArguments.php \
--exclude tests/PHPStan/DependencyInjection/AttributeServices/data \
--exclude tests/PHPStan/Reflection/BetterReflection/SourceLocator/data/symbol-finder/class-name-before-global-constant.php \
--exclude tests/PHPStan/Rules/Keywords/data/continue-break.php \
--exclude tests/PHPStan/Rules/Keywords/data/continue-break-property-hook.php \
Expand Down
24 changes: 24 additions & 0 deletions build/baseline-pre-8.0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ parameters:
count: 2
path: PHPStan/Build/TurboAttributeCollector.php

-
rawMessage: 'Call to an undefined method ReflectionClass<object>::getAttributes().'
identifier: method.notFound
count: 1
path: ../src/DependencyInjection/AttributeServices/AttributeServicesDiscoverer.php

-
rawMessage: 'Call to an undefined method ReflectionParameter::getAttributes().'
identifier: method.notFound
count: 1
path: ../src/DependencyInjection/AttributeServices/AttributeServicesDiscoverer.php

-
rawMessage: 'Class AttributeServicesFixtures\DiscoveredConditionalRuleNotMet not found.'
identifier: class.notFound
count: 3
path: ../tests/PHPStan/DependencyInjection/AttributeServices/AttributeServicesIntegrationTest.php

-
rawMessage: 'Class AttributeServicesFixtures\DiscoveredConditionalService not found.'
identifier: class.notFound
count: 1
path: ../tests/PHPStan/DependencyInjection/AttributeServices/AttributeServicesIntegrationTest.php

-
rawMessage: 'Call to an undefined method ReflectionClass::isReadOnly().'
identifier: method.notFound
Expand Down
3 changes: 3 additions & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,15 @@ parametersSchema:
analysedPathsFromConfig: listOf(string())
usedLevel: string()
cliAutoloadFile: schema(string(), nullable())
attributeServicesDirectories: listOf(string())
attributeServicesClasses: listOf(string())

# internal - editor mode
singleReflectionFile: schema(string(), nullable())
singleReflectionInsteadOfFile: schema(string(), nullable())

expandRelativePaths:
- '[attributeServicesDirectories][]'
- '[parameters][paths][]'
- '[parameters][excludePaths][]'
- '[parameters][excludePaths][analyse][]'
Expand Down
2 changes: 2 additions & 0 deletions e2e/result-cache-extension-attribute-services/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor
/composer.lock
8 changes: 8 additions & 0 deletions e2e/result-cache-extension-attribute-services/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "phpstan/result-cache-extension-attribute-services-e2e",
"autoload": {
"psr-4": {
"ResultCacheE2EAttributeServices\\": "src/"
}
}
}
11 changes: 11 additions & 0 deletions e2e/result-cache-extension-attribute-services/foo.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- src/Foo.php
+++ src/Foo.php
@@ -5,4 +5,8 @@
class Foo
{

+ public function bar(): void
+ {
+ }
+
}
8 changes: 8 additions & 0 deletions e2e/result-cache-extension-attribute-services/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
attributeServicesDirectories:
- src

parameters:
level: 0
tmpDir: tmp
paths:
- src
11 changes: 11 additions & 0 deletions e2e/result-cache-extension-attribute-services/rule.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--- src/MyRule.php
+++ src/MyRule.php
@@ -22,7 +22,7 @@

public function processNode(Node $node, Scope $scope): array
{
- return [RuleErrorBuilder::message('the rule says one thing')->identifier('test.myRule')->build()];
+ return [RuleErrorBuilder::message('the rule says another thing')->identifier('test.myRule')->build()];
}

}
8 changes: 8 additions & 0 deletions e2e/result-cache-extension-attribute-services/src/Foo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace ResultCacheE2EAttributeServices;

class Foo
{

}
28 changes: 28 additions & 0 deletions e2e/result-cache-extension-attribute-services/src/MyRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types = 1);

namespace ResultCacheE2EAttributeServices;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* @implements Rule<Node\Stmt\Class_>
*/
#[RegisteredRule(level: 0)]
class MyRule implements Rule
{

public function getNodeType(): string
{
return Node\Stmt\Class_::class;
}

public function processNode(Node $node, Scope $scope): array
{
return [RuleErrorBuilder::message('the rule says one thing')->identifier('test.myRule')->build()];
}

}
2 changes: 2 additions & 0 deletions e2e/result-cache-extension-attribute-services/tmp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.*
4 changes: 2 additions & 2 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,13 @@ parameters:
rawMessage: 'Call to static method escape() of internal class Nette\DI\Helpers from outside its root namespace Nette.'
identifier: staticMethod.internalClass
count: 1
path: src/DependencyInjection/AutowiredAttributeServicesExtension.php
path: src/DependencyInjection/AttributeServices/AttributeServicesRegistrar.php

-
rawMessage: 'Call to static method expand() of internal class Nette\DI\Helpers from outside its root namespace Nette.'
identifier: staticMethod.internalClass
count: 1
path: src/DependencyInjection/AutowiredAttributeServicesExtension.php
path: src/DependencyInjection/AttributeServices/AttributeServicesRegistrar.php

-
rawMessage: 'Call to static method expand() of internal class Nette\DI\Helpers from outside its root namespace Nette.'
Expand Down
68 changes: 42 additions & 26 deletions src/Analyser/ResultCache/ResultCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ final class ResultCacheManager
* @param string[] $analysedPaths
* @param string[] $analysedPathsFromConfig
* @param string[] $composerAutoloaderProjectPaths
* @param string[] $attributeServicesDirectories
* @param list<string> $attributeServicesClasses classes registered from attributeServicesDirectories
* @param string[] $bootstrapFiles
* @param string[] $scanFiles
* @param string[] $scanDirectories
Expand All @@ -213,6 +215,10 @@ public function __construct(
#[AutowiredParameter]
private array $composerAutoloaderProjectPaths,
#[AutowiredParameter]
private array $attributeServicesDirectories,
#[AutowiredParameter]
private array $attributeServicesClasses,
#[AutowiredParameter]
private string $usedLevel,
#[AutowiredParameter]
private ?string $cliAutoloadFile,
Expand Down Expand Up @@ -2805,6 +2811,15 @@ private function readFrame($handle, int $length): mixed
*/
private function changedPackagesProvideContainerClass(?array $projectConfig, array $changedPackagesLookup): bool
{
// Directories from the attributeServicesDirectories section register services straight from
// their classes, so a changed package owning one of them can affect the analysis of every file.
foreach ($this->attributeServicesDirectories as $attributeServicesDirectory) {
$package = $this->packageDependencyResolver->resolveDirectoryPackage($attributeServicesDirectory);
if ($package !== null && array_key_exists($package, $changedPackagesLookup)) {
return true;
}
}

// Extensions registered directly in the project config (services:/rules:) or via an included
// extension neon file: resolve each service class to the package that owns its file.
if ($projectConfig !== null) {
Expand Down Expand Up @@ -2849,42 +2864,43 @@ private function getProjectExtensionFiles(?array $projectConfig, array $dependen
{
$this->alreadyProcessed = [];
$projectExtensionFiles = [];
$classes = $this->attributeServicesClasses;
if ($projectConfig !== null) {
$classes = ProjectConfigHelper::getServiceClassNames($projectConfig);
foreach ($classes as $class) {
try {
// does not use static reflection to reduce file-parsing
$classReflection = new ReflectionClass($class); /** @phpstan-ignore argument.type */
} catch (ReflectionException) {
continue;
}
$classes = array_merge(ProjectConfigHelper::getServiceClassNames($projectConfig), $classes);
}
foreach ($classes as $class) {
try {
// does not use static reflection to reduce file-parsing
$classReflection = new ReflectionClass($class); /** @phpstan-ignore argument.type */
} catch (ReflectionException) {
continue;
}

$fileName = $classReflection->getFileName();
if ($fileName === false) {
continue;
}
$fileName = $classReflection->getFileName();
if ($fileName === false) {
continue;
}

if (str_starts_with($fileName, 'phar://')) {
if (str_starts_with($fileName, 'phar://')) {
continue;
}

$allServiceFiles = $this->getAllDependencies($fileName, $dependencies);
if (count($allServiceFiles) === 0) {
if ($this->packageDependencyResolver->isFileOfInstalledPackage($fileName)) {
continue;
}

$allServiceFiles = $this->getAllDependencies($fileName, $dependencies);
if (count($allServiceFiles) === 0) {
if ($this->packageDependencyResolver->isFileOfInstalledPackage($fileName)) {
continue;
}
$projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class];
continue;
}

$projectExtensionFiles[$fileName] = [$this->getFileHash($fileName), false, $class];
foreach ($allServiceFiles as $serviceFile) {
if (array_key_exists($serviceFile, $projectExtensionFiles)) {
continue;
}

foreach ($allServiceFiles as $serviceFile) {
if (array_key_exists($serviceFile, $projectExtensionFiles)) {
continue;
}

$projectExtensionFiles[$serviceFile] = [$this->getFileHash($serviceFile), true, $class];
}
$projectExtensionFiles[$serviceFile] = [$this->getFileHash($serviceFile), true, $class];
}
}

Expand Down
16 changes: 16 additions & 0 deletions src/Dependency/PackageDependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use function is_file;
use function is_string;
use function realpath;
use function rtrim;
use function str_starts_with;
use function strlen;
use function strtolower;
Expand Down Expand Up @@ -113,6 +114,21 @@ public function isFileOfInstalledPackage(string $file): bool
return false;
}

/**
* Unlike resolvePackage(), also matches a directory that IS a package's install path.
*/
public function resolveDirectoryPackage(string $directory): ?string
{
$normalizedDirectory = rtrim($this->fileHelper->normalizePath($directory, '/'), '/');
foreach ($this->getInstallPathToPackage() as $installPath => $package) {
if ($normalizedDirectory === $installPath || str_starts_with($normalizedDirectory, $installPath . '/')) {
return $package;
}
}

return null;
}

private function doResolvePackage(string $file): ?string
{
// Normalize with a forward slash regardless of platform: normalizePath() defaults to
Expand Down
Loading
Loading