Skip to content

Commit

Permalink
Merge pull request #6789 from ohader/issue-6788
Browse files Browse the repository at this point in the history
❗Allow plugins to modify Config::$fileExtensions early
  • Loading branch information
orklah committed Jan 30, 2022
2 parents 1220320 + 3fedb5c commit 64d06c6
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 196 deletions.
11 changes: 11 additions & 0 deletions UPGRADING.md
Expand Up @@ -198,3 +198,14 @@
- [BC] Method `Psalm\DocComment::parse()` was removed
- [BC] Class `Psalm\Type\Atomic\THtmlEscapedString` has been removed
- [BC] Property `Psalm\Context::$vars_from_global` has been renamed to `$referenced_globals`
- [BC] Self-registration of file type scanners and file type analyzers has been changed
- `Psalm\Plugin\RegistrationInterface::addFileTypeScanner` was removed
- `Psalm\Plugin\RegistrationInterface::addFileTypeAnalyzer` was removed
- :information_source: migration possible using `Psalm\Plugin\FileExtensionsInterface`
- `Psalm\PluginRegistrationSocket::addFileTypeScanner` was removed
- `Psalm\PluginRegistrationSocket::getAdditionalFileTypeScanners` was removed
- `Psalm\PluginRegistrationSocket::addFileTypeAnalyzer` was removed
- `Psalm\PluginRegistrationSocket::getAdditionalFileTypeAnalyzers` was removed
- `Psalm\PluginRegistrationSocket::getAdditionalFileExtensions` was removed
- `Psalm\PluginRegistrationSocket::addFileExtension` was removed
- :information_source: migration possible using `Psalm\PluginFileExtensionsSocket`
13 changes: 10 additions & 3 deletions docs/running_psalm/checking_non_php_files.md
Expand Up @@ -23,14 +23,21 @@ Plugins can register their own custom scanner and analyzer implementations for
namespace Psalm\Example;

use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\PluginFileExtensionsInterface;
use Psalm\Plugin\FileExtensionsInterface;
use Psalm\Plugin\RegistrationInterface;

class CustomPlugin implements PluginEntryPointInterface
class CustomPlugin implements PluginEntryPointInterface, PluginFileExtensionsInterface
{
public function __invoke(RegistrationInterface $registration, ?\SimpleXMLElement $config = null): void
{
$registration->addFileTypeScanner('phpt', TemplateScanner::class);
$registration->addFileTypeAnalyzer('phpt', TemplateAnalyzer::class);
// ... regular plugin processes, stub registration, hook registration
}

public function processFileExtensions(FileExtensionsInterface $fileExtensions, ?SimpleXMLElement $config = null): void
{
$fileExtensions->addFileTypeScanner('phpt', TemplateScanner::class);
$fileExtensions->addFileTypeAnalyzer('phpt', TemplateAnalyzer::class);
}
}
```
4 changes: 2 additions & 2 deletions docs/running_psalm/plugins/authoring_plugins.md
Expand Up @@ -39,8 +39,8 @@ To register a stub file manually use `Psalm\Plugin\RegistrationInterface::addStu
In addition to XML configuration node `<fileExtensions>` plugins can register their own custom scanner
and analyzer implementations for particular file extensions, e.g.

* `Psalm\Plugin\RegistrationInterface::addFileTypeScanner('html', CustomFileScanner::class)`
* `Psalm\Plugin\RegistrationInterface::addFileTypeAnalyzer('html', CustomFileAnalyzer::class)`
* `Psalm\Plugin\FileExtensionsInterface::addFileTypeScanner('html', CustomFileScanner::class)`
* `Psalm\Plugin\FileExtensionsInterface::addFileTypeAnalyzer('html', CustomFileAnalyzer::class)`

## Publishing your plugin on Packagist

Expand Down
132 changes: 91 additions & 41 deletions src/Psalm/Config.php
Expand Up @@ -36,6 +36,8 @@
use Psalm\Issue\PropertyIssue;
use Psalm\Issue\VariableIssue;
use Psalm\Plugin\PluginEntryPointInterface;
use Psalm\Plugin\PluginFileExtensionsInterface;
use Psalm\Plugin\PluginInterface;
use Psalm\Progress\Progress;
use Psalm\Progress\VoidProgress;
use SimpleXMLElement;
Expand Down Expand Up @@ -593,6 +595,11 @@ class Config
"xdebug" => false,
];

/**
* @var array<class-string, PluginInterface>
*/
private $plugins = [];

protected function __construct()
{
self::$instance = $this;
Expand Down Expand Up @@ -1372,6 +1379,40 @@ public function getPluginClasses(): array
return $this->plugin_classes;
}

public function processPluginFileExtensions(ProjectAnalyzer $projectAnalyzer): void
{
$projectAnalyzer->progress->debug('Process plugin adjustments...' . PHP_EOL);
$socket = new PluginFileExtensionsSocket($this);
foreach ($this->plugin_classes as $pluginClassEntry) {
$pluginClassName = $pluginClassEntry['class'];
$pluginConfig = $pluginClassEntry['config'];
$plugin = $this->loadPlugin($projectAnalyzer, $pluginClassName);
if (!$plugin instanceof PluginFileExtensionsInterface) {
continue;
}
try {
$plugin->processFileExtensions($socket, $pluginConfig);
} catch (Throwable $t) {
throw new ConfigException(
'Failed to process plugin file extensions ' . $pluginClassName,
1_635_800_581,
$t
);
}
$projectAnalyzer->progress->debug('Initialized plugin ' . $pluginClassName . ' successfully' . PHP_EOL);
}
// populate additional aspects after plugins have been initialized
foreach ($socket->getAdditionalFileExtensions() as $fileExtension) {
$this->file_extensions[] = $fileExtension;
}
foreach ($socket->getAdditionalFileTypeScanners() as $extension => $className) {
$this->filetype_scanners[$extension] = $className;
}
foreach ($socket->getAdditionalFileTypeAnalyzers() as $extension => $className) {
$this->filetype_analyzers[$extension] = $className;
}
}

/**
* Initialises all the plugins (done once the config is fully loaded)
*/
Expand All @@ -1387,38 +1428,22 @@ public function initializePlugins(ProjectAnalyzer $project_analyzer): void
$plugin_class_name = $plugin_class_entry['class'];
$plugin_config = $plugin_class_entry['config'];

try {
// Below will attempt to load plugins from the project directory first.
// Failing that, it will use registered autoload chain, which will load
// plugins from Psalm directory or phar file. If that fails as well, it
// will fall back to project autoloader. It may seem that the last step
// will always fail, but it's only true if project uses Composer autoloader
if ($this->composer_class_loader
&& ($plugin_class_path = $this->composer_class_loader->findFile($plugin_class_name))
) {
$project_analyzer->progress->debug(
'Loading plugin ' . $plugin_class_name . ' via require' . PHP_EOL
);

self::requirePath($plugin_class_path);
} else {
if (!class_exists($plugin_class_name)) {
throw new UnexpectedValueException($plugin_class_name . ' is not a known class');
}
}
$plugin = $this->loadPlugin($project_analyzer, $plugin_class_name);
if (!$plugin instanceof PluginEntryPointInterface) {
continue;
}

/**
* @psalm-suppress InvalidStringClass
*
* @var PluginEntryPointInterface
*/
$plugin_object = new $plugin_class_name;
$plugin_object($socket, $plugin_config);
} catch (Throwable $e) {
throw new ConfigException('Failed to load plugin ' . $plugin_class_name, 0, $e);
try {
$plugin($socket, $plugin_config);
} catch (Throwable $t) {
throw new ConfigException(
'Failed to invoke plugin ' . $plugin_class_name,
1_635_800_582,
$t
);
}

$project_analyzer->progress->debug('Loaded plugin ' . $plugin_class_name . ' successfully' . PHP_EOL);
$project_analyzer->progress->debug('Initialized plugin ' . $plugin_class_name . ' successfully' . PHP_EOL);
}

foreach ($this->filetype_scanner_paths as $extension => $path) {
Expand Down Expand Up @@ -1447,28 +1472,53 @@ public function initializePlugins(ProjectAnalyzer $project_analyzer): void

foreach ($this->plugin_paths as $path) {
try {
$plugin_object = new FileBasedPluginAdapter($path, $this, $codebase);
$plugin_object($socket);
$plugin = new FileBasedPluginAdapter($path, $this, $codebase);
$plugin($socket);
} catch (Throwable $e) {
throw new ConfigException('Failed to load plugin ' . $path, 0, $e);
}
}
// populate additional aspects after plugins have been initialized
foreach ($socket->getAdditionalFileExtensions() as $fileExtension) {
$this->file_extensions[] = $fileExtension;
}
foreach ($socket->getAdditionalFileTypeScanners() as $extension => $className) {
$this->filetype_scanners[$extension] = $className;
}
foreach ($socket->getAdditionalFileTypeAnalyzers() as $extension => $className) {
$this->filetype_analyzers[$extension] = $className;
}

new HtmlFunctionTainter();

$socket->registerHooksFromClass(HtmlFunctionTainter::class);
}

private function loadPlugin(ProjectAnalyzer $projectAnalyzer, string $pluginClassName): PluginInterface
{
if (isset($this->plugins[$pluginClassName])) {
return $this->plugins[$pluginClassName];
}
try {
// Below will attempt to load plugins from the project directory first.
// Failing that, it will use registered autoload chain, which will load
// plugins from Psalm directory or phar file. If that fails as well, it
// will fall back to project autoloader. It may seem that the last step
// will always fail, but it's only true if project uses Composer autoloader
if ($this->composer_class_loader
&& ($pluginclas_class_path = $this->composer_class_loader->findFile($pluginClassName))
) {
$projectAnalyzer->progress->debug(
'Loading plugin ' . $pluginClassName . ' via require' . PHP_EOL
);

self::requirePath($pluginclas_class_path);
} else {
if (!class_exists($pluginClassName)) {
throw new UnexpectedValueException($pluginClassName . ' is not a known class');
}
}
if (!is_a($pluginClassName, PluginInterface::class, true)) {
throw new UnexpectedValueException($pluginClassName . ' is not a PluginInterface implementation');
}
$this->plugins[$pluginClassName] = new $pluginClassName;
$projectAnalyzer->progress->debug('Loaded plugin ' . $pluginClassName . PHP_EOL);
return $this->plugins[$pluginClassName];
} catch (Throwable $e) {
throw new ConfigException('Failed to load plugin ' . $pluginClassName, 0, $e);
}
}

private static function requirePath(string $path): void
{
/** @psalm-suppress UnresolvableInclude */
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
Expand Up @@ -306,6 +306,7 @@ public function __construct(
$this->stdout_report_options = $stdout_report_options;
$this->generated_report_options = $generated_report_options;

$this->config->processPluginFileExtensions($this);
$file_extensions = $this->config->getFileExtensions();

foreach ($this->config->getProjectDirectories() as $dir_name) {
Expand Down
21 changes: 21 additions & 0 deletions src/Psalm/Plugin/FileExtensionsInterface.php
@@ -0,0 +1,21 @@
<?php

namespace Psalm\Plugin;

use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Internal\Scanner\FileScanner;

interface FileExtensionsInterface
{
/**
* @param string $fileExtension e.g. `'html'`
* @param class-string<FileScanner> $className
*/
public function addFileTypeScanner(string $fileExtension, string $className): void;

/**
* @param string $fileExtension e.g. `'html'`
* @param class-string<FileAnalyzer> $className
*/
public function addFileTypeAnalyzer(string $fileExtension, string $className): void;
}
2 changes: 1 addition & 1 deletion src/Psalm/Plugin/PluginEntryPointInterface.php
Expand Up @@ -4,7 +4,7 @@

use SimpleXMLElement;

interface PluginEntryPointInterface
interface PluginEntryPointInterface extends PluginInterface
{
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void;
}
13 changes: 13 additions & 0 deletions src/Psalm/Plugin/PluginFileExtensionsInterface.php
@@ -0,0 +1,13 @@
<?php

namespace Psalm\Plugin;

use SimpleXMLElement;

interface PluginFileExtensionsInterface extends PluginInterface
{
public function processFileExtensions(
FileExtensionsInterface $fileExtensions,
?SimpleXMLElement $config = null
): void;
}
7 changes: 7 additions & 0 deletions src/Psalm/Plugin/PluginInterface.php
@@ -0,0 +1,7 @@
<?php

namespace Psalm\Plugin;

interface PluginInterface
{
}
17 changes: 0 additions & 17 deletions src/Psalm/Plugin/RegistrationInterface.php
Expand Up @@ -2,9 +2,6 @@

namespace Psalm\Plugin;

use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Internal\Scanner\FileScanner;

interface RegistrationInterface
{
public function addStubFile(string $file_name): void;
Expand All @@ -13,18 +10,4 @@ public function addStubFile(string $file_name): void;
* @param class-string $handler
*/
public function registerHooksFromClass(string $handler): void;

/**
* @param string $fileExtension e.g. `'html'`
* @param class-string<FileScanner> $className
* @deprecated will be removed in v5.0, use \Psalm\Plugin\FileExtensionsInterface instead (#6788)
*/
public function addFileTypeScanner(string $fileExtension, string $className): void;

/**
* @param string $fileExtension e.g. `'html'`
* @param class-string<FileAnalyzer> $className
* @deprecated will be removed in v5.0, use \Psalm\Plugin\FileExtensionsInterface instead (#6788)
*/
public function addFileTypeAnalyzer(string $fileExtension, string $className): void;
}

0 comments on commit 64d06c6

Please sign in to comment.