ข้ามไปยังเนื้อหา

Strict types ทุกที่

Spec: ISO 32000-2, §7.5.5 Evidence: Code-backed PHPStan: Level 10, no src baseline

NextPDF รัน PHPStan ที่ Level 10 บนซอร์สของเอนจินโดยไม่มี baseline สำหรับระงับข้อผิดพลาด หน้านี้อธิบายว่าเหตุใด “ไม่มี baseline” จึงเป็นการตัดสินใจด้านการออกแบบมากกว่ารายละเอียดของเครื่องมือ และความเข้มงวดนี้ช่วยอะไรกับไปป์ไลน์ที่ต้องหลีกเลี่ยงการจัดการข้อมูลผิดพลาดแบบเงียบๆอย่างแท้จริง

ในแอปพลิเคชันส่วนใหญ่ การกำหนดชนิดแบบเข้มงวดเป็นเพียงสุขลักษณะของโค้ด แต่ในเอนจิน PDF การกำหนดชนิดแบบเข้มงวดใกล้เคียงกับกลไกสร้างความถูกต้องมากกว่า รูปแบบไฟล์นี้ไม่ผ่อนปรนต่อข้อผิดพลาด ตัวอ่านควรค้นหาเนื้อหาด้วยการอ่านไฟล์จากส่วนท้ายผ่าน trailer และ cross-reference table ดังนั้น byte offset ที่ฝั่งตัวเขียนกำหนดไว้จึงต้องแม่นยำ ลองพิจารณาชนิดข้อมูลที่ถูกขยายเป็น mixed แบบเงียบๆ ค่า int ที่กลายเป็น string แบบเงียบๆ หรือค่า nullable ที่ถูก dereference โดยไม่มีการตรวจสอบ สิ่งเหล่านี้ล้วนอาจสร้างไฟล์ที่เปิดได้ตามปกติในโปรแกรมดูหนึ่ง แต่ไม่ผ่านการตรวจสอบความถูกต้องในอีกโปรแกรมหนึ่งหลังจากนั้นหลายสัปดาห์ โดยไม่มี stack trace ที่ชี้กลับไปยังต้นเหตุ

ความล้มเหลวที่มีต้นทุนสูงในขอบเขตงานนี้คือความล้มเหลวแบบเงียบๆ การกำหนดชนิดแบบเข้มงวดร่วมกับตัววิเคราะห์แบบเข้มงวดคือวิธีที่เอนจินใช้แปลงความล้มเหลวขณะรันแบบเงียบๆประเภทหนึ่งให้กลายเป็นความล้มเหลวที่ชัดเจนในเวลาบิลด์

  • ซอร์สของเอนจินถูกวิเคราะห์ที่ PHPStan Level 10 ซึ่งเป็นระดับที่เข้มงวดที่สุด ยืนยันได้จาก phpstan.neon.dist
  • ในเอนจินนี้ ไม่มี baseline สำหรับการระงับข้อผิดพลาดของซอร์ส การกำหนดค่าล็อกการวิเคราะห์ซอร์สไว้ที่ศูนย์ข้อผิดพลาด การถดถอยจะทำให้บิลด์ล้มเหลวแทนที่จะถูกกลบไว้ในไฟล์ ignore ที่ขยายตัวขึ้นเรื่อยๆ
  • รายการ ignoreErrors เพียงไม่กี่รายการที่มีอยู่นั้น แคบ จำกัดขอบเขตด้วย identifier และ path และมีเหตุผลรองรับเป็นรายตัว ในการกำหนดค่า (ขอบเขต soft-dependency ข้ามแพ็กเกจและรอยต่อของเทสต์ที่เป็น reflection-target) ไม่ใช่ baseline แบบเหมารวม
  • นอกจากนี้ โปรไฟล์เข้มงวด แยกต่างหากรันที่ level: max และห้ามเพิ่มรายการ ignore ใหม่ใดๆ ดังนั้นโค้ดใหม่จึงถูกยึดตามมาตรฐานที่เข้มงวดยิ่งขึ้น
  • ผลที่ตั้งใจให้เกิดคือแรงกดดันด้านการออกแบบ โค้ดที่ไม่สามารถแสดงออกได้อย่างซื่อตรงต่อชนิดข้อมูลจะไม่ผ่าน จึงต้องถูกออกแบบใหม่แทนที่จะถูกระงับ

ความแตกต่างระหว่าง “เราใช้ตัววิเคราะห์แบบเข้มงวด” กับ “เราใช้ตัววิเคราะห์แบบเข้มงวดโดยไม่มี baseline” คือแก่นของเรื่องทั้งหมด จึงต้องกล่าวให้แม่นยำ

โดยทั่วไป baseline จะบันทึกทุกการละเมิดที่มีอยู่และสั่งให้ตัววิเคราะห์เพิกเฉยต่อรายการเหล่านั้นพอดี วิธีนี้ใช้ได้จริงเมื่อต้องนำการวิเคราะห์แบบสถิตมาใช้กับโค้ดเบสรุ่นเก่า แต่ก็มีต้นทุน baseline กลายเป็นบัญชีหนี้เงียบที่ระบบชนิดข้อมูลตกลงว่าจะไม่มอง การละเมิดรายการใหม่ประเภทเดียวกันอาจหลุดรอดเข้ามาอยู่ข้างรายการเดิมได้ คำมั่นของตัววิเคราะห์จึงอ่อนลงจาก “โค้ดนี้สะอาดด้านชนิดข้อมูล” เป็น “โค้ดนี้ไม่ได้แย่ลงกว่าเดิม”

NextPDF ไม่ยอมแลกแบบนั้นกับซอร์สของเอนจิน การกำหนดค่าตรึงการวิเคราะห์ซอร์สไว้ที่ศูนย์ข้อผิดพลาดและเปิดใช้ reportUnmatchedIgnoredErrors ดังนั้นแม้แต่การระงับที่ ค้างเก่า หรือรายการที่ไม่ตรงกับสิ่งใดอีกต่อไป ก็ทำให้บิลด์ล้มเหลว รายการ ignore แบบแคบที่ยังเหลืออยู่ถูกจำกัดขอบเขตไว้กับ identifier ของข้อผิดพลาดและไฟล์ที่เฉพาะเจาะจง แต่ละรายการมีคำอธิบายในบรรทัดว่าเหตุใดขอบเขตนั้นจึงตั้งใจไว้เช่นนี้ (ตัวอย่างเช่น core ที่เขียนให้ทำงานกับอินเทอร์เฟซ Pro/Enterprise ที่จงใจไม่พึ่งพาแบบรูปธรรม) ผู้รีวิวจึงอ่านและประเมินแต่ละรายการได้ ไม่มีรายการคลุมเครือที่จะหลุดจากการติดตาม

ขั้นตอนการทำงานที่รักษาความซื่อตรงนี้ไว้:

  1. Change proposed New or modified engine code.
  2. Level 10 analysis Strictest PHPStan level over src/, treatPhpDocTypesAsCertain on.
  3. Zero-error gate No source baseline; unmatched ignores also fail.
  4. Strict profile level: max; no new ignore entries permitted.
  5. Redesign, not suppress If it cannot be expressed honestly, the design changes.
การเปลี่ยนแปลงเข้าถึงซอร์สของเอนจินได้อย่างไร: การเปลี่ยนแปลงที่ไม่ซื่อตรงต่อชนิดข้อมูลไม่สามารถผ่านด่านได้ จึงถูกออกแบบใหม่แทนที่จะถูกระงับ

treatPhpDocTypesAsCertain เป็นส่วนหนึ่งของเรื่องนี้ annotation ของ PHPDoc ถูกถือเป็นข้อเท็จจริงพื้นฐาน ดังนั้น @param list<T> หรือ @return non-empty-string จึงไม่ใช่คอมเมนต์ที่ตัววิเคราะห์เพียงรับทราบแล้วปล่อยผ่าน แต่เป็นคำมั่นที่ถูกตรวจสอบ annotation และชนิดข้อมูลขณะรันจึงถูกบังคับให้สอดคล้องกัน

หน้านี้เป็น Evidence: Code-backed การกำหนดค่าคือหลักฐาน:

  • phpstan.neon.dist ตั้งค่า level: 10, phpVersion: 80400 วิเคราะห์ src และไม่มี คีย์ baseline: ทั้งยังไม่มี phpstan-baseline.neon สำหรับการวิเคราะห์ซอร์ส
  • ไฟล์เดียวกันตั้งค่า treatPhpDocTypesAsCertain: true และ reportUnmatchedIgnoredErrors: true พร้อมหมายเหตุในบรรทัดว่าการวิเคราะห์ซอร์สแบบ L10 ถูกล็อกไว้ที่ศูนย์ข้อผิดพลาด และการถดถอยใดๆต้องทำให้ CI ล้มเหลว
  • รายการ ignoreErrors ที่เหลือแต่ละรายการถูกจำกัดขอบเขตด้วย identifier และมักจำกัดด้วย path พร้อมคอมเมนต์อธิบายเหตุผลของ soft-dependency และ reflection-target รายการเหล่านี้ไม่ใช่ baseline ที่สร้างขึ้นแบบเหมารวม
  • phpstan-strict.neon.dist รับช่วงการกำหนดค่านั้น ยกระดับเป็น max และตรึงรายการ ignore ไว้เพื่อให้ ไม่สามารถเพิ่มรายการใหม่ ภายใต้โปรไฟล์เข้มงวดได้

มุมมองด้านมาตรฐานนั้นตรงไปตรงมา เอนจินต้องสร้างไฟล์ที่ตัวอ่านสามารถนำทางได้จาก trailer และ cross-reference table ตาม Spec: ISO 32000-2, §7.5.5 byte offset ที่แม่นยำเป็น ปัญหาด้านชนิดข้อมูลก่อนจะเป็นปัญหาด้านการ serialize offset เพราะ offset เป็น จำนวนเต็มที่ต้องไม่กลายเป็นสิ่งอื่นใดแบบเงียบๆ ไปป์ไลน์ที่ สะอาดด้านชนิดข้อมูลที่ Level 10 ได้กำจัดวิธีส่วนใหญ่ที่การคำนวณทางคณิตศาสตร์ จะผิดพลาดแบบเงียบๆออกไปแล้ว

การกำหนดชนิดแบบเข้มงวดเห็นได้ชัดที่สุดเมื่อกฎของขอบเขตงานถูกเข้ารหัสเป็นชนิดข้อมูลแทนการตรวจสอบขณะรัน ตัวจำแนกความสอดคล้องตอบคำถามระดับสเปกด้วย match ที่ครอบคลุมครบทุกกรณี ดังนั้นกรณีที่ไม่ได้จัดการจึงเป็นข้อผิดพลาดด้านชนิดข้อมูล ไม่ใช่ PDF ที่ผิด:

declare(strict_types=1);
enum ConformanceMode: string
{
case Plain = 'plain';
case PdfUa2 = 'pdfua2';
case PdfA4 = 'pdfa4';
/** @return 2|3|4|null */
public function pdfaPart(): ?int
{
return match ($this) {
self::PdfA4 => 4,
default => null,
};
}
}

บรรทัด @return 2|3|4|null ไม่ใช่เอกสารประกอบ ภายใต้ treatPhpDocTypesAsCertain annotation นี้จะถูกตรวจสอบ ผู้เรียกที่สมมติว่าผลลัพธ์เป็น int เสมอจะได้รับแจ้งในเวลาวิเคราะห์ ก่อนที่จะมีการเขียนหมายเลขส่วน PDF/A ที่ไม่สอดคล้องตามมาตรฐานแม้แต่ไบต์เดียว

กับดักคือการตีความ “ไม่มี baseline” ว่าเป็น “บังเอิญที่โค้ดไม่มีการละเมิด” นั่นเป็นความเข้าใจที่กลับด้าน การไม่มี baseline คือ สาเหตุ ไม่ใช่ผลลัพธ์ที่โชคดี เนื่องจากไม่มีที่ให้พักการละเมิด โค้ดที่อาจก่อให้เกิดการละเมิดจึงต้องถูกเขียนอีกแบบหนึ่ง Level 10 ที่ไม่มี baseline สำหรับซอร์สเป็นข้อจำกัดที่กำหนดรูปแบบการออกแบบ ไม่ใช่ใบรายงานผลที่อธิบายการออกแบบนั้นในภายหลัง

ความเข้าใจผิดที่สองคือรายการ ignoreErrors เพียงไม่กี่รายการเป็น baseline ในอีกชื่อหนึ่ง ไม่ใช่เช่นนั้น baseline ถูกสร้างขึ้นแบบเหมารวมและคลุมเครือ แต่รายการเหล่านี้ถูกเขียนเป็นรายตัว จำกัดขอบเขตด้วย identifier มีคำอธิบาย และมี reportUnmatchedIgnoredErrors คอยกำกับ จึงไม่สามารถเสื่อมสภาพไปโดยไม่มีใครสังเกตได้

หน้านี้เกี่ยวกับการวิเคราะห์ ซอร์สของเอนจิน ชุดเทสต์ถูกวิเคราะห์ภายใต้ขอบเขตและการกำหนดค่าที่แยกต่างหากซึ่งจงใจให้แตกต่าง “ไม่มี baseline” ในที่นี้เป็นคำกล่าวเกี่ยวกับ src/ ไม่ใช่การอ้างว่าการวิเคราะห์เสริมทุกชนิดในรีพอซิทอรีปราศจาก baseline PHPStan พิสูจน์ความถูกต้องด้านชนิดข้อมูล ไม่ใช่ความถูกต้องด้านพฤติกรรม PHPStan ไม่ได้แทนที่พีระมิดการทดสอบ เพียงแต่กำจัดความล้มเหลวประเภทหนึ่งที่ไม่เช่นนั้นการทดสอบจะต้องไล่ตาม ระดับ แฟล็ก และชุด ignore ที่แน่นอนเป็นจริง ณ วันที่รีวิวของหน้านี้ แหล่งข้อมูลที่เชื่อถือได้คือ phpstan.neon.dist และ phpstan-strict.neon.dist ในรีพอซิทอรี core เสมอ

เอดิชันไม่เปลี่ยนวินัยนี้ ทุกเอดิชันถูกสร้างจากซอร์ส Level 10 เดียวกัน:

การวิเคราะห์ซอร์สที่ Level 10 — edition availability
Edition Availability
Core ซอร์สของ Core ถูกวิเคราะห์ที่ Level 10 โดยไม่มี baseline สำหรับซอร์ส
Pro Pro ถูกสร้างบนวินัยซอร์ส Level 10 เดียวกัน
Enterprise Enterprise ถูกสร้างบนวินัยซอร์ส Level 10 เดียวกัน
  • PHPStan Level 10 — ระดับการวิเคราะห์ที่เข้มงวดที่สุด ถือว่าค่าที่ไม่มีชนิดและค่าที่กำหนดชนิดแบบหลวมเป็นข้อผิดพลาดแทนที่จะเป็นคำเตือน
  • Baseline — บันทึกที่สร้างขึ้นจากการละเมิดที่มีอยู่ซึ่งตัววิเคราะห์ถูกสั่งให้เพิกเฉย NextPDF ไม่ใช้ baseline เลยสำหรับซอร์สของเอนจิน
  • treatPhpDocTypesAsCertain — การตั้งค่าของ PHPStan ที่ถือว่า annotation ชนิดข้อมูลของ PHPDoc เป็นข้อเท็จจริงที่ถูกตรวจสอบ ไม่ใช่คอมเมนต์เชิงคำแนะนำ
  • reportUnmatchedIgnoredErrors — การตั้งค่าที่ทำให้บิลด์ล้มเหลวเมื่อรายการ ignore ไม่ตรงกับสิ่งใดอีกต่อไป เพื่อป้องกันการระงับที่ค้างเก่า
  • แรงกดดันด้านการออกแบบ — ผลของข้อจำกัดที่บังคับให้โค้ดถูกเขียนในแบบใดแบบหนึ่ง ตรงกันข้ามกับการตรวจสอบที่เพียงแค่วัดผลเท่านั้น