|
@@ -1,454 +0,0 @@
|
|
|
-<?php
|
|
|
-
|
|
|
-
|
|
|
-namespace Knuckles\Scribe\Tools;
|
|
|
-
|
|
|
-
|
|
|
-use Illuminate\Support\Arr;
|
|
|
-use PhpParser;
|
|
|
-use PhpParser\{Node, NodeFinder, Lexer, NodeTraverser, NodeVisitor, Parser, ParserFactory, PrettyPrinter};
|
|
|
-
|
|
|
-class Upgrader
|
|
|
-{
|
|
|
- public const CHANGE_REMOVED = 'removed';
|
|
|
- public const CHANGE_MOVED = 'moved';
|
|
|
- public const CHANGE_ADDED = 'added';
|
|
|
- public const CHANGE_ARRAY_ITEM_ADDED = 'added_to_array';
|
|
|
-
|
|
|
- private array $configFiles = [];
|
|
|
- private array $movedKeys = [];
|
|
|
- private array $dontTouchKeys = [];
|
|
|
- private array $userFacingChanges = [];
|
|
|
- private array $catchUps = [];
|
|
|
-
|
|
|
- /** @var Node\Stmt[] */
|
|
|
- private ?array $outgoingConfigFileAst = [];
|
|
|
- /** @var Node[] */
|
|
|
- private array $incomingConfigFileAstForModification = [];
|
|
|
- /** @var Node\Stmt[]|null */
|
|
|
- private ?array $incomingConfigFileOriginalAst;
|
|
|
- private array $incomingConfigFileOriginalTokens;
|
|
|
-
|
|
|
- public function __construct(string $userOldConfigRelativePath, string $sampleNewConfigAbsolutePath)
|
|
|
- {
|
|
|
- $this->configFiles['user_relative'] = $userOldConfigRelativePath;
|
|
|
- $this->configFiles['package_absolute'] = $sampleNewConfigAbsolutePath;
|
|
|
- }
|
|
|
-
|
|
|
- public static function ofConfigFile(string $userOldConfigRelativePath, string $sampleNewConfigAbsolutePath): self
|
|
|
- {
|
|
|
- return new self($userOldConfigRelativePath, $sampleNewConfigAbsolutePath);
|
|
|
- }
|
|
|
-
|
|
|
- public function upgrade()
|
|
|
- {
|
|
|
- $this->fetchUserFacingChanges();
|
|
|
- $this->applyChanges();
|
|
|
- }
|
|
|
-
|
|
|
- public function dryRun(): array
|
|
|
- {
|
|
|
- $this->fetchUserFacingChanges(true);
|
|
|
- return $this->userFacingChanges;
|
|
|
- }
|
|
|
-
|
|
|
- protected function fetchUserFacingChanges($forDisplay = false)
|
|
|
- {
|
|
|
- $userCurrentConfig = require(getcwd() . '/' . ltrim($this->configFiles['user_relative'], '/'));
|
|
|
- $incomingConfig = require $this->configFiles['package_absolute'];
|
|
|
-
|
|
|
- $forDisplay && $this->fetchAddedItems($userCurrentConfig, $incomingConfig);
|
|
|
- $this->fetchRemovedAndRenamedItems($userCurrentConfig, $incomingConfig);
|
|
|
- }
|
|
|
-
|
|
|
- protected function fetchRemovedAndRenamedItems(array $userCurrentConfig, $incomingConfig, string $rootKey = '')
|
|
|
- {
|
|
|
-
|
|
|
- if (is_array($incomingConfig)) {
|
|
|
- $arrayKeys = array_keys($incomingConfig);
|
|
|
- if (($arrayKeys[0] ?? null) === 0) {
|
|
|
- // We're dealing with a list of items (numeric array); will be handled by the method that fetches added items
|
|
|
- // Here, we'll just get any extra items the user added
|
|
|
-
|
|
|
- $outgoing = $this->getOutgoingConfigItem($rootKey);
|
|
|
- if (!$outgoing instanceof Node\Expr\Array_) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- /** @var Node\Expr\Array_ $incoming */
|
|
|
- $incoming = $this->getIncomingConfigItem($rootKey);
|
|
|
-
|
|
|
- foreach ($outgoing->items as $i => $outgoingItem) {
|
|
|
- if ($outgoingItem->value instanceof Node\Scalar
|
|
|
- && $incoming->items[0]->value instanceof Node\Scalar) {
|
|
|
- $inIncoming = Arr::first(
|
|
|
- $incoming->items,
|
|
|
- // @phpstan-ignore-next-line
|
|
|
- fn(Node\Expr\ArrayItem $incomingItem) => $incomingItem->value->value === $outgoingItem->value->value
|
|
|
- );
|
|
|
- if (!$inIncoming) {
|
|
|
- $this->catchUps[$rootKey . ".$i"] = $outgoingItem;
|
|
|
- }
|
|
|
- } else if ($outgoingItem->value instanceof Node\Expr\ClassConstFetch
|
|
|
- && $incoming->items[0]->value instanceof Node\Expr\ClassConstFetch) {
|
|
|
- // Handle ::class statements
|
|
|
- $inIncoming = Arr::first(
|
|
|
- $incoming->items,
|
|
|
- function (Node\Expr\ArrayItem $incomingItem) use ($outgoingItem) {
|
|
|
- // Rough equality check using final segments of class name
|
|
|
- $classNamePartsReversed = array_reverse($outgoingItem->value->class->parts);
|
|
|
- foreach ($classNamePartsReversed as $i => $classNamePart) {
|
|
|
- // @phpstan-ignore-next-line
|
|
|
- $incomingClassNamePartsReversed = array_reverse($incomingItem->value->class->parts);
|
|
|
- if (isset($incomingClassNamePartsReversed[$i])
|
|
|
- && $incomingClassNamePartsReversed[$i] === $classNamePart) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- );
|
|
|
- if (!$inIncoming) {
|
|
|
- $this->catchUps[$rootKey . ".$i"] = $outgoingItem;
|
|
|
- }
|
|
|
- } else {
|
|
|
- $this->catchUps[$rootKey . ".$i"] = $outgoingItem;
|
|
|
- }
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Loop over the old config
|
|
|
- foreach ($userCurrentConfig as $key => $value) {
|
|
|
- $fullKey = $this->getFullKey($key, $rootKey);
|
|
|
-
|
|
|
- $outgoing = $this->getOutgoingConfigItem($fullKey);
|
|
|
-
|
|
|
- // Key is in old, but was moved somewhere else in new
|
|
|
- if ($this->wasKeyMoved($fullKey)) {
|
|
|
- $this->userFacingChanges[] = [
|
|
|
- 'type' => self::CHANGE_MOVED,
|
|
|
- 'key' => $fullKey,
|
|
|
- 'new_key' => $this->movedKeys[$fullKey],
|
|
|
- 'new_value' => $outgoing,
|
|
|
- 'description' => "- `$fullKey` will be moved to `{$this->movedKeys[$fullKey]}`.",
|
|
|
- ];
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // Key is in old, but not in new
|
|
|
- if (!array_key_exists($key, $incomingConfig)) {
|
|
|
- $this->userFacingChanges[] = [
|
|
|
- 'type' => self::CHANGE_REMOVED,
|
|
|
- 'key' => $fullKey,
|
|
|
- 'description' => "- `$fullKey` will be removed.",
|
|
|
- ];
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- if (!$this->shouldntTouch($fullKey) && is_array($value)) {
|
|
|
- // Recurse into the array
|
|
|
- $this->fetchRemovedAndRenamedItems($value, data_get($incomingConfig, $key), $fullKey);
|
|
|
- } else {
|
|
|
- // This key is present in both existing and incoming configs
|
|
|
- // Save the user's value so we can replace the default in the incoming
|
|
|
- $this->catchUps[$fullKey] = $outgoing;
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Report the new items in the incoming config
|
|
|
- */
|
|
|
- protected function fetchAddedItems(array $userCurrentConfig, array $incomingConfig, string $rootKey = '')
|
|
|
- {
|
|
|
- if (is_array($incomingConfig)) {
|
|
|
- $arrayKeys = array_keys($incomingConfig);
|
|
|
- if (($arrayKeys[0] ?? null) === 0) {
|
|
|
- // We're dealing with a list of items (numeric array)
|
|
|
- $diff = array_diff($incomingConfig, $userCurrentConfig);
|
|
|
- if (!empty($diff)) {
|
|
|
- // TODO this evalutaes dynamic expressions
|
|
|
- foreach ($diff as $item) {
|
|
|
- $this->userFacingChanges[] = [
|
|
|
- 'type' => self::CHANGE_ARRAY_ITEM_ADDED,
|
|
|
- 'key' => $rootKey,
|
|
|
- 'value' => $item,
|
|
|
- 'description' => "- '$item' will be added to `$rootKey`.",
|
|
|
- ];
|
|
|
- }
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- foreach ($incomingConfig as $key => $value) {
|
|
|
- $fullKey = $this->getFullKey($key, $rootKey);
|
|
|
-
|
|
|
- if ($this->shouldntTouch($fullKey)) {
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- // Key is in new, but not in old
|
|
|
- if (!array_key_exists($key, $userCurrentConfig)) {
|
|
|
- $this->userFacingChanges[] = [
|
|
|
- 'type' => self::CHANGE_ADDED,
|
|
|
- 'key' => $fullKey,
|
|
|
- 'description' => "- `{$fullKey}` will be added.",
|
|
|
- ];
|
|
|
- } else {
|
|
|
- if (is_array($value)) {
|
|
|
- // Key is in both old and new; recurse into array and compare the inner items
|
|
|
- $this->fetchAddedItems(data_get($userCurrentConfig, $key), $value, $fullKey);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
-
|
|
|
- protected function applyChanges()
|
|
|
- {
|
|
|
- // First, get the new config file and replace defaults with user's old values
|
|
|
- $ast = $this->getIncomingConfigFileAst();
|
|
|
- foreach ($this->catchUps as $key => $value) {
|
|
|
- if (preg_match('/.*\.\d+$/', $key)) {
|
|
|
- // Array item (xxx.0, xxx.1), etc.
|
|
|
- // Discard the index and push them onto the array
|
|
|
- $this->pushValue($ast, preg_replace('/.\d+$/', '', $key), $value);
|
|
|
- } else {
|
|
|
- $this->setValue($ast, $key, $value);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- // Next, make the "migration" changes (rename config keys)
|
|
|
- foreach ($this->userFacingChanges as $change) {
|
|
|
- switch ($change['type']) {
|
|
|
- case self::CHANGE_REMOVED:
|
|
|
- // Do nothing; the new config already doesn't have this
|
|
|
- break;
|
|
|
- case self::CHANGE_MOVED:
|
|
|
- $this->setValue($ast, $change['new_key'], $change['new_value']);
|
|
|
- break;
|
|
|
- case self::CHANGE_ARRAY_ITEM_ADDED:
|
|
|
- $this->pushValue($ast, $change['key'], $change['value']);
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Finally, print out the changes into the user's config file (saving the old one as a backup)
|
|
|
- $prettyPrinter = new PrettyPrinter\Standard(['shortArraySyntax' => true]);
|
|
|
- $newCode = $prettyPrinter->printFormatPreserving($ast, $this->incomingConfigFileOriginalAst, $this->incomingConfigFileOriginalTokens);
|
|
|
-
|
|
|
- $outputFile = $this->configFiles['user_relative'];
|
|
|
- rename($outputFile, "$outputFile.bak");
|
|
|
- copy($this->configFiles['package_absolute'], $outputFile);
|
|
|
- file_put_contents($outputFile, $newCode);
|
|
|
- }
|
|
|
-
|
|
|
- protected function getOutgoingConfigItem(string $fullKey): ?Node\Expr
|
|
|
- {
|
|
|
- $ast = $this->getOutgoingConfigAst();
|
|
|
- return $this->getConfigItemNode($ast, $fullKey);
|
|
|
- }
|
|
|
-
|
|
|
- protected function getIncomingConfigItem(string $fullKey): ?Node\Expr
|
|
|
- {
|
|
|
- $ast = $this->getIncomingConfigFileAst();
|
|
|
- return $this->getConfigItemNode($ast, $fullKey);
|
|
|
- }
|
|
|
-
|
|
|
- protected function getConfigItemNode(array $configFileAst, string $fullKey): ?Node\Expr
|
|
|
- {
|
|
|
- $nodeFinder = new NodeFinder;
|
|
|
- /** @var Node\Stmt\Return_ $returnStatement */
|
|
|
- $returnStatement = $nodeFinder->findFirst(
|
|
|
- $configFileAst, fn(Node $node) => $node instanceof Node\Stmt\Return_
|
|
|
- );
|
|
|
- if (!$returnStatement->expr instanceof Node\Expr\Array_) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- $searchArray = $returnStatement->expr->items;
|
|
|
- $keySegments = explode('.', $fullKey);
|
|
|
- $foundItem = null;
|
|
|
- try {
|
|
|
- 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) {
|
|
|
- $foundItem = $item;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- if (count($keySegments) && $foundItem) {
|
|
|
- // @phpstan-ignore-next-line
|
|
|
- $searchArray = $foundItem->value->items ?? [];
|
|
|
- } else if ($foundItem) {
|
|
|
- return $foundItem->value;
|
|
|
- } else {
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (\Throwable $e) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- protected function getOutgoingConfigAst(): ?array
|
|
|
- {
|
|
|
- if (!empty($this->outgoingConfigFileAst)) {
|
|
|
- return $this->outgoingConfigFileAst;
|
|
|
- }
|
|
|
-
|
|
|
- $sourceCode = file_get_contents($this->configFiles['user_relative']);
|
|
|
-
|
|
|
- $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
|
|
- return $this->outgoingConfigFileAst = $parser->parse($sourceCode);
|
|
|
- }
|
|
|
-
|
|
|
- protected function getIncomingConfigFileAst(): ?array
|
|
|
- {
|
|
|
- if (!empty($this->incomingConfigFileAstForModification)) {
|
|
|
- return $this->incomingConfigFileAstForModification;
|
|
|
- }
|
|
|
-
|
|
|
- $sourceCode = file_get_contents($this->configFiles['package_absolute']);
|
|
|
-
|
|
|
- // Doing this because we need to preserve the formatting when printing later
|
|
|
- $lexer = new Lexer\Emulative([
|
|
|
- 'usedAttributes' => [
|
|
|
- 'comments',
|
|
|
- 'startLine', 'endLine',
|
|
|
- 'startTokenPos', 'endTokenPos',
|
|
|
- ],
|
|
|
- ]);
|
|
|
- $parser = new Parser\Php7($lexer);
|
|
|
- $this->incomingConfigFileOriginalAst = $parser->parse($sourceCode);
|
|
|
- $this->incomingConfigFileOriginalTokens = $lexer->getTokens();
|
|
|
- $traverser = new NodeTraverser();
|
|
|
- $traverser->addVisitor(new NodeVisitor\CloningVisitor());
|
|
|
- $clonedAst = $traverser->traverse($this->incomingConfigFileOriginalAst);
|
|
|
-
|
|
|
- return $this->incomingConfigFileAstForModification = $clonedAst;
|
|
|
- }
|
|
|
-
|
|
|
- protected function setValue($configFileAst, string $key, $newValue)
|
|
|
- {
|
|
|
- $nodeFinder = new NodeFinder;
|
|
|
- /** @var Node\Stmt\Return_ $returnStatement */
|
|
|
- $returnStatement = $nodeFinder->findFirst(
|
|
|
- $configFileAst, fn(Node $node) => $node instanceof Node\Stmt\Return_
|
|
|
- );
|
|
|
- if (!$returnStatement->expr instanceof Node\Expr\Array_) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- $searchArray = $returnStatement->expr->items;
|
|
|
- $keySegments = explode('.', $key);
|
|
|
- $foundItem = null;
|
|
|
- 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
|
|
|
- ) {
|
|
|
- $foundItem = $item;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (count($keySegments) && $foundItem) {
|
|
|
- // @phpstan-ignore-next-line
|
|
|
- $searchArray = $foundItem->value->items ?? [];
|
|
|
- } else if ($foundItem) {
|
|
|
- $foundItem->value = $newValue;
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- protected function pushValue($configFileAst, string $arrayKey, $newValue)
|
|
|
- {
|
|
|
- $nodeFinder = new NodeFinder;
|
|
|
- /** @var Node\Stmt\Return_ $returnStatement */
|
|
|
- $returnStatement = $nodeFinder->findFirst(
|
|
|
- $configFileAst, fn(Node $node) => $node instanceof Node\Stmt\Return_
|
|
|
- );
|
|
|
- if (!$returnStatement->expr instanceof Node\Expr\Array_) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- $searchArray = $returnStatement->expr->items;
|
|
|
- $keySegments = explode('.', $arrayKey);
|
|
|
- $foundItem = null;
|
|
|
- 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
|
|
|
- ) {
|
|
|
- $foundItem = $item;
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (count($keySegments) && $foundItem) {
|
|
|
- // @phpstan-ignore-next-line
|
|
|
- $searchArray = $foundItem->value->items ?? [];
|
|
|
- } else if ($foundItem) {
|
|
|
- // @phpstan-ignore-next-line
|
|
|
- $foundItem->value->items[] = $newValue;
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Resolve config item key with dot notation
|
|
|
- */
|
|
|
- private function getFullKey(string $key, string $rootKey = ''): string
|
|
|
- {
|
|
|
- if (empty($rootKey)) {
|
|
|
- return $key;
|
|
|
- }
|
|
|
-
|
|
|
- return "$rootKey.$key";
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * "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 += $keys;
|
|
|
- return $this;
|
|
|
- }
|
|
|
-
|
|
|
- protected function shouldntTouch(string $key): bool
|
|
|
- {
|
|
|
- return in_array($key, $this->dontTouchKeys);
|
|
|
- }
|
|
|
-
|
|
|
- public function move(string $oldKey, string $newKey): self
|
|
|
- {
|
|
|
- $this->movedKeys[$oldKey] = $newKey;
|
|
|
- return $this;
|
|
|
- }
|
|
|
-
|
|
|
- protected function wasKeyMoved(string $oldKey): bool
|
|
|
- {
|
|
|
- return array_key_exists($oldKey, $this->movedKeys);
|
|
|
- }
|
|
|
-
|
|
|
-}
|