Skip to content

Commit

Permalink
feature: universal object crates (vimeo#3948)
Browse files Browse the repository at this point in the history
* feature: universal object crates

* docs: document universal object crate config option

Co-authored-by: Matthew Brown <github@muglug.com>
  • Loading branch information
2 people authored and danog committed Jan 29, 2021
1 parent c4cbe8d commit a19f738
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 2 deletions.
7 changes: 7 additions & 0 deletions config.xsd
Expand Up @@ -21,6 +21,7 @@
<xs:element name="issueHandlers" type="IssueHandlersType" minOccurs="0" maxOccurs="1" />
<xs:element name="ignoreExceptions" type="ExceptionsType" minOccurs="0" maxOccurs="1" />
<xs:element name="globals" type="GlobalsType" minOccurs="0" maxOccurs="1" />
<xs:element name="universalObjectCrates" type="UniversalObjectCratesType" minOccurs="0" maxOccurs="1" />
</xs:choice>

<xs:attribute name="autoloader" type="xs:string" />
Expand Down Expand Up @@ -128,6 +129,12 @@
</xs:sequence>
</xs:complexType>

<xs:complexType name="UniversalObjectCratesType">
<xs:sequence>
<xs:element name="class" maxOccurs="unbounded" type="NameAttributeType" />
</xs:sequence>
</xs:complexType>

<xs:complexType name="ExceptionsType">
<xs:sequence>
<xs:element name="class" minOccurs="0" maxOccurs="unbounded" type="ExceptionType" />
Expand Down
3 changes: 3 additions & 0 deletions docs/running_psalm/configuration.md
Expand Up @@ -387,6 +387,9 @@ Optional. If you don't want Psalm to complain about every single issue it finds
#### &lt;mockClasses&gt;
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 `<class name="Your\Namespace\ClassName" />`

#### &lt;universalObjectCrates&gt;
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 `<class name="Your\Namespace\ClassName" />`. By default, `stdClass` and `SimpleXMLElement` are configured to be universal object crates.

#### &lt;stubs&gt;
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 `<file name="path/to/file.php" />`.

Expand Down
35 changes: 35 additions & 0 deletions src/Psalm/Config.php
Expand Up @@ -84,6 +84,7 @@
use const LIBXML_NONET;
use const PHP_EOL;
use const SCANDIR_SORT_NONE;
use function array_map;

/**
* @psalm-suppress PropertyNotSetInConstructor
Expand Down Expand Up @@ -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<int, class-string>
*/
protected $universal_object_crates = [
\stdClass::class,
SimpleXMLElement::class,
];

/**
* @var static|null
*/
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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<int, lowercase-string>
*/
public function getUniversalObjectCrates(): array
{
return array_map('strtolower', $this->universal_object_crates);
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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
)
)
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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());

Expand Down
18 changes: 18 additions & 0 deletions tests/Config/ConfigTest.php
Expand Up @@ -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),
'<?xml version="1.0"?>
<psalm>
<universalObjectCrates>
<class name="Foo" />
</universalObjectCrates>
</psalm>'
)
);

$this->assertContains('foo', $this->project_analyzer->getConfig()->getUniversalObjectCrates());
}
}
29 changes: 29 additions & 0 deletions tests/PropertyTypeTest.php
Expand Up @@ -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',
'<?php
class Foo { }
$f = new Foo();
// reads are fine
$f->bar;
// sets are fine
$f->buzz = false;
'
);

$this->analyzeFile('somefile.php', new Context());
}

/**
* @return void
*/
public function testForgetPropertyAssignmentsInBranchWithThrowNormally(): void
{
$this->addFile(
Expand Down

0 comments on commit a19f738

Please sign in to comment.