- PVSM.RU - https://www.pvsm.ru -
Программы статического анализа кода — это необычный класс программ-верификаторов, и в течение некоторого времени я не был убежден в необходимости их использования при разработке для фейсбука. Я не терплю стилистические правила на своей шее, и ложные предупреждения об ошибках могут испортить всю задачу. Впрочем, в них есть и хорошее: если проверяющий механически ищет проблемы, которые традиционно не контролируются компилятором, то это должно почти всегда улучшать качество кода, как только проблема будет исправлена.
Флинт [1], программа Фейсбука для статического анализа, выдает ошибки анализа, которые автоматически появляются в нашей системе ревью (phabricator [2]) рядом с каждым предложенным изменением кода, уведомляя программиста, что что-то может пойти не так. Flint стал важной частью работы, которую мы делаем в Фейсбуке, и я очень рад открыть его исходники, чтобы каждый мог проверить, что же мы делаем, и попробовать это для себя.
Написание анализатора кода для С++ — задача не для слабонервных, ведь С++ весьма сложен в разборе. Но тем не менее, в настоящее время есть целая куча анализаторов [3] со множеством фич, некоторые даже с открытым кодом. Так что вопрос, почему мы решили написать свой собственный, а не использовать существующие, вполне логичен.
Когда мы начинали это проект, все опробованные нами программы были слишком медленными и не поддерживали большинства нововведений С++11, которые уже использовались нами в разработке. Clang, который сегодня будет логичной отправной точкой для анализа кода на С++, предлагал слишком мало поддержки в то время. И даже сейчас он не может компилировать часть нашего кода на C++.
И самое главное, правила анализаторов кода очень сильно зависят от характера организаций, которые их используют. Мы представляли, что мы ищем, и выяснили, что какой бы анализатор мы не выбрали, нам придется долго дорабатывать его. Так что мы решил разработать собственный анализатор кода.
Основываясь на принципе «простейшее решение, которое будет работать», флинт является токен-ориентированным, что противопоставляется построению дерева разбора кода. Анализатор загружает входной файл, конвертирует в массив токенов и по-разному анализирует это массив. Каждый токен сохраняет предшествующий комментарий (если он есть), так что комментирующая информация сохранятся. Некоторые из наших правил требуют использования комментариев в специальном стиле, вы увидите это ниже.
Целью такого дизайна было реализовать быстрый токенайзер, добавить пару простых правил и выпустить его на свободу от фейсбука с надеждой, что люди будут добавлять анализатору интересные правила. Решение добавить парсинг выкинули, но наши инженеры добавили около двух дюжин правил, которые мы проверим за минуту.
Флинт написан на D, и это первый опенсорсный проект на этом языке от Фейсбука. На самом деле, наша первая версия была написана на С++; перевод на D начинался как эксперимент. По результатам измерений и историй, в которых мы участвовали, перевод на D был победой по всем направлениям: версия на D оказалась значительно меньше, значительно быстрее собиралась, значительно быстрее запускалась и была более легкой для добавления изменений.
Для переезда с С++ на D я решил сделать полумеханический перевод, т.е использовать ближайший код на D c той же семантикой, что и С++ код.
При реализации на С++ я выяснил, что использование генератора лексем приносило больше проблем, чем пользы, так что я просто написал быстрый, выделенный лексер с использованием макросов. В D нет макросов, что делает перевод один в один невозможным. Но D имеет кое-что получше — полную интерпретацию в течение компиляции, которая сочетается с возможностью самоанализа, генерации и компиляции сгенерированного кода налету.
Я написал функцию на 58 строк, которая генерирует развернутое дерево совпадений. Для С++ код разворачивается в 965 строк, которые затем подаются обратно в компилятор с примесью выражений.
В таком матчинге есть как минимум один плюс — он независим от языка, который мы разбиваем на лексемы; такой подход делает возможным засунуть его в библиотеку и оставить только язык-специфичные части (такие как парсинг чисел или комментариев). При этом подходе становится легче создавать и использовать оптимальные лексеры для любого языка без необходимости использования стороннего генератора. Реализация такого решения заняла часть первого дня, после чего у меня был работающий лексер, последующие дни были потрачены на портирование анализатора и его проверку на самом себе.
После перевода цикл редактирования/сборки/тестирования flint'а стал гораздо приятнее. Сборка flint на D примерно в пять раз быстрее сборки на С++, что оказалось очень важным в итерационной разработке. Скорость запуска стала немного лучше, между 5 и 25% — в зависимости от файла, выигрыш был больше для больших файлов.
Быстрая сборка дала интересный побочный эффект для меня. Разница между обычным временем сборки в C++ и D не удивительна, но это был первый раз, когда я работал над похожими проектами параллельно, переключаясь между ними, что дало мне возможность почувствовать разницу.
С++ может производить быстрый код, но сборка С++ сопровождается целой кучей лишних телодвижений и множеством шума. Инициация сборки неизбежно сопровождается некоторой церемониальностью и пышностью («назад, парни, возможна отдача!») и в течение дня я бы аккуратно следил за сборкой, чтобы максимизировать пользу от потраченного времени. Цикл сборки проектов на D происходит обескураживающе быстро — даже как-то обидно иногда. Бойкость, с которой компилятор D проходит сквозь код, сначала удивила меня, я даже заподозрил ошибку — что мой код вообще не был скомпилирован. На самом-то деле, новый язык просто способен лучше пробегать через код на значительно больших скоростях.
Каждая проверка соответствует настоящему названию функции в кодовой базе, так что вы можете просто найти её, чтобы увидеть, как эта проверка работает.
class X {
..
int a_;
X(const X& rhs) : a_(a_) {}
X(int a) : a_(a_) {}
};
В этом примере необходимо использовать a_(rhs.a_) в первом конструкторе и a_(a) во втором. В другом написании почти никогда не бывает смысла, а компилятор помалкивает на таком коде. Мы любим говорить: «Для этого есть правило флинта», когда решаем проблему.
shared_ptr<T> p(make_shared<T>(args))
вместо shared_ptr<T> p(new T)
(checkSmartPtrUsage). Первая версия делает только одно выделение памяти вместо двух.class C {
...
// Плохие паттерны, флинт выдаст диагностическое сообщение
C(int a)
C(const C&&);
// Хорошие паттерны, флинт такие любит
explicit C(int a);
/* implicit */ C(char* a);
C(int a, double b);
};
class C {
// Плохие паттерны, флинт выдаст диагностическое сообщение
operator string();
operator bool();
// Хорошие паттерны, флинт такие любит
/* неявно! */ operator string();
explicit operator bool();
};
class MyException : std::exception {
...
};
Автор хотел определить собственный класс исключения, но забыл указать наследование публичным. В соответствии с правилами языка, наследование по умолчанию будет приватным. В конечном счете, интересный эффект проявится в том, что код, который должен будет перехватывать все стандартные и пользовательские исключения — catch (const std::exception&), не сможет отработать правильно, потому что приватное наследование исключает неявное преобразование типов. Это не тот тип ошибок, который легко обнаружить без весьма сложного тестирования. Для предотвращения таких ошибок флинт статически отключает непубличное наследование от std::exception25.
Мы очень рады открыть исходники флинта, потому что это хорошая иллюстрация «простейшего решение, способного работать» и интересный пример кросс-языкового перевода. Надеемся, что вы сочтете флинт полезным — мы уже сочли. Оставайтесь с нами для будущих улучшений, мы с нетерпением ждем ваших отзывов.
Большое спасибо Nicholas Ormrod и Robbert Haarman за просмотр раннего черновика этой статьи. Слишком много инженеров внесли свой вклад в исходники флинта, чтобы указать их всех здесь; их вклад отмечен в журнале git'а.
Автор: vovochkin
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/60868
Ссылки в тексте:
[1] Флинт: https://github.com/facebook/flint
[2] phabricator: http://phabricator.org/
[3] целая куча анализаторов: http://en.wikipedia.org/wiki/List_of_tools_for_static_code_analysis#C.2FC.2B.2B
[4] Источник: http://habrahabr.ru/post/224419/
Нажмите здесь для печати.