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:
parent
0358719037
commit
4ec7903e8b
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
186
src/Psalm/Type/Atomic/TObjectWithProperties.php
Normal file
186
src/Psalm/Type/Atomic/TObjectWithProperties.php
Normal 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();
|
||||
}
|
||||
}
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user