1
0
mirror of https://github.com/flarum/core.git synced 2025-07-23 17:51:24 +02:00
Files
php-flarum/php-packages/phpstan/src/Properties/SchemaAggregator.php
2021-12-28 20:45:22 -05:00

433 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use Illuminate\Support\Str;
use PhpParser;
use PhpParser\NodeFinder;
/**
* @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaAggregator.php
*/
final class SchemaAggregator
{
/** @var array<string, SchemaTable> */
public $tables = [];
/**
* @param array<int, PhpParser\Node\Stmt> $stmts
*/
public function addStatements(array $stmts): void
{
$nodeFinder = new NodeFinder();
/** @var PhpParser\Node\Stmt\Class_[] $classes */
$classes = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Class_::class);
foreach ($classes as $stmt) {
$this->addClassStatements($stmt->stmts);
}
}
/**
* @param array<int, PhpParser\Node\Stmt> $stmts
*/
private function addClassStatements(array $stmts): void
{
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
&& $stmt->name->name !== 'down'
&& $stmt->stmts
) {
$this->addUpMethodStatements($stmt->stmts);
}
}
}
/**
* @param PhpParser\Node\Stmt[] $stmts
*/
private function addUpMethodStatements(array $stmts): void
{
$nodeFinder = new NodeFinder();
$methods = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Expression::class);
foreach ($methods as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\StaticCall
&& ($stmt->expr->class instanceof PhpParser\Node\Name)
&& $stmt->expr->name instanceof PhpParser\Node\Identifier
&& ($stmt->expr->class->toCodeString() === '\Illuminate\Support\Facades\Schema' || $stmt->expr->class->toCodeString() === '\Schema')
) {
switch ($stmt->expr->name->name) {
case 'create':
$this->alterTable($stmt->expr, true);
break;
case 'table':
$this->alterTable($stmt->expr, false);
break;
case 'drop':
case 'dropIfExists':
$this->dropTable($stmt->expr);
break;
case 'rename':
$this->renameTable($stmt->expr);
}
}
}
}
private function alterTable(PhpParser\Node\Expr\StaticCall $call, bool $creating): void
{
if (! isset($call->args[0])
|| ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
) {
return;
}
$tableName = $call->getArgs()[0]->value->value;
if ($creating) {
$this->tables[$tableName] = new SchemaTable($tableName);
}
if (! isset($call->args[1])
|| ! $call->getArgs()[1]->value instanceof PhpParser\Node\Expr\Closure
|| count($call->getArgs()[1]->value->params) < 1
|| ($call->getArgs()[1]->value->params[0]->type instanceof PhpParser\Node\Name
&& $call->getArgs()[1]->value->params[0]->type->toCodeString()
!== '\\Illuminate\Database\Schema\Blueprint')
) {
return;
}
$updateClosure = $call->getArgs()[1]->value;
if ($call->getArgs()[1]->value->params[0]->var instanceof PhpParser\Node\Expr\Variable
&& is_string($call->getArgs()[1]->value->params[0]->var->name)
) {
$argName = $call->getArgs()[1]->value->params[0]->var->name;
$this->processColumnUpdates($tableName, $argName, $updateClosure->stmts);
}
}
/**
* @param string $tableName
* @param string $argName
* @param PhpParser\Node\Stmt[] $stmts
*
* @throws \Exception
*/
private function processColumnUpdates(string $tableName, string $argName, array $stmts): void
{
if (! isset($this->tables[$tableName])) {
return;
}
$table = $this->tables[$tableName];
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\MethodCall
&& $stmt->expr->name instanceof PhpParser\Node\Identifier
) {
$rootVar = $stmt->expr;
$firstMethodCall = $rootVar;
$nullable = false;
while ($rootVar instanceof PhpParser\Node\Expr\MethodCall) {
if ($rootVar->name instanceof PhpParser\Node\Identifier
&& $rootVar->name->name === 'nullable'
) {
$nullable = true;
}
$firstMethodCall = $rootVar;
$rootVar = $rootVar->var;
}
if ($rootVar instanceof PhpParser\Node\Expr\Variable
&& $rootVar->name === $argName
&& $firstMethodCall->name instanceof PhpParser\Node\Identifier
) {
$firstArg = $firstMethodCall->getArgs()[0]->value ?? null;
$secondArg = $firstMethodCall->getArgs()[1]->value ?? null;
if ($firstMethodCall->name->name === 'foreignIdFor') {
if ($firstArg instanceof PhpParser\Node\Expr\ClassConstFetch
&& $firstArg->class instanceof PhpParser\Node\Name
) {
$modelClass = $firstArg->class->toCodeString();
} elseif ($firstArg instanceof PhpParser\Node\Scalar\String_) {
$modelClass = $firstArg->value;
} else {
continue;
}
$columnName = Str::snake(class_basename($modelClass)).'_id';
if ($secondArg instanceof PhpParser\Node\Scalar\String_) {
$columnName = $secondArg->value;
}
$table->setColumn(new SchemaColumn($columnName, 'int', $nullable));
continue;
}
if (! $firstArg instanceof PhpParser\Node\Scalar\String_) {
if ($firstMethodCall->name->name === 'timestamps'
|| $firstMethodCall->name->name === 'timestampsTz'
|| $firstMethodCall->name->name === 'nullableTimestamps'
|| $firstMethodCall->name->name === 'nullableTimestampsTz'
|| $firstMethodCall->name->name === 'rememberToken'
) {
switch (strtolower($firstMethodCall->name->name)) {
case 'droptimestamps':
case 'droptimestampstz':
$table->dropColumn('created_at');
$table->dropColumn('updated_at');
break;
case 'remembertoken':
$table->setColumn(new SchemaColumn('remember_token', 'string', $nullable));
break;
case 'dropremembertoken':
$table->dropColumn('remember_token');
break;
case 'timestamps':
case 'timestampstz':
case 'nullabletimestamps':
$table->setColumn(new SchemaColumn('created_at', 'string', true));
$table->setColumn(new SchemaColumn('updated_at', 'string', true));
break;
}
continue;
}
if ($firstMethodCall->name->name === 'softDeletes'
|| $firstMethodCall->name->name === 'softDeletesTz'
|| $firstMethodCall->name->name === 'dropSoftDeletes'
|| $firstMethodCall->name->name === 'dropSoftDeletesTz'
) {
$columnName = 'deleted_at';
} else {
continue;
}
} else {
$columnName = $firstArg->value;
}
$secondArgArray = null;
if ($secondArg instanceof PhpParser\Node\Expr\Array_) {
$secondArgArray = [];
foreach ($secondArg->items as $array_item) {
if ($array_item !== null && $array_item->value instanceof PhpParser\Node\Scalar\String_) {
$secondArgArray[] = $array_item->value->value;
}
}
}
switch (strtolower($firstMethodCall->name->name)) {
case 'biginteger':
case 'increments':
case 'integer':
case 'integerincrements':
case 'mediumincrements':
case 'mediuminteger':
case 'smallincrements':
case 'smallinteger':
case 'tinyincrements':
case 'tinyinteger':
case 'unsignedbiginteger':
case 'unsignedinteger':
case 'unsignedmediuminteger':
case 'unsignedsmallinteger':
case 'unsignedtinyinteger':
case 'bigincrements':
$table->setColumn(new SchemaColumn($columnName, 'int', $nullable));
break;
case 'char':
case 'datetimetz':
case 'date':
case 'datetime':
case 'ipaddress':
case 'json':
case 'jsonb':
case 'linestring':
case 'longtext':
case 'macaddress':
case 'mediumtext':
case 'multilinestring':
case 'string':
case 'text':
case 'time':
case 'timestamp':
case 'uuid':
case 'binary':
$table->setColumn(new SchemaColumn($columnName, 'string', $nullable));
break;
case 'boolean':
$table->setColumn(new SchemaColumn($columnName, 'bool', $nullable));
break;
case 'geometry':
case 'geometrycollection':
case 'multipoint':
case 'multipolygon':
case 'multipolygonz':
case 'point':
case 'polygon':
case 'computed':
$table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
break;
case 'double':
case 'float':
case 'unsigneddecimal':
case 'decimal':
$table->setColumn(new SchemaColumn($columnName, 'float', $nullable));
break;
case 'after':
if ($secondArg instanceof PhpParser\Node\Expr\Closure
&& $secondArg->params[0]->var instanceof PhpParser\Node\Expr\Variable
&& ! ($secondArg->params[0]->var->name instanceof PhpParser\Node\Expr)) {
$argName = $secondArg->params[0]->var->name;
$this->processColumnUpdates($tableName, $argName, $secondArg->stmts);
}
break;
case 'dropcolumn':
case 'dropifexists':
case 'dropsoftdeletes':
case 'dropsoftdeletestz':
case 'removecolumn':
case 'drop':
$table->dropColumn($columnName);
break;
case 'dropforeign':
case 'dropindex':
case 'dropprimary':
case 'dropunique':
case 'foreign':
case 'index':
case 'primary':
case 'renameindex':
case 'spatialIndex':
case 'unique':
case 'dropspatialindex':
break;
case 'dropmorphs':
$table->dropColumn($columnName.'_type');
$table->dropColumn($columnName.'_id');
break;
case 'enum':
$table->setColumn(new SchemaColumn($columnName, 'enum', $nullable, $secondArgArray));
break;
case 'morphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', $nullable));
$table->setColumn(new SchemaColumn($columnName.'_id', 'int', $nullable));
break;
case 'nullablemorphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', true));
$table->setColumn(new SchemaColumn($columnName.'_id', 'int', true));
break;
case 'nullableuuidmorphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', true));
$table->setColumn(new SchemaColumn($columnName.'_id', 'string', true));
break;
case 'rename':
case 'renamecolumn':
if ($secondArg instanceof PhpParser\Node\Scalar\String_) {
$table->renameColumn($columnName, $secondArg->value);
}
break;
case 'set':
$table->setColumn(new SchemaColumn($columnName, 'set', $nullable, $secondArgArray));
break;
case 'softdeletestz':
case 'timestamptz':
case 'timetz':
case 'year':
case 'softdeletes':
$table->setColumn(new SchemaColumn($columnName, 'string', true));
break;
case 'uuidmorphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', $nullable));
$table->setColumn(new SchemaColumn($columnName.'_id', 'string', $nullable));
break;
default:
// We know a property exists with a name, we just don't know its type.
$table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
break;
}
}
}
}
}
private function dropTable(PhpParser\Node\Expr\StaticCall $call): void
{
if (! isset($call->args[0])
|| ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
) {
return;
}
$tableName = $call->getArgs()[0]->value->value;
unset($this->tables[$tableName]);
}
private function renameTable(PhpParser\Node\Expr\StaticCall $call): void
{
if (! isset($call->args[0], $call->args[1])
|| ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
|| ! $call->getArgs()[1]->value instanceof PhpParser\Node\Scalar\String_
) {
return;
}
$oldTableName = $call->getArgs()[0]->value->value;
$newTableName = $call->getArgs()[1]->value->value;
if (! isset($this->tables[$oldTableName])) {
return;
}
$table = $this->tables[$oldTableName];
unset($this->tables[$oldTableName]);
$table->name = $newTableName;
$this->tables[$newTableName] = $table;
}
}