update bug daashboards

This commit is contained in:
2026-04-18 19:47:13 +07:00
parent e2fa4d2d7b
commit ef902b2604
271 changed files with 44303 additions and 308 deletions

21
vendor/shalvah/upgrader/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Shalvah
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

54
vendor/shalvah/upgrader/README.md vendored Normal file
View File

@@ -0,0 +1,54 @@
# Upgrader
[![Latest Stable Version](https://poser.pugx.org/shalvah/upgrader/v/stable)](https://packagist.org/packages/shalvah/upgrader) [![Total Downloads](https://poser.pugx.org/shalvah/upgrader/downloads)](https://packagist.org/packages/shalvah/upgrader)
Releasing a new version of your PHP library with changes to the config file? Use this tool to offer an automated upgrade process to your users. Used for implementing automated upgrades in [Scribe](https://scribe.knuckles.wtf/laravel/migrating-v4) — just run `php artisan scribe:upgrade`.
Give `Upgrader` a sample of your new config file, and the path to the user's old config file, and it'll figure out what's been added or removed in the new version. You can also tell it to move/rename certain fields or ignore others.
```php
// Create a CLI `upgrade` command, where you call Upgrader
// Relative path to the config file in the user's project
$userOldConfigFile = 'config/my_library.php';
// Absolute path to a sample of the new config in your project
$sampleNewConfigFile = __DIR__ . '/../../config/my_library.php';
$upgrader = Upgrader::ofConfigFile($userOldConfigFile, $sampleNewConfigFile)
->move('path', 'static.path')
->dontTouch('ip_addresses');
// If this is a dry run, print the expected changes
if ($this->option('dry-run')) {
$changes = $upgrader->dryRun();
if (empty($changes)) {
$this->info("No changes needed! Looks like you're all set.");
return;
}
$this->info('The following changes will be made to your config file:');
foreach ($changes as $change) {
$this->info($change["description"]);
}
return;
}
// Otherwise, run the upgrade 🚀
$upgrader->upgrade();
```
Upgrader:
- Comes with "dry run" functionality, so you can review expected changes.
- Will back up the user's old config file to `{$file}.bak` so you can revert if you need to.
- Supports keys as dot notation
Upgrader is still early days (0.x), with more robust features and docs planned. Read how I built it [here](https://blog.shalvah.me/posts/implementing-programmatic-file-transformations-in-php).
## Installation
PHP 8+ is required.
```bash
composer require shalvah/upgrader
```

55
vendor/shalvah/upgrader/composer.json vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"name": "shalvah/upgrader",
"license": "MIT",
"description": "Create automatic upgrades for your package.",
"keywords": [
"upgrade"
],
"homepage": "http://github.com/shalvah/upgrader",
"authors": [
{
"name": "Shalvah",
"email": "hello@shalvah.me"
}
],
"require": {
"php": ">=8.0",
"illuminate/support": ">=8.0",
"nikic/php-parser": "^5.0"
},
"require-dev": {
"dms/phpunit-arraysubset-asserts": "^0.2.0",
"pestphp/pest": "^1.21",
"phpstan/phpstan": "^1.0",
"spatie/ray": "^1.33"
},
"autoload": {
"psr-4": {
"Shalvah\\Upgrader\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Shalvah\\Upgrader\\Tests\\": "tests/"
}
},
"scripts": {
"lint": "phpstan analyse -c ./phpstan.neon src --memory-limit 1G",
"test": "pest --stop-on-failure --coverage --min=90",
"test-ci": "pest"
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"process-timeout": 600,
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"funding": [
{
"type": "patreon",
"url": "https://patreon.com/shalvah"
}
]
}

6
vendor/shalvah/upgrader/phpstan.neon vendored Normal file
View File

@@ -0,0 +1,6 @@
parameters:
level: 5
reportUnmatchedIgnoredErrors: true
inferPrivatePropertyTypeFromConstructor: true
ignoreErrors:
- '#Method .+ should return .+\|null but return statement is missing.#'

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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
View 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;
}
}