1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Fix #390 - add support for object{foo:int, bar:string} annotation

This commit is contained in:
Matthew Brown 2019-01-18 00:56:24 -05:00
parent 0358719037
commit 4ec7903e8b
7 changed files with 328 additions and 5 deletions

View File

@ -455,6 +455,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
case Type\Atomic\TMixed::class:
case Type\Atomic\TNonEmptyMixed::class:
case Type\Atomic\TObject::class:
case Type\Atomic\TObjectWithProperties::class:
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
$has_mixed_method_call = true;

View File

@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
/**
* @internal
@ -262,6 +263,21 @@ class PropertyFetchAnalyzer
$has_valid_fetch_type = true;
if ($lhs_type_part instanceof TObjectWithProperties
&& isset($lhs_type_part->properties[$prop_name])
) {
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes(
$lhs_type_part->properties[$prop_name],
$stmt->inferredType
);
} else {
$stmt->inferredType = $lhs_type_part->properties[$prop_name];
}
continue;
}
// stdClass and SimpleXMLElement are special cases where we cannot infer the return types
// but we don't want to throw an error
// Hack has a similar issue: https://github.com/facebook/hhvm/issues/5164
@ -269,6 +285,7 @@ class PropertyFetchAnalyzer
|| in_array(strtolower($lhs_type_part->value), ['stdclass', 'simplexmlelement'], true)
) {
$stmt->inferredType = Type::getMixed();
continue;
}

View File

@ -5,6 +5,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Codebase;
use Psalm\Type;
use Psalm\Type\Atomic\ObjectLike;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TArrayKey;
@ -661,7 +662,11 @@ class TypeAnalyzer
return false;
}
if ($container_type_part instanceof ObjectLike && $input_type_part instanceof ObjectLike) {
if (($container_type_part instanceof ObjectLike
&& $input_type_part instanceof ObjectLike)
|| ($container_type_part instanceof TObjectWithProperties
&& $input_type_part instanceof TObjectWithProperties)
) {
$all_types_contain = true;
foreach ($container_type_part->properties as $key => $container_property_type) {
@ -971,10 +976,81 @@ class TypeAnalyzer
) {
$has_scalar_match = true;
}
} elseif ($container_type_part instanceof TObject &&
!$input_type_part instanceof TArray &&
!$input_type_part instanceof TResource
} elseif ($container_type_part instanceof TObject
&& $input_type_part instanceof TNamedObject
) {
/** @psalm-suppress RedundantCondition due to some sort of Psalm bug */
if ($container_type_part instanceof TObjectWithProperties
&& $input_type_part->value !== 'stdClass'
) {
$all_types_contain = true;
foreach ($container_type_part->properties as $property_name => $container_property_type) {
if (!is_string($property_name)) {
continue;
}
if (!$codebase->properties->propertyExists($input_type_part . '::$' . $property_name)) {
$all_types_contain = false;
continue;
}
$property_declaring_class = (string) $codebase->properties->getDeclaringClassForProperty(
$input_type_part . '::$' . $property_name
);
$class_storage = $codebase->classlike_storage_provider->get($property_declaring_class);
$input_property_storage = $class_storage->properties[$property_name];
$input_property_type = $input_property_storage->type ?: Type::getMixed();
if (!$input_property_type->isEmpty()
&& !self::isContainedBy(
$codebase,
$input_property_type,
$container_property_type,
false,
false,
$property_has_scalar_match,
$property_type_coerced,
$property_type_coerced_from_mixed,
$property_type_to_string_cast,
$property_type_coerced_from_scalar,
$allow_interface_equality
)
&& !$property_type_coerced_from_scalar
) {
if (self::isContainedBy(
$codebase,
$container_property_type,
$input_property_type,
false,
false,
$inverse_property_has_scalar_match,
$inverse_property_type_coerced,
$inverse_property_type_coerced_from_mixed,
$inverse_property_type_to_string_cast,
$inverse_property_type_coerced_from_scalar,
$allow_interface_equality
)
|| $inverse_property_type_coerced_from_scalar
) {
$type_coerced = true;
}
$all_types_contain = false;
}
}
if ($all_types_contain === true) {
return true;
}
return false;
}
return true;
} elseif ($input_type_part instanceof TObject && $container_type_part instanceof TNamedObject) {
$type_coerced = true;

View File

@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TSingleLetter;
use Psalm\Type\Atomic\TString;
@ -349,7 +350,7 @@ abstract class Type
$properties[$property_key] = $property_type;
}
if ($type !== 'array') {
if ($type !== 'array' && $type !== 'object') {
throw new TypeParseTreeException('Unexpected brace character');
}
@ -357,6 +358,10 @@ abstract class Type
throw new TypeParseTreeException('No properties supplied for ObjectLike');
}
if ($type === 'object') {
return new TObjectWithProperties($properties);
}
return new ObjectLike($properties);
}

View File

@ -0,0 +1,186 @@
<?php
namespace Psalm\Type\Atomic;
use Psalm\Type\Union;
use Psalm\Type\Atomic;
class TObjectWithProperties extends TObject
{
/**
* @var array<string|int, Union>
*/
public $properties;
/**
* Constructs a new instance of a generic type
*
* @param array<string|int, Union> $properties
*/
public function __construct(array $properties)
{
$this->properties = $properties;
}
public function __toString()
{
return 'object{' .
implode(
', ',
array_map(
/**
* @param string|int $name
* @param Union $type
*
* @return string
*/
function ($name, Union $type) {
return $name . ($type->possibly_undefined ? '?' : '') . ':' . $type;
},
array_keys($this->properties),
$this->properties
)
) .
'}';
}
public function getId()
{
return 'object{' .
implode(
', ',
array_map(
/**
* @param string|int $name
* @param Union $type
*
* @return string
*/
function ($name, Union $type) {
return $name . ($type->possibly_undefined ? '?' : '') . ':' . $type->getId();
},
array_keys($this->properties),
$this->properties
)
) .
'}';
}
/**
* @param string|null $namespace
* @param array<string> $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)
{
if ($use_phpdoc_format) {
return 'object';
}
return 'object{' .
implode(
', ',
array_map(
/**
* @param string|int $name
* @param Union $type
*
* @return string
*/
function (
$name,
Union $type
) use (
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
) {
return $name . ($type->possibly_undefined ? '?' : '') . ':' . $type->toNamespacedString(
$namespace,
$aliased_classes,
$this_class,
$use_phpdoc_format
);
},
array_keys($this->properties),
$this->properties
)
) .
'}';
}
/**
* @param string|null $namespace
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param int $php_major_version
* @param int $php_minor_version
*
* @return string
*/
public function toPhpString($namespace, array $aliased_classes, $this_class, $php_major_version, $php_minor_version)
{
return $this->getKey();
}
/**
* @return bool
*/
public function canBeFullyExpressedInPhp()
{
return false;
}
public function __clone()
{
foreach ($this->properties as &$property) {
$property = clone $property;
}
}
public function setFromDocblock()
{
$this->from_docblock = true;
foreach ($this->properties as $property_type) {
$property_type->setFromDocblock();
}
}
/**
* @return bool
*/
public function equals(Atomic $other_type)
{
if (!$other_type instanceof self) {
return false;
}
if (count($this->properties) !== count($other_type->properties)) {
return false;
}
foreach ($this->properties as $property_name => $property_type) {
if (!isset($other_type->properties[$property_name])) {
return false;
}
if (!$property_type->equals($other_type->properties[$property_name])) {
return false;
}
}
return true;
}
/**
* @return string
*/
public function getAssertionString()
{
return $this->getKey();
}
}

View File

@ -740,6 +740,24 @@ class AnnotationTest extends TestCase
'MissingReturnType' => \Psalm\Config::REPORT_INFO,
]
],
'objectWithPropertiesAnnotation' => [
'<?php
/** @param object{foo:string} $o */
function foo(object $o) : string {
return $o->foo;
}
$s = new \stdClass();
$s->foo = "hello";
foo($s);
class A {
/** @var string */
public $foo = "hello";
}
foo(new A);',
],
];
}
@ -1162,6 +1180,18 @@ class AnnotationTest extends TestCase
function foo() {}',
'error_message' => 'MissingReturnType',
],
'objectWithPropertiesAnnotationNoMatchingProperty' => [
'<?php
/** @param object{foo:string} $o */
function foo(object $o) : string {
return $o->foo;
}
class A {}
foo(new A);',
'error_message' => 'InvalidArgument',
],
];
}
}

View File

@ -289,6 +289,14 @@ class TypeParseTest extends TestCase
$this->assertSame('array{a:int, b:string}', (string) Type::parseString('array{a:int, b:string}'));
}
/**
* @return void
*/
public function testObjectWithSimpleArgs()
{
$this->assertSame('object{a:int, b:string}', (string) Type::parseString('object{a:int, b:string}'));
}
/**
* @return void
*/