mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Fix #16 - add support for enums
This commit is contained in:
parent
7bc426268a
commit
6250c2a14c
@ -37,7 +37,7 @@ class CommentChecker
|
||||
) {
|
||||
$var_id = null;
|
||||
|
||||
$var_type_string = null;
|
||||
$var_type_tokens = null;
|
||||
$original_type = null;
|
||||
|
||||
$var_comments = [];
|
||||
@ -71,7 +71,7 @@ class CommentChecker
|
||||
}
|
||||
|
||||
try {
|
||||
$var_type_string = Type::fixUpLocalType(
|
||||
$var_type_tokens = Type::fixUpLocalType(
|
||||
$line_parts[0],
|
||||
$aliases,
|
||||
$template_types
|
||||
@ -89,16 +89,16 @@ class CommentChecker
|
||||
}
|
||||
}
|
||||
|
||||
if (!$var_type_string || !$original_type) {
|
||||
if (!$var_type_tokens || !$original_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$defined_type = Type::parseString($var_type_string, false, $template_types ?: []);
|
||||
$defined_type = Type::parseTokens($var_type_tokens, false, $template_types ?: []);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
if (is_int($came_from_line_number)) {
|
||||
throw new DocblockParseException(
|
||||
$var_type_string .
|
||||
implode('', $var_type_tokens) .
|
||||
' is not a valid type' .
|
||||
' (from ' .
|
||||
$source->getCheckedFilePath() .
|
||||
@ -108,7 +108,7 @@ class CommentChecker
|
||||
);
|
||||
}
|
||||
|
||||
throw new DocblockParseException($var_type_string . ' is not a valid type');
|
||||
throw new DocblockParseException(implode('', $var_type_tokens) . ' is not a valid type');
|
||||
}
|
||||
|
||||
$defined_type->setFromDocblock();
|
||||
@ -197,12 +197,9 @@ class CommentChecker
|
||||
}
|
||||
|
||||
if (count($line_parts) > 1) {
|
||||
if (preg_match('/^' . self::TYPE_REGEX . '$/', $line_parts[0])
|
||||
&& !preg_match('/\[[^\]]+\]/', $line_parts[0])
|
||||
if (!preg_match('/\[[^\]]+\]/', $line_parts[0])
|
||||
&& preg_match('/^(\.\.\.)?&?\$[A-Za-z0-9_]+,?$/', $line_parts[1])
|
||||
&& !strpos($line_parts[0], '::')
|
||||
&& $line_parts[0][0] !== '{'
|
||||
&& !in_array($line_parts[0], ['null', 'false', 'true'], true)
|
||||
) {
|
||||
if ($line_parts[1][0] === '&') {
|
||||
$line_parts[1] = substr($line_parts[1], 1);
|
||||
@ -438,10 +435,45 @@ class CommentChecker
|
||||
|
||||
$return_block = preg_replace('/[ \t]+/', ' ', $return_block);
|
||||
|
||||
$quote_char = null;
|
||||
$escaped = false;
|
||||
|
||||
for ($i = 0, $l = strlen($return_block); $i < $l; ++$i) {
|
||||
$char = $return_block[$i];
|
||||
$next_char = $i < $l - 1 ? $return_block[$i + 1] : null;
|
||||
|
||||
if ($quote_char) {
|
||||
if ($char === $quote_char && $i > 1 && !$escaped) {
|
||||
$quote_char = null;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '\\' && !$escaped && ($next_char === $quote_char || $next_char === '\\')) {
|
||||
$escaped = true;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$escaped = false;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '"' || $char === '\'') {
|
||||
$quote_char = $char;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '[' || $char === '{' || $char === '(' || $char === '<') {
|
||||
$brackets .= $char;
|
||||
} elseif ($char === ']' || $char === '}' || $char === ')' || $char === '>') {
|
||||
|
@ -721,7 +721,11 @@ class TypeChecker
|
||||
}
|
||||
|
||||
if ($input_type_part instanceof Scalar) {
|
||||
if ($container_type_part instanceof Scalar) {
|
||||
if ($container_type_part instanceof Scalar
|
||||
&& !$container_type_part instanceof TLiteralInt
|
||||
&& !$container_type_part instanceof TLiteralString
|
||||
&& !$container_type_part instanceof TLiteralFloat
|
||||
) {
|
||||
$has_scalar_match = true;
|
||||
}
|
||||
} elseif ($container_type_part instanceof TObject &&
|
||||
|
@ -79,20 +79,25 @@ abstract class Type
|
||||
*/
|
||||
public static function parseString($type_string, $php_compatible = false, array $template_types = [])
|
||||
{
|
||||
// remove all unacceptable characters
|
||||
$type_string = preg_replace('/\?(?=[a-zA-Z])/', 'null|', $type_string);
|
||||
|
||||
if (preg_match('/[^A-Za-z0-9\-_\\\\&|\? \<\>\{\}=:\.,\]\[\(\)\$]/', trim($type_string))) {
|
||||
throw new TypeParseTreeException('Unrecognised character in type');
|
||||
}
|
||||
|
||||
$type_tokens = self::tokenize($type_string);
|
||||
return self::parseTokens(self::tokenize($type_string), $php_compatible, $template_types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string type representation
|
||||
*
|
||||
* @param array<int, string> $type_tokens
|
||||
* @param bool $php_compatible
|
||||
* @param array<string, string> $template_types
|
||||
*
|
||||
* @return Union
|
||||
*/
|
||||
public static function parseTokens(array $type_tokens, $php_compatible = false, array $template_types = [])
|
||||
{
|
||||
if (count($type_tokens) === 1) {
|
||||
$only_token = $type_tokens[0];
|
||||
|
||||
// Note: valid identifiers can include class names or $this
|
||||
if (!preg_match('@^(\$this$|[a-zA-Z_\x7f-\xff])@', $only_token)) {
|
||||
if (!preg_match('@^(\$this|\\\\?[a-zA-Z_\x7f-\xff][\\\\\-0-9a-zA-Z_\x7f-\xff]*)$@', $only_token)) {
|
||||
throw new TypeParseTreeException("Invalid type '$only_token'");
|
||||
}
|
||||
|
||||
@ -367,39 +372,97 @@ abstract class Type
|
||||
throw new \InvalidArgumentException('Unrecognised parse tree type ' . get_class($parse_tree));
|
||||
}
|
||||
|
||||
if ($parse_tree->value[0] === '"' || $parse_tree->value[0] === '\'') {
|
||||
return new TLiteralString(substr($parse_tree->value, 1, -1));
|
||||
}
|
||||
|
||||
if (preg_match('/^\-?(0|[1-9][0-9]*)$/', $parse_tree->value)) {
|
||||
return new TLiteralInt((int) $parse_tree->value);
|
||||
}
|
||||
|
||||
if (!preg_match('@^(\$this|\\\\?[a-zA-Z_\x7f-\xff][\\\\\-0-9a-zA-Z_\x7f-\xff]*)$@', $parse_tree->value)) {
|
||||
throw new TypeParseTreeException('Invalid type \'' . $parse_tree->value . '\'');
|
||||
}
|
||||
|
||||
$atomic_type = self::fixScalarTerms($parse_tree->value, $php_compatible);
|
||||
|
||||
return Atomic::create($atomic_type, $php_compatible, $template_types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $return_type
|
||||
* @param string $string_type
|
||||
* @param bool $ignore_space
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public static function tokenize($return_type, $ignore_space = true)
|
||||
public static function tokenize($string_type, $ignore_space = true)
|
||||
{
|
||||
$return_type_tokens = [''];
|
||||
// remove all unacceptable characters
|
||||
$string_type = preg_replace('/\?(?=[a-zA-Z])/', 'null|', $string_type);
|
||||
|
||||
$type_tokens = [''];
|
||||
$was_char = false;
|
||||
$quote_char = null;
|
||||
$escaped = false;
|
||||
|
||||
if ($ignore_space) {
|
||||
$return_type = str_replace(' ', '', $return_type);
|
||||
}
|
||||
|
||||
if (isset(self::$memoized_tokens[$return_type])) {
|
||||
return self::$memoized_tokens[$return_type];
|
||||
if (isset(self::$memoized_tokens[$string_type])) {
|
||||
return self::$memoized_tokens[$string_type];
|
||||
}
|
||||
|
||||
// index of last type token
|
||||
$rtc = 0;
|
||||
|
||||
$chars = str_split($return_type);
|
||||
$chars = str_split($string_type);
|
||||
for ($i = 0, $c = count($chars); $i < $c; ++$i) {
|
||||
$char = $chars[$i];
|
||||
|
||||
if (!$quote_char && $char === ' ' && $ignore_space) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($was_char) {
|
||||
$return_type_tokens[++$rtc] = '';
|
||||
$type_tokens[++$rtc] = '';
|
||||
}
|
||||
|
||||
if ($quote_char) {
|
||||
if ($char === $quote_char && $i > 1 && !$escaped) {
|
||||
$quote_char = null;
|
||||
|
||||
$type_tokens[$rtc] .= $char;
|
||||
$was_char = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$was_char = false;
|
||||
|
||||
if ($char === '\\'
|
||||
&& !$escaped
|
||||
&& $i < $c - 1
|
||||
&& ($chars[$i + 1] === $quote_char || $chars[$i + 1] === '\\')
|
||||
) {
|
||||
$escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$escaped = false;
|
||||
|
||||
$type_tokens[$rtc] .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '"' || $char === '\'') {
|
||||
if ($type_tokens[$rtc] === '') {
|
||||
$type_tokens[$rtc] = $char;
|
||||
} else {
|
||||
$type_tokens[++$rtc] = $char;
|
||||
}
|
||||
|
||||
$quote_char = $char;
|
||||
|
||||
$was_char = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '<'
|
||||
@ -418,99 +481,112 @@ abstract class Type
|
||||
|| $char === ':'
|
||||
|| $char === '='
|
||||
) {
|
||||
if ($return_type_tokens[$rtc] === '') {
|
||||
$return_type_tokens[$rtc] = $char;
|
||||
if ($type_tokens[$rtc] === '') {
|
||||
$type_tokens[$rtc] = $char;
|
||||
} else {
|
||||
$return_type_tokens[++$rtc] = $char;
|
||||
$type_tokens[++$rtc] = $char;
|
||||
}
|
||||
|
||||
$was_char = true;
|
||||
} elseif ($char === '.') {
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '.') {
|
||||
if ($i + 2 > $c || $chars[$i + 1] !== '.' || $chars[$i + 2] !== '.') {
|
||||
throw new TypeParseTreeException('Unexpected token ' . $char);
|
||||
}
|
||||
|
||||
if ($return_type_tokens[$rtc] === '') {
|
||||
$return_type_tokens[$rtc] = '...';
|
||||
if ($type_tokens[$rtc] === '') {
|
||||
$type_tokens[$rtc] = '...';
|
||||
} else {
|
||||
$return_type_tokens[++$rtc] = '...';
|
||||
$type_tokens[++$rtc] = '...';
|
||||
}
|
||||
|
||||
$was_char = true;
|
||||
|
||||
$i += 2;
|
||||
} else {
|
||||
$return_type_tokens[$rtc] .= $char;
|
||||
$was_char = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$type_tokens[$rtc] .= $char;
|
||||
$was_char = false;
|
||||
}
|
||||
|
||||
self::$memoized_tokens[$return_type] = $return_type_tokens;
|
||||
self::$memoized_tokens[$string_type] = $type_tokens;
|
||||
|
||||
return $return_type_tokens;
|
||||
return $type_tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $return_type
|
||||
* @param string $string_type
|
||||
* @param Aliases $aliases
|
||||
* @param array<string, string>|null $template_types
|
||||
*
|
||||
* @return string
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function fixUpLocalType(
|
||||
$return_type,
|
||||
$string_type,
|
||||
Aliases $aliases,
|
||||
array $template_types = null
|
||||
) {
|
||||
$return_type_tokens = self::tokenize($return_type);
|
||||
$type_tokens = self::tokenize($string_type);
|
||||
|
||||
for ($i = 0, $l = count($return_type_tokens); $i < $l; $i++) {
|
||||
$return_type_token = $return_type_tokens[$i];
|
||||
for ($i = 0, $l = count($type_tokens); $i < $l; $i++) {
|
||||
$string_type_token = $type_tokens[$i];
|
||||
|
||||
if (in_array(
|
||||
$return_type_token,
|
||||
$string_type_token,
|
||||
['<', '>', '|', '?', ',', '{', '}', ':', '[', ']', '(', ')', '&'],
|
||||
true
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($return_type_tokens[$i + 1]) && $return_type_tokens[$i + 1] === ':') {
|
||||
if ($string_type_token[0] === '"'
|
||||
|| $string_type_token[0] === '\''
|
||||
|| preg_match('/[1-9]/', $string_type_token[0])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$return_type_tokens[$i] = $return_type_token = self::fixScalarTerms($return_type_token);
|
||||
|
||||
if (isset(self::$PSALM_RESERVED_WORDS[$return_type_token])) {
|
||||
if (isset($type_tokens[$i + 1]) && $type_tokens[$i + 1] === ':') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($template_types[$return_type_token])) {
|
||||
$type_tokens[$i] = $string_type_token = self::fixScalarTerms($string_type_token);
|
||||
|
||||
if (isset(self::$PSALM_RESERVED_WORDS[$string_type_token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($return_type_tokens[$i + 1])) {
|
||||
$next_char = $return_type_tokens[$i + 1];
|
||||
if (isset($template_types[$string_type_token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($type_tokens[$i + 1])) {
|
||||
$next_char = $type_tokens[$i + 1];
|
||||
if ($next_char === ':') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($next_char === '?' && isset($return_type_tokens[$i + 2]) && $return_type_tokens[$i + 2] === ':') {
|
||||
if ($next_char === '?' && isset($type_tokens[$i + 2]) && $type_tokens[$i + 2] === ':') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($return_type_token[0] === '$') {
|
||||
if ($string_type_token[0] === '$') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$return_type_tokens[$i] = self::getFQCLNFromString(
|
||||
$return_type_token,
|
||||
$type_tokens[$i] = self::getFQCLNFromString(
|
||||
$string_type_token,
|
||||
$aliases
|
||||
);
|
||||
}
|
||||
|
||||
return implode('', $return_type_tokens);
|
||||
return $type_tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -243,12 +243,12 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
|
||||
if ($docblock_info->properties) {
|
||||
foreach ($docblock_info->properties as $property) {
|
||||
$pseudo_property_type_string = Type::fixUpLocalType(
|
||||
$pseudo_property_type_tokens = Type::fixUpLocalType(
|
||||
$property['type'],
|
||||
$this->aliases
|
||||
);
|
||||
|
||||
$pseudo_property_type = Type::parseString($pseudo_property_type_string);
|
||||
$pseudo_property_type = Type::parseTokens($pseudo_property_type_tokens);
|
||||
$pseudo_property_type->setFromDocblock();
|
||||
|
||||
if ($property['tag'] !== 'property-read') {
|
||||
@ -1020,14 +1020,14 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
|
||||
if ($docblock_return_type) {
|
||||
try {
|
||||
$fixed_type_string = Type::fixUpLocalType(
|
||||
$fixed_type_tokens = Type::fixUpLocalType(
|
||||
$docblock_return_type,
|
||||
$this->aliases,
|
||||
$this->function_template_types + $this->class_template_types
|
||||
);
|
||||
|
||||
$storage->return_type = Type::parseString(
|
||||
$fixed_type_string,
|
||||
$storage->return_type = Type::parseTokens(
|
||||
$fixed_type_tokens,
|
||||
false,
|
||||
$this->function_template_types + $this->class_template_types
|
||||
);
|
||||
@ -1229,7 +1229,7 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
$code_location->setCommentLine($docblock_param['line_number']);
|
||||
|
||||
try {
|
||||
$new_param_type = Type::parseString(
|
||||
$new_param_type = Type::parseTokens(
|
||||
Type::fixUpLocalType(
|
||||
$docblock_param['type'],
|
||||
$this->aliases,
|
||||
|
70
tests/EnumTest.php
Normal file
70
tests/EnumTest.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
|
||||
class EnumTest extends TestCase
|
||||
{
|
||||
use Traits\FileCheckerInvalidCodeParseTestTrait;
|
||||
use Traits\FileCheckerValidCodeParseTestTrait;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function providerFileCheckerValidCodeParse()
|
||||
{
|
||||
return [
|
||||
'enumStringOrEnumIntCorrect' => [
|
||||
'<?php
|
||||
/** @psalm-param ( "foo\"with" | "bar" | 1 | 2 | 3 ) $s */
|
||||
function foo($s) : void {}
|
||||
foo("foo\"with");
|
||||
foo("bar");
|
||||
foo(1);
|
||||
foo(2);
|
||||
foo(3);',
|
||||
],
|
||||
'enumStringOrEnumIntWithoutSpacesCorrect' => [
|
||||
'<?php
|
||||
/** @psalm-param "foo\"with"|"bar"|1|2|3 $s */
|
||||
function foo($s) : void {}
|
||||
foo("foo\"with");
|
||||
foo("bar");
|
||||
foo(1);
|
||||
foo(2);
|
||||
foo(3);',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function providerFileCheckerInvalidCodeParse()
|
||||
{
|
||||
return [
|
||||
'enumStringOrEnumIntIncorrectString' => [
|
||||
'<?php
|
||||
/** @psalm-param ( "foo" | "bar" | 1 | 2 | 3 ) $s */
|
||||
function foo($s) : void {}
|
||||
foo("bat");',
|
||||
'error_message' => 'InvalidArgument',
|
||||
],
|
||||
'enumStringOrEnumIntIncorrectInt' => [
|
||||
'<?php
|
||||
/** @psalm-param ( "foo" | "bar" | 1 | 2 | 3 ) $s */
|
||||
function foo($s) : void {}
|
||||
foo(4);',
|
||||
'error_message' => 'InvalidArgument',
|
||||
],
|
||||
'enumStringOrEnumIntWithoutSpacesIncorrect' => [
|
||||
'<?php
|
||||
/** @psalm-param "foo\"with"|"bar"|1|2|3 $s */
|
||||
function foo($s) : void {}
|
||||
foo(4);',
|
||||
'error_message' => 'InvalidArgument',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -482,6 +482,27 @@ class TypeParseTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function testEnum()
|
||||
{
|
||||
$docblock_type = Type::parseString('( \'foo\\\'with\' | "bar\"bar" | "baz" | "bat\\\\" | \'bang bang\' | 1 | 2 | 3)');
|
||||
|
||||
$resolved_type = new Type\Union([
|
||||
new Type\Atomic\TLiteralString('foo\'with'),
|
||||
new Type\Atomic\TLiteralString('bar"bar'),
|
||||
new Type\Atomic\TLiteralString('baz'),
|
||||
new Type\Atomic\TLiteralString('bat\\'),
|
||||
new Type\Atomic\TLiteralString('bang bang'),
|
||||
new Type\Atomic\TLiteralInt(1),
|
||||
new Type\Atomic\TLiteralInt(2),
|
||||
new Type\Atomic\TLiteralInt(3)
|
||||
]);
|
||||
|
||||
$this->assertSame($resolved_type->getId(), $docblock_type->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerTestValidCallMapType
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user