Repository: VKCOM/noverify
Pull request #1131 (2021-08-29 23:23:37)

linter: added `parentNotFound` checker
```php
class Foo {
  public function f() {
    parent::b(); // Class Foo has no parent.
  }
}
```


Patch:
diff --git a/docs/checkers_doc.md b/docs/checkers_doc.md
index baace2e..844a6f4 100644
--- a/docs/checkers_doc.md
+++ b/docs/checkers_doc.md
@@ -4,7 +4,7 @@
 
 | Total checks | Checks enabled by default | Disabled checks by default | Autofixable checks |
 | ------------ | ------------------------- | -------------------------- | ------------------ |
-| 96           | 83                        | 13                         | 12                 |
+| 97           | 84                        | 13                         | 12                 |
 
 ## Table of contents
  - Enabled by default
@@ -59,6 +59,7 @@
    - [`oldStyleConstructor` checker](#oldstyleconstructor-checker)
    - [`paramClobber` checker](#paramclobber-checker)
    - [`parentConstructor` checker](#parentconstructor-checker)
+   - [`parentNotFound` checker](#parentnotfound-checker)
    - [`phpdocLint` checker](#phpdoclint-checker)
    - [`phpdocRef` checker](#phpdocref-checker)
    - [`phpdocType` checker](#phpdoctype-checker)
@@ -1183,6 +1184,32 @@ class Foo extends Bar {
 <p><br></p>
 
 
+### `parentNotFound` checker
+
+#### Description
+
+Report using `parent::` in a class without a parent class.
+
+#### Non-compliant code:
+```php
+class Foo {
+  public function f() {
+    parent::b(); // Class Foo has no parent.
+  }
+}
+```
+
+#### Compliant code:
+```php
+class Foo extends Boo {
+  public function f() {
+    parent::b(); // Ok.
+  }
+}
+```
+<p><br></p>
+
+
 ### `phpdocLint` checker
 
 #### Description
diff --git a/src/linter/block_linter.go b/src/linter/block_linter.go
index b287730..04e402d 100644
--- a/src/linter/block_linter.go
+++ b/src/linter/block_linter.go
@@ -1197,6 +1197,11 @@ func (b *blockLinter) checkMethodCall(e *ir.MethodCallExpr) {
 }
 
 func (b *blockLinter) checkStaticCall(e *ir.StaticCallExpr) {
+	if utils.NameNodeToString(e.Class) == "parent" && b.classParseState().CurrentParentClass == "" {
+		b.report(e, LevelError, "parentNotFound", "Cannot call method on parent as this class does not extend another")
+		return
+	}
+
 	call := resolveStaticMethodCall(b.walker.ctx.sc, b.classParseState(), e)
 	if !call.canAnalyze {
 		return
diff --git a/src/linter/report.go b/src/linter/report.go
index d0b8bff..64e459c 100644
--- a/src/linter/report.go
+++ b/src/linter/report.go
@@ -991,6 +991,23 @@ function main(): void {
 			Before:   `$a =+ 100;`,
 			After:    `$a += 100;`,
 		},
+
+		{
+			Name:     "parentNotFound",
+			Default:  true,
+			Quickfix: false,
+			Comment:  "Report using `parent::` in a class without a parent class.",
+			Before: `class Foo {
+  public function f() {
+    parent::b(); // Class Foo has no parent.
+  }
+}`,
+			After: `class Foo extends Boo {
+  public function f() {
+    parent::b(); // Ok.
+  }
+}`,
+		},
 	}
 
 	for _, info := range allChecks {


Test patch:
diff --git a/src/tests/inline/testdata/checkers/parentNotFound.php b/src/tests/inline/testdata/checkers/parentNotFound.php
new file mode 100644
index 0000000..ba2b353
--- /dev/null
+++ b/src/tests/inline/testdata/checkers/parentNotFound.php
@@ -0,0 +1,35 @@
+<?php
+
+class Foo {
+  public function f() {
+    parent::b(); // want `Cannot call method on parent as this class does not extend another`
+    self::f(); // ok
+  }
+}
+
+interface IFoo {}
+
+class Boo {
+  public function b() {}
+}
+
+class Foo1 extends Boo {
+  public function f() {
+    parent::b(); // ok
+    self::b(); // ok
+  }
+}
+
+class Foo2 extends Boo implements IFoo {
+  public function f() {
+    parent::b(); // ok
+    self::b(); // ok
+  }
+}
+
+class Foo3 implements IFoo {
+  public function f() {
+    parent::b(); // want `Cannot call method on parent as this class does not extend another`
+    self::f(); // ok
+  }
+}