Skip to content

Commit

Permalink
Merge branch 'master' into php-ext-with-deprecation
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/Psalm/Config.php
  • Loading branch information
alies-dev committed Dec 12, 2022
2 parents c8b6b0b + 72e7386 commit 1115cfb
Show file tree
Hide file tree
Showing 15 changed files with 735 additions and 48 deletions.
1 change: 1 addition & 0 deletions config.xsd
Expand Up @@ -264,6 +264,7 @@
<xs:element name="InvalidDocblockParamName" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidEnumBackingType" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidEnumCaseValue" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidEnumMethod" type="MethodIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidExtendClass" type="ClassIssueHandlerType" minOccurs="0" />
<xs:element name="InvalidFalsableReturnType" type="IssueHandlerType" minOccurs="0" />
<xs:element name="InvalidFunctionCall" type="IssueHandlerType" minOccurs="0" />
Expand Down
1 change: 1 addition & 0 deletions docs/running_psalm/error_levels.md
Expand Up @@ -45,6 +45,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even
- [InaccessibleProperty](issues/InaccessibleProperty.md)
- [InterfaceInstantiation](issues/InterfaceInstantiation.md)
- [InvalidAttribute](issues/InvalidAttribute.md)
- [InvalidEnumMethod](issues/InvalidEnumMethod.md)
- [InvalidExtendClass](issues/InvalidExtendClass.md)
- [InvalidGlobal](issues/InvalidGlobal.md)
- [InvalidParamDefault](issues/InvalidParamDefault.md)
Expand Down
1 change: 1 addition & 0 deletions docs/running_psalm/issues.md
Expand Up @@ -69,6 +69,7 @@
- [InvalidDocblockParamName](issues/InvalidDocblockParamName.md)
- [InvalidEnumBackingType](issues/InvalidEnumBackingType.md)
- [InvalidEnumCaseValue](issues/InvalidEnumCaseValue.md)
- [InvalidEnumMethod](issues/InvalidEnumMethod.md)
- [InvalidExtendClass](issues/InvalidExtendClass.md)
- [InvalidFalsableReturnType](issues/InvalidFalsableReturnType.md)
- [InvalidFunctionCall](issues/InvalidFunctionCall.md)
Expand Down
15 changes: 15 additions & 0 deletions docs/running_psalm/issues/InvalidEnumMethod.md
@@ -0,0 +1,15 @@
# InvalidEnumMethod

Enums may not define most of the magic methods like `__get`, `__toString`, etc.

```php
<?php
enum Status: string {
case Open = 'open';
case Closed = 'closed';

public function __toString(): string {
return "SomeStatus";
}
}
```
19 changes: 17 additions & 2 deletions src/Psalm/Config.php
Expand Up @@ -2077,6 +2077,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) {
Expand Down Expand Up @@ -2117,16 +2127,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;
}

$ext_stubs_dir = $dir_lvl_2 . DIRECTORY_SEPARATOR . "stubs" . DIRECTORY_SEPARATOR . "extensions";
foreach ($this->php_extensions as $ext => $enabled) {
if ($enabled) {
Expand Down
4 changes: 4 additions & 0 deletions src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
Expand Up @@ -2020,6 +2020,10 @@ private function getFunctionInformation(

MethodAnalyzer::checkMethodSignatureMustOmitReturnType($storage, $codeLocation);

if ($appearing_class_storage->is_enum) {
MethodAnalyzer::checkForbiddenEnumMethod($storage);
}

if (!$context->calling_method_id || !$context->collect_initializations) {
$context->calling_method_id = strtolower((string)$method_id);
}
Expand Down
45 changes: 42 additions & 3 deletions src/Psalm/Internal/Analyzer/MethodAnalyzer.php
Expand Up @@ -9,6 +9,7 @@
use Psalm\Context;
use Psalm\Internal\Codebase\InternalCallMapHandler;
use Psalm\Internal\MethodIdentifier;
use Psalm\Issue\InvalidEnumMethod;
use Psalm\Issue\InvalidStaticInvocation;
use Psalm\Issue\MethodSignatureMustOmitReturnType;
use Psalm\Issue\NonStaticSelfCall;
Expand All @@ -27,6 +28,24 @@
*/
class MethodAnalyzer extends FunctionLikeAnalyzer
{
// https://github.com/php/php-src/blob/a83923044c48982c80804ae1b45e761c271966d3/Zend/zend_enum.c#L77-L95
private const FORBIDDEN_ENUM_METHODS = [
'__construct',
'__destruct',
'__clone',
'__get',
'__set',
'__unset',
'__isset',
'__tostring',
'__debuginfo',
'__serialize',
'__unserialize',
'__sleep',
'__wakeup',
'__set_state',
];

/** @psalm-external-mutation-free */
public function __construct(
PhpParser\Node\Stmt\ClassMethod $function,
Expand Down Expand Up @@ -266,13 +285,17 @@ public static function checkMethodSignatureMustOmitReturnType(
return;
}

$cased_method_name = $method_storage->cased_name;
if ($method_storage->cased_name === null) {
return;
}

$method_name_lc = strtolower($method_storage->cased_name);
$methodsOfInterest = ['__clone', '__construct', '__destruct'];

if (in_array($cased_method_name, $methodsOfInterest)) {
if (in_array($method_name_lc, $methodsOfInterest, true)) {
IssueBuffer::maybeAdd(
new MethodSignatureMustOmitReturnType(
'Method ' . $cased_method_name . ' must not declare a return type',
'Method ' . $method_storage->cased_name . ' must not declare a return type',
$code_location
)
);
Expand All @@ -288,4 +311,20 @@ public function getMethodId(?string $context_self = null): MethodIdentifier
strtolower($function_name)
);
}

public static function checkForbiddenEnumMethod(MethodStorage $method_storage): void
{
if ($method_storage->cased_name === null || $method_storage->location === null) {
return;
}

$method_name_lc = strtolower($method_storage->cased_name);
if (in_array($method_name_lc, self::FORBIDDEN_ENUM_METHODS, true)) {
IssueBuffer::maybeAdd(new InvalidEnumMethod(
'Enums cannot define ' . $method_storage->cased_name,
$method_storage->location,
$method_storage->defining_fqcln . '::' . $method_storage->cased_name
));
}
}
}
9 changes: 9 additions & 0 deletions src/Psalm/Issue/InvalidEnumMethod.php
@@ -0,0 +1,9 @@
<?php

namespace Psalm\Issue;

final class InvalidEnumMethod extends MethodIssue
{
public const ERROR_LEVEL = -1;
public const SHORTCODE = 314;
}
113 changes: 110 additions & 3 deletions stubs/Php80.phpstub
Expand Up @@ -17,14 +17,24 @@ class ReflectionAttribute
{
}

/**
* @return non-empty-string
*
* @psalm-pure
*/
public function getName() : string
{
}

/**
* @psalm-pure
* @return int-mask-of<Attribute::TARGET_*>
*/
public function getTarget() : int
{
}

/** @psalm-pure */
public function isRepeated() : bool
{
}
Expand All @@ -48,13 +58,101 @@ class ReflectionAttribute
}
}

/**
* @template-covariant T as object
*
* @property-read class-string<T> $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;
Expand All @@ -76,11 +174,20 @@ class Attribute
}
}

class ReflectionUnionType extends ReflectionType {
class ReflectionProperty implements Reflector
{
/**
* @return non-empty-list<ReflectionNamedType>
* @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<ReflectionNamedType> */
public function getTypes(): array {}
}

class UnhandledMatchError extends Error {}
Expand Down

0 comments on commit 1115cfb

Please sign in to comment.