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:
parent
8534955572
commit
1c03d6f076
@ -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"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user