Skip to content

Commit

Permalink
feat: JIRA-13731 Allow supporting database path
Browse files Browse the repository at this point in the history
  • Loading branch information
owenvoke authored and bpotmalnik committed Oct 15, 2024
1 parent f893ab8 commit fee91f0
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 31 deletions.
6 changes: 2 additions & 4 deletions larastan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rules:
- Worksome\CodingStyle\PHPStan\Laravel\DisallowPartialRouteResource\DisallowPartialRouteVariableResourceRule
- Worksome\CodingStyle\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule
- Worksome\CodingStyle\PHPStan\Laravel\DisallowEnvironmentCheck\DisallowEnvironmentCheckRule
- Worksome\CodingStyle\PHPStan\Laravel\Migrations\RequireWithoutTimestampsRule
services:
-
class: Worksome\CodingStyle\PHPStan\Laravel\DisallowPartialRouteResource\PartialRouteResourceInspector
Expand Down Expand Up @@ -81,10 +82,7 @@ services:
- secure_asset
tags:
- phpstan.rules.rule
-
class: Worksome\CodingStyle\PHPStan\Laravel\Migrations\RequireWithoutTimestampsRule
tags:
- phpstan.rules.rule

# -
# class: Vural\LarastanStrictRules\Rules\NoLocalQueryScopeRule
# tags:
Expand Down
39 changes: 16 additions & 23 deletions src/PHPStan/Laravel/Migrations/RequireWithoutTimestampsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,44 @@
namespace Worksome\CodingStyle\PHPStan\Laravel\Migrations;

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PHPStan\Analyser\Scope;
use PHPStan\Node\FileNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use Illuminate\Support\Str;

/**
* Custom PHPStan rule to check if migration files using 'update' or 'insert'
* methods include 'withoutTimestamps' somewhere in the file.
*/
class RequireWithoutTimestampsRule implements Rule
readonly class RequireWithoutTimestampsRule implements Rule
{
public function __construct(private string $migrationsPath = 'database/migrations')
{
}

public function getNodeType(): string
{
return Node\Stmt\Class_::class;
return FileNode::class;
}

/**
* @param Node\Stmt\Class_ $node
* @param FileNode $node
* @param Scope $scope
* @return string[] Errors
*/
public function processNode(Node $node, Scope $scope): array
{
$filePath = $scope->getFile();

if (! Str::contains($filePath, 'database/migrations')) {
if (!str_contains($filePath, $this->migrationsPath)) {
return [];
}

$fileContent = file_get_contents($filePath);

if (preg_match('/\b(update|insert|save|saveQuietly|create)\s*\(/', $fileContent)) {
if (!str_contains($fileContent, 'withoutTimestamps')) {
$filenameWithExtension = basename($filePath);
$filename = Str::before($filenameWithExtension, '.');

return [
RuleErrorBuilder::message(
"$filename file uses 'update' or 'create' action without 'withoutTimestamps()' protection."
)
->identifier('worksome.requireWithoutTimestamps')
->build(),
];
}
}
$traverser = new NodeTraverser();
$visitor = new WithoutTimestampsVisitor();
$traverser->addVisitor($visitor);
$traverser->traverse($node->getNodes());

return [];
return $visitor->errors;
}
}
81 changes: 81 additions & 0 deletions src/PHPStan/Laravel/Migrations/WithoutTimestampsVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Worksome\CodingStyle\PHPStan\Laravel\Migrations;

use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Rules\RuleErrorBuilder;

class WithoutTimestampsVisitor extends NodeVisitorAbstract
{
private array $contextStack = [];
public array $errors = [];

public function enterNode(Node $node)
{
// Track context for 'withoutTimestamps' method calls
if ($this->isWithoutTimestampsCall($node)) {
$this->contextStack[] = 'withoutTimestamps';
} else {
$this->contextStack[] = null;
}

if ($this->isUpdateOrInsertCall($node)) {
if (!$this->isWithinWithoutTimestampsContext() && !$this->hasWithoutTimestampsChain($node)) {
$this->errors[] = RuleErrorBuilder::message(
sprintf(
"Line %d: The '%s()' method call should be within 'withoutTimestamps()' context to prevent unintended timestamp updates.",
$node->getLine(),
$node->name->name
)
)->line($node->getLine())->build();
}
}
}

public function leaveNode(Node $node): void
{
array_pop($this->contextStack);
}

private function isWithoutTimestampsCall(Node $node): bool
{
return ($node instanceof MethodCall || $node instanceof StaticCall)
&& $node->name instanceof Node\Identifier
&& $node->name->name === 'withoutTimestamps';
}

private function isUpdateOrInsertCall(Node $node): bool
{
return ($node instanceof MethodCall || $node instanceof StaticCall)
&& $node->name instanceof Node\Identifier
&& in_array($node->name->name, ['update', 'insert', 'save', 'saveQuietly', 'create'], true);
}

private function isWithinWithoutTimestampsContext(): bool
{
return in_array('withoutTimestamps', $this->contextStack);
}

private function hasWithoutTimestampsChain(Node $node): bool
{
$currentNode = $node;

while ($currentNode instanceof MethodCall || $currentNode instanceof StaticCall) {
if ($currentNode->name instanceof Node\Identifier
&& $currentNode->name->name === 'withoutTimestamps') {
return true;
}

$currentNode = $currentNode instanceof MethodCall
? $currentNode->var
: ($currentNode instanceof StaticCall ? $currentNode->class : null);
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('test', 50)
->nullable();
});

Client::withTrashed()->update([
'test' => 'test',
]);

User::withoutTimestamps(function(){
User::withTrashed() ->update([
'test' => 'test',
]);
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ return new class extends Migration
});

User::withoutTimestamps(function(){
User::withTrashed() ->update([
User::withTrashed()->update([
'test' => 'test',
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
use Worksome\CodingStyle\PHPStan\Laravel\Migrations\RequireWithoutTimestampsRule;

it('checks withoutTimestamps in migration rule', function (string $path, array ...$errors) {
$this->rule = new RequireWithoutTimestampsRule();
$this->rule = new RequireWithoutTimestampsRule($path);


expect($path)->toHaveRuleErrors($errors);
})->with([
Expand All @@ -15,8 +16,15 @@
'invalid migration' => [
__DIR__ . '/Fixture/database/migrations/invalid_migration.php.inc',
[
"invalid_migration file uses 'update' or 'create' action without 'withoutTimestamps()' protection.",
7,
"Line 19: The 'update()' method call should be within 'withoutTimestamps()' context to prevent unintended timestamp updates.",
19,
],
],
'invalid migration with multiple models' => [
__DIR__ . '/Fixture/database/migrations/invalid_migration.php.inc',
[
"Line 19: The 'update()' method call should be within 'withoutTimestamps()' context to prevent unintended timestamp updates.",
19,
],
]
]);

0 comments on commit fee91f0

Please sign in to comment.