diff --git a/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php b/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php index 078c5b848..49a683f9d 100644 --- a/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php +++ b/src/Psalm/Checker/Statements/Expression/Call/MethodCallChecker.php @@ -192,6 +192,7 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker case Type\Atomic\TArray::class: case Type\Atomic\ObjectLike::class: case Type\Atomic\TString::class: + case Type\Atomic\TSingleLetter::class: case Type\Atomic\TLiteralString::class: case Type\Atomic\TLiteralClassString::class: case Type\Atomic\TNumericString::class: diff --git a/src/Psalm/Checker/Statements/Expression/Fetch/ArrayFetchChecker.php b/src/Psalm/Checker/Statements/Expression/Fetch/ArrayFetchChecker.php index a6eebb827..e7cc03cb5 100644 --- a/src/Psalm/Checker/Statements/Expression/Fetch/ArrayFetchChecker.php +++ b/src/Psalm/Checker/Statements/Expression/Fetch/ArrayFetchChecker.php @@ -38,6 +38,7 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TSingleLetter; use Psalm\Type\Atomic\TString; class ArrayFetchChecker @@ -576,23 +577,37 @@ class ArrayFetchChecker } } + if ($type instanceof TSingleLetter) { + $valid_offset_type = Type::getInt(false, 0); + } elseif ($type instanceof TLiteralString) { + $valid_offsets = []; + + for ($i = 0, $l = strlen($type->value); $i < $l; $i++) { + $valid_offsets[] = new TLiteralInt($i); + } + + $valid_offset_type = new Type\Union($valid_offsets); + } else { + $valid_offset_type = Type::getInt(); + } + if (!TypeChecker::isContainedBy( $project_checker->codebase, $offset_type, - Type::getInt(), + $valid_offset_type, true )) { - $expected_offset_types[] = 'int'; + $expected_offset_types[] = $valid_offset_type->getId(); } else { $has_valid_offset = true; } if (!$array_access_type) { - $array_access_type = Type::getString(); + $array_access_type = Type::getSingleLetter(); } else { $array_access_type = Type::combineUnionTypes( $array_access_type, - Type::getString() + Type::getSingleLetter() ); } diff --git a/src/Psalm/Checker/TypeChecker.php b/src/Psalm/Checker/TypeChecker.php index 538914895..df7d0d5e5 100644 --- a/src/Psalm/Checker/TypeChecker.php +++ b/src/Psalm/Checker/TypeChecker.php @@ -28,6 +28,7 @@ use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TScalar; +use Psalm\Type\Atomic\TSingleLetter; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTrue; @@ -604,7 +605,10 @@ class TypeChecker return true; } - if (get_class($container_type_part) === TString::class && $input_type_part instanceof TLiteralString) { + if ((get_class($container_type_part) === TString::class + || get_class($container_type_part) === TSingleLetter::class) + && $input_type_part instanceof TLiteralString + ) { return true; } @@ -622,7 +626,9 @@ class TypeChecker return false; } - if (get_class($input_type_part) === TString::class && $container_type_part instanceof TLiteralString) { + if ((get_class($input_type_part) === TString::class || get_class($container_type_part) === TSingleLetter::class) + && $container_type_part instanceof TLiteralString + ) { $type_coerced = true; $type_coerced_from_scalar = true; @@ -651,6 +657,7 @@ class TypeChecker if (($input_type_part instanceof TClassString || $input_type_part instanceof TLiteralClassString) && (get_class($container_type_part) === TString::class + || get_class($container_type_part) === TSingleLetter::class || get_class($container_type_part) === Type\Atomic\GetClassT::class) ) { return true; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 71cbcd08b..578f42dd4 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -24,6 +24,7 @@ use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TResource; +use Psalm\Type\Atomic\TSingleLetter; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Atomic\TVoid; @@ -742,6 +743,16 @@ abstract class Type return new Union([$type]); } + /** + * @return Type\Union + */ + public static function getSingleLetter() + { + $type = new TSingleLetter; + + return new Union([$type]); + } + /** * @param string $class_type * diff --git a/src/Psalm/Type/Atomic/TSingleLetter.php b/src/Psalm/Type/Atomic/TSingleLetter.php new file mode 100644 index 000000000..b72ab6082 --- /dev/null +++ b/src/Psalm/Type/Atomic/TSingleLetter.php @@ -0,0 +1,6 @@ +strings = null; - $combination->value_types[$type_key] = $type; + + if (!isset($combination->value_types['string'])) { + $combination->value_types[$type_key] = $type; + } elseif (get_class($combination->value_types['string']) !== TString::class) { + if (get_class($type) === TString::class) { + $combination->value_types[$type_key] = $type; + } elseif (get_class($combination->value_types['string']) !== get_class($type)) { + $combination->value_types[$type_key] = new TString(); + } + } } } elseif ($type instanceof TInt) { if ($type instanceof TLiteralInt) { diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index c11446834..79e2cfbf7 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -243,6 +243,13 @@ class ArrayAccessTest extends TestCase echo $x["a"];', 'error_message' => 'InvalidArrayOffset', ], + 'noImpossibleStringAccess' => [ + ' 'InvalidArrayOffset', + ], ]; } }