From e4eb00b8c6b2b72fb9ab474822781045ea2d9144 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 19 Nov 2022 15:54:02 +0100 Subject: [PATCH] Declaring more precise types and purity boundaries on `ext-reflection` symbols in `.phpstub` files Also: * added PHP 8.2 stubs * refined types to make impossible scenarios more clear (like `ReflectionIntersectionType#allowsNull()`) This is a first attempt at refining these types: the structure of these stubs is quite confusing to me, so I don't know if this approach is correct, and if the stubs are merged together, or if entire symbols need to be completely re-declared for each PHP version. --- src/Psalm/Config.php | 10 + stubs/Php80.phpstub | 113 ++++++++- stubs/Php81.phpstub | 28 +-- stubs/Php82.phpstub | 7 + stubs/Reflection.phpstub | 504 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 620 insertions(+), 42 deletions(-) create mode 100644 stubs/Php82.phpstub diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 5f6d8c54182..67d0c4dc530 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -2107,6 +2107,16 @@ public function visitPreloadedStubFiles(Codebase $codebase, ?Progress $progress $core_generic_files[] = $stringable_path; } + if (PHP_VERSION_ID < 8_02_00 && $codebase->analysis_php_version_id >= 8_02_00) { + $stringable_path = dirname(__DIR__, 2) . '/stubs/Php82.phpstub'; + + if (!file_exists($stringable_path)) { + throw new UnexpectedValueException('Cannot locate PHP 8.2 classes'); + } + + $core_generic_files[] = $stringable_path; + } + $stub_files = array_merge($core_generic_files, $this->preloaded_stub_files); if (!$stub_files) { diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 50d9d78e4be..5d3c9fcf4cd 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -17,14 +17,24 @@ class ReflectionAttribute { } + /** + * @return non-empty-string + * + * @psalm-pure + */ public function getName() : string { } + /** + * @psalm-pure + * @return int-mask-of + */ public function getTarget() : int { } + /** @psalm-pure */ public function isRepeated() : bool { } @@ -48,13 +58,101 @@ class ReflectionAttribute } } +/** + * @template-covariant T as object + * + * @property-read class-string $name + */ +class ReflectionClass implements Reflector { + /** + * @return non-empty-string|false + * @psalm-pure + */ + public function getFileName(): string|false {} + + /** + * @return positive-int|false + * @psalm-pure + */ + public function getStartLine(): int|false {} + + /** + * @return positive-int|false + * @psalm-pure + */ + public function getEndLine(): int|false {} + + /** + * @return non-empty-string|false + * @psalm-pure + */ + public function getDocComment(): string|false {} + + /** + * @param ReflectionClass|class-string $class + * + * @psalm-pure + */ + public function isSubclassOf(self|string $class): bool {} + + /** + * @param self|class-string $interface + * + * @psalm-pure + */ + public function implementsInterface(self|string $interface): bool {} + + /** + * @return non-empty-string|false + * + * @psalm-pure + */ + public function getExtensionName(): string|false {} +} + +/** @psalm-immutable */ class ReflectionClassConstant { public const IS_PUBLIC = 1; public const IS_PROTECTED = 2; public const IS_PRIVATE = 4; + + /** @return non-empty-string|false */ + public function getDocComment(): string|false {} +} + +abstract class ReflectionFunctionAbstract implements Reflector +{ + /** + * @return non-empty-string|false + * + * @psalm-pure + */ + public function getDocComment(): string|false {} + + /** + * @return positive-int|false + * + * @psalm-pure + */ + public function getStartLine(): int|false {} + + /** + * @return positive-int|false + * + * @psalm-pure + */ + public function getEndLine(): int|false {} + + /** + * @return non-empty-string|false + * + * @psalm-pure + */ + public function getFileName(): string|false {} } +/** @psalm-immutable */ class Attribute { public int $flags; @@ -76,11 +174,20 @@ class Attribute } } -class ReflectionUnionType extends ReflectionType { +class ReflectionProperty implements Reflector +{ /** - * @return non-empty-list + * @return non-empty-string|false + * + * @psalm-pure */ - public function getTypes() {} + public function getDocComment(): string|false {} +} + +/** @psalm-immutable */ +class ReflectionUnionType extends ReflectionType { + /** @return non-empty-list */ + public function getTypes(): array {} } class UnhandledMatchError extends Error {} diff --git a/stubs/Php81.phpstub b/stubs/Php81.phpstub index 694e4e9ce72..984fef8609c 100644 --- a/stubs/Php81.phpstub +++ b/stubs/Php81.phpstub @@ -26,6 +26,12 @@ namespace { public static function tryFrom(string|int $value): ?static; } + class ReflectionClass implements Reflector { + /** @psalm-pure */ + public function isEnum(): bool {} + } + + /** @psalm-immutable */ class ReflectionEnum extends ReflectionClass implements Reflector { public function getBackingType(): ?ReflectionType; @@ -36,32 +42,26 @@ namespace { public function isBacked(): bool; } + /** @psalm-immutable */ class ReflectionEnumUnitCase extends ReflectionClassConstant implements Reflector { - /** - * @psalm-pure - */ public function getEnum(): ReflectionEnum; - - /** - * @psalm-pure - */ public function getValue(): UnitEnum; } + /** @psalm-immutable */ class ReflectionEnumBackedCase extends ReflectionEnumUnitCase implements Reflector { - /** - * @psalm-pure - */ public function getBackingValue(): int|string; } + /** @psalm-immutable */ class ReflectionIntersectionType extends ReflectionType { - /** - * @return non-empty-list - */ - public function getTypes() {} + /** @return non-empty-list */ + public function getTypes(): array {} + + /** @return false */ + public function allowsNull(): bool {} } } diff --git a/stubs/Php82.phpstub b/stubs/Php82.phpstub new file mode 100644 index 00000000000..fd153f3e5c0 --- /dev/null +++ b/stubs/Php82.phpstub @@ -0,0 +1,7 @@ +|interface-string|trait-string|enum-string $argument + * @psalm-pure */ public function __construct($argument) {} /** * @return class-string + * @psalm-pure */ public function getName(): string {} + + /** @psalm-pure */ + public function isInternal(): bool {} + + /** @psalm-pure */ + public function isUserDefined(): bool {} + + /** @psalm-pure */ + public function isInstantiable(): bool {} + + /** @psalm-pure */ + public function isCloneable(): bool {} + + /** + * @return non-empty-string|false + * @psalm-pure + */ + public function getFileName() {} + + /** + * @return positive-int|false + * @psalm-pure + */ + public function getStartLine() {} + + /** + * @return positive-int|false + * @psalm-pure + */ + public function getEndLine() {} + + /** + * @return non-empty-string|false + * @psalm-pure + */ + public function getDocComment() {} + + /** @psalm-pure */ + public function getConstructor(): ?ReflectionMethod {} + + /** @psalm-pure */ + public function hasMethod(string $name): bool {} + + /** + * @param non-empty-string $name + * + * @psalm-pure + * @throws ReflectionException + */ + public function getMethod(string $name): ReflectionMethod {} + + /** + * @param int-mask-of|null $filter + * @return list + * @psalm-pure + */ + public function getMethods(?int $filter = null): array {} + + /** + * @param non-empty-string $name + * + * @psalm-pure + * @throws ReflectionException + */ + public function hasProperty(string $name): bool {} + + /** + * @param non-empty-string $name + * + * @psalm-pure + * @throws ReflectionException + */ + public function getProperty(): ReflectionProperty {} + + /** + * @param int-mask-of|null $filter + * @return list + * + * @psalm-pure + */ + public function getProperties(?int $filter = null): array {} + + /** + * @param non-empty-string $name + * + * @psalm-pure + */ + public function hasConstant(string $name): bool {} + + /** + * @param non-empty-string $name + * @return mixed + * + * @psalm-pure + * @throws ReflectionException + */ + public function getConstant(string $name) {} + + /** + * @param non-empty-string $name + * @return ReflectionClassConstant|false + * + * @psalm-pure + * @throws ReflectionException + */ + public function getReflectionConstant(string $name) {} + + /** + * @param int-mask-of|null $filter + * @return array + * + * @psalm-pure + */ + public function getConstants(?int $filter = null): array {} + + /** + * @param int-mask-of|null $filter + * @return array + * + * @psalm-pure + */ + public function getReflectionConstants(?int $filter = null): array {} + + /** + * @return array + * + * @psalm-pure + */ + public function getInterfaces(): array {} + + /** + * @return list + * + * @psalm-pure + */ + public function getInterfaceNames(): array {} + + /** @psalm-pure */ + public function isInterface(): bool {} + + /** + * @return array + * + * @psalm-pure + */ + public function getTraits(): array {} + + /** + * @return list + * + * @psalm-pure + */ + public function getTraitAliases(): array {} + + /** @psalm-pure */ + public function isTrait(): bool {} + + /** @psalm-pure */ + public function isAbstract(): bool {} + + /** @psalm-pure */ + public function isFinal(): bool {} + + /** + * @return int-mask-of + * @psalm-pure + */ + public function getModifiers(): bool {} + + /** + * @template TCheckedObject of object + * + * @param TCheckedObject $object + * + * @return (TCheckedObject is T ? true : bool) + * + * @psalm-pure + */ + public function isInstance(object $object): bool {} /** * @param mixed ...$args @@ -41,9 +222,66 @@ class ReflectionClass implements Reflector { */ public function newInstanceWithoutConstructor(): object {} + /** @psalm-pure */ + public function getParentClass(): ?ReflectionClass {} + + /** + * @param ReflectionClass|class-string $class + * + * @psalm-pure + */ + public function isSubclassOf($class): bool {} + + /** @return array */ + public function getStaticProperties(): array {} + + /** + * @return array + * + * @psalm-pure + */ + public function getDefaultProperties(): array {} + + /** @psalm-pure */ + public function isIterateable(): bool {} + + /** @psalm-pure */ + public function isIterable(): bool {} + + /** + * @param self|class-string $interface + * + * @psalm-pure + */ + public function implementsInterface($interface): bool {} + + /** @psalm-pure */ + public function getExtension(): ?ReflectionExtension {} + + /** + * @return non-empty-string|false + * + * @psalm-pure + */ + public function getExtensionName() {} + + /** @psalm-pure */ + public function inNamespace(): bool {} + + /** @psalm-pure */ + public function getNamespaceName(): string {} + + /** + * @return non-empty-string + * + * @psalm-pure + */ + public function getShortName(): string {} + /** * @return ?array * @psalm-ignore-nullable-return + * @psalm-pure */ public function getTraitNames(): array {} @@ -51,13 +289,138 @@ class ReflectionClass implements Reflector { * @since 8.0 * @template TClass as object * @param class-string|null $name - * @return ($name is null ? array> : array>) + * @return ($name is null ? list> : list>) + * @psalm-pure */ public function getAttributes(?string $name = null, int $flags = 0): array {} } -class ReflectionFunction implements Reflector +abstract class ReflectionFunctionAbstract implements Reflector { + /** @psalm-pure */ + public function inNamespace(): bool {} + + /** @psalm-pure */ + public function isClosure(): bool {} + + /** @psalm-pure */ + public function isDeprecated(): bool {} + + /** @psalm-pure */ + public function isInternal(): bool {} + + /** @psalm-pure */ + public function isUserDefined(): bool {} + + public function getClosureThis(): ?object {} + + /** @psalm-pure */ + public function getClosureScopeClass(): ?ReflectionClass {} + + /** @psalm-pure */ + public function getClosureCalledClass(): ?ReflectionClass {} + + /** + * @return non-empty-string|false + * + * @psalm-pure + */ + public function getDocComment() {} + + /** + * @return positive-int|false + * + * @psalm-pure + */ + public function getStartLine() {} + + /** + * @return positive-int|false + * + * @psalm-pure + */ + public function getEndLine() {} + + /** @psalm-pure */ + public function getExtension(): ?ReflectionExtension {} + + /** + * @return non-empty-string + * + * @psalm-pure + */ + public function getExtensionName(): string {} + + /** + * @return non-empty-string|false + * + * @psalm-pure + */ + public function getFileName() {} + + /** + * @return non-empty-string + * + * @psalm-pure + */ + public function getName(): string {} + + /** @psalm-pure */ + public function getNamespaceName(): string {} + + /** + * @return positive-int|0 + * + * @psalm-pure + */ + public function getNumberOfParameters(): int {} + + /** + * @return positive-int|0 + * + * @psalm-pure + */ + public function getNumberOfRequiredParameters(): int {} + + /** + * @return list + * + * @psalm-pure + */ + public function getParameters(): array {} + + /** + * @psalm-assert-if-true ReflectionType $this->getReturnType() + * + * @psalm-pure + */ + public function hasReturnType(): bool {} + + /** @psalm-pure */ + public function getReturnType(): ?ReflectionType {} + + /** + * @return non-empty-string + * + * @psalm-pure + */ + public function getShortName(): string {} + + /** @psalm-pure */ + public function returnsReference(): bool {} + + /** @psalm-pure */ + public function isGenerator(): bool {} + + /** @psalm-pure */ + public function isVariadic(): bool {} + + /** @psalm-pure */ + public function isDisabled(): bool {} + + /** @psalm-pure */ + public function getClosure(): Closure {} + /** * @since 8.0 * @template TClass as object @@ -67,10 +430,23 @@ class ReflectionFunction implements Reflector public function getAttributes(?string $name = null, int $flags = 0): array {} } +class ReflectionFunction extends ReflectionFunctionAbstract +{ + /** @psalm-pure */ + public function __construct(callable $function) {} + + /** + * @return non-empty-string + * + * @psalm-pure + */ + public function __toString(): string {} +} + class ReflectionProperty implements Reflector { /** - * @var string + * @var non-empty-string * @readonly */ public $name; @@ -81,6 +457,13 @@ class ReflectionProperty implements Reflector */ public $class; + /** + * @return non-empty-string + * + * @psalm-pure + */ + public function getName(): string {} + /** * @since 8.0 * @template TClass as object @@ -92,32 +475,74 @@ class ReflectionProperty implements Reflector /** * @since 7.4 * @psalm-assert-if-true ReflectionType $this->getType() + * @psalm-pure */ public function hasType() : bool {} /** * @since 7.4 - * @psalm-mutation-free + * @psalm-pure */ public function getType() : ?ReflectionType {} + /** @psalm-pure */ + public function isPublic(): bool {} + + /** @psalm-pure */ + public function isPrivate(): bool {} + + /** @psalm-pure */ + public function isProtected(): bool {} + + /** @psalm-pure */ + public function isStatic(): bool {} + + /** @psalm-pure */ + public function isDefault(): bool {} + /** - * @since 8.0 - */ + * @return int-mask-of + * @psalm-pure + */ + public function getModifiers(): int {} + + /** @psalm-pure */ + public function getDeclaringClass(): ReflectionClass {} + + /** + * @return non-empty-string|false + * + * @psalm-pure + */ + public function getDocComment() {} + + /** + * @since 8.0 + * @psalm-pure + */ public function hasDefaultValue(): bool {} /** - * @since 8.0 - */ + * @return mixed + * + * @psalm-pure + */ + public function getDefaultValue() {} + + /** + * @since 8.0 + * @psalm-pure + */ public function isPromoted(): bool {} /** - * @since 8.1 - */ + * @since 8.1 + * @psalm-pure + */ public function isReadOnly(): bool {} } -class ReflectionMethod implements Reflector +class ReflectionMethod extends ReflectionFunctionAbstract { /** * @var string @@ -131,21 +556,33 @@ class ReflectionMethod implements Reflector */ public $class; + /** @psalm-pure */ + public function isStatic(): bool {} + + /** @psalm-pure */ + public function isConstructor(): bool {} + + /** @psalm-pure */ + public function isDestructor(): bool {} + /** - * @since 8.0 - * @template TClass as object - * @param class-string|null $name - * @return ($name is null ? array> : array>) + * @return int-mask-of + * @psalm-pure */ - public function getAttributes(?string $name = null, int $flags = 0): array {} + public function getModifiers(): bool {} - public function isStatic(): bool {} + /** @psalm-pure */ + public function getDeclaringClass(): ReflectionClass {} + + /** @psalm-pure */ + public function getPrototype(): ReflectionMethod {} } +/** @psalm-immutable */ class ReflectionClassConstant implements Reflector { /** - * @var string + * @var non-empty-string * @readonly */ public $name; @@ -170,17 +607,27 @@ class ReflectionClassConstant implements Reflector * @return ($name is null ? array> : array>) */ public function getAttributes(?string $name = null, int $flags = 0): array {} + + /** @return non-empty-string */ + public function getName(): string {} + + /** @return int-mask-of */ + public function getModifiers(): int {} + + /** @return non-empty-string|false */ + public function getDocComment() {} } -/** - * @psalm-immutable - */ +/** @psalm-immutable */ class ReflectionParameter implements Reflector { /** - * @var string + * @var non-empty-string * @readonly */ public $name; + + /** @return non-empty-string */ + public function getName(): string {} /** * @psalm-assert-if-true ReflectionType $this->getType() @@ -203,15 +650,22 @@ class ReflectionParameter implements Reflector { public function isPromoted(): bool {} } -/** - * @psalm-immutable - */ +/** @psalm-immutable */ +abstract class ReflectionType +{ +} + +/** @psalm-immutable */ class ReflectionNamedType extends ReflectionType { + /** @return non-empty-string */ public function getName(): string {} /** * @psalm-assert-if-false class-string|'self'|'static' $this->getName() */ public function isBuiltin(): bool {} + + /** @return non-empty-string */ + public function __toString(): string {} }