Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ohader committed Sep 10, 2022
0 parents commit 4f098c1
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Psalm Static Plugin

This plugin helps to resolve `static::class`, `self::class` and `paren::class` statements.
21 changes: 21 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "h4ck3r31/psalm-static-plugin",
"description": "Resolves static::class, self:class or parent::class references within PsalmPHP",
"type": "psalm-plugin",
"license": "MIT",
"autoload": {
"psr-4": {
"H4ck3r31\\PsalmStaticPlugin\\": "src/"
}
},
"authors": [
{
"name": "Oliver Hader",
"email": "[email protected]"
}
],
"require": {
"ext-simplexml": "*",
"vimeo/psalm": "dev-master"
}
}
17 changes: 17 additions & 0 deletions src/Plugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);

namespace H4ck3r31\PsalmStaticPlugin;

use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use SimpleXMLElement;

class Plugin implements PluginEntryPointInterface
{
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
{
class_exists(SpecialClassNameAnalysisHandler::class);
$registration->registerHooksFromClass(SpecialClassNameAnalysisHandler::class);
}
}
21 changes: 21 additions & 0 deletions src/SpecialClassNameAnalysisHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);

namespace H4ck3r31\PsalmStaticPlugin;

use PhpParser\NodeTraverser;
use Psalm\Plugin\EventHandler\BeforeFileAnalysisInterface;
use Psalm\Plugin\EventHandler\Event\BeforeFileAnalysisEvent;

class SpecialClassNameAnalysisHandler implements BeforeFileAnalysisInterface
{
public static function beforeAnalyzeFile(BeforeFileAnalysisEvent $event): void
{
$filePath = $event->getStatementsSource()->getFilePath();
$statements = $event->getCodebase()->getStatementsForFile($filePath);
// resolves special class names (`static::class`, `self::class`, `parent::class`)
$traverser = new NodeTraverser();
$traverser->addVisitor(new SpecialClassNameVisitor());
$traverser->traverse($statements);
}
}
79 changes: 79 additions & 0 deletions src/SpecialClassNameVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);

namespace H4ck3r31\PsalmStaticPlugin;

use PhpParser\ErrorHandler\Throwing;
use PhpParser\NameContext;
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\NodeVisitorAbstract;

/**
* Resolves and updates `resolvedName` node attribute for special class constants
* `self::class`, `static::class` and `parent::class`
*/
class SpecialClassNameVisitor extends NodeVisitorAbstract
{
private NameContext $nameContext;
private ?Class_ $classContext = null;

public function __construct()
{
$this->nameContext = new NameContext(new Throwing());
}

public function beforeTraverse(array $nodes): ?array
{
$this->nameContext->startNamespace();
return null;
}

public function enterNode(Node $node): ?Node
{
if ($node instanceof Namespace_) {
$this->nameContext->startNamespace($node->name);
} elseif ($node instanceof Class_ && $node->name !== null) {
$this->classContext = $node;
$this->addNamespacedName($node);
} elseif ($node instanceof ClassConstFetch
&& $this->classContext !== null
&& $node->class instanceof Name
&& $node->class->isSpecialClassName()
) {
$resoledName = null;
$currentName = $node->class->getAttribute('resolvedName')
?? $node->class->toString();
if ($currentName === 'parent') {
$resoledName = $this->classContext->extends->getAttribute('resolvedName')
?? $this->classContext->extends->toString();
} elseif ($currentName === 'self' || $currentName === 'static') {
$resoledName = $this->classContext->namespacedName->toString();
}
if ($resoledName !== null) {
$node->class->setAttribute('resolvedName', $resoledName);
}
}
return null;
}

public function leaveNode(Node $node): ?Node
{
if ($node instanceof Class_) {
$this->classContext = null;
}
return null;
}

private function addNamespacedName(ClassLike $node): void {
if ($node->namespacedName !== null) {
return;
}
$node->namespacedName = Name::concat(
$this->nameContext->getNamespace(), (string) $node->name);
}
}

0 comments on commit 4f098c1

Please sign in to comment.