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
69 changes: 69 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ jobs:
../bashunit -a contains 'UsesHolder.php:9:Function ResultCacheE2EValueDependency\usesHolder() should return int but returns string.' "$OUTPUT"
../bashunit -a contains 'UsesMayThrow.php:9:Dead catch - RuntimeException is never thrown in the try block.' "$OUTPUT"
../bashunit -a contains 'UsesRegion.php:7:Region eu is used.' "$OUTPUT"
../bashunit -a contains 'UsesView.php:7:View dashboard does not exist.' "$OUTPUT"
../bashunit -a contains 'UsesMake.php:7:Call to an undefined method object::run().' "$OUTPUT"
../bashunit -a not_contains 'UsesMailer.php' "$OUTPUT"
# The extensions read container.json, which the result cache knows nothing about. A rule and a
# dynamic return type extension both declare the service they ask about - it is recorded once.
Expand Down Expand Up @@ -526,6 +528,73 @@ jobs:
echo "$OUTPUT"
../bashunit -a contains 'UsesRegion.php:7:Region us is used.' "$OUTPUT"
../bashunit -a not_contains 'Region eu is used.' "$OUTPUT"
# So does a parameter closure this extension: $this in the closure follows the parameter.
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["closureThis"] = "ResultCacheE2EValueDependency\\Logger"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'UsesWithThis.php:8:Call to an undefined method ResultCacheE2EValueDependency\Logger::send().' "$OUTPUT"
# So does a type-specifying extension: the narrowed type follows the parameter.
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["allowedClass"] = "ResultCacheE2EValueDependency\\Logger"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'UsesIsAllowed.php:8:Call to an undefined method ResultCacheE2EValueDependency\Logger::send().' "$OUTPUT"
# A rule tracking a directory: a file the pattern doesn't match changes nothing...
echo 'notes' > views/notes.txt
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '0' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
# ...a matching file appearing re-analyses the file that looked for it.
echo '<p>dashboard</p>' > views/dashboard.html
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a not_contains 'View dashboard does not exist.' "$OUTPUT"
# ...and so does a matching file whose contents change.
echo '<p>dashboard, changed</p>' > views/dashboard.html
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw"
# A dynamic return type extension tracking a class named in a string: the class appearing - outside
# the analysed paths, so no fallback for new files kicks in - re-analyses the file naming it...
printf '<?php declare(strict_types = 1);\n\nnamespace ResultCacheE2EValueDependency\\Lib;\n\nclass Gadget extends Base\n{\n}\n' > lib/Gadget.php
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a not_contains 'UsesMake.php' "$OUTPUT"
# ...a method body edited in its parent class does not...
sed -i.bak 's/return 1;/return 2;/' lib/Base.php && rm lib/Base.php.bak
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '0' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
# ...and a signature changed there does.
printf '<?php declare(strict_types = 1);\n\nnamespace ResultCacheE2EValueDependency\\Lib;\n\nclass Base\n{\n\n\tpublic function run(): string\n\t{\n\t\treturn \x27x\x27;\n\t}\n\n}\n' > lib/Base.php
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'UsesMake.php:7:Function ResultCacheE2EValueDependency\usesMake() should return int but returns string.' "$OUTPUT"
# A class reflection extension tracking on the class: every file using the class is analysed again,
# also one that got the magic method from the reflection cache without asking the extension.
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["greeting"] = "1"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
INFO=$(../../bin/phpstan result-cache-info --json)
echo "$INFO"
../bashunit -a equals '3' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'UsesMagic.php:7:Function ResultCacheE2EValueDependency\usesMagic() should return string but returns int.' "$OUTPUT"
../bashunit -a contains 'UsesMagicToo.php:7:Function ResultCacheE2EValueDependency\usesMagicToo() should return string but returns int.' "$OUTPUT"
- script: |
cd e2e/result-cache-file-dependency
composer install
Expand Down
9 changes: 8 additions & 1 deletion e2e/result-cache-value-dependency/composer.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"autoload-dev": {
"classmap": ["extension/"]
"classmap": [
"extension/"
]
},
"autoload": {
"psr-4": {
"ResultCacheE2EValueDependency\\Lib\\": "lib/"
}
}
}
5 changes: 4 additions & 1 deletion e2e/result-cache-value-dependency/container.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"timezone": "Europe/Prague",
"debug": "yes",
"throwing": "no",
"region": "eu"
"region": "eu",
"closureThis": "ResultCacheE2EValueDependency\\Mailer",
"allowedClass": "ResultCacheE2EValueDependency\\Mailer",
"greeting": "hello"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace ResultCacheE2EValueDependency;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\DependencyTracker;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\ObjectType;

/**
* isAllowed($object) narrows $object to the class in the "allowedClass" parameter.
*/
final class IsAllowedTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private TypeSpecifier $typeSpecifier;

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
{
return $functionReflection->getName() === 'ResultCacheE2EValueDependency\isAllowed' && $context->true() && isset($node->getArgs()[0]);
}

/**
* @param Scope&DependencyTracker $scope
*/
public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
$scope->trackValueDependency(ParameterValueExtension::class, 'allowedClass');
$class = Container::getParameter('allowedClass');
if ($class === null) {
return new SpecifiedTypes();
}

return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectType($class), $context, $scope);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php declare(strict_types = 1);

namespace ResultCacheE2EValueDependency;

use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Type;

final class MagicMethodReflection implements MethodReflection
{

public function __construct(private ClassReflection $declaringClass, private string $name, private Type $returnType)
{
}

public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}

public function isStatic(): bool
{
return false;
}

public function isPrivate(): bool
{
return false;
}

public function isPublic(): bool
{
return true;
}

public function getDocComment(): ?string
{
return null;
}

public function getName(): string
{
return $this->name;
}

public function getPrototype(): ClassMemberReflection
{
return $this;
}

public function getVariants(): array
{
return [new FunctionVariant(TemplateTypeMap::createEmpty(), null, [], false, $this->returnType)];
}

public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function getDeprecatedDescription(): ?string
{
return null;
}

public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function getThrowType(): ?Type
{
return null;
}

public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace ResultCacheE2EValueDependency;

use PHPStan\Analyser\DeclarationDependencyTracker;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;

/**
* Magic::greet() returns the "greeting" parameter - an int when it's numeric. A class reflection
* extension gets no Scope, and what it declares is reused by every file using the class, so it
* tracks the parameter on the class.
*/
final class MagicMethodsExtension implements MethodsClassReflectionExtension
{

public function __construct(private DeclarationDependencyTracker $declarationDependencyTracker)
{
}

public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
return $classReflection->getName() === Magic::class && $methodName === 'greet';
}

public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
$this->declarationDependencyTracker->trackValueDependency($classReflection, ParameterValueExtension::class, 'greeting');
$greeting = Container::getParameter('greeting') ?? '';

return new MagicMethodReflection(
$classReflection,
$methodName,
is_numeric($greeting) ? new ConstantIntegerType((int) $greeting) : new ConstantStringType($greeting),
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace ResultCacheE2EValueDependency;

use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\DependencyTracker;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;

/**
* make('ClassName') returns an instance of the class named in the string.
*/
final class MakeReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'ResultCacheE2EValueDependency\make';
}

/**
* @param Scope&DependencyTracker $scope
*/
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
{
$arg = $functionCall->getArgs()[0]->value ?? null;
if (!$arg instanceof String_) {
return null;
}

$scope->trackClassDependency($arg->value);
if (!$this->reflectionProvider->hasClass($arg->value)) {
return null;
}

return new ObjectType($arg->value);
}

}
50 changes: 50 additions & 0 deletions e2e/result-cache-value-dependency/extension/ViewRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1);

namespace ResultCacheE2EValueDependency;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\DependencyTracker;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/**
* Reports view('name') when there's no views/name.html - whether there is depends on the views/
* directory, not on a single file.
*
* @implements Rule<FuncCall>
*/
final class ViewRule implements Rule
{

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

/**
* @param Scope&DependencyTracker $scope
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Name || $node->name->getLast() !== 'view' || !isset($node->getArgs()[0]) || !$node->getArgs()[0]->value instanceof String_) {
return [];
}

$directory = dirname(__DIR__) . '/views';
$scope->trackDirectoryDependency($directory, '*.html');

$name = $node->getArgs()[0]->value->value;
if (is_file($directory . '/' . $name . '.html')) {
return [];
}

return [
RuleErrorBuilder::message(sprintf('View %s does not exist.', $name))->identifier('resultCacheE2E.view')->build(),
];
}

}
Loading
Loading