diff --git a/src/PHPStan/KeywordSelfRule.php b/src/PHPStan/KeywordSelfRule.php
new file mode 100644
index 0000000..802a847
--- /dev/null
+++ b/src/PHPStan/KeywordSelfRule.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SilverStripe\Standards\PHPStan;
+
+use PhpParser\Node;
+use PhpParser\Node\Name;
+use PhpParser\Node\Expr\ClassConstFetch;
+use PhpParser\Node\Expr\New_;
+use PhpParser\Node\Expr\StaticCall;
+use PhpParser\Node\Expr\StaticPropertyFetch;
+use PhpParser\Node\Identifier;
+use PHPStan\Analyser\Scope;
+use PHPStan\Rules\Rule;
+use PHPStan\Rules\RuleErrorBuilder;
+
+/**
+ * Validates that the `self` keyword is only used in situations where it's not avoidable.
+ *
+ * @implements Rule<Node>
+ */
+class KeywordSelfRule implements Rule
+{
+    public function getNodeType(): string
+    {
+        return Node::class;
+    }
+
+    public function processNode(Node $node, Scope $scope): array
+    {
+        $gettingConst = false;
+        switch (get_class($node)) {
+            // fetching a constant, e.g. `self::MY_CONST`
+            case ClassConstFetch::class:
+                // Traits can use `self` to get const values - but otherwise follow same
+                // logic as methods or properties.
+                if ($scope->isInTrait()) {
+                    return [];
+                }
+            // static method call, e.g. `self::myMethod()`
+            case StaticCall::class:
+            // fetching a static property, e.g. `self::$my_property`
+            case StaticPropertyFetch::class:
+                if (!is_a($node->class, Name::class) || $node->class->toString() !== 'self') {
+                    return [];
+                }
+                break;
+            // `self` as a type (for a property, argument, or return type)
+            case Name::class:
+                // Trait can use `self` for typehinting
+                if ($scope->isInTrait() || $node->toString() !== 'self') {
+                    return [];
+                }
+                break;
+            // instantiating a new object from `self`, e.g. `new self()`
+            case New_::class:
+                if ($node->class->toString() !== 'self') {
+                    return [];
+                }
+                break;
+            default:
+                return [];
+        }
+
+		$actualClass = $scope->isInTrait() ? 'self::class' : $scope->getClassReflection()->getName();
+        return [
+            RuleErrorBuilder::message(
+                "Can't use keyword 'self'. Use '$actualClass' instead."
+            )->build()
+        ];
+    }
+}
diff --git a/tests/PHPStan/KeywordSelfRuleTest.php b/tests/PHPStan/KeywordSelfRuleTest.php
new file mode 100644
index 0000000..6651b65
--- /dev/null
+++ b/tests/PHPStan/KeywordSelfRuleTest.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SilverStripe\Standards\Tests\PHPStan;
+
+use PHPStan\Rules\Rule;
+use PHPStan\Testing\RuleTestCase;
+use SilverStripe\Standards\PHPStan\KeywordSelfRule;
+use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\ClassUsesTrait;
+use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\TestClass;
+use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\TestInterface;
+use SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest\TestTrait;
+
+/**
+ * @extends RuleTestCase<KeywordSelfRule>
+ */
+class KeywordSelfRuleTest extends RuleTestCase
+{
+    protected function getRule(): Rule
+    {
+        return new KeywordSelfRule();
+    }
+
+    public function provideRule()
+    {
+        return [
+            'interface' => [
+                'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestInterface.php'],
+                'errorMessage' => "Can't use keyword 'self'. Use '" . TestInterface::class . "' instead.",
+                'errorLines' => [13, 18, 18],
+            ],
+            'class' => [
+                'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestClass.php'],
+                'errorMessage' => "Can't use keyword 'self'. Use '" . TestClass::class . "' instead.",
+                'errorLines' => [9, 11, 16, 16, 18, 20, 21, 21, 25],
+            ],
+            'trait' => [
+                'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestTrait.php', __DIR__ . '/KeywordSelfRuleTest/ClassUsesTrait.php'],
+                'errorMessage' => "Can't use keyword 'self'. Use 'self::class' instead.",
+                'errorLines' => [17, 19, 20, 24],
+            ],
+            'trait no errors' => [
+                'filePaths' => [__DIR__ . '/KeywordSelfRuleTest/TestTrait.php', __DIR__ . '/KeywordSelfRuleTest/ClassUsesTrait.php'],
+                'errorMessage' => "Can't use keyword 'self'. Use 'self::class' instead.",
+                'errorLines' => [],
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider provideRule
+     */
+    public function testRule(array $filePaths, string $errorMessage, array $errorLines): void
+    {
+        $errors = [];
+        foreach ($errorLines as $line) {
+            $errors[] = [$errorMessage, $line];
+        }
+        $this->analyse($filePaths, $errors);
+    }
+}
diff --git a/tests/PHPStan/KeywordSelfRuleTest/Anothertest.php b/tests/PHPStan/KeywordSelfRuleTest/Anothertest.php
new file mode 100644
index 0000000..e69de29
diff --git a/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTrait.php b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTrait.php
new file mode 100644
index 0000000..8594e10
--- /dev/null
+++ b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTrait.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest;
+
+/**
+ * PHPStan doesn't analyse traits unless there's a class that uses it
+ */
+class ClassUsesTrait
+{
+    use TestTrait;
+}
diff --git a/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTraitCorrect.php b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTraitCorrect.php
new file mode 100644
index 0000000..1768848
--- /dev/null
+++ b/tests/PHPStan/KeywordSelfRuleTest/ClassUsesTraitCorrect.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest;
+
+/**
+ * PHPStan doesn't analyse traits unless there's a class that uses it
+ */
+class ClassUsesTraitCorrect
+{
+    use TestTraitCorrect;
+}
diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestClass.php b/tests/PHPStan/KeywordSelfRuleTest/TestClass.php
new file mode 100644
index 0000000..83234d6
--- /dev/null
+++ b/tests/PHPStan/KeywordSelfRuleTest/TestClass.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest;
+
+class TestClass
+{
+    private const MY_CONST = 'some value';
+
+    private static string $myProperty = self::MY_CONST;
+
+    private static self $mySecondProperty;
+
+    /**
+     * self::class is ignored here because this is a comment.
+     */
+    public static function function1(self $someProperty): self
+    {
+        $self = new self();
+        $self::class; // $self:: isn't seen as self::
+        self::self();
+        self::$myProperty = self::class;
+        /* intentionally commented out to prove even multi-line comments are ignored.
+        self::$myProperty = self::class;
+        */
+        return new self();
+    }
+
+    public static function self(){}
+}
diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestInterface.php b/tests/PHPStan/KeywordSelfRuleTest/TestInterface.php
new file mode 100644
index 0000000..848fe55
--- /dev/null
+++ b/tests/PHPStan/KeywordSelfRuleTest/TestInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest;
+
+/**
+ * Usage of `self` in an interface refers to the interface itself, unlike with traits.
+ * This means we should avoid using self, since it's not actually needed here.
+ */
+interface TestInterface
+{
+    public const MY_CONST = 'some value';
+
+    public const MY_SECOND_CONST = self::MY_CONST;
+
+    /**
+     * self::class is ignored here because this is a comment.
+     */
+    public static function function1(self $someProperty): self;
+
+    public static function self();
+}
diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestTrait.php b/tests/PHPStan/KeywordSelfRuleTest/TestTrait.php
new file mode 100644
index 0000000..701c9fd
--- /dev/null
+++ b/tests/PHPStan/KeywordSelfRuleTest/TestTrait.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest;
+
+trait TestTrait
+{
+    // Can't define a const on a trait, but the trait can access consts on the class that uses it
+    private static string $myProperty = self::MY_CONST;
+
+    private static self $mySecondProperty;
+
+    /**
+     * self::class is ignored here because this is a comment.
+     */
+    public static function function1(self $someProperty): self
+    {
+        $self = new self();
+        $self::class; // $self:: isn't seen as self::
+        self::self();
+        self::$myProperty = self::class;
+        /* intentionally commented out to prove even multi-line comments are ignored.
+        self::$myProperty = self::class;
+        */
+        return new self();
+    }
+
+    public static function self(){}
+}
diff --git a/tests/PHPStan/KeywordSelfRuleTest/TestTraitCorrect.php b/tests/PHPStan/KeywordSelfRuleTest/TestTraitCorrect.php
new file mode 100644
index 0000000..4d56826
--- /dev/null
+++ b/tests/PHPStan/KeywordSelfRuleTest/TestTraitCorrect.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace SilverStripe\Standards\Tests\PHPStan\KeywordSelfRuleTest;
+
+trait TestTraitCorrect
+{
+    // Can't define a const on a trait, but the trait can access consts on the class that uses it
+    private static string $myProperty = self::MY_CONST;
+
+    private static self $mySecondProperty;
+
+    /**
+     * self::class is ignored here because this is a comment.
+     */
+    public static function function1(self $someProperty): self
+    {
+        $self = new (self::class)();
+        $self::class; // $self:: isn't seen as self::
+        self::class::self();
+        self::class::$myProperty = self::class;
+        /* intentionally commented out to prove even multi-line comments are ignored.
+        self::$myProperty = self::class;
+        */
+        return new (self::class)();
+    }
+
+    public static function self(){}
+}