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 e979611b86e..8d54e0ab894 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 eb45a87cce7..60e7e2664a2 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 84fbbe2cc56..62e85011f88 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -84,6 +84,7 @@ 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 @@ private static function fromXmlAndPaths(string $base_dir, string $file_contents, } } + 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 @@ private function getPHPVersionFromComposerJson(): ?string } 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 f589ee69046..e03085e69ca 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 @@ 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 @@ public static function analyze( ( 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 c4be8a8c41f..e01ee0c3950 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 @@ public static function analyze( // 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 8e9f6223693..3c8361944a4 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -1290,4 +1290,22 @@ public function testSetsUsePhpStormMetaPath(): void $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 88de6ca0603..784a3ae14fe 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -118,6 +118,35 @@ public function getX(bool $b): int { $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(