Skip to content
Draft
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
16 changes: 11 additions & 5 deletions src/Resolution/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@

/**
* The lexical scope at a point in a document: the one answer to what encloses
* it — namespace, class-like, and innermost function-like — and what follows
* from that.
* it — namespace, class-like, and the innermost function-like inside that
* class-like — and what follows from that.
*
* A scope provides everything variable resolution needs: the parameters and
* statements that can introduce variables, the class context for self/parent,
Expand Down Expand Up @@ -62,9 +62,12 @@
) {
$classLike ??= $n;
}
// A function-like around the class-like is not this scope's: a class
// body sees nothing of the code that declares the class.
if (
$n instanceof Stmt\Function_ || $n instanceof Stmt\ClassMethod
|| $n instanceof Closure || $n instanceof ArrowFunction || $n instanceof PropertyHook
$classLike === null
&& ($n instanceof Stmt\Function_ || $n instanceof Stmt\ClassMethod
|| $n instanceof Closure || $n instanceof ArrowFunction || $n instanceof PropertyHook)
) {
$function ??= $n;
}
Expand All @@ -73,7 +76,8 @@
}

/**
* The scope holding this one's function-like, or null for a global scope.
* The scope holding this one's function-like, or null for a global scope or
* a class body.
*/
public function outer(): ?self
{
Expand All @@ -99,7 +103,9 @@
*/
public function getStatements(): array
{
return match (true) {

Check warning on line 106 in src/Resolution/Scope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing

Escaped Mutant for Mutator "MatchArmRemoval": @@ @@ public function getStatements(): array { return match (true) { - // A class body holds declarations, whose values cannot read a variable. - $this->sourceNode === null && $this->classLike !== null => [], $this->sourceNode === null => $this->namespace->stmts ?? $this->tree, // Arrow functions have an expression body, not a statement list; their // captured variables come from the enclosing scope, not local assignments.
// A class body holds declarations, whose values cannot read a variable.
$this->sourceNode === null && $this->classLike !== null => [],
$this->sourceNode === null => $this->namespace->stmts ?? $this->tree,
// Arrow functions have an expression body, not a statement list; their
// captured variables come from the enclosing scope, not local assignments.
Expand Down
12 changes: 12 additions & 0 deletions tests/Fixtures/TopLevel/this_binding.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public function instanceMethod(): void
$inStaticClosure = 1;
};
$staticArrow = static fn () => $inStaticArrow = 1;
// A class body sees neither the method's variables nor its `$this`.
$anonymous = new class {
public int $inAnonymousClassBody = 1;
};
}

public static function staticMethod(\DateTime $param): void
Expand Down Expand Up @@ -70,3 +74,11 @@ function freeFunction(): void
$inClosureInFunction = 1;
};
}

$beforeTopLevelClass = 1;

// A class body sees none of the file's variables.
class AfterTopLevelCode
{
public int $inTopLevelClassBody = 1;
}
29 changes: 29 additions & 0 deletions tests/Resolution/VariableBindingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PhpParser\Node\Stmt;
use PhpParser\NodeFinder;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(VariableBindings::class)]
Expand Down Expand Up @@ -178,6 +179,34 @@ public function testThisIsBoundWherePhpBindsIt(): void
);
}

#[DataProvider('classBodyProperties')]
public function testAClassBodyBindsNoVariables(string $propertyName): void
{
$ast = self::parseWithParents($this->loadFixture('TopLevel/this_binding.php'));
$property = self::findFirst(
$ast,
fn (Node $n) => $n instanceof Node\PropertyItem && $n->name->toString() === $propertyName,
);
self::assertInstanceOf(Node\PropertyItem::class, $property);

self::assertSame(
[],
array_keys(VariableBindings::visible(Scope::at($property, $ast), $property->getEndFilePos())),
'a property default sees no variables and no $this from what holds the class',
);
}

/**
* @return array<string, array{string}>
*/
public static function classBodyProperties(): array
{
return [
'an anonymous class in an instance method' => ['inAnonymousClassBody'],
'a class after top-level code' => ['inTopLevelClassBody'],
];
}

public function testCodeNeverBindsThis(): void
{
$ast = self::parseWithParents($this->loadFixture('TopLevel/this_binding.php'));
Expand Down
Loading