From 8d2baf584e26e32dc47638548160919883dd4ab2 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Thu, 1 Feb 2018 01:10:27 -0500 Subject: [PATCH] Fix #479 - allow PhpStorm generic syntax behind a config flag --- config.xsd | 1 + src/Psalm/Codebase.php | 79 +++++++++++++++++++++++++++++++++++ src/Psalm/Config.php | 10 +++++ tests/AnnotationTest.php | 90 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+) diff --git a/config.xsd b/config.xsd index c42fde1c3..6b46c4857 100644 --- a/config.xsd +++ b/config.xsd @@ -28,6 +28,7 @@ + diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 4da2f2814..ba5ba6145 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -383,6 +383,42 @@ class Codebase $this->populateFileStorage($file_storage); } + if ($this->config->allow_phpstorm_generics) { + foreach ($this->classlike_storage_provider->getAll() as $class_storage) { + foreach ($class_storage->properties as $property_storage) { + if ($property_storage->type) { + $this->convertPhpStormGenericToPsalmGeneric($property_storage->type, true); + } + } + + foreach ($class_storage->methods as $method_storage) { + if ($method_storage->return_type) { + $this->convertPhpStormGenericToPsalmGeneric($method_storage->return_type); + } + + foreach ($method_storage->params as $param_storage) { + if ($param_storage->type) { + $this->convertPhpStormGenericToPsalmGeneric($param_storage->type); + } + } + } + } + + foreach ($all_file_storage as $file_storage) { + foreach ($file_storage->functions as $function_storage) { + if ($function_storage->return_type) { + $this->convertPhpStormGenericToPsalmGeneric($function_storage->return_type); + } + + foreach ($function_storage->params as $param_storage) { + if ($param_storage->type) { + $this->convertPhpStormGenericToPsalmGeneric($param_storage->type); + } + } + } + } + } + if ($this->debug_output) { echo 'FileStorage is populated' . PHP_EOL; } @@ -629,6 +665,49 @@ class Codebase $storage->populated = true; } + /** + * @param Type\Union $candidate + * @param bool $is_property + * + * @return void + */ + private function convertPhpStormGenericToPsalmGeneric(Type\Union $candidate, $is_property = false) + { + $atomic_types = $candidate->getTypes(); + + if (isset($atomic_types['array']) && count($atomic_types) > 1) { + $iterator_name = null; + $generic_params = null; + + foreach ($atomic_types as $type) { + if ($type instanceof Type\Atomic\TNamedObject + && (!$type->from_docblock || $is_property) + && ( + strtolower($type->value) === 'traversable' + || $this->interfaceExtends( + $type->value, + 'Traversable' + ) + || $this->classImplements( + $type->value, + 'Traversable' + ) + ) + ) { + $iterator_name = $type->value; + } elseif ($type instanceof Type\Atomic\TArray) { + $generic_params = $type->type_params; + } + } + + if ($iterator_name && $generic_params) { + $generic_iterator = new Type\Atomic\TGenericObject($iterator_name, $generic_params); + $candidate->removeType('array'); + $candidate->addType($generic_iterator); + } + } + } + /** * @param ClassLikeStorage $storage * @param ClassLikeStorage $parent_storage diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index b269ebf09..c95f3698d 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -172,6 +172,11 @@ class Config /** @var bool */ public $use_igbinary = false; + /** + * @var bool + */ + public $allow_phpstorm_generics = false; + /** * Psalm plugins * @@ -382,6 +387,11 @@ class Config $config->use_igbinary = $attribute_text === 'igbinary'; } + if (isset($config_xml['allowPhpStormGenerics'])) { + $attribute_text = (string) $config_xml['allowPhpStormGenerics']; + $config->allow_phpstorm_generics = $attribute_text === 'true' || $attribute_text === '1'; + } + if (isset($config_xml->projectFiles)) { $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true); } diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index db506ff71..14354db3b 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1,11 +1,87 @@ allow_phpstorm_generics = true; + + $this->addFile( + 'somefile.php', + 'offsetGet("a"); + takesString($s); + + foreach ($i as $s2) { + takesString($s2); + } + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } + + /** + * @expectedException \Psalm\Exception\CodeException + * @expectedExceptionMessage InvalidScalarArgument + * + * @return void + */ + public function testPhpStormGenericsInvalidArgument() + { + Config::getInstance()->allow_phpstorm_generics = true; + + $this->addFile( + 'somefile.php', + 'offsetGet("a"); + takesInt($s); + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } + + /** + * @expectedException \Psalm\Exception\CodeException + * @expectedExceptionMessage PossiblyInvalidMethodCall + * + * @return void + */ + public function testPhpStormGenericsNoTypehint() + { + Config::getInstance()->allow_phpstorm_generics = true; + + $this->addFile( + 'somefile.php', + 'offsetGet("a"); + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } + /** * @return array */ @@ -733,6 +809,20 @@ class AnnotationTest extends TestCase function bar(array $arr): void {}', 'error_message' => 'InvalidDocblock', ], + 'noPhpStormAnnotationsThankYou' => [ + ' 'MismatchingDocblockParamType', + ], + 'noPhpStormAnnotationsPossiblyInvalid' => [ + 'offsetGet("a"); + }', + 'error_message' => 'PossiblyInvalidMethodCall', + ], ]; } }