Compare commits

...

6 Commits

Author SHA1 Message Date
Daniil Gentili 7cea5c8b26
Continue work on composer injector 2020-11-04 16:54:36 +01:00
Daniil Gentili 5418c27788
Refactoring 2020-11-04 16:24:45 +01:00
Daniil Gentili 6a3d3fec57
Improve composer API 2020-11-04 12:00:16 +01:00
Daniil Gentili e3409513c3
Update 2020-11-04 10:49:05 +01:00
Daniil Gentili d22df4bd9c
Composer integration improvements 2020-11-03 22:01:34 +01:00
Daniil Gentili 0b572094f7
Turn directly into plugin 2020-11-02 11:53:29 +01:00
15 changed files with 457 additions and 257 deletions

View File

@ -2,7 +2,6 @@
$config = new Amp\CodeStyle\Config();
$config->getFinder()
->in(__DIR__ . '/examples')
->in(__DIR__ . '/src')
->in(__DIR__ . '/test');

View File

@ -1,10 +1,10 @@
{
"name": "phabel/phabel",
"description": "Write and deploy modern PHP 8 code, today.",
"type": "project",
"type": "composer-plugin",
"require": {
"nikic/php-parser": "^4.7",
"phabel/phabel-plugin": "*"
"composer-plugin-api": "^2"
},
"require-dev": {
"phpunit/phpunit": "^7 | ^8 | ^9",
@ -23,6 +23,9 @@
"Phabel\\": "src"
}
},
"extra": {
"class": "Phabel\\Composer\\Plugin"
},
"scripts": {
"check": [
"@cs",
@ -32,4 +35,4 @@
"cs-fix": "php-cs-fixer fix -v --diff",
"test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit --coverage-text"
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Phabel\Composer;
use Composer\Semver\Constraint\ConstraintInterface;
interface PhabelConstraintInterface extends ConstraintInterface
{
public function getConfig(): array;
public function setConfig(array $config);
}

View File

@ -0,0 +1,7 @@
<?php
namespace Phabel\Composer;
interface PhabelRepositoryInterface
{
}

View File

@ -5,11 +5,13 @@ namespace Phabel\Composer;
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\Installer\InstallerEvent;
use Composer\Installer\InstallerEvents;
use Composer\Installer\PackageEvent;
use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Phabel\Composer\Traits\Repository;
use Phabel\Tools;
use ReflectionObject;
/**
* @author Daniil Gentili <daniil@daniil.it>
@ -31,9 +33,19 @@ class Plugin implements PluginInterface, EventSubscriberInterface
*/
public function activate(Composer $composer, IOInterface $io): void
{
$rootPackage = $composer->getPackage();
Repository::preparePackage($rootPackage, null);
var_dump($rootPackage->getRequires());
$repoManager = $composer->getRepositoryManager();
$repos = $repoManager->getRepositories();
$repoManager->prependRepository(new Repository($repos[0]));
$reflect = (new ReflectionObject($repoManager))->getProperty('repositories');
$reflect->setAccessible(true);
$reflect->setValue($repoManager, []);
foreach ($repos as $repo) {
$repoManager->prependRepository(Tools::cloneWithTrait($repo, Repository::class, PhabelRepositoryInterface::class));
}
//\var_dump(\array_map('get_class', $repoManager->getRepositories()));
$this->io = $io;
}
@ -69,8 +81,6 @@ class Plugin implements PluginInterface, EventSubscriberInterface
public static function getSubscribedEvents()
{
return [
InstallerEvents::PRE_DEPENDENCIES_SOLVING =>
['onDependencySolve', 100000],
PackageEvents::POST_PACKAGE_INSTALL =>
['onInstall', 100000],
];
@ -78,7 +88,6 @@ class Plugin implements PluginInterface, EventSubscriberInterface
public function onInstall(PackageEvent $event): void
{
\var_dumP($event);
}
/**
@ -90,6 +99,5 @@ class Plugin implements PluginInterface, EventSubscriberInterface
*/
public function onDependencySolve(InstallerEvent $event): void
{
\var_dump($event);
}
}

View File

@ -1,226 +0,0 @@
<?php
namespace Phabel\Composer;
use Composer\DependencyResolver\Pool;
use Composer\Package\Link;
use Composer\Package\PackageInterface;
use Composer\Repository\ComposerRepository;
use Composer\Semver\Constraint\Constraint;
use Composer\Semver\Constraint\ConstraintInterface;
/**
* @author Daniil Gentili <daniil@daniil.it>
* @license MIT
*/
class Repository extends ComposerRepository
{
/**
* Configuration prefix.
*/
private const CONFIG_PREFIX = 'phabel-config';
/**
* Previous repository .
*/
private ComposerRepository $repository;
/**
* Constructor.
*
* @param RepositoryInterface[] $repositories Previous repositories
*/
public function __construct(ComposerRepository $repository)
{
$this->repository = $repository;
$this->packages = [];
}
/**
* Get repository configuration.
*
* @return mixed
*/
public function getRepoConfig()
{
return $this->repository->getRepoConfig();
}
/**
* Set root aliases.
*
* @param array $rootAliases Root aliases
*
* @return void
*/
public function setRootAliases(array $rootAliases): void
{
$this->repository->setRootAliases($rootAliases);
}
/**
* Checks if specified package registered (installed).
*
* @param PackageInterface $package package instance
*
* @return bool
*/
public function hasPackage(PackageInterface $package): bool
{
return $this->repository->hasPackage($package);
}
/**
* Look for phabel configuration parameters in constraint.
*
* @param string|\Composer\Semver\Constraint\ConstraintInterface|null &$constraint package version or version constraint to match against
*
* @return array
*/
private static function prepareConstraint(&$constraint): array
{
if (!$constraint instanceof ConstraintInterface && !\is_string($constraint)) {
return [];
}
$constraint = (string) $constraint;
if (!str_starts_with($constraint, self::CONFIG_PREFIX)) {
return [];
}
[$config, $constraint] = \explode("\n", $constraint, 2);
return \json_decode(\substr($config, 0, \strlen(self::CONFIG_PREFIX)), true) ?: [];
}
/**
* Prepare package.
*
* @param PackageInterface $package Package
* @param array $config Configuration inherited from constraint
*
* @return void
*/
private static function preparePackage(PackageInterface $package, array $config): void
{
/**
* Phabel configuration of current package.
* @var array
*/
$myConfig = $package->getExtra()['phabel'] ?? [];
$havePhabel = false;
foreach ($package->getRequires() as $link) {
if ($link->getTarget() === 'phabel/phabel') {
$havePhabel = true;
}
if ($link->getTarget() === 'php') {
$myConfig['target'] = $link->getConstraint();
if ($havePhabel) {
break;
}
}
}
if (!$havePhabel) {
return;
}
// Config merging logic here...
$links = [];
foreach ($package->getRequires() as $link) {
$version = self::CONFIG_PREFIX.\json_encode($config)."\n".($link->getConstraint() ?? '');
$links []= new Link($link->getSource(), $link->getTarget(), new Constraint('>=', $version), $link->getDescription());
}
$package->setRequires($links);
}
/**
* Searches for the first match of a package by name and version.
*
* @param string $name package name
* @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against
*
* @return PackageInterface|null
*/
public function findPackage(string $name, $constraint): ?PackageInterface
{
$config = self::prepareConstraint($constraint);
if (!$package = $this->repository->findPackage($name, $constraint)) {
return null;
}
return self::preparePackage($package, $config);
}
/**
* Searches for all packages matching a name and optionally a version.
*
* @param string $name package name
* @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against
*
* @return PackageInterface[]
*/
public function findPackages($name, $constraint = null)
{
$config = self::prepareConstraint($constraint);
foreach ($packages = $this->repository->findPackages($name, $constraint) as $package) {
self::preparePackage($package, $config);
}
return $packages;
}
/**
* Returns list of registered packages.
*
* @return PackageInterface[]
*/
public function getPackages()
{
$packages = $this->repository->getPackages();
foreach ($packages as $package) {
self::preparePackage($package, []);
}
return $packages;
}
/**
* {@inheritDoc}
*/
public function search($query, $mode = 0, $type = null)
{
return $this->repository->search($query, $mode, $type);
}
public function getProviderNames()
{
return $this->repository->getProviderNames();
}
public function hasProviders()
{
return $this->repository->hasProviders();
}
public function resetPackageIds()
{
return $this->repository->resetPackageIds();
}
public function addPackage(PackageInterface $package)
{
$this->repository->addPackage($package);
}
/**
* @param Pool $pool
* @param string $name package name
* @param bool $bypassFilters If set to true, this bypasses the stability filtering, and forces a recompute without cache
* @return array|mixed
*/
public function whatProvides(Pool $pool, $name, $bypassFilters = false)
{
$whatProvides = $this->repository->whatProvides($pool, $name, $bypassFilters);
foreach ($whatProvides as $package => &$versions) {
foreach ($versions as &$version) {
if (!isset($version['require']['phabel/phabel'])) {
continue;
}
$config = $version['extra']['phabel'] ?? [];
if (!isset($config['target']) && isset($version['require']['php'])) {
$config['target'] = $version['require']['php'];
}
foreach ($version['require'] as $package => &$version) {
$version = self::CONFIG_PREFIX.\json_encode($config)."\n".$version;
}
}
}
return $whatProvides;
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Phabel\Composer\Traits;
trait Constraint
{
/**
* Phabel config.
*/
private array $config;
/**
* Get phabel config.
*
* @return array
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Set phabel config.
*
* @param array $config Phabel config
*
* @return self
*/
public function setConfig($config): self
{
$this->config = is_string($config) ? json_decode($config, true) : $config;
return $this;
}
}

View File

@ -0,0 +1,215 @@
<?php
namespace Phabel\Composer\Traits;
use Composer\Package\AliasPackage;
use Composer\Package\Link;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Repository\PlatformRepository;
use Composer\Semver\Constraint\Constraint as ComposerConstraint;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MultiConstraint;
use Phabel\Composer\PhabelConstraintInterface;
use Phabel\Target\Php;
use Phabel\Tools;
/**
* @author Daniil Gentili <daniil@daniil.it>
* @license MIT
*/
trait Repository
{
/**
* TODO v3 should make this private once we can drop PHP 5.3 support.
*
* @param string $name package name (must be lowercased already)
* @private
*/
public function isVersionAcceptable($constraint, $name, $versionData, array $acceptableStabilities = null, array $stabilityFlags = null)
{
self::extractConfig($constraint);
return parent::isVersionAcceptable($constraint, $name, $versionData, $acceptableStabilities, $stabilityFlags);
}
public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags, array $alreadyLoaded = [])
{
$configs = [];
foreach ($packageNameMap as $key => &$constraint) {
$configs[$key] = self::extractConfig($constraint);
}
$packages = parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded);
foreach ($packages['packages'] as &$package) {
self::preparePackage($package, $configs[$package->getName()] ?? null);
}
return $packages;
}
private static function trickleMergeConfig(?array $prev, ?array $new): ?array
{
if ($prev === null || empty($prev)) {
return $new;
}
return $prev;
}
/**
* Prepare package.
*
* @param PackageInterface $package Package
* @param ?array $config Configuration inherited from constraint
*
* @return void
*/
public static function preparePackage(PackageInterface $package, ?array $config): void
{
/**
* Phabel configuration of current package.
* @var array
*/
$myConfig = $package->getExtra()['phabel'] ?? [];
$havePhabel = false;
foreach ($package->getRequires() as $link) {
if ($link->getTarget() === 'phabel/phabel') {
$havePhabel = true;
}
if ($link->getTarget() === 'php') {
$myConfig['target'] = Php::normalizeVersion($link->getConstraint()->getLowerBound()->getVersion());
if ($havePhabel) {
break;
}
}
}
if (!$havePhabel && $config === null) {
\var_dump("Skipping ".$package->getName());
return;
}
\var_dump("Applying ".$package->getName());
$config = self::trickleMergeConfig($config, $myConfig);
self::processRequires($package, \json_encode($config));
}
private static function processRequires(PackageInterface $package, string $config)
{
$links = [];
foreach ($package->getRequires() as $link) {
if (PlatformRepository::isPlatformPackage($link->getTarget())) {
continue;
}
//var_dumP($link->getTarget(), (string) $link->getConstraint());
$links []= new Link(
$link->getSource(),
$link->getTarget(),
self::injectConfig($link->getConstraint(), $config),
$link->getDescription(),
$link->getPrettyConstraint()
);
}
if ($package instanceof Package) {
$package->setRequires($links);
} elseif ($package instanceof AliasPackage) {
Tools::setVar($package, 'requires', $links);
self::processRequires($package->getAliasOf(), $config);
}
}
/**
* Inject config into constraint.
*
* @param ConstraintInterface $constraint
* @param string $config
* @return ConstraintInterface
*/
private static function injectConfig(ConstraintInterface $constraint, string $config): ConstraintInterface
{
if ($constraint instanceof ComposerConstraint) {
$version = $constraint->getVersion();
$version = "phabel$version\0$config";
return new ComposerConstraint($constraint->getOperator(), $version);
} elseif ($constraint instanceof MultiConstraint) {
$constraints = $constraint->getConstraints();
foreach ($constraints as &$cur) {
$cur = self::injectConfig($cur, $config);
}
return new MultiConstraint($constraints, $constraint->isConjunctive());
}
return $constraint;
}
/**
* Look for phabel configuration parameters in constraint.
*
* @param \Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against
*
* @return ?array
*/
public static function extractConfig(ConstraintInterface &$constraint): ?array
{
$config = null;
if ($constraint instanceof ComposerConstraint) {
$version = $constraint->getVersion();
if (str_starts_with($version, 'phabel')) {
[$version, $config] = \explode("\0", \substr($version, 6));
$config = \json_decode($config, true);
if ($config === false) {
$config = null;
}
$constraint = new ComposerConstraint($constraint->getOperator(), $version);
}
} elseif ($constraint instanceof MultiConstraint) {
$constraints = $constraint->getConstraints() ;
foreach ($constraints as &$cur) {
$config = self::trickleMergeConfig($config, self::extractConfig($cur));
}
$constraint = new MultiConstraint($constraints, $constraint->isConjunctive());
}
return $config;
}
/**
* Searches for the first match of a package by name and version.
*
* @param string $name package name
* @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against
*
* @return PackageInterface|null
*/
public function findPackage($name, $constraint)
{
$config = self::extractConfig($constraint);
if (!$package = parent::findPackage($name, $constraint)) {
return null;
}
return self::preparePackage($package, $config);
}
/**
* Searches for all packages matching a name and optionally a version.
*
* @param string $name package name
* @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against
*
* @return PackageInterface[]
*/
public function findPackages($name, $constraint = null)
{
$config = self::extractConfig($constraint);
foreach ($packages = parent::findPackages($name, $constraint) as $package) {
self::preparePackage($package, $config);
}
return $packages;
}
/**
* Returns list of registered packages.
*
* @return PackageInterface[]
*/
public function getPackages()
{
$packages = parent::getPackages();
foreach ($packages as $package) {
self::preparePackage($package, null);
}
return $packages;
}
}

View File

@ -308,7 +308,7 @@ class Context
}
/**
* Gets name context
* Gets name context.
*
* @return NameContext
*/

View File

@ -119,35 +119,35 @@ abstract class Plugin extends Tools implements PluginInterface
/**
* {@inheritDoc}
*/
public static function composerRequires(): array
public static function composerRequires(array $config): array
{
return [];
}
/**
* {@inheritDoc}
*/
public static function runAfter(): array
public static function runAfter(array $config): array
{
return [];
}
/**
* {@inheritDoc}
*/
public static function runBefore(): array
public static function runBefore(array $config): array
{
return [];
}
/**
* {@inheritDoc}
*/
public static function runWithBefore(): array
public static function runWithBefore(array $config): array
{
return [];
}
/**
* {@inheritDoc}
*/
public static function runWithAfter(): array
public static function runWithAfter(array $config): array
{
return [];
}

View File

@ -62,7 +62,7 @@ class GraphInternal
* @param PackageContext $ctx Package context
*
* @psalm-param class-string<PluginInterface> $plugin Plugin to add
*
*
* @return Node[]
*/
public function addPlugin(string $plugin, array $config, PackageContext $ctx): array

View File

@ -24,7 +24,7 @@ interface PluginInterface
*
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public static function runAfter(): array;
public static function runAfter(array $config): array;
/**
* Specify which plugins should run before this plugin.
*
@ -32,7 +32,7 @@ interface PluginInterface
*
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public static function runBefore(): array;
public static function runBefore(array $config): array;
/**
* Specify which plugins does this plugin extend.
*
@ -44,21 +44,21 @@ interface PluginInterface
*
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public static function runWithBefore(): array;
public static function runWithBefore(array $config): array;
/**
*
* @return array Plugin name(s)
*
* @psalm-return class-string<PluginInterface>[]|array<class-string<PluginInterface>, array>
*/
public static function runWithAfter(): array;
public static function runWithAfter(array $config): array;
/**
* Specify a list of composer dependencies.
*
* @return array
*/
public static function composerRequires(): array;
public static function composerRequires(array $config): array;
/**
* Set configuration array.
*

View File

@ -3,7 +3,6 @@
namespace Phabel\Target;
use Phabel\Plugin;
use Phabel\Target\Php55\YieldDetector;
/**
* Makes changes necessary to polyfill syntaxes of various PHP versions.
@ -30,6 +29,14 @@ class Php extends Plugin
* Default target.
*/
private const DEFAULT_TARGET = '70';
public static function normalizeVersion(string $target): string
{
if (\preg_match(":^\D*(\d+\.\d+)\..*:", $target, $matches)) {
$target = $matches[1];
}
$target = \str_replace('.', '', $target);
return in_array($target, self::VERSIONS) ? $target : self::DEFAULT_TARGET;
}
/**
* Get PHP version range to target.
*
@ -39,10 +46,10 @@ class Php extends Plugin
private static function getRange(array $config): array
{
$target = $config['target'] ?? PHP_MAJOR_VERSION.PHP_MINOR_VERSION;
if (preg_match(":^\D*(\d+\.\d+)\..*:", $config['target'], $matches)) {
if (\preg_match(":^\D*(\d+\.\d+)\..*:", $config['target'], $matches)) {
$target = $matches[1];
}
$key = \array_search(str_replace('.', '', $target), self::VERSIONS);
$key = \array_search(\str_replace('.', '', $target), self::VERSIONS);
return \array_slice(
self::VERSIONS,
$key === false ? self::DEFAULT_TARGET : $key
@ -59,12 +66,42 @@ class Php extends Plugin
{
$classes = [];
foreach (self::getRange($config) as $version) {
foreach (scandir(__DIR__."/Php$version") as $file) {
if (substr($file, -4) !== '.php') continue;
$class = basename($version, '.php');
foreach (\scandir(__DIR__."/Php$version") as $file) {
if (\substr($file, -4) !== '.php') {
continue;
}
$class = \basename($version, '.php');
$classes[$class] = $config[$class] ?? [];
}
}
return $classes;
}
/**
* {@inheritDoc}
*
public static function mergeConfigs(array ...$configs): array
{
$configsByTarget = [];
foreach ($configs as $config) {
$configsByTarget[$config['target'] ?? PHP_MAJOR_VERSION.PHP_MINOR_VERSION] = [
$config
];
}
return $configs;
}
/**
* {@inheritDoc}
*
public static function splitConfig(array $config): array
{
$target = $config['target'] = $config['target'] ?? PHP_MAJOR_VERSION.PHP_MINOR_VERSION;
unset($config['target']);
$chunks = array_chunk($target, 1, true);
foreach ($chunks as $k => $chunk) {
$chunks[$k]['target'] = $target;
}
return $chunks;
}*/
}

View File

@ -63,10 +63,10 @@ abstract class Tools
$nodeNew->setAttributes($node->getAttributes());
return $nodeNew;
}
return new $class(
return new $class(...[
...\array_map(fn (string $name) => $node->{$name}, $node->getSubNodeNames()),
$node->getAttributes()
);
]);
}
/**
* Replace type in-place.
@ -216,4 +216,116 @@ abstract class Tools
}
return false;
}
/**
* Create a new object extended from this object, with the specified additional trait + interface
*
* @param object $obj
* @param string $trait
* @param string $interface
*
* @return object
*/
public static function cloneWithTrait(object $obj, string $trait, string $interface): object
{
$reflect = new ReflectionClass($obj);
$r = $reflect;
while ($r && $r->isAnonymous()) {
$r = $r->getParentClass();
}
$extend = "extends \\".$r->getName();
$eval = "\$newObj = new class $extend implements \\$interface {
use \\$trait;
public function __construct() {}
};";
eval($eval);
$reflectNew = new ReflectionClass($newObj);
do {
if ($tmp = $reflectNew->getParentClass()) {
$reflectNew = $tmp;
}
foreach ($reflect->getProperties() as $prop) {
if ($reflectNew->hasProperty($prop->getName())) {
$propNew = $reflectNew->getProperty($prop->getName());
$propNew->setAccessible(true);
$prop->setAccessible(true);
$propNew->setValue($newObj, $prop->getValue($obj));
}
}
} while ($reflect = $reflect->getParentClass());
return $newObj;
}
/**
* Checks private property exists in an object.
*
* @param object $obj Object
* @param string $var Attribute name
*
* @psalm-suppress InvalidScope
*
* @return bool
* @access public
*/
public static function hasVar($obj, string $var): bool
{
return \Closure::bind(
function () use ($var) {
return isset($this->{$var});
},
$obj,
\get_class($obj)
)->__invoke();
}
/**
* Accesses a private variable from an object.
*
* @param object $obj Object
* @param string $var Attribute name
*
* @psalm-suppress InvalidScope
*
* @return mixed
* @access public
*/
public static function &getVar($obj, string $var)
{
return \Closure::bind(
function & () use ($var) {
return $this->{$var};
},
$obj,
\get_class($obj)
)->__invoke();
}
/**
* Sets a private variable in an object.
*
* @param object $obj Object
* @param string $var Attribute name
* @param mixed $val Attribute value
*
* @psalm-suppress InvalidScope
*
* @return void
*
* @access public
*/
public static function setVar($obj, string $var, &$val): void
{
\Closure::bind(
function () use ($var, &$val) {
$this->{$var} =& $val;
},
$obj,
\get_class($obj)
)->__invoke();
}
}

View File

@ -3,7 +3,6 @@
namespace Phabel;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\Parser;
use PhpParser\ParserFactory;
use SplQueue;