Один із найактуальніших сценаріїв використання аналізатора PVS-Studio — його інтеграція із CI системами. І хоча аналіз проекту PVS-Studio практично з-під будь-якої continuous integration системи можна вбудувати всього в кілька команд, ми продовжуємо робити цей процес ще зручніше. У PVS-Studio з'явилася підтримка перетворення виведення аналізатора на формат для TeamCity - TeamCity Inspections Type. Погляньмо, як це працює.
Інформація про ПЗ
Інформація про досліджуваний проект
Спробуємо цю функціональність на практичному прикладі – проаналізуємо проект OpenRCT2.
Налаштування
З метою економії часу я, мабуть, опущу процес встановлення та почну з того моменту, коли у мене на комп'ютері запущено сервер TeamCity. Нам потрібно перейти: localhost:{вказаний у процесі встановлення порт}(у моєму випадку, localhost:9090) та ввести дані для авторизації. Після входу нас зустріне:
Натисніть кнопку Create Project. Далі виберемо Manually, заповнимо поля.
Після натискання на кнопку Створювати, нас зустрічає вікно з налаштуваннями.
Натиснемо Create build configuration.
Заповнюємо поля, натискаємо Створювати. Ми бачимо вікно із пропозицією вибору системи контролю версій. Оскільки вихідники вже лежать локально, тиснемо Пропускати.
Зрештою, ми переходимо до налаштувань проекту.
Додамо кроки збирання, для цього тиснемо: Build steps -> Add build step.
Тут виберемо:
- Runner type -> Command Line
- Run -> Custom Script
Так як ми проводитимемо аналіз під час компіляції проекту, складання та аналіз повинні бути одним кроком, тому заповнимо поле Користувацький скрипт:
На окремих кроках ми зупинимося згодом. Важливо, щоб завантаження аналізатора, складання проекту, його аналіз, виведення звіту та його форматування зайняло лише одинадцять рядків коду.
Останнє, що нам потрібно зробити, — встановити змінні оточення, якими я позначив деякі шляхи для покращення їхньої читабельності. Для цього перейдемо: Parameters -> Add new parameter і додамо три змінні:
Залишається натиснути на кнопку прогін в правому верхньому куті. Поки йде складання та аналіз проекту розповім вам про скрипт.
Безпосередньо скрипт
Для початку нам потрібно викачати новий дистрибутив PVS-Studio. Для цього ми використовуємо пакетний менеджер Сhocolatey. Для тих, хто хоче дізнатися про це детальніше, є відповідна
choco install pvs-studio -y
Далі запустимо утиліту відстеження складання проекту CLMonitor.
%CLmon% monitor –-attach
Потім зробимо складання проекту, як змінне оточення MSB виступає шлях до потрібної мені для збирання версії MSBuild
%MSB% %ProjPath% /t:clean
%MSB% %ProjPath% /t:rebuild /p:configuration=release
%MSB% %ProjPath% /t:g2
%MSB% %ProjPath% /t:PublishPortable
Введемо логін та ключ ліцензії PVS-Studio:
%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%
Після завершення складання ще раз запустимо CLMonitor для генерації препроцесованих файлів та статичного аналізу:
%CLmon% analyze -l "c:ptest.plog"
Після скористаємося ще однією утилітою нашого дистрибутива. PlogConverter перетворює звіт зі стандартного на специфічний для TeamCity формат. Завдяки цьому ми зможемо подивитися його прямо у вікні збирання.
%PlogConverter% "c:ptest.plog" --renderTypes=TeamCity -o "C:temp"
Останньою дією виведемо форматований звіт у stdout, де його підхопить парсер TeamCity
type "C:tempptest.plog_TeamCity.txt"
Повний код скрипту:
choco install pvs-studio -y
%CLmon% monitor --attach
set platform=x64
%MSB% %ProjPath% /t:clean
%MSB% %ProjPath% /t:rebuild /p:configuration=release
%MSB% %ProjPath% /t:g2
%MSB% %ProjPath% /t:PublishPortable
%PVS-Studio_cmd% credentials --username %PVS_Name% --serialNumber %PVS_Key%
%CLmon% analyze -l "c:ptest.plog"
%PlogConverter% "c:ptest.plog" --renderTypes=TeamCity -o "C:temp"
type "C:tempptest.plog_TeamCity.txt"
Тим часом, складання та аналіз проекту успішно завершилися, ми можемо перейти на вкладку Завдання і переконатися в цьому.
Тепер клікнемо на Inspections Total, Щоб перейти до перегляду звіту аналізатора:
Попередження згруповано за номерами діагностичних правил. Для здійснення навігації за кодом потрібно натиснути на номер рядка з попередженням. Натиснувши на знак питання у верхньому правому куті, відкриє вам нову вкладку з документацією. Також можна здійснити навігацію за кодом, натиснувши номер рядка з попередженням аналізатора. Навігація з віддаленого комп'ютера можлива при застосуванні SourceTreeRoot маркер. Той, кому цікавий даний режим роботи аналізатора, може ознайомитись із відповідним розділом
Перегляд результатів роботи аналізатора
Після того, як ми закінчили з розгортанням та налаштуванням складання, пропоную подивитися на деякі цікаві попередження, виявлені у досліджуваному проекті.
Попередження N1
Object* CreateObjectFromJson(....)
{
Object* result = nullptr;
....
result = CreateObject(entry);
....
if (readContext.WasError())
{
throw std::runtime_error("Object has errors");
}
....
}
Object* CreateObject(const rct_object_entry& entry)
{
Object* result;
switch (entry.GetType())
{
case OBJECT_TYPE_RIDE:
result = new RideObject(entry);
break;
case OBJECT_TYPE_SMALL_SCENERY:
result = new SmallSceneryObject(entry);
break;
case OBJECT_TYPE_LARGE_SCENERY:
result = new LargeSceneryObject(entry);
break;
....
default:
throw std::runtime_error("Invalid object type");
}
return result;
}
Аналізатор помітив помилку, що полягає в тому, що після динамічного виділення пам'яті CreateObject, у разі виключення пам'ять не очищається, відповідно, виникає витік пам'яті.
Попередження N2
static uint64_t window_cheats_page_enabled_widgets[] =
{
MAIN_CHEAT_ENABLED_WIDGETS |
(1ULL << WIDX_NO_MONEY) |
(1ULL << WIDX_ADD_SET_MONEY_GROUP) |
(1ULL << WIDX_MONEY_SPINNER) |
(1ULL << WIDX_MONEY_SPINNER_INCREMENT) |
(1ULL << WIDX_MONEY_SPINNER_DECREMENT) |
(1ULL << WIDX_ADD_MONEY) |
(1ULL << WIDX_SET_MONEY) |
(1ULL << WIDX_CLEAR_LOAN) |
(1ULL << WIDX_DATE_SET) |
(1ULL << WIDX_MONTH_BOX) | // <=
(1ULL << WIDX_MONTH_UP) |
(1ULL << WIDX_MONTH_DOWN) |
(1ULL << WIDX_YEAR_BOX) |
(1ULL << WIDX_YEAR_UP) |
(1ULL << WIDX_YEAR_DOWN) |
(1ULL << WIDX_DAY_BOX) |
(1ULL << WIDX_DAY_UP) |
(1ULL << WIDX_DAY_DOWN) |
(1ULL << WIDX_MONTH_BOX) | // <=
(1ULL << WIDX_DATE_GROUP) |
(1ULL << WIDX_DATE_RESET),
....
};
Мало хто, окрім статичного аналізатора, зміг би пройти цей тест на уважність. Цей приклад копіпасти гарний саме цим.
Попередження N3
struct RCT12SpriteBase
{
....
uint8_t flags;
....
};
struct rct1_peep : RCT12SpriteBase
{
....
uint8_t flags;
....
};
Звичайно, використання змінної з одним ім'ям у базовому класі та у спадкоємці далеко не завжди є помилкою. Проте технологія спадкування сама собою передбачає наявність всіх полів батьківського класу в дочірньому. Оголосивши ж у спадкоємця поля з таким самим ім'ям, ми вносимо плутанину.
Попередження N4
void vehicle_visual_observation_tower(...., int32_t imageDirection, ....)
{
if ((imageDirection / 8) && (imageDirection / 8) != 3)
{
....
}
....
}
Давайте розберемося докладніше. Вираз imageDirection / 8 буде false у тому випадку, якщо imageDirection знаходиться в діапазоні від -7 до 7. Друга частина: (imageDirection / 8)! = 3 перевіряє imageDirection на перебування поза діапазоном: від -31 до -24 і від 24 до 31 відповідно. Мені здається досить дивним перевіряти числа на входження у певний діапазон у такий спосіб і, навіть якщо в даному фрагменті коду немає помилки, я б рекомендував переписати ці умови на більш явні. Це суттєво спростило б життя людям, які читатимуть та підтримуватимуть цей код.
Попередження N5
void process_mouse_over(....)
{
....
switch (window->widgets[widgetId].type)
{
case WWT_VIEWPORT:
ebx = 0;
edi = cursorId; // <=
// Window event WE_UNKNOWN_0E was called here,
// but no windows actually implemented a handler and
// it's not known what it was for
cursorId = edi; // <=
if ((ebx & 0xFF) != 0)
{
set_cursor(cursorId);
return;
}
break;
....
}
....
}
Цей фрагмент коду, швидше за все, було отримано шляхом декомпіляції. Потім, судячи з залишеного коментаря, було видалено частину неробочого коду. Однак залишилася пара операцій над cursorId, які також не мають особливого сенсу.
Попередження N6
void Network::ProcessPlayerList()
{
....
auto* player = GetPlayerByID(pendingPlayer.Id);
if (player == nullptr)
{
// Add new player.
player = AddPlayer("", "");
if (player) // <=
{
*player = pendingPlayer;
if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)
{
_serverConnection->Player = player;
}
}
newPlayers.push_back(player->Id); // <=
}
....
}
Цей код виправити досить просто, потрібно або втретє перевіряти гравець на нульовий покажчик, або внести їх у тіло умовного оператора. Я запропонував би другий варіант:
void Network::ProcessPlayerList()
{
....
auto* player = GetPlayerByID(pendingPlayer.Id);
if (player == nullptr)
{
// Add new player.
player = AddPlayer("", "");
if (player)
{
*player = pendingPlayer;
if (player->Flags & NETWORK_PLAYER_FLAG_ISSERVER)
{
_serverConnection->Player = player;
}
newPlayers.push_back(player->Id);
}
}
....
}
Попередження N7
std::optional<ServerListEntry> ServerListEntry::FromJson(...)
{
auto name = json_object_get(server, "name");
.....
if (name == nullptr || version == nullptr)
{
....
}
else
{
....
entry.name = (name == nullptr ? "" : json_string_value(name));
....
}
....
}
Можна одним махом позбавитися від рядка коду, що важко читати, і вирішити проблему з перевіркою на nullptr. Пропоную змінити код таким чином:
std::optional<ServerListEntry> ServerListEntry::FromJson(...)
{
auto name = json_object_get(server, "name");
.....
if (name == nullptr || version == nullptr)
{
name = ""
....
}
else
{
....
entry.name = json_string_value(name);
....
}
....
}
Попередження N8
void CustomListView::MouseUp(....)
{
....
if (!ColumnHeaderPressedCurrentState)
{
ColumnHeaderPressed = std::nullopt;
ColumnHeaderPressedCurrentState = false;
Invalidate();
}
}
Код має досить дивний вигляд. Мені здається, мала місце помилка або в умові, або при повторному присвоєнні змінної ColumnHeaderPressedCurrentState значення false.
Висновок
Як бачимо, інтегрувати статичний аналізатор PVS-Studio у свій проект на TeamCity досить просто. Для цього достатньо написати лише один маленький файл конфігурації. Перевірка коду дозволить виявляти проблеми відразу після складання, що допоможе усувати їх тоді, коли складність і вартість правок ще малі.
Якщо хочете поділитися цією статтею з англомовною аудиторією, прошу використати посилання на переклад: Vladislav Stolyarov.
Джерело: habr.com