Skip to content

Commit f65e6a2

Browse files
authored
fix: parsing mixed & and | in TypeExpression (#8210)
1 parent 3a83b03 commit f65e6a2

File tree

5 files changed

+116
-15
lines changed

5 files changed

+116
-15
lines changed

dev-tools/phpstan/baseline.php

+12
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,18 @@
523523
'count' => 2,
524524
'path' => __DIR__ . '/../../src/DocBlock/TypeExpression.php',
525525
];
526+
$ignoreErrors[] = [
527+
// identifier: offsetAccess.notFound
528+
'message' => '#^Offset int\\<0, max\\> might not exist on non\\-empty\\-list\\<array\\{start_index\\: int\\<0, max\\>, value\\: string, next_glue\\: string\\|null, next_glue_raw\\: string\\|null\\}\\>\\.$#',
529+
'count' => 2,
530+
'path' => __DIR__ . '/../../src/DocBlock/TypeExpression.php',
531+
];
532+
$ignoreErrors[] = [
533+
// identifier: assign.propertyType
534+
'message' => '#^Property PhpCsFixer\\\\DocBlock\\\\TypeExpression\\:\\:\\$typesGlue \\(\'&\'\\|\'\\|\'\\) does not accept non\\-empty\\-string\\.$#',
535+
'count' => 1,
536+
'path' => __DIR__ . '/../../src/DocBlock/TypeExpression.php',
537+
];
526538
$ignoreErrors[] = [
527539
// identifier: offsetAccess.notFound
528540
'message' => '#^Offset int might not exist on list\\<PhpCsFixer\\\\Doctrine\\\\Annotation\\\\Token\\>\\.$#',

src/DocBlock/Annotation.php

+9-3
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,17 @@ public function setTypes(array $types): void
228228
*/
229229
public function getNormalizedTypes(): array
230230
{
231-
$normalized = array_map(static fn (string $type): string => strtolower($type), $this->getTypes());
231+
$typeExpression = $this->getTypeExpression();
232+
if (null === $typeExpression) {
233+
return [];
234+
}
232235

233-
sort($normalized);
236+
$normalizedTypeExpression = $typeExpression
237+
->mapTypes(static fn (TypeExpression $v) => new TypeExpression(strtolower($v->toString()), null, []))
238+
->sortTypes(static fn (TypeExpression $a, TypeExpression $b) => $a->toString() <=> $b->toString())
239+
;
234240

235-
return $normalized;
241+
return $normalizedTypeExpression->getTypes();
236242
}
237243

238244
/**

src/DocBlock/TypeExpression.php

+52-7
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ final class TypeExpression
210210

211211
private bool $isUnionType;
212212

213+
/** @var '&'|'|' */
213214
private string $typesGlue;
214215

215216
/** @var list<array{start_index: int, expression: self}> */
@@ -257,6 +258,9 @@ public function isUnionType(): bool
257258
return $this->isUnionType;
258259
}
259260

261+
/**
262+
* @return '&'|'|'
263+
*/
260264
public function getTypesGlue(): string
261265
{
262266
return $this->typesGlue;
@@ -392,12 +396,13 @@ public function allowsNull(): bool
392396

393397
private function parse(): void
394398
{
395-
$typesGlue = null;
399+
$seenGlues = null;
400+
$innerValues = [];
396401

397402
$index = 0;
398403
while (true) {
399404
Preg::match(
400-
'{\G'.self::REGEX_TYPE.'(?:\h*(?<glue>[|&])\h*|$)}',
405+
'{\G'.self::REGEX_TYPE.'(?<glue_raw>\h*(?<glue>[|&])\h*(?!$)|$)}',
401406
$this->value,
402407
$matches,
403408
PREG_OFFSET_CAPTURE,
@@ -408,17 +413,24 @@ private function parse(): void
408413
throw new \Exception('Unable to parse phpdoc type '.var_export($this->value, true));
409414
}
410415

411-
if (null === $typesGlue) {
416+
if (null === $seenGlues) {
412417
if (($matches['glue'][0] ?? '') === '') {
413418
break;
414419
}
415420

416-
$typesGlue = $matches['glue'][0];
421+
$seenGlues = ['|' => false, '&' => false];
417422
}
418423

419-
$this->innerTypeExpressions[] = [
424+
if (($matches['glue'][0] ?? '') !== '') {
425+
\assert(isset($seenGlues[$matches['glue'][0]]));
426+
$seenGlues[$matches['glue'][0]] = true;
427+
}
428+
429+
$innerValues[] = [
420430
'start_index' => $index,
421-
'expression' => $this->inner($matches['type'][0]),
431+
'value' => $matches['type'][0],
432+
'next_glue' => $matches['glue'][0] ?? null,
433+
'next_glue_raw' => $matches['glue_raw'][0] ?? null,
422434
];
423435

424436
$consumedValueLength = \strlen($matches[0][0]);
@@ -427,8 +439,41 @@ private function parse(): void
427439
if (\strlen($this->value) <= $index) {
428440
\assert(\strlen($this->value) === $index);
429441

442+
$seenGlues = array_filter($seenGlues);
443+
\assert([] !== $seenGlues);
444+
430445
$this->isUnionType = true;
431-
$this->typesGlue = $typesGlue;
446+
$this->typesGlue = array_key_first($seenGlues);
447+
448+
if (1 === \count($seenGlues)) {
449+
foreach ($innerValues as $innerValue) {
450+
$this->innerTypeExpressions[] = [
451+
'start_index' => $innerValue['start_index'],
452+
'expression' => $this->inner($innerValue['value']),
453+
];
454+
}
455+
} else {
456+
for ($i = 0; $i < \count($innerValues); ++$i) {
457+
$innerStartIndex = $innerValues[$i]['start_index'];
458+
$innerValue = '';
459+
while (true) {
460+
$innerValue .= $innerValues[$i]['value'];
461+
462+
if (($innerValues[$i]['next_glue'] ?? $this->typesGlue) === $this->typesGlue) {
463+
break;
464+
}
465+
466+
$innerValue .= $innerValues[$i]['next_glue_raw'];
467+
468+
++$i;
469+
}
470+
471+
$this->innerTypeExpressions[] = [
472+
'start_index' => $innerStartIndex,
473+
'expression' => $this->inner($innerValue),
474+
];
475+
}
476+
}
432477

433478
return;
434479
}

tests/DocBlock/AnnotationTest.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,8 @@ public static function provideNormalizedTypesCases(): iterable
576576

577577
yield [['bool', 'int'], '* @param bool|int $foo'];
578578

579+
yield [['bool', 'int'], '* @param bool&int $foo'];
580+
579581
yield [['bool', 'int'], '* @param bool|int ...$foo'];
580582

581583
yield [['bool', 'int'], '* @param bool|int &$foo'];
@@ -588,7 +590,9 @@ public static function provideNormalizedTypesCases(): iterable
588590

589591
yield [['bool', 'int'], '* @param bool|int&...$foo'];
590592

591-
yield [['bar', 'baz', 'foo'], '* @param Foo|Bar&Baz&$param'];
593+
yield [['bar&baz', 'foo'], '* @param Foo|Bar&Baz&$param'];
594+
595+
yield [['bar&baz', 'foo'], '* @param Baz&Bar|Foo&$param'];
592596
}
593597

594598
public function testGetTypesOnBadTag(): void

tests/DocBlock/TypeExpressionTest.php

+38-4
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ public function testGetTypes(string $typesExpression, ?array $expectedTypes = nu
4747
null,
4848
[]
4949
);
50-
self::assertSame(
51-
[$unionTestNs.'\A', ...$expectedTypes, $unionTestNs.'\Z'],
52-
[...$unionExpression->getTypes()]
53-
);
50+
if (!$expression->isUnionType() || '|' === $expression->getTypesGlue()) {
51+
self::assertSame(
52+
[$unionTestNs.'\A', ...$expectedTypes, $unionTestNs.'\Z'],
53+
[...$unionExpression->getTypes()]
54+
);
55+
}
5456
}
5557

5658
public static function provideGetTypesCases(): iterable
@@ -372,6 +374,24 @@ public static function provideParseInvalidExceptionCases(): iterable
372374

373375
yield ['((unclosed_parenthesis)'];
374376

377+
yield ['|vertical_bar_start'];
378+
379+
yield ['&ampersand_start'];
380+
381+
yield ['~tilde_start'];
382+
383+
yield ['vertical_bar_end|'];
384+
385+
yield ['ampersand_end&'];
386+
387+
yield ['tilde_end~'];
388+
389+
yield ['class||double_vertical_bar'];
390+
391+
yield ['class&&double_ampersand'];
392+
393+
yield ['class~~double_tilde'];
394+
375395
yield ['array<'];
376396

377397
yield ['array<<'];
@@ -476,6 +496,10 @@ public static function provideIsUnionTypeCases(): iterable
476496
yield [false, '?int'];
477497

478498
yield [true, 'Foo|Bar'];
499+
500+
yield [true, 'Foo&Bar'];
501+
502+
yield [true, 'Foo&Bar&?Baz'];
479503
}
480504

481505
/**
@@ -1014,6 +1038,16 @@ public static function provideSortTypesCases(): iterable
10141038
'18_446_744_073_709_551_616|-8.2023437675747321e-18_446_744_073_709_551_616',
10151039
'-8.2023437675747321e-18_446_744_073_709_551_616|18_446_744_073_709_551_616',
10161040
];
1041+
1042+
yield 'mixed 2x | and & glue' => [
1043+
'Foo|Foo2|Baz&Bar',
1044+
'Bar&Baz|Foo|Foo2',
1045+
];
1046+
1047+
yield 'mixed | and 2x & glue' => [
1048+
'Foo|Baz&Baz2&Bar',
1049+
'Bar&Baz&Baz2|Foo',
1050+
];
10171051
}
10181052

10191053
private static function makeLongArrayShapeType(): string

0 commit comments

Comments
 (0)