update after stop build
All checks were successful
All checks were successful
This commit is contained in:
92
vendor/shalvah/upgrader/src/ComparesAstNodes.php
vendored
Normal file
92
vendor/shalvah/upgrader/src/ComparesAstNodes.php
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Shalvah\Upgrader;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\ArrayItem;
|
||||
use PhpParser\NodeDumper;
|
||||
use PhpParser\PrettyPrinter;
|
||||
|
||||
trait ComparesAstNodes
|
||||
{
|
||||
/**
|
||||
* @param Expr\ArrayItem[] $arrayItems
|
||||
* @param string $key
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getItem(array $arrayItems, string $key)
|
||||
{
|
||||
return Arr::first(
|
||||
// @phpstan-ignore-next-line PHPStan doesn't yet support ??
|
||||
$arrayItems, fn(ArrayItem $node) => ($node->key->value ?? null) === $key
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Expr\ArrayItem[] $arrayItems
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasItem(array $arrayItems, string $key): bool
|
||||
{
|
||||
return boolval($this->getItem($arrayItems, $key));
|
||||
}
|
||||
|
||||
protected function expressionNodeIsArray(Expr $expressionNode): bool
|
||||
{
|
||||
return $expressionNode instanceof Expr\Array_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Expr\ArrayItem[] $arrayItems
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function arrayIsList(array $arrayItems): bool
|
||||
{
|
||||
// List arrays, like ['a', 'b', 'c'] have all `key`s as null when parsed
|
||||
return isset($arrayItems[0]) && $arrayItems[0]->key === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get values in $list that are not in $otherList.
|
||||
* Replaces array_diff($list, $otherList)
|
||||
*
|
||||
* @template T of Node
|
||||
* @param T[] $list
|
||||
* @param T[] $otherList
|
||||
*
|
||||
* @return array<array{ast: T, text: string}>
|
||||
*/
|
||||
protected function subtractOtherListFromList(array $list, array $otherList): array
|
||||
{
|
||||
$diff = [];
|
||||
// There's no easy way to compare two AST nodes for equality
|
||||
// So we'll just convert them to strings and check if they're equal
|
||||
$otherListWithItemsAsText = array_map(
|
||||
fn($item) => $this->convertAstNodesToText($item), $otherList
|
||||
);
|
||||
foreach ($list as $item) {
|
||||
$itemAsText = $this->convertAstNodesToText($item);
|
||||
if (!in_array($itemAsText, $otherListWithItemsAsText)) {
|
||||
$diff[] = ['ast' => $item, 'text' => $itemAsText];
|
||||
}
|
||||
}
|
||||
|
||||
return $diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node[]|Node $nodes
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function convertAstNodesToText($nodes): string
|
||||
{
|
||||
return (new NodeDumper())->dump($nodes);
|
||||
}
|
||||
}
|
||||
80
vendor/shalvah/upgrader/src/ModifiesAsts.php
vendored
Normal file
80
vendor/shalvah/upgrader/src/ModifiesAsts.php
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Shalvah\Upgrader;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use PhpParser\Node\ArrayItem;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node;
|
||||
|
||||
trait ModifiesAsts
|
||||
{
|
||||
protected function setValue(array &$allArrayItems, string $dotPath, $newValue)
|
||||
{
|
||||
$keySegments = explode('.', $dotPath);
|
||||
$childKey = array_pop($keySegments);
|
||||
$this->findInnerArrayByKey($allArrayItems, $keySegments, function (array $searchArray) use ($childKey, $newValue) {
|
||||
$item = Arr::first(
|
||||
// @phpstan-ignore-next-line PHPStan doesn't yet support ??
|
||||
$searchArray, fn(ArrayItem $node) => ($node->key->value ?? null) === $childKey
|
||||
);
|
||||
$item->value = $newValue;
|
||||
});
|
||||
}
|
||||
|
||||
protected function addKey(array &$allArrayItems, string $dotPath, $newItem)
|
||||
{
|
||||
$keySegments = explode('.', $dotPath);
|
||||
array_pop($keySegments);
|
||||
$this->findInnerArrayByKey($allArrayItems, $keySegments, function (array &$searchArray) use ($newItem) {
|
||||
$searchArray[] = $newItem;
|
||||
});
|
||||
}
|
||||
|
||||
protected function deleteKey(array &$allArrayItems, string $dotPath)
|
||||
{
|
||||
$keySegments = explode('.', $dotPath);
|
||||
$childKey = array_pop($keySegments);
|
||||
$this->findInnerArrayByKey($allArrayItems, $keySegments, function (array &$searchArray) use ($childKey) {
|
||||
foreach ($searchArray as $index => $item) {
|
||||
if ($item->key->value === $childKey) {
|
||||
unset($searchArray[$index]);
|
||||
}
|
||||
}
|
||||
$searchArray = array_values($searchArray);
|
||||
});
|
||||
}
|
||||
|
||||
protected function pushItemOntoList(array &$allArrayItems, string $listDotPath, $newValue)
|
||||
{
|
||||
$keySegments = explode('.', $listDotPath);
|
||||
$this->findInnerArrayByKey($allArrayItems, $keySegments, function (array &$list) use ($newValue) {
|
||||
$list[] = new Expr\ArrayItem($newValue, null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Expr\ArrayItem[] $arrayItems
|
||||
* @param array $keySegments The dot notation key, split into an array
|
||||
* @param callable $callback The operation to be performed on the found array
|
||||
*/
|
||||
protected function findInnerArrayByKey(array &$arrayItems, array $keySegments, callable $callback)
|
||||
{
|
||||
$searchArray =& $arrayItems;
|
||||
while (count($keySegments)) {
|
||||
$nextKeySegment = array_shift($keySegments);
|
||||
foreach ($searchArray as $item) {
|
||||
if (
|
||||
($item->key instanceof Node\Scalar\String_
|
||||
|| $item->key instanceof Node\Scalar\LNumber)
|
||||
&& $item->key->value === $nextKeySegment
|
||||
) {
|
||||
$searchArray =& $item->value->items;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$callback($searchArray);
|
||||
}
|
||||
}
|
||||
84
vendor/shalvah/upgrader/src/ReadsAndWritesAsts.php
vendored
Normal file
84
vendor/shalvah/upgrader/src/ReadsAndWritesAsts.php
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Shalvah\Upgrader;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeTraverser;
|
||||
use PhpParser\ParserFactory;
|
||||
use PhpParser\NodeVisitor;
|
||||
use PhpParser\PrettyPrinter;
|
||||
use PhpParser\Node\Stmt;
|
||||
|
||||
trait ReadsAndWritesAsts
|
||||
{
|
||||
|
||||
protected array $originalAst;
|
||||
protected array $originalTokens;
|
||||
|
||||
protected function parseFile(string $filePath): array
|
||||
{
|
||||
$sourceCode = file_get_contents($filePath);
|
||||
$parser = (new ParserFactory)->createForHostVersion();
|
||||
$ast = $parser->parse($sourceCode);
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor(new NodeVisitor\NameResolver(null, [
|
||||
'preserveOriginalNames' => true
|
||||
]));
|
||||
return $traverser->traverse($ast);
|
||||
}
|
||||
|
||||
protected function parseFilePreservingFormat(string $filePath): array
|
||||
{
|
||||
$sourceCode = file_get_contents($filePath);
|
||||
|
||||
// Doing this because we need to preserve the formatting when printing later
|
||||
$parser = (new ParserFactory())->createForHostVersion();
|
||||
$this->originalAst = $parser->parse($sourceCode);
|
||||
$this->originalTokens = $parser->getTokens();
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor(new NodeVisitor\CloningVisitor());
|
||||
$traverser->addVisitor(new NodeVisitor\NameResolver(null, [
|
||||
'preserveOriginalNames' => true
|
||||
]));
|
||||
|
||||
return $traverser->traverse($this->originalAst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print out the changes into the user's config file (saving the old one as a backup)
|
||||
*/
|
||||
protected function writeAstToFile(array $newConfigAst, string $configFilePath)
|
||||
{
|
||||
$newConfigAst = $this->cleanUpAstForPrinting($newConfigAst);
|
||||
|
||||
$prettyPrinter = new PrettyPrinter\Standard(['shortArraySyntax' => true]);
|
||||
$astAsText = $prettyPrinter->printFormatPreserving($newConfigAst, $this->originalAst, $this->originalTokens);
|
||||
|
||||
rename($configFilePath, "$configFilePath.bak");
|
||||
file_put_contents($configFilePath, $astAsText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorten namespaces back before printing and add any missing use statements
|
||||
*/
|
||||
protected function cleanUpAstForPrinting(array $ast): array
|
||||
{
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor(new UnresolveNamespaces);
|
||||
$ast = $traverser->traverse($ast);
|
||||
|
||||
$sampleConfigAst = $this->getSampleNewConfigFileAsAst();
|
||||
$newUseStatements = Arr::where(
|
||||
$sampleConfigAst, fn(Node $node) => $node instanceof Stmt\Use_
|
||||
);
|
||||
$alreadyPresentUseStatements = Arr::where(
|
||||
$ast, fn(Node $node) => $node instanceof Stmt\Use_
|
||||
);
|
||||
$newUseStatements = $this->subtractOtherListFromList($newUseStatements, $alreadyPresentUseStatements);
|
||||
foreach ($newUseStatements as $newUseStatement) {
|
||||
array_unshift($ast, $newUseStatement['ast']);
|
||||
}
|
||||
return $ast;
|
||||
}
|
||||
}
|
||||
21
vendor/shalvah/upgrader/src/UnresolveNamespaces.php
vendored
Normal file
21
vendor/shalvah/upgrader/src/UnresolveNamespaces.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Shalvah\Upgrader;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\NodeVisitor;
|
||||
|
||||
class UnresolveNamespaces implements NodeVisitor
|
||||
{
|
||||
public function leaveNode(Node $node) {
|
||||
if ($node instanceof Node\Name\FullyQualified) {
|
||||
return $node->getAttribute('originalName', $node);
|
||||
}
|
||||
}
|
||||
|
||||
public function beforeTraverse(array $nodes) {}
|
||||
|
||||
public function enterNode(Node $node) {}
|
||||
|
||||
public function afterTraverse(array $nodes) {}
|
||||
}
|
||||
277
vendor/shalvah/upgrader/src/Upgrader.php
vendored
Normal file
277
vendor/shalvah/upgrader/src/Upgrader.php
vendored
Normal file
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Shalvah\Upgrader;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use PhpParser;
|
||||
use PhpParser\{Node,
|
||||
Node\Stmt,
|
||||
Node\Expr};
|
||||
|
||||
class Upgrader
|
||||
{
|
||||
use ReadsAndWritesAsts, ComparesAstNodes, ModifiesAsts;
|
||||
|
||||
public const CHANGE_REMOVED = 'removed';
|
||||
public const CHANGE_MOVED = 'moved';
|
||||
public const CHANGE_ADDED = 'added';
|
||||
public const CHANGE_LIST_ITEM_ADDED = 'added_to_list';
|
||||
|
||||
protected array $changes = [];
|
||||
protected array $configFiles = [];
|
||||
protected array $movedKeys = [];
|
||||
protected array $dontTouchKeys = [];
|
||||
|
||||
/** @var Stmt[] */
|
||||
protected ?array $userOldConfigFileAst = [];
|
||||
/** @var Stmt[] */
|
||||
protected ?array $sampleNewConfigFileAst = [];
|
||||
|
||||
public function __construct(string $userOldConfigRelativePath, string $sampleNewConfigAbsolutePath)
|
||||
{
|
||||
$this->configFiles['user_old'] = $userOldConfigRelativePath;
|
||||
$this->configFiles['sample_new'] = $sampleNewConfigAbsolutePath;
|
||||
}
|
||||
|
||||
public static function ofConfigFile(string $userOldConfigRelativePath, string $sampleNewConfigAbsolutePath): self
|
||||
{
|
||||
return new self($userOldConfigRelativePath, $sampleNewConfigAbsolutePath);
|
||||
}
|
||||
|
||||
public function move(string $oldKey, string $newKey): self
|
||||
{
|
||||
$this->movedKeys[$oldKey] = $newKey;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Don't touch" these config items.
|
||||
* Useful if they contain arrays with keys specified by the user,
|
||||
* or lists with values provided entirely by the user
|
||||
*/
|
||||
public function dontTouch(string ...$keys): self
|
||||
{
|
||||
$this->dontTouchKeys = [...$this->dontTouchKeys, ...$keys];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dryRun(): array
|
||||
{
|
||||
return $this->fetchChanges();
|
||||
}
|
||||
|
||||
public function upgrade()
|
||||
{
|
||||
$this->fetchChanges();
|
||||
$upgradedConfig = $this->applyChanges();
|
||||
$this->writeAstToFile($upgradedConfig, $this->configFiles['user_old']);
|
||||
}
|
||||
|
||||
protected function fetchChanges(): array
|
||||
{
|
||||
if (!empty($this->changes)) {
|
||||
return $this->changes;
|
||||
}
|
||||
|
||||
[$userCurrentConfigFile, $sampleNewConfigFile] = $this->parseConfigFiles();
|
||||
|
||||
$userCurrentConfigArray = Arr::first(
|
||||
$userCurrentConfigFile, fn(Node $node) => $node instanceof Stmt\Return_
|
||||
)->expr->items;
|
||||
$sampleNewConfigArray = Arr::first(
|
||||
$sampleNewConfigFile, fn(Node $node) => $node instanceof Stmt\Return_
|
||||
)->expr->items;
|
||||
$this->fetchAddedItems($userCurrentConfigArray, $sampleNewConfigArray);
|
||||
$this->fetchRemovedAndMovedItems($userCurrentConfigArray, $sampleNewConfigArray);
|
||||
return $this->changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Expr\ArrayItem[] $userCurrentConfig
|
||||
* @param Expr\ArrayItem[] $incomingConfig
|
||||
*/
|
||||
protected function fetchAddedItems(
|
||||
array $userCurrentConfig, array $incomingConfig, string $rootKey = ''
|
||||
)
|
||||
{
|
||||
if ($this->arrayIsList($incomingConfig)) {
|
||||
// We're dealing with a list of items (numeric array)
|
||||
$diff = $this->subtractOtherListFromList($incomingConfig, $userCurrentConfig);
|
||||
foreach ($diff as $item) {
|
||||
$this->changes[] = [
|
||||
'type' => self::CHANGE_LIST_ITEM_ADDED,
|
||||
'key' => $rootKey,
|
||||
'value' => $item['ast']->value,
|
||||
'description' => "- '{$item['text']}' will be added to `$rootKey`.",
|
||||
];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO handle cases of mixed assoc- and list array
|
||||
foreach ($incomingConfig as $arrayItem) {
|
||||
// @phpstan-ignore-next-line Not yet sure how to handle mixed
|
||||
$key = $arrayItem->key->value;
|
||||
$value = $arrayItem->value;
|
||||
|
||||
$fullKey = $this->getFullKey($key, $rootKey);
|
||||
if ($this->shouldntTouch($fullKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key is in new, but not in old
|
||||
if (!$this->hasItem($userCurrentConfig, $key)) {
|
||||
$this->changes[] = [
|
||||
'type' => self::CHANGE_ADDED,
|
||||
'key' => $fullKey,
|
||||
'description' => "- `{$fullKey}` will be added.",
|
||||
'value' => $value,
|
||||
'item' => $this->getItem($incomingConfig, $key),
|
||||
];
|
||||
} else {
|
||||
if ($this->expressionNodeIsArray($value)) {
|
||||
// Key is in both old and new; recurse into array and compare the inner items
|
||||
$this->fetchAddedItems(
|
||||
$this->getItem($userCurrentConfig, $key)->value->items ?? [], $value->items ?? [], $fullKey
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Expr\ArrayItem[] $userCurrentConfig
|
||||
* @param Expr\ArrayItem[]|null $incomingConfig
|
||||
*/
|
||||
protected function fetchRemovedAndMovedItems(
|
||||
array $userCurrentConfig, $incomingConfig, string $rootKey = ''
|
||||
)
|
||||
{
|
||||
if ($this->arrayIsList($incomingConfig)) {
|
||||
// A list of items (numeric array)
|
||||
// We only add, not remove.
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop over the old config
|
||||
// TODO handle cases of mixed assoc- and list array
|
||||
foreach ($userCurrentConfig as $arrayItem) {
|
||||
// @phpstan-ignore-next-line Not yet sure how to handle mixed
|
||||
$key = $arrayItem->key->value;
|
||||
$value = $arrayItem->value;
|
||||
|
||||
$fullKey = $this->getFullKey($key, $rootKey);
|
||||
|
||||
// Key is in old, but was moved somewhere else in new
|
||||
if ($this->wasKeyMoved($fullKey)) {
|
||||
$this->changes[] = [
|
||||
'type' => self::CHANGE_MOVED,
|
||||
'key' => $fullKey,
|
||||
'new_key' => $this->movedKeys[$fullKey],
|
||||
'description' => "- `$fullKey` will be moved to `{$this->movedKeys[$fullKey]}`.",
|
||||
'new_value' => $value,
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key is in old, but not in new
|
||||
if (!$this->hasItem($incomingConfig, $key)) {
|
||||
$this->changes[] = [
|
||||
'type' => self::CHANGE_REMOVED,
|
||||
'key' => $fullKey,
|
||||
'description' => "- `$fullKey` will be removed.",
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->shouldntTouch($fullKey) && $this->expressionNodeIsArray($value)) {
|
||||
// Key is in both old and new; recurse into array and compare the inner items
|
||||
$this->fetchRemovedAndMovedItems(
|
||||
$value->items ?? [], $this->getItem($incomingConfig, $key)->value->items ?? [], $fullKey
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function wasKeyMoved(string $oldKey): bool
|
||||
{
|
||||
return array_key_exists($oldKey, $this->movedKeys);
|
||||
}
|
||||
|
||||
protected function shouldntTouch(string $key): bool
|
||||
{
|
||||
return in_array($key, $this->dontTouchKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve config item key with dot notation
|
||||
*/
|
||||
private function getFullKey(string $key, string $rootKey = ''): string
|
||||
{
|
||||
if (empty($rootKey)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
return "$rootKey.$key";
|
||||
}
|
||||
|
||||
public function parseConfigFiles(): array
|
||||
{
|
||||
$userCurrentConfig = $this->getUserOldConfigFileAsAst();
|
||||
$incomingConfig = $this->getSampleNewConfigFileAsAst();
|
||||
|
||||
return [$userCurrentConfig, $incomingConfig];
|
||||
}
|
||||
|
||||
protected function getUserOldConfigFileAsAst(): array
|
||||
{
|
||||
if (!empty($this->userOldConfigFileAst)) {
|
||||
return $this->userOldConfigFileAst;
|
||||
}
|
||||
|
||||
$this->userOldConfigFileAst = $this->parseFilePreservingFormat($this->configFiles['user_old']);
|
||||
return $this->userOldConfigFileAst;
|
||||
}
|
||||
|
||||
protected function getSampleNewConfigFileAsAst(): array
|
||||
{
|
||||
if (!empty($this->sampleNewConfigFileAst)) {
|
||||
return $this->sampleNewConfigFileAst;
|
||||
}
|
||||
|
||||
$this->sampleNewConfigFileAst = $this->parseFile($this->configFiles['sample_new']);
|
||||
return $this->sampleNewConfigFileAst;
|
||||
}
|
||||
|
||||
protected function applyChanges(): array
|
||||
{
|
||||
$userConfigAst = $this->getUserOldConfigFileAsAst();
|
||||
$configArray =& Arr::first(
|
||||
$userConfigAst, fn(Node $node) => $node instanceof Stmt\Return_
|
||||
)->expr->items;
|
||||
|
||||
foreach ($this->changes as $change) {
|
||||
switch ($change['type']) {
|
||||
case self::CHANGE_ADDED:
|
||||
$this->addKey($configArray, $change['key'], $change['item']);
|
||||
break;
|
||||
case self::CHANGE_REMOVED:
|
||||
$this->deleteKey($configArray, $change['key']);
|
||||
break;
|
||||
case self::CHANGE_MOVED:
|
||||
// Move old value to new key
|
||||
$this->setValue($configArray, $change['new_key'], $change['new_value']);
|
||||
// Then delete old key
|
||||
$this->deleteKey($configArray, $change['key']);
|
||||
break;
|
||||
case self::CHANGE_LIST_ITEM_ADDED:
|
||||
$this->pushItemOntoList($configArray, $change['key'], $change['value']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $userConfigAst;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user