Bỏ qua để đến nội dung

Kiểu nghiêm ngặt ở mọi nơi

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

NextPDF chạy PHPStan ở Level 10 trên mã nguồn của engine mà không dùng baseline để chặn lỗi. Trang này giải thích vì sao “không có baseline” là một quyết định thiết kế, chứ không phải chi tiết công cụ, và sự nghiêm ngặt đó thực sự mang lại điều gì cho một pipeline có nhiệm vụ tránh âm thầm xử lý sai dữ liệu.

Trong hầu hết ứng dụng, dùng kiểu nghiêm ngặt là một thói quen tốt. Với một engine PDF, nó gần với cơ chế đảm bảo tính đúng đắn hơn. Định dạng này không khoan nhượng. Một reader được kỳ vọng sẽ định vị nội dung bằng cách đọc tệp từ cuối lên, thông qua trailer và bảng cross-reference, nên các byte offset ở phía ghi phải chính xác. Hãy hình dung một kiểu âm thầm nới rộng thành mixed, một int âm thầm trở thành string, hoặc một giá trị nullable bị truy xuất mà không kiểm tra. Bất kỳ trường hợp nào trong số này cũng có thể tạo ra một tệp mở được trong một trình xem nhưng lại không vượt qua kiểm tra hợp lệ trong một trình xem khác, nhiều tuần sau đó, mà không có stack trace nào chỉ ra nguyên nhân.

Những lỗi tốn kém trong lĩnh vực này thường là lỗi âm thầm. Dùng kiểu nghiêm ngặt cùng một analyzer nghiêm ngặt là cách engine biến cả một nhóm lỗi âm thầm lúc chạy thành lỗi rõ ràng lúc build.

  • Mã nguồn của engine được phân tích ở PHPStan Level 10 — mức nghiêm ngặt nhất — và được xác minh trong phpstan.neon.dist.
  • Hoàn toàn không có baseline nào để chặn lỗi cho mã nguồn. Cấu hình khóa kết quả phân tích mã nguồn ở trạng thái không có lỗi. Một lỗi tái xuất sẽ làm build thất bại, thay vì bị nuốt vào một tệp ignore ngày càng phình to.
  • Một vài mục ignoreErrors hiện có đều hẹp, giới hạn theo identifier và path, và được biện minh riêng cho từng mục trong cấu hình (các ranh giới soft-dependency giữa các package và các test seam nhắm tới reflection) — không phải một baseline hàng loạt.
  • Một profile nghiêm ngặt riêng chạy level: max và cấm thêm bất kỳ mục ignore mới nào, nên mã mới được giữ ở tiêu chuẩn còn chặt chẽ hơn.
  • Tác dụng mong muốn là áp lực thiết kế: mã không thể biểu đạt trung thực về kiểu sẽ không vượt qua, nên phải được thiết kế lại thay vì bị che giấu.

Khác biệt giữa “chúng tôi dùng một analyzer nghiêm ngặt” và “chúng tôi dùng một analyzer nghiêm ngặt không có baseline” chính là điểm cốt lõi, nên cần nói thật chính xác.

Một baseline ghi lại mọi vi phạm hiện có và yêu cầu analyzer bỏ qua đúng những vi phạm đó. Đó là một cách thực dụng để đưa phân tích tĩnh vào một codebase cũ, nhưng nó có cái giá của nó. Baseline trở thành một sổ nợ âm thầm mà hệ thống kiểu đã đồng ý không nhìn tới. Những vi phạm mới cùng loại có thể lọt vào bên cạnh các vi phạm đã có. Lời cam kết của analyzer suy yếu từ “mã này sạch về kiểu” thành “mã này không tệ hơn trước.”

NextPDF không chấp nhận đánh đổi đó cho mã nguồn của engine. Cấu hình cố định kết quả phân tích mã nguồn ở trạng thái không có lỗi và bật reportUnmatchedIgnoredErrors, nên kể cả một mục chặn lỗi cũ kỹ — một mục không còn khớp với bất cứ thứ gì — cũng làm build thất bại. Những mục ignore hẹp còn lại được giới hạn theo một error identifier và một tệp cụ thể. Mỗi mục đều kèm theo lời giải thích inline về lý do ranh giới đó là có chủ ý (ví dụ, core lập trình theo một interface của Pro/Enterprise mà nó chủ ý không phụ thuộc cụ thể vào đó). Người review có thể đọc và đánh giá từng mục. Không có một danh sách mờ ám nào để rồi mất dấu.

Quy trình giữ cho điều này luôn trung thực:

  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.
Cách một thay đổi đi vào mã nguồn của engine: một thay đổi không trung thực về kiểu không thể vượt qua cổng kiểm soát, nên nó được thiết kế lại thay vì bị che giấu.

treatPhpDocTypesAsCertain là một phần của cơ chế này. Các chú thích PHPDoc được xem là sự thật nền tảng, nên một @param list<T> hay @return non-empty-string không phải là một comment để analyzer lịch sự bỏ qua. Đó là một lời cam kết được kiểm tra. Chú thích và kiểu lúc chạy buộc phải nhất quán với nhau.

Trang này là Evidence: Code-backed . Chính cấu hình là bằng chứng:

  • phpstan.neon.dist đặt level: 10, phpVersion: 80400, phân tích src, và không chứa khóa baseline: nào — không có phpstan-baseline.neon nào dành cho phân tích mã nguồn.
  • Cùng tệp đó đặt treatPhpDocTypesAsCertain: truereportUnmatchedIgnoredErrors: true, kèm một ghi chú inline rằng kết quả phân tích mã nguồn ở L10 bị khóa ở mức không có lỗi và mọi lỗi tái xuất đều phải làm CI thất bại.
  • Các ignoreErrors còn lại đều được giới hạn theo identifier và thường là cả path, kèm các comment giải thích lý do về soft-dependency và reflection-target — chúng không phải một baseline sinh ra hàng loạt.
  • phpstan-strict.neon.dist kế thừa cấu hình đó, nâng level lên max, và đóng băng danh sách ignore để không một mục mới nào có thể được thêm vào trong profile nghiêm ngặt.

Góc nhìn từ tiêu chuẩn rất rõ ràng. Engine phải tạo ra những tệp mà reader có thể điều hướng từ trailer và bảng cross-reference theo Spec: ISO 32000-2, §7.5.5 . Các byte offset chính xác là một vấn đề về kiểu trước khi là một vấn đề về serialization. Một offset là một số nguyên không bao giờ được phép âm thầm trở thành bất cứ thứ gì khác. Một pipeline sạch về kiểu ở Level 10 đã loại bỏ phần lớn những cách mà phép tính số học có thể âm thầm sai lệch.

Việc dùng kiểu nghiêm ngặt thể hiện rõ nhất ở nơi một quy tắc nghiệp vụ được mã hóa thành một kiểu, thay vì một phép kiểm tra lúc chạy. Bộ phân biệt mức tuân thủ trả lời các câu hỏi ở cấp đặc tả bằng một match bao quát mọi trường hợp, nên một trường hợp chưa được xử lý là lỗi kiểu, chứ không phải một PDF sai:

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,
};
}
}

Khai báo @return 2|3|4|null không phải là tài liệu. Dưới treatPhpDocTypesAsCertain, nó được kiểm tra. Một caller giả định kết quả luôn là một int sẽ được báo ngay lúc phân tích, trước khi dù chỉ một byte của một số part PDF/A không tuân thủ được ghi ra.

Cái bẫy là hiểu “không có baseline” thành “mã tình cờ không có vi phạm nào.” Cách hiểu đó bị đảo ngược. Việc không có baseline là nguyên nhân, chứ không phải một kết quả may mắn. Vì không có chỗ nào để gửi tạm một vi phạm, mã đáng lẽ tạo ra vi phạm đó buộc phải được viết theo cách khác. Level 10 không có baseline cho mã nguồn là một ràng buộc định hình thiết kế, chứ không phải một phiếu báo cáo mô tả thiết kế sau khi mọi việc đã xong.

Một hiểu lầm thứ hai: một số ít mục ignoreErrors chỉ là một baseline mang tên khác. Không phải vậy. Một baseline được sinh ra hàng loạt và mờ ám. Những mục này được viết riêng cho từng trường hợp, giới hạn theo identifier, có giải thích, và được bảo vệ bởi reportUnmatchedIgnoredErrors, nên chúng không thể mục ruỗng mà không ai hay biết.

Trang này nói về việc phân tích mã nguồn của engine. Bộ test được phân tích trong một phạm vi và cấu hình riêng, được cố tình tách biệt; “không có baseline” ở đây là một khẳng định về src/, chứ không phải một tuyên bố rằng mọi phân tích phụ trợ trong repository đều không có baseline. PHPStan chứng minh tính đúng đắn về kiểu, chứ không phải tính đúng đắn về hành vi. Nó không thay thế kim tự tháp kiểm thử, mà chỉ loại bỏ một nhóm lỗi mà nếu không thì các bài test sẽ phải truy đuổi. Mức level, các flag và tập hợp ignore chính xác là đúng tính đến ngày soát xét của trang này. Nguồn có thẩm quyền luôn là phpstan.neon.distphpstan-strict.neon.dist trong repository core.

Phiên bản không làm thay đổi nguyên tắc này. Mọi phiên bản đều được build từ cùng một mã nguồn Level 10:

Level 10 source analysis — edition availability
Edition Availability
Core Mã nguồn Core được phân tích ở Level 10 mà không có baseline cho mã nguồn.
Pro Pro được xây dựng trên cùng nguyên tắc mã nguồn ở Level 10.
Enterprise Enterprise được xây dựng trên cùng nguyên tắc mã nguồn ở Level 10.
  • PHPStan Level 10 — mức phân tích nghiêm ngặt nhất, coi các giá trị không có kiểu và có kiểu lỏng lẻo là lỗi chứ không phải cảnh báo.
  • Baseline — một bản ghi được sinh ra về các vi phạm hiện có mà analyzer được yêu cầu bỏ qua. NextPDF không dùng baseline nào cho mã nguồn của engine.
  • treatPhpDocTypesAsCertain — một thiết lập của PHPStan coi các chú thích kiểu PHPDoc là những sự thật được kiểm tra, chứ không phải comment mang tính khuyến nghị.
  • reportUnmatchedIgnoredErrors — một thiết lập làm build thất bại khi một mục ignore không còn khớp với bất cứ thứ gì, nhằm ngăn các mục chặn lỗi lỗi thời.
  • Áp lực thiết kế — tác dụng của một ràng buộc buộc mã phải được viết theo một cách nhất định, trái ngược với một phép kiểm tra chỉ đo lường nó.