1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 12:24:49 +01:00

Plugin interface segregation (#1076)

* Split Plugin into PluginApi\Hook\* interfaces

* dropped Psalm\Plugin

* updated docs

* s/PluginApi/Plugin/g
This commit is contained in:
Bruce Weirdan 2018-11-12 17:11:08 +02:00 committed by Matthew Brown
parent 8b0f2579a5
commit a338e76ef6
24 changed files with 209 additions and 214 deletions

View File

@ -23,12 +23,6 @@
<MissingConstructor errorLevel="suppress" />
<DeprecatedProperty errorLevel="suppress" />
<LessSpecificReturnType>
<errorLevel type="suppress">
<file name="src/Psalm/Plugin.php" />
</errorLevel>
</LessSpecificReturnType>
<UnusedProperty>
<errorLevel type="info">
<file name="src/Psalm/FileManipulation/FunctionDocblockManipulator.php" />
@ -44,7 +38,6 @@
<PossiblyUnusedMethod>
<errorLevel type="suppress">
<file name="src/Psalm/Type/Atomic/GenericTrait.php" />
<file name="src/Psalm/Plugin.php" />
</errorLevel>
<errorLevel type="info">
<file name="src/Psalm/Codebase.php" />

View File

@ -28,7 +28,7 @@
"bin": ["psalm", "psalter", "psalm-language-server"],
"autoload": {
"psr-4": {
"Psalm\\PluginApi\\": "src/Psalm/PluginApi",
"Psalm\\Plugin\\": "src/Psalm/Plugin",
"Psalm\\": "src/Psalm"
}
},

View File

@ -2,24 +2,25 @@
Psalm can be extended through plugins to find domain-specific issues.
All plugins must extend `Psalm\Plugin`
Plugins may implement one of (or more than one of) `Psalm\Plugin\Hook\*` interface(s).
```php
<?php
class SomePlugin extends \Psalm\Plugin
class SomePlugin implements \Psalm\Plugin\Hook\AfterStatementAnalysisInterface
{
}
```
`Psalm\Plugin` offers six methods that you can override:
- `afterStatementAnalysis` - called after Psalm evaluates each statement
- `afterExpressionAnalysis` - called after Psalm evaluates each expression
- `afterClassLikeVisit` - called after Psalm crawls the parsed Abstract Syntax Tree for a class-like (class, interface, trait). Due to caching the AST is crawled the first time Psalm sees the file, and is only re-crawled if the file changes, the cache is cleared, or you're disabling cache with `--no-cache`
- `afterClassLikeExistenceCheck` - called after Psalm analyzes a reference to a class-like
- `afterMethodCallAnalysis` - called after Psalm analyzes a method call
- `afterFunctionCallAnalysis` - called after Psalm analyzes a function call
`Psalm\Plugin\Hook\*` offers six interfaces that you can implement:
An example plugin that checks class references in strings is provided [here](https://github.com/vimeo/psalm/blob/master/examples/StringChecker.php).
- `AfterStatementAnalysisInterface` - called after Psalm evaluates each statement
- `AfterExpressionAnalysisInterface` - called after Psalm evaluates each expression
- `AfterClassLikeVisitInterface` - called after Psalm crawls the parsed Abstract Syntax Tree for a class-like (class, interface, trait). Due to caching the AST is crawled the first time Psalm sees the file, and is only re-crawled if the file changes, the cache is cleared, or you're disabling cache with `--no-cache`
- `AfterClassLikeExistenceCheckInterface` - called after Psalm analyzes a reference to a class-like
- `AfterMethodCallAnalysisInterface` - called after Psalm analyzes a method call
- `AfterFunctionCallAnalysisInterface` - called after Psalm analyzes a function call
An example plugin that checks class references in strings is provided [here](https://github.com/vimeo/psalm/blob/master/examples/plugins/StringChecker.php).
To ensure your plugin runs when Psalm does, add it to your [config](Configuration):
```php
@ -55,7 +56,7 @@ Composer-based plugin is a composer package which conforms to these requirements
1. Its `type` field is set to `psalm-plugin`
2. It has `extra.psalm.pluginClass` subkey in its `composer.json` that reference an entry-point class that will be invoked to register the plugin into Psalm runtime.
3. Entry-point class implements `Psalm\PluginApi\PluginEntryPointInterface`
3. Entry-point class implements `Psalm\Plugin\PluginEntryPointInterface`
### Using skeleton project
@ -63,10 +64,10 @@ Run `composer create-project weirdan/psalm-plugin-skeleton:dev-master your-plugi
### Upgrading file-based plugin to composer-based version
Create new plugin project using skeleton, then pass the class name of you file-based plugin to `registerHooksFromClass()` method of the `Psalm\PluginApi\RegistrationInterface` instance that was passed into your plugin entry point's `__invoke()` method. See the [conversion example](https://github.com/vimeo/psalm/examples/composer-based/echo-checker/).
Create new plugin project using skeleton, then pass the class name of you file-based plugin to `registerHooksFromClass()` method of the `Psalm\Plugin\RegistrationInterface` instance that was passed into your plugin entry point's `__invoke()` method. See the [conversion example](https://github.com/vimeo/psalm/examples/plugins/composer-based/echo-checker/).
### Registering stub files
Use `Psalm\PluginApi\RegistrationInterface::addStubFile()`. See the [sample plugin] (https://github.com/weirdan/psalm-doctrine-collections/).
Use `Psalm\Plugin\RegistrationInterface::addStubFile()`. See the [sample plugin] (https://github.com/weirdan/psalm-doctrine-collections/).
Stub files provide a way to override third-party type information when you cannot add Psalm's extended docblocks to the upstream source files directly.

View File

@ -5,10 +5,11 @@ use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\FileManipulation\FileManipulation;
use Psalm\FileSource;
use Psalm\Plugin\Hook\AfterClassLikeExistenceCheckInterface;
use Psalm\StatementsSource;
use Psalm\Type;
class ClassUnqualifier extends \Psalm\Plugin
class ClassUnqualifier implements AfterClassLikeExistenceCheckInterface
{
/**
* @param string $fq_class_name

View File

@ -10,9 +10,10 @@ use Psalm\Context;
use Psalm\FileManipulation\FileManipulation;
use Psalm\IssueBuffer;
use Psalm\Issue\TypeCoercion;
use Psalm\Plugin\Hook\AfterExpressionAnalysisInterface;
use Psalm\StatementsSource;
class StringChecker extends \Psalm\Plugin
class StringChecker implements AfterExpressionAnalysisInterface
{
/**
* Called after an expression has been checked

View File

@ -8,9 +8,10 @@ use Psalm\Context;
use Psalm\FileManipulation\FileManipulation;
use Psalm\IssueBuffer;
use Psalm\Issue\TypeCoercion;
use Psalm\Plugin\Hook\AfterStatementAnalysisInterface;
use Psalm\StatementsSource;
class EchoChecker extends \Psalm\Plugin
class EchoChecker implements AfterStatementAnalysisInterface
{
/**
* Called after a statement has been checked

View File

@ -1,13 +1,13 @@
<?php
namespace Psalm\Example\Plugin\ComposerBased;
use Psalm\PluginApi;
use Psalm\Plugin;
use SimpleXMLElement;
class PluginEntryPoint implements PluginApi\PluginEntryPointInterface
class PluginEntryPoint implements Plugin\PluginEntryPointInterface
{
/** @return void */
public function __invoke(PluginApi\RegistrationInterface $registration, ?SimpleXMLElement $config = null)
public function __invoke(Plugin\RegistrationInterface $registration, ?SimpleXMLElement $config = null)
{
require_once __DIR__ . '/EchoChecker.php';
$registration->registerHooksFromClass(EchoChecker::class);

View File

@ -50,22 +50,14 @@
</MissingConstructor>
<DeprecatedProperty errorLevel="suppress" />
<LessSpecificReturnType>
<errorLevel type="suppress">
<file name="src/Psalm/Plugin.php" />
</errorLevel>
</LessSpecificReturnType>
<UnusedParam>
<errorLevel type="suppress">
<file name="src/Psalm/Plugin.php" />
<directory name="examples" />
</errorLevel>
</UnusedParam>
<PossiblyUnusedParam>
<errorLevel type="suppress">
<file name="src/Psalm/Plugin.php" />
<directory name="examples" />
</errorLevel>
</PossiblyUnusedParam>
@ -96,8 +88,7 @@
<PossiblyUnusedMethod>
<errorLevel type="suppress">
<directory name="tests" />
<directory name="src/Psalm/PluginApi" />
<file name="src/Psalm/Plugin.php" />
<directory name="src/Psalm/Plugin" />
<file name="src/Psalm/Internal/LanguageServer/Client/TextDocument.php" />
<file name="src/Psalm/Internal/LanguageServer/Server/TextDocument.php" />
<referencedMethod name="Psalm\Codebase::getParentInterfaces" />

View File

@ -116,8 +116,8 @@ return [
},
function ($filePath, $prefix, $contents) {
$ret = str_replace(
$prefix . '\Psalm\PluginApi',
'Psalm\PluginApi',
$prefix . '\Psalm\Plugin\\',
'Psalm\Plugin\\',
$contents
);
return $ret;
@ -125,6 +125,6 @@ return [
],
'whitelist' => [
\Composer\Autoload\ClassLoader::class,
'Psalm\PluginApi\*',
'Psalm\Plugin\*',
]
];

View File

@ -810,7 +810,7 @@ class Config
$plugin_class_name = $plugin_class_entry['class'];
$plugin_config = $plugin_class_entry['config'];
try {
/** @var PluginApi\PluginEntryPointInterface $plugin_object */
/** @var Plugin\PluginEntryPointInterface $plugin_object */
$plugin_object = new $plugin_class_name;
$plugin_object($socket, $plugin_config);
} catch (\Throwable $e) {

View File

@ -4,10 +4,9 @@ namespace Psalm;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Scanner\FileScanner;
use Psalm\PluginApi;
use SimpleXMLElement;
class FileBasedPluginAdapter implements PluginApi\PluginEntryPointInterface
class FileBasedPluginAdapter implements Plugin\PluginEntryPointInterface
{
/** @var string */
private $path;
@ -26,9 +25,9 @@ class FileBasedPluginAdapter implements PluginApi\PluginEntryPointInterface
}
/** @return void */
public function __invoke(PluginApi\RegistrationInterface $registration, SimpleXMLElement $config = null)
public function __invoke(Plugin\RegistrationInterface $registration, SimpleXMLElement $config = null)
{
$fq_class_name = $this->getPluginClassForPath($this->path, Plugin::class);
$fq_class_name = $this->getPluginClassForPath($this->path);
/** @psalm-suppress UnresolvableInclude */
require_once($this->path);
@ -36,7 +35,7 @@ class FileBasedPluginAdapter implements PluginApi\PluginEntryPointInterface
$registration->registerHooksFromClass($fq_class_name);
}
private function getPluginClassForPath(string $path, string $must_extend): string
private function getPluginClassForPath(string $path): string
{
$codebase = $this->codebase;
@ -58,16 +57,6 @@ class FileBasedPluginAdapter implements PluginApi\PluginEntryPointInterface
$fq_class_name = reset($declared_classes);
if (!$codebase->classExtends(
$fq_class_name,
$must_extend
)
) {
throw new \InvalidArgumentException(
'This plugin must extend ' . $must_extend . ' - ' . $path . ' does not'
);
}
return $fq_class_name;
}
}

View File

@ -1,120 +0,0 @@
<?php
namespace Psalm;
use PhpParser;
use Psalm\FileManipulation\FileManipulation;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Type\Union;
abstract class Plugin
{
/**
* Called after an expression has been checked
*
* @param PhpParser\Node\Expr $expr
* @param Context $context
* @param StatementsSource $file_soure
* @param string[] $suppressed_issues
* @param FileManipulation[] $file_replacements
*
* @return null|false
*/
public static function afterExpressionAnalysis(
PhpParser\Node\Expr $expr,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
) {
return null;
}
/**
* Called after a statement has been checked
*
* @param string[] $suppressed_issues
* @param FileManipulation[] $file_replacements
*
* @return null|false
*/
public static function afterStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
) {
return null;
}
/**
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterClassLikeVisit(
PhpParser\Node\Stmt\ClassLike $stmt,
ClassLikeStorage $storage,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
) {
}
/**
* @param string $fq_class_name
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterClassLikeExistenceCheck(
string $fq_class_name,
CodeLocation $code_location,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
) {
}
/**
* @param PhpParser\Node\Expr\MethodCall|PhpParser\Node\Expr\StaticCall $expr
* @param string $method_id - the method id being checked
* @param string $appearing_method_id - the method id of the class that the method appears in
* @param string $declaring_method_id - the method id of the class or trait that declares the method
* @param string|null $var_id - a reference to the LHS of the variable
* @param PhpParser\Node\Arg[] $args
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterMethodCallAnalysis(
PhpParser\Node\Expr $expr,
string $method_id,
string $appearing_method_id,
string $declaring_method_id,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = [],
Union $return_type_candidate = null
) {
}
/**
* @param string $function_id - the method id being checked
* @param PhpParser\Node\Arg[] $args
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterFunctionCallAnalysis(
PhpParser\Node\Expr\FuncCall $expr,
string $function_id,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = [],
Union &$return_type_candidate = null
) {
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Psalm\Plugin\Hook;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\FileManipulation\FileManipulation;
use Psalm\StatementsSource;
interface AfterClassLikeExistenceCheckInterface
{
/**
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterClassLikeExistenceCheck(
string $fq_class_name,
CodeLocation $code_location,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
);
}

View File

@ -0,0 +1,24 @@
<?php
namespace Psalm\Plugin\Hook;
use PhpParser\Node\Stmt\ClassLike;
use Psalm\Codebase;
use Psalm\FileManipulation\FileManipulation;
use Psalm\StatementsSource;
use Psalm\Storage\ClassLikeStorage;
interface AfterClassLikeVisitInterface
{
/**
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterClassLikeVisit(
ClassLike $stmt,
ClassLikeStorage $storage,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
);
}

View File

@ -0,0 +1,26 @@
<?php
namespace Psalm\Plugin\Hook;
use PhpParser\Node\Expr;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\FileManipulation\FileManipulation;
use Psalm\StatementsSource;
interface AfterExpressionAnalysisInterface
{
/**
* Called after an expression has been checked
*
* @param FileManipulation[] $file_replacements
*
* @return null|false
*/
public static function afterExpressionAnalysis(
Expr $expr,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
);
}

View File

@ -0,0 +1,27 @@
<?php
namespace Psalm\Plugin\Hook;
use PhpParser\Node\Expr\FuncCall;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\FileManipulation\FileManipulation;
use Psalm\StatementsSource;
use Psalm\Type\Union;
interface AfterFunctionCallAnalysisInterface
{
/**
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterFunctionCallAnalysis(
FuncCall $expr,
string $function_id,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = [],
Union &$return_type_candidate = null
);
}

View File

@ -0,0 +1,32 @@
<?php
namespace Psalm\Plugin\Hook;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\FileManipulation\FileManipulation;
use Psalm\StatementsSource;
use Psalm\Type\Union;
interface AfterMethodCallAnalysisInterface
{
/**
* @param MethodCall|StaticCall $expr
* @param FileManipulation[] $file_replacements
*
* @return void
*/
public static function afterMethodCallAnalysis(
Expr $expr,
string $method_id,
string $appearing_method_id,
string $declaring_method_id,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = [],
Union $return_type_candidate = null
);
}

View File

@ -0,0 +1,26 @@
<?php
namespace Psalm\Plugin\Hook;
use PhpParser\Node\Stmt;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\FileManipulation\FileManipulation;
use Psalm\StatementsSource;
interface AfterStatementAnalysisInterface
{
/**
* Called after a statement has been checked
*
* @param FileManipulation[] $file_replacements
*
* @return null|false
*/
public static function afterStatementAnalysis(
Stmt $stmt,
Context $context,
StatementsSource $statements_source,
Codebase $codebase,
array &$file_replacements = []
);
}

View File

@ -1,7 +1,8 @@
<?php
namespace Psalm\PluginApi;
namespace Psalm\Plugin;
use SimpleXMLElement;
use Psalm\Plugin\RegistrationInterface;
interface PluginEntryPointInterface
{

View File

@ -1,5 +1,5 @@
<?php
namespace Psalm\PluginApi;
namespace Psalm\Plugin;
interface RegistrationInterface
{

View File

@ -1,7 +1,8 @@
<?php
namespace Psalm;
use Psalm\PluginApi\RegistrationInterface;
use Psalm\Plugin\Hook;
use Psalm\Plugin\RegistrationInterface;
class PluginRegistrationSocket implements RegistrationInterface
{
@ -29,50 +30,27 @@ class PluginRegistrationSocket implements RegistrationInterface
throw new \InvalidArgumentException('Plugins must be loaded before registration');
}
if (!is_subclass_of($handler, Plugin::class)) {
throw new \InvalidArgumentException(
'This handler must extend ' . Plugin::class . ' - ' . $handler . ' does not'
);
}
// check that handler class (or one of its ancestors, but not Plugin) actually redefines specific hooks,
// so that we don't register empty handlers provided by Plugin
$handlerClass = new \ReflectionClass($handler);
if ($handlerClass->getMethod('afterMethodCallAnalysis')->getDeclaringClass()->getName()
!== Plugin::class
) {
if (is_subclass_of($handler, Hook\AfterMethodCallAnalysisInterface::class)) {
$this->config->after_method_checks[$handler] = $handler;
}
if ($handlerClass->getMethod('afterFunctionCallAnalysis')->getDeclaringClass()->getName()
!== Plugin::class
) {
if (is_subclass_of($handler, Hook\AfterFunctionCallAnalysisInterface::class)) {
$this->config->after_function_checks[$handler] = $handler;
}
if ($handlerClass->getMethod('afterExpressionAnalysis')->getDeclaringClass()->getName()
!== Plugin::class
) {
if (is_subclass_of($handler, Hook\AfterExpressionAnalysisInterface::class)) {
$this->config->after_expression_checks[$handler] = $handler;
}
if ($handlerClass->getMethod('afterStatementAnalysis')->getDeclaringClass()->getName()
!== Plugin::class
) {
if (is_subclass_of($handler, Hook\AfterStatementAnalysisInterface::class)) {
$this->config->after_statement_checks[$handler] = $handler;
}
if ($handlerClass->getMethod('afterClassLikeExistenceCheck')->getDeclaringClass()->getName()
!== Plugin::class
) {
if (is_subclass_of($handler, Hook\AfterClassLikeExistenceCheckInterface::class)) {
$this->config->after_classlike_exists_checks[$handler] = $handler;
}
if ($handlerClass->getMethod('afterClassLikeVisit')->getDeclaringClass()->getName()
!== Plugin::class
) {
if (is_subclass_of($handler, Hook\AfterClassLikeVisitInterface::class)) {
$this->config->after_visit_classlikes[$handler] = $handler;
}
}

View File

@ -5,9 +5,8 @@ use Prophecy\Argument;
use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Config;
use Psalm\Context;
use Psalm\Plugin;
use Psalm\PluginApi\PluginEntryPointInterface;
use Psalm\PluginApi\RegistrationInterface;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use SimpleXMLElement;
class PluginTest extends TestCase

View File

@ -1,6 +1,8 @@
<?php
class BasePlugin extends \Psalm\Plugin
use Psalm\Plugin\Hook\AfterFunctionCallAnalysisInterface;
class BasePlugin implements Psalm\Plugin\Hook\AfterFunctionCallAnalysisInterface
{
public static function afterFunctionCallAnalysis(
\PhpParser\Node\Expr\FuncCall $expr,

View File

@ -1,7 +1,7 @@
<?php
use Psalm\PluginApi\PluginEntryPointInterface;
use Psalm\PluginApi\RegistrationInterface;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
require_once __DIR__ . '/extending_plugin.php';