Skip to content

Commit ba6d89c

Browse files
ondrejmirtesclaude
andcommitted
Cover ConstantArrayType::generalize() with a data provider
Twelve cases pinning the current behaviour: empty arrays (legacy null vs. bleeding-edge NeverType marker), sealed inputs at each precision (lessSpecific, moreSpecific, templateArgument), single and multi-key shapes, list shape, all-optional keys, and unsealed inputs — including the "no explicit keys + real extras" case where the current implementation returns `$this` unchanged instead of generalizing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d3e4cbb commit ba6d89c

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Type\BooleanType;
1414
use PHPStan\Type\CallableType;
1515
use PHPStan\Type\ClassStringType;
16+
use PHPStan\Type\GeneralizePrecision;
1617
use PHPStan\Type\Generic\GenericClassStringType;
1718
use PHPStan\Type\Generic\TemplateTypeFactory;
1819
use PHPStan\Type\Generic\TemplateTypeScope;
@@ -1859,6 +1860,135 @@ public function testGetFiniteTypes(ConstantArrayType $type, array $expectedDescr
18591860
);
18601861
}
18611862

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+
18621992
public function testTraverseSimultaneouslyVisitsUnsealedValue(): void
18631993
{
18641994
$left = new ConstantArrayType(

0 commit comments

Comments
 (0)