@@ -106,6 +106,54 @@ function testComplexNestedShapeParam(array $data): bool
106106 return true ;
107107}
108108
109+ /**
110+ * Test function for Issue #20: Implicit keyless tuple array shapes
111+ *
112+ * @param array{list<positive-int>, list<non-empty-string>} $tuple
113+ */
114+ function testKeylessImplicitTupleShape (array $ tuple ): bool
115+ {
116+ return true ;
117+ }
118+
119+ /**
120+ * Helpers for Issue #20 Edge Cases
121+ *
122+ * @phpstan-type LocalTupleAlias array{list<positive-int>, list<non-empty-string>}
123+ * @phpstan-type MixedTupleShape array{non-empty-string, code: positive-int, list<int>}
124+ * @phpstan-import-type SharedTupleShape from \TypePHP\Tests\Fixtures\Types\GlobalTypes as ImportedTuple
125+ *
126+ * @param LocalTupleAlias $payload
127+ * @param MixedTupleShape $mixedPayload
128+ */
129+ function testLocalTupleAliasParam (array $ payload , array $ mixedPayload ): bool
130+ {
131+ return true ;
132+ }
133+
134+ /**
135+ * @phpstan-import-type SharedTupleShape from \TypePHP\Tests\Fixtures\Types\GlobalTypes as ImportedTuple
136+ *
137+ * @param ImportedTuple $tuple
138+ */
139+ function testImportedTupleAliasParam (array $ tuple ): bool
140+ {
141+ return true ;
142+ }
143+
144+ /**
145+ * @return array{list<positive-int>, non-empty-string}
146+ */
147+ function testReturnKeylessTuple (bool $ valid ): array
148+ {
149+ if (! $ valid ) {
150+ return [[10 , -5 ], 'bundle ' ];
151+ }
152+
153+ return [[10 , 20 ], 'bundle ' ];
154+ }
155+
156+
109157describe ('Class Object Arrays (Dog[]) ' , function () {
110158 test ('accepts array of matching class instances ' , function () {
111159 expect (testDogArrayParam ([new Dog (), new Dog ()]))->toBe (2 );
@@ -297,3 +345,38 @@ function testComplexNestedShapeParam(array $data): bool
297345 ;
298346 });
299347});
348+
349+ describe ('Issue #20 Edge Cases: Keyless Tuples in Type Aliases, Returns, and Mixed Keys ' , function () {
350+ test ('resolves keyless tuple shapes defined inside local @phpstan-type aliases ' , function () {
351+ expect (testLocalTupleAliasParam (
352+ [[10 , 20 ], ['a ' , 'b ' ]],
353+ ['status_ok ' , 'code ' => 200 , [1 , 2 , 3 ]]
354+ ))->toBeTrue ();
355+
356+ expect (fn () => testLocalTupleAliasParam (
357+ [[10 , -5 ], ['a ' , 'b ' ]],
358+ ['status_ok ' , 'code ' => 200 , [1 , 2 , 3 ]]
359+ ))->toThrow (TypeError::class, "Argument \$payload['0'][1] must be of type positive-int " );
360+
361+ expect (fn () => testLocalTupleAliasParam (
362+ [[10 , 20 ], ['a ' , 'b ' ]],
363+ ['status_ok ' , 'code ' => -100 , [1 , 2 , 3 ]]
364+ ))->toThrow (TypeError::class, "Argument \$mixedPayload['code'] must be of type positive-int " );
365+ });
366+
367+ test ('resolves keyless tuple shapes imported via @phpstan-import-type ' , function () {
368+ expect (testImportedTupleAliasParam ([[100 , 200 ], 'valid_string ' ]))->toBeTrue ();
369+
370+ expect (fn () => testImportedTupleAliasParam ([[100 , 200 ], '' ]))
371+ ->toThrow (TypeError::class, "Argument \$tuple['1'] must be of type non-empty-string " )
372+ ;
373+ });
374+
375+ test ('validates keyless tuple shapes returned from functions ' , function () {
376+ expect (testReturnKeylessTuple (true ))->toBe ([[10 , 20 ], 'bundle ' ]);
377+
378+ expect (fn () => testReturnKeylessTuple (false ))
379+ ->toThrow (TypeError::class, "Return value['0'][1] must be of type positive-int " )
380+ ;
381+ });
382+ });
0 commit comments