Адзін з самых актуальных сцэнарыяў выкарыстання аналізатара 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
Потым зробім зборку праекту, у якасці зменнай асяроддзя МСБ выступае шлях да патрэбнай мне для зборкі версіі 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"
Апошнім дзеяннем выведзем фарматаваную справаздачу ў выбіты, дзе яго падхопіць парсер 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 значэння ілжывы.
Выснова
Як мы бачым, інтэграваць статычны аналізатар PVS-Studio у свой праект на TeamCity даволі проста. Для гэтага дастаткова напісаць усяго адзін маленькі файл канфігурацыі. Праверка кода ж дазволіць выяўляць праблемы адразу пасля зборкі, што дапаможа ўстараняць іх тады, калі складанасць і кошт правак яшчэ малыя.
Калі хочаце падзяліцца гэтым артыкулам з англамоўнай аўдыторыяй, то прашу выкарыстаць спасылку на пераклад: Vladislav Stolyarov.
Крыніца: habr.com