From 7d07e258a3d15757b446e2cae905e457b12b908b Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sun, 4 Feb 2024 11:17:13 +1300 Subject: [PATCH 1/4] Add MissingOverrideAttribute issue --- config.xsd | 2 ++ docs/running_psalm/configuration.md | 8 +++++++ docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + .../issues/MissingOverrideAttribute.md | 23 +++++++++++++++++++ src/Psalm/Config.php | 6 +++++ src/Psalm/Issue/MissingOverrideAttribute.php | 9 ++++++++ tests/DocumentationTest.php | 3 +++ 8 files changed, 53 insertions(+) create mode 100644 docs/running_psalm/issues/MissingOverrideAttribute.md create mode 100644 src/Psalm/Issue/MissingOverrideAttribute.php 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/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 @@ +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; } From c71ad2221cae8ebe317a6dc77290b91657c70cc3 Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sun, 4 Feb 2024 15:30:36 +1300 Subject: [PATCH 2/4] Move Override tests to separate file --- tests/AttributeTest.php | 85 ------------------------------ tests/OverrideTest.php | 111 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 85 deletions(-) create mode 100644 tests/OverrideTest.php diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 4a631c5b3dd..f1051773882 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -293,36 +293,6 @@ class Foo 'ignored_issues' => [], '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' => ' [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'overrideInterface' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + ]; + } + + public function providerInvalidCodeParse(): iterable + { + return [ + 'noParent' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'constructor' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'privateMethod' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'interfaceWithNoParent' => [ + 'code' => ' 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + ]; + } +} From 8396360d30050ee2c8565c132d5e8c5391896efb Mon Sep 17 00:00:00 2001 From: Evan Shaw Date: Sun, 4 Feb 2024 11:35:03 +1300 Subject: [PATCH 3/4] Emit MissingOverrideAttribute --- .../Analyzer/FunctionLikeAnalyzer.php | 40 ++++++++--- tests/OverrideTest.php | 69 +++++++++++++++++++ 2 files changed, 98 insertions(+), 11 deletions(-) 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/tests/OverrideTest.php b/tests/OverrideTest.php index 1798bf7804d..17782bc9eca 100644 --- a/tests/OverrideTest.php +++ b/tests/OverrideTest.php @@ -2,6 +2,7 @@ namespace Psalm\Tests; +use Psalm\Config; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; @@ -12,9 +13,33 @@ class OverrideTest extends TestCase use ValidCodeAnalysisTestTrait; use InvalidCodeAnalysisTestTrait; + protected function makeConfig(): Config + { + $config = parent::makeConfig(); + $config->ensure_override_attribute = true; + return $config; + } + public function providerValidCodeParse(): iterable { return [ + 'constructor' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'overrideClass' => [ 'code' => ' [], 'php_version' => '8.3', ], + 'classMissingAttribute' => [ + 'code' => ' 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], + 'classUsingTrait' => [ + 'code' => ' 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], 'constructor' => [ 'code' => ' [], 'php_version' => '8.3', ], + 'interfaceMissingAttribute' => [ + 'code' => ' 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_levels' => [], + 'php_version' => '8.3', + ], 'privateMethod' => [ 'code' => ' Date: Sun, 4 Feb 2024 15:41:01 -0400 Subject: [PATCH 4/4] Apply suggestions from code review --- tests/OverrideTest.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/OverrideTest.php b/tests/OverrideTest.php index 17782bc9eca..ab808a06df9 100644 --- a/tests/OverrideTest.php +++ b/tests/OverrideTest.php @@ -6,8 +6,6 @@ use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; -use const DIRECTORY_SEPARATOR; - class OverrideTest extends TestCase { use ValidCodeAnalysisTestTrait; @@ -83,7 +81,7 @@ class C { public function f(): void {} } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ], @@ -97,7 +95,7 @@ class C2 extends C { public function f(): void {} } ', - 'error_message' => 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_message' => 'MissingOverrideAttribute', 'error_levels' => [], 'php_version' => '8.3', ], @@ -113,7 +111,7 @@ class C { public function f(): void {} } ', - 'error_message' => 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:9:25', + 'error_message' => 'MissingOverrideAttribute', 'error_levels' => [], 'php_version' => '8.3', ], @@ -131,7 +129,7 @@ class C2 extends C { public function __construct() {} } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ], @@ -145,7 +143,7 @@ interface I2 extends I { public function f(): void; } ', - 'error_message' => 'MissingOverrideAttribute - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_message' => 'MissingOverrideAttribute', 'error_levels' => [], 'php_version' => '8.3', ], @@ -160,7 +158,7 @@ class C2 extends C { private function f(): void {} } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ], @@ -171,7 +169,7 @@ interface I { public function f(): void; } ', - 'error_message' => 'InvalidOverride - src' . DIRECTORY_SEPARATOR . 'somefile.php:3:25', + 'error_message' => 'InvalidOverride', 'error_levels' => [], 'php_version' => '8.3', ],