` 隐式 final 的覆盖约束)
开发工具代码质量质量保障【免费下载链接】psalmA PHP static analysis tool for finding errors and security vulnerabilities in PHP applications项目地址https://gitcode.com/gh_mirrors/ps/psalm点击查看免费下载OverriddenFinalProperty是 Psalm 静态分析器中针对属性覆盖的一组声明级错误检测规则当子类试图重新声明一个被父类标记为final的属性时Psalm 会报出该错误。本文以官方文档 OverriddenFinalProperty.md 为核心骨架结合仓库源码中的检测实现、反射解析逻辑与测试用例完整讲解该错误的触发条件、错误级别、底层原理及修复方式帮助你写出符合 Psalm 规范的、可继承安全的 PHP 类层次结构。什么是 OverriddenFinalProperty在 PHP 中final关键字最常见的用途是修饰类与方法。PHP 8.4 之前属性无法声明为final但从 PHP 8.4 引入不对称可见性asymmetric visibility开始属性的final语义也随之落地。Psalm 的OverriddenFinalProperty就是在子类覆盖了父类中不可覆盖属性时发出的错误提示。官方文档给出的定义如下Emitted when a property declared as final is redeclared in a child class. Properties declared withprivate(set)asymmetric visibility are implicitly final.即直接触发子类重新声明redeclare了一个在父类中被声明为final的属性隐式触发使用private(set)不对称可见性声明的属性隐式地是final的因此同样禁止被子类覆盖。这一点与final方法的语义完全对称final阻止继承体系中对声明本身的改写保证父类对该成员的定义在整个继承链中保持一致。官方示例private(set) 属性的隐式 final官方文档中的触发示例使用了 PHP 8.4 的不对称可见性语法?php class Foo { public private(set) string $bar baz; } class Bar extends Foo { public string $bar foobar; }这段代码中Foo::$bar声明为public private(set)即读取可见性为 public但写入赋值可见性为 private——外部只能读、不能写根据 PHP 8.4 语言规范private(set)属性隐式地带有final语义子类Bar重新声明了同名属性$bar这相当于覆盖了一个 final 属性因此 Psalm 报出OverriddenFinalProperty。运行 Psalm 后会得到类似下面的报告ERROR: OverriddenFinalProperty - file.php:15:5 Property Bar::$bar cannot override final property Foo::$bar (private(set) properties are implicitly final)注意错误信息末尾的括号说明private(set) properties are implicitly final这是 Psalm 在检测到被覆盖属性是经由private(set)隐式变为 final 时附加的提示文案见下文源码分析帮助开发者理解为什么一个没有写final关键字的属性会触发该错误。显式 final 属性的覆盖private(set)只是触发方式之一。属性同样可以显式地写成 final。下面是仓库测试tests/AsymmetricVisibilityTest.php中对应的显式 final 用例overrideExplicitlyFinalProperty?php class A { final public int $foo 1; } final class B extends A { public int $foo 2; }该测试断言error_message为OverriddenFinalProperty说明显式 final 属性被覆盖与private(set) 隐式 final 属性被覆盖走的是同一条检测路径报的是同一个错误码。错误级别与错误码OverriddenFinalProperty的错误定义位于 src/Psalm/Issue/OverriddenFinalProperty.phpnamespace Psalm\Issue; final class OverriddenFinalProperty extends PropertyIssue { public const ERROR_LEVEL 6; public const SHORTCODE 370; }两个关键常量ERROR_LEVEL 6该错误在 Psalm 错误级别6 及以下作为 error 报告在更宽松的 level 7、8 中降级为 info见 error_levels.md 中 level 6 一栏的 OverriddenFinalProperty 条目。若你的项目使用默认的 level 8 配置这类问题不会导致构建失败但依然值得关注——它是继承设计缺陷的信号。SHORTCODE 370Psalm 为每种错误分配的唯一短码用于机器可读的输出格式如 JSON、JUnit、SARIF 报告以及--show-infofalse之类的过滤场景。你在 baseline 文件中也会看到形如OverriddenFinalProperty的条目名。此外它继承自PropertyIssue说明这是一个属性property相关的声明级错误报告位置指向子类中覆盖属性的声明处。底层检测实现两段式源码剖析该错误的完整生命周期由仓库中的两段代码协作完成属性元数据解析阶段负责记录is_final类分析阶段负责在覆盖发生时比较并报错。第一段反射解析阶段记录 final 语义PHP 8.4 的private(set)语法解析由 src/Psalm/Internal/PhpVisitor/Reflector/PropertyVisibilityResolver.php 负责。它从 php-parser 的 modifier flags 中解析出读取可见性与写入set可见性通过Modifiers::VISIBILITY_MASK解析读取可见性PROTECTED/PRIVATE/ 默认PUBLIC通过Modifiers::VISIBILITY_SET_MASK解析写入可见性PUBLIC_SET/PROTECTED_SET/PRIVATE_SET解析结果写入PropertyStorage的$visibility与$set_visibility字段字段定义见 src/Psalm/Storage/PropertyStorage.php。其中最关键的一段逻辑在PropertyVisibilityResolver::resolve()末尾if ($set_visibility ClassLikeAnalyzer::VISIBILITY_PRIVATE) { // private(set) properties are implicitly final $property_storage-is_final true; }也就是说Psalm在元数据层就把private(set)属性标记为 final与Modifiers::FINAL位合并$property_storage-is_final $property_storage-is_final || (bool) ($flags Modifiers::FINAL);PropertyStorage::$is_final与$set_visibility一起成为后续覆盖检测的直接数据来源。第二段ClassAnalyzer 中的覆盖检测真正的报错发生在类分析阶段。src/Psalm/Internal/Analyzer/ClassAnalyzer.php 的addContextProperties静态方法中Psalm 遍历ClassLikeStorage::$overridden_property_ids记录每个属性被哪些类覆盖的映射逐条与被覆盖的父类属性做比较if ($guide_property_storage-is_final $property_class_name $fq_class_name $property_storage-location ) { IssueBuffer::maybeAdd( new OverriddenFinalProperty( Property . $fq_class_name . ::$ . $property_name . cannot override final property . $guide_class_name . ::$ . $property_name . ($guide_property_storage-set_visibility self::VISIBILITY_PRIVATE ? (private(set) properties are implicitly final) : ), $property_storage-location, $guide_class_name . ::$ . $property_name, ), ); }这段逻辑的要点判据只要父类属性的is_final为 true且当前正在分析的这个类恰好是覆盖发生所在的类$property_class_name $fq_class_name就触发错误信息增强如果父类属性是通过private(set)隐式变为 final 的判断条件为set_visibility VISIBILITY_PRIVATE错误消息会追加括号提示帮助区分「显式 final」与「隐式 final」两种情形报告位置错误指向子类中覆盖属性的声明位置$property_storage-location并通过IssueBuffer::maybeAdd进入报告队列遵守项目配置中的错误级别、baseline 与 suppress 规则。完整检测上下文同方法内的相关比较addContextProperties在同一次遍历中还顺带完成了其他属性覆盖检查理解它们有助于区分不同错误详见 ClassAnalyzer.php检查项触发条件对应 Issuefinal 属性被覆盖父类属性is_final trueOverriddenFinalProperty可见性变宽子类visibility高于父类OverriddenPropertyAccessset 可见性更严格子类set_visibility高于父类如protected(set)改为private(set)OverriddenPropertyAccess签名类型不一致子类与父类signature_type不相等NonInvariantPropertyType可以看到OverriddenFinalProperty在源码中与OverriddenPropertyAccess、NonInvariantPropertyType并排出现共同构成 Psalm 对 PHP 属性覆盖的完整约束矩阵。private(set)隐式 final 的用例恰好同时涉及前三者写入可见性收紧会触发OverriddenPropertyAccess而声明本身被覆盖则触发OverriddenFinalProperty。测试用例验证仓库对OverriddenFinalProperty有直接的测试覆盖集中在 tests/AsymmetricVisibilityTest.phpoverrideImplicitlyFinalPrivateSetProperty第 443 行起class A { public private(set) int $foo 1; }被子类final class B extends A { public int $foo 2; }覆盖断言错误为OverriddenFinalPropertyphp_version为8.4overrideExplicitlyFinalProperty第 456 行起final public int $foo 1被子类覆盖同样断言OverriddenFinalPropertyoverrideWithNarrowerSetVisibility第 430 行起父类protected(set)、子类收紧为private(set)断言的是OverriddenPropertyAccess可见性问题而非OverriddenFinalProperty——注意区分这两类错误。这些测试同时印证了private(set)需要 PHP 8.4 及以上版本测试通过php_version 8.4指定分析目标版本。版本前提与边界由于该错误的两种触发方式都与 PHP 8.4 的不对称可见性密切相关使用时有几个明确的版本前提分析目标版本Psalm 通过配置中的 PHP 版本设置如composer.json的platform.php或 psalm.xml 中的phpVersion决定是否识别不对称可见性语法。PropertyVisibilityResolver会在analysis_php_version_id 8_04_00时报告ParseErrorAsymmetric property visibility is only available in PHP 8.4 and later静态属性在静态属性上使用不对称可见性如public static private(set) int $foo需要 PHP 8.5 及以上PropertyVisibilityResolver对此有单独检查类型必填带不对称可见性的属性必须有显式类型否则报ParseError测试用例asymmetricVisibilityRequiresType即验证此点可见性约束set 可见性不能弱于读取可见性不能写private protected(set)且readonly属性不能是public(set)这些都在PropertyVisibilityResolver中作为声明级错误处理。如何修复与避免针对OverriddenFinalProperty可以从三个层面处理删除子类中的重复声明推荐private(set)属性禁止覆盖的语义是「子类不得重定义该属性的声明」如果子类只是想初始化不同的默认值应在构造函数中赋值或提供受控的初始化方法而不是重新声明属性。注意在类内部包括子类内部读取private(set)属性是允许的限制的只是外部写入与声明覆盖。显式调整父类设计如果确实需要子类拥有独立的同名属性说明父类不应把该属性设计为 final——去掉父类的final关键字或改用protected(set)/public(set)的不对称可见性使属性可以被安全覆盖。但此时需同步考虑 Psalm 的OverriddenPropertyAccess与NonInvariantPropertyType检查保证可见性与类型在继承链上保持一致。抑制该错误谨慎使用在确认当前设计有意的前提下可以使用psalm-suppress OverriddenFinalProperty注解或将其加入 psalm-baseline。但如本节开头所述这通常是设计缺陷的信号建议优先从继承结构上解决问题而不是压制报告。相关文档导航官方错误说明docs/running_psalm/issues/OverriddenFinalProperty.md错误级别定义docs/running_psalm/error_levels.md所有 issue 索引docs/running_psalm/issues.md同类错误OverriddenFinalConstantfinal 常量被覆盖docs/running_psalm/issues/OverriddenFinalConstant.md属性覆盖的可见性检查OverriddenPropertyAccessdocs/running_psalm/issues/OverriddenPropertyAccess.md属性类型不变性检查NonInvariantPropertyTypedocs/running_psalm/issues/NonInvariantPropertyType.md小结OverriddenFinalPropertySHORTCODE 370level 6是 Psalm 对 PHP 属性覆盖体系中「final 不可覆盖」约束的落地实现它统一处理显式final属性与 PHP 8.4private(set)隐式 final 属性检测逻辑分别位于PropertyVisibilityResolver元数据解析与ClassAnalyzer::addContextProperties覆盖比对两处并由AsymmetricVisibilityTest中的多个用例锁定行为。理解这条检测规则不仅能帮助你正确驾驭 PHP 8.4 的不对称可见性新语法也能让你更清晰地把握 Psalm 对属性继承一致性的整体检查策略。赞分享开发工具代码质量质量保障【免费下载链接】psalmA PHP static analysis tool for finding errors and security vulnerabilities in PHP applications项目地址https://gitcode.com/gh_mirrors/ps/psalm点击查看免费下载相关推荐Psalm 的 InvalidExtendClass 检查final 类、final 注解与 psalm-inheritors 继承限制全解析Psalm 的 InvalidExtendClass 检查final 类、 final 注解与 psalm inheritors 继承限制全解析 Psal开发工具代码质量质量保障PHPStan 错误标识符详解property.finalPrivate —— final 与 private 属性修饰符冲突的检测与修复PHPStan 错误标识符详解property.finalPrivate —— final 与 private 属性修饰符冲突的检测与修复 本篇文章围绕 PH开发工具代码质量静态分析Psalm 的 OverriddenFinalConstant 问题详解final 类常量被覆盖时的静态分析Psalm 的 OverriddenFinalConstant 问题详解final 类常量被覆盖时的静态分析 OverriddenFinalConstant开发工具代码质量质量保障上一篇XO代码检查终极指南如何快速提升JavaScript/TypeScript代码质量下一篇利用UMAP进行高效聚类分析的实践指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考