From a19f7389674aa68a3f798fc35a1c8dc22d21f5f9 Mon Sep 17 00:00:00 2001 From: feek <5747667+mr-feek@users.noreply.github.com> Date: Wed, 7 Oct 2020 06:56:21 -0700 Subject: [PATCH] feature: universal object crates (#3948) * feature: universal object crates * docs: document universal object crate config option Co-authored-by: Matthew Brown --- config.xsd | 7 ++++ docs/running_psalm/configuration.md | 3 ++ src/Psalm/Config.php | 35 +++++++++++++++++++ .../InstancePropertyAssignmentAnalyzer.php | 7 +++- .../Fetch/InstancePropertyFetchAnalyzer.php | 3 +- tests/Config/ConfigTest.php | 18 ++++++++++ tests/PropertyTypeTest.php | 29 +++++++++++++++ 7 files changed, 100 insertions(+), 2 deletions(-) diff --git a/config.xsd b/config.xsd index e979611b8..8d54e0ab8 100644 --- a/config.xsd +++ b/config.xsd @@ -21,6 +21,7 @@ + @@ -128,6 +129,12 @@ + + + + + + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index eb45a87cc..60e7e2664 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -387,6 +387,9 @@ Optional. If you don't want Psalm to complain about every single issue it finds #### <mockClasses> Optional. Do you use mock classes in your tests? If you want Psalm to ignore them when checking files, include a fully-qualified path to the class with `` +#### <universalObjectCrates> +Optional. Do you have objects with properties that cannot be determined statically? If you want Psalm to treat all properties on a given classlike as mixed, include a fully-qualified path to the class with ``. By default, `stdClass` and `SimpleXMLElement` are configured to be universal object crates. + #### <stubs> Optional. If your codebase uses classes and functions that are not visible to Psalm via reflection (e.g. if there are internal packages that your codebase relies on that are not available on the machine running Psalm), you can use stub files. Used by PhpStorm (a popular IDE) and others, stubs provide a description of classes and functions without the implementations. You can find a list of stubs for common classes [here](https://github.com/JetBrains/phpstorm-stubs). List out each file with ``. diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 84fbbe2cc..62e85011f 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -84,6 +84,7 @@ use const LIBXML_ERR_FATAL; use const LIBXML_NONET; use const PHP_EOL; use const SCANDIR_SORT_NONE; +use function array_map; /** * @psalm-suppress PropertyNotSetInConstructor @@ -128,6 +129,15 @@ class Config 'MixedReturnTypeCoercion', ]; + /** + * These are special object classes that allow any and all properties to be get/set on them + * @var array + */ + protected $universal_object_crates = [ + \stdClass::class, + SimpleXMLElement::class, + ]; + /** * @var static|null */ @@ -953,6 +963,15 @@ class Config } } + if (isset($config_xml->universalObjectCrates) && isset($config_xml->universalObjectCrates->class)) { + /** @var \SimpleXMLElement $universal_object_crate */ + foreach ($config_xml->universalObjectCrates->class as $universal_object_crate) { + /** @var class-string $classString */ + $classString = $universal_object_crate['name']; + $config->addUniversalObjectCrate($classString); + } + } + if (isset($config_xml->ignoreExceptions)) { if (isset($config_xml->ignoreExceptions->class)) { /** @var \SimpleXMLElement $exception_class */ @@ -1987,4 +2006,20 @@ class Config } return null; } + + /** + * @param class-string $class + */ + public function addUniversalObjectCrate(string $class): void + { + $this->universal_object_crates[] = $class; + } + + /** + * @return array + */ + public function getUniversalObjectCrates(): array + { + return array_map('strtolower', $this->universal_object_crates); + } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index f589ee690..e03085e69 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -4,6 +4,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Assignment; use PhpParser; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Stmt\PropertyProperty; +use Psalm\Config; use Psalm\Internal\Analyzer\ClassAnalyzer; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; @@ -269,7 +270,11 @@ class InstancePropertyAssignmentAnalyzer ( in_array( strtolower($lhs_type_part->value), - ['stdclass', 'simplexmlelement', 'dateinterval', 'domdocument', 'domnode'], + Config::getInstance()->getUniversalObjectCrates() + [ + 'dateinterval', + 'domdocument', + 'domnode' + ], true ) ) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php index c4be8a8c4..e01ee0c39 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Fetch; use PhpParser; +use Psalm\Config; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\NamespaceAnalyzer; @@ -427,7 +428,7 @@ class InstancePropertyFetchAnalyzer // but we don't want to throw an error // Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164 if ($lhs_type_part instanceof TObject - || in_array(strtolower($lhs_type_part->value), ['stdclass', 'simplexmlelement'], true) + || in_array(strtolower($lhs_type_part->value), Config::getInstance()->getUniversalObjectCrates(), true) ) { $statements_analyzer->node_data->setType($stmt, Type::getMixed()); diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 8e9f62236..3c8361944 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -1290,4 +1290,22 @@ class ConfigTest extends \Psalm\Tests\TestCase $this->assertFalse($this->project_analyzer->getConfig()->use_phpstorm_meta_path); } + + /** @return void */ + public function testSetsUniversalObjectCrates() + { + $this->project_analyzer = $this->getProjectAnalyzerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__, 2), + ' + + + + + ' + ) + ); + + $this->assertContains('foo', $this->project_analyzer->getConfig()->getUniversalObjectCrates()); + } } diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 88de6ca06..784a3ae14 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -118,6 +118,35 @@ class PropertyTypeTest extends TestCase $this->analyzeFile('somefile.php', new Context()); } + /** + * @return void + */ + public function testUniversalObjectCrates(): void + { + /** @var class-string $classString */ + $classString = 'Foo'; + Config::getInstance()->addUniversalObjectCrate($classString); + + $this->addFile( + 'somefile.php', + 'bar; + + // sets are fine + $f->buzz = false; + ' + ); + + $this->analyzeFile('somefile.php', new Context()); + } + + /** + * @return void + */ public function testForgetPropertyAssignmentsInBranchWithThrowNormally(): void { $this->addFile(