Psalm กับ PHPStan ทำงานคล้ายกัน แต่ Psalm ไล่เส้นทางของข้อมูลได้
ทั้งสองตัวเป็น static analysis คืออ่านโค้ดทั้งโปรเจกต์เพื่อหาบั๊กโดยไม่ต้องรัน ในโปรเจกต์ Laravel เรามักใช้ PHPStan กับ Larastan เป็นหลัก (โพสต์นี้แยกให้เห็นว่าเครื่องมือแต่ละแบบทำหน้าที่อะไร) จุดที่ Psalm เด่นกว่ามานานคือ taint analysis
แนวคิดคือมี source กับ sink source คือที่ที่ข้อมูลจากคนนอกเข้ามา เช่น $_GET, body ของ request หรือไฟล์ที่อัปโหลด sink คือที่ที่ข้อมูลไปแล้วอันตราย เช่น query SQL, shell_exec() หรือ HTML ที่ไม่ escape ถ้า Psalm เห็นทางที่ข้อมูลจาก source ไปถึง sink โดยไม่ผ่านการกรอง ก็รายงานเป็น TaintedSql, TaintedShell, TaintedHtml ตามชนิดของ sink
taint analysis รันพร้อมการตรวจปกติทุกครั้ง
ใน Psalm 6 taint analysis เป็นอีกโหมดที่ต้องสั่งแยก รุ่น 7 รวมการตรวจชนิดข้อมูล การตรวจความปลอดภัย และการหา dead code ไว้ในรอบเดียว (PR #11384) taint analysis จึงเปิดอยู่เสมอ ที่ทำได้เพราะเปลี่ยนมาเก็บ taint เป็น bitmask และสรุปผลของแต่ละฟังก์ชันให้สั้นลงก่อนเชื่อมเป็นกราฟ
ข้อมูลไหลผ่านได้หลายทางกว่าเดิม ทั้ง closure, generator, trait และ parameter ที่รับแบบ reference
$run = function (string $command): void {
shell_exec($command);
};
$run((string) $_GET['cmd']); // TaintedShell ผ่าน closure
ของใหม่อื่นในส่วนนี้
- ชนิดของ issue ใหม่ เช่น
TaintedNosqlสำหรับ MongoDB,TaintedLlmPromptสำหรับ prompt injection เมื่อเอาข้อความของผู้ใช้ใส่ prompt ของ AI ตรง ๆ และTaintedUrlComponentกับTaintedUrlPathสำหรับ URL ที่ประกอบจากข้อมูลของผู้ใช้ - source ใหม่ เช่น
php://input, ค่าใน$_SERVERและ$_FILESที่ client ส่งมาเอง,$argvและข้อความของ exception - stub ของไลบรารีที่ใช้บ่อย เช่น Guzzle, PSR-7, Monolog, PhpSpreadsheet บอก Psalm ว่าเมธอดไหนเป็น source หรือ sink และโหลดเฉพาะเมื่อโปรเจกต์ใช้ไลบรารีนั้น
- รายงานแบบ SARIF ให้ issue ด้านความปลอดภัยมีระดับ
SECURITYใช้ส่งขึ้นหน้า code scanning ของ GitHub ได้
ถ้ายังไม่อยากเปิด ใส่ runTaintAnalysis="false" ใน psalm.xml ได้ตามคู่มืออัปเกรด
capabilities บอกว่าฟังก์ชันมี side effect แบบไหน
เดิม Psalm แบ่งฟังก์ชันแค่ว่า pure (คืนค่าจาก argument อย่างเดียว ไม่แตะอะไรข้างนอก) หรือไม่ pure รุ่น 7 (PR #11984) ยืมแนวคิด contexts and capabilities ของ Hack มาให้ฟังก์ชันประกาศว่ามี side effect อะไรได้บ้าง ได้แก่ read-props, write-this-props, write-props, read-globals, write-globals, write-refs, io และ time โดยมี pure กับ impure อยู่สองปลาย
/** @psalm-capabilities time */
function today(): string
{
return date('Y-m-d'); // ผ่าน เพราะประกาศว่าอ่านเวลาได้
}
/** @psalm-pure */
function todayWrong(): string
{
return date('Y-m-d'); // ImpureFunctionCall เพราะ date() ต้องใช้ time
}
Psalm จัดกลุ่มฟังก์ชันของ PHP เองไว้แล้วด้วย อ่านเขียนไฟล์และ random_int() ต้องมี io, time() กับ date() ที่ไม่ส่ง timestamp ต้องมี time ส่วน mt_rand() กับ ini_set() ต้องมี write-globals โค้ดที่เคยติด @psalm-pure แล้วเรียกพวกนี้จะเริ่มโดนรายงาน
@psalm-pure กับ @psalm-immutable ยังเป็นคำที่แนะนำให้ใช้ ส่วน @psalm-mutation-free และ @psalm-external-mutation-free ยังใช้ได้แต่ไม่อยู่ในเอกสารแล้ว เทียบได้กับ read-props และ read-props|write-this-props|write-refs ตามลำดับ นอกจากนี้มี purity template ให้ฟังก์ชันอย่าง array_map() เป็น pure เมื่อ callback ที่ส่งเข้ามาเป็น pure
dead code หาจากกราฟการเรียกจริง
PR #11939 เปลี่ยนวิธีนับว่าโค้ดไหนยังใช้อยู่ ตอนนี้โค้ดต้องเรียกถึงได้จาก entry point ถึงจะนับว่าใช้ ถ้ามีสองคลาสที่เรียกกันเองแต่ไม่มีใครเรียกถึง Psalm จะรายงาน UnusedClass ทั้งคู่ exception ที่สร้างขึ้นแต่ไม่เคย throw ก็โดนรายงานด้วย
กับโปรเจกต์ Laravel ต้องดูผลส่วนนี้ให้ดี เพราะ Laravel เรียก controller, listener และ job ผ่าน route, event และ container ซึ่ง Psalm อาจมองไม่เห็นถ้า plugin ไม่ได้บอกไว้
ของอื่นในรุ่นนี้
- plugin มากับตัว plugin ของ PHPUnit, Symfony, Laravel, Mockery และ webmozart/assert เปิดให้เองเมื่อ
composer.jsonใช้ไลบรารีนั้น (loadLibraryPlugins) - template มีค่าเริ่มต้นได้ เช่น
@template TError of Throwable = RuntimeException - รองรับ PHP 8.5 และ 8.6 อ่านชนิดข้อมูลของแต่ละขั้นใน pipe
|>ได้ --init-ciสร้าง workflow ของ GitHub Actions ให้ และ Docker image ย้ายไปอยู่ที่ghcr.io/psalm/psalm- ออก release ทุกวัน ไม่ต้องตาม
dev-masterอีก
อัปเกรดจาก Psalm 6
composer require --dev vimeo/psalm:^7.0 -W
- ต้องใช้ PHP 8.2 ถึง 8.6 แบบ 64-bit และ
psalm/plugin-phpunit^0.21 - capability แต่ละตัวหมายถึงตัวเองอย่างเดียวแล้ว
write-propsไม่รวมread-propsเหมือนเดิม ต้องเขียนเต็มเป็นread-props|write-props - คำแนะนำให้ใส่ annotation (
MissingPureAnnotationและพวก) ปิดไว้เป็นค่าเริ่มต้น ถ้า baseline เดิมมีรายการพวกนี้ ให้รัน--update-baselineลบออก หรือเปิดsuggestPurityAnnotations="true" - คนเขียน plugin ให้ require
psalm/psalm-plugin-apiแทน เพราะเลขเวอร์ชันของ Psalm ไม่ได้บอกการเปลี่ยนของ plugin API แล้ว
รายการเปลี่ยนทั้งหมดอยู่ใน release notes และบทความต้นฉบับครับ






