Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

For a full diff see [`2.13.1...main`][2.13.1...main].

### Added

- Added `Constants\NoAccessToConstantViaInstanceRule`, `Methods\NoAccessToStaticMethodViaInstanceRule`, and `Properties\NoAccessToStaticPropertyViaInstanceRule`, which report an error when a static constant, method, or property is accessed via an instance instead of a class name ([#1020]), by [@localheinz]

## [`2.13.1`][2.13.1]

For a full diff see [`2.13.0...2.13.1`][2.13.0...2.13.1].
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ This package provides the following rules for use with [`phpstan/phpstan`](https
- [`Ergebnis\PHPStan\Rules\Closures\NoParameterPassedByReferenceRule`](https://github.com/ergebnis/phpstan-rules#closuresnoparameterpassedbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Closures\NoParameterWithNullableTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#closuresnoparameterwithnullabletypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Closures\NoParameterWithNullDefaultValueRule`](https://github.com/ergebnis/phpstan-rules#closuresnoparameterwithnulldefaultvaluerule)
- [`Ergebnis\PHPStan\Rules\Constants\NoAccessToConstantViaInstanceRule`](https://github.com/ergebnis/phpstan-rules#constantsnoaccesstoconstantviainstancerule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoAssignByReferenceRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoassignbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoCompactRule`](https://github.com/ergebnis/phpstan-rules#expressionsnocompactrule)
- [`Ergebnis\PHPStan\Rules\Expressions\NoErrorSuppressionRule`](https://github.com/ergebnis/phpstan-rules#expressionsnoerrorsuppressionrule)
Expand All @@ -66,6 +67,7 @@ This package provides the following rules for use with [`phpstan/phpstan`](https
- [`Ergebnis\PHPStan\Rules\Functions\NoReturnByReferenceRule`](https://github.com/ergebnis/phpstan-rules#functionsnoreturnbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Methods\FinalInAbstractClassRule`](https://github.com/ergebnis/phpstan-rules#methodsfinalinabstractclassrule)
- [`Ergebnis\PHPStan\Rules\Methods\InvokeParentHookMethodRule`](https://github.com/ergebnis/phpstan-rules#methodsinvokeparenthookmethodrule)
- [`Ergebnis\PHPStan\Rules\Methods\NoAccessToStaticMethodViaInstanceRule`](https://github.com/ergebnis/phpstan-rules#methodsnoaccesstostaticmethodviainstancerule)
- [`Ergebnis\PHPStan\Rules\Methods\NoConstructorParameterWithDefaultValueRule`](https://github.com/ergebnis/phpstan-rules#methodsnoconstructorparameterwithdefaultvaluerule)
- [`Ergebnis\PHPStan\Rules\Methods\NoNullableReturnTypeDeclarationRule`](https://github.com/ergebnis/phpstan-rules#methodsnonullablereturntypedeclarationrule)
- [`Ergebnis\PHPStan\Rules\Methods\NoParameterPassedByReferenceRule`](https://github.com/ergebnis/phpstan-rules#methodsnoparameterpassedbyreferencerule)
Expand All @@ -74,6 +76,7 @@ This package provides the following rules for use with [`phpstan/phpstan`](https
- [`Ergebnis\PHPStan\Rules\Methods\NoParameterWithNullDefaultValueRule`](https://github.com/ergebnis/phpstan-rules#methodsnoparameterwithnulldefaultvaluerule)
- [`Ergebnis\PHPStan\Rules\Methods\NoReturnByReferenceRule`](https://github.com/ergebnis/phpstan-rules#methodsnoreturnbyreferencerule)
- [`Ergebnis\PHPStan\Rules\Methods\PrivateInFinalClassRule`](https://github.com/ergebnis/phpstan-rules#methodsprivateinfinalclassrule)
- [`Ergebnis\PHPStan\Rules\Properties\NoAccessToStaticPropertyViaInstanceRule`](https://github.com/ergebnis/phpstan-rules#propertiesnoaccesstostaticpropertyviainstancerule)
- [`Ergebnis\PHPStan\Rules\Statements\NoSwitchRule`](https://github.com/ergebnis/phpstan-rules#statementsnoswitchrule)


Expand Down Expand Up @@ -255,6 +258,25 @@ parameters:
enabled: false
```

### Constants

#### `Constants\NoAccessToConstantViaInstanceRule`

This rule reports an error when a class constant is accessed via an instance (e.g., `$foo::BAZ`) instead of via a class name (e.g., `Foo::BAZ`, `self::BAZ`, `static::BAZ`, or `parent::BAZ`).

:bulb: This rule does not report an error for `$foo::class`, which is an idiomatic way to get the runtime class of an object.

##### Disabling the rule

You can set the `enabled` parameter to `false` to disable this rule.

```neon
parameters:
ergebnis:
noAccessToStaticMemberViaInstance:
enabled: false
```

### Expressions

#### `Expressions\NoAssignByReferenceRule`
Expand Down Expand Up @@ -520,6 +542,21 @@ parameters:
- `hasContent`: one of `"yes"`, `"no"`, `"maybe"`
- `invocation`: one of `"any"` (needs to be invoked), `"first"` (needs to be invoked before all other statements in the overriding hook method, `"last"` (needs to be invoked after all other statements in the overriding hook method)

#### `Methods\NoAccessToStaticMethodViaInstanceRule`

This rule reports an error when a static method is called via an instance (e.g., `$foo::bar()`) instead of via a class name (e.g., `Foo::bar()`, `self::bar()`, `static::bar()`, or `parent::bar()`).

##### Disabling the rule

You can set the `enabled` parameter to `false` to disable this rule.

```neon
parameters:
ergebnis:
noAccessToStaticMemberViaInstance:
enabled: false
```

#### `Methods\NoConstructorParameterWithDefaultValueRule`

This rule reports an error when a constructor declared in
Expand Down Expand Up @@ -696,6 +733,23 @@ parameters:
enabled: false
```

### Properties

#### `Properties\NoAccessToStaticPropertyViaInstanceRule`

This rule reports an error when a static property is accessed via an instance (e.g., `$foo::$BAR`) instead of via a class name (e.g., `Foo::$BAR`, `self::$BAR`, `static::$BAR`, or `parent::$BAR`).

##### Disabling the rule

You can set the `enabled` parameter to `false` to disable this rule.

```neon
parameters:
ergebnis:
noAccessToStaticMemberViaInstance:
enabled: false
```

### Statements

#### `Statements\NoSwitchRule`
Expand Down
20 changes: 20 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ conditionalTags:
phpstan.rules.rule: %ergebnis.noEval.enabled%
Ergebnis\PHPStan\Rules\Expressions\NoIssetRule:
phpstan.rules.rule: %ergebnis.noIsset.enabled%
Ergebnis\PHPStan\Rules\Constants\NoAccessToConstantViaInstanceRule:
phpstan.rules.rule: %ergebnis.noAccessToStaticMemberViaInstance.enabled%
Ergebnis\PHPStan\Rules\Methods\NoAccessToStaticMethodViaInstanceRule:
phpstan.rules.rule: %ergebnis.noAccessToStaticMemberViaInstance.enabled%
Ergebnis\PHPStan\Rules\Properties\NoAccessToStaticPropertyViaInstanceRule:
phpstan.rules.rule: %ergebnis.noAccessToStaticMemberViaInstance.enabled%
Ergebnis\PHPStan\Rules\Files\DeclareStrictTypesRule:
phpstan.rules.rule: %ergebnis.declareStrictTypes.enabled%
Ergebnis\PHPStan\Rules\Files\NoPhpstanIgnoreRule:
Expand Down Expand Up @@ -108,6 +114,8 @@ parameters:
enabled: %ergebnis.allRules%
noReturnByReference:
enabled: %ergebnis.allRules%
noAccessToStaticMemberViaInstance:
enabled: %ergebnis.allRules%
noSwitch:
enabled: %ergebnis.allRules%
privateInFinalClass:
Expand Down Expand Up @@ -186,6 +194,9 @@ parametersSchema:
noReturnByReference: structure([
enabled: bool(),
])
noAccessToStaticMemberViaInstance: structure([
enabled: bool(),
])
noSwitch: structure([
enabled: bool(),
])
Expand Down Expand Up @@ -298,5 +309,14 @@ services:
-
class: Ergebnis\PHPStan\Rules\Methods\PrivateInFinalClassRule

-
class: Ergebnis\PHPStan\Rules\Constants\NoAccessToConstantViaInstanceRule

-
class: Ergebnis\PHPStan\Rules\Methods\NoAccessToStaticMethodViaInstanceRule

-
class: Ergebnis\PHPStan\Rules\Properties\NoAccessToStaticPropertyViaInstanceRule

-
class: Ergebnis\PHPStan\Rules\Statements\NoSwitchRule
95 changes: 95 additions & 0 deletions src/Constants/NoAccessToConstantViaInstanceRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/

namespace Ergebnis\PHPStan\Rules\Constants;

use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;

/**
* @implements Rules\Rule<Node\Expr\ClassConstFetch>
*/
final class NoAccessToConstantViaInstanceRule implements Rules\Rule
{
private Reflection\ReflectionProvider $reflectionProvider;

public function __construct(Reflection\ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}

public function getNodeType(): string
{
return Node\Expr\ClassConstFetch::class;
}

public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if ($node->class instanceof Node\Name) {
return [];
}

if (!$node->name instanceof Node\Identifier) {
return [];
}

if ('class' === $node->name->toLowerString()) {
return [];
}

$classNames = $scope->getType($node->class)->getObjectClassNames();

if ([] === $classNames) {
return [];
}

if (
!$node->class instanceof Node\Expr\Variable
|| 'this' !== $node->class->name
) {
if ($this->isAnonymousClass(...$classNames)) {
return [];
}
}

$message = \sprintf(
'Static constant %s should be accessed via class name, not via instance.',
$node->name->toString(),
);

return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noAccessToStaticMemberViaInstance()->toString())
->build(),
];
}

private function isAnonymousClass(string ...$classNames): bool
{
foreach ($classNames as $className) {
if (
$this->reflectionProvider->hasClass($className)
&& $this->reflectionProvider->getClass($className)->isAnonymous()
) {
return true;
}
}

return false;
}
}
5 changes: 5 additions & 0 deletions src/ErrorIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public static function noReturnByReference(): self
return new self('noReturnByReference');
}

public static function noAccessToStaticMemberViaInstance(): self
{
return new self('noAccessToStaticMemberViaInstance');
}

public static function noSwitch(): self
{
return new self('noSwitch');
Expand Down
91 changes: 91 additions & 0 deletions src/Methods/NoAccessToStaticMethodViaInstanceRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2018-2026 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/phpstan-rules
*/

namespace Ergebnis\PHPStan\Rules\Methods;

use Ergebnis\PHPStan\Rules\ErrorIdentifier;
use PhpParser\Node;
use PHPStan\Analyser;
use PHPStan\Reflection;
use PHPStan\Rules;

/**
* @implements Rules\Rule<Node\Expr\StaticCall>
*/
final class NoAccessToStaticMethodViaInstanceRule implements Rules\Rule
{
private Reflection\ReflectionProvider $reflectionProvider;

public function __construct(Reflection\ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}

public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}

public function processNode(
Node $node,
Analyser\Scope $scope
): array {
if ($node->class instanceof Node\Name) {
return [];
}

if (!$node->name instanceof Node\Identifier) {
return [];
}

$classNames = $scope->getType($node->class)->getObjectClassNames();

if ([] === $classNames) {
return [];
}

if (
!$node->class instanceof Node\Expr\Variable
|| 'this' !== $node->class->name
) {
if ($this->isAnonymousClass(...$classNames)) {
return [];
}
}

$message = \sprintf(
'Static method %s() should be called via class name, not via instance.',
$node->name->toString(),
);

return [
Rules\RuleErrorBuilder::message($message)
->identifier(ErrorIdentifier::noAccessToStaticMemberViaInstance()->toString())
->build(),
];
}

private function isAnonymousClass(string ...$classNames): bool
{
foreach ($classNames as $className) {
if (
$this->reflectionProvider->hasClass($className)
&& $this->reflectionProvider->getClass($className)->isAnonymous()
) {
return true;
}
}

return false;
}
}
Loading
Loading