Skip to content

Commit

Permalink
Merge pull request #10326 from robchett/consistent_type_docblock_parsing
Browse files Browse the repository at this point in the history
Consistent type docblock parsing
  • Loading branch information
orklah authored Nov 4, 2023
2 parents e6564c6 + 3cf9334 commit 302fb72
Show file tree
Hide file tree
Showing 14 changed files with 110 additions and 49 deletions.
5 changes: 3 additions & 2 deletions src/Psalm/Internal/Analyzer/CommentAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,10 @@ private static function decorateVarDocblockComment(
public static function sanitizeDocblockType(string $docblock_type): string
{
$docblock_type = (string) preg_replace('@^[ \t]*\*@m', '', $docblock_type);
$docblock_type = (string) preg_replace('/,\n\s+}/', '}', $docblock_type);
$docblock_type = (string) preg_replace('/,[\n\s]+}/', '}', $docblock_type);
$docblock_type = (string) preg_replace('/[ \t]+/', ' ', $docblock_type);

return str_replace("\n", '', $docblock_type);
return trim(str_replace("\n", '', $docblock_type));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ private static function analyzeStatement(
$check_type_string,
$statements_analyzer->getAliases(),
);
$check_type = Type::parseString($fq_check_type_string);
$check_type = Type::parseString(CommentAnalyzer::sanitizeDocblockType($fq_check_type_string));
/** @psalm-suppress InaccessibleProperty We just created this type */
$check_type->possibly_undefined = $possibly_undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ public static function parse(
$templates = [];
if (isset($parsed_docblock->combined_tags['template'])) {
foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) {
$template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line));
$template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line));
if ($template_type === false) {
throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg());
throw new IncorrectDocblockException('Invalid @template tag: '.preg_last_error_msg());
}

$template_name = array_shift($template_type);
Expand Down Expand Up @@ -111,7 +111,7 @@ public static function parse(

if (isset($parsed_docblock->combined_tags['template-covariant'])) {
foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) {
$template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line));
$template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line));
if ($template_type === false) {
throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg());
}
Expand Down Expand Up @@ -171,20 +171,16 @@ public static function parse(

if (isset($parsed_docblock->tags['psalm-require-extends'])
&& count($extension_requirements = $parsed_docblock->tags['psalm-require-extends']) > 0) {
$info->extension_requirement = trim((string) preg_replace(
'@^[ \t]*\*@m',
'',
$info->extension_requirement = CommentAnalyzer::sanitizeDocblockType(
$extension_requirements[array_key_first($extension_requirements)],
));
);
}

if (isset($parsed_docblock->tags['psalm-require-implements'])) {
foreach ($parsed_docblock->tags['psalm-require-implements'] as $implementation_requirement) {
$info->implementation_requirements[] = trim((string) preg_replace(
'@^[ \t]*\*@m',
'',
$info->implementation_requirements[] = CommentAnalyzer::sanitizeDocblockType(
$implementation_requirement,
));
);
}
}

Expand All @@ -199,7 +195,7 @@ public static function parse(
if (isset($parsed_docblock->tags['psalm-yield'])) {
$yield = (string) reset($parsed_docblock->tags['psalm-yield']);

$info->yield = trim((string) preg_replace('@^[ \t]*\*@m', '', $yield));
$info->yield = CommentAnalyzer::sanitizeDocblockType($yield);
}

if (isset($parsed_docblock->tags['deprecated'])) {
Expand Down Expand Up @@ -552,7 +548,7 @@ protected static function addMagicPropertyToInfo(

$end = $offset + strlen($line_parts[0]);

$line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]));
$line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);

if ($line_parts[0] === ''
|| ($line_parts[0][0] === '$'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
use function get_class;
use function implode;
use function preg_match;
use function preg_replace;
use function preg_split;
use function str_replace;
use function strtolower;
Expand Down Expand Up @@ -940,7 +939,7 @@ public function handleTraitUse(PhpParser\Node\Stmt\TraitUse $node): void
$this->useTemplatedType(
$storage,
$node,
trim((string) preg_replace('@^[ \t]*\*@m', '', $template_line)),
CommentAnalyzer::sanitizeDocblockType($template_line),
);
}
}
Expand Down Expand Up @@ -1912,10 +1911,7 @@ private static function getTypeAliasesFromCommentLines(
continue;
}

$var_line = (string) preg_replace('/[ \t]+/', ' ', (string) preg_replace('@^[ \t]*\*@m', '', $var_line));
$var_line = (string) preg_replace('/,\n\s+\}/', '}', $var_line);
$var_line = str_replace("\n", '', $var_line);

$var_line = CommentAnalyzer::sanitizeDocblockType($var_line);
$var_line_parts = preg_split('/( |=)/', $var_line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);

if (!$var_line_parts) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,7 @@ public static function parse(
$line_parts[1] = substr($line_parts[1], 1);
}

$line_parts[0] = str_replace(
"\n",
'',
(string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]),
);
$line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);

if ($line_parts[0] === ''
|| ($line_parts[0][0] === '$'
Expand Down Expand Up @@ -194,14 +190,10 @@ public static function parse(
$line_parts = CommentAnalyzer::splitDocLine($param);

if (count($line_parts) > 0) {
$line_parts[0] = str_replace(
"\n",
'',
(string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]),
);
$line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);

$info->self_out = [
'type' => str_replace("\n", '', $line_parts[0]),
'type' => $line_parts[0],
'line_number' => $comment->getStartLine() + substr_count(
$comment_text,
"\n",
Expand All @@ -225,10 +217,10 @@ public static function parse(
foreach ($parsed_docblock->tags['psalm-if-this-is'] as $offset => $param) {
$line_parts = CommentAnalyzer::splitDocLine($param);

$line_parts[0] = str_replace("\n", '', (string) preg_replace('@^[ \t]*\*@m', '', $line_parts[0]));
$line_parts[0] = CommentAnalyzer::sanitizeDocblockType($line_parts[0]);

$info->if_this_is = [
'type' => str_replace("\n", '', $line_parts[0]),
'type' => $line_parts[0],
'line_number' => $comment->getStartLine() + substr_count(
$comment->getText(),
"\n",
Expand Down Expand Up @@ -454,7 +446,7 @@ public static function parse(
$templates = [];
if (isset($parsed_docblock->combined_tags['template'])) {
foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) {
$template_type = preg_split('/[\s]+/', (string) preg_replace('@^[ \t]*\*@m', '', $template_line));
$template_type = preg_split('/[\s]+/', CommentAnalyzer::sanitizeDocblockType($template_line));
if ($template_type === false) {
throw new AssertionError(preg_last_error_msg());
}
Expand Down
27 changes: 23 additions & 4 deletions src/Psalm/Internal/Type/ParseTreeCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use function in_array;
use function preg_match;
use function strlen;
use function strpos;
use function strtolower;

/**
Expand Down Expand Up @@ -143,6 +144,7 @@ public function create(): ParseTree

case 'is':
case 'as':
case 'of':
$this->handleIsOrAs($type_token);
break;

Expand Down Expand Up @@ -771,7 +773,7 @@ private function handleIsOrAs(array $type_token): void
array_pop($current_parent->children);
}

if ($type_token[0] === 'as') {
if ($type_token[0] === 'as' || $type_token[0] == 'of') {
$next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null;

if (!$this->current_leaf instanceof Value
Expand Down Expand Up @@ -824,13 +826,30 @@ private function handleValue(array $type_token): void
break;

case '{':
++$this->t;

$nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null;

if ($nexter_token && strpos($nexter_token[0], '@') !== false) {
$this->t = $this->type_token_count;
if ($type_token[0] === '$this') {
$type_token[0] = 'static';
}

$new_leaf = new Value(
$type_token[0],
$type_token[1],
$type_token[1] + strlen($type_token[0]),
$type_token[2] ?? null,
$new_parent,
);
break;
}

$new_leaf = new KeyedArrayTree(
$type_token[0],
$new_parent,
);
++$this->t;

$nexter_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null;

if ($nexter_token !== null && $nexter_token[0] === '}') {
$new_leaf->terminated = true;
Expand Down
8 changes: 4 additions & 4 deletions src/Psalm/Internal/Type/TypeTokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
use Psalm\Internal\Type\TypeAlias\InlineTypeAlias;
use Psalm\Type;

use function array_slice;
use function array_splice;
use function array_unshift;
use function count;
use function implode;
use function in_array;
use function is_numeric;
use function preg_match;
Expand Down Expand Up @@ -146,11 +148,9 @@ public static function tokenize(string $string_type, bool $ignore_space = true):
$type_tokens[++$rtc] = [' ', $i - 1];
$type_tokens[++$rtc] = ['', $i];
} elseif ($was_space
&& ($char === 'a' || $char === 'i')
&& ($chars[$i + 1] ?? null) === 's'
&& ($chars[$i + 2] ?? null) === ' '
&& in_array(implode('', array_slice($chars, $i, 3)), ['as ', 'is ', 'of '])
) {
$type_tokens[++$rtc] = [$char . 's', $i - 1];
$type_tokens[++$rtc] = [$char . $chars[$i+1], $i - 1];
$type_tokens[++$rtc] = ['', ++$i];
$was_char = false;
continue;
Expand Down
4 changes: 2 additions & 2 deletions stubs/CoreGenericIterators.phpstub
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ class EmptyIterator implements Iterator {
}

/**
* @template-extends SeekableIterator<string, FilesystemIterator|SplFileInfo|string>
* @template-extends DirectoryIterator<string, FilesystemIterator|SplFileInfo|string>
*/
class FilesystemIterator extends DirectoryIterator
{
Expand Down Expand Up @@ -523,7 +523,7 @@ class FilesystemIterator extends DirectoryIterator


/**
* @template-extends SeekableIterator<string, GlobIterator|SplFileInfo|string>
* @template-extends FilesystemIterator<string, GlobIterator|SplFileInfo|string>
*/
class GlobIterator extends FilesystemIterator implements Countable {
/**
Expand Down
2 changes: 1 addition & 1 deletion stubs/Reflection.phpstub
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ class ReflectionProperty implements Reflector
public function isDefault(): bool {}

/**
* @return int-mask-of<self::IS_>
* @return int-mask-of<self::IS_*>
* @psalm-pure
*/
public function getModifiers(): int {}
Expand Down
4 changes: 2 additions & 2 deletions tests/AnnotationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1679,15 +1679,15 @@ class A {
}',
'error_message' => 'InvalidDocblock',
],
'noCrashOnInvalidClassTemplateAsType' => [
'SKIPPED-noCrashOnInvalidClassTemplateAsType' => [
'code' => '<?php
/**
* @template T as ' . '
*/
class A {}',
'error_message' => 'InvalidDocblock',
],
'noCrashOnInvalidFunctionTemplateAsType' => [
'SKIPPED-noCrashOnInvalidFunctionTemplateAsType' => [
'code' => '<?php
/**
* @template T as ' . '
Expand Down
9 changes: 9 additions & 0 deletions tests/MethodSignatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,15 @@ public static function foo() {
'$b' => 'B',
],
],
'returnIgnoresInlineComments' => [
'code' => '<?php
class A {
/** @return bool {@see true}*/
public static function foo():bool {
return true;
}
}',
],
'allowSomeCovariance' => [
'code' => '<?php
interface I1 {
Expand Down
26 changes: 26 additions & 0 deletions tests/Template/TraitTemplateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,32 @@ class B {
use T;
}',
],
'multilineTemplateUse' => [
'code' => '<?php
/**
* @template T1
* @template T2
* @template T3
*/
trait MyTrait {}
class Foo {
/**
* @template-use MyTrait<int, int, array{
* foo: mixed,
* bar: mixed,
* }>
*/
use MyTrait;
}
class Bar {
/**
* @template-use MyTrait<int, string, bar>
*/
use MyTrait;
}',
],
'allowTraitExtendAndImplementWithExplicitParamType' => [
'code' => '<?php
/**
Expand Down
14 changes: 14 additions & 0 deletions tests/TypeAnnotationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,20 @@ class Foo {
'$output===' => 'array{phone: string}',
],
],
'multilineTypeWithExtraSpace' => [
'code' => '<?php
/**
* @psalm-type PhoneType = array{
* phone: string, ' . '
* }
*/
class Foo {
/** @var PhoneType */
public static $phone;
}
$output = Foo::$phone;
',
],
'combineAliasOfArrayAndArrayCorrectly' => [
'code' => '<?php
Expand Down
8 changes: 8 additions & 0 deletions tests/TypeParseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,14 @@ public function testClassStringMap(): void
);
}

public function testClassStringMapOf(): void
{
$this->assertSame(
'class-string-map<T as Foo, T>',
Type::parseString('class-string-map<T of Foo, T>')->getId(false),
);
}

public function testVeryLargeType(): void
{
$very_large_type = 'array{a: Closure():(array<array-key, mixed>|null), b?: Closure():array<array-key, mixed>, c?: Closure():array<array-key, mixed>, d?: Closure():array<array-key, mixed>, e?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), p?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), q: string, r?: Closure():(array<array-key, mixed>|null), s: array<array-key, mixed>}|null';
Expand Down

0 comments on commit 302fb72

Please sign in to comment.