diff --git a/src/Internal/SprintfHelper.php b/src/Internal/SprintfHelper.php new file mode 100644 index 0000000000..76f4fa19bd --- /dev/null +++ b/src/Internal/SprintfHelper.php @@ -0,0 +1,13 @@ +dim !== null) { $dimType = $scope->getType($node->dim); - $unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', $dimType->describe(VerbosityLevel::value())); + $unknownClassPattern = sprintf('Access to offset %s on an unknown class %%s.', SprintfHelper::escapeFormatString($dimType->describe(VerbosityLevel::value()))); } else { $dimType = null; $unknownClassPattern = 'Access to an offset on an unknown class %s.'; diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index c9080f772f..205e3994d4 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -5,6 +5,7 @@ use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; @@ -90,25 +91,27 @@ public function check( $errors[] = RuleErrorBuilder::message(sprintf('Constructor of attribute class %s is not public.', $name))->line($attribute->getLine())->build(); } + $attributeClassName = SprintfHelper::escapeFormatString($attributeClass->getDisplayName()); + $parameterErrors = $this->functionCallParametersCheck->check( ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants()), $scope, $attributeConstructor->getDeclaringClass()->isBuiltin(), new New_($attribute->name, $attribute->args, $attribute->getAttributes()), [ - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.', - 'Attribute class ' . $attributeClass->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor expects %s, %s given.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, at least %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, at least %d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameter, %d-%d required.', + 'Attribute class ' . $attributeClassName . ' constructor invoked with %d parameters, %d-%d required.', + 'Parameter %s of attribute class ' . $attributeClassName . ' constructor expects %s, %s given.', '', // constructor does not have a return type - 'Parameter %s of attribute class ' . $attributeClass->getDisplayName() . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClass->getDisplayName(), - 'Missing parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.', - 'Unknown parameter $%s in call to ' . $attributeClass->getDisplayName() . ' constructor.', - 'Return type of call to ' . $attributeClass->getDisplayName() . ' constructor contains unresolvable type.', + 'Parameter %s of attribute class ' . $attributeClassName . ' constructor is passed by reference, so it expects variables only', + 'Unable to resolve the template type %s in instantiation of attribute class ' . $attributeClassName, + 'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.', + 'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.', + 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', ] ); diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 64d4e11bec..4ab9d2e083 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\ClassConstFetch; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; @@ -118,7 +119,7 @@ public function processNode(Node $node, Scope $scope): array $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $class, - sprintf('Access to constant %s on an unknown class %%s.', $constantName), + sprintf('Access to constant %s on an unknown class %%s.', SprintfHelper::escapeFormatString($constantName)), static function (Type $type) use ($constantName): bool { return $type->canAccessConstants()->yes() && $type->hasConstant($constantName)->yes(); } diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index b1fb7c1402..71d4b62949 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; @@ -175,6 +176,8 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ ))->build(); } + $classDisplayName = SprintfHelper::escapeFormatString($classReflection->getDisplayName()); + return array_merge($messages, $this->check->check( ParametersAcceptorSelector::selectFromArgs( $scope, @@ -185,19 +188,19 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ $constructorReflection->getDeclaringClass()->isBuiltin(), $node, [ - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.', - 'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.', - 'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor expects %s, %s given.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, at least %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, at least %d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameter, %d-%d required.', + 'Class ' . $classDisplayName . ' constructor invoked with %d parameters, %d-%d required.', + 'Parameter %s of class ' . $classDisplayName . ' constructor expects %s, %s given.', '', // constructor does not have a return type - 'Parameter %s of class ' . $classReflection->getDisplayName() . ' constructor is passed by reference, so it expects variables only', - 'Unable to resolve the template type %s in instantiation of class ' . $classReflection->getDisplayName(), - 'Missing parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.', - 'Unknown parameter $%s in call to ' . $classReflection->getDisplayName() . ' constructor.', - 'Return type of call to ' . $classReflection->getDisplayName() . ' constructor contains unresolvable type.', + 'Parameter %s of class ' . $classDisplayName . ' constructor is passed by reference, so it expects variables only', + 'Unable to resolve the template type %s in instantiation of class ' . $classDisplayName, + 'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.', + 'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.', + 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', ] )); } diff --git a/src/Rules/Classes/UnusedConstructorParametersRule.php b/src/Rules/Classes/UnusedConstructorParametersRule.php index 1d27034d71..fcfbfd1547 100644 --- a/src/Rules/Classes/UnusedConstructorParametersRule.php +++ b/src/Rules/Classes/UnusedConstructorParametersRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\Variable; use PhpParser\Node\Param; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\UnusedFunctionParametersCheck; @@ -50,7 +51,7 @@ public function processNode(Node $node, Scope $scope): array $message = sprintf( 'Constructor of class %s has an unused parameter $%%s.', - $scope->getClassReflection()->getDisplayName() + SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()) ); if ($scope->getClassReflection()->isAnonymous()) { $message = 'Constructor of an anonymous class has an unused parameter $%s.'; diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index b0efc815d6..cbe6728ed7 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Functions; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\InaccessibleMethod; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; @@ -104,7 +105,7 @@ static function (Type $type): bool { if ($type instanceof ClosureType) { $callableDescription = 'closure'; } else { - $callableDescription = sprintf('callable %s', $type->describe(VerbosityLevel::value())); + $callableDescription = sprintf('callable %s', SprintfHelper::escapeFormatString($type->describe(VerbosityLevel::value()))); } return array_merge( diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index f7fd2b7d27..81371eadb7 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\FunctionCallParametersCheck; @@ -41,6 +42,7 @@ public function processNode(Node $node, Scope $scope): array } $function = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = SprintfHelper::escapeFormatString($function->getName()); return $this->check->check( ParametersAcceptorSelector::selectFromArgs( @@ -52,19 +54,19 @@ public function processNode(Node $node, Scope $scope): array $function->isBuiltin(), $node, [ - 'Function ' . $function->getName() . ' invoked with %d parameter, %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameter, at least %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, at least %d required.', - 'Function ' . $function->getName() . ' invoked with %d parameter, %d-%d required.', - 'Function ' . $function->getName() . ' invoked with %d parameters, %d-%d required.', - 'Parameter %s of function ' . $function->getName() . ' expects %s, %s given.', - 'Result of function ' . $function->getName() . ' (void) is used.', - 'Parameter %s of function ' . $function->getName() . ' is passed by reference, so it expects variables only.', - 'Unable to resolve the template type %s in call to function ' . $function->getName(), - 'Missing parameter $%s in call to function ' . $function->getName() . '.', - 'Unknown parameter $%s in call to function ' . $function->getName() . '.', - 'Return type of call to function ' . $function->getName() . ' contains unresolvable type.', + 'Function ' . $functionName . ' invoked with %d parameter, %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameters, at least %d required.', + 'Function ' . $functionName . ' invoked with %d parameter, %d-%d required.', + 'Function ' . $functionName . ' invoked with %d parameters, %d-%d required.', + 'Parameter %s of function ' . $functionName . ' expects %s, %s given.', + 'Result of function ' . $functionName . ' (void) is used.', + 'Parameter %s of function ' . $functionName . ' is passed by reference, so it expects variables only.', + 'Unable to resolve the template type %s in call to function ' . $functionName, + 'Missing parameter $%s in call to function ' . $functionName . '.', + 'Unknown parameter $%s in call to function ' . $functionName . '.', + 'Return type of call to function ' . $functionName . ' contains unresolvable type.', ] ); } diff --git a/src/Rules/Functions/ExistingClassesInTypehintsRule.php b/src/Rules/Functions/ExistingClassesInTypehintsRule.php index 0b3cfdc77b..04256e7074 100644 --- a/src/Rules/Functions/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Functions/ExistingClassesInTypehintsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; @@ -32,7 +33,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $functionName = $scope->getFunction()->getName(); + $functionName = SprintfHelper::escapeFormatString($scope->getFunction()->getName()); return $this->check->checkFunction( $node->getOriginalNode(), diff --git a/src/Rules/Generics/ClassAncestorsRule.php b/src/Rules/Generics/ClassAncestorsRule.php index 00a83bac5c..96c64dfef7 100644 --- a/src/Rules/Generics/ClassAncestorsRule.php +++ b/src/Rules/Generics/ClassAncestorsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; @@ -69,21 +70,23 @@ public function processNode(Node $node, Scope $scope): array $implementsTags = $resolvedPhpDoc->getImplementsTags(); } + $escapedClassName = SprintfHelper::escapeFormatString($className); + $extendsErrors = $this->genericAncestorsCheck->check( $originalNode->extends !== null ? [$originalNode->extends] : [], array_map(static function (ExtendsTag $tag): Type { return $tag->getType(); }, $extendsTags), - sprintf('Class %s @extends tag contains incompatible type %%s.', $className), - sprintf('Class %s has @extends tag, but does not extend any class.', $className), - sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $className), + sprintf('Class %s @extends tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s has @extends tag, but does not extend any class.', $escapedClassName), + sprintf('The @extends tag of class %s describes %%s but the class extends %%s.', $escapedClassName), 'PHPDoc tag @extends contains generic type %s but class %s is not generic.', 'Generic type %s in PHPDoc tag @extends does not specify all template types of class %s: %s', 'Generic type %s in PHPDoc tag @extends specifies %d template types, but class %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of class %s.', 'PHPDoc tag @extends has invalid type %s.', - sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $className), - sprintf('in extended type %%s of class %s', $className) + sprintf('Class %s extends generic class %%s but does not specify its types: %%s', $escapedClassName), + sprintf('in extended type %%s of class %s', $escapedClassName) ); $implementsErrors = $this->genericAncestorsCheck->check( @@ -91,16 +94,16 @@ public function processNode(Node $node, Scope $scope): array array_map(static function (ImplementsTag $tag): Type { return $tag->getType(); }, $implementsTags), - sprintf('Class %s @implements tag contains incompatible type %%s.', $className), - sprintf('Class %s has @implements tag, but does not implement any interface.', $className), - sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $className), + sprintf('Class %s @implements tag contains incompatible type %%s.', $escapedClassName), + sprintf('Class %s has @implements tag, but does not implement any interface.', $escapedClassName), + sprintf('The @implements tag of class %s describes %%s but the class implements: %%s', $escapedClassName), 'PHPDoc tag @implements contains generic type %s but interface %s is not generic.', 'Generic type %s in PHPDoc tag @implements does not specify all template types of interface %s: %s', 'Generic type %s in PHPDoc tag @implements specifies %d template types, but interface %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @implements is not subtype of template type %s of interface %s.', 'PHPDoc tag @implements has invalid type %s.', - sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $className), - sprintf('in implemented type %%s of class %s', $className) + sprintf('Class %s implements generic interface %%s but does not specify its types: %%s', $escapedClassName), + sprintf('in implemented type %%s of class %s', $escapedClassName) ); foreach ($this->crossCheckInterfacesHelper->check($classReflection) as $error) { diff --git a/src/Rules/Generics/ClassTemplateTypeRule.php b/src/Rules/Generics/ClassTemplateTypeRule.php index ec726ce894..f611c41093 100644 --- a/src/Rules/Generics/ClassTemplateTypeRule.php +++ b/src/Rules/Generics/ClassTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\Rules\Rule; use PHPStan\Type\Generic\TemplateTypeScope; @@ -38,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array if ($classReflection->isAnonymous()) { $displayName = 'anonymous class'; } else { - $displayName = 'class ' . $classReflection->getDisplayName(); + $displayName = 'class ' . SprintfHelper::escapeFormatString($classReflection->getDisplayName()); } return $this->templateTypeCheck->check( diff --git a/src/Rules/Generics/FunctionSignatureVarianceRule.php b/src/Rules/Generics/FunctionSignatureVarianceRule.php index a6d4699100..6e07cbc4c5 100644 --- a/src/Rules/Generics/FunctionSignatureVarianceRule.php +++ b/src/Rules/Generics/FunctionSignatureVarianceRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InFunctionNode; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\Rule; @@ -37,7 +38,7 @@ public function processNode(Node $node, Scope $scope): array return $this->varianceCheck->checkParametersAcceptor( ParametersAcceptorSelector::selectSingle($functionReflection->getVariants()), - sprintf('in parameter %%s of function %s()', $functionName), + sprintf('in parameter %%s of function %s()', SprintfHelper::escapeFormatString($functionName)), sprintf('in return type of function %s()', $functionName), sprintf('in function %s()', $functionName), false diff --git a/src/Rules/Generics/FunctionTemplateTypeRule.php b/src/Rules/Generics/FunctionTemplateTypeRule.php index 2a6d2bc921..03d59f3397 100644 --- a/src/Rules/Generics/FunctionTemplateTypeRule.php +++ b/src/Rules/Generics/FunctionTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; @@ -52,14 +53,16 @@ public function processNode(Node $node, Scope $scope): array $docComment->getText() ); + $escapedFunctionName = SprintfHelper::escapeFormatString($functionName); + return $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithFunction($functionName), $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $functionName), - sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $functionName), - sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $functionName), - sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $functionName) + sprintf('PHPDoc tag @template for function %s() cannot have existing class %%s as its name.', $escapedFunctionName), + sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName), + sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName) ); } diff --git a/src/Rules/Generics/InterfaceAncestorsRule.php b/src/Rules/Generics/InterfaceAncestorsRule.php index ee0e33b4e9..fad7ac5ba4 100644 --- a/src/Rules/Generics/InterfaceAncestorsRule.php +++ b/src/Rules/Generics/InterfaceAncestorsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\Tag\ExtendsTag; use PHPStan\PhpDoc\Tag\ImplementsTag; @@ -66,21 +67,23 @@ public function processNode(Node $node, Scope $scope): array $implementsTags = $resolvedPhpDoc->getImplementsTags(); } + $escapedInterfaceName = SprintfHelper::escapeFormatString($interfaceName); + $extendsErrors = $this->genericAncestorsCheck->check( $originalNode->extends, array_map(static function (ExtendsTag $tag): Type { return $tag->getType(); }, $extendsTags), - sprintf('Interface %s @extends tag contains incompatible type %%s.', $interfaceName), - sprintf('Interface %s has @extends tag, but does not extend any interface.', $interfaceName), - sprintf('The @extends tag of interface %s describes %%s but the interface extends: %%s', $interfaceName), + sprintf('Interface %s @extends tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s has @extends tag, but does not extend any interface.', $escapedInterfaceName), + sprintf('The @extends tag of interface %s describes %%s but the interface extends: %%s', $escapedInterfaceName), 'PHPDoc tag @extends contains generic type %s but interface %s is not generic.', 'Generic type %s in PHPDoc tag @extends does not specify all template types of interface %s: %s', 'Generic type %s in PHPDoc tag @extends specifies %d template types, but interface %s supports only %d: %s', 'Type %s in generic type %s in PHPDoc tag @extends is not subtype of template type %s of interface %s.', 'PHPDoc tag @extends has invalid type %s.', - sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $interfaceName), - sprintf('in extended type %%s of interface %s', $interfaceName) + sprintf('Interface %s extends generic interface %%s but does not specify its types: %%s', $escapedInterfaceName), + sprintf('in extended type %%s of interface %s', $escapedInterfaceName) ); $implementsErrors = $this->genericAncestorsCheck->check( @@ -88,8 +91,8 @@ public function processNode(Node $node, Scope $scope): array array_map(static function (ImplementsTag $tag): Type { return $tag->getType(); }, $implementsTags), - sprintf('Interface %s @implements tag contains incompatible type %%s.', $interfaceName), - sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $interfaceName), + sprintf('Interface %s @implements tag contains incompatible type %%s.', $escapedInterfaceName), + sprintf('Interface %s has @implements tag, but can not implement any interface, must extend from it.', $escapedInterfaceName), '', '', '', diff --git a/src/Rules/Generics/InterfaceTemplateTypeRule.php b/src/Rules/Generics/InterfaceTemplateTypeRule.php index 87f8b8cded..b26ab74fea 100644 --- a/src/Rules/Generics/InterfaceTemplateTypeRule.php +++ b/src/Rules/Generics/InterfaceTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; @@ -52,14 +53,16 @@ public function processNode(Node $node, Scope $scope): array $docComment->getText() ); + $escapadInterfaceName = SprintfHelper::escapeFormatString($interfaceName); + return $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithClass($interfaceName), $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $interfaceName), - sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $interfaceName), - sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $interfaceName), - sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $interfaceName) + sprintf('PHPDoc tag @template for interface %s cannot have existing class %%s as its name.', $escapadInterfaceName), + sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName), + sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName) ); } diff --git a/src/Rules/Generics/MethodSignatureVarianceRule.php b/src/Rules/Generics/MethodSignatureVarianceRule.php index 49e452476a..0b96397f2a 100644 --- a/src/Rules/Generics/MethodSignatureVarianceRule.php +++ b/src/Rules/Generics/MethodSignatureVarianceRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -36,7 +37,7 @@ public function processNode(Node $node, Scope $scope): array return $this->varianceCheck->checkParametersAcceptor( ParametersAcceptorSelector::selectSingle($method->getVariants()), - sprintf('in parameter %%s of method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), + sprintf('in parameter %%s of method %s::%s()', SprintfHelper::escapeFormatString($method->getDeclaringClass()->getDisplayName()), SprintfHelper::escapeFormatString($method->getName())), sprintf('in return type of method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), sprintf('in method %s::%s()', $method->getDeclaringClass()->getDisplayName(), $method->getName()), $method->getName() === '__construct' || $method->isStatic() diff --git a/src/Rules/Generics/MethodTemplateTypeRule.php b/src/Rules/Generics/MethodTemplateTypeRule.php index 57ac147a2c..09e7b26b54 100644 --- a/src/Rules/Generics/MethodTemplateTypeRule.php +++ b/src/Rules/Generics/MethodTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; @@ -57,14 +58,16 @@ public function processNode(Node $node, Scope $scope): array ); $methodTemplateTags = $resolvedPhpDoc->getTemplateTags(); + $escapedClassName = SprintfHelper::escapeFormatString($className); + $escapedMethodName = SprintfHelper::escapeFormatString($methodName); $messages = $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithMethod($className, $methodName), $methodTemplateTags, - sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $className, $methodName), - sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $className, $methodName), - sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $className, $methodName), - sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $className, $methodName) + sprintf('PHPDoc tag @template for method %s::%s() cannot have existing class %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName), + sprintf('PHPDoc tag @template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName) ); $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes(); diff --git a/src/Rules/Generics/TemplateTypeCheck.php b/src/Rules/Generics/TemplateTypeCheck.php index 1d6ca66b19..5286a2625b 100644 --- a/src/Rules/Generics/TemplateTypeCheck.php +++ b/src/Rules/Generics/TemplateTypeCheck.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Generics; use PhpParser\Node; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; @@ -132,12 +133,13 @@ public function check( return $type; }); + $escapedTemplateTagName = SprintfHelper::escapeFormatString($templateTagName); $genericObjectErrors = $this->genericObjectTypeCheck->check( $boundType, - sprintf('PHPDoc tag @template %s bound contains generic type %%s but class %%s is not generic.', $templateTagName), - sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of class %%s: %%s', $templateTagName), - sprintf('PHPDoc tag @template %s bound has type %%s which specifies %%d template types, but class %%s supports only %%d: %%s', $templateTagName), - sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s is not subtype of template type %%s of class %%s.', $templateTagName) + sprintf('PHPDoc tag @template %s bound contains generic type %%s but class %%s is not generic.', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s bound has type %%s which does not specify all template types of class %%s: %%s', $escapedTemplateTagName), + sprintf('PHPDoc tag @template %s bound has type %%s which specifies %%d template types, but class %%s supports only %%d: %%s', $escapedTemplateTagName), + sprintf('Type %%s in generic type %%s in PHPDoc tag @template %s is not subtype of template type %%s of class %%s.', $escapedTemplateTagName) ); foreach ($genericObjectErrors as $genericObjectError) { $messages[] = $genericObjectError; diff --git a/src/Rules/Generics/TraitTemplateTypeRule.php b/src/Rules/Generics/TraitTemplateTypeRule.php index 21b89339cd..3f756febf4 100644 --- a/src/Rules/Generics/TraitTemplateTypeRule.php +++ b/src/Rules/Generics/TraitTemplateTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeScope; @@ -52,14 +53,16 @@ public function processNode(Node $node, Scope $scope): array $docComment->getText() ); + $escapedTraitName = SprintfHelper::escapeFormatString($traitName); + return $this->templateTypeCheck->check( $node, TemplateTypeScope::createWithClass($traitName), $resolvedPhpDoc->getTemplateTags(), - sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $traitName), - sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $traitName), - sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $traitName), - sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $traitName) + sprintf('PHPDoc tag @template for trait %s cannot have existing class %%s as its name.', $escapedTraitName), + sprintf('PHPDoc tag @template for trait %s cannot have existing type alias %%s as its name.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s has invalid bound type %%s.', $escapedTraitName), + sprintf('PHPDoc tag @template %%s for trait %s with bound type %%s is not supported.', $escapedTraitName) ); } diff --git a/src/Rules/Generics/UsedTraitsRule.php b/src/Rules/Generics/UsedTraitsRule.php index 9e102bf341..49ff292ece 100644 --- a/src/Rules/Generics/UsedTraitsRule.php +++ b/src/Rules/Generics/UsedTraitsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\Rules\Rule; use PHPStan\Type\FileTypeMapper; @@ -57,10 +58,10 @@ public function processNode(Node $node, Scope $scope): array $useTags = $resolvedPhpDoc->getUsesTags(); } - $description = sprintf('class %s', $className); + $description = sprintf('class %s', SprintfHelper::escapeFormatString($className)); $typeDescription = 'class'; if ($traitName !== null) { - $description = sprintf('trait %s', $traitName); + $description = sprintf('trait %s', SprintfHelper::escapeFormatString($traitName)); $typeDescription = 'trait'; } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index c64ea1db91..8007179b40 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\FunctionCallParametersCheck; @@ -60,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->var, - sprintf('Call to method %s() on an unknown class %%s.', $name), + sprintf('Call to method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), static function (Type $type) use ($name): bool { return $type->canCallMethods()->yes() && $type->hasMethod($name)->yes(); } @@ -124,7 +125,7 @@ static function (Type $type) use ($name): bool { $methodReflection = $type->getMethod($name, $scope); $declaringClass = $methodReflection->getDeclaringClass(); - $messagesMethodName = $declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'; + $messagesMethodName = SprintfHelper::escapeFormatString($declaringClass->getDisplayName() . '::' . $methodReflection->getName() . '()'); $errors = []; if (!$scope->canCallMethod($methodReflection)) { $errors[] = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 68fcacc52f..cdef6c8fdf 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -150,7 +151,7 @@ public function processNode(Node $node, Scope $scope): array $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $class, - sprintf('Call to static method %s() on an unknown class %%s.', $methodName), + sprintf('Call to static method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($methodName)), static function (Type $type) use ($methodName): bool { return $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(); } @@ -256,16 +257,16 @@ static function (Type $type) use ($methodName): bool { ]; } - $lowercasedMethodName = sprintf( + $lowercasedMethodName = SprintfHelper::escapeFormatString(sprintf( '%s %s', $method->isStatic() ? 'static method' : 'method', $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()' - ); - $displayMethodName = sprintf( + )); + $displayMethodName = SprintfHelper::escapeFormatString(sprintf( '%s %s', $method->isStatic() ? 'Static method' : 'Method', $method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()' - ); + )); $errors = array_merge($errors, $this->check->check( ParametersAcceptorSelector::selectFromArgs( diff --git a/src/Rules/Methods/ExistingClassesInTypehintsRule.php b/src/Rules/Methods/ExistingClassesInTypehintsRule.php index 3ca81d6c3b..7632ef1489 100644 --- a/src/Rules/Methods/ExistingClassesInTypehintsRule.php +++ b/src/Rules/Methods/ExistingClassesInTypehintsRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\Rules\FunctionDefinitionCheck; @@ -36,21 +37,24 @@ public function processNode(Node $node, Scope $scope): array throw new \PHPStan\ShouldNotHappenException(); } + $className = SprintfHelper::escapeFormatString($scope->getClassReflection()->getDisplayName()); + $methodName = SprintfHelper::escapeFormatString($methodReflection->getName()); + return $this->check->checkClassMethod( $methodReflection, $node->getOriginalNode(), sprintf( 'Parameter $%%s of method %s::%s() has invalid type %%s.', - $scope->getClassReflection()->getDisplayName(), - $methodReflection->getName() + $className, + $methodName ), sprintf( 'Method %s::%s() has invalid return type %%s.', - $scope->getClassReflection()->getDisplayName(), - $methodReflection->getName() + $className, + $methodName ), - sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $scope->getClassReflection()->getDisplayName(), $methodReflection->getName()), - sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $scope->getClassReflection()->getDisplayName(), $methodReflection->getName()) + sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $className, $methodName), + sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $className, $methodName) ); } diff --git a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php index ac85f668dd..83e97fa8a3 100644 --- a/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatibleClassConstantPhpDocTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Generics\GenericObjectTypeCheck; @@ -102,27 +103,30 @@ private function processSingleConstant(ClassReflection $classReflection, string } } + $className = SprintfHelper::escapeFormatString($constantReflection->getDeclaringClass()->getDisplayName()); + $escapedConstantName = SprintfHelper::escapeFormatString($constantName); + return array_merge($errors, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( 'PHPDoc tag @var for constant %s::%s contains generic type %%s but class %%s is not generic.', - $constantReflection->getDeclaringClass()->getDisplayName(), - $constantName + $className, + $escapedConstantName ), sprintf( 'Generic type %%s in PHPDoc tag @var for constant %s::%s does not specify all template types of class %%s: %%s', - $constantReflection->getDeclaringClass()->getDisplayName(), - $constantName + $className, + $escapedConstantName ), sprintf( 'Generic type %%s in PHPDoc tag @var for constant %s::%s specifies %%d template types, but class %%s supports only %%d: %%s', - $constantReflection->getDeclaringClass()->getDisplayName(), - $constantName + $className, + $escapedConstantName ), sprintf( 'Type %%s in generic type %%s in PHPDoc tag @var for constant %s::%s is not subtype of template type %%s of class %%s.', - $constantReflection->getDeclaringClass()->getDisplayName(), - $constantName + $className, + $escapedConstantName ) )); } diff --git a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php index 6a05e6131b..5da471cdb6 100644 --- a/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; @@ -96,23 +97,25 @@ public function processNode(Node $node, Scope $scope): array } $isParamSuperType = $nativeParamType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocParamType)); + $escapedParameterName = SprintfHelper::escapeFormatString($parameterName); + $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $phpDocParamType, sprintf( 'PHPDoc tag @param for parameter $%s contains generic type %%s but class %%s is not generic.', - $parameterName + $escapedParameterName ), sprintf( 'Generic type %%s in PHPDoc tag @param for parameter $%s does not specify all template types of class %%s: %%s', - $parameterName + $escapedParameterName ), sprintf( 'Generic type %%s in PHPDoc tag @param for parameter $%s specifies %%d template types, but class %%s supports only %%d: %%s', - $parameterName + $escapedParameterName ), sprintf( 'Type %%s in generic type %%s in PHPDoc tag @param for parameter $%s is not subtype of template type %%s of class %%s.', - $parameterName + $escapedParameterName ) )); diff --git a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php index e2494d919f..0f2ab5119d 100644 --- a/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php +++ b/src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Node\ClassPropertyNode; use PHPStan\Rules\Generics\GenericObjectTypeCheck; use PHPStan\Rules\Rule; @@ -88,31 +89,34 @@ public function processNode(Node $node, Scope $scope): array ))->build(); } + $className = SprintfHelper::escapeFormatString($propertyReflection->getDeclaringClass()->getDisplayName()); + $escapedPropertyName = SprintfHelper::escapeFormatString($propertyName); + $messages = array_merge($messages, $this->genericObjectTypeCheck->check( $phpDocType, sprintf( '%s for property %s::$%s contains generic type %%s but class %%s is not generic.', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + $className, + $escapedPropertyName ), sprintf( 'Generic type %%s in %s for property %s::$%s does not specify all template types of class %%s: %%s', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + $className, + $escapedPropertyName ), sprintf( 'Generic type %%s in %s for property %s::$%s specifies %%d template types, but class %%s supports only %%d: %%s', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + $className, + $escapedPropertyName ), sprintf( 'Type %%s in generic type %%s in %s for property %s::$%s is not subtype of template type %%s of class %%s.', $description, - $propertyReflection->getDeclaringClass()->getDisplayName(), - $propertyName + $className, + $escapedPropertyName ) )); diff --git a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php index 9acb9457f1..c5d0c35576 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocVarTagTypeRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; @@ -113,12 +114,13 @@ public function processNode(Node $node, Scope $scope): array } } + $escapedIdentifier = SprintfHelper::escapeFormatString($identifier); $errors = array_merge($errors, $this->genericObjectTypeCheck->check( $varTagType, - sprintf('%s contains generic type %%s but class %%s is not generic.', $identifier), - sprintf('Generic type %%s in %s does not specify all template types of class %%s: %%s', $identifier), - sprintf('Generic type %%s in %s specifies %%d template types, but class %%s supports only %%d: %%s', $identifier), - sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of class %%s.', $identifier) + sprintf('%s contains generic type %%s but class %%s is not generic.', $escapedIdentifier), + sprintf('Generic type %%s in %s does not specify all template types of class %%s: %%s', $escapedIdentifier), + sprintf('Generic type %%s in %s specifies %%d template types, but class %%s supports only %%d: %%s', $escapedIdentifier), + sprintf('Type %%s in generic type %%s in %s is not subtype of template type %%s of class %%s.', $escapedIdentifier) )); foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($varTagType) as [$innerName, $genericTypeNames]) { diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index 7ab6fe9409..6de912f2fe 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -5,6 +5,7 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; @@ -72,7 +73,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $typeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->var, - sprintf('Access to property $%s on an unknown class %%s.', $name), + sprintf('Access to property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), static function (Type $type) use ($name): bool { return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(); } diff --git a/src/Rules/Properties/AccessStaticPropertiesRule.php b/src/Rules/Properties/AccessStaticPropertiesRule.php index 4233156fdf..b29beb813b 100644 --- a/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassNameNodePair; @@ -146,7 +147,7 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, $classTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->class, - sprintf('Access to static property $%s on an unknown class %%s.', $name), + sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)), static function (Type $type) use ($name): bool { return $type->canAccessProperties()->yes() && $type->hasProperty($name)->yes(); } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 9c042af244..30277d2480 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -303,4 +303,15 @@ public function testBug4842(): void $this->analyse([__DIR__ . '/data/bug-4842.php'], []); } + public function testBug5669(): void + { + $this->analyse([__DIR__ . '/data/bug-5669.php'], [ + [ + 'Access to offset \'%customer…\' on an unknown class Bug5669\arr.', + 26, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-5669.php b/tests/PHPStan/Rules/Arrays/data/bug-5669.php new file mode 100644 index 0000000000..a280cce420 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-5669.php @@ -0,0 +1,30 @@ + + */ + public function getReplacer() + { + return []; + } + +} + +class c extends a +{ + + public function getReplacer() + { + $replacer = parent::getReplacer(); + $replacer['%customer_salutation%'] = 'test'; + + return $replacer; + } +}