diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index ee172a4300a..a8a5da7101a 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -2085,6 +2085,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) { @@ -2125,16 +2135,21 @@ public function visitStubFiles(Codebase $codebase, ?Progress $progress = null): $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'SPL.phpstub', ]; - if (PHP_VERSION_ID >= 8_00_00 && $codebase->analysis_php_version_id >= 8_00_00) { + if ($codebase->analysis_php_version_id >= 8_00_00) { $stringable_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'Php80.phpstub'; $this->internal_stubs[] = $stringable_path; } - if (PHP_VERSION_ID >= 8_01_00 && $codebase->analysis_php_version_id >= 8_01_00) { + if ($codebase->analysis_php_version_id >= 8_01_00) { $stringable_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'Php81.phpstub'; $this->internal_stubs[] = $stringable_path; } + if ($codebase->analysis_php_version_id >= 8_02_00) { + $stringable_path = $dir_lvl_2 . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . 'Php82.phpstub'; + $this->internal_stubs[] = $stringable_path; + } + foreach ($this->php_extensions as $ext => $enabled) { if ($enabled) { $this->internal_stubs[] = $dir_lvl_2 . DIRECTORY_SEPARATOR . "stubs" 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..06d6d0debc3 100644 --- a/stubs/Php81.phpstub +++ b/stubs/Php81.phpstub @@ -26,6 +26,32 @@ namespace { public static function tryFrom(string|int $value): ?static; } + class ReflectionClass implements Reflector { + /** @psalm-pure */ + public function isEnum(): bool {} + } + + class ReflectionProperty implements Reflector + { + /** + * Starting from PHP 8.1, this method is pure, and has no effect. + * + * @psalm-pure + */ + public function setAccessible(bool $accessible): void {} + } + + class ReflectionMethod extends ReflectionFunctionAbstract + { + /** + * Starting from PHP 8.1, this method is pure, and has no effect. + * + * @psalm-pure + */ + public function setAccessible(bool $accessible): void {} + } + + /** @psalm-immutable */ class ReflectionEnum extends ReflectionClass implements Reflector { public function getBackingType(): ?ReflectionType; @@ -36,32 +62,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..5ac5ee5f316 --- /dev/null +++ b/stubs/Php82.phpstub @@ -0,0 +1,13 @@ + */ + public function getTypes(): array {} + } +} diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 6449fa5d396..4d464ebc707 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -14,13 +14,186 @@ class ReflectionClass implements Reflector { /** * @param T|class-string|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 {} + + /** @psalm-pure */ + public function isInstance(object $object): bool {} /** * @param mixed ...$args @@ -41,9 +214,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 +281,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,6 +422,23 @@ class ReflectionFunction implements Reflector public function getAttributes(?string $name = null, int $flags = 0): array {} } +class ReflectionFunction extends ReflectionFunctionAbstract +{ + /** + * @param callable-string|Closure $function + * + * @psalm-pure + */ + public function __construct(callable $function) {} + + /** + * @return non-empty-string + * + * @psalm-pure + */ + public function __toString(): string {} +} + class ReflectionProperty implements Reflector { /** @@ -81,6 +453,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 +471,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 +552,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 +603,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 +646,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 {} } diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 717a61a6ed7..0544fd1b0df 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -125,7 +125,7 @@ function foo(string $s) : void { $b = $a->getAttributes(); ', 'assertions' => [ - '$b' => 'array>', + '$b' => 'list>', ], 'ignored_issues' => [], 'php_version' => '8.0'