Skip to content
Open
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
8 changes: 6 additions & 2 deletions src/DependencyInjection/ContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -379,11 +379,15 @@ private function processSchema(array $statements, bool $required = true): Schema
$parameterSchema = null;
foreach ($statements as $statement) {
$processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments);
$entity = $statement->getEntity();
if (!is_string($entity)) {
throw new ShouldNotHappenException();
}
if ($parameterSchema === null) {
/** @var Type|AnyOf|Structure $parameterSchema */
$parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments);
$parameterSchema = Expect::{$entity}(...$processedArguments);
} else {
$parameterSchema->{$statement->getEntity()}(...$processedArguments);
$parameterSchema->{$entity}(...$processedArguments);
}
}

Expand Down
32 changes: 9 additions & 23 deletions src/Rules/Classes/ClassConstantRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
use PhpParser\Node;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\NullsafeOperatorHelper;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Php\PhpVersion;
Expand All @@ -18,6 +16,7 @@
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\ClassNameUsageLocation;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
Expand All @@ -44,8 +43,7 @@ public function __construct(
private RuleLevelHelper $ruleLevelHelper,
private ClassNameCheck $classCheck,
private PhpVersion $phpVersion,
#[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')]
private bool $checkNonStringableDynamicAccess,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
)
{
}
Expand All @@ -68,25 +66,13 @@ public function processNode(Node $node, Scope $scope): array
$constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
}

if ($this->checkNonStringableDynamicAccess) {
$nameTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->name,
'',
static fn (Type $type) => $type->isString()->yes(),
);

$nameType = $nameTypeResult->getType();
if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) {
$className = $node->class instanceof Name
? $scope->resolveName($node->class)
: $scope->getType($node->class)->describe(VerbosityLevel::typeOnly());

$errors[] = RuleErrorBuilder::message(sprintf('Class constant name for %s must be a string, but %s was given.', $className, $nameType->describe(VerbosityLevel::precise())))
->identifier('classConstant.nameNotString')
->build();
}
}
$errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringName(
$scope,
$node->name,
'Class constant name for %s must be a string, but %s was given.',
$node->class,
'classConstant.nameNotString',
));
}

foreach ($constantNameScopes as $constantName => $constantScope) {
Expand Down
10 changes: 10 additions & 0 deletions src/Rules/Methods/CallMethodsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\Rule;
use function array_merge;

Expand All @@ -27,6 +28,7 @@ final class CallMethodsRule implements Rule
public function __construct(
private MethodCallCheck $methodCallCheck,
private FunctionCallParametersCheck $parametersCheck,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
)
{
}
Expand All @@ -48,6 +50,14 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
$name = $constantString->getValue();
$methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
}

$errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringName(
$scope,
$node->name,
'Method name for %s must be a string, but %s was given.',
$node->var,
'method.nameNotString',
));
}

foreach ($methodNameScopes as $methodName => $methodScope) {
Expand Down
10 changes: 10 additions & 0 deletions src/Rules/Methods/CallStaticMethodsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\Rule;
use function array_merge;
use function sprintf;
Expand All @@ -28,6 +29,7 @@ final class CallStaticMethodsRule implements Rule
public function __construct(
private StaticMethodCallCheck $methodCallCheck,
private FunctionCallParametersCheck $parametersCheck,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
)
{
}
Expand All @@ -49,6 +51,14 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
$name = $constantString->getValue();
$methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
}

$errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringName(
$scope,
$node->name,
'Method name for %s must be a string, but %s was given.',
$node->class,
'staticMethod.nameNotString',
));
}

foreach ($methodNameScopes as $methodName => $methodScope) {
Expand Down
111 changes: 111 additions & 0 deletions src/Rules/NonStringableDynamicAccessCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function sprintf;

/**
* Checks whether the name of a dynamically accessed variable or member
* (`$$name`, `$obj->{$name}`, `$obj->{$name}()`, `Foo::{$name}()`,
* `Foo::${$name}`, `Foo::{$name}`) can actually be used as a name at runtime.
*
* Gated behind the `checkNonStringableDynamicAccess` feature toggle.
*/
#[AutowiredService]
final class NonStringableDynamicAccessCheck
{

public function __construct(
private RuleLevelHelper $ruleLevelHelper,
#[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')]
private bool $checkNonStringableDynamicAccess,
)
{
}

/**
* For names that PHP casts to string at runtime (variable variables,
* property and static property names) objects implementing __toString are
* accepted.
*
* @param Name|Expr|null $classNode the node whose name/type fills the leading `%s` placeholder, or null when the message has none
* @return list<IdentifierRuleError>
*/
public function checkStringCastableName(Scope $scope, Expr $name, string $messageFormat, $classNode, string $identifier): array
{
if (!$this->checkNonStringableDynamicAccess) {
return [];
}

$nameType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$name,
'',
static fn (Type $type) => !$type->toString() instanceof ErrorType && $type->toString()->isString()->yes(),

Check warning on line 52 in src/Rules/NonStringableDynamicAccessCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $scope, $name, '', - static fn (Type $type) => !$type->toString() instanceof ErrorType && $type->toString()->isString()->yes(), + static fn (Type $type) => !$type->toString() instanceof ErrorType && !$type->toString()->isString()->no(), )->getType(); if (
)->getType();

if (
!$nameType instanceof ErrorType
&& ($nameType->toString() instanceof ErrorType || !$nameType->toString()->isString()->yes())
) {
return [$this->buildError($scope, $name, $scope->getType($name), $messageFormat, $classNode, $identifier)];
}

return [];
}

/**
* For names that must be actual strings (method, static method and class
* constant names) objects implementing __toString are not accepted.
*
* @param Name|Expr|null $classNode the node whose name/type fills the leading `%s` placeholder, or null when the message has none
* @return list<IdentifierRuleError>
*/
public function checkStringName(Scope $scope, Expr $name, string $messageFormat, $classNode, string $identifier): array
{
if (!$this->checkNonStringableDynamicAccess) {
return [];
}

$nameType = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$name,
'',
static fn (Type $type) => $type->isString()->yes(),

Check warning on line 82 in src/Rules/NonStringableDynamicAccessCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $scope, $name, '', - static fn (Type $type) => $type->isString()->yes(), + static fn (Type $type) => !$type->isString()->no(), )->getType(); if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) {
)->getType();

if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) {

Check warning on line 85 in src/Rules/NonStringableDynamicAccessCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ static fn (Type $type) => $type->isString()->yes(), )->getType(); - if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) { + if (!$nameType instanceof ErrorType && $nameType->isString()->no()) { return [$this->buildError($scope, $name, $nameType, $messageFormat, $classNode, $identifier)]; }

Check warning on line 85 in src/Rules/NonStringableDynamicAccessCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ static fn (Type $type) => $type->isString()->yes(), )->getType(); - if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) { + if (!$nameType instanceof ErrorType && $nameType->isString()->no()) { return [$this->buildError($scope, $name, $nameType, $messageFormat, $classNode, $identifier)]; }
return [$this->buildError($scope, $name, $nameType, $messageFormat, $classNode, $identifier)];
}

return [];
}

/**
* @param Name|Expr|null $classNode
*/
private function buildError(Scope $scope, Expr $name, Type $nameType, string $messageFormat, $classNode, string $identifier): IdentifierRuleError
{
$messageArgs = [];
if ($classNode !== null) {
$messageArgs[] = $classNode instanceof Name
? $scope->resolveName($classNode)
: $scope->getType($classNode)->describe(VerbosityLevel::typeOnly());
}
$messageArgs[] = $nameType->describe(VerbosityLevel::precise());

return RuleErrorBuilder::message(sprintf($messageFormat, ...$messageArgs))
->line($name->getStartLine())
->identifier($identifier)
->build();
}

}
30 changes: 8 additions & 22 deletions src/Rules/Properties/AccessPropertiesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use PHPStan\Reflection\MissingPropertyFromReflectionException;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Type\Constant\ConstantStringType;
Expand All @@ -39,12 +40,11 @@ public function __construct(
private ReflectionProvider $reflectionProvider,
private RuleLevelHelper $ruleLevelHelper,
private PhpVersion $phpVersion,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
#[AutowiredParameter]
private bool $reportMagicProperties,
#[AutowiredParameter]
private bool $checkDynamicProperties,
#[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')]
private bool $checkNonStringableDynamicAccess,
)
{
}
Expand All @@ -60,28 +60,14 @@ public function check(PropertyFetch $node, Scope $scope, bool $write): array
} else {
$names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings());

if (!$write && $this->checkNonStringableDynamicAccess) {
$nameTypeResult = $this->ruleLevelHelper->findTypeToCheck(
if (!$write) {
$errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringCastableName(
$scope,
$node->name,
'',
static fn (Type $type) => !$type->toString() instanceof ErrorType && $type->toString()->isString()->yes(),
);
$nameType = $nameTypeResult->getType();
if (
!$nameType instanceof ErrorType
&& (
$nameType->toString() instanceof ErrorType
|| !$nameType->toString()->isString()->yes()
)
) {
$originalNameType = $scope->getType($node->name);
$className = $scope->getType($node->var)->describe(VerbosityLevel::typeOnly());
$errors[] = RuleErrorBuilder::message(sprintf('Property name for %s must be a string, but %s was given.', $className, $originalNameType->describe(VerbosityLevel::precise())))
->line($node->name->getStartLine())
->identifier('property.nameNotString')
->build();
}
'Property name for %s must be a string, but %s was given.',
$node->var,
'property.nameNotString',
));
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/Rules/Properties/AccessStaticPropertiesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PHPStan\Rules\ClassNameNodePair;
use PHPStan\Rules\ClassNameUsageLocation;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Type\Constant\ConstantStringType;
Expand All @@ -48,6 +49,7 @@ public function __construct(
private RuleLevelHelper $ruleLevelHelper,
private ClassNameCheck $classCheck,
private PhpVersion $phpVersion,
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
#[AutowiredParameter(ref: '%tips.discoveringSymbols%')]
private bool $discoveringSymbolsTip,
)
Expand All @@ -59,13 +61,23 @@ public function __construct(
*/
public function check(StaticPropertyFetch $node, Scope $scope, bool $write): array
{
$errors = [];
if ($node->name instanceof Node\VarLikeIdentifier) {
$names = [$node->name->name];
} else {
$names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings());

if (!$write) {
$errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringCastableName(
$scope,
$node->name,
'Static property name for %s must be a string, but %s was given.',
$node->class,
'staticProperty.nameNotString',
));
}
}

$errors = [];
foreach ($names as $name) {
$errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name, $write));
}
Expand Down
10 changes: 10 additions & 0 deletions src/Rules/Variables/DefinedVariableRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_merge;
Expand All @@ -25,6 +26,7 @@ final class DefinedVariableRule implements Rule
{

public function __construct(
private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck,
#[AutowiredParameter]
private bool $cliArgumentsVariablesRegistered,
#[AutowiredParameter]
Expand All @@ -50,6 +52,14 @@ public function processNode(Node $node, Scope $scope): array
$name = $constantString->getValue();
$variableNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
}

$errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringCastableName(
$scope,
$node->name,
'Variable variable name must be a string, but %s was given.',
null,
'variable.nameNotString',
));
}

foreach ($variableNameScopes as $name => $variableScope) {
Expand Down
2 changes: 2 additions & 0 deletions tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Rules\FunctionCallParametersCheck;
use PHPStan\Rules\Methods\CallMethodsRule;
use PHPStan\Rules\Methods\MethodCallCheck;
use PHPStan\Rules\NonStringableDynamicAccessCheck;
use PHPStan\Rules\NullsafeCheck;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
Expand Down Expand Up @@ -49,6 +50,7 @@ protected function getRule(): Rule
checkExtraArguments: true,
checkMissingTypehints: true,
),
new NonStringableDynamicAccessCheck($ruleLevelHelper, true),
);
}

Expand Down
Loading
Loading