|
13 | 13 | use PHPStan\Type\BooleanType; |
14 | 14 | use PHPStan\Type\CallableType; |
15 | 15 | use PHPStan\Type\ClassStringType; |
| 16 | +use PHPStan\Type\GeneralizePrecision; |
16 | 17 | use PHPStan\Type\Generic\GenericClassStringType; |
17 | 18 | use PHPStan\Type\Generic\TemplateTypeFactory; |
18 | 19 | use PHPStan\Type\Generic\TemplateTypeScope; |
@@ -1859,6 +1860,135 @@ public function testGetFiniteTypes(ConstantArrayType $type, array $expectedDescr |
1859 | 1860 | ); |
1860 | 1861 | } |
1861 | 1862 |
|
| 1863 | + public static function dataGeneralize(): iterable |
| 1864 | + { |
| 1865 | + $never = new NeverType(true); |
| 1866 | + $sealedMarker = [$never, $never]; |
| 1867 | + |
| 1868 | + yield 'sealed empty (legacy null unsealed)' => [ |
| 1869 | + new ConstantArrayType([], []), |
| 1870 | + GeneralizePrecision::lessSpecific(), |
| 1871 | + 'array{}', |
| 1872 | + ]; |
| 1873 | + |
| 1874 | + yield 'sealed empty (bleeding-edge NeverType marker)' => [ |
| 1875 | + new ConstantArrayType([], [], unsealed: $sealedMarker), |
| 1876 | + GeneralizePrecision::lessSpecific(), |
| 1877 | + 'array{}', |
| 1878 | + ]; |
| 1879 | + |
| 1880 | + yield 'sealed single explicit key' => [ |
| 1881 | + new ConstantArrayType( |
| 1882 | + [new ConstantStringType('a')], |
| 1883 | + [new ConstantIntegerType(1)], |
| 1884 | + unsealed: $sealedMarker, |
| 1885 | + ), |
| 1886 | + GeneralizePrecision::lessSpecific(), |
| 1887 | + 'non-empty-array<string, int>', |
| 1888 | + ]; |
| 1889 | + |
| 1890 | + yield 'sealed two explicit keys, lessSpecific' => [ |
| 1891 | + new ConstantArrayType( |
| 1892 | + [new ConstantStringType('a'), new ConstantStringType('b')], |
| 1893 | + [new ConstantIntegerType(1), new ConstantStringType('x')], |
| 1894 | + unsealed: $sealedMarker, |
| 1895 | + ), |
| 1896 | + GeneralizePrecision::lessSpecific(), |
| 1897 | + 'non-empty-array<string, int|string>', |
| 1898 | + ]; |
| 1899 | + |
| 1900 | + yield 'sealed two explicit keys, moreSpecific' => [ |
| 1901 | + new ConstantArrayType( |
| 1902 | + [new ConstantStringType('a'), new ConstantStringType('b')], |
| 1903 | + [new ConstantIntegerType(1), new ConstantStringType('x')], |
| 1904 | + unsealed: $sealedMarker, |
| 1905 | + ), |
| 1906 | + GeneralizePrecision::moreSpecific(), |
| 1907 | + "non-empty-array<literal-string&lowercase-string&non-falsy-string, int|(literal-string&lowercase-string&non-falsy-string)>&hasOffsetValue('a', int)&hasOffsetValue('b', literal-string&lowercase-string&non-falsy-string)", |
| 1908 | + ]; |
| 1909 | + |
| 1910 | + yield 'sealed list, lessSpecific' => [ |
| 1911 | + new ConstantArrayType( |
| 1912 | + [new ConstantIntegerType(0), new ConstantIntegerType(1)], |
| 1913 | + [new ConstantIntegerType(1), new ConstantIntegerType(2)], |
| 1914 | + unsealed: $sealedMarker, |
| 1915 | + isList: TrinaryLogic::createYes(), |
| 1916 | + ), |
| 1917 | + GeneralizePrecision::lessSpecific(), |
| 1918 | + 'non-empty-list<int>', |
| 1919 | + ]; |
| 1920 | + |
| 1921 | + yield 'sealed only-optional keys' => [ |
| 1922 | + new ConstantArrayType( |
| 1923 | + [new ConstantStringType('a')], |
| 1924 | + [new ConstantIntegerType(1)], |
| 1925 | + optionalKeys: [0], |
| 1926 | + unsealed: $sealedMarker, |
| 1927 | + ), |
| 1928 | + GeneralizePrecision::lessSpecific(), |
| 1929 | + 'array<string, int>', |
| 1930 | + ]; |
| 1931 | + |
| 1932 | + yield 'unsealed only, lessSpecific' => [ |
| 1933 | + new ConstantArrayType([], [], unsealed: [new IntegerType(), new ConstantStringType('foo')]), |
| 1934 | + GeneralizePrecision::lessSpecific(), |
| 1935 | + "array{...<int, 'foo'>}", |
| 1936 | + ]; |
| 1937 | + |
| 1938 | + yield 'unsealed with explicit key, lessSpecific' => [ |
| 1939 | + new ConstantArrayType( |
| 1940 | + [new ConstantStringType('a')], |
| 1941 | + [new ConstantIntegerType(1)], |
| 1942 | + unsealed: [new IntegerType(), new StringType()], |
| 1943 | + ), |
| 1944 | + GeneralizePrecision::lessSpecific(), |
| 1945 | + 'non-empty-array<int|string, int|string>', |
| 1946 | + ]; |
| 1947 | + |
| 1948 | + yield 'unsealed with explicit key, moreSpecific' => [ |
| 1949 | + new ConstantArrayType( |
| 1950 | + [new ConstantStringType('a')], |
| 1951 | + [new ConstantIntegerType(1)], |
| 1952 | + unsealed: [new IntegerType(), new StringType()], |
| 1953 | + ), |
| 1954 | + GeneralizePrecision::moreSpecific(), |
| 1955 | + "non-empty-array<int|(literal-string&lowercase-string&non-falsy-string), int|string>&hasOffsetValue('a', int)", |
| 1956 | + ]; |
| 1957 | + |
| 1958 | + yield 'unsealed with optional explicit key' => [ |
| 1959 | + new ConstantArrayType( |
| 1960 | + [new ConstantStringType('a')], |
| 1961 | + [new ConstantIntegerType(1)], |
| 1962 | + optionalKeys: [0], |
| 1963 | + unsealed: [new IntegerType(), new StringType()], |
| 1964 | + ), |
| 1965 | + GeneralizePrecision::lessSpecific(), |
| 1966 | + 'array<int|string, int|string>', |
| 1967 | + ]; |
| 1968 | + |
| 1969 | + yield 'templateArgument routes through traverse' => [ |
| 1970 | + new ConstantArrayType( |
| 1971 | + [new ConstantStringType('a')], |
| 1972 | + [new ConstantIntegerType(1)], |
| 1973 | + unsealed: [new IntegerType(), new ConstantStringType('foo')], |
| 1974 | + ), |
| 1975 | + GeneralizePrecision::templateArgument(), |
| 1976 | + // `traverse` recurses into both explicit and unsealed values |
| 1977 | + // (see commit history c. unsealed-aware traverse): `1` → |
| 1978 | + // `int`, `'foo'` → `string`. |
| 1979 | + 'array{a: int, ...<int, string>}', |
| 1980 | + ]; |
| 1981 | + } |
| 1982 | + |
| 1983 | + #[DataProvider('dataGeneralize')] |
| 1984 | + public function testGeneralize(ConstantArrayType $type, GeneralizePrecision $precision, string $expectedDescription): void |
| 1985 | + { |
| 1986 | + $this->assertSame( |
| 1987 | + $expectedDescription, |
| 1988 | + $type->generalize($precision)->describe(VerbosityLevel::precise()), |
| 1989 | + ); |
| 1990 | + } |
| 1991 | + |
1862 | 1992 | public function testTraverseSimultaneouslyVisitsUnsealedValue(): void |
1863 | 1993 | { |
1864 | 1994 | $left = new ConstantArrayType( |
|
0 commit comments