Как уже известно нашим читателям, статический анализатор PVS-Studio исследует новое направление развития — платформу Linux; как вы могли заметить из предыдущих статей, дела идут хорошо. В этой статье показано, как легко можно проверить проект с помощью версии анализатора для Linux, ведь чем проще PVS-Studio для Linux, тем больше у него будет сторонников. На этот раз нашим выбором стал проект CodeLite. CodeLite был скомпилирован и протестирован в Linux. Посмотрим, какие результаты мы получили.
О проекте
CodeLite — это бесплатная кросс-платформенная среда разработки C, C++, PHP и Node.js с открытым исходным кодом, в которой используется набор инструментов wxWidgets. Чтобы соответствовать духу программного обеспечения с открытым исходным кодом, CodeLite компилируется и отлаживается исключительно с помощью бесплатных инструментов (MinGW и GDB).
Возможности CodeLite: управление проектами, завершение кода (ctags + clang), рефакторинг кода, подсветка синтаксиса, интеграция с Subversion и Git, интеграция с Cscope, интеграция с UnitTest++, интерактивный отладчик, построенный на основе GDB, и мощный редактор исходного кода (на основе Scintilla) .
Codelite распространяется под лицензией GNU General Public License версии 2 или более поздней. Это бесплатно. Codelite, будучи хорошо разработанным и отлаженным, может использоваться в качестве платформы для разработки.
Современные версии CodeLite также поддерживают проекты на PHP и Node.js.
Исходный код CodeLite доступен на GitHub
Результаты анализа
Для проверки я использовал PVS-Studio для Linux. Кратко расскажу о рабочем процессе.
Прежде чем приступить к работе, я прочитал инструкцию по запуску и использованию PVS-Studio для Linux. Анализатор можно использовать двумя способами: интегрировать в систему сборки (считается лучшим способом) или использовать как утилиту pvs-studio-analyzer. Чтобы быстро сделать проверку и приступить к анализу ошибок, я решил воспользоваться вторым способом.
Итак, поехали.
Сначала я скачал исходный код проекта.
Затем я создал простой конфиг-файл — PVS-Studio.cfg — где прописал следующее:
exclude-path = /usr/include/
lic-file = /path/to/PVS-Studio.lic
output-file = /path/to/PVS-Studio.log
Так как CodeLite является проектом cmake, для сборки я использовал утилиту cmake с нужным для дальнейшей работы с анализатором флагом.
$ mkdir codelite/build
$ cd build
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On ../
После того, как проект был успешно собран, я приступил к анализу:
$ pvs-studio-analyzer analyze --cfg /path/to/PVS-Studio.cfg -j4
В итоге я получил файл PVS-Studio.log по указанному пути в PVS-Studio.cfg. Чтобы получить от него полезную информацию, я воспользовался утилитой plog-converter, входящей в состав дистрибутива PVS-Studio.
Для просмотра отчета анализатора я запустил plog-converter следующим образом:
$ plog-converter -a GA:1,2 -t tasklist -o /path/to/codelite.tasks
/path/to/PVS-Studio.log
После этой команды я получил codelite.tasks в указанном каталоге, который я открыл с помощью Qt Creator.
Обработка указателя
Предупреждение V595 Указатель pResult был использован до того, как он был проверен на соответствие nullptr. Проверить строки: 522, 526. SqliteDatabaseLayer.cpp 522
bool CodeBlocksImporter::isSupportedWorkspace()
{
....
wxXmlNode* root = codeBlocksProject.GetRoot();
wxString nodeName = root->GetName(); // <=
if(root && // <=
(nodeName == wxT("CodeBlocks_workspace_file") ||
nodeName == wxT("CodeBlocks_project_file")))
return true;
}
return false;
}
В приведенном выше коде анализатор обнаружил ошибку, связанную с возможным разыменованием указателя root. Возможно, что указатель никогда не будет нулевым, и программист уверен в этом, но тогда зачем ему снова сверяться с нулевым? Это приводит только к путанице при чтении кода. На мой взгляд, это реальный баг, и код надо менять.
Похожие предупреждения анализатора:
- V595 Указатель pResult использовался до того, как он был проверен на соответствие nullptr. Проверить строки: 522, 526. SqliteDatabaseLayer.cpp 522
- V595 Указатель «ms_instance» использовался до того, как он был проверен на соответствие nullptr. Проверить строки: 24, 25. php_parser_thread.cpp 24
Предупреждение V512 Вызов функции memset приведет к недополнению буфера EndTimestampListHandles. md5.cpp 243
class MD5 { .... // assumes char is 1 word long typedef unsigned char uint1; // next, the private data: .... uint1 buffer[64]; // input buffer .... static void memset(uint1 *start, uint1 val, uint4 length); .... };
void MD5::finalize () { .... // Zeroize sensitive information memset (buffer, 0, sizeof(*buffer)); // <= finalized=1; }
Здесь ошибка связана с неправильным значением третьего аргумента, передаваемого в функцию memset. Оператор sizeof(*buffer) возвращает не фактический размер буфера, а размер первого элемента, что является ошибкой. В этом конкретном примере в memset будет передан только 1 байт вместо 64.
Примечание. Обратите внимание, что здесь программист использует «пользовательскую» функцию memset. Как анализатор узнает, что он используется неправильно? Название этой и некоторых других функций настолько принципиально схожи, что используются они одинаково. Поэтому для этой, да и для некоторых других функций анализатор не обращает внимания, в каком именно пространстве имен или в каком классе они объявлены, главное, чтобы количество и тип аргументов совпадали. Как видим, такие действия помогают найти ошибки.
Предупреждение V668 Нет смысла проверять указатель буфер на нуль, так как память была выделена с помощью оператора новый. Исключение будет сгенерировано в случае ошибки выделения памяти. ShapeDataObject.cpp 65
wxString wxSFShapeDataObject::SerializeSelectedShapes(....) { .... char *buffer = new char [outstream.GetSize()];
if(buffer) //<= { memset(buffer, 0, outstream.GetSize()); outstream.CopyTo(buffer, outstream.GetSize()-1); wxString output(buffer, wxConvUTF8); delete [] buffer; return output; } else return wxT(....); }
Здесь у нас есть бессмысленная проверка указателя. Согласно стандартам языка C++, при выделении памяти через new не имеет смысла проверять указатель на нуль, так как может быть выдано исключение std::bad_alloc() в случае, если память выделить не получится. В таких случаях следует использовать блок try… catch для обработки таких критических ситуаций. Если вы хотите избежать использования исключений, тогда есть new, который не генерирует исключений. Например:
char *buffer = new char (std::nothrow) [outstream.GetSize()];
Конечно, использование try..catch или std::nothrow не являются примерами изящных решений и представлены здесь только как варианты быстрых и грубых исправлений.
Были обнаружены и другие подобные ситуации (здесь приведены только некоторые из сообщений, всего их 19):
- V668 Проверять указатель pResultSet на null нет смысла, так как память была выделена с помощью оператора new. Исключение будет сгенерировано в случае ошибки выделения памяти. SqliteDatabaseLayer.cpp 199
- V668 Проверять указатель pReturnStatement на null нет смысла, так как память была выделена с помощью оператора new. Исключение будет сгенерировано в случае ошибки выделения памяти. SqliteDatabaseLayer.cpp 223
- V668 Проверять указатель m_proc на null нет смысла, так как память была выделена с помощью оператора new. Исключение будет сгенерировано в случае ошибки выделения памяти. async_executable_cmd.cpp 182
- и так далее…
Это невнимание…
Предупреждение V519 Переменной m_commentEndLine два раза подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 175, 176. PhpLexerAPI.h 176
struct WXDLLIMPEXP_CL phpLexerUserData {
....
int m_commentStartLine;
int m_commentEndLine;
....
void ClearComment()
{
m_comment.clear();
m_commentEndLine = wxNOT_FOUND; //<=
m_commentEndLine = wxNOT_FOUND;
}
};
Явная ошибка копирования-вставки. В классе phpLexerUserData помимо переменной commentEndLine есть переменная commentStartLine. По сути, метод ClearComment должен быть таким:
void ClearComment()
{
m_comment.clear();
m_commentStartLine = wxNOT_FOUND;
m_commentEndLine = wxNOT_FOUND;
}
Такая же ошибка была обнаружена еще в нескольких местах:
- V519 Переменной m_commentEndLine два раза подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 171, 172. CxxLexerAPI.h 172
- V519 Переменной m_commentEndLine два раза подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 143, 144. JSLexerAPI.h 144
Предупреждение V547 Выражение type.Lower() == Array всегда ложно. NodeJSOuptutParser.h 61
struct NodeJSHandle {
wxString type;
....
bool IsString() const {return type.Lower() == "string";}
bool IsArray() const {return type.Lower() == "Array"; } //<=
};
Метод IsArray всегда будет возвращать false из-за небольшой опечатки. Чтобы исправить это, нам нужно просто заменить «Массив» на «массив», и все будет работать так, как должно.
Предупреждение V517 Обнаружено использование шаблона if (A) {…} else if (A) {…}. Существует вероятность наличия логической ошибки. Проверьте строки: 383, 386. MainFrame.cpp 383
void MainFrame::OnSignal(wxCommandEvent& e) { if(m_process) { int sigid = e.GetId(); if(sigid == ID_SIGHUP) wxKill(m_process->GetPid(), wxSIGHUP);
else if(sigid == ID_SIGINT) wxKill(m_process->GetPid(), wxSIGINT);
else if(sigid == ID_SIGKILL) wxKill(m_process->GetPid(), wxSIGKILL);
else if(sigid == ID_SIGKILL) // <= wxKill(m_process->GetPid(), wxSIGTERM); } }
Осмелюсь предположить, что программист решил ускорить написание этого метода, скопировав предыдущую строку, но забыл изменить константу. Прирост производительности, конечно, велик, но не следует забывать об осторожности. Правильная версия:
void MainFrame::OnSignal(wxCommandEvent& e) { .... else if(sigid == ID_SIGKILL) wxKill(m_process->GetPid(), wxSIGKILL);
else if(sigid == ID_SIGTERM) wxKill(m_process->GetPid(), wxSIGTERM); } }
Еще одно предупреждение анализатора:
- V517 Обнаружено использование шаблона «if (A) {…} else if (A) {…}». Существует вероятность наличия логической ошибки. Строки проверки: 212, 222. new_quick_watch_dlg.cpp 212
Предупреждение V530 Необходимо использовать возвращаемое значение функции пусто. act_network.cpp 56
StringTokenizer::StringTokenizer(const wxString& str, const wxString& strDelimiter, const bool &bAllowEmptyTokens /* false */) { .... wxString token; while( nEnd != -1 ) { if( nEnd != nStart) token = str.substr(nStart, nEnd-nStart); else token.empty(); // <=
if(!token.empty()) m_tokensArr.push_back(token); .... } }
Функция empty() не изменяет объект, она только возвращает логический результат. Другими словами, ветка else ничего не делает. Вместо token.empty() программист должен был написать token.Empty(), который обнуляет строку, или, возможно, что-то еще.
Ой! Что-то было забыто
Предупреждение V729 Тело функции содержит метку find_rule, которая не используется никакими операторами goto. include_finder.cpp 716
.... #define YY_DECL int yylex YY_PROTO(( void )) .... YY_DECL { .... yy_find_action: yy_current_state = *--yy_state_ptr; yy_lp = yy_accept[yy_current_state];
/* we branch to this label when backing up */ find_rule: //<= for ( ; ; ) /* until we find what rule we matched */ .... }
Здесь ошибка в том, что среди большого количества строк кода есть метка find_rule, на которую не ссылается ни один из операторов goto. Это могло произойти из-за рефакторинга кода или, возможно, из-за чего-то еще. Пока эта одинокая этикетка не несет смысловой нагрузки, она лишь намекает, что где-то что-то забыли.
Такое предупреждение было найдено еще в нескольких местах:
- Тело функции V729 содержит метку «find_rule», которая не используется никакими операторами «goto». comment_parser.cpp 672
- Тело функции V729 содержит метку «find_rule», которая не используется никакими операторами «goto». cpp_expr_lexer.cpp 1090
- Тело функции V729 содержит метку «find_rule», которая не используется никакими операторами «goto». cpp_lexer.cpp 1138
Предупреждения V523 Оператор then эквивалентен оператору else. art_metro.cpp 402
void wxRibbonMetroArtProvider::DrawTab(
wxDC& dc,
wxWindow* WXUNUSED(wnd),
const wxRibbonPageTabInfo& tab)
{
....
if (tab.active)
dc.SetPen(m_tab_border_pen);
else
// TODO: introduce hover border pen colour
dc.SetPen(m_tab_border_pen); // <=
....
}
В приведенном выше фрагменте кода программист начал работать над какой-то идеей, но потом поставил пометку и остановился. Нетрудно догадаться, что в else-ветви не должно быть повторяющейся строки кода. Впрочем, судя по комментарию, это, вероятно, временное решение.
Похожие предупреждения анализатора:
- V523 Оператор "then" эквивалентен оператору "else". art_metro.cpp 402
- V523 Оператор "then" эквивалентен оператору "else". php_workspace_view.cpp 948
Предупреждение V560 Часть условного выражения всегда ложна: 0. entry.c 397
extern void openTagFile (void) { .... boolean fileExists; setDefaultTagFileName (); TagFile.name = eStrdup (Option.tagFileName); fileExists = doesFileExist (TagFile.name);
/* allways override old files */ if (fileExists && /*! isTagFile (TagFile.name)*/ 0) //<= error (FATAL, "\"%s\" doesn't look like a tag file; ....", TagFile.name);
if (Option.etags) { .... }
Здесь мы видим, что условие (fileExists && /*! isTagFile (TagFile.name)*/ 0) всегда ложно из-за 0. Возможно, так и должно было быть, но, скорее всего, это ошибка. Это могло попасть в код, когда программист занимался отладкой и изменил условие, но потом, закончив работу, забыл вернуть условие обратно.
Лишнее сравнение
Предупреждение V728 Излишняя проверка может быть упрощена. Оператор || окружен противоположными выражениями !found и found. editor_config.cpp 120
bool EditorConfig::Load()
{
....
if(userSettingsLoaded) {
if(!found || (found && version != this->m_version)) { //<=
if(DoLoadDefaultSettings() == false) {
return false;
}
}
}
....
}
Ошибки здесь нет, но такое условие плохо читается. Его можно сократить до:
if(!found && version != this->m_version)
Предупреждение V571 Периодическая проверка. Условие isInStatement уже проверено в строке 2292. ASBeautifier.cpp 2293
void ASBeautifier::parseCurrentLine(const string& line)
{
....
if(isInStatement && !inStatementIndentStack->empty()) {
if(prevNonSpaceCh == '=' &&
isInStatement && !inStatementIndentStack->empty()) //<=
inStatementIndentStack->back() = 0;
}
}
....
}
Одно и то же подвыражение записывается двумя проверками, которые выполняются друг за другом. Возможно, эта ошибка попала сюда через копипаст, возможно, это условие нужно отредактировать, но в любом случае стоит пересмотреть.
Вывод
В проекте CodeLite примерно 600 тысяч строк кода, написанного на C и C++. Конечно, были и некоторые ошибки, допущенные из-за невнимательности и обращения с указателями, как это происходит в большинстве проектов. Всего анализатор выдал 360 предупреждений первого и второго уровня. Около 40 из них — это те, которые нужно пересмотреть и, скорее всего, исправить.
Чтобы избежать накопления ошибок в вашем коде, важно регулярно использовать статические анализаторы кода. Как показали результаты, отличным вариантом анализатора будет PVS-Studio.
Если вы хотите проверить свой проект, или любой интересующий вас проект, с помощью PVS-Studio для Linux, его можно скачать здесь.