分析實(shí)戰(zhàn):PossiblyNullPropertyAssignmentValue 錯(cuò)誤解析與修復(fù)指南)
開(kāi)發(fā)工具代碼質(zhì)量質(zhì)量保障【免費(fèi)下載鏈接】psalmA PHP static analysis tool for finding errors and security vulnerabilities in PHP applications項(xiàng)目地址https://gitcode.com/gh_mirrors/ps/psalm點(diǎn)擊查看免費(fèi)下載導(dǎo)讀PossiblyNullPropertyAssignmentValue是 Psalm 在將可能為null的值賦給一個(gè)非空non-nullable屬性時(shí)報(bào)告的類(lèi)型安全錯(cuò)誤屬于 Psalm 類(lèi)型系統(tǒng)對(duì)屬性賦值這一環(huán)節(jié)的靜態(tài)校驗(yàn)。本文以 Psalm 官方文檔 docs/running_psalm/issues/PossiblyNullPropertyAssignmentValue.md 為骨架結(jié)合倉(cāng)庫(kù)源碼 InstancePropertyAssignmentAnalyzer.php、錯(cuò)誤定義與測(cè)試用例系統(tǒng)講解該錯(cuò)誤的觸發(fā)條件、源碼判定邏輯、與同類(lèi)錯(cuò)誤的區(qū)別、影響等級(jí)以及三種實(shí)戰(zhàn)修復(fù)方案。讀完本文你將能在自己的 PHP 項(xiàng)目中準(zhǔn)確識(shí)別并消除此類(lèi)告警理解 Psalm 判定可能為 null的依據(jù)并能靈活運(yùn)用空值收窄、psalm-suppress與配置調(diào)級(jí)等手段。一、錯(cuò)誤含義非空屬性被賦入了可空值官方文檔對(duì)這一錯(cuò)誤的定義非常簡(jiǎn)短當(dāng)嘗試把一個(gè)可能為null的值賦給一個(gè)只接受非空值的屬性時(shí)Psalm 會(huì)報(bào)告PossiblyNullPropertyAssignmentValue。文檔給出的最小觸發(fā)示例?php class A { /** var string */ public $foo bar; } function assignToA(?string $s) { $a new A(); $a-foo $s; }解讀示例代碼類(lèi)A的屬性$foo通過(guò) docblock 聲明為/** var string */即非空的string類(lèi)型函數(shù)assignToA接收參數(shù)?string $s即string|null的聯(lián)合類(lèi)型可空將$s賦給$a-foo時(shí)Psalm 發(fā)現(xiàn)賦值類(lèi)型?string包含了null而屬性聲明類(lèi)型string不包含null于是觸發(fā)該錯(cuò)誤。也就是說(shuō)Psalm 報(bào)告的信息是可能為 null而非一定為 null——正因?yàn)橘x值類(lèi)型在聯(lián)合類(lèi)型層面與屬性類(lèi)型不匹配才產(chǎn)生了這一可能性告警。二、源碼級(jí)判定邏輯錯(cuò)誤從何而來(lái)該錯(cuò)誤在 Psalm 內(nèi)部由屬性賦值分析器InstancePropertyAssignmentAnalyzer負(fù)責(zé)判定。文件位于 src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php是處理$obj-prop $value這類(lèi)語(yǔ)句的核心分析器。2.1 類(lèi)型比對(duì)入口分析器首先通過(guò)UnionTypeComparator::isContainedBy()判斷賦值類(lèi)型是否被屬性聲明類(lèi)型包含見(jiàn) InstancePropertyAssignmentAnalyzer.php$type_match_found UnionTypeComparator::isContainedBy( $codebase, $assignment_type, $class_property_type, true, true, $union_comparison_results, );$assignment_type右側(cè)賦值表達(dá)式的推斷類(lèi)型如?string$class_property_type屬性的聲明類(lèi)型如string當(dāng)賦值類(lèi)型可以完全被屬性類(lèi)型包含時(shí)$type_match_found為true。2.2 觸發(fā) PossiblyNullPropertyAssignmentValue 的條件在$type_match_found成立的前提下分析器繼續(xù)做可空性專(zhuān)項(xiàng)檢查見(jiàn) InstancePropertyAssignmentAnalyzer.phpif ($type_match_found) { if (!$assignment_type-ignore_nullable_issues $assignment_type-isNullable() !$class_property_type-isNullable() ) { if (IssueBuffer::accepts( new PossiblyNullPropertyAssignmentValue( $var_id . with non-nullable declared type \ . $class_property_type . \ cannot be assigned nullable type \ . $assignment_type . \, ... ), ... )) { return; } } ... }三個(gè)條件缺一不可$assignment_type-isNullable()—— 賦值類(lèi)型包含null分支例如?string、string|null!$class_property_type-isNullable()—— 屬性聲明類(lèi)型不含null分支例如純string!$assignment_type-ignore_nullable_issues—— 賦值類(lèi)型未被標(biāo)記為忽略可空問(wèn)題見(jiàn)下文忽略機(jī)制。三者同時(shí)滿(mǎn)足時(shí)Psalm 會(huì)構(gòu)造PossiblyNullPropertyAssignmentValue錯(cuò)誤實(shí)例錯(cuò)誤消息形如$foo with non-nullable declared type string cannot be assigned nullable type ?string2.3 配套的 PossiblyFalsePropertyAssignmentValue同一段源碼中緊隨其后還實(shí)現(xiàn)了可能為 false的同類(lèi)檢查InstancePropertyAssignmentAnalyzer.php當(dāng)賦值類(lèi)型包含false而屬性聲明類(lèi)型不含bool/scalar時(shí)會(huì)觸發(fā)姊妹錯(cuò)誤PossiblyFalsePropertyAssignmentValue。兩者邏輯同構(gòu)可對(duì)照理解。2.4 與完全類(lèi)型不匹配錯(cuò)誤的邊界值得注意的是當(dāng)賦值類(lèi)型根本無(wú)法被屬性類(lèi)型包含!$type_match_found !type_coerced時(shí)Psalm 會(huì)改為報(bào)告更嚴(yán)重的 InvalidPropertyAssignmentValue例如把int賦給string屬性。而PossiblyNullPropertyAssignmentValue的定位是類(lèi)型大體匹配、僅空值分支越界屬于程度較輕的告警。此外如果賦值類(lèi)型是mixed分析器會(huì)直接跳過(guò)見(jiàn) InstancePropertyAssignmentAnalyzer.php由MixedPropertyTypeCoercion等錯(cuò)誤另行接管。三、錯(cuò)誤定義與錯(cuò)誤等級(jí)該錯(cuò)誤在源碼中被定義為一個(gè)獨(dú)立的 Issue 類(lèi) src/Psalm/Issue/PossiblyNullPropertyAssignmentValue.php。final class PossiblyNullPropertyAssignmentValue extends PropertyIssue { public const ERROR_LEVEL 3; public const SHORTCODE 148; }關(guān)鍵信息繼承自PropertyIssuePropertyIssue定義于 src/Psalm/Issue/PropertyIssue.php在CodeIssue基礎(chǔ)上額外攜帶property_id字段形如A::$foo用于在報(bào)告中精確定位到具體屬性ERROR_LEVEL 3表示該錯(cuò)誤屬于level 3 及以下級(jí)別。根據(jù) error_levels.md 的錯(cuò)誤等級(jí)體系Psalm 的errorLevel取值為 1最嚴(yán)格到 8最寬松默認(rèn)是level 2。PossiblyNullPropertyAssignmentValue在 level 1~3 下作為錯(cuò)誤報(bào)告在 level 4 及以上則被降級(jí)為info非阻塞提示SHORTCODE 148Psalm 為每個(gè) Issue 分配的唯一短代碼可用于基線(xiàn)baseline文件與結(jié)果輸出中的穩(wěn)定標(biāo)識(shí)。同類(lèi)Possibly*系列錯(cuò)誤如PossiblyNullArgument、PossiblyNullPropertyFetch、PossiblyFalsePropertyAssignmentValue同樣列于 level 3 分組中參見(jiàn) error_levels.md它們共同代表類(lèi)型可能越界但未必一定出錯(cuò)的灰色地帶。四、與相鄰錯(cuò)誤的辨析PossiblyNullPropertyAssignmentValue容易與以下兩個(gè)錯(cuò)誤混淆務(wù)必區(qū)分錯(cuò)誤觸發(fā)場(chǎng)景判定焦點(diǎn)PossiblyNullPropertyAssignmentValue屬性對(duì)象確定非空但賦值給屬性的值可能為 null賦值的右值類(lèi)型 vs 屬性聲明類(lèi)型PossiblyNullPropertyAssignment屬性對(duì)象本身可能為 null如$a可能是null向其寫(xiě)屬性被賦值的對(duì)象左值是否可空NullPropertyAssignment屬性對(duì)象確定是 null仍向其寫(xiě)屬性左值確定為空測(cè)試文件 tests/PropertyTypeTest.php 中給出了后兩者的對(duì)照示例// PossiblyNullPropertyAssignment對(duì)象本身可能為 null class Foo { /** var string */ public $foo ; } $a rand(0, 10) ? new Foo() : null; $a-foo hello; // 報(bào)告 PossiblyNullPropertyAssignment // NullPropertyAssignment對(duì)象確定是 null $a null; $a-foo hello; // 報(bào)告 NullPropertyAssignment而本文主題錯(cuò)誤的左值$a是確定存在的對(duì)象問(wèn)題只出在右值$s的可空性上。理解了這組對(duì)比在收到相關(guān)告警時(shí)就能第一時(shí)間判斷出問(wèn)題出在賦值者還是被賦值對(duì)象上。五、實(shí)戰(zhàn)修復(fù)三種標(biāo)準(zhǔn)方案方案一空值收窄推薦最規(guī)范既然錯(cuò)誤源于賦值值可能為 null最直接的修復(fù)是讓 Psalm 看到null 分支已被排除。Psalm 會(huì)自動(dòng)收窄?string類(lèi)型的變量因此可以function assignToA(?string $s) { $a new A(); if ($s ! null) { // 收窄后 $s 為 string $a-foo $s; } }或者在業(yè)務(wù)允許時(shí)提前返回/拋出異常將收窄與失敗路徑結(jié)合function assignToA(?string $s) { if ($s null) { throw new InvalidArgumentException($s cannot be null); } $a new A(); $a-foo $s; // 此時(shí) $s 已被收窄為 string }此外若在類(lèi)內(nèi)部通過(guò)$this-foo ...賦值也可以使用構(gòu)造函數(shù)參數(shù)提升、屬性默認(rèn)值或初始化器消除告警——只要最終賦值點(diǎn)上的類(lèi)型不含null分支即可。方案二單點(diǎn)抑制psalm-suppress對(duì)于高度確信不會(huì)為 null的代碼路徑可在賦值語(yǔ)句緊鄰的上方使用行內(nèi)抑制注釋function assignToA(?string $s) { $a new A(); /** psalm-suppress PossiblyNullPropertyAssignmentValue */ $a-foo $s; }psalm-suppress也可以寫(xiě)在函數(shù)/方法的 docblock 上作用于整個(gè)函數(shù)體參見(jiàn) dealing_with_code_issues.md 與 supported_annotations.md 中的使用說(shuō)明使用psalm-suppress all可一次性抑制該作用域內(nèi)所有錯(cuò)誤。需要提醒的是抑制只是掩蓋問(wèn)題應(yīng)配合代碼注釋說(shuō)明為什么這里不會(huì)為 null多余的抑制注釋會(huì)被--find-unused-psalm-suppress報(bào)告為 UnusedPsalmSuppress因此應(yīng)定期清理。方案三調(diào)整 errorLevel 或?qū)懭牖€(xiàn)由于該錯(cuò)誤屬于 level 3 分組在psalm.xml配置中把errorLevel提升到4 及以上PossiblyNullPropertyAssignmentValue將從錯(cuò)誤降級(jí)為info不阻塞構(gòu)建psalm errorLevel4 ... /psalm不過(guò)這樣做會(huì)同時(shí)放寬 level 3 分組下的所有其他錯(cuò)誤PossiblyNullArgument、PossiblyNullPropertyFetch、PossiblyInvalidArgument等屬于一刀切不推薦僅為一處告警全局調(diào)級(jí)。更精細(xì)的做法是將個(gè)別告警加入 ErrorBaseline基線(xiàn)文件通過(guò)--update-baseline生成基線(xiàn)來(lái)記錄并豁免既有問(wèn)題使新增問(wèn)題仍能被攔截。六、進(jìn)階Psalm 的可空性忽略機(jī)制前文源碼條件中的ignore_nullable_issues標(biāo)志值得單獨(dú)說(shuō)明。該標(biāo)志允許特定類(lèi)型主動(dòng)聲明忽略可空問(wèn)題從而跳過(guò)PossiblyNullPropertyAssignmentValue的判定。在倉(cāng)庫(kù)中可見(jiàn)到多種設(shè)置途徑屬性讀取時(shí)的自動(dòng)標(biāo)記在 InstancePropertyFetchAnalyzer.php 中$stmt_type-ignore_nullable_issues true被用于屬性 fetch 結(jié)果類(lèi)型的處理內(nèi)置函數(shù)返回值標(biāo)注例如 ArrayPopReturnTypeProvider.php 對(duì)array_pop這類(lèi)可能返回 null 但調(diào)用方通常已知情的內(nèi)置函數(shù)返回值設(shè)置該標(biāo)志psalm-ignore-nullable-return注解開(kāi)發(fā)者可以在函數(shù) docblock 上使用該注解參見(jiàn) supported_annotations.md告訴 Psalm此函數(shù)返回 null 的可能性不必?fù)?dān)憂(yōu)使返回類(lèi)型攜帶的null不再觸發(fā)下游的可空性錯(cuò)誤。官方文檔將其描述為略取巧但在特定場(chǎng)景下有用的手段——例如某個(gè)第三方函數(shù)以極小概率返回 null 而你確信當(dāng)前路徑不會(huì)命中時(shí)。此外還有一個(gè)值得關(guān)注的源碼細(xì)節(jié)在析構(gòu)函數(shù)__destruct中Psalm 會(huì)自動(dòng)抑制PossiblyNullPropertyAssignmentValue見(jiàn) FunctionLikeAnalyzer.php因?yàn)閷?duì)象生命周期結(jié)束時(shí)向?qū)傩詫?xiě)入可空值并無(wú)實(shí)際危害。這體現(xiàn)了 Psalm 在靜態(tài)嚴(yán)謹(jǐn)與實(shí)踐務(wù)實(shí)之間的平衡。七、在項(xiàng)目中定位該錯(cuò)誤在運(yùn)行 Psalm 后告警輸出會(huì)攜帶錯(cuò)誤名、文件路徑與位置信息例如ERROR: PossiblyNullPropertyAssignmentValue - src/example.php:15:9 $foo with non-nullable declared type string cannot be assigned nullable type ?string定位排查建議按以下順序進(jìn)行確認(rèn)屬性聲明類(lèi)型查看該屬性是原生類(lèi)型聲明如public string $foo;要求 PHP 7.4還是 docblock 聲明如/** var string */兩者都會(huì)被 Psalm 納入$class_property_type確認(rèn)賦值值類(lèi)型檢查右值來(lái)源——是函數(shù)參數(shù)?string、函數(shù)返回值、數(shù)組取值$arr[key]可能為 null還是屬性 fetch選擇修復(fù)策略?xún)?yōu)先空值收窄確認(rèn)安全才用psalm-suppress涉及存量代碼批量豁免時(shí)使用基線(xiàn)而不是全局調(diào)級(jí)。八、小結(jié)PossiblyNullPropertyAssignmentValue是 Psalm 在屬性賦值環(huán)節(jié)對(duì)可空值越界的精確提示。理解它的關(guān)鍵在于三點(diǎn)其一它由 InstancePropertyAssignmentAnalyzer.php 中的三條件判定賦值類(lèi)型可空、屬性類(lèi)型不可空、未忽略可空問(wèn)題其二它屬于 level 3 分組ERROR_LEVEL 3src/Psalm/Issue/PossiblyNullPropertyAssignmentValue.php默認(rèn)配置下作為錯(cuò)誤報(bào)告其三它與PossiblyNullPropertyAssignment左值對(duì)象可空在語(yǔ)義上有明確分工。掌握空值收窄、精準(zhǔn)抑制與基線(xiàn)豁免三種處置手段即可在日常開(kāi)發(fā)中既保持類(lèi)型嚴(yán)謹(jǐn)又不被誤報(bào)拖累效率。贊分享開(kāi)發(fā)工具代碼質(zhì)量質(zhì)量保障【免費(fèi)下載鏈接】psalmA PHP static analysis tool for finding errors and security vulnerabilities in PHP applications項(xiàng)目地址https://gitcode.com/gh_mirrors/ps/psalm點(diǎn)擊查看免費(fèi)下載相關(guān)推薦Psalm 靜態(tài)分析實(shí)戰(zhàn)NullIterator 錯(cuò)誤詳解與修復(fù)指南Psalm 靜態(tài)分析實(shí)戰(zhàn)NullIterator 錯(cuò)誤詳解與修復(fù)指南 導(dǎo)讀 NullIterator 是 PsalmPHP 靜態(tài)分析工具在檢測(cè)到代碼試圖對(duì)開(kāi)發(fā)工具代碼質(zhì)量質(zhì)量保障Psalm 靜態(tài)分析InvalidFalsableReturnType 錯(cuò)誤詳解與修復(fù)指南Psalm 靜態(tài)分析InvalidFalsableReturnType 錯(cuò)誤詳解與修復(fù)指南 導(dǎo)讀 InvalidFalsableReturnType 是 Ps開(kāi)發(fā)工具代碼質(zhì)量質(zhì)量保障Psalm 靜態(tài)分析InvalidScope 錯(cuò)誤的觸發(fā)場(chǎng)景、源碼實(shí)現(xiàn)與修復(fù)指南Psalm 靜態(tài)分析InvalidScope 錯(cuò)誤的觸發(fā)場(chǎng)景、源碼實(shí)現(xiàn)與修復(fù)指南 導(dǎo)讀 InvalidScope 是 PsalmPHP 靜態(tài)分析工具內(nèi)置的開(kāi)發(fā)工具代碼質(zhì)量質(zhì)量保障創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考