Skip to content

Commit

Permalink
fix: correctly handle multiline type alias in classes
Browse files Browse the repository at this point in the history
Fixes the following case:

```php
/**
 * @phpstan-type SomeTypeAlias = array{
 *     foo: string,
 *     bar: int
 * }
 */
final class SomeClass
{
    /** @var SomeTypeAlias */
    public array $someArray;
}
```

Co-authored-by: Romain Canon <[email protected]>
  • Loading branch information
franmomu and romm authored Sep 4, 2023
1 parent f3f3429 commit c231020
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 14 deletions.
3 changes: 0 additions & 3 deletions src/Definition/Exception/InvalidTypeAliasImportClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@
/** @internal */
final class InvalidTypeAliasImportClass extends LogicException
{
/**
* @param class-string $className
*/
public function __construct(ClassType $type, string $className)
{
parent::__construct(
Expand Down
45 changes: 34 additions & 11 deletions src/Utility/Reflection/DocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use ReflectionParameter;
use ReflectionProperty;

use function array_merge;
use function end;
use function explode;
use function preg_match;
use function preg_match_all;
use function str_replace;
Expand Down Expand Up @@ -68,21 +70,24 @@ public static function localTypeAliases(ReflectionClass $reflection): array
return [];
}

$cases = self::splitStringBy($doc, '@phpstan-type', '@psalm-type');

$types = [];

preg_match_all('/@(phpstan|psalm)-type\s+(?<name>[a-zA-Z]\w*)\s*=?\s*(?<type>.*)/', $doc, $matches);
foreach ($cases as $case) {
if (! preg_match('/\s*(?<name>[a-zA-Z]\w*)\s*=?\s*(?<type>.*)/s', $case, $matches)) {
continue;
}

foreach ($matches['name'] as $key => $name) {
/** @var string $name */
$types[$name] = self::findType($matches['type'][$key]);
$types[$matches['name']] = self::findType($matches['type']);
}

return $types;
}

/**
* @param ReflectionClass<object> $reflection
* @return array<class-string, string[]>
* @return array<string, string[]>
*/
public static function importedTypeAliases(ReflectionClass $reflection): array
{
Expand All @@ -92,15 +97,16 @@ public static function importedTypeAliases(ReflectionClass $reflection): array
return [];
}

$types = [];
$cases = self::splitStringBy($doc, '@phpstan-import-type', '@psalm-import-type');

preg_match_all('/@(phpstan|psalm)-import-type\s+(?<name>[a-zA-Z]\w*)\s*from\s*(?<class>\w+)/', $doc, $matches);
$types = [];

foreach ($matches['name'] as $key => $name) {
/** @var class-string $class */
$class = $matches['class'][$key];
foreach ($cases as $case) {
if (! preg_match('/\s*(?<name>[a-zA-Z]\w*)\s*from\s*(?<class>\w+)/', $case, $matches)) {
continue;
}

$types[$class][] = $name;
$types[$matches['class']][] = $matches['name'];
}

return $types;
Expand Down Expand Up @@ -214,4 +220,21 @@ private static function sanitizeDocComment(string|false $doc): ?string

return preg_replace('/^\s*\*\s*(\S*)/m', '$1', $doc); // @phpstan-ignore-line
}

/**
* @param non-empty-string ...$cases
* @return list<string>
*/
private static function splitStringBy(string $string, string ...$cases): array
{
$result = [$string];

foreach ($cases as $case) {
foreach ($result as $value) {
$result = array_merge($result, explode($case, $value));
}
}

return $result;
}
}
19 changes: 19 additions & 0 deletions tests/Integration/Mapping/Object/LocalTypeAliasMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public function test_values_are_mapped_properly(): void
'foo' => 'foo',
'bar' => 1337,
],
'aliasShapedArrayMultiline' => [
'foo' => 'foo',
'bar' => 1337,
],
'aliasGeneric' => [42, 1337],
];

Expand All @@ -31,6 +35,7 @@ public function test_values_are_mapped_properly(): void
self::assertSame(42, $result->aliasWithEqualsSign);
self::assertSame(42, $result->aliasWithoutEqualsSign);
self::assertSame($source['aliasShapedArray'], $result->aliasShapedArray);
self::assertSame($source['aliasShapedArrayMultiline'], $result->aliasShapedArrayMultiline);
self::assertSame($source['aliasGeneric'], $result->aliasGeneric->aliasArray);
} catch (MappingError $error) {
$this->mappingFail($error);
Expand Down Expand Up @@ -72,6 +77,10 @@ class GenericObjectWithPhpStanLocalAlias
* @phpstan-type AliasWithEqualsSign = int
* @phpstan-type AliasWithoutEqualsSign int
* @phpstan-type AliasShapedArray = array{foo: string, bar: int}
* @phpstan-type AliasShapedArrayMultiline = array{
* foo: string,
* bar: int
* }
* @phpstan-type AliasGeneric = GenericObjectWithPhpStanLocalAlias<int>
*/
class PhpStanLocalAliases
Expand All @@ -85,6 +94,9 @@ class PhpStanLocalAliases
/** @var AliasShapedArray */
public array $aliasShapedArray;

/** @var AliasShapedArrayMultiline */
public array $aliasShapedArrayMultiline;

/** @var AliasGeneric */
public GenericObjectWithPhpStanLocalAlias $aliasGeneric;
}
Expand Down Expand Up @@ -125,6 +137,10 @@ class GenericObjectWithPsalmLocalAlias
* @psalm-type AliasWithEqualsSign = int
* @psalm-type AliasWithoutEqualsSign int
* @psalm-type AliasShapedArray = array{foo: string, bar: int}
* @psalm-type AliasShapedArrayMultiline = array{
* foo: string,
* bar: int
* }
* @psalm-type AliasGeneric = GenericObjectWithPsalmLocalAlias<int>
*/
class PsalmLocalAliases
Expand All @@ -138,6 +154,9 @@ class PsalmLocalAliases
/** @var AliasShapedArray */
public array $aliasShapedArray;

/** @var AliasShapedArrayMultiline */
public array $aliasShapedArrayMultiline;

/** @var AliasGeneric */
public GenericObjectWithPsalmLocalAlias $aliasGeneric;
}
Expand Down

0 comments on commit c231020

Please sign in to comment.