Skip to content
Draft
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
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ parameters:
rawMessageInBaseline: true
reportNestedTooWideType: false
assignToByRefForeachExpr: true
checkTypeCoercions: true
3 changes: 3 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ parameters:
rawMessageInBaseline: false
reportNestedTooWideType: false
assignToByRefForeachExpr: false
checkTypeCoercions: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -221,6 +222,8 @@ parameters:
- [parameters, memoryLimitFile]
- [parameters, pro]
- parametersSchema
allowedTypeCoercions:
boolToString: true

extensions:
rules: PHPStan\DependencyInjection\RulesExtension
Expand Down
4 changes: 4 additions & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ parametersSchema:
rawMessageInBaseline: bool()
reportNestedTooWideType: bool()
assignToByRefForeachExpr: bool()
checkTypeCoercions: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down Expand Up @@ -163,6 +164,9 @@ parametersSchema:
string(),
listOf(string()),
)))
allowedTypeCoercions: structure([
boolToString: bool()
])

# playground mode
sourceLocatorPlaygroundMode: bool()
Expand Down
6 changes: 4 additions & 2 deletions src/Rules/Cast/InvalidPartOfEncapsedStringRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +25,7 @@ final class InvalidPartOfEncapsedStringRule implements Rule
public function __construct(
private ExprPrinter $exprPrinter,
private RuleLevelHelper $ruleLevelHelper,
private TypeCoercionRuleHelper $typeCoercionRuleHelper,
)
{
}
Expand All @@ -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;
}
Expand Down
34 changes: 34 additions & 0 deletions src/Rules/TypeCoercionRuleHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules;

use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Type;

#[AutowiredService]
final class TypeCoercionRuleHelper
{

public function __construct(
#[AutowiredParameter(ref: '%featureToggles.checkTypeCoercions%')]
private readonly bool $checkTypeCoercions,
#[AutowiredParameter(ref: '%allowedTypeCoercions.boolToString%')]
private readonly bool $allowBoolToString,
)
{
}

public function coerceToString(Type $type): Type
{
if (!$this->checkTypeCoercions) {
return $type->toString();
}
if (!$this->allowBoolToString && !$type->isBoolean()->no()) {
return new ErrorType();
}
return $type->toString();
}

}
68 changes: 67 additions & 1 deletion tests/PHPStan/Rules/Cast/InvalidPartOfEncapsedStringRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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),
);
}

Expand All @@ -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,
],
]);
}
Expand All @@ -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,
],
]);
}

}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Cast/data/invalid-encapsed-part-enum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php // lint >= 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}";
}
61 changes: 58 additions & 3 deletions tests/PHPStan/Rules/Cast/data/invalid-encapsed-part.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,64 @@
<?php

function (
namespace InvalidPartOfEncapsedString;

class ClassWithToString
{
public function __toString(): string
{
return 'str';
}
}

function foo(
string $str,
\stdClass $std
\stdClass $std,
bool $bool,
int $int,
float $float,
array $array,
ClassWithToString $objectWithToString
) {
$null = null;
$resource = fopen('php://input');
assert($resource !== false);
"$str bar";
"$std bar";
};
"$bool bar";
"$int bar";
"$float bar";
"$array bar";
"$objectWithToString bar";
"$null bar";
"$resource bar";
}

/**
* @param string|\stdClass $std
* @param string|bool $bool
* @param string|int $int
* @param string|float $float
* @param string|array $array
* @param string|ClassWithToString $objectWithToString
* @param string|null $null
* @param string|resource $resource
*/
function checkUnions(
$std,
$bool,
$int,
$float,
$array,
$objectWithToString,
$null,
$resource
) {
"$std bar";
"$bool bar";
"$int bar";
"$float bar";
"$array bar";
"$objectWithToString bar";
"$null bar";
"$resource bar";
}
Loading