1
0
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:
Matthew Brown 2018-05-20 17:19:53 -04:00
parent 7bc426268a
commit 6250c2a14c
6 changed files with 271 additions and 68 deletions

View File

@ -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 === '>') {

View File

@ -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 &&

View File

@ -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;
}
/**

View File

@ -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
View 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',
],
];
}
}

View File

@ -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
*