1010use PhpParser \Node \Name ;
1111use PHPStan \Analyser \ConstantResolver ;
1212use PHPStan \Analyser \NameScope ;
13+ use PHPStan \DependencyInjection \AutowiredParameter ;
1314use PHPStan \DependencyInjection \AutowiredService ;
15+ use PHPStan \DependencyInjection \ReportUnsafeArrayStringKeyCastingToggle ;
1416use PHPStan \PhpDoc \Tag \TemplateTag ;
1517use PHPStan \PhpDocParser \Ast \ConstExpr \ConstExprArrayNode ;
1618use PHPStan \PhpDocParser \Ast \ConstExpr \ConstExprFalseNode ;
106108use PHPStan \Type \TypeAliasResolver ;
107109use PHPStan \Type \TypeAliasResolverProvider ;
108110use PHPStan \Type \TypeCombinator ;
111+ use PHPStan \Type \TypeTraverser ;
109112use PHPStan \Type \TypeUtils ;
110113use PHPStan \Type \UnionType ;
111114use PHPStan \Type \ValueOfType ;
128131use function strtolower ;
129132use function substr ;
130133
134+ /**
135+ * @phpstan-import-type Level from ReportUnsafeArrayStringKeyCastingToggle as ReportUnsafeArrayStringKeyCastingLevel
136+ */
131137#[AutowiredService]
132138final class TypeNodeResolver
133139{
134140
135141 /** @var array<string, true> */
136142 private array $ genericTypeResolvingStack = [];
137143
144+ /**
145+ * @param ReportUnsafeArrayStringKeyCastingLevel $reportUnsafeArrayStringKeyCasting
146+ */
138147 public function __construct (
139148 private TypeNodeResolverExtensionRegistryProvider $ extensionRegistryProvider ,
140149 private ReflectionProvider \ReflectionProviderProvider $ reflectionProviderProvider ,
141150 private TypeAliasResolverProvider $ typeAliasResolverProvider ,
142151 private ConstantResolver $ constantResolver ,
143152 private InitializerExprTypeResolver $ initializerExprTypeResolver ,
153+ #[AutowiredParameter]
154+ private ?string $ reportUnsafeArrayStringKeyCasting ,
144155 )
145156 {
146157 }
@@ -661,7 +672,7 @@ private function resolveConditionalTypeForParameterNode(ConditionalTypeForParame
661672 private function resolveArrayTypeNode (ArrayTypeNode $ typeNode , NameScope $ nameScope ): Type
662673 {
663674 $ itemType = $ this ->resolve ($ typeNode ->type , $ nameScope );
664- return new ArrayType (new BenevolentUnionType ([new IntegerType (), new StringType ()]), $ itemType );
675+ return new ArrayType (( new BenevolentUnionType ([new IntegerType (), new StringType ()]))-> toArrayKey ( ), $ itemType );
665676 }
666677
667678 private function resolveGenericTypeNode (GenericTypeNode $ typeNode , NameScope $ nameScope ): Type
@@ -686,9 +697,23 @@ static function (string $variance): TemplateTypeVariance {
686697
687698 if (in_array ($ mainTypeName , ['array ' , 'non-empty-array ' ], true )) {
688699 if (count ($ genericTypes ) === 1 ) { // array<ValueType>
689- $ arrayType = new ArrayType (new BenevolentUnionType ([new IntegerType (), new StringType ()]), $ genericTypes [0 ]);
700+ $ arrayType = new ArrayType (( new BenevolentUnionType ([new IntegerType (), new StringType ()]))-> toArrayKey ( ), $ genericTypes [0 ]);
690701 } elseif (count ($ genericTypes ) === 2 ) { // array<KeyType, ValueType>
691- $ keyType = TypeCombinator::intersect ($ genericTypes [0 ]->toArrayKey (), new UnionType ([
702+ $ originalKey = $ genericTypes [0 ];
703+ if ($ this ->reportUnsafeArrayStringKeyCasting === ReportUnsafeArrayStringKeyCastingToggle::PREVENT ) {
704+ $ originalKey = TypeTraverser::map ($ originalKey , static function (Type $ type , callable $ traverse ) {
705+ if ($ type instanceof UnionType || $ type instanceof IntersectionType) {
706+ return $ traverse ($ type );
707+ }
708+
709+ if ($ type instanceof StringType) {
710+ return TypeCombinator::intersect ($ type , new AccessoryDecimalIntegerStringType (inverse: true ));
711+ }
712+
713+ return $ type ;
714+ });
715+ }
716+ $ keyType = TypeCombinator::intersect ($ originalKey ->toArrayKey (), new UnionType ([
692717 new IntegerType (),
693718 new StringType (),
694719 ]))->toArrayKey ();
0 commit comments