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', + ], + ]; + } +}