Перейти к содержимому

Строгие типы везде

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

NextPDF запускает PHPStan на Level 10 для исходного кода движка без базовой линии подавлений. На этой странице объясняется, почему “отсутствие базовой линии” — это проектное решение, а не деталь инструментария, и что именно такая строгость даёт конвейеру, который не должен молча искажать данные.

В большинстве приложений строгая типизация — это гигиена кода. В движке PDF она ближе к механизму корректности. Формат не прощает ошибок. Предполагается, что программа чтения находит содержимое, читая файл с конца — через трейлер и таблицу перекрёстных ссылок, поэтому байтовые смещения, записываемые при формировании файла, должны быть точными. Представьте тип, который незаметно расширяется до mixed, int, который молча становится string, или nullable-значение, которое разыменовывается без проверки. Любая из этих ошибок может породить файл, который нормально открывается в одной программе просмотра и не проходит проверку в другой — спустя недели и без трассировки стека, указывающей на причину.

В этой области особенно дорого обходятся именно молчаливые сбои. Строгая типизация вместе со строгим анализатором позволяет движку превращать целый класс молчаливых сбоев во время выполнения в явные ошибки на этапе сборки.

  • Исходный код движка анализируется на PHPStan Level 10 — самом строгом уровне; это закреплено в phpstan.neon.dist.
  • Для исходного кода нет базовой линии подавления. Конфигурация фиксирует анализ исходного кода на нуле ошибок. Регрессия приводит к сбою сборки, а не растворяется в разрастающемся файле игнорирования.
  • Те немногие записи ignoreErrors, которые существуют, являются узкими, ограниченными по идентификатору и пути и отдельно обоснованными в конфигурации (границы мягких межпакетных зависимостей и точки сопряжения тестов с целями рефлексии), а не массовой базовой линией.
  • Отдельный строгий профиль работает на level: max и запрещает любые новые записи игнорирования, поэтому для нового кода действует ещё более жёсткий стандарт.
  • Предполагаемый эффект — проектное давление: код, который нельзя выразить типобезупречно, не проходит проверку; его перепроектируют, а не подавляют.

Разница между “мы используем строгий анализатор” и “мы используем строгий анализатор без базовой линии” — в этом и заключается весь смысл, поэтому важно быть точными.

Сама базовая линия фиксирует каждое существующее нарушение и указывает анализатору игнорировать именно его. Это прагматичный способ внедрить статический анализ в унаследованной кодовой базе, но у него есть цена. Базовая линия превращается в тихий реестр долга, на который система типов согласилась не смотреть. Новые нарушения того же рода могут незаметно проскользнуть рядом с уже существующими. Обещание анализатора ослабевает с “этот код типобезупречен” до “этот код не хуже, чем был”.

NextPDF не идёт на этот компромисс для исходного кода движка. Конфигурация фиксирует анализ исходного кода на нуле ошибок и включает reportUnmatchedIgnoredErrors, поэтому даже устаревшее подавление — то, которое больше ничему не соответствует, — приводит к сбою сборки. Оставшиеся узкие игнорирования ограничены конкретным идентификатором ошибки и файлом. Каждое из них сопровождается встроенным пояснением, почему эта граница преднамеренна (например, ядро программируется на интерфейс 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 — часть этого подхода. Аннотации PHPDoc рассматриваются как непреложная истина, поэтому @param list<T> или @return non-empty-string — это не комментарий, который анализатор вежливо игнорирует, а проверяемое обещание. Аннотация и тип во время выполнения должны совпадать.

Эта страница имеет статус Evidence: Code-backed . Доказательством служит сама конфигурация:

  • phpstan.neon.dist задаёт level: 10, phpVersion: 80400, анализирует src и не содержит ключа baseline: — для анализа исходного кода нет файла phpstan-baseline.neon.
  • Тот же файл задаёт treatPhpDocTypesAsCertain: true и reportUnmatchedIgnoredErrors: true, со встроенным примечанием о том, что анализ исходного кода на L10 зафиксирован на нуле ошибок и любая регрессия должна приводить к сбою CI.
  • Каждая из оставшихся ignoreErrors ограничена по identifier и часто по path, с комментариями, объясняющими обоснование мягкой зависимости и цели рефлексии; это не массово сгенерированная базовая линия.
  • phpstan-strict.neon.dist наследует эту конфигурацию, повышает уровень до max и замораживает список игнорирования, поэтому в строгом профиле нельзя добавить ни одной новой записи.

Связь со стандартами здесь прямая. Движок должен создавать файлы, по которым программа чтения может перемещаться от трейлера и таблицы перекрёстных ссылок, согласно Spec: ISO 32000-2, §7.5.5 . Точные байтовые смещения являются проблемой типов ещё до того, как станут проблемой сериализации. Смещение — это целое число, которое никогда не должно молча стать чем-то иным. Конвейер, который типобезупречен на 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 она проверяется. Вызывающий код, который предполагает, что результат всегда int, получает сообщение об этом на этапе анализа — прежде чем будет записан хотя бы один байт несоответствующего номера части PDF/A.

Ловушка в том, чтобы прочитать “отсутствие базовой линии” как “так получилось, что в коде нет нарушений”. Это перевёрнутая логика. Отсутствие базовой линии — это причина, а не удачный исход. Поскольку нарушение некуда отложить, код, который его породил бы, приходится писать иначе. Level 10 без базовой линии для исходного кода — это ограничение, которое формирует проект, а не отчётная ведомость, описывающая его постфактум.

Второе заблуждение: что горстка записей ignoreErrors — это та же базовая линия под другим названием. Это не так. Базовая линия генерируется массово и непрозрачна. Эти записи написаны по отдельности, ограничены по идентификатору, снабжены пояснениями и защищены reportUnmatchedIgnoredErrors, поэтому не могут незаметно устареть.

Эта страница посвящена анализу исходного кода движка. Набор тестов анализируется в отдельной, намеренно отличающейся области и конфигурации; “отсутствие базовой линии” здесь — это утверждение о src/, а не утверждение, что любой вспомогательный анализ в репозитории не имеет базовой линии. PHPStan доказывает типовую корректность, а не корректность поведения. Он не заменяет пирамиду тестирования, а лишь устраняет категорию сбоев, которые тестам иначе пришлось бы выискивать. Точный уровень, флаги и набор игнорирований актуальны на дату проверки этой страницы. Авторитетным источником всегда являются phpstan.neon.dist и phpstan-strict.neon.dist в основном репозитории.

Редакция не меняет эту дисциплину. Каждая редакция собирается из одного и того же исходного кода уровня Level 10:

Level 10 source analysis — edition availability
Edition Availability
Core Исходный код Core анализируется на Level 10 без базовой линии для исходного кода.
Pro Pro построена на той же дисциплине исходного кода уровня Level 10.
Enterprise Enterprise построена на той же дисциплине исходного кода уровня Level 10.
  • PHPStan Level 10 — самый строгий уровень анализа, при котором нетипизированные и слабо типизированные значения рассматриваются как ошибки, а не как предупреждения.
  • Базовая линия — сгенерированная запись существующих нарушений, которые анализатор должен игнорировать. NextPDF не использует её для исходного кода движка.
  • treatPhpDocTypesAsCertain — настройка PHPStan, которая рассматривает аннотации типов PHPDoc как проверяемые факты, а не как рекомендательные комментарии.
  • reportUnmatchedIgnoredErrors — настройка, которая приводит к сбою сборки, когда запись игнорирования больше ничему не соответствует, предотвращая устаревшие подавления.
  • Проектное давление — эффект ограничения, которое вынуждает писать код определённым образом, в отличие от проверки, которая лишь его измеряет.