[Перевод] Под капотом: сборка и открытие исходников flint
Программы статического анализа кода — это необычный класс программ-верификаторов, и в течение некоторого времени я не был убежден в необходимости их использования при разработке для фейсбука. Я не терплю стилистические правила на своей шее, и ложные предупреждения об ошибках могут испортить всю задачу. Впрочем, в них есть и хорошее: если проверяющий механически ищет проблемы, которые традиционно не контролируются компилятором, то это должно почти всегда улучшать качество кода, как только проблема будет исправлена.Флинт, программа Фейсбука для статического анализа, выдает ошибки анализа, которые автоматически появляются в нашей системе ревью (phabricator) рядом с каждым предложенным изменением кода, уведомляя программиста, что что-то может пойти не так. Flint стал важной частью работы, которую мы делаем в Фейсбуке, и я очень рад открыть его исходники, чтобы каждый мог проверить, что же мы делаем, и попробовать это для себя.Но почему бы не использовать существующие анализаторы кода?
Написание анализатора кода для С++ — задача не для слабонервных, ведь С++ весьма сложен в разборе. Но тем не менее, в настоящее время есть целая куча анализаторов со множеством фич, некоторые даже с открытым кодом. Так что вопрос, почему мы решили написать свой собственный, а не использовать существующие, вполне логичен.Когда мы начинали это проект, все опробованные нами программы были слишком медленными и не поддерживали большинства нововведений С++11, которые уже использовались нами в разработке. Clang, который сегодня будет логичной отправной точкой для анализа кода на С++, предлагал слишком мало поддержки в то время. И даже сейчас он не может компилировать часть нашего кода на C++.И самое главное, правила анализаторов кода очень сильно зависят от характера организаций, которые их используют. Мы представляли, что мы ищем, и выяснили, что какой бы анализатор мы не выбрали, нам придется долго дорабатывать его. Так что мы решил разработать собственный анализатор кода.Токены, комментарии и язык D, ох!
Основываясь на принципе «простейшее решение, которое будет работать», флинт является токен-ориентированным, что противопоставляется построению дерева разбора кода. Анализатор загружает входной файл, конвертирует в массив токенов и по-разному анализирует это массив. Каждый токен сохраняет предшествующий комментарий (если он есть), так что комментирующая информация сохранятся. Некоторые из наших правил требуют использования комментариев в специальном стиле, вы увидите это ниже.Целью такого дизайна было реализовать быстрый токенайзер, добавить пару простых правил и выпустить его на свободу от фейсбука с надеждой, что люди будут добавлять анализатору интересные правила. Решение добавить парсинг выкинули, но наши инженеры добавили около двух дюжин правил, которые мы проверим за минуту.Флинт написан на D, и это первый опенсорсный проект на этом языке от Фейсбука. На самом деле, наша первая версия была написана на С++; перевод на D начинался как эксперимент. По результатам измерений и историй, в которых мы участвовали, перевод на D был победой по всем направлениям: версия на D оказалась значительно меньше, значительно быстрее собиралась, значительно быстрее запускалась и была более легкой для добавления изменений.Перевод флинта с С++ на D
Для переезда с С++ на D я решил сделать полумеханический перевод, т.е использовать ближайший код на D c той же семантикой, что и С++ код.При реализации на С++ я выяснил, что использование генератора лексем приносило больше проблем, чем пользы, так что я просто написал быстрый, выделенный лексер с использованием макросов. В D нет макросов, что делает перевод один в один невозможным. Но D имеет кое-что получше — полную интерпретацию в течение компиляции, которая сочетается с возможностью самоанализа, генерации и компиляции сгенерированного кода налету.Я написал функцию на 58 строк, которая генерирует развернутое дерево совпадений. Для С++ код разворачивается в 965 строк, которые затем подаются обратно в компилятор с примесью выражений.В таком матчинге есть как минимум один плюс — он независим от языка, который мы разбиваем на лексемы; такой подход делает возможным засунуть его в библиотеку и оставить только язык-специфичные части (такие как парсинг чисел или комментариев). При этом подходе становится легче создавать и использовать оптимальные лексеры для любого языка без необходимости использования стороннего генератора. Реализация такого решения заняла часть первого дня, после чего у меня был работающий лексер, последующие дни были потрачены на портирование анализатора и его проверку на самом себе.После перевода цикл редактирования/сборки/тестирования flint’а стал гораздо приятнее. Сборка flint на D примерно в пять раз быстрее сборки на С++, что оказалось очень важным в итерационной разработке. Скорость запуска стала немного лучше, между 5 и 25% — в зависимости от файла, выигрыш был больше для больших файлов.Быстрая сборка дала интересный побочный эффект для меня. Разница между обычным временем сборки в C++ и D не удивительна, но это был первый раз, когда я работал над похожими проектами параллельно, переключаясь между ними, что дало мне возможность почувствовать разницу.С++ может производить быстрый код, но сборка С++ сопровождается целой кучей лишних телодвижений и множеством шума. Инициация сборки неизбежно сопровождается некоторой церемониальностью и пышностью («назад, парни, возможна отдача!») и в течение дня я бы аккуратно следил за сборкой, чтобы максимизировать пользу от потраченного времени. Цикл сборки проектов на D происходит обескураживающе быстро — даже как-то обидно иногда. Бойкость, с которой компилятор D проходит сквозь код, сначала удивила меня, я даже заподозрил ошибку — что мой код вообще не был скомпилирован. На самом-то деле, новый язык просто способен лучше пробегать через код на значительно больших скоростях.Проверки, совершаемые флинтом
Каждая проверка соответствует настоящему названию функции в кодовой базе, так что вы можете просто найти её, чтобы увидеть, как эта проверка работает.Черный список последовательностей токенов (checkBlacklistedSequences). Некоторые последовательности могут быть просто запрещены в организации. В Фейсбуке мы считаем «volatile» запрещенным, но не всегда: мы разрешаем последовательность «asm volatile», которая означает немного другое.
Черный список идентификаторов (checkBlacklistedIdentifiers). Некоторые идентификаторы могут быть запрещены в организации. В фейсбуке мы исключаем печально известную функцию языка C — strtok. Для нее есть безопасные альтернативы, так что нет ни одной причины для использования strtok.
Резервированные идентификаторы (checkDefinedNames). И в С, и в С++ есть часто забываемое правило именования, согласно которому все идентификаторы, начинающиеся с подчеркивания и последующей заглавной буквы, а также все идентификаторы, содержащиеся два последовательных подчеркивания, зарезервированы для служебных нужд. (Конечно в нашем коде есть исключения от этого правила, такие как _GNU_SOURCE или _XOPEN_SOURCE, поэтому флинт использует и whitelist при проверке зарезервированных идентификаторов).
Идиома включения гардов (checkIncludeGuard). Большинство заголовочных файлов должно быть защищено явным образом (путем использования директивы #pragma once или #ifndef макроса), так что мы добавили правило для проверки этой защиты.
Порядок аргументов в memset (checkMemset). Многие из нас иногда писали memset (&foo, sizeof (foo), 0); и рассказывали об этом жуткие истории, пугая племянниц на Хэллоуин. Соответствующее диагностическое правило флинта позволит легко избежать этой пагубной ошибки.
Сомнительные инклуды (#include) — checkQuestionableIncludes. Многие организации используют несколько библиотек, которые хранятся только для обеспечения обратной совместимости и никогда не используются в новом коде. Значит, такие хидеры не должны включаться — хорошая работенка для анализатора. Одной из проблем, которую мы нашли в Фейсбуке, было то, что некоторые хидеры после препроцессора становились слишком большими, чтобы их можно было включить в другие — это приводило к слишком большому времени компиляции. Нам пришлось научить флинт решать и эту задачу.
Выделение »…-inl.h» файлов (checkInlHeaderInclusions). Популярным способом организации встраиваемого и тяжелого шаблонного кода является соответствующее разделение инлайновых и шаблонных артифактов — например, «Widget.h» превращается в «Widget-inl.h». Последний файл не должен быть включен где-то, кроме «Widget.h», и специальное правило следит за этим.
Инициализация переменной самой себя (checkInitializeFromItself). Мы выяснили, что люди пишут конструкторы в стиле
class X {
…
int a_;
X (const X& rhs) : a_(a_) {}
X (int a) : a_(a_) {}
};
В этом примере необходимо использовать a_(rhs.a_) в первом конструкторе и a_(a) во втором. В другом написании почти никогда не бывает смысла, а компилятор помалкивает на таком коде. Мы любим говорить: «Для этого есть правило флинта», когда решаем проблему.
Предпочтительнее использовать shared_ptr