diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index f6f9d7778a..1b0dbe5e6d 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -14,3 +14,4 @@ parameters: rawMessageInBaseline: true reportNestedTooWideType: false assignToByRefForeachExpr: true + checkTypeCoercions: true diff --git a/conf/config.neon b/conf/config.neon index c3b634f673..88b50144c5 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -38,6 +38,7 @@ parameters: rawMessageInBaseline: false reportNestedTooWideType: false assignToByRefForeachExpr: false + checkTypeCoercions: false fileExtensions: - php checkAdvancedIsset: false @@ -221,6 +222,8 @@ parameters: - [parameters, memoryLimitFile] - [parameters, pro] - parametersSchema + allowedTypeCoercions: + boolToString: true extensions: rules: PHPStan\DependencyInjection\RulesExtension diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 001603aacd..582c590c3f 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -41,6 +41,7 @@ parametersSchema: rawMessageInBaseline: bool() reportNestedTooWideType: bool() assignToByRefForeachExpr: bool() + checkTypeCoercions: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() @@ -163,6 +164,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 d8d899e72d..b1432a90f1 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 0000000000..1e2772932d --- /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 b42c44cd09..6f459c1509 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,11 +16,14 @@ class InvalidPartOfEncapsedStringRuleTest extends RuleTestCase { + private ?TypeCoercionRuleHelper $typeCoercionRuleHelper = null; + protected function getRule(): Rule { return new InvalidPartOfEncapsedStringRule( new ExprPrinter(new Printer()), new RuleLevelHelper(self::createReflectionProvider(), true, false, true, false, false, false, true), + $this->typeCoercionRuleHelper ?? new TypeCoercionRuleHelper(true, true), ); } @@ -28,7 +32,50 @@ 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, + ], + ]); + } + + 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, ], ]); } @@ -44,4 +91,23 @@ public function testRuleWithNullsafeVariant(): void ]); } + #[RequiresPhp('>= 8.1')] + 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 0000000000..439c250419 --- /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 f29a9275c0..1b6ae69677 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 @@