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/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ parameters:
reportWrongPhpDocTypeInVarTag: false
reportAnyTypeWideningInVarTag: false
reportNonIntStringArrayKey: false
reportUnsafeArrayStringKeyCasting: prevent
reportPossiblyNonexistentGeneralArrayOffset: false
reportPossiblyNonexistentConstantArrayOffset: false
checkMissingOverrideMethodAttribute: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ parametersSchema:
reportWrongPhpDocTypeInVarTag: bool()
reportAnyTypeWideningInVarTag: bool()
reportNonIntStringArrayKey: bool()
reportUnsafeArrayStringKeyCasting: schema(string(), pattern('detect|prevent'), nullable())
reportPossiblyNonexistentGeneralArrayOffset: bool()
reportPossiblyNonexistentConstantArrayOffset: bool()
checkMissingOverrideMethodAttribute: bool()
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/ContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ public static function postInitializeContainer(Container $container): void
$container->getService('typeSpecifier');

BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']);
ReportUnsafeArrayStringKeyCastingToggle::setLevel($container->getParameter('reportUnsafeArrayStringKeyCasting'));
}

public function getCurrentWorkingDirectory(): string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection;

/**
* @phpstan-type Level = self::DETECT|self::PREVENT|null
*/
final class ReportUnsafeArrayStringKeyCastingToggle
{

public const DETECT = 'detect';

public const PREVENT = 'prevent';

/** @var Level */
private static ?string $level = null;

/**
* @return Level
*/
public static function getLevel(): ?string
{
return self::$level;
}

/**
* @param Level $level
*/
public static function setLevel(?string $level): void
{
self::$level = $level;
}

}
1 change: 1 addition & 0 deletions src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry
}

}, new OversizedArrayBuilder(), true),
reportUnsafeArrayStringKeyCasting: null,
),
),
);
Expand Down
31 changes: 28 additions & 3 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use PhpParser\Node\Name;
use PHPStan\Analyser\ConstantResolver;
use PHPStan\Analyser\NameScope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
Expand Down Expand Up @@ -106,6 +108,7 @@
use PHPStan\Type\TypeAliasResolver;
use PHPStan\Type\TypeAliasResolverProvider;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
use PHPStan\Type\ValueOfType;
Expand All @@ -128,19 +131,27 @@
use function strtolower;
use function substr;

/**
* @phpstan-import-type Level from ReportUnsafeArrayStringKeyCastingToggle as ReportUnsafeArrayStringKeyCastingLevel
*/
#[AutowiredService]
final class TypeNodeResolver
{

/** @var array<string, true> */
private array $genericTypeResolvingStack = [];

/**
* @param ReportUnsafeArrayStringKeyCastingLevel $reportUnsafeArrayStringKeyCasting
*/
public function __construct(
private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider,
private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider,
private TypeAliasResolverProvider $typeAliasResolverProvider,
private ConstantResolver $constantResolver,
private InitializerExprTypeResolver $initializerExprTypeResolver,
#[AutowiredParameter]
private ?string $reportUnsafeArrayStringKeyCasting,
)
{
}
Expand Down Expand Up @@ -661,7 +672,7 @@ private function resolveConditionalTypeForParameterNode(ConditionalTypeForParame
private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type
{
$itemType = $this->resolve($typeNode->type, $nameScope);
return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $itemType);
return new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $itemType);
}

private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type
Expand All @@ -686,9 +697,23 @@ static function (string $variance): TemplateTypeVariance {

if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) {
if (count($genericTypes) === 1) { // array<ValueType>
$arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]);
$arrayType = new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $genericTypes[0]);
} elseif (count($genericTypes) === 2) { // array<KeyType, ValueType>
$keyType = TypeCombinator::intersect($genericTypes[0]->toArrayKey(), new UnionType([
$originalKey = $genericTypes[0];
if ($this->reportUnsafeArrayStringKeyCasting === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
$originalKey = TypeTraverser::map($originalKey, static function (Type $type, callable $traverse) {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type instanceof StringType) {
return TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true));
}

return $type;
});
}
$keyType = TypeCombinator::intersect($originalKey->toArrayKey(), new UnionType([
new IntegerType(),
new StringType(),
]))->toArrayKey();
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Accessory/AccessoryDecimalIntegerStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ public function toArray(): Type
public function toArrayKey(): Type
{
if ($this->inverse) {
return new StringType();
return $this;
}

return new IntegerType();
Expand Down
17 changes: 13 additions & 4 deletions src/Type/Accessory/AccessoryNumericStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Type\Accessory;

use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
Expand Down Expand Up @@ -209,12 +210,20 @@ public function toArray(): Type

public function toArrayKey(): Type
{
$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
new UnionType([
new IntegerType(),
new IntersectionType([
new StringType(),
new AccessoryNumericStringType(),
]),
]);
}

return new UnionType([
new IntegerType(),
new IntersectionType([
new StringType(),
new AccessoryNumericStringType(),
]),
new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]),
]);
}

Expand Down
44 changes: 39 additions & 5 deletions src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Type;

use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
Expand All @@ -12,6 +13,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
use PHPStan\Type\Accessory\HasOffsetValueType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
Expand All @@ -32,6 +34,7 @@
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
use function array_merge;
use function count;
use function in_array;
use function sprintf;

/** @api */
Expand All @@ -47,14 +50,16 @@ class ArrayType implements Type

private Type $keyType;

private ?Type $cachedIterableKeyType = null;

/** @api */
public function __construct(Type $keyType, private Type $itemType)
{
if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') {
if (in_array($keyType->describe(VerbosityLevel::value()), ['(int|string)', '(int|non-decimal-int-string)'], true)) {
$keyType = new MixedType();
}
if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) {
$keyType = new UnionType([new StringType(), new IntegerType()]);
$keyType = (new UnionType([new StringType(), new IntegerType()]))->toArrayKey();
}

$this->keyType = $keyType;
Expand Down Expand Up @@ -198,15 +203,44 @@ public function getArraySize(): Type

public function getIterableKeyType(): Type
{
if ($this->cachedIterableKeyType !== null) {
return $this->cachedIterableKeyType;
}
$keyType = $this->keyType;
if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) {
return new BenevolentUnionType([new IntegerType(), new StringType()]);
$keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
}
if ($keyType instanceof StrictMixedType) {
return new BenevolentUnionType([new IntegerType(), new StringType()]);
$keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
}

$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
if ($level === null) {
return $this->cachedIterableKeyType = $keyType;
}

if ($level === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
return $this->cachedIterableKeyType = $keyType;
}

if ($level !== ReportUnsafeArrayStringKeyCastingToggle::DETECT) { // @phpstan-ignore notIdentical.alwaysFalse
throw new ShouldNotHappenException();
}

return $keyType;
return $this->cachedIterableKeyType = TypeTraverser::map($keyType, static function (Type $type, callable $traverse): Type {
if ($type instanceof UnionType) {
return $traverse($type);
}

if ($type->isString()->yes() && !$type->isDecimalIntegerString()->no()) {
return TypeCombinator::union(
new IntegerType(),
TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)),
);
}

return $type;
});
}

public function getFirstIterableKeyType(): Type
Expand Down
12 changes: 11 additions & 1 deletion src/Type/StringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace PHPStan\Type;

use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
use PHPStan\Php\PhpVersion;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
Expand Down Expand Up @@ -177,7 +179,15 @@ public function toArray(): Type

public function toArrayKey(): Type
{
return $this;
$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
return $this;
}

return new UnionType([
new IntegerType(),
TypeCombinator::intersect($this, new AccessoryDecimalIntegerStringType(inverse: true)),
]);
}

public function toCoercedArgumentType(bool $strictTypes): Type
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Rules\Methods\CallMethodsRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use function array_merge;

/**
* @extends RuleTestCase<CallMethodsRule>
*/
class ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest extends RuleTestCase
{

public function getRule(): Rule
{
return self::getContainer()->getByType(CallMethodsRule::class);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [
[
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<string, stdClass> given.',
33,
],
[
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<string, stdClass> given.',
39,
],
]);
}

public static function getAdditionalConfigFiles(): array
{
return array_merge(
parent::getAdditionalConfigFiles(),
[
__DIR__ . '/reportUnsafeArrayStringKeyCastingDetect.neon',
],
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use function array_merge;

class ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest extends TypeInferenceTestCase
{

public static function dataAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/report-unsafe-array-string-key-casting-detect.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataAsserts')]
public function testAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return array_merge(
parent::getAdditionalConfigFiles(),
[
__DIR__ . '/reportUnsafeArrayStringKeyCastingDetect.neon',
],
);
}

}
Loading
Loading