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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ lint:
--exclude tests/PHPStan/Rules/Classes/data/implements-error.php \
--exclude tests/PHPStan/Rules/Classes/data/interface-extends-error.php \
--exclude tests/PHPStan/Rules/Classes/data/trait-use-error.php \
--exclude tests/PHPStan/Rules/Classes/data/nested-class-declaration.php \
--exclude tests/PHPStan/Rules/Methods/data/method-in-enum-without-body.php \
--exclude tests/PHPStan/Rules/Properties/data/default-value-for-native-property-type.php \
--exclude tests/PHPStan/Rules/Arrays/data/empty-array-item.php \
Expand Down
4 changes: 3 additions & 1 deletion src/Analyser/StmtHandler/ClassLikeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ public function processStmt(
$name = $stmt->namespacedName ?? $stmt->name;
$scope = $scope->invalidateExistenceCheckExpressions($existenceCheckFunctionNames, $name instanceof Name ? $name->toString() : null);

if (!$context->isTopLevel()) {
// PHP does not compile a class declared inside another class (NestedClassDeclarationRule),
// and entering it here would replace the class the scope is already in
if (!$context->isTopLevel() || (isset($stmt->namespacedName) && $scope->isInClass())) {
return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: [], dependencies: $declarationDependencies);
}
if (isset($stmt->namespacedName)) {
Expand Down
55 changes: 55 additions & 0 deletions src/Rules/Classes/NestedClassDeclarationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Classes;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function sprintf;

/**
* PHP does not compile a class, interface, trait or enum declared inside a class: a method body,
* or a closure in one. The analysis skips such a declaration, and this rule reports it.
*
* @implements Rule<Node\Stmt\ClassLike>
*/
#[RegisteredRule(level: 0)]
final class NestedClassDeclarationRule implements Rule
{

public function getNodeType(): string
{
return Node\Stmt\ClassLike::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!isset($node->namespacedName) || !$scope->isInClass()) {
return [];
}

if ($node instanceof Node\Stmt\Interface_) {
$kind = 'Interface';
} elseif ($node instanceof Node\Stmt\Trait_) {
$kind = 'Trait';
} elseif ($node instanceof Node\Stmt\Enum_) {
$kind = 'Enum';
} else {
$kind = 'Class';
}

return [
RuleErrorBuilder::message(sprintf(
'%s %s is declared in a method, but class declarations may not be nested.',
$kind,
$node->namespacedName->toString(),
))
->identifier('class.inner')
->nonIgnorable()
->build(),
];
}

}
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '28c29b8';
public const EXPECTED_EXTENSION_VERSION = '7d92742';

private static bool $active = false;

Expand Down
51 changes: 51 additions & 0 deletions tests/PHPStan/Rules/Classes/NestedClassDeclarationRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Classes;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<NestedClassDeclarationRule>
*/
class NestedClassDeclarationRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new NestedClassDeclarationRule();
}

#[RequiresPhp('>= 8.1.0')]
public function testRule(): void
{
$this->analyse([__DIR__ . '/data/nested-class-declaration.php'], [
[
'Class NestedClassDeclaration\NestedClass is declared in a method, but class declarations may not be nested.',
10,
],
[
'Interface NestedClassDeclaration\NestedInterface is declared in a method, but class declarations may not be nested.',
14,
],
[
'Trait NestedClassDeclaration\NestedTrait is declared in a method, but class declarations may not be nested.',
18,
],
[
'Enum NestedClassDeclaration\NestedEnum is declared in a method, but class declarations may not be nested.',
22,
],
[
'Class NestedClassDeclaration\InClosure is declared in a method, but class declarations may not be nested.',
27,
],
[
'Class NestedClassDeclaration\InTraitMethod is declared in a method, but class declarations may not be nested.',
62,
],
]);
}

}
74 changes: 74 additions & 0 deletions tests/PHPStan/Rules/Classes/data/nested-class-declaration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php // lint >= 8.1

namespace NestedClassDeclaration;

class Foo
{

public function doFoo(): void
{
class NestedClass
{
}

interface NestedInterface
{
}

trait NestedTrait
{
}

enum NestedEnum
{
}

$f = function (): void {
class InClosure
{
}
};

$anonymous = new class {

public function doBar(): int
{
return 1;
}

};
}

}

function doBaz(): void
{
class InFunction
{
}

$f = function (): void {
class InClosureInFunction
{
}
};
}

trait DeclaresInMethod
{

public function doTrait(): void
{
class InTraitMethod
{
}
}

}

class UsesTheTrait
{

use DeclaresInMethod;

}
9 changes: 9 additions & 0 deletions turbo-ext/src/ClassLikeHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,15 @@ class ClassLikeHandler
zv::Val classReflection;
zval *namespacedName = rawNodeProperty(pt_clh_namespaced_name_site, stmt, PT_LC("namespacedName"));
if (namespacedName != NULL) ZVAL_DEREF(namespacedName);
/* PHP does not compile a class declared inside another class, and entering it would
* replace the class the scope is already in - the PHP twin's isset() && isInClass() */
if (namespacedName != NULL && Z_TYPE_P(namespacedName) != IS_UNDEF && Z_TYPE_P(namespacedName) != IS_NULL) {
bool isInClass;
if (UNEXPECTED(!pt_scope_is_in_class(Z_OBJ_P(resultScope), isInClass))) return zv::Val();
if (isInClass) {
return pt_internal_statement_result_new(resultScope, false, false, &emptyArray, &emptyArray, &emptyArray, NULL, NULL, -1, declarationDependencies.raw());
}
}
if (namespacedName != NULL && Z_TYPE_P(namespacedName) != IS_UNDEF && Z_TYPE_P(namespacedName) != IS_NULL) {
if (UNEXPECTED(Z_TYPE_P(namespacedName) != IS_OBJECT)) {
memberCallOnNonObject("toString", namespacedName);
Expand Down
Loading