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

Всё началось с того, что мы заметили аномалию: в сгенерированной документации бесследно исчезли важные структуры (например, BITMAPINFO и RGBQUAD).
Самое страшное заключалось в том, что пайплайн проходил успешно. Doxygen возвращал exit code 0, не выдавал ни одного предупреждения, но XML-вывод для этих файлов оказывался практически пустым. Он просто проглатывал код и молчал.
Расследование бинарным поиском

Когда парсер молчит, единственный способ найти причину — метод половинного деления. Мы брали проблемный заголовочный файл на несколько тысяч строк, удаляли половину, запускали парсер и смотрели, появятся ли нужные структуры.
Сузив круг поиска до 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 и почему нам не пришлось переписывать половину проекта — читайте в следующем эпизоде.