From 4002504ff03ac2894f96b25b852eb774bff712d2 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Fri, 31 May 2019 09:43:46 -0400 Subject: [PATCH] Allow trait_exists to inform type for ReflectionClass --- .../Statements/Expression/AssertionFinder.php | 37 +++++++++++ src/Psalm/Internal/Analyzer/TypeAnalyzer.php | 11 ++++ .../Internal/Stubs/CoreGenericClasses.php | 2 +- src/Psalm/Internal/Type/TypeCombination.php | 12 +++- src/Psalm/Type.php | 1 + src/Psalm/Type/Atomic.php | 4 ++ src/Psalm/Type/Atomic/TTraitString.php | 66 +++++++++++++++++++ src/Psalm/Type/Reconciler.php | 12 +++- src/Psalm/Type/Union.php | 4 ++ tests/FunctionCallTest.php | 28 ++++++++ 10 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 src/Psalm/Type/Atomic/TTraitString.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 5226596ad..22323df3a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -1674,6 +1674,14 @@ class AssertionFinder $if_types[$first_var_name] = [['=class-string']]; } } + } elseif ($class_exists_check_type = self::hasTraitExistsCheck($expr)) { + if ($first_var_name) { + if ($class_exists_check_type === 2) { + $if_types[$first_var_name] = [[$prefix . 'trait-string']]; + } elseif (!$prefix) { + $if_types[$first_var_name] = [['=trait-string']]; + } + } } elseif (self::hasInterfaceExistsCheck($expr)) { if ($first_var_name) { $if_types[$first_var_name] = [[$prefix . 'interface-string']]; @@ -2319,6 +2327,35 @@ class AssertionFinder return 0; } + /** + * @param PhpParser\Node\Expr\FuncCall $stmt + * + * @return 0|1|2 + */ + protected static function hasTraitExistsCheck(PhpParser\Node\Expr\FuncCall $stmt) + { + if ($stmt->name instanceof PhpParser\Node\Name + && strtolower($stmt->name->parts[0]) === 'trait_exists' + ) { + if (!isset($stmt->args[1])) { + return 2; + } + + $second_arg = $stmt->args[1]->value; + + if ($second_arg instanceof PhpParser\Node\Expr\ConstFetch + && $second_arg->name instanceof PhpParser\Node\Name + && strtolower($second_arg->name->parts[0]) === 'true' + ) { + return 2; + } + + return 1; + } + + return 0; + } + /** * @param PhpParser\Node\Expr\FuncCall $stmt * diff --git a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php index ea886ea6c..1640b6e15 100644 --- a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php @@ -38,6 +38,7 @@ use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TSingleLetter; use Psalm\Type\Atomic\TString; +use Psalm\Type\Atomic\TTraitString; use Psalm\Type\Atomic\TTrue; /** @@ -1092,6 +1093,16 @@ class TypeAnalyzer ); } + if ($container_type_part instanceof TString && $input_type_part instanceof TTraitString) { + return true; + } + + if ($container_type_part instanceof TTraitString && get_class($input_type_part) === TString::class) { + $type_coerced = true; + + return false; + } + if (($input_type_part instanceof TClassString || $input_type_part instanceof TLiteralClassString) && (get_class($container_type_part) === TString::class diff --git a/src/Psalm/Internal/Stubs/CoreGenericClasses.php b/src/Psalm/Internal/Stubs/CoreGenericClasses.php index 94efc1ac2..dc33a5973 100644 --- a/src/Psalm/Internal/Stubs/CoreGenericClasses.php +++ b/src/Psalm/Internal/Stubs/CoreGenericClasses.php @@ -1167,7 +1167,7 @@ class ReflectionClass implements Reflector { public $name; /** - * @param T|class-string $argument + * @param T|class-string|trait-string $argument */ public function __construct($argument) {} diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 368a9d81a..9cd680229 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -31,6 +31,7 @@ use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Atomic\TTraitString; use Psalm\Type\Atomic\TTrue; use Psalm\Internal\Type\TypeCombination; use Psalm\Type\Union; @@ -902,7 +903,9 @@ class TypeCombination } } - if ($has_non_literal_class_string || !$type instanceof TClassString) { + if ($has_non_literal_class_string || + !$type instanceof TClassString + ) { $combination->value_types[$type_key] = new TString(); } else { if (isset($shared_classlikes[$type->as])) { @@ -952,6 +955,13 @@ class TypeCombination } else { $combination->value_types[$type_key] = new TClassString(); } + } elseif ($combination->value_types['string'] instanceof TTraitString + && $type instanceof TClassString + ) { + $combination->value_types['trait-string'] = $combination->value_types['string']; + $combination->value_types['class-string'] = $type; + + unset($combination->value_types['string']); } elseif (get_class($combination->value_types['string']) !== get_class($type)) { $combination->value_types[$type_key] = new TString(); } diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index c3d4b3e3d..15cb8bf93 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -59,6 +59,7 @@ abstract class Type 'numeric-string' => true, 'class-string' => true, 'callable-string' => true, + 'trait-string' => true, 'mysql-escaped-string' => true, 'html-escaped-string' => true, 'boolean' => true, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 802fe6018..6b7f9be92 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -45,6 +45,7 @@ use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TScalarClassConstant; use Psalm\Type\Atomic\TString; +use Psalm\Type\Atomic\TTraitString; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Atomic\TVoid; @@ -166,6 +167,9 @@ abstract class Atomic case 'interface-string': return new TClassString(); + case 'trait-string': + return new TTraitString(); + case 'callable-string': return new TCallableString(); diff --git a/src/Psalm/Type/Atomic/TTraitString.php b/src/Psalm/Type/Atomic/TTraitString.php new file mode 100644 index 000000000..a726bd5d0 --- /dev/null +++ b/src/Psalm/Type/Atomic/TTraitString.php @@ -0,0 +1,66 @@ +getKey(); + } + + public function getId() + { + return $this->getKey(); + } + + /** + * @param string|null $namespace + * @param array $aliased_classes + * @param string|null $this_class + * @param int $php_major_version + * @param int $php_minor_version + * + * @return string|null + */ + public function toPhpString( + $namespace, + array $aliased_classes, + $this_class, + $php_major_version, + $php_minor_version + ) { + return 'string'; + } + + /** + * @param string|null $namespace + * @param array $aliased_classes + * @param string|null $this_class + * @param bool $use_phpdoc_format + * + * @return string + */ + public function toNamespacedString($namespace, array $aliased_classes, $this_class, $use_phpdoc_format) + { + return 'trait-string'; + } + + /** + * @return bool + */ + public function canBeFullyExpressedInPhp() + { + return false; + } +} diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 2de28af41..0b1aeb379 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -2205,9 +2205,13 @@ class Reconciler || $scalar_type === 'class-string' || $scalar_type === 'interface-string' || $scalar_type === 'callable-string' + || $scalar_type === 'trait-string' ) { if ($existing_var_type->hasMixed() || $existing_var_type->hasScalar()) { - if ($scalar_type === 'class-string' || $scalar_type === 'interface-string') { + if ($scalar_type === 'class-string' + || $scalar_type === 'interface-string' + || $scalar_type === 'trait-string' + ) { return new Type\Union([new Type\Atomic\TLiteralClassString($value)]); } @@ -2245,7 +2249,10 @@ class Reconciler ); } } else { - if ($scalar_type === 'class-string' || $scalar_type === 'interface-string') { + if ($scalar_type === 'class-string' + || $scalar_type === 'interface-string' + || $scalar_type === 'trait-string' + ) { $existing_var_type = new Type\Union([new Type\Atomic\TLiteralClassString($value)]); } else { $existing_var_type = new Type\Union([new Type\Atomic\TLiteralString($value)]); @@ -2401,6 +2408,7 @@ class Reconciler } elseif ($scalar_type === 'string' || $scalar_type === 'class-string' || $scalar_type === 'interface-string' + || $scalar_type === 'trait-string' || $scalar_type === 'callable-string' ) { if ($existing_var_type->hasString()) { diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 232f3e8c6..848bac84b 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -516,6 +516,7 @@ class Union } unset($this->types['class-string']); + unset($this->types['trait-string']); } elseif ($type_string === 'int' && $this->literal_int_types) { foreach ($this->literal_int_types as $literal_key => $_) { unset($this->types[$literal_key]); @@ -646,6 +647,7 @@ class Union { return isset($this->types['string']) || isset($this->types['class-string']) + || isset($this->types['trait-string']) || isset($this->types['numeric-string']) || isset($this->types['array-key']) || $this->literal_string_types @@ -727,6 +729,7 @@ class Union || isset($this->types['float']) || isset($this->types['string']) || isset($this->types['class-string']) + || isset($this->types['trait-string']) || isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']) @@ -1529,6 +1532,7 @@ class Union return isset($this->types['string']) || isset($this->types['class-string']) + || isset($this->types['trait-string']) || isset($this->types['numeric-string']) || $this->literal_string_types; } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 4afdd4d56..fe2dfb981 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1680,6 +1680,34 @@ class FunctionCallTest extends TestCase $xml = new SimpleXMLElement(""); echo count($xml);' ], + 'refineWithTraitExists' => [ + ' [ + '