Repository: tighten/tlint (php)

Issue:
`DeprecatedDatesAttribute` Lint
In Laravel 8 the `$dates` property was deprecated in favor of `$casts`.
https://github.com/illuminate/database/blob/0073d465400dd893cb4c042588d1ac308f651a33/Eloquent/Concerns/HasAttributes.php#L89-L96

There should be a linter that warns about the usage of `$dates`.

Pull request description:
Add NoDatesPropertyOnModels linter
Closes #201.

Patch:
diff --git a/src/Linters/NoDatesPropertyOnModels.php b/src/Linters/NoDatesPropertyOnModels.php
new file mode 100644
index 0000000..45f281a
--- /dev/null
+++ b/src/Linters/NoDatesPropertyOnModels.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Tighten\Linters;
+
+use PhpParser\Node;
+use PhpParser\Node\Stmt\Property;
+use PhpParser\NodeTraverser;
+use PhpParser\NodeVisitor\FindingVisitor;
+use PhpParser\Parser;
+use Tighten\BaseLinter;
+use Tighten\Concerns\IdentifiesExtends;
+
+class NoDatesPropertyOnModels extends BaseLinter
+{
+    use IdentifiesExtends;
+
+    public const description = 'The `$dates` property was deprecated in Laravel 8. Use `$casts` instead.';
+
+    public function lint(Parser $parser)
+    {
+        $traverser = new NodeTraverser;
+
+        $traverser->addVisitor($visitor = new FindingVisitor(function (Node $node) {
+            static $model = false;
+
+            if ($this->extendsAny($node, ['Model', 'Pivot', 'Authenticatable'])) {
+                $model = true;
+            }
+
+            return $model && $node instanceof Property && (string) $node->props[0]->name === 'dates';
+        }));
+
+        $traverser->traverse($parser->parse($this->code));
+
+        return $visitor->getFoundNodes();
+    }
+}
diff --git a/src/Presets/LaravelPreset.php b/src/Presets/LaravelPreset.php
index f3bfcc2..07bd291 100644
--- a/src/Presets/LaravelPreset.php
+++ b/src/Presets/LaravelPreset.php
@@ -19,6 +19,7 @@ class LaravelPreset implements PresetInterface
             Linters\ModelMethodOrder::class,
             Linters\NewLineAtEndOfFile::class,
             Linters\NoCompact::class,
+            Linters\NoDatesPropertyOnModels::class,
             Linters\NoDump::class,
             Linters\NoLeadingSlashesOnRoutePaths::class,
             Linters\NoParensEmptyInstantiations::class,
diff --git a/src/Presets/TightenPreset.php b/src/Presets/TightenPreset.php
index 7f8d101..d2984dd 100644
--- a/src/Presets/TightenPreset.php
+++ b/src/Presets/TightenPreset.php
@@ -20,6 +20,7 @@ class TightenPreset implements PresetInterface
             Linters\ModelMethodOrder::class,
             Linters\NewLineAtEndOfFile::class,
             Linters\NoCompact::class,
+            Linters\NoDatesPropertyOnModels::class,
             Linters\NoDump::class,
             Linters\NoDocBlocksForMigrationUpDown::class,
             Linters\NoLeadingSlashesOnRoutePaths::class,


Test patch:
diff --git a/tests/Linting/Linters/NoDatesPropertyOnModelsTest.php b/tests/Linting/Linters/NoDatesPropertyOnModelsTest.php
new file mode 100644
index 0000000..73e73ca
--- /dev/null
+++ b/tests/Linting/Linters/NoDatesPropertyOnModelsTest.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace testing\Linting\Linters;
+
+use PHPUnit\Framework\TestCase;
+use Tighten\Linters\NoDatesPropertyOnModels;
+use Tighten\TLint;
+
+class NoDatesPropertyOnModelsTest extends TestCase
+{
+    /** @test */
+    function lints_dates_property_on_model()
+    {
+        $file = <<<'file'
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+class Post extends Model
+{
+    protected $dates = ['published_at'];
+
+    public function user()
+    {
+        return $this->belongsTo(User::class);
+    }
+}
+file;
+
+        $lints = (new TLint)->lint(new NoDatesPropertyOnModels($file));
+
+        $this->assertEquals(9, $lints[0]->getNode()->getLine());
+    }
+
+    /** @test */
+    function lints_dates_property_on_pivot_model()
+    {
+        $file = <<<'file'
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Relations\Pivot;
+
+class AuthorPost extends Pivot
+{
+    public $incrementing = false;
+
+    protected $dates = ['reviewed_at'];
+}
+file;
+
+        $lints = (new TLint)->lint(new NoDatesPropertyOnModels($file));
+
+        $this->assertEquals(11, $lints[0]->getNode()->getLine());
+    }
+
+    /** @test */
+    function lints_dates_property_on_authenticatable()
+    {
+        $file = <<<'file'
+<?php
+
+namespace App\Models;
+
+use Illuminate\Foundation\Auth\User as Authenticatable;
+use Illuminate\Notifications\Notifiable;
+
+class User extends Authenticatable
+{
+    use Notifiable;
+
+    protected $fillable = ['name', 'email'];
+    protected $dates = ['reviewed_at'];
+}
+file;
+
+        $lints = (new TLint)->lint(new NoDatesPropertyOnModels($file));
+
+        $this->assertEquals(13, $lints[0]->getNode()->getLine());
+    }
+
+    /** @test */
+    function ignores_dates_property_on_non_model()
+    {
+        $file = <<<'file'
+<?php
+
+namespace App\Events;
+
+class Birthday
+{
+    protected $dates = ['date'];
+}
+file;
+
+        $this->assertEmpty((new TLint)->lint(new NoDatesPropertyOnModels($file)));
+    }
+}