Увольнение Doxygen: Архитектура нативных парсеров и внедрение libclang

 · 3 min read

Doxygen был с нами с самого первого дня. На этапе MVP это был осознанный и абсолютно верный выбор: он позволил нам не писать парсер C++ с нуля, а сразу сфокусироваться на бизнес-логике и рендеринге документации.

Firing Doxygen Native Cpp Parser

Но по мере перехода от локальных скриптов к облачному SaaS, Doxygen превратился в обузу. Он не умел кэшировать AST, его бинарник был слишком громоздким, а его внутренний парсер ломался от C++ шаблонов (вспомните инцидент со SFINAE из прошлого эпизода, заставивший нас писать костыли через INPUT_FILTER).

Пришло время сделать то, что мы планировали с самого начала: уволить Doxygen и перевести систему на нативные парсеры.

Разделяй и властвуй

Firing Doxygen Native Cpp Parser Part 2

Мы никогда не питали иллюзий насчет создания “универсального парсера”. Как и в случае с диалектами Doxygen, мы изначально проектировали систему под раздельные парсеры для каждого языка. Инструмента, который одинаково хорошо понимает адские шаблоны C++ со SFINAE и динамические декораторы Python, просто не существует.

Поэтому мы провели исследование и подобрали для каждого языка свой идеальный, нативный инструмент:

  • Python: Встроенный модуль ast. Никто не распарсит Python лучше, чем сам интерпретатор (запланировано).

  • C# и Java: tree-sitter — невероятно быстрый и современный инкрементальный парсер, ставший стандартом индустрии (запланировано).

  • C++: Наш самый сложный язык и первый кандидат на реализацию.

Битва с боссом: C++ и libclang

Мы начали внедрение нативных парсеров именно с C++, так как это был самый критичный вектор. Пытаться распарсить современный C++ регулярками или легковесными парсерами — это чистое самоубийство. Препроцессоры, макросы, шаблоны и перегрузки операторов сломают любой самописный инструмент.

Поэтому мы взяли libclang. Только настоящий фронтенд компилятора может выдать 100% точное AST для C++. Да, интеграция с C++ API из Python — задача не из легких, но это единственная гарантия того, что мы правильно поймем весь код клиента.

Триумф заложенной архитектуры (Intermediate Representation)

Самое прекрасное в переходе на libclang заключалось в том, что нам не пришлось переписывать рендереры (тот самый hugo_markdown, о котором мы рассказывали в прошлой статье).

Это стало возможным благодаря тому, что наша архитектура с самого первого дня базировалась на промежуточном представлении (Intermediate Representation, IR). Все парсеры обязаны конвертировать результат своей работы в унифицированную структуру ProjectCatalog.

Новый парсер на базе libclang просто начал собирать тот же самый ProjectCatalog, который раньше отдавал Doxygen. Слой рендеринга даже не узнал, что под капотом сменился двигатель. Архитектура, заложенная на старте проекта, сработала именно так, как задумывалось.

Итог

Ядро освободилось от тяжелой сторонней зависимости, мешавшей реализовать кэш. Мы закрыли огромную дыру в безопасности (убрав INPUT_FILTER) и значительно облегчили наши облачные воркеры.

Переход на нативный парсер — это тот технический шаг, который окончательно превратил MVP-утилиту в серьезный продукт.

Но как только мы подключили libclang к реальным проектам, нас ждал сюрприз. Нативный парсер оказался слеп: он наотрез отказывался видеть стандартные библиотеки C++. О том, как мы искали пути к системным заголовкам GCC на Linux и выживали с MSVC-STL на Windows — читайте в следующем эпизоде.