1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00
psalm/tests/PluginTest.php

777 lines
24 KiB
PHP
Raw Normal View History

2018-10-29 16:41:02 +01:00
<?php
namespace Psalm\Tests;
use Psalm\Codebase;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Analyzer\FileAnalyzer;
2018-10-29 16:41:02 +01:00
use Psalm\Config;
use Psalm\Context;
use Psalm\PluginRegistrationSocket;
use Psalm\Plugin\Hook\AfterCodebasePopulatedInterface;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\RegistrationInterface;
use Psalm\Tests\Internal\Provider;
2018-10-29 16:41:02 +01:00
class PluginTest extends TestCase
{
/** @var TestConfig */
protected static $config;
/** @var ?\Psalm\Internal\Analyzer\ProjectAnalyzer */
2018-11-11 18:01:14 +01:00
protected $project_analyzer;
2018-10-29 16:41:02 +01:00
/**
* @return void
*/
public static function setUpBeforeClass()
{
self::$config = new TestConfig();
if (!defined('PSALM_VERSION')) {
define('PSALM_VERSION', '2.0.0');
}
if (!defined('PHP_PARSER_VERSION')) {
define('PHP_PARSER_VERSION', '4.0.0');
}
}
/**
* @return void
*/
public function setUp()
{
2018-11-06 03:57:36 +01:00
FileAnalyzer::clearCache();
2018-10-29 16:41:02 +01:00
$this->file_provider = new Provider\FakeFileProvider();
}
/**
* @param Config $config
*
2018-11-06 03:57:36 +01:00
* @return \Psalm\Internal\Analyzer\ProjectAnalyzer
2018-10-29 16:41:02 +01:00
*/
2018-11-06 03:57:36 +01:00
private function getProjectAnalyzerWithConfig(Config $config)
2018-10-29 16:41:02 +01:00
{
2018-11-06 03:57:36 +01:00
return new \Psalm\Internal\Analyzer\ProjectAnalyzer(
2018-10-29 16:41:02 +01:00
$config,
2018-11-06 03:57:36 +01:00
new \Psalm\Internal\Provider\Providers(
2018-10-29 16:41:02 +01:00
$this->file_provider,
new Provider\FakeParserCacheProvider()
)
);
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage InvalidClass
*
* @return void
*/
2018-11-06 03:57:36 +01:00
public function testStringAnalyzerPlugin()
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
2018-11-06 03:57:36 +01:00
<plugin filename="examples/plugins/StringChecker.php" />
</plugins>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
2018-11-06 03:57:36 +01:00
$a = "Psalm\Internal\Analyzer\ProjectAnalyzer";'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage InvalidClass
*
* @return void
*/
2018-11-06 03:57:36 +01:00
public function testStringAnalyzerPluginWithClassConstant()
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
2018-11-06 03:57:36 +01:00
<plugin filename="examples/plugins/StringChecker.php" />
</plugins>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class A {
const C = [
2018-11-06 03:57:36 +01:00
"foo" => "Psalm\Internal\Analyzer\ProjectAnalyzer",
];
}'
);
$this->analyzeFile($file_path, new Context());
}
2018-10-29 16:41:02 +01:00
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage UndefinedMethod
*
* @return void
*/
2018-11-06 03:57:36 +01:00
public function testStringAnalyzerPluginWithClassConstantConcat()
2018-10-29 16:41:02 +01:00
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
2018-10-29 16:41:02 +01:00
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
2018-11-06 03:57:36 +01:00
<plugin filename="examples/plugins/StringChecker.php" />
2018-10-29 16:41:02 +01:00
</plugins>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
2018-10-29 16:41:02 +01:00
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
class A {
const C = [
2018-11-06 03:57:36 +01:00
"foo" => \Psalm\Internal\Analyzer\ProjectAnalyzer::class . "::foo",
2018-10-29 16:41:02 +01:00
];
}'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @return void
*/
2018-11-06 03:57:36 +01:00
public function testEchoAnalyzerPluginWithJustHtml()
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
2018-11-06 03:57:36 +01:00
<plugin filename="examples/plugins/composer-based/echo-checker/EchoChecker.php" />
</plugins>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<h3>This is a header</h3>'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage TypeCoercion
*
* @return void
*/
2018-11-06 03:57:36 +01:00
public function testEchoAnalyzerPluginWithUnescapedConcatenatedString()
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
2018-11-06 03:57:36 +01:00
<plugin filename="examples/plugins/composer-based/echo-checker/EchoChecker.php" />
</plugins>
<issueHandlers>
<UndefinedGlobalVariable errorLevel="suppress" />
<MixedArgument errorLevel="suppress" />
<MixedOperand errorLevel="suppress" />
</issueHandlers>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?= $unsafe . "safeString" ?>'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage TypeCoercion
*
* @return void
*/
2018-11-06 03:57:36 +01:00
public function testEchoAnalyzerPluginWithUnescapedString()
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
2018-11-06 03:57:36 +01:00
<plugin filename="examples/plugins/composer-based/echo-checker/EchoChecker.php" />
</plugins>
<issueHandlers>
<UndefinedGlobalVariable errorLevel="suppress" />
<MixedArgument errorLevel="suppress" />
</issueHandlers>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?= $unsafe ?>'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @return void
*/
2018-11-06 03:57:36 +01:00
public function testEchoAnalyzerPluginWithEscapedString()
{
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
2018-11-06 03:57:36 +01:00
<plugin filename="examples/plugins/composer-based/echo-checker/EchoChecker.php" />
</plugins>
<issueHandlers>
<UndefinedGlobalVariable errorLevel="suppress" />
<MixedArgument errorLevel="suppress" />
</issueHandlers>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
/**
* @param mixed $s
* @return html-escaped-string
*/
function escapeHtml($s) : string {
if (!is_scalar($s)) {
throw new \UnexpectedValueException("bad value passed to escape");
}
/** @var html-escaped-string */
return htmlentities((string) $s);
}
?>
Some text
<?= escapeHtml($unsafe) ?>'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage NoFloatAssignment
*
* @return void
*/
public function testFloatCheckerPlugin()
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<plugin filename="examples/plugins/PreventFloatAssignmentChecker.php" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
$a = 5.0;'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @return void
*/
public function testFloatCheckerPluginIssueSuppressionByConfig()
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<plugin filename="examples/plugins/PreventFloatAssignmentChecker.php" />
</plugins>
<issueHandlers>
<PluginIssue name="NoFloatAssignment" errorLevel="suppress" />
<PluginIssue name="SomeOtherCustomIssue" errorLevel="suppress" />
</issueHandlers>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
$a = 5.0;'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @return void
*/
public function testFloatCheckerPluginIssueSuppressionByDocblock()
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<plugin filename="examples/plugins/PreventFloatAssignmentChecker.php" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
/** @psalm-suppress NoFloatAssignment */
$a = 5.0;'
);
$this->analyzeFile($file_path, new Context());
}
2018-11-06 03:57:36 +01:00
Plugin loading (#855) * add ability to load plugins by class names - Plugins need to implement `__invoke(PluginFacade $psalm):void` method - Plugins are enabled by adding `<pluginClass class="Qualified\Class\Name"/>` - `PluginFacade` provides a single point of contact with Psalm, so that plugins cannot become coupled to Psalm internals * added `psalm-plugin` cli tool to manage plugins Available commands: `psalm-plugin list` - lists available and enabled plugins `psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`) `psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`) Plugin installation: `composer install plugin-vendor/plugin-package-name` Plugin authoring: Plugins are identified by package `type` field, which should contain `psalm-plugin` string. `extra.pluginClass` should refer to the name of the class implementing `__invoke(PluginFacade $psalm):void` function Todo: - better config file search - better output for `psalm-plugin` - better formatting for modified xml file - composer skeleton project for plugins - ability to refer to plugins by package name (cli only) - composer plugin to (optionally) enable plugin upon installation - documentation on plugin installation and authoring - interfaces for plugin dependencies - interface for plugin entry point - migration path for legacy plugins * documented previously undocumented plugin methods * split legacy plugin registration into a wrapper class also added `PluginApi` namespace and `RegistrationInterface` * reuse psalm's config search algorithm * enable/disable plugins by composer package name * allow specifying alternative config file name * whitelist PluginApi namespace three times, but well, it works now * interface for plugin entry points * psalm-plugin as a symfony console app * fixed errors found by psalm * suppressed false positive UnusedMethods * cs fix * better psalm-plugin output * don't leave empty `plugins` node to avoid old schema violation * removed junk file that shouldn't be there * cs fix * fixed phpunit failure (constant redefinition) * work around missing docblock in on symfony console * php 7.0 compatibility * allow `pluginClass` child elements as plugin configuration * decouple console commands from undelying implementation - introduce PluginListFactory - add `PluginList::enable(string $class)` and `PluginList::disable(string $class)` * PluginList tests * ComposerLock test * droppped debugging statement * added part of console command tests * added tests for EnableCommand * added DisableCommand tests * ignore unused args * ConfigFile test * disable travis cache in attempt to fix builds * nah, that didn't work * update for upstream changes * rebase fixes * namespaced `extra` entry for entry point * s/PluginFacade/PluginRegistrationSocket/g * Added $config parameter to PluginEntryPointInterface::__invoke() * cs fixes * entry point interface php7.0 compatibility * cleaned up old cruft - dropped todos I'm not going to pursues - locked entry point to be a class implementing entry point interface * fixed legacy plugins docs * Added RegistrationInterface::registerHooksFromClass() It mimics the way old plugins were registered in Psalm\Config, so handler classes extending Psalm\Plugin should be fully compatible with it. Since Psalm\Plugin-style plugin registration was moved to RegistrationSocket, LegacyPlugin now only load file-based plugins, so it was renamed to FileBasedPluginAdapter. * Converted EchoChecker plugin to composer-based format - Its subfolder is registered as a local composer package in the root composer.json, so it's directly installable with ``` composer require psalm/echo-checker-plugin ``` - Migration is trivial: drop the plugin into a separate folder, then add simple composer.json and the entry point class. * Updated docs * Don't reject hook handlers that inherit handling methods * strip void return type in stub file
2018-11-11 05:23:36 +01:00
/** @return void */
public function testInheritedHookHandlersAreCalled()
{
require_once __DIR__ . '/stubs/extending_plugin_entrypoint.php';
2018-11-11 18:01:14 +01:00
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
Plugin loading (#855) * add ability to load plugins by class names - Plugins need to implement `__invoke(PluginFacade $psalm):void` method - Plugins are enabled by adding `<pluginClass class="Qualified\Class\Name"/>` - `PluginFacade` provides a single point of contact with Psalm, so that plugins cannot become coupled to Psalm internals * added `psalm-plugin` cli tool to manage plugins Available commands: `psalm-plugin list` - lists available and enabled plugins `psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`) `psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`) Plugin installation: `composer install plugin-vendor/plugin-package-name` Plugin authoring: Plugins are identified by package `type` field, which should contain `psalm-plugin` string. `extra.pluginClass` should refer to the name of the class implementing `__invoke(PluginFacade $psalm):void` function Todo: - better config file search - better output for `psalm-plugin` - better formatting for modified xml file - composer skeleton project for plugins - ability to refer to plugins by package name (cli only) - composer plugin to (optionally) enable plugin upon installation - documentation on plugin installation and authoring - interfaces for plugin dependencies - interface for plugin entry point - migration path for legacy plugins * documented previously undocumented plugin methods * split legacy plugin registration into a wrapper class also added `PluginApi` namespace and `RegistrationInterface` * reuse psalm's config search algorithm * enable/disable plugins by composer package name * allow specifying alternative config file name * whitelist PluginApi namespace three times, but well, it works now * interface for plugin entry points * psalm-plugin as a symfony console app * fixed errors found by psalm * suppressed false positive UnusedMethods * cs fix * better psalm-plugin output * don't leave empty `plugins` node to avoid old schema violation * removed junk file that shouldn't be there * cs fix * fixed phpunit failure (constant redefinition) * work around missing docblock in on symfony console * php 7.0 compatibility * allow `pluginClass` child elements as plugin configuration * decouple console commands from undelying implementation - introduce PluginListFactory - add `PluginList::enable(string $class)` and `PluginList::disable(string $class)` * PluginList tests * ComposerLock test * droppped debugging statement * added part of console command tests * added tests for EnableCommand * added DisableCommand tests * ignore unused args * ConfigFile test * disable travis cache in attempt to fix builds * nah, that didn't work * update for upstream changes * rebase fixes * namespaced `extra` entry for entry point * s/PluginFacade/PluginRegistrationSocket/g * Added $config parameter to PluginEntryPointInterface::__invoke() * cs fixes * entry point interface php7.0 compatibility * cleaned up old cruft - dropped todos I'm not going to pursues - locked entry point to be a class implementing entry point interface * fixed legacy plugins docs * Added RegistrationInterface::registerHooksFromClass() It mimics the way old plugins were registered in Psalm\Config, so handler classes extending Psalm\Plugin should be fully compatible with it. Since Psalm\Plugin-style plugin registration was moved to RegistrationSocket, LegacyPlugin now only load file-based plugins, so it was renamed to FileBasedPluginAdapter. * Converted EchoChecker plugin to composer-based format - Its subfolder is registered as a local composer package in the root composer.json, so it's directly installable with ``` composer require psalm/echo-checker-plugin ``` - Migration is trivial: drop the plugin into a separate folder, then add simple composer.json and the entry point class. * Updated docs * Don't reject hook handlers that inherit handling methods * strip void return type in stub file
2018-11-11 05:23:36 +01:00
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="ExtendingPluginRegistration" />
</plugins>
</psalm>'
)
);
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
Plugin loading (#855) * add ability to load plugins by class names - Plugins need to implement `__invoke(PluginFacade $psalm):void` method - Plugins are enabled by adding `<pluginClass class="Qualified\Class\Name"/>` - `PluginFacade` provides a single point of contact with Psalm, so that plugins cannot become coupled to Psalm internals * added `psalm-plugin` cli tool to manage plugins Available commands: `psalm-plugin list` - lists available and enabled plugins `psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`) `psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`) Plugin installation: `composer install plugin-vendor/plugin-package-name` Plugin authoring: Plugins are identified by package `type` field, which should contain `psalm-plugin` string. `extra.pluginClass` should refer to the name of the class implementing `__invoke(PluginFacade $psalm):void` function Todo: - better config file search - better output for `psalm-plugin` - better formatting for modified xml file - composer skeleton project for plugins - ability to refer to plugins by package name (cli only) - composer plugin to (optionally) enable plugin upon installation - documentation on plugin installation and authoring - interfaces for plugin dependencies - interface for plugin entry point - migration path for legacy plugins * documented previously undocumented plugin methods * split legacy plugin registration into a wrapper class also added `PluginApi` namespace and `RegistrationInterface` * reuse psalm's config search algorithm * enable/disable plugins by composer package name * allow specifying alternative config file name * whitelist PluginApi namespace three times, but well, it works now * interface for plugin entry points * psalm-plugin as a symfony console app * fixed errors found by psalm * suppressed false positive UnusedMethods * cs fix * better psalm-plugin output * don't leave empty `plugins` node to avoid old schema violation * removed junk file that shouldn't be there * cs fix * fixed phpunit failure (constant redefinition) * work around missing docblock in on symfony console * php 7.0 compatibility * allow `pluginClass` child elements as plugin configuration * decouple console commands from undelying implementation - introduce PluginListFactory - add `PluginList::enable(string $class)` and `PluginList::disable(string $class)` * PluginList tests * ComposerLock test * droppped debugging statement * added part of console command tests * added tests for EnableCommand * added DisableCommand tests * ignore unused args * ConfigFile test * disable travis cache in attempt to fix builds * nah, that didn't work * update for upstream changes * rebase fixes * namespaced `extra` entry for entry point * s/PluginFacade/PluginRegistrationSocket/g * Added $config parameter to PluginEntryPointInterface::__invoke() * cs fixes * entry point interface php7.0 compatibility * cleaned up old cruft - dropped todos I'm not going to pursues - locked entry point to be a class implementing entry point interface * fixed legacy plugins docs * Added RegistrationInterface::registerHooksFromClass() It mimics the way old plugins were registered in Psalm\Config, so handler classes extending Psalm\Plugin should be fully compatible with it. Since Psalm\Plugin-style plugin registration was moved to RegistrationSocket, LegacyPlugin now only load file-based plugins, so it was renamed to FileBasedPluginAdapter. * Converted EchoChecker plugin to composer-based format - Its subfolder is registered as a local composer package in the root composer.json, so it's directly installable with ``` composer require psalm/echo-checker-plugin ``` - Migration is trivial: drop the plugin into a separate folder, then add simple composer.json and the entry point class. * Updated docs * Don't reject hook handlers that inherit handling methods * strip void return type in stub file
2018-11-11 05:23:36 +01:00
$this->assertContains(
'ExtendingPlugin',
2018-11-11 18:01:14 +01:00
$this->project_analyzer->getCodebase()->config->after_function_checks
Plugin loading (#855) * add ability to load plugins by class names - Plugins need to implement `__invoke(PluginFacade $psalm):void` method - Plugins are enabled by adding `<pluginClass class="Qualified\Class\Name"/>` - `PluginFacade` provides a single point of contact with Psalm, so that plugins cannot become coupled to Psalm internals * added `psalm-plugin` cli tool to manage plugins Available commands: `psalm-plugin list` - lists available and enabled plugins `psalm-plugin enable 'Plugin\Class\Name'` - enables plugin (modifies `psalm.xml`) `psalm-plugin disable 'Plugin\Class\Name'` - disables plugin (modifies `psalm.xml`) Plugin installation: `composer install plugin-vendor/plugin-package-name` Plugin authoring: Plugins are identified by package `type` field, which should contain `psalm-plugin` string. `extra.pluginClass` should refer to the name of the class implementing `__invoke(PluginFacade $psalm):void` function Todo: - better config file search - better output for `psalm-plugin` - better formatting for modified xml file - composer skeleton project for plugins - ability to refer to plugins by package name (cli only) - composer plugin to (optionally) enable plugin upon installation - documentation on plugin installation and authoring - interfaces for plugin dependencies - interface for plugin entry point - migration path for legacy plugins * documented previously undocumented plugin methods * split legacy plugin registration into a wrapper class also added `PluginApi` namespace and `RegistrationInterface` * reuse psalm's config search algorithm * enable/disable plugins by composer package name * allow specifying alternative config file name * whitelist PluginApi namespace three times, but well, it works now * interface for plugin entry points * psalm-plugin as a symfony console app * fixed errors found by psalm * suppressed false positive UnusedMethods * cs fix * better psalm-plugin output * don't leave empty `plugins` node to avoid old schema violation * removed junk file that shouldn't be there * cs fix * fixed phpunit failure (constant redefinition) * work around missing docblock in on symfony console * php 7.0 compatibility * allow `pluginClass` child elements as plugin configuration * decouple console commands from undelying implementation - introduce PluginListFactory - add `PluginList::enable(string $class)` and `PluginList::disable(string $class)` * PluginList tests * ComposerLock test * droppped debugging statement * added part of console command tests * added tests for EnableCommand * added DisableCommand tests * ignore unused args * ConfigFile test * disable travis cache in attempt to fix builds * nah, that didn't work * update for upstream changes * rebase fixes * namespaced `extra` entry for entry point * s/PluginFacade/PluginRegistrationSocket/g * Added $config parameter to PluginEntryPointInterface::__invoke() * cs fixes * entry point interface php7.0 compatibility * cleaned up old cruft - dropped todos I'm not going to pursues - locked entry point to be a class implementing entry point interface * fixed legacy plugins docs * Added RegistrationInterface::registerHooksFromClass() It mimics the way old plugins were registered in Psalm\Config, so handler classes extending Psalm\Plugin should be fully compatible with it. Since Psalm\Plugin-style plugin registration was moved to RegistrationSocket, LegacyPlugin now only load file-based plugins, so it was renamed to FileBasedPluginAdapter. * Converted EchoChecker plugin to composer-based format - Its subfolder is registered as a local composer package in the root composer.json, so it's directly installable with ``` composer require psalm/echo-checker-plugin ``` - Migration is trivial: drop the plugin into a separate folder, then add simple composer.json and the entry point class. * Updated docs * Don't reject hook handlers that inherit handling methods * strip void return type in stub file
2018-11-11 05:23:36 +01:00
);
}
/** @return void */
public function testAfterCodebasePopulatedHookIsLoaded()
{
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
</psalm>'
)
);
$hook = new class implements AfterCodebasePopulatedInterface
{
/** @return void */
public static function afterCodebasePopulated(Codebase $codebase)
{
}
};
$codebase = $this->project_analyzer->getCodebase();
$config = $codebase->config;
(new PluginRegistrationSocket($config, $codebase))->registerHooksFromClass(get_class($hook));
$this->assertContains(
get_class($hook),
$this->project_analyzer->getCodebase()->config->after_codebase_populated
);
}
/** @return void */
public function testPropertyProviderHooks()
{
require_once __DIR__ . '/Plugin/PropertyPlugin.php';
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Plugin\\PropertyPlugin" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Ns;
class Foo {}
$foo = new Foo();
echo $foo->magic_property;'
);
$this->analyzeFile($file_path, new Context());
}
/** @return void */
public function testMethodProviderHooks()
{
require_once __DIR__ . '/Plugin/MethodPlugin.php';
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Plugin\\MethodPlugin" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Ns;
class Foo {}
$foo = new Foo();
echo $foo->magicMethod("hello");
echo $foo::magicMethod("hello");'
);
$this->analyzeFile($file_path, new Context());
}
/** @return void */
public function testFunctionProviderHooks()
{
require_once __DIR__ . '/Plugin/FunctionPlugin.php';
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Plugin\\FunctionPlugin" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
magicFunction("hello");'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage InvalidPropertyAssignmentValue
*
* @return void
*/
public function testPropertyProviderHooksInvalidAssignment()
{
require_once __DIR__ . '/Plugin/PropertyPlugin.php';
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Plugin\\PropertyPlugin" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Ns;
class Foo {}
$foo = new Foo();
$foo->magic_property = 5;'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage InvalidScalarArgument
*
* @return void
*/
public function testMethodProviderHooksInvalidArg()
{
require_once __DIR__ . '/Plugin/MethodPlugin.php';
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Plugin\\MethodPlugin" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
namespace Ns;
class Foo {}
$foo = new Foo();
echo $foo->magicMethod(5);'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage InvalidScalarArgument
*
* @return void
*/
public function testFunctionProviderHooksInvalidArg()
{
require_once __DIR__ . '/Plugin/FunctionPlugin.php';
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Plugin\\FunctionPlugin" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$file_path = getcwd() . '/src/somefile.php';
$this->addFile(
$file_path,
'<?php
magicFunction(5);'
);
$this->analyzeFile($file_path, new Context());
}
/**
* @return void
*/
public function testAfterAnalysisHooks()
{
require_once __DIR__ . '/Plugin/AfterAnalysisPlugin.php';
$this->project_analyzer = $this->getProjectAnalyzerWithConfig(
TestConfig::loadFromXML(
dirname(__DIR__) . DIRECTORY_SEPARATOR,
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests/DummyProject" />
</projectFiles>
<plugins>
<pluginClass class="Psalm\\Test\\Plugin\\AfterAnalysisPlugin" />
</plugins>
</psalm>'
)
);
$this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer);
$this->project_analyzer->output_format = \Psalm\Internal\Analyzer\ProjectAnalyzer::TYPE_JSON;
$this->project_analyzer->check('tests/DummyProject', true);
\Psalm\IssueBuffer::finish($this->project_analyzer, true, microtime(true));
}
2018-10-29 16:41:02 +01:00
}