1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-02 09:37:59 +01:00

Implemented multiple composer roots for plugins (#1723)

Refs vimeo/psalm#1710
This commit is contained in:
Bruce Weirdan 2019-06-02 18:23:56 +03:00 committed by Matthew Brown
parent 8534955572
commit 1c03d6f076
4 changed files with 138 additions and 63 deletions

View File

@ -5,12 +5,13 @@ use RuntimeException;
class ComposerLock class ComposerLock
{ {
/** @var string */ /** @var string[] */
private $file_name; private $file_names;
public function __construct(string $file_name) /** @param string[] $file_names */
public function __construct(array $file_names)
{ {
$this->file_name = $file_name; $this->file_names = $file_names;
} }
@ -45,17 +46,17 @@ class ComposerLock
return $ret; return $ret;
} }
private function read(): array private function read(string $file_name): array
{ {
/** @psalm-suppress MixedAssignment */ /** @psalm-suppress MixedAssignment */
$contents = json_decode(file_get_contents($this->file_name), true); $contents = json_decode(file_get_contents($file_name), true);
if ($error = json_last_error()) { if ($error = json_last_error()) {
throw new RuntimeException(json_last_error_msg(), $error); throw new RuntimeException(json_last_error_msg(), $error);
} }
if (!is_array($contents)) { if (!is_array($contents)) {
throw new RuntimeException('Malformed ' . $this->file_name . ', expecting JSON-encoded object'); throw new RuntimeException('Malformed ' . $file_name . ', expecting JSON-encoded object');
} }
return $contents; return $contents;
@ -79,13 +80,23 @@ class ComposerLock
private function getAllPackages(): array private function getAllPackages(): array
{ {
$composer_lock_contents = $this->read(); $packages = [];
if (!isset($composer_lock_contents["packages"]) || !is_array($composer_lock_contents["packages"])) { foreach ($this->file_names as $file_name) {
throw new RuntimeException('packages section is missing or not an array'); $composer_lock_contents = $this->read($file_name);
if (!isset($composer_lock_contents["packages"]) || !is_array($composer_lock_contents["packages"])) {
throw new RuntimeException('packages section is missing or not an array');
}
if (!isset($composer_lock_contents["packages-dev"]) || !is_array($composer_lock_contents["packages-dev"])) {
throw new RuntimeException('packages-dev section is missing or not an array');
}
$packages = array_merge(
$packages,
array_merge(
$composer_lock_contents["packages"],
$composer_lock_contents["packages-dev"]
)
);
} }
if (!isset($composer_lock_contents["packages-dev"]) || !is_array($composer_lock_contents["packages-dev"])) { return $packages;
throw new RuntimeException('packages-dev section is missing or not an array');
}
return array_merge($composer_lock_contents["packages"], $composer_lock_contents["packages-dev"]);
} }
} }

View File

@ -3,19 +3,59 @@ namespace Psalm\Internal\PluginManager;
class PluginListFactory class PluginListFactory
{ {
/** @var string */
private $project_root;
/** @var string */
private $psalm_root;
public function __construct(string $project_root, string $psalm_root)
{
$this->project_root = $project_root;
$this->psalm_root = $psalm_root;
}
public function __invoke(string $current_dir, string $config_file_path = null): PluginList public function __invoke(string $current_dir, string $config_file_path = null): PluginList
{ {
$stub_composer_lock = (object)[
"packages" => [],
"packages-dev" => [],
];
$config_file = new ConfigFile($current_dir, $config_file_path); $config_file = new ConfigFile($current_dir, $config_file_path);
$lock_file = is_readable('composer.lock') ? $composer_lock = new ComposerLock($this->findLockFiles());
'composer.lock' :
'data:application/json,' . urlencode(json_encode($stub_composer_lock));
$composer_lock = new ComposerLock($lock_file);
return new PluginList($config_file, $composer_lock); return new PluginList($config_file, $composer_lock);
} }
/** @return non-empty-array<int,string> */
private function findLockFiles(): array
{
// use cases
// 1. plugins are installed into project vendors - composer.lock is PROJECT_ROOT/composer.lock
// 2. plugins are installed into separate composer environment (either global or bamarni-bin)
// - composer.lock is PSALM_ROOT/../../../composer.lock
// 3. plugins are installed into psalm vendors - composer.lock is PSALM_ROOT/composer.lock
// 4. none of the above - use stub (empty virtual composer.lock)
if ($this->psalm_root === $this->project_root) {
// managing plugins for psalm itself
$composer_lock_filenames = [
rtrim($this->psalm_root, DIRECTORY_SEPARATOR) . '/composer.lock',
];
} else {
$composer_lock_filenames = [
rtrim($this->project_root, DIRECTORY_SEPARATOR) . '/composer.lock',
rtrim($this->psalm_root, DIRECTORY_SEPARATOR) . '/../../../composer.lock',
rtrim($this->psalm_root, DIRECTORY_SEPARATOR) . '/composer.lock',
];
}
$composer_lock_filenames = array_filter($composer_lock_filenames, 'is_readable');
if (empty($composer_lock_filenames)) {
$stub_composer_lock = (object)[
"packages" => [],
"packages-dev" => [],
];
$composer_lock_filenames[] = 'data:application/json,' . urlencode(json_encode($stub_composer_lock));
}
return $composer_lock_filenames;
}
} }

View File

@ -15,7 +15,10 @@ requireAutoloaders($current_dir, false, $vendor_dir);
$app = new Application('psalm-plugin', (string) Versions::getVersion('vimeo/psalm')); $app = new Application('psalm-plugin', (string) Versions::getVersion('vimeo/psalm'));
$plugin_list_factory = new PluginListFactory; $psalm_root = dirname(__DIR__) . DIRECTORY_SEPARATOR;
$plugin_list_factory = new PluginListFactory($current_dir, $psalm_root);
$app->addCommands([ $app->addCommands([
new ShowCommand($plugin_list_factory), new ShowCommand($plugin_list_factory),
new EnableCommand($plugin_list_factory), new EnableCommand($plugin_list_factory),

View File

@ -12,17 +12,8 @@ class ComposerLockTest extends TestCase
*/ */
public function pluginIsPackageOfTypePsalmPlugin() public function pluginIsPackageOfTypePsalmPlugin()
{ {
$lock = new ComposerLock($this->jsonFile((object)[])); $lock = new ComposerLock([$this->jsonFile((object)[])]);
$this->assertTrue($lock->isPlugin([ $this->assertTrue($lock->isPlugin($this->pluginEntry('vendor/package', 'Some\Class')));
'name' => 'vendor/package',
'type' => 'psalm-plugin',
'extra' => [
'psalm' => [
'pluginClass' => 'Some\Class',
],
],
]));
// counterexamples // counterexamples
$this->assertFalse($lock->isPlugin([]), 'Non-package should not be considered a plugin'); $this->assertFalse($lock->isPlugin([]), 'Non-package should not be considered a plugin');
@ -44,20 +35,12 @@ class ComposerLockTest extends TestCase
*/ */
public function seesNonDevPlugins() public function seesNonDevPlugins()
{ {
$lock = new ComposerLock($this->jsonFile((object)[ $lock = new ComposerLock([$this->jsonFile((object)[
'packages' => [ 'packages' => [
(object)[ (object)$this->pluginEntry('vendor/package', 'Vendor\Package\PluginClass')
'name' => 'vendor/package',
'type' => 'psalm-plugin',
'extra' => (object)[
'psalm' => (object) [
'pluginClass' => 'Vendor\Package\PluginClass',
],
],
],
], ],
'packages-dev' => [], 'packages-dev' => [],
])); ])]);
$plugins = $lock->getPlugins(); $plugins = $lock->getPlugins();
$this->assertArrayHasKey('vendor/package', $plugins); $this->assertArrayHasKey('vendor/package', $plugins);
@ -70,20 +53,12 @@ class ComposerLockTest extends TestCase
*/ */
public function seesDevPlugins() public function seesDevPlugins()
{ {
$lock = new ComposerLock($this->jsonFile((object)[ $lock = new ComposerLock([$this->jsonFile((object)[
'packages' => [], 'packages' => [],
'packages-dev' => [ 'packages-dev' => [
(object)[ (object) $this->pluginEntry('vendor/package', 'Vendor\Package\PluginClass')
'name' => 'vendor/package',
'type' => 'psalm-plugin',
'extra' => (object)[
'psalm' => (object)[
'pluginClass' => 'Vendor\Package\PluginClass',
],
],
],
], ],
])); ])]);
$plugins = $lock->getPlugins(); $plugins = $lock->getPlugins();
$this->assertArrayHasKey('vendor/package', $plugins); $this->assertArrayHasKey('vendor/package', $plugins);
@ -101,10 +76,10 @@ class ComposerLockTest extends TestCase
'type' => 'library', 'type' => 'library',
]; ];
$lock = new ComposerLock($this->jsonFile((object)[ $lock = new ComposerLock([$this->jsonFile((object)[
'packages' => [$nonPlugin], 'packages' => [$nonPlugin],
'packages-dev' => [$nonPlugin], 'packages-dev' => [$nonPlugin],
])); ])]);
$this->assertEmpty($lock->getPlugins()); $this->assertEmpty($lock->getPlugins());
} }
@ -114,7 +89,7 @@ class ComposerLockTest extends TestCase
*/ */
public function failsOnInvalidJson() public function failsOnInvalidJson()
{ {
$lock = new ComposerLock('data:application/json,['); $lock = new ComposerLock(['data:application/json,[']);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$lock->getPlugins(); $lock->getPlugins();
@ -126,7 +101,7 @@ class ComposerLockTest extends TestCase
*/ */
public function failsOnNonObjectJson() public function failsOnNonObjectJson()
{ {
$lock = new ComposerLock('data:application/json,null'); $lock = new ComposerLock(['data:application/json,null']);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$lock->getPlugins(); $lock->getPlugins();
@ -141,7 +116,7 @@ class ComposerLockTest extends TestCase
$noPackagesFile = $this->jsonFile((object)[ $noPackagesFile = $this->jsonFile((object)[
'packages-dev' => [], 'packages-dev' => [],
]); ]);
$lock = new ComposerLock($noPackagesFile); $lock = new ComposerLock([$noPackagesFile]);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$lock->getPlugins(); $lock->getPlugins();
} }
@ -155,11 +130,57 @@ class ComposerLockTest extends TestCase
$noPackagesDevFile = $this->jsonFile((object)[ $noPackagesDevFile = $this->jsonFile((object)[
'packages' => [], 'packages' => [],
]); ]);
$lock = new ComposerLock($noPackagesDevFile); $lock = new ComposerLock([$noPackagesDevFile]);
$this->expectException(\RuntimeException::class); $this->expectException(\RuntimeException::class);
$lock->getPlugins(); $lock->getPlugins();
} }
/** @test */
public function mergesMultipleComposerLockFiles(): void
{
$lock = new ComposerLock([
$this->jsonFile([
'packages' => [
(object) $this->pluginEntry('vendor/packageA', 'Vendor\PackageA\PluginClass')
],
'packages-dev' => [
(object) $this->pluginEntry('vendor/packageB', 'Vendor\PackageB\PluginClass')
],
]),
$this->jsonFile([
'packages' => [
(object) $this->pluginEntry('vendor/packageC', 'Vendor\PackageC\PluginClass')
],
'packages-dev' => [
(object) $this->pluginEntry('vendor/packageD', 'Vendor\PackageD\PluginClass')
],
]),
]);
$this->assertEquals(
[
'vendor/packageA' => 'Vendor\PackageA\PluginClass',
'vendor/packageB' => 'Vendor\PackageB\PluginClass',
'vendor/packageC' => 'Vendor\PackageC\PluginClass',
'vendor/packageD' => 'Vendor\PackageD\PluginClass',
],
$lock->getPlugins()
);
}
private function pluginEntry(string $package_name, string $package_class): array
{
return [
'name' => $package_name,
'type' => 'psalm-plugin',
'extra' => [
'psalm' => [
'pluginClass' => $package_class,
],
],
];
}
/** @param mixed $data */ /** @param mixed $data */
private function jsonFile($data): string private function jsonFile($data): string
{ {