|
6 | 6 | use Closure; |
7 | 7 | use PhpParser\Comment\Doc; |
8 | 8 | use PhpParser\Node; |
| 9 | +use PhpParser\Node\Arg; |
9 | 10 | use PhpParser\Node\Expr; |
10 | 11 | use PhpParser\Node\Expr\Array_; |
11 | 12 | use PhpParser\Node\Expr\ArrayDimFetch; |
|
109 | 110 | use PHPStan\Reflection\ReflectionProvider; |
110 | 111 | use PHPStan\ShouldNotHappenException; |
111 | 112 | use PHPStan\TrinaryLogic; |
112 | | -use PHPStan\Type\Accessory\NonEmptyArrayType; |
113 | 113 | use PHPStan\Type\ArrayType; |
114 | 114 | use PHPStan\Type\ClosureType; |
115 | 115 | use PHPStan\Type\Constant\ConstantArrayType; |
116 | 116 | use PHPStan\Type\Constant\ConstantArrayTypeBuilder; |
117 | 117 | use PHPStan\Type\Constant\ConstantBooleanType; |
118 | | -use PHPStan\Type\Constant\ConstantIntegerType; |
119 | 118 | use PHPStan\Type\Constant\ConstantStringType; |
120 | 119 | use PHPStan\Type\ErrorType; |
121 | 120 | use PHPStan\Type\FileTypeMapper; |
| 121 | +use PHPStan\Type\GeneralizePrecision; |
122 | 122 | use PHPStan\Type\Generic\GenericClassStringType; |
123 | 123 | use PHPStan\Type\Generic\TemplateTypeHelper; |
124 | 124 | use PHPStan\Type\Generic\TemplateTypeMap; |
@@ -1843,75 +1843,80 @@ function (MutatingScope $scope) use ($expr, $nodeCallback, $context): Expression |
1843 | 1843 | && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true) |
1844 | 1844 | && count($expr->getArgs()) >= 2 |
1845 | 1845 | ) { |
1846 | | - $argumentTypes = []; |
1847 | | - foreach (array_slice($expr->getArgs(), 1) as $callArg) { |
1848 | | - $callArgType = $scope->getType($callArg->value); |
1849 | | - if ($callArg->unpack) { |
1850 | | - $iterableValueType = $callArgType->getIterableValueType(); |
1851 | | - if ($iterableValueType instanceof UnionType) { |
1852 | | - foreach ($iterableValueType->getTypes() as $innerType) { |
1853 | | - $argumentTypes[] = $innerType; |
| 1846 | + $arrayArg = $expr->getArgs()[0]->value; |
| 1847 | + $arrayType = $scope->getType($arrayArg); |
| 1848 | + $callArgs = array_slice($expr->getArgs(), 1); |
| 1849 | + |
| 1850 | + /** |
| 1851 | + * @param Arg[] $callArgs |
| 1852 | + * @param callable(?Type, Type, bool=): void $setOffsetValueType |
| 1853 | + */ |
| 1854 | + $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void { |
| 1855 | + foreach ($callArgs as $callArg) { |
| 1856 | + $callArgType = $scope->getType($callArg->value); |
| 1857 | + if ($callArg->unpack) { |
| 1858 | + if ($callArgType->isIterableAtLeastOnce()->no()) { |
| 1859 | + continue; |
1854 | 1860 | } |
1855 | | - } else { |
1856 | | - $argumentTypes[] = $iterableValueType; |
| 1861 | + if (!$callArgType instanceof ConstantArrayType) { |
| 1862 | + $nonConstantArrayWasUnpacked = true; |
| 1863 | + } |
| 1864 | + $iterableValueType = $callArgType->getIterableValueType(); |
| 1865 | + $isOptional = !$callArgType->isIterableAtLeastOnce()->yes(); |
| 1866 | + if ($iterableValueType instanceof UnionType) { |
| 1867 | + foreach ($iterableValueType->getTypes() as $innerType) { |
| 1868 | + $setOffsetValueType(null, $innerType, $isOptional); |
| 1869 | + } |
| 1870 | + } else { |
| 1871 | + $setOffsetValueType(null, $iterableValueType, $isOptional); |
| 1872 | + } |
| 1873 | + continue; |
1857 | 1874 | } |
1858 | | - continue; |
| 1875 | + $setOffsetValueType(null, $callArgType); |
1859 | 1876 | } |
| 1877 | + }; |
1860 | 1878 |
|
1861 | | - $argumentTypes[] = $callArgType; |
1862 | | - } |
| 1879 | + if ($arrayType instanceof ConstantArrayType) { |
| 1880 | + $prepend = $functionReflection->getName() === 'array_unshift'; |
| 1881 | + $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($arrayType); |
1863 | 1882 |
|
1864 | | - $arrayArg = $expr->getArgs()[0]->value; |
1865 | | - $originalArrayType = $scope->getType($arrayArg); |
1866 | | - $constantArrays = TypeUtils::getOldConstantArrays($originalArrayType); |
1867 | | - if ( |
1868 | | - $functionReflection->getName() === 'array_push' |
1869 | | - || ($originalArrayType->isArray()->yes() && count($constantArrays) === 0) |
1870 | | - ) { |
1871 | | - $arrayType = $originalArrayType; |
1872 | | - foreach ($argumentTypes as $argType) { |
1873 | | - $arrayType = $arrayType->setOffsetValueType(null, $argType); |
1874 | | - } |
1875 | | - |
1876 | | - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType($arrayArg, TypeCombinator::intersect($arrayType, new NonEmptyArrayType())); |
1877 | | - } elseif (count($constantArrays) > 0) { |
1878 | | - $defaultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); |
1879 | | - foreach ($argumentTypes as $argType) { |
1880 | | - $defaultArrayBuilder->setOffsetValueType(null, $argType); |
1881 | | - } |
| 1883 | + $setOffsetValueTypes( |
| 1884 | + $scope, |
| 1885 | + $callArgs, |
| 1886 | + static function (?Type $offsetType, Type $valueType, bool $optional = false) use (&$arrayTypeBuilder): void { |
| 1887 | + $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional); |
| 1888 | + }, |
| 1889 | + $nonConstantArrayWasUnpacked, |
| 1890 | + ); |
1882 | 1891 |
|
1883 | | - $defaultArrayType = $defaultArrayBuilder->getArray(); |
1884 | | - if (!$defaultArrayType instanceof ConstantArrayType) { |
1885 | | - $arrayType = $originalArrayType; |
1886 | | - foreach ($argumentTypes as $argType) { |
1887 | | - $arrayType = $arrayType->setOffsetValueType(null, $argType); |
| 1892 | + if ($prepend) { |
| 1893 | + $keyTypes = $arrayType->getKeyTypes(); |
| 1894 | + $valueTypes = $arrayType->getValueTypes(); |
| 1895 | + foreach ($keyTypes as $k => $keyType) { |
| 1896 | + $arrayTypeBuilder->setOffsetValueType( |
| 1897 | + $keyType instanceof ConstantStringType ? $keyType : null, |
| 1898 | + $valueTypes[$k], |
| 1899 | + $arrayType->isOptionalKey($k), |
| 1900 | + ); |
1888 | 1901 | } |
| 1902 | + } |
1889 | 1903 |
|
1890 | | - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType($arrayArg, TypeCombinator::intersect($arrayType, new NonEmptyArrayType())); |
1891 | | - } else { |
1892 | | - $arrayTypes = []; |
1893 | | - foreach ($constantArrays as $constantArray) { |
1894 | | - $arrayTypeBuilder = ConstantArrayTypeBuilder::createFromConstantArray($defaultArrayType); |
1895 | | - foreach ($constantArray->getKeyTypes() as $i => $keyType) { |
1896 | | - $valueType = $constantArray->getValueTypes()[$i]; |
1897 | | - if ($keyType instanceof ConstantIntegerType) { |
1898 | | - $keyType = null; |
1899 | | - } |
1900 | | - $arrayTypeBuilder->setOffsetValueType( |
1901 | | - $keyType, |
1902 | | - $valueType, |
1903 | | - $constantArray->isOptionalKey($i), |
1904 | | - ); |
1905 | | - } |
1906 | | - $arrayTypes[] = $arrayTypeBuilder->getArray(); |
1907 | | - } |
| 1904 | + $arrayType = $arrayTypeBuilder->getArray(); |
1908 | 1905 |
|
1909 | | - $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType( |
1910 | | - $arrayArg, |
1911 | | - TypeCombinator::union(...$arrayTypes), |
1912 | | - ); |
| 1906 | + if ($arrayType instanceof ConstantArrayType && $nonConstantArrayWasUnpacked) { |
| 1907 | + $arrayType = $arrayType->generalize(GeneralizePrecision::lessSpecific()); |
1913 | 1908 | } |
| 1909 | + } else { |
| 1910 | + $setOffsetValueTypes( |
| 1911 | + $scope, |
| 1912 | + $callArgs, |
| 1913 | + static function (?Type $offsetType, Type $valueType) use (&$arrayType): void { |
| 1914 | + $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType); |
| 1915 | + }, |
| 1916 | + ); |
1914 | 1917 | } |
| 1918 | + |
| 1919 | + $scope = $scope->invalidateExpression($arrayArg)->specifyExpressionType($arrayArg, $arrayType); |
1915 | 1920 | } |
1916 | 1921 |
|
1917 | 1922 | if ( |
|
0 commit comments