From 6127c936b03898ed9693d1d1740cd27df06c146c Mon Sep 17 00:00:00 2001 From: schlndh Date: Fri, 3 Oct 2025 12:35:48 +0200 Subject: [PATCH 1/2] add more tests for InvalidPartOfEncapsedStringRule --- .../InvalidPartOfEncapsedStringRuleTest.php | 33 +++++++++- .../Cast/data/invalid-encapsed-part-enum.php | 24 ++++++++ .../Rules/Cast/data/invalid-encapsed-part.php | 61 ++++++++++++++++++- 3 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-enum.php diff --git a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php index e366eb79e33..4feec1cfde1 100644 --- a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php @@ -37,7 +37,19 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/invalid-encapsed-part.php'], [ [ 'Part $std (stdClass) of encapsed string cannot be cast to string.', - 8, + 26, + ], + [ + 'Part $array (array) of encapsed string cannot be cast to string.', + 30, + ], + [ + 'Part $std (stdClass|string) of encapsed string cannot be cast to string.', + 56, + ], + [ + 'Part $array (array|string) of encapsed string cannot be cast to string.', + 60, ], ]); } @@ -53,4 +65,23 @@ public function testRuleWithNullsafeVariant(): void ]); } + #[RequiresPhp('>= 8.1.0')] + public function testRuleWithEnum(): void + { + $this->analyse([__DIR__ . '/data/invalid-encapsed-part-enum.php'], [ + [ + 'Part $unitEnum (InvalidEncapsedPartEnum\\FooUnitEnum) of encapsed string cannot be cast to string.', + 21, + ], + [ + 'Part $intEnum (InvalidEncapsedPartEnum\\IntEnum) of encapsed string cannot be cast to string.', + 22, + ], + [ + 'Part $stringEnum (InvalidEncapsedPartEnum\\StringEnum) of encapsed string cannot be cast to string.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-enum.php b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-enum.php new file mode 100644 index 00000000000..439c2504193 --- /dev/null +++ b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-enum.php @@ -0,0 +1,24 @@ += 8.1 + +namespace InvalidEncapsedPartEnum; + +enum FooUnitEnum +{ + case A; +} + +enum IntEnum: int +{ + case A = 1; +} + +enum StringEnum: string +{ + case A = 'a'; +} + +function doFoo(FooUnitEnum $unitEnum, IntEnum $intEnum, StringEnum $stringEnum) { + "{$unitEnum}"; + "{$intEnum}"; + "{$stringEnum}"; +} diff --git a/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part.php b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part.php index f29a9275c0b..1b6ae696771 100644 --- a/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part.php +++ b/tests/PHPStan/Rules/Cast/data/invalid-encapsed-part.php @@ -1,9 +1,64 @@ Date: Fri, 3 Oct 2025 13:37:44 +0200 Subject: [PATCH 2/2] WIP: add option to prohibit coercing bool to string --- conf/bleedingEdge.neon | 1 + conf/config.neon | 3 ++ conf/parametersSchema.neon | 4 +++ .../Cast/InvalidPartOfEncapsedStringRule.php | 6 ++-- src/Rules/TypeCoercionRuleHelper.php | 34 ++++++++++++++++++ .../InvalidPartOfEncapsedStringRuleTest.php | 35 +++++++++++++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/Rules/TypeCoercionRuleHelper.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 7098877fb44..3178fc58652 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -34,3 +34,4 @@ parameters: unusedVariable: true unusedParameters: true reuseCollectedDataRuleErrors: true + checkTypeCoercions: true diff --git a/conf/config.neon b/conf/config.neon index 0eb6c0c764b..68f65bcf981 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -66,6 +66,7 @@ parameters: unusedVariable: false unusedParameters: false reuseCollectedDataRuleErrors: false + checkTypeCoercions: false fileExtensions: - php checkAdvancedIsset: false @@ -267,6 +268,8 @@ parameters: - [parameters, tmpDir] - [parameters, pro] - parametersSchema + allowedTypeCoercions: + boolToString: true autowiredAttributeServices: level: null diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 62dab9d9c3b..9320c1021d4 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -63,6 +63,7 @@ parametersSchema: unusedVariable: bool() unusedParameters: bool() reuseCollectedDataRuleErrors: bool() + checkTypeCoercions: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() @@ -199,6 +200,9 @@ parametersSchema: string(), listOf(string()), ))) + allowedTypeCoercions: structure([ + boolToString: bool() + ]) # playground mode sourceLocatorPlaygroundMode: bool() diff --git a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php index d8d899e72d1..b1432a90f11 100644 --- a/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php +++ b/src/Rules/Cast/InvalidPartOfEncapsedStringRule.php @@ -9,6 +9,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Rules\TypeCoercionRuleHelper; use PHPStan\Type\ErrorType; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; @@ -24,6 +25,7 @@ final class InvalidPartOfEncapsedStringRule implements Rule public function __construct( private ExprPrinter $exprPrinter, private RuleLevelHelper $ruleLevelHelper, + private TypeCoercionRuleHelper $typeCoercionRuleHelper, ) { } @@ -45,14 +47,14 @@ public function processNode(Node $node, Scope $scope): array $scope, $part, '', - static fn (Type $type): bool => !$type->toString() instanceof ErrorType, + fn (Type $type): bool => !$this->typeCoercionRuleHelper->coerceToString($type) instanceof ErrorType, ); $partType = $typeResult->getType(); if ($partType instanceof ErrorType) { continue; } - $stringPartType = $partType->toString(); + $stringPartType = $this->typeCoercionRuleHelper->coerceToString($partType); if (!$stringPartType instanceof ErrorType) { continue; } diff --git a/src/Rules/TypeCoercionRuleHelper.php b/src/Rules/TypeCoercionRuleHelper.php new file mode 100644 index 00000000000..1e2772932d7 --- /dev/null +++ b/src/Rules/TypeCoercionRuleHelper.php @@ -0,0 +1,34 @@ +checkTypeCoercions) { + return $type->toString(); + } + if (!$this->allowBoolToString && !$type->isBoolean()->no()) { + return new ErrorType(); + } + return $type->toString(); + } + +} diff --git a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php index 4feec1cfde1..8b2beff5036 100644 --- a/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php +++ b/tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Node\Printer\Printer; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Rules\TypeCoercionRuleHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -15,6 +16,8 @@ class InvalidPartOfEncapsedStringRuleTest extends RuleTestCase { + private ?TypeCoercionRuleHelper $typeCoercionRuleHelper = null; + protected function getRule(): Rule { return new InvalidPartOfEncapsedStringRule( @@ -29,6 +32,7 @@ protected function getRule(): Rule checkBenevolentUnionTypes: false, discoveringSymbolsTip: true, ), + $this->typeCoercionRuleHelper ?? new TypeCoercionRuleHelper(true, true), ); } @@ -54,6 +58,37 @@ public function testRule(): void ]); } + public function testRuleWithStrictCoercions(): void + { + $this->typeCoercionRuleHelper = new TypeCoercionRuleHelper(true, false); + $this->analyse([__DIR__ . '/data/invalid-encapsed-part.php'], [ + [ + 'Part $std (stdClass) of encapsed string cannot be cast to string.', + 26, + ], + [ + 'Part $bool (bool) of encapsed string cannot be cast to string.', + 27, + ], + [ + 'Part $array (array) of encapsed string cannot be cast to string.', + 30, + ], + [ + 'Part $std (stdClass|string) of encapsed string cannot be cast to string.', + 56, + ], + [ + 'Part $bool (bool|string) of encapsed string cannot be cast to string.', + 57, + ], + [ + 'Part $array (array|string) of encapsed string cannot be cast to string.', + 60, + ], + ]); + } + #[RequiresPhp('>= 8.0.0')] public function testRuleWithNullsafeVariant(): void {