Crash Test C++: Скрытые ловушки SFINAE и тишина в парсере

 · 2 min read

Мы всё еще пытались выжать максимум из старого доброго Doxygen в нашем облачном конвейере, как вдруг столкнулись с классической проблемой сурового энтерпрайзного C++ — парсер начал молча “съедать” целые куски кодовой базы.

Crash Test Sfinae Doxygen

Всё началось с того, что мы заметили аномалию: в сгенерированной документации бесследно исчезли важные структуры (например, BITMAPINFO и RGBQUAD).

Самое страшное заключалось в том, что пайплайн проходил успешно. Doxygen возвращал exit code 0, не выдавал ни одного предупреждения, но XML-вывод для этих файлов оказывался практически пустым. Он просто проглатывал код и молчал.

Расследование бинарным поиском

Crash Test Sfinae Doxygen Part 2

Когда парсер молчит, единственный способ найти причину — метод половинного деления. Мы брали проблемный заголовочный файл на несколько тысяч строк, удаляли половину, запускали парсер и смотрели, появятся ли нужные структуры.

Сузив круг поиска до 50 строк, мы нашли виновника. Им оказался классический C++ трюк — SFINAE (Substitution Failure Is Not An Error). В коде повсеместно использовалась конструкция:

typename std::enable_if<CONDITION, bool>::type = VALUE

Для C++ компилятора это абсолютно валидный код. Но легаси-парсер Doxygen при виде такого нагромождения угловых скобок (< >) сходил с ума. Его внутренний конечный автомат ломался, и он переставал понимать синтаксис C++ до самого конца файла. Именно поэтому структуры, объявленные ниже злополучного SFINAE-шаблона, просто выпадали из реальности.

Решение в стиле UNIX

Мы не могли пойти к разработчикам продукта и сказать: “Перепишите весь свой C++ код, потому что наш парсер его не понимает”. Нам нужно было системное решение, не трогающее исходники.

К счастью, у Doxygen есть механизм INPUT_FILTER. Это скрипт, через который прогоняется каждый файл перед тем, как его увидит парсер.

Мы написали небольшой Python-скрипт (sfinae_filter.py) с хитрым регулярным выражением. Скрипт на лету находил все конструкции std::enable_if и заменял их на эквивалентный по количеству аргументов синтаксис: bool = VALUE.

При этом мы жестко соблюдали важное правило: скрипт высчитывал количество переносов строк в удаленном блоке и добавлял ровно столько же пустых строк в замену. Благодаря этому номера строк в исходном коде не смещались, и все предупреждения парсера (warnings) по-прежнему указывали на правильные строки в IDE разработчиков.

Итог

Маленький фильтр на Python решил проблему в десятках файлов, вернув потерянные классы в документацию. Мы снова победили.

Но этот инцидент стал “последней каплей”. Костыли с регулярными выражениями поверх C++ кода — это путь в никуда. Язык слишком сложен, чтобы парсить его текстом, а небезопасный INPUT_FILTER открывал дыры в облачной безопасности.

Пришло время сделать то, чего мы так долго избегали: навсегда уволить Doxygen. О том, как мы перешли на нативный компиляторный фронтенд libclang и почему нам не пришлось переписывать половину проекта — читайте в следующем эпизоде.