diff --git a/config.xsd b/config.xsd
index 4cdb3298e7d..c15a74e6080 100644
--- a/config.xsd
+++ b/config.xsd
@@ -42,6 +42,7 @@
+
@@ -319,6 +320,7 @@
+
diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md
index f4976aaad83..d00baae88d3 100644
--- a/docs/running_psalm/configuration.md
+++ b/docs/running_psalm/configuration.md
@@ -273,6 +273,14 @@ When `true`, Psalm will complain when referencing an explicit string offset on a
```
When `true`, Psalm will complain when referencing an explicit integer offset on an array e.g. `$arr[7]` without a user first asserting that it exists (either via an `isset` check or via an object-like array). Defaults to `false`.
+#### ensureOverrideAttribute
+```xml
+
+```
+When `true`, Psalm will report class and interface methods that override a method on a parent, but do not have an `Override` attribute. Defaults to `false`.
+
#### phpVersion
```xml
*/
@@ -1081,6 +1086,7 @@ private static function fromXmlAndPaths(
'includePhpVersionsInErrorBaseline' => 'include_php_versions_in_error_baseline',
'ensureArrayStringOffsetsExist' => 'ensure_array_string_offsets_exist',
'ensureArrayIntOffsetsExist' => 'ensure_array_int_offsets_exist',
+ 'ensureOverrideAttribute' => 'ensure_override_attribute',
'reportMixedIssues' => 'show_mixed_issues',
'skipChecksOnUnresolvableIncludes' => 'skip_checks_on_unresolvable_includes',
'sealAllMethods' => 'seal_all_methods',
diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
index 964b6495148..70786016bf7 100644
--- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
+++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
@@ -37,6 +37,7 @@
use Psalm\Issue\MethodSignatureMismatch;
use Psalm\Issue\MismatchingDocblockParamType;
use Psalm\Issue\MissingClosureParamType;
+use Psalm\Issue\MissingOverrideAttribute;
use Psalm\Issue\MissingParamType;
use Psalm\Issue\MissingThrowsDocblock;
use Psalm\Issue\ReferenceConstraintViolation;
@@ -1973,20 +1974,37 @@ private function getFunctionInformation(
true,
);
- if ($codebase->analysis_php_version_id >= 8_03_00
- && (!$overridden_method_ids || $storage->cased_name === '__construct')
- && array_filter(
+ if ($codebase->analysis_php_version_id >= 8_03_00) {
+ $has_override_attribute = array_filter(
$storage->attributes,
static fn(AttributeStorage $s): bool => $s->fq_class_name === 'Override',
- )
- ) {
- IssueBuffer::maybeAdd(
- new InvalidOverride(
- 'Method ' . $storage->cased_name . ' does not match any parent method',
- $codeLocation,
- ),
- $this->getSuppressedIssues(),
);
+
+ if ($has_override_attribute
+ && (!$overridden_method_ids || $storage->cased_name === '__construct')
+ ) {
+ IssueBuffer::maybeAdd(
+ new InvalidOverride(
+ 'Method ' . $storage->cased_name . ' does not match any parent method',
+ $codeLocation,
+ ),
+ $this->getSuppressedIssues(),
+ );
+ }
+
+ if (!$has_override_attribute
+ && $codebase->config->ensure_override_attribute
+ && $overridden_method_ids
+ && $storage->cased_name !== '__construct'
+ ) {
+ IssueBuffer::maybeAdd(
+ new MissingOverrideAttribute(
+ 'Method ' . $storage->cased_name . ' should have the "Override" attribute',
+ $codeLocation,
+ ),
+ $this->getSuppressedIssues(),
+ );
+ }
}
if ($overridden_method_ids
diff --git a/src/Psalm/Issue/MissingOverrideAttribute.php b/src/Psalm/Issue/MissingOverrideAttribute.php
new file mode 100644
index 00000000000..0e146e19971
--- /dev/null
+++ b/src/Psalm/Issue/MissingOverrideAttribute.php
@@ -0,0 +1,9 @@
+ [],
'php_version' => '8.2',
],
- 'override' => [
- 'code' => ' [],
- 'ignored_issues' => [],
- 'php_version' => '8.3',
- ],
- 'overrideInterface' => [
- 'code' => ' [],
- 'ignored_issues' => [],
- 'php_version' => '8.3',
- ],
'sensitiveParameter' => [
'code' => ' 'UndefinedAttributeClass - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:36',
],
- 'overrideWithNoParent' => [
- 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25',
- 'error_levels' => [],
- 'php_version' => '8.3',
- ],
- 'overrideConstructor' => [
- 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:25',
- 'error_levels' => [],
- 'php_version' => '8.3',
- ],
- 'overridePrivate' => [
- 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25',
- 'error_levels' => [],
- 'php_version' => '8.3',
- ],
- 'overrideInterfaceWithNoParent' => [
- 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25',
- 'error_levels' => [],
- 'php_version' => '8.3',
- ],
'tooFewArgumentsToAttributeConstructor' => [
'code' => 'project_analyzer->getConfig()->ensure_array_string_offsets_exist = $is_array_offset_test;
$this->project_analyzer->getConfig()->ensure_array_int_offsets_exist = $is_array_offset_test;
+ $this->project_analyzer->getConfig()->ensure_override_attribute = $error_message === 'MissingOverrideAttribute';
+
foreach ($ignored_issues as $error_level) {
$this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
}
@@ -313,6 +315,7 @@ public function providerInvalidCodeParse(): array
break;
case 'InvalidOverride':
+ case 'MissingOverrideAttribute':
$php_version = '8.3';
break;
}
diff --git a/tests/OverrideTest.php b/tests/OverrideTest.php
new file mode 100644
index 00000000000..ab808a06df9
--- /dev/null
+++ b/tests/OverrideTest.php
@@ -0,0 +1,178 @@
+ensure_override_attribute = true;
+ return $config;
+ }
+
+ public function providerValidCodeParse(): iterable
+ {
+ return [
+ 'constructor' => [
+ 'code' => ' [],
+ 'ignored_issues' => [],
+ 'php_version' => '8.3',
+ ],
+ 'overrideClass' => [
+ 'code' => ' [],
+ 'ignored_issues' => [],
+ 'php_version' => '8.3',
+ ],
+ 'overrideInterface' => [
+ 'code' => ' [],
+ 'ignored_issues' => [],
+ 'php_version' => '8.3',
+ ],
+ ];
+ }
+
+ public function providerInvalidCodeParse(): iterable
+ {
+ return [
+ 'noParent' => [
+ 'code' => ' 'InvalidOverride',
+ 'error_levels' => [],
+ 'php_version' => '8.3',
+ ],
+ 'classMissingAttribute' => [
+ 'code' => ' 'MissingOverrideAttribute',
+ 'error_levels' => [],
+ 'php_version' => '8.3',
+ ],
+ 'classUsingTrait' => [
+ 'code' => ' 'MissingOverrideAttribute',
+ 'error_levels' => [],
+ 'php_version' => '8.3',
+ ],
+ 'constructor' => [
+ 'code' => ' 'InvalidOverride',
+ 'error_levels' => [],
+ 'php_version' => '8.3',
+ ],
+ 'interfaceMissingAttribute' => [
+ 'code' => ' 'MissingOverrideAttribute',
+ 'error_levels' => [],
+ 'php_version' => '8.3',
+ ],
+ 'privateMethod' => [
+ 'code' => ' 'InvalidOverride',
+ 'error_levels' => [],
+ 'php_version' => '8.3',
+ ],
+ 'interfaceWithNoParent' => [
+ 'code' => ' 'InvalidOverride',
+ 'error_levels' => [],
+ 'php_version' => '8.3',
+ ],
+ ];
+ }
+}