Вы можете открыть всю документацию по PVS-Studio одной страницей.
Анализатор PVS-Studio активно развивается. Например, наша команда постоянно совершенствует его интеграцию с различными CI/CD системами и средами разработки, добавляет поддержку новых платформ и компиляторов. Однако лучше всего развитие возможностей анализатора показывает график количества диагностик.
Рисунок 1. Увеличение количества диагностик в PVS-Studio
Как видите, мы активно совершенствуем возможности анализатора по выявлению новых паттернов ошибок. Более подробная информация по нововведениям в различных версиях анализатора представлена ниже. Вы также можете почитать об изменениях в PVS-Studio за последний год в нашем блоге.
Мы постарались сгруппировать большинство диагностик в несколько групп, чтобы вы могли получить общее представление о возможностях анализатора кода PVS-Studio.
Так как деление диагностик весьма условно, то некоторые диагностики входят в несколько групп. Например, неправильное условие "if (abc == abc)", можно одновременно интерпретировать и как простую опечатку, и как проблему безопасности, так как ошибка приводит к уязвимости кода при некорректных входных данных.
Некоторым ошибкам наоборот места в таблице не нашлось - уж слишком они специфичны. Тем не менее, таблица в целом дает представление о функциональности статического анализатора кода.
Список диагностик анализатора в формате XML, предназначенный для автоматического разбора, доступен по постоянной ссылке здесь.
Основные диагностические возможности PVS-Studio |
Диагностики |
---|---|
64-битные ошибки |
C, C++: V101-V128, V201-V207, V220, V221, V301-V303 |
Адрес локальной переменной возвращается из функции по ссылке |
C, C++: V506, V507, V558, V723, V758, V1017, V1047 |
Арифметическое переполнение, потеря значимости |
C, C++: V569, V636, V658, V784, V786, V1012, V1026, V1028, V1029, V1033, V1070, V1081, V1083, V1085, V1112
C#: V3041, V3200, V3204
Java: V6011, V6088, V6117 |
Выход за границу массива |
C, C++: V557, V582, V643, V781, V1038, V1111
C#: V3106
Java: V6025, V6079 |
Двойное освобождение ресурсов |
C, C++: V586, V749, V1002, V1006 |
Мёртвый код |
C, C++: V606, V607
Java: V6021 |
Микрооптимизации |
C, C++: V801, V802, V803, V804, V805, V806, V807, V808, V809, V810, V811, V812, V813, V814, V815, V816, V817, V818, V819, V820, V821, V822, V823, V824, V825, V826, V827, V828, V829, V830, V831, V832, V833, V834, V835, V836, V837, V838, V839
C#: V4001, V4002, V4003, V4004, V4005, V4006, V4007 |
Недостижимый код |
C, C++: V517, V551, V695, V734, V776, V779, V785
C#: V3136, V3142, V3202
Java: V6018, V6019 |
Неинициализированные переменные |
C, C++: V573, V614, V679, V730, V737, V788, V1007, V1050, V1077, V1086
C#: V3070, V3128
Java: V6036, V6050, V6052, V6090 |
Неиспользуемые переменные |
C, C++: V603, V751, V763, V1001, V1079
C#: V3061, V3065, V3077, V3117, V3137, V3143, V3196, V3203
Java: V6021, V6022, V6023 |
Некорректные операции сдвига |
C, C++: V610, V629, V673, V684, V770, V1093
C#: V3134
Java: V6034, V6069 |
Неопределенное/неуточняемое поведение |
C, C++: V567, V610, V611, V681, V694, V704, V708, V726, V736, V772, V1007, V1016, V1026, V1032, V1061, V1066, V1069, V1082, V1091, V1094, V1097, V1099 |
Неправильная работа с типами (HRESULT, BSTR, BOOL, VARIANT_BOOL, float, double) |
C, C++: V543, V544, V545, V556, V615, V636, V676, V716, V721, V724, V745, V750, V767, V768, V771, V772, V775, V1014, V1027, V1034, V1046, V1060, V1066, V1084
C#: V3041, V3059, V3076, V3111, V3121, V3148
Java: V6038, V6108 |
Неправильное представление о работе функции/класса |
C, C++: V515, V518, V530, V540, V541, V554, V575, V597, V598, V618, V630, V632, V663, V668, V698, V701, V702, V717, V718, V720, V723, V725, V727, V738, V742, V743, V748, V762, V764, V780, V789, V797, V1014, V1024, V1031, V1035, V1045, V1052, V1053, V1054, V1057, V1060, V1066, V1098, V1100, V1107, V1115
C#: V3010, V3057, V3068, V3072, V3073, V3074, V3078, V3082, V3084, V3094, V3096, V3097, V3102, V3103, V3104, V3108, V3114, V3115, V3118, V3123, V3126, V3145, V3178, V3186, V3192, V3194, V3195, V3197
Java: V6009, V6010, V6016, V6026, V6029, V6049, V6055, V6058, V6064, V6068, V6081, V6110, V6116, V6122, V6125 |
Опечатки |
C, C++: V501, V503, V504, V508, V511, V516, V519, V520, V521, V525, V527, V528, V529, V532, V533, V534, V535, V536, V537, V539, V546, V549, V552, V556, V559, V560, V561, V564, V568, V570, V571, V575, V577, V578, V584, V587, V588, V589, V590, V592, V602, V604, V606, V607, V616, V617, V620, V621, V622, V625, V626, V627, V633, V637, V638, V639, V644, V646, V650, V651, V653, V654, V655, V657, V660, V661, V662, V666, V669, V671, V672, V678, V682, V683, V693, V715, V722, V735, V741, V747, V753, V754, V756, V765, V767, V768, V770, V771, V787, V791, V792, V796, V1013, V1015, V1021, V1040, V1051, V1055, V1074, V1094, V1113
C#: V3001, V3003, V3005, V3007, V3008, V3009, V3011, V3012, V3014, V3015, V3016, V3020, V3028, V3029, V3034, V3035, V3036, V3037, V3038, V3050, V3055, V3056, V3057, V3060, V3062, V3063, V3066, V3081, V3086, V3091, V3092, V3093, V3102, V3107, V3109, V3110, V3112, V3113, V3116, V3118, V3122, V3124, V3132, V3140, V3170, V3174, V3185, V3187
Java: V6001, V6005, V6009, V6012, V6014, V6015, V6016, V6017, V6021, V6026, V6028, V6029, V6030, V6031, V6037, V6041, V6042, V6043, V6045, V6057, V6059, V6061, V6062, V6063, V6077, V6080, V6085, V6091, V6105, V6112 |
Отсутствие виртуального деструктора |
C, C++: V599, V689 |
Оформление кода не совпадает с логикой его работы |
C, C++: V563, V612, V628, V640, V646, V705, V709, V715, V1044, V1073
C#: V3007, V3018, V3033, V3043, V3067, V3069, V3138, V3150, V3172, V3183
Java: V6040, V6047, V6063, V6086, V6089 |
Ошибки из-за Copy-Paste |
C, C++: V501, V517, V519, V523, V524, V571, V581, V649, V656, V666, V691, V760, V766, V778, V1037
C#: V3001, V3003, V3004, V3008, V3012, V3013, V3021, V3030, V3058, V3127, V3139, V3140
Java: V6003, V6004, V6012, V6021, V6027, V6032, V6033, V6039, V6067, V6072 |
Ошибки при работе с исключениями |
C, C++: V509, V565, V596, V667, V668, V740, V741, V746, V759, V1022, V1045, V1067, V1090
C#: V3006, V3052, V3100, V3141, V3163, V3164, V5606, V5607
Java: V6006, V6051, V6103 |
Переполнение буфера |
C, C++: V512, V514, V594, V635, V641, V645, V752, V755 |
Проблемы безопасности |
C, C++: V505, V510, V511, V512, V518, V531, V541, V547, V559, V560, V569, V570, V575, V576, V579, V583, V597, V598, V618, V623, V631, V642, V645, V675, V676, V724, V727, V729, V733, V743, V745, V750, V771, V774, V782, V1003, V1005, V1010, V1017, V1055, V1072, V1076, V1113
C#: V3022, V3023, V3025, V3027, V3039, V3053, V3063, V5601, V5608, V5609, V5610, V5611, V5612, V5613, V5614, V5615, V5616, V5617, V5618, V5619, V5620, V5621, V5622, V5623, V5624, V5625, V5626, V5627, V5628
Java: V5305, V5309, V6007, V6046, V6054, V6109 |
Путаница с приоритетом операций |
C, C++: V502, V562, V593, V634, V648, V727, V733, V1003, V1104
C#: V3130, V3133, V3177, V3207
Java: V6044 |
Разыменование нулевого указателя/нулевой ссылки |
C, C++: V522, V595, V664, V713, V757, V769
C#: V3019, V3042, V3080, V3095, V3105, V3125, V3141, V3145, V3146, V3148, V3149, V3153, V3156, V3168
Java: V6008, V6060, V6093 |
Разыменование параметров без предварительной проверки |
C, C++: V595, V664, V783, V1004
C#: V3095
Java: V6060 |
Ошибки синхронизации |
C, C++: V712, V720, V744, V1011, V1018, V1025, V1036, V1088, V1089, V1114
C#: V3032, V3054, V3079, V3082, V3083, V3089, V3090, V3147, V3167, V3168, V3190
Java: V6064, V6070, V6074, V6082, V6095, V6102, V6125 |
Утечки ресурсов |
C, C++: V599, V701, V773, V1020, V1023, V1100, V1106, V1110
Java: V6114, V6115 |
Целочисленное деление на 0 |
C, C++: V609
C#: V3064, V3151, V3152
Java: V6020 |
Ошибки сериализации / десериализации |
C, C++: V513, V663, V739, V1024, V1095
C#: V3094, V3096, V3097, V3099, V3103, V3104, V3193, V5611
Java: V6065, V6075, V6076, V6083, V6087 |
Диагностики, созданные по специальным просьбам пользователей |
C, C++: V2001, V2002, V2003, V2004, V2005, V2006, V2007, V2008, V2009, V2010, V2011, V2012, V2013, V2014, V2022 |
Таблица 1 – Возможности PVS-Studio.
Как видите, анализатор максимально проявляет себя в таких областях, как поиск ошибок, возникших из-за опечаток и Copy-Paste. Хорошо диагностирует проблемы, которые связаны с безопасностью кода.
Как всё это работает на практике можно узнать, заглянув в базу ошибок. Мы собираем в эту базу все ошибки, которые нашли, проверяя различные открытые проекты с помощью PVS-Studio.
Анализатор PVS-Studio позволяет производить анализ проектов, написанных на языках программирования C, C++, C# и Java. Он может быть запущен на операционных системах Windows, Linux и macOS. Перед началом использования анализатора необходимо произвести активацию лицензии. В связи с большим числом сценариев использования продукта (например, в IDE, на локальных и облачных CI/CD), далее будут перечислены все возможные способы активации. Перейдите в раздел, который Вам подходит, и следуйте инструкции.
Важно. Все действия выполняются после установки анализатора. Вы можете скачать его на странице "Скачать PVS-Studio".
Лицензия анализатора состоит из имени пользователя и 16-символьного лицензионного ключа в формате "ХХХХ-ХХХХ-ХХХХ-ХХХХ". Если у вас отсутствует лицензия, ее можно получить через форму запроса триальной версии.
Ниже приведен пример того, как может выглядеть лицензионная информация:
JohnSmith <--- Имя пользователя
ASD1-DAS3-5KK3-LODR <--- Лицензионный ключ
Вы можете ввести лицензию во время установки PVS-Studio. В процессе установки вам предложат запросить лицензию или ввести существующую.
Выберите пункт I have a license and want to activate it и нажмите Next:
В поле 'License Name' необходимо подставить имя пользователя, а в 'License Key' – лицензионный ключ из своей лицензии. Если вы ввели валидные данные, то у вас отобразится сообщение с данными лицензии.
Перейдите в меню Visual Studio Extensions > PVS-Studio > Options (до 2015 версии Visual Studio просто PVS-Studio > Options):
После этого справа в меню перейдите на страницу PVS-Studio > Registration:
В поле 'Name' необходимо подставить имя пользователя, а в 'LicenseKey' – лицензионный ключ из своей лицензии. Если вы ввели валидные данные, то у вас отобразится сообщение с данными лицензии:
В случае если введённые данные лицензии недействительны, то вам также отобразится уведомление об этом:
Для ввода лицензии анализатора необходимо открыть любой проект, после чего открыть окно настроек вашей IDE:
В открывшемся окне настроек плагина перейти на страницу PVS-Studio > Registration.
В поле 'Name' необходимо подставить имя пользователя, а в 'License Key' – лицензионный ключ из своей лицензии:
Если вы ввели правильную лицензию, то надпись 'Invalid License' будет заменена на 'Valid License'. Для подтверждения и сохранения введённой лицензии нажмите кнопку Apply или OK.
Перейдите в меню утилиты Tools > Options > Registration, чтобы ввести лицензию:
В поле 'Name' необходимо подставить имя пользователя, а в 'LicenseKey' – лицензионный ключ из своей лицензии.
Для ввода лицензии в Visual Studio Code откройте View > Command Palette.
Введите в строку 'PVS-Studio: Show settings' и откройте их.
В открывшемся окне выберите вкладку 'License'.
В поле 'User name' необходимо подставить имя пользователя, а в 'Key' – лицензионный ключ из своей лицензии. Если вы ввели валидные данные, то у вас отобразится сообщение с данными лицензии:
Для ввода лицензии в Qt Creator перейдите в Analyze > PVS-Studio > Options...
Далее выберите PVS-Studio и перейдите на вкладку Registration. В поле 'Name' необходимо подставить имя пользователя, а в 'License Key' – лицензионный ключ из своей лицензии. Если вы ввели валидные данные, то у вас отобразится сообщение с данными лицензии:
В случае если введённые данные лицензии недействительны, то вам также отобразится уведомление об этом:
Для подтверждения и сохранения введённой лицензии нажмите кнопку Apply или OK.
На Windows, когда нет возможности ввести лицензию через GUI, можно воспользоваться самим анализатором в специальном режиме.
Строка запуска может выглядеть следующим образом:
PVS-Studio_Cmd.exe credentials --userName %USER_NAME% ^
--licenseKey %LICENSE_KEY%
Вместо переменной 'USER_NAME' необходимо подставить имя пользователя, а вместо 'LICENSE_KEY' – лицензионный ключ из своей лицензии.
При таком запуске анализатор запишет лицензию в файл настроек с расположением по умолчанию: "%APPDATA%/PVS-Studio/Settings.xml". Если файла настроек не существует, он будет создан. Используя флаг ‑‑settings, можно указать путь до файла настроек в нестандартном расположении.
С помощью флага ‑‑licInfo можно получить информацию о текущей лицензии.
При использовании анализатора на Linux/macOS платформах, когда нет возможности ввести лицензию при помощи GUI, используется специальная утилита pvs-studio-analyzer.
На Linux / macOS строка запуска может выглядеть следующим образом:
pvs-studio-analyzer credentials ${USER_NAME} ${LICENSE_KEY}
Вместо переменной 'USER_NAME' необходимо подставить имя пользователя, а вместо 'LICENSE_KEY' – лицензионный ключ из своей лицензии.
При таком запуске анализатор запишет лицензию в файл настроек с расположением по умолчанию: "~/.config/PVS-Studio/PVS-Studio.lic". Если файла настроек не существует, он будет создан.
Поскольку Java анализатор может быть установлен независимо от других компонентов PVS-Studio, активацию лицензии можно также произвести через плагины для сборочных систем (Maven, Gradle) и ядро Java анализатора.
При использовании Maven команда для ввода лицензии может выглядеть следующим образом:
mvn pvsstudio:pvsCredentials "-Dpvsstudio.userName=${USER_NAME}" \
"-Dpvsstudio.licenseKey=${LICENSE_KEY}"
При использовании Gradle активация происходит с помощью следующей команды:
./gradlew pvsCredentials "-Ppvsstudio.userName=${USER_NAME}" \
"-Ppvsstudio.licenseKey=${LICENSE_KEY}"
При использовании ядра Java анализатора из консоли активировать лицензию возможно командой:
java -jar pvs-studio.jar --activate-license --user-name $USER_NAME \
--license-key $LICENSE_KEY
Вместо переменной 'USER_NAME' необходимо подставить имя пользователя, а вместо 'LICENSE_KEY' – лицензионный ключ из своей лицензии.
Способы запуска анализатора приведены на соответствующих страницах:
После загрузки дистрибутива PVS-Studio и запроса ключа для знакомства с инструментом, вы получаете полнофункциональную версию, которая работает одну неделю. В этой версии нет совершенно никаких ограничений – это абсолютно полноценная лицензия. При заполнении формы вы можете выбрать, какой тип лицензии хотели бы испробовать: Team License или Enterprise License.
Отличия Enterprise License от Team License представлены на этой странице.
Если вам не хватило недели на знакомство с инструментом – просто напишите нам об этом в ответном письме. И мы пришлем еще один ключ.
PVS-Studio работает на Windows (x86-64 и ARM), Linux (x86-64) и macOS (Intel и Apple Silicon). Поддерживается анализ кода для кроссплатформенных компиляторов, предназначенных для 32-битных, 64-битных, встраиваемых ARM платформ и других.
PVS-Studio требует как минимум 2 GB оперативной памяти (рекомендуется 4 GB и больше) для каждого процессорного ядра, когда анализатор работает на многоядерной системе (чем больше ядер, тем быстрее работает анализ кода).
Список поддерживаемых анализатором языков программирования и компиляторов доступен здесь.
Минимально поддерживаемая версия GCC — 4.2.
Плагин PVS-Studio может быть интегрирован в:
Для всех IDE (кроме IDEA и Android Studio) необходимо иметь в системе установленное ядро анализатора для соответствующего языка (C, C++, C# или Java).
Поддерживаются Windows 11, Windows 10, Windows 8, Windows Server 2019, Windows Server 2016 и Windows Server 2012. PVS-Studio работает как на 64-битных версиях Windows, так и на версиях для ARM.
Для работы PVS-Studio требуется .NET Framework версии 4.7.2 или выше (будет автоматически установлен при установке PVS-Studio, если подходящей версии нет в системе).
Для анализа .NET, .NET Standard и .NET Framework SDK-style проектов требуется .NET SDK 9.0. Для анализа классических .NET Framework проектов достаточно .NET Framework 4.7.2 при наличии установленных в системе Visual Studio или MSBuild версий 2017, 2019 или 2022.
Плагин PVS-Studio может быть интегрирован в Microsoft Visual Studio 2022, 2019, 2017, 2015, 2013, 2012, 2010. Для анализа C и C++ кода для встраиваемых систем соответствующие компиляторы должны быть установлены в системе, на которой запускается анализатор.
Примечание: интеграция с Visual Studio 2022 для версий 17.12 и выше поддерживается, начиная с версии PVS-Studio 7.33.
PVS-Studio работает в 64-битных дистрибутивах Linux с ядром версий 3.2.0 и выше. Для анализа C и C++ проектов для Linux, кросс-платформенных приложений или встраиваемых систем, соответствующие компиляторы должны быть установлены в системе. Для утилиты оповещения команд разработчиков blame-notifier должен быть установлен .NET Runtime 9.0.
Список протестированных дистрибутивов, на которых работа PVS-Studio гарантируется:
Анализаторы PVS-Studio были проверены на совместимость и корректную работу с операционными системами РЕД ОС 7.3 и РЕД ОС 8.
PVS-Studio работает на процессорах Intel (x86-64) в macOS 10.13.2 High Sierra и выше. Также поддерживается работа на процессорах Apple Silicon (arm64) в macOS 11 Big Sur и выше. Для анализа C и C++ кода соответствующие компиляторы должны быть установлены в системе. Для утилиты оповещения команд разработчиков blame-notifier должен быть установлен .NET Runtime 9.0.
PVS-Studio для Java работает на Windows (x86-64 и ARM), Linux (x86-64) и macOS (Intel и Apple Silicon). Java 11 (64-битная) является минимальной версией для работы анализатора. Анализируемый проект может использовать любую версию Java.
PVS-Studio предоставляет статические анализаторы для языков C, C++, C# и Java на платформах Windows, Linux и macOS. Несмотря на некоторые различия, накладываемые особенностями отдельных языков, в целом все анализаторы PVS-Studio используют ряд общих технологий и подходов к реализации статического анализа.
PVS-Studio разрабатывается с учётом требований, предъявляемых к статическим анализаторам в ГОСТ Р 71207–2024. Выявляет критические ошибки (потенциальные уязвимости) и может использоваться при разработке безопасного программного обеспечения.
В составе PVS-Studio можно выделить 3 отдельных программных инструмента для статического анализа: анализатор C и C++, анализатор C# и анализатор Java.
Анализатор PVS-Studio для языков C и C++ написан на языке C++ и основан на библиотеке разбора кода с закрытым исходным кодом VivaCore, также разрабатываемой командой PVS-Studio.
Анализатор PVS-Studio для языка C# написан на языке C# и использует для разбора кода (построения абстрактного синтаксического дерева и семантической модели) и интеграции с проектной системой MSBuild \ .NET открытую платформу Roslyn.
Анализатор PVS-Studio для языка Java написан на языке Java. Он использует возможности внутренней C++ библиотеки VivaCore для анализа потока данных. Для разбора кода (построения абстрактного синтаксического дерева и семантической модели) анализатор использует открытую библиотеку Spoon.
Все анализаторы PVS-Studio реализуют алгоритмы и механизмы для проведения data-flow анализа (включая символьное выполнение, межпроцедурный контекстно-чувствительный анализ и межмодульный анализ), основанные на собственных разработках компании PVS-Studio.
Рассмотрим далее подходы и методики, на которых основана технология статического анализа кода PVS-Studio.
Вначале рассмотрим два термина, применяющихся в теории разработки компиляторов и статических анализаторов кода.
Абстрактное синтаксическое дерево (AST). AST — это конечное ориентированное дерево, в котором внутренние вершины соответствуют операторам языка программирования, а листья — соответствующим операндам. Абстрактные деревья синтаксиса используются компиляторами и интерпретаторами как промежуточное представление между деревьями разбора и внутренним представлением кода. Преимущество AST — достаточная компактность (абстрактность) структуры, достигаемая за счёт отсутствия узлов для конструкций, не влияющих на семантику программы.
Основным преимуществом использования абстрактного синтаксического дерева, по сравнению с прямым анализом текста программ (исходного кода), является независимость построенных на его основе анализаторов от конкретного синтаксиса: имён, стиля написания кода, его форматирования и т.п.
Дерево разбора (DT). Результат грамматического анализа. Дерево разбора отличается от абстрактного синтаксического дерева наличием узлов для тех синтаксических правил, которые не влияют на семантику программы. Классическим примером таких узлов являются группирующие скобки, в то время как в AST группировка операндов явно задаётся структурой дерева.
Высокоуровнево можно говорить, что ядра всех анализаторов PVS-Studio для разных языков работают с абстрактным синтаксическим деревом (AST). Однако на практике всё немного сложнее. В некоторых случаях диагностики требуют информации о необязательных узлах или даже вообще о количестве пробелов в начале строки. В этом случае анализ спускается на уровень дерева разбора и извлекает дополнительную информацию. Все используемые библиотеки (Roslyn, Spoon, VivaCore) предоставляют возможность получать информацию на уровне дерева разбора, и анализатор в ряде случаев этим пользуется.
Анализаторы PVS-Studio используют AST представление программы для поиска потенциальных дефектов методом сигнатурного анализа (сопоставление с шаблоном, pattern-based analysis). Это класс достаточно простых диагностических правил, которым для принятия решения об опасности кода достаточно сопоставить конструкцию, встретившуюся в коде, с заранее заданным шаблоном потенциальной ошибки.
Следует отметить, что поиск по шаблону — более высокоуровневая и эффективная технология, чем использование регулярных выражений. Регулярные выражения вообще не подходят по множеству причин для построения эффективного статического анализатора. Поясним это на простом примере. Допустим, требуется найти опечатки, когда выражение сравнивается само с собой. Это можно сделать регулярными выражениями для простейших случаев:
if (A + B == A + B)
if (V[i] == V[i])
Однако, если ошибочные выражения записаны чуть по-другому, регулярные выражения бессильны. Вернее, их просто невозможно написать для всех случаев альтернативной записи:
if (A + B == B + A)
if (A + (B) == (B) + A)
if (V[i] == ((V[i])))
if (V[(i)] == (V[i]))
В свою очередь, для сопоставления по шаблону, если используется AST, выявление подобных ошибок не является проблемой.
Представление кода в виде абстрактного синтаксического дерева также является подготовкой к проведению следующего уровня анализа — построения семантической модели и вывода типов.
Помимо синтаксического анализа кода, все анализаторы PVS-Studio на основе построенного на предыдущем этапе представления кода в виде абстрактного синтаксического дерева проводят также и семантический анализ — построение полной семантической модели проверяемого кода.
Обобщённая семантическая модель представляет собой словарь соответствий семантических (смысловых) символов и элементов синтаксического представления этого же кода (для которого PVS-Studio использует узлы упомянутого выше абстрактного синтаксического дерева).
Каждый такой символ определяет семантику (смысл) соответствующей синтаксической конструкции языка. Эта семантика может быть неочевидна и невыводима из самого локального синтаксиса. Для выведения такой семантики требуется обращаться к другим частям синтаксического представления кода. Поясним это на примере фрагмента кода на языке C:
A = B(C);
Не зная, что собой представляет 'B', невозможно сказать, какая перед нами конструкция языка. Это может быть как вызов функции, так и явное приведение типа (functional cast expression).
Семантическая модель, таким образом, позволяет анализировать семантику кода без необходимости постоянно совершать обход синтаксического представления этого кода для разрешения не выводимых из локального контекста семантических фактов. Семантическая модель "запоминает" семантику по ходу разбора кода для дальнейшего её использования.
На основе семантической модели анализаторы PVS-Studio получают возможность проводить вывод типов (type inference) у любой встречаемой синтаксической конструкции (как, например, идентификаторы переменных, выражения и т.п.), для которой это может потребоваться при проведении анализа кода на потенциальные дефекты. Семантическая модель дополняет анализ на основе шаблонов в случаях, когда одного синтаксического представления недостаточно для принятия решения об опасности проверяемой конструкции.
Построение полной и корректной семантической модели требует непротиворечивости и, соответственно, собираемости (компилируемости) проверяемого кода. Компилируемость исходного кода является обязательным условием для полноценной и корректной работы всех анализаторов PVS-Studio. Анализаторы PVS-Studio имеют механизмы отказоустойчивости при работе с некомпилируемым кодом. Однако такой код может ухудшать точность работы диагностических правил.
Препроцессирование C и C++ кода — это процесс раскрытия в исходном коде директив компилятора и подстановка значений макросов. В частности, в результате работы препроцессора на месте директив #include происходит подстановка содержимого заголовочных файлов, пути до которых записаны в данной директиве. Директивы и макросы раскрываются в случае такой подстановки последовательно, так же, как и во всех раскрываемых директивой #include заголовочных файлах. Препроцессирование является первым этапом работы компилятора — подготовкой единицы компиляции и её зависимостей к непосредственной трансляции исходного кода во внутреннее представление компилятора.
Раскрытие директив #include приводит к объединению исходного файла и всех используемых в нём заголовочных файлов в единый файл, часто называемый промежуточным (intermediate). По аналогии с компилятором, С и C++ анализатор PVS-Studio использует препроцессирование перед началом анализа. PVS-Studio использует для препроцессирования проверяемого кода целевой компилятор (в режиме препроцессора), для сборки которым этот код был изначально предназначен. PVS-Studio поддерживает работу с большим количеством препроцессоров, которые перечислены на странице продукта. Так как выходной формат препроцессоров различных компиляторов отличается, для корректной работы анализатора требуется использовать правильный препроцессор, соответствующий используемому для сборки проверяемого кода компилятору.
Перед началом анализа C и C++ анализатор PVS-Studio осуществляет запуск препроцессора для каждой единицы трансляции проверяемого кода. Помимо содержимого исходных файлов, на работу препроцессора также влияют параметры компиляции. PVS-Studio использует для препроцессирования параметры сборки, применяемые при компиляции проверяемого кода. Информацию о списке единиц трансляции, а также параметры компиляции PVS-Studio получает из сборочной системы проверяемого проекта либо путём отслеживания (перехвата) вызовов компиляторов в процессе сборки проекта.
Работа анализатора C и С++ PVS-Studio основана на результате работы соответствующего препроцессора — анализатор не анализирует исходный код напрямую. Препроцессирование C и C++ кода за счёт раскрытия директив компилятора позволяет анализатору построить полную семантическую модель проверяемого кода.
Технология отслеживания (мониторинга) PVS-Studio позволяет перехватывать запуск процессов на уровне API операционной системы. Перехват запуска позволяет извлекать у процесса полную информацию о его работе, параметрах его запуска и рабочем окружении. Отслеживание запуска процессов PVS-Studio поддерживается на платформах Windows (реализовано через прямую работу с WinAPI) и Linux (реализовано с помощью стандартной системной утилиты strace).
C и C++ анализаторы PVS-Studio могут использовать отслеживание процессов компиляции как один из вариантов проведения анализа С++ кода. Несмотря на то, что PVS-Studio имеет средства прямой интеграции с наиболее распространёнными сборочными системами для C и C++ проектов, экосистема данных языков очень многообразна, и в ней существует большое количество сборочных систем (например, в embedded сегменте), интеграцию с которыми PVS-Studio не поддерживает. Хотя прямая, низкоуровневая интеграция C++ анализатора PVS-Studio в такие системы возможна, она является достаточно трудоёмкой, т.к. требует передачи в анализатор параметров компиляции для каждой единицы трансляции — исходного C или C++ файла.
Использование системы отслеживания процессов компиляции PVS-Studio позволяет упростить и автоматизировать процесс передачи анализатору всей необходимой для проведения анализа информации. Система отслеживания собирает параметры компиляции у процессов, анализирует и модифицирует их (например, активируя режим препроцессирования у компилятора — анализатору нужно проведение только этого этапа) и передаёт непосредственно в C++ анализатор PVS-Studio.
Таким образом, система отслеживания запуска процессов позволяет реализовать в PVS-Studio универсальную систему проверки C и C++ проектов, не зависящую от используемой сборочной системы, легко настраиваемую и полностью учитывающую оригинальные параметры компиляции проверяемого кода.
Анализ потоков данных — это методика, позволяющая статическому анализатору делать предположения о значениях переменных и выражений в различных частях исходного кода. Под предполагаемыми значениями понимаются какие-то конкретные значения, диапазоны значений или множество возможных значений. Дополнительно собирается и обрабатывается информация о том, освобождена память по указателю или нет, каковы размеры массивов и так далее.
Предположения о значениях строятся на основе анализа движения значений переменных по графу выполнения программы (control-flow graph). В большинстве случаев анализатор не может знать точного значения переменной или выражения. Но он может, используя прямые и косвенные ограничения, накладываемые на рассматриваемые выражения по ходу обхода графа выполнения, делать предположения о диапазонах или множествах значений, которые данные выражения могут принимать в различных точках этого графа.
Все анализаторы PVS-Studio используют data-flow анализ для уточнения работы своих диагностических правил в случаях, когда для принятия решения об опасности рассматриваемого кода недостаточно информации, доступной только из синтаксической (AST) или семантической структуры этого кода. Анализаторы PVS-Studio используют собственную внутреннюю реализацию методики анализа потоков данных. Анализ потоков данных PVS-Studio поддерживает чувствительность к потокам и путям управления (flow-sensitive и path-sensitive dataflow анализ). Все ветвления в проверяемом коде учитываются в построении модели потоков данных этого кода.
Анализатор PVS-Studio для С, C++ и анализатор PVS-Studio для Java используют общую внутреннюю C++ библиотеку для работы data-flow анализа. Анализатор PVS-Studio для C# имеет реализацию data-flow алгоритмов, написанную на языке C#.
В случаях когда у анализатора при разборе кода отсутствует возможность напрямую рассчитать диапазон значений выражения, применяется методика символьного выполнения (symbolic execution). Символьное выполнение представляет возможные значения переменных и выражений в символическом виде с помощью формул. В таком представлении анализатор оперирует не конкретными значениями переменных, а символами, абстрагирующими данные переменные.
Рассмотрим пример на языке C++:
int F(std::vector<int> &v, int x)
{
int denominator = v[x] - v[x];
return x / denominator;
}
Даже если ничего неизвестно о том, с какими значениями вызывается функция, анализатор PVS-Studio обнаружит здесь деление на ноль.
Строя при обходе графа выполнения формулы для встречаемых выражений, анализатор может в различных точках этого графа вычислять ограничения значений данных выражений, подставляя в такие формулы известные ограничения на символы, от которых данное выражение зависит. Таким образом, решая созданные при обходе графа формулы, алгоритмы символьного выполнения позволяют вычислять ограничения значений одних выражений или переменных в зависимости от значений других выражений или переменных. Можно сказать, что вычисление конечного значения выражения откладывается до момента, когда оно понадобится (например, при работе диагностического правила, где оно будет вычислено по заранее сформулированной формуле).
Анализатор PVS-Studio для C, C++ и анализатор Java используют в рамках работы своих data-flow алгоритмов методику символьного выполнения.
Межпроцедурным анализом называют способность статического анализатора раскрывать точки вызова функций и учитывать влияние таких вызовов на состояние программы и переменных в локальном проверяемом контексте. Анализаторы PVS-Studio используют межпроцедурный анализ для уточнения ограничений и диапазонов значений переменных и выражений, рассчитываемых с помощью data-flow механизмов.
Благодаря использованию при анализе AST представления кода и наличию полной семантической модели анализаторы PVS-Studio, встречая вызов функции, могут получать аналогичное AST представление тела этой функции, в котором также будет доступна вся семантическая информация.
Межпроцедурный анализ в PVS-Studio позволяет учитывать при анализе потока данных, значения, которые возвращают вызываемые функции. Также отслеживается состояния передаваемых в функции переменных и выражений, позволяя для передаваемых значений детектировать потенциально опасные конструкции и операции внутри тел функций. Таким образом, возможно обнаружение потенциальных дефектов как внутри тел самих вызываемых функций, так и определение взаимосвязи ограничений, накладываемых на возвращаемые значения функций их входными значениями.
Работа межпроцедурного анализа ограничена доступностью исходного кода функций, которые анализатору требуется раскрыть. Для раскрытия функций, определённых в исходных файлах, отличных от текущего проверяемого, PVS-Studio поддерживает механизм межмодульного анализа. Прямой учёт при анализе функций, описанных в сторонних библиотеках, невозможен (из-за отсутствия при проверке их исходных кодов), однако анализаторы PVS-Studio способны учитывать их возвращаемые значения с помощью механизма аннотаций.
Помимо межпроцедурного, анализаторы PVS-Studio также поддерживают межмодульный анализ. Под межмодульным анализом в PVS-Studio понимается расширение возможностей межпроцедурного анализа на функции, объявленные в программных модулях, отличных от того, к которому относится текущий проверяемый файл.
В различных языках программирования под модулями могут пониматься различные сущности, однако в общем мы понимаем в данном контексте под модулем единицу компиляции. Для языков C и C++ — это отдельный файл с исходным кодом (файл с расширением .c или .cpp). Для языка C# — это проект. Для языка Java — это исходный файл (файл с расширением .java) с объявленным в нём классом.
Анализаторы PVS-Studio для Java и C# способны получать исходный код функций, находящихся как в том же исходном файле, так и в других исходных файлах, относящихся к проверяемому проекту. Анализатор PVS-Studio для C# может также получать и анализировать исходный код функций, объявленных в других проектах, если эти проекты также были переданы в анализатор для проверки.
Анализатор PVS-Studio для C++ может получать тела методов, объявленных в проверяемой единице компиляции (препроцессированном исходном файле с раскрытыми включениями заголовочных файлов). Межмодульный режим работы C++ анализатора позволяет также получать data-flow информацию из других единиц компиляции. Для этого анализатор использует двухпроходный режим анализа. Первый проход собирает межпроцедурную data-flow информацию для всех проверяемых исходных файлов. На втором проходе эта информация используется при непосредственном анализе исходных файлов.
Если при разборе кода анализатор встречает функцию, тело которой невозможно раскрыть для анализа, он может воспользоваться механизмом аннотирования функций. Аннотирование функций позволяет задавать в декларативном виде информацию как об ограничениях в возвращаемых функцией значениях, так и об ограничениях, накладываемых на её входные параметры.
Аннотирование в анализаторах PVS-Studio можно разделить на 2 категории: библиотечные функции и пользовательские функции. Все анализаторы PVS-Studio содержат в себе аннотации на множество функций стандартных и распространённых библиотек. C++ анализатор PVS-Studio также предоставляет возможность задавать аннотации в виде специального декларативного синтаксиса для пользовательских функций, специфичных для конкретного проверяемого проекта.
Taint анализ (анализ помеченных данных) — это методика отслеживания распространения по программе внешних непроверенных, "загрязнённых" (отсюда и название taint) данных. Попадание в определённые уязвимые приёмники таких данных может привести к возникновению целого ряда уязвимостей безопасности, таких как SQL инъекции, межсайтовый скриптинг (XSS, cross-site scripting) и многих других. Потенциальные уязвимости ПО, связанные с распространением заражённых данных, описаны в стандартах безопасной разработки, таких как OWASP ASVS (Application Security Verification Standard).
Обычно программу невозможно полностью защитить от ввода в неё потенциально опасных данных. Поэтому основным способом борьбы с внешними taint данными является проверка таких данных перед их использованием или попаданием в уязвимые приёмники — так называемая очистка (санитизация) данных.
Анализатор PVS-Studio для C и C++, а также анализатор PVS-Studio для C# могут с помощью технологий межпроцедурного data-flow анализа отслеживать распространение по программе taint данных. На механизме отслеживания taint данных основана целая группа диагностических правил PVS-Studio.
Анализаторы PVS-Studio контролируют всю трассу распространения заражённых данных с учётом их передачи между программными модулями, а также их проверки (очистки). PVS-Studio сгенерирует предупреждение о потенциальной угрозе безопасности кода только в случае, если анализатор отследит полный путь прохождения taint данных от источника до приёмника без проверки. Таким образом, PVS-Studio контролирует не только попадание опасных данных в программу, но и их использование без проверки — опасным анализатор посчитает именно использование данных, а не только их ввод.
Многие современные приложения используют сторонние компоненты: библиотеки, пакеты и т.п. Некоторые из этих компонентов содержат уязвимости. Если приложение использует такой компонент, оно тоже может быть уязвимо.
Для поиска "опасных" зависимостей используют инструменты, которые проводят анализ компонентного состава ПО (software composition analysis, SCA).
Анализатор PVS-Studio для C# поддерживает анализ компонентного состава ПО. Алгоритм работы данного механизма:
Подробнее об SCA в C# анализаторе PVS-Studio можно прочитать в документации к диагностическому правилу V5625.
PVS-Studio_Cmd
и pvs-studio-dotnet
был добавлен режим проверки модифицированных файлов, позволяющий автоматически обнаружить изменённые между запусками анализа исходные файлы. Данный режим является альтернативой инкрементальному анализу и может быть полезен при проверке Pull Request'ов. Подробнее о новом режиме можно прочитать в документации. plog-converter
была добавлена возможность отображения критических ошибок согласно ГОСТ Р 71207-2024..pvsconfig
. Также данный механизм можно будет использовать для проверки Unreal Engine проектов с помощью Unreal Build Tool, начиная с версии 5.5.2. Подробнее можно прочитать в документации.pvs-studio-analyzer
изменено поведение флага ‑‑sourcetree-root (-r)
. При подмене путей в генерируемом отчёте путь до базовой директории проверяется на существование. Если часть путей не получилось подменить, то выдаётся сообщение с предупреждением, но код возврата остаётся равным 0. Если же не получилось подменить ни один путь, то помимо сообщения с предупреждением код возврата будет ненулевым.Старую историю версий для прошлых релизов смотрите здесь.
Новую историю версий смотрите здесь.
Плагин PVS-Studio для среды разработки Microsoft Visual Studio не только предоставляет удобный графический интерфейс и возможности статического анализа, но и широкий спектр дополнительных возможностей по работе с сообщениями анализатора.
Установить его вы можете как из официального репозитория плагинов, так и при помощи нашего установщика для Windows, который доступен на странице загрузки.
Чтобы установить плагин PVS-Studio для Visual Studio из Marketplace, нужно перейти в управление плагинами (Manage Extensions) с помощью команды Extensions -> Manage Extensions и ввести в поиске PVS-Studio. В результатах поиска появится нужный плагин:
После нажатия на кнопку Download начнётся автоматическая загрузка установщика PVS-Studio для Windows, который содержит не только сам плагин, но также ядро анализатора и вспомогательные инструменты.
Установщик попросит закрыть все активные процессы в доступных средах разработки и предложит на выбор плагины для всех установленных IDE:
Первое, что нужно сделать после установки – ввести лицензию. Процесс ввода лицензии в Visual Studio подробно описан в документации.
Помимо Registration есть и другие настройки – давайте рассмотрим их подробнее.
Первый раздел в списке – это Common Analyzer Settings:
По умолчанию в этой вкладке выставляются оптимальные для использования настройки. Так ThreadCount (количество потоков) равно числу ядер на используемом процессоре.
RemoveIntermediateFiles же может понадобиться, если будет обнаружена какая-либо проблема в работе анализатора и нужно будет сообщить о ней нам. При выборе false после анализа останутся артефакты (препроцессированные файлы и файлы конфигурации), которые помогут обнаружить проблему.
Этот раздел позволяет выбрать, какие именно предупреждения вы хотите получать. Можно скрыть или показать не только отдельные срабатывания, но и целые классы диагностик:
Крайне полезный раздел, который поможет улучшить качество и скорость анализа при помощи исключения лишних файлов, которые, например, не относятся напрямую к проекту:
По умолчанию уже указаны самые распространённые исключаемые директории и расширения. Если у вас будут срабатывания, например, на third party код, то стоит добавить его в PathMasks.
Подробнее про Don't Check Files можно посмотреть в соответствующем разделе документации.
При помощи Keyword Message Filtering можно указать ключевые слова, которые нужно исключить из результатов анализа:
Например, если вам встречаются сообщения об ошибках, связанные с printf, а вы считаете, что ошибок, связанных с ними, быть не может, то добавьте ключевое слово printf. Сообщения, содержащие указанное значение, сразу же будут отфильтрованы без перезапуска анализа.
По умолчанию плагин настроен для работы в самых распространённых условиях, однако если проект имеет специфичные особенности, то можно дополнительно настроить анализатор при помощи раздела Specific Analyzer Settings:
Подробное описание дополнительных настроек можно найти в специальном разделе документации.
В плагине PVS-Studio поддерживается анализ не только всего решения, но также отдельных проектов и файлов. Для запуска анализа можно выбрать интересующие файлы и проекты, и запустить на них анализ из контекстного меню:
Также всегда можно запустить анализ для текущего файла:
Дополнительно в элементе меню "Check" имеется несколько пунктов меню для более специфичных вариантов анализа:
Плагин PVS-Studio для Visual Studio поддерживает проверку проектов и исходных файлов для языков C, С++ и C#. Для языка C++ поддерживается проверка стандартных Visual C++ проектов, использующих сборочную систему MSBuild.
Для проверки специфичных проектов, таких, как, например, NMake проекты, можно воспользоваться системой отслеживания вызовов компилятора.
После анализа в IDE появится панель с результатами анализа:
Здесь можно помечать заинтересовавшие вас сообщения, подавлять ложно-позитивные срабатывания, сортировать их по разным категориям, выбирать уровни достоверности, открывать документацию к диагностикам, а также совершать многие другие манипуляции с предупреждениями.
При помощи двойного клика по строке можно легко перейти на участок кода, содержащий подозрительное место:
А при нажатии на три полоски открывается дополнительное меню панели PVS-Studio:
Подробнее про работу со списком предупреждений можно прочитать в документации.
При первом запуске анализатора на большом проекте может быть действительно много срабатываний. Разумеется, стоит выписать себе самые интересные, а вот остальные можно со спокойной душой скрыть при помощи механизма подавления предупреждений, ведь ваш код уже протестирован и работает. Так как маловероятно, что вы найдете среди них критичные для работы приложения проблемы (однако и такое может быть).
Для подавления всех предупреждений нужно выбрать пункт Suppress All Messages в дополнительной панели плагина:
После этого они будут добавлены в специальные *.suppress файлы. Так как при подавлении учитываются и соседние строки, при смещении кода сообщения не появятся вновь. Однако, если внести правку рядом с предупреждением, тогда оно будет вновь показано при последующем запуске анализа.
Подробное описание подавления предупреждений и работы с *.suppress файлами вы найдёте в разделе документации Подавление сообщений анализатора.
Также предлагаем познакомиться со статьёй "Как внедрить статический анализатор кода в legacy проект и не демотивировать команду".
Так или иначе во время работы с анализатором будут появляться ложные срабатывания (false positive). Для таких ситуаций предусмотрен специальный механизм подавления и фильтрации.
Чтобы пометить сообщение как ложное, кликните на строку с предупреждением правой кнопкой мыши и выберите Mark selected messages as False Alarms:
После этого к строке с предупреждением добавится комментарий типа //-Vxxx, где xxx – это номер диагностического правила PVS-Studio. Такой комментарий также можно добавить в код вручную. При помощи команды контекстного меню Remove False Alarm marks from selected messages можно удалить отметку ложного срабатывания с выбранных сообщений.
Если же вы не хотите добавлять комментарии в код, тогда можно воспользоваться механизмом подавления сообщений и добавить предупреждение в *.suppress файл. Чтобы это сделать, выберите другой пункт контекстного меню Add selected messages to suppression file:
Подробное описание работы механизма подавления ложных срабатываний вы найдёте в разделе документации Подавление ложно-позитивных предупреждений.
Если Вы только начали изучать инструмент статического анализа и хотели бы узнать на что он способен, то можете воспользоваться механизмом Best Warnings. Данный механизм покажет вам наиболее важные и достоверные предупреждения.
Чтобы посмотреть наиболее интересные предупреждения с точки зрения анализатора нажмите на кнопку 'Best', как показано на скриншоте ниже:
После чего в таблице с результатами анализа останутся максимум десять наиболее критичных предупреждений анализатора.
Плагин для Visual Studio добавляет окно просмотра результатов анализа PVS-Studio, которое имеет контекстное меню, появляющееся при нажатии правой кнопкой мыши в окне с результатами анализа:
Некоторым действиям из этого меню назначены горячие клавиши, что позволяет выполнять их без использования мыши.
Кнопки со стрелками, предназначенные для навигации по сообщениям анализатора, также имеют горячие клавиши:
Использование горячие клавиш полезно, потому что позволяет ускорить процесс обработки результатов анализа. Их можно назначать\переопределять в настройках: Tools -> Options -> Keyboard. Чтобы быстрее их найти, необходимо ввести 'PVSStudio' в поле поиска окна Keyboard.
Разработка для встраиваемых систем имеет свою специфику и подходы, но контроль качества кода в этой сфере не менее важен, чем в других. PVS-Studio поддерживает анализ проектов, которые используют следующие компиляторы:
Поддерживаемыми платформами для разработки являются Windows, Linux и macOS.
После установки анализатора в Linux или macOS станет доступна утилита для анализа проектов — pvs-studio-analyzer. Подробнее о работе утилиты можно узнать тут.
В утилиту добавлено автоматическое определение поддерживаемых компиляторов, но если используется модифицированный или расширенный пакет разработки, то с помощью параметра-compiler можно перечислить имена используемых embedded-компиляторов:
-C [COMPILER_NAME...], --compiler [COMPILER_NAME...]
Filter compiler commands by compiler name
После установки анализатора в Windows будет доступен большой набор различных утилит, предназначенных для разных режимов работы анализатора.
Консольный режим
Анализ проекта можно автоматизировать с помощью последовательного запуска следующих команд утилиты CLMonitor:
"C:\Program Files (x86)\PVS-Studio\CLMonitor.exe" monitor
<build command for your project>
"C:\Program Files (x86)\PVS-Studio\CLMonitor.exe" analyze ... -l report.plog ...
Примечание. Команда monitor запускает процесс в неблокирующем режиме.
Графический режим
В утилите Сompiler Monitoring UI необходимо перейти в режим мониторинга сборки в меню Tools > Analyze Your Files (C/C++) или щёлкнув по значку "око" на панели инструментов:
Перед запуском мониторинга сборки будет доступно следующее меню для дополнительной настройки анализа:
После запуска мониторинга следует выполнить сборку проекта в IDE или с помощью сборочных скриптов, после чего в следующем окне нажать Stop Monitoring:
Результаты анализа будут доступны в утилите Сompiler Monitoring UI после анализа файлов, участвующих в компиляции.
Примечание: Используемый по умолчанию метод отслеживания запусков компиляторов может не успеть определить все файлы исходного кода. Эта проблема особенно актуальна для Embedded проектов, поскольку они состоят из быстро компилирующихся файлов на языке C. Чтобы отследить все компилируемые файлы, читайте раздел Wrap Compilers документации утилиты мониторинга.
В отчёте анализатора могут встречаться подобные предупреждения:
V001: A code fragment from 'source.cpp' cannot be analyzed.
Разработчики компиляторов для встраиваемых систем часто отходят от стандартов и добавляют в компилятор нестандартные расширения. В сфере микроконтроллеров это особенно распространено и не является чем-то необычным для разработчиков.
Но для анализатора кода это нестандартный C или C++ код, который требует дополнительной поддержки. Если на вашем коде возникают такие предупреждения, пришлите нам архив с препроцессированными *.i файлами, полученными из проблемных исходников, и мы добавим поддержку новых расширений компилятора.
Включить режим сохранения таких файлов во время анализа можно следующими способами:
Рынок пакетов разработки для встраиваемых систем очень обширен, поэтому, если вы не нашли свой компилятор в списке поддерживаемых, сообщите нам через форму обратной связи о своём желании попробовать PVS-Studio, подробно описав используемые инструменты разработки.
Для повышения качества кода или безопасности устройств в сфере разработки для встраиваемых систем часто следуют различным стандартам кодирования, например SEI CERT Coding Standard и MISRA, а также стараются избегать появления потенциальных уязвимостей, руководствуясь списком Common Weakness Enumeration (CWE). Проверка соответствия кода таким критериям присутствует и в PVS-Studio.
Для проверки embedded проектов с помощью PVS-Studio можно использовать мультиплатформенную среду разработки PlatformIO. Она берёт на себя работу по поддержке инструментов сборки и отладки, а также по управлению библиотеками. Запускается под основными операционными системами, такими как Windows, macOS и Linux.
Для включения анализа с помощью PVS-Studio нужно добавить в конфигурационный файл проекта (platformio.ini):
check_tool = pvs-studio
check_flags = pvs-studio: --analysis-mode=4
После чего вызвать в терминале команду:
pio check
Более подробно о поддержке статического анализа в PlatformIO можно узнать на сайте проекта, а также на странице, посвящённой конфигурации PVS-Studio.
В этом документе собраны особенности запуска анализатора и проверки проектов для встраиваемых систем. В остальном запуск анализатора и его настройка производятся так же, как и для других типов проектов. Перед началом использования анализатора рекомендуется ознакомиться со следующими страницами документации:
В данном разделе описаны различные способы проверки кода на соответствие OWASP ASVS с помощью PVS-Studio.
Все существующие диагностические правила описаны на странице "Сообщения PVS-Studio". Если вас интересуют именно правила из группы OWASP, то вам также могут быть полезны соответствия предупреждений PVS-Studio согласно:
Как было отмечено выше, правила, связанные с OWASP, по умолчанию отключены. Далее представлены различные возможности их включения и настройки.
Если вы запускаете анализ PVS-Studio при помощи плагина для Visual Studio или какой-либо другой IDE, то активировать правила из группы OWASP можно через графический интерфейс.
Для этого необходимо перейти в настройки анализатора. К примеру, в Visual Studio 2019 нужно перейти в Extensions -> PVS-Studio -> Options:
Для других IDE способы их открытия могут немного отличаться. Более подробную информацию, включая информацию о способах открытия настроек в других IDE, можно найти в разделе "Знакомство со статическим анализатором кода PVS-Studio на Windows".
В настройках необходимо перейти на вкладку "Detectable Errors" для интересующего вас языка. Здесь нужно изменить значение для группы OWASP:
По умолчанию для группы OWASP выбрано значение 'Disabled'. Оно приводит к полному отключению соответствующих диагностик для данного языка. Чтобы включить их, нужно выбрать 'Custom', 'Show All' или 'Hide All' в зависимости от ваших целей. Подробнее об этих и других настройках можно прочитать в документации. Например, описание вкладки "Detectable Errors" представлено в разделе "Настройки: Detectable Errors".
Если было выбрано значение 'Show All', то все правила из группы будут включены и соответствующие предупреждения будут отображаться в окне вывода PVS-Studio. При этом в верхней части окна появится кнопка, отвечающая за отображение или скрытие предупреждений из соответствующей группы:
Здесь можно обратить внимание, что для предупреждений из группы OWASP указан соответствующий SAST-идентификатор. Кроме того, почти для всех предупреждений указывается подходящий им CWE. Подробно о способах настройки окна вывода предупреждений можно прочесть в разделе "Работа со списком диагностических сообщений в Visual Studio". Работа с окном в других IDE производится схожим образом.
Если анализ запускается через интерфейс командной строки, то скорее всего файл настроек будет нужно отредактировать вручную (то есть не через графический интерфейс).
В некоторых случаях настройка анализатора для разных языков отличается; также может отличаться работа под разными ОС. Ниже представлены краткие описания способов включения OWASP-правил в различных ситуациях.
Главным образом настройка C#-анализатора под любой ОС состоит в редактировании файла Settings.xml. Стоит заметить, что это тот же самый файл, который используется плагинами для Visual Studio, CLion и Rider.
По умолчанию файл лежит по пути:
Также путь к настройкам может быть передан в качестве аргумента командной строки при запуске анализа. Подробнее об этом можно прочитать в разделе "Проверка проектов Visual Studio/MSBuild/.NET из командной строки с помощью PVS-Studio".
Настройки анализатора хранятся в формате XML. Для включения правил из группы OWASP необходимо обратить внимание на следующие узлы:
Соответственно, для включения проверки соответствия кода стандарту OWASP необходимо записать в 'DisableOWASPAnalysis' и 'DisableOWASPAnalysisCs' соответствующие значения, а также убедиться, что интересующие вас правила не записаны в узле 'DisableDetectableErrors'.
Windows
Для настройки C++ анализатора на Windows также может быть использован файл Settings.xml (описание выше). Это возможно, если для анализа используется:
Для включения правил из группы OWASP необходимо установить подходящие значения для узлов 'DisableOWASPAnalysis' (аналог 'DisableOWASPAnalysisCs' для C++) и 'DisableDetectableErrors'.
Иногда может возникать необходимость в запуске C++ анализатора напрямую (через PVS-Studio.exe). Задание настроек в таком случае будет производиться с помощью файла '*.cfg'. Путь к нему должен передаваться в качестве значения параметра '‑‑cfg':
PVS-Studio.exe --cfg "myConfig.cfg"
Подробно про запуск анализа таким образом можно прочитать в разделе "Прямая интеграция анализатора в системы автоматизации сборки (C/C++)". Для включения же правил группы OWASP в конфигурационном файле необходимо проверить значение параметра 'analysis-mode'. Оно позволяет управлять включёнными группами диагностик. Оно должно быть суммой чисел, соответствующих включённым группам. К примеру, '4' соответствует анализу общего назначения, а '128' – анализу с использованием правил группы OWASP. Для того, чтобы использовать только эти 2 группы, в 'analysis-mode' нужно передать '132'. Если данный параметр не задан (или значение 0), то при анализе будут использоваться все доступные группы, в том числе и OWASP.
Кроме того, аналогичный параметр можно задавать в качестве аргумента командной строки:
PVS-Studio.exe --analysis-mode 132 ....
Linux
Задание настроек под Linux производится примерно так же, как и при использовании 'PVS-Studio.exe'. Анализ производится с помощью команды 'pvs-studio-analyzer analyze'. Среди её параметров стоит отметить 'analysis-mode' и 'cfg'.
'‑‑analysis-mode' (или '-a') устанавливает группы предупреждений, которые будут включены при анализе. Для включения правил из группы OWASP необходимо, чтобы в списке групп, разделяемых ';', присутствовало значение 'OWASP'. Например:
pvs-studio-analyzer analyze .... -a GA;MISRA;OWASP
'‑‑cfg' (или '-c') позволяет задать путь к специальному файлу конфигурации '*.cfg', который аналогичен тому, что используется для работы 'PVS-Studio.exe' (описание приведено чуть выше).
Подробную информацию можно найти в разделе "Как запустить PVS-Studio в Linux и macOS".
Работе с Java анализатором посвящён раздел "Работа с ядром Java анализатора из командной строки". В нём представлена вся информация, необходимая для запуска анализа, включения диагностик группы OWASP и т. д. Ниже записаны краткие сведения о настройках, которые нужно задать для активации OWASP.
В зависимости от того, каким именно образом вы производите анализ проектов, меняется и способ включения правил группы OWASP.
К примеру, если вы используете плагин для Maven ('pvsstudio-maven-plugin'), то в его конфигурации (в файле 'pom.xml') необходимо добавить в узел <analysisMode> значение 'OWASP', а также проверить значения в узлах <enabledWarnings> и <disabledWarnings>. Пример:
<build>
<plugins>
<plugin>
<groupId>com.pvsstudio</groupId>
<artifactId>pvsstudio-maven-plugin</artifactId>
<version>7.14.50353</version>
<configuration>
<analyzer>
<outputType>text</outputType>
<outputFile>path/to/output.txt</outputFile>
<analysisMode>GA,OWASP</analysisMode>
</analyzer>
</configuration>
</plugin>
</plugins>
</build>
Если для анализа используется плагин для Gradle, необходимо открыть файл 'build.gradle' и отредактировать в блоке 'pvsstudio' значение 'analysisMode', а также проверить 'enabledWarnings' и 'disabledWarnings'. Пример:
apply plugin: com.pvsstudio.PvsStudioGradlePlugin
pvsstudio {
outputType = 'text'
outputFile = 'path/to/output.txt'
analysisMode = ["GA", "OWASP"]
}
Для обоих плагинов существует и возможность конфигурирования через командную строку.
После включения правил из группы OWASP может оказаться, что срабатывания некоторых из них на вашем проекте неактуальны. Например, V5606 отвечает за проверку соответствия кода позиции 7.4.2 из OWASP ASVS:
Verify that exception handling (or a functional equivalent) is used across the codebase to account for expected and unexpected error conditions.
В соответствии с указанным требованием, правило V5606 генерирует сообщение в случае, если обнаруживает пустой блок catch или finally.
Вполне возможно, что в вашем проекте наличие пустых блоков обработки исключений является приемлемым. В этом случае сообщения V5606 вам совершенно не интересны и для удобства их стоит убрать из вывода анализатора, использовав один из описанных далее способов.
Одним из вариантов в описанной ситуации является скрытие сообщений некоторых правил с помощью общих настроек анализатора. Для этого можно либо вручную отредактировать настройки (этот момент описан выше), либо сделать это с помощью плагина для IDE:
Стоит отметить, что при использовании настроек в плагине для IDE или редактировании 'Settings.xml' отдельные диагностики нельзя по-настоящему отключить (только группы целиком). Вместо этого генерируемые сообщения просто будут скрыты при просмотре лога в IDE. В некоторых случаях такое поведение может быть удобным, однако в других логичнее будет полностью отключить диагностику.
Кроме того, бывают ситуации, когда необходимо отключить срабатывания определённых уровней.
К примеру, некоторые правила, реализующие taint-анализ, считают параметры публично доступных методов источниками потенциально заражённых данных. Пояснение такого поведения можно найти в заметке "Почему важно проверять значения параметров общедоступных методов". Предупреждение, в котором источником заражения является параметр метода, выдаётся с низким уровнем.
Вполне возможно, что для вашего проекта предупреждения о необходимости валидации параметров неактуальны, однако полностью отключать правило или скрывать его предупреждения тоже не хочется. Было бы куда удобнее отключить предупреждения с конкретным уровнем.
Такую возможность, а также полное отключение диагностики, переопределение уровня и т.д. предоставляют файлы pvsconfig.
Файлы с расширением pvsconfig позволяют обеспечить дополнительную настройку анализа. На данный момент они могут быть использованы при анализе проектов на C++ и C#.
На странице "Подавление ложных предупреждений" описаны как возможности отключения диагностик с помощью pvsconfig, так и другие способы исключения нежелательных срабатываний. Прочие возможности, такие как изменение уровней предупреждений или замена подстрок в сообщениях, описаны в разделе "Дополнительная настройка диагностик".
В общем для использования функционала достаточно просто добавить файл с расширением pvsconfig в проект или решение.
Удобнее всего pvsconfig создавать через IDE, используя специальный шаблон:
В этом случае созданный файл будет заполнен различными закомментированными примерами, что серьёзно упростит написание собственных инструкций:
# Example of PVS-Studio rules configuration file.
# Full documentation is available at
# https://pvs-studio.com/en/docs/manual/full/
# https://pvs-studio.com/en/docs/manual/0040/
#
# Filtering out messages by specifying a fragment from source code:
# //-V:textFromSourceCode:3001,3002,3003
#
# Turning off specific analyzer rules:
# //-V::3021,3022
#
# Changing in analyzer's output message:
# //+V3022:RENAME:{oldText0:newText0},{oldText1:newText1}
#
# Appends message to analyzer's message:
# //+V3023:ADD:{Message}
#
# Excluding directories from the analysis:
# //V_EXCLUDE_PATH \thirdParty\
# //V_EXCLUDE_PATH C:\TheBestProject\thirdParty
# //V_EXCLUDE_PATH *\UE4\Engine\*
#
# Redefining levels:
# //V_LEVEL_1::501,502
# //V_LEVEL_2::522,783,579
# //V_LEVEL_3::773
#
# Disabling groups of diagnostics:
# //-V::GA
# //-V::GA,OWASP
#
# Disabling messages with specified warning levels:
# //-V::3002:3
# //-V::3002,3008:3
# //-V::3002,3008:2,3
#
# Rule filters should be written without '#' character.
Несложно заметить, что pvsconfig предоставляет достаточно большое количество различных способов настройки анализа – от изменения уровня до изменения самих выводимых сообщений или полного отключения диагностик. Далее представлены примеры использования некоторых возможностей pvsconfig.
Примечание. Некоторые функции могут быть доступны только для конкретных анализаторов – подробнее об этом в разделах "Подавление ложных предупреждений" и "Дополнительная настройка диагностик".
Для отключения, к примеру, правила V5606 (о пустых catch и finally), необходимо добавить в файл следующую строку:
//-V::5606
Также можно и отключить выдачу диагностикой предупреждений конкретного уровня. Ранее мы рассматривали пример с taint-диагностиками, выдающими предупреждения на 3 уровне, если источником заражения является параметр. Если вам нужно, к примеру, убрать предупреждения о потенциальных SQL-инъекциях, где источником заражения будет являться параметр, то в pvsconfig нужно добавить строку
//-V::5608:3
В результате предупреждения 3 уровня диагностики V5608 будут исключены из результатов анализа.
Ещё одной удобной возможностью pvsconfig является переопределение уровня выдаваемых диагностикой предупреждений. К примеру, можно переопределить уровень всех срабатываний V5609:
//V_LEVEL_2::5609
Данная строка проинструктирует анализатор формировать предупреждения правила V5609 со 2 (средним) уровнем достоверности.
Анализатор PVS-Studio можно использовать при работе в средах разработки IntelliJ IDEA и Android Studio. Плагин PVS-Studio для этих IDE предоставляет удобный графический интерфейс для запуска анализа проектов и отдельных файлов, а также для работы с предупреждениями анализатора.
Плагины PVS-Studio для IntelliJ IDEA и Android Studio можно установить из официального репозитория плагинов JetBrains или из репозитория на нашем сайте. Ещё один способ установки — через установщик PVS-Studio для Windows, доступный на странице загрузки.
Для установки плагина PVS-Studio из официального репозитория JetBrains нужно открыть окно настроек с помощью команды 'File -> Settings -> Plugins', выбрать в окне вкладку 'Marketplace', и ввести в строке поиска "PVS-Studio". В результатах поиска появится плагин PVS-Studio:
Далее нужно нажать кнопку 'Install' напротив найденного плагина PVS-Studio. После того как установка плагина будет завершена, нужно нажать кнопку 'Restart IDE':
Перезапустив среду разработки, можно начать пользоваться плагином PVS-Studio для проверки кода.
Примечание: нет необходимости загружать и устанавливать отдельно ядро Java анализатора — плагин сам загрузит и установит необходимую версию ядра при запуске анализа в IntelliJ IDEA или Android Studio.
Помимо официального репозитория JetBrains, плагин PVS-Studio также доступен из собственного репозитория PVS-Studio. Для установки плагина из репозитория PVS-Studio сначала нужно добавить репозиторий в IDE. Для этого нужно открыть окно установки плагинов с помощью команды меню 'File -> Settings -> Plugins', нажать на шестеренку в правом верхнем углу и в выпадающем списке выбрать 'Manage Plugin Repositories':
В открывшемся окне добавьте путь: https://files.pvs-studio.com/java/pvsstudio-idea-plugins/updatePlugins.xml. И нажмите 'ОК':
Последний шаг установки аналогичен установке плагина из официального репозитория — нужно открыть вкладку 'Marketplace', где в поиск ввести "PVS-Studio". После применения данного фильтра выбрать плагин 'PVS-Studio for IDEA and Android Studio', нажать 'Install' и перезапустить среду разработки.
Панель настроек плагина состоит из нескольких вкладок. Рассмотрим каждую из них подробнее.
Misc — настройки ядра анализатора PVS-Studio. При наведении курсора мыши на название настройки появляется подсказка с описанием того, для чего эта настройка предназначена.
Warnings — список типов всех предупреждений, поддерживаемых анализатором. Если убрать галочку у предупреждения, то все предупреждения данного типа в таблице вывода результатов работы анализатора будут отфильтрованы, а при следующем запуске анализа соответствующее правило анализатора не будет запущено (не выдаст никаких предупреждений):
Excludes – содержит пути до файлов или директорий, которые будут исключены из анализа. Вы можете указать как абсолютный путь, так и путь относительно корневой директории проекта:
API Compatibility Issue Detection – настройки для диагностики V6078 (по умолчанию отключена). Данная диагностика позволяет узнать, изменится ли или вообще исчезнет API JDK, которое вы используете в проекте, в одной из следующих версий JDK:
Environment – настройки, которые будут использованы плагином для запуска ядра Java анализатора (отдельного JVM процесса pvs-studio.jar). Стандартные значения этих настроек берутся из файла global.json. Если вы измените настройки на этой вкладке, они сохранятся в отдельный файл, переопределяющий настройки из global.json при запуске анализа. Сами измененные настройки никак не влияют на содержимое global.json файла:
Registration — отображает информацию о действующей в системе лицензии PVS-Studio. Также на этой вкладке вы можете изменить эти данные и убедиться, что введенная лицензия валидна.
По умолчанию анализатор запускает ядро с java из переменной окружения PATH. Если вам необходимо запустить анализ с какой-то другой версией, ее можно указать вручную. Для этого на вкладке 'Environment' в настройках плагина необходимо в поле 'Java executable' указать путь до java файла из JDK. Версия этой JDK (версия языка Java) будет использована при анализе исходного кода проекта:
Перед запуском анализа вам необходимо ввести лицензию. Как это сделать, можно узнать в этой документации.
Имеется возможность анализировать:
Обратите внимание: при анализе проекта плагин запускает ядро Java анализатора. При запуске ядра анализатора будет использована версия языка Java, соответствующая версии JDK, из которого используется файл java для запуска ядра Java анализатора (поле Java executable на вкладке Environment в настройках плагина). Если хотите изменить версию языка Java, которая будет использована при анализе, то используйте для запуска ядра Java анализатора файл java из JDK соответствующей версии.
Для анализа текущего проекта можно воспользоваться пунктом меню 'Tools -> PVS-Studio -> Check Project':
Для анализа файла, открытого на редактирование, можно использовать:
Также можно выбрать несколько элементов в окне 'Explorer' через CTRL/SHIFT + левая кнопка мыши, после чего нажать правую кнопку мыши и выбрать 'Analyze with PVS-Studio':
В примерах, приведенных выше, будут проанализированы:
Во время анализа результаты работы анализатора выводятся в таблицу окна 'PVS-Studio':
Таблица состоит из 7 столбцов (слева направо: 'Favorite', 'Code', 'CWE', 'SAST', 'Message', 'Position', 'False Alarms'). Имеется возможность сортировать сообщения в таблице по любому столбцу. Для изменения порядка сортировки необходимо кликнуть на заголовок столбца. Крайний левый столбец ('Favorite') используется для пометки предупреждений, которая позволяет быстро найти все помеченные предупреждения, включив сортировку по столбцу 'Favorite':
При нажатии на строку в столбцах 'Code'/'CWE' будет открыта страница в браузере с подробным описанием предупреждения или потенциальной уязвимости. В столбце 'SAST' указывается идентификатор уязвимости кода по стандарту OWASP ASVS, для предупреждения. В столбце 'Message' содержится краткое описание предупреждения. Столбец 'Position' содержит список файлов, связанных с сообщением. Крайний правый столбец 'False Alarms' — служит для отображения сообщений, помеченных, как ложные срабатывания. Подробнее про работу с ложными срабатываниями будет описано дальше, в соответствующем подразделе.
Чтобы открыть файл на строке, в которой было найдено предупреждение анализатора, дважды нажмите левой кнопкой мыши на предупреждение в таблице:
Также над таблицей имеются стрелки, позволяющие переключаться между предыдущим/следующим сообщением анализатора, и открывать файл, на который это предупреждение выдано, в редакторе кода. Над таблицей имеется несколько фильтров по уровню опасности предупреждений: High, Medium, Low и Fails (ошибки анализатора):
При нажатии на лупу откроется дополнительная панель с полями ввода для столбцов 'Code', 'CWE', 'SAST', 'Message' и 'Position'. Каждое поле – это строковый фильтр для столбца, позволяющий отфильтровать сообщения из таблицы по введённому в эти поля тексту:
В левом верхнем углу, над таблицей, расположена кнопка с тремя горизонтальными полосками. При нажатии на эту кнопку откроется дополнительная панель настроек:
При нажатии на шестеренку открывается главное окно настроек плагина, также доступное через команду меню 'Tools -> PVS-Studio -> Settings'.
Если Вы только начали изучать инструмент статического анализа и хотели бы узнать на что он способен, то можете воспользоваться механизмом Best Warnings. Данный механизм покажет вам наиболее важные и достоверные предупреждения.
Чтобы посмотреть наиболее интересные предупреждения с точки зрения анализатора, нажмите на кнопку 'Best', как показано на скриншоте ниже:
После чего в таблице с результатами анализа останутся максимум десять наиболее критичных предупреждений анализатора.
Бывают ситуации, когда сообщение анализатора указывает на код, но программисту совершенно очевидно, что в этом коде нет ошибки. Такая ситуация называется ложным срабатыванием (false positive).
В плагине PVS-Studio имеется возможность пометить сообщение анализатора как ложное срабатывание. Такая пометка позволяет скрывать эти сообщения анализатора при последующем анализе кода.
Для разметки ложных срабатываний необходимо выбрать одно или несколько сообщений анализатора в таблице 'PVS-Studio', нажать правой кнопкой мыши на любой строке в таблице и в контекстном меню выбрать пункт 'Mark selected messages as False Alarms':
После выполнения данной команды анализатор добавит к строке, на которую выдаётся предупреждение анализатора, комментарий специального вида: \\-Vxxxx, где xxxx – это номер диагностического правила PVS-Studio. Такой комментарий также можно добавить в код вручную.
Помеченные ранее ложные срабатывания можно показать в таблице окна PVS-Studio с помощью настройки 'Show False Alarms', доступной через команду меню 'Tools -> PVS-Studio -> Settings':
С помощью команды контекстного меню 'Remove False Alarm marks from selected messages' можно удалить отметку ложного срабатывания с выбранных сообщений.
Подробную информацию о подавлении предупреждений, выдаваемых анализатором PVS-Studio, а также сведения о других способах подавления сообщений анализатора, можно найти в разделе документации Подавление ложных предупреждений.
Часто начать регулярно использовать статический анализ мешают многочисленные срабатывания на legacy-коде. Такой код обычно уже хорошо оттестирован и стабильно работает, поэтому править в нём все срабатывания анализатора может оказаться нецелесообразно. Тем более, если размер кодовой базы достаточно велик, такая правка может потребовать большого времени. При этом, такие сообщения на существующий код мешают смотреть сообщения на новый код, находящийся в разработке.
Чтобы решить данную проблему и начать сразу регулярно использовать статический анализ, PVS-Studio предлагает возможность "отключить" сообщения на старом коде. Чтобы подавить сообщения анализатора на старом коде можно воспользоваться кнопкой 'Suppress All Messages' на панели окна PVS-Studio. Механизм подавления работает с помощью специального suppress-файла. В него добавляются подавленные сообщения анализатора после выполнения команды 'Suppress All Messages'. При последующем запуске анализа все сообщения, добавленные в suppress-файл, не попадут в отчёт анализатора. Система подавления через suppress-файл достаточно гибкая и способна "отслеживать" подавленные сообщения даже при модификации и сдвигах участков кода, в которых выдаётся подавленное сообщение.
В IDEA подавленные сообщения добавляются в suppress-файл suppress_base.json, который записывается в директорию .PVS-Studio, в корневой директории открытого в IDEA проекта. Чтобы вернуть все сообщения в вывод анализатора, необходимо удалить этот файл и перезапустить анализ.
Более подробное описание подавления предупреждений анализатора, и описание работы с suppress-файлами, можно прочитать в разделе документации Массовое подавление сообщений анализатора.
Также предлагаем познакомиться со статьей: "Как внедрить статический анализатор кода в legacy проект и не демотивировать команду".
При нажатии правой кнопкой мыши, на строке с сообщением анализатора в таблице окна PVS-Studio откроется контекстное меню, содержащее дополнительные команды для выбранных сообщений анализатора.
Команда 'Copy Selected Messages To Clipboard' копирует все выбранные в окне с результатами анализа плагина PVS-Studio предупреждения в буфер обмена.
Команда 'Mark Selected Messages As Important' позволяет пометить звёздочкой предупреждение, чтобы в дальнейшем легко его найти при сортировке по столбцу 'Favorite' (крайний левый столбец).
Команда 'Mark selected messages as False Alarms / Remove false alarm masks' позволяет разметить сообщение анализатора, как ложное срабатывание, добавив в код, на который выдано предупреждение, комментарий специального вида.
Команда 'Add Selected Messages To Suppression File' позволяет подавить предупреждения анализатора, выбранные в окне с результатами анализа плагина PVS-Studio. При следующих запусках анализа эти предупреждения не будут добавлены в окно с результатами анализа PVS-Studio.
Пункт меню 'Show Columns' открывает список с именами столбцов, которые можно скрыть или отобразить.
Команда 'Exclude from analysis' позволяет добавить путь или часть пути к файлу, в котором найдено предупреждение анализатора, в список исключённых из анализа директорий. Все файлы, пути до которых попадут под данный фильтр, будут исключены из анализа.
Для сохранения или загрузки результатов работы анализатора можно воспользоваться командами главного меню, доступными через 'Tools -> PVS-Studio':
Команда 'Open Report' открывает .json-файл отчёта и загружает его содержимое в таблицу окна 'PVS-Studio'.
Подменю 'Recent Reports' показывает список из нескольких последних открытых файлов отчетов. При нажатии на элемент в списке будет открыт соответствующий отчет (если отчет еще существует по такому пути), и его содержимое будет загружено в таблицу окна 'PVS-Studio'.
Команда 'Save Report' сохраняет все сообщения из таблицы (даже отфильтрованные) в .json-файл отчёта. Если текущий результат анализа ещё ни разу не сохранялся, то будет предложено задать имя и место для сохранения отчёта.
Аналогично, команда 'Save Report As' сохраняет все предупреждения из таблицы (даже отфильтрованные) в .json-файл отчёта, всегда предлагая выбрать место сохранения отчёта на диске.
Команда 'Export Report To...' позволяет сохранить отчёт анализатора в разных форматах (xml, txt, tasks, pvslog, log, html, err). Каждый формат может быть полезен в различных ситуациях и утилитах.
Команда 'Export Report To HTML...' позволяет сохранить отчёт анализатора в выбранную папку в формате HTML. Этот формат позволяет прямо в браузере просматривать предупреждения анализатора и проводить навигацию по файлам исходного кода в браузере. В выбранной папке будет создана папка с именем 'fullhtml', содержащая файл отчёта анализатора (index.html).
Внимание. Вместо использования команды 'Export Report To HTML...' более предпочтительным сценарием является использование консольных утилит PlogConverter (Windows) и plog-converter (Linux и macOS). Эти утилиты позволяют конвертировать отчёт анализатора в большее количество форматов (например, SARIF), а также предоставляют дополнительные возможности: фильтрация предупреждений из отчёта, преобразование путей в отчёте с абсолютных на относительные и наоборот, получение разницы между отчётами и др.;
Плагины PVS-Studio для IntelliJ IDEA и Android Studio добавляют окно просмотра результатов анализа PVS-Studio, которое имеет контекстное меню, появляющееся при нажатии правой кнопкой мыши в окне с результатами анализа:
Некоторым действиям из этого меню назначены горячие клавиши, что позволяет выполнять их без использования мыши.
Кнопки со стрелками, предназначенные для навигации по сообщениям анализатора, также имеют горячие клавиши:
Использование горячих клавиш полезно, потому что позволяет ускорить процесс обработки результатов анализа. Их можно назначать/переопределять в настройках: 'File -> Settings -> Keymap'. Чтобы быстрее их найти, введите 'PVS-Studio' в поле поиска окна 'Keymap'.
В случае наличия обновления PVS-Studio сообщение об этом выведется в отчёт по завершению анализа.
Для обновления плагина нужно открыть окно настроек плагинов 'File -> Settings -> Plugins', найти в списке Installed плагин PVS-Studio for IDEA and Android Studio, нажать 'Update' и перезапустить среду разработки.
При использовании прокси вам необходимо загрузить при помощи прокси ZIP архив для Java на странице загрузки. В этом архиве содержится ядро Java анализатора (папка с именем 7.34.87460 в директории pvs-studio-java). Ядро Java анализатора надо распаковать по стандартному пути установки ядра Java анализатора:
В результате в папке по стандартному пути установки ядра Java анализатора должна находиться папка с именем версии ядра Java анализатора, скопированная из архива.
Статический анализатор кода PVS-Studio Java состоит из 2-х основных частей: ядра, выполняющего анализ, и плагинов для интеграции в сборочные системы (Maven и Gradle) и IDE (PVS-Studio для IntelliJ IDEA и Android Studio).
Функции плагинов:
Для интеграции плагина необходимо добавить следующий код в скрипт сборки build.gradle:
buildscript {
repositories {
mavenCentral()
maven {
url uri('https://files.pvs-studio.com/java/pvsstudio-maven-repository/')
}
}
dependencies {
classpath 'com.pvsstudio:pvsstudio-gradle-plugin:latest.release'
}
}
apply plugin: com.pvsstudio.PvsStudioGradlePlugin
pvsstudio {
outputType = 'text'
outputFile = 'path/to/output.txt'
analysisMode = ['GA', 'OWASP']
}
Перед первым запуском анализа вам необходимо будет ввести лицензию. Как это сделать — можно узнать в этой документации.
Для запуска анализа выполните команду:
./gradlew pvsAnalyze
Обратите внимание: при анализе проекта плагин запускает ядро Java анализатора. При запуске ядра анализатора будет использована версия языка Java, соответствующая версии JDK, из которого используется файл java для запуска ядра Java анализатора (настройка плагина javaPath). Если хотите изменить версию языка Java, которая будет использована при анализе, то используйте для запуска ядра Java анализатора файл java из JDK соответствующей версии.
Для работы плагина нужно скачать его зависимости. Если вам нужно работать с плагином в системе без доступа в сеть, вы должны создать офлайн репозиторий c зависимостями плагина.
Для загрузки зависимостей и подготовки к офлайн использованию примените такую команду:
./gradlew build --refresh-dependencies
Запускать эту команду нужно из директории с файлом build.gradle (корневой директории проекта). В этом случае все необходимые зависимости для сборки и анализа проекта будут сохранены в стандартной папке локального репозитория: %userprofile%/.gradle/caches/modules-2/files-2.1 на Windows или ~/.gradle/caches/modules-2/files-2.1 на Linux/macOS.
Вы должны иметь подключение к сети во время запуска этой команды, чтобы зависимости могли загрузиться. Для последующей работы интернет-соединение уже не нужно.
В системе должно быть установлено ядро Java той же версии, что и плагин. Узнать, как установить ядро Java анализатора, можно в отдельной документации.
Использование анализатора в таком случае не отличается от обычного. Для того чтобы gradle не начинал скачивать зависимости, используйте флаг ‑‑offline. Команда запуска анализа в офлайн режиме:
./gradlew pvsAnalyze –-offline
Ниже представлен список настроек анализатора, которые могут быть указаны в блоке pvsstudio файла build.gradle:
Настройки могут быть переданы через командную строку в момент запуска анализа. Формат определения:
-Ppvsstudio.<nameSingleParam>=value
-Ppvsstudio.<nameMultipleParam>=value1;value2;value3
Пример:
./gradlew pvsAnalyze -Ppvsstudio.outputType=text
-Ppvsstudio.outputFile=path/to/output.txt
-Ppvsstudio.disabledWarnings=V6001;V6002;V6003
Обратите внимание, что параметры, явно переданные в командной строке, имеют наивысший приоритет.
По умолчанию анализатор запускает ядро с java из переменной окружения PATH. Если необходимо запустить анализ с другой версией, её можно указать вручную. Для этого в настройке анализатора javaPath укажите путь до java из JDK. Версия этой JDK (версия языка Java) будет использована при анализе исходного кода проекта:
....
javaPath = "C:/Program Files/Java/jdk19.0.5/bin/java"
....
Благодаря использованию latest.release в качестве версии плагина в файле build.gradle у вас всегда будет использоваться последняя версия PVS-Studio для анализа.
При использовании прокси необходимо указать логин и пароль для корректной загрузки ядра анализатора.
Это можно сделать через аргументы:
Команда для запуска анализа через плагин для Gradle с прокси:
./gradlew pvsAnalyze "-Dhttp.proxyUser=USER" "-Dhttp.proxyPassword=PASS"
Статический анализатор кода PVS-Studio Java состоит из 2-х основных частей: ядра, выполняющего анализ и плагинов для интеграции в сборочные системы (Maven и Gradle) и IDE (PVS-Studio для IntelliJ IDEA и Android Studio).
Функции плагинов:
Для интеграции плагина необходимо добавить следующий код в файл pom.xml:
<pluginRepositories>
<pluginRepository>
<id>pvsstudio-maven-repo</id>
<url>https://files.pvs-studio.com/java/pvsstudio-maven-repository/</url>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>com.pvsstudio</groupId>
<artifactId>pvsstudio-maven-plugin</artifactId>
<version>7.34.87460</version>
<configuration>
<analyzer>
<outputType>text</outputType>
<outputFile>path/to/output.txt</outputFile>
<analysisMode>GA,OWASP</analysisMode>
</analyzer>
</configuration>
</plugin>
</plugins>
</build>
Перед запуском анализа вам необходимо будет ввести лицензию. Как это сделать — можно узнать в этой документации.
Для запуска анализа выполните команду:
mvn pvsstudio:pvsAnalyze
Кроме того, анализ можно включить в цикл сборки проекта, добавив элемент <execution>:
<plugin>
<groupId>com.pvsstudio</groupId>
<artifactId>pvsstudio-maven-plugin</artifactId>
<version>7.34.87460</version>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>pvsAnalyze</goal>
</goals>
</execution>
</executions>
</plugin>
Обратите внимание: при анализе проекта плагин запускает ядро Java анализатора. При запуске ядра анализатора будет использована версия языка Java, соответствующая версии JDK, из которого используется файл java для запуска ядра Java анализатора (настройка плагина <javaPath>). Если хотите изменить версию языка Java, которая будет использована при анализе, то используйте для запуска ядра Java анализатора файл java из JDK соответствующей версии.
Для работы плагина нужно скачать его зависимости. Если вам нужно работать с плагином в системе без доступа в сеть, вы должны создать офлайн репозиторий c зависимостями плагина. Для этого вы можете использовать maven-dependency-plugin.
Для загрузки зависимостей и подготовки к офлайн использованию примените такую команду:
mvn dependency:go-offline
Запускать эту команду нужно из директории с файлом pom.xml (корневой директории проекта). В этом случае все необходимые зависимости для сборки и анализа проекта будут сохранены в стандартной папке локального репозитория: %userprofile%/.m2/repository на Windows или ~/.m2/repository на Linux/macOS.
Для сохранения офлайн репозитория в другую папку используйте параметр maven.repo.local. Команда в таком случае будет выглядеть так:
mvn dependency:go-offline -Dmaven.repo.local=/custom/path
Вы должны иметь подключение к сети во время запуска этой команды, чтобы зависимости могли загрузиться. Для последующей работы интернет-соединение уже не нужно.
В системе должно быть установлено ядро Java той же версии, что и плагин. Узнать, как установить ядро Java анализатора, можно в отдельной документации.
Использование анализатора в таком случае не отличается от обычного. Для того чтобы maven не начинал скачивать зависимости, используйте флаг ‑‑offline(-o). Пример команды запуска анализа в офлайн режиме с использованием пользовательского локального репозитория:
mvn -o pvsstudio:pvsAnalyze -Dmaven.repo.local=/custom/path
В блоке <analyzer> производится настройка анализатора. Ниже представлен список настроек анализатора.
Настройки могут быть переданы через командную строку в момент запуска анализа. Формат определения:
-Dpvsstudio.<nameSingleParam>=value
-Dpvsstudio.<nameMultipleParam>=value1;value2;value3
Пример:
mvn pvsstudio:pvsAnalyze -Dpvsstudio.outputType=text
-Dpvsstudio.outputFile=path/to/output.txt
-Dpvsstudio.disabledWarnings=V6001;V6002;V6003
Обратите внимание, что параметры, явно переданные в командной строке, имеют наивысший приоритет.
По умолчанию анализатор запускает ядро с java из переменной окружения PATH. Если необходимо запустить анализ с другой версией, её можно указать вручную. Для этого в настройке анализатора <javaPath> укажите путь до java из JDK. Версия этой JDK (версия языка Java) будет использована при анализе исходного кода проекта:
....
<javaPath>C:/Program Files/Java/jdk19.0.5/bin/java</javaPath>
....
Для обновления pvsstudio-maven-plugin необходимо изменить версию плагина в файле pom.xml.
При использовании прокси необходимо указать логин и пароль для корректной загрузки ядра анализатора.
Это можно сделать через аргументы:
Команда для запуска анализа через плагин для Maven с прокси:
mvn pvsstudio:pvsAnalyze "-Dhttp.proxyUser=USER" "-Dhttp.proxyPassword=PASS"
PVS-Studio - статический анализатор С, С++, C# и Java кода, предназначенный для облегчения задачи поиска и исправления различного рода ошибок. Анализатор можно использовать в Windows, Linux и macOS.
При работе с Windows анализатор интегрируется в Visual Studio в качестве плагина, предоставляя программисту удобный интерфейс для ориентации в коде и поиска проблемных мест. Также присутствует приложение C and C++ Compiler Monitoring UI (Standalone.exe), независимое от Visual Studio и позволяющее проверять файлы, компилируемые не только с помощью Visual C++, но и с использованием GCC (MinGW), Clang. Консольная утилита PVS-Studio_Cmd.exe позволит выполнять проверку MSBuild / Visual Studio проектов без запуска IDE или Compiler Monitoring UI, что позволит, например, использовать анализатор как часть процесса CI.
PVS-Studio для Linux представляет собой консольное приложение.
В данном документе описываются основы работы с PVS-Studio на операционных системах семейства Windows. Для получения информации о работе в среде Linux обратитесь к разделам документации "Установка и обновление PVS-Studio в Linux" и "Как запустить PVS-Studio в Linux и macOS".
Статический анализатор не заменяет другие инструменты поиска ошибок - он дополняет их. Внедрение такого инструмента позволяет устранять многие ошибки на этапе "зарождения", тем самым экономя время и ресурсы на их последующее устранение. Ведь чем раньше найдена ошибка, тем дешевле стоимость её исправления. Из этого следует ещё один вывод - статический анализатор необходимо использовать регулярно, именно в таком случае он проявит себя наиболее эффективно.
PVS-Studio разделяет все предупреждения по 3 уровням достоверности: High, Medium и Low. Также некоторые сообщения относятся к особой категории Fails. Рассмотрим эти уровни подробнее:
Стоит помнить, что конкретный код ошибки не обязательно привязывает её к определённому уровню достоверности, а распределение сообщений по уровням сильно зависит от контекста, в котором они были сгенерированы. При использовании плагина для Microsoft Visual Studio или в приложении C and C++ Compiler Monitoring UI окно вывода диагностических сообщений содержит кнопки уровней, позволяющие сортировать сообщения по мере необходимости.
Также имеется возможность упростить ознакомление и начальную работу с отчетом с помощью механизма отображения наиболее интересных предупреждений.
Анализатор содержит 5 видов диагностических правил:
Краткие обозначения групп диагностик (GA, OP, 64, CS, MISRA), наряду с номерами уровней достоверности предупреждений (1, 2, 3), используются для сокращенной формы записи, например, в параметрах командной строки. Пример: GA:1,2.
Выбор группы диагностических правил отображает или скрывает соответствующие сообщения.
С подробным списком диагностических правил вы можете ознакомиться в соответствующем разделе документации.
Сообщения анализатора можно группировать и фильтровать по различным критериям. Для получения более подробной информации о работе со списком предупреждений анализатора обратитесь к разделу документации "Работа со списком диагностических сообщений".
При установке PVS-Studio присутствует возможность выбора того, в какие среды Microsoft Visual Studio необходимо провести интеграцию.
После выбора необходимых пунктов и установки, PVS-Studio интегрируется IDE. На скриншоте ниже можно увидеть соответствующий пункт в меню Visual Studio, а также окно вывода диагностических сообщений.
В меню настроек есть возможность настроить PVS-Studio оптимальным образом для удобства работы. Например, присутствуют следующие возможности:
Скорее всего, для первого знакомства все эти настройки вам не понадобятся, но в будущем они позволят оптимизировать работу с PVS-Studio.
Подробное описание возможностей плагина для Visual Studio можно найти в разделе документации "Работа PVS-Studio в Visual Studio".
При установке анализатора присутствует возможность интеграции плагина PVS-Studio в IDE IntelliJ IDEA, что позволит проводить анализ и работать с отчётами анализатора непосредственно из IDE.
После установки плагин будет доступен в меню 'Tools' ('Tools' > 'PVS-Studio'). Ниже представлен скриншот IntelliJ IDEA с интегрированным плагином PVS-Studio.
В меню настроек, как и в случае с плагином для Visual Studio, также присутствуют возможности отключения диагностических правил, исключение файлов / директорий из анализа и пр.
Особенности работы Java анализатора, а также альтернативные способы установки (включая установку плагинов для Maven, Gradle) описаны в разделе документации "Работа с ядром Java анализатора из командной строки".
При установке анализатора присутствует возможность интеграции плагина PVS-Studio в IDE JetBrains Rider, что позволит проводить анализ и работать с отчётами анализатора непосредственно из IDE.
После установки плагин доступен в меню 'Tools'. Анализ текущего solution / проекта можно выполнить следующим образом: 'Tools' > 'PVS-Studio' > 'Check Current Solution/Project'.
Ниже представлен скриншот IDE JetBrains Rider с интегрированным плагином PVS-Studio.
Более подробно использование плагина PVS-Studio для IDE JetBrains Rider описано в разделе документации "Работа PVS-Studio в JetBrains Rider".
PVS-Studio может использоваться независимо от интегрированной среды разработки Microsoft Visual Studio. Compiler Monitoring UI позволяет проверять проекты в процессе их сборки. В этой версии также поддерживаются переходы по коду по диагностическим сообщениям, поиск фрагментов кода, определений макросов и типов данных. Более подробно работа с этим приложением описана в разделе документации "Просмотр результатов анализа в приложении C and C++ Compiler Monitoring UI".
PVS-Studio_Cmd.exe - модуль, позволяющий выполнять анализ решений Visual Studio (.sln), а также Visual C++ и Visual C# проектов (.vcxproj, .csproj) из командной строки. Это может быть полезно, например, в случае необходимости интеграции статического анализа на сборочном сервере. PVS-Studio_Cmd.exe позволяет проводить как полный анализ целевого проекта, так и инкрементальный (анализ файлов, изменившихся с момента последний сборки). Представление кода возврата работы утилиты в виде битовой маски позволяет получить подробную информацию о результатах анализа и идентифицировать проблемы, в случае их наличия. Таким образом, используя модуль PVS-Studio_Cmd.exe, можно достаточно 'тонко' настроить сценарий статического анализа кода и внедрить его в процесс CI. Более подробно использование модуля PVS-Studio_Cmd.exe описано в разделе документации "Проверка Visual C++ (.vcxproj) и Visual C# (.csproj) проектов из командной строки с помощью PVS-Studio".
PVS-Studio обладает обширной справочной системой диагностических сообщений. Эта база доступна как при работе с инструментом PVS-Studio, так и на официальном сайте. К различным диагностическим сообщениям прилагаются примеры кода, содержащего подобные ошибки, описание проблемы, а также возможные варианты исправления.
Чтобы изучить описание той или иной диагностики, достаточно кликнуть левой кнопкой мыши по номеру диагностики в окне вывода сообщений. Эти номера оформлены как гиперссылки.
Поддержка для PVS-Studio осуществляется посредством электронной почты. Так как с клиентами общаются непосредственно разработчики анализатора, это позволяет быстро получать ответы на самые разные вопросы.
Анализатор PVS-Studio интегрируется в среды разработки Microsoft Visual Studio 2022, 2019, 2017, 2015, 2013, 2012, 2010. Системные требования к анализатору можно найти в соответствующем разделе документации.
Получив установочный пакет PVS-Studio, можно приступить к установке программы.
После подтверждения лицензионного соглашения будет предоставлен выбор вариантов интеграции PVS-Studio в поддерживаемые среды разработки: Microsoft Visual Studio. Варианты интеграции, недоступные на текущей системе, будут затемнены. В случае, если на машине установлено несколько версий одной или разных IDE, возможна интеграция анализатора во все имеющиеся версии.
Для того чтобы удостовериться, что инструмент PVS-Studio корректно установлен, можно запустить IDE и открыть окно About (пункт меню Help). При этом анализатор PVS-Studio должен присутствовать в списке установленных компонентов.
При работе в IDE Visual Studio можно запускать различные варианты анализа - на решение, проект, файл, выбранные элементы и т.п. Например, запуск анализа решения выполняется следующим образом: "PVS-Studio -> Check -> Solution".
После запуска проверки на экране появится индикатор прогресса с кнопками Pause (приостановить анализ) и Stop (прервать анализ). Обнаруженные потенциально опасные конструкции во время анализа будут выводиться в окно найденных дефектов.
Термин "потенциально опасная конструкция" означает, что данную конкретную строку кода анализатор посчитал дефектом. Является ли эта строка реальным дефектом в приложении или нет - определить может только программист, основываясь на своем знании приложения. Этот принцип работы с анализаторами кода очень важно правильно понимать. Никакой инструмент не может полностью заменить программиста в работе по исправлению ошибок в программах. Только программист, основываясь на своих знаниях, может это сделать. Но инструмент может и должен помочь программисту в этом. Поэтому задача анализатора кода - это сократить количество мест в программе, которое должен просмотреть и разобрать программист.
Скорее всего, в больших реальных проектах диагностических сообщений будет не несколько десятков, а несколько сотен или даже тысяч. И просмотр всех подобных сообщений может быть непростой задачей. Для того чтобы облегчить ее, в анализаторе PVS-Studio имеются несколько механизмов, таких как фильтрация сообщений по коду ошибки, фильтрация по содержимому текста диагностического сообщения, фильтрация на основе путей к файлам и другие. Рассмотрим примеры использования систем фильтрации.
Предположим, что вы уверены, что диагностические сообщения с кодом V112 (использование магических чисел) никогда не являются реальными ошибками в вашем приложении. Тогда можно отключить показ этих диагностических сообщений с помощью настроек анализатора кода на вкладке "Detectable Errors (C++)".
После отключения предупреждения с определённым кодом соответствующие предупреждения будут отфильтрованы в окне вывода, перезапускать анализ не требуется. Если же включить обратно показ таких сообщений, то они вновь появятся в списке предупреждений.
Теперь рассмотрим другой вариант фильтрации на основе текста диагностических сообщений. Рассмотрим пример предупреждения анализатора и кода, на который оно было выдано:
obj.specialFunc(obj);
Предупреждение анализатора: V678 An object is used as an argument to its own method. Consider checking the first actual argument of the 'specialFunc' function.
Анализатор счёл подозрительным, что в качестве аргумента методу передаётся тот же объект, у которого этот метод и вызывается. Программисту, в отличии от анализатора, может быть известно о том, что такое использование данного метода вполне допустимо. Поэтому может возникнуть необходимость отфильтровать все подобные предупреждения. Сделать это можно, добавив соответствующий фильтр в настройках "Keyword Message Filtering".
После этого все диагностические сообщения, текст которых содержит указанное выражение, пропадут из списка без необходимости перезапускать анализатора кода. Вернуть их можно просто удалив выражение из фильтра.
Ещё одним механизмом сокращения количества диагностических сообщений является фильтрация по маскам имён файлов проекта и путям к ним.
Предположим, в вашем проекте используется библиотека Boost. Анализатор будет, конечно же, сообщать и о потенциальных проблемах в этой библиотеке. Скорее всего, вас эти предупреждения интересовать не будут, поэтому целесообразно отфильтровать их. Сделать это можно, просто добавив путь к папке с Boost на странице "Don't check files":
После этого диагностические сообщения, относящиеся к файлам в этой папке, не будут показаны.
Также в PVS-Studio имеется функция "Mark as False Alarm". Благодаря ей возможно пометить в исходном коде те строки, на которые анализатор выдал предупреждения, являющиеся ложными. После разметки анализатор более не будет выдавать диагностических сообщений на этот код.
Так в этом примере отключен вывод диагностического сообщения с кодом V640:
for (int i = 0; i < m; ++i)
for (int j = 0; j < n; ++j)
matrix[i][j] = Square(i) + 2*Square(j);
cout << "Matrix initialization." << endl; //-V640
....
Подробно этот механизм описана в разделе "Подавление ложных предупреждений".
Есть также ряд других способов повлиять на выводимые диагностические сообщения путем настроек анализатора кода, но в рамках данного документа они не рассматриваются. Рекомендуем обратиться к документации по настройкам анализатора кода.
Когда вы просмотрите все сообщения, которые выдал анализатор кода, то вы найдете как реальные ошибки в программах, так и конструкции, не являющиеся ошибочными. Дело в том, что анализатор не может на 100% точно определить все ошибки в программах без так называемых "ложных срабатываний". Только программист, зная и понимая программу, может определить есть в конкретном месте ошибка или нет. Анализатор кода же только существенно сокращает количество мест, которые необходимо просмотреть разработчику.
Таким образом, добиваться исправления всех потенциальных проблем, на которые указывает анализатор кода, смысла, конечно же, нет.
Механизмы подавления отдельных предупреждений и массового подавления сообщений анализатора описаны в разделах документации "Подавление ложных предупреждений" и "Массовое подавление сообщений анализатора" соответственно.
В данном документе описывается использование command line утилит для анализа MSBuild проектов (.vcxproj / .csproj) и решений Visual Studio (solutions).
В данном документе описывается использование command-line утилит. Использование плагинов для IDE Visual Studio и Rider описано в соответствующих разделах документации: "Знакомство со статическим анализатором кода PVS-Studio на Windows", "Работа PVS-Studio в JetBrains Rider".
Для анализа C# проектов может потребоваться установка дополнительных .NET SDK. Подробнее про это можно узнать в документации "Системные требования анализатора PVS-Studio".
Command line анализатор MSBuild / .NET Core проектов имеет разные имена на разных поддерживаемых анализатором платформах:
Описываемые ниже особенности актуальны для обеих утилит. Примеры с PVS-Studio_Cmd / pvs-studio-dotnet являются взаимозаменяемыми, если явно не указано обратное.
Примечание. Для анализа C++ проектов, не использующих сборочную систему MSBuild, на Windows следует воспользоваться системой мониторинга компиляции или прямой интеграцией анализатора в сборочную систему. Анализ C++ проектов на Linux / macOS подробно описан в этом разделе документации.
Command line утилиты по умолчанию распаковываются в следующие директории:
Команда '‑‑help' выведет все доступные аргументы анализатора:
PVS-Studio_Cmd.exe --help
Приведём пример запуска проверки решения "mysolution.sln":
PVS-Studio_Cmd.exe -t "mysolution.sln" -o "mylog.plog"
Рассмотрим основные аргументы анализатора:
У консольной утилиты также имеются дополнительные режимы работы:
В режиме "suppression" имеются дополнительные флаги, которых нет в основном режиме (или у них отличается название):
Command line версия анализатора PVS-Studio поддерживает все настройки по фильтрации\отключению сообщений, доступные в плагинах для Visual Studio и Rider. Вы можете как задать их вручную в xml файле, переданном с помощью аргумента '‑‑settings', так и использовать настройки, заданные через UI плагина, не передавая данного аргумента. Обратите внимание, что IDE плагин PVS-Studio использует отдельный набор настроек для каждого пользователя в системе.
Актуально только для PVS-Studio_Cmd. Если вы установили несколько экземпляров PVS-Studio различных версий для текущего пользователя системы, то все экземпляры программы будут использовать установочную директорию, указанную при последней установке. Во избежание конфликтов в работе анализатора в настройках, передаваемых с аргументом ‑‑settings (-s), необходимо указать путь до установочной директории (значение элемента <InstallDir>).
PVS-Studio_Cmd позволяет провести выборочную проверку отдельных файлов (например, только файлов, в которых были внесены изменения), заданных в списке, передаваемом с помощью флага '‑‑sourceFiles' (-f). Это позволяет значительно уменьшить время, необходимое для анализа, а также получить отчёт анализатора только для определённых изменений в исходном коде.
Список файлов представляет собой простой текстовый файл, в котором построчно указаны пути к проверяемым файлам. Относительные пути к файлам будут раскрыты относительно текущей рабочей директории. Можно указывать как исходные компилируемые файлы (c/cpp для C++ и cs для C#), так и заголовочные файлы (h/hpp для C++).
Для получения списка изменившихся файлов для флага '‑‑sourceFiles' (-f) можно использовать системы контроля версий (SVN, Git и тд.). Это позволит предоставлять анализатору актуальную информацию об изменениях в коде.
При использовании данного режима для анализа C и C++ файлов генерируется кэш зависимостей компиляции, который будет использован при последующих запусках анализа. По умолчанию кэши зависимостей сохраняются в специальной поддиректории '.pvs-studio', рядом с проектными файлами (.vcxproj). При необходимости можно изменить место их хранения при помощи флага '‑‑dependencyRoot' (-D). Также возможно использовать флаг ‑‑dependencyCacheSourcesRoot (-R), который позволяет создавать файлы кэшей зависимостей с относительными путями, что позволяет использовать один и тот же файл кэша зависимостей на разных системах.
Более подробную информацию о кэшах зависимостей вы можете найти в одноименном разделе документации для C++ проектов.
Для того, чтобы определить набор анализируемых файлов, пути и названия которых соответствуют определенному шаблону, флагу '‑‑sourceFiles' (-f) должен быть передан специально сформированный XML файл. В нем могут содержаться списки абсолютных и относительных путей и\или масок до проверяемых файлов.
<SourceFilesFilters>
<SourceFiles>
<Path>C:\Projects\Project1\source1.cpp</Path>
<Path>\Project2\*</Path>
<Path>source_*.cpp</Path>
</SourceFiles>
<SourcesRoot>C:\Projects\</SourcesRoot>
</SourceFilesFilters>
Включение данного режима позволяет анализатору учитывать информацию не только из анализируемого файла, но и из связанных с ним файлов. Это позволяет производить более глубокий и качественный анализ. Однако, на сбор необходимой информации требуется дополнительное время, что отразится на времени анализа вашего проекта.
Данный режим актуален для C и C++ проектов. C# проекты обеспечивают межмодульный анализ по умолчанию.
Для запуска межмодульного анализа необходимо передать флаг ‑‑intermodular в консольную утилиту.
Утилиты PVS-Studio_Cmd / pvs-studio-dotnet имеют несколько ненулевых кодов возврата, которые не означают проблемы в работе самой утилиты, т.е. даже если утилита вернула не '0', это ещё не означает, что она "упала". Код возврата представляет собой битовую маску, маскирующую все возможные состояния, возникшие во время работы утилиты. Например, утилита вернёт ненулевой код возврата в случае, если анализатор нашёл в проверяемом коде потенциальные ошибки. Это позволяет обрабатывать такую ситуацию отдельно, например, на сборочном сервере, когда политика использования анализатора не подразумевает наличия срабатываний в коде, заложенном в систему контроля версий.
Рассмотрим далее все возможные коды состояния утилиты, из которых формируется битовая маска кода возврата.
Приведём пример скрипта Windows batch для расшифровки кода возврата утилиты PVS-Studio_Cmd:
@echo off
"C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe"
-t "YourSolution.sln" -o "YourSolution.plog"
set /A FilesFail = "(%errorlevel% & 1) / 1"
set /A GeneralExeption = "(%errorlevel% & 2) / 2"
set /A IncorrectArguments = "(%errorlevel% & 4) / 4"
set /A FileNotFound = "(%errorlevel% & 8) / 8"
set /A IncorrectCfg = "(%errorlevel% & 16) / 16"
set /A InvalidSolution = "(%errorlevel% & 32) / 32"
set /A IncorrectExtension = "(%errorlevel% & 64) / 64"
set /A IncorrectLicense = "(%errorlevel% & 128) / 128"
set /A AnalysisDiff = "(%errorlevel% & 256) / 256"
set /A SuppressFail = "(%errorlevel% & 512) / 512"
set /A LicenseRenewal = "(%errorlevel% & 1024) / 1024"
if %FilesFail% == 1 echo FilesFail
if %GeneralExeption% == 1 echo GeneralExeption
if %IncorrectArguments% == 1 echo IncorrectArguments
if %FileNotFound% == 1 echo FileNotFound
if %IncorrectCfg% == 1 echo IncorrectConfiguration
if %InvalidSolution% == 1 echo IncorrectCfg
if %IncorrectExtension% == 1 echo IncorrectExtension
if %IncorrectLicense% == 1 echo IncorrectLicense
if %AnalysisDiff% == 1 echo AnalysisDiff
if %SuppressFail% == 1 echo SuppressFail
if %LicenseRenewal% == 1 echo LicenseRenewal
Примечание. Так как максимальное значение кода возврата под Unix ограничено 255, коды возврата утилит PVS-Studio_Cmd (где может возвращаться значение больше 255) и pvs-studio-dotnet отличаются.
Рассмотрим далее все возможные коды состояния утилиты, из которых формируется битовая маска кода возврата.
Примечание. Данный раздел актуален для Windows. Анализ C++ проектов на Linux / macOS описан в соответствующем разделе документации.
Если ваш C/C++ проект не использует стандартные сборочные системы Visual Studio (VCBuild/MSBuild) или даже использует собственную сборочную систему / make-файлы через NMake проекты Visual Studio, то вы не сможете проверить такой проект с помощью PVS-Studio_Cmd.
В таком случае вы можете воспользоваться системой отслеживания компиляторов, которая позволяет анализировать проекты, независимо от их сборочной системы, "перехватывая" запуск процессов компиляции. Система отслеживания компиляции может использоваться как из командной строки, так и через пользовательский интерфейс приложения C and C++ Compiler Monitoring UI.
Также вы можете напрямую встроить запуск command line ядра анализатора непосредственно в вашу сборочную систему. Заметьте, что это потребует прописывать вызов ядра анализатора PVS-Studio.exe для каждого компилируемого файла, по аналогии с тем, как вызывается C++ компилятор.
При запуске анализа кода из командной строки по умолчанию используются те же настройки, что и при запуске анализа из IDE (Visual Studio / Rider). Также можно указать, какой файл настроек использовать, с помощью аргумента ‑‑settings, как было описано выше.
Что касается, к примеру, системы фильтров (Keyword Message Filtering и Detectable Errors), то она НЕ применяется при анализе из командной строки. В том смысле, что в файле отчета независимо от заданных параметров будут все сообщения об ошибках. Но при загрузке файла с результатами в IDE фильтры уже будут применены. Это происходит из-за того, что фильтры применяются динамически к результатам (и при запуске из IDE также). Это очень удобно, поскольку, получив список сообщений, вы можете захотеть отключить некоторые из них (например, V201). Достаточно отключить их в настройках и соответствующие сообщения пропадут из списка БЕЗ перезапуска анализа.
Формат отчёта анализатора не предназначен для прямого отображения или чтения человеком. Однако, если необходимо каким-либо образом отфильтровать результаты анализа и преобразовать их в "читаемый" вид, можно воспользоваться утилитой PlogConverter, распространяемой вместе с PVS-Studio.
Для работы с отчётами разных форматов требуется использование разных утилит:
Исходный код обеих утилит открыт и доступен для загрузки: PlogConverter; plog-converter, что позволяет достаточно просто добавлять поддержку новых форматов на основе существующих алгоритмов.
Более подробно данные утилиты описываются в соответствующих разделах документации:
В данном документе описывается система мониторинга компиляции для Windows. Мониторинг проектов под Linux описан здесь (подраздел "Любой проект (только для Linux)").
Система мониторинга компиляции (PVS-Studio Compiler Monitoring, CLMonitoring) предназначена для "бесшовной" интеграции статического анализа PVS-Studio в любую сборочную систему на ОС семейства Windows. Эта сборочная система должна использовать для компиляции файлов один из препроцессоров, поддерживаемых command-line анализатором PVS-Studio.exe (Visual C++, GCC, Clang, Keil MDK ARM Compiler 5/6, IAR C/C++ Compiler for ARM).
Анализатору PVS-Studio.exe для корректного анализа исходных C/C++ файлов требуется промежуточный .i (intermediate) файл — результат работы препроцессора, содержащий все включённые в исходный файл заголовки и раскрытые макросы. Это требование обуславливает невозможность "просто проверить" исходные файлы на диске. Ведь помимо содержимого самих файлов статическому анализатору требуется также информация, необходимая для генерации такого .i файла. Заметим, что PVS-Studio не содержит в себе препроцессора и при своей работе полагается на внешний.
Рассматриваемая система, как следует из её названия, основана на "отслеживании" запусков компилятора во время сборки проекта. Она позволяет собрать всю необходимую информацию для запуска анализа (т.е. для генерации препроцессированных .i файлов) на исходниках, сборка которых была отслежена. Это, в свою очередь, позволяет проанализировать проект, просто запустив его пересборку, без необходимости от пользователя как-либо модифицировать свои сборочные сценарии.
Система представлена сервером, отслеживающим компиляцию (command-line утилита CLMonitor.exe), и приложением C and C++ Compiler Monitoring UI (Standalone.exe), осуществляющим непосредственный запуск статического анализа. При необходимости использования из командной строки CLMonitor.exe может также быть использован и в качестве клиента.
В режиме работы по умолчанию система не производит анализа иерархии запущенных процессов, а отслеживает запуск всех процессов в системе. Это значит, что, если будет запущена параллельная сборка нескольких проектов, система также отследит запуски компиляторов у обоих проектов.
CLMonitor.exe также умеет отслеживать только такие запуски процессов компилятора, которые были порождены от указанного (по PID) родительского процесса. Такой режим работы предусмотрен для случая, когда в системе была запущена параллельная сборка нескольких проектов, но вам нужно отследить запуски процессов компилятора только для определённого собираемого проекта или solution'а. Режим отслеживания дочерних процессов будет описан ниже.
Сервер мониторинга (CLMonitor.exe) отслеживает запуск процессов, соответствующих целевому компилятору (например, cl.exe в случае Visual C++ или g++.exe в случае GCC) и собирает информацию об окружении этих процессов. Сервер мониторинга отслеживает запуски процессов только для того пользователя, из-под которого он сам запущен. Эта информация необходима для последующего запуска статического анализа и включает:
После завершения сборки проекта серверу мониторинга (CLMonitor.exe) необходимо послать сигнал о прекращении отслеживания. Это можно сделать как с помощью того же CLMonitor.exe, запустив его в режиме клиента, так и через интерфейс Compiler Monitoring UI.
По завершении мониторинга сервер, используя собранную о процессах информацию, запускает генерацию промежуточных (intermediate) файлов для исходных файлов, которые были скомпилированы во время работы мониторинга. Затем уже выполняется запуск непосредственно статического анализатора (PVS-Studio.exe), выдавая на выход стандартный отчёт о работе PVS-Studio, с которым можно работать как из Compiler Monitoring UI, так и из любого IDE плагина PVS-Studio.
Примечание: далее будет описано использование CLMonitor.exe для интеграции анализа в автоматизированную систему сборки. Если вы хотите просто проверить свой проект, то воспользуйтесь приложением Compiler Monitoring UI.
CLMonitor.exe представляет собой сервер мониторинга, который осуществляет непосредственно само отслеживание запусков компиляторов. Его необходимо запустить непосредственно перед началом сборки вашего проекта. В режиме отслеживания сервер будет перехватывать запуски всех поддерживаемых компиляторов.
Перечислим далее поддерживаемые компиляторы:
Но, если вы хотите интегрировать анализ непосредственно в вашу сборочную систему (или систему непрерывной интеграции и т.п.), вы не можете "просто" запустить сервер мониторинга. Ведь данный процесс на время своей работы блокирует остальную сборку. Поэтому вам нужно запустить CLMonitor.exe с аргументом monitor:
CLMonitor.exe monitor
В этом режиме CLMonitor запустит сам себя в режиме отслеживания и завершит свою работу, а ваша сборочная система сможет продолжить выполнять свои оставшиеся задачи. При этом второй (запущенный из первого) процесс CLMonitor будет оставаться запущенным и производить отслеживание сборки.
Т.к. в таком режиме работы ни одна консоль не подключена к процессу CLMonitor, то помимо стандартных потоков ввода\вывода (stdin\stdout) сервер мониторинга также выводит свои сообщения в журнал событий Windows (Event Logs -> Windows Logs -> Application).
Также можно отслеживать только те запуски компиляторов, которые были порождены от определённого указанного по PID процесса. Для этого необходимо запустить CLMonitor.exe в режиме отслеживания с аргументами trace и ‑‑parentProcessID ('-p' сокращённый вариант). Аргумент ‑‑parentProcessID в качестве параметра должен принимать PID процесса, который, как предполагается, будет выступать родительским для запускаемых процессов компилятора. Строка запуска СLMonitor.exe в таком режиме может выглядеть следующим образом:
CLMonitor.exe trace –-parentProcessID 10256
Если вы выполняете сборку из консоли и хотите, чтобы CLMonitor.exe отследил только сборку, запускаемую из этой же консоли, то вы можете запустить CLMonitor.exe с аргументом ‑‑attach (-a):
CLMonitor.exe monitor –-attach
В таком режиме работы программа отследит только те запуски компиляторов, которые были дочерними по отношению к процессу консоли, из-под которой была запущена сборка.
Стоит учитывать, что при сборке проектов, использующих сборочную систему MSBuild, оставшиеся с момента выполнения предыдущих сборок процессы MSBuild.exe не всегда завершаются. В этом случае CLMonitor.exe, запускаемый в режиме отслеживания дочерних процессов, не сможет отследить запуски компиляторов, которые были порождены от оставшихся в системе работающих процессов MSBuild.exe. Ведь данные процессы MSBuild.exe, скорее всего, не входят в иерархию процесса, указанного с помощью аргумента ‑‑parentProcessID. В связи с этим, перед запуском CLMonitor.exe в режиме отслеживания дочерних процессов рекомендуется завершать процессы MSBuild.exe, оставшиеся в системе с момента выполнения предыдущей сборки.
Примечание: для корректной работы сервера отслеживания он должен быть запущен с правами, эквивалентными правам, с которыми запускаются и сами процессы компиляторов.
Для корректной записи сообщений в системные журналы событий процесс CLMonitor.exe необходимо запустить от имени администратора хотя бы один раз. Если процесс ни разу не стартовал с правами администратора, сообщения об ошибках не будут попадать с системный журнал.
Обратите внимание, что в системные логи сервер записывает только сообщения об ошибках в своей работе (обработанных исключениях), а не диагностические сообщения от анализатора!
После завершения сборки запустите CLMonitor.exe в режиме клиента для генерации препроцессированных файлов и непосредственного запуска статического анализа:
CLMonitor.exe analyze -l "d:\ptest.plog"
В качестве параметра '-l' передаётся полный путь до файла, в который будут записаны непосредственные результаты работы статического анализатора.
После запуска в режиме клиента CLMonitor.exe подключится к запущенному ранее серверу и получит от него информацию обо всех отловленных процессах компиляторов, после чего сервер завершит свою работу. Клиент же начнёт запуск препроцессоров и анализаторов PVS-Studio.exe для всех отслеженных исходных файлов.
В результате работы CLMonitor.exe будет получен файл-отчёта (D:\ptest.plog), который можно открыть в любом IDE плагине PVS-Studio или в Compiler Monitoring UI (PVS-Studio|Open/Save|Open Analysis Report).
Вы можете использовать в CLMonitor подавление сообщений анализатора с помощью аргумента '-u':
CLMonitor.exe analyze -l "d:\ptest.plog" -u "d:\ptest.suppress" -s
В параметре '-u' передаётся путь до suppress файла, полученного с помощью диалога Message Suppression в Compiler Monitoring UI (Tools|Message Suppression...). Параметр '-s' является необязательным и позволяет дописывать в переданный через '-u' suppress файл все новые сообщения текущей проверки.
Также возможно запустить CLMonitor.exe в режиме клиента для генерации препроцессированных файлов и непосредственного запуска анализатора в режиме межмодульного анализа:
CLMonitor.exe analyze -l "d:\ptest.plog" --intermodular
Флаг ‑‑intermodular включает режим межмодульного анализа. В этом режиме анализатор выполняет более глубокий анализ кода, но тратит на это больше времени.
Для задания дополнительных параметров отображения и фильтрации сообщений вы можете передать путь до файла конфигурации диагностик (.pvsconfig) с помощью аргумента '-c':
CLMonitor.exe analyze -l "d:\ptest.plog" -c "d:\filter.pvsconfig"
При необходимости завершить мониторинг без запуска анализа следует воспользоваться командой abortTrace:
CLMonitor.exe abortTrace
CLMonitor.exe позволяет сохранять отловленную информацию о компиляции в отдельном дамп-файле. Это позволит в дальнейшем перезапустить анализ без необходимости повторно собирать проект и мониторить эту сборку. Для сохранения дампа мониторинга компиляции нужно сначала запустить мониторинг в обычном режиме командами trace или monitor, как было описано выше. После того как сборка завершена, можно завершить мониторинг и сохранить файл дампа. Для этого нужно запустить CLMonitor.exe с командой saveDump:
CLMonitor.exe saveDump -d d:\monitoring.zip
Также можно завершить мониторинг, сохранить файл дампа и запустить анализ отловленных файлов. Это можно сделать, дополнительно указав команде CLMonitor.exe analyze путь до места сохранения дампа:
CLMonitor.exe analyze -l "d:\ptest.plog" -d d:\monitoring.zip
Запустить анализ из уже готового dump файла можно без предварительного запуска мониторинга:
CLMonitor.exe analyzeFromDump -l "d:\ptest.plog"
-d d:\monitoring.zip
Файл дампа представляет собой обычный zip архив, содержащий список отловленных у процессов параметров (параметры запуска, текущая директория, переменные окружения и т.п.) в формате XML. Команда analyzeFromDump поддерживает запуск как из заархивированного в zip дампа, так и из неупакованного XML файла. Если вы используете разархивированный xml файл, убедитесь, что у него расширение xml.
Анализ из дампа также поддерживает возможность запуска анализа в межмодульном режиме. Для этого, как и в режиме анализа, необходимо передать флаг ‑‑intermodular:
CLMonitor.exe analyzeFromDump -l "d:\ptest.plog"
-d d:\monitoring.zip --intermodular
Для "ручной" проверки отдельных проектов через CLMonitor можно воспользоваться интерфейсом приложения Compiler Monitoring UI, который можно запустить из меню Start.
Для запуска отслеживания откройте диалог через Tools -> Analyze Your Files... (рисунок 1):
Рисунок 1 — Диалог запуска мониторинга сборки
Нажмите "Start Monitoring". После этого будет запущен CLMonitor.exe, а основное окно среды будет свёрнуто.
Выполните сборку, а по её завершении нажмите на кнопку "Stop Monitoring" в окне в правом нижнем углу экрана (рисунок 2):
Рисунок 2 — Диалог управления мониторингом
Если серверу мониторинга удалось отследить запуски компиляторов, будет запущен статический анализ исходных файлов. По окончании вы получите обычный отчёт о работе PVS-Studio (рисунок 3):
Рисунок 3 — Результаты работы сервера мониторинга и статического анализатора
Результаты работы могут быть сохранены в виде XML файла (файла с расширением plog) с помощью команды меню File -> Save PVS-Studio Log As...
Удобная навигация по сообщениям анализатора и навигации по коду среды доступна в среде разработки Visual Studio с помощью плагина PVS-Studio. Если проверяемый проект можно открывать в Visual Studio, но при этом "обычная" проверка (PVS-Studio|Check|Solution) не работает (например, это актуально для makefile проектов), можно использовать преимущества работы из IDE, загрузив полученные результаты анализа (plog файл) в PVS-Studio с помощью команды (PVS-Studio|Open/Save|Open Analysis Report...). Данное действие можно также автоматизировать с помощью средств автоматизации Visual Studio, привязав его, а также саму проверку, например, к сборке проекта. Приведём в качестве примера интеграцию анализа с помощью системы отслеживания вызовов компилятора PVS-Studio в makefile проект. Такой тип проектов, например, используется системой сборки Unreal Engine проектов на Windows.
В качестве команды запуска сборки впишем файл run.bat:
Рисунок 4 — настройка сборки makefile проекта.
Содержимое файла run.bat:
set slnPath=%1
set plogPath="%~2test.plog"
"%ProgramFiles(X86)%\PVS-Studio\CLMonitor.exe" monitor
waitfor aaa /t 10 2> NUL
nmake
"%ProgramFiles(X86)%\PVS-Studio\CLMonitor.exe" analyze -l %plogPath%
cscript LoadPlog.vbs %slnPath% %plogPath%
В качестве параметров в run.bat мы передаём пути до solution'а и проекта. Мы запускаем отслеживание запусков компиляторов с помощью CLMonitor.exe. Команда waitfor нужна для создания задержки между запуском отслеживания компиляторов и запуском сборки. Без неё мониторинг может "не успеть" отловить первые запуски компиляторов. Далее мы запускаем сборку проекта через nmake. По завершении сборки мы запускаем анализ и, когда анализ завершится (результат анализа будет записан рядом с проектным файлом), мы загружаем результаты анализа в Visual Studio с помощью другого скрипта — LoadPlog.vbs. Вот его содержимое:
Set objArgs = Wscript.Arguments
Dim objSln
Set objSln = GetObject(objArgs(0))
Call objSln.DTE.ExecuteCommand("PVSStudio.OpenAnalysisReport",
objArgs(1))
Здесь мы используем команду автоматизации Visual Studio DTE.ExecuteCommand для обращения из командной строки напрямую к запущенному экземпляру Visual Studio, в котором открыт наш проект. Фактически, выполнение данной команды эквивалентно клику по PVS-Studio|Open/Save|Open Analysis Report...
Для нахождения запущенного экземпляра Visual Studio мы использовали метод GetObject. Обратите внимание, что это метод идентифицирует запущенную Visual Studio по открытому в ней solution'у. При его использовании не следует держать открытыми несколько IDE с одинаковыми загруженными solution'ами. Он может "промахнуться", и результаты анализа откроются не в той IDE, в которой вы запускали сборку\анализ.
Использование Incredibuild позволяет снизить время анализа проекта в разы при распределении нагрузки по нескольким машинам. Однако утилита CLMonitor.exe не может отслеживать удалённые вызовы компилятора и поддерживает трассировку только для локальных сборок. Поэтому результат анализа мониторинга компилятора, запущенного Incredibuild-ом, может оказаться некорректным.
Вы можете запустить анализ через мониторинг компилятора с использованием распределённых сборок. Для этого необходимо получить дамп мониторинга локального компилятора при помощи CLMonitor.exe (получение дампа описано в предыдущих разделах) и запустить анализ дампа в распределённом режиме при помощи Incredibuild. Более подробная информация о настройке Incredibuild для данного режима имеется в соответствующем разделе документации: "Ускорение анализа C/C++ кода с помощью систем распределённой сборки (Incredibuild)".
При сборке проекта в среде IAR Embedded Workbench текущая рабочая директория процесса компилятора(iccarm.exe) в некоторых случаях может быть установлена средой в C:\Windows\System32. Такое поведение может вызывать проблемы, так как CLMonitoring сохраняет промежуточные файлы в текущей рабочей директории процесса компилятора.
Чтобы избежать записи промежуточных файлов в C:\Windows\System32, а также ошибок, связанных с недостаточными правами для записи в C:\Windows\System32, необходимо открывать рабочее пространство(workspace) двойным щелчком мыши на файле с расширением eww из проводника Windows. В этом случае промежуточные файлы будут сохранены в директории рабочего пространства.
В случае необходимости инкрементального анализа при использовании системы мониторинга компиляции, достаточно "отслеживать" инкрементальную сборку, т.е. компиляцию тех файлов, которые были изменены с момента последней сборки. Этот сценарий использования позволит анализировать только изменённый/новый код.
Такой сценарий использования естественен для системы мониторинга компиляции. Соответственно, режим анализа (полный или анализ только модифицированных файлов) зависит только от того, какая сборка отслеживается: полная или инкрементальная.
Включение данного режима позволяет анализатору учитывать информацию не только из анализируемого файла, но и из связанных с ним файлов. Это позволяет производить более глубокий и качественный анализ. Однако на сбор необходимой информации требуется дополнительное время, что отразится на времени анализа вашего проекта.
Для запуска межмодульного анализа (режим 'analyze') или межмодульного анализа из дампа (режим 'analyzeFromDump') необходимо передать флаг ‑‑intermodular.
Режимы 'analyze' и 'analyzeFromDump' позволяют провести выборочную проверку группы файлов. С помощью флага '‑‑sourceFiles' (-f) задается путь к текстовому файлу, в котором построчно указываются пути к проверяемым файлам. Относительные пути к файлам будут раскрыты в соответствии с текущей рабочей директорией. Можно указывать как исходные компилируемые файлы (.c, .cpp и т.д.), так и заголовочные файлы (.h/.hpp).
В режиме проверки списка файлов генерируется кэш зависимостей компиляции, который будет использован при последующих запусках анализа. По умолчанию кэш зависимостей сохраняются в специальной поддиректории '.pvs-studio' рабочей директории под названием 'CLMonitor.deps.json'. При необходимости можно указать другое место сохранения/загрузки при помощи флага '‑‑dependencyRoot' (-D).
По умолчанию, в кэше зависимостей сохраняются абсолютные пути к файлам исходного кода. Для того чтобы сделать переносимый кэш, можно указать произвольную корневую папку проекта (относительно которой будут сохраняться и загружаться пути) при помощи флага '‑‑dependencyCacheSourcesRoot' (-R).
Используемый по умолчанию метод отслеживания запусков компиляторов может не успеть определить все файлы исходного кода. Эта проблема особенно актуальна для Embedded проектов, поскольку они состоят из быстро компилирующихся файлов на языке C. Для того чтобы гарантировать перехват всех процессов компиляции, утилита мониторинга может использовать более агрессивный подход — через механизм Image File Execution Options (IFEO) в реестре Windows. Этот механизм позволяет запустить специальный обработчик перед непосредственным запуском каждого процесса компиляции. Затем обработчик передаст серверу мониторинга необходимую информацию и продолжит запуск компилятора. Работа этого режима прозрачна для сборочной системы, но требует прав администратора для внесения изменений в реестр Windows.
Чтобы включить этот метод отслеживания в консольном варианте мониторинга нужно передать утилите CLMonitor.exe в режимах 'monitor' или 'trace' флаг '‑‑wrapCompilers (-W)' со списком компиляторов. Список компиляторов разделяется запятой, например:
CLMonitor.exe trace --wrapCompilers gcc.exe,g++.exe
Обратите внимание, что нужно указывать имена исполняемых файлов компилятора с расширением .exe и без путей.
Для того чтобы включить данный режим перехвата при использовании графического интерфейса, заполните поле Wrap Compilers в окне запуска мониторинга.
Несмотря на преимущества использования механизма IFEO, необходимо соблюдать некоторые меры предосторожности.
Для того чтобы подключить обработчик перед запуском процессов, монитор модифицирует путь реестра "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options". В нём создаётся новый ключ с именем исполняемого файла процесса, с полем "Debugger". В этом поле указывается команда запуска обработчика, который будет запущен вместо процесса с указанным именем исполняемого файла. Ошибка в этом поле может сделать невозможным запуск некоторых процессов. PVS-Studio не позволяет использовать этот режим с произвольным набором исполняемых файлов, а только с теми, которые опознаются как известные компиляторы. После успешного завершения процесса мониторинга записи реестра будут возвращены в исходное состояние. Если процесс мониторинга будет завершен нештатно (в случае принудительного завершения, ошибки или выключения компьютера), реестр автоматически восстановлен не будет. Однако при модификации реестра утилита мониторинга создаст файл восстановления в "%AppData%\PVS-Studio\wrapperBackup.reg", при использовании которого модифицированные ключи реестра будут возвращены в исходное состояние. Если при очередном запуске монитора файл восстановления существует, он будет применен автоматически. Перед автоматическим восстановлением файл восстановления проходит проверку. Если этот файл содержит подозрительные записи, он не будет использован и будет переименован в "wrapperBackup-rejected.reg". В этом случае отклонённый файл восстановления должен быть проверен ответственным лицом. Возможно, это свидетельствует о неправильной настройке или наличии вредоносного ПО на компьютере.
Несмотря на удобство используемой в данном режиме работы "бесшовной" интеграции анализа в автоматизированный сборочный процесс (через CLMonitor.exe), тем не менее, необходимо помнить и о естественных ограничениях, присущих данному режиму. А именно, о невозможности на 100% гарантировать перехват всех запусков компилятора при сборке. Это в свою очередь, может быть вызвано как влиянием внешнего окружения (например, антивирусами), так и особенностями аппаратно-программного окружения (например, компилятор может отработать слишком быстро при использовании SSD диска, а при этом производительности CPU может оказаться недостаточно для того, чтобы "успеть" отловить такой запуск).
Поэтому мы рекомендуем, по возможности, выполнить полноценную интеграцию статического анализатора PVS-Studio.exe в вашу сборочную систему (если вы используете сборочную систему, отличную от MSBuild), либо же воспользоваться IDE плагином PVS-Studio.
Этот раздел документации описывает устаревший метод запуска анализа, который может быть удалён в будущих версиях.
Мы рекомендуем всем использовать PVS-Studio из сред разработки Microsoft Visual Studio, куда инструмент прекрасно интегрируется. Однако, бывают ситуации, когда необходим запуск из командной строки, например, для кроссплатформенных проектов, использующих сборочную систему, основанную на makefile'ах.
В случае, если у вас есть файлы проектов (.vcproj/.vcxproj) и решения (.sln), и запуск из командной строки нужен, к примеру, для ежедневных проверок, то рекомендуем ознакомиться со статьей "Проверка Visual C++ (.vcxproj) и Visual C# (.csproj) проектов из командной строки с помощью PVS-Studio".
Также, независимо от сборочной системы, вы можете воспользоваться системой отслеживания вызовов компилятора.
Итак, как работает С++ анализатор кода (PVS-Studio или любой другой)?
Когда пользователь анализатора дает команду проверить какой-либо файл (например, file.cpp), то анализатор сначала выполняет препроцессирование этого файла. В результате раскрываются все макросы, подставляются #include-файлы. Препроцессированный i-файл уже может разбирать анализатор кода. Обратите внимание, что анализатор не может разбирать файл, который не был препроцессирован. Поскольку тогда он не будет иметь информации об используемых типах, функциях, классах. Работа любого анализатора кода состоит как минимум из двух этапов: препроцессирование и собственно анализ.
Возможна ситуация, в которой программы не имеют связанных с ними проектных файлов, например, в случае мультиплатформенного ПО или старых проектов, собираемых с помощью пакетных утилит командной строки. Зачастую в таких случаях для управления сборочным процессом используют различные Make системы, например, Microsoft NMake, GNU Make и т.п.
Для проверки подобных проектов потребуется встроить в сборочный процесс прямой вызов анализатора (по умолчанию, файл расположен в %programfiles%\PVS-Studio\x64\PVS-Studio.exe) и передать ему все необходимые для препроцессирования аргументы. Фактически анализатор необходимо вызывать для тех же файлов, для которых вызывается компилятор (cl.exe в случае Visual C++).
Анализатор PVS-Studio должен быть вызван в пакетном режиме для каждого C/C++ файла проекта либо для целой группы файлов (файлы с расширением c/cpp/cxx и т.п., анализатор не нужно вызывать для заголовочных h файлов) с аргументами следующего вида:
PVS-Studio.exe --cl-params %ClArgs% --source-file %cppFile%
--lic-file %licPath% --cfg %cfgPath% --output-file %ExtFilePath%
%ClArgs% — все аргументы, передаваемые используемому компилятору при обычной компиляции, в том числе и путь до файла (или файлов) с исходным кодом.
%cppFile% — путь до анализируемого C/C++ файла или пути до группы C/C++ файлов (имена файлов, разделённые пробелами)
Параметры %ClArgs% и %cppFile% должны быть переданы анализатору PVS-Studio аналогично тому, как они передаются компилятору Visual C++ , т.е. полный путь до файла должен быть передан дважды, в каждом параметре.
%licPath% — путь до файла с лицензией PVS-Studio.
%cfgPath% — путь до конфигурационного файла PVS-Studio.cfg. Этот файл общий для всех C/C++ файлов, который может быть создан вручную (пример будет показан ниже).
%ExtFilePath% — необязательный параметр, путь до внешнего файла, в который будут сохранены результаты работы анализатора. Если данный параметр не указан, анализатор будет выдавать сообщения о найденных ошибках в stdout. Полученные здесь результаты анализа можно открыть в окне PVS-Studio среды Visual Studio с помощью команды PVS-Studio/Open Analysis Report (выбрав в качестве типа файла Unparsed output). Обратите внимание на то, что, начиная с версии PVS-Studio 4.52, анализатор при запуске из командной строки поддерживает работу с одним выходным файлом (‑‑output-file) из нескольких процессов PVS-Studio.exe. Это сделано для того, чтобы, как и в обычных makefile, при компиляции можно было запускать несколько процессов анализатора. При этом выходной файл не будет перезаписан и потерян, так как используется механизм блокировок файла.
Приведём пример вызова анализатора в независимом режиме для отдельного файла с препроцессором Visual C++ (cl.exe):
PVS-Studio.exe --cl-params "C:\Test\test.cpp" /D"WIN32" /I"C:\Test\"
--source-file "C:\Test\test.cpp" --cfg "C:\Test\PVS-Studio.cfg"
--output-file "C:\Test\test.log" --lic-file ...
Конфигурационный файл PVS-Studio.cfg (параметр ‑‑cfg) должен содержать следующие строки:
exclude-path = C:\Program Files (x86)\Microsoft Visual Studio 10.0
vcinstalldir = C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\
platform = Win32
preprocessor = visualcpp
language = C++
skip-cl-exe = no
Рассмотрим эти параметры:
Вы можете фильтровать диагностические сообщения анализатора с помощью опций analyzer-errors и analysis-mode (заданных или в cfg-файле, или в командной строке). Это необязательные параметры:
Также существует возможность подать на вход анализатору готовый препроцессированный файл (i-файл), пропустив этап препроцессирования и сразу приступив к анализу. Для этого необходимо использовать параметр skip-cl-exe, указав значение yes. В этом режиме не требуется использование параметра cl-params. Вместо этого необходимо указать путь к i-файлу (‑‑i-file) и задать тип препроцессора, использованного для создания этого i-файла. Указание пути к файлу исходного кода (‑‑source-file) также необходимо. Несмотря на то, что i-файл уже содержит достаточную информацию для анализа, иногда может потребоваться сравнение i-файла с файлом исходного кода, например, когда анализатору требуется посмотреть на нераскрытый макрос. Таким образом, вызов анализатора в независимом режиме с заданным i-файлом для препроцессора Visual C++ (cl.exe) мог бы иметь вид:
PVS-Studio.exe --source-file "C:\Test\test.cpp"
--cfg "C:\Test\PVS-Studio.cfg" --output-file "C:\Test\test.log"
Конфигурационный файл PVS-Studio.cfg (параметр ‑‑cfg) должен содержать следующие строки:
exclude-path = C:\Program Files (x86)\Microsoft Visual Studio 10.0
vcinstalldir = C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\
platform = Win32
preprocessor = visualcpp
language = C++
skip-cl-exe = yes
i-file = C:\Test\test.i
Полный список параметров командной строки можно получить с помощью запуска:
PVS-Studio.exe –help
Следует отметить, что при явном вызове PVS-Studio.exe, лицензионная информация, сохранённая в файле 'Settings.xml', не учитывается. При запуске следует явно указывать путь до файла с лицензией. Это текстовый файл в кодировке UTF-8, состоящий из двух строк – имени и ключа.
Путь до файла с лицензией можно указать как в конфигурационном файле PVS-Studio, так и передать как аргумент командной строки. Соответствующий параметр: lic-file.
Например, чтобы указать путь до файла с лицензией в .cfg файле, следует добавить следующую строку:
lic-file = D:\Test\license.lic
Возьмём для примера Makefile проект, в котором сборка осуществляется компилятором Visual C++, и она описана в makefile проекта следующим правилом:
$(CC) $(CFLAGS) $<
Здесь $(CC) вызывает cl.exe, которому передаются параметры компиляции $(CFLAGS), и наконец с помощью макроса $< осуществляется подстановка всех С\С++ файлов, от которых зависит текущая цель сборки. Таким образом, для всех файлов с исходным кодом будет вызван компилятор cl.exe с необходимыми параметрами.
Модифицируем данный сценарий таким образом, чтобы перед вызовом компилятора для каждого файла осуществлялась проверка статическим анализатором PVS-Studio:
$(PVS) --source-file $< --cl-params $(CFLAGS) $<
--cfg "C:\CPP\PVS-Studio.cfg"
$(CC) $(CFLAGS) $<
Здесь $(PVS) - путь до исполняемого файла анализатора (%programfiles%\PVS-Studio\x64\PVS-Studio.exe). Обратите внимание, что после вызова анализатора на следующей строке вызывается компилятор Visual C++ с теми же параметрами, что и изначально. Это делается для того, чтобы были построены правильные цели (targets) и сборка не остановился из-за отсутствия .obj-файлов.
Инструмент PVS-Studio разрабатывался для работы в рамках сред Visual Studio. И его работа из командной строки - это дополнение к основному режиму работы. Тем не менее, все его диагностические возможности доступны.
Полученные в данном режиме ошибки могут быть также перенаправлены в файл с помощью аргумента командной строки ‑‑output-file. Данный файл будет содержать необработанный и неотфильтрованный вывод анализатора.
Такой файл можно открыть в IDE окне PVS-Studio или приложении C and C++ Compiler Monitoring UI (Standalone.exe) командой Open Analysis Report (выбрав в качестве типа файла 'Unparsed output') и в дальнейшем его можно будет сохранить в стандартном формате отчета PVS-Studio log file (plog), что позволит избежать дублирования сообщений, а также использовать все стандартные механизмы фильтрации сообщений.
Также 'сырой' отчёт можно преобразовать в один из поддерживаемых форматов (xml, html, csv и т.п.) с помощью консольной утилиты PlogConverter.
Пользователи, знакомые с режимом инкрементального анализа в PVS-Studio, при работе из IDE естественно не хотят от него отказываться и в версии для командной строки. К счастью, любая система сборки автоматически позволяет получить инкрементальный анализ "из коробки". Ведь говоря "make" мы компилируем только изменившиеся файлы. Соответственно и PVS-Studio.exe будет вызван только для изменившихся файлов. Так что инкрементальный анализ в версии для командной строки есть автоматически.
Несмотря на то, что полученный в независимом режиме неотфильтрованный текстовый файл с диагностическими сообщениями анализатора возможно открыть в окне PVS-Studio среды разработки (что в свою очередь позволит использовать навигацию по файлам и механизмы фильтрации), в самой среде Visual Studio будет работать фактически только текстовый редактор, а дополнительная функциональность системы IntelliSense (автодополнение, переход к местам декларации типов и телам функций, и т.п.) будет недоступна. А это, в свою очередь, создаёт очень большие неудобства при работе с результатами анализа, особенно для крупных проектов, вынуждая каждый раз вручную искать объявления классов и функций и значительно увеличивая время работы над каждым диагностическим сообщением.
Решить рассматриваемую проблему можно, создав пустой проект Visual C++ (например, Makefile проект) в одной директории с проверяемыми анализатором исходными C++ файлами (vcproj/vcxproj файл при этом должен находиться в папке, являющейся корневой для всех проверяемых исходных файлов). После этого можно включить режим Show All Files проекта (кнопка Show All Files в окне Solution Explorer), что позволит увидеть в древовидной структуре окна Solution Explorer все файлы, располагающиеся в нижележащих директориях файловой системы. Затем через контекстную команду Include in Project можно добавить в проект все необходимые cpp, с и h файлы (возможно, что для некоторых файлов также придётся прописать пути до include директорий, если в них, например, присутствуют включения из сторонних библиотек). Стоит помнить, что при включении в проект лишь части от всех проверяемых файлов, IntelliSense может не распознать часть из упоминаемых в них типов, если эти типы определяются как раз в отсутствующих файлах.
Рисунок 1 — добавление файлов в проект
Созданный таким образом проектный файл Visual C++ не может быть использован для сборки или анализа исходного кода с помощью PVS-Studio, но всё же позволит значительно упростить работу с результатами анализа, и может быть сохранён для дальнейшего использования на следующих итерациях работы анализатора в независимом режиме.
Как известно, компилятор cl.exe может обрабатывать файлы как по одному, так и группой. В первом случае вызов компилятора происходит в несколько запусков:
cl.exe ... file1.cpp
cl.exe ... file2.cpp
cl.exe ... file2.cpp
Во втором случае - за один запуск:
cl.exe ... file1.cpp file2.cpp file3.cpp
И тот, и другой режим работы поддерживается в консольной версии PVS-Sudio.exe, примеры приведены выше.
Пользователю может оказаться полезно понимать логику работы программы в первом и втором случае. При раздельном запуске PVS-Studio.exe для каждого файла сначала вызывает препроцессор, а затем выполняется анализ. При обработке нескольких файлов за раз PVS-Studio.exe сначала выполняет препроцессирование всех файлов, а затем для каждого файла еще раз запускается PVS-Studio.exe для анализа уже препроцессированных файлов.
Проверка Unreal Engine проектов доступна только для Enterprise лицензии PVS-Studio. Вы можете запросить пробную Enterprise лицензию здесь.
Для сборки проектов для Unreal Engine под Windows используется специализированная сборочная система Unreal Build System. Данная система интегрируется поверх сборочной системы, используемой средой Visual Studio \ JetBrains Rider (MSBuild), с помощью автогенерируемых makefile проектов MSBuild. Это специальный тип Visual C++ (vcxproj) проектов, в которых для непосредственного выполнения сборки выполняется команда, вызывающая стороннюю утилиту, например, (но далеко не обязательно) Make. Использование makefile проектов позволяет работать с кодом Unreal Engine из среды Visual Studio \ JetBrains Rider, используя такие преимущества, как автодополнение, подсветка синтаксиса, навигация по символам и т.п.
В связи с тем, что makefile проекты MSBuild сами по себе не содержат полной информации, необходимой для компиляции, а, следовательно, и препроцессирования исходных C/C++ файлов, PVS-Studio не поддерживает проверку таких проектов с помощью Visual Studio плагина или консольной утилиты PVS-Studio_Cmd.exe. Поэтому, для проверки таких проектов с помощью PVS-Studio можно пойти двумя путями - отслеживание вызовов компилятора (Compiler Monitoring) и прямая интеграция C/C++ анализатора PVS-Studio.exe в сборочную утилиту UnrealBuildTool. Рассмотрим эти варианты подробнее.
Для сборки на Windows Unreal Build System использует компилятор Visual C++ - cl.exe. Этот компилятор поддерживается системой отслеживания компиляторов PVS-Studio на Windows. Эту систему можно использовать как из приложения C and C++ Compiler Monitoring UI, так и с помощью консольной утилиты CLMonitor.exe.
Запуск мониторинга компиляций можно осуществлять вручную из приложения Compiler Monitoring UI, или назначить на событие начала\окончания сборки в Visual Studio. Результатом работы системы мониторинга является XML файл отчёта plog, который можно открыть в Visual Studio плагине PVS-Studio, или преобразовать к одному из стандартных форматов (txt, html, csv) с помощью специальной утилиты PlogConverter.
Подробное описание системы отслеживания вызовов компилятора доступно в данном разделе документации. Мы рекомендуем использовать данный способ запуска анализа для знакомства с анализатором, как наиболее простой в настройке.
Для того чтобы проанализировать проект, необходимо предварительно провести трассировку его полной сборки. Сборка проекта под Linux выполняется при помощи скрипта AutomationTool.
pvs-studio-analyzer trace -- \
<UnrealEngine source location>/UnrealEngine/Engine/
Build/BatchFiles/RunUAT.sh \
BuildCookRun -project="<Project path>/<Project name>.uproject" \
-build -Clean -targetplatform=Linux ....
pvs-studio-analyzer analyze ....
Подробная документация к утилите pvs-studio-analyzer находится на странице "Кроссплатформенная проверка C и C++ проектов в PVS-Studio".
Инструкции для работы с полученным после анализа отчётом находятся на странице "Просмотр и конвертация результатов анализа".
В Unreal Engine версии от 5.0.0, 5.0.1 и 5.0.2 имеется баг, из-за которого Unreal Engine Build Tool не находит ядро анализатора по пути по умолчанию: %ProgramFiles(x86)%\PVS-Studio\x64\PVS-Studio.exe:
На данный момент существует временное решение данной проблемы - необходимо скопировать файл PVS-Studio.exe расположенный в папке "%ProgramFiles(x86)%\PVS-Studio\x64" в папку "...\UE_5.0\Engine\Restricted\NoRedist\Extras\ThirdPartyNotUE\PVS-Studio".
Важно. Этот баг исправлен в релизной версии Unreal Engine 5.0.3.
В данном разделе рассматривается проверка Unreal Engine проектов на операционной системе Windows.
В случае с Unreal Build System, разработчики из Epic Games предоставляют возможность использовать PVS-Studio с помощью интеграции со сборочной утилитой UnrealBuildTool, начиная с версии Unreal Engine 4.17.
Перед началом анализа вам будет необходимо ввести лицензию на анализатор. Для этого нужно ввести ваши данные в IDE:
Обратите внимание, что до версии Unreal Engine 4.20, UBT не умел подхватывать лицензию из общего файла настроек PVS-Studio. В случае, если UBT не видит лицензию, введённую через интерфейс, вам необходимо создать отдельный файл лицензии с именем PVS-Studio.lic вручную и подложить его в директорию '%USERPROFILE%\AppData\Roaming\PVS-Studio'.
Примечание 1. При интеграции PVS-Studio со сборочной утилитой UnrealBuildTool сборки проекта не происходит. Это связано с тем, что UnrealBuildTool заменяет процесс сборки на процесс анализа.
UnrealBuildTool позволяет запускать анализ PVS-Studio, добавив данный флаг в строку запуска:
-StaticAnalyzer=PVSStudio
Например, полная строка запуска UnrealBuildTool может выглядеть так:
UnrealBuildTool.exe UE4Client Win32 Debug -WaitMutex -FromMsBuild
-StaticAnalyzer=PVSStudio -DEPLOY
Для включения анализа при запуске из IDE, откройте свойства проекта для выбранной конфигурации:
и добавьте флаг -StaticAnalyzer=PVSStudio в опции сборки и пересборки (Build Command Line / Rebuild All Command Line).
Примечание 1. Обратите внимание, что при таком сценарии использования сборка проекта выполняться не будет. Вместо этого будет выполнено препроцессирование всех (команда Rebuild) или изменившихся (команда Build) файлов проекта, а после запущен его анализ.
Примечание 2. Интеграция PVS-Studio с UnrealBuildTool поддерживает не все настройки анализатора, доступные из Visual Studio (PVS-Studio|Options...). В данный момент поддерживается выбор групп диагностик для анализа, добавление директорий-исключений через 'PVS-Studio|Options...|Don't Check Files' и фильтрация загруженных результатов анализа через 'Detectable Errors'.
Примечание 3. При открытии файла проекта uproject напрямую (не sln файла) данный способ не поддерживается. Используйте способы, описанные ниже.
Примечание 4. При использовании Unreal Engine версии 5.3 может возникнуть падение анализа из-за невозможности найти generated.h файлы самого Unreal Engine. Проблема связана с тем, как UE 5.3 запускает анализ. При запуске анализа с помощью флага, Unreal Build Tool создаёт в папке для промежуточных файлов ещё одну папку 'UnrealEditorSA'. Эта папка определяется как та, в которой нужно искать заголовочные файлы. Проектные файлы попадают в эту папку, а самого UE - нет. При этом, заголовочные файлы движка могут использоваться в проекте. Данная проблема была исправлена в Unreal Engine 5.4. Для решения проблемы, продолжая использовать в Unreal Engine 5.3, используйте интеграция через target-файл.
Вы можете интегрировать PVS-Studio в процесс сборки, модифицировав target-файл. Такой сценарий будет удобен при частой перегенерации проектных файлов.
Для интеграции вам необходимо добавить параметр 'StaticAnalyzer' со значением ' PVSStudio':
public MyProjectTarget(TargetInfo Target) : base(Target)
{
...
WindowsPlatform.StaticAnalyzer = WindowsStaticAnalyzer.PVSStudio;
...
}
public MyProjectTarget(TargetInfo Target) : base(Target)
{
...
StaticAnalyzer = StaticAnalyzer.PVSStudio;
...
}
Теперь UnrealBuildTool будет автоматически запускать анализ вашего проекта.
Вы можете интегрировать PVS-Studio в процесс сборки, модифицировав BuildConfiguration.xml файл.
Данный файл может быть найден по следующим путям:
Ниже приводим минимальный рабочий пример такого файла:
<?xml version="1.0" encoding="utf-8" ?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
<BuildConfiguration>
<StaticAnalyzer>
PVSStudio
</StaticAnalyzer>
</BuildConfiguration>
</Configuration>
Примечание 1. Пример выше будет отличаться в версии Unreal Engine ниже 5.1. Приводим ссылку на документацию и минимальный пример для этих версий:
<?xml version="1.0" encoding="utf-8" ?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
<WindowsPlatform>
<StaticAnalyzer>
PVSStudio
</StaticAnalyzer>
</WindowsPlatform>
</Configuration>
Анализ всего проекта может занять много времени. Инкрементный анализ помогает ускорить анализ, проверяя только те файлы, которые были изменены с момента последней сборки. Инкрементальный анализ запускается только, если ранее была произведена полная сборка проекта. Чтобы запустить инкрементный анализ, вам необходимо выполнить сборку Unreal Engine проекта (Build).
Например, если в проекте имеются файлы A.cpp, B.cpp и C.cpp, то в таком случае при первой "сборке" (анализе) проекта в Unreal Engine до версии 4.25 будут проанализированы все файлы. При следующей "сборке" (анализе) проекта, если не было изменено ни одного файла, то ни один файл и не будет проанализирован. Однако, если файлы A.cpp и B.cpp измененить, то проанализированы будут только эти два файла.
Важно. В версии UE 4.25 или новее вместо анализа измененных файлов, при "сборке" (анализе) проекта будут проанализированы все файлы из тех Unreal Engine модулей, в которые включены измененные файлы. Например, если имеется собранный ранее проект с двумя модулями "A_Module" и "B_Module", и в модуль "A_Module" включены файлы A1.cpp и A2.cpp, а в модуль "B_Module"файлы B1.cpp и B2.cpp, то при изменении файла B2.cpp и "сборке" (анализе) проекта будут проанализированы файлы B1.cpp и B2.cpp из модуля "B_Module". Это изменение ухудшило инкрементальный анализ тем, что теперь анализируются все файлы из модуля, если в нём был изменён хотя бы один файл. Однако, даже такой инкрементальный анализ позволяет ускорить время анализа проекта, если проект поделен на модули.
Если вам требуется настроить одновременную сборку проекта и его анализ в рамках одной конфигурации Visual Studio, для этого можно создать вспомогательные скрипты (в нашем примере назовём их, соответственно, BuildAndAnalyze и RebuildAndAnalyze) на основе стандартных скриптов Build и Rebuild. Основное изменение в скрипте RebuildAndAnalyze - вызов для сборки нового скрипта BuildAndAnalyze.bat, а не Build.bat.
В скрипт BuildAndAnalyze необходимо добавить удаление кэша действий и запуск UnrealBuildTool с флагом анализа после проведения успешной сборки.
В кэш сохраняются действия, выполняемые UBT в ходе работы (сборки, анализ и т.п.).
Восстановление кэша из бэкапа после анализа необходимо для того, чтобы вновь получить сохранённые действия сборки. Если UBT не обнаружит сохранённых действий сборки - она будет выполнена заново.
Удаление/восстановление кэша необходимо для того, чтобы не сохранять действия анализа, но при этом и не потерять действия по сборке проекта. Не сохранять действия анализа в кэш необходимо, потому что, если анализ будет проведен обновленной версией анализатора, в которой добавлены новые диагностические правила, то не модифицированные файлы проверены не будут. Удаление/восстановление кэша позволяет избежать подобной ситуации. Благодаря этому даже неизменные файлы будут проверены новыми диагностиками, В результате, эти диагностики могут обнаружить потенциальные ошибки или уязвимости, которые ранее не были обнаружены.
Примечание 1. Изменения, описанные ниже, основываются на стандартном скрипте Build и стандартной строке запуска скрипта. В случае, если используется модифицированный скрипт или нестандартный порядок аргументов, может потребоваться внесение дополнительных изменений.
Первоначально необходимо определить ряд переменных, которые будут необходимы для удаления/восстановления файла кэша действий.
Примечание 2. Файлы кэша действий в разных версиях движка Unreal Engine могут отличаться как расширением, так и расположением. Примите это во внимание при формировании скриптов.
SET PROJECT_NAME=%1%
SET PLATFORM=%2%
SET CONFIGURATION=%3%
SET UPROJECT_FILE=%~5
for %%i in ("%UPROJECT_FILE%") do SET "PROJECT_PATH=%%~dpi"
SET PREFIX=%PROJECT_PATH%Intermediate\Build\%PLATFORM%
SET ACTIONHISTORY_PATH=....
SET ACTIONHISTORY_BAC_PATH= "%ACTIONHISTORY_PATH:"=%.bac"
Для разных версий движка во фрагменте скрипта, приведённом выше, необходимо задать соответствующее значение ACTIONHISTORY_PATH.
SET ACTIONHISTORY_PATH="%PREFIX%\%PROJECT_NAME%\ActionHistory.bin"
SET ACTIONHISTORY_PATH="%PREFIX%\%PLATFORM%\%PROJECT_NAME%\ActionHistory.dat"
SET ACTIONHISTORY_PATH="%PREFIX%\%PROJECT_NAME%\ActionHistory.dat"
REM Если у вас есть конфигурации сборки для Client/Server,
REM то вам необходимо учесть их при определении переменной UE_FOLDER.
echo %PROJECT_NAME% | findstr /c:"Editor">nul ^
&& (SET UE_FOLDER=UE4Editor) || (SET UE_FOLDER=UE4)
SET ACTIONHISTORY_PATH="%PREFIX%\%UE_FOLDER%\%CONFIGURATION%\ActionHistory.bin"
REM Если у вас есть конфигурации сборки для Client/Server,
REM то вам необходимо учесть их при определении переменной UE_FOLDER.
echo %PROJECT_NAME% | findstr /c:"Editor">nul ^
&& (SET UE_FOLDER=UnrealEditor) || (SET UE_FOLDER=UnrealGame)
SET ACTIONHISTORY_PATH="%PREFIX%\%UE_FOLDER%\%CONFIGURATION%\ActionHistory.bin"
После вызова UnrealBuildTool для сборки (и команды 'popd') необходимо добавить следующий код:
SET "UBT_ERR_LEVEL=!ERRORLEVEL!"
SET "NEED_TO_PERFORM_ANALYSIS="
IF "!UBT_ERR_LEVEL!"=="0" (
SET "NEED_TO_PERFORM_ANALYSIS=TRUE"
)
IF "!UBT_ERR_LEVEL!"=="2" (
SET "NEED_TO_PERFORM_ANALYSIS=TRUE"
)
IF DEFINED NEED_TO_PERFORM_ANALYSIS (
pushd "%~dp0\..\..\Source"
ECHO Running static analysis
IF EXIST %ACTIONHISTORY_PATH% (
ECHO Backup %ACTIONHISTORY_PATH%
COPY %ACTIONHISTORY_PATH% %ACTIONHISTORY_BAC_PATH%
ECHO Removing %ACTIONHISTORY_PATH%
DEL %ACTIONHISTORY_PATH%
)
..\..\Engine\Binaries\DotNET\UnrealBuildTool.exe
%* -StaticAnalyzer=PVSStudio -DEPLOY
popd
IF EXIST %ACTIONHISTORY_BAC_PATH% (
ECHO Recovering %ACTIONHISTORY_PATH%
COPY %ACTIONHISTORY_BAC_PATH% %ACTIONHISTORY_PATH%
ECHO Removing %ACTIONHISTORY_BAC_PATH%
DEL %ACTIONHISTORY_BAC_PATH%
)
)
Наиболее важными операциями из приведённого выше кода являются удаление и восстановление кэша, а также запуск UnrealBuildTool с флагом -StaticAnalyzer=PVSStudio для проведения анализа.
При необходимости можно использовать модифицированный скрипт при работе из IDE. Для этого необходимо указать его в качестве используемого в свойствах проекта:
Примечание. Обратите внимание, что при использовании модифицированных скриптов флаг -StaticAnalyzer=PVSStudio в аргументах запуска скрипта указывать не нужно, так как он уже выставляется в скрипте при запуске UnrealBuildTool для анализа.
Проверка самого Unreal Engine не отличается от проверки другого UE проекта.
В обоих случаях будут проанализированы все файлы, переданные на сборку. Если при сборке проекта должны собираться модули самого Unreal Engine, то они тоже будут проверены.
Для анализа отдельного решения Unreal Engine добавьте код, представленный в пункте интеграции через target файл в UnrealEditor.Target.cs (располагается в директории UnrealEngine/Engine/Source):
После этого запустите сборку с таргетом Editor (например, Development Editor).
Вы также можете воспользоваться вторым способом запуска анализа: откройте свойства проекта UE*, выберите в пункте NMake поле Build Command Line и добавьте в команду флаг
-StaticAnalyzer=PVSStudio
После этого запустите сборку Unreal Engine.
Включение различных групп диагностик анализатора доступно начиная с версии Unreal Engine 4.25.
Для выбора нужных групп диагностик необходимо модифицировать target-файлы проекта.
Например, включить диагностики микро-оптимизаций можно следующим образом:
public MyUEProjectTarget( TargetInfo Target) : base(Target)
{
....
WindowsPlatform.PVS.ModeFlags =
UnrealBuildTool.PVSAnalysisModeFlags.Optimizations;
}
Допустимые значения для включения соответствующих групп диагностик:
Для включения нескольких групп диагностик используйте оператор '|':
WindowsPlatform.PVS.ModeFlags =
UnrealBuildTool.PVSAnalysisModeFlags.GeneralAnalysis
| UnrealBuildTool.PVSAnalysisModeFlags.Optimizations;
C версии Unreal Engine 5.1 стали доступны ещё несколько настроек, которые можно задать в target-файлах:
WindowsPlatform.PVS.AnalysisTimeoutFlag — задаёт таймаут для анализа одного файла. Этому свойству возможно присвоить одно из значений перечисления AnalysisTimeoutFlags:
WindowsPlatform.PVS.EnableNoNoise — отключает выдачу всех предупреждений 3-его уровня (Low).
WindowsPlatform.PVS.EnableReportDisabledRules — включает отображение исключений из правил анализатора, которые могут быть указаны в комментариях и файлах .pvsconfig. Сообщения о том, откуда взята информация об исключении правил анализатора, выводятся в результаты анализа в виде предупреждений с кодом V012.
Если в target-файле включено использование настроек из Settings.xml файла:
public MyUEProjectTarget( TargetInfo Target) : base(Target)
{
....
WindowsPlatform.PVS.UseApplicationSettings = true;
}
то можно задать настройки через интерфейсы плагинов для Visual Studio или Rider.
Настройки по включению и отключению групп диагностик в Visual Studio находятся в 'Extensions -> PVS-Studio -> Options... -> Detectable Errors (C, C++)':
В Rider настройки по включению и отключению групп диагностик находятся в 'Tools -> PVS-Studio -> Settings... -> Warnings':
С версии Unreal Engine 5.1 также доступны некоторые дополнительные настройки через интерфейс плагинов для Visual Studio и Rider.
В Visual Studio в 'Extensions -> PVS-Studio -> Options... -> Specific Analyzer Settings -> Analysis':
В Rider в 'Tools -> PVS-Studio -> Settings... -> Analysis -> Analysis Timeout':
По умолчанию, UnrealBuildTool объединяет единицы трансляции в файлы большого размера для оптимизации сборки. Такое поведение помогает оптимизировать время сборки, однако может помешать при анализе кода. Т.к. на анализ идут файлы большого размера, то возможна ситуация нехватки памяти.
Мы настоятельно рекомендуем отключать Unity Build, чтобы избежать проблемы нехватки памяти при анализе.
Unity Build можно отключить в *.Traget.cs файле, указав bUseUnityBuild = false;
Чтобы данная настройка влияла только на процесс анализа, не замедляя сборку, вы можете добавить в *.Target.cs файл следующий код:
Для Unreal Engine версии 5.0 и ниже:
public UE4ClientTarget(TargetInfo Target) : base(Target)
{
...
if (WindowsPlatform.StaticAnalyzer == WindowsStaticAnalyzer.PVSStudio)
{
bUseUnityBuild = false;
}
...
}
Для Unreal Engine версии 5.1 и выше:
public UE5ClientTarget(TargetInfo Target) : base(Target)
{
...
if (StaticAnalyzer == StaticAnalyzer.PVSStudio)
{
bUseUnityBuild = false;
}
...
}
Для запуска анализа вам достаточно указать флаг -StaticAnalyzer=PVSStudio в команде сборки NMake. UnrealBuildTool будет выключать Unity Build, если для параметра StaticAnalyzer указано значение PVSStudio.
В Unreal Engine, начиная с версии 5.4, доступна настройка, позволяющая запускать анализатор только на файлах проекта (пропуская модуль ядра Unreal Engine). Использование этой настройки позволяет значительно ускорить процесс анализа.
Включить настройку можно добавлением в строку запуска UnrealBuildTool флага:
-StaticAnalyzerProjectOnly
В target.cs файле эта настройка включается так:
bStaticAnalyzerProjectOnly = true;
Для отключения анализа автогенерируемых файлов воспользуйтесь инструкцией по исключению файлов из анализа при помощи PathMasks. Добавьте маску *.gen.cpp в настройку плагина PathMasks.
Примечание: начиная с Unreal Engine 5.4, анализ автогенерируемых файлов отключён по умолчанию. Для возвращения этой функциональности вы можете использовать флаг ‑StaticAnalyzerIncludeGenerated.
В Unreal Engine 5.4 добавили настройку, которая задаёт уровень предупреждений, выдаваемых UnrealBuildTool при анализе. Она не влияет на работу PVS-Studio, однако может приводить к замедлению при получении отчёта. По умолчанию настройка включена (значение = 1).
Выключение (значение = 0) помогает избежать вышеуказанной проблемы. Для этого нужно добавить в строку запуска UnrealBuildTool флаг:
-StaticAnalyzerPVSPrintLevel=0
В target.cs файле настройка задаётся так:
StaticAnalyzerPVSPrintLevel = 0;
Для автоматического запуска анализа, например, в CI/CD системах или скриптах, достаточно вызвать команду сборки проекта.
Для получения команды сборки откройте свойства проекта, выберите пункт NMake. В поле Build Command Line будет содержаться искомая команда.
Если вы интегрируете PVS-Studio через .target.cs файл или BuildConfiguration.xml файл, то просто используйте полученную команду сборки. Если используете интеграцию через флаг UnrealBuildTool, то добавьте его в команду сборки.
Команда сборки может выглядеть так:
"Path_to_UE\Engine\Build\BatchFiles\Build.bat" ProjectGame Win64 DebugGame ^
-Project="Path_to_projet\ProjcetGame.uproject" ^
-WaitMutex -FromMsBuild -StaticAnalyzer=PVSStudio
При запуске анализа в Rider непосредственно из .uproject файла есть несколько вариантов запуска анализа.
О первых двух вариантах написано в пунктах выше: Интеграция PVS-Studio через файл target файл и Интеграция PVS-Studio через Build Configuration файл.
Третий вариант заключается в генерации .sln файла и открытии проекта через него. В таком случае появится возможность добавить -StaticAnalyzer=PVSStudio флаг в команду сборки NMake.
Примечание. Открытие Unreal Engine проектов непосредственно через .uproject файл доступно в версиях Rider 2022.3 и выше. В остальных версиях возможно открыть проект только с помощью .sln файла.
Путь до файла с результатами анализа будет выведен в Output (Build) окно IDE (или stdout, если вы запускали UnrealBuildTool вручную из командной строки). Данный файл с результатами анализа имеет "нераспарсенный" вид - его можно открыть в IDE:
А также результаты анализа можно преобразовать с помощью утилиты PlogConverter, как это было описано в разделе выше для XML лога.
Особенности работы со списком диагностических предупреждений более подробно описаны в статье "Работа со списком диагностических сообщений", а работа с отчётом анализатора - в статье "Работа с XML отчётом (.plog файл)".
Более удобным вариантом использования является автоматическая загрузка лога анализа в окно вывода PVS-Studio при работе в IDE. Для такого сценария необходимо включить соответствующую опцию:
В настройках плагинов PVS-Studio для Visual Studio и PVS-Studio for Rider и утилиты C and C++ Compiler Monitoring UI (Standalone.exe) имеется возможность указать маски директорий. Если в полном пути до файла имеется фрагмент, совпадающий с одной из масок в PathMasks, то этот файл исключается из анализа.
Важно. Для Unreal Engine проектов актуальны только маски директорий (PathMasks), но не маски имен файлов (FileNameMasks).
В плагине для Visual Studio данные настройки находятся в Extensions > PVS-Studio > Options... > Don't Check Files:
В Rider маски для исключения директорий из анализа находятся в Tools > PVS-Studio > Settings > Excludes:
Аналогичные настройки имеются и в C and C++ Compiler Monitoring UI (Standalone.exe) в Tools > Options... > Don't Check Files:
Среди этих масок по умолчанию имеется специальная маска для исключения исходного кода Unreal Engine: \Epic Games\UE_. Эта маска добавлена по умолчанию, чтобы результаты анализа не засорялись лишними предупреждениями, информация о которых вряд ли окажется полезной большинству разработчиков.
Примечание. Если Unreal Engine установлен не по пути по умолчанию, то в отчёт анализатора будут попадать предупреждения, выдающиеся на исходный код Unreal Engine. Если они вам мешают, то добавьте в PathMasks директорию, в которую установлен Unreal Engine.
Более подробная информация об исключении файлов из анализа описана в разделе "Настройки: Don't Check Files".
Многочисленные срабатывания анализатора на старом legacy коде часто мешают регулярному использованию статического анализа. Такой код обычно уже хорошо оттестирован и стабильно работает, поэтому править в нём все срабатывания может быть нецелесообразно. Тем более, если размер кодовой базы достаточно велик, такая правка может занять огромное количество сил и времени. Если же срабатывания анализатора на существующий код не трогать, то они будут мешать работать со срабатываниями на свежий код.
Чтобы решить данную проблему и начать сразу регулярно использовать статический анализ, PVS-Studio предлагает возможность "отключить" сообщения на старом коде. Для этого в плагинах PVS-Studio для Visual Studio и JetBrains Rider имеются элементы интерфейса, которые позволяют подавлять предупреждения анализатора в этих IDE.
Имеется единственное отличие для пользователя в механизме подавления предупреждений для Unreal Engine проектов в этих IDE. Заключается оно в том, что для UE проектов пользователю необходимо добавить вручную один suppress файл к solution, а для не-UE проектов suppress файлы также могут добавляться автоматически к каждому проекту.
Если вы попытаетесь подавить предупреждения в Visual Studio или Rider для UE проекта, когда solution suppress файл не существует, то вы получите предупреждение. В Visual Studio:
И в Rider:
В этих сообщениях содержится описание действий для добавления suppress файла уровня solution.
Более подробную информацию о подавлении предупреждений в этих IDE вы найдете в соответствующих разделах документации:
Более подробная информация о механизме подавления предупреждений находится в разделе документации "Подавление сообщений анализатора (отключение выдачи предупреждений на существующий код)".
Если вы запускаете сборку Unreal Engine проекта через командную сроку (с помощью UnrealBuildTool.exe через пакетные файлы Build.bat или Rebuild.bat), вам может быть удобно использовать подавления предупреждений анализатора через консольную утилиту PVS-Studio_Cmd.exe. Для этого в ней имеются режимы SuppressOnly и FilterFromSuppress.
Режим SuppressOnly позволяет сохранить полученные ранее предупреждения анализатора в suppress файл. Пример команды создания suppress файла:
PVS-Studio_Cmd.exe -t path/to/solution/file ^
-a SuppressOnly ^
-o path/to/report.pvslog ^
-u path/to/suppress_file.suppress
В результате выполнения данной команды все предупреждения анализатора из отчёта path/to/report.pvslog будут добавлены в path/to/suppress_file.suppress. Если suppress файла из аргумента -u не существует, то он будет создан.
Режим FilterFromSuppress позволяет использовать suppress файл для фильтрации сообщений из отчёта. Пример команды фильтрации отчёта анализатора при помощи suppress файла:
PVS-Studio_Cmd.exe -t path/to/solution/file ^
-a FilterFromSuppress ^
-o path/to/report.pvslog ^
-u path/to/suppress_file.suppress
В результате выполнения этой команды рядом с отчётом path/to/report.pvslog будет создан файл path/to/report_filtered.plog. В него будут записаны все предупреждения анализатора, которые отсутствуют в path/to/suppress_file.suppress.
Чтобы иметь в отчете предупреждения анализатора только для нового или измененного кода, изначально необходимо получить suppress файл, который будет служить начальной "точкой отсчёта". Для получения suppress файла необходимо выполнить следующие действия:
Отчёт анализатора в формате .plog, полученный после фильтрации в режиме FilterFromSuppress, возможно открыть в Visual Studio с установленным плагином для PVS-Studio или в утилите C and C++ Compiler Monitoring UI. Также возможно конвертировать .plog отчет в другие форматы при помощи утилиты PlogConverter.exe в Windows.
Начиная с релиза Unreal Engine 5.5, появилась возможность подавления предупреждений с помощью файла '.Build.cs'. Для этого необходимо прописать в файле следующую строку:
StaticAnalyzerPVSDisabledErrors.Add("V###");
где "V###" - номер диагностического правила, которое необходимо отключить.
Если необходимо отключить несколько предупреждений, их номера можно перечислить через пробел:
StaticAnalyzerPVSDisabledErrors.Add("V### V###");
Важно отметить, что файл '.Build.cs' генерируется для каждой директории проекта, и диагностические правила, прописанные в этом файле, отключаются только для той директории, в которой он находится.
SN-DBS – это система распределённой сборки приложений, разрабатываемая компанией SN Systems. До версии UE 5.5 интеграция PVS-Studio не работала вместе с интеграцией SN-DBS. При попытке запуска параллельного анализа проекта анализ проходил только на части файлов, анализируемых на мастер-ноде SN-DBS. Логи сборки при этом содержали сообщение "fatal error C1510: Cannot load language resource clui.dll."
Способы решения:
Данная проблема была исправлена в релизе UE 5.5.
Связка UnrealBuildTool + PVS-Studio работает следующим образом:
На любом из вышеперечисленных этапов может возникнуть ошибка - например, если анализатор не смог проверить один из исходных файлов. Поэтому, если анализ завершился неудачно, то для выяснения и исправления причин сразу же пишите нам, и мы постараемся исправить проблему как можно быстрее.
Пожалуйста, прикрепите к письму дополнительные файлы, которые помогут нам понять причину ошибки и воспроизвести её:
Unity проекты, написанные на C#, можно проверить с помощью PVS-Studio из консоли или IDE. Проверяемый Unity проект должен успешно собираться, чтобы результаты проверки были максимально корректными и полными.
Загрузить анализатор можно на этой странице.
Перед началом работы с PVS-Studio нужно ввести данные лицензии. Здесь написано, как это сделать.
Перед началом работы с кодом Unity проекта необходимо установить в настройках Unity предпочитаемый редактор скриптов. Это можно сделать с помощью параметра "External Script Editor" на вкладке "External Tools" в окне "Preferences". Чтобы открыть это окно, используйте опцию меню "Edit" -> "Preferences" в редакторе Unity:
После этого открыть Unity проект в IDE можно, используя опцию "Assets" -> "Open C# Project" в редакторе Unity:
Также вы можете открыть сгенерированный Unity .sln файл из IDE. Если .sln файл отсутствует, то необходимо произвести вышеописанные действия в редакторе Unity. Тогда .sln будет сгенерирован.
Проанализировать Unity проект можно из Visual Studio. Руководство по использованию плагина доступно по ссылке.
Анализ может быть выполнен для следующих элементов:
Запустить анализ можно из подменю плагина PVS-Studio в меню "Extensions" на верхней панели меню:
Также запуск анализа доступен из окна "Solution Explorer" через контекстное меню по нажатию на элементе решения.
Отчёт об анализе доступен в окне "PVS-Studio":
Проанализировать Unity проект можно из JetBrains Rider. Возможности этого плагина и его применение аналогичны таковым для плагина Visual Studio. Подробная инструкция по использованию плагина для JetBrains Rider доступна по ссылке.
Запустить анализ можно через главное меню:
PVS-Studio можно запускать из командной строки. Это удобно, если вы хотите интегрировать анализ проекта в вашу инфраструктуру CI/CD.
Руководство по использованию CLI версии PVS-Studio доступно по ссылке.
Вы можете проверить всё решение (sln), отдельные проекты (csproj) или некоторый набор файлов исходного кода. В последнем случае путь к .sln/.csproj файлу также обязателен.
Для анализа C# проектов на Windows используется "PVS-Studio_Cmd.exe", на Unix-like системах "pvs-studio-dotnet".
Пример запуска PVS-Studio через CLI на Windows:
PVS-Studio_Cmd.exe -t D:\UnityProjects\MyProject\MyProject.sln
Пример запуска PVS-Studio через CLI на Unix-like системах:
pvs-studio-dotnet -t ./MyUnityProject.sln
По умолчанию анализатор генерирует отчёты в формате:
Отчёты можно открывать с помощью IDE плагинов PVS-Studio.
Примечание: отчёты в формате plog не поддерживается в Rider. Кроме того, под Linux и macOS не поддерживается конвертация таких отчётов.
Ниже представлено меню плагина под Visual Studio:
Отчёты можно сконвертировать в более удобный для восприятия человеком формат.
Для конвертации используется CLI утилита PlogConverter. На Windows используется "PlogConverter.exe", на Unix-like системах "plog-converter". Руководство к утилите доступно здесь.
Пример конвертации отчёта в формат HTML на Windows (вводится в одну строку):
PlogConverter.exe D:\UnityProjects\MyProject\MyProject.plog
-t FullHtml
-o D:\UnityProjects\MyProject\
Пример конвертации отчёта в формат HTML на Unix-like системах:
plog-converter –t fullhtml ./ ./MyProject.plog
Эти команды конвертируют отчёт в формат HTML, который удобен для просмотра в браузере и для рассылки по почте. Пример отчёта в формате FullHTML:
Если в Unity в настройках генерации C# проектов выбрана опция "Player projects", то происходит создание solution файла с дубликатами названий проектов. При анализе проекта с помощью PVS-Studio это может приводить к ошибке следующего вида: The solution file has two projects named "UnityEngine.UI". Для проведения анализа эту опцию требуется отключить.
После этого может понадобиться заново сгенерировать проектные файлы. Для этого есть кнопка ниже – "Regenerate project files".
После того как дубликаты из sln-файла пропадут, анализ должен работать нормально. Подробнее эта проблема с повторяющимися именами проектов описана здесь.
Во время анализа мы рекомендуем не совершать действий, которые могут привести к изменению sln/csproj файлов или файлов исходного кода. Например, это такие действия, как изменение или создание Assembly Definitions и Assembly Definition References, добавление новых скриптов.
Статический анализатор кода PVS-Studio Java состоит из 2-х основных частей: ядра, выполняющего анализ, и плагинов для интеграции в сборочные системы (Maven, Gradle) и IDE (PVS-Studio для IntelliJ IDEA и Android Studio).
Функции плагинов:
На Windows ядро Java анализатора возможно установить через инсталлятор PVS-Studio для Windows. Скачать его можно на странице загрузки.
Кроме этого, на всех операционных системах возможно скачать ZIP архив для Java на странице загрузки. В этом архиве содержится ядро Java анализатора (папка с именем 7.34.87460 в директории pvs-studio-java). Ядро анализатора надо распаковать в необходимое вам место или по стандартному пусти установки ядра Java анализатора:
Для получения информации про все доступные аргументы анализатора, необходимо выполнить команду ‑‑help:
java -jar pvs-studio.jar --help
Аргументы анализатора:
Перед запуском анализа вам необходимо будет ввести лицензию. Как это сделать — можно узнать в этой документации.
Примеры быстрого запуска ядра Java анализатора:
java -jar pvs-studio.jar -s A.java B.java C.java -e Lib1.jar Lib2.jar -j4
-o report.txt -O text --user-name someName –-license-key someSerial
java -jar pvs-studio.jar -s src/main/java --ext-file classpath.txt -j4
-o report.txt -O text --license-path PVS-Studio.lic
Обратите внимание:
По умолчанию анализатор запускает ядро с java из переменной окружения PATH. Если необходимо запустить анализ с какой-то другой версией, ее можно указать вручную. Для этого запустите ядро Java анализатора, используя полный путь до java файла из JDK. Версия этой JDK (версия языка Java) будет использована при анализе исходного кода проекта:
/path/to/jdk_folder/bin/java -jar pvs-studio.jar ^
-s A.java B.java C.java -e Lib1.jar Lib2.jar -j4 ^
-o report.txt -O text --user-name someName –-license-key someSerial
Для упрощения команды запуска анализа можно вынести аргументы командной строки в специальный JSON-файл. В дальнейшем этот файл можно передать в ядро анализатора через флаг ‑‑cfg.
Синтаксис файла выглядит следующим образом:
{
"single-value-parameter": "value",
"multiple-values-parameter": ["value1", "value2", "value3"]
}
Каждый параметр в файле конфигурации представляет собой полное имя флага командной строки со значением, которое будет передано в этом флаге.
Пример файла конфигурации:
{
"src": ["A.java", "B.java", "C.java"],
"threads": 4,
"output-file": "report.txt",
"output-type": "text",
"user-name": "someName",
"license-key": "someSerial"
....
}
В таком случае запуск анализатора сведётся к следующей строке:
java -jar pvs-studio.jar –-cfg cfg.json
Обратите внимание, что параметры, переданные через командную строку, имеют более высокий приоритет, чем параметры, заданные в файле конфигурации.
Часть настроек ядро Java анализатора берёт из файла global.json. Этот файл расположен в папке по стандартному пути установки ядра Java анализатора:
Вот список этих настроек:
Эти значения по умолчанию используются для всех ядер Java анализатора в системе, а также плагинами PVS-Studio для Java. При необходимости значения этих параметров можно изменить. Например, чтобы все плагины PVS-Studio для Java использовали одно и то же количество потоков для анализа.
Информацию о том, какие из этих настроек возможно переопределить в плагинах PVS-Studio для Java, вы найдёте в соответствующих разделах документации:
Каждый раз при запуске анализа проверяется наличие новой версии анализатора PVS-Studio. Если вышла новая версия анализатора, то в файле с результатами анализа будет содержаться сообщение: "A newer version of PVS-Studio is available (7.34.87460)". Это сообщение содержит последнюю версию ядра Java анализатора.
Кроме этого, получить последнюю версию PVS-Studio можно из файла по ссылке.
Для обновления ядра Java анализатора скачайте ZIP архив на странице загрузки. В этом архиве содержится ядро Java анализатора (папка с именем 7.34.87460 в директории pvs-studio-java). Ядро Java анализатора надо распаковать в необходимое место или в стандартную директорию:
Весь этот процесс можно автоматизировать при помощи различных скриптов, что позволит всегда использовать последнюю версию ядра Java анализатора.
Статический анализатор С/C++ кода PVS-Studio представляет собой консольное приложение с именем pvs-studio и несколько вспомогательных утилит. Также присутствуют интеграции с VS Code, Qt Creator, CLion в качестве плагина, предоставляющие удобный интерфейс для анализа проектов. Для работы программы необходимо иметь настроенное окружение для сборки вашего проекта.
Существуют три основных режима работы анализатора:
Примеры команд для установки анализатора из пакетов и репозиториев приведены на этих страницах:
Вы можете запросить лицензию для знакомства с PVS-Studio через форму. О том, как ввести полученную лицензию в Linux и macOS, написано здесь.
Перед запуском анализа необходимо выполнить один из следующих шагов для получения модели сборки проекта.
Важно. Проект должен успешно компилироваться и быть собран перед анализом.
На этой странице приведены инструкции для запуска анализа на основе файлов compile_commands.json. Он подходит для широко используемых инструментов сборки, таких как: CMake, Ninja, GNU Make, Qt Build System, Xcode.
Для работы этого метода необходима установленная утилита strace. С её помощью анализатор может собрать необходимую информацию о компиляции проекта во время его сборки.
Собрать проект и отследить процесс его компиляции можно с помощью команды:
pvs-studio-analyzer trace -- make
Вместо команды make может быть любая команда запуска сборки проекта со всеми необходимыми параметрами, например:
pvs-studio-analyzer trace -- make debug
В результате трассировки по умолчанию будет сформирован файл strace_out. Подробнее о режиме трассировки можно узнать здесь.
После получения файла трассировки компиляции (strace_out) или JSON Compilation Database (compile_commands.json) можно запустить анализ, выполнив команду:
pvs-studio-analyzer analyze -o /path/to/PVS-Studio.log \
-e /path/to/exclude-path \
-j<N>
plog-converter -a GA:1,2 \
-t json \
-o /path/to/Analysis_Report.json \
/path/to/PVS-Studio.log
Команда analyze предполагает наличие файлов strace_out или compile_commands.json в текущем рабочем каталоге. Можно указать явно расположение этих файлов при помощи флага ‑‑file (-f).
Предупреждения анализатора будут сохранены в указанный Analysis_Report.json файл. Разные способы просмотра и фильтрации полученного отчёта приведены в разделе этого документа "Просмотр и фильтрация отчёта анализатора".
В таком случае компиляторы могут иметь специальные имена и анализатор не сможет их найти. Для проверки такого проекта необходимо явно перечислить имена компиляторов без путей:
pvs-studio-analyzer analyze ... --compiler COMPILER_NAME
--compiler gcc --compiler g++ --compiler COMPILER_NAME
plog-converter ...
Также при использовании кросс-компиляторов изменится каталог с заголовочными файлами компилятора. Чтобы анализатор не выдавал предупреждения на эти файлы, необходимо исключить такие директории из анализа с помощью флага -e:
pvs-studio-analyzer ... -e /path/to/exclude-path ...
Если pvs-studio-analyzer неправильно определяет тип кросс-компилятора и, как следствие, неправильно запускает препроцессор, вы также можете задать его явно через этот флаг:
pvs-studio-analyzer analyze ... ‑‑compiler CustomCompiler=gcc
Теперь pvs-studio-analyzer запустит CustomCompiler с флагами препроцессирования gcc. Подробнее об этом можно прочитать тут.
При интеграции анализатора в сборочную систему проблем с кросс-компиляторами не возникает.
Вы можете передавать response-файлы в pvs-studio-analyzer. Response-файл – это файл, содержащий аргументы командной строки.
Response-файл можно передать через параметр командной строки, который начинается на символ '@'. После этого символа следует путь до response-файла (например, '@/путь/до/файла.txt'). Аргументы в response-файле разделены пробелами/табуляцией/переносами строк. Если вы хотите передать аргумент, который содержит пробельный символ, то вы можете экранировать этот символ с помощью обратного слэша (\) или обернуть весь аргумент в одинарные ('') или двойные ("") кавычки. Кавычки внутри кавычек экранировать нельзя. Разницы между одинарными и двойными кавычками нет. Обратите внимание, что аргументы передаются без изменений, подстановок значений переменных среды и раскрытия glob'ов не происходит. Рекурсивные response-файлы поддерживаются.
Для утилиты pvs-studio-analyzer доступен режим инкрементального анализа (анализ только изменённых файлов). Для этого необходимо запустить утилиту с параметром ‑‑incremental:
pvs-studio-analyzer analyze ... --incremental ...
Этот режим работает независимо от инкрементальной сборки проекта. Т.е. если Ваш проект полностью скомпилирован, то первый запуск инкрементального анализа всё равно будет анализировать все файлы. А при следующем запуске будут анализироваться только изменённые.
Для отслеживания изменённых файлов, анализатор сохраняет служебную информацию в каталоге с именем .PVS-Studio в директории запуска. Поэтому для использования этого режима необходимо всегда запускать анализатор в одной и той же директории.
Утилита pvs-studio-analyzer позволяет анализировать списки указанных файлов проекта. С помощью этого режима можно настроить анализатор для проверки коммитов и Pull Requests. Для анализа необходимо запустить утилиту с параметром ‑‑source-files или -S и указать путь к файлу со списком исходных файлов для анализа:
pvs-studio-analyzer analyze ... -S source_file_list ...
Более подробно изучить анализ списков файлов можно на странице "Проверка коммитов и Pull Requests".
Тестовые проекты доступны в официальном репозитории PVS-Studio на GitHub:
На рисунке 1 представлен пример просмотра предупреждений анализатора в CLion (подробнее здесь):
Рисунок 1 - Просмотр предупреждений PVS-Studio в CLion
На рисунке 2 представлен пример просмотра предупреждений анализатора в QtCreator:
Рисунок 2 - Просмотр предупреждений PVS-Studio в QtCreator
Инструкция по проверке CMake проектов в среде Qt Creator находится на странице "Использование PVS-Studio в среде Qt Creator".
Для QtCreator существует расширение PVS-Studio for QtCreator, о нём больше информации тут.
На рисунке 3 представлен пример просмотра предупреждений анализатора в Eclipse CDT:
Рисунок 3 - Просмотр предупреждений PVS-Studio в Eclipse CDT
Анализатор проверяет не исходные файлы, а препроцессированные файлы. Такой способ позволяет проводить более глубокий и качественный анализ исходного кода.
В связи с этим, у нас есть ограничения на передаваемые параметры компиляции. К ним относятся параметры, мешающие запуску компилятора в режиме препроцессора, либо "портящие" вывод препроцессора. Ряд флажков оптимизации и отладки, например, -O2, -O3, -g3, -ggdb3 и другие, вносят изменения, "портящие" вывод препроцессора. Информация о недопустимых параметрах будет выведена анализатором при их обнаружении.
Этот факт ни в коем случае не обязывает вносить изменения в настройки проверяемого проекта, но для правильного запуска анализатора часть параметров необходимо исключить.
При интеграции анализатора в сборочную систему ему необходимо передать файл с настройками (*.cfg). Имя конфигурационного файла может быть произвольным, и его необходимо передать с флагом "‑‑cfg".
Возможные значения настроек в конфигурационном файле:
'skip-analysis' - задаёт директорию, файлы из которой проверять не надо. Обычно это каталоги системных файлов или подключаемых библиотек.
'skip-settings' - игнорирует настройки, расположенные в исходных файлах и файлах '.pvsconfig' по указанному пути.
'skip' - игнорирует настройки, расположенные в исходных файлах и файлах '.pvsconfig' по указанному пути. Также будут отфильтрованы предупреждения, сгенерированные для файлов исходного кода по указанному пути или маске.
Необязательно создавать новый конфигурационный файл для проверки каждого файла. В него достаточно сохранить постоянные настройки, например, lic-file и т.п.
Любой из перечисленных способов интеграции анализатора в сборочную систему можно автоматизировать в системе Continuous Integration. Это можно сделать в Jenkins, TeamCity и других, настроив автоматический запуск анализа и уведомление о найденных ошибках.
Также возможна интеграция с платформой непрерывного анализа SonarQube с помощью плагина PVS-Studio. Плагин предоставляется с анализатором в .tgz архиве, доступном для загрузки. Инструкция по настройке доступна на странице: "Интеграция результатов анализа PVS-Studio в SonarQube".
Результаты анализа PVS-Studio можно загружать в DevSecOps платформу DefectDojo, преобразовав в специальный формат. Подробнее об интеграции отчетов можно прочесть тут.
Для конвертации отчёта анализатора о найденных ошибках в различные форматы (*.xml, *.tasks и т.п.) можно воспользоваться утилитой Plog Converter, которая распространяется с открытым исходным кодом. Подробнее об утилите можно прочесть тут.
Пример команды, которая подойдёт большинству пользователей для открытия отчёта в QtCreator:
plog-converter -a GA:1,2 -t tasklist
-o /path/to/project.tasks /path/to/project.log
На рисунке 4 представлен пример просмотра .tasks файла в QtCreator:
Рисунок 4 - Просмотр .tasks файла в QtCreator
Конвертер отчётов анализатора позволяет генерировать Html отчёт двух видов:
1. FullHtml - полноценный отчёт для просмотра результатов анализа. Есть возможность поиска и сортировки сообщений по типу, файлу, уровню, коду и тексту предупреждения. Особенностью этого отчёта является возможность навигации к месту ошибки в файле с исходным кодом. Сами файлы с исходным кодом, на которых были предупреждения анализатора, копируются в html и являются частью отчёта. Примеры отчёта приведены на рисунках 5-6.
Рисунок 5 - Пример главной страницы Html отчёта
Рисунок 6 - Просмотр предупреждения в коде
Пример команды для получения такого отчёта:
plog-converter -a GA:1,2 -t fullhtml
/path/to/project.log -o /path/to/report_dir
Такой отчёт удобно рассылать в архиве или предоставлять к нему доступ по локальной сети с помощью любого веб-сервера, например, Lighttpd и т.п.
2. Html - легковесный отчёт, состоящий из одного файла в формате .html. Содержит краткую информацию о найденных предупреждениях и подходит для уведомления о результатах по электронной почте. Пример отчёта приведен на рисунке 7.
Рисунок 7 - Пример простой Html страницы
Пример команды для получения такого отчёта:
plog-converter -a GA:1,2 -t html
/path/to/project.log -o /path/to/project.html
Пример команд для открытия отчёта в редакторе gVim:
$ plog-converter -a GA:1,2 -t errorfile
-o /path/to/project.err /path/to/project.log
$ gvim /path/to/project.err
:set makeprg=cat\ %
:silent make
:cw
На рисунке 8 представлен пример просмотра .err файла в gVim:
Рисунок 8 - Просмотр .err файла в gVim
Пример команд для открытия отчёта в редакторе Emacs:
plog-converter -a GA:1,2 -t errorfile
-o /path/to/project.err /path/to/project.log
emacs
M-x compile
cat /path/to/project.err 2>&1
На рисунке 9 представлен пример просмотра .err файла в Emacs:
Рисунок 9 - Просмотр .err файла в Emacs
Пример команд для конвертации отчёта в CSV формат
plog-converter -a GA:1,2 -t csv
-o /path/to/project.csv /path/to/project.log
После открытия файла project.csv в LibreOffice Calc необходимо добавить автофильтр: Menu Bar --> Data --> AutoFilter. На рисунке 10 представлен пример просмотра .csv файла в LibreOffice Calc:
Рисунок 10 - Просмотр .csv файла в LibreOffice Calc
Более объёмные настройки можно сохранить в файл конфигурации со следующими опциями:
Имя опции отделяется от значений символом '='. Каждая опция указывается на отдельной строке. Комментарии пишутся на отдельных строках, перед комментарием ставится символ #.
Утилита blame-notifier предназначена для автоматизации процесса оповещения разработчиков, заложивших в репозиторий код, на который анализатор PVS-Studio выдал предупреждения. Отчет анализатора подается на вход blame-notifier с указанием дополнительных параметров; утилита находит файлы, в которых были обнаружены предупреждения и формирует HTML-отчет на каждого "виновного" разработчика. Также возможен вариант рассылки полного отчета: внутри него будут содержаться все предупреждения, относящиеся к каждому "виновному" разработчику.
Способы установки и использования утилиты описаны в соответствующем разделе документации: "Оповещение команд разработчиков (утилита blame-notifier)".
Массовое подавление предупреждений позволяет легко внедрить анализатор в любой проект и сразу начать получать выгоду от этого, т.е. находить новые ошибки. Этот механизм позволяет запланировать исправление пропущенных предупреждений в будущем, не отвлекая разработчиков от выполнения текущих задач.
Есть несколько способов использования этого механизма, в зависимости от варианта интеграции анализатора. О том, как использовать механизм подавления в pvs-studio-analyzer можно узнать тут.
Прямая интеграция анализатора может выглядеть следующим образом:
.cpp.o:
$(CXX) $(CFLAGS) $(DFLAGS) $(INCLUDES) $< -o $@
$(CXX) $(CFLAGS) $< $(DFLAGS) $(INCLUDES) -E -o $@.PVS-Studio.i
pvs-studio --cfg $(PVS_CFG) --source-file $< --i-file $@.PVS-Studio.i
--output-file $@.PVS-Studio.log
В этом режиме анализатор не может одновременно проверять исходные файлы и фильтровать их. Поэтому для фильтрации и подавления предупреждений потребуются дополнительные команды.
Для подавления всех предупреждений анализатора также необходимо выполнять команду:
pvs-studio-analyzer suppress /path/to/report.log
Для фильтрации нового лога необходимо воспользоваться следующими командами:
pvs-studio-analyzer filter-suppressed /path/to/report.log
plog-converter ...
Файл с подавленными предупреждениями также имеет имя по умолчанию suppress_file.suppress.json, для которого при необходимости можно задать произвольное имя.
1. Утилита strace выдаёт сообщение вида:
strace: invalid option -- 'y'
Вам необходимо обновить версию программы strace. Анализ проекта без интеграции в сборочную систему - сложная задача, а с этой опцией анализатору удаётся получить важную информацию о компиляции проекта.
2. Утилита strace выдаёт сообщение вида:
strace: umovestr: short read (512 < 2049) @0x7ffe...: Bad address
Такие ошибки возникают в системных процессах и на сборку/анализ проекта не влияют.
3. Утилита pvs-studio-analyzer выдаёт сообщение вида:
No compilation units found
Анализатору не удалось обнаружить файлы для анализа. Возможно, вы используете кросс-компиляторы для сборки проекта. Смотрите раздел "Если вы используете кросс-компиляторы" в этой документации.
4. Отчёт анализатора состоит из подобных строк:
r-vUVbw<6y|D3 h22y|D3xJGy|D3pzp(=a'(ah9f(ah9fJ}*wJ}*}x(->'2h_u(ah
Анализатор сохраняет отчёт в промежуточном формате. Для просмотра отчёта его необходимо преобразовать в читабельный формат с помощью утилиты plog-converter, которая устанавливается вместе с анализатором.
5. Анализатор выдаёт ошибку:
Incorrect parameter syntax:
The ... parameter does not support multiple instances.
При вызове анализатора какой-то из параметров задали несколько раз.
Такое может возникнуть, если часть параметров анализатора определили в конфигурационном файле, а часть передали через параметры командной строки. При этом случайно определив какой-нибудь параметр несколько раз.
При использовании утилиты pvs-studio-analyzer почти все параметры определяются автоматически, поэтому она может работать без конфигурационного файла. Дублирование таких параметров тоже может приводить к такой ошибке.
6. Анализатор выдаёт предупреждение:
V001 A code fragment from 'path/to/file' cannot be analyzed.
Если анализатору не удаётся понять какой-нибудь фрагмент кода, то он его пропускает и выдаёт предупреждение V001. На анализ других фрагментов кода такая ситуация обычно не влияет, но если такой код находится в заголовочном файле, то таких предупреждений может быть очень много. Для поддержки проблемной конструкции пришлите нам препроцессированный файл (.i) для проблемного файла с исходным кодом.
Если у вас возникли вопросы или проблемы с запуском анализатора, то напишите нам.
Для удобного просмотра результатов анализа можно использовать расширение (плагин) PVS‑Studio для Qt Creator. Подробнее про его установку и использование описано в документации "Использование расширения PVS‑Studio для Qt Creator".
Проанализировать такие проекты можно через встроенный механизм генерации файла compile_commands.json.
Анализатор PVS-Studio может быть использован для анализа CMake проектов в среде Qt Creator.
Важно. Перед использованием CMake-модуля необходимо ввести лицензию PVS-Studio при помощи специальных команд для Windows и Linux/macOS.
Затем добавьте к вашему проекту CMake-модуль PVS-Studio. После чего добавьте код для создания цели анализа в файл CMakeLists.txt вашего проекта:
include(PVS-Studio.cmake)
pvs_studio_add_target(TARGET ${PROJECT_NAME}.analyze ALL
OUTPUT
FORMAT errorfile
ANALYZE ${PROJECT_NAME}
MODE GA:1,2
LOG ${PROJECT_NAME}.log
ARGS -e C:/Qt/
HIDE_HELP)
По умолчанию, при открытии CMakeLists.txt проекта, Qt Creator скрывает дополнительные цели сборки. Чтобы их отобразить, отключите опцию 'Hide Generated Files' в фильтрах обозревателя проекта:
Для сборки и запуска анализа нажмите Build "ИмяПроекта.analyze" из контекстного меню цели анализа:
После завершения анализа найденные анализатором ошибки будут добавлены в панель 'Issues':
Если для сборки проектов под Windows вы используете набор инструментов MSVC, Qt Creator поменяет механизм разбора предупреждений на совместимый с выводом компиляторов Visual Studio. Этот формат вывода не совместим с форматами вывода PVS-Studio, в результате сообщения анализатора не появятся в панели 'Issues'. Чтобы настроить поддержку формата errorfile нужно выполнить следующие шаги:
2. В открывшемся окне нажмите на кнопку 'Add...'. В строках 'Error message capture pattern' вставьте следующие регулярные выражения:
Во вкладке Error
(.*):(\d+): error: (.*)
Во вкладке Warning
(.*):(\d+): warning: (.*)
Либо, если нужно отображение предупреждений низкого уровня
(.*):(\d+): (?:warning|note): (.*)
3. После настройки регулярный выражений нажмите 'Ok'. Выберите новый парсер и перезапустите анализ.
В результате предупреждения анализатора появятся в панели 'Issues'.
Плагин PVS-Studio для Qt Creator предоставляет удобный графический интерфейс для запуска анализа проектов и отдельных файлов, а также для работы с предупреждениями анализатора. Далее будут описаны установка, настройка и основные сценарии использования.
Для начала необходимо получить нужную версию расширения. Её можно найти в директории установки PVS-Studio.
Из-за ограничений интегрированной среды разработки Qt Creator, в ней могут быть запущены только те версии плагина, что были сделаны специально для неё. Список поддерживаемых версий IDE указан в таблице ниже:
Версия IDE Qt Creator |
Статус поддержки плагинов PVS-Studio |
---|---|
8.0.x |
Поддержка окончена. Последняя доступная версия — 7.31 |
9.0.x |
Поддерживается |
10.0.x |
Поддерживается |
11.0.x |
Поддерживается |
12.0.x |
Поддерживается |
13.0.x |
Поддерживается |
14.0.x |
Поддерживается |
Символ 'х' в названии файла плагина означает, что на его месте может быть любая цифра. То есть поддержка версии 9.0.х означает, что работоспособность плагина проверена на версиях 9.0.0, 9.0.1 и 9.0.2.
Целевая версия Qt Creator и платформа указаны в названии архива плагина. Например, 'pvs-studio-qtcreator-11.0.x-7.25.73595.503.windows.zip' или 'libpvs-studio-qtcreator-11.0.x-7.25.73595.503.linux.tar.gz'.
Чтобы установить расширение PVS-Studio откройте Qt Creator и выберите в строке меню пункт 'Help' -> 'About Plugins':
Нажмите кнопку 'Install Plugin...':
В появившемся мастере установки укажите местоположение архива плагина (с расширением '.zip' или '.tar.gz' в зависимости от платформы).
Замечание: не нужно распаковывать архив с плагином перед установкой. Мастер установки плагинов QtCreator заявляет, что поддерживает установку плагинов напрямую, но на практике этот способ часто приводит к сбоям. Поэтому мы советуем устанавливать плагины, не извлекая из архивов.
Далее мастер установки спросит место для установки плагина. Выберите подходящий вам вариант, нажмите далее и подтвердите установку.
Замечание: для установки в "Qt Creator installation" могут потребоваться права администратора. В противном случае будет выдана ошибка об отсутствии прав для записи файла.
По окончанию установки плагина не будет выдано никакого окна подтверждения. Сразу после закрытия мастера установки плагинов нужно нажать кнопку "ОК". В таком случае появится сообщение о том, что новые плагины будут доступны только после перезапуска QtCreator. Соглашаемся, и IDE перезапустится автоматически.
В случае невозможности установки плагина с помощью мастера установки, можно воспользоваться ручной установкой. Для этого необходимо скопировать файл плагина (файл с расширением '.dll' или '.so' в зависимости от вашей платформы) в директорию с плагинами для Qt Creator. По умолчанию, Qt Creator ищет плагины в следующих директориях:
Windows:
%директория_установки_Qt%\Tools\QtCreator\lib\qtcreator\plugins
%APPDATA%\Local\QtProject\qtcreator\plugins\%версия_qt_creator%
Linux:
/opt/Qt/Tools/QtCreator/lib/qtcreator/plugins
Данный способ можно использовать при невозможности установить плагин в стандартные директории. При запуске Qt Creator можно указать дополнительные директории для поиска плагинов с помощью флага '-pluginpath'.
Например, запуск IDE можно произвести с помощью следующей команды:
Для обновления плагина достаточно удалить файлы прошлых версий плагина и установить новую версию удобным для вас способом. Найти место расположения плагина можно, выбрав пункт 'Details' в списке установленных плагинов.
В случае, если при запуске Qt Creator появляется сообщение, что плагин PVS-Studio не может быть загружен, т.к. не найдены подходящие зависимости (например, как рисунке ниже), то необходимо проверить используемую версию плагина и Qt Creator. Используемую версию Qt Creator можно узнать в меню 'Help' -> 'About Qt Creator', а версия используемого плагина указана в названии его файла.
Так как Qt Creator не обладает системой обновления плагинов, то после очередного обновления плагина может появиться следующее окно.
Данное сообщение означает, что плагин обнаружил одновременное использование нескольких версий и автоматически выключил все версии, кроме самой последней. Крайне желательно удалить старые версии. Для вашего удобства в сообщении также перечислены все обнаруженные версии плагина и пути, где они расположены. Текущий активный плагин отмечен записью [Active].
Плагин PVS-Studio для IDE Qt Creator интегрируется в главное меню среды разработки и панель вывода.
В главном меню появляется дополнительный пункт в разделе 'Analyze', содержащий следующие пункты:
Check – подменю, позволяющее запустить анализ;
Analysis after Build – быстрый доступ к настройке для активации инкрементального анализа;
Open/Save – подменю, позволяющее загружать и сохранять отчёты;
Recent Reports – хранит список последних открытых отчётов для их быстрой загрузки. Клик по пункту меню инициирует загрузку выбранного файла;
Help – подменю, содержащее в себе ссылки на документацию и наиболее важные разделы сайта анализатора;
Options – открывает настройки IDE Qt Creator в разделе PVS-Studio.
В области вывода появляется дополнительная панель с именем 'PVS-Studio'.
Панель вывода PVS-Studio состоит из следующих элементов:
1 – панель управления отчётом. Первая кнопка позволяет очистить текущую таблицу, предупреждений, а две другие (со стрелочками) позволяют перемещаться между позициями в таблице. Обратите внимание, что навигация будет производиться именно между позициями (т.е. будут открываться указанные в предупреждениях файлы), а не просто строками таблицы.
2 – панель быстрых фильтров. Содержит кнопки показа дополнительного меню, показа расширенных фильтров, а также переключатели отображаемых уровней и категорий предупреждений. При клике по кнопке дополнительного меню появляется всплывающее меню со следующими пунктами:
3 – основная область просмотра. Целиком состоит из таблицы предупреждений. Подробно этот элемент будет рассмотрен позже.
4 – элементы управления панелью. Первая кнопка позволяет развернуть область просмотра по высоте, а вторая – скрыть панель.
5 – кнопка для показа панели PVS-Studio.
Интеграция в настройки состоит из добавления нового раздела "PVS-Studio" в список уже существующих разделов.
Раздел настроек PVS-Studio разделён на 5 разделов (по вкладке на каждый). Более подробно о назначении каждого раздела и настроек, входящих в них, описано в разделе "Конфигурация плагина".
Плагин поддерживает анализ проектов, их частей, а также отдельных файлов. На данный момент поддерживаются следующие типы проектов:
Запуск анализа можно произвести несколькими способами:
При запуске любым из перечисленных способов в области уведомлений появится окно с прогрессом анализа, а в таблице, по мере поступления, начнут появляться предупреждения анализатора. Остановить уже запущенный процесс анализа можно с помощью специальной кнопки в заголовке панели вывода или с помощью "крестика", который находится в конце прогресс-бара уведомления (виден только при наведении курсора на прогресс-бар).
При запуске анализа также проверяется содержимое папки '.PVS-Studio' в директории исходного кода проекта. Если в ней будут найдены пользовательские файлы конфигурации (с расширением *.pvsconfig) или suppress-файлы (с суффиксом *.suppress.json), то они будут переданы анализатору для дальнейшей обработки.
Внимание: на данный момент плагин PVS-Studio для Qt Creator поддерживает работу только с одним suppress-файлом. Если при запуске будет найдено несколько файлов, то в отчёте появится соответствующая запись. В ней также будет указано, какой файл используется при анализе.
В случае каких-либо проблем с запуском будет выведено всплывающее окно с подробным описанием проблемы.
В плагине реализована поддержка инкрементального анализа. В этом режиме после каждой успешной сборки проекта будет автоматически запускаться его анализ. Однако проанализированы будут только те файлы, что изменились с момента прошлого запуска анализатора. Подробнее о режиме инкрементального анализа можно узнать в отдельной документации.
Активировать инкрементальный анализ можно в настройках плагина (General->Incremental analysis) или с помощью пункта меню 'Analysis after Build' в подменю PVS-Studio главного окна.
Также в плагине доступен режим межмодульного анализа. Под межмодульным анализом в PVS-Studio понимается расширение возможностей межпроцедурного анализа на функции, объявленные в единицах трансляции, отличных от той, к которой относится текущий проверяемый файл. Более подробную информацию о том, что такое межмодульный анализ и чем он может быть полезен, можно найти в документации.
Для запуска межмодульного анализа необходимо выбрать пункт 'Analyze with PVS-Studio (Intermodular)' в контекстном меню проекта верхнего уровня. Межмодульный анализ отдельных частей проекта невозможен.
Обратите внимание: расширение PVS-Studio для Qt Creator поддерживает только отчёты в формате JSON. Для отображения отчёта в другом формате вам потребуется выполнить его преобразование в JSON-формат.
Для преобразования можно воспользоваться утилитами командной строки PlogConverter.exe для Windows и plog-converter для Linux / macOS. Эти утилиты позволяют не только конвертировать отчёт PVS-Studio в разные форматы, но и дополнительно обрабатывать его. Например, проводить фильтрацию предупреждений. Подробнее о них можно прочитать здесь.
Пример команды конвертации отчёта PVS-Studio в JSON-формат при помощи PlogConverter.exe (Windows):
PlogConverter.exe path\to\report.plog -t json ^
-n PVS-Studio
Пример команды конвертации отчёта PVS-Studio в JSON-формат при помощи plog-converter (Linux и macOS):
plog-converter path/to/report/file.json -t json \
-o PVS-Studio.json
В случае если у вас уже есть готовый отчёт анализатора, и вы хотите посмотреть его в Qt Creator, то откройте панель PVS-Studio, нажмите на кнопку меню и выберите пункт 'Open/Save -> Open Report...':
Также отчёт может быть открыт с помощью пункта меню 'Analyze -> PVS-Studio -> Open/Save -> Open Report...':
После выбора и загрузки отчёта появится область просмотра предупреждений, а сами предупреждения отобразятся в таблице:
Окно вывода результатов PVS-Studio в первую очередь предназначено для упрощения навигации по коду проекта и переходу к участкам кода, содержащим потенциальные ошибки. Двойной клик по предупреждению в таблице автоматически откроет позицию (файл и строку), на которую оно выдано.
Клик левой кнопкой мыши по заголовку таблицы сортирует содержимое по выбранному столбцу.
Клик правой кнопкой мыши по заголовку таблицы открывает всплывающее меню, с помощью которого можно скрыть/отобразить дополнительные столбцы, а также активировать отображение полных путей до файлов в столбце позиций.
В таблице предупреждений поддерживается множественный выбор. Для его активации можно использовать протяжку с зажатой левой кнопкой мыши, а также привычные сочетания клавиш:
Примечание: практически все элементы в плагине имеют всплывающие подсказки. Для их активации достаточно задержать курсор мыши над элементом на пару секунд.
Level – неименованный первый столбец, отображает соответствие между уровнем предупреждения и цветом (в порядке убывания важности/достоверности): красный – 1 уровень, оранжевый – 2 уровень, жёлтый – 3 уровень.
Star – показывает, отмечено ли срабатывание как избранное. Клик по ячейке в этом столбце устанавливает/снимает отметку "избранное" с соответствующего предупреждения. Полезно для выделения интересных предупреждений, например, тех, к которым имеет смысл вернуться позже.
ID – показывает порядковый номер предупреждения в отчёте. Полезно при необходимости отсортировать отчёт в порядке получения предупреждений от анализатора.
Code – указывает к какой диагностике относится те или иные предупреждения. Клик по значению в данном столбце открывает документацию по соответствующей диагностике.
CWE – отображает соответствие предупреждения согласно классификации CWE. Клик по значениям в данном столбце открывает документацию с описанием соответствующего недостатка безопасности.
SAST – отображает соответствие предупреждения согласно различным стандартам защищённости или безопасности, например, SEI CERT, MISRA, AUTOSAR, и т.д.
Message – текст предупреждения, выданного анализатором.
Project – содержит название проекта, анализ которого привёл к выдаче предупреждения.
Position – показывает позицию (имя файла и строка через двоеточие), к которой относится срабатывание. В случае необходимости отобразить полный путь до файла – необходимо кликнуть правой кнопкой мыши по заголовку таблицы и выбрать пункт 'Show full path to file'. Если предупреждение анализатора содержит сразу несколько позиций, то в его конце появляется метка (...). В таком случае при клике в столбце позиции появится список со всеми дополнительными позициями.
FA – показывает, установлена ли для предупреждения отметка ложного срабатывания.
Примечание: некоторые столбцы могут быть скрыты по умолчанию. Для их отображения /скрытия необходимо кликнуть правой кнопкой мыши по заголовку таблицы и в появившемся меню выбрать пункт 'Show Columns', а в нем столбец, который вас интересует.
При клике на каком-либо предупреждении правой кнопкой мыши появляется всплывающее меню с перечнем доступных дополнительных действий:
Меню 'Mark As' – содержит команды для быстрой установки/снятия отметок с выбранных предупреждений. На данный момент доступны отметки предупреждений как избранные и ложные. Обратите внимание, что содержимое этого меню меняется в зависимости от состояния выбранных на данный момент предупреждений.
'Suppress selected messages' – позволяет подавить текущие выбранные предупреждения в файл подавления. Подробнее о подавлении предупреждений написано в пункте "Подавление предупреждений".
Меню 'Copy to clipboard' – позволяет скопировать информацию о выбранных предупреждениях. Содержит несколько подпунктов:
'Hide all %N errors' – позволяет скрыть из отчёта все предупреждения, относящиеся к данной диагностике. При клике появляется всплывающее меню для подтверждения операции. В случае положительного выбора сообщения будут отфильтрованы мгновенно.
'Don't check files from' – подменю, содержащее части пути до файла позиции. Используется, если вам необходимо скрыть из отчёта все срабатывания, полученные на файлы из выбранной директории. При выборе какого-либо значения появится всплывающее окно для подтверждения фильтрации и описанием как её отключить в будущем:
Меню 'Analyzed source files' – содержит список файлов, анализ которых привёл к появлению данного предупреждения. Полезно при выдаче срабатывания на заголовочных файлах.
Механизмы фильтрации окна вывода PVS-Studio позволяют быстро найти и отобразить как отдельные диагностические сообщения, так и целые их группы.
Все перечисленные ниже механизмы фильтрации предупреждений (быстрые и расширенные фильтры) можно совмещать между собой одновременно, а также с сортировкой. Например, можно отфильтровать сообщения по уровню и группам отображаемых предупреждений, исключить все сообщения кроме тех, что содержат определённый текст и дополнительно отсортировать по позициям.
Панель инструментов содержит ряд переключателей, позволяющих включить либо отключить отображение предупреждений из соответствующих им групп. При изменении состава активных категорий также будут пересчитаны счётчики.
Примечание: переключатель группы 'Fails' отображается только при наличии ошибок от анализатора (их 'Code' начинается с V0..) в отчёте.
Детальное описание уровней достоверности предупреждений и групп диагностических правил приведено в разделе документации "Знакомство со статическим анализатором кода PVS-Studio".
Расширенные фильтры отображаются при нажатии кнопки 'Quick Filters'. Состояние панели дополнительных фильтров (показана/скрыта) не влияет на активные фильтры. Т.е. их можно скрыть и при этом фильтры не будут сброшены.
При её активации появляется дополнительная панель, которая содержит поля ввода для фильтрации всех столбцов таблицы, а также кнопка для быстрой очистки всех фильтров (Clear All).
После ввода текста в поля ввода необходимо нажать 'Enter' для активации фильтра. Обратите внимание, что некоторые поля поддерживают множественную фильтрацию (например, Code), о чём сообщается во всплывающей подсказке при наведении курсора мыши на поле ввода.
При первом запуске анализатора на большом проекте может быть действительно много срабатываний. Разумеется, стоит выписать себе самые интересные, а вот остальные можно скрыть при помощи механизма подавления предупреждений.
Для подавления всех предупреждений нужно выбрать пункт 'Suppress All Messages' в меню плагина:
При его активации появится дополнительное окно с вопросом, какие именно предупреждения вы хотите подавить:
При выборе нужного пункта предупреждения будут подавлены в существующий файл подавления предупреждений. Если файл подавления предупреждений не будет найден, то будет создан в следующей директории: '%корневой_каталог_исходного_код_проекта%/.PVS-Studio'.
Если предложенный выше вариант вам не подходит, то можно воспользоваться точечным подавлением предупреждений. Для этого необходимо выбрать нужные строки в таблице, открыть контекстное меню и выбрать 'Add message to suppression file'.
В настройки плагина PVS-Studio для IDE Qt Creator можно попасть с помощью выбора раздела PVS-Studio в общем списке настроек или с помощью пунктов меню 'Options...' плагина.
Настройки плагина хранятся в файле 'qtcsettings.json', который расположен:
Все настройки плагина разделены на 5 вкладок:
Содержит основные настройки анализатора и плагина.
Incremental analysis – включает режим инкрементального запуска анализатора.
Remove intermediate files – автоматическое удаление временных файлов, создаваемых анализатором в процессе работы.
Analysis Timeout – позволяет задать временя (в секундах), после которого проверка текущего файла будет пропущена.
Thread Count – количество потоков, используемых при анализе. Указание большего значения может ускорить анализ, но также может привести к аварийному завершению работы анализатора в связи с нехваткой памяти. Оптимальное значение равно количеству физических ядер используемого процессора.
Display false alarms – включает/отключает отображение ложных срабатываний в отчёте. При активации появляется дополнительный столбец в таблице визуализации отчёта.
Save file after False Alarm mark – если активно, то сохраняет изменённый файл после вставки комментария ложного срабатывания.
Source Tree Root – содержит путь, который должен использоваться при открытии позиций, использующих относительные пути. Например, если в предупреждении записан относительный путь '\test\mylist.cpp', а настройка Source Tree Root содержит путь 'C:\dev\mylib', то при попытке перехода на позицию из предупреждения будет открыт файл 'C:\dev\mylib\test\mylist.cpp'.
Детальное описание использования относительных путей в файлах отчётов PVS-Studio смотрите здесь.
Help Language – указывает, на какой язык предпочтительно открывать документацию анализатора. Используется при переходе в документацию на сайте анализатора.
Данная вкладка содержит список и описание всех предупреждений анализатора, а также позволяет включать/выключать как отдельные диагностики, так и целые категории.
В верхней части окна доступен полнотекстовый поиск по описанию диагностики и её номеру. При клике на код диагностики открывается её документация. При наведении на текст появляется всплывающее сообщение с полным текстом. При нажатии 'OK' или 'Apply' происходит обновление таблицы предупреждений для соответствия текущим фильтрам.
Все диагностики сгруппированы в категории. Для них можно выставить следующие состояния:
Полный список диагностик всегда доступен в разделе "Сообщения PVS-Studio".
Содержит списки для фильтрации предупреждений по маскам имён файлов или путей. Если имя или путь будет удовлетворять хотя бы одной маске – он будет скрыт из отчёта.
Поддерживаются следующие символы подстановки:
* – любое количество любых символов
? – один любой символ
Для добавления записи необходимо нажать кнопку 'Add' и ввести текст в появившееся поле. Для удаления необходимо выбрать запись и нажать кнопку 'Remove'. Записи с пустыми полями будут удалены автоматически. Редактировать уже существующие записи можно при помощи двойного клика по ней или выбора и нажатия кнопки 'Edit'.
При нажатии кнопок 'OK' или 'Apply' происходит обновление таблицы предупреждений для соответствия текущим фильтрам.
Вкладка содержит редактор ключевых слов, предупреждения с которыми будут скрыты в отчётах. Ключевые слова из этого списка проверяются только по данным в столбце 'Message'.
Функция может быть полезна при необходимости скрыть предупреждения от определённой функции или класса, достаточно указать их здесь.
Вкладка содержит форму для ввода данных лицензии, которая будет использоваться при запуске анализатора. Процесс ввода лицензии в Qt Creator подробно описан в документации.
Использование горячих клавиш позволяет ускорить процесс обработки полученных результатов анализа. Их можно назначать/переопределять в настройках 'Options -> Environment -> Keyboard'. Чтобы быстрее их найти, необходимо ввести 'PVS-Studio' в поле поиска окна 'Keyboard Shortcuts'.
Одним из способов представления структуры C++ проекта является формат JSON Compilation Database. Это файл, в котором записаны параметры компиляции, необходимые для создания объектных файлов из исходного кода проекта. Обычно он имеет имя 'compile_commands.json'. База данных компиляции в JSON-формате состоит из массива "командных объектов", где каждый объект определяет один из способов компиляции единицы трансляции в проекте.
Файл 'compile_commands.json' может использоваться для компиляции проекта или для анализа сторонними утилитами. C и C++ анализатор PVS-Studio тоже умеет работать с этим форматом.
Анализ проекта на Linux и macOS выполняется с помощью утилиты 'pvs-studio-analyzer'. Анализ на Windows – c помощью утилиты 'CompileCommandsAnalyzer.exe', которая обычно размещена в папке 'C:\Program Files (x86)\PVS-Studio'. Дополнительную информацию о CompileCommandsAnalyzer и pvs-studio-analyzer можно узнать тут.
Важно: Проект должен успешно компилироваться и быть собран перед запуском анализа.
Для запуска анализа и получения отчета необходимо выполнить две команды.
Пример команд для Linux и macOS:
pvs-studio-analyzer analyze -f path_to_compile_commands.json \
-o pvs.log -e excludepath -j<N>
plog-converter -a GA:1,2 -t tasklist -o project.tasks pvs.log
Пример команд для Windows:
CompilerCommandsAnalyzer.exe analyze
-f path_to_compile_commands.json ^
-o pvs.log -e exclude-path -j<N>
PlogConverter.exe -a GA:1,2 -t Plog -o path_to_output_directory ^
-n analysis_report pvs.log
Если анализ выполняется из директории с файлом 'compile_commands.json', флаг '-f' можно опустить.
Чтобы исключить из анализа директории с third-party библиотеками и/или тестами, можно использовать флаг '-e'. Если путей несколько, необходимо писать флаг '-e' для каждого пути:
-e third-party -e tests
Анализ может быть распараллелен на несколько потоков с помощью флага '-j'.
Более подробные инструкции по утилитам на Linux/macOS и Windows доступны здесь и здесь.
Если проект по умолчанию не содержит 'compile_commands.json', то можно воспользоваться одним из способов генерации такого файла.
Для генерации 'compile_commands.json необходимо добавить один флаг к вызову CMake:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On .
Создание 'compile_commands.json' происходит только в том случае, если генератор поддерживает JSON-формат. Такими генераторами являются, например, Makefile и Ninja:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On -G Ninja .
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On -G "NMake Makefiles" .
Для использования генератора Ninja под Windows часто требуется выполнять команды из командной строки разработчика от Visual Studio (например, 'x64 Native Tools Command Prompt for VS', и т.п.).
Если сборка проекта осуществляется напрямую с помощью Ninja и в папке проекта есть файл 'build.ninja', то можно воспользоваться следующей командой для генерации 'compile_commands.json':
ninja -t compdb > compile_commands.json
Для генерации 'compile_commands.json' в проекте, использующем Qt Build System, необходимо выполнить следующую команду:
qbs generate --generator clangdb
Когда сборка проекта происходит с использованием GNU make, и получить 'compile_commands.json' не получается, то следует попробовать инструмент Text Toolkit. Сгенерировать базу данных компиляции можно либо через Web-интерфейс (только для Linux и macOS), либо запустив скрипт на Python. Для генерации online нужно:
Для генерации 'compile_commands.json' с помощью Python, нужно клонировать репозиторий с GitHub и выполнить следующую команду:
ninja -nv | python path_to_texttoolkit_dir\cdg.py
Утилита Bear (версии 2.4 или выше) собирает параметры компиляции, перехватывая вызовы компилятора во время сборки проекта. Для генерации 'compile_commands.json' используется следующая команда
bear -- <build_command>
В качестве 'build_command' может быть любая команда для сборки, например 'make all' или './build.sh'.
Утилита 'intercept-build' в scan-build аналогична утилите Bear. Команда для генерации 'compile_commands.json':
intercept-build <build_command>
Compile Database Generator (compiledb) – это утилита для генерации базы данных компиляции для Makefile-based сборочных систем. Пример генерации 'compile_commands.json':
compiledb -n make
Флаг '-n' означает, что сборка не произойдет (dry run).
С помощью утилиты xcpretty можно сгенерировать 'compile_commands.json'. Для этого необходимо выполнить следующую команду:
xcodebuild [flags] | xcpretty -r json-compilation-database
Для генерации 'compile_commands.json' в проекте, использующем qmake, можно воспользоваться IDE QtCreator версии 4.8 и выше. Для этого необходимо открыть в ней нужный проект и выбрать в строке меню пункт 'Сборка -> Создать базу данных компиляции для %название_проекта%' ('Build->Generate Compilation Database for %название_проекта%'):
Сгенерированный файл 'compile_commands.json' появится в директории сборки проекта.
Примечание: данный способ получения 'compile_commands.json' не предусматривает автоматизации и рекомендуется к использованию только в целях тестирования.
Для генерации 'compile_commands.json' в проекте, использующем систему сборки SCons, необходимо в файл SConstruct (аналог Makefile файла для утилиты Make) в директории проекта добавить следующие строчки:
env = Environment(COMPILATIONDB_USE_ABSPATH=True)
env.Tool('compilation_db')
env.CompilationDatabase()
env.Program('programm_for_build.c')
После этого для создания файла 'compile_commands.json' необходимо запустить в директории проекта (где расположен файл SConstruct) команду:
scons -Q
Более подробное описание создания 'compile_commands.json' в SCons находится в соответствующем разделе документации SCons.
Для генерации 'compile_commands.json' в проекте, использующем систему сборки Bazel, используйте утилиту bazel-compile-commands-extractor (это утилита, которая не требует полной сборки проекта, и основана на Action Graph Query (aquery)). Инструкция по её настройке расположена здесь.
Есть несколько других вариантов создания файла 'compile_commands.json' для Bazel проекта кроме bazel-compile-commands-extractor:
Помимо режима работы с compile_commands.json, работать с PVS-Studio в CMake можно с помощью специального CMake-модуля. Он позволяет вам интегрировать анализатор в проект на основе CMake более глубоким образом. Например, вы можете указать конкретные цели для анализа, чтобы не проверять весь проект полностью, а лишь те части, которые вам нужны.
Убедитесь, что вы ввели лицензионный ключ, иначе анализ не заработает. О том, как это сделать, подробно написано здесь.
Самый простой и рекомендованный способ добавить модуль в проект — это использовать FetchContent для автоматической загрузки. Сделать это можно примерно так:
include(FetchContent)
FetchContent_Declare(
PVS_CMakeModule
GIT_REPOSITORY "https://github.com/viva64/pvs-studio-cmake-module.git"
GIT_TAG "master"
)
FetchContent_MakeAvailable(PVS_CMakeModule)
include("${pvs_cmakemodule_SOURCE_DIR}/PVS-Studio.cmake")
Этот код самостоятельно загрузит Git-репозиторий с модулем в генерируемую папку кэша и позволит интегрировать анализатор в ваш проект. Обратите внимание, что master — это самая свежая версия. Если у вас возникнут проблемы с ней, то попробуйте взять последний релизный тег текущей версии анализатора.
Вы также можете загрузить файл с модулем PVS-Studio.cmake самостоятельно, если вы не хотите иметь лишних зависимостей от FetchContent.
Пожалуйста, не забывайте обновлять модуль с выходом новой версии анализатора во избежание проблем в его работе. В параметре GIT_TAG также можно указать master-ветку, чтобы всегда использовать последнюю версию модуля.
Для запуска анализатора CMake-модуль добавляет отдельную цель для сборки. При запуске сборки этой цели на самом деле будет запущен анализ с параметрами, которые были указаны при добавлении этой цели. Чтобы добавить цель для анализа, нужно использовать команду pvs_studio_add_target. Например:
cmake_minimum_required(VERSION 3.5)
project(pvs-studio-cmake-example CXX)
add_executable(example main.cpp)
# Optional:
# include(FetchContent)
# FetchContent_Declare(....)
# FetchContent_MakeAvailable(....)
include(PVS-Studio.cmake)
pvs_studio_add_target(TARGET example.analyze ALL
OUTPUT FORMAT json
ANALYZE example
MODE GA:1,2
LOG target.err
ARGS -e /path/to/exclude-path)
Этот небольшой CMake-файл содержит одну цель для сборки исполняемого файла и одну цель для запуска анализа. Разберем параметры команды pvs_studio_add_target:
Для исключения файлов из анализа можно воспользоваться опцией ARGS, передав пути через флаг -e (‑‑exclude-path), как указано в примере выше. Вы можете задать абсолютные, относительные пути или маску поиска (glob). Учтите, что относительные пути будут раскрыты относительно каталога сборки. Такой подход позволяет, например, исключить из анализа сторонние библиотеки.
Для запуска анализа надо собрать цель, которая была добавлена в pvs_studio_add_target. Например, так выглядит запуск анализа для примера выше:
cmake --build <path-to-cache-dir> --target example.analyze
Перед запуском будут собраны все цели, указанные для анализа в параметре ANALYZE.
Здесь вы можете найти примеры интеграции PVS-Studio в CMake.
Docker — программное обеспечение для автоматизации развёртывания и управления приложениями в средах с поддержкой контейнеризации. Позволяет "упаковать" приложение со всем его окружением и зависимостями в контейнер, который может быть перенесён на любую систему, где установлен Docker.
Ниже будут рассмотрены:
Для сборки готового образа с последней версией анализатора PVS-Studio можно использовать Dockerfile.
Для debian-based систем:
FROM gcc:7
# INSTALL DEPENDENCIES
RUN apt update -yq \
&& apt install -yq --no-install-recommends wget \
&& apt clean -yq
# INSTALL PVS-Studio
RUN wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt | apt-key add - \
&& wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list \
&& apt update -yq \
&& apt install -yq pvs-studio strace \
&& pvs-studio --version \
&& apt clean -yq
Для zypper-based систем:
FROM opensuse:42.3
# INSTALL DEPENDENCIES
RUN zypper update -y \
&& zypper install -y --no-recommends wget \
&& zypper clean --all
# INSTALL PVS-Studio
RUN wget -q -O /tmp/viva64.key https://files.pvs-studio.com/etc/pubkey.txt \
&& rpm --import /tmp/viva64.key \
&& zypper ar -f https://files.pvs-studio.com/rpm viva64 \
&& zypper update -y \
&& zypper install -y --no-recommends pvs-studio strace \
&& pvs-studio --version \
&& zypper clean -all
Для yum-based систем:
FROM centos:7
# INSTALL DEPENDENCIES
RUN yum update -y -q \
&& yum install -y -q wget \
&& yum clean all -y -q
# INSTALL PVS-Studio
RUN wget -q -O /etc/yum.repos.d/viva64.repo \
https://files.pvs-studio.com/etc/viva64.repo \
&& yum install -y -q pvs-studio strace \
&& pvs-studio --version \
&& yum clean all -y -q
Примечание. PVS-Studio для Linux также может быть скачан по следующим постоянным ссылкам:
Команда для сборки образа:
docker build -t viva64/pvs-studio:7.34 -f Dockerfile
Примечание. Базовый образ и зависимости необходимо изменить для целевого проекта.
Запустить анализ, например, CMake-проекта можно с помощью следующей команды:
docker run --rm -v "~/Project":"/mnt/Project" \
-w "/mnt/Project" viva64/pvs-studio:7.34 \
sh -c 'mkdir build && cd build &&
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On .. && make -j8 &&
pvs-studio-analyzer analyze ... -o report.log -j8 ...'
При этом конвертер отчётов анализатора (plog-converter) рекомендуется запускать вне контейнера, чтобы отчёты содержали корректные пути до исходных файлов. Единственный тип отчёта, который имеет смысл сгенерировать в контейнере, это fullhtml, (HTML отчёт с сортировкой предупреждений и навигацией по коду). Для получения других типов отчёта требуется дополнительная настройка анализатора.
При проверке не CMake-проектов в контейнере в режиме трассировки вызовов компилятора может возникнуть такая ошибка:
strace: ptrace(PTRACE_TRACEME, ...): Operation not permitted
Error: Command strace returned 1 code.
Для исправления ошибки необходимо запустить докер с дополнительными правами:
docker run ... --security-opt seccomp:unconfined ...
или так:
docker run ... --cap-add SYS_PTRACE ...
Указание лицензии анализатора
Т.к. время жизни контейнера ограничено, файл лицензии анализатора необходимо закоммитить в образ, либо указывать анализатору через смонтированный каталог:
pvs-studio-analyzer analyze ... -l /path/to/PVS-Studio.lic ...
Восстановление путей к исходникам в отчёте
Чтобы получить отчёт анализатора с корректными путями до файлов с исходным кодом, анализатору предварительно необходимо указать директорию проекта:
pvs-studio-analyzer analyze ... -r /path/to/project/in/container ...
После чего запустить конвертер отчёта вне контейнера.
На Linux или macOS:
plog-converter ... -r /path/to/project/on/host ...
на Windows:
PlogConverter.exe ... -r /path/to/project/on/host
Также в Windows можно открыть отчёт без конвертации в утилите Compiler Monitoring UI.
Исключение директорий из анализа
Исключить из анализа директории со сторонними библиотеками, тестами и директорию компилятора можно с помощью параметра -e:
pvs-studio-analyzer analyze ... -e /path/to/tests ... -e /path/to/contrib ...
Настройка кросс-компилятора
Если контейнер содержит кросс-компилятор или компилятор без алиасов (например, g++-7), то имя компилятора надо указать дополнительно:
pvs-studio-analyzer analyze ... -C g++-7 -C compilerName ...
Вариант установки из архива
FROM openkbs/ubuntu-bionic-jdk-mvn-py3
ARG PVS_CORE="7.34.87460"
RUN wget "https://files.pvs-studio.com/java/pvsstudio-cores/${PVS_CORE}.zip"\
-O ${PVS_CORE}.zip \
&& mkdir -p ~/.config/PVS-Studio-Java \
&& unzip ${PVS_CORE}.zip -d ~/.config/PVS-Studio-Java \
&& rm -rf ${PVS_CORE}.zip
Команда для сборки образа:
docker build -t viva64/pvs-studio:7.34 -f Dockerfile
Вариант коммита слоя с анализатором
Анализатор выкачивается автоматически при первом анализе проекта. Можно предварительно задать имя контейнера и выполнить анализ проекта:
docker run --name analyzer
-v "D:\Project":"/mnt/Project"
openkbs/ubuntu-bionic-jdk-mvn-py3
sh -c "cd /mnt/Project && mvn package
&& mvn pvsstudio:pvsAnalyze -Dpvsstudio.licensePath=/path/to/PVS-Studio.lic"
после чего выполнить коммит в новый образ:
docker commit analyzer viva64/pvs-studio:7.34
Примечание. Базовый образ и зависимости необходимо изменить для целевого проекта. Установку и запуск анализатора следует выполнять от имени одного и того же пользователя.
Регулярно запускать анализ проекта следует аналогичным образом, добавив параметр ‑‑rm:
docker run --rm -v "D:\Project":"/mnt/Project"
openkbs/ubuntu-bionic-jdk-mvn-py3
sh -c "cd /mnt/Project
&& mvn package
&& mvn pvsstudio:pvsAnalyze -Dpvsstudio.licensePath=/path/to/PVS-Studio.lic"
Настройку анализатора при интегрировании в сборочную систему Maven или Gradle можно производить согласно инструкциям из документации:
Для сборки готового образа с последней версией анализатора PVS-Studio можно использовать следующий Dockerfile:
# escape=`
FROM mcr.microsoft.com/dotnet/framework/runtime:4.8
SHELL ["cmd", "/S", "/C"]
# INSTALL chocolatey
RUN `
@"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile`
-InputFormat None -ExecutionPolicy Bypass `
-Command " [System.Net.ServicePointManager]::SecurityProtocol = 3072; `
iex ((New-Object System.Net.WebClient).DownloadString `
('https://chocolatey.org/install.ps1'))" `
&& `
SET "PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin"
# INSTALL Visual Studio Build Tools components (minimal)
RUN `
choco install -y visualstudio2019buildtools `
--package-parameters "--quiet --wait --norestart --nocache `
--add Microsoft.VisualStudio.Workload.VCTools;includeRecommended `
--add Microsoft.VisualStudio.Workload.ManagedDesktopBuildTools`
;includeRecommended"
# INSTALL PVS-Studio
RUN `
choco install -y pvs-studio
Выполнив в директории Dockerfile следующую команду, можно получить готовый образ:
docker build -t viva64/pvs-studio:7.34 .
Полученный Docker образ будет иметь минимальные зависимости, чтобы проанализировать С++/С# "Hello Word" проекты. Если ваш проект требует дополнительные компоненты Visual Studio Build Tools, то нужно их установить, скорректировав скрипт. С перечнем доступных компонентов можно ознакомиться здесь.
В данном образе устанавливаются последние из доступных версий Build Tools для Visual Studio 2019 и PVS-Studio при помощи Chocolatey. Чтобы установить конкретную версию Build Tools 2019, нужно явно указать ее при установке. Например,
choco install visualstudio2019buildtools --version=16.10.0.0 ...
О доступных версиях можно узнать здесь.
Если вам нужно установить Build Tools для Visual Studio 2017, то инструкция для установки идентична.
Если установка через Chocolatey по каким-либо соображениям не подходит, то можно всё установить самостоятельно, подготовив все необходимые инсталляторы. Рядом с Dockerfile вам необходимо будет создать директорию с инсталляторами нужных версий (PVS-Studio, VS Build Tools и т.д.). Dockerfile:
# escape=`
FROM mcr.microsoft.com/dotnet/framework/runtime:4.8
SHELL ["cmd", "/S", "/C"]
ADD .\installers C:\Installers
# INSTALL Visual Studio Build Tools components (minimal)
RUN `
C:\Installers\vs_BuildTools.exe --quiet --wait --norestart --nocache `
--add Microsoft.VisualStudio.Workload.VCTools;includeRecommended `
--add Microsoft.VisualStudio.Workload.ManagedDesktopBuildTools`
;includeRecommended `
|| IF "%ERRORLEVEL%"=="3010" EXIT 0
# INSTALL PVS-Studio
RUN `
C:\Installers\PVS-Studio_setup.exe `
/verysilent /suppressmsgboxes /norestart /nocloseapplications
# Cleanup
RUN `
RMDIR /S /Q C:\Installers
Примечание. Если ваш проект потребует дополнительной настройки окружения и зависимостей, то необходимо будет самостоятельно модифицировать Dockerfile соответствующим образом.
Чтобы запустить анализ, при запуске контейнера нужно смонтировать все необходимые внешние зависимости: директорию с проектом, файл с настройками анализатора (Settings.xml) и т.д.
Команда запуска анализа может выглядеть следующим образом:
docker run --rm -v "path\to\files":"C:\mnt" -w "C:\mnt" \
viva64/pvs-studio:7.34 \
"C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" \
--target ".\Project\Project.sln" --output ".\Report.plog" \
--settings ".\Settings.xml" --sourceTreeRoot "C:\mnt"
После чего у вас появится отчет "path\to\files\Report.plog", который вы можете открыть в Visual Studio плагине или в утилите Compiler Monitoring UI.
Примечание. Опция 'sourceTreeRoot' - корневая часть пути, которую PVS-Studio будет использовать при генерации относительных путей в диагностических сообщениях. Это позволит избежать недействительных путей в отчете.
Настраивать анализатор можно через:
Чтобы ядро анализатора работало, необходимо иметь лишь Java 11+. Если вы используете сборочную систему (Maven, Gradle), то вам также необходимо иметь настроенное для неё окружение.
Чтобы получить Docker образ с Maven и последней версией ядра анализатора PVS-Studio, можно воспользоваться одним из следующих вариантов.
Вариант установки из архива
# escape=`
FROM csanchez/maven:3.8.3-azulzulu-11-windowsservercore-ltsc2019
SHELL ["cmd", "/S", "/C"]
ARG PVS_CORE="7.34.87460"
RUN `
powershell -Command `
Invoke-WebRequest `
"https://files.pvs-studio.com/java/pvsstudio-cores/%PVS_CORE%.zip" `
-OutFile .\pvs-studio.zip`
&& `
powershell -Command `
Expand-Archive `
-LiteralPath '.\pvs-studio.zip' `
-DestinationPath \"%APPDATA%\PVS-Studio-Java\" `
&& `
DEL /f .\pvs-studio.zip
Выполнив в директории Dockerfile следующую команду, можно получить готовый образ:
docker build -t viva64/pvs-studio:7.34 .
Вариант коммита слоя с анализатором
Анализатор выкачивается автоматически при первом анализе проекта. Можно предварительно задать имя контейнера и выполнить анализ проекта:
docker run --name analyzer ^
-v "path\to\project":"C:/mnt/Project" ^
-w C:\mnt\Project ^
csanchez/maven:3.8.3-azulzulu-11-windowsservercore-ltsc2019 ^
mvn package pvsstudio:pvsAnalyze
После чего выполнить коммит в новый образ:
docker commit analyzer viva64/pvs-studio:7.34
Примечание. В случае с Gradle иметь предустановленную сборочную систему необязательно, так как gradlew сделает все за вас. Поэтому за основу Dockerfile достаточно взять образ только с Java 11+.
Регулярно запускать анализ проекта следует аналогичным образом:
docker run --name analyzer ^
--rm ^
-v "path\to\project":"C:/mnt/Project"^
-w C:\mnt\Project^
viva64/pvs-studio:7.34 ^
mvn package pvsstudio:pvsAnalyze '-Dpvsstudio.licensePath=./PVS-Studio.lic'
Отличие запуска в том, что указывается опция '‑‑rm', чтобы после запуска контейнер не оставался в памяти. А также нужно указать путь до лицензии. В данном примере лицензия была помещена в корень проекта.
Стоит учесть, что при каждом запуске анализа Maven будет скачивать все необходимые зависимости в свой локальный репозиторий. Чтобы этого избежать, при запуске можно смонтировать локальный репозиторий Maven хостовой машины. Например:
docker run ... -v "%M2_REPO%":"C:\Users\ContainerUser\.m2" ...
Настройку анализатора при интегрировании в сборочную систему Maven или Gradle можно производить согласно инструкциям из документации:
Для автоматизации процедуры анализа в CI (Continuous Integration) необходимо запускать анализатор как консольное приложение.
В Jenkins Вы можете создать один из следующих шагов сборки:
и вписать команду анализа (и команду конвертации отчёта в нужный формат).
Примеры команд запуска анализатора и интеграции в сборочные системы приведены на следующих страницах документации:
Warnings NG Plugin поддерживает отчёты анализатора PVS-Studio, начиная с версии плагина 6.0.0. Этот плагин предназначен для визуализации результатов работы различных анализаторов.
Установить плагин можно из стандартного репозитория Jenkins в меню Manage Jenkins > Manage Plugins > Available > Warnings Next Generation Plugin:
Для публикации результатов анализа в настройках проекта необходимо добавить послесборочный шаг (секция Post-build Actions) Record compiler warnings and static analysis results. Далее необходимо раскрыть список Tool и выбрать PVS-Studio. В поле Report File Pattern можно указать маску или путь к отчёту анализатора. Поддерживаются отчёты с расширением .plog и .xml.
В поле Report Encoding указывается кодировка, в которой будет считан файл отчета. Если поле пустое, то будет использована кодировка операционной системы, в которой запущен Jenkins. Поля Custom ID и Custom Name переопределяют отображаемые в интерфейсе идентификатор и имя выбранной утилиты соответственно.
Для публикации результатов анализа через pipeline скрипты добавьте следующее:
recordIssues enabledForFailure: true,sourceCodeEncoding:'UTF-8',
tool: PVSStudio(pattern: 'report.plog')
где report.plog отчет анализатора.
Сгенерировать отчёт в нужном формате можно следующими способами.
Windows: C, C++, C#
Отчёты с расширением .plog являются стандартными для Windows.
Linux/macOS: C, C++
plog-converter ... --renderTypes xml ...
Windows/Linux/macOS: Java
В настройках плагинов для Maven и Gradle в поле outputType указать значение xml.
После сборки проекта слева в списке появится новый элемент меню PVS-Studio Warnings. По нажатию на него открывается страница, где представлена визуализация данных отчета, созданного анализатором PVS-Studio:
Также при нажатии на значение в столбце File в браузере будет открыт файл с исходным кодом на строчке, где была найдена ошибка.
Примечание. Просмотр предупреждений из загруженного отчёта анализатора в файлах исходного кода в Jenkins (ссылки в столбце File) работает, только если в момент запуска шага Record compiler warnings and static analysis results (секция Post-build Actions) в рабочей директории Jenkins задачи расположены файлы проекта, пути до которых указываются в отчёте анализатора (*.plog файл). Файлы исходного кода, на которые были выданы предупреждения анализатора, кэшируются для каждой сборки Jenkins задачи. Поэтому после прохождения шага Record compiler warnings and static analysis results (секция Post-build Actions) можно очищать рабочую директорию Jenkins задачи без потери возможности просмотра предупреждений в файлах исходного кода в Jenkins. Если открытие файла не работает, значит отчёт был сформирован вне рабочей директории Jenkins задачи или кэшированные файлы исходного кода из в директории сборки Jenkins задачи, участвовавшие в составлении отчёта, перемещены или удалены.
В других CI настройка запуска анализатора и работа с отчётом выполняются аналогичным образом.
Для автоматизации процесса анализа в TeamCity необходимо запускать анализатор как консольное приложение.
В TeamCity необходимо создать Build Step со следующими параметрами:
В скрипте напишите команду анализа и опционально команду конвертации отчёта в нужный формат.
Примеры команд для запуска анализатора, обработки результатов анализа и интеграции в сборочные системы приведены на следующих страницах документации:
В TeamCity можно прикреплять отчёты анализатора в формате HTML к сборкам, указав их в артефактах.
Сгенерировать HTML-отчёт с навигацией по коду можно следующими способами:
Windows: C, C++, C#
PlogConverter.exe ... --renderTypes FullHtml ...
Linux/macOS: C, C++
plog-converter ... --renderTypes fullhtml ...
Windows/Linux/macOS: Java
В настройках плагинов для Maven и Gradle в поле 'outputType' указать значение 'fullhtml'.
В меню 'Edit Configuration Settings -> General Settings -> Artifact paths' укажите каталог с HTML-отчётом.
После успешного выполнения сборки отчёт анализатора формате fullhtml будет доступен в артефактах. Чтобы открыть его, нужно кликнуть на файл 'index.html' на вкладке 'Artifacts'. Также можно сделать так, чтобы отчёт анализатора отображался на специальной вкладке отчёта о сеансе сборки. Для этого необходимо перейти в настройки проекта, открыть 'Report Tabs' и добавить новую вкладку отчёта сборки ('Create new build report tab').
В окне добавления вкладки в поле 'Start page' необходимо указать путь к файлу 'index.html' относительно корневой папки артефактов. Например, если содержимое вкладки 'Artifacts' выглядит примерно так:
то в поле 'Start Page' нужно записать путь 'fullhtml/index.html'. После добавления вкладки результаты анализа можно будет просматривать на ней:
При переходе к предупреждениям анализатора будет открываться дополнительная вкладка браузера:
Утилита "plog-converter" поддерживает стандартные отчёты для TeamCity - TeamCity Inspections Type. После генерации отчёта его необходимо вывести в stdout на любом шаге сборки.
Сгенерировать такой отчёт и вывести его в stdout можно следующими способами:
Windows: C, C++, C#
PlogConverter.exe ... –-renderTypes=TeamCity -o TCLogsDir ...
Type TCLogsDir\MyProject.plog_TeamCity.txt
Linux/macOS: C, C++
plog-converter ... -t teamcity -o report_tc.txt ...
cat report_tc.txt
Windows/Linux/macOS: Java
Поддержка скоро появится.
После успешного выполнения сборки отчёт анализатора появится на новой вкладке в информации об этой сборке:
Навигацию по коду можно осуществить при помощи нажатия на номер строки слева от диагностического правила. Переход осуществится при условии наличия абсолютного пути к исходному файлу, открытого проекта в IDE (Eclipse, Visual Studio, IntelliJ IDEA) и установленного плагина TeamCity.
Travis CI – сервис для сборки и тестирования программного обеспечения, использующего GitHub в качестве хранилища. Travis CI не требует изменения программного кода для использования сервиса, все настройки происходят в файле '.travis.yml', расположенном в корне репозитория.
В данной документации рассматривается пример по интеграции PVS-Studio для анализа C и C++ кода. Команды запуска PVS-Studio для анализа C# или Java кода будут отличаться. Смотрите соответствующие разделы документации: "Проверка проектов Visual Studio / MSBuild / .NET из командной строки с помощью PVS-Studio" и "Работа с ядром Java анализатора из командной строки".
Для начала создайте переменные, которые используются для формирования файла лицензии анализатора и отсылки его отчетов. Перейдите на страницу настроек - кнопка "Settings" справа от нужного репозитория.
Откроется окно настроек.
Краткое описание настроек:
В секции "Environment Variables" создайте переменные 'PVS_USERNAME' и 'PVS_KEY', содержащие, соответственно, имя пользователя и лицензионный ключ для статического анализатора.
Тут же добавьте переменные 'MAIL_USER' и 'MAIL_PASSWORD', содержащие имя пользователя и пароль от почтового ящика, который нужно использовать для отправки отчетов.
При запуске задачи Travis CI берет инструкции из файла '.travis.yml', лежащего в корне репозитория.
Используя Travis CI, можно запускать статический анализ как в виртуальной машине, так и используя для этого предварительно настроенный контейнер. Результаты этих подходов ничем не отличаются друг от друга, но использование предварительно настроенного контейнера может пригодиться, например, если у нас уже есть контейнер с каким-то специфическим окружением, внутри которого собирается и тестируется программный продукт, и нет желания восстанавливать это окружение в Travis CI.
В качестве примера для сборки и тестирования используется виртуальная машина на базе Ubuntu Trusty, ее описание можно посмотреть по ссылке.
Первым делом указывается язык, на котором написан проект (в данном случае это С) и перечисляются компиляторы, которые используются для сборки:
language: c
compiler:
- gcc
- clang
Примечание: при указании более одного компилятора, задачи будут запускаться параллельно для каждого из них. Подробнее можно прочитать в документации.
Перед началом сборки нам необходимо добавить репозиторий анализатора, установить зависимости и дополнительные пакеты:
before_install:
- sudo add-apt-repository ppa:ubuntu-lxc/daily -y
- wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt |sudo apt-key add -
- sudo wget -O /etc/apt/sources.list.d/viva64.list
https://files.pvs-studio.com/etc/viva64.list
- sudo apt-get update -qq
- sudo apt-get install -qq coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls-dev libselinux1-dev linux-libc-dev pvs-studio
libio-socket-ssl-perl libnet-ssleay-perl sendemail
ca-certificates
Перед сборкой проекта необходимо подготовить окружение:
script:
- ./coccinelle/run-coccinelle.sh -i
- git diff --exit-code
- export CFLAGS="-Wall -Werror"
- export LDFLAGS="-pthread -lpthread"
- ./autogen.sh
- rm -Rf build
- mkdir build
- cd build
- ../configure --enable-tests --with-distro=unknown
Далее нам необходимо создать файл с лицензией и запустить анализ проекта.
Первой командой создаем файл с лицензией для анализатора. Данные для переменных '$PVS_USERNAME' и '$PVS_KEY' берутся из настроек проекта.
- pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
Следующей командой запускаем трассировку сборки проекта:
- pvs-studio-analyzer trace -- make -j4
После запускаем статический анализ.
Примечание: при использовании триальной лицензии необходимо указывать параметр '‑‑disableLicenseExpirationCheck'.
- pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic
-o PVS-Studio-${CC}.log
–-disableLicenseExpirationCheck
Последней командой файл с результатами работы анализатора конвертируется в html-отчет.
- plog-converter -t html PVS-Studio-${CC}.log
-o PVS-Studio-${CC}.html
Так как TravisCI не позволяет изменять формат почтовых уведомлений, то для отсылки отчетов на последнем шаге воспользуемся пакетом 'sendemail':
- sendemail -t mail@domain.com
-u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-s smtp.gmail.com:587
-xu $MAIL_USER
-xp $MAIL_PASSWORD
-o tls=yes
-f $MAIL_USER
-a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
Полный текст конфигурационного файла для запуска анализатора в виртуальной машине:
language: c
compiler:
- gcc
- clang
before_install:
- sudo add-apt-repository ppa:ubuntu-lxc/daily -y
- wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt |sudo apt-key add -
- sudo wget -O /etc/apt/sources.list.d/viva64.list
https://files.pvs-studio.com/etc/viva64.list
- sudo apt-get update -qq
- sudo apt-get install -qq coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls-dev libselinux1-dev linux-libc-dev pvs-studio
libio-socket-ssl-perl libnet-ssleay-perl sendemail
ca-certificates
script:
- ./coccinelle/run-coccinelle.sh -i
- git diff --exit-code
- export CFLAGS="-Wall -Werror"
- export LDFLAGS="-pthread -lpthread"
- ./autogen.sh
- rm -Rf build
- mkdir build
- cd build
- ../configure --enable-tests --with-distro=unknown
- pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
- pvs-studio-analyzer trace -- make -j4
- pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic
-o PVS-Studio-${CC}.log
--disableLicenseExpirationCheck
- plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
- sendemail -t mail@domain.com
-u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT"
-s smtp.gmail.com:587
-xu $MAIL_USER
-xp $MAIL_PASSWORD
-o tls=yes
-f $MAIL_USER
-a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
Для запуска статического анализатора в контейнере, предварительно создайте его, используя следующий 'Dockerfile':
FROM docker.io/ubuntu:trusty
ENV CFLAGS="-Wall -Werror"
ENV LDFLAGS="-pthread -lpthread"
RUN apt-get update && apt-get install -y software-properties-common wget \
&& wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt |
sudo apt-key add - \
&& wget -O /etc/apt/sources.list.d/viva64.list
https://files.pvs-studio.com/etc/viva64.list \
&& apt-get update \
&& apt-get install -yqq coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls-dev libselinux1-dev linux-libc-dev
pvs-studio git libtool autotools-dev automake
pkg-config clang make libio-socket-ssl-perl
libnet-ssleay-perl sendemail ca-certificates \
&& rm -rf /var/lib/apt/lists/*
Конфигурационный файл для запуска контейнера может выглядеть так:
before_install:
- docker pull docker.io/oandreev/lxc
env:
- CC=gcc
- CC=clang
script:
- docker run
--rm
--cap-add SYS_PTRACE
-v $(pwd):/pvs
-w /pvs
docker.io/oandreev/lxc
/bin/bash -c " ./coccinelle/run-coccinelle.sh -i
&& git diff --exit-code
&& ./autogen.sh
&& mkdir build && cd build
&& ../configure CC=$CC
&& pvs-studio-analyzer credentials
$PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
&& pvs-studio-analyzer trace -- make -j4
&& pvs-studio-analyzer analyze -j2
-l PVS-Studio.lic
-o PVS-Studio-$CC.log
--disableLicenseExpirationCheck
&& plog-converter -t html
-o PVS-Studio-$CC.html
PVS-Studio-$CC.log
&& sendemail -t mail@domain.com
-u 'PVS-Studio $CC report, commit:$TRAVIS_COMMIT'
-m 'PVS-Studio $CC report, commit:$TRAVIS_COMMIT'
-s smtp.gmail.com:587
-xu $MAIL_USER -xp $MAIL_PASSWORD
-o tls=yes -f $MAIL_USER
-a PVS-Studio-${CC}.log PVS-Studio-${CC}.html"
Примечание: при запуске контейнера необходимо указывать параметр '‑‑cap-add SYS_PTRACE' либо '‑‑security-opt seccomp:unconfined', так как для трассировки компиляции используется системный вызов 'ptrace'.
После загрузки конфигурационного файла '.travis.yml' в корень репозитория, Travis CI получит уведомление о наличии изменений в проекте и автоматически запустит сборку.
Подробную информацию о ходе сборки и проверке анализатором можно увидеть в консоли.
После окончания тестов на почту будет отправлено 2 письма: одно с результатами статического анализа при сборке проекта с использованием 'gcc', а второе с использованием 'clang'.
CircleCI – облачный CI-сервис для автоматизации сборки, тестирования и публикации программного обеспечения. Поддерживает сборку проектов как в контейнерах, так и в виртуальных машинах с ОС Windows, Linux и macOS.
В данной документации рассматривается пример по интеграции PVS-Studio для анализа C и C++ кода. Команды запуска PVS-Studio для анализа C# или Java кода будут отличаться. Смотрите соответствующие разделы документации: "Проверка проектов Visual Studio / MSBuild / .NET из командной строки с помощью PVS-Studio" и "Работа с ядром Java анализатора из командной строки".
При запуске сборки проекта CircleCI читает конфигурацию задачи из файла в репозитории по пути '.circleci/config.yml'.
Перед добавлением файла с конфигурацией необходимо добавить в проект переменные, содержащие лицензионные данные для анализатора. Для этого в левой панели навигации необходимо нажать 'Settings', потом в группе 'ORGANIZATION' выбрать пункт 'Projects' и нажать на шестерёнку справа от нужного проекта.
В открытом окне с настройками в разделе 'Environment Variables' создайте переменные 'PVS_USERNAME' и 'PVS_KEY', содержащие имя пользователя и лицензионный ключ для PVS-Studio.
Теперь создадим '.circleci/config.yml'.
Вначале необходимо указать образ виртуальной машины, на которой будет происходить сборка и анализ. Полный список образов доступен по ссылке.
version: 2.1
jobs:
build:
machine:
image: ubuntu-2204:current
Далее надо загрузить исходники проекта. А также, через менеджер пакетов, необходимо добавить репозитории и установить инструменты и зависимости проекта:
steps:
# Downloading sources from the Github repository
- checkout
# Setting up the environment
- run: sudo apt-get install -y cmake
- run: sudo apt-get update
- run: sudo apt-get install -y build-essential
Далее добавляется репозиторий PVS-Studio и устанавливается анализатор:
- run: wget -q -O - https://cdn.pvs-studio.com/etc/pubkey.txt
| sudo apt-key add –
- run: sudo wget -O /etc/apt/sources.list.d/viva64.list
https://cdn.pvs-studio.com/etc/viva64.list
- run: sudo apt-get -y update && sudo apt-get -y install pvs-studio
Зарегистрировать лицензию анализатора можно следующей командой:
- run: pvs-studio-analyzer credentials -o PVS.lic ${PVS_USERNAME}
${PVS_KEY}
Одним из возможных вариантов анализа (C++) является создание файла compile_commands.json при сборке проекта:
- run: mkdir build && cd build && cmake ..
-DCMAKE_EXPORT_COMPILE_COMMANDS=On
После получения файла compile_commands.json анализ производится следующей командой:
- run: pvs-studio-analyzer analyze -j2 -l PVS.lic -o PVS-Studio.log
-f ./build/compile_commands.json
--disableLicenseExpirationCheck
Полученный файл с "сырыми" результатами работы анализатора необходимо сконвертировать в html-отчет:
- run: plog-converter -t html -o PVS-Studio.html PVS-Studio.log
После завершения тестов отчеты анализатора сохраняются как артефакт:
- run: mkdir PVS_Result && cp PVS-Studio.* ./PVS_Result/
- store_artifacts:
path: ./PVS_Result
Полный текст '.circleci/config.yml':
version: 2.1
jobs:
build:
machine:
image: ubuntu-2204:current
steps:
# Downloading sources from the Github repository
- checkout
# Setting up the environment
- run: sudo apt-get install -y cmake
- run: sudo apt-get update
- run: sudo apt-get install -y build-essential
# Installation of PVS-Studio
- run: wget -q -O - https://cdn.pvs-studio.com/etc/pubkey.txt
| sudo apt-key add -
- run: sudo wget -O /etc/apt/sources.list.d/viva64.list
https://cdn.pvs-studio.com/etc/viva64.list
- run: sudo apt-get -y update && sudo apt-get -y install pvs-studio
# PVS-Studio license activation
- run: pvs-studio-analyzer credentials -o PVS.lic ${PVS_ PVS_USERNAME}
${PVS_KEY}
# Building the project
- run: mkdir build && cd build && cmake ..
-DCMAKE_EXPORT_COMPILE_COMMANDS=On
# Running analysis. The compile_commands.json file obtained
# when building the project is used
- run: pvs-studio-analyzer analyze -j2 -l PVS.lic -o PVS-Studio.log
-f ./build/compile_commands.json
--disableLicenseExpirationCheck
# Converting the analyzer report to HTML format
- run: plog-converter -t html -o PVS-Studio.html PVS-Studio.log
# Creating a directory with analysis artifacts
# and copying analyzer reports (PVS-Studio.log and PVS-Studio.html)
# into it
- run: mkdir PVS_Result && cp PVS-Studio.* ./PVS_Result/
# Saving workflow artifacts
- store_artifacts:
path: ./PVS_Result
После загрузки сценария в репозиторий, CircleCI автоматически начнет сборку проекта.
По окончании работы сценария, файлы с результатами работы анализатора можно скачать через вкладку 'Artifacts'.
GitLab – это онлайн-сервис, предназначенный для управления репозиториями. Его можно использовать прямо в браузере на официальном сайте, зарегистрировав аккаунт, или установить и развернуть на собственном сервере.
В данной документации рассматривается пример по интеграции PVS-Studio для анализа C и C++ кода. Команды запуска PVS-Studio для анализа C# или Java кода будут отличаться. Смотрите соответствующие разделы документации: "Проверка проектов Visual Studio / MSBuild / .NET из командной строки с помощью PVS-Studio" и "Работа с ядром Java анализатора из командной строки".
При запуске задачи GitLab CI берет инструкции из файла '.gitlab-ci.yml'. Его можно добавить либо кликнув на кнопку 'Set up CI/CD', либо создав в локальном репозитории и загрузив на сайт. Воспользуемся первым вариантом:
Составим пример для скрипта:
image: debian
job:
script:
Скачиваем анализатор и утилиту 'sendemail', которая понадобится нам в дальнейшем:
- apt-get update && apt-get -y install wget gnupg
- wget -O - https://files.pvs-studio.com/etc/pubkey.txt | apt-key add -
- wget -O /etc/apt/sources.list.d/viva64.list
https://files.pvs-studio.com/etc/viva64.list
- apt-get update && apt-get -y install pvs-studio
sendemail
Далее, устанавливаются зависимости и утилиты для сборки. Для примера показана сборка OBS:
- apt-get -y install build-essential cmake
make pkg-config libx11-dev libgl1-mesa-dev
libpulse-dev libxcomposite-dev
libxinerama-dev libv4l-dev libudev-dev libfreetype6-dev
libfontconfig-dev qtbase5-dev
libqt5x11extras5-dev libx264-dev libxcb-xinerama0-dev
libxcb-shm0-dev libjack-jackd2-dev libcurl4-openssl-dev
libavcodec-dev libqt5svg5 libavfilter-dev
libavdevice-dev libsdl2-dev ffmpeg
qt5-default qtscript5-dev libssl-dev
qttools5-dev qttools5-dev-tools qtmultimedia5-dev
libqt5svg5-dev libqt5webkit5-dev libasound2
libxmu-dev libxi-dev freeglut3-dev libasound2-dev
libjack-jackd2-dev libxrandr-dev libqt5xmlpatterns5-dev
libqt5xmlpatterns5 coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls28-dev libselinux1-dev linux-libc-dev
libtool autotools-dev
libio-socket-ssl-perl
libnet-ssleay-perl ca-certificates
Теперь нужно создать файл с лицензией анализатора. По умолчанию будет создан файл 'PVS-Studio.lic' в директории '~/.config/PVS-Studio'. В этом случае файл лицензии можно не указывать в параметрах запуска анализатора, он будет подхвачен автоматически:
- pvs-studio-analyzer credentials $PVS_NAME $PVS_KEY
Здесь 'PVS_NAME' и 'PVS_KEY' – переменные для имени пользователя и лицензионного ключа PVS-Studio, значения которых задаются в настройках репозитория. Чтобы установить их перейдём в 'Settings -> CI/CD -> Variables'.
Сборка проекта осуществляется, используя 'cmake':
- cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On /builds/Stolyarrrov/obscheck/
- make -j4
Далее запускается анализатор:
- pvs-studio-analyzer analyze -o PVS-Studio.log
В полученном 'PVS-Studio.log' хранятся результаты анализа в "сыром" виде. Их необходимо сконвертировать в один из необходимых форматов при помощи утилиты 'plog-converter'.
Перевод отчета в html формат:
- plog-converter -t html PVS-Studio.log -o PVS-Studio.html
Отчёт можно выгрузить при помощи артефактов. Но в данном примере предлагаем воспользоваться отправкой отчета на почту при помощи утилиты 'sendemail':
- sendemail -t $MAIL_TO
-m "PVS-Studio report, commit:$CI_COMMIT_SHORT_SHA"
-s $GMAIL_PORT
-o tls=auto
-f $MAIL_FROM
-xu $MAIL_FROM
-xp $MAIL_FROM_PASS
-a PVS-Studio.log PVS-Studio.html
Полный '.gitlab-ci.yml':
image: debian
job:
script:
- apt-get update && apt-get -y install wget gnupg
- wget -O - https://files.pvs-studio.com/etc/pubkey.txt | apt-key add -
- wget -O /etc/apt/sources.list.d/viva64.list
https://files.pvs-studio.com/etc/viva64.list
- apt-get update && apt-get -y install pvs-studio
sendemail
- apt-get -y install build-essential cmake
pkg-config libx11-dev libgl1-mesa-dev
libpulse-dev libxcomposite-dev
libxinerama-dev libv4l-dev libudev-dev libfreetype6-dev
libfontconfig-dev qtbase5-dev
libqt5x11extras5-dev libx264-dev libxcb-xinerama0-dev
libxcb-shm0-dev libjack-jackd2-dev libcurl4-openssl-dev
libavcodec-dev libqt5svg5 libavfilter-dev
libavdevice-dev libsdl2-dev ffmpeg
qt5-default qtscript5-dev libssl-dev
qttools5-dev qttools5-dev-tools qtmultimedia5-dev
libqt5svg5-dev libqt5webkit5-dev libasound2
libxmu-dev libxi-dev freeglut3-dev libasound2-dev
libjack-jackd2-dev libxrandr-dev libqt5xmlpatterns5-dev
libqt5xmlpatterns5 coccinelle parallel
libapparmor-dev libcap-dev libseccomp-dev
python3-dev python3-setuptools docbook2x
libgnutls28-dev libselinux1-dev linux-libc-dev
libtool autotools-dev
make libio-socket-ssl-perl
libnet-ssleay-perl ca-certificates
- pvs-studio-analyzer credentials $PVS_NAME $PVS_KEY
- cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On /builds/Stolyarrrov/obscheck/
- make -j4
- pvs-studio-analyzer analyze -o PVS-Studio.log
- plog-converter -t html PVS-Studio.log -o PVS-Studio.html
- sendemail -t $MAIL_TO
-m "PVS-Studio report, commit:$CI_COMMIT_SHORT_SHA"
-s $GMAIL_PORT
-o tls=auto
-f $MAIL_FROM
-xu $MAIL_FROM
-xp $MAIL_FROM_PASS
-a PVS-Studio.log PVS-Studio.html
Если всё сделано правильно, при нажатии на кнопку 'commit changes', появится надпись: 'This GitLab CI configuration is valid'. Чтобы отследить прогресс выполнения задачи перейдите во вкладку 'CI/CD -> Pipelines'.
Нажав на кнопку 'running' можно увидеть окно терминала виртуальной машины, на которой выполняется заданный сценарий сборки и анализа. Спустя некоторое время получаем сообщение: 'Job succeeded'.
Для преобразования результатов анализа PVS-Studio в отчет Code Quality воспользуйтесь утилитой Plog Converter.
Чтобы пути до исходных файлов в отчете отобразились корректно, нужно использовать флаг ‑‑sourcetree-root (-r) при запуске анализа. Команда запуска анализа:
- pvs-studio-analyzer analyze -r "path/to/build/project" -o PVS-Studio.log
Команда конвертации:
- plog-converter -t gitlab -o PVS-Studio.log.gitlab.json PVS-Studio.log
Для задач на Widows используйте команду:
- PlogConverter.exe -t GitLab -o .\ PVS-Studio.plog
После того как отчет сформирован, его необходимо сохранить как артефакт. Для этого добавьте в конфигурационный файл '.gitlab-ci.yml' следующий шаг:
artifacts:
reports:
codequality: [./PVS-Studio.log.gitlab.json]
В результатах выполненной задачи во вкладке Сode Quality добавятся предупреждения анализатора.
GitHub Actions – это платформа, позволяющая решать CI/CD задачи, связанные с кодом в репозиториях GitHub. При помощи скриптованных Workflows она автоматизирует реакции на события, происходящие в репозитории. Это позволяет автоматически проверять собираемость проекта и проводить тестирование сразу, как только в репозиторий добавляют новый код. Средой выполнения рабочих нагрузок могут выступать как облачные виртуальные машины, так и локальные агенты с предоставленной вами конфигурацией.
В данной документации рассматривается пример по интеграции PVS-Studio для анализа C и C++ кода. Команды запуска PVS-Studio для анализа C# или Java кода будут отличаться. Смотрите соответствующие разделы документации: "Проверка проектов Visual Studio / MSBuild / .NET из командной строки с помощью PVS-Studio" и "Работа с ядром Java анализатора из командной строки".
Для того чтобы создать новый Workflow, нужно добавить YAML скрипт в директорию '.github/workflows' репозитория.
Рассмотрим следующий пример скрипта 'build-analyze.yml', который позволяет полностью проверить проект в PVS-Studio:
name: PVS-Studio build analysis
on: workflow_dispatch
jobs:
build-analyze:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v2
- name: Install tools
run: |
wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt \
| sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list
sudo apt update
sudo apt install pvs-studio
pvs-studio-analyzer credentials ${{ secrets.PVS_STUDIO_CREDENTIALS }}
- name: Build
run: |
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On -B build .
cmake --build build -j
- name: Analyze
run: |
pvs-studio-analyzer analyze -f build/compile_commands.json -j
- name: Convert report
run: |
plog-converter -t sarif -o pvs-report.sarif PVS-Studio.log
- name: Publish report
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: pvs-report.sarif
category: PVS-Studio
Поле 'name' в начале задаёт имя текущего Workflow, которое будет отображаться в интерфейсе GitHub.
Поле 'on' определяет, при каком событии Workflow должен запуститься. Значение 'workflow_dispatch' указывает, что задача запускается вручную. Чтобы её запустить, нужно нажать кнопку 'Run workflow' у соответствующего Workflow.
Поле 'runs-on' указывает, на какой системе должна выполняться задача. GitHub Actions предоставляет облачные серверы на системах Windows, Linux, macOS. В данном случае используется Ubuntu.
Далее идёт последовательность шагов 'steps', которые выполняют некоторые действия или последовательность команд оболочки.
Шаг 'Check out repository code' скачивает текущую версию кода репозитория.
Шаг 'Install tools' устанавливает и активирует PVS-Studio регистрационными данными в систему. Для активации PVS-Studio используется шифрованная переменная 'secrets.PVS_STUDIO_CREDENTIALS', которая содержит идентификатор пользователя и ключ.
Чтобы создать секрет, перейдите во вкладку 'Settings > Secrets > Actions' и нажмите кнопку 'New repository secret'.
Создайте новую переменную с именем пользователя и ключом. GitHub сохранит её в зашифрованном виде, после чего её нельзя будет посмотреть. Даже в выводе консоли текст переменной будет видоизменен.
Шаг 'Build' выполняет сборку проекта, в данном случае при помощи CMake. Также в нем генерируется файл 'compile_commands.json', который используется анализатором для определения целей анализа.
Шаг 'Analyze' запускает анализ проекта и сохраняет результат в виде внутреннего представления в файл по умолчанию 'PVS-Studio.log'.
Более подробно о параметрах запуска pvs-studio-analyzer смотрите в документации.
Шаг 'Convert report' преобразует отчёт анализатора в требуемое представление, в данном случае – SARIF. Утилита plog-converter позволяет преобразовывать, объединять отчёты с разных запусков анализа и фильтровать сообщения в них.
Наконец, шаг 'Publish report' публикует итоговый отчёт, после чего его можно просмотреть во вкладке 'Security'.
Для того чтобы анализировать изменения файлов сразу после того, как они были отправлены в репозиторий, создайте новый скрипт 'analyze-changes.yml'.
name: PVS-Studio analyze changes
on:
push:
paths:
- '**.h'
- '**.c'
- '**.cpp'
jobs:
analyze-changes:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
....
- name: Get list of changed source files
run: |
echo "$(git diff --name-only \
${{ github.event.before }}..${{ github.event.after }})" \
> source-files.txt
cat source-files.txt
- name: Install tools
....
- name: Build
run: |
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On -B build .
cmake --build build -j
- name: Analyze
run: |
pvs-studio-analyzer analyze -f build/compile_commands.json \
-S source-files.txt -j
- name: Convert report
....
- name: Publish report
....
В нём используется событие запуска 'push', которое стартует, когда в репозитории происходят изменения. При этом в нём заданы фильтры путей 'paths', чтобы анализ запускался только при изменении файлов с исходным кодом.
Шаг 'Get list of changed source files' для шага 'Analyze' получает список изменившихся файлов, которые необходимо проанализировать.
Сборка проекта при этом происходит на случай, если в нём есть генерируемые файлы, содержимое которых появляется только в процессе сборки.
Используя средства GitHub Actions, возможно реализовать самые разные сценарии, в том числе при изменениях не только в главной ветке, но и в других ветках или Pull Request'ах.
Azure DevOps – облачная платформа, предоставляющая возможность разработки, выполнения приложений и хранения данных на удаленных серверах. В состав платформы входят инструменты Azure Pipeline, Azure Board, Azure Artifacts и другие, позволяющие ускорить процесс создания программного обеспечения и повысить его качество.
В данной документации рассматривается пример по интеграции PVS-Studio для анализа C# кода. Команды запуска PVS-Studio для анализа C, C++ или Java кода будут отличаться. Смотрите соответствующие разделы документации: "Кроссплатформенная проверка C и C++ проектов в PVS-Studio" и "Работа с ядром Java анализатора из командной строки".
Рассмотрим пример для интеграции анализа в сборку проекта.
Перейдите в раздел 'Pipelines -> Builds' и создайте новый Build pipeline.
Укажите источник проекта. Например, GitHub.
Авторизуйте приложение Azure Pipelines и укажите репозиторий с проектом.
В окне выбора шаблона укажите 'Starter pipeline'.
Запустить статический анализ кода проекта можно, используя Microsoft-hosted либо self-hosted агенты.
Microsoft-hosted агенты представляют собой обычные виртуальные машины, которые запускаются при запуске Pipeline и удаляются после завершения задачи. Использование таких агентов позволяет не тратить время на их поддержку и обновление.
Заменим сценарий сборки по умолчанию следующим образом:
# Настройка триггеров запуска. Запуск для изменений только в master-ветке
trigger:
- master
# Так как установка произвольного ПО в виртуальные машины
# запрещена, воспользуемся Docker-контейнером,
# запущенном в виртуальной машине с Windows Server 1803
pool:
vmImage: 'win1803'
container: microsoft/dotnet-framework:4.7.2-sdk-windowsservercore-1803
steps:
# Загрузка дистрибутива анализатора
- task: PowerShell@2
inputs:
targetType: 'inline'
script: 'Invoke-WebRequest
-Uri https://files.pvs-studio.com/PVS-Studio_setup.exe
-OutFile PVS-Studio_setup.exe'
- task: CmdLine@2
inputs:
workingDirectory: $(System.DefaultWorkingDirectory)
script: |
# Восстанавливаем проект и скачиваем зависимости
nuget restore .\ShareX.sln
# Создаем директорию, куда будут сохранены файлы с отчетами анализатора
md .\PVSTestResults
# Устанавка анализатора
PVS-Studio_setup.exe /VERYSILENT /SUPPRESSMSGBOXES
/NORESTART /COMPONENTS=Core
# Регистрация лицензионной информации
"C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe"
credentials
-u $(PVS_USERNAME)
-n $(PVS_KEY)
# Запуск анализа PVS-Studio
"C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe"
-t .\ShareX.sln
-o .\PVSTestResults\ShareX.plog
# Преобразование отчета в html формат.
"C:\Program Files (x86)\PVS-Studio\PlogConverter.exe"
-t html
-o .\PVSTestResults\
.\PVSTestResults\ShareX.plog
# Публикация отчетов анализатора
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: PVSTestResults
artifactName: PVSTestResults
Далее нужно добавить переменные, которые используются для создания файла лицензии. Для этого откройте окно редактирования Pipeline, и в правом верхнем углу нажмем кнопку 'Variables'.
Добавьте переменные – 'PVS_USERNAME' и 'PVS_KEY', содержащие имя пользователя и лицензионный ключ соответственно. При создании переменной 'PVS_KEY' не забудьте отметить пункт 'Keep this value secret' для шифрования значения переменной, а также подавления вывода значения переменной в лог выполнения задачи.
Для проверки запустите Pipeline кнопкой 'Run'.
Второй вариант запуска анализа – использовать self-hosted агент. Self-hosted агенты - это агенты, настраиваемые и управляемые самостоятельно. Такие агенты дают больше возможностей для установки программного обеспечения, которое необходимо для сборки и тестирования программного продукта.
Перед использованием таких агентов их необходимо настроить согласно инструкции, а также установить и настроить статический анализатор.
Для запуска задачи на self-hosted агенте заменим предлагаемую конфигурацию по умолчанию на следующую:
# Настройка триггеров запуска. Анализ для master-ветки
trigger:
- master
# Задача запускается на self-hosted агенте из пула 'MyPool'
pool: 'MyPool'
steps:
- task: CmdLine@2
inputs:
workingDirectory: $(System.DefaultWorkingDirectory)
script: |
# Восстанавливаем проект и скачиваем зависимости
nuget restore .\ShareX.sln
# Создаем директорию, куда будут сохранены файлы с отчетами анализатора
md .\PVSTestResults
# Запуск анализа PVS-Studio.
"C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe"
-t .\ShareX.sln
-o .\PVSTestResults\ShareX.plog
# Преобразование отчета в html формат.
"C:\Program Files (x86)\PVS-Studio\PlogConverter.exe"
-t html
-o .\PVSTestResults\
.\PVSTestResults\ShareX.plog
# Публикация отчетов анализатора
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: PVSTestResults
artifactName: PVSTestResults
После выполнения задачи, архив с отчетами анализатора можно скачать во вкладке 'Summary', либо можно воспользоваться расширением Send Mail, позволяющим настроить отправку электронной почты.
Если вы хотите, чтобы при наличии предупреждений анализатора сборочный шаг завершился и выдал сообщение об ошибке, вы можете использовать утилиту PlogConverter. С помощью PlogConverter можно задать порог, при котором будет происходить остановка работы. Далее приведен пример кода по добавлению в конец конфигурации шага остановки:
- task : PowerShell@2
inputs:
targetType: 'inline'
script: |
& "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t json -a GA:1
-o .\PVSTestResults\ .\PVSTestResults\TestTask.plog
--indicateWarnings --noHelpMessages
IF ($LASTEXITCODE -eq 0) {exit 0} ELSE {Write-Host
"##vso[task.logissue type=error]Analysis log contains High level warnings.";
Write-Host "##vso[task.complete result=Failed;]"; exit 0 }
Что бы изменить тип предупреждений, на которые будет реагировать задача, воспользуйтесь флагом ‑‑analyzer (-a) утилиты PlogConverter.
Для просмотра отчета анализатора на странице результатов выполнения можно воспользоваться расширением SARIF SAST Scans Tab.
Чтобы конвертировать отчет в формат SARIF и воспользоваться расширением, добавьте следующие шаги:
- task: CmdLine@2
inputs:
workingDirectory: $(System.DefaultWorkingDirectory)
script: "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t sarif
-o .\PVSTestResults\ .\PVSTestResults\TestTask.plog
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: .\PVSTestResults\TestTask.plog.sarif
ArtifactName: CodeAnalysisLogs
После выполнения задачи на странице с результатами во вкладке Scans будет добавлен отчет анализатора.
AppVeyor — web-сервис непрерывной интеграции. Он предназначен для сборки и тестирования ПО, расположенного на GitHub и ряде других сервисов хранения исходного кода.
В данной документации рассматривается пример по интеграции PVS-Studio для анализа C и C++ кода. Команды запуска PVS-Studio для анализа C# или Java кода будут отличаться. Смотрите соответствующие разделы документации: "Проверка проектов Visual Studio / MSBuild / .NET из командной строки с помощью PVS-Studio" и "Работа с ядром Java анализатора из командной строки".
Необходимо задать переменные окружения, с помощью которых сформируется файл лицензии. Для этого необходимо перейти в интересующий проект, открыть вкладку 'Settings', в появившейся боковой панели перейти на вкладку 'Environment'. Далее нужно добавить две переменные — 'PVS_KEY' и 'PVS_USERNAME':
Они будут содержать лицензионный ключ и имя пользователя соответственно. Эти переменные необходимы для проверки лицензии анализатора.
Чтобы запустить анализ, необходимо добавить скрипт. Для этого на панели настроек нужно перейти на вкладку 'Tests', в появившемся окне нажать 'Script':
В появившуюся форму нужно добавить код:
sudo apt-get update && sudo apt-get -y install jq
wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt \
| sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list
sudo apt-get update && sudo apt-get -y install pvs-studio
pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY
PWD=$(pwd -L)
pvs-studio-analyzer analyze -j8 \
-o PVS-Studio.log \
--disableLicenseExpirationCheck
plog-converter -t errorfile PVS-Studio.log --cerr -w
Примечание. Присваивание значения команды 'pwd' переменной '$PWD' необходимо для корректной работы анализатора, поскольку AppVeyor модифицирует переменную для своих служебных целей в другое значение.
Результат анализа проекта будет сохранён в файл 'PVS-Studio.errorfile'.
Документация по использующимся утилитам:
Для анализа pull requests необходимо произвести дополнительные настройки.
На вкладке 'General' нужно включить сохранение кэша для сборки Pull Requests (checkbox находится внизу страницы):
Далее необходимо прейти на вкладку 'Environment', где нужно указать папку для кэширования (поле для добавления находится внизу страницы):
Без данной настройки проект будет анализироваться целиком.
Чтобы запустить анализ, необходимо добавить скрипт. Для этого на панели настроек нужно перейти на вкладку 'Tests', в появившемся окне нажать 'Script':
В появившуюся форму нужно добавить код:
sudo apt-get update && sudo apt-get -y install jq
wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt \
| sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list
sudo apt-get update && sudo apt-get -y install pvs-studio
pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY
PWD=$(pwd -L)
if [ "$APPVEYOR_PULL_REQUEST_NUMBER" != '' ]; then
PULL_REQUEST_ID="pulls/$APPVEYOR_PULL_REQUEST_NUMBER"
MERGE_BASE=`wget -qO - \
https://api.github.com/repos/${APPVEYOR_REPO_NAME}/${PULL_REQUEST_ID} \
| jq -r ".base.ref"`
git diff --name-only HEAD origin/$MERGE_BASE > .pvs-pr.list
pvs-studio-analyzer analyze -j8 \
-o PVS-Studio.log \
--disableLicenseExpirationCheck \
--dump-files --dump-log pvs-dump.log \
-S .pvs-pr.list
else
pvs-studio-analyzer analyze -j8 \
-o PVS-Studio.log \
--disableLicenseExpirationCheck
fi
plog-converter -t errorfile PVS-Studio.log --cerr -w
Примечание. Присваивание значения команды 'pwd' переменной '$PWD' необходимо для корректной работы анализатора, поскольку AppVeyor модифицирует переменную для своих служебных целей в другое значение.
Если анализируется pull request, то будет получена разница между ветками. После этого запустится анализ для изменённых файлов. В противном случае проект будет проанализирован полностью.
Результат анализа проекта будет сохранён в файл 'PVS-Studio.errorfile'.
Документация по использующимся утилитам:
С документацией по анализу pull/merge requests можно ознакомиться здесь.
Buddy — платформа, позволяющая автоматизировать сборку, тестирование и публикацию программного обеспечения. Поддерживает сервисы хостинга: GitHub, Bitbucket, GitLab.
В данной документации рассматривается пример по интеграции PVS-Studio для анализа C и C++ кода. Команды запуска PVS-Studio для анализа C# или Java кода будут отличаться. Смотрите соответствующие разделы документации: "Проверка проектов Visual Studio / MSBuild / .NET из командной строки с помощью PVS-Studio" и "Работа с ядром Java анализатора из командной строки".
В первую очередь необходимо создать pipeline, в котором будет запускаться анализ. Для этого нужно перейти в настройки проекта, после чего нажать 'New pipeline':
В появившемся окне необходимо указать конфигурацию, название pipeline'а и условие для его запуска. После внесения данных нужно нажать 'Add pipeline':
Для анализа pull requests в качестве триггера необходимо выбрать 'On events'. Событие для срабатывания — 'Git create branch, tag or PR'. Далее необходимо прейти в настройки триггера, где на вкладке 'Wildcard' нужно добавить 'all pull requests':
После создания pipeline'а нужно указать компилятор, который будет использоваться для сборки проекта. Разберём на примере GCC, написав его название в поиск:
В списке найденных компиляторов выберите нужный.
После выбора компилятора необходимо задать переменные окружения, с помощью которых будет сформирован файл лицензии. Для этого нужно нажать на 'Variables' и добавить переменные 'PVS_USERNAME' и 'PVS_KEY', которые будут содержать имя пользователя и лицензионный ключ соответственно:
Необходимо перейти на вкладку 'Docker' и нажать 'Package & Tools':
В появившуюся форму нужно ввести команды установки анализатора:
apt-get update && apt-get -y install wget gnupg jq
wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt | apt-key add -
wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list
apt-get update && apt-get -y install pvs-studio
Для анализа всего проекта необходимо перейти на вкладку 'Run':
В появившуюся форму нужно ввести команды запуска анализатора:
pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY
pvs-studio-analyzer analyze -j8 \
-o PVS-Studio.log \
--disableLicenseExpirationCheck
plog-converter -t errorfile PVS-Studio.log --cerr -w
После внесения данных необходимо нажать 'Add this action'.
При выполнении данного pipeline'а сформируется отчёт с результатом анализа. Он будет сохранён в файл 'PVS-Studio.errorfile'.
Документация по использующимся утилитам:
Для анализа pull requests необходимо перейти на вкладку 'Run'.
В появившуюся форму нужно ввести команды анализа pull/merge requests:
pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY
if [ "$BUDDY_EXECUTION_PULL_REQUEST_NO" != '' ]; then
PULL_REQUEST_ID="pulls/$BUDDY_EXECUTION_PULL_REQUEST_NO"
MERGE_BASE=`wget -qO - \
https://api.github.com/repos/${BUDDY_REPO_SLUG}/${PULL_REQUEST_ID} \
| jq -r ".base.ref"`
git fetch origin
git diff --name-only HEAD origin/$MERGE_BASE > .pvs-pr.list
pvs-studio-analyzer analyze -j8 \
-o PVS-Studio.log \
--disableLicenseExpirationCheck \
-S .pvs-pr.list
else
pvs-studio-analyzer analyze -j8 \
-o PVS-Studio.log \
--disableLicenseExpirationCheck
fi
plog-converter -t errorfile PVS-Studio.log --cerr -w
После внесения данных необходимо нажать 'Add this action'.
При выполнении данного pipeline'а сформируется отчёт с результатом проверки изменённых файлов. Он будет сохранён в 'PVS-Studio.errorfile'.
Документация по использующимся утилитам:
С документацией по анализу pull/merge requests можно ознакомиться здесь.
Утилита blame-notifier предназначена для автоматизации процесса оповещения разработчиков, заложивших в репозиторий код, на который анализатор PVS-Studio выдал предупреждения. Отчет анализатора подается на вход blame-notifier с указанием дополнительных параметров; утилита находит файлы, в которых были обнаружены предупреждения и формирует HTML-отчет на каждого "виновного" разработчика. Также возможен вариант рассылки полного отчета: внутри него будут содержаться все предупреждения, относящиеся к каждому "виновному" разработчику.
Утилита blame-notifier доступна только при наличии Enterprise лицензии. Вы можете запросить пробную Enterprise лицензию здесь.
Примечание. Название утилиты различается под разными платформами. Под Windows она имеет название BlameNotifier.exe, под Linux и macOS - blame-notifier. Если речь не идёт про утилиту под конкретную ОС, во избежание дублирования в данном документе используется название blame-notifier.
Для работы утилиты blame-notifier под Linux и macOS требуется .NET Runtime 8.0.
Утилиту BlameNotifier можно найти в установочной директории PVS-Studio (по умолчанию это путь "C:\Program Files (x86)\PVS-Studio\").
Для debian-based систем:
wget -q -O - https://files.pvs-studio.com/etc/pubkey.txt | \
sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://files.pvs-studio.com/etc/viva64.list
sudo apt-get update
sudo apt-get install blame-notifier
Для yum-based систем:
wget -O /etc/yum.repos.d/viva64.repo \
https://files.pvs-studio.com/etc/viva64.repo
yum update
yum install blame-notifier
Для zypper-based систем:
wget -q -O /tmp/viva64.key https://files.pvs-studio.com/etc/pubkey.txt
sudo rpm --import /tmp/viva64.key
sudo zypper ar -f https://files.pvs-studio.com/rpm viva64
sudo zypper update
sudo zypper install blame-notifier
Прямые ссылки для загрузки.
Установка:
brew install viva64/pvs-studio/blame-notifier
Обновление:
brew upgrade blame-notifier
Прямые ссылки для загрузки.
Флаг "‑‑help" выводит основную информацию об утилите:
blame-notifier --help
Пример использования утилиты blame-notifier (одной строкой):
blame-notifier path/to/PVS-Studio.log
--VCS Git
--recipientsList recipients.txt
--server ... --sender ... --login ... --password ...
Ниже приводится краткое описание параметров утилиты:
При использовании утилиты должен быть указан как минимум один из флагов, через который задаётся список получателей отчётов: '‑‑recipientsList' или '‑‑vcsBasedRecipientsList'. При необходимости эти флаги могут использоваться совместно.
Формат файла со списком получателей отчётов:
# Получатели полного отчёта
username_1 *email_1
...
username_N *email_N
# Получатели собственных ошибок
username_1 email_1
...
username_N email_N
Закомментировать строку можно символом "#". Для получателей полных отчетов необходимо добавить символ "*" в начале или в конце email адреса. Полный отчет будет включать все предупреждения, отсортированные по разработчикам.
Маски фильтрации имеют следующим вид: MessageType:MessageLevels.
MessageType может принимать одно из следующих значений: GA, OP, 64, CS, MISRA, Fail.
MessageLevels может принимать значение от 1 до 3.
Возможна комбинация разных масок через ";" (без пробелов), например:
--analyzer=GA:1,2;64:1
В таком случае будут обработаны предупреждения общего назначения (GA) уровней 1 и 2 и 64-битные предупреждения (64) уровня 1.
В данной статье рассматривается встраивание PVS-Studio в процесс непрерывной интеграции в среде Windows. Встраивание в процесс CI в среде Linux описано в статье "Как запустить PVS-Studio в Linux".
Прежде чем перейти к основной теме данной статьи, стоит отметить, что использование PVS-Studio исключительно на сборочном сервере эффективно, но не оптимально. Лучшим вариантом будет построение двухуровневой системы проверки исходного кода - локально на машинах разработчиков и на сборочном сервере.
Этот принцип происходит из того факта, что чем раньше обнаружена ошибка, тем меньше стоимость и сложность её исправления. Поэтому необходимо стремиться к как можно более раннему обнаружению и исправлению ошибок, для чего и необходимо использование PVS-Studio на машинах разработчиков. Рекомендуется использование режима инкрементального анализа, позволяющего автоматически запускать проверку только изменённого кода после сборки проекта.
Тем не менее использование PVS-Studio на машинах разработчиков не исключает возможности попадания ошибок в систему контроля версий. Для отслеживания таких ситуаций и нужно иметь второй уровень защиты - регулярный запуск статического анализатора на сборочном сервере. Даже в случае попадания ошибки в систему контроля версий, её удастся своевременно выявить и исправить. Встраивание анализа в процесс ночных сборок позволит на следующий день получать информацию обо всех ошибках, допущенных в течение прошлого дня, и оперативно поправить проблемный код.
Примечание. Не рекомендуется конфигурировать статический анализ кода на сервере таким образом, чтобы он запускался на каждый коммит, так как анализ может занимать продолжительное время. Если необходим именно такой сценарий использования и при этом сборочной системой проекта является MSBuild, можно воспользоваться режимом инкрементального анализа command line модуля 'PVS-Studio_Cmd.exe'. За дополнительной информацией обратитесь к разделу "Инкрементальный анализ в command line модуле 'PVS-Studio_Cmd.exe'" текущего документа. Также для анализа файлов при инкрементальной сборке (вне зависимости от сборочной системы) можно использовать утилиту 'CLMonitor.exe' (только для анализа C, C++ кода). Более подробно использование утилиты 'CLMonitor.exe' описано в разделе "Система мониторинга компиляции" текущего документа.
Подготовка к встраиванию PVS-Studio в процесс непрерывной интеграции - важный этап, позволяющий сэкономить время в дальнейшем и использовать статический анализатор более эффективно. В этом разделе будут рассмотрены те особенности настройки PVS-Studio, которые упростят последующую работу.
Для установки PVS-Studio требуются права администратора. Автоматическая установка осуществляется следующей командой, выполненной в командной строке (одной строкой):
PVS-Studio_setup.exe /verysilent /suppressmsgboxes
/norestart /nocloseapplications
Выполнение этой команды приведёт к установке всех доступных компонентов PVS-Studio. Обратите внимание, что PVS-Studio может потребоваться перезагрузка компьютера в случае, например, если обновляемые файлы были заблокированы. Если установщик запустить без флага 'NORESTART', перезагрузка может произойти без предварительных уведомлений или диалогов.
В состав дистрибутива входит утилита 'PVS-Studio-Updater.exe', позволяющая проверять наличие обновлений анализатора, а в случае их наличия, загрузить и установить их на локальной машине. Для запуска утилиты в 'silent' режиме следует использовать те же параметры, что и для установки дистрибутива:
PVS-Studio-Updater.exe /verysilent /suppressmsgboxes
Файл настроек генерируется автоматически при запуске Visual Studio с установленным плагином PVS-Studio или приложением C and C++ Compiler Monitoring UI (Standalone.exe), и в дальнейшем он может быть отредактирован или скопирован на другие машины. Информация о лицензии также хранится в файле настроек. Расположение файла по умолчанию:
%AppData%\PVS-Studio\Settings.xml
Для более детальной информации по автоматическому развёртыванию PVS-Studio обратитесь к статье "Автоматическое развёртывание PVS-Studio".
Перед использованием анализатора необходимо выполнить его предварительную настройку, что поможет оптимизировать работу со списком предупреждений, а также (возможно) повысить скорость проведения анализа.
Примечание. Описанные ниже опции можно изменить, напрямую отредактировав файл настроек, или воспользовавшись интерфейсом страницы настроек в плагине к Visual Studio или в приложении C and C++ Compiler Monitoring UI.
Часто бывает полезно исключить из анализа определённые файлы или даже целые директории. Это позволит не анализировать код сторонних библиотек, сократив общее время анализа, и оставив в выводе только те предупреждения, которые актуальны именно для вашего проекта. По умолчанию в настройках анализатора уже присутствуют исключения для некоторых файлов или путей, например, для библиотеки boost. Более подробно исключение файлов из анализа описано в статье "Настройки: Don't Check Files".
На этапе внедрения также следует отключить те диагностические правила PVS-Studio, которые неактуальны для проверяемого проекта. Существует возможность отключения как отдельных диагностических правил, так и целых групп. Если вы изначально знаете диагностики, неактуальные для вашего проекта (например, 64-битные ошибки), есть смысл отключить их сразу. Это также может ускорить работу анализатора. Если список таких диагностических правил неизвестен, то при необходимости их можно будет отключить позже. Более подробно отключение диагностических правил описано в статье "Настройки: Detectable Errors".
В случае внедрения статического анализа в существующий проект, когда объём кодовой базы уже существенен, проверка исходного кода может выявить большое количество ошибок в существующем коде. Команда разработчиков, занимающихся данным проектом, может не иметь ресурсов, необходимых для исправления всех этих предупреждений. В таком случае возникает необходимость убрать все предупреждения, обнаруженные в старом коде, чтобы с текущего момента видеть только те предупреждения анализатора, которые были выданы на вновь написанный/модифицированный код.
Достичь желаемого результата можно с использованием механизма массового подавления сообщений анализатора. Более подробно про это написано в статье: "Массовое подавление сообщений анализатора".
Примечание 1. Если необходимо скрыть только отдельные предупреждения анализатора, следует воспользоваться механизмом подавления ложных срабатываний, описанном в статье "Подавление ложных предупреждений".
Примечание 2. Используя SonarQube, можно изменять отображение предупреждений, найденных за определённый период времени. Таким образом, можно настроить отображение только тех предупреждений, которые были найдены после внедрения анализатора (т.е. отключить отображение предупреждений на старый код).
PVS-Studio достаточно просто встраивается в процесс непрерывной интеграции, а также предоставляет средства для удобной обработки результатов работы анализатора.
Интеграция PVS-Studio с платформой SonarQube доступна только при наличии Enterprise лицензии. Вы можете запросить пробную Enterprise лицензию здесь.
Ниже будут описаны принципы анализа проектов, основанных на различных сборочных системах, а также утилиты для работы с результатами анализа.
В данном разделе будет описан наиболее эффективный вариант анализа MSBuild / Visual Studio проектов, то есть файлов решений Visual Studio (.sln), проектов Visual C++ (.vcxproj) и Visual C# (.csproj).
Анализ из командной строки проектов, перечисленных выше, производится за счёт использования модуля 'PVS-Studio_Cmd.exe', находящегося в установочной директории PVS-Studio. Расположение установочной директории по умолчанию - 'C:\Program Files (x86)\PVS-Studio\'.
Передавая модулю 'PVS-Studio_Cmd.exe' различные аргументы, можно регулировать параметры анализа. Увидеть список всех доступных аргументов можно, выполнив команду:
PVS-Studio_Cmd.exe --help
Анализатор имеет один обязательный аргумент - '‑‑target', с помощью которого задаётся объект для проверки (.sln, .vcxproj или .csproj файл). Остальные аргументы являются опциональными и подробно описываются в статье "Проверка Visual C++ (.vcxproj) и Visual C# (.csproj) проектов из командной строки с помощью PVS-Studio".
Рассмотрим пример запуска анализа на .sln файле (одной строкой):
PVS-Studio_Cmd.exe --target "targetsolution.sln" --platform "Any CPU"
--output "results.plog" --configuration "Release"
Исполнение этой команды запустит анализ .sln файла 'targetsolution.sln' для платформы 'Any CPU' и конфигурации 'Release'. Выходной файл ('results.plog') будет создан в директории проверяемого проекта. При анализе будут использоваться стандартные настройки анализатора, так как явно не указано обратное.
Модуль 'PVS-Studio_Cmd.exe' имеет ряд ненулевых кодов возврата, посредством которых информирует о результирующем состоянии анализа. Код возврата представляет собой битовую маску, маскирующую все возможные состояния, возникшие во время работы утилиты. То есть ненулевой код не является свидетельством того, что утилита закончила свою работу некорректно. Подробное описание кодов возврата приведено в упомянутой выше статье "Проверка Visual C++ (.vcxproj) и Visual C# (.csproj) проектов из командной строки с помощью PVS-Studio".
Примечание. Если перед анализом нужно выполнить команды из CustomBuild task (например, для генерации файлов с кодом), можно указать PVS-Studio специальную директиву. В таком случае анализатор сначала выполнит необходимые команды и только затем запустит анализ. Подробности описаны в разделе "Выполнение команд из CustomBuild task перед анализом".
При регулярном запуске анализатора может возникнуть необходимость в получении отчёта анализатора, содержащего предупреждения, выданные только на вновь написанный / модифицированный код. При ночном анализе на сборочном сервере это позволит видеть только те предупреждения, которые были допущены в течение предыдущего дня.
В случае необходимости получения такого результата при запуске модуля 'PVS-Studio_Cmd.exe' необходимо указывать аргумент командной строки '‑‑suppressAll'. При наличии этого флага все сообщения будут добавлены в базу подавленных сообщений (.suppress файлы соответствующих проектов) после сохранения результатов проверки. Следовательно, при следующей проверке эти сообщения выданы не будут. Если всё же потребуется посмотреть старые сообщения, рядом с .plog файлом, содержащим новые сообщения, будет сохранён полный лог проверки.
Механизм массового подавления предупреждений анализатора более подробно описан в статье "Массовое подавление сообщений анализатора".
Примечание. При использовании платформы SonarQube можно наблюдать только за новыми предупреждениями, не используя механизмы подавления. Для этого необходимо настроить отображение результатов анализа таким образом, чтобы видеть изменения только за последний день.
Режим инкрементального анализа в PVS-Studio позволяет проверять только файлы, изменённые/затронутые с момента последней сборки проекта. Этот режим доступен как при работе с использованием плагина для Visual Studio, так и при проверке проектов с использованием command line модуля PVS-Studio. Использование режима инкрементального анализа позволит получить предупреждения только на модифицированный код, а также сократить время анализа, так как те части проекта, которые не изменялись, проанализированы не будут.
Данный режим актуален, если система непрерывной интеграции настроена на автоматическую инкрементальную сборку после обнаружения изменений в системе контроля версий, то есть, если сборка и анализ проекта на сервере происходит неоднократно в течение дня.
Использование инкрементального анализа в модуле 'PVS-Studio_Cmd.exe' регулируется с помощью флага '‑‑incremental'. Доступны следующие режимы работы:
Для получения подробной информации об инкрементальном анализе в PVS-Studio обратитесь к статье "Режим инкрементального анализа PVS-Studio".
Примечание. При использовании режима инкрементального анализа следует учитывать некоторые особенности. Если PVS-Studio использует препроцессор Visual C++ ('cl.exe'), возможно возникновение ситуации с блокировкой файлов. Это связано с тем, что при попытке записи файла, он может быть заблокирован компилятором 'cl.exe', выполняющим препроцессирование. При использовании препроцессора Clang данная ситуация возникает значительно реже. Это следует учитывать при решении конфигурирования сервера в пользу инкрементального анализа вместо полноценного анализа по ночам.
Если стоит задача анализа CMake проектов, рекомендуется генерировать из них решения для среды Visual Studio и в дальнейшем работать с ними. Это позволит полноценно использовать модуль 'PVS-Studio_Cmd.exe' и все связанные с этим фактом преимущества.
В случае, если проект не использует сборочную систему MSBuild, его не получится проанализировать с использованием command line модуля 'PVS-Studio_Cmd.exe'. Тем не менее, в состав дистрибутива входят утилиты, с помощью которых можно проверить даже проекты со специфичными сборочными системами.
Система мониторинга компиляции (PVS-Studio Compiler Monitoring, CLMonitoring) предназначена для 'бесшовной' интеграции статического анализа PVS-Studio в любую сборочную систему на ОС семейства Windows, использующую для компиляции файлов один из препроцессоров, поддерживаемых command line анализатором 'PVS-Studio.exe'.
Принцип работы сервера мониторинга (CLMonitor.exe) основан на отслеживании запуска процессов, соответствующих целевому компилятору, и сборе информации об окружении этих процессов. Сервер мониторинга отслеживает запуски процессов только для того пользователя, из-под которого он сам запущен.
Поддерживаемые компиляторы:
При интеграции сервера мониторинга в сборочный процесс первоначально необходимо запустить модуль 'CLMonitor.exe' с аргументом 'monitor':
CLMonitor.exe monitor
Это позволит серверу мониторинга запустить самого себя в режиме отслеживания и завершить работу, а сборочной системе - продолжить выполнять оставшиеся задачи. При этом второй (запущенный из первого) процесс CLMonitor будет оставаться запущенным и производить отслеживание сборки.
После завершения сборки необходимо запустить модуль 'CLMonitor.exe' в режиме клиента для генерации препроцессированных файлов и непосредственного запуска статического анализа:
CLMonitor.exe analyze -l "c:\ptest.plog" -u "c:\ptest.suppress" -s
Рассмотрим эту команду более подробно:
Более подробно использование системы мониторинга компиляции описано в статье "Система мониторинга компиляции в PVS-Studio".
Примечание. Системе мониторинга компиляции присущ ряд недостатков, связанный с естественными ограничениями такого подхода, а именно - невозможность гарантировать на 100% перехват всех запусков компилятора при сборке (например, при сильной загрузке системы). Также следует помнить о том, что если параллельно выполняются несколько сборок, система может отследить запуски компиляторов сторонней сборки.
Примечание. Прямая интеграция анализатора возможна только для проверки С/С++ кода.
Прямая интеграция анализатора может быть необходима в случаях отсутствия возможности использования command line модуля 'PVS-Studio_Cmd.exe' (проект собирается не с помощью сборочной системы MSBuild) и системы мониторинга компиляции (см. примечание соответствующего раздела).
В таком случае может потребоваться встроить в сборочный процесс прямой вызов анализатора ('PVS-Studio.exe') и передать ему все необходимые для препроцессирования аргументы. То есть анализатор необходимо вызвать для тех же файлов, для которых вызывается компилятор.
Подробное описание прямой интеграции анализатора в системы автоматизированной сборки приведено в статье "Прямая интеграция анализатора в системы автоматизации сборки (C/C++)".
Результатом работы анализатора является .plog файл, имеющий формат XML. Этот файл не предназначен для ручной обработки, например, для чтения программистом. Тем не менее, в состав дистрибутива входят специальные утилиты, цель которых - сделать работу с .plog файлом максимально удобной.
Отчёт анализатора можно отфильтровать ещё перед запуском анализа с помощью настройки No Noise. При работе на большой кодовой базе, анализатор неизбежно генерирует большое количество предупреждений. При этом, часто нет возможности поправить все предупреждения сразу. Для того, чтобы иметь возможность сконцентрироваться на правке наиболее важных предупреждений, можно сделать анализ менее "шумным" с помощью данной настройки. Она позволяет полностью отключить генерацию предупреждений низкого уровня достоверности (Low Certainty, 3-ий уровень предупреждений). После перезапуска анализа, сообщения этого уровня полностью пропадут из вывода анализатора.
Когда обстоятельства позволят и более существенные предупреждения анализатора будут исправлены, можно выключить режим No Noise - при следующем анализе все пропавшие ранее предупреждения станут вновь доступны.
Для включения этой настройки используйте окно Specific Analyzer Settings.
Для преобразования результатов работы анализатора в один из форматов для непосредственной работы предназначена утилита 'PlogConverter.exe', входящая в состав дистрибутива. 'PlogConverter.exe' позволяет выполнять преобразование .plog файлов в следующие форматы:
Пример запуска утилиты 'PlogConverter.exe' (одной строкой):
PlogConverter.exe test1.plog -o "C:\Results" -r "C:\Test"
-a GA:1 -t Html
Данная команда преобразует файл 'test1.plog' в формат .html с отображением диагностических правил первого уровня категории GA (диагностики общего назначения). Результат будет записан в директорию 'C:\Results'. Оригинальный .plog файл изменён не будет.
Получить подробную справка по всем параметрам утилиты 'PlogConverter' можно с помощью команды:
PlogConverter.exe --help
Примечание. Утилита 'PlogConverter' поставляется вместе с исходными файлами (C#), которые доступны в архиве 'PlogConverter_src.zip'. Для создания собственного формата вывода результатов анализа можно переиспользовать алгоритм разбора структуры .plog файла.
Более подробно утилита 'PlogConverter' описана в статье "Работа с результатами анализа (.plog файл)".
Результаты статического анализа могут быть импортированы платформой непрерывного анализа и измерения качества кода SonarQube. Для импорта результатов анализа предназначен плагин 'sonar-pvs-studio-plugin', входящий в состав дистрибутива. Использование плагина позволяет добавлять сообщения, найденные анализатором PVS-Studio, в базу сообщений сервера SonarQube. Это, в свою очередь, даёт возможность видеть статистику появления / исправления ошибок, осуществлять навигацию по предупреждениям анализатора, просматривать документацию к диагностическим правилам и многое другое.
Все сообщения PVS-Studio, добавленные в SonarQube, имеют тип Bug. В интерфейсе SonarQube сохранено разделение сообщений по группам диагностик аналогично тому, как это сделано в анализаторе.
Более подробная информация об интеграции результатов анализа в SonarQube приведена в статье "Интеграция результатов анализа PVS-Studio в SonarQube".
Эффективным способом оповещения разработчиков о результатах анализа является их рассылка с использованием специализированных утилит, например, SendEmail. При использовании платформы контроля качества кода SonarQube, выполнять рассылку писем можно её средствами.
Ещё одним вариантом оповещения разработчиков является использование утилиты 'BlameNotifier', поставляемой в составе дистрибутива PVS-Studio. Она позволяет формировать вариативные отчёты. Например, разработчики, заложившие код с ошибками в систему контроля версий, получат индивидуальный отчёт. Лидеры команд, руководитель разработки и т.д. могут получить полный лог отчёта, включающий в себя информацию обо всех найденных ошибках и разработчиках, которые эти ошибки допустили.
Следующая команда выведет основную информацию об утилите:
BlameNotifier.exe --help
Более подробно утилита 'BlameNotifier' рассматривается в статье "Работа с результатами анализа (.plog файл)" в разделе "Оповещение разработчиков".
Если у вас остались вопросы, пожалуйста, воспользуйтесь формой обратной связи.
Режим серверного инкрементального анализа из командной строки доступен только для Enterprise лицензии PVS-Studio. Вы можете запросить пробную Enterprise лицензию здесь. Инкрементальный анализ из IDE при работе на машине разработчика доступен для всех типов лицензий PVS-Studio.
Полный анализ можно регулярно запускать отдельно, например, раз в сутки во время ночных сборок. Однако наибольшего эффекта от использования анализатора можно добиться только за счёт более раннего обнаружения и исправления выявленных им дефектов. То есть самым оптимальным вариантом использования статического анализатора является проверка нового кода сразу после его написания. Такой сценарий работы, несомненно, осложняется необходимостью постоянно вручную запускать проверку для всех модифицированных файлов и каждый раз ждать завершения анализа. Это несовместимо с интенсивной разработкой и отладкой нового кода. Да и просто неудобно. Однако PVS-Studio предлагает решение этой проблемы.
Стоит помнить, что после первой полной проверки проекта желательно просмотреть все диагностические сообщения для нужных файлов и исправить найденные в коде ошибки. А остальные сообщения либо разметить как ложные срабатывания, либо отключить отображение неактуальных для проекта кодов сообщений или групп диагностик, либо подавить оставшиеся срабатывания, и, возможно, вернуться к их правке позже. Такой подход позволит иметь список сообщений, не забитый бессмысленными и ненужными предупреждениями.
Включить режим послесборочного инкрементального анализа можно в меню Extensions > PVS-Studio > Analysis after Build (Modified Files Only):
Данный пункт активирован в PVS-Studio по умолчанию.
После активации режима инкрементального анализа PVS-Studio станет автоматически в фоновом режиме производить анализ всех затронутых модификациями файлов сразу после окончания сборки проекта. После окончания инкрементальной сборки, когда запустится анализ, в области уведомлений Windows появится анимированная иконка PVS-Studio:
Контекстное меню области уведомлений позволяет на время приостановить (команда Pause) или отменить (команда Abort) текущую проверку.
Анализатор ориентируется на сборочную систему для определения изменившихся файлов, которые необходимо проверить. Полная пересборка проекта приведёт к анализу всех его файлов - используйте инкрементальную сборку для проверки только изменившихся файлов. В случае, если анализатор во время инкрементального анализа обнаружит ошибки в коде, в названии открытой в Visual Studio вкладки окна PVS-Studio отразится число найденных ошибок, и будет показано Windows уведомление:
Клик по иконке в области уведомлений (либо по самому уведомлению) откроет окно PVS-Studio Output.
При работе из Visual Studio можно задать ограничение на время работы инкрементального анализа или уровень выводимых предупреждений. Данные настройки можно регулировать в меню PVS-Studio > Options > Special Analyzer Settings > IncrementalAnalysisTimeout и PVS-Studio > Options > Specific Analyzer Settings > IncrementalResultsDisplayDepth.
Режим инкрементального анализа для решений Visual Studio доступен также в модуле командной строки (PVS-Studio_Cmd.exe). Этот режим позволяет ускорить статический анализ на сервере непрерывной интеграции. В нём реализованы подходы, аналогичные подходам MSBuild для инкрементальной сборки.
Настроить инкрементальный анализ на сервере можно с помощью нескольких команд:
PVS-Studio_Cmd.exe ... --incremental Scan ...
MSBuild.exe ... -t:Build ...
PVS-Studio_Cmd.exe ... --incremental Analyze ...
Все режимы работы инкрементального анализа выглядят так:
В случае необходимости инкрементального анализа при использовании системы мониторинга компиляции, достаточно "отслеживать" инкрементальную сборку, т.е. компиляцию тех файлов, которые были изменены с момента последней сборки. Этот сценарий использования позволит анализировать только изменённый/новый код.
Такой сценарий использования естественен для системы мониторинга компиляции, т.к. она основана на "отслеживании" запусков компилятора во время сборки проекта, что позволяет получить всю необходимую информацию для запуска анализа на исходных файлах, компиляция которых была отслежена. Следовательно, режимы анализа зависят от того, какая сборка отслеживается: полная или инкрементальная.
Более подробно система мониторинга компиляции описана в статье "Система мониторинга компиляции в PVS-Studio".
Для проверки CMake-проекта можно использовать файл JSON Compilation Database. Для получения необходимого анализатору файла compile_commands.json необходимо добавить один флаг к вызову CMake:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=On <src-tree-root>
Для инкрементального анализа такого проекта необходимо добавить флаг ‑‑incremental к команде анализа:
pvs-studio-analyzer analyze ... --incremental ...
В каталоге .PVS-Studio будут сохраняться зависимости между файлами и время модификаций, т.к. в этом режиме анализатор работает независимо от сборочной системы. Этот каталог необходимо сохранять для работы этого режима анализа.
Если используемый CMake генератор не поддерживает создание файла compile_commands.json, или генерация такого файла неудобна, можно воспользоваться прямой интеграцией PVS-Studio с CMake - использование модуля прямой интеграции позволит выполнять инкрементальный анализ одновременно с инкрементальной сборкой.
В скрипты сборочной системы Make и подобных можно прописать вызов анализатора рядом с компилятором:
$(CXX) $(CFLAGS) $< ...
pvs-studio --source-file $< ...
В таком случае инкрементальный анализ и инкрементальная сборка будут работать совместно, получая информацию об изменённых файлах от сборочной системы.
Примеры интеграции PVS-Studio в Makefile собраны в репозитории на GitHub: pvs-studio-makefile-examples.
Проверить любой проект без интеграции анализатора в сборочную систему можно с помощью следующих команд:
pvs-studio-analyzer trace – make
pvs-studio-analyzer analyze ...
Вместо команды make может быть любая команда запуска сборки проекта со всеми необходимыми параметрами.
В этом режиме анализатор отслеживает и производит логирование дочерних процессов сборочной системы и ищет среди них процессы компиляции. Если собирать проект в режиме инкрементальной сборки, то и проверяться будут только изменённые файлы.
Режим инкрементального анализа C# проектов под Linux и macOS аналогичен описанному выше в разделе "Command line analyzer for MSBuild projects (PVS-Studio_Cmd.exe)" с поправками на то, что:
Включить режим послесборочного инкрементального анализа можно в плагине для IntelliJ IDEA в меню Analyze > PVS-Studio > Settings > PVS-Studio > Misc > Run incremental analysis on every build:
После активации режима инкрементального анализа PVS-Studio станет автоматически в фоновом режиме производить анализ изменённых файлов сразу после окончания сборки проекта. Если PVS-Studio выдаст предупреждения, то они будут выведены в окне PVS-Studio:
В maven плагине инкрементальный режим включается флажком incremental:
<plugin>
<groupId>com.pvsstudio</groupId>
<artifactId>pvsstudio-maven-plugin</artifactId>
....
<configuration>
<analyzer>
....
<incremental>true</incremental>
....
</analyzer>
</configuration>
</plugin>
После активации режима инкрементального анализа команда pvsstudio:pvsAnalyze будет запускать анализ только на измененных файлах относительно последнего запуска.
В gradle плагине инкрементальный режим включается аналогичным флажком incremental:
apply plugin: com.pvsstudio.PvsStudioGradlePlugin
pvsstudio {
....
incremental = true
....
}
После активации режима инкрементального анализа команда pvsAnalyze будет запускать анализ только на измененных файлах относительно последнего запуска.
Проверка запросов на слияние и модифицированных файлов доступна только при наличии Enterprise лицензии. Вы можете запросить пробную Enterprise лицензию здесь.
С помощью режима анализа коммитов и слияния веток можно проанализировать только те файлы, которые были изменены относительно текущего состояния ветки (в которую производится коммит или запрос на слияние). Это сократит время анализа и упростит просмотр его результатов: отчёт анализатора будет содержать предупреждения, выданные из-за ошибок в изменённых файлах.
Этот документ описывает общие принципы анализа запросов на слияние. Примеры для конкретных CI можно найти в следующих разделах документации:
Для анализа файлов, изменённых при слиянии веток, эти файлы необходимо выделить из всех остальных файлов проекта. Для этого после прохождения слияния нужно получить разницу между HEAD веткой, из которой производится запрос на слияние, и той, куда будут вноситься изменения.
Рассмотрим дерево коммитов:
В данном случае была создана ветка 'quickFix'. После работы в ней открывается запрос на слияние. Для получения разницы между текущим состоянием ветки 'master' и последним коммитом в ветке 'quickFix' можно использовать следующую команду (на момент её выполнения нужно находиться в ветке 'quickFix'):
git diff --name-only HEAD master > .pvs-pr.list
Так мы получим список изменённых файлов относительно последних коммитов в ветках 'master' и 'quickFix'. Названия изменённых файлов будут сохранены в '.pvs-pr.list'.
Примечание. В примере получения файлов для анализа используется система контроля версий 'git'. Однако подойдёт любая система контроля версий, позволяющая получить список изменённых файлов.
Для проверки отправленного запроса на слияние нужно проанализировать полученный список файлов.
В данном разделе приведены примеры команд для получения списка модифицированных файлов.
Для Git:
Для получения списка изменённых файлов перед коммитом, выполните команду:
git diff --cached --name-only > .pvs-pr.list
Для получения списка изменённых файлов между двумя коммитами, выполните команду:
git diff --name-only CommitA CommitB > .pvs-pr.list
Для SVN:
На Windows можно выполнить следующую команду в PowerShell:
Write-Host (svn status | Select-String -Pattern '^[AM]\W*(.*)'
| %{$_.Matches.Groups[1].value}) -Separator "`b`n" > .pvs-pr.lits
На Linux:
svn status -q | grep -oP "^[M|A]\W*\K(.*)" > .pvs-pr.list
Для Mercurial:
hg log --template "{files % '{file}\n'}\n" > .pvs-pr.list
Для Perforce:
p4 diff -f -sa > .pvs-pr.list
Большинство систем контроля версий поддерживают возможность отслеживания событий в репозитории с помощью специальных хуков. Как правило, хуки представляют из себя обычные файлы скриптов, которые запускает VCS. Они могут быть использованы как на стороне клиента (локальная машина разработчика), так и на стороне сервера системы контроля версии (если у вас развёрнут собственный сервер VCS (Version Control System)). Использование хуков на сервере VCS позволяет настроить глобальную политику для всех разработчиков компании.
Сценарии использования хуков индивидуальны для каждой системы контроля версий. Подробнее об этом лучше узнать в документации по вашей системе контроля версий.
Вы можете попробовать интегрировать PVS-Studio непосредственно на сервер VCS, следуя этому плану:
Стоит учесть, что, как правило, операция выполнения хуков является блокирующей. Это означает, что пока хук не пройдет, операция коммита или отправки изменений не завершится. Следовательно, использование хуков может замедлить отправку изменений в репозиторий.
Обратите внимание, что такой сценарий довольно труден в реализации, и мы настоятельно рекомендуем использовать хуки VCS только для создания триггеров CI систем.
?modified_files Режим проверки модифицированных файлов
В качестве альтернативы режиму проверки списка файлов существует режим проверки модифицированных файлов. С помощью режима анализа модифицированных файлов можно проанализировать только те файлы, которые были изменены относительно сохраненного состояния директории проекта. Информацию о модификации файлов анализатор получает и сохраняет в кэш зависимостей компиляции. Кэш зависимостей представляет собой список файлов исходного кода проекта и их зависимостей. В режиме модифицированных файлов для каждого исходного файла, представленного в кэше, вычисляется хэш этого файла. При последующем запуске хэш в кэше зависимостей сравнивается с фактическим значением хэша файла. Если хэши различаются, файл будет проанализирован.
В текущий момент анализ модифицированных файлов доступен на Windows для MSBuild С, С++ и С# проектов с помощь утилиты PVS-Studio_Cmd и на Linux/macOS для С# проектов с помощью утилиты pvs-studio-dotnet.
Анализ модифицированных файлов включается с помощью флага ‑‑analyzeModifiedFiles (-F).
Пример команды запуска анализа модифицированных файлов:
PVS-Studio_Cmd.exe -t MyProject.sln -F ^
-D path\to\depCacheDir ^
-R depCacheRoot ^
-o analysis_report.json
В результате работы этой команды проанализируются изменённые и добавленные в проекты файлы, которые содержатся в решении MyProject.sln. Для получения информации об изменениях будут использованы файлы кэшей зависимостей, расположенные по пути, указанном во флаге -D. Пути в файлах кэшей будут записаны относительно пути, указанного в флаге -R. Результаты анализа будут сохранены в файл analysis_report.json.
Вы можете использовать флаг //V_SOLUTION_DIR_AS_DEPENDENCY_CACHE_SOURCE_TREE_ROOT в файле *.pvsconfig для использования пути каталога решения (папка, где расположен файл *.sln), в качестве корневой части пути, которая будет использована для построения относительных путей файлов в кэше зависимостей компиляции.
Если файл кэша зависимостей компиляции отсутствует, то он будет сгенерирован, и при этом запустится анализ всех исходных файлов из проекта. Для того, чтобы не запускался анализ всех файлов из проекта при первом запуске анализа, можно запустить анализатор в режиме генерации файлов кэшей зависимостей без непосредственного запуска анализа (флаг -W) с вычислением хэшей для файлов исходного кода (флаг -H). Для этого воспользуйтесь следующей командой:
PVS-Studio_Cmd.exe -t MyProject.sln -W -H ^
-D path\to\depCacheDir ^
-R depCacheRoot
В этом случае будут сгенерированы файлы кэшей зависимостей, но не будет запущен анализ. Следующий запуск анализа модифицированных файлов (флаг -F) будет анализировать уже только добавленные или изменившиеся файлы исходного кода из проектов.
Для проверки коммита или запроса на слияние воспользуйтесь следующим алгоритмом:
Для проверки списка файлов анализатору необходимо передать текстовый файл, который содержит абсолютные или относительные пути к файлам для анализа (относительные пути будут раскрыты относительно рабочей директории). Каждый из путей должен быть записан на новой строке. Текст, который не является путём к файлу с исходным кодом, игнорируется (может быть полезно для комментирования).
Пример содержимого файла с путями:
D:\MyProj\Tests.cs
D:\MyProj\Common.cpp
D:\MyProj\Form.h
Далее будут рассмотрены варианты запуска анализа для разных языков и ОС.
Для проверки C# файлов используется утилита 'PVS-Studio_Cmd' для Windows и 'pvs-studio-dotnet' для Linux и macOS.
Путь к файлу, который содержит список файлов для анализа, передаётся с помощью аргумента '-f' (подробную информацию об аргументах можно найти в документации). О формате этого файла написано выше в разделе "Режим проверки списка файлов".
Для получения информации о наличии предупреждений анализатора можно проверить код возврата. Коды возврата описаны в документации.
Пример команды запуска анализа:
PVS-Studio_Cmd.exe -t MyProject.sln ^
-f .pvs-pr.list ^
-o Analysis_Report.json
Будут проанализированы файлы из '.pvs-pr.list', которые содержатся в решении 'MyProject.sln'. Результаты анализа будут сохранены в файл 'Analysis_Report.json'.
Пример команды запуска анализа:
pvs-studio-dotnet -t MyProject.sln \
-f .pvs-pr.list \
-o Analysis_Report.json
Будут проанализированы файлы из '.pvs-pr.list', которые содержатся в решении 'MyProject.sln'. Результаты анализа будут сохранены в файл 'Analysis_Report.json'.
Для проверки C и C++ файлов можно использовать утилиты:
Способ проверки для каждой утилиты описан далее.
При первом запуске анализа генерируется файл зависимостей всех исходных файлов проекта от заголовочных. При последующих запусках он будет обновляться анализатором автоматически. Существует возможность создания/обновления этого файла без запуска анализа. Данный процесс будет описан для каждой из утилит в соответствующем разделе.
В данном разделе более подробно расписывается механизм работы файлов кэшей зависимостей компиляции в консольных утилитах PVS-Studio_Cmd.exe и pvs-studio-analyzer/CompilerCommandsAnalyzer.exe.
В этих утилитах имеются специальные флаги:
При первом запуске анализа с использованием флага анализа списка файлов анализатор препроцессирует все C и C++ файлы из проектов, переданных на анализ. Эта информация о зависимостях сохраняется в отдельный файл кэша зависимостей для каждого проекта.
При последующих запусках анализатора с флагом анализа списка файлов информация в кэше зависимости добавляется/обновляется для:
После этапа обновления информации о зависимостях производится анализ всех файлов, указанных в списке файлов для анализа, а также файлов, зависящих от них (используется информация о зависимостях, полученная на предыдущем этапе).
Перед каждым запуском анализа кэш зависимостей компиляции будет обновлён для всех файлов, переданных на анализ, и файлов, в зависимостях которых есть файлы, переданные в списке файлов для анализа. При наличии изменений в зависимостях файлов по отношению к закэшированным зависимостям эти изменения будут учтены уже в текущем запуске анализа, а кэш обновлён для последующих запусков.
Для поддержания кэша зависимостей в актуальном состоянии требуется запускать анализ с флагом для анализа списка файлов для каждого изменения исходных файлов, либо одновременно для всех изменившихся файлов из нескольких коммитов.
Пропуск анализа изменений исходных файлов, либо отдельные запуски анализа изменённых файлов в порядке, отличающемся от порядка их модификации, может привести к последующему пропуску анализа из-за изменений в структуре зависимостей проекта. Так, например, если в .cpp файл была добавлена зависимость от .h файла, то этот .cpp файл должен попасть в список файлов на проверку для обновления его кэша зависимостей. Иначе при попадании вновь добавленного .h файла в список файлов на анализ анализатор не сможет найти единицу трансляции, для которой необходимо выполнить препроцессирование.
Если вы не можете обеспечить гарантированную передачу всех изменяющихся файлов проекта в режиме анализа списка файлов, мы рекомендуем использовать режим анализа списка файлов совместно с режимом перегенерации кэшей зависимостей с запуском анализа. В таком случае время работы анализатора несколько возрастёт (для перегенерации кэша необходимо произвести препроцессирование всех единиц трансляции проектов), однако оно всё равно будет значительно меньше, чем время полного анализа (т.к. будет производиться только препроцессирование всех исходных файлов проектов, без анализа). При этом актуальность кэша будет гарантирована на каждом запуске анализа независимо от возможного пропуска изменившихся файлов или от порядка, в котором файлы передавались на анализ.
Примечание. Сценарий анализа файлов проекта, который использует сборочную систему MSBuild, описан в разделе "PVS-Studio_Cmd (Windows, Visual Studio\MSBuild)".
В зависимости от ОС, на которой выполняется анализ, утилита будет иметь разные названия:
В примерах данной документации используется название 'pvs-studio-analyzer'. Способ анализа файлов для 'CompilerCommandsAnalyzer.exe' аналогичен описанным здесь.
Для использования 'pvs-studio-analyzer' нужно сгенерировать либо файл 'compile_commands.json', либо файл с результатами трассировки компиляции (актуально только для Linux). Они необходимы для того, чтобы анализатор имел информацию о компиляции конкретных файлов.
Получение 'compile_commands.json'
Со способами получения файла 'compile_commands.json' можно ознакомится в документации.
Получение файла трассировки (только для Linux)
Со способами получения файла трассировки можно ознакомиться в документации. По умолчанию результат трассировки записывается в файл 'strace_out'.
Существует два варианта анализа с использованием файла трассировки. Можно либо производить полную трассировку сборки всего проекта при каждом запуске, либо кэшировать результат трассировки и использовать его.
Минус первого способа в том, что полная трассировка противоречит идее быстрой проверки коммитов или запросов на слияние.
Второй способ плох тем, что результат анализа может оказаться неполным, если после трассировки поменяется структура зависимостей исходных файлов (например, в один из исходных файлов будет добавлен новый #include).
Из-за этого мы не рекомендуем использовать режим проверки списка файлов с логом трассировки для проверки коммитов или запросов на слияние. В случае, если вы можете делать инкрементальную сборку при проверке коммита, рассмотрите возможность использовать режим инкрементального анализа.
Пример команд для анализа файлов и обновления зависимостей
Рассмотрим пример использования 'pvs-studio-analyzer'. Путь к файлу, который содержит список файлов для анализа, передаётся с помощью аргумента '-S' (подробную информацию об аргументах утилиты можно найти в документации). О формате этого файла написано выше в разделе "Режим проверки списка файлов".
Примечание. Если информация о компиляции была получена с использованием режима трассировки компиляции, с помощью флага '-f' передаётся файл трассировки (по умолчанию его название — 'strace_out').
Пример команды для анализа файлов:
pvs-studio-analyzer analyze -S .pvs-pr.list \
-f compile_commands.json \
-o Analysis_Report.json
При выполнении данной команды сгенерируется отчёт с результатом проверки файлов, содержащихся в '.pvs-pr.list'. Результаты анализа будут сохранены в файл 'Analysis_Report.json'.
Для генерации или обновления файла зависимостей без запуска анализа используется флаг ‑‑regenerate-depend-info-without-analysis. Вместе с этим флагом нельзя использовать флаг -S. Команда для обновления будет выглядеть следующим образом:
pvs-studio-analyzer analyze -f compile_commands.json \
–-regenerate-depend-info-without-analysis
Для принудительного обновления кэша зависимостей с последующим запуском анализа используется флаг ‑‑regenerate-depend-info. Его также можно использовать вместе с флагом -S. В этом случае для всех файлов из проекта обновится кэш зависимостей. Однако анализироваться при этом будут:
Команда для обновления кэша зависимостей для всего проекта и анализа переданных файлов будет выглядеть следующим образом:
pvs-studio-analyzer analyze -S .pvs-pr.list \
-f compile_commands.json \
-o Analysis_Report.json \
–-regenerate-depend-info
По умолчанию файл с кэшем зависимостей генерируется в папку '.PVS-Studio', которая создаётся в рабочей директории. Сам кэш содержится в файле 'depend_info.json'.
Получение информации о наличии/отсутствии предупреждений в отчёте анализатора
Есть срабатывания в отчёте анализатора или нет, можно понять по коду возврата консольных утилит:
С документацией по данным утилитам можно ознакомиться здесь.
Пример использования 'PlogConverter.exe':
PlogConverter.exe Analysis_Report.json ^
-t html ^
-n PVS-Studio ^
--indicateWarnings
Пример использования 'plog-converter':
plog-converter Analysis_Report.json \
-t html \
-n PVS-Studio \
--indicate-warnings
В качестве первого аргумента командной строки передается путь до файла с результатами анализа. С помощью аргумента '-t' указывается формат, в котором необходимо сохранить отчёт. При помощи аргумента '-n' задаётся имя файла с преобразованным отчётом. Флаги '‑‑indicateWarnings' для 'PlogConverter.exe' и '‑‑indicate-warnings' для 'plog-converter' позволяют установить код возврата 2, если в отчёте есть предупреждения анализатора.
Путь к файлу, который содержит список файлов для анализа, передаётся с помощью аргумента '-f' (подробную информацию об аргументах можно найти в документации). О формате этого файла написано в разделе "Режим проверки списка файлов".
Пример команды запуска анализа:
CLMonitor.exe analyze -l "Analysis_Report.json" ^
-f ".pvs-pr.list"
При выполнении данной команды будет сгенерирован отчёт с результатом проверки файлов, содержащихся в '.pvs-pr.list'. Результаты анализа будут сохранены в файл 'Analysis_Report.json'.
По коду возврата консольной утилиты 'PlogConverter.exe' можно понять, есть срабатывания в отчёте анализатора или нет. Если предупреждения анализатора отсутствуют, код возврата — 0. При наличии предупреждений код возврата — 2. С документацией по данным утилитам можно ознакомиться здесь.
Пример использования 'PlogConverter.exe':
PlogConverter.exe Analysis_Report.json \
-t html \
-n PVS-Studio \
--indicate-warnings
В качестве первого аргумента командной строки передается путь до файла с результатами анализа. С помощью аргумента '-t' указывается формат, в котором необходимо сохранить отчёт. При помощи аргумента '-n' задаётся имя файла с преобразованным отчётом. Флаг '‑‑indicateWarnings' для 'PlogConverter.exe' позволяет задать код возврата 2, если в отчёте есть предупреждения анализатора.
Если нужные файлы с кодом включены в проект Visual Studio, который использует сборочную систему MSBuild, то анализ производится с помощью утилиты PVS-Studio_Cmd.
Путь к файлу, который содержит список файлов для анализа, передаётся с помощью аргумента '-f' (подробную информацию об аргументах утилит можно найти в документации). О формате этого файла написано выше в разделе "Режим проверки списка файлов".
Для получения информации о наличии предупреждений анализатора можно проверить код возврата. Коды возврата описаны в документации.
Пример команды запуска анализа:
PVS-Studio_Cmd.exe -t MyProject.sln ^
-f .pvs-pr.list ^
-o Analysis_Report.json
Будут проанализированы файлы из '.pvs-pr.list', которые содержатся в решении 'MyProject.sln'. Результаты анализа будут сохранены в файл 'Analysis_Report.json'.
Для обновления зависимостей без запуска анализа используется флаг -W. Вместе с ним нельзя использовать флаг -f:
PVS-Studio_Cmd.exe -t MyProject.sln ^
-W
Для принудительного обновления кэша зависимостей с последующим запуском анализа используется флаг -G. Его также можно использовать вместе с флагом -f. В этом случае для всех файлов исходного кода из проектов, переданных для анализа, будут обновлены кэши зависимостей. Однако, анализироваться при этом будут:
Команда для обновления кэшей зависимостей для всех проектов и анализа переданных файлов будет выглядеть следующим образом:
PVS-Studio_Cmd.exe -t MyProject.sln ^
-f .pvs-pr.list ^
-G
По умолчанию файл с кэшем зависимостей генерируется на уровне проекта и сохраняется в папку '.pvs-studio'. Файл, содержащий кэш, имеет название вида 'projectName.vcxproj.deps.json' (часть имени файла, в данном случае 'projectName.vcxproj', соответствует имени проекта). Соответственно, если проанализировать файлы, относящиеся к одному решению, но к разным проектам, то в директории каждого из проектов будет создана папка '.pvs-studio' с файлом зависимостей.
Существует возможность изменить директорию сохранения кэша. Для этого используется параметр '-D'. В качестве его значения передаётся путь до директории, в которую необходимо сохранить кэш.
Для задания относительных путей в кэшах зависимостей используется флаг '-R'. В качестве аргумента ему необходимо передать путь, относительно которого будут раскрыты пути в файлах кэшей зависимостей.
Получение информации о наличии/отсутствии предупреждений в отчёте анализатора
По коду возврата консольной утилиты PVS-Studio_Cmd.exe можно понять, есть срабатывания в отчёте анализатора или нет. Код возврата 256 означает, что в отчёте есть предупреждения анализатора.
Также возможно использование консольных утилит PlogConverter.exe (Windows) или plog-converter (Linux/macOS) с флагом ‑‑indicateWarnings. При использовании этих флагов, если в отчёте анализатора имелись предупреждения, код возврата будет равен 2.
Пример использования PlogConverter.exe:
PlogConverter.exe Analysis_Report.json ^
-t html ^
-n PVS-Studio ^
--indicateWarnings
Пример использования plog-converter:
plog-converter Analysis_Report.json \
-t html \
-n PVS-Studio \
--indicate-warnings
Для проверки Java файлов используется утилита 'pvs-studio.jar'. Подробную информацию о самой утилите и её аргументах можно найти в документации.
Путь к файлу, который содержит список файлов для анализа, передаётся с помощью флага '‑‑analyze-only-list'. О формате этого файла написано выше в разделе "Режим проверки списка файлов".
Для анализа списка файлов также необходимо передать путь до проекта, содержащего их. Делается это при помощи аргумента '-s'. С помощью аргумента '-e' определяется classpath. Если требуется использовать несколько сущностей classpath, то они разделяются пробелом.
Пример команды запуска анализа:
java -jar pvs-studio.jar -s projectDir ^
--analyze-only-list .pvs-pr.list ^
-e Lib1.jar Lib2.jar ^
-j4 ^
-o report.json ^
-O json ^
--user-name userName ^
--license-key key
В результате будут проанализированы файлы, записанные в '.pvs-pr.list'. Результаты анализа сохранятся в файл 'report.json'.
Для получения информации о наличии срабатываний можно воспользоваться флагом '‑‑fail-on-warnings'. При его использовании анализатор вернёт код 53, если в результате анализа будут выданы предупреждения.
DefectDojo – это DevSecOps платформа, инструмент для отслеживания ошибок и уязвимостей. Он предоставляет функции для работы с отчётами, включая возможность объединения результатов, запоминания ложных срабатываний и удаления дубликатов срабатываний, умеет сохранять метрики и строить графики их изменения. Используя его, можно удобно работать с результатами анализа PVS-Studio и управлять процессом устранения ошибок.
DefectDojo имеет несколько вариантов разворачивания. Здесь описано, как его можно установить и настроить. На этой странице находится общая документация по эксплуатации.
Для работы с отчётом PVS-Studio в DefectDojo нужно преобразовать его в специальный формат.
Для преобразования можно воспользоваться утилитами командной строки PlogConverter.exe для Windows и plog-converter для Linux/macOS. Эти утилиты позволяют не только конвертировать отчёт PVS-Studio в разные форматы, но и дополнительно обрабатывать его. Например, проводить фильтрацию сообщений.
Пример команды конвертации отчёта PVS-Studio для DefectDojo при помощи PlogConverter.exe (Windows):
PlogConverter.exe path\to\report.plog ^
-t DefectDojo ^
-o output\dir ^
-n converted_report_name
Пример команды конвертации отчёта PVS-Studio для DefectDojo при помощи plog-converter (Linux/macOS):
plog-converter path/to/report.json \
-t defectdojo \
-o path/to/report.defectdojo.json \
-n converted_report_name
После того как вы преобразовали отчёт, его необходимо загрузить в DefectDojo. Для этого нужно добавить в DefectDojo новый engagement, в котором будут находиться результаты анализа. Для добавления engagement нужно выбрать пункт 'Add New Interactive Engagement'.
Далее нужно загрузить получившийся отчёт в созданный engagement. Для этого можно использовать API DefectDojo или вручную загрузить отчёт.
Для ручной загрузки отчёта откройте engagement, нажмите на кнопку меню в таблице 'Tests' и выберите пункт 'Import Scan Result'.
В открывшемся окне в поле 'Scan type' укажите 'Generic Findings Imports', в поле 'Choose report file' укажите отчёт, который нужно загрузить.
Для автоматизации загрузки отчёта используйте API DefectDojo. Пример команды загрузки отчёта:
curl -X POST
-H Authorization:"Token 44ac826dc4f3b6add1161dab11b49402618efaba"
-F scan_type="Generic Findings Import"
-F file=@"path/to/report.json"
-F engagement=1
-H Content-Type:multipart/form-data
-H accept:application/json defctdojohost/api/v2/import-scan/
Токен для авторизации можно найти в меню пользователя DefectDojo, выбрав пункт 'API v2 Key'.
В параметре 'engagement' указывается идентификатор того engagement, в который нужно загрузить отчёт. Идентификатор можно узнать из URL выбранного engagement.
В параметре 'file' нужно указать путь до отчёта.
После загрузки отчёта, его можно посмотреть в DefectDojo. Выберите engagement, в который загрузили отчёт, и откройте нужный результат анализа. Последний загруженный отчёт будет первым в списке.
В результате откроется страница, которая будет содержать список предупреждений анализатора.
DefectDojo поддерживает фильтрацию и сортировку предупреждений по разным параметрам. Например, можно оставить только некоторые диагностики или предупреждения с определённым идентификатором CWE.
При нажатии кнопки 'Column visibility' открывается меню со списком для скрытия/отображения столбцов.
Поле 'Name' содержит номер диагностического правила. Нажмите на это поле для просмотра более подробной информации о предупреждении.
На открывшейся странице содержится следующая информация:
DefectDojo позволяет отмечать предупреждения как False Positive.
Для того чтобы разметка ложных сообщений сохранялась при загрузке нового отчёта, требуется дополнительная настройка DefectDojo.
Откройте настройки DefectDojo ('Configuration -> System Settings' или 'http://defectdojohost/system_settings') и включите опцию 'False positive history'.
Затем добавьте следующую запись в файл 'local_settings.py':
HASHCODE_FIELDS_PER_SCANNER=
{"PVS-Studio Scan (Generic Findings Import)":["unique_id_from_tool"]}
Файл должен быть расположен в директории 'dojo/settings/'. Если вы запускаете DefectDojo через 'docker-compose.yml', то файл ('local_settings.py') нужно расположить в папке 'docker/extra_settings/'. В этом случае 'local_settings.py' копируется в папку 'dojo/settings/' при старте docker контейнера.
Эти настройки обеспечивают то, что предупреждения, отмеченные как False Positive, при загрузке нового отчёта не потеряют этот статус. Статус не изменится, даже если строка кода, на которую было выдано срабатывание, сдвинулась.
Дополнительно можно включить настройку 'Deduplicate findings'. В этом случае, когда в отчёте встретится уже загруженное предупреждение, оно будет иметь дополнительный статус 'Duplicate'.
Для того чтобы отметить предупреждение как ложное, нужно выделить его, нажать на кнопку 'Bulk Edit', выделить пункты 'Status' и 'False Positive'.
Quality Gate — это индикатор соответствия кода проекта заданным пороговым значениям метрик. В DefectDojo нет функционала для настройки Quality Gate через Веб-интерфейс. Однако с помощью API можно получить необходимую информацию для реализации механизма Quality Gate.
Пример скрипта для использования Quality Gate в DefectDojo можно найти здесь.
Для работы скрипта нужно выставить две переменные окружения:
Пример команды запуска скрипта:
python ./qualitygate.py --engagement 6 --critical 0 --high 10 ^
--medium 50 --low 250
'engagement' – номер engagement, в который загружается отчёт.
'critical', 'high', 'medium', 'low' – пороговые значения количества предупреждений PVS-Studio различных уровней достоверности.
Скрипт получает последний отчёт из предоставленного engagement и определяет количество сообщений по уровням достоверности. После этого происходит проверка, что количество предупреждений меньше переданных.
Например, из приведённого выше примера следует, что скрипт вернёт код 1, если в отчёте будет хотя бы одно Critical предупреждение или больше 10 High, 50 Medium, 250 Low предупреждений.
Вы можете указать свою логику Quality Gate, изменив функцию quality_gate().
Есть несколько вариантов отправки оповещений в DefectDojo. Рассмотрим только отправку оповещений на примере почты.
Настройка рассылки результатов анализа происходит в два этапа: настройки почты отправителя и настройки оповещений в интерфейсе DefectDojo.
Для настройки отправителя в файл docker-compose.yml нужно добавить строки, в которых будут указаны данные для подключения к почте, с которой будут отправляться сообщения.
uwsgi:
....
DD_EMAIL_URL: "smtp+tls://email%40domain.com:YourPassword@YourSMTPServer:port"
celeryworker:
....
DD_EMAIL_URL: "smtp+tls://email%40domain.com:YourPassword@YourSMTPServer:port"
Теперь перейдем к настройке оповещений в интерфейсе DefectDojo. Сначала нужно выбрать почту как способ отправки отчетов. Для этого у пользователя с правами SuperUser нужно перейти в Configuration > System Settings. В поле Email from указать почту, с которой будут отправляться сообщения. А также выбрать пункт Enable mail notifications, чтобы можно было выбрать оповещения для отправки на почту.
Далее нужно настроить пользователя. В поле Email address нужно указать почту, на которую будут приходить оповещения.
Затем нужно перейти в Configuration > Notifications. Здесь можно выбрать необходимые оповещения. Чтобы они приходили на почту, нужно выбрать соответствующую колонку.
Также оповещения можно настроить для конкретного проекта. Для этого нужно перейти в Products, выбрать проект и в разделе Notifications выбрать все нужные оповещения.
После этого оповещения будут приходить на почту с адреса, указанного в настройках в файле docker-compose.yml.
Интеграция PVS-Studio с платформой SonarQube доступна только при наличии Enterprise лицензии. Вы можете запросить пробную Enterprise лицензию здесь.
SonarQube - это открытая платформа для обеспечения непрерывного контроля качества исходного кода, поддерживающая большое количество языков программирования и позволяющая получать отчеты по таким метрикам, как дублирование кода, соответствие стандартам кодирования, покрытие тестами, сложность кода, потенциальные ошибки и т.д. SonarQube удобно визуализирует результаты анализа и позволяет отслеживать динамику развития проекта во времени.
Демонстрация возможностей платформы SonarQube доступна здесь.
Для импорта результатов анализа в SonarQube PVS-Studio предоставляет плагин. Использование плагина позволяет добавлять сообщения, найденные анализатором PVS-Studio, в базу сообщений сервера SonarQube. С помощью Web интерфейса SonarQube можно фильтровать сообщения, осуществлять навигацию по коду для анализа ошибок, назначать задачи на исполнителей и контролировать их выполнение, анализировать динамику количества ошибок и оценивать уровень качества кода проекта.
Пользователям PVS-Studio доступны следующие плагины для SonarQube:
После установки сервера SonarQube скопируйте плагин (sonar-pvs-studio-plugin.jar) в следующую директорию:
SONARQUBE_HOME/extensions/plugins
В зависимости от языка, для которого вы загружаете результаты анализа, установите также соответствующие плагины из списка (часть из них может быть установлена по умолчанию, в зависимости от используемой вами редакции SonarQube):
После этого перезапустите сервер SonarQube.
Примечание. Если вы сразу начали использовать обновленную версию плагина SonarCXX (2.0.+) для своего проекта, использующего языки C/C++, то для вас нижеизложенная информация неактуальна.
В SonarCXX 2.0.+ разработчиками были внесены многочисленные изменения с учетом опыта предыдущих версий. Одно из кардинальных изменений – переименование языкового ключа с 'C++' на 'CXX'. Это изменение привело к тому, что PVS-Studio плагин теперь по умолчанию формирует разные несвязанные между собой репозитории для этих языков. Поэтому, если вы загрузите отчет PVS-Studio после обновления SonarCXX плагина без предварительной настройки, то все существующие ранее Issues от PVS-Studio, будут считаться удаленными, а все Issues из отчета будут считаться новыми.
Если вам необходимо обновить SonarCXX с сохранением истории Issues от PVS-Studio (например, сохранить статус Won't Fix или False Positive со всеми комментариями), то вам необходимо следовать данной инструкции:
После этого вы сможете загружать отчеты PVS-Studio для проектов с использованием Quality Profile для языка CXX.
ВАЖНО: Если вы ранее загружали отчеты PVS-Studio с использованием плагина SonarCXX версии 2.0.+ без добавления строки из инструкции в файл "SonarQubeFolder/conf/sonar.properties", то все Issues, которые были ранее загружены, изменят свой статус на Removed и будут удалены со временем (по умолчанию 30 дней).
Самый простой и быстрый способ развёртывания сервера SonarQube – это использование образа SonarQube из Dockerhub.
Для загрузки отчётов анализатора PVS-Studio в SonarQube необходимо установить последнюю версию плагина PVS-Studio для SonarQube (которую можно скачать на этой странице), а также один из плагинов для C/C++, если в анализируемых вами проектах используются эти языки.
Ниже приведён пример Docker-файла, который позволит создать образ с SonarQube Community Edition и всеми необходимыми плагинами:
# Для использования SonarQube Enterprise Edition замените "lts-community"
# на "lts" в теге базового docker образа.
FROM sonarqube:lts-community
USER root
# Установка утилиты curl
RUN apk add curl || (apt-get update && apt-get install -y curl)
# Используйте последнюю версию плагина.
# Найти её можно по на странице:
# https://pvs-studio.com/en/pvs-studio/download-all/
ARG PVS_VER=7.27.75620.2023
# Установка плагина PVS-Studio для SonarQube.
RUN curl -Lo \
/opt/sonarqube/extensions/plugins/sonar-pvs-studio-plugin-$PVS_VER.jar \
https://cdn.pvs-studio.com/sonar-pvs-studio-plugin-$PVS_VER.jar
# Если вам необходима другая версия плагина CXX, измените её здесь.
ARG C_VER=cxx-2.1.1
ARG C_JAR=sonar-cxx-plugin-2.1.1.488.jar
# Установка плагина SonarCXX (нужен для работы с C/C++ проектами).
RUN curl -Lo \
/opt/sonarqube/extensions/plugins/$C_JAR \
https://github.com/SonarOpenCommunity/sonar-cxx/releases/download/$C_VER/$C_JAR
# Смена пользователя, который будет использоваться в контейнере
USER sonarqube
После создания файла необходимо собрать Docker-образ при помощи команды:
docker build -f dockerfile -t sonarqube-pvs-studio ./
Для запуска контейнера SonarQube из ранее собранного образа, используйте команду:
docker run -p 9000:9000 sonarqube-pvs-studio
Важно: чтобы не потерять данные из SonarQube при удалении контейнера, монтируйте каталоги по путям, определённым в инструкции к контейнеру SonarQube в DockerHub. Вот пример команды запуска контейнера с монтированными каталогами:
docker run -v /path/to/data:/opt/sonarqube/data
-v /path/to/logs:/opt/sonarqube/logs
-v /path/to/extentions:/opt/sonarqube/extensions
-p 9000:9000 sonarqube-pvs-studio
Чтобы увидеть в браузере SonarQube запущенный в Docker-контейнере, перейдите по ссылке http://localhost:9000/.
Для входа введите стандартные значения login-а и пароля администратора SonarQube:
Login: admin
Password: admin
После входа установите новый пароль для аккаунта администратора.
Примечание. Ниже приведена настройка Quality Profile с предустановленными плагинами PVS-Studio и SonarC++ Community(v2.0.4). Примите во внимание то, что, если Вы используете SonarC++ Community плагин версии ниже 2.0+, то вместо языка CXX, будут С++(Community) / С(Community).
Quality Profile - это коллекция диагностик, которые выполняются во время анализа. Вы можете включать диагностики PVS-Studio в существующие профили, либо создать новый. Профиль привязывается к конкретному языку программирования, но вы можете создавать несколько профилей с разными наборами правил. Все действия доступны пользователям из группы sonar-administrators.
Создать новый профиль можно в меню Quality Profiles -> Create:
Для включения диагностик анализатора в активный профиль, выберите интересующий вас репозиторий в меню Rules -> Repository:
Далее добавьте все диагностические правила в свой профиль, нажав на кнопку Bulk Change, или выберите конкретные правила вручную.
Окно активации диагностик выглядит следующим образом:
Вы также можете фильтровать правила по тегам, прежде чем выбрать их для профиля:
После создания/настройки соответствующего профиля, назначьте один из них профилем по умолчанию:
Профиль по умолчанию запускается на файлах для указанного языка программирования. Профили не обязательно делить по используемым утилитам. Вы можете создать единый профиль для вашего проекта, добавив туда диагностические правила из разных утилит.
Так как в SonarQube каждое расширение анализируемого файла должно быть однозначно присвоено одному языку программирования, SonarC++ Community плагин версии 2.0+ во избежание конфликтов с другими языковыми плагинами по умолчанию не определяет расширения файлов. Для этого Вам нужно самостоятельно их определить:
При выходе новой версии PVS-Studio могут добавиться новые диагностики, поэтому нужно обновить версию плагина на сервере SonarQube и добавить новые диагностики в Quality Profile, который использует диагностики PVS-Studio. Настройка автоматического обновления описана ниже, в отдельном разделе.
Для импорта результатов анализа в SonarQube используется утилита SonarQube Scanner. Для работы ей требуется конфигурационный файл с именем sonar-project.properties, размещённый в корневом каталоге проекта. Конфигурационный файл содержит настройки для анализа заданного проекта, при этом часть или все настройки можно перенести в параметры запуска утилиты SonarQube Scanner.
Перед первым запуском анализа вам необходимо ввести лицензию. Как это сделать — можно узнать в этой документации.
Далее будут рассмотрены типовые режимы запуска сканера для импорта результатов анализа PVS-Studio в SonarQube на разных платформах. SonarQube Scanner будет автоматически подхватывать конфигурационный файл с именем sonar-project.properties в текущей директории запуска.
Для C# проектов возможно использовать два варианта SonarScanner-а:
Разница между ними заключается в том, что SonarQube Scanner for .NET является более специализированным. Он отображает больше статистики, которую предоставляет SonarQube для C# проекта, чем SonarQube Scanner. Например, для отчёта, загруженного SonarQube Scanner, на вкладке со списком проектов (превью проектов) в SonarQube не отображается краткая статистика по проекту, в отличии от проекта, загруженного при помощи SonarQube Scanner for .NET:
MSBuild-проекты проверяются с помощью утилиты PVS-Studio_Cmd.exe.
Вариант 1
Одним запуском PVS-Studio_Cmd вы можете получить отчёт анализатора и конфигурационный файл sonar-project.properties:
PVS-Studio_Cmd.exe ... -o Project.plog --sonarqubedata ...
Команда запуска сканера будет выглядеть следующим образом:
sonar-scanner.bat ^
-Dsonar.projectKey=ProjectKey ^
-Dsonar.projectName=ProjectName ^
-Dsonar.projectVersion=1.0 ^
-Dsonar.pvs-studio.reportPath=Project.plog ^
-Dsonar.login=admin ^
-Dsonar.password=NEW_ADMIN_PASSWORD
Вариант 2
При использовании SonarQube Scanner for .NET для C# проектов необходимо использовать специальный набор команд, а также не нужно генерировать файл sonar-project.properties (аргумент ‑‑sonarqubedata из PVS-Studio_Cmd.exe) при запуске анализа:
SonarScanner.MSBuild.exe begin ... /d:sonar.pvs-studio.reportPath=Project.plog
MSBuild.exe Project.sln /t:Rebuild ...
PVS-Studio_Cmd.exe -t Project.sln ... -o Project.plog
SonarScanner.MSBuild.exe end
dotnet <path to SonarScanner.MSBuild.dll>
begin /d:sonar.pvs-studio.reportPath=Project.plog
dotnet build Project.sln /t:Rebuild ...
PVS-Studio_Cmd.exe -t Project.sln ... -o Project.plog
dotnet <path to SonarScanner.MSBuild.dll> end
Добавьте следующие строки в проверяемый анализатором PVS-Studio Java проект (в зависимости от типа проверяемого проекта):
Maven
<outputType>json</outputType>
<outputFile>output.json</outputFile>
<sonarQubeData>sonar-project.properties</sonarQubeData>
Gradle
outputType = 'json'
outputFile = 'output.json'
sonarQubeData='sonar-project.properties'
Ядро Java анализатора
Windows:
java -jar pvs-studio.jar ^
-s ./ ^
--ext-file /path/to/file/with/classpath/entries ^
-o output.json ^
--output-type json ^
--sonarqubedata sonar-project.properties
Linux/macOS:
java -jar pvs-studio.jar \
-s ./ \
--ext-file /path/to/file/with/classpath/entries \
-o output.json \
--output-type json \
--sonarqubedata sonar-project.properties
После завершения работы Java анализатора конфигурационный файл SonarQube также будет создан автоматически.
Команда запуска сканера будет выглядеть следующим образом:
Windows:
sonar-scanner.bat ^
-Dsonar.projectKey=ProjectKey ^
-Dsonar.projectName=ProjectName ^
-Dsonar.projectVersion=1.0 ^
-Dsonar.pvs-studio.reportPath=output.json ^
-Dsonar.login=admin ^
-Dsonar.password=NEW_ADMIN_PASSWORD
Linux/macOS:
sonar-scanner \
-Dsonar.projectKey=ProjectKey \
-Dsonar.projectName=ProjectName \
-Dsonar.projectVersion=1.0 \
-Dsonar.pvs-studio.reportPath=output.json \
-Dsonar.login=admin \
-Dsonar.password=NEW_ADMIN_PASSWORD
В случае С/C++ проекта, конфигурационный файл необходимо создать самостоятельно. Например, он может быть с таким содержимым:
sonar.projectKey=my:project
sonar.projectName=My project
sonar.projectVersion=1.0
sonar.pvs-studio.reportPath=report.json
sonar.sources=path/to/directory/with/project/sources
Вариант 1
В случае C# проекта получить отчет анализатора вместе с конфигурационным файлом можно с помощью следующей команды:
pvs-studio-dotnet .... -o report.json –sonarqubedata
Запуск sonar-scanner осуществляется следующим образом:
sonar-scanner\
-Dsonar.projectKey=ProjectKey \
-Dsonar.projectName=ProjectName \
-Dsonar.projectVersion=1.0 \
-Dsonar.pvs-studio.reportPath=report.json \
-Dsonar.login=admin \
-Dsonar.password=NEW_ADMIN_PASSWORD
Вариант 2
При использовании SonarQube Scanner for .NET для C# проектов необходимо использовать специальный набор команд, а также не нужно генерировать файл sonar-project.properties (аргумент ‑‑sonarqubedata из pvs-studio-dotnet) при запуске анализа:
dotnet <path to SonarScanner.MSBuild.dll>
begin /d:sonar.pvs-studio.reportPath=report.json
dotnet build Project.sln /t:Rebuild ...
pvs-studio-dotnet .... -o report.json
dotnet <path to SonarScanner.MSBuild.dll> end
Для более глубокой настройки анализа своего проекта, вы можете самостоятельно составить конфигурационный файл из следующих настроек (или отредактировать создаваемый по умолчанию файл при проверке MSBuild и Java проектов):
Описание остальных стандартных параметров конфигурации сканера доступно в общей документации SonarQube.
При использовании плагина PVS-Studio вместе с плагином SonarCFamily необходимо дополнительно использовать SonarSource Build Wrapper или Compilation Database (с версии SonarQube 9.1) при загрузке отчёта PVS-Studio. Подробная инструкция об их использовании находится на сайте SonarQube.
При загрузке отчёта необходимо будет указать свойство sonar.cfamily.build-wrapper-output или sonar.cfamily.compile-commands как аргумент при запуске sonar-scanner (например: -Dsonar.cfamily.build-wrapper-output=директория_с_результатами_работы_Build_Wrapper) или добавить его в файл sonar-project.properties.
Когда директории подпроектов расположены на разных уровнях, загрузить в SonarQube результаты анализа нескольких подпроектов в один проект со стандартными настройками становится невозможным. Это связано с тем, что для такой структуры подпроектов требуется дополнительная настройка индексатора в утилите SonarScanner.
Правильно настроить такой проект можно с помощью модулей, где каждый модуль настраивается для одного подпроекта:
sonar.projectKey=org.mycompany.myproject
sonar.projectName=My Project
sonar.projectVersion=1.0
sonar.sources=src
sonar.modules=module1,module2
module1.sonar.projectName=Module 1
module1.sonar.projectBaseDir=modules/mod1
module2.sonar.projectName=Module 2
module2.sonar.projectBaseDir=modules/mod2
Для указания пути к файлу с результатами анализа PVS-Studio есть 2 пути.
Первый способ
Указать разные отчёты для модулей:
....
sonar.modules=module1,module2
module1.sonar.projectName=Module 1
module1.sonar.projectBaseDir=modules/mod1
module1.sonar.pvs-studio.reportPath=/path/to/report1.plog
module2.sonar.projectName=Module 2
module2.sonar.projectBaseDir=modules/mod2
module2.sonar.pvs-studio.reportPath=/path/to/report2.plog
Второй способ
Указать один отчёт на уровне проекта:
sonar.projectKey=org.mycompany.myproject
sonar.projectName=My Project
sonar.projectVersion=1.0
sonar.sources=src
sonar.pvs-studio.reportPath=/path/to/report.plog
sonar.modules=module1,module2
....
В этом случае каждый модуль загрузит из отчёта только те предупреждения, которые относятся к нему. К сожалению, на файлы из других модулей будет выдан WARN об отсутствии файлов в вывод утилиты SonarScanner, но все результаты анализа будет загружены корректно.
Со всеми возможности анализатора по поиску потенциальных уязвимостей в коде вы можете ознакомиться на странице PVS-Studio SAST (Static Application Security Testing).
Информация о безопасности проверяемого кода, предоставляемая анализатором PVS-Studio, дополнительно помечается в отображаемых SonarQube результатах анализа.
В меню Issues -> Tag или Rules -> Tag доступны следующие теги для группировки предупреждений PVS-Studio по разным стандартам безопасности:
Также вы можете выбрать конкретный CWE ID, если такой доступен (если сообщение анализатора соответствует нескольким идентификаторам CWE, оно будет отмечено одним общим тегом cwe - используйте префиксы в тексте предупреждений для фильтрации таких идентификаторов):
В SonarQube [7.8, 8.4], на страницах Issues и Rules доступен новый фильтр по категориям безопасности. При помощи этого фильтра SonarQube позволяет классифицировать правила в соответствии стандартам безопасности, таким как:
Правила и проблемы от PVS-Studio, которые сопоставлены с CWE ID, также можно группировать в этом меню (Security Category -> CWE):
Примечание. Начиная с версии SonarQube 8.5, во вкладку Security Category могут попасть только Issues/Rules, связанные с безопасностью, а именно те Issues/Rules, у которых тип 'Vulnerability' или 'Security Hotspot'.
Все правила PVS-Studio по умолчанию имеют тип 'Bug'. Если вам необходимо изменить тип правила с 'Bug' на 'Vulnerability' при наличии CWE ID или если правило относится к рейтингу OWASP Top 10, то для этого необходимо в конфигурационном файле сервера '$SONARQUBE_HOME\conf \sonar.properties' добавить следующую строчку:
sonar.pvs-studio.treatPVSWarningsAsVulnerabilities=active
Чтобы изменения вступили в силу нужно перезапустить сервер SonarQube. После того как вы это сделали, правила с CWE ID или относящиеся к рейтингу OWASP Top 10 будут иметь тип 'Vulnerability', а новые сгенерированные проблемы уже будут учитывать это изменение.
Примечание. Если у вас до этого были старые проблемы, то это изменение их не коснется. Для этого вам необходимо вручную изменить тип у этих проблем.
В конфигурационном файле sonar-project.properties доступны следующие опции:
sonar.pvs-studio.cwe=active
sonar.pvs-studio.misra=active
Они включают режим добавления идентификаторов CWE и MISRA к предупреждениям анализатора. Выглядит это следующим образом:
Возможность фильтровать предупреждения по соответствующим тегам доступна всегда и не зависит от указанных опций.
На вкладке Projects -> Your Project -> Measures доступны различные метрики кода, вычисляемые после каждого запуска анализа. Всю собираемую информацию можно выводить в полезные графики. В секции Security вы можете следить за количеством предупреждений, выдаваемых на проект с пометкой CWE и MISRA:
Остальные, общие метрики сообщений анализатора PVS-Studio, доступны в отдельной секции PVS-Studio.
Большинство действий, доступных для пользователей SonarQube, являются стандартными для этой платформы. Например, просмотр и сортировка результатов анализа, изменение статуса предупреждения и т.п. Поэтому в этом разделе будут описаны только дополнительные возможности, появляющиеся с установкой плагина PVS-Studio.
Предупреждения PVS-Studio делятся на несколько групп, из которых не все могут быть интересны для конкретного проекта. Поэтому вы можете фильтровать диагностики по следующим тегам при создании профиля или просмотре результатов анализа:
Группа диагностик в PVS-Studio |
Тег в SonarQube |
---|---|
Диагностики общего назначения |
pvs-studio#ga |
Диагностики микро-оптимизаций |
pvs-studio#op |
Диагностики 64-битных ошибок |
pvs-studio#64 |
Стандарт MISRA |
pvs-studio#misra |
Диагностики, реализованные по запросам пользователей |
pvs-studio#cs |
Проблемы при работе анализатора кода |
pvs-studio#fails |
И стандартные теги, которые используются для предупреждений PVS-Studio:
Стандарты контроля качества кода |
Тег в SonarQube |
---|---|
Стандарт CWE |
cwe |
Стандарт CERT |
cert |
Стандарт MISRA |
misra |
Стандартные теги SonarQube, в отличии от группы тегов pvs-studio#, могут включать, в зависимости от используемого quality профиля, сообщения и от других инструментов, помимо PVS-Studio.
На вкладке Projects -> Your Project -> Measures доступны различные метрики кода, вычисляемые после каждого запуска анализа. С плагином добавляется секция PVS-Studio, где вы можете найти полезную информацию для проекта и строить графики:
При работе на большой кодовой базе, анализатор неизбежно генерирует большое количество предупреждений. При этом часто нет возможности поправить все предупреждения сразу. Для того, чтобы иметь возможность сконцентрироваться на правке наиболее важных предупреждений и не "засорять" статистику, можно провести предварительную настройку анализатора и фильтрацию лога до запуска SonarQube Scanner. Это можно сделать несколькими способами.
1. Анализ можно сделать менее "шумным" с помощью настройки No Noise. Она позволяет полностью отключить генерацию предупреждений низкого уровня достоверности (Low Certainty, третий уровень предупреждений). После перезапуска анализа, сообщения этого уровня полностью пропадут из вывода анализатора. Для включения этой настройки в анализаторе, воспользуйтесь окном настроек "Specific Analyzer Settings" в Windows и общей документацией для Linux и macOS.
2. Анализ можно ускорить, исключив из проверки внешние библиотеки, код тестов и т.д. Для добавления файлов и каталогов в исключения, воспользуйтесь окном настроек "Don't Check Files" в Windows и общей документацией для Linux и macOS.
3. Если вам нужен дополнительный контроль над тем, какие сообщения анализатора будут попадать в отображаемые результаты, например, по уровню достоверности или кодам ошибок, воспользуйтесь утилитой для фильтрации и преобразования результатов (Plog Converter) на соответствующей платформе.
4. Если вам необходимо изменить важность предупреждения, это делается в настройках самого анализатора, а не в SonarQube. Сообщения PVS-Studio имеют следующие уровни достоверности: High, Medium, Low и Fails. В SonarQube им соответствуют уровни Critical, Major, Minor и Info. Изменение уровней описано на странице "Дополнительная настройка диагностик"
Автоматизировать процесс обновления позволяет SonarQube Web Api. Предположим, на вашем билд-сервере настроена система автоматического обновления PVS-Studio (как это описано в статье Развертывание PVS-Studio в больших командах). Для того, чтобы обновить плагины PVS-Studio и добавить новые диагностики в Quality Profile без использования веб-интерфейса, выполните следующие шаги (пример для Windows, на других ОС данная процедура выполняется аналогично):
Допустим, сервер SonarQube установлен в директории C:\Sonarqube\ и запущен как сервис. PVS-Studio установлена в директории C:\Program Files (x86)\PVS-Studio\. Тогда скрипт для автоматического обновления дистрибутива PVS-Studio и плагина sonar-pvs-studio-plugin будет иметь вид:
set PVS-Studio_Dir="C:\Program Files (x86)\PVS-Studio"
set SQDir="C:\Sonarqube\extensions\plugins\"
rem Update PVS-Studio
cd /d "C:\temp\"
xcopy %PVS-Studio_Dir%\PVS-Studio-Updater.exe . /Y
call PVS-Studio-Updater.exe /VERYSILENT /SUPPRESSMSGBOXES
del PVS-Studio-Updater.exe
rem Stop the SonarQube server
sc stop SonarQube
rem Wait until the server is stopped
ping -n 60 127.0.0.1 >nul
xcopy %PVS-Studio_Dir%\sonar-pvs-studio-plugin.jar %SQDir% /Y
sc start SonarQube
rem Wait until the server is started
ping -n 60 127.0.0.1 >nul
curl http://localhost:9000/api/qualityprofiles/search
-v -u admin:admin
Ответ сервера имеет следующий формат:
{
"profiles": [
{
"key":"c++-sonar-way-90129",
"name":"Sonar way",
"language":"c++",
"languageName":"c++",
"isInherited":false,
"isDefault":true,
"activeRuleCount":674,
"rulesUpdatedAt":"2016-07-28T12:50:55+0000"
},
{
"key":"c-c++-c-pvs-studio-60287",
"name":"PVS-Studio",
"language":"c/c++/c#",
"languageName":"c/c++/c#",
"isInherited":false,
"isDefault":true,
"activeRuleCount":347,
"rulesUpdatedAt":"2016-08-05T09:02:21+0000"
}
]
}
Допустим, нам нужно добавить новые диагностики в профиль PVS-Studio для языка 'c/c++/c#'. Его ключом является значение c-c++-c-pvs-studio-60287.
Обратите внимание на то, что ключ профиля может содержать специальные символы, и при отправке его в POST-запросе нужно произвести экранирование URL символов. Так, ключ профиля c-c++-c-pvs-studio-60287 должен быть преобразован в c-c%2B%2B-c-pvs-studio-60287
В параметре tags передайте теги диагностик, которые нужно активировать в профиле. Чтобы активировать все диагностики PVS-Studio, укажите тег pvs-studio.
В результате, запрос на добавление всех диагностик в профиль PVS-Studio будет иметь вид (одной строкой):
curl --request POST -v -u admin:admin -data
"profile_key=c-c%2B%2B-c-pvs-studio-60287&tags=pvs-studio"
http://localhost:9000/api/qualityprofiles/activate_rules
Настройка рассылки результата анализа происходит в несколько этапов. Сначала нужно настроить почту, с которой будут приходить оповещения. После этого каждый пользователь может выбрать необходимые оповещения.
Для настройки почты, находясь в роли Администратора, нужно перейти в раздел Administration > Configuration > General.
После этого необходимо заполнить параметры:
Примечание. В качестве Server base URL (sonar.core.serverBaseURL) нужно указать тот IP-адрес, на котором находится SonarQube. Это нужно, чтобы гиперссылки в письме ссылались на правильный сервер (по умолчанию они всегда будут отправлять на localhost).
Данные параметры можно задать в файле настроек сервера SonarQube, расположенном по пути sonarqube\conf\sonar.properties.
Чтобы проверить подключение к почте, можно отправить тестовое письмо в разделе Test Configuration.
После настройки отправителя каждый пользователь может выбрать нужные оповещения. Для этого нужно перейти в раздел My Account > Notifications. Есть различные типы оповещений, которые может выбрать пользователь. Отдельно можно выбрать оповещения для каждого проекта.
CodeChecker — это интерфейс для работы со статическими анализаторами для Linux и macOS. С помощью данного инструмента можно удобно просматривать результаты анализа PVS-Studio и управлять ими.
Для того, чтобы запустить CodeChecker нужно создать виртуальное окружение Python (версии >= 3.8) и установить инструмент с помощью пакетного менеджера pip:
python3 -m venv .venv
source ./.venv/bin/activate
pip3 install codechecker
Поддержка анализатора PVS-Studio во встроенном инструменте для преобразования отчётов report-converter появится в версии CodeChecker 6.25.0, однако использовать PVS-Studio с CodeChecker можно уже сейчас. Для этого нужно клонировать исходный код инструмента из репозитория на GitHub, а далее установить более свежую версию компонента report-converter из репозитория в виртуальное окружение, которое было создано этапом ранее:
cd tools/report-converter/
python setup.py install
Для корректного отображения severity у диагностических правил PVS-Studio необходимо скачать файл CodeChecker.json и положить его в этом виртуальном окружении Python, по пути .venv/share/codechecker/config/labels/analyzers с названием pvs-studio.json.
Теперь для запуска веб-сервера CodeChecker потребуется выполнить следующую команду:
CodeChecker server
По умолчанию веб-сервер CodeChecker доступен по адресу http://localhost:8001.
О других способах установки и развёртывания CodeChecker можно прочитать в специальном разделе документации самого инструмента.
Для работы с отчётом PVS-Studio в CodeChecker его нужно привести в нужный формат. Помочь нам с этим может встроенный инструмент CodeChecker — report-converter.
Важно, что report-converter может преобразовать только JSON-отчёт PVS-Studio. Для преобразования отчёта PVS-Studio другого формата в JSON или фильтрации предупреждений необходимо использовать утилиты PlogConverter.exe для Windows и plog-converter для Linux/macOS:
Пример команды конвертации отчёта PVS-Studio при помощи plog-converter (Linux/macOS):
plog-converter -t json -a 'GA:1,2;OWASP:1'
-o /home/user/Logs/PVS-Studio.json PVS-Studio.log
Пример команды конвертации отчёта PVS-Studio при помощи PlogConverter.exe (Windows):
PlogConverter.exe -t Json -a 'GA:1,2;OWASP:1' -o /home/user/Logs
-n PVS-Studio PVS-Studio.plog
Для преобразования отчёта PVS-Studio в формат CodeChecker необходимо выполнить следующую команду:
report-converter -t pvs-studio -o ./pvs_studio_reports ./PVS-Studio.json
Флаг -t указывает формат входного отчёта, -o – директорию, в которую будут сохранены преобразованные предупреждения. Подробнее об остальных функциях report-converter можно прочитать в его документации.
Чтобы сохранить преобразованный отчёт на веб-сервере для дальнейшей работы с ним, необходимо выполнить следующую команду:
CodeChecker store ./codechecker_pvs_studio_reports -n default
После флага -n необходимо передать название запуска CodeChecker.
После выполнения описанных выше действий в списке запусков для проекта появится запуск с указанным названием.
Нажав на его название, можно увидеть список предупреждений для этого проекта:
На данной странице можно фильтровать и сортировать предупреждения по определённым параметрам, например по уровню достоверности или названию диагностического правила.
Нажав на локацию конкретного срабатывания, можно увидеть место в коде, где это срабатывание произошло:
На этой же странице можно поменять Review status открытого срабатывания. Эта функция может быть полезна при разметке предупреждений анализатора после проведения анализа. Каждому срабатыванию можно поставить один из следующих статусов:
При пометке срабатывания как False Positive оно будет исключено из отображения отчёта, в том числе при всех следующих запусках.
Срабатывания, которых не было при предыдущих запусках помечаются в списке специальным значком, а также есть возможность просматривать разницу между конкретными запусками:
После нажатия кнопки Diff отобразится список только различающихся срабатываний:
Jira – система управления задачами и проектами. Исправлять предупреждения от статического анализатора кода может быть удобно в рамках оформленной задачи.
Оформлять новую задачу на каждое предупреждение не рекомендуется по следующим причинам:
Тем не менее, исправление некоторых предупреждений или ошибок требует фиксации и контроля в системе управления задачами.
У пользователей, которые используют PVS-Studio, SonarQube и Jira одновременно, есть возможность в полуавтоматическом режиме создавать задачи из предупреждений анализатора. Делается это с помощью встроенного в Jira приложения SonarQube Connector for Jira. Добавить его к своему проекту можно в меню Jira Software > Apps > Find new apps > SonarQube Connector for Jira.
Для создания задачи из предупреждения PVS-Studio надо проделать несколько шагов:
Создание задач из выбранных предупреждений выглядит так:
Важно: для загрузки предупреждений анализатора в SonarQube требуется Enterprise лицензия PVS-Studio. Вы можете запросить пробную Enterprise лицензию здесь.
В данной статье рассматривается работа в среде Windows. Работа в среде Linux описана в статье "Как запустить PVS-Studio в Linux".
Для установки PVS-Studio требуются права администратора.
Автоматическая установка выполняется указанием дополнительных параметров командной строки:
PVS-Studio_setup.exe /verysilent /suppressmsgboxes
/norestart /nocloseapplications
PVS-Studio может потребоваться перезагрузка компьютера в случае, например, если обновляемые файлы были заблокированы. Для установки без перезагрузки необходимо использовать флаг 'NORESTART'. Обратите внимание, что без этого флага при запуске установщика PVS-Studio в silent режиме перезагрузка может произойти без предварительных уведомлений или диалогов.
По умолчанию будут установлены все доступные компоненты PVS-Studio. В случае, если это не желательно, возможно указание компонентов для установки с помощью флага 'COMPONENTS' (далее перечислены все доступные компоненты):
PVS-Studio_setup.exe /verysilent /suppressmsgboxes
/nocloseapplications /norestart /components= Core,
Standalone,MSVS,MSVS\2010,MSVS\2012,MSVS\2013,MSVS\2015,MSVS\2017,
MSVS\2019,MSVS\2022,IDEA,JavaCore,Rider,CLion,VSCode,DOTNET
Вы можете использовать нотацию /components="Core,*MSVS" для установки плагинов для всех Visual Studio одновременно.
Краткое описание компонентов:
Во время установки PVS-Studio все экземпляры Visual Studio / IntelliJ IDEA / Rider должны быть выключены, однако для предотвращения потерь несохраненных документов установочный пакет не выгружает Visual Studio / IntelliJ IDEA / Rider самостоятельно.
Инсталлятор завершит работу с кодом завершения 100 в случае, если не сможет установить модуль расширения (*.vsix) для какой-либо из выбранных версий Visual Studio.
Получить дополнительную информацию о флагах можно с помощью 'HELP':
PVS-Studio_setup.exe /help
Утилита PVS-Studio-Updater.exe позволяет проверить наличие обновлений анализатора, а в случае их наличия, скачать и установить их на локальной машине. Для запуска утилиты обновления в "тихом" режиме можно использовать те же параметры, что и для дистрибутива:
PVS-Studio-Updater.exe /VERYSILENT /SUPPRESSMSGBOXES
В случае отсутствия обновлений на сервере, утилита завершит работу с кодом 0. Т.к. PVS-Studio-Updater.exe производит локальную установку PVS-Studio, во время его работы в системе также не должен быть запущен процесс devenv.exe.
Если в вашей системе используется прокси с авторизацией, утилита PVS-Studio-Updater.exe предложит вам ввести учетные данные для авторизации. Если учетные данные были введены корректно, PVS-Studio-Updater.exe сохранит их в Windows Credential Manager, и в дальнейшем будет использовать эти учетные данные для авторизации на прокси. Если вы хотите использовать утилиту с прокси без авторизации, вы можете сделать это с помощью флага proxy (/proxy=ip:port).
Начиная с версии 7.24 PVS-Studio для Windows поддерживает возможность изменения источника обновлений. Это может быть полезно, если вы не хотите, чтобы разработчики обновляли PVS-Studio сразу после выхода релиза.
Например, вы можете создать группу пользователей, которые получат обновления первыми и группу пользователей, которые должны получить обновление после того, как первая группа протестирует продукт.
Для первой группы никаких изменений в настройках не требуется, они будут получать обновления из релизного канала PVS-Studio.
Для второй группы необходимо развернуть альтернативный канал обновления. Для этого достаточно на своем сервере разместить файл version.xml и дистрибутив для установки PVS-Studio. После этого нужно изменить источник обновления на машинах разработчиков второй группы.
Информация об источнике обновления хранится в системном реестре: 'HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\ProgramVerificationSystems\PVS-Studio' в ключе 'UpdateUrl'. По умолчанию, там прописан стандартный адрес до файла version.xml, который содержит информацию об обновлении. Вы можете указать адрес до альтернативного файла version.xml, расположенного на вашем сервере. Например: https://myserver.com/version.xml
Файл version.xml имеет следующую структуру:
<ApplicationUpdate>
<!--Версия обновления PVS-Studio-->
<Version>7.23</Version>
<!--Ссылка для скачивания-->
<FileURL>https://myserver.com/PVS-Studio_setup.exe</FileURL>
<CompatibilityVersion>1</CompatibilityVersion>
</ApplicationUpdate>
Теперь PVS-Studio будет проверять обновления через файл version.xml, который лежит на сервере myserver.com и при наличии обновления, скачает его с этого сервера.
Ещё один возможный вариант установки заключается в использовании менеджера пакетов Chocolatey. При использовании этого варианта установке в системе уже должен быть установлен сам пакетный менеджер.
Команда установки последней доступной версии пакета PVS-Studio:
choco install pvs-studio
Команда установки конкретной версии пакета PVS-Studio:
choco install pvs-studio --version=7.05.35617.2075
При установке пакета также можно задать список устанавливаемых компонентов по аналогии с перечисленными в разделе "Автоматическое развёртывание" данного документа. Для указания компонентов используется флаг '‑‑package-parameters'. Компоненты эквивалентны описанным выше и отличаются только синтаксисом некоторых параметров:
По умолчанию устанавливается только компонент 'Core'. При перечислении компонентов установки 'Core' указывать не нужно.
Пример команды, которая установит анализатор с компонентами 'Core' и 'Standalone':
choco install pvs-studio --package-parameters="'/Standalone'"
Способы ввода лицензии при использовании различного окружения описаны в разделе документации "Как ввести лицензию PVS-Studio, и что делать дальше".
При развертывании на большое количество машин можно установить лицензию без ручного ввода ключа. Для этого необходимо скопировать файл настроек в папку, находящуюся в профиле пользователя.
Если несколько пользователей используют один и тот же компьютер, лицензия должна быть скопирована для каждого.
Расположение файла настроек по умолчанию:
%USERPROFILE%\AppData\Roaming\PVS-Studio\Settings.xml
Этот файл можно редактировать любым текстовым редактором - это простой xml-файл. Следует отметить, что некоторые настройки могут отсутствовать в этом файле. В этом случае будут применены настройки по умолчанию.
Работа анализатора PVS-Studio зависит от окружения, поэтому невозможно его использование в полностью portable режиме, без предварительной настройки. Однако, если на нескольких машинах настроено одинаковое окружение, необходимое для корректной работы анализатора, то можно скопировать файлы анализатора с одного компьютера на другой. Это позволит использовать PVS-Studio на разных компьютерах без непосредственной установки на каждый из них.
Также, вы можете заложить файлы анализатора в систему контроля версий. Это облегчает развертывание и обновление PVS-Studio на большом количестве машин.
Шаг 1. Скачайте установщик PVS-Studio (.exe) с сайта и запустите его.
Шаг 2. После окончания установки введите лицензию как указано в документации.
Шаг 3. Скопируйте папку 'C:\Program Files (x86)\PVS-Studio' и файл настроек 'Settings.xml' на другой компьютер. Расположение файла настроек по умолчанию:
%USERPROFILE%\AppData\Roaming\PVS-Studio\Settings.xml
В результате станет возможно запускать 'PVS-Studio_Cmd.exe' из папки, которая была скопирована на компьютер пользователя. При запуске анализа в аргументе '-s' необходимо указать путь до файла настроек:
PVS-Studio_Cmd.exe .... -s <pathToSettingsXml> ....
Шаг 1. Скачайте установщик PVS-Studio (.exe) с сайта и запустите его;
Шаг 2. Cоздайте файл 'PVS-Studio.lic' с данными о лицензии рядом с ядром Java анализатора по пути 'C:\Program Files (x86)\PVS-Studio-Java'. В файле лицензии на отдельных строках должны быть указаны имя пользователя и серийный номер лицензии, например:
UserName
ХХХХ-ХХХХ-ХХХХ-ХХХХ
После этого вы сможете перенести папку 'C:\Program Files (x86)\PVS-Studio-Java' на другой компьютер, где установлена Java версии 8 и выше. Это позволит использовать Java анализатор из скопированной папки, указав в аргументе '‑‑license-path' путь до файла лицензии, созданный ранее:
java -jar pvs-studio.jar .... --license-path PVS-Studio.lic ....
Для ускорения прохождения анализа можно использовать системы распределённой сборки, например, Incredibuild. Анализ C/C++ кода в PVS-Studio можно разделить на 2 этапа: препроцессирование и собственно анализ. Каждый из этих этапов может быть выполнен удалённо системой распределённой сборки. Для анализа каждого проверяемого C/C++ компилируемого файла, PVS-Studio запускает сначала внешний препроцессор, а затем непосредственно C++ анализатор. Каждый такой процесс может быть выполнен удалённо.
В зависимости от типа проверяемого проекта, анализ PVS-Studio запускается либо через утилиту PVS-Studio_Cmd.exe (для MSBuild проектов), либо с помощью утилиты для отслеживания вызовов компилятора CLMonitor.exe \ Standalone.exe (для любой сборочной системы). Далее, одна из этих утилит будет запускать для каждого проверяемого файла сначала препроцессор (cl.exe, clang.exe для Visual C++ проектов, для остальных – тот же процесс, что использовался при компиляции), а затем C++ анализатор PVS-Studio.exe.
Задание для настройки 'ThreadCount' значения, большего '16' (или большего, чем количество ядер процессора, если у процессора более 16 ядер), доступно только при наличии Enterprise лицензии PVS-Studio. Вы можете запросить пробную Enterprise лицензию здесь.
Данные процессы запускаются параллельно, в зависимости от настройки PVS-Studio|Options...|Common AnalyzerSettings|ThreadCount. Увеличивая количество параллельно проверяемых файлов с помощью этой настройки, и разнеся выполнение этих процессов дополнительно на удалённые машины, можно существенно (в несколько раз) сократить полное время анализа.
Приведём пример ускорения анализа PVS-Studio c использованием распределённой системы Incredibuild. Для этого нам потребуется консольная утилита управления IBConsole. Мы будем использовать интерфейс для автоматического перехвата (Automatic Interception Interface), который позволяет удалённо выполнять любой процесс, перехваченный этой системой. Запуск утилиты IBConsole для распределённого анализа с помощью PVS-Studio будет иметь следующий вид:
ibconsole /command=analyze.bat /profile=profile.xml
Файл analyze.bat должен содержать строку запуска анализатора, PVS-Studio_Cmd.exe или CLMonitor.exe, со всеми необходимыми им параметрами (более подробно можно посмотреть в соответствующем разделе документации). Файл profile.xml содержит конфигурацию для интерфейса автоматического перехвата. Вот пример такой конфигурации для анализа MSBuild проекта с помощью PVS-Studio_Cmd.exe:
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<Profile FormatVersion="1">
<Tools>
<Tool Filename="PVS-Studio_Cmd" AllowIntercept="true" />
<Tool Filename="cl" AllowRemote="true" />
<Tool Filename="clang" AllowRemote="true" />
<Tool Filename="PVS-Studio" AllowRemote="true" />
</Tools>
</Profile>
Посмотрим, что означает каждая запись в этом файле. Мы видим, что для PVS-Studio_Cmd задан атрибут AllowIntercept со значением 'true'. Это означает, что процесс с таким именем не будет сам запускаться распределённо, но система автоматического перехвата будет следить за порождаемыми этим процессом дочерними процессами.
Для процессов препроцессора cl и clang и C/C++ анализатора PVS-Studio задан атрибут AllowRemote. Это означает, что процессы с такими именами, будучи отловленными у процессов с AllowIntercept, будут потенциально выполнены на других (удалённых) агентах Incredibuild.
Перед запуском IBConsole необходимо задать настройку PVS-Studio|Options...|Common AnalyzerSettings|ThreadCount в соответствии с суммарным количеством ядер, доступных на всех Incredibuild агентах. Если этого не сделать, эффекта от использования Incredibuild не будет!
Примечание: во время анализа Visual C++ проектов, PVS-Studio использует clang.exe, поставляемый в дистрибутиве PVS-Studio, для препроцессирования C/C++ файлов перед анализом, вместо препроцессора cl.exe. Это сделано для ускорения препроцессирования, т.к. clang работает быстрее, чем cl. Некоторые старые версии Incredibuild выполняют распределённый запуск препроцессора clang.exe не совсем корректно, что приводит к ошибкам препероцессирования. Поэтому, clang не стоит прописывать в файле конфигурации IBConsole, если ваша версия Incredibuild работает с clang некорректно.
Используемый тип препроцессора во время анализа задаётся настройкой PVS-Studio|Options...|Common AnalyzerSettings|Preprocessor. Если выбрать для этой настройки значение 'VisualCpp', PVS-Studio будет использовать для препроцессирования только cl.exe, который будет выполняться распределённо, но медленне чем clang, который распределённо выполняться не может. Данную настройку стоит выбрать в зависимости от типа проекта и количества доступных для анализа агентов – при большом количестве агентов выбор VisualCpp будет оправдан. При небольшом количестве агентов, локальное препроцессирование с помощью clang может оказаться быстрее.
Имеется возможность использования системы отслеживания вызовов компиляторов (утилиты CLMonitor.exe и Compiler Monitoring UI) при распределенном анализе проекта с помощью Incredibuild. Подобная связка позволит намного быстрее провести анализ проекта, однако, имеет свои особенности. Системы отслеживания вызовов компиляторов не могут отслеживать удаленные вызовы компилятора, и CLMonitor.exe поддерживает трассировку только для локальных сборок.
Анализ мониторинга компилятора, запущенного Incredibuild-ом, может оказаться некорректным, потому что системы отслеживания вызовов компиляторов не отловят вызовы компилятора на других машинах. Тем не менее, имеется возможность распараллелить при помощи Incredibuild анализ дампа, полученного при помощи систем отслеживания вызовов компиляторов. Для этого необходимо получить дамп запуска компилятора.
В случае использования CLMonitor.exe порядок действий для получения дампа мониторинга компилятора такой:
CLMonitor.exe monitor;
CLMonitor.exe saveDump -d c:\monitoring.zip
В случае использования Compiler Monitoring UI дамп возможно получить выполнив следующие действия после запуска Compiler Monitoring UI:
Аналогично предыдущему примеру для распределенного анализа дампа используем ibconsole:
ibconsole /command=analyze.bat /profile=profile.xml
Однако теперь в файле analyze.bat вместо вызова PVS-Studio_Cmd.exe необходимо вызвать CLMonitor.exe в режиме анализа дампа файла:
CLMonitor.exe analyzeFromDump -l "с:\ptest.plog" -d "c:\monitoring.zip"
В случае с Compiler Monitoring UI вместо PVS-Studio_Cmd.exe необходимо вызвать Standalone.exe:
Standalone.exe
В файле настроек же необходимо заменить PVS-Studio_Cmd на CLMonitor / Standalone и cl, при необходимости, заменить на тот тип препроцессора, который используется при сборке (gcc, clang). Например:
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<Profile FormatVersion="1">
<Tools>
<Tool Filename="CLMonitor" AllowIntercept="true" />
<Tool Filename="gcc" AllowRemote="true" />
<Tool Filename="PVS-Studio" AllowRemote="true" />
</Tools>
</Profile>
Также не забываем задать настройку PVS-Studio|Options...|Common AnalyzerSettings|ThreadCount в соответствии с суммарным количеством ядер, доступных на всех Incredibuild агентах. Если этого не сделать, эффекта от использования Incredibuild не будет!
При задании настройки ThreadCount следует помнить, что машина-координатор анализа (т.е. та, на которой будет работать PVS-Studio_Cmd/CLMonitor/Standalone) будет заниматься обработкой результатов, приходящих ото всех процессов PVS-Studio.exe. Данная задача сейчас не может быть распределена – поэтому, особенно когда ThreadCount задаётся очень большим (более 50 процессов одновременно), стоит подумать о том, чтобы "разгрузить" машину-координатор от непосредственно задач анализа (т.е. выполнения процессов анализатора и препроцессора). Это можно сделать с помощью флага IBConsole '/AvoidLocal' или в настройках локального агента Incredibuild на машине координаторе.
При использовании Compiler Monitoring UI для запуска анализа файла дампа мониторинга компиляции необходимо выбрать пункт меню Tools -> Analyze Your Files... и указать путь до файла дампа, прежде чем запустить анализ дампа:
Итогом распределенного анализа дампа будет файл с:\ptest.plog при использовании CLMonitor.exe или предупреждения анализатора в таблице в интерфейсе Compiler Monitoring UI:
Сообщения из таблицы возможно сохранить в файлы в различных форматах используя пункты меню:
При подобном варианте анализа процессы PVS-Studio.exe будут распределены Incredibuild-ом по всем используемым машинам, а CLMonitor.exe распараллелен не будет.
Больше всего пользы подобный вид анализа принесет на большом проекте, в котором редко изменяется структура проекта (добавление, удаление, переименование файлов и т.п.), но часто изменяется их содержимое.
PVS-Studio распространяется в виде Deb/Rpm пакетов или архива. Воспользовавшись установкой из репозитория, вы сможете получать обновления о выходе новой версии программы.
В дистрибутиве содержатся следующие файлы:
Установить анализатор можно следующими способами:
До Debian 11 и Ubuntu 22.04:
wget -q -O - https://cdn.pvs-studio.com/etc/pubkey.txt | \
sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://cdn.pvs-studio.com/etc/viva64.list
sudo apt-get update
sudo apt-get install pvs-studio
После Debian 11 и Ubuntu 22.04:
wget -qO- https://cdn.pvs-studio.com/etc/pubkey.txt | \
sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/viva64.gpg
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://cdn.pvs-studio.com/etc/viva64.list
sudo apt-get update
sudo apt-get install pvs-studio
wget -O /etc/yum.repos.d/viva64.repo \
https://cdn.pvs-studio.com/etc/viva64.repo
yum update
yum install pvs-studio
wget -q -O /tmp/viva64.key https://cdn.pvs-studio.com/etc/pubkey.txt
sudo rpm --import /tmp/viva64.key
sudo zypper ar -f https://cdn.pvs-studio.com/rpm viva64
sudo zypper update
sudo zypper install pvs-studio
Вы можете скачать PVS-Studio для Linux здесь.
Также вам необходимо установить утилиту strace версии 4.11 или выше для функционирования режима трассировки компиляции.
sudo gdebi pvs-studio-VERSION.deb
или
sudo dpkg -i pvs-studio-VERSION.deb
sudo apt-get -f install
sudo dnf install pvs-studio-VERSION.rpm
или
sudo zypper install pvs-studio-VERSION.rpm
или
sudo yum install pvs-studio-VERSION.rpm
или
sudo rpm -i pvs-studio-VERSION.rpm
tar -xzf pvs-studio-VERSION.tgz
sudo ./install.sh
После успешной установки анализатора на ваш компьютер, для проверки проекта следуйте инструкциям на этой странице: "Как запустить PVS-Studio в Linux".
PVS-Studio распространяется в виде графического инсталлятора, архива или через Homebrew репозиторий. Воспользовавшись установкой из репозитория, вы сможете получать обновления анализатора автоматически.
В дистрибутиве содержатся следующие файлы:
Установить анализатор можно следующими способами:
Установка:
brew install viva64/pvs-studio/pvs-studio
Обновление:
brew upgrade pvs-studio
Запуститите .pkg файл и следуйте инструкциям инсталлятора:
Распакуйте архив и поместите исполняемые файлы в каталог, доступный в PATH.
tar -xzf pvs-studio-VERSION.tgz
После успешной установки анализатора на ваш компьютер, для проверки проекта следуйте инструкциям на этой странице: "Как запустить PVS-Studio в Linux и macOS".
Анализатор PVS-Studiо позволяет легко посмотреть наиболее интересные предупреждения, чтобы ускорить и упростить ознакомление и начальную работу с отчетом. Это предупреждения наших самых интересных и опасных диагностических правил, которые с наибольшей вероятностью являются реальными ошибками.
Данный механизм предназначен прежде всего для знакомства с возможностями анализатора.
Режим отображения интересных предупреждений не является заменой работы с полным отчётом анализатора, т.к. в полном отчёте могут содержаться не менее опасные потенциальные ошибки, на которые также нужно обратить внимание.
В основе механизма отбора интересных предупреждений лежит оценка срабатываний анализатора на основе ряда критериев, которые мы называем весами. Для каждой диагностики задан изначальный вес, который отражает вероятность обнаружить с её помощью качественную (интересную) ошибку. Веса были проставлены в соответствие с нашим многолетним опытом работы со статическим анализатором и статистикой анализа разнообразных проектов. Далее эти веса корректируются с учетом:
В итоге на выходе получается список пар "предупреждение-вес". Из этого списка будет выбрано не более десяти самых "тяжелых" предупреждений.
Для Microsoft Visual Studio доступен плагин PVS-Studio, удобно интегрированный в IDE. Он позволяет запускать анализ всего solution'а, конкретных проектов или отдельных файлов, а также поддерживает инкрементальный анализ.
Механизм отображения интересных предупреждений находится в специальном окне просмотра результатов анализа в Visual Studio:
Данное окно появляется автоматически, например, при выполнении анализа проекта. Если же его необходимо отобразить вручную, то это делается из настроек расширения PVS-Studio:
Само окно позволяет выполнять навигацию по найденным предупреждениям и переходить к коду для его исправления. Также оно предоставляет широкие возможности фильтрации и сортировки результатов. Дополнительно присутствует возможность быстрого перехода к документации выбранной диагностики.
Чтобы отобразить лучшие срабатывания, необходимо нажать на кнопку 'Best'.
В результате в окне окажутся только лучшие предупреждения. При повторном нажатии в окне будут отображаться все найденные ошибки.
Начиная с версии PVS-Studio 7.22 механизм Best Warning поддерживается в плагинах Rider, IntelliJ IDEA и СLion. Чтобы посмотреть наиболее интересные предупреждения с точки зрения анализатора, нажмите на кнопку 'Best', как показано на скриншоте ниже:
После чего в таблице с результатами анализа останутся максимум десять наиболее критичных предупреждений анализатора. При повторном нажатии в окне будут отображаться все найденные ошибки.
Если вы новый пользователь, то также рекомендуем ознакомиться с:
Механизм подавления предупреждений анализатора, также называемый созданием baseline-уровня сообщений, подходит для следующих сценариев:
В этих ситуациях предупреждения анализатора можно подавить специальным образом, чтобы они больше не попадали в новые отчёты. Использование этого режима не требует модификации файлов с исходным кодом проекта.
Анализатор поддерживает анализ исходного кода для языков программирования C, C++, C# и Java. Анализ может быть выполнен на операционных системах Windows, Linux и macOS. В связи с этим, способы подавления предупреждений могут отличаться в зависимости от используемой платформы и типа проектов, поэтому перейдите в раздел, который Вам подходит, и следуйте приведённой там инструкции.
Механизм подавления сообщений основан на использовании специальных файлов, которые добавляются рядом с проектом (или в любом заданном месте). Такие файлы подавления содержат сообщения, размеченные для данного проекта как "ненужные". Заметим, что модификация исходного файла, содержащего размеченные сообщения, и, в частности, сдвиг строк, не приведёт к повторному появлению подавленных сообщений. Однако, правка строки, содержащей сообщение анализатора, может привести к его повторному появлению, т.к. такое сообщение уже считается "новым".
Предупреждение считается подавленным, если в suppress-файле имеется запись, элементы которой эквивалентны соответствующим полям предупреждения из отчёта анализатора (всего их 6):
При записи в файл подавления сообщение предупреждения нормализуется, а именно:
При вычислении хэш-кодов обрабатывается не вся строка, а только символы до первого комментария ложного срабатывания (строчный комментарий, попадающий под один из паттернов: //-VXXXX, //-VXXX, //-vXXXX или //-vXXX, где X – это символ числа от 0 до 9). При этом символы табуляции и пробелов игнорируются.
Изменения исходного кода, которые отменяют подавление предупреждений:
Изменения исходного кода, которые не отменяют подавление предупреждений:
Для Microsoft Visual Studio доступен плагин PVS-Studio, удобно интегрированный в IDE. Он позволяет запускать анализ всего solution'а, конкретных проектов или отдельных файлов, а также поддерживает инкрементальный анализ.
В меню PVS-Studio доступен пункт Suppress Messages, открывающий окно для работы с подавленными предупреждениями анализатора.
В открывшемся окне доступно несколько действий:
Для просмотра результатов анализа в Visual Studio существует специальное окно.
Специальное окно позволяет выполнять навигацию по найденным предупреждениям и переходить к коду для его исправления. Окно PVS-Studio предоставляет широкие возможности фильтрации и сортировки результатов. Также присутствует возможность быстрого перехода к документации выбранной диагностики.
Дополнительные возможности работы с каждым сообщением доступны в контекстном меню по нажатию на правый клик мыши на сообщении.
Для добавления нескольких выбранных предупреждений во все suppress файлы, связанные с предупреждениями, имеется пункт меню "Add selected messages to all suppression files". Кроме этого пункта меню имеется пункт "Add selected messages to primary suppression files". Он позволяет подавить несколько выбранных предупреждений только в primary suppress файлы, связанные с выбранными предупреждениями.
Primary – это метка для suppress файлов, позволяющая приоритизировать использование файлов с такой меткой при подавлении предупреждений. По умолчанию при использовании обычных suppress файлов выбранные предупреждения будут подавлены во все suppress файлы проекта или решения. Если вы хотите подавлять новые предупреждения анализатора только в какой-то конкретный suppress файл, но при этом у вас имеются другие suppress файлы с подавленными ранее предупреждениями, которые вы не хотите модифицировать, то пометьте ваш suppress файл для новых срабатываний как primary.
Использование primary метки позволяет разделить предупреждения, подавленные при внедрении PVS-Studio в проект, от предупреждений, которые были подавлены в ходе дальнейшего регулярного использования анализатора. В таком случае подавленные первоначально предупреждения обычно требуют обработки в будущем (так как их обычно не изучают внимательно), а предупреждения, полученные в результате регулярного использования анализатора, уже по определению просмотрены пользователем.
Primary suppress файлы в выпадающих списках обозначаются дополнительным текстом в скобках:
Добавление Primary suppress файла в проект или решение происходит аналогично добавлению обычного suppress файла через пункт меню "New item..." в контекстном меню проекта/решения:
Также имеется возможность пометить все suppress файлы как primary при подавлении всех предупреждений ("Suppress All") или только отфильтрованных предупреждений ("Suppress Filtered"). Для этого необходимо выбрать чекбокс в окне сообщения, которое выдаётся при подавлении:
Для расподавления нескольких выделенных предупреждений имеется пункт меню "Remove selected messages from all suppression files". Этот пункт отображается в контекстном меню окна вывода результатов, если выбрано более одного предупреждения и хотя бы одно из них подавлено:
При выборе одного предупреждения в контекстном меню доступны пункты не только для подавления/расподавления, но и для перемещения. При перемещении предупреждение удаляется из всех suppress файлов и добавляется в выбранные suppress файлы:
Также при выборе одного предупреждения в окне вывода результатов при наведении мыши на пункты меню, связанные с suppress файлами, появляются выпадающие списки suppress файлов:
В этих выпадающих списках возможно выбрать один suppress файл, все suppress файлы или все primary suppress файлы из списка. Выбранные suppress файлы будут использованы при выполнении команды подавления/расподавления, перемещения.
Пролистывать список suppress файлов возможно при помощи элементов интерфейса сверху и снизу списка со стрелками, либо при помощи клавиш стрелок "Вверх" и "Вниз". Выбрать элемент из списка можно при помощи клавиши "Enter" или левой кнопки мыши.
Из подменю окна вывода результатов плагина возможно включить отображения подавленных предупреждений:
Также включить отображение подавленных предупреждений возможно в окне работы с suppress файлами "Extensions -> PVS-Studio -> Suppress Messages...":
Кроме этого, в подменю таблицы при отображении подавленных предупреждений появляется список с отображаемыми suppress файлами. Убирая или выставляя галочку в чекбоксах из этого списка, возможно включать или отключать отображение подавленных предупреждений из определённых suppress файлов:
Изменения отображения подавленных предупреждений в окне вывода результатов происходят только после закрытия списка отображаемых suppress файлов.
Пролистывать список отображаемых suppress файлов возможно при помощи элементов интерфейсаI сверху и снизу списка со стрелками, либо при помощи клавиш стрелок "Вверх" и "Вниз". Выбирать элемент из списка при помощи клавиши "Enter" или левой кнопки мыши.
Вы можете добавить suppress файл в проект как некомпилируемый\текстовый файл с помощью команд меню 'Add New Item...' или 'Add Existing Item...'. Такое добавление позволяет держать suppress файлы и файлы проектов в разных директориях. Поддерживается добавление нескольких suppress файлов в проект.
Вы можете добавить suppress файл в решение (solution). Сделать это можно с помощью команд меню 'Add New Item...' или 'Add Existing Item...'. Как и для проектов, для решений поддерживается добавление нескольких suppress файлов.
Suppress файл уровня solution позволяет подавлять сообщения во всех проектах соответствующего solution. Если у проектов есть отдельные suppress файлы, анализатор будет учитывать как предупреждения, подавленные в suppress файле solution, так и в suppress файле проекта.
Механизм подавления сообщений можно также использовать напрямую из командной строки. Утилита командной строки PVS-Studio_Cmd.exe автоматически подхватывает существующие suppress файлы при проверке. Также её можно использовать для подавления ранее сгенерированных сообщений анализатора, сохранённых в plog файле. Для подавления сообщений из существующего plog файла необходимо запустить PVS-Studio_Cmd.exe с флагом '‑‑suppressAll'. Например (в одну строку):
"C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe"
-t "Solution.sln" -o "results.plog" --suppressAll SuppressOnly
Исполнение команды сгенерирует suppress файлы для всех проектов, содержащихся в Solution.sln, на которые были сгенерированы сообщения в results.plog.
Флаг '‑‑suppressAll' поддерживает два режима работы:
Также через аргумент -u возможно указать путь до suppress файла. Этот suppress файл будет создан, если он ещё не существовал. В этот suppress файл будут добавлены все предупреждения анализатора.
В PVS-Studio-Cmd.exe имеется режим работы suppression. Он предназначен для:
В этом режиме, аналогично основному режиму работы PVS-Studio-Cmd.exe через флаг -u возможно передать пути до suppress файлов. Эти suppress файлы будут использованы наравне с suppress файлами проектов и решения.
В режиме "suppression" в PVS-Studio_Cmd.exe через флаг -m указывается режим работы:
В режимах CreateEmptySuppressFiles, Suppress и UnSuppress будут использоваться только suppress файлы, чьи имена совпали с паттерном имени suppress файлов (флаг -P). В этом паттерне имеется переменная %projName%, вместо которой подставляется имя проекта.
Пометка suppress файлов как primary (флаг -M) происходит только в режимах CreateEmptySuppressFiles и Suppress.
В режимах Suppress и UnSuppress возможно указать фильтры для предупреждений из отчёта анализатора через флаги фильтров:
Пример команды запуска PVS-Studio_Cmd.exe в режиме suppression:
"C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" suppression
-t "Solution.sln" -R "results.plog" --mode Suppress
-g GA:1,2,3|OWASP:2|64 -E V501,V1001,V3001
-f filename*11,54|absolute/path/filename*1|relative/path/to/directory
-P Prefix_%projName%_Postifx -M
PVS-Studio на Windows можно использовать не только для проверки MSBuild \ Visual Studio проектов. С помощью системы отслеживания вызовов компилятора, вы можете запустить статический анализ любых типов проектов, использующих один из поддерживаемых в PVS-Studio C++ компиляторов.
При запуске анализа, после мониторинга сборки, с помощью команды
clmonitor.exe analyze --useSuppressFile %PathToSuppressFile%
можно с помощью дополнительного флага ‑‑useSuppressFile (-u) передать анализатору путь до suppress файла, который будет использован при проверке.
Помимо консольного инструмента CLMonitor.exe, использовать мониторинг компиляции можно также и с помощью графической утилиты C and C++ Compiler Monitoring UI. Эта утилита предоставляет возможности для проверки кода, независимо от используемого компилятора или сборочной системы, а затем позволяет работать с результатами анализа, предоставляя пользовательский интерфейс, схожий с Visual Studio плагином PVS-Studio.
Тем не менее, если у вас есть проект, который можно открыть в Visual Studio, мы рекомендуем вам, для просмотра результатов анализа использовать Visual Studio плагин PVS-Studio, т.к. возможности встроенного в Compiler Monitoring UI редактора кода значительно уступают редактору кода Visual Studio. Для этого вы можете сохранить отчёт анализатора и переоткрыть его в Visual Studio.
Меню для запуска анализа и подавления предупреждений выглядит следующим образом.
При выборе пункта меню для запуска анализа появится окно "Compiler Monitoring (C and C++)".
Для фильтрации предупреждений анализатора, перед анализом необходимо указать файл с подавленными ранее предупреждениями. Создать и пополнять такой файл можно через меню "Message Suppression...", которое является таким же, как было представлено в разделе про Visual Studio на рисунке 2. После завершения анализа в окне PVS-Studio будут отображены только новые ошибки. Без указания файла анализатор выдаст все результаты.
В Linux/macOS команды подавления и фильтрации сообщений анализатора выполняются только в консоли, при необходимости этот процесс может быть автоматизирован на сервере, выполняющем автоматический запуск анализа. Есть несколько способов использования этого механизма, в зависимости от варианта интеграции анализатора.
Для подавления всех предупреждений анализатора (первый раз и в последующих случаях) необходимо выполнять команду:
pvs-studio-analyzer suppress /path/to/report.log
Если вы хотите подавить предупреждение для какого-либо конкретного файла, воспользуйтесь флагом ‑‑file(-f):
pvs-studio-analyzer suppress -f test.c /path/to/report.log
Помимо самого файла, вы можете явно указать номер строки для подавления:
pvs-studio-analyzer suppress -f test.c:22 /path/to/report.log
При такой записи будут подавлены все предупреждения, которые находятся на строке 22 файла 'test.c'.
Этот флаг можно указывать несколько раз, тем самым подавив предупреждения сразу в нескольких файлах.
Помимо явного указания файла, есть механизм подавления конкретных диагностик:
pvs-studio-analyzer suppress -v512 /path/to/report.log
Флаг ‑‑warning(-v) так же можно указывать несколько раз:
pvs-studio-analyzer suppress -v1040 -v512 /path/to/report.log
Указанные выше флаги ‑‑file и ‑‑warning можно комбинировать для более точечного подавления предупреждений:
pvs-studio-analyzer suppress -f test.c:22 -v512 /path/to/report.log
Так, указанная выше команда подавит все предупреждения диагностики V512 на 22 строке файла 'test.c'.
Анализ проекта можно запускать как прежде. При этом подавленные предупреждения будут фильтроваться:
pvs-studio-analyzer analyze ... -o /path/to/report.log
plog-converter ...
При таком запуске подавленные предупреждения будут сохраняться в текущем каталоге, в файле с именем suppress_file.suppress.json, который надо хранить с проектом. Новые подавленные предупреждения будут дописываться в этот файл. Если необходимо указать другое имя или расположение файла, то команды выше можно дополнить, указав путь до файла с подавленными предупреждениями.
Прямая интеграция анализатора может выглядеть следующим образом:
.cpp.o:
$(CXX) $(CFLAGS) $(DFLAGS) $(INCLUDES) $< -o $@
pvs-studio --cfg $(CFG_PATH) --source-file $< --language C++
--cl-params $(CFLAGS) $(DFLAGS) $(INCLUDES) $<
В этом режиме интеграции происходит вызов C++ ядра анализатора напрямую, поэтому анализатор не может одновременно проверять исходные файлы и фильтровать их. Поэтому для фильтрации и подавления предупреждений потребуется вызывать дополнительные команды.
Для подавления всех предупреждений анализатора необходимо выполнить команду:
pvs-studio-analyzer suppress /path/to/report.log
Для фильтрации нового лога по ранее сгенерированному файлу подавления, необходимо воспользоваться следующими командами:
pvs-studio-analyzer filter-suppressed /path/to/report.log
plog-converter ...
Файл с подавленными предупреждениями также имеет имя по умолчанию suppress_file.suppress.json, для которого при необходимости можно задать произвольное имя.
Для просмотра результатов анализа в IntelliJ IDEA существует специальное окно.
Специальное окно позволяет выполнять навигацию по найденным предупреждениям и переходить к коду для его исправления. Окно PVS-Studio предоставляет широкие возможности фильтрации и сортировки результатов. Также присутствует возможность быстрого перехода к документации выбранной диагностики.
Дополнительные возможности работы с каждым сообщением доступны в контекстном меню по нажатию на правый клик мыши на сообщении. Здесь доступна команда для подавления выделенного предупреждения.
В плагине PVS-Studio для IntelliJ IDEA есть механизм, который позволяет подавить сразу все сообщения одним нажатием кнопки:
По умолчанию файл подавления расположен по пути {projectPath}/.PVS-Studio/suppress_base.json, но в настройках плагина есть возможность изменить этот путь:
Какой бы способ подавления Вы не использовали, при следующем анализе подавленные Вами сообщения не попадут в отчет.
Для подавления предупреждений анализатора необходимо выполнять команду:
./gradlew pvsSuppress "-Ppvsstudio.report=/path/to/report.json"
"-Ppvsstudio.output=/path/to/suppress_base.json"
Для подавления предупреждений анализатора необходимо выполнять команду:
mvn pvsstudio:pvsSuppress "-Dpvsstudio.report=/path/to/report.json"
"-Dpvsstudio.output=/path/to/suppress_base.json"
Для подавления предупреждений анализатора необходимо выполнять команду:
java -jar pvs-studio.jar --convert toSuppress
--src-convert "/path/to/report.json"
--dst-convert "/path/to/suppress_base.json"
Использование baseline-инга для Unreal Engine проектов описано в отдельном разделе про использование PVS-Studio для Unreal Engine проектов.
В плагинах PVS-Studio for CLion и PVS-Studio for Rider на данный момент имеется возможность подавления всех сообщений анализатора. Чтобы подавить сообщения анализатора на старом коде, можно воспользоваться кнопкой 'Suppress All Messages' на панели окна PVS-Studio:
В Rider также имеется возможность подавить все сообщения из главного меню 'Tools -> PVS-Studio -> Suppress All Messages':
Механизм подавления работает с помощью специальных *.suppress файлов, в которые добавляются подавленные сообщения анализатора после выполнения команды 'Suppress All Messages'. При последующем запуске анализа все сообщения, добавленные в *.suppress файлы, не попадут в отчёт анализатора. Система подавления через *.suppress файлы достаточно гибкая и способна "отслеживать" подавленные сообщения даже при модификации и сдвигах участков кода, в которых выдаётся подавленное сообщение.
При работе с Rider файлы *.suppress создаются на уровне проекта, рядом с каждым проектным файлом, но их также можно добавить в любой проект или solution (например, чтобы использовать один общий *.suppress файл для нескольких проектов или всего solution'а). Чтобы вернуть подавленные сообщения анализатора, необходимо удалить *.suppress файлы для соответствующих проектов и перезапустить анализ.
В CLion подавленные сообщения добавляются в файл suppress_file.suppress.json, который записывается в директорию .PVS-Studio, которая находится в корневой директории CLion проекта. Чтобы вернуть все сообщения анализатора, необходимо удалить этот файл и перезапустить анализ.
С версии PVS-Studio 7.27 внутреннее содержимое файлов подавления сортируется. Это требуется для корректного использования в системе контроля версий и избежания проблем слияния файлов.
Сообщения сортируются в таком порядке: имя исходного файла, код диагностики, хэш строки, описание диагностики.
SonarQube (бывший Sonar) — платформа с открытым исходным кодом для непрерывного анализа (англ. continuous inspection) и измерения качества кода.Пользователям этой системы доступен плагин для PVS-Studio. SonarQube сводит результаты анализа к единой информационной панели, ведя историю прогонов и позволяя тем самым увидеть общую тенденцию изменения качества программного обеспечения в ходе разработки. Дополнительным преимуществом является возможность объединять результаты разных анализаторов.
Так, получив результаты анализа одного или нескольких анализаторов, необходимо перейти к списку предупреждений и кликнуть на кнопку "Bulk Change", после чего откроется следующее меню.
В этом окне можно разметить все предупреждения анализатора как "won't fix" и в дальнейшем работать только с новыми ошибками.
PVS-Studio можно быстро интегрировать в новый проект, настроить его автоматическую ежедневную проверку в CI и одновременно постепенно разбираться с подавленными предупреждениями в IDE.
Выглядеть это будет примерно так:
Настроить статический анализ на сборочном сервере и компьютерах разработчиков. В дальнейшем исправлять новые предупреждения анализатора и не давать им накапливаться. Также стоит запланировать поиск и исправление ошибок среди подавленных предупреждений.
Дополнительный контроль за качеством кода поможет обеспечить рассылка результатов по почте. Рассылать предупреждения только для тех разработчиков, которые внесли ошибочный код, возможно с помощью утилиты BlameNotifier, которая входит в Windows дистрибутив PVS-Studio.
Некоторым может быть удобно загружать результаты в Jenkins или TeamCity с помощью плагина PVS-Studio, и рассылать ссылку на эту страницу.
На странице приведены все возможные способы подавления предупреждений анализатора на данный момент. Описание этого механизма основывается на документации к анализатору PVS-Studio, но детали по этой теме были рассмотрены подробнее. Общие сведения могут быть не очень информативны для новых пользователей, поэтому следует ознакомиться с документацией по ссылкам ниже.
При работе с большим количеством сообщений (а при первичной проверке крупных проектов, когда ещё не настроены фильтры и не размечены ложные срабатывания, число сообщений может достигать десятков тысяч), разумно воспользоваться средствами навигации поиска, и фильтрации встроенными в окно вывода результатов PVS-Studio.
Если Вы только начали изучать инструмент статического анализа и хотели бы узнать на что он способен, то можете воспользоваться механизмом Best Warnings. Данный механизм покажет вам наиболее важные и достоверные предупреждения.
Чтобы посмотреть наиболее интересные предупреждения с точки зрения анализатора, нажмите на кнопку 'Best', как показано на скриншоте ниже:
После чего в таблице с результатами анализа останутся максимум десять наиболее критичных предупреждений анализатора.
Окно вывода результатов PVS-Studio в первую очередь предназначено для упрощения навигации по коду анализируемого проекта и переходу к участкам кода, содержащим потенциальные ошибки. Двойной щелчок мыши по любому из сообщений в списке автоматически откроет в редакторе кода файл, на который данное сообщение указывает, переведёт курсор на интересующую строку и выделит её. Кнопки быстрой навигации (рисунок 1) позволяют легко просматривать обнаруженные потенциально опасные места в исходном коде без необходимости постоянно переключаться между окнами среды разработки.
Рисунок 1 — Кнопки быстрого перехода
Для представления результатов анализа окно PVS-Studio использует виртуальную таблицу, позволяющую быстро отображать и осуществлять сортировку сгенерированных сообщений даже для очень крупных проектов (виртуальная таблица позволяет работать со списками из сотен тысяч строк без заметного ущерба для производительности). Крайний левый столбец таблицы предназначен для пометки интересных сообщений, например, тех, к которым имеет смысл вернуться повторно. Данный столбец также поддерживает сортировку, поэтому найти все сообщения, размеченные подобным образом, не составит труда. Пункт контекстного меню "Show Columns" позволяет настроить отображаемые в таблице столбцы (рисунок 2):
Рисунок 2 — Настройка отображения таблицы вывода результатов
Таблица поддерживает множественное выделение с помощью стандартных комбинаций Ctrl и Shift, при этом выделение строк сохраняется и после пересортировки по любой другой колонке. Пункт меню "Copy selected messages to clipboard" (либо сочетание Ctrl+C) позволяет скопировать в буфер обмена содержимое всех выделенных в таблице строк.
Механизмы фильтрации окна вывода PVS-Studio позволяют быстро найти и отобразить как отдельные диагностические сообщения, так целые их группы. Панель инструментов окна содержит ряд переключателей, позволяющих включить либо отключить отображение сообщений из соответствующих им групп сообщений (рисунок 3).
Рисунок 3 — Группы фильтрации сообщений
Все переключатели можно разбить на 3 группы: фильтры по уровню диагностической достоверности сообщений, фильтры по принадлежности сообщения к определённому типу диагностических правил, фильтр размеченных в коде ложных срабатываний. Отключение этих фильтров мгновенно отключает отображение соответствующих им сообщений в списке.
Детальное описание уровней достоверности предупреждений и наборов диагностических правил приведено в разделе документации "Знакомство со статическим анализатором кода PVS-Studio".
Механизм быстрой фильтрации (quick filters) позволяет отфильтровать отчёт анализатора по заданным ключевым словам. Открыть панель быстрой фильтрации можно с помощью кнопки Quick Filters панели инструментов окна (рисунок 4).
Рисунок 4 — Панель быстрой фильтрации
Быстрая фильтрация позволяет отобразить сообщения в соответствии с фильтрами по 3-м ключевым словам: по коду сообщения, по тексту сообщения и по файлу, содержащему данное сообщение. Например, отобразить все сообщения, содержащие слово 'odd' из файла 'command.cpp'. Изменения в списке сообщений становятся видны сразу после выхода из поля ввода ключевого слова (при потере фокуса). Кнопка Reset Filters очищает заданные в данный момент ключевые слова.
Все перечисленные механизмы фильтрации сообщений можно совмещать между собой, фильтруя, например, уровень отображаемых сообщений и файл, к которому сообщения должны относиться, исключая сообщения, помеченные как ложные срабатывания.
При необходимости перехода на какое-либо конкретное сообщение в таблице можно воспользоваться диалогом быстрого перехода к строке, который вызывается через пункт контекстного меню "Navigate to ID..."(рисунок 5):
Рисунок 5 - Вызов диалога быстрого перехода
Рисунок 6 - Диалог быстрого перехода к сообщению
Каждое сообщение в списке вывода PVS-Studio имеет уникальный идентификатор — порядковый номер добавления этого сообщения в таблицу, который отображён в колонке ID. Диалог быстрого перехода позволяет выделить и автоматически сфокусировать сообщение с заданным идентификатором ID, независимо от текущей сортировки таблицы и выделенных строк. Обратите внимание, что ID идентификаторы отображённых в таблице сообщений не всегда идут последовательно, т.к. часть сообщений может быть скрыта с помощью механизмов фильтрации. Переход к таким сообщениям невозможен.
Зачастую в разработке крупных проектов принимают участие распределённые группы разработчиков, а поэтому очень часто один человек не имеет возможности оценить каждое из сообщений статического анализатора на предмет ложно-позитивного срабатывания и, тем более, внести исправления в соответствующий участок исходного кода. В такой ситуации имеет смысл делегировать рассмотрение подобного сообщения разработчику, непосредственно отвечающему за данный участок.
PVS-Studio позволяет автоматически сгенерировать и внести в код комментарий TODO специального вида, содержащий всю необходимую информацию для оценки и анализа отмеченного им фрагмента программы. Такой комментарий будет сразу отображён в окне задач Visual Studio (окно Task List, для версии Visual Studio 2010 необходимо включить разбор комментариев в настройках Tools->Options->Text Editor->C++->Formatting->Enumerate Comment Tasks->true) при условии, что в настройках Tools->Options->Environment->Task List->Tokens задана соответствующая TODO лексема (присутствует в настройках по умолчанию). Комментарий может быть добавлен с помощью команды контекстного меню 'Add TODO comments for selected messages' (рисунок 7)
Рисунок 7 - Вставка TODO комментария
TODO комментарий будет вставлен в строку, сгенерировавшую сообщение анализатора, и будет содержать код ошибки, текст сообщения анализатора и ссылку на online документацию для данного типа ошибок. Благодаря окну заданий (Task List) данный комментарий может быть легко найден любым имеющим доступ к исходному коду разработчиком, а сам текст комментария позволит выявить и исправить потенциальную ошибку даже в случае отсутствия у программиста установленной версии PVS-Studio или полного отчёта о работе анализатора (рисунок 8).
Рисунок 8 - Окно заданий Visual Studio
В Visual Studio открыть окно Task List можно через меню View->Other Windows->Task List. Комментарии TODO отображаются в разделе Comments окна.
В данном разделе описаны механизмы подавления ложноположительных предупреждений, выдаваемых анализатором. С помощью описанных здесь механизмов можно управлять как отдельными сообщениями, выдаваемыми на определённые строки кода, так и подавлять множественные срабатывания, возникающие, например, из-за использования C/C++ макросов. Описан способ, как с помощью комментариев указать анализатору выключить то или иное сообщение анализатора или изменить выдаваемый текст сообщения.
Механизмы, описанные в данном разделе, применимы как для C/C++, так и для C# анализаторов PVS-Studio, если явно не указано обратное.
Любой анализатор кода всегда выдает помимо полезных сообщений об ошибках еще множество так называемых "ложных срабатываний". Это ситуации, когда программисту совершенно очевидно, что в коде нет ошибки, а анализатору это не очевидно. Такие ложные срабатывания называют False Alarm. Рассмотрим пример кода:
obj.specialFunc(obj);
Анализатор считает подозрительным, что у объекта вызывается метод, в качестве аргумента в который передаётся тот же самый объект, поэтому он выдаст на данный код предупреждение V678. Программист же может знать, что использование метода 'specialFunc' таким образом вполне допустимо, поэтому предупреждение анализатора в данном случае является ложным срабатыванием. О том, что предупреждение V678, выданное на этот код, является ложным, можно сообщить анализатору.
Это можно сделать либо вручную, либо с помощью команды контекстного меню. По умолчанию, ложные срабатывания не отображаются в итоговом отчете или плагине. Для включения отображения размеченных подобным образом сообщений можно воспользоваться настройкой 'PVS-Studio -> Options... -> Specific Analyzer Settings -> DisplayFalseAlarms'.
Не рекомендуется использовать разметку сообщений как ложных предупреждений без предварительного просмотра соответствующего кода, так как это противоречит идеологии статического анализа. Только программист может определить, является ли сообщение об ошибке ложным.
Обычно в компиляторах для подавления отдельных сообщений об ошибках используют '#pragma'-директивы. Приведём пример кода:
unsigned arraySize = n * sizeof(float);
Компилятор выдает сообщение:
warning C4267: 'initializing' : conversion from 'size_t' to 'unsigned int', possible loss of data x64Sample.cpp 151
Это сообщение можно подавить с помощью следующей конструкции:
#pragma warning (disable:4267)
Точнее, чтобы подавить конкретно это сообщение, лучше оформить код так:
#pragma warning(push)
#pragma warning (disable:4267)
unsigned arraySize = n * sizeof(float);
#pragma warning(pop)
Анализатор PVS-Studio в качестве разметки использует комментарии специального вида. Для той же строчки кода подавление сообщения PVS-Studio будет выглядеть так:
unsigned arraySize = n * sizeof(INT_PTR); //-V103
Теперь анализатор пометит предупреждение V103 на эту строку как ложное. Такой подход был выбран для повышения наглядности конечного кода. Дело в том, что PVS-Studio может сообщать о проблемах в середине многострочных выражений, как, например, здесь:
size_t n = 100;
for (unsigned i = 0;
i < n; // <= анализатор сообщит о проблеме здесь
i++)
{
// ...
}
Чтобы подавить это сообщение при использовании комментария, достаточно написать:
size_t n = 100;
for (unsigned i = 0;
i < n; //-V104
i++)
{
// ...
}
Если же в это выражение пришлось бы добавлять '#pragma'-директиву, то код выглядел бы значительно менее наглядно.
Хранение разметки в исходном коде позволяет вносить в него модификации без опасения потерять информацию о строках с ошибками.
Иногда необходимо подавить более одного срабатывания на одной строке. В этом случае следует указать отдельный комментарий для каждого из них. Вот пара примеров:
1) подавление срабатываний разных диагностик:
struct Small { int *pointer; };
struct Big { int *array[20]; };
int Add(const Small &a, Big b) //-V835 //-V813
{
return *a.pointer + *b.array[10];
}
2) подавление срабатываний одной диагностики:
struct Small { int *pointer; };
int Add(const Small &a, const Small &b) //-V835 //-V835
{
return *a.pointer + *b.pointer;
}
Можно также использовать отдельную базу, в которой хранить информацию примерно так: код ошибки, имя файла, номер строки. Данный подход отдельно реализован в PVS-Studio и называется "Mass Suppression".
Для работы с ложными срабатываниями пользователю предоставляется две команды, доступные из контекстного меню PVS-Studio (рисунок 1).
Рисунок 1 - Команды для работы с механизмом подавления ложных предупреждений
Рассмотрим команды, относящиеся к подавлению ложных предупреждений:
1. Mark selected messages as False Alarms. Вы можете выбрать одно или несколько предупреждений в списке (рисунок 2) и воспользоваться этой командой для разметки соответствующего кода, как безопасного.
Рисунок 2 - Выбор предупреждений перед выполнением команды "Mark selected messages as False Alarms"
2. Remove False Alarm marks from selected messages. Убирает комментарий, помечающий код как безопасный. Функция, например, может быть полезна, если вы поспешили и ошибочно отметили код как безопасный. Как и в предыдущем случае, вы должны выбрать сообщения из списка, которые планируете обработать.
В макросах (#define) анализатор также, разумеется, может находить потенциальные проблемы и выдавать на них диагностические сообщения. Но при этом анализатор будет выдавать сообщения в тех местах, где макрос используется, то есть где фактически происходит подстановка тела макроса в код. Пример:
#define TEST_MACRO \
int a = 0; \
size_t b = 0; \
b = a;
void func1()
{
TEST_MACRO // V1001 here
}
void func2()
{
TEST_MACRO // V1001 here
}
Чтобы подавить это сообщение, можно использовать команду "Mark as False Alarm". Тогда код с расставленными командами подавления будет выглядеть так:
#define TEST_MACRO \
int a = 0; \
size_t b = 0; \
b = a;
void func1()
{
TEST_MACRO //-V1001
}
void func2()
{
TEST_MACRO //-V1001
}
Однако если макрос используется очень активно, то везде размечать его как False Alarm не очень удобно. Есть возможность в коде сделать вручную специальную пометку, чтобы анализатор автоматически размечал диагностики в этом макросе как False Alarm. С этой пометкой код будет выглядеть так:
//-V:TEST_MACRO:1001
#define TEST_MACRO \
int a = 0; \
size_t b = 0; \
b = a;
void func1()
{
TEST_MACRO
}
void func2()
{
TEST_MACRO
}
При проверке такого кода сообщения о проблемах в макросе уже сразу будут помечены как False Alarm. Причём можно указывать несколько диагностик сразу, через запятую:
//-V:TEST_MACRO:1001, 105, 201
Обратите внимание, что если макрос содержит вложенные макросы, то для автоматической пометки нужно указывать всё равно имя макроса самого верхнего уровня:
#define NO_ERROR 0
#define VB_NODATA ((long)(77))
size_t stat;
#define CHECK_ERROR_STAT \
if( stat != NO_ERROR && stat != VB_NODATA ) \
return stat;
size_t testFunc()
{
{
CHECK_ERROR_STAT // #1
}
{
CHECK_ERROR_STAT // #2
}
return VB_NODATA; // #3
}
В указанном примере диагностика V126 появляется в трех местах. Чтобы автоматически помечать ее как False Alarm в местах #1 и #2 нужно добавить такой код:
//-V:CHECK_ERROR_STAT:126
А чтобы и в #3 это сработало, необходимо указать еще:
//-V:VB_NODATA:126
К сожалению, просто указать "сразу помечать V126 в макросе VB_NODATA" и не указывать про макрос CHECK_ERROR_STAT нельзя из-за технических особенностей механизма препроцессирования.
Всё написанное в этом разделе про макросы справедливо также и для любого фрагмента кода. То есть если, например, вы хотите подавить все срабатывания диагностики V103 на вызов функции 'MyFunction', необходимо добавить такую строку:
//-V:MyFunction:103
Этот пункт относится только к анализатору языков C и C++.
Иногда нужно отключить некоторое правило не глобально, а для определенной части кода. Например, нужно отключить диагностику для определенного файла или части файла, при этом остальные диагностики должны продолжать работать. Подавленное предупреждение должно продолжать выдаваться при анализе остального кода.
Анализатор предоставляет механизм подавления с использованием специальных директив 'pragma'. Этот способ аналогичен тому, который используется в компиляторе для управления предупреждениями.
Анализатор использует следующие директивы:
Так же, как и в случае с '#pragma warning', поддерживается вложенность.
Пример:
void func(int* p1, int* p2, int* p3)
{
if (!p1 || !p2 || !p3)
return;
#pragma pvs(push)
#pragma pvs(disable: 547)
if (p1) // V547 off
do_something();
#pragma pvs(push)
#pragma pvs(enable: 547)
if (p2) // V547 Expression 'p2' is always true.
do_something_else();
#pragma pvs(pop)
if (p3) // V547 off
do_other();
#pragma pvs(pop)
}
Примечание: хотя компиляторы игнорируют неизвестные директивы 'pragma', в зависимости от настроек, они могут выдавать предупреждения о таких директивах. В этом случае предупреждение можно отключить, передав специальный параметр в командную строку компилятора:
Отображением и фильтрацией сообщений можно управлять с помощью комментариев специального вида. Такие комментарии можно писать в специальных файлах конфигурации (.pvsconfig) для всех анализаторов, либо непосредственно в коде проекта (только для C/C++ анализатора).
Файлы конфигурации диагностик анализатора представляют собой простые текстовые файлы, добавляемые в Visual Studio проект либо solution. Для добавления файла конфигурации, выделите интересующий вас проект или solution в окне Solution Explorer среды Visual Studio и выберите пункт контекстного меню 'Add New Item...'. В появившемся окне выберите тип файла 'PVS-Studio Filters File' (рисунок 3):
Рисунок 3 - Добавление в solution файла конфигурации диагностик анализатора.
Из-за особенностей некоторых версий среды Visual Studio, тип файлов 'PVS-Studio Filters File' может отсутствовать на некоторых версиях и редакциях Visual Studio в окне добавления нового файла для solution и\или проекта. В таком случае, можно добавить в проект обычный текстовый файл, задав ему расширение 'pvsconfig'. В свойствах этого файла (после добавления), должно быть указано, что файл не участвует в сборке.
Файл конфигурации, добавленный в проект, действует на все файлы данного проекта. Файл конфигурации, добавленный в solution, действует на все файлы всех проектов, добавленных в данный solution.
Также можно разместить файл конфигурации .pvsconfig в текущей папке пользовательских данных (%AppData%\PVS-Studio\) - такой файл будет подхвачен автоматически при запуске проверки, без необходимости как-либо модифицировать проектные файлы.
Примечание. '.pvsconfig' файлов в '%AppData%\PVS-Studio\' может быть несколько, и все они будут автоматически подхвачены анализатором. Стоит также учитывать, что конфигурация из '%AppData%\PVS-Studio\' будет глобальна для анализатора и будет безусловно использоваться при каждом запуске.
При использовании инструмента командной строки PVS-Studio_Cmd указать путь к файлу конфигурации .pvsconfig можно через параметр ‑‑rulesConfig (-C), например:
PVS-Studio_Cmd.exe -t D:\project\project.sln
-C D:\project\rules.pvsconfig
Файлы конфигурации диагностик .pvsconfig имеют простой синтаксис. Любая строка, начинающаяся с символа '#' считается комментарием и игнорируется. Фильтры записываются в формате однострочных C++/C# комментариев, т.е. должны начинаться с символов '//'.
Для C/C++ кода, фильтры также могут быть записаны в виде комментариев непосредственно в исходном коде. Обратите внимание, что такой формат записи не поддерживается в C# проектах!
Далее рассмотрим различные варианты фильтров, допустимых в файлах конфигурации диагностик.
Предположим, есть следующая структура:
struct MYRGBA
{
unsigned data;
};
И ряд функций, которые её используют:
void f1(const struct MYRGBA aaa)
{
}
long int f2(int b, const struct MYRGBA aaa)
{
return int();
}
long int f3(float b, const struct MYRGBA aaa, char c)
{
return int();
}
На все эти функции анализатор выдаст три сообщения V801: Decreased performance. It is better to redefine the N function argument as a reference. Сообщение в подобном коде будет ложным, так как компилятор сам оптимизирует код, и проблемы не будет.
Можно, конечно, каждое сообщение пометить как False Alarm с помощью функции Mark As False Alarm. Однако, есть способ лучше. Достаточно добавить в код строку:
//-V:MYRGBA:801
Для C/C++ проектов, мы рекомендуем добавлять такую строку в .h-файл рядом с объявлением структуры, но если это невозможно (например, структура в системном .h-файле), то можно прописать это в stdafx.h.
И тогда, после перепроверки, все три сообщения V801 будут автоматически помечены как False Alarm.
Обратите внимание: при использовании комментариев вида //-V:MY_STRING:Vxxx PVS-Studio будет подавлять все сообщения Vxxx, выданные на строки, в которых содержится подстрока MY_STRING.
Описанный механизм может применяться для подавления предупреждений не только для отдельных слов. Иногда, это очень удобно.
Рассмотрим несколько примеров:
//-V:<<:128
Подавит предупреждения V128 в строках, где имеется оператор <<.
buf << my_vector.size();
Если вы хотите подавлять предупреждение V128 только при записи данных в объект с именем 'log', то можно написать так:
//-V:log<<:128
buf << my_vector.size(); // Есть предупреждение
log << my_vector.size(); // Нет предупреждения
Примечание. Обратите внимание, что строка для поиска не должна содержать пробелов.
Правильно: //-V:log<<:128
Неправильно: //-V:log <<:128
При поиске подстроки пробелы игнорируются. Но не беспокойтесь, следующая ситуация обработается корректно:
//-V:ABC:501
AB C = x == x; // Есть предупреждение
AB y = ABC == ABC; // Нет предупреждения
Существует возможность с помощью комментария полностью выключить предупреждение. В этом случае после двух двоеточий указывается номер отключаемой диагностики. Синтаксис:
//-V::(number)
Если требуется проигнорировать предупреждение V122, то можно указать в начале файла:
//-V::122
Для отключения нескольких диагностик можно перечислить их номера через запятую. Синтаксис:
//-V::(number1),(number2),...,(numberN)
Если требуется, например, игнорировать предупреждения V502, V507 и V525, то в начале файла можно указать:
//-V::502,507,525
При необходимости можно отключать предупреждения определённых диагностик на указанных уровнях. Синтаксис:
//-V::(number1),(number2),...,(numberN):1,2,3
Например, если требуется игнорировать предупреждения V3161 и V3165 на уровнях 'Medium' и 'Low', можно указать:
//-V::3161,3165:2,3
Анализатор также поддерживает возможность фильтрации предупреждений по номеру диагностики и подстроке. Синтаксис:
//-V::(number1),(number2),...,(numberN)::{substring}
Например, можно исключить из отчёта все предупреждения V3022 и V3063, содержащие подстроку "always true":
//-V::3022,3063::{always true}
Этот функционал можно комбинировать с фильтрацией по уровню:
//-V::(number1),(number2),...,(numberN):1,2,3:{substring}
Например, можно исключить все срабатывания V5625, имеющие 2 уровень и содержащие подстроку "Google.Protobuf 3.6.1":
//-V::5625:2:{Google.Protobuf 3.6.1}
Существует также возможность отключить группу диагностик. Синтаксис:
//-V::GA
//-V::X64
//-V::OP
//-V::CS
//-V::MISRA
Для отключения сразу нескольких групп диагностик их можно перечислить через запятую. Синтаксис:
//-V::X64,CS,...
Для отключения всех диагностик C++ или C# анализатора следует использовать следующую форму:
//-V::C++
//-V::C#
Поскольку анализатор не будет выдавать указанные предупреждения, то это может значительно уменьшить размер лога проверки, если какая-то диагностика даёт слишком много ложных срабатываний.
В случае необходимости можно задать исключение из анализа файлов / директорий, которые попадают под заданные маски. Это может быть удобно, например, для исключения из анализа кода сторонних библиотек или автоматически сгенерированных файлов.
Несколько примеров масок:
//V_EXCLUDE_PATH C:\TheBestProject\thirdParty
//V_EXCLUDE_PATH *\UE4\Engine\*
//V_EXCLUDE_PATH *.autogen.cs
Синтаксис масок идентичен синтаксису для опций 'FileNameMasks' и 'PathMasks', описанному в документе "Настройки: Don't Check Files".
Перед запуском анализа 'PVS-Studio_Cmd' формирует конфигурацию диагностических правил из:
Если у вас большое количество проектов, для которых конфигурация формируется разными способами, то в некоторых случаях конфигурация из глобальных файлов может приводить к путанице в результирующей конфигурации. Это связано с тем, что конфигурация из глобальных файлов применяется всегда над каким бы проектом вы ни работали. Другими словами, настройки, которые характерны только для проекта X, будут также применены к другим проектам.
Поэтому, если вам необходимо проигнорировать конфигурацию из глобальных файлов, нужно добавить специальный флаг в соответствующий '.pvsconfig' файл:
//IGNORE_GLOBAL_PVSCONFIG
Правила действия флага следующие:
Использование этого флага позволит гибко выключать глобальные настройки для определенных случаев.
Возможны ситуации, в которых определённый тип диагностик не актуален для анализируемого проекта, или какая-либо из диагностик анализатора выдаёт предупреждения на код, в корректности которого вы уверены. В таком случае можно воспользоваться системой группового подавления сообщений, основанной на фильтрации полученных результатов анализа. Список доступных режимов фильтрации можно открыть через общее меню PVS-Studio -> Options.
Следует отметить, что применение фильтров для группового подавления ложных срабатываний не требует перезапуска анализа, результаты фильтрации будут сразу отображены в окне вывода PVS-Studio.
Во-первых, можно отключить диагностику тех или иных ошибок по их коду. Это делается с помощью вкладки "Настройки: Detectable Errors". На вкладке обнаруживаемых ошибок можно указать номера ошибок, которые не надо показывать в отчете по анализу. Иногда бывает целесообразно убрать в отчете ошибки с определенными кодами. Например, если вы уверены, что ошибки, связанные с явным приведением типа (коды V201, V202, V203), вас не интересуют, то вы можете скрыть их показ. Также отображение ошибок определённого типа можно отключить с использованием команды контекстного меню "Hide all Vxxx errors". Соответственно, в случае, если необходимо включить отображение обратно, настроить это можно на упоминавшейся выше вкладке "Detectable Errors".
Во-вторых, можно отключить анализ некоторых частей проекта (некоторых папок или файлов проекта). Раздел "Настройки: Don't Check Files". На этой вкладке можно ввести информацию о библиотеках, включения (через директиву #include) из файлов которых анализировать не надо. Это может потребоваться для уменьшения количества лишних диагностических сообщений. Например, в проекте используется библиотека Boost. И хотя на какой-то код из этой библиотеки анализатор выдает диагностические сообщения, вы считаете, что эта библиотека является достаточно надежной и написана хорошо. Поэтому, возможно, не имеет смысла получать диагностические сообщения по поводу кода в этой библиотеке. В этом случае можно отключить анализ файлов из этой библиотеки, указав путь к ней на странице настроек. Кроме того, возможно ввести файловые маски для исключения некоторых файлов из анализа. Анализатор не будет проверять файлы, удовлетворяющие условиям маски. Например, подобным образом можно исключить из анализа автогенерируемые файлы.
Маски путей для файлов, сообщения из которых попали в текущий сгенерированный отчёт, можно автоматически добавить в список Don't Check Files с помощью команды контекстного меню "Don't check files and hide all messages from..." для выделенного в окне PVS-Studio Output сообщения (рисунок 4).
Рисунок 4 - Добавление масок путей через контекстное меню
Данная команда позволит добавить в фильтры исключений как отдельный выбранный файл, так и маску по целой директории, в которой данный файл находится.
В-третьих, можно подавлять отдельные сообщения по тексту. На вкладке "Настройки: Keyword Message Filtering" можно настроить фильтрацию ошибок по содержащемуся в них тексту, а не по коду. При необходимости можно скрыть из отчета сообщения о диагностированных ошибках, содержащих определенные слова или фразы. Например, если в отчете есть ошибки, в которых указаны названия функций printf и scanf, а вы считаете, что ошибок, связанных с ними, быть не может, то просто добавьте эти два слова с помощью редактора подавляемых сообщений.
Иногда, особенно на стадии внедрения статического анализа в крупных проектах, может возникнуть необходимость 'подавить' все предупреждения анализатора на имеющуюся кодовую базу, т.к. разработчики могут не иметь необходимых ресурсов для исправления найденных анализатором ошибок в старом коде. В таком случае может быть полезно 'скрыть' все предупреждения, выданные на имеющийся код, чтобы отслеживать только вновь появляющиеся ошибки. Этого можно достичь за счёт использования механизма "массового подавления сообщений анализатора". Использование соответствующего механизма в среде Windows описано в документе "Массовое подавление сообщений анализатора", в среде Linux – в соответствующем разделе документа "Как запустить PVS-Studio в Linux".
В редких случаях автоматически расставленные разметки могут быть поставлены не в том месте, где должны быть. И тогда анализатор вновь выдаст эти же сообщения об ошибках, так как маркер не будет найден. Это проблема препроцессора, связанная с многострочными #pragma-директивами определенного типа, из-за которых также сбивается нумерация строк. Решением проблемы является пометка сообщений, на которых заметен сбой, вручную. PVS-Studio всегда сообщает о подобных ошибках сообщением "V002. Some diagnostic messages may contain incorrect line number".
Как и с любой другой процедурой, включающей в себя массовую обработку файлов, при использовании разметки сообщений как ложных стоит помнить о потенциально возможных конфликтах доступа. Так как во время разметки файлов один из файлов может быть открыт во внешнем редакторе и там модифицирован, результат совместной работы с файлом предсказать невозможно. Поэтому мы рекомендуем либо иметь копию исходного кода, либо пользоваться системами контроля версий.
Результатом анализа проекта с помощью PVS-Studio может быть:
Полученные файлы можно просматривать в плагине PVS-Studio для IDE или в приложении C and C++ Compiler Monitoring UI.
Эти форматы отчёта неудобны для прямого просмотра в текстовом редакторе или рассылки по электронной почте. Поэтому в состав дистрибутива PVS-Studio входит специальная утилита, позволяющая преобразовывать результаты анализа в другие форматы.
Название утилиты преобразования отчётов зависит от платформы:
Приведём далее список форматов, поддерживаемых утилитой конвертации отчётов.
Формат |
Расширение |
Инструменты |
Описание |
---|---|---|---|
PVS-Studio Log (Plog) |
.plog |
Visual Studio, SonarQube, Compiler Monitoring UI |
Для Windows пользователей Visual Studio и SonarQube |
JSON |
.json |
Visual Studio IntelliJ IDEA Rider CLion |
Для пользователей плагинов PVS-Studio в IDE и SonarQube |
SARIF |
.sarif |
Visual Studio, Visual Studio Code, есть визуализация в GitHub Actions |
Универсальный формат отчёта статического анализатора |
TaskList |
.tasks |
Qt Creator |
Для работы с отчётом в Qt Creator |
TaskList Verbose |
.tasks |
Qt Creator |
Расширение формата TaskList с поддержкой отображения дополнительных позиций |
CSV |
.csv |
Microsoft Excel LibreOffice Calc |
Для просмотра предупреждений в табличном виде |
Simple Html |
.html |
Email Client Browser |
Для рассылки отчётов почтой |
Full Html |
Folder |
Browser |
Для просмотра предупреждений с навигацией по коду в браузере |
Error File |
.err |
IDEs, Vim, Emacs, etc |
Для просмотра отчётов в любом редакторе, поддерживающем формат вывода компилятора |
Error File Verbose |
.err |
IDEs, Vim, Emacs, etc |
Расширение формата Error File с поддержкой отображения дополнительных позиций |
TeamCity |
.txt |
TeamCity |
Для загрузки и просмотра предупреждений в TeamCity |
MISRA Compliance |
.html |
Email Client Browser |
Для проверки кода на соответствие стандартам MISRA |
GitLab |
.json |
GitLab |
Для просмотра предупреждений в формате GitLab Code Quality |
DefectDojo |
.json |
DefectDojo |
Для загрузки и просмотра предупреждений в DefectDojo |
Для преобразования отчёта PVS-Studio в один из перечисленных форматов можно воспользоваться утилитой PlogConverter, поставляемой в дистрибутиве PVS-Studio для Windows. Утилиту PlogConverter можно найти в установочной директории PVS-Studio (по умолчанию это путь C:\Program Files (x86)\PVS-Studio). Кроме этого, исходный код утилиты доступен на GitHub.
Флаг "‑‑help" выведет основную информацию об утилите:
PlogConverter.exe –-help
Для запуска утилиты в командной строке терминала выполнить:
PlogConverter.exe [options] <path to PVS-Studio log>
Параметры утилиты:
Утилита PlogConverter имеет несколько ненулевых кодов возврата, которые не означают проблемы в работе самой утилиты. Если утилита вернула не '0', это ещё не значит, что она отработала с ошибкой.
Описание кодов возврата PlogConverter:
PlogConverter.exe -t Json,Csv -a GA:1,2;OWASP:1 -o D:\Logs -r
D:\projects\projectName -m CWE,OWASP -n PVS-Log PVS-Studio.log
В данном примере происходит следующее:
Для преобразования отчёта PVS-Studio в один из перечисленных форматов можно воспользоваться утилитой plog-converter, поставляемой вместе с пакетами и архивами PVS-Studio С/C++ для Linux и macOS. После установки пакета или из архива PVS-Studio C/C++ вы сможете использовать утилиту plog-converter для конвертации отчётов анализатора в различные форматы (*.xml, *.tasks и т.п.). Кроме этого, исходный код утилиты доступен на GitHub.
Флаг "‑‑help" выведет основную информацию об утилите:
plog-converter --help
Для запуска утилиты в командной строке терминала выполнить:
plog-converter [options] <path to PVS-Studio log>
Все опции могут быть указаны в произвольном порядке.
Доступные опции:
Описание кодов возврата plog-converter:
plog-converter -t json -t csv -a 'GA:1,2;OWASP:1' -o /home/user/Logs
-r /home/user/projects/projectName -m cwe -m owasp -n PVS-Log PVS-Studio.log
В данном примере происходит следующее:
В составе дистрибутива PVS-Studio поставляется утилита BlameNotifier. Она позволяет оповещать разработчиков, заложивших в репозиторий код, на который анализатор выдал предупреждения. Также возможна настройка оповещения обо всех обнаруженных предупреждениях определённого круга лиц. Это может быть полезно менеджерам и руководителям команд.
Более подробно данная утилита описана в соответствующем разделе документации: "Оповещение команд разработчиков (утилита blame-notifier)".
По умолчанию, при генерации диагностических сообщений, PVS-Studio выдаёт абсолютные, полные пути до файлов, в которых анализатор нашёл ошибки. Поэтому при сохранении отчёта именно эти полные пути и попадут в результирующий файл (XML plog файл). В дальнейшем это может вызывать неудобства, например, при необходимости работы с таким файлом отчёта на машине, отличной от той, на которой отчёт был сгенерирован. Ведь пути до файлов с исходным кодом на двух машинах могут и различаться. А это приведёт к невозможности открывать файлы и использовать встроенный механизм навигации по коду в таком лог-файле.
Хотя решить данную проблему можно и ручной правкой путей в XML отчёте, гораздо удобнее сразу получать от анализатора сообщения с путями, записанными относительно какой-нибудь фиксированной директории (например, корневой директории дерева исходников проекта). Такой порядок работы позволяет получить файл отчёта с корректными путями и на любой другой машине путём простой замены "корня", относительно которого раскрываются все пути в файле отчёта PVS-Studio. PVS-Studio может автоматически производить такую генерацию относительных путей и подмену их "корня" с помощью настройки 'SourceTreeRoot' на странице "PVS-Studio -> Options -> Specific Analyzer Settings".
Приведём пример использования данного режима работы. По умолчанию поле настроек 'SourceTreeRoot' пусто, а анализатор всегда генерирует полные пути в выдаваемых диагностических сообщениях. Предположим, что проверяемый проект расположен в директории "C:\MyProjects\Project1". В нашем примере в качестве "корня" дерева исходников проекта можно взять путь "C:\MyProjects\", запишем этот путь в поле 'SourceTreeRoot' и запустим анализ.
По завершению анализа PVS-Studio автоматически заменит заданную нами корневую директорию исходников на специальный маркер, т.е. в сообщении на файл "C:\MyProjects\Project1\main.cpp", путь до данного файла будет передан, как "|?|Project1\main.cpp." Сообщения же на файлы, лежащие вне заданной корневой директории, затронуты не будут. Т.е. сообщение на файл из директории "C:\MyCommonLib\lib1.cpp" будет содержать абсолютный, полный путь до данного файла.
В дальнейшем, при работе с таким фалом отчёта через IDE плагин PVS-Studio, маркер |?| будет автоматически заменяться на значение настройки 'SourceTreeRoot', например, при использовании False Alarm разметки или навигации по сообщениям. Если же будет необходимо работать с данным файлом отчёта на другой машине, достаточно лишь указать в настройках IDE плагина новый путь до корня дерева исходников, например, "C:\Users\User\Projects\", и плагин будет автоматически осуществлять корректное раскрытие полных путей.
Данный режим можно также использовать и в Independent режиме анализатора, при прямой его интеграции в сборочную систему (make, msbuild и т.п.). Это позволяет распределить процесс полного анализа исходников и дальнейшую работу с результатами анализа, что может быть особенно полезно на крупном проекте. Например, проект может быть полностью проверен один раз на сборочном сервере, а с результатами проверки уже может работать сразу несколько разработчиков на своих локальных машинах.
Вы также можете использовать настройку 'UseSolutionDirAsSourceTreeRoot', представленную на той же странице. Данная настройка включает или выключает режим использования пути до папки, содержащей файл решения *.sln, в качестве параметра 'SourceTreeRoot'. При включении этого режима (True) в поле 'SourceTreeRoot' будет отображено значение '<Using solution path>'. При этом фактическое значение параметра 'SourceTreeRoot', сохраненное в файле настроек, не меняется. При выключении настройки 'UseSolutionDirAsSourceTreeRoot' (False) это значение (если ранее оно было задано) будет вновь отображено в поле 'SourceTreeRoot'. Таким образом, настройка 'UseSolutionDirAsSourceTreeRoot' просто меняет механизм генерации пути до файла, позволяя использовать в качестве параметра 'SourceTreeRoot' либо указанное пользователем значение, либо путь до папки, содержащей файл решения.
С версии PVS-Studio 7.27 вы можете указать параметр '//V_SOLUTION_DIR_AS_SOURCE_TREE_ROOT' в файле конфигурации диагностик (.pvsconfig). Данный параметр копирует поведение настройки 'UseSolutionDirAsSourceTreeRoot'. Приоритет этого параметра выше, чем настройки из Settings.xml.
PVS-Studio можно использовать независимо от интегрированной среды разработки Visual Studio. Ядро анализатора представляет собой command-line утилиту, позволяющую проверять C/C++ файлы, компилируемые с помощью Visual C++, GCC или Clang. Поэтому мы разработали отдельное приложение, которое представляет оболочку для command-line утилиты и помогает работать с полученным логом сообщений.
PVS-Studio предоставляет удобное расширение среды Visual Studio, позволяющее "в один клик" проверять vcproj/vcxproj-проекты этой IDE. Однако существуют и другие сборочные системы, которые желательно поддержать. Хотя ядро PVS-Studio не зависит от конкретного формата, используемого той или иной сборочной системой (как например MSBuild, GNU Make, NMake, CMake, ninja и т.п.), для интеграции статического анализа PVS-Studio в систему сборки, отличную от поддерживаемых средой Visual Studio проектов VCBuild/MSBuild, от пользователя потребуется самостоятельно выполнить ряд действий. Перечислим их:
Для решения этих проблем можно воспользоваться инструментом C and C++ Compiler Monitoring UI (Standalone.exe).
Рисунок 1 - Compiler Monitoring UI
Compiler Monitoring UI предоставляет возможности для "бесшовной" проверки кода, независимо от используемых компилятора или сборочной системы, а затем позволяет работать с результатами анализа, предоставляя пользовательский интерфейс, схожий с Visual Studio плагином PVS-Studio. Также Compiler Monitoring UI позволяет работать и с отчётом анализатора, полученным с помощью прямой его интеграции в сборочную систему, при отсутствии у пользователя среды Visual Studio. Рассмотрим далее эти возможности.
Compiler Monitoring UI предоставляет пользовательский интерфейс для системы отслеживания компиляции. Сама система отслеживания (консольная утилита CLMonitor.exe) может использоваться и независимо от Compiler Monitoring UI, например, для интеграции статического анализа в систему автоматизированной сборки. Подробнее об использовании системы отслеживания компилятора можно почитать в этом разделе документации.
Для запуска отслеживания откройте диалог через Tools -> Analyze Your Files... (рисунок 2):
Рисунок 2 - Диалог запуска мониторинга сборки
Нажмите "Start Monitoring". После этого будет запущен CLMonitor.exe, а основное окно среды будет свёрнуто.
Выполните сборку, а по её завершении нажмите на кнопку "Stop Monitoring" в окне в правом нижнем углу экрана (рисунок 3):
Рисунок 3 - Диалог управления мониторингом
Если серверу мониторинга удалось отследить запуски компиляторов, будет запущен статический анализ исходных файлов. По окончании вы получите обычный отчёт о работе PVS-Studio (рисунок 4):
Рисунок 4 - Результаты работы сервера мониторинга и статического анализатора
Результаты работы могут быть сохранены в виде XML файла (файла с расширением plog) для дальнейшего использования с помощью команды меню File -> Save PVS-Studio Log As...
Способ проведения инкрементального анализа аналогичен тому, как проводится анализ всего проекта. Ключевое отличие состоит в необходимости выполнения не полной, а инкрементальной сборки. В таком случае будут отслежены запуски компиляторов для модифицированных файлов, что позволит проверить только их. В остальном процесс анализа полностью идентичен тому, что описан выше, в разделе "Анализ исходных файлов с помощью отслеживания запуска компиляторов".
После того, как был получен отчёт с предупреждениями анализатора, можно сразу начать просматривать сообщения и править код. Также в Compiler Monitoring UI можно загрузить и отчёт, полученный ранее. Для этого нужно воспользоваться пунктом меню 'File|Open PVS-Studio Log...'.
Различные механизмы подавления и фильтрации сообщений идентичны плагину Visual Studio и доступны в окне настроек 'Tools|Options...' (рисунок 5).
Рисунок 5 - Настройки анализа и механизмы фильтрации сообщений
С помощью окна Analyzer Output можно осуществлять навигацию по предупреждениям анализатора, размечать сообщения как ложные срабатывания и добавлять фильтры для сообщений. Интерфейс для работы с предупреждениями анализатора в Compiler Monitoring UI идентичен интерфейсу окна вывода в плагине для Visual Studio. С подробным описанием окна вывода сообщений можно ознакомиться в данном разделе документации.
Если Вы только начали изучать инструмент статического анализа и хотели бы узнать на что он способен, то можете воспользоваться механизмом Best Warnings. Данный механизм покажет вам наиболее важные и достоверные предупреждения.
Чтобы посмотреть наиболее интересные предупреждения с точки зрения анализатора, нажмите на кнопку 'Best', как показано на скриншоте ниже:
После чего в таблице с результатами анализа останутся максимум десять наиболее критичных предупреждений анализатора.
Хотя встроенный редактор Compiler Monitoring UI не имеет такой же мощной и удобной системы навигации и автодополнения, как Microsoft IntelliSense в среде Visual Studio или им подобным, Compiler Monitoring UI предоставляет несколько механизмов поиска, которые могут упростить работу с результатами анализа.
Помимо обычного поиска в открытом файле (Ctrl + F), Compiler Monitoring UI имеет также диалог Code Search для текстового поиска в открытых файлах и директориях файловой системы. Этот диалог доступен через пункт меню 'Edit|Find & Replace|Search in Source Files...' (Рисунок 6):
Рисунок 6 - Диалог поиска Compiler Monitoring UI
Диалог поддерживает поиск в текущем файле, всех открытых файлах, либо в произвольной директории файловой системы. Поиск можно в любой момент остановить, нажав Cancel в появившемся модальном окне. Результаты, при появлении первого совпадения, сразу начнут отображаться в дочернем окне Code Search Results (рисунок 7):
Рисунок 7 - Результаты текстового поиска в исходных файлах проекта
Конечно, при необходимости найти места объявления и\или использования какого-либо идентификатора или макроса, обычный текстовый поиск может оказаться чересчур громоздким или долгим. В этом случае вы можете воспользоваться механизмом поиска по зависимостям и навигации по #include макросам.
Поиск по зависимостям в файлах позволяет искать символ\макрос именно в тех файлах, которые непосредственно участвовали при компиляции, а точнее, при последующем препроцессировании файлов, при их проверке анализатором. Для запуска поиска по зависимостям, откройте контекстное меню на символе, использование которого вы хотите посмотреть (рисунок 8):
Рисунок 8 - Поиск символа по зависимостям
Результаты поиска, по аналогии с текстовым поиском, начнут выдаваться в отдельном дочернем окне Find Symbol Results. Поиск можно в любой момент остановить, нажав на Cancel в status bar'е главного окна Compiler Monitoring UI рядом с индикатором прогресса.
Навигация по #include макросам позволяет открывать в редакторе кода Compiler Monitoring UI файл, добавленный в текущий файл через такой макрос. Для открытия include'а также нужно воспользоваться контекстным меню редактора (рисунок 9):
Рисунок 9 - Навигация по include'ам
Стоит помнить, что информация о зависимостях доступна не для любого исходного файла, открытого в Compiler Monitoring UI. В ситуации, когда база зависимостей недоступна для Compiler Monitoring UI, описанные выше пункты контекстного меню доступны также не будут.
База зависимостей создаётся только тогда, когда анализ запущен непосредственно из самого Compiler Monitoring UI. При открытии произвольного C/C++ исходника такой информации у Compiler Monitoring UI не будет. Заметим, что при сохранении результата работы анализатора в виде plog файла, в случае если этот результат был получен в самом приложении Compiler Monitoring UI, рядом с plog файлом будет создан также соответствующий ему специальный dpn файл, содержащий зависимости проверенных файлов. Наличие dpn файла рядом с plog файлом отчёта сделает возможным поиск по зависимостям, если такой plog файл будет открыт в Compiler Monitoring UI.
Файл конфигурации служит для отображения и фильтрации сообщений анализатора. Также в нём можно задать дополнительные настройки анализа. Использование данных файлов возможно только для проектов, написанных на C, C++ или C#.
Файлы конфигурации поддерживаются плагинами для следующих IDE:
Утилиты, поддерживающие файлы конфигурации:
Для использования файла конфигурации в Visual Studio необходимо добавить его на уровне проекта или решения. Для этого выделите интересующий проект или решение в окне Solution Explorer среды Visual Studio. Выберите пункт контекстного меню 'Add New Item...'. В появившемся окне выберите тип файла 'PVS-Studio Filters File'.
Если шаблона нет, то вы можете просто добавить в проект или решение обычный текстовый файл с расширением ".pvsconfig".
Для каждого проекта/решения можно добавить несколько файлов конфигурации.
Файлы конфигурации, добавленные на уровне проекта, действуют на все файлы данного проекта. Файлы конфигурации, добавленные на уровне решения, будут действовать на все файлы всех проектов в этом решении.
Специального шаблона для добавления файла конфигурации для CLion нет.
Добавить файл конфигурации для CLion можно только на уровне проекта. Чтобы использовать его в CLion, добавьте в папку .PVS-Studio новый файл с расширением .pvsconfig через контекстное меню New > File.
Специального шаблона для добавления файла конфигурации для Rider нет.
Добавить файл конфигурации для Rider можно только на уровне проекта. Чтобы использовать файл конфигурации диагностик в Rider, добавьте в проект новый файл с расширением .pvsconfig через Solution Explorer.
При анализе через PVS-Studio_Cmd.exe или pvs-studio-dotnet автоматически используются файлы конфигурации из анализируемого проекта или решения. Также можно передать путь к дополнительному файлу .pvsconfig с помощью параметра ‑‑rulesConfig (-C):
PVS-Studio_Cmd.exe -t ProjName.sln -C \path\to\.pvsconfig
pvs-studio-dotnet -t ProjName.sln -C /path/to/.pvsconfig
В этом случае при анализе учитываются настройки и из файлов в проекте/решении, и из файла, переданного в качестве аргумента.
Путь к файлу конфигурации необходимо передать в качестве аргумента командной строки (параметр -c):
CLMonitor.exe analyzeFromDump -d /path/to/compileDump.gz -c /path/to/.pvsconfig
Если вы используете утилиту CompilerCommandsAnalyzer.exe, то можете передать путь до .pvsconfig-файла через параметр -R:
CompilerCommandsAnalyzer.exe analyze ... -R /path/to/.pvsconfig
В Standalone.exe вы можете указать путь к файлу при запуске мониторинга.
Глобальный файл конфигурации диагностик используется при проверке всех проектов. Таких файлов конфигурации .pvsconfig может быть несколько, и все они будут использованы инструментами PVS-Studio.
Для добавления глобального файла конфигурации создайте файл с расширением pvsconfig в папке:
Задание настроек в файлах конфигурации производится при помощи специальных директив, начинающихся с символов '//'. Каждая директива пишется с новой строки.
Пример:
//-V::122
//-V::123
Также существует возможность добавлять комментарии. Для этого необходимо написать символ '#' в начало строки.
Пример:
# I am a comment
Для полного отключения определённой диагностики используется запись:
//-V::number
'number' – номер диагностики, которую нужно выключить (например, 3022).
Пример использования:
//-V::3022
В данном случае будут игнорироваться срабатывания диагностики V3022.
Для отключения нескольких диагностик перечислите номера через запятую:
//-V::number1,number2,...,numberN
Пример:
//-V::3022,3080
При использовании этой директивы будут полностью отключены диагностики V3022 и V3080.
Для отключения диагностик некоторой категории используются следующие директивы:
//-V::GA
//-V::X64
//-V::OP
//-V::CS
//-V::MISRA
//-V::OWASP
Пояснение для каждой из категорий:
Можно комбинировать фильтры категорий, перечисляя их через запятую.
Пример комбинации:
//-V::GA,MISRA
Для отключения всех диагностик C++ или C# анализатора используются директивы:
//-V::C++
//-V::C#
Если требуется исключить срабатывания определённого уровня, используйте запись вида:
//-V::number1,number2,...,numberN:level
Цифре 1 соответствуют срабатывания уровня 'High', цифре 2 – 'Medium', цифре 3 – 'Low'.
Можно исключать предупреждения сразу нескольких уровней. Для этого нужно написать уровни через запятую.
Пример:
//-V::3022,5623:1,3
Эта запись позволит исключить срабатывания диагностик V3022 и V5623 уровня 'High' и 'Low'.
Анализатор поддерживает возможность исключения предупреждений по номеру диагностики и подстроке, содержащейся в сообщении.
Запись для подавления:
//-V::number::{substring}
При использовании такого шаблона будут игнорироваться срабатывания диагностик с номером 'number', сообщения которых содержат подстроку 'substring'.
Пример подавления по подстроке:
//-V::3022::{always true}
В данном случае будут отключены срабатывания V3022, в сообщении которых есть подстрока 'always true'.
Также можно добавить фильтрацию по уровню и подстроке. Такая запись будет иметь вид:
//-V::number1,number2,...,numberN:level:{substring}
Цифре 1 соответствуют срабатывания уровня 'High', цифре 2 – 'Medium', цифре 3 – 'Low'.
Можно исключать предупреждения диагностик сразу нескольких уровней. Для этого нужно написать уровни через запятую.
Пример:
//-V::3022,5623:1,3:{always true}
Будут исключены срабатывания уровня 'High' и 'Low' диагностик V3022, V5623, в сообщении которых есть подстрока 'always true'.
Для исключения предупреждений некоторых категорий по уровням используется запись вида:
//-V::category1,category2,...,categoryN:level
Можно комбинировать фильтры категорий и уровней, перечисляя их через запятую.
Пример комбинации:
//-V::GA,MISRA:1,3
Будут исключены срабатывания уровня 'High' и 'Low' диагностик, которые относятся к категориям 'GA' и 'MISRA'.
Примечание: Данная настройка доступна только для C, С++ и C# проектов.
Для включения определённой диагностики используется запись:
//+V::number
'number' – номер диагностики, которую нужно выключить (например, 3022).
Пример использования:
//+V::3022
В данном случае будут включены срабатывания диагностики V3022.
Для включения нескольких диагностик перечислите номера через запятую:
//+V::number1,number2,...,numberN
Пример:
//+V::3022,3080
При использовании этой директивы будут полностью включены диагностики V3022 и V3080.
Примечание: Данная настройка доступна только для C, С++ и C# проектов.
Для включения диагностик некоторой категории используются следующие директивы:
//+V::GA
//+V::X64
//+V::OP
//+V::CS
//+V::MISRA
//+V::OWASP
Можно комбинировать фильтры категорий, перечисляя их через запятую.
Пример комбинации:
//+V::GA,MISRA
Добавление FA-метки для предупреждений на строки с некоторым фрагментом производится с помощью следующей директивы:
//-V:substring:number
Примечание 1. Искомая подстрока ('substring') не должна содержать пробелов.
Примечание 2. Сообщения, отфильтрованные данным способом, не будут удалены из отчёта. Они будут отмечены как False Alarm (FA).
Пример использования:
public string GetNull()
{
return null;
}
public void Foo()
{
string nullStr = GetNull();
Console.WriteLine(nullStr.Length);
}
На данный код анализатор выдаст предупреждение: "V3080 Possible null dereference. Consider inspecting 'nullStr'.".
Для добавления FA-метки для срабатываний на такой код используйте в .pvsconfig следующую запись:
//-V:Console:3080
Такая директива добавит отметку False Alarm на все предупреждения V3080, указывающие на строку кода, в которой есть 'Console'.
Аналогичным образом можно добавлять отметку False Alarm на срабатывания сразу нескольких диагностик. Для этого перечислите их номера через запятую:
//-V:substring:number1,number2,...,number
Пример:
//-V:str:3080,3022,3175
Сообщения диагностик V3080, V3082, V3175 будут помечены как False Alarm, если в строке, на которую указывает анализатор, есть подстрока 'str'.
С PVS-Studio версии 7.28 появилась возможность ставить дополнительный хэш-код к метке False Alarm. При изменении строки с этим хэш-кодом предупреждения, выданные на эту строку, не будут отмечены как ложные срабатывания, так как хэш-код изменённой строки отличается от хэш кода метки.
Эта настройка помогает распознавать ситуации, когда строка с меткой False Alarm изменяется.
Для включения этой функции добавьте в файл конфигурации следующий флаг:
//V_ENABLE_FALSE_ALARMS_WITH_HASH
В коде метка False Alarm c хэш-кодом выглядит следующим образом:
//-V817 //-VH"3652460326"
С версии PVS-Studio 7.30 появилась возможность для подавления только тех сообщений, к False Alarm метке которых поставлен дополнительный хэш-код:
//V_HASH_ONLY ENABLE
//V_HASH_ONLY ENABLE_VERBOSE
В случае применения данной настройки те строчки, которые имеют метку False Alarm, но не имеют хэш-кода, не будут подавлены.
Настройка ENABLE приведёт к попаданию одного на весь проект сообщения V018 в отчёт. В случае применения настройки ENABLE_VERBOSE такое предупреждение будет выдаваться на каждую строчку кода, в которой присутствует метка False Alarm без хэш-кода.
Выключение настройки выполняется следующим образом:
//V_HASH_ONLY DISABLE
Данная ситуация может возникнуть в случае, если применение данной настройки необходимо только на определённой части кода.
Для исключения из анализа файла или группы файлов используйте шаблон:
//V_EXCLUDE_PATH fileMask
'fileMask' – маска файла.
Пример использования некоторых масок:
//V_EXCLUDE_PATH C:\TheBestProject\thirdParty
//V_EXCLUDE_PATH *\UE4\Engine\*
//V_EXCLUDE_PATH *.autogen.cs
Начиная с версии 7.34 PVS-Studio можно использовать шаблон //V_ANALYSIS_PATHS с режимом skip-analysis.
Например:
//V_ANALYSIS_PATHS skip-analysis=C:\TheBestProject\thirdParty
//V_ANALYSIS_PATHS skip-analysis=*\UE4\Engine\*
//V_ANALYSIS_PATHS skip-analysis=*.autogen.cs
С синтаксисом формирования масок можно ознакомится в документации.
Начиная с версии 7.32, утилита PVS-Studio_Cmd.exe и плагин для Visual Studio поддерживают исключение из анализа проектов по шаблону:
//V_EXCLUDE_PROJECT projMask
'projMask' — маска файла проекта.
Пример использования некоторых масок:
//V_EXCLUDE_PROJECT C:\TheBestProject\thirdParty\3rdparty.vcxproj
//V_EXCLUDE_PROJECT *\TOCSharp.csproj
//V_EXCLUDE_PROJECT *\elsewhere\*.*proj
Синтаксис формирования масок совпадает с синтаксисом, используемым для исключения файлов из анализа. Исключить из проверки можно только .vcxproj и .csproj проекты.
Также вы можете исключить проект из анализа, указав аналогичный путь для флага //V_EXCLUDE_PATH.
Примечание: Данная настройка доступна только для C и С++ проектов.
Начиная с версии 7.34 PVS-Studio поддерживает возможность игнорировать настройки, которые находятся в исходных файлах и файлах конфигурации диагностик '.pvsconfig'.
Для этого можно использовать флаг //V_ANALYSIS_PATHS с режимом skip-settings
Например:
//V_ANALYSIS_PATHS skip-settings=*\path\to\folder\*
//V_ANALYSIS_PATHS skip-settings=*\path\*\custom_pvsconfig
//V_ANALYSIS_PATHS skip-settings=*custom_settings.h
Если необходимо исключить файлы из анализа и одновременно игнорировать настройки для этих файлов, можно использовать флаг //V_ANALYSIS_PATHS с режимом skip.
Например:
//V_ANALYSIS_PATHS skip=*\path\to\source_or_pvsconfig
С синтаксисом формирования масок можно ознакомится в документации.
Перед запуском анализа 'PVS-Studio_Cmd' формирует конфигурацию диагностических правил из:
Может возникнуть ситуация, когда глобальная конфигурация не должна применяться при анализе каких-либо проектов или решений. Для её отключения добавьте в соответствующий файл конфигурации следующий флаг:
//IGNORE_GLOBAL_PVSCONFIG
При запуске анализа через интерфейс плагинов (Visual Studio, Rider и CLion) или в C and C++ Compiler Monitoring UI (Standalone.exe) имеется возможность указания timeout-а по истечению которого анализ файла будет прерван. При превышении timeout-а анализа в результаты анализа будет добавлено предупреждение V006 с информацией о том, на каком файле был превышен timeout.
Настройки timeout-а анализа файлов можно указать и в .pvsconfig. Например, этой строчкой указывается timeout в 10 минут (600 секунд):
//V_ANALYSIS_TIMEOUT 600
Если в .pvsconfig указана строка с timeout-ом равным 0, то файлы будут анализироваться без ограничения по времени.
Благодаря настройке timeout-ов через .pvsconfig файлы разных уровней, можно ограничить время анализа файлов в определенных проектах, solution-ах или во всей системе.:
Предупреждения анализатора имеют три уровня достоверности: High, Medium, Low. В зависимости от используемых в коде конструкций анализатор оценивает достоверность предупреждений и присваивает им соответствующий уровень в отчёте.
В некоторых проектах поиск определённых типов ошибок может быть очень важен, независимо от степени достоверности предупреждения. Бывает и обратная ситуация, когда сообщения малополезны, но совсем их отключать не хочется. В таких случаях для диагностик можно вручную задать уровень High/Medium/Low. Для этого следует использовать следующие директивы:
Для изменения уровня используйте директиву следующего вида:
//V_LEVEL_1::number
'number' – номер диагностики.
Например, чтобы изменить уровень предупреждений для диагностики V3176 на третий, используйте запись:
//V_LEVEL_3::3176
Для изменения подстроки в сообщении анализатора используйте следующий синтаксис:
//+Vnnn:RENAME:{originalString:replacementString}, ...
Разберём работу директивы на примере. Диагностика V624, встречая в коде число 3.1415, предлагает заменить его на 'M_PI' из библиотеки '<math.h>'. Но в проекте используется специальная математическая библиотека, и нужно использовать математические константы именно из неё. Для корректной работы следует добавить директиву в файл конфигурации.
Эта директива будет иметь следующий вид:
//+V624:RENAME:{M_PI:OUR_PI},{<math.h>:"math/MMath.h"}
Теперь анализатор сообщит, что нужно использовать константу 'OUR_PI' из заголовочного файла 'math/MMath.h'.
Существует возможность добавить строку к сообщению.
Директива, позволяющая сделать это, имеет следующий вид:
//+Vnnn:ADD:{message}
Разберём пример. Для этого рассмотрим сообщение диагностики V2003: "Explicit conversion from 'float/double' type to signed integer type.".
Чтобы добавить дополнительную информацию в это сообщение, нужно использовать директиву следующего вида:
//+V2003:ADD:{ Consider using boost::numeric_cast instead.}
Теперь анализатор будет выдавать модифицированное сообщение: "Explicit conversion from 'float/double' type to signed integer type. Consider using boost::numeric_cast instead.".
При запуске анализа через интерфейс плагина Visual Studio или в C and C++ Compiler Monitoring UI (Standalone.exe) имеется возможность отключить синхронизацию suppress файлов с помощью настройки Specific Analyzer Settings\DisableSynchronizationOfSuppressFiles.
Отключить синхронизацию также можно через .pvsconfig файл уровня решения. Для этого необходимо добавить в соответствующий конфигурационный файл следующий флаг:
//DISABLE_SUPPRESS_FILE_SYNC
Для включения синхронизации через .pvsconfig независимо от значения настройки DisableSynchronizationOfSuppressFiles необходимо использовать флаг:
//ENFORCE_SUPPRESS_FILE_SYNC
Этот флаг применим только в .pvsconfig уровня решения.
Начиная с версии 7.24 утилита PVS-Studio_Cmd.exe и плагин для Visual Studio поддерживают возможность указать версию ядра PVS-Studio для анализа C++ проектов, если на компьютере установлено несколько версий PVS-Studio.
Для того, чтобы PVS-Studio_Cmd.exe запускал анализ на нужной версии ядра PVS-Studio необходимо в файл .pvsconfig уровня решения добавить флаг //PVS_VERSION::Major.Minor, где
Major - мажорное число версии, а Minor - минорное число.
Например:
//PVS_VERSION::7.24
PVS-Studio_Cmd.exe вычисляет путь до ядра используя информацию из системного реестра, которую пишет инсталлятор при установки PVS-Studio.
В случае установки новой версии PVS-Studio в уже существующую директорию, содержащую другую версию PVS-Studio, номер версии в реестре для этой директории будет обновлён.
Последняя установка PVS-Studio, считается установкой по умолчанию. А значит, если последней была установлена версия PVS-Studio 7.22, то такой же версии будут все плагины и PVS-Studio_Cmd.exe. Следовательно, вы не сможете воспользоваться механизмом выбора версий ядра PVS-Studio. Поэтому, если вы хотите использовать старые версии PVS-Studio (7.23 и ниже), то вам нужно в начале установить их и только потом поставить последнюю версию PVS-Studio 7.24 или выше.
Для всех версий ниже 7.24 необходимо в реестре прописать соотношение версии и пути до каталога установки этой версии, чтобы PVS-Studio_Cmd.exe смог найти путь до ядра PVS-Studio. Информация записывается в раздел 'Computer\HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\ProgramVerificationSystems\PVS-Studio\Versions'.
Начина с версии 7.25 утилита PVS-Studio_Cmd.exe и плагин для Visual Studio поддерживают возможность явно задать приоритет файлов конфигурации одного уровня. Для этого необходимо использовать флаг //CONFIG_PRIORITY::number, где number – номер приоритета.
Например:
//CONFIG_PRIORITY::1
Чем меньше номер, тем приоритетней файл конфигурации. Файлы, в которых нет данного флага, имеют минимальный приоритет. Файлы имеющий одинаковый приоритет применяются в алфавитном порядке. Например, среди файлов Filter1.pvsconfig, Filter2.pvsconfig, Filter3.pvsconfig настройки из Filter3.pvsconfig будут приоритетными.
Флаг //CONFIG_PRIORITY влияет только на файлы конфигурации одного уровня. В порядке возрастания приоритета файлы настроек применяются так:
Чтобы PVS-Studio перед анализом выполнил команды из CustomBuild task, в файл .pvsconfig нужно добавить следующую директиву:
//EXECUTE_CUSTOM_BUILD_COMMANDS
Эта директива применима только для .pvsconfig файлов, передаваемых через командную строку, а также расположенных на глобальном уровне или уровне решения.
Рассмотрим случай, в котором директива может пригодиться.
Некоторые Visual C++ проекты при сборке могут генерировать исходный код с помощью команд, записанных в CustomBuild task. Запуск анализа без генерации этих файлов может привести к ошибкам. Если нужно только сгенерировать файлы, то выполнять полную сборку смысла нет (это может быть долго).
В таком случае будет полезно указать PVS-Studio соответствующую директиву, чтобы анализатор сначала выполнил команды генерации файлов, а затем провёл анализ.
Иногда анализатор может выдавать сообщение об ошибке парсинга на полностью компилирующийся проект. Эти ошибки могут быть не критичны для качества анализа. В таком случае их можно подавить.
Ошибка парсинга имеет код:
Подавление V051 (C# анализатор)
C# анализатор выдаёт V051 при наличии хотя бы одной ошибки компиляции. Увидеть все ошибки можно запустив command line версию анализатора с флагом '‑‑logCompilerErrors'. Синтаксис для подавления этих ошибок выглядит следующим образом:
//V_EXCLUDE_PARSING_ERROR:V051:{"ProjectName": "MyProject", "ErrorCode": "CS0012", "Message": "Some message"}
В данном случае для проекта (.csproj) MyProject будет подавлена ошибка компиляции с кодом CS0012 и сообщением "Some message".
Также не обязательно комбинировать информацию для подавления:
При указании сообщения вы можете воспользоваться масками. Например:
//V_EXCLUDE_PARSING_ERROR:V051:{Message: "Some*"}
Примечание: на данный момент подавление ошибок парсинга доступно только для V051 (C# анализатор).
В глобальном файле конфигурации Settings.xml есть ряд опций, которые влияют на результат анализа. Например, настройки выключения диагностических групп.
Вы можете использовать флаг '//V_IGNORE_GLOBAL_SETTINGS ON' для того, чтобы настройки из Settings.xml не учитывались при анализе. В этом случае включаются все диагностические группы и не применяются фильтры путей.
Для гибкой настройки анализа используйте файлы конфигурации (.pvsconfig).
Эта опция доступна только в файле конфигурации уровня решения и влияет на работу только PVS-Studio_Cmd.exe и плагинов для Visual Studio.
Вы можете использовать флаг '//V_SOLUTION_DIR_AS_SOURCE_TREE_ROOT' для того, чтобы включить использование каталога решения в качестве значения SourceTreeRoot.
С настройкой SourceTreeRoot можно ознакомиться в отдельной документации.
Параметр является более приоритетным, чем UseSolutionDirAsSourceTreeRoot из файла настроек Settings.xml.
Эта опция доступна только в файле конфигурации уровня решения и влияет на работу только PVS-Studio_Cmd.exe и плагинов для Visual Studio.
Начиная с PVS-Studio версии 7.34 вы можете использовать флаг //V_SOLUTION_DIR_AS_DEPENDENCY_CACHE_SOURCE_TREE_ROOT для того, чтобы включить использование каталога решения в качестве корневой части пути, который будет использован для построения относительных путей файлов в кэше зависимостей компиляции.
Этот флаг следует использовать в режиме проверки списка файлов и в режиме проверки модифицированных файлов. Данная настройка позволяет получить файлы кэшей зависимостей компиляции, которые затем можно использовать на машинах с отличающимся расположением проверяемых исходных файлов.
Эта настройка похожа на //V_SOLUTION_DIR_AS_SOURCE_TREE_ROOT, только относительные пути для файлов будут строиться в файлах кэшей зависимостей компиляции, а не в отчёте анализатора.
Эта опция доступна только в файле конфигурации уровня решения и влияет на работу только PVS-Studio_Cmd.exe.
С версии PVS-Studio 7.27 подавленные сообщения сохраняются в сортированном виде. Подробнее об этом можно узнать в документации.
Если вам требуется сохранить старое поведение и отключить сортировку, вы можете указать параметр //V_DISABLE_SUPPRESS_FILE_SORTING.
Существует возможность указания специфичных правил для определённой версии PVS-Studio.
Синтаксис:
//V_SECTION_BEGIN
//V_WHEN_VERSION: <CONDITION_SEQUENCE>
....
//V_SECTION_END
Каждая секция содержит три обязательных элемента:
Синтаксис условий:
<CONDITION_SEQUENCE> ::= <CONDITION> | <CONDITION_SEQUENCE> "|" <CONDTION>
<CONDITION> ::= <SINGLE_VERSION_COMPARISON> | <RANGE_VERSIONS_COMPARISON>
<SINGLE_VERSION_COMPARISON> ::= <OP> <VERSION>
<RANGE_VERSIONS_COMPARISON> ::= "IN" <VERSION> "," <VERSION>
<OP> ::= "EQ" | "NE" | "LT" | "LE" | "GT" | "GE"
<VERSION> ::= <NUMBER> [ "." <NUMBER> ]
Условия в V_WHEN_VERSION могут быть скомбинированы с помощью символа '|' (аналог оператора ИЛИ). Каждое подвыражение вычисляется по отдельности. Если хотя бы одно из них истинно, то секция со всеми директивами внутри неё применяется. В противном случае — отбрасывается.
Если необходимо указать не точную версию, а диапазон, то можно воспользоваться оператором IN. Значения указываются через запятую включительно. Например, так можно указать все версии с 7.20 до 7.25 (включительно):
....
//V_WHEN_VERSION: in 7.20,7.25
....
Поддерживаемые операторы в условиях, их псевдонимы и описание:
# |
Оператор |
Alias |
Описание |
---|---|---|---|
1 |
EQ |
== |
Равно |
2 |
NE |
!= |
Не равно |
3 |
LT |
< |
Меньше |
4 |
LE |
<= |
Меньше или равно |
5 |
GT |
> |
Больше |
6 |
GE |
>= |
Больше или равно |
7 |
IN |
отсутствует |
Диапазон значений |
Регистр текстовых операторов не имеет значения. Такая запись условия также будет корректной:
....
//V_WHEN_VERSION: == 7.17 | In 7.20,7.25 | GT 8
....
Ограничения:
Примечания:
Пример секции:
//V_SECTION_BEGIN
//V_WHEN_VERSION: eq 7.30 | in 7.32,7.35 | gt 8
//+V::860
//V_ASSERT_CONTRACT
//-V::1100
//V_SECTION_END
При проверке проектов с большим количеством сторонних библиотек бывает необходимо отсеять лишний шум и сосредоточиться на потенциальных проблемах в своём коде. Для таких целей в инструментах PVS-Studio предусмотрен механизм исключения файлов из анализа. Благодаря механизму исключений вы можете не только убрать ненужные предупреждения, но и значительно увеличить скорость анализа вашего проекта.
Для Visual Studio откройте настройки плагина Extensions -> PVS-Studio -> Options -> Don't Check Files.
Если Вы используете утилиту C and C++ Compiler Monitoring UI, то откройте Tools -> Option... -> Don't Check Files.
В появившимся окне вы можете указать файлы и каталоги, которые должны быть исключены из анализа. Вы также можете использовать wildcard-маски.
Стоит отметить, что не все маски в плагине для Visual Studio можно применить для фильтрации существующего отчёта. Если маска неприменима для фильтрации, вы получите сообщение о необходимости перезапуска анализа для её применения. Маски, в которых используется символ '*' в начале и/или в конце, сразу применяются для фильтрации предупреждений в таблице плагина.
Подробнее об исключении файлов через настройки плагина для Visual Studio можно прочесть тут.
Вы также можете исключить файлы из анализа через контекстное меню в отчёте анализатора. Для этого щёлкните правой кнопкой мыши по срабатыванию и выберите пункт 'Don't check files and hide all messages from...', в раскрывшемся меню вы сможете выбрать уровень вложенности каталогов, с которого следует исключить файлы.
Обратите внимание, что информация об исключаемых каталогах и файлах записывается в глобальный файл настроек Settings.xml и может быть использована другими инструментами PVS-Studio, если им не передан путь для файла настроек.
Чтобы исключить файлы или каталоги из анализа, перейдите в настройки плагина PVS-Studio (Tools -> PVS-Studio -> Settings) и выберите раздел Excludes.
В этом разделе вы можете управлять масками имён файлов и путей, которые будут исключены из анализа.
Вы также можете исключить из анализа файл или каталог непосредственно через контекстное меню отчёта PVS-Studio. Для этого нажмите правой кнопкой мыши по сообщению и выберите пункт 'Exclude From Analysis', в раскрывшемся меню вы сможете выбрать уровень вложенности каталогов, с которого следует исключить файлы из анализа.
Обратите внимание, что информация об исключаемых каталогах и файлах записывается в глобальный файл настроек Settings.xml и может быть использована другими инструментами PVS-Studio, если им не передан путь для альтернативного файла настроек.
Консольные инструменты могут использовать как глобальный файл настроек Settings.xml, так и специфичный, переданный в качестве аргумента командной строки. Использование специфичного файла может быть полезно, если у вас есть специальные настройки анализа для проекта, которые не должны повлиять на результаты анализа других проектов. Например, список исключаемых каталогов.
Чтобы создать альтернативный файл настроек, скопируйте файл Settings.xml из каталога '%APPDATA%/PVS-Studio/' (Windows) или '~/.config/PVS-Studio' (Linux) в любое место (например, в каталог проверяемого проекта). Затем добавьте исключаемые файлы и каталоги в узел ApplicationSettings/PathMasks (если это каталог) или в ApplicationSettings/FileMasks (если это файл).
Например, так:
<ApplicationSettings ...>
...
<PathMasks>
...
<string>\EU*\Engine\Source</string>
</PathMasks>
...
</ApplicationSettings>
Теперь, для того чтобы передать собственный файл настроек в PVS-Studio_Cmd.exe или pvs-studio-dotnet (Linux и macOS), используйте параметр ‑‑settings (-s):
PVS-Studio_Cmd.exe -t ProjName.sln -s /path/to/NonDefaultSettings.xml
Для передачи параметра в CLMonitor.exe используйте флаг ‑‑settings (-t) в любом режиме запуска:
CLMonitor.exe monitor -t /path/to/NonDefaultSettings.xml
Вы можете создать файл .pvsconfig (файл конфигурации диагностик анализатора), в котором с помощью специальных комментариев можно описать файлы и каталоги, исключаемые из анализа. Подробнее про файл .pvsconfig можно узнать тут.
Для исключения файла или каталога файлов из анализа вам достаточно указать до него путь или маску пути, используя специальный комментарий //V_EXCLUDE_PATH.
Например:
//V_EXCLUDE_PATH C:\TheBestProject\ThirdParty
//V_EXCLUDE_PATH *\UE*\Engine\Source\*
//V_EXCLUDE_PATH *.autogen.cs
Начиная с версии 7.34 PVS-Studio можно использовать комментарий //V_ANALYSIS_PATHS с режимом skip-analysis.
Например:
//V_ANALYSIS_PATHS skip-analysis=C:\TheBestProject\ThirdParty
//V_ANALYSIS_PATHS skip-analysis=*\UE*\Engine\Source\*
//V_ANALYSIS_PATHS skip-analysis=*.autogen.cs
Теперь вы можете использовать файл .pvsconfig с утилитами PVS-Studio:
Для CLMonitor.exe через флаг -с (‑‑pvsconfig):
CLMonitor.exe analyze ... -c /path/to/.pvsconfig
Для PVS-Studio_Cmd.exe и pvs-studio-dotnet через флаг -С (‑‑rulesConfig):
PVS-Studio_Cmd.exe -t target.sln -o PVS-Studio.log -C /path/to/.pvsconfig
pvs-studio-dotnet -t target.csproj -o PVS-Studio.log -C /path/to/.pvsconfig
Для CompilerCommandsAnalyzer.exe (Windows) и pvs-studio-analyzer (Linux, macOS) через флаг -R (‑‑rules-config):
CompilerCommandsAnalyzer analyze --cfg /path/to/PVS-Studio.cfg \
-R /path/to/.pvsconfig
Если вы используете утилиту C and C++ Compiler Monitoring UI, то путь до файла .pvsconfig можно передать через интерфейс запуска мониторинга:
Плагины PVS-Studio (для Visual Studio, Rider) умеют автоматически определять файл .pvsconfig, если он добавлен в Solution или проект и имеет расширение .pvsconfig. Файлы конфигурации, добавленные в Solution, имеют глобальную область видимости и распространяются на все проекты этого Solution'a. Файлы .pvsconfig, добавленные в проект, применяются только для проекта, в который они добавлены.
Если для проверки C и C++ проектов используется кроссплатформенная утилита pvs-studio-analyzer(Linux, macOS) / CompilerCommandsAnalyzer.exe (Windows), то исключить файлы из анализа возможно следующими способами.
Позволяет исключить файлы из анализа, передав пути до них в качестве аргументов запуска pvs-studio-analyzer/CompilerCommandsAnalyzer.exe через флаг -e (‑‑exclude-path):
pvs-studio-analyzer analyze ... -e /third-party/ \
-e /test/ \
-e /path/to*/exclude-path
Или через флаг ‑‑analysis-paths с режимом skip-analysis:
pvs-studio-analyzer analyze ... \
--analysis-paths skip-analysis=/third-party/ \
--analysis-paths skip-analysis=/test/ \
--analysis-paths skip-analysis=/path/to*/exclude-path
При указании пути вы также можете использовать шаблоны командных оболочек (glob).
Создайте текстовый файл (например, MyProject.cfg). Поместите в него список исключаемых каталогов через параметр exclude-path.
Пример:
exclude-path=/third-party/
exclude-path=*/test/*
exclude-path=*/lib-*/*
Или используя параметр analysis-paths с режимом skip-analysis:
analysis-paths=skip-analysis=/third-party/
analysis-paths=skip-analysis=*/test/*
analysis-paths=skip-analysis=*/lib-*/*
Затем запустите анализ, передав путь до файла конфигурации через флаг ‑‑cfg:
pvs-studio-analyzer analyze ... --cfg ./MyProject.cfg
Вы можете вынести в файл конфигурации и другие параметры запуска. Подробнее об этом написано тут.
Смотреть отчёты PVS-Studio в Visual Studio Code можно с помощью специального расширения PVS-Studio. Ниже описано, как установить расширение, запустить анализ проекта или открыть уже существующий отчёт PVS-Studio и работать с ним.
Обратите внимание, что предварительно вам потребуется скачать и установить анализатор.
Чтобы установить расширение PVS-Studio, откройте Visual Studio Code и перейдите на вкладку 'Extensions', затем введите в поле поиска 'PVS-Studio' и кликните по кнопке 'Install':
При установке анализатора на Windows вы можете выбрать пункт 'Integration with Visual Studio Code' и тогда расширение добавится в Visual Studio Code автоматически.
Также на Windows можно выполнить установку расширения, используя файл pvs-studio-vscode-*.vsix, расположенный в каталоге PVS-Studio (по умолчанию "%PROGRAMFILES(x86)%\PVS-Studio").
Для установки плагина из .vsix-файла на вкладке 'Extensions' в правом верхнем углу необходимо нажать на три точки и в появившемся меню выбрать пункт 'Install from VSIX...':
В открывшемся окне выберите .vsix файл плагина PVS-Studio. После установки плагина перезапустите Visual Studio Code.
Также установить расширение или скачать .vsix файл можно cо страницы PVS-Studio в Visual Studio Marketplace.
После установки расширения окно PVS-Studio появится в качестве одной из вкладок на нижней панели Visual Studio Code. В случае, если окно PVS-Studio было скрыто, вы можете снова отобразить его с помощью команды 'PVS-Studio: Show window' в палитре команд (Ctrl + Shift + P).
Запустить анализ можно несколькими способами:
1. С помощью кнопки 'Analyze project' в окне PVS-Studio:
В этом случае будет запущен анализ всего решения.
2. Через контекстное меню редактора кода:
В этом случае будет запущен анализ только одного файла. Обратите внимание, что отсутствие данного пункта меню означает, что анализ текущего файла не поддерживается.
3. Через контекстное меню проводника.
В этом случае будут проанализированы все поддерживаемые файлы, которые были выделены. Кроме того, если среди выделенных элементов имеется папка, все поддерживаемые файлы, содержащиеся в ней и в ее поддиректориях, будут проанализированы.
4. Через меню группы вкладок редактора:
В этом случае будут проанализированы все поддерживаемые файлы, открытые в текущей группе вкладок.
5. С помощью команд 'Run regular analysis' и 'Run intermodular analysis' в палитре команд (Ctrl+Shift+P):
Обе команды запускают общий анализ решения. Разница между ними имеет значение только при анализе C и C++ кода. В этом режиме анализатор выполняет более глубокий анализ, но тратит на это больше времени.
В случае если в открытой в VS Code директории не найдено подходящей цели для анализа, вы получите соответствующее сообщение:
В обратном случае вам, возможно, будет предложено настроить параметры, специфичные для анализа проектов, использующих ту или иную сборочную систему. Больше информации об анализе проекта конкретного типа можно найти в разделах этой документации: 'Анализ C, C++ (CMake) проектов', 'Анализ C# (MSBuild) проектов' и 'Анализ Java проектов'.
В случае успешного запуска анализа окно PVS-Studio примет следующий вид:
В правом верхнем углу окна будет отображаться прогресс анализа в процентах. Рядом с ним появится кнопка, с помощью которой можно прервать анализ. Срабатывания будут появляться в таблице по мере поступления.
Перед запуском анализа необходимо установить расширение CMake Tools для VSCode.
Плагин совместим со следующими генераторами для сборки проектов:
При обнаружении неподдерживаемого генератора или его отсутствии появится сообщение, предлагающее задать этот параметр в настройках CMake-Tools:
При клике по кнопке 'Edit setting' будет открыта страница с этой настройкой.
Перед запуском анализа вам будет предложено выбрать его тип: межмодульный анализ (Intermodular analysis) или обычный анализ (Regular analysis). Их отличие заключается в том, что межмодульный анализ выполняется дольше, чем обычный, но даёт лучший результат. Подробнее про этот режим вы можете прочитать в документации.
При запуске анализа также учитывается содержимое папки '.PVS-Studio' в директории исходного кода проекта. Если в ней будут найдены пользовательские файлы конфигурации (с расширением *.pvsconfig) или suppress-файлы (с суффиксом *.suppress.json), то они будут переданы анализатору для дальнейшей обработки.
Внимание: на данный момент при проверке CMake проектов поддерживается работа только с одним suppress-файлом. Если при запуске будет найдено несколько файлов, то в отчёте появится соответствующая запись. В ней также будет указано, какой файл используется при анализе.
При первом анализе решения вы получите уведомление о создании файла в "[workspace folder]/.PVS-Studio/MSBuildAnalyzerConfig.json", в котором можно задать некоторые настройки анализа, аналогичные параметрам консольной версии анализатора.
Варианты действий:
В этом файле вы можете указать файлы и параметры, которые будут учитываться при анализе:
Внимание: при запуске анализа конкретных файлов через контекстные меню или меню групп вкладок редактора будут проанализированы только файлы, включенные в выбранное решение.
Кроме того, если среди анализируемых файлов окажется файл проекта (файл с расширением .csproj), будет выполнен анализ всех поддерживаемых файлов с кодом, которые включены в этот проект.
Внимание: чтобы расширение смогло определить ваш проект в открытой директории, в VS Code также должно быть установлено и активировано расширение "Project manager for Java".
По умолчанию расширение ищет ядро анализатора в одной из следующих директорий:
Если ядро находится в другой папке, следует указать путь к ней в настройках расширения (File > Preferences > Settings > PVS-Studio: Java Projects Analyzer)
Для запуска ядра анализатора требуется JDK с 11-19 версией. По умолчанию используется JDK, путь к которому задан в переменных окружения. Если версия вашего JDK не входит в допустимый диапазон, установите поддерживаемый JDK и укажите путь к нему в настройках VS Code (File > Preferences > Settings > PVS-Studio: Java For Running Analyzer).
Перед анализом рекомендуется выполнить команду "Java: Reload Projects" в палитре команд (Ctrl + Shift + P). Если после этого появится выпадающий список с модулями вашего проекта, убедитесь, что галочкой отмечены все модули, которые нужно проанализировать.
Запускаем анализ
При первом анализе проекта вы получите уведомление о создании файла в "[workspace folder]/.PVS-Studio/JavaAnalyzerConfig.json", в котором можно задать некоторые настройки анализа, аналогичные параметрам консольной версии анализатора.
Варианты действий:
В этом файле вы можете указать параметры, которые будут учитываться при анализе, например:
Обратите внимание: расширение PVS-Studio для Visual Studio Code поддерживает только отчёты в формате JSON. В зависимости от типа проверяемого проекта и способа запуска анализа, PVS-Studio может генерировать отчёт в нескольких форматах. Чтобы отобразить отчёт в расширении, Вам потребуется выполнить его преобразование в JSON-формат.
Для преобразования можно воспользоваться утилитами командной строки PlogConverter.exe для Windows и plog-converter для Linux и macOS. Эти утилиты позволяют не только конвертировать отчёт PVS-Studio в разные форматы, но и дополнительно обрабатывать его. Например, проводить фильтрацию предупреждений. Подробнее о них можно прочитать здесь.
Пример команды конвертации отчёта PVS-Studio в JSON-формат при помощи PlogConverter.exe (Windows):
PlogConverter.exe path\to\report.plog -t json ^
-n PVS-Studio
Пример команды конвертации отчёта PVS-Studio в JSON-формат при помощи plog-converter (Linux и macOS):
plog-converter path/to/report/file.plog -t json \
-o PVS-Studio.json
Для просмотра отчёта в Visual Studio Code нажмите 'Open report' в окне PVS-Studio и выберите нужный файл. Также открыть отчёт можно, выбрав в палитре команд (Ctrl+Shift+P) 'PVS-Studio: Load Report'.
После этого предупреждения из отчёта отобразятся в таблице:
Для удобной работы с таблицей закрепите её на панели. Для этого кликните правой кнопкой мыши на заголовке панели и выберите "Keep 'PVS-Studio'".
Для работы с отчётом, содержащим относительные пути, необходимо воспользоваться настройкой 'Source Tree Root'. Для этого откройте окно настроек, во вкладке 'Other' нажмите кнопку 'Browse' и выберите директорию, относительно которой будут разворачиваться все пути в файле отчёта.
Механизмы фильтрации окна вывода PVS-Studio позволяют быстро найти и отобразить как отдельные диагностические сообщения, так целые их группы. Среди инструментов окна есть ряд переключателей, позволяющих включить либо отключить отображение предупреждений из соответствующих им групп.
Все переключатели можно разбить на 3 группы: фильтры по уровню достоверности предупреждений, фильтры сообщений по диагностическим группам, фильтры по ключевым словам. Отфильтровать можно по коду сообщения, по тексту сообщения и по файлу, содержащему сообщение анализатора.
Детальное описание уровней достоверности предупреждений и групп диагностических правил приведено в разделе документации 'Знакомство со статическим анализатором кода PVS-Studio'.
Переключить видимость группы фильтраций можно с помощью кнопок 'Column', 'Level', 'Group'.
Все перечисленные механизмы фильтрации предупреждений можно совмещать между собой. Например, фильтровать сообщения по уровню и группам отображаемых предупреждений, исключать сообщения, помеченные как ложные срабатывания и т.п.
При первом запуске анализатора на большом проекте может быть действительно много срабатываний. Разумеется, стоит выписать себе самые интересные, а вот остальные можно скрыть при помощи механизма подавления предупреждений.
Для подавления всех предупреждений нужно нажать кнопку 'Suppress All Messages' в правой верхней части окна:
При её активации появится дополнительное окно с вопросом, какие именно предупреждения вы хотите подавить:
При выборе нужного пункта предупреждения будут подавлены в существующий файл подавления предупреждений. Если файл подавления предупреждений не будет найден, то будет создан в следующей директории: "[корневой каталог исходного кода проекта]/.PVS-Studio".
Если предложенный выше вариант вам не подходит, то можно воспользоваться точечным подавлением предупреждений. Для этого необходимо выбрать нужные строки в таблице, открыть контекстное меню и выбрать "Add message to suppression file".
Окно вывода результатов PVS-Studio в первую очередь предназначено для упрощения навигации по коду проекта и переходу к участкам кода, содержащим потенциальные ошибки. Двойной щелчок мыши по любому из предупреждений в списке автоматически откроет файл, на который данное сообщение указывает.
Для выделения интересных предупреждений, например, тех, к которым имеет смысл вернуться повторно, можно использовать "звёздочку" в соответствующей колонке.
Правый клик по заголовку таблицы вызывает контекстное меню, с помощью которого можно настроить отображаемые столбцы.
Таблица поддерживает множественное выделение с помощью стандартных комбинаций 'Ctrl' и 'Shift '. Контекстное меню таблицы предупреждений содержит несколько подпунктов:
Кнопки в правой части панели предназначены, соответственно, для запуска анализа, подавления предупреждений, сохранения отчёта, открытия нового отчёта, открытия настроек расширения. При наличии изменений в отчёте кнопка сохранения отчёта поменяет свой фон на красный, как показано на скриншоте ниже.
Для открытия окна настроек нажмите на крайнюю правую кнопку окна PVS-Studio или нажмите 'Ctrl+Shift+P' и введите 'PVS-Studio: Show Settings'.
Кнопка 'Save settings' сохраняет настройки в формате JSON. Для загрузки сохранённых настроек воспользуйтесь кнопкой 'Load settings'.
Во вкладке False alarms можно настроить:
Данная вкладка настроек позволяет отметить какие столбцы должны отображаться в таблице срабатываний.
Вкладка Diagnostics позволяет отметить, какие предупреждения должны отображаться в таблице срабатываний. Все срабатывания разбиты на группы. Для поиска определённого диагностического правила можно воспользоваться полем 'Code and Messages'. Кнопки 'Check all/Uncheck all' предназначены для включения/выключения отображения всех диагностик из определённой группы. При отключении всех диагностик из соответствующей группы переключатель этой группы убирается из соответствующей группы фильтрации в окне PVS-Studio.
Во вкладке Exclude paths в поле 'New excluded path' можно ввести пути или маски путей. Сообщения, выданные на файлы, удовлетворяющие условиям маски, не будут отображаться в таблице срабатываний.
Во вкладке Analysis в поле 'Timeout' можно ввести время в секундах, по истечению которого анализ будет прерван. В поле 'Thread Count' можно ввести количество параллельно запускаемых процессов ядра анализатора, которые будут задействованы при анализе.
Во вкладке License можно ввести имя пользователя и лицензионный ключ из своей лицензии. Если вы ввели валидные данные, то у вас отобразится сообщение с данными лицензии.
Documentation language. Настройка позволяет задать язык для встроенной справки по диагностическим сообщениям PVS-Studio, доступных на нашем сайте.
Данная настройка не меняет язык интерфейса расширения PVS-Studio или выдаваемых анализатором диагностических сообщений.
Source Tree Root. Для работы с отчётом PVS-Studio, содержащим пути до файлов в относительной форме нужно заменить их на абсолютные. Настройка позволяет задать директорию, относительно которой раскрываются все пути в файле отчёта.
Детальное описание использования относительных путей в файлах отчётов PVS-Studio смотрите здесь.
Security Related Issues. Сообщения в отчёте, классифицируемые по ГОСТ Р 71207-2024, будут выделены дополнительной маркировкой в столбце 'SAST'.
Любой статический анализатор кода работает медленнее компилятора. Это объясняется тем, что компилятор должен отработать очень быстро, пусть и в ущерб глубине анализа. Статические анализаторы вынуждены хранить дерево разбора, чтобы иметь возможность собрать большее количество информации. Хранение дерева увеличивает расход памяти, а множество проверок делает обход дерева ресурсоемкой и медленной операцией. На самом деле всё это не так критично, так как анализ более редкая операция, чем компиляция и пользователи готовы подождать. Тем не менее, всегда хочется, чтобы инструмент работал быстрее. В статье собраны рекомендации, позволяющие существенно увеличить скорость работы PVS-Studio.
Сначала перечислим коротко все рекомендации для того, чтобы пользователи сразу же могли понять, как им улучшить время работы анализатора:
Рассмотрим все эти рекомендации более подробно, с пояснениями, почему они позволяют работать быстрее.
PVS-Studio давно (с версии 3.00, вышедшей в 2009 году) поддерживает работу в несколько потоков. Распараллеливание выполняется на уровне файлов. Если анализ выполняется на четырех ядрах, то одновременно проверяется четыре файла. Такой уровень параллелизма позволяет обеспечить существенное повышение производительности. По нашим замерам разница во времени работы на тестовых проектах очень заметна. Анализ в один поток выполняется за 3 часа 11 минут, в то время как анализ в четыре потока выполняется за 1 час 11 минут (данные получены на машине с четырьмя ядрами и восемью гигабайтами оперативной памяти). Отличие в 2.7 раза.
Для каждого потока анализатора рекомендуется иметь не менее одного гигабайта оперативной памяти. В противном случае (если будет много потоков и мало оперативной памяти) будет использоваться файл подкачки, что приведет к замедлению работы. При необходимости можно ограничить количество потоков анализатора с помощью настроек PVS-Studio Options -> Common Analyzer Settings -> Thread Count (документация). По умолчанию запускается столько потоков, сколько ядер в системе.
Рекомендуем использовать машину с четырьмя ядрами и восемью гигабайтами оперативной памяти или мощнее.
Медленный жесткий диск, как ни странно, является очень слабым местом в работе анализатора кода. Но надо пояснить механизм работы анализатора для того, чтобы было понятно, почему это так. Для того, чтобы выполнить анализ файла, необходимо сначала его препроцессировать, т.е. раскрыть все #define, подставить все #include и т.п. Файл после препроцессинга имеет размер в среднем 10 мегабайт и записывается на диск в папку проекта. Затем уже читается анализатором и начинается его разбор. Рост файла происходит как раз за счет вставки содержимого #include-файлов, которые читаются из системных папок.
Привести конкретные результаты измерений влияния SSD на скорость анализа сложно, так как для этого надо тестировать абсолютно одинаковые машины, различающиеся лишь типом диска, однако, на глаз ускорение очень существенно.
Анализатор кода является сложной и подозрительной программой с точки зрения антивируса по характеру своей работы. Сразу оговоримся, что речь не идет о том, что анализатор распознается как вирус - мы это регулярно проверяем. Кроме того, мы используем сертификат подписи кода. Обратимся опять к описанию работы анализатора кода.
На каждый анализируемый файл запускается отдельный процесс анализатора (модуль PVS-Studio.exe). Если в проекте 3000 файлов, то столько раз и будет запущен PVS-Studio.exe. Для своей работы PVS-Studio.exe вызывает настройку переменных окружения Visual C++ (файлы vcvars*.bat). Также во время работы создается большое количество препроцессированных файлов (*.i), по одному на каждый компилируемый файл. Используются вспомогательные командные (.cmd) файлы.
Все это хоть и не является вирусной активностью, тем не менее, заставляет любой антивирус тратить большое количество ресурсов на бесполезную проверку одного и того же.
Мы рекомендуем сделать следующие исключения в настройках антивируса:
Возможно, приведенный список избыточен, но он дан именно в таком виде для того, чтобы можно было вне зависимости от конкретного антивируса указать, какие файлы и процессы проверять не надо.
Иногда, может быть, антивирус вообще не используется на машине (например, на специальном компьютере для сборок и запуска анализатора кода). В этом случае скорость работы будет наибольшая. Так как даже если в антивирусе указаны перечисленные выше исключения, на их проверку все равно будет тратиться время.
Наши тестовые измерения показывают, что агрессивно настроенный антивирус может давать замедление работы анализатора кода в два и более раз.
Для препроцессирования файлов в PVS-Studio используется внешний препроцессор. В среде Visual Studio по умолчанию используется нативный препроцессор Microsoft Visual C++ - cl.exe. В PVS-Studio 4.50 появилась поддержка второго независимого препроцессора Clang, который лишен ряда недостатков препроцессора от Microsoft (хотя и имеет свои недостатки).
В некоторых старых версия Visual Studio (2010 и 2012), препроцессор cl.exe работает значительно медленнее, чем clang. Использование препроцессора Clang в этих средах позволяет повысить скорость работы в 1.5-1.7 раз.
Однако, здесь есть нюанс, который надо учитывать. Указать используемый препроцессор можно в настройках PVS-Studio Options -> Common Analyzer Settings -> Preprocessor (документация). Доступны варианты: VisualCPP, Clang и VisualCPPAfterClang. Первые два варианта очевидны, а третий вариант означает, что сначала будет использоваться Clang. В случае, если при препроцессировании будут ошибки, то затем файл будет заново препроцессирован с помощью Visual C++.
Если ваш проект проверяется без проблем с помощью Clang, то вы можете использовать стандартное значение VisualCPPAfterClang или Clang - без разницы. Однако, если ваш проект может быть проверен только с помощью Visual C++, рекомендуем указать именно это значение, чтобы анализатор не запускал напрасно Clang в попытках препроцессировать им.
Любой крупный программный проект использует много сторонних библиотек, таких как zlib, libjpeg, Boost и др. Иногда эти библиотеки собираются отдельно, и тогда в основном проекте доступны только заголовочные и библиотечные (lib) файлы. А иногда библиотеки очень плотно интегрированы в проект, фактически становясь его частью. В этом случае при компиляции основного проекта компилируются также и файлы с кодом от этих библиотек.
Анализатору PVS-Studio можно указать, чтобы он не проверял код сторонних библиотек. Ведь даже если там и будут найдены какие-то ошибки, то, скорее всего, вы их править не станете. Но исключая из анализа такие папки можно существенно увеличить общую скорость анализа.
Также есть смысл исключить из анализа код, который долгое время не будет гарантированно меняться.
Для того чтобы исключить из анализа какие-то папки или отдельные файлы, используйте настройки PVS-Studio -> Don't Check Files (документация).
Для исключения папок можно указать в списке папок либо одну общую папку вроде c:\external-libs, либо явно перечислить некоторые из них: c:\external-libs\zlib, c:\external-libs\libjpeg и т.д. Можно указывать полный путь, относительный путь или маску. Например, достаточно указать zlib и libjpeg в списке папок - это автоматически будет трактоваться как папка с маской *zlib* и *libjpeg*. Подробнее смотрите в документации.
Перечислим еще раз способы повысить скорость работы PVS-Studio:
Наибольший эффект достигается при одновременном применении наибольшего числа из этих рекомендаций.
В PVS-Studio можно выделить 2 главных компонента: command-line анализатор PVS-Studio.exe и IDE плагин, интегрирующий этот command-line анализатор в одну из поддерживаемых сред разработки (Microsoft Visual Studio). При этом сам command-line анализатор по принципу своей работы очень схож с компилятором — он вызывается для каждого из проверяемых файлов с параметрами, включающими в себя, в том числе, и оригинальные параметры компиляции этого файла. Затем анализатор вызывает необходимый ему препроцессор (опять, в соответствии с препроцессором, используемым при компиляции проверяемого файла) и производит непосредственный анализ полученного препроцессированного временного файла, т.е. файла, в котором раскрыты все include и define директивы.
Таким образом, command-line анализатор, как, впрочем, и компилятор (например, компилятор Visual C++ cl.exe), не предназначен для непосредственного использования конечным пользователем. Продолжая аналогию, компиляторы используются в большинстве случаев не напрямую, а с помощью специальной сборочной системы. Такая система подготавливает параметры запуска для каждого из собираемых файлов, а также, обычно, осуществляет распараллеливание самой сборки по имеющимся логическим процессорам для оптимизации затрачиваемого времени. Аналогом как раз такой сборочной системы для конечного пользователя и выступает IDE плагин PVS-Studio.
Но использование анализатора PVS-Studio.exe не ограничивается IDE плагинами. Как уже было сказано выше, command-line анализатор очень близок в своём использовании непосредственно к компилятору. Соответственно, его вызов можно встроить, при необходимости, и напрямую в сборочную систему, наравне с самим компилятором. Такой вариант может оказаться полезным при использовании для сборки не поддерживаемого нами сценария, такого как, например, custom-made сборочная система или IDE, отличная от Visual Studio. Заметьте, что PVS-Studio.exe поддерживает проверку исходных файлов, предназначенных для компиляторов gcc, clang и cl (включая специфические ключевые слова и конструкции).
Например, если ваш проект собирается в IDE Eclipse с помощью gcc, вы можете встроить проверку PVS-Studio в ваши makefile сборочные сценарии. Единственное ограничение - PVS-Studio.exe работает только в операционных системах семейства Windows NT.
Помимо IDE плагинов в дистрибутиве мы предоставляем также плагин для сборочной системы Microsoft MSBuild, которая используется для сборки Visual C++ проектах в IDE Visual Studio, начиная с версии 2010. Но, не стоит путать этот плагин с IDE плагином для Visual Studio!
Таким образом, проекты в Visual Studio (2010 версии и выше), можно проверить двумя различными способами: напрямую через наш IDE плагин, либо интегрировав проверку в сборочную систему (MSBuild плагин). Но ничто не мешает вам, при необходимости, написать и свой плагин для вызова статического анализа, как для MSBuild, так и для любой другой сборочной системы, либо даже встроить вызов PVS-Studio.exe напрямую, если это возможно, как в случае с make сборочными сценариями.
Если PVS-Studio выдаёт для вашего файла сообщение вида "C/C++ код не найден", убедитесь, что файл, который вы пытаетесь проверить, включён в сборочный проект (исключённые из сборки файлы PVS-Studio игнорирует). Если вы получаете такое сообщение на всём проекте, убедитесь, что ваш проект является C/C++ проектом поддерживаемого типа. В Visual Studio поддерживаются только Visual C++ проекты для версий 2005 и выше, и соответствующие им Platform Toolset. Проверка расширений проектов, использующих другие компиляторы (например, проекты для компилятора C++ от Intel) или параметры (Windows DDK драйверы) не поддерживается. Несмотря на то, что сам command-line анализатор PVS-Studio.exe поддерживает проверку файлов для компиляторов gcc/clang, проектные расширения IDE, использующие данные компиляторы, не поддерживаются.
Если ваш случай не подходит под описанное выше, вы можете обратиться в нашу поддержку. По возможности, предоставьте нам временные конфигурационные файлы для проблемных файлов. Их можно получить, переключив настройку PVS-Studio -> Options -> Common Analyzer Settings -> Remove Intermediate Files в False, файлы с именем вида %SourceFilename.cpp%.PVS-Studio.cfg должны появиться в той же директории, где лежит проектный файл (.vcxproj). Если это возможно, создайте пустой тестовый проект, на котором будет повторяться ваша проблема, и также пришлите его нам.
Если после проверки вашего файла\проекта PVS-Studio выдаёт в окне отчёта сообщения с кодом V008 и\или сообщения об ошибке от препроцессора (clang/cl), удостоверьтесь, что файл(ы), который вы пытаетесь проверять, компилируются без ошибок. Для работы анализатора PVS-Studio требуются корректно компилируемые исходные C/C++ файлы. Ошибки линковки же для работы анализатора не имеют значения.
Ошибка V008 означает, что отработавший препроцессор вернул ненулевой код. Обычно сообщение V008 сопровождается сообщением от самого препроцессора о причине ошибки (например, не найден include файл). Заметьте, что в IDE Visual Studio наш плагин, для оптимизации времени работы, использует специальный режим дуального препроцессирования: сначала файл будет препроцессирован с помощью более быстрого препроцессора clang, а в случае ошибки препроцессирования (clang не поддерживает некоторые специфичные для Visual C++ конструкции) будет запущен стандартный препроцессор cl.exe. Если у вас возникают ошибки препроцессирования clang, вы можете попробовать переключить IDE плагин на постоянное использование одного препроцессора (настройка PVS-Studio -> Options -> Common Analyzer Settings -> Preprocessor).
Иногда V008 может возникать из-за слишком длинных путей к исходным файлам. Если вы используете Windows, то можете попробовать увеличить максимальное значение длины путей. Как это сделать описано вот тут.
Если вы уверены, что ваш файл корректно собирается в IDE/сборочной системе, то, возможно, какие-то из его параметров компиляции некорректно передаются в анализатор PVS-Studio.exe. В таком случае отправьте, пожалуйста, в нашу поддержку временные конфигурационные файлы для этих файлов, а также текст сообщения из лога PVS-Studio. Временные конфигурационные файлы можно получить, переключив настройку PVS-Studio -> Options -> Common Analyzer Settings -> Remove Intermediate Files в False, файлы с именем вида %SourceFilename.cpp%.PVS-Studio.cfg должны появиться в той же директории, где лежит проектный файл (.vcxproj). Если это возможно, создайте пустой тестовый проект, на котором будет повторяться ваша проблема, и также пришлите его нам.
Если вы столкнулись с аварийной остановкой работы плагина с выдачей диалога PVS-Studio Internal Error, пришлите, пожалуйста, в нашу поддержку стек работы анализатора, который можно скопировать из данного диалогового окна.
Если вам удаётся стабильно повторить ошибку, то помимо стека падения пришлите нам лог трассировки работы плагина. Его можно получить, включив режим трассировки в настройках PVS-Studio -> Options -> Specific Analyzer Settings -> TraceMode (режим Verbose). Лог трассировки будет сохранён в стандартную пользовательскую директорию Application Data\Roaming\PVS-Studio с именем PVSTracexxxx_yyy.log, где xxxx- PID процесса devenv.exe / bds.exe, yyy - номер лога для данного процесса.
Если вы наблюдаете систематическое падение вашей среды разработки, предположительно вызванное работой PVS-Studio, проверьте, пожалуйста, системные event логи Windows (Event Viewer) и пришлите в нашу поддержку сигнатуру и стек падения (при наличии) для приложения devenv.exe \ bds.exe (уровень сообщения Error), доступные в списке Windows Logs -> Application.
Если вы наблюдаете систематическое необработанное падение анализатора PVS-Studio.exe, то просим вас повторить шаги, описанные в пункте "Падение IDE при использовании PVS-Studio", только для процесса PVS-Studio.exe.
Ошибка V003 означает, что файл не был проверен анализатором PVS-Studio.exe из-за обработанного внутреннего исключения. Если вы видите в логе анализатора ошибки с кодом V003, просим вас прислать нам временный файл препроцессора (i-файл, содержащий все раскрытые include и define директивы) для файла, который вызывает ошибку v003 (можно посмотреть имя в поле file). Получить такой файл можно, переключив настройку PVS-Studio -> Options -> Common Analyzer Settings -> Remove Intermediate Files в False. Intermediate файлы с именами вида SourceFileName.i будут появляться после перезапуска анализа в директории проверяемого проекта (т.е. директории, где находятся файлы vcproj/vcxproj/cbproj и т.п.).
Также, анализатор не всегда может полностью проанализировать файл с исходным кодом. Это не всегда происходит по вине PVS-Studio и подробнее с причинами можно ознакомиться в документации для ошибки V001. Независимо от причины выдачи сообщения V001, это сообщение не критично. Как правило, неполный разбор файла несущественен с точки зрения анализа. PVS-Studio просто пропускает функцию/класс с ошибкой и продолжает анализ файла. Непроанализированным остается совсем небольшой участок кода. Если же данный участок содержит критичные для вас фрагменты, вы можете также прислать нам i-файл, полученный для данного исходного файла.
Если вам кажется, что анализатор не находит ошибки во фрагменте вашего кода, содержащем её, или же, наоборот, анализатор генерирует ложные срабатывания для фрагмента кода, который, по вашему мнению, корректен, просим вас прислать нам временный файл препроцессора. Получить такой файл можно, переключив настройку PVS-Studio -> Options -> Common Analyzer Settings -> Remove Intermediate Files в False. Intermediate файлы с именами вида SourceFileName.i будут появляться после перезапуска анализа в директории проверяемого проекта (т.е. директории, где находятся файлы vcproj/vcxproj/cbproj и т.п.). Также включите в ваше письмо фрагмент кода из исходного файла, который вызывает проблемы.
Мы обязательно рассмотрим возможность реализации диагностики вашего примера, либо правки имеющихся диагностик для уменьшения числа ложных срабатываний в вашем коде.
Если вы столкнулись с проблемами при работе с отчётом PVS-Studio из окна нашего IDE плагина, а именно: некорректно осуществляется навигация по проверенным исходным файлам и\или файлы вообще не открываются для навигации, происходит некорректная разметка вашего кода маркерами ложных срабатываний или комментариями и т.п. В таком случае просим вас прислать в поддержку лог трассировки работы плагина. Его можно получить, включив режим трассировки в настройках PVS-Studio -> Options -> Specific Analyzer Settings -> TraceMode (режим Verbose). Лог трассировки будет сохранён в стандартную пользовательскую директорию Application Data\Roaming\PVS-Studio с именем PVSTracexxxx_yyy.log, где xxxx- PID процесса devenv.exe /bds.exe, yyy - номер лога для данного процесса.
Также, если это возможно, создайте пустой тестовый проект, на котором будет повторяться ваша проблема, и тоже пришлите его нам.
IDE плагин PVS-Studio способен распараллеливать анализ кода на уровне проверяемых файлов, т.е. возможен параллельный запуск проверки для любых проверяемых файлов, даже внутри одного проекта. По умолчанию плагин устанавливает количество потоков для проверки в соответствии с количеством процессоров в системе. Изменить количество параллельно запускаемых анализаторов можно с помощью настройки PVS-Studio -> Options -> Common Analyzer Settings -> ThreadCount.
Если вам кажется, что не все логические процессоры вашей системы достаточно загружены, вы можете увеличить количество параллельных потоков для анализа. Однако следует учитывать, что статический анализ, в отличие от компиляции, более требователен к ресурсам основной памяти системы: на каждый экземпляр анализатора требуется примерно 1.5 гигабайта памяти.
Если ваша система, даже при наличии многоядерного процессора, не удовлетворяет данным требованиям, возможно резкое проседание производительности анализатора, вызываемое необходимостью задействования файла подкачки. В таком случае мы рекомендуем снизить количество параллельных потоков анализатора до удовлетворения требования в 1.5 гигабайта на поток, даже если это значение будет ниже количества ядер в системе.
Следует учитывать, что при большом количестве параллельных потоков, узким местом может стать жесткий диск, где создаются временные препроцессированные *.i файлы. Иногда размер этих файлов достаточно велик. Одним из способов существенно уменьшить время анализа является использование SSD дисков или RAID массива.
Падение производительности возможно также и при использовании ненастроенного антивирусного ПО. В связи с тем, что плагин PVS-Studio запускает множество экземпляров анализатора и интерпретатора командной строки cmd.exe, антивирус может находить такое поведение подозрительным. Для оптимизации времени выполнения анализа мы рекомендуем добавлять PVS-Studio.exe, а также все соответствующие директории в исключения вашего антивируса, либо отключать real-time защиту на время выполнения анализа.
Если же вы используете антивирус Security Essentials (ставший частью Windows Defender начиная с Windows 8), то возможно резкое падение производительности анализа для некоторых проектов/настроек. Вы можете ознакомиться с соответствующей статьёй нашего блога для более подробного разъяснения причин этого явления.
Не проверяются проекты, выключенные в общей сборке через окно Configuration Manager среды Visual Studio.
Для корректного анализа C или C++ проектов с помощью статического анализатора PVS-Studio эти проекты должны быть компилируемы в Visual C++ и собираться без ошибок. Поэтому при проверке группы проектов или отдельного проекта PVS-Studio произведёт анализ только для проектов, включённых в общую сборку.
Не включённые в сборку проекты будут пропущены. Если ни один из имеющихся проектов не включён в сборку, либо если выбрана проверка одного не включённого в сборку проекта, будет выдано сообщение "Files with C or C++ source code for analysis not found" и анализ не будет запущен. Посмотреть, какие проекты включены, а какие выключены в общей сборке можно с помощью окна Configuration Manager для текущего решения Visual Studio.
Если при проверке анализатором проекта, который компилируется в Visual Studio без ошибок, на некоторых файлах возникают ошибки, связанные с ненайденными заголовочными файлами, некорректно заданными флагами компиляции (например, флаг /MD) или макросами препроцессора, то возможно, что такое поведение является проявлением проблемы с некорректно подставленными при препроцессировании файлами, используемыми для создания предкомпилированных заголовков.
Такая проблема возникает из-за различий в поведении компилятора Visual C++ (cl.exe) в режимах компиляции и препроцессирования. При обычной сборке компилятор работает в "стандартном" режиме компиляции (т.е. результатом его работы являются объектные, бинарные файлы). Однако, для осуществления статического анализа, PVS-Studio запускает компилятор в режиме препроцессора. В таком режиме компилятор осуществляет раскрытие директив подстановки и макросов.
Но, если компилируемый файл использует предкомпилированный заголовок, компилятор не будет использовать сам заголовочный файл, когда он встретится в директиве #include. Вместо этого компилятор возьмёт заранее предкомпилированный pch файл. Однако, в режиме препроцессирования компилятор проигнорирует факт наличия предкомпилированного pch, и попробует раскрыть такой #include "обычным способом", т.е. вставив содержимое самого заголовочного файла.
Очень часто в рамках одного решения Visual Studio сразу несколько проектов используют предкомпилированные заголовки с одинаковыми именами (чаще всего это stdafx.h). Это зачастую может приводить к тому, что, из-за описанного выше различия в поведении компилятора, препроцессор включает в файл предкомпилированный заголовок, относящийся к другому проекту. Это может случиться по разным причинам. Например, файлу корректно указан необходимый pch, но в списке Include директорий содержатся несколько путей, содержащих несколько разных stdafx.h, и неправильная версия оказывается в приоритете для включения (т.е. путь до её директории встретится первым в строке запуска компилятора). Ещё один возможный сценарий - несколько проектов содержат один и тот же C++ файл. Этот файл собирается с разными параметрами в разных проектах и использует, соответственно, разные pch. Но такой файл физически лежит в одном месте, и рядом с ним лежит stdafx.h от одного из проектов, в который он включен. Если же в этот файл stdafx.h включён через директиву #include, использующую кавычки, то препроцессор всегда будет отдавать при включении приоритет файлу из текущей директории, какие бы пути и в каком бы порядке не были указаны в его Include'ах.
Включение неправильного файла предкомпилированных заголовков не всегда будет приводить к ошибке препроцессирования. Однако, если, например, один из проектов использует MFC, а другой не использует, или проекты используют разные наборы Include'ов, файлы предкомпилированных заголовков окажутся несовместимыми и возникнет ошибка - статический анализ для такого файла работать не будет.
К сожалению, со стороны анализатора нет возможности как-либо обойти подобную проблему, т.к. она связана с поведением препроцессора cl.exe. Если вы столкнулись с ней на одном из ваших проектов, вы можете решить её одним из следующих способов, в зависимости от причины её возникновения.
Если файл предкомпилированных заголовков некорректно подхватывается из-за того, что include с путём до его директории передан в командной строке компилятора раньше пути до корректной директории, вы можете просто разместить путь до корректной директории самым первым в списке Include'ов.
Если же некорректный файл предкомпилированных заголовков подхватился из-за нахождения его в одной директории с использующим его исходным файлом, вы можете использовать для его включения директиву #include с угловыми скобками, например:
#include <stdafx.h>
При такой форме записи компилятор будет игнорировать файлы из текущей директории при подстановке.
При проверке крупных (более 1000 файлов) проектов в PVS-Studio на операционной системе Windows 8 с использованием Visual Studio 2010 или более поздних версий возможно возникновение ошибок вида 'Library not registered' или остановка анализа в произвольный момент с сообщением 'PVS-Studio is unable to continue due to IDE being busy'.
Подобные ошибки могут возникать из-за нескольких причин: некорректно завершённый процесс установки среды Visual Studio или конфликты совместимости нескольких разных версий среды, установленных в системе. Даже если в вашей системе установлена только одна версия среды Visual Studio, но была раньше установлена и затем удалена другая версия IDE, есть вероятность, что удаление предыдущей версии прошло некорректно. В частности, конфликт совместимости может возникнуть, если в вашей системе установлены одновременно одна из Visual Studio 2010\2012\2013\2015\2017\2019\2022 и Visual Studio 2005 и\или 2008.
К сожалению, PVS-Studio не может самостоятельно обойти подобные ошибки, т.к. они вызваны конфликтами в COM интерфейсах, используемых в API среды Visual Studio. Если вы столкнулись с подобной проблемой, вы можете решить её несколькими способами. Использование PVS-Studio на системе с "чистой" установкой Visual Studio одной версии должно решить данную проблему. Если это невозможно, вы можете попробовать проверить ваш проект частями, в несколько заходов. Также следует заметить, что описанная ошибка возникает обычно в ситуации, когда помимо непосредственной работы анализатора PVS-Studio, IDE выполняет в фоновом режиме ещё какую-либо операцию (например, разбор #include директив системой автодополнения IntelliSense). Если вы дождётесь завершения этой операции перед запуском анализатора, это, возможно, позволит вам проверить проект полностью.
Вы также можете воспользоваться альтернативными способами запуска анализатора для проверки ваших файлов. Например, проверить любой проект с помощью режима отслеживания запусков компилятора из приложения C and C++ Compiler Monitoring UI (Standalone.exe).
К сожалению, из-за особенностей реализации механизма расширений для среды Visual Studio, PVS-Studio не может автоматически "подхватить" вновь установленную версию Visual Studio, если сама инсталляция новой версии запускалась после инсталляции PVS-Studio.
Приведём пример. Предположим, что перед установкой PVS-Studio на машине присутствовала только Visual Studio 2013. После установки анализатора, в меню Visual Studio 2013 появится пункт 'PVS-Studio' (если была выбрана соответствующая опция установщика), который позволит проверять ваши проекты в этой IDE. Теперь если на этой же машине установить Visual Studio 2015 (уже после того, как была установлена PVS-Studio), меню этой версии среды не будет содержать пункта 'PVS-Studio'.
Для того, чтобы добавить во вновь установленную IDE интеграцию анализатора, необходимо перезапустить инсталлятор PVS-Studio (файл PVS-Studio_Setup.exe). Если у вас не осталось этого файла, вы можете скачать его с нашего сайта. На странице выбора версий Visual Studio в инсталляторе, checkbox напротив нужной вам версии должен стать доступен для выбора после установки соответствующей ему версии IDE.
Причины возникновения проблемы и способы её решения описаны в отдельной заметке.
Иногда PVS-Studio может некорректно производить расчёт нужной для анализа версии Visual Studio. Это может приводить к аварийному завершению работы анализатора с различными ошибками.
Пример ошибки:
Can't reevaluate project MyProj.vcxproj with parameters: Project ToolsVersion 'Current'; PlatformToolset 'v142'. Previous evaluation error: 'The imported project "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Microsoft\VC\v170\Microsoft.Cpp.Default.props" was not found.
Как видно из сообщения, PVS-Studio неправильно сформировал путь до Microsoft.Cpp.Default.props. Жирным шрифтом выделен некорректный фрагмент. v170 используется для Visual Studio 2022. В данном случае требуется Visual Studio 2019, а следовательно, корректным фрагментом является v160.
Данная проблема может появляться по множеству причин. Вот некоторые из них:
С решением проблем может помочь установка корректного значения переменной окружения VisualStudioVersion. Например, Visual Studio 2019 соответствует значение 16.0, а Visual Studio 2022 – 17.0.
Ошибка маловероятна при использовании плагина для Visual Studio, так как при запуске анализа из IDE нужное значение записывается в VisualStudioVersion автоматически.
Проблему нехватки памяти можно решить увеличением доступного объема памяти и стека.
В плагине для Maven:
<jvmArguments>-Xmx4096m, -Xss256m</jvmArguments>
В плагине для Gradle:
jvmArguments = ["-Xmx4096m", "-Xss256m"]
В плагине для IntelliJ IDEA:
1) Analyze -> PVS-Studio -> Settings
2) Вкладка Environment -> JVM arguments
Обычно объема памяти по умолчанию может не хватать при анализе сгенерированного кода с большим количеством вложенных конструкций.
Возможно, стоит исключить этот код из анализа (с помощью exclude), чтобы не тратить на него лишнее время.
В случае если не удается запустить анализ, пожалуйста, напишите нам в поддержку и приложите *.json файлы из директории .PVS-Studio (находится в директории с проектом).
Пользовательские аннотации представляют собой комментарии специального вида, которые можно указать в исходном коде для дополнительной настройки диагностических правил. Аннотация может быть расположена в одном из следующих мест:
Далее представлен список пользовательских аннотаций для изменения поведения диагностических правил. Этот функционал доступен только для С и C++ анализатора. При проверке проектов, написанных на других языках, описанные здесь аннотации будут игнорироваться.
Примечание. По умолчанию пользовательские аннотации не применяются к виртуальным функциям. О том, как включить данный функционал, вы можете прочитать здесь.
Если ваш проект основан на игровом движке Unreal Engine, то анализатор также применяет ряд диагностических правил (например, V1100, V1102), находящих характерные UE-проектам ошибки. Диагностические правила активируются лишь тогда, когда анализатор обнаруживает включение заголовочных файлов из директории с исходным кодом UE.
Если в проекте присутствуют компилируемые файлы, которые не включают такие заголовочные файлы, то диагностические правила не применяются даже если они включены. Это сделано для того, чтобы не генерировать нерелевантные предупреждения для проектов, не использующих UE.
Если вы хотите принудительно активировать набор диагностических правил для произвольного компилируемого файла или группы файлов, то добавьте следующий комментарий:
//V_TREAT_AS_UNREAL_ENGINE_FILE
Существует множество системных функций, которые при определённых условиях возвращают нулевой указатель. Хорошими примерами являются такие функции, как 'malloc', 'realloc', 'calloc'. Эти функции возвращают нулевой указатель в случае, когда невозможно выделить буфер памяти указанного размера.
Можно изменить поведение анализатора и заставить его считать, что, например, функция 'malloc' не может вернуть нулевой указатель. Например, это может потребоваться, если пользователь использует системные библиотеки, в которых ситуации нехватки памяти обрабатываются особым образом.
Также возможна обратная ситуация. Пользователь может помочь анализатору, подсказав ему, что определённая системная или его собственная функция может вернуть нулевой указатель.
Для этого существует возможность с помощью пользовательских аннотаций указать анализатору, что функция может или, наоборот, не может вернуть нулевой указатель.
Формат аннотации:
//V_RET_[NOT]_NULL, function: [namespace::][class::]functionName
Предположим, пользователь хочет подсказать анализатору, что функция 'Foo' класса 'Bar', лежащего в пространстве имён 'Space', не может возвращать нулевой указатель. Тогда аннотация будет выглядеть следующим образом:
//V_RET_NOT_NULL, function: Space::Bar::Foo
Аннотации поддерживают вложенные пространства имён и вложенные классы. Предположим, в пространстве имён 'Space1' лежит пространство имён 'Space2'. В пространстве имён 'Space2' лежит класс 'Bar1'. В классе 'Bar1' лежит класс 'Bar2'. У класса 'Bar2' есть функция 'Foo', которая не может возвращать нулевой указатель. Тогда можно проаннотировать эту функцию следующим образом:
//V_RET_NOT_NULL, function: Space1::Space2::Bar1::Bar2::Foo
Для системных функций аннотация может быть расположена в глобальном заголовочном файле (например, предкомпилированный заголовочный файл) или файле конфигурации диагностических правил.
Для наглядности рассмотрим два примера аннотации системных функций.
Функция не возвращает нулевой указатель:
//V_RET_NOT_NULL, function:malloc
Теперь анализатор считает, что функция 'malloc' не может вернуть нулевой указатель и не будет выдавать предупреждение V522 для следующего фрагмента кода:
int *p = (int *)malloc(sizeof(int) * 100);
p[0] = 12345; // ok
Функция возвращает потенциально нулевой указатель:
//V_RET_NULL, function: Memory::QuickAlloc
После добавления этого комментария анализатор начнёт выдавать предупреждение для следующего кода:
char *p = Memory::QuickAlloc(strlen(src) + 1);
strcpy(p, src); // Warning!
В проектах с особыми требованиями качества может понадобится найти все функции, возвращающие указатель. Для этого можно воспользоваться следующим комментарием:
//V_RET_NULL_ALL
Мы не рекомендуем использовать этот режим из-за выдачи очень большого количества предупреждений. Но если в вашем проекте это действительно необходимо, то вы можете воспользоваться этим специальным комментарием, чтобы добавить в код проверку возвращаемого указателя для всех таких функций.
По умолчанию анализатор одинаково проверяет код, в котором присутствует макрос 'assert', независимо от конфигурации проекта (Debug, Release, ...). А именно – не учитывает, что при ложном условии в макросе выполнение кода прерывается.
Чтобы задать иное поведение анализатора, используйте следующий комментарий в коде:
//V_ASSERT_CONTRACT
Обратите внимание, что в таком режиме результаты анализа могут отличаться в зависимости от того, как раскрывается макрос в проверяемой конфигурации проекта.
Для пояснения рассмотрим следующий код:
MyClass *p = dynamic_cast<MyClass *>(x);
assert(p);
p->foo();
Оператор 'dynamic_cast' может вернуть значение 'nullptr'. Поэтому в стандартном режиме анализатор выдаст предупреждение, что при вызове функции 'foo' может произойти разыменовывание нулевого указателя.
После добавления комментария '//V_ASSERT_CONTRACT' предупреждение исчезнет.
Можно указать имя макроса, который анализатор будет обрабатывать так же, как 'assert'. Для этого используйте следующую аннотацию:
//V_ASSERT_CONTRACT, assertMacro:[MACRO_NAME]
Ключ 'assetMacro' – имя макроса, который анализатор будет обрабатывать как 'assert'. Вместо '[MACRO_NAME]' необходимо подставить имя аннотируемого макроса.
Пример:
//V_ASSERT_CONTRACT, assertMacro:MY_CUSTOM_MACRO_NAME
Теперь анализатор будет обрабатывать макрос 'MY_CUSTOM_MACRO_NAME' как 'assert'.
Если необходимо указать несколько имен макросов, для каждого из них следует добавить отдельную директиву '//V_ASSERT_CONTRACT'.
Иногда в проектах используются собственные реализации разных системных функций, например, 'memcpy', 'malloc' и т.п. В этом случае анализатор не будет понимать, что поведение таких функций идентично стандартным аналогам. Существует возможность ставить имена своих функций в соответствие системным.
Формат записи:
//V_FUNC_ALIAS, implementation:imp, function:f, namespace:n, class:c
Пример использования:
//V_FUNC_ALIAS, implementation:memcpy, function:MyMemCpy
Теперь анализатор будет обрабатывать вызовы функции 'MyMemCpy' так же, как вызовы 'memcpy'.
Можно самостоятельно указать имена своих собственных функций, для которых следует выполнять проверку формата. Подразумевается, что принцип форматирования строк совпадает с функцией 'printf'.
Для этого используется специальная пользовательская аннотация. Пример использования:
//V_FORMATTED_IO_FUNC, function:Log, format_arg:1, ellipsis_arg:2
void Log(const char *fmt, ...);
Log("%f", time(NULL)); // <= V576
Формат аннотации:
Наиболее полный пример использования:
namespace A
{
class B
{
void C(int, const char *fmt, ...);
};
}
//V_FORMATTED_IO_FUNC, function:A::B::C, format_arg:2, ellipsis_arg:3
Механизм JSON аннотаций — это способ разметки пользовательских функций и типов в файлах формата JSON с целью дать анализатору дополнительную информацию о пользовательском коде. Благодаря этой информации анализатор может как находить больше ошибок, так и выдавать меньше ложных срабатываний.
Использование отдельных файлов с аннотациями позволяет решить следующие проблемы:
В случае, если данные сценарии для вас не актуальны, и вы хотели бы проводить разметку прямо в исходном коде, то стоит ознакомиться с отдельной документацией.
На данный момент механизм доступен для следующих языков:
Для того, чтобы воспользоваться механизмом, необходимо:
Доступный функционал отличается в зависимости от используемого языка. После ознакомления с общей документацией желательно также ознакомиться с языко-специфичной частью:
Подключить уже готовый файл аннотаций можно следующими способами:
Способ N1. Добавить специальный комментарий в исходный код или в файл конфигурации диагностических правил (.pvsconfig):
//V_PVS_ANNOTATIONS, language:%язык_проекта%, path:%путь/до/файла.json%
Вместо символа подстановки %язык_проекта% предполагается использование одного из следующих значений:
Вместо символа подстановки %путь/до/файла.json% предполагается путь до подключаемого файла аннотаций. Поддерживаются как абсолютные, так и относительные пути. Относительные пути раскрываются относительно файла, в котором указан комментарий для подключения аннотации.
Способ N2 (только для С и C++ анализатора). Указать специальный флаг ‑‑annotation-file (-A) при запуске pvs-studio-analyzer или CompilerCommandsAnalyzer:
pvs-studio-analyzer --annotation-file=%путь/до/файла.json%
Вместо символа подстановки %путь/до/файла.json% предполагается путь до подключаемого файла аннотаций. Поддерживаются как абсолютные, так и относительные пути. Относительные пути раскрываются относительно текущей рабочей директории (CWD).
Примечание 1. Может быть подключено несколько файлов с аннотациями. Для каждого файла необходимо указать отдельный флаг или комментарий.
Примечание 2. До версии 7.33, для языков С и С++ аннотации можно было подключить с помощью комментария следующего вида:
//V_PVS_ANNOTATIONS %путь/до/файла%
Начиная с версии 7.33, в таком случае будет выдаться специальное сообщение об использовании устаревшего синтаксиса и предложение перейти на новый.
Для облегчения знакомства с механизмом пользовательских аннотаций мы подготовили коллекцию примеров для наиболее часто встречающихся сценариев.
Больше примеров использования можно увидеть в документации механизма для конкретного языка:
Для каждого доступного языка составлена JSON-схема с поддержкой версионирования. Благодаря этим схемам современные текстовые редакторы и IDE могут проводить валидацию, а также показывать подсказки прямо во время редактирования.
Для этого при составлении собственного файла аннотаций необходимо добавить в него поле $schema, в котором следует указать схему для необходимого языка. Например, для C и С++ анализатора поле будет выглядеть так:
{
"version": 1,
"$schema": "https://files.pvs-studio.com/media/custom_annotations/v1/cpp-annotations.schema.json",
"annotations": [
{ .... }
]
}
Например, так Visual Studio Code сможет давать подсказки при составлении аннотаций:
На данный момент JSON-схемы доступны для аннотаций на следующих языках:
Далеко не все проблемы можно диагностировать на уровне валидации JSON-схемы. Если при работе с файлом с аннотациями произошла ошибка, то анализатор сгенерирует предупреждение V019. Оно даст подсказку, что пошло не так. Например: файл с аннотациями отсутствует, произошла ошибка при разборе, аннотация пропущена из-за допущенных в ней ошибок и т.д.
Механизм пользовательских аннотаций — это способ разметки типов и функций в формате JSON с целью дать анализатору дополнительную информацию. Благодаря этой информации анализатор сможет находить больше ошибок в коде. Механизм работает только для языков С и С++.
Допустим, что в проекте требуется запретить вызов некоторой функции, т.к. она нежелательна к использованию:
void DeprecatedFunction(); // should not be used
void foo()
{
DeprecatedFunction(); // unwanted call site
}
Для того, чтобы анализатор сгенерировал предупреждение V2016 в месте вызова этой функции, необходимо создать специальный файл формата JSON со следующим содержимым:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "DeprecatedFunction",
"attributes": [ "dangerous" ]
}
]
}
После этого достаточно подключить файл (все способы подключения рассмотрены здесь):
//V_PVS_ANNOTATIONS, language: cpp, path: %path/to/annotations.json%
void DeprecatedFunction();
void foo()
{
DeprecatedFunction(); // <= V2016 will be issued here
}
Примечание. По умолчанию диагностическое правило V2016 отключена. Для выдачи предупреждений включите диагностику в настройках.
Подробнее о способе подключения файла аннотаций вы можете узнать в этой документации.
Содержимое файла — JSON-объект, состоящий из двух обязательных полей: version и annotations.
Поле version принимает значение целого типа и задаёт версию механизма. В зависимости от значения файл с разметкой может обрабатываться по-разному. На данный момент поддерживается только одно значение — 1.
Поле annotations — массив объектов "аннотация":
{
"version": 1,
"annotations":
[
{
...
},
{
...
}
]
}
Аннотации могут быть двух видов:
Если аннотация объявляется непосредственно в массиве annotations, то она считается аннотацией верхнего уровня. В ином случае считается, что это вложенная аннотация.
Объект аннотации типа состоит из следующих полей:
Обязательное поле. Принимает строку с одним из значений: "record", "class", "struct", "union". Последние три варианта являются псевдонимами "record" и добавлены для удобства.
Обязательное поле. Принимает строку с квалифицированным (полным) именем сущности. Анализатор будет искать эту сущность, начиная с глобальной области видимости. Если сущность находится в глобальной области, то "::" в начале имени может быть опущено.
Опциональное поле. Массив объектов вложенных аннотаций.
Опциональное поле. Массив строк, который задаёт свойства сущности. Для аннотаций типов доступны следующие атрибуты:
Объект аннотации функции состоит из следующих полей:
Обязательное поле. Принимает строку со значением function. Для вложенных аннотаций функций (в поле members аннотаций типов) также становится доступным значение ctor. Оно показывает, что аннотируется конструктор пользовательского типа.
Принимает строку с именем функции. Поле обязательно, если type имеет значение "function", иначе должно быть опущено. По этому имени анализатор будет искать аннотируемую сущность, начиная с глобальной области видимости.
Для аннотаций верхнего уровня указывается квалифицированное (полное) имя, для вложенных — неквалифицированное.
Если функция находится в глобальной области видимости, то '::' в начале имени может быть опущено.
Опциональное поле. Массив объектов, описывающих формальные параметры. Данное поле совместно с name задаёт сигнатуру функции, по которой анализатор будет сравнивать аннотацию с её объявлением в коде программы. В случае с функциями-членами анализатор также рассматривает поле qualifiers.
Каждый объект содержит следующие поля:
Если аннотацию нужно применить для всех перегрузок вне зависимости от параметров, то поле можно опустить:
// Code
void foo(); // dangerous
void foo(int); // dangerous
void foo(float); // dangerous
// Annotation
{
....
"type": "function",
"name": "foo",
"attributes": [ "dangerous" ]
....
}
Если же интересует перегрузка, не принимающая параметров, следует явно указать пустой массив:
// Code
void foo(); // dangerous
void foo(int); // ok
void foo(float); // ok
// Annotation
{
....
"type": "function",
"name": "foo",
"attributes": [ "dangerous" ],
"params": []
....
}
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
immutable |
Подсказывает анализатору, что после вызова функции переданный аргумент не был модифицирован. Например, функция printf имеет побочные эффекты (печать в stdout), однако не производит модификации переданных аргументов. |
2 |
not_null |
Действует только для параметров nullable-типа. В функцию необходимо передавать аргумент в состоянии "валидный". |
3 |
unique_arg |
Передаваемые аргументы должны отличаться. Например, нет смысла передавать в std::swap два одинаковых аргумента. |
4 |
format_arg |
Параметр обозначает форматную строку. Анализатор будет проверять аргументы согласно форматной спецификации printf. |
5 |
pointer_to_free |
Указатель, по которому в функции будет освобождена память с помощью free. Указатель может быть нулевым. |
6 |
pointer_to_gfree |
Указатель, по которому в функции будет освобождена память с помощью g_free. Указатель может быть нулевым. |
7 |
pointer_to_delete |
Указатель, по которому в функции будет освобождена память с помощью 'operator delete'. Указатель может быть нулевым. |
8 |
pointer_to_delete[] |
Указатель, по которому в функции будет освобождена память с помощью 'operator delete[]'. Указатель может быть нулевым. |
9 |
pointer_to_unmap |
Указатель, по которому в функции будет освобождена память с помощью 'munmap'. Указатель может быть нулевым. |
10 |
taint_source |
Данные, возвращающиеся через параметр, получены из недостоверного источника. |
11 |
taint_sink |
Данные, передаваемые через параметр, могут привести к эксплуатации уязвимости, если они получены из недостоверного источника. |
12 |
non_empty_string |
Параметр должен принимать любую непустую строку. |
Все поля ограничений — опциональные. Далее приведён список полей, которые задают условия ограничения.
Поля, задающие список разрешённых и запрещённых значений параметра:
Каждая строка внутри массива — это интервал от минимальной до максимальной границы включительно. Строка с интервалами указывается в формате "x..y", где 'x' и 'y' — это левая и правая границы соответственно. Одну из границ можно опустить. Тогда строка будет иметь вид "x.." или "..y". В таком случае интервал будет от 'x' до плюс бесконечности и от минус бесконечности до 'y' соответственно.
Примеры интервалов:
Массив может содержать несколько интервалов. При их чтении анализатор нормализует все интервалы в массиве, т.е. объединяет пересекающиеся и рядом стоящие интервалы, располагая их в порядке возрастания.
Если поля allowed и disallowed указаны одновременно, то анализатор вычитает "disallowed" интервалы из "allowed", чтобы получить множество разрешённых значений. Если значения из поля disallowed полностью перекрывают значения из allowed, то пользователю будет выдано предупреждение V019.
Опциональное поле. Объект, внутри которого возможно использование только поля attributes — массива строк, который позволяет задать атрибуты возвращаемого значения.
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
not_null |
Функция всегда возвращает объект nullable-типа в состоянии "валидный". |
2 |
maybe_null |
Функция может вернуть объект nullable-типа в состоянии "невалидный", и его стоит проверить перед разыменованием. |
3 |
taint_source |
Функция может вернуть данные из недостоверного источника. |
Опциональное поле. Массив строк, позволяющий задать шаблонные параметры функции. Поле необходимо, когда шаблонные параметры используются в сигнатуре функции:
// Code
template <typename T1, class T2>
void MySwap(T1 &lhs, T2 &rhs);
// Annotation
{
....
"template_params": [ "typename T1", "class T2" ],
"name": "MySwap",
"params": [
{ "type": "T1 &", attributes: [ "unique_arg" ] },
{ "type": "T2 &", attributes: [ "unique_arg" ] }
]
....
}
Опциональное поле. Позволяет применить аннотацию только к функции-члену с определённым набором cvref-квалификаторов. Доступно только для вложенных аннотаций, у которых поле type имеет значение "function". Данное поле совместно с name и params задаёт сигнатуру нестатической функции-члена, по которой анализатор будет сравнивать аннотацию с её объявлением в коде программы. Принимает массив строк со следующими возможными значениями: "const", "volatile", "&" или "&&".
Пример:
// Code
struct Foo
{
void Bar(); // don't need to annotate this overload
void Bar() const; // want to annotate this overload
void Bar() const volatile; // and this one
};
// Annotation
{
....
"type": "record",
"name": "Foo",
"members": [
{
"type": "function",
"name": "Bar",
"qualifiers": [ "const" ]
},
{
"type": "function",
"name": "Bar",
"qualifiers": [ "const", "volatile" ]
}
]
....
}
Если аннотацию надо применить ко всем квалифицированным и неквалифицированным версиям, то нужно опустить поле:
// Code
struct Foo
{
void Bar(); // want to annotate this overload
void Bar() const; // and this one
};
// Annotation
{
....
"type": "record",
"name": "Foo",
"members": [
{
"type": "function",
"name": "Bar",
}
]
....
}
Если надо применить аннотацию только к неквалифицированной версии, то значением поля должен быть пустой массив:
// Code
struct Foo
{
void Bar(); // want to annotate this overload
void Bar() const; // but NOT this one
};
// Annotation
{
....
"type": "record",
"name": "Foo",
"members": [
{
"type": "function",
"name": "Bar",
"qualifiers": []
}
]
....
}
Опциональное поле. Массив строк, который задаёт свойства сущности.
# |
Название атрибута |
Описание атрибута |
Примечание |
---|---|---|---|
1 |
pure |
Функция считается чистой. |
Функция чистая, когда она не имеет побочных эффектов, не модифицирует переданные аргументы, и результат функции одинаков при исполнении на одном и том же наборе аргументов. |
2 |
noreturn |
Функция не возвращает управление вызывающей функции. |
|
3 |
nodiscard |
Результат функции должен использоваться. |
|
4 |
nullable_uninitialized |
Функция-член пользовательского nullable-типа переводит объект в состоянии "невалидный". |
|
5 |
nullable_initialized |
Функция-член пользовательского nullable-типа переводит объект в состоянии "валидный". |
|
6 |
nullable_checker |
Функция проверяет состояние пользовательского nullable-типа. Если функция возвращает true, то объект считается в состоянии "валидный", не возвращает — "невалидный". Результат функции должен неявно конвертироваться к типу bool. |
|
7 |
nullable_getter |
Функция производит доступ к внутренним данным пользовательского nullable-типа. Объект при этом должен быть в состоянии "валидный". |
|
8 |
dangerous |
Функция помечена как опасная, и код программы не должен содержать её вызова. |
Можно использовать в качестве пометки функции как устаревшей (deprecated). Для выдачи предупреждений требуется включить диагностическое правило V2016 в настройках. |
Ниже приведена таблица применимости различных атрибутов с аннотациями функций:
# |
Атрибут |
Свободная функция |
Конструктор |
Функция-член |
---|---|---|---|---|
1 |
pure |
✓ |
✕ |
✓ |
2 |
noreturn |
✓ |
✕ |
✓ |
3 |
nodiscard |
✓ |
✓ |
✓ |
4 |
nullable_uninitialized |
✕ |
✓ |
✓ |
5 |
nullable_initialized |
✕ |
✓ |
✓ |
6 |
nullable_checker |
✕ |
✕ |
✓ |
7 |
nullable_getter |
✕ |
✕ |
✓ |
8 |
dangerous |
✓ |
✕ |
✓ |
JSON схемы поставляется в дистрибутиве, а также доступны по ссылкам ниже:
Допустим, есть следующий пользовательский nullable-тип:
constexpr struct MyNullopt { /* .... */ } my_nullopt;
template <typename T>
class MyOptional
{
public:
MyOptional();
MyOptional(MyNullopt);
template <typename U>
MyOptional(U &&val);
public:
bool HasValue() const;
T& Value();
const T& Value() const;
private:
/* implementation */
};
Примечания по коду:
Тогда аннотация класса и его функций-членов будет выглядеть следующим образом:
{
"version": 1,
"annotations": [
{
"type": "class",
"name": "MyOptional",
"attributes": [ "nullable" ],
"members": [
{
"type": "ctor",
"attributes": [ "nullable_uninitialized" ]
},
{
"type": "ctor",
"attributes": [ "nullable_uninitialized" ],
"params": [
{
"type": "MyNullopt"
}
]
},
{
"type": "ctor",
"template_params": [ "typename U" ],
"attributes": [ "nullable_initialized" ],
"params": [
{
"type": "U &&val"
}
]
},
{
"type": "function",
"name": "HasValue",
"attributes": [ "nullable_checker", "pure", "nodiscard" ]
},
{
"type": "function",
"name": "Value",
"attributes": [ "nullable_getter", "nodiscard" ]
}
]
}
]
}
Допустим, есть следующий код:
namespace Foo
{
template <typaname CharT>
size_t my_strlen(const CharT *ptr);
}
Функция Foo::my_strlen имеет следующие свойства:
Тогда аннотация функции будет выглядеть следующим образом:
{
"version": 1,
"annotations":
[
{
"type": "function",
"name": "Foo::my_strlen",
"attributes": [ "pure" ],
"template_params": [ "typename CharT" ],
"params": [
{
"type": "const CharT *",
"attributes": [ "not_null" ]
}
]
}
]
}
Допустим, есть следующая функция Foo::LogAtError:
namespace Foo
{
void LogAtError(const char *, ...);
}
О ней известны следующие факты:
Анализатор может проверять переданные аргументы согласно форматной строке, а также понимать, что после вызова этой функции код недостижим. Для этого надо разметить функцию следующим образом:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "Foo::LogAtError",
"attributes": [ "noreturn" ],
"params": [
{
"type": "const char *",
"attributes" : [ "format_arg", "not_null", "immutable" ]
},
{
"type": "...",
"attributes": [ "immutable" ]
}
]
}
]
}
Допустим, что в предыдущем примере программист добавил несколько перегрузок функции Foo::LogAtExit:
namespace Foo
{
void LogAtExit(const char *fmt, ...);
void LogAtExit(const char8_t *fmt, ...);
void LogAtExit(const wchar_t *fmt, ...);
void LogAtExit(const char16_t *fmt, ...);
void LogAtExit(const char32_t *fmt, ...);
}
В этом случае можно не писать аннотации для всех перегрузок, а лишь для одной, воспользовавшись символом подстановки:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "Foo::LogAtExit",
"attributes": [ "noreturn" ],
"params": [
{
"type": "?",
"attributes" : [ "format_arg", "not_null", "immutable" ]
},
{
"type": "...",
"attributes": [ "immutable" ]
}
]
}
]
}
Допустим, есть две перегрузки функции Foo::Bar:
namespace Foo
{
void Bar(int i);
void Bar(double d);
}
Требуется запретить использование первой перегрузки. Для этого надо разметить функцию следующим образом:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "Foo::Bar",
"attributes": [ "dangerous" ],
"params": [
{
"type": "int"
}
]
}
]
}
Допустим, что есть функция, которая возвращает внешние данные через out-параметр и возвращаемое значение.
std::string ReadStrFromStream(std::istream &input, std::string &str)
{
....
input >> str;
return str;
....
}
Для того, чтобы пометить эту функцию как источник недостоверных данных, необходимо её разметить следующим образом:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "ReadStrFromStream",
"params": [
{
"type": "std::istream &input"
},
{
"type": "std::string &str",
"attributes": [ "taint_source" ]
}
],
"returns": { "attributes": [ "taint_source" ] }
}
]
}
Предположим, что существует функция, при попадании в которую недостоверных данных может быть эксплуатирована какая-либо уязвимость.
void DoSomethingWithData(std::string &str)
{
.... // Some vulnerability
}
Чтобы пометить такую функцию как приемник недостоверных данных (функцию-сток), необходимо написать следующую аннотацию:
{
"version": 1,
"annotations": [
{
{
"type": "function",
"name": "DoSomethingWithData",
"params": [ { "type": "std::string &str",
"attributes": [ "taint_sink" ] }]
}
}
]
}
В C++ анализаторе есть диагностическое правило V1116, которое срабатывает при обнаружении создания объекта исключения с пустым сообщением. По умолчанию данное правило работает только с типами исключений из стандартной библиотеки C++. Для поддержки пользовательских типов исключений необходимо добавить специальную аннотацию.
Предположим, есть класс, определяющий объекты, которые будут использованы для выброса исключения:
class MyException: public std::runtime_error
{
public:
MyException(const std::string& what_arg )
: std::runtime_error(what_arg){}
MyException(const char *what_arg)
: std::runtime_error(what_arg){}
};
В аннотации необходимо определить тип исключения с атрибутом 'exception' и затем конструкторы, способные принимать поясняющие сообщения. Для каждого параметра конструктора, который должен принимать сообщение, следует указать атрибут 'non_empty_string'.
Например, аннотация класса 'MyException' может выглядеть так:
{
"version": 2,
"language": "cpp",
"annotations": [
{
"type": "class",
"name": "MyException",
"attributes": ["exception"],
"members": [
{
"type": "ctor",
"params": [{ "type": "const char*",
"attributes": ["non_empty_string"]}]
},
{
"type": "ctor",
"params": [{ "type": "const std::string&",
"attributes": ["non_empty_string"]}]
}
]
}
]
}
Примечание. На данный момент в C# анализаторе поддержаны только аннотации для taint-анализа. В будущем также будут поддержаны аннотации общего назначения.
Способы подключения файла c аннотациями
Подробнее о способе подключения файла аннотаций вы можете узнать в этой документации.
Содержимое файла — JSON-объект, состоящий из трёх обязательных полей: language, version и annotations.
Поле language должно иметь значение 'csharp'. Поле version принимает значение целого типа и задаёт версию механизма. В зависимости от значения файл с разметкой может обрабатываться по-разному. На данный момент поддерживается только одно значение — 1.
Поле annotations — массив объектов "аннотация":
{
"language": "csharp",
"version": 1,
"annotations":
[
{
...
},
{
...
}
]
}
Аннотации могут быть трёх типов:
В анализаторе существует ряд аннотаций для taint-анализа. С их помощью можно размечать источники и приёмники заражения. Также существует возможность помечать методы/конструкторы, которые производят валидацию taint-данных. Таким образом, если taint-данные прошли валидацию, то при их попадании в приёмник предупреждения анализатора не будет.
За каждый из видов уязвимостей отвечает отдельное диагностическое правило. На данный момент в анализаторе представлены следующие taint-диагностики:
Для каждого из диагностических правил существуют специальные аннотации, которые позволяют разметить приёмники и методы/конструкторы, производящие валидацию.
Что касается источников taint-данных, то они являются общими для всех диагностик. Такие данные также можно разметить с помощью аннотаций.
Примечание. Атрибуты для разметки taint-аннотаций описаны в следующих разделах.
Стоит отметить, что, помимо пользовательских аннотаций, в анализаторе уже имеется ряд taint-аннотаций для различных библиотек. Например, при передаче результата выполнения метода System.Console.ReadLine в конструктор System.Data.SqlClient.SqlCommand возможно возникновение SQL injection. В анализаторе есть аннотации, которые говорят, что System.Console.ReadLine — источник taint-данных, а System.Data.SqlClient.SqlCommand — приёмник, при попадании в который taint-данных может возникнуть SQL injection.
Таким образом, размечая источники taint-данных, анализатор будет учитывать их для уже существующих приёмников и наоборот. Если добавить аннотацию приёмника, то анализатор будет выдавать предупреждение при попадании в него уже размеченных источников taint-данных (например, System.Console.ReadLine).
Примечание. Объект аннотации метода должен содержать хотя бы одно опциональное поле.
Объект аннотации метода состоит из следующих полей:
Обязательное поле. Принимает строку со значением method.
Обязательное поле. Принимает строку с именем пространства имён, которое содержит метод.
Обязательное поле. Принимает строку с именем класса, в котором определён метод.
Обязательное поле. Принимает строку с именем метода.
Опциональное поле. Массив строк, который задаёт свойства сущности.
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
not_apply_to_child_class |
Аннотация не будет применяться при вызове проаннотированного метода у объекта дочернего класса. |
2 |
caller_is_xml_parser |
Объект, вызывающий метод, является XML-парсером, который может быть уязвим (V5614, V5615). |
Опциональное поле. Данное поле описано в разделе "Аннотации параметров".
Примечание. Объект аннотации возвращаемого значения должен либо содержать поля 'namespace_name' и ' type_name', либо оба поля должны отсутствовать (или иметь значение null). Если оба поля отсутствуют, то при выборе аннотации не будет учитываться тип возвращаемого значения.
Опциональное поле. Объект возвращаемого значения, состоит из следующих полей:
Опциональное поле. Принимает строку с именем пространства имён, которое содержит тип возвращаемого значения метода.
Опциональное поле. Принимает строку с именем класса, в котором определён тип возвращаемого значения метода.
Опциональное поле. Массив строк, который задаёт свойства возвращаемого значения метода.
Возможные атрибуты возвращаемого значения
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
not_apply_to_child_class |
Аннотация не будет применяться для методов, тип возвращаемого значения которых является дочерним для проаннотированного типа. |
2 |
always_taint |
Метод возвращает taint-данные. |
3 |
transfer_annotations_from_caller |
Если вызывающий объект содержит аннотацию, она будет перенесена на возвращаемое значение метода. |
Примечание. Объект аннотации конструктора должен содержать хотя бы одно опциональное поле.
Объект аннотации конструктора состоит из следующих полей:
Обязательное поле. Принимает строку со значением ctor.
Обязательное поле. Принимает строку с именем пространства имён, которое содержит конструктор.
Обязательное поле. Принимает строку с именем класса, в котором определён конструктор.
Опциональное поле. Массив строк, который задаёт свойства сущности.
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
not_apply_to_child_class |
Аннотация не будет применяться для дочерних реализаций проаннотированного конструктора. |
2 |
create_taint_object |
Созданный конструктором объект — taint. |
Опциональное поле. Данное поле описано в разделе "Аннотации параметров".
Объект аннотации свойства состоит из следующих полей:
Обязательное поле. Принимает строку со значением property.
Обязательное поле. Принимает строку с именем пространства имён, которое содержит свойство.
Обязательное поле. Принимает строку с именем класса, в котором определено свойство.
Опциональное поле. Массив строк, который задаёт свойства сущности.
Примечание. К каждому из атрибутов taint-приёмников прикреплена ссылка на соответствующую диагностику.
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
not_apply_to_child_class |
Аннотация не будет применяться при обращении к проаннотированному свойству у объекта дочернего класса. |
2 |
transfer_annotation_to_return_value |
Если на вызывающем объекте есть аннотация, то она будет перенесена на возвращаемое значение. |
3 |
transfer_annotation_to_caller |
Если свойству присваивается значение, то аннотации этого значения будут перенесены на вызывающий объект свойства. |
4 |
return_taint |
Свойство возвращает taint-данные. |
5 |
sql_injection_target |
Запись в это свойство заражённых данных приводит к SQL Injection (V5608). |
6 |
path_traversal_target |
Запись в это свойство заражённых данных приводит к Path Traversal (V5609). |
7 |
xss_injection_target |
Запись в это свойство заражённых данных приводит к XSS Injection (V5610). |
8 |
insecure_deserialization_target |
Запись в это свойство заражённых данных приводит к Insecure Deserialization (V5611). |
9 |
command_injection_target |
Запись в это свойство заражённых данных приводит к Command Injection (V5616). |
10 |
ssrf_target |
Запись в это свойство заражённых данных приводит к Server-Side Request Forgery (V5618). |
11 |
log_injection_target |
Запись в это свойство заражённых данных приводит к Log Injection (V5619). |
12 |
ldapi_injection_target |
Запись в это свойство заражённых данных приводит к LDAP Injection (V5620). |
13 |
xpath_injection_target |
Запись в это свойство заражённых данных приводит к XPath Injection (V5622). |
14 |
open_redirect_target |
Запись в это свойство заражённых данных приводит к Open Redirect (V5623). |
15 |
configuration_attack_target |
Запись в это свойство заражённых данных приводит к Configuration Attack (5624). |
16 |
nosql_injection_target |
Запись в это свойство заражённых данных приводит к NoSQL Injection (V5627). |
17 |
redos_target |
Запись в это свойство заражённых данных приводит к ReDoS (V5626). |
18 |
zipslip_target |
Запись в это свойство заражённых данных приводит к ZipSlip (V5628). |
Примечание 1. Объект аннотации параметра может находиться только внутри массива 'params', объекта аннотации метода или конструктора.
Примечание 2. Объект аннотации параметра должен либо содержать поля 'namespace_name' и 'type_name', либо оба поля должны отсутствовать (или иметь значение null).
Объект аннотации параметра состоит из следующих полей:
Обязательное поле. Принимает строку с именем пространства имён, которое содержит тип параметра.
Обязательное поле. Принимает строку с именем класса, в котором определён тип параметра.
Опциональное поле. Массив строк, который задаёт свойства сущности.
Примечание. К каждому из атрибутов taint-приёмников и taint-валидации прикреплена ссылка на соответствующую диагностику.
# |
Название атрибута |
Описание атрибута |
---|---|---|
1 |
ignore_current_and_next |
При подборе соответствующей аннотации не будут учитываться текущий и следующие параметры (данная аннотация может быть только у последнего аргумента). |
2 |
transfer_annotation_to_return_value |
Если параметр содержит аннотацию, она будет перенесена на возвращаемое значение метода. |
3 |
object_creation_infector |
Заражение нового созданного объекта происходит через этот параметр (актуально только для конструкторов). |
4 |
sql_injection_target |
Передача в этот параметр заражённых данных приводит к SQL Injection (V5608). |
5 |
sql_injection_validation |
Вызов метода сбрасывает SQL Injection taint-статус для данного параметра (V5608). |
6 |
path_traversal_target |
Передача в этот параметр заражённых данных приводит к Path Traversal (V5609). |
7 |
path_traversal_validation |
Вызов метода сбрасывает Path Traversal taint-статус для данного параметра (V5609). |
8 |
xss_injection_target |
Передача в этот параметр заражённых данных приводит к XSS Injection (V5610). |
9 |
xss_injection_validation |
Вызов метода сбрасывает XSS Injection taint-статус для данного параметра (V5610). |
10 |
insecure_deserialization_target |
Передача в этот параметр заражённых данных приводит к Insecure Deserialization (V5611). |
11 |
insecure_deserialization_validation |
Вызов метода сбрасывает Insecure Deserialization taint-статус для данного параметра (V5611). |
12 |
command_injection_target |
Передача в этот параметр заражённых данных приводит к Command Injection (V5616). |
13 |
command_injection_validation |
Вызов метода сбрасывает Command Injection taint-статус для данного параметра (V5616). |
14 |
xml_source_to_parse |
Параметр - источник XML, который будет парситься. Это может быть сам XML-файл, путь до него, поток с XML-файлов, парсер, содержащий поток XML-файла, и т. п. (V5614, V5615). |
15 |
transfer_xml_settings_to_return |
Передаёт настройки XML-парсера из этого аргумента в возвращаемое значение. (V5614, V5615). |
16 |
ssrf_target |
Передача в этот параметр заражённых данных приводит к Server-Side Request Forgery (V5618). |
17 |
ssrf_validation |
Вызов метода сбрасывает Server-Side Request Forgery taint-статус для данного параметра (V5618). |
18 |
log_injection_target |
Передача в этот параметр заражённых данных приводит к Log Injection (V5619). |
19 |
log_injection_validation |
Вызов метода сбрасывает Log Injection taint-статус для данного параметра (V5619). |
20 |
ldapi_injection_target |
Передача в этот параметр заражённых данных приводит к LDAP Injection (V5620). |
21 |
ldapi_injection_validation |
Вызов метода сбрасывает LDAP Injection taint-статус для данного параметра (V5620). |
22 |
xpath_injection_target |
Передача в этот параметр заражённых данных приводит к XPath Injection (V5622). |
23 |
xpath_injection_validation |
Вызов метода сбрасывает XPath Injection taint-статус для данного параметра (V5622). |
24 |
open_redirect_target |
Передача в этот параметр заражённых данных приводит к Open Redirect (V5623). |
25 |
open_redirect_validation |
Вызов метода сбрасывает Open Redirect taint-статус для данного параметра (V5623). |
26 |
configuration_attack_target |
Передача в этот параметр заражённых данных приводит к Configuration Attack (5624). |
27 |
configuration_attack_validation |
Вызов метода сбрасывает Configuration Attack taint-статус для данного параметра (5624). |
28 |
nosql_injection_target |
Передача в этот параметр заражённых данных приводит к NoSQL Injection (V5627). |
29 |
nosql_injection_validation |
Вызов метода сбрасывает NoSQL Injection taint-статус для данного параметра (V5627). |
30 |
redos_target |
Строка, которая разбирается с помощью регулярного выражения. Передача в этот параметр заражённых данных приводит к ReDoS, если регулярное выражение небезопасно (V5626). |
31 |
redos_validation |
Вызов метода сбрасывает ReDoS taint-статус для данного параметра (V5626). |
32 |
zipslip_target |
Строка, которая может быть использована как путь для извлечения файла из архива. Передача в этот параметр заражённых данных приводит к ZipSlip (V5628). |
33 |
zipslip_validation |
Вызов метода сбрасывает ZipSlip taint-статус для данного параметра (V5628). |
34 |
regex |
Параметр является регулярным выражением. |
Чтобы проигнорировать тип параметра, не нужно указывать поля 'namespace_name' и 'type_name' или нужно записать в оба поля null.
Схема JSON поставляется в дистрибутиве или доступна по ссылке.
Рассмотрим метод:
namespace MyNamespace
{
public class MyClass
{
public string GetUserInput()
{
....
}
}
}
Допустим, данный метод возвращает пользовательский ввод, который может содержать taint-данные. Аннотация, которая позволит анализатору понять это, будет выглядеть следующим образом:
{
"version": 1,
"language": "csharp",
"annotations": [
{
"type": "method",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"method_name": "GetUserInput",
"returns": {
"attributes": [ "always_taint" ]
}
}
]
}
Рассмотрим конструктор:
namespace MyNamespace
{
public class MyClass
{
public MyClass()
{
....
}
}
}
Допустим, данный конструктор создаёт объект, который может содержать taint-данные. Аннотация, которая позволит анализатору понять это, будет выглядеть следующим образом:
{
"version": 1,
"language": "csharp",
"annotations": [
{
"type": "ctor",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"attributes": [ "create_taint_object" ]
}
]
}
Рассмотрим свойство:
namespace MyNamespace
{
public class MyClass
{
public string UserInput
{
get
{
....
}
}
}
}
Допустим, данное свойство возвращает пользовательский ввод, который может содержать taint-данные. Аннотация, которая позволит анализатору понять это, будет выглядеть следующим образом:
{
"version": 1,
"language": "csharp",
"annotations": [
{
"type": "property",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"property_name": "UserInput",
"attributes": [ "return_taint" ]
}
]
}
Примечание. В качестве примера используется аннотация метода. Игнорирование типа параметров конструктора производится аналогичным образом (не указывать 'type_name' и 'namespace_name' у аннотации параметра).
Рассмотрим две перегрузки метода 'GetUserInput':
namespace MyNamespace
{
public class MyClass
{
public string GetUserInput(string str)
{
....
}
public string GetUserInput(int index)
{
....
}
}
}
Допустим, данный метод возвращает пользовательский ввод, который может содержать taint-данные, вне зависимости от типа параметра. Аннотация, которая позволит анализатору понять это, будет выглядеть следующим образом:
{
"version": 1,
"language": "csharp",
"annotations": [
{
"type": "method",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"method_name": "GetUserInput",
"params": [
{ }
],
"returns": {
"attributes": [ "always_taint" ]
}
}
]
}
В данном случае для первого параметра нет аннотаций. Также при подборе аннотации метода неважно, какой тип будет иметь первый параметр. Поэтому аннотация параметра представлена пустым объектом.
Примечание. В качестве примера используется аннотация метода. Игнорирование параметров конструктора производится аналогичным образом (с помощью аннотации 'ignore_current_and_next').
Рассмотрим две перегрузки метода 'GetUserInput':
namespace MyNamespace
{
public class MyClass
{
public string GetUserInput(string str)
{
....
}
public string GetUserInput(string str, bool flag1, bool flag2)
{
....
}
}
}
Допустим, данный метод возвращает пользовательский ввод, который может содержать taint-данные, если использована перегрузка с одним или более параметрами. Также, если в первый параметр передать taint-данные, возникнет SQL injection. Аннотация, которая позволит анализатору понять это, будет выглядеть следующим образом:
{
"version": 1,
"language": "csharp",
"annotations": [
{
"type": "method",
"namespace_name": "MyNamespace",
"type_name": "MyClass",
"method_name": "GetUserInput",
"params": [
{
"namespace_name": "System",
"type_name": "String",
"attributes": [ "sql_injection_target" ]
},
{
"attributes": [ "ignore_current_and_next" ]
}
],
"returns": {
"attributes": [ "always_taint" ]
}
}
]
}
Для второго параметра есть аннотация 'ignore_current_and_next'. Она позволяет игнорировать количество параметров (включая проаннотированный параметр) при обработке аннотации.
Наряду со многими другими способами фильтрации и подавления диагностических сообщений, в PVS-Studio есть предопределённый макрос PVS_STUDIO.
Первый случай, когда он может пригодиться — чтобы какой-то код не попадал в анализатор на проверку. Например, анализатор выдаёт диагностическое сообщение на такой код:
int rawArray[5];
rawArray[-1] = 0;
Однако, если его "обернуть" с помощью этого макроса, то сообщения не будет:
int rawArray[5];
#ifndef PVS_STUDIO
rawArray[-1] = 0;
#endif
Второй случай — переопределение стандартных и пользовательских макросов. Например, на следующем коде будет выдано предупреждение о разыменовании потенциально нулевого указателя:
char *st = (char*)malloc(10);
TEST_MACRO(st != NULL);
st[0] = '\0'; //V522
Чтобы подсказать анализатору, что выполнение программы прерывается при неверном условии, можно переопределить макрос следующим образом:
#ifdef PVS_STUDIO
#undef TEST_MACRO
#define TEST_MACRO(expr) if (!(expr)) throw "PVS-Studio";
#endif
char *st = (char*)malloc(10);
TEST_MACRO(st != NULL);
st[0] = '\0';
Этот способ позволяет устранить предупреждения анализатора на коде, тестируемом с помощью разных библиотек, а также на любых других макросах, использующихся для отладки и тестирования.
См. также обсуждение "Mark variable as not NULL after BOOST_REQUIRE in PVS-Studio" на сайте StackOverflow.com.
Начиная с версии 7.30, в PVS-Studio были добавлены макросы PVS_STUDIO_MAJOR и PVS_STUDIO_MINOR, которые отражают мажорную и минорную версии ядра анализатора.
При помощи этих макросов можно настраивать поведение анализа для блока кода в зависимости от версии анализатора. Например, можно включать или отключать диагностические правила только для определённых версий PVS-Studio:
// Auxiliary macros
#if defined(PVS_STUDIO) \
&& defined(PVS_VERSION_MAJOR) \
&& defined(PVS_VERSION_MINOR) \
#define PVS_VERSION_MACROS_INTRODUCED 1
#define PVS_MAKE_VERSION(major, minor) ( ((major) << 16) | (minor) )
#define PVS_CURRENT_VERSION \
PVS_MAKE_VERSION(PVS_VERSION_MAJOR, PVS_VERSION_MINOR)
#else
#define PVS_VERSION_MACROS_INTRODUCED 0
#endif
// ....
// Need to disable V691 on code block
#if PVS_VERSION_MACROS_INTRODUCED
#if PVS_CURRENT_VERSION < PVS_MAKE_VERSION(7, 35)
#pragma pvs(push)
#pragma pvs(disable: 591)
#endif
#endif
// code block
#if PVS_VERSION_MACROS_INTRODUCED
#if PVS_CURRENT_VERSION < PVS_MAKE_VERSION(7, 35)
#pragma pvs(pop)
#endif
#endif
Макросы PVS_STUDIO, PVS_STUDIO_MAJOR и PVS_STUDIO_MINOR будут автоматически подставлены при проверке кода из IDE и утилит PVS-Studio_Cmd.exe, pvs-studio-analyzer / CompilerCommandsAnalyzer. Если же для проверки проекта вы используете прямой вызов ядра анализатора, то по умолчанию макросы не передаются анализатору, и это нужно сделать вручную.
Многие инструменты PVS-Studio используют XML-файл для хранения конфигурации.
Такой файл может использоваться неявно (глобальный файл Settings.xml, расположенный в специальном каталоге), или передаваться явно инструментам PVS-Studio через специальный флаг.
По умолчанию инструменты PVS-Studio используют глобальный файл конфигурации Settings.xml, расположенный:
Глобальный файл конфигурации используется практически всеми инструментами PVS-Studio, если им не передан альтернативный файл. Ниже перечислены инструменты, которые не используют глобальный файл конфигурации:
Примечание: Плагины PVS-Studio для Visual Studio, Rider и CLion, а также утилита C and C++ Compiler Monitoring UI используют только глобальный файл Settings.xml.
Вы можете вынести настройки анализа для проекта в специальный XML-файл, который затем может быть передан консольным инструментам PVS-Studio через специальный флаг.
Обратите внимание: чтобы не допустить ошибку при написании файла конфигурации, мы рекомендуем скопировать глобальный файл Settings.xml и модифицировать его.
Ниже приведены примеры запуска утилит со специальным файлом конфигурации CustomSettings.xml (вы можете назвать его так, как вам удобно).
Для утилиты PVS-Studio_Cmd.exe:
PVS-Studio_Cmd.exe -t "path\to\Solution.sln" ... \
--settings "path\to\CustomSettings.xml"
CLMonitor.exe [analyze|monitor|analyzeFromDump|trace] ... \
--settings "\path\to\CustomSettings.xml"
Для утилиты BlameNotifier.exe:
BlameNotifier.exe "path\to\PVS-Studio.plog" \
--VCS Git \
--recipientsList "path\to\recipients.txt" \
--server ... --sender ... \
--login ... --password ... \
--settings "path\to\CustomSettings.xml"
Для утилиты PlogConverter.exe (only Windows):
PlogConverter.exe -t json ... --settings "path\to\CustomSettings.xml" \
"path\to\PVS-Studio.plog"
Файл конфигурации имеет формат XML со следующей структурой:
<?xml version="1.0" encoding="utf-8"?>
<ApplicationSettings>
...
<Tag>Value</Tag>
...
</ApplicationSettings>
Вместо Tag записывается идентификатор опции (например, FileMasks), а вместо Value — её значение. Описание доступных опций и их значений будет приведено ниже.
Значения опций могут быть следующих типов:
Далее будет дано описание ключевых опций файла конфигурации и области их применения.
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion.
Тег описывает список файловых масок, которые будут применены для исключения файлов из анализа. Маски данного типа используются только для фильтрации по именам файлов, без учета директорий. Вы можете указать как полное имя файла, так и маску, используя wildcard-символы: '*' - любое число символов, '?' - один любой символ.
Тег FileMasks принимает список тегов <string>, каждый из которых содержит файловую маску.
Например, следующая запись исключает все файлы SourceTest.cpp и C*Test.cpp из анализа:
<FileMasks>
<string>SourceTest.cpp</string>
<string>C*Test.cpp</string>
</FileMasks>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion.
Тег описывает список Path-масок, используемых для исключения каталогов из анализа. Маски данного типа позволяют исключить файлы из анализа на основе их расположения.
Тег PathMasks принимает список тегов <string>, каждый из которых содержит маску пути.
Например, следующая запись исключит все cpp и hpp файлы, расположенные в каталогах ThirdParty и Tests:
<PathMasks>
<string>*/ThirdParty/*.?pp</string>
<string>*/Tests/*.?pp</string>
</PathMasks>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio.
Данная опция отключает синхронизацию suppress-файлов между проектами одного решения. Это может быть полезно, если вы хотите, чтобы сообщение, подавленное в одном проекте, не подавлялось в других.
Тег DisableSynchronizeSuppressFiles может принимать одно из двух значений: true, false. Если установлено значение true, то автоматическая синхронизация suppress-файлов отключена. По умолчанию устанавливается значение false.
Пример:
<DisableSynchronizeSuppressFiles>true</DisableSynchronizeSuppressFiles>
Используется в: PlogConverter.exe (Windows), BlameNotifier, IDE плагины для Visual Studio и Rider
Тег задаёт список диагностик, которые должны быть скрыты в отчёте при его просмотре в IDE плагине PVS-Studio.
Тег DisableDetectableErrors принимает список диагностических правил, разделённый пробелами. Обратите внимание, что после последнего кода должен быть также указан пробел.
Данная опция используется утилитами PlogConverter.exe (Windows) и BlameNotifier ещё и для исключения предупреждений из результирующего отчёта.
Например, можно использовать такую запись, чтобы исключить не интересующие вас диагностики при конвертации отчёта с помощью PlogConverter.exe:
<DisableDetectableErrors>V126 V203 V2001 V2006 </DisableDetectableErrors>
Таким образом, вы можете передать файл *.xml в PlogConverter.exe через флаг ‑‑setting и получить отчёт без предупреждений V126, V203, V2001, V2006.
Используется в: PVS-Studio_Cmd, CLMonitor, BlameNotifier, IDE плагины для Visual Studio, Rider и СLion.
Тег задает имя, необходимое для проверки валидности лицензии.
Примечание: вы можете игнорировать данный тег в специальном файле настроек, если вы уже выполняли активацию. Тогда PVS-Studio будет искать лицензионные данные в глобальном файле Settings.xml.
Пример:
<UserName>Name</UserName>
Используется в: PVS-Studio_Cmd, CLMonitor, BlameNotifier, IDE плагины для Visual Studio, Rider и СLion.
Тег задаёт лицензионный ключ для проверки валидности лицензии.
Примечание: вы можете игнорировать данный тег в специальном файле настроек, если вы уже выполняли активацию. Тогда PVS-Studio будет искать лицензионные данные в глобальном файле Settings.xml.
Пример:
<SerialNumber>XXXX-XXXX-XXXX-XXXX</SerialNumber>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion.
Данный тег включает/выключает автоматическое удаление временных файлов анализатора, таких как файлы конфигурации ядра анализатора и препроцессированные файлы.
Тег RemoveIntermediateFiles принимает значение true или false. Если true, все временные файлы будут удалены.
По умолчанию принимает значение true. Сохранение временных файлов бывает полезным при исследовании проблем работы анализатора.
Пример:
<RemoveIntermediateFiles>false</RemoveIntermediateFiles>
Используется в: PVS-Studio_Cmd, IDE плагины для Visual Studio.
Тег позволяет включать отображение источников подавления предупреждений. Опция помогает узнать, какие механизмы учaствуют в отключении диагностик.
Тег ReportDisabledRules принимает значение true или false. Если true, то отчёт будет содержать сообщения с кодом V012 и информацией об источниках отключения диагностик.
По умолчанию установлено значение false.
Пример:
<ReportDisabledRules>true</ReportDisabledRules>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio и СLion.
Тег отключает группу 64-битных диагностик. Диагностические правила из отключённой группы не будут применены.
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<Disable64BitAnalysis>true</Disable64BitAnalysis>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio и СLion.
Тег отключает группу диагностик общего назначения для C и C++ (General Analysis).
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableGAAnalysis>true</DisableGAAnalysis>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion.
Тег отключает группу диагностик микрооптимизаций для C и C++.
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableOPAnalysis>true</DisableOPAnalysis>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion.
Тег отключает группу специфичных диагностических правил, добавленных по просьбе пользователей (Customer Specific), для C и C++.
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableCSAnalysis>true</DisableCSAnalysis>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio и СLion.
Тег отключает группу диагностик, проверяющих C и C++ код на соответствие стандартам MISRA.
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableMISRAAnalysis>true</DisableMISRAAnalysis>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio и СLion.
Тег отключает группу диагностик, проверяющих C++ код на соответствие стандартам AUTOSAR.
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableAUTOSARAnalysis>true</DisableAUTOSARAnalysis>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio и СLion.
Тег отключает группу диагностик, проверяющих C и C++ код на соответствие стандарту OWASP ASVS.
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableOWASPAnalysis>true</DisableOWASPAnalysis>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio и Rider.
Тег отключает группу диагностик, проверяющих C# код на соответствие стандарту OWASP ASVS.
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableOWASPAnalysisCs>true</DisableOWASPAnalysisCs>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio и Rider.
Тег отключает группу диагностик общего назначения для C# (General Analysis).
Если установлено значение true, все диагностики этой группы будут отключены.
Пример:
<DisableGAAnalysisCs>true</DisableGAAnalysisCs>
Используется в: PVS-Studio_Cmd, IDE плагины для Visual Studio.
Включение данной настройки позволяет выполнять действия, записанные в секции Custom Build Step проектного файла Visual Studio (vcproj/vcxproj), перед запуском анализа. Заметим, что анализатору для корректной работы необходим компилирующийся код. Так, например, если Custom Build Step используется для генерации *.h файлов перед компиляцией, его необходимо будет выполнить (включив данную настройку) и перед анализом проекта.
Тег PerformPreBuildStep принимает значение true/false.
Если значение true, настройка включена.
Пример:
<PerformPreBuildStep>true</PerformPreBuildStep>
Используется в: PVS-Studio_Cmd, CLMonitor, BlameNotifier, IDE плагины для Visual Studio.
Тег включает автоматический импорт настроек (xml-файлов) из каталога '%APPDATA%\PVS-Studio\SettingsImports'. При импорте настройки-флаги (true\false), а также настройки, хранящие одно значение (например, строку), заменяются настройками из 'SettingsImports'. Настройки, имеющие несколько значений (например, исключаемые директории), объединяются.
При наличии в папке 'SettingsImports' нескольких xml-файлов, эти файлы будут применены к текущим настройкам последовательно по порядку, в соответствии с их именем.
Тег AutoSettingsImport принимает значение true/false.
Если значение true, автоматический импорт включён.
Пример:
<AutoSettingsImport>true</AutoSettingsImport>
Используется в: PVS-Studio_Cmd, IDE плагины для Visual Studio.
Тег отключает генерацию предупреждений низкого уровня достоверности.
Если установлено значение true, в отчёт не попадут сообщения 3-го уровня достоверности.
Пример:
<NoNoise>false</NoNoise>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion
Тег устанавливает количество параллельно запускаемых процессов ядра анализатора, которые будут задействованы при анализе. По умолчанию устанавливается значение, равное количеству ядер процессора.
Обратите внимание, что не рекомендуется задавать значение данной настройки больше, чем количество процессорных ядер. Также стоит учесть, что для не Enterprise-лицензий действует ограничение в 16 ядер.
Пример:
<ThreadCount>8</ThreadCount>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion.
Опция позволяет указать корневую часть пути до исходных файлов в отчёте анализатора. При формировании отчёта корневая часть будет заменена на специальный маркер |?|. По умолчанию данная опция пуста и анализатор всегда генерирует отчёт с абсолютными путями до файлов. Настройка позволяет получить отчёт анализатора с относительными путями, который затем можно использовать на машинах с отличающимся расположением проверяемых исходных файлов.
Пример:
<SourceTreeRoot>D:\ProjectRoot\</SourceTreeRoot>
Подробнее об этом можно узнать в разделе документации "Использование относительных путей в файлах отчётов PVS-Studio".
Используется в: PVS-Studio_Cmd, IDE плагины для Visual Studio, Rider и СLion.
Тег позволяет включить использование каталога решения в качестве значения SourceTreeRoot.
Если значение true, то часть пути, содержащая путь до каталога решения, будет заменена на специальный маркер, параметр SourceTreeRoot будет проигнорирован.
Пример:
<UseSolutionDirAsSourceTreeRoot>false</UseSolutionDirAsSourceTreeRoot>
Используется в: PVS-Studio_Cmd, CLMonitor, IDE плагины для Visual Studio, Rider и СLion.
Тег задает время, по истечению которого анализ файла будет прерван. Параметр может принимать следующие значения:
Обратите внимание, что снятие временного ограничения может привести к зависанию анализа.
Пример:
<AnalysisTimeout>After_10_minutes</AnalysisTimeout>
Используется в: PVS-Studio_Cmd, IDE плагины для Visual Studio, Rider и СLion.
Тег задаёт ограничение по времени для инкрементального анализа. Возможные значения:
Обратите внимание, что снятие ограничения может привести к зависанию анализа.
Пример:
<IncrementalAnalysisTimeout>After_2_minutes</IncrementalAnalysisTimeout>
Используется в: PVS-Studio_Cmd, IDE плагин для Visual Studio.
Тег включает добавление маркировки по ГОСТ Р 71207-2024 в поле сообщений SAST. Возможные значения:
Пример:
<AnalysisTimeout>true</AnalysisTimeout>
При разработке PVS-Studio простота использования ставилась во главу угла. Мы учли наш опыт общения с традиционными lint-подобными анализаторами кода. Именно поэтому возможность сразу же приступить к работе является одним из основных преимуществ PVS-Studio по сравнению с другими анализаторами кода. Кроме того, PVS-Studio проектировалась так, чтобы разработчику, который будет использовать анализатор, вообще не пришлось его настраивать. Эту задачу удалось решить: разработчик имеет мощнейший анализатор кода, который нет необходимости настраивать при первом запуске.
Однако надо понимать, что анализатор кода - это мощный инструмент, который нуждается в грамотном применении. Именно грамотное применение анализатора (благодаря системе настроек) позволяет добиться по-настоящему значительных результатов. Работа с анализатором кода предполагает, что есть инструмент (программа), которая делает рутинную работу по поиску потенциально опасных конструкций в коде, и есть мастер (разработчик), который на основе знаний о проверяемом проекте может принимать те или иные решения. Так, например, разработчик может сообщить анализатору о том, что:
Корректная настройка данных параметров может во много раз уменьшить количество диагностических сообщений, выдаваемых анализатором кода. Это означает, что если разработчик поможет анализатору и сообщит ему некоторую дополнительную информацию в виде настроек, то анализатор, в свою очередь, значительно сократит количество мест, на которые разработчик должен будет обратить внимание при анализе результатов проверки.
Настройки PVS-Studio можно открыть через команду главного меню IDE: PVS-Studio -> Options. Выбрав эту команду, Вы увидите диалог настройки PVS-Studio.
Каждая страница настроек подробно описана в документации к PVS-Studio.
На вкладке общих настроек анализатора кода указываются настройки, не зависящие от конкретного используемого модуля анализа.
Анализатор может автоматически проверять наличие своей новой версии на сайте pvs-studio.com. Для этого используется собственный модуль обновления.
В случае, если опция CheckForNewVersions имеет значение True, то при запуске проверки кода (команды Check Current File, Check Current Project, Check Solution меню PVS-Studio) выполняется загрузка специального текстового файла с сайта pvs-studio.com. В этом файле указан номер самой последней версии PVS-Studio, доступной на сайте. Если версия на сайте окажется новее, чем версия, установленная у пользователя, то пользователь увидит запрос на обновление. В случае разрешения этого обновления, будет запущено специальное отдельное приложение PVS-Studio-Updater, которое автоматически загрузит новый дистрибутив PVS-Studio с сайта и запустит его установку. В случае, если опция CheckForNewVersions установлена в False, проверка новой версии производиться не будет.
Анализ файлов выполняется быстрее на многоядерных машинах. Так, на четырех-ядерном компьютере анализатор может использовать все четыре ядра для своей работы. Однако, если по каким-то причинам надо ограничить количество используемых ядер, то это вполне можно сделать, указав допустимое количество. По умолчанию устанавливается значение, равное количеству ядер процессора.
Задание для настройки 'ThreadCount' значения, большего '16' (или большего, чем количество ядер процессора, если у процессора более 16 ядер), доступно только при наличии Enterprise лицензии. Вы можете запросить пробную Enterprise лицензию здесь.
При запуске анализа на одной машине, мы не рекомендуем задавать значение этой настройки более, чем количество процессорных ядер. Задание большего значения может замедлить анализ в целом. Если вы хотите запускать больше одновременных задач анализа, вы можете воспользоваться системой распределённой сборки, например, Incredibuild. Подробнее про такой режим использования PVS-Studio можно почитать в соответствующем разделе документации.
Для своей работы анализатор создает большое количество временных командных файлов для запуска собственно модуля анализа, выполнения препроцессинга и управления общим процессом анализа. Подобные файлы создаются для каждого анализируемого файла проекта. Обычно они не представляют интереса для пользователя и поэтому после анализа сразу же удаляются. Однако, в некоторых случаях может быть полезно посмотреть на эти файлы. Поэтому можно указать анализатору не удалять их. В таком случае можно будет запускать анализатор вне IDE из командной строки.
Данная страница настроек позволяет управлять отображением типов сообщений анализатора в списке результатов PVS-Studio.
Все диагностические предупреждения анализатора разделены на несколько групп. Отображением каждого типа сообщений (показывать/скрывать) можно управлять по отдельности, а для всей группы сообщений можно выполнить следующие действия:
Иногда бывает целесообразно убрать в отчете ошибки с определенными кодами. Например, если вы уверены, что ошибки с кодом V505 и V506 вас не интересуют, то вы можете скрыть их показ, сняв галочку в соответствующей строчке.
Обратите внимание! При включении "Show All" и "Hide All" нет необходимости перезапускать анализ проекта заново. Анализатор всегда выдаёт все типы ошибок, при этом показ тех или иных ошибок задается настройками на этой странице. При включении/выключении отображения ошибок, они сразу будут показаны/скрыты в отчете без перезапуска анализа всего проекта.
Полное отключение группы диагностик (Disabled) можно использовать для того, чтобы ускорить работу анализатора и получить файлы с отчетами (plog-файлы) меньшего размера.
На вкладке "Don't Check Files" настроек можно ввести файловые маски для исключения некоторых файлов или папок из анализа. Анализатор не будет проверять файлы, удовлетворяющие условиям масок.
Например, подобным образом можно исключить из анализа автогенерируемые файлы. Кроме того, можно исключить файлы по имени папки, в которой они находятся.
Маска задается с помощью специальных wildcard символов: "*" (раскрывается в любое количество произвольных символов) и "?" (раскрывается в один произвольный символ).
Регистр символов не имеет значения. Символ "*" может быть добавлен в любой части маски. После задания масок вида '*\mask\*', сообщения из соответствующих им файлов исчезнут из окна вывода PVS-Studio, а в следующую проверку они включены уже не будут. Таким образом, исключение файлов и директорий посредством масок может позволить существенно сократить общее время анализа всего проекта.
Примечание: маски вида 'a*b' будут применены только после перезапуска анализа.
В окне можно задавать 2 типа масок: маски по путям (Path Mask) и маски по именам файлов (FileName Mask). Маски, заданные в списке FileNameMasks, используются для фильтрации сообщений только непосредственно по именам файлов, без учёта директории, в которой эти файлы находятся. Маски из списка PathMasks фильтруют диагностические сообщения с учётом расположения фалов в файловой системе на диске и позволяют подавлять сообщения как для отдельных файлов, так и для целых директорий и поддиректорий. Так, для фильтрации сообщений в одном конкретном файле, полный путь до него необходимо добавить в список PathMasks, а для фильтрации всех файлов с одинаковыми (либо удовлетворяющими wildcard маске) именами можно добавить такое имя или маску в список FileNameMasks.
Примеры допустимых масок для списка FileNameMasks:
Примеры допустимых масок для списка PathMasks:
Примечание. Если в PathMasks символы "*" не указаны, они все равно будут автоматически добавлены.
На вкладке подавления отдельных сообщений по ключевым словам можно настроить фильтрацию ошибок по содержащемуся в них тексту.
При необходимости можно скрыть из отчета сообщения о диагностированных ошибках, содержащих определенные слова или фразы. Например, если в отчете есть ошибки, в которых указаны названия функций printf и scanf, а вы считаете, что ошибок, связанных с ними быть не может, то просто добавьте эти два слова с помощью редактора подавляемых сообщений.
Обратите внимание! При изменении списка скрываемых сообщений нет необходимости перезапускать анализ проекта заново. Анализатор всегда генерирует все диагностические сообщения, при этом показ тех или иных диагностических сообщений управляется с помощью этой страницы настроек. При модификации фильтров сообщений изменения сразу же покажутся в отчете без перезапуска анализа всего проекта.
Откройте страницу настроек PVS-Studio (PVS-Studio Menu -> Options...).
На вкладке регистрации вводится лицензионная информация.
После покупки анализатора вы получите регистрационную информацию: имя и серийный номер. Эти данные должны быть введены на этой странице. При этом в поле LicenseType будет указан режим лицензирования.
Информацию по условиям лицензирования смотрите на странице заказа на сайте.
На вкладке "Specific Analyzer Settings" размещены дополнительные расширенные настройки анализатора.
Данная настройка позволяет задать лимит времени, по истечении которого анализ отдельных файлов завершается с ошибкой V006. File cannot be processed. Analysis aborted by timeout, или совсем отключить прерывание анализа по лимиту времени. Перед изменением данной настройки мы рекомендуем внимательно ознакомиться с описанием ошибки, приведенным по указанной выше ссылке. Часто бывает, что таймаут возникает из-за нехватки оперативной памяти. В этом случае рационально не увеличивать время, а уменьшить количество используемых параллельных потоков. Это может дать существенный прирост производительности, когда процессорных ядер много, а оперативной памяти мало.
Данная настройка позволяет задать лимит времени, по истечении которого инкрементальный анализ файлов будет остановлен. Все предупреждения, обнаруженные на момент остановки анализа, будут выданы в окно PVS-Studio. Дополнительно к ним будет выдано предупреждение о том, что анализатор не успел обработать все изменённые файлы, а также информация суммарном и проанализированном количестве файлов.
Данная опция актуальна только при работе из IDE Visual Studio.
Данная настройка отключает инкрементальный анализ исходных файлов кода на C и C++, затрагиваемых изменениями в предварительно компилируемом заголовочном файле и заголовочных файлах, включенных в него. Настройка полезна в ситуациях, когда эти файлы часто меняются и из-за этого время инкрементального анализа значительно увеличивается.
При изменении настройки из IDE Visual Studio изменения вступят в силу после следующего открытия решения или проекта. Если анализ запускается через консольную утилиту PVS-Studio_Cmd, то анализатор сразу задействует новое значение настройки.
При работе на большой кодовой базе, анализатор неизбежно генерирует большое количество предупреждений. При этом, часто нет возможности поправить все предупреждения сразу. Для того, чтобы иметь возможность сконцентрироваться на правке наиболее важных предупреждений, можно сделать анализ менее "шумным" с помощью данной настройки. Она позволяет полностью отключить генерацию предупреждений низкого уровня достоверности (Low Certainty, 3-ий уровень предупреждений). После перезапуска анализа, сообщения этого уровня полностью пропадут из вывода анализатора.
Когда обстоятельства позволят и более существенные предупреждения анализатора будут исправлены, можно выключить режим No Noise – при следующем анализе все пропавшие ранее предупреждения станут вновь доступны.
Включение данной настройки позволяет анализатору учитывать информацию не только из анализируемого файла, но и из связанных с ним файлов. Это позволяет производить более глубокий и качественный анализ. Однако, на сбор необходимой информации требуется дополнительное время, что отразится на времени анализа вашего проекта.
Данный режим актуален для C и C++ проектов. C# проекты обеспечивают межмодульный анализ по умолчанию.
Начиная с версии PVS-Studio 7.26 настройка убрана. Теперь для запуска межмодульного анализа нужно использовать меню запуска анализа.
Включение данной настройки позволяет выполнять действия, записанные в секции Custom Build Step проектного файла Visual Studio (vcproj/vcxproj), перед запуском анализа. Заметим, что анализатору для корректной работы необходим компилирующийся код. Так, например, если Custom Build Step используется для генерации h файлов перед компиляцией, его необходимо будет выполнить (включив данную настройку) и перед анализом проекта. Если же на данном шаге выполняются действия, относящиеся, например, к линковке, они не повлияют на результат работы анализатора. Действия Custom Build Step задаются на уровне проекта и будут выполнены PVS-Studio при первоначальном обходе дерева проектов. Если данная настройка включена и в результате её выполнения получен ненулевой код, проверка такого проекта не будет запущена.
Включение данной настройки позволяет автоматически выполнять checkout с использованием Team Foundation Version Control Tool при редактировании файлов, содержащих подавленные предупреждения анализатора (.suppress файлы). Включение данной настройки не повлияет на работу с проектами, не находящимися под управлением системы контроля версий TF или не добавленными в workspace Visual Studio.
Дополнительная информация, в случае её наличия (в т.ч. информация об ошибках), будет выводиться в окно PVS-Studio.
Данная опция актуальна только при работе из IDE Visual Studio.
Пометка сообщения как ложного срабатывания требует внесения изменений в файлы с исходным кодом. По умолчанию анализатор сохраняет каждый файл с исходным кодом после каждой такой пометки. Однако, если подобное частое сохранение не желательно (например, если файлы находятся на другой машине локальной сети), оно может быть отключено с помощью данного пункта настроек.
Будьте осторожны при изменении данной настройки, т.к. отсутствие сохранений файлов с исходным кодом после их разметки на ложные срабатывания может привести к потере результатов работы в случае закрытия среды разработки.
Позволяет включить отображение сообщений, размеченных как 'Ложные Срабатывания', в окне вывода PVS-Studio. Эта настройка будет применена в окне сообщений PVS-Studio сразу, без необходимости запускать анализ повторно. Когда выставлено значение 'True', специальный индикатор 'FA', отображающий количество ложных срабатываний, появится на панели окна вывода.
Настройка включает добавление дополнительного хэш-кода к метке ложного срабатывания. Хэш-код вычисляется на основе содержимого строки кода. Если строка кода изменится, предупреждения с тем же хэш-кодом, выданные для этой строки, больше не будут считаться ложными срабатываниями.
Данная настройка отключает автоматическую синхронизацию suppress-файлов между проектами в рамках одного решения.
По умолчанию синхронизация suppress-файлов включена. Если какой-либо файл исходного кода используется в нескольких проектах и предупреждения из этого файла были добавлены в suppress-файл хотя бы одного проекта, то при анализе других проектов эти предупреждения автоматически будут дописаны в suppress-файлы анализируемых проектов.
Настройка позволяет задать язык для встроенной справки по диагностическим сообщениям (клик по коду ошибки в окне вывода сообщений PVS-Studio) и документации (команда меню PVS-Studio -> Help -> Open PVS-Studio Documentation (html, online)) PVS-Studio, доступных на нашем сайте.
Данная настройка не меняет язык интерфейса IDE плагина PVS-Studio или выдаваемых анализатором диагностических сообщений.
Настройка позволяет использовать офлайн справку по диагностическим сообщениям (клик по коду ошибки в окне вывода сообщений PVS-Studio).
Данная опция отвечает за отображение кнопки 'Best' в интерфейсе PVS-Studio. По умолчанию опция включена и кнопка 'Best' доступна в таблице срабатываний. Используя её, можно просмотреть 10 наиболее интересных предупреждений анализатора. Вы можете скрыть эту кнопку, определив данный параметр как 'False'.
Данная настройка позволяет управлять уведомлениями о работе анализатора PVS-Studio. После завершения анализа, в случае, если окно PVS-Studio Output содержит сообщения об ошибках (сообщения об ошибках могут быть скрыты фильтрами как ложные срабатывания, по именам проверяемых файлов и т.п.; такие сообщения не попадают в окно PVS-Studio), анализатор проинформирует об их наличии с помощью всплывающего сообщения в области уведомления Windows (notification area, system tray). Одинарный щелчок мыши по данному сообщению, либо по значку PVS-Studio, откроет окно вывода сообщений PVS-Studio, содержащее найденные ошибки.
Настройка задаёт режим отображения уровней сообщений в окне PVS-Studio Output для результатов работы инкрементального анализа. Установка в данном поле глубины уровня отображения (соответственно, только 1 уровень; 1 и 2 уровни; 1, 2 и 3 уровни) приведёт к автоматическому включению данных уровней после завершения инкрементального анализа. Значение "Preserve_Current_Levels" сохранит существующие настройки уровней без изменений.
Данная опция может быть полезна при регулярном совместном использовании инкрементального и обычного режимов запуска, т.к. случайное отключение, например, 1 уровня анализа при просмотре большого отчёта обычной проверки, приведёт к сокрытию результатов работы уже инкрементального анализа. А так как инкрементальный анализ работает в фоновом режиме, подобная ситуация может привести к потере для пользователя диагностик о реальных ошибках, найденных анализатором в этом режиме работы.
Настройка задаёт режим трассировки (протоколирования хода выполнения программы) для IDE пакета расширения PVS-Studio (плагина для сред Visual Studio). Трассировка имеет несколько уровней детализации вывода (режим Verbose соответствует наиболее подробному протоколированию). При включении режима трассировки, PVS-Studio автоматически создаёт лог-файл в директории AppData\PVS-Studio с расширением log (например, c:\Users\admin\AppData\Roaming\PVS-Studio\PVSTrace2168_000.log). При этом каждый запущенный процесс IDE будет использовать отдельный лог-файл для сохранения результатов своей трассировки.
Данная настройка позволяет включить автоматический импорт настроек (xml файлов) из директории %AppData%\PVS-Studio\SettingsImports\. Настройки импортируются при каждой загрузке сохранённых настроек, т.е. при запуске Visual Studio или command line версии анализатора, при сбросе настроек и т.п. При импорте, настройки-флаги (true\false), а также настройки, хранящие одно значение (например, строку), заменяются настройками из SettingsImports. Настройки, имеющие несколько значений (например, исключаемые директории) объединяются.
При наличии в папке SettingsImports нескольких xml файлов, эти файлы будут применены к текущим настройкам последовательно по порядку, в соответствии с их именем.
По умолчанию PVS-Studio предлагает сохранять файл отчета (.plog) в той же папке, что и .sln-файл.
Изменение данной настройки позволяет восстановить стандартное поведение файловых диалогов Windows, т.е. диалог будет помнить последнюю открытую в нём папку и использовать именно её в качестве начальной.
Эта настройка задаёт то, как будет отображаться запрос на сохранение отчёта работы анализатора перед началом нового анализа или загрузкой другого отчёта в случае, если окно вывода уже содержит новые, не сохранённые или модифицированные результаты работы анализатора. Установка настройки 'Yes' включит автоматическое сохранение результатов в текущий файл отчёта (после того, как такой файл в первый раз будет указан в диалоге о сохранении файла). После установки настройки 'No' IDE плагин будет перезаписывать текущие результаты работы анализатора без сохранения. Выбор значения 'Ask_Always' (используется по умолчанию) приведет к выдаче запроса на сохранение отчёта каждый раз, позволяя пользователю самому сделать выбор.
По умолчанию, при генерации диагностических сообщений, PVS-Studio выдаёт абсолютные, полные пути до файлов, в которых анализатор нашёл ошибки. С помощью данной настройки можно задать корневую часть пути, которую анализатор будет автоматически подменять на специальный маркер в случае, если путь до файла, на который сгенерировано данное сообщение, начинается с заданного здесь корня. Например, абсолютный путь до файла C:\Projects\Project1\main.cpp будет заменён на относительный путь |?|Project1\main.cpp, если C:\Projects\ был указан в качестве корня.
При работе с отчётом PVS-Studio, содержащим пути до файлов в такой относительной форме, IDE плагин автоматически подставляет значение данной настройки вместо маркера |?|. Тем самым, использование данной настройки позволит работать с фалом отчёта PVS-Studio на любой машине, имеющей доступ к проверенным исходникам, вне зависимости от их местоположения в файловой системе.
Детальное описание данного режима работы доступно здесь.
Данная настройка включает или выключает режим использования пути до папки, содержащей файл решения *.sln, в качестве параметра Source Tree Root.
Данная настройка позволяет автоматически загружать отчёт, полученный в результате анализа Unreal Engine проекта, в окно вывода сообщений PVS-Studio.
Данная опция актуальна только при работе из IDE Visual Studio.
Примечание. Для установки анализатора на операционных системах семейства Windows можно воспользоваться инсталлятором, доступным на странице загрузки анализатора. Windows инсталлятор поддерживает установку как в графическом, так и в unattended (установка из командной строки) режимах.
Для работы C# анализатора PVS-Studio требуется ряд дополнительных пакетов. В зависимости от того, каким образом будет произведена установка PVS-Studio C#, эти зависимые пакеты будут автоматически установлены пакетным менеджером, либо их нужно будет установить вручную.
Для работы анализатора на машине должен быть установлен .NET SDK 9.0. Инструкции для добавления репозитория .NET в различные дистрибутивы Linux можно найти здесь.
Инсталлятор .NET SDK для macOS можно загрузить с этой страницы.
Примечание. При установке pvs-studio-dotnet с использованием пакетного менеджера на Linux необходимая для работы анализатора версия пакета .NET SDK будет установлена автоматически, но репозиторий .NET необходимо предварительно добавить вручную.
Для работы C# анализатору PVS-Studio требуется наличие C++ анализатора PVS-Studio (pvs-studio).
Примечание. При установке пакета C# анализатора PVS-Studio (pvs-studio-dotnet) через пакетный менеджер, пакет C++ анализатора (pvs-studio) будет установлен автоматически, и этот шаг можно пропустить.
При установке C# анализатора через распаковку архива необходимо также установить C++ анализатор (pvs-studio). С++ анализатор должен быть установлен в следующие директории:
Инструкции по установке pvs-studio доступны в соответствующих разделах документации: Linux; macOS.
Установка из репозитория – рекомендуемый способ, позволяющий автоматически установить необходимые зависимости и получать обновления.
wget -q -O - https://cdn.pvs-studio.com/etc/pubkey.txt | \
sudo apt-key add -
sudo wget -O /etc/apt/sources.list.d/viva64.list \
https://cdn.pvs-studio.com/etc/viva64.list
sudo apt-get update
sudo apt-get install pvs-studio-dotnet
wget -O /etc/yum.repos.d/viva64.repo \
https://cdn.pvs-studio.com/etc/viva64.repo
yum update
yum install pvs-studio-dotnet
wget -q -O /tmp/viva64.key https://cdn.pvs-studio.com/etc/pubkey.txt
sudo rpm --import /tmp/viva64.key
sudo zypper ar -f https://cdn.pvs-studio.com/rpm viva64
sudo zypper update
sudo zypper install pvs-studio-dotnet
Прямые ссылки для загрузки пакетов / архива доступны на странице загрузки. Ниже представлены команды установки / распаковки.
sudo gdebi pvs-studio-dotnet-VERSION.deb
или
sudo apt-get -f install pvs-studio-dotnet-VERSION.deb
sudo dnf install pvs-studio-dotnet-VERSION.rpm
или
sudo zypper install pvs-studio-dotnet-VERSION.rpm
или
sudo yum install pvs-studio-dotnet-VERSION.rpm
или
sudo rpm -i pvs-studio-dotnet-VERSION.rpm
tar -xzf pvs-studio-dotnet-VERSION.tar.gz
sudo ./install.sh
Команды установки:
brew install viva64/pvs-studio/pvs-studio
brew install viva64/pvs-studio/pvs-studio-dotnet
Команды обновления:
brew upgrade pvs-studio
brew upgrade pvs-studio-dotnet
Команда распаковки архива:
tar -xzf pvs-studio-dotnet-VERSION.tar.gz
sudo sh install.sh
Прежде, чем начать использовать анализатор PVS-Studio, нужно ввести лицензию. О том, как сделать это, вы можете подробно прочитать здесь.
Использование анализатора описано в соответствующем разделе документации.
Анализатор PVS-Studio можно использовать при работе в средах разработки JetBrains Rider и CLion. Плагины PVS-Studio для этих IDE предоставляют удобный графический интерфейс для запуска анализа проектов и отдельных файлов, а также для работы с предупреждениями анализатора.
Плагины PVS-Studio для Rider и CLion можно установить из официального репозитория плагинов JetBrains или из репозитория на нашем сайте. Ещё один способ установки - через установщик PVS-Studio для Windows, доступный на странице загрузки.
Для установки плагина PVS-Studio из официального репозитория JetBrains нужно открыть окно настроек с помощью команды 'File -> Settings -> Plugins', выбрать в окне вкладку Marketplace, и ввести в строке поиска 'PVS-Studio'. В результатах поиска появится плагин PVS-Studio.
Установка плагина PVS-Studio в Rider:
Установка плагина PVS-Studio в CLion:
Далее нужно нажать кнопку Install напротив найденного плагина PVS-Studio. После того как установка плагина будет завершена, нужно нажать кнопку Restart IDE.
В Rider:
В CLion:
Перезапустив среду разработки, можно начать пользоваться плагином PVS-Studio для проверки кода. Следующим шагом необходимо установить анализатор PVS-Studio.
Помимо официального репозитория JetBrains, плагин PVS-Studio также доступен из собственного репозитория PVS-Studio. Для установки плагина из репозитория PVS-Studio сначала нужно добавить репозиторий в IDE. Для этого нужно открыть окно установки плагинов с помощью команды меню File -> Settings -> Plugins.
Для Rider это будет выглядеть так:
В CLion:
Далее, нажать на шестеренку в правом верхнем углу и в выпадающем списке выбрать Manage Plugin Repositories.
Для Rider:
Для CLion:
В открывшемся окне добавьте путь:
И нажмите Ок.
В Rider:
В CLion:
Последний шаг установки аналогичен установке плагина из официального репозитория - нужно открыть вкладку Marketplace, где в поиск ввести "PVS-Studio". После применения данного фильтра выбрать плагин 'PVS-Studio for Rider' или 'PVS-Studio for CLion', нажать Install и перезапустить среду разработки.
Для работы анализатора PVS-Studio в средах Rider и CLion, помимо установки IDE плагина, также требуется установить ядро анализатора и его зависимости.
Если вы устанавливали плагин через Windows установщик PVS-Studio, то все необходимые для работы компоненты уже должны быть установлены в системе и можно пропустить этот шаг.
Если же плагин ставился отдельно (добавлением репозитория или из официального репозитория JetBrains), необходимо перед началом работы скачать и установить С++ или C# анализатор для необходимой платформы отсюда.
Первое, что нужно сделать после установки – ввести лицензию. Процесс ввода лицензии в Rider/CLion подробно описан в документации.
Панель настроек плагина состоит из нескольких вкладок. Рассмотрим каждую из них подробнее.
Settings - настройки ядра анализатора PVS-Studio. При наведении курсора мыши на название настройки появляется подсказка с описанием того, для чего эта настройка предназначена. Для Rider настройки выглядят так:
Для CLion:
Warnings - список типов всех предупреждений, поддерживаемых анализатором. Если убрать галочку у предупреждения, то все предупреждения данного типа в таблице вывода результатов работы анализатора будут отфильтрованы:
Excludes - содержит маски для имен файлов и путей, которые будут исключены из анализа.
Registration - содержит информацию о действующей лицензии.
JetBrains Rider может открывать проекты в 2-х режимах - непосредственно проект, и директорию с исходниками. При открытии проекта Rider может открывать как отдельные csproj файлы, так и solution, содержащий один или несколько таких проектов.
Если открыт файл проекта или solution'а, то имеется возможность анализировать:
Для анализа текущего проекта или solution'а можно воспользоваться пунктом меню Tools -> PVS-Studio -> Check Current Solution/Project. Также имеется пункт меню для запуска межмодульного анализа. В этом режиме анализатор выполняет более глубокий анализ кода, но тратит на это больше времени:
Для анализа файла, открытого на редактирование, можно использовать:
Также можно выбрать несколько элементов в окне Explorer через CTRL/SHIFT + mouse Left Click, после чего выбрать пункт меню Tools -> PVS-Studio -> Check Selected Items:
Или аналогичный пункт меню (Check Selected Items) в контекстном меню Explorer, которое вызывается нажатием правой кнопки мыши:
В примерах, приведенных выше, будут проанализированы все *.cs и *.csproj файлы из папок Core, Controllers, а также файл Startup.cs.
Если в Rider открыта директория, PVS-Studio не может точно знать, какой именно проект, файл или solution нужно анализировать, поэтому команда проверки текущего проекта \ solution'а и команда проверки открытого файла не доступны. Доступна только проверка solution'а, при помощи команды Tools -> PVS-Studio -> Check Selected Items:
Или через контекстное меню в окне Explorer (Check Selected Items):
В Rider есть возможность работать с проектами Unreal Engine. Стандартный сценарий работы отличается от описанного здесь. Подробнее читайте об этом в специальной документации.
JetBrains CLion позволяет открывать CMake проекты.
Имеется возможность анализировать:
Для анализа текущего проекта можно воспользоваться пунктом меню Tools -> PVS-Studio -> Check Project. Также имеется пункт меню для запуска межмодульного анализа. При межмодульном режиме происходит более глубокий анализ кода проекта, но на это тратится больше времени:
Для анализа файла, открытого на редактирование, можно использовать:
Также можно выбрать несколько элементов в окне Explorer через CTRL/SHIFT + mouse Left Click, после чего нажать правую кнопку мыши -> Analyze with PVS-Studio:
В примерах, приведенных выше, будут проанализированы все *.cpp файлы из папок 3rdparty, parallel, а также файл samples.cpp.
Во время анализа результаты работы анализатора выводятся в таблицу окна 'PVS-Studio', для Rider окно выглядит так:
Для CLion:
Таблица состоит из 8 столбцов (слева направо: Favorite, Code, CWE, SAST, Message, Position, Projects, False Alarms). Имеется возможность сортировать сообщения в таблице по любому столбцу. Для изменения порядка сортировки необходимо кликнуть на заголовок столбца. Крайний левый столбец (Favorite) используется для пометки предупреждений, которая позволяет быстро найти все помеченные предупреждения, включив сортировку по столбцу Favorite.
При клике на строке в столбце Code / CWE в браузере будет открыта страница с подробным описанием предупреждения или потенциальной уязвимости. В столбце Message содержится краткое описание предупреждения. Столбец Position содержит список файлов, связанных с сообщением. Столбец Projects - список проектов, включающих в себя файл, на который выдано предупреждение анализатора. Крайний правый столбец False Alarms - служит для отображения сообщений, помеченных, как ложные срабатывания. Подробнее про работу с ложными срабатываниями будет описано дальше, в соответствующем подразделе.
При двойном клике левой кнопкой мыши на строке в таблице будет открыт файл на строке, в которой было найдено предупреждение анализатора:
Также над таблицей имеются кнопки-стрелки, позволяющие переключаться между предыдущим / следующим сообщением анализатора, и открывать файл, на который это предупреждение выдано, в редакторе кода. Над таблицей имеется несколько фильтров по уровню опасности предупреждений: High, Medium, Low и Fails (ошибки анализатора).
При нажатии на кнопку-лупу откроется дополнительная панель с полями ввода для столбцов Code, CWE, Message и Position. Каждое поле – это строковый фильтр для столбца, позволяющий отфильтровать сообщения из таблицы по введённому в эти поля тексту.
В левом верхнем углу, над таблицей, расположена кнопка с тремя горизонтальными полосками. При нажатии на эту кнопку откроется дополнительная панель настроек:
При нажатии на кнопку-шестеренку открывается главное окно настроек плагина, также доступное через команду меню 'Tools -> PVS-Studio -> Settings'.
Если Вы только начали изучать инструмент статического анализа и хотели бы узнать на что он способен, то можете воспользоваться механизмом Best Warnings. Данный механизм покажет вам наиболее важные и достоверные предупреждения.
Чтобы посмотреть наиболее интересные предупреждения с точки зрения анализатора, нажмите на кнопку 'Best', как показано на скриншоте ниже:
После чего в таблице с результатами анализа останутся максимум десять наиболее критичных предупреждений анализатора.
Бывают ситуации, когда сообщение анализатора указывает на код, но программисту совершенно очевидно, что в этом коде нет ошибки. Такая ситуация называется ложным срабатыванием (false positive).
В плагине PVS-Studio имеется возможность пометить сообщение анализатора как ложное срабатывание. Такая пометка позволяет скрывать эти сообщения анализатора при последующем анализе кода.
Для разметки ложных срабатываний необходимо выбрать одно или несколько сообщений анализатора в таблице 'PVS-Studio', кликнуть правой кнопкой мыши на любой строке в таблице и в контекстном меню выбрать пункт 'Mark selected messages as False Alarms':
После выполнения данной команды анализатор добавит к строке, на которую выдаётся предупреждение анализатора, комментарий специального вида: \\-Vxxx, где xxx – это номер диагностического правила PVS-Studio. Такой комментарий также можно добавить в код вручную.
Помеченные ранее ложные срабатывания можно показать в таблице окна PVS-Studio с помощью настройки 'Show False Alarms', доступной через команду меню 'Tools -> PVS-Studio -> Settings', для Rider:
Для CLion:
С помощью команды контекстного меню 'Remove False Alarm marks from selected messages' можно удалить отметку ложного срабатывания с выбранных сообщений.
Подробную информацию о подавлении предупреждений, выдаваемых анализатором PVS-Studio, а также сведения о других способах подавления сообщений анализатора с помощью файлов конфигурации (.pvsconfig), добавляемых в проект, можно найти в разделе документации Подавление ложных предупреждений.
Часто начать регулярно использовать статический анализ мешают многочисленные срабатывания на старом legacy коде. Такой код обычно уже хорошо оттестирован и стабильно работает, поэтому править в нём все срабатывания анализатора может оказаться нецелесообразно. Тем более, если размер кодовой базы достаточно велик, такая правка может потребовать большого времени. При этом, такие сообщения на существующий код мешают смотреть сообщения на новый код, находящийся в разработке.
Чтобы решить данную проблему и начать сразу регулярно использовать статический анализ, PVS-Studio предлагает возможность "отключить" сообщения на старом коде. Чтобы подавить сообщения анализатора на старом коде можно воспользоваться командой главного меню 'Tools -> PVS-Studio -> Suppress All Messages' или кнопкой 'Suppress All Messages' на панели окна PVS-Studio. Механизм подавления работает с помощью специальных *.suppress файлов, в которые добавляются подавленные сообщения анализатора после выполнения команды 'Suppress All Messages'. При последующем запуске анализа все сообщения, добавленные в такие *.suppress файлы, не попадут в отчёт анализатора. Система подавления через *.suppress файлы достаточно гибкая и способна "отслеживать" подавленные сообщения даже при модификации и сдвигах участков кода, в которых выдаётся подавленное сообщение.
При работе с Rider файлы *.suppress создаются на уровне проекта, рядом с каждым проектным файлов, но их также можно добавить в любой проект или solution (например, чтобы использовать один общий suppress файл для нескольких проектов или всего soulution'а). Чтобы вернуть сообщения в вывод анализатора, необходимо удалить suppress файлы для соответствующих проектов и перезапустить анализ.
В CLion подавленные сообщения добавляются в suppress файл suppress_file.suppress.json, который записывается в директорию .PVS-Studio, в корневой директории открытого в CLion проекта. Чтобы вернуть все сообщения в вывод анализатора, необходимо удалить этот файл и перезапустить анализ.
Более подробное описание подавления предупреждений анализатора, и описание работы с *.suppress файлами, можно прочитать в разделе документации Массовое подавление сообщений анализатора.
Также предлагаем познакомиться со статьёй "Как внедрить статический анализатор кода в legacy проект и не демотивировать команду".
При нажатии правой кнопкой мыши на строке с сообщением анализатора в таблице окна PVS-Studio, откроется контекстное меню, содержащее дополнительные команды для выбранных сообщений анализатора.
Команда 'Mark selected messages as False Alarms / Remove false alarm masks' позволяет разметить сообщение анализатора, как ложное срабатывание, добавив в код, на который выдано предупреждение, комментарий специального вида (более подробно про работу с ложными срабатываниями рассказано, в соответствующем подразделе).
Команда 'Exclude from analysis' позволяет добавить путь или часть пути к файлу, в котором найдено предупреждение анализатора, в список исключённых из анализа директорий. Все файлы, пути до которых попадут под данный фильтр, будут исключены из анализа.
Для сохранения или загрузки результатов работы анализатора можно воспользоваться командами главного меню, доступными через 'Tools -> PVS-Studio':
Команда 'Open Report' открывает .json файл отчета и загружает его содержимое в таблицу окна 'PVS-Studio'.
Подменю 'Recent Reports' показывает список из нескольких последних открытых файлов отчетов. При нажатии на элемент в списке будет открыт соответствующий отчет (если отчет еще существует по такому пути), и его содержимое будет загружено в таблицу окна 'PVS-Studio'.
Команда 'Save Report' сохраняет все сообщения из таблицы (даже отфильтрованные) в .json файл отчета. Если текущий результат анализа еще ни разу не сохранялся, то будет предложено задать имя и место для сохранения отчета.
Аналогично, команда 'Save Report As' сохраняет все предупреждения из таблицы (даже отфильтрованные) в .json файл отчета, всегда предлагая выбрать место сохранения отчёта на диске.
Плагины для Rider и CLion добавляют окно просмотра результатов анализа PVS-Studio, которое имеет контекстное меню, появляющееся при нажатии правой кнопкой мыши в окне с результатами анализа:
Некоторым действиям из этого меню назначены горячие клавиши, что позволяет выполнять их без использования мыши.
Кнопки со стрелками, предназначенные для навигации по сообщениям анализатора, также имеют горячие клавиши:
Использование горячих клавиш полезно, потому что позволяет ускорить процесс обработки результатов анализа. Их можно назначать\переопределять в настройках: File -> Settings -> Keymap. Чтобы быстрее их найти, введите 'PVS-Studio' в поле поиска окна Keymap.
Анализатор не всегда может полностью проанализировать файл с исходным кодом.
Это может произойти по трем причинам:
1) Ошибка в коде
В коде имеется шаблонный класс или шаблонная функция с ошибкой в коде. Если эта функция не инстанцируется, то компилятор не диагностирует в ней наличие некоторых ошибок. Другими словами наличие такой ошибки не мешает компиляции. Анализатор PVS-Studio пытается искать потенциальные ошибки даже в классах и функциях, которые нигде не используются. В случае если анализатор не может разобрать код, то он и выдаст предупреждение V001. Рассмотрим пример кода:
template <class T>
class A
{
public:
void Foo()
{
//Забыли ;
int x
}
};
Этот код будет скомпилирован Visual C++, если класс A не будет нигде использоваться. Однако ошибка в нем присутствует, что мешает работе PVS-Studio.
2) Ошибка в препроцессоре Visual C++
Анализатор в процессе своей работы использует препроцессор Visual C++. Этот препроцессор очень редко, но все же допускает ошибки при генерации препроцессированных *.i файлов. В результате анализатор получает на вход некорректные данные. Рассмотрим пример:
hWnd = CreateWindow (
wndclass.lpszClassName, // window class name
__T("NcFTPBatch"), // window caption
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
// window style
100, // initial x position
100, // initial y position
450, // initial x size
100, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL); // creation parameters
if (hWnd == NULL) {
...
Приведенный фрагмент кода, препроцессор Visual C++ превратил в:
hWnd = // window class name// window caption// window style//
initial x position// initial y position// initial x size//
initial y size// parent window handle// window menu handle//
program instance handleCreateWindowExA(0L,
wndclass.lpszClassName, "NcFTPBatch", 0x00000000L | 0x00C00000L |
0x00080000L | 0x00020000L, 100, 100,450, 100, ((void *)0),
((void *)0), hInstance, ((void *)0)); // creation parameters
if (hWnd == NULL) {
...
Получается, что в коде написано следующее:
hWnd = // длинный комментарий
if (hWnd == NULL) {
...
Этот код некорректен, о чем и выдаст сообщение PVS-Studio. Конечно, это недоработка PVS-Studio и со временем мы это исправим.
Отметим также, что Visual C++ успешно компилирует этот код, так как алгоритмы, используемые в нем для компиляции и для генерации препроцессированных *.i-файлов различны.
3) Недоработка внутри PVS-Studio
В редких случаях PVS-Studio неспособен разобрать сложный шаблонный код.
Какова не была бы причина выдачи сообщения V001, это сообщение не критично. Как правило, неполный разбор файла не несущественен с точки зрения анализа. PVS-Studio просто пропускает функцию/класс с ошибкой и продолжает анализ файла. Непроанализированным остается совсем небольшой участок кода.
Анализатор обнаружил проблему, из-за которой позиции предупреждений могут указывать на некорректные строки кода. Это может происходить либо из-за некорректной работы внешнего препроцессора, либо из-за вставленных разработчиком директив '#line' в исходный код.
Анализатор PVS-Studio для языков C и C++ работает только с препроцессированными файлами, т.е. с файлами, в которых раскрыты все макросы ('#define') и подставлены все включаемые файлы ('#include'). При этом в препроцессированном файле содержится информация о том, какие файлы куда подставились и в какие позиции. Осуществляется это как раз с помощью директив '#line', имеющих следующий формат:
#line linenum "filename" // MSVC
# linenum "filename" // GCC-like
Следующая строка после этой директивы в препроцессированном файле интерпретируется как пришедшая из файла 'filename' и имеющая номер 'linenum'. Таким образом, в препроцессированных файлах помимо готового к анализу кода содержится также информация о том, из какого файла поступил тот или иной участок.
Препроцессирование выполняется в любом случае. Для пользователя эта процедура происходит незаметно. Иногда препроцессор является частью анализатора кода, а иногда (как в случае с PVS-Studio) используется внешний препроцессор. Утилита анализа запускает компилятор, которым собирается проверяемый проект, для каждого проверяемого файла на языке C или C++. С его помощью генерируется препроцессированный файл с расширением '*.PVS-Studio.i'.
Рассмотрим ситуацию, при которой происходит сбой в позиционировании предупреждений анализатора. Речь пойдёт о написании директив '#pragma' в несколько строк с применением лексемы продолжения предыдущей строки ('\'):
#pragma \
warning(push)
void test()
{
int a;
if (a == 1) // V614 должно быть выдано здесь,
return; // а выдастся здесь
}
Компилятор MSVC неверно формирует директивы '#line' в препроцессированном файле при написании такого кода, при этом GCC и Clang делают всё верно. Однако если немного изменить пример, то все внешние препроцессоры отрабатывают корректно:
#pragma warning \
(push)
void test()
{
int a;
if (a == 1) // V614 теперь выдаётся корректно
return;
}
Наша рекомендация — это либо не использовать многострочные директивы '#pragma', либо писать их так, чтобы они корректно обрабатывались внешним препроцессором.
Анализатор пытается обнаружить сдвиг строк в обрабатываемом файле и сообщить об этом пользователю предупреждением V002. При этом он не пытается скорректировать позиции выданных предупреждений в коде программы. Алгоритм нахождения сдвига строк работает следующим образом.
Шаг N1. Анализатор открывает исходный файл и ищет самую последнюю лексему. Выбираются только те лексемы, которые не короче трех символов. Например, для следующего кода последней лексемой будет считаться 'return':
1 #include "stdafx.h"
2
3 int foo(int a)
4 {
5 assert( a >= 0
6 && a <= 1000);
7 int b = a + 1;
8 return b;
9 }
Шаг N2. Найдя последнюю лексему, анализатор определит номер строки, в которой она находится. В примере это строка под номером 8. Далее анализатор ищет последнюю лексему в уже препроцессированном файле. Если последние лексемы не совпадают, то, видимо, в конце файла раскрылся макрос. В такой ситуации анализатор не сможет понять, корректно ли расположены строки. Но подобное происходит крайне редко, и почти во всех случаях последние лексемы в исходном и препроцессированном файле совпадают. Если это так, определяется номер строки, в которой расположена лексема в препроцессированном файле.
Шаг N3. После двух предыдущих шагов имеются номера строк, где расположена последняя лексема в исходном файле и в препроцессированном файле соответственно. Если эти номера строк не совпадают, то произошёл сдвиг в нумерации строк. И в данном случае анализатор сгенерирует предупреждение V002.
Примечание N1. Следует учитывать, что если некорректная директива '#line' будет находиться в файле ниже всех найденных подозрительных участков кода, то все позиции выданных предупреждений будут корректны. И хотя анализатор сгенерирует дополнительно предупреждение V002, это не помешает вам работать с результатами анализа.
Примечание N2. Хоть это и не ошибка непосредственно анализатора кода PVS-Studio, тем не менее, это приводит к его некорректной работе.
Если пользователь хочет попытаться самостоятельно найти строку в исходном файле, из-за которой возник сдвиг, то можно попробовать воспользоваться следующим алгоритмом:
Шаг N1. Перезапустите анализ решения/проекта/файла, сохранив промежуточные файлы анализа (выключив настройку "Remove Intermediate Files").
Шаг N2. Откройте отчёт в одном из плагинов для IDE.
Шаг N3. Отфильтруйте предупреждения согласно файлу, в котором произошёл сдвиг позиций. Если производился анализ единичного файла, то фильтровать ничего не требуется.
Шаг N4. Отсортируйте предупреждения по номерам строк или позиций (колонка 'Line' или 'Positions').
Шаг N5. Найдите первое предупреждение, позиция которого смещена.
Шаг N6. Откройте препроцессированный файл, соответствующий исходному, с расширением '*.PVS-Studio.i'.
Шаг N7. Найдите строку, полученную на шаге N5, в препроцессированном файле.
Шаг N8. Двигайтесь вверх по препроцессированному файлу, начиная от полученной на шаге N7 позиции, и найдите первую ближайшую директиву '#line'.
Шаг N9. В исходном файле перейдите на соответствующую строку, указанную в директиве '#line', полученную на шаге N8. Между этой строкой и строкой, на которой выдано предупреждение, находится код, который приводит к сдвигу. Это могут быть многострочные вызовы макросов, многострочные директивы компиляторов и т.д.
Схематически работу алгоритма можно отобразить следующим образом:
Наличие предупреждения V003 в отчёте означает, что произошла критическая ошибка внутри анализатора. Скорее всего, в этом случае вообще никаких других предупреждений про анализируемый файл не будет выдано.
Хотя предупреждение V003 возникает достаточно редко, мы будем благодарны, если вы поможете нам исправить проблему, которая привела к этому предупреждению. Для этого пришлите, пожалуйста, нам файлы, описанные далее, через форму обратной связи.
При получении предупреждения вида:
V003 Unrecognized error found: stacktrace was written to the file
source.cpp.PVS-Studio.stacktrace.txt 1
Вы можете перейти к текстовому файлу, который имеет примерно следующий вид:
PVS-Studio version: 7.XX.XXXXX.XXXX
#NN Object "[0xffffffffffffffff]", at 0xffffffffffffffff, in
....
#1 Object "[0xb39b7e]", at 0xb39b7e, in
#0 Object "[0xcfac19]", at 0xcfac19, in
В файле содержатся только адреса функций, т.к. дистрибутив PVS-Studio распространяется без отладочной информации. Однако мы сможем расшифровать этот файл на нашей стороне и определить источник ошибки.
Примечание. Файл с трассировкой стека не содержит никакой информации об исходном коде.
Анализ исходного кода на языках C и C++ осуществляется на основе препроцессированного ('*.PVS-Studio.i') и конфигурационного ('*.PVS-Studio.cfg') файлов. Препроцессированный файл формируется из исходного файла (например, 'file.cpp') препроцессором путём раскрытия макросов и директив '#include'. Конфигурационный файл содержит настройки, необходимые для корректного запуска анализа на файле. Набор этих файлов позволяет локализовать проблему в ядре C и C++ анализатора.
Далее описаны способы генерации этих файлов различными компонентами продукта.
Плагины для IDE. На вкладке 'Common Analyzer Settings' настроек PVS-Studio установите опцию 'RemoveIntermediateFiles' в 'False' и заново выполните анализ файлов, на которых происходит падение.
PVS-Studio_Cmd / pvs-studio-dotnet. В файле конфигурации анализа ('Settings.xml') установите опцию 'RemoveIntermediateFiles' в 'False' и заново выполните анализ файлов, на которых происходит падение.
CompilerCommandsAnalyzer / pvs-studio-analyzer. Добавьте флаг '‑‑dump-files' в строку запуска анализа:
pvs-studio-analyzer analyze .... --dump-files
После этого в папке проекта надо найти соответствующий i-файл (например, 'file.PVS-Studio.i') и соответствующий ему 'file.PVS-Studio.cfg'.
Примечание. Препроцессированные файлы содержат часть исходного кода проекта. Мы понимаем, что политика компании может запрещать распространение фрагментов исходного кода проекта. Поэтому в случае необходимости мы готовы заключить NDA. Передача препроцессированных и конфигурационных файлов может существенно упростить и ускорить процесс исправления ошибки в анализаторе.
Анализатор, выявляя проблемы 64-битного кода, всегда должен проверять именно 64-битную конфигурацию проекта. Ведь именно в 64-битной конфигурации правильно раскрываются типы данных, правильно выбираются ветки вида "#ifdef WIN64" и так далее. Пытаться выявлять проблемы 64-битного кода в 32-битной конфигурации некорректно.
Однако иногда может оказаться полезным проверить 32-битную конфигурацию проекта. Сделать это можно в том случае, когда 64-битной конфигурации пока нет, но требуется оценить предстоящий объем работ по миграции кода на 64-битную платформу. Тогда можно проверить проект в 32-битном режиме. Проверка 32-битной конфигурации вместо 64-битной покажет, сколько диагностических сообщений выдаст анализатор при проверке 64-битной конфигурации. Наши эксперименты показывают, что, конечно же, далеко не все диагностические сообщения выдаются при проверке 32-битной конфигурации. Однако порядка 95% диагностических сообщений при проверке 32-битной конфигурации проекта совпадают с сообщениями при проверке 64-битной конфигурации. Это позволяет оценить предстоящий объем работ.
Обратите внимание! Если вы исправите даже все ошибки, выявленные при проверке 32-битной конфигурации проекта, то нельзя считать код полностью совместимым с 64-битными системами. Необходимо обязательно выполнять окончательную проверку в 64-битной конфигурации проекта.
Сообщение V004 выдается только один раз на каждый проект, который проверяется в режиме 32-битной конфигурации. Предупреждение относится к файлу, который будет проанализирован первым при проверке проекта. Это сделано для того, чтобы не выводить множество однотипных предупреждений в отчет.
Данная проблема в работе PVS-Studio вызвана несоответствием для рассматриваемого проекта платформенных конфигураций заданных в solution файле и платформенных конфигураций в самом файле данного проекта.
В частности, в solution файле рассматриваемого проекта могут присутствовать строки следующего вида:
{F56ECFEC-45F9-4485-8A1B-6269E0D27E49}.Release|x64.ActiveCfg = Release|x64
Однако в самом файле проекта может отсутствовать упоминание конфигурации Release|x64. Поэтому, при попытке проверки данного проекта PVS-Studio не может найти конфигурацию Release|x64. В такой ситуации ожидается, что среда Visual Studio должна добавить в автоматически сгенерированный solution-файл строку следующего вида:
{F56ECFEC-45F9-4485-8A1B-6269E0D27E49}.Release|x64.ActiveCfg = Release|Win32
В данном автоматически сгенерированном файле активной конфигурации solution файла (Release|x64.ActiveCfg) для рассматриваемого проекта приравнивается одна из существующих конфигураций данного проекта (т.е. в данном случае Release|Win32). Подобная ситуация является ожидаемой и отрабатывается PVS-Studio корректно.
Сообщение V006 возникает, когда процесс анализатора не может обработать какой-то файл за определенное время и завершается аварийно. Эта ситуация может возникать в двух случаях.
Первая возможная причина – это ошибка в анализаторе, при которой не удается разобрать тот или иной фрагмент кода. Такое случается достаточно редко, но, тем не менее, возможно. Хотя сообщение V006 возникает достаточно редко, мы будем вам благодарны, если вы поможете нам исправить проблему, которая привела к этому сообщению. Если вы работали с проектом на языках C/C++, пришлите, пожалуйста, препроцессированный i-файл, на котором возникла проблема, и соответствующие ему конфигурационные файлы запуска (*.PVS-Studio.cfg и *.PVS-Studio.cmd) на адрес support@viva64.com.
Примечание. Препроцессированный i-файл получается из исходного файла (например, file.cpp) после работы препроцессора. Для того чтобы получить такой файл и конфигурационные файлы запуска нужно на вкладке "Common Analyzer Settings" настроек PVS-Studio установить опцию RemoveIntermediateFiles в False и заново выполнить анализ одного этого файла. После этого в папке проекта надо найти соответствующий i-файл (например, file.i и соответствующие ему file.PVS-Studio.cfg и file.PVS-Studio.cmd).
Вторая возможная причина – из-за загруженности процессора анализатору достается мало системных ресурсов и, хотя файл он мог бы обработать корректно, он не успевает сделать это за отведенное время. По-умолчанию запускается столько потоков для анализа, сколько имеется ядер в процессоре. Например, на машине с четырьмя ядрами файлы будут запускаться на анализ по четыре штуки сразу. Каждый экземпляр процесса анализатора требует примерно 1.5 гигабайта оперативной памяти. И если оперативной памяти на машине недостаточно, то из-за использования файла подкачки анализ может работать медленно и не успеть в отведенный временной интервал. Кроме того, если на компьютере кроме анализатора запущены параллельно другие "тяжелые" приложения, то это тоже может вызвать проблему.
Решением может служить явное указание, что использовать нужно не все ядра. Количество используемых при анализе ядер можно задать в настройках PVS-Studio (опция ThreadCount на вкладке "Common Analyzer Settings").
Сообщение V007 возникает, когда для анализа выбираются проекты, использующие спецификацию Microsoft C++/Common Language Infrastructure, которые содержат один из устаревших /clr флагов компилятора. Хотя вы можете продолжать проверять такие проекты, PVS-Studio не поддерживает работу с данными флагами компилятора, поэтому некоторые диагностические сообщения анализатора могут оказаться некорректными.
PVS-Studio не смог запустить анализ указанного файла. Данное сообщение означает, что внешний C++ препроцессор, запущенный анализатором для создания препроцессированного файла с исходным кодом, завершил свою работу с ненулевым выходным кодом ошибки. При этом std error зачастую будет содержать подробное описание произошедшей ошибки, которое также можно увидеть в окне PVS-Studio Output для данного файла.
Ошибка V008 может возникнуть по следующим причинам:
1) Код не компилируется
Если исходный C++ код по какой то причине не компилируется (например, не найден включаемый заголовочный файл), препроцессор завершает свою работу с ненулевым кодом и выдаёт в std error сообщение об ошибке вида "fatal compilation error". PVS-Studio не сможет начать анализ в случае, если C++ файл не был успешно препроцессирован. Для устранения ошибки необходимо обеспечить компилируемость проверяемого файла.
2) Повреждён\заблокирован исполняемый файл препроцессора
Такая ситуация возможна в случаях, когда исполняемый файл препроцессора повреждён или заблокирован системным антивирусом. Окно вывода PVS-Studio может также содержать сообщения вида " The system cannot execute the specified program ". Для устранения ошибки необходимо убедиться в целостности исполняемого файла используемого препроцессора и смягчить политики безопасности системного антивирусного ПО.
3) Заблокирован один из вспомогательных командных файлов PVS-Studio
Анализатор PVS-Studio не запускает C++ препроцессор напрямую, а использует для этого собственные предварительно сгенерированные командные файлы. В случае жёстких системных политик безопасности, антивирусное ПО может препятствовать корректному запуску C++ препроцессора. В этом случае ошибка также может быть устранена путём смягчения политики безопасности в отношении анализатора.
4) Используются файлы, в пути которых присутствуют не латинские символы. Эти символы могут некорректно отображаться для текущей кодовой страницы консоли.
PVS-Studio использует бат-файл 'preprocessing.cmd' (в директории установки PVS-Studio) для запуска препроцессирования. В этом файле можно выставить корректную кодовую страницу (при помощи chcp).
Вы ввели лицензионный ключ, предназначенный для бесплатного использования анализатора. Чтобы этот ключ работал, исходные файлы в вашем проекте должны содержать комментарии специального вида. Комментарии должны быть вставлены в файлы с расширением: .c, .cc, .cpp, .cp, .cxx, .c++, .cs, .java. Заголовочные файлы модифицировать не требуется.
Вы можете вставить комментарии вручную или использовать специальную open source утилиту, скачав её с сайта GitHub: how-to-use-pvs-studio-free.
Варианты комментариев:
Комментарии для студентов (академическая лицензия):
// This is a personal academic project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com
Комментарии для открытых бесплатных проектов:
// This is an open source non-commercial project. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com
Комментарии для индивидуальных разработчиков:
// This is an independent project of an individual developer. Dear PVS-Studio, please check it.
// PVS-Studio Static Code Analyzer for C, C++, C#, and Java: https://pvs-studio.com
Некоторые разработчики могут не хотеть видеть в начале файла строчки с комментарием, не относящимся к сути проекта. Это их право, и они могут просто не использовать анализатор. Или они могут приобрести коммерческую лицензию и использовать анализатор без ограничений. Мы рассматриваем наличие этих комментариев, как благодарность за предоставленную лицензию и, заодно, как дополнительную рекламу нашего продукта.
По всем вопросам просим обращаться в поддержку.
Предупреждение V010 выдаётся при попытке проверки .vcxproj-проектов, имеющих конфигурационный тип 'makefile' или 'utility'. PVS-Studio не поддерживает работу с такими проектами при использовании плагина или command line версии анализатора. Это связано с тем, что в makefile/utility проектах необходимая анализатору информация (в частности, параметры компиляции) о деталях сборки недоступна.
При необходимости проверки таких проектов, пожалуйста, воспользуйтесь системой мониторинга компиляции или прямой интеграцией анализатора. Также вы можете отключить это предупреждение на странице настроек PVS-Studio (вкладка Detectable Errors (C++), список Fails).
Директива #line генерируется препроцессором и предназначена для того, чтобы понять, какому файлу и строке соответствует та или иная строка в препроцессированном файле.
Поясним это с помощью примера.
#line 20 "a.h"
void X(); // Объявление функции X находится в 20 строке в файле a.h
void Y(); // Объявление функции Y находится в 21 строке в файле a.h
void Z(); // Объявление функции Z находится в 22 строке в файле a.h
#line 5 "a.cpp"
int foo; // Переменная foo объявлена в 5 строке в файле a.cpp
int X() { // Реализация функции X начинается в 6 строке в файле a.cpp
return 0; // 7 строка
} // 8 строка
Директивы #line используют различные инструменты, в том числе и анализатор PVS-Studio, чтобы осуществлять навигацию по файлу.
Иногда бывает так, что по какой-то причине директивы #line содержатся в файлах с исходным кодом (*.c; *.cpp; *.h и т.д.). Например, такое может произойти, если файл был сгенерирован каким-либо способом автоматически (пример).
При препроцессировании эти директивы #line будут добавлены в препроцессированный *.i файл. Например, пусть есть файл A.cpp следующего вида:
int a;
#line 30 "My.y"
int b = 10 / 0;
После препроцессирования получится файл A.i следующего содержания:
#line 1 "A.cpp"
int a;
#line 30 "My.y"
int b = 10 / 0;
В результате, навигация будет сломана. Анализатор обнаружит деление на ноль и выдаст сообщение, что эта ошибка находится в 30 строке в файле My.y. Формально анализатор прав, ведь ошибка действительно появилась из-за некорректного текста программы в файле My.y. Однако навигация сломана, и вы не сможете просмотреть файл My.y, так как этого файла может просто не быть в проекте. При этом вы не узнаете, что сейчас деление на 0 на самом деле возникает в 3 строке файла A.cpp.
Чтобы исправить ситуацию, мы предлагаем удалить все директивы #line, которые присутствуют в исходных файлах вашего проекта. Как правило, эти директивы оказались в файле случайно и никому не помогают, а только вносят путаницу, мешая работе различных инструментов, таких как анализаторы кода.
Чтобы обнаружить вредные #line в исходном коде и предназначена диагностика V011. Анализатор указывает на 10 первых #line в файле. Большее количество указывать не рационально. Вы можете самостоятельно легко найти с помощью поиска в используемом вами редакторе кода все директивы #line и удалить их.
Исправленный вариант кода:
int a;
int b = 10 / 0;
После препроцессирования будет получен *.i файл следующего содержания:
#line 1 "A.cpp"
int a;
int b = 10 / 0;
Теперь навигация будет работать правильно и анализатор предупредит, что деление на 0 возникнет во второй строке файла A.cpp.
Некоторые способы подавления ложных срабатываний позволяют полностью отключать диагностики анализатора. В результате часть сообщений будет не просто отмечена как ложные срабатывания, а могут совсем не появиться в отчете.
Чтобы выяснить, какие механизмы участвуют в отключении диагностик, можно включить добавление специальных сообщений в отчёт анализатора.
pvs-studio-analyzer analyze ... --cfg source.cpp.PVS-Studio.cfg
В результате, в отчете анализатора появятся сообщения с кодом V012 и информацией об источниках отключения диагностик. IDE плагины PVS-Studio поддерживают навигацию к причине отключения диагностик в файлах исходного кода и файлах конфигурации правил (.pvsconfig). Пути до файлов конфигурации, содержащих правила исключения срабатываний анализатора, также будут добавлены в отчёт анализатора, в виде сообщений V012.
Запуск межмодульного анализа на ограниченном списке файлов проекта приведет к потере межпроцедурной информации. Данное предупреждение актуально только для C и C++ проектов.
Межмодульный анализ выполняется в 3 этапа:
Для обеспечения наибольшей эффективности межмодульного анализа рекомендуется запускать его на всём проекте целиком. В таком случае объем собранных межпроцедурных фактов будет максимальным, и, следовательно, тем более качественным будет анализ и тем больше сообщений гипотетически анализатор сможет найти.
Ваш suppress файл имеет старый формат. Вы все еще можете использовать его для подавления предупреждений, однако для добавления новых вам придется его перегенерировать.
Для создания нового suppress файла вам нужно:
Подробнее про работу с suppress файлом можно прочитать в документации.
Предупреждение V015 говорит о том, что у вас имеются предупреждения анализатора, но они не отображены в окне PVS-Studio. Это может происходить как из-за настроек фильтрации в окне PVS-Studio, так из-за глобальных настроек анализатора.
Чтобы отобразить все "спрятанные" предупреждения, необходимо убедиться в том, что настроенные фильтры ничего не скрывают. Ниже приведено более подробное описание фильтров.
Первое, на что нужно обратить внимание – активные фильтры для просмотра предупреждений. Очень часто именно эти фильтры скрывают от вас предупреждения.
Окно PVS-Studio предоставляет следующие механизмы фильтрации:
Убедитесь в том, что перечисленные фильтры не скрывают срабатывания анализатора. Для этого активируйте кнопки-фильтры, щелкнув на них мышкой, и сбросьте все пользовательские фильтры по предупреждениям, нажав на 'Clear'.
Также на отображение предупреждений влияет настройка вкладки 'PVS-Studio > Options ... > Don't Check Files', в которой прописаны файловые маски для исключения некоторых файлов или папок из анализа. Убедитесь в том, что среди файловых масок нет таких, которые исключают предупреждения по интересующим вас файлам/путям.
Если V015 по-прежнему не пропадает, проверьте предупреждения, помеченные как FA (false alarm). По умолчанию эти предупреждения не отображаются в окне PVS-Studio. Чтобы они отображались, вам нужно активировать соответствующую настройку: 'PVS-Studio > Options ... > Specific Analyzer Settings > False Alarms > DisplayFalseAlarms > True'. После этого в окне PVS-Studio отобразятся предупреждения из группы FA, если они у вас были.
При помощи механизма пользовательских аннотаций можно дополнительно настроить диагностические правила анализатора. Одним из вариантов пользовательских аннотаций являются аннотации функций.
Пример такой аннотации:
//V_FORMATTED_IO_FUNC, function:Log, format_arg:1, ellipsis_arg:2
void Log(const char *fmt, ...);
Однако аннотации по умолчанию не применяются к виртуальным функциям. Диагностическое предупреждение V016 сообщает пользователю о том, что его аннотация не была применена к виртуальной функции, и рекомендует исправить это при помощи специальных флагов, которые дописываются в аннотацию:
Например, аннотация для виртуальной функции 'Log' класса 'Base' будет выглядеть следующим образом:
// Комментарий должен быть расположен на одной линии
//V_FORMATTED_IO_FUNC, function:Base::Log,
format_arg:1, ellipsis_arg:2,
enable_on_virtual
struct Base
{
virtual void Log(const char *fmt, ...);
}
Вместо 'enable_on_virtual' может быть указан флаг 'propagate_on_virtual'. Тогда аннотация будет применена и к переопределениям функции в дочерних классах:
// Комментарий должен быть расположен на одной линии
//V_FORMATTED_IO_FUNC, function: Base::Log,
format_arg:1, ellipsis_arg:2,
propagate_on_virtual
struct Base
{
virtual void Log(const char *fmt, ...);
}
struct Derived
{
// Аннотация будет действовать и на эту функцию
virtual void Log(const char *fmt, ...) override;
}
Наличие предупреждения V017 в отчёте означает, что анализ был экстренно остановлен из-за нехватки свободной оперативной памяти, необходимой для работы. Обычно такое происходит, когда анализ запускается с большим количеством потоков относительно малого количества свободной оперативной памяти.
Для стабильной работы ядра C и C++ анализатора на каждый поток анализа требуется не менее 2 ГБ памяти. Существуют несколько вариантов решения проблемы:
Примечание. Попытка решить проблему только путём увеличения размера файла подкачки может привести к предупреждению V006.
Далее описаны способы снижения числа потоков анализа в различных компонентах продукта.
Плагины для IDE. В настройках плагина PVS-Studio установите опцию 'ThreadCount' в меньшее значение.
PVS-Studio_Cmd / pvs-studio-dotnet. В файле конфигурации анализа ('Settings.xml') установите опцию 'ThreadCount' в меньшее значение.
CompilerCommandsAnalyzer / pvs-studio-analyzer. Уменьшите число, передаваемое в параметр '-j' / '‑‑threads'.
Интеграция с Unreal Build Tool. Воспользуйтесь файлом 'BuildConfiguration.xml' со следующими опциями:
<?xml version="1.0" encoding="utf-8" ?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
<ParallelExecutor>
<MaxProcessorCount>1</MaxProcessorCount>
</ParallelExecutor>
</Configuration>
Если после применения всех советов в отчёте продолжает появляться предупреждение V017, мы будем благодарны, если вы пришлёте нам файлы, описанные здесь, через форму обратной связи.
Наличие предупреждения V018 в отчёте означает, что в исходном коде анализируемой программы содержатся строчки, отмеченные False Alarm меткой без хэша, и при этом в настройках анализатора включена опция V_HASH_ONLY.
//V_HASH_ONLY ENABLE
Если включить эту настройку, то в следующем фрагменте False Alarm метка на первой строчке не сработает:
int b = a; //-V614
int* c = nullptr;
b = *c; //-V522 //-VH"1949"
Подробнее об этой настройке можно прочитать в документации.
Чтобы исправить данное срабатывание, можно добавить хеши к тем False Alarm меткам, в которых они отсутствуют. В этом случае предупреждения будут выдаваться, только если строчка, содержащая False Alarm метку с хэшем, изменилась после добавления этого хэша. Подробнее об этом режиме можно прочитать в этой и этой документации.
Помимо этого, можно убрать False Alarm метки без хешей.
Если способы, указанные выше, не подходят, то удалите настройку:
//V_HASH_ONLY ENABLE
Также настройку можно выключить через директиву:
//V_HASH_ONLY DISABLE
Подобное отключение директивы может быть полезно в случае, если необходимо применить настройку к конкретному блоку кода.
Наличие предупреждения V019 в отчёте означает, что возникли проблемы при работе с пользовательскими аннотациями.
Предупреждение может быть выдано в следующих ситуациях:
Наличие предупреждения V020 в отчёте означает, что возникли проблемы при работе с файлами конфигурации диагностических правил (.pvsconfig). Конкретная причина ошибки указывается вместе с сообщением диагностики.
В случае, если проблему не удалось устранить самостоятельно, мы будем рады вам помочь. Для этого вы можете написать нам через форму обратной связи, какие именно настройки (или их сочетание) привели к появлению проблемы. Для упрощения её выявления просим также приложить к сообщению файл конфигурации, который стал причиной появления предупреждения.
Сообщение V051 означает, что после загрузки в анализатор, C# проект содержит ошибки компиляции. Обычно к таким ошибкам относятся неизвестные типы данных, пространства имён и сборки (dll файлы). Чаще всего подобная ошибка возникает, когда производится попытка проверить проект, имеющий в зависимостях сборки из nuget пакетов, отсутствующие на локальной машине, либо сторонние библиотеки, отсутствующие среди проектов в выбранном решении.
Несмотря на данную ошибку, анализатор попытается проверить ту часть кода, которая не содержит неопределённых типов, однако результаты такого анализа могут быть неполными - часть сообщений анализатора может быть потеряна. Это связано с тем, что для корректной работы большинству диагностик анализатора требуется полная информация обо всех типах данных, встречающихся в проверяемых исходных файлах, в том числе и о типах, реализованных в сторонних сборках.
Даже если восстановление файлов зависимостей проекта включено в его сборочный сценарий, анализатор не будет выполнять полной пересборки проекта самостоятельно. Поэтому перед проверкой проекта мы рекомендуем обеспечить его полную собираемость, включая наличие всех сборок (dll файлов), от которых проект зависит.
Иногда анализатор может ошибаться, и выдавать такое сообщение на полностью компилирующийся проект, содержащий все зависимости. Это может произойти, например, если проект использует нестандартный сборочный сценарий MSBuild. Например, проектные файлы csproj импортируют какие-либо дополнительные props и target файлы. В таком случае вы можете игнорировать сообщение V051 либо отключить его отображение в настройках анализатора.
Если вы хотите узнать, какая именно ошибка компилятора приводит к V051, запустите анализ вашего проекта с помощью cmd версии анализатора, добавив к строке запуска флаг '‑‑logCompilerErrors' (одной строчкой):
PVS-Studio_Cmd.exe –t MyProject.sln –p "Any CPU" –c "Debug"
--logCompilerErrors
Вы можете подавить ошибки, которые приводят к выдаче V051, с помощью .pvsconfig файла. Подробнее про это можно почитать в документации.
Сообщение V052 означает, что произошла критическая ошибка внутри анализатора. Скорее всего, в этом случае часть файлов не будет проанализирована.
Дополнительную информацию об этой ошибке вы можете получить из двух источников: файла отчёта анализатора (plog) и стандартного потока вывода сообщений об ошибках stderr (при использовании command line версии).
При использовании IDE Visual Studio или приложения Standalone стек ошибки выводится в окно PVS-Studio. Стек будет записан в самое начало plog файла. При этом стек разбивается на подстроки, каждая из которых фиксируется и отображается как отдельная ошибка без номера.
В случае работы из командной строки вы можете проанализировать код возврата command line версии, понять, что произошло исключение, и уже затем изучить plog, не открывая его в IDE Visual Studio или приложении Standalone. Для этого отчет можно преобразовать, например, в текстовый файл при помощи утилиты PlogConverter. Коды возврата command line версии описаны в разделе документации "Проверка Visual C++ (.vcxproj) и Visual C# (.csproj) проектов из командной строки с помощью PVS-Studio", использование утилиты PlogConverter - "Работа с результатами анализа (.plog файл)".
Хотя сообщение V052 возникает достаточно редко, мы будем вам благодарны, если вы поможете нам исправить проблему, которая привела к этому сообщению. Для этого пришлите, пожалуйста, стек ошибки из окна PVS-Studio (или из stderr в случае использования command line версии) на адрес support@viva64.com.
Сообщение V061 означает, что во время работы анализатора произошла какая-то ошибка.
Например, возникло непредвиденное исключение в анализаторе, не удалось построить семантическую модель программы и т.п.
Пожалуйста, напишите нам в поддержку и приложите текстовые файлы из директории .PVS-Studio (находится в директории с проектом), чтобы мы смогли как можно скорее исправить данную ошибку.
Кроме того, можно использовать параметр 'verbose', чтобы анализатор в процессе работы сохранял дополнительную информацию в директории .PVS-Studio. Эта информация также будет для нас полезна.
В плагине для Maven:
<verbose>true</verbose>
В плагине для Gradle:
verbose = true
В плагине для IntelliJ IDEA:
1) Analyze -> PVS-Studio -> Settings
2) Вкладка Misc -> убрать 'Remove intermediate files'
Сообщение V062 означает, что плагину не удалось запустить ядро анализатора. Скорее всего это связано с тем, что ядро запускалось с некорректной версией Java. Для корректной работы ядро должно запускаться с 64-битной Java версией 11 или выше. По умолчанию анализатор использует Java интерпретатор из переменной окружения PATH.
Путь до требуемого Java интерпретатора можно указать вручную.
В плагине для Maven:
<javaPath>C:/Program Files/Java/jdk11.0.17/bin/java.exe</javaPath>
В плагине для Gradle:
javaPath = "C:/Program Files/Java/jdk11.0.17/bin/java.exe"
В плагине для IntelliJ IDEA:
1) Analyze -> PVS-Studio -> Settings
2) Вкладка Environment -> Java executable
В случае если все же не удается запустить анализ, пожалуйста, напишите нам в поддержку и приложите текстовые файлы из директории .PVS-Studio (находится в директории с проектом). Мы постараемся как можно скорее разобраться в Вашей проблеме.
Сообщение V063 означает, что анализатор не смог проанализировать какой-либо файл за отведенное время (по умолчанию 10 минут). Зачастую подобные сообщения также сопровождаются сообщениями "GC overhead limit exceeded".
Иногда эту проблему можно решить просто увеличив доступный анализатору объем памяти и стека.
В плагине для Maven:
<jvmArguments>-Xmx4096m, -Xss256m</jvmArguments>
В плагине для Gradle:
jvmArguments = ["-Xmx4096m", "-Xss256m"]
В плагине для IntelliJ IDEA:
1) Analyze -> PVS-Studio -> Settings
2) Вкладка Environment -> JVM arguments
Обычно объема памяти по умолчанию может не хватать при анализе сгенерированного кода с большим количеством вложенных конструкций.
Возможно, стоит исключить этот код из анализа (с помощью 'exclude'), чтобы не тратить на него лишнее время.
Кроме того, V063 может возникать, если из-за загруженности процессора анализатору достается мало системных ресурсов и, хотя файл он мог бы обработать корректно, он не успевает сделать это за отведенное время.
Если все же не удается проверить какой-то файл, это может свидетельствовать об ошибке в анализаторе. В таком случае, пожалуйста, напишите нам в поддержку и приложите текстовые файлы из директории .PVS-Studio (находится в директории с проектом), а также примерный код, на котором возникает проблема, чтобы мы смогли как можно скорее исправить данную ошибку.
Анализатор обнаружил потенциально возможную ошибку, связанную с неявным приведением типа при выполнении оператора присваивания "=". Ошибка может заключаться в некорректном вычислении значения выражения, стоящего справа от оператора присваивания "=".
Пример кода, вызывающего предупреждение:
size_t a;
unsigned b;
...
a = b; // V101
Сама по себе операция приведения 32-битного типа к memsize-типу безопасна, поскольку не происходит потери данных. Например, в переменной типа size_t всегда можно сохранить значение переменной типа unsigned. Но наличие такого приведения типа может указывать на скрытую ошибку, допущенную ранее.
Первой причиной возникновения ошибки на 64-битной системе может служить изменение процесса вычисления выражений. Рассмотрим пример:
unsigned a = 10;
int b = -11;
ptrdiff_t c = a + b; //V101
cout << c << endl;
Данный код на 32-битной системе распечатает значение -1, а на 64-битной системе 4294967295. Данное поведение полностью согласуется с правилами приведения типов в языке Си++, но скорее всего приведет к ошибке в реальном коде.
Поясним приведенный пример. Согласно правилам языка Си++ выражение a+b имеет тип unsigned и содержит значение 0xFFFFFFFFu. На 32-битной системе тип ptrdiff_t представляет собой знаковый 32-битный тип. После присвоения 32-битной знаковой переменной значения 0xFFFFFFFFu она будет содержать значение -1. На 64-битной системе тип ptrdiff_t представляет собой знаковый 64-битный тип. Это означает, что число 0xFFFFFFFFu будет представлено как оно есть. То есть значение переменной после присваивания будет равняться 4294967295.
Ошибку можно исправить, исключив смешанное использования memsize и не memsize-типов в одном выражении. Пример исправления кода:
size_t a = 10;
ptrdiff_t b = -11;
ptrdiff_t c = a + b;
cout << c << endl;
Еще более корректным способом исправления будет отказ от смешенного использования знаковых и беззнаковых типов данных.
Второй причиной ошибки может стать переполнение, возникающее в 32-битных типах данных. В этом случае ошибка может содержаться раньше самого оператора присваивания, но обнаружить ее можно только косвенно. Такие ошибки часто встречаются в коде, выделяющим большие объемы памяти. Рассмотрим пример:
unsigned Width = 1800;
unsigned Height = 1800;
unsigned Depth = 1800;
// Real error is here
unsigned CellCount = Width * Height * Depth;
// Here we get a diagnostic message V101
size_t ArraySize = CellCount * sizeof(char);
cout << ArraySize << endl;
void *Array = malloc(ArraySize);
Предположим, что на 64-битной системе мы решили обрабатывать массивы данных более 4 гигабайт. В этом случае показанный код приведет к выделению ошибочного объема памяти. Программист планирует выделить 5832000000 байт оперативной памяти, а вместо этого получит в свое распоряжение только 1537032704. Это происходит из-за переполнения при вычислении выражения Width * Height * Depth. К сожалению мы не можем диагностировать ошибку в строке с этим выражением, но мы можем косвенно указать на наличие ошибки, обнаружив приведение типа в строке:
size_t ArraySize = CellCount * sizeof(char); //V101
Исправление ошибки заключается в использовании типов, позволяющих хранить необходимый диапозон значений. Заметьте, что исправлние следующего вида не является корректным:
size_t CellCount = Width * Height * Depth;
Здесь мы по-прежнему имеем переполнение. Приведем два примера корректного исправления кода:
// 1)
unsigned Width = 1800;
unsigned Height = 1800;
unsigned Depth = 1800;
size_t CellCount =
static_cast<size_t>(Width) *
static_cast<size_t>(Height) *
static_cast<size_t>(Depth);
// 2)
size_t Width = 1800;
size_t Height = 1800;
size_t Depth = 1800;
size_t CellCount = Width * Height * Depth;
Следует учитывать, что ошибка может находиться не просто выше, а вообще в другом модуле. Приведем соответствующий пример. В нем ошибка заключается в некорректном вычислении индекса, если размер массива превысит 4 Гб.
Пусть приложение использует большой одномерный массив, и функция CalcIndex позволяет адресоваться к этому массиву как к двумерному массиву.
extern unsigned ArrayWidth;
unsigned CalcIndex(unsigned x, unsigned y) {
return x + y * ArrayWidth;
}
...
const size_t index = CalcIndex(x, y); //V101
Анализатор сообщит о проблеме в строке: const size_t index = CalcIndex(x, y). Но ошибка заключается в неправильной реализации функции CalcIndex. Если взять функцию CalcIndex отдельно, то она абсолютно корректна. Типом выходного и входных значений является unsigned. Вычисления также происходят с участием только типов unsigned. Никаких явных или неявных приведений типов нет, и анализатор не имеет возможности распознать логическую проблему, связанную с функцией CalcIndex. Ошибка заключается в том, что неверно выбран результат, возвращаемый функцией, а возможно и входных значений. Результат функции должен иметь тип memsize.
К счастью анализатор смог обнаружить неявное приведение результата функции CalcIndex к типу size_t. Это позволяет вам проанализировать ситуацию и внести в программу необходимые изменения. Исправление ошибки, например, может выглядеть следующим образом:
extern size_t ArrayWidth;
size_t CalcIndex(size_t x, size_t y) {
return x + y * ArrayWidth;
}
...
const size_t index = CalcIndex(x, y);
Если вы уверены, что код верен и размер массива никогда не приблизится к границе в 4Гб, вы можете подавить предупреждение анализатора, используя явное приведение типа:
extern unsigned ArrayWidth;
unsigned CalcIndex(unsigned x, unsigned y) {
return x + y * ArrayWidth;
}
...
const size_t index = static_cast<size_t>(CalcIndex(x, y));
В ряде случаев и сам анализатор может понять, что переполнение при вычислении невозможно и предупреждение не будет распечатано.
Рассмотрим последний пример, связанный с некорректными операциями сдвига.
ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) {
ptrdiff_t mask = 1 << bitNum; //V101
return value | mask;
}
Выражение " mask = 1 << bitNum " является опасным, так как данный код не может выставить в единицы старшие разряды 64-битной переменной mask. Если попробовать использовать функцию SetBitN для выставления, например, 33-го бита, то произойдет переполнение при выполнении операции сдвига и желаемый результат не будет достигнут.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку в адресной арифметике с указателями. Ошибка может заключаться в переполнении при вычислении выражения.
Рассмотрим пример.
short a16, b16, c16;
char *pointer;
...
pointer += a16 * b16 * c16;
Данный пример корректно работает с указателями, если значение выражения "a16 * b16 * c16" не превышает 'INT_MAX' (2Гб). Такой код мог всегда корректно работать на 32-битной платформе, в силу того, что программа никогда не выделяла массивов больших размеров. На 64-битной архитектуре программиста, использующего старый код для работы с массивом большего размера, ждет разочарование. Допустим, мы хотим сдвинуть значение указателя на 3000000000 байт, и по этому переменные 'a16', 'b16' и 'c16' имеют значения 3000, 1000 и 1000 соответственно. При вычислении выражения "a16 * b16 * c16" все переменные, согласно правилам языка Си++, будут приведены к типу int, а уже затем будет произведено их умножение. В ходе выполнения умножения, произойдет переполнение, в результате которого будет получено число -1294967296. Некорректный результат выражения будет расширен до типа 'ptrdiff_t' и произойдет вычисление указателя. В результате нас будет ожидать аварийное завершение программы, при попытке использования некорректного указателя.
Для предотвращения подобных ошибок следует использовать memsize типы. В нашем случае будет корректно заменить типы переменных 'a16', 'b16', 'c16' или использовать их явное приведение к типу 'ptrdiff_t', как показано ниже:
short a16, b16, c16;
char *pointer;
...
pointer += static_cast<ptrdiff_t>(a16) *
static_cast<ptrdiff_t>(b16) *
static_cast<ptrdiff_t>(c16)
Хочется отметить, что не всегда использование в арифметике с указателями не memsize типов является ошибочным. Рассмотрим следующую ситуацию:
char ch;
short a16;
int *pointer;
...
int *decodePtr = pointer + ch * a16;
Анализатор не выдает на нее сообщения, в силу ее корректности. Здесь не происходит вычислений, которые могли бы привести к переполнению и результат этого выражения всегда будет корректен как на 32-битной, так и на 64-битной платформе.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с неявным приведением memsize типа к 32-битному типу. Ошибка заключается в потере старших бит в 64-битном типе, что влечет потерю значения. Компилятор также диагностирует подобные приведения типов и выдает предупреждения. К сожалению, часто подобные предупреждения отключают, особенно когда в проекте присутствует много старого унаследованного кода или используются старые библиотеки. Чтобы не заставлять программиста просматривать сотни и тысячи подобных предупреждений, выдаваемых компилятором, анализатор информирует только о тех из них, которые могут быть причиной некорректной работы кода на 64-битной платформе.
Первый пример.
Наше приложение работает с видеоизображениями, и мы хотим посчитать, какой размер файла нам потребуется, чтобы сохранить все кадры, находящиеся в памяти, в файл.
size_t Width, Height, FrameCount;
...
unsigned BufferSizeForWrite = Width * Height * FrameCount *
sizeof(RGBStruct);
Раньше общий объем видеокадров в памяти никогда не мог превышать 4 Гб (на практике 2-3 Гб, в зависимости от разновидности ОС Windows). На 64-битной платформе мы получили возможность хранить в памяти намного больше кадров, и предположим, что их общий объем составляет 10 Гб. В результате при помещении результата выражения "Width * Height * FrameCount * sizeof(RGBStruct)" в переменную 'BufferSizeForWrite', мы отбросим старшие биты и будем работать с некорректным значением.
Правильным решением будет замена типа переменной 'BufferSizeForWrite' на тип 'size_t'.
size_t Width, Height, FrameCount;
...
size_t BufferSizeForWrite = Width * Height * FrameCount *
sizeof(RGBStruct);
Второй пример.
Сохранение в 32-битном типе результата вычитания одного указателя из другого.
char *ptr_1, *ptr_2;
...
int diff = ptr_2 - ptr_1;
Если указатели различаются более чем на 'INT_MAX' байт (2 Гб), то произойдет обрезание значения при присваивании. В результате переменная 'diff' будет иметь некорректное значение. Для хранения полученного значения следует использовать тип 'ptrdiff_t' или другой memsize тип.
char *ptr_1, *ptr_2;
...
ptrdiff_t diff = ptr_2 - ptr_1;
В тех случаях, когда вы точно уверены в корректности кода, и неявное приведение типа не влечет ошибок при переходе на 64-битную архитектуру, вы можете использовать явное приведение типа для того, чтобы избежать вывода диагностических сообщений в этой строке. Пример:
unsigned BitCount = static_cast<unsigned>(sizeof(RGBStruct) * 8);
В том случае, если вы подозреваете наличие в своем коде некорректных явных приведений memsize типов к 32-битным типам, на которые анализатор не выдает предупреждения, то вы можете воспользоваться правилом V202.
Как было сказано ранее, анализатор информирует только о тех приведениях типов, которые могут быть причиной некорректной работы кода на 64-битной платформе. Приведенный ниже код не будет диагностироваться как ошибочный, хотя в нем происходит приведение memsize типов к типу int:
int size = sizeof(float);
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку внутри арифметического выражения, связанную с неявным приведением к типу memsize. Ошибка переполнения может быть связана с изменением допустимого интервала значений переменных, входящих в выражение.
Первый пример.
Некорректные выражения сравнения. Рассмотрим код:
size_t n;
unsigned i;
// Infinite loop (n > UINT_MAX).
for (i = 0; i != n; ++i) { ... }
В примере показана ошибка, связанная с неявным приведением типа 'unsigned' к типу 'size_t' при выполнении операции сравнения.
На 64-битной платформе у вас может появиться возможность обрабатывать больший объем данных, и значение переменной 'n' может превысить число 'UINT_MAX' (4 Гб). В результате, условие "i != n" всегда будет истинно, что приведет к вечному циклу.
Пример исправленного кода:
size_t n;
size_t i;
for (i = 0; i != n; ++i) { ... }
Второй пример.
char *begin, *end;
int bufLen, bufCount;
...
ptrdiff_t diff = begin - end + bufLen * bufCount;
Неявное приведение типа 'int' к типу 'ptrdiff_t' зачастую служит признаком ошибки. Следует обратить внимание, что приведение происходит не при выполнении оператора "=" (так как выражение "begin - end + bufLen * bufCount" имеет тип 'ptrdiff_t'), а внутри этого выражения. Подвыражение "begin - end" согласно правилам языка С++ имеет тип 'ptrdiff_t', а правая "bufLen * bufCount" тип 'int'. При переходе на 64-битную платформу программа может начать обрабатывать больший объем данных, в результате чего может произойти переполнение при вычислении подвыражения "bufLen * bufCount".
Следует изменить тип переменных 'bufLen' и 'bufCount' на memsize тип или использовать явное приведение типа, как показано на примере:
char *begin, *end;
int bufLen, bufCount;
...
ptrdiff_t diff = begin - end +
ptrdiff_t(bufLen) * ptrdiff_t(bufCount);
Отметим, что неявное приведение к типу memsize внутри выражений не всегда является ошибочным. Рассмотрим следующую ситуацию:
size_t value;
char c1, c2;
size_t result = value + c1 * c2;
Анализатор не выдает сообщения, хотя здесь происходит приведение типа 'int' к 'size_t', так как при вычислении подвыражения "c1 * c2" переполнения произойти не может.
Если вы подозреваете в программе наличие ошибок, связанных с некорректным явным приведением типов в выражениях, то Вы можете воспользоваться правилом V201. Пример ситуации, при которой явное приведение типа к 'size_t' скрывает ошибку, представлен ниже:
int i;
size_t st;
...
st = size_t(i * i * i) * st;
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку внутри арифметического выражения, связанную с неявным приведением к типу memsize. Ошибка переполнения может быть связана с изменением допустимого интервала значений переменных, входящих в выражение. Данное предупреждение практически эквивалентно предупреждению V104, за тем исключением, что неявное приведение типа возникает за счет использования операции '?:'.
Приведем пример неявного приведения типов при использовании операции:
int i32;
float f = b != 1 ? sizeof(int) : i32;
В арифметическом выражении используется тернарная операция '?:', имеющая три операнда:
Результатом выражения "b != 1 ? sizeof(int) : i32" является значение типа 'size_t', которое затем преобразуется в значение типа 'float'. Таким образом, неявное приведение типа осуществляется для 3-его операнда операции '?:'.
Рассмотрим пример некорректного кода:
bool useDefaultVolume;
size_t defaultVolume;
unsigned width, height, depth;
...
size_t volume = useDefaultVolume ?
defaultVolume :
width * height * depth;
Предположим, мы разрабатываем приложение численного моделирования, для которого требуется трехмерная счетная область. Количество используемых счетных элементов определяется в зависимости от значения переменной 'useDefaultSize' и задается по умолчанию или произведением длинны, высоты и глубины счетной области. На 32-битной платформе объем уже выделенной памяти не может превышать 2-3 Гб (в зависимости от версии ОС Windows) и, соответственно, результат выражения "width * height * depth" всегда будет корректен. На 64-битной платформе, пользуясь возможностью работать с большим объемом памяти, количество счетных элементов может превысить значение 'UINT_MAX' (4 Гб). В этом случае произойдет переполнение при вычислении выражения "width * height * depth", так как результат этого выражения имеет тип 'unsigned'.
Исправление кода может заключаться в изменении типа переменных 'width', 'height' и 'depth' на memsize тип, как показано ниже:
...
size_t width, height, depth;
...
size_t volume = useDefaultVolume ?
defaultVolume :
width * height * depth;
Или использовании явного приведения типов:
unsigned width, height, depth;
...
size_t volume = useDefaultVolume ?
defaultVolume :
size_t(width) * size_t(height) * size_t(depth);
Дополнительно мы рекомендуем изучить описание аналогичного предупреждения V104, где можно ознакомиться с другими эффектами неявного приведения к типу memsize.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с неявным приведением фактического аргумента функции к memsize типу.
Первый пример.
Программа работает с большими массивами, используя контейнер 'CArray' из библиотеки MFC. На 64-битной платформе количество элементов массиве может превысить значение 'INT_MAX' (2Гб), что приведет к неработоспособности следующего кода:
CArray<int, int> myArray;
...
int invalidIndex = 0;
INT_PTR validIndex = 0;
while (validIndex != myArray.GetSize()) {
myArray.SetAt(invalidIndex, 123);
++invalidIndex;
++validIndex;
}
Данный код заполняет все элементы массива 'myArray' значением 123. Он выглядит совершенно корректно, и вы не получите от компилятора никаких предупреждений, несмотря на его неработоспособность на 64-битной архитектуре. Ошибка заключается в использовании в качестве индекса переменной 'invalidIndex' типа 'int'. Когда значение переменной 'invalidIndex' превысит 'INT_MAX', произойдет ее переполнение, и оно получит значение равное "-1". Анализатор диагностирует данную ошибку, предупреждая, что первый аргумент функции 'SetAt' неявно приводится к memsize типу (которым в данном случае является тип 'INT_PTR'). Получив такое предупреждение, вы можете исправить ошибку, изменив тип 'int' на более подходящий.
Данный пример показателен тем, что обвинять программиста в некачественном коде не очень честно. Дело в том, что в старой версии библиотеки MFC функция 'SetAt' в классе 'CArray' была объявлена следующим образом:
void SetAt(int nIndex, ARG_TYPE newElement);
А в новой:
void SetAt(INT_PTR nIndex, ARG_TYPE newElement);
Даже разработчики Microsoft, создавая MFC, не смогли учесть все возможные последствия использования типа 'int' для индексации в массиве, и можно простить простого разработчика, написавшего такой код.
Приведем исправленный пример:
...
INT_PTR invalidIndex = 0;
INT_PTR validIndex = 0;
while (validIndex != myArray.GetSize()) {
myArray.SetAt(invalidIndex, 123);
++invalidIndex;
++validIndex;
}
Второй пример.
Программа вычисляет необходимый ей размер массива данных, а затем выделяет его с использованием функции 'malloc', как показано ниже:
unsigned GetArraySize();
...
unsigned size = GetArraySize();
void *p = malloc(size);
Анализатор выдаст предупреждение на строку "void *p = malloc(size);". Посмотрев определение функции 'malloc', мы увидим, что ее формальный аргумент, задающий размер выделяемой памяти, представлен типом 'size_t'. В программе же в качестве фактического аргумента используется переменная 'size' типа 'unsigned'. Если вашей программе на 64-битной архитектуре понадобится массив более 'UINT_MAX' байт (4Гб), то можно с уверенностью утверждать, что приведенный код неверен, так как тип 'unsigned' не может хранить значение более 'UINT_MAX'. Исправление программы заключается в изменении типов переменных и функций, участвующих в вычислении размера массива данных. В приведенном примере необходимо заменить тип 'unsigned' на один из memsize типов, а также, если это необходимо модифицировать код функции 'GetArraySize'.
...
size_t GetArraySize();
...
size_t size = GetArraySize();
void *p = malloc(size);
Анализатор выдает предупреждения на неявное приведение типа только в том случае, если оно может привести к ошибке при переносе программы на 64-битную платформу. Ниже приведен код, в котором присутствует неявное приведения типов, но которое не вызывает ошибок:
void MyFoo(SSIZE_T index);
...
char c = 'z';
MyFoo(0);
MyFoo(c);
Если вы точно уверены, что неявное приведение типа фактического аргумента функции совершенно корректно, то для подавления предупреждения анализатора вы можете использовать явное приведение типа, как показано ниже:
typedef size_t TYear;
void MyFoo(TYear year);
int year;
...
MyFoo(static_cast<TYear>(year));
Иногда явное приведение типа может маскировать ошибку. В этом случае вы можете воспользоваться правилом V201.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с неявным приведением фактического аргумента функции, имеющего тип memsize к 32-битному типу.
Рассмотрим пример кода, в котором реализована функция поиска максимального элемента в массиве:
float FindMaxItem(float *array, int arraySize) {
float max = -FLT_MAX;
for (int i = 0; i != arraySize; ++i) {
float item = *array++;
if (max < item)
max = item;
}
return max;
}
...
float *beginArray;
float *endArray;
float maxValue = FindMaxItem(beginArray, endArray - beginArray);
Данный код успешно может работать на 32-битной платформе, но не сможет обрабатывать массивы, состоящие более чем из 'INT_MAX' (2Гб) элементов на 64-битной архитектуре. Это ограничение вызвано использованием типа 'int' для аргумента 'arraySize'. Обратите внимание, что код функции выглядит совершенно корректно не только с точки зрения компилятора, но и анализатора. В этой функции отсутствует приведение типов, и невозможно выявить потенциальную проблему.
Анализатор предупредит о неявном приведении memsize типа к 32-битному типу при вызове функции 'FindMaxItem'. Попробуем разобраться, почему это происходит. Согласно правилам языка Си++, результат вычитания одного указателя из другого имеет тип 'ptrdiff_t'. При вызове функции 'FindMaxItem' произойдет неявное приведение типа 'ptrdiff_t' к типу 'int', в результате чего произойдет потеря старших битов. Это может стать причиной некорректного поведения программы, при обработке большого объема данных.
Правильным решением будет заменить тип 'int' на тип 'ptrdiff_t', так как он позволит хранить весь диапазон допустимых значений. Исправленный код:
float FindMaxItem(float *array, ptrdiff_t arraySize) {
float max = -FLT_MAX;
for (ptrdiff_t i = 0; i != arraySize; ++i) {
float item = *array++;
if (max < item)
max = item;
}
return max;
}
Анализатор по возможности старается распознать безопасные приведения типов и не выдавать на них предупреждения. Например, анализатор не выдаст предупреждение на вызов функции FindMaxItem в следующем коде:
float Arr[1000];
float maxValue =
FindMaxItem(Arr, sizeof(Arr)/sizeof(float));
В тех случаях, когда вы точно уверены в корректности кода и неявное приведение типа фактического аргумента функции не влечет ошибок, вы можете использовать явное приведение типа для того, чтобы избежать вывода диагностических сообщений. Пример:
extern int nPenStyle
extern size_t nWidth;
extern COLORREF crColor;
...
// Call constructor CPen::CPen(int, int, COLORREF)
CPen myPen(nPenStyle, static_cast<int>(nWidth), crColor);
В том случае, если вы подозреваете наличие в своем коде некорректных явных приведений memsize типов к 32-битным типам, на которые анализатор не выдает предупреждения, то вы можете воспользоваться правилом V202.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку индексации больших массивов. Ошибка может заключаться в некорректном вычислении индекса.
Первый пример.
extern char *longString;
extern bool *isAlnum;
...
unsigned i = 0;
while (*longString) {
isAlnum[i] = isalnum(*longString++);
++i;
}
Данный код совершенно корректен для 32-битной платформы, где принципиально невозможна обработка массивов более 'UINT_MAX' байт (4 Гб). На 64-битной платформе можно обработать массив размером более 4 Гб, что иногда очень удобно. Но корректно обработать большой массив приведенным алгоритмом невозможно. Ошибка заключается в использовании для индексации массива 'isAlnum' переменной типа 'unsigned'. Когда мы заполним первые 'UINT_MAX' элементов, произойдет переполнение переменной 'i', и она приравняется нулю. В результате мы начнем перезаписывать элементы массива 'isAlnum' расположенные в начале, а часть элементов оставим неинициализированными.
Корректным исправлением является изменения типа переменной 'i' на memsize тип:
...
size_t i = 0;
while (*longString)
isAlnum[i++] = isalnum(*longString++);
Второй пример.
class Region {
float *array;
int Width, Height, Depth;
float Region::GetCell(int x, int y, int z) const;
...
};
float Region::GetCell(int x, int y, int z) const {
return array[x + y * Width + z * Width * Height];
}
Для программ численного моделирования важным ресурсом является объем оперативной памяти, и возможность на 64-битной архитектуре использовать более 4 Гб памяти существенно увеличивает вычислительные возможности. В таких программах часто используют одномерные массивы, работая затем с ними как с трехмерными. Для этого существуют функции, аналогичные 'GetCell', обеспечивающие доступ к необходимым элементам счетной области. Но приведенный код может корректно работать с массивами, содержащими не более 'INT_MAX' (2 Гб) элементов. Причина заключается в использовании 32-битных типов 'int', участвующих в вычислении индекса элемента. Если количество элементов в массиве 'array' превысит 'INT_MAX' (2 Гб), то произойдет переполнение и значение индекса будет вычислено некорректно. Программисты часто допускают ошибку, пытаясь исправить код следующим образом:
float Region::GetCell(int x, int y, int z) const {
return array[static_cast<ptrdiff_t>(x) + y * Width +
z * Width * Height];
}
Они знают, что по правилам языка Си++ выражение для вычисления индекса будет иметь тип 'ptrdiff_t' и надеются за счет этого избежать переполнения. К сожалению, переполнение может произойти внутри подвыражения "y * Width или z * Width * Height" ,так как для их вычисления по-прежнему используется тип 'int'.
Если вы хотите исправить код, не изменяя типов переменных, участвующих в выражении, то вы должны явно привести каждую переменную к memsize типу:
float Region::GetCell(int x, int y, int z) const {
return array[ptrdiff_t(x) +
ptrdiff_t(y) * ptrdiff_t(Width) +
ptrdiff_t(z) * ptrdiff_t(Width) *
ptrdiff_t(Height)];
}
Другое решение - изменить типы переменных на memsize тип:
class Region {
float *array;
ptrdiff_t Width, Height, Depth;
float
Region::GetCell(ptrdiff_t x, ptrdiff_t y, ptrdiff_t z) const;
...
};
float Region::GetCell(ptrdiff_t x, ptrdiff_t y, ptrdiff_t z) const
{
return array[x + y * Width + z * Width * Height];
}
Если вы используете для индексации выражения, имеющие тип отличный от memsize, но при этом точно уверены в корректности кода, то для подавления диагностирующих сообщений анализатора можно использовать явное приведение типов, как показано ниже.
bool *Seconds;
int min, sec;
...
bool flag = Seconds[static_cast<size_t>(min * 60 + sec)];
Если вы подозреваете в программе наличие ошибок, связанных с некорректным явным приведением типов в выражениях, то вы можете воспользоваться предупреждением V201.
По возможности анализатор пытается понять, когда использование не memsize-типа в качестве индекса массива безопасно и не выдавать в этом месте предупреждения. В результате поведение анализатора иногда может показаться странным. В таких ситуациях мы просим не спешить и постараться разобраться, в ситуации. Рассмотрим следующий код:
char Arr[] = { '0', '1', '2', '3', '4' };
char *p = Arr + 2;
cout << p[0u + 1] << endl;
cout << p[0u - 1] << endl; //V108
Данный код исправно работает в 32-битном режиме и печатает на экране числа 3 и 1. При проверке этого кода мы получим предупреждение только на одну строку с выражением "p[0u - 1]". И это совершенно верно. Если вы скомпилируете и запустите данный пример в 64-битном режиме, то увидите, как на экране будет распечатано значение 3, после чего произойдет аварийное завершений программы.
Ошибка связана с тем, что индексация "p[0u - 1]" на 64-битной системе некорректна, о чем и предупреждает анализатор. Согласно правилам языка Си++ выражение "0u - 1" будет иметь тип unsigned и равняться 0xFFFFFFFFu. На 32-битной архитектуре сложение указателя с этим числом будет эквивалентно вычитанию единицы. А на 64-битной системе к указателю будет честно прибавлено 0xFFFFFFFFu и произойдет обращение к памяти за приделами массива.
Конечно, часто индексация к массивам с использованием таких типов как int и unsigned безопасна. В этом случае предупреждения анализатора могут казаться не уместными. Но следует учитывать, что такой код все равно может оказаться ненадежным в случае его модернизации для обработки другого набора данных. Код с использованием типов int и unsigned может оказаться менее производительным, чем это возможно на 64-битной архитектуре.
Если вы уверены в корректности индексации, то вы можете воспользоваться функцией "Suppression of false alarms" или использовать фильтры. Можно использовать явное приведение типов в коде:
for (int i = 0; i != n; ++i)
Array[static_cast<ptrdiff_t>(i)] = 0;
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с неявным приведением типа возвращаемого значения. Ошибка может заключаться в некорректном вычислении возвращаемого значения.
Рассмотрим пример.
extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
return x + y * Width + z * Width * Height;
}
...
array[GetIndex(x, y, z)] = 0.0f;
В случае работы с большими массивами (более 'INT_MAX' элементов) данный код будет вести себя некорректно, и мы будет адресоваться не к тем элементам массива 'array', к которым рассчитываем. Но анализатор не выдаст предупреждение на строчку "array[GetIndex(x, y, z)] = 0.0f;",так как она совершенно корректна. Анализатор информирует о потенциальной ошибке внутри функции и совершенно прав, так как ошибка находится именно там и связана с арифметическим переполнением. Несмотря на то, что мы возвращаем значение типа 'size_t', выражение "x + y * Width + z * Width * Height" вычисляется с использованием типа 'int'.
Для исправления ошибки следует использовать явное приведение всех переменных участвующих в выражении к memsize типам.
extern int Width, Height, Depth;
size_t GetIndex(int x, int y, int z) {
return (size_t)(x) +
(size_t)(y) * (size_t)(Width) +
(size_t)(z) * (size_t)(Width) * (size_t)(Height);
}
Другим вариантом исправления является использование других типов для переменных, участвующих в выражении.
extern size_t Width, Height, Depth;
size_t GetIndex(size_t x, size_t y, size_t z) {
return x + y * Width + z * Width * Height;
}
В тех случаях, когда вы точно уверены в корректности кода, и неявное приведение типа не влечет ошибок при переходе на 64-битную архитектуру, вы можете использовать явное приведение типа для того, чтобы избежать вывода диагностических сообщений в этой строке. Пример:
DWORD_PTR Calc(unsigned a) {
return (DWORD_PTR)(10 * a);
}
В том случае, если вы подозреваете, наличие в своем коде некорректных явных приведений к memsize типам, на которые анализатор не выдает предупреждения, то вы можете воспользоваться правилом V201.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с неявным приведением возвращаемого значения. Ошибка заключается в отбрасывании старших бит в 64-битном типе, что влечет потерю значения.
Рассмотрим пример.
extern char *begin, *end;
unsigned GetSize() {
return end - begin;
}
Результат выражения "end - begin" имеет тип 'ptrdiff_t'. Но поскольку функция возвращает тип 'unsigned', то происходит неявное приведение типа, при котором старшие биты результата теряются. Таким образом, если указатели 'begin' и 'end' ссылаются на начало и конец массива, по размеру большего 'UINT_MAX' (4 Гб), то функция вернет некорректное значение.
Исправление должно заключаться в модификации программы таким образом, чтобы размеры массивов хранились и передавались в memsize типах. Тогда правильный код функции 'GetSize' должен выглядеть следующим образом:
extern char *begin, *end;
size_t GetSize() {
return end - begin;
}
В ряде случаев анализатор не выдает предупреждение на приведение типа, если оно явно корректно. Например, анализатор не выдаст предупреждение на следующий код, где результатом оператора sizeof() хотя и является тип size_t, но результат безопасно может быть помещен в тип unsigned:
unsigned GetSize() {
return sizeof(double);
}
В тех случаях, когда вы точно уверены в корректности кода и неявное приведение типа не влечет ошибок при переходе на 64-битную архитектуру, вы можете использовать явное приведение типа для того, чтобы избежать вывода диагностических сообщений. Пример:
unsigned GetBitCount() {
return static_cast<unsigned>(sizeof(TypeRGBA) * 8);
}
Если вы подозреваете наличие в своем коде некорректных явных приведений типов возвращаемых значений, на которые анализатор не выдает предупреждения, то вы можете воспользоваться правилом V202.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с передачей фактического аргумента типа memsize в функцию с переменным количеством аргументов. Потенциальная ошибка может заключаться в изменении требований, предъявляемых к функции на 64-битной системе.
Рассмотрим первый пример.
const char *invalidFormat = "%u";
size_t value = SIZE_MAX;
printf(invalidFormat, value);
Данный код не учитывает, что тип 'size_t' не эквивалентен типу 'unsigned' на 64-битной платформе. Это приведет к выводу на печать некорректного результата, в случае если "value > UINT_MAX". Анализатор предупреждает вас, что в качестве фактического аргумента используется тип memsize. А это значит, что вам следует проверить строку 'invalidFormat', задающую формат вывода. Исправленный вариант может выглядеть, как показано ниже.
const char *validFormat = "%Iu";
size_t value = SIZE_MAX;
printf(validFormat, value);
В коде реального приложения эта ошибка может встретиться, например, в следующем виде:
wsprintf(szDebugMessage,
_T("%s location %08x caused an access violation.\r\n"),
readwrite,
Exception->m_pAddr);
Второй пример.
char buf[9];
sprintf(buf, "%p", pointer);
Автор этого неаккуратного кода не учел, что размер указателя в будущем может стать более 32 бит. В результате данный код на 64-битной архитектуре приведет к переполнению буфера. Проанализировав код, на который выдано предупреждение V111, вы можете пойти двумя путями. Увеличить размер буфера, или переписать код с использованием безопасных конструкций.
char buf[sizeof(pointer) * 2 + 1];
sprintf(buf, "%p", pointer);
// --- or ---
std::stringstream s;
s << pointer;
Третий пример.
char buf[9];
sprintf_s(buf, sizeof(buf), "%p", pointer);
Рассматривая второй пример, вы могли справедливо заметить, что для предотвращения переполнения следует использовать функции with security enhancements. В этом случае переполнение буфера не произойдет, но, к сожалению, и не будет получен корректный результат.
Если типы аргументов не изменили своей разрядности, то код считается корректным и предупреждающих сообщений выдано не будет. Пример:
printf("%d", 10*5);
CString str;
size_t n = sizeof(float);
str.Format(StrFormat, static_cast<int>(n));
Диагностируя описанный тип ошибок, к сожалению, часто нет возможности отличить корректный код от не корректного кода. Данное предупреждение будет выдаваться на многие вызовы функций с переменным количеством аргументов, даже когда вызов совершенно верен. Это связано с принципиальной опасностью использования таких конструкций языка С++. Чаще всего проблемы возникают с использованием разновидности следующих функций: 'printf', 'scanf', 'CString::Format'. Общепринятой практикой является отказ от них и использование безопасных методик программирования. Мы настоятельно рекомендуем модифицировать код и использовать безопасные методы. Например, можно заменить 'printf' на 'cout', а 'sprintf' на 'boost::format' или 'std::stringstream'.
Примечание. Борьба с ложными срабатываниями при работе с функциями форматированного вывода
Диагностика V111 очень проста. Когда анализатор ничего не знает об эллипсис-функции, то он предупреждает о всех случаях, когда в функцию передается тип, меняющий свой размер. Когда информация о функции доступна анализатору, начинает работать более точная диагностика V576, и диагностика V111 не выдается. Если диагностика V576 отключена, то V111 выдается на всех функциях.
Соответственно, можно сократить количество ложных срабатываний, предоставив анализатору информацию о функциях форматирования. Анализатор сам знает о типовых функциях, таких как 'printf', 'sprintf' и так далее, поэтому речь сейчас идёт о разметке функций, реализованных самостоятельно. Как аннотировать функции рассказывается в описании диагностики V576.
Рассмотрим пример. Пользователь спрашивает: "Почему в случае N1 анализатор не выдает предупреждение V111, а в случае N2 выдаёт?".
void OurLoggerFn(wchar_t const* const _Format, ...)
{
....
}
void Foo(size_t length)
{
wprintf( L"%Iu", length ); // N1
OurLoggerFn( L"%Iu", length ); // N2
}
Дело в том, что анализатор знает, как работает стандартная функция 'wprintf'. А вот что такое 'OurLoggerFn' он не знает и на всякий случай предупреждает о передаче memsize-типа данных (в данном случае это тип 'size_t') в качестве фактического аргумента в эллипсис функцию.
Чтобы предупреждение V111 исчезло, следует проаннотировать функцию 'OurLoggerFn' следующим образом:
//+V576, function:OurLoggerFn, format_arg:1, ellipsis_arg:2
void OurLoggerFn(wchar_t const* const _Format, ...)
.....
Дополнительные материалы по данной теме:
Анализатор обнаружил использование опасной магической константы. Потенциальная ошибка может заключаться в использовании числовой константы в качестве специальных значений или размера memsize типа.
Примечание. Данная диагностика предназначена для решения узкоспециализированной задачи: найти магические числа на этапе портирования кода на 64-битную систему. Рационально один раз просмотреть все места, где используются потенциально опасные константы, после чего отключить диагностику. Нет смысла постоянно отвлекаться на предупреждения, сообщающие, что в коде используется, например константа 32. Регулярный просмотр таких сообщений утомляет и не приносит какой-либо пользы.
Рассмотрим первый пример.
size_t ArraySize = N * 4;
size_t *Array = (size_t *)malloc(ArraySize);
Программист при написании программы полагался на то, что размер 'size_t' всегда будет равен 4 и записал вычисление размера массива как "N * 4". Данный код не учитывает, что тип 'size_t' на 64-битной системе будет занимать 8 байт и выделит меньшее количество памяти, чем необходимо. Исправление кода заключается в использовании оператора 'sizeof' вместо константы 4.
size_t ArraySize = N * sizeof(size_t);
size_t *Array = (size_t *)malloc(ArraySize);
Второй пример.
size_t n = static_cast<size_t>(-1);
if (n == 0xffffffffu) { ... }
Иногда в качестве кода ошибки или другого специального маркера используют значение "-1", записывая его как "0xffffffff". На 64-битной платформе записанное выражение некорректно, и следует явно использовать значение "-1".
size_t n = static_cast<size_t>(-1);
if (n == static_cast<size_t>(-1)) { ... }
Перечислим магические константы, которые могут влиять на работоспособность приложения при переносе его на 64-битную систему и поэтому диагностируются анализатором.
Следует внимательно изучить код на наличия магических констант и заменить их безопасными константами и выражениями. Для этого можно использовать оператор 'sizeof()', специальные значения из <limits.h>, <inttypes.h> и так далее.
В ряде случаев использование магических констант не считается опасным. Например, на данный код предупреждение выдано не будет:
float Color[4];
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с неявным преобразованием memsize типа в тип 'double' или наоборот. Потенциальная ошибка может заключаться в невозможности хранения всего диапазона значений memsize типа в переменных типа 'double'.
Рассмотрим пример.
SIZE_T size = SIZE_MAX;
double tmp = size;
size = tmp; // x86: size == SIZE_MAX
// x64: size != SIZE_MAX
Тип 'double' имеет размер 64-бита, и совместим со стандартом IEEE-754 на 32-битных и 64-битных системах. Некоторые программисты используют тип 'double' для хранения и работы с целочисленными типами.
Приведенный пример еще можно пытаться оправдывать на 32-битной системе, так как тип 'double' имеет 52 значащих бит и способен без потери хранить 32-битное целое значение. Но при попытке сохранить в переменной типа 'double' 64-битное целое число точное значение может быть потеряно (см. рисунок).
Если приближенное значение применимо для алгоритма работы вашей программе, то никаких исправлений вносить не требуется. Но мы хотим предупредить вас об эффектах изменения поведения подобного кода на 64-битных системах. И в любом случае не рекомендуется смешивать целочисленную арифметику и арифметику с плавающей точкой.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с опасным явным приведением указателя одного типа к указателю другого типа. Ошибка может заключаться в некорректной работе с объектами, на которые ссылается указатель.
Рассмотрим пример. В нем присутствует явное приведение указателя на 'int' к указателю на 'size_t'.
int array[4] = { 1, 2, 3, 4 };
size_t *sizetPtr = (size_t *)(array);
cout << sizetPtr[1] << endl;
Как видите, результат вывода программы отличается в 32-битном и 64-битном варианте. На 32-битной системе доступ к элементам массива осуществляется корректно, так как размеры типов 'size_t' и 'int' совпадают, и мы видим вывод "2". На 64-битной системы мы получили в выводе "17179869187", так как именно значение 17179869187 находится в 1-ом элементе массива 'sizetPtr'.
Исправление описанной ситуации заключается в отказе от опасных приведений типов путем модернизации программы. Другим вариантом является создание нового массива и копирование в него значений из исходного массива.
Естественно, не все явные приведения типов указателей являются опасными. В показанном далее примере результат работы не зависит он разрядности системы, так как типы 'enum' и 'int' имеют одинаковый размер как на 32-битной, так и на 64-битной системе. Соответственно анализатор не выдаст на данный код никаких сообщений.
int array[4] = { 1, 2, 3, 4 };
enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };
ENumbers *enumPtr = (ENumbers *)(array);
cout << enumPtr[1] << endl;
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с использованием memsize типа для генерации исключения. Ошибка может заключаться в некорректном перехвате и обработке таких исключений.
Рассмотрим пример. Рассмотрим код, генерирующий и обрабатывающий исключение.
char *ptr1, *ptr2;
...
try {
throw ptr2 - ptr1;
}
catch(int) {
Foo();
}
Ошибка в том, что на 64-битной системе обработчик исключения не сработает и функция 'Foo()' не будет вызвана. Это связано с тем, что выражение "ptr2 - ptr1" имеет тип 'ptrdiff_t', который на 64-битной системе не совпадает с типом 'int'.
Исправление описанной ситуации заключается в использовании корректного типа для перехвата исключения. В данном случае - 'ptrdiff_t', как показано ниже.
try {
throw ptr2 - ptr1;
}
catch(ptrdiff_t) {
Foo();
}
Более корректное исправление будет состоять в отказе от подобной практики программирования. Следует использовать специальные классы для передачи информации о возникшей ошибке.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с использованием memsize типа для перехвата исключения. Ошибка может заключаться в некорректном перехвате и обработке исключений.
Рассмотрим пример. Рассмотрим код, генерирующий и обрабатывающий исключение.
try {
try {
throw UINT64(-1);
}
catch(size_t) {
cout << "x64 portability issues" << endl;
}
}
catch(UINT64) {
cout << "OK" << endl;
}
Результат работы на 32-битной системе: OKРезультат работы на 64-битной системе: x64 portability issues
Изменение поведения связано с тем, что на 64-битной системе тип 'size_t' начинает совпадать с 'UINT64'.
Исправление описанной ситуации заключается в изменении кода для достижения необходимой логики работы.
Более корректное исправление будет состоять в отказе от подобной практики программирования. Следует использовать специальные классы для передачи информации о возникшей ошибке.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с использованием memsize внутри объединения (union). Ошибка может возникнуть при работе с такими объединениями без учета изменения размеров memsize типов на 64-битной системе.
Следует внимательно относиться к объединениям, имеющим в своем составе указатели и другие члены типа memsize.
Первый пример.
Иногда возникает необходимость работать с указателем, как с целым числом. Приведенный в примере код удобен тем, что для работы с числовым представлением указателя не используются явные приведения типов.
union PtrNumUnion {
char *m_p;
unsigned m_n;
} u;
...
u.m_p = str;
u.m_n += delta;
'm_n' на 64-битной системе, мы работаем только с частью указателя 'm_p'. Следует использовать тип, который будет соответствовать размеру указателя, как показано ниже.
union PtrNumUnion {
char *m_p;
size_t m_n; //type fixed
} u;
Второй пример.
Другое частое использование объединения заключается в представлении одного члена, набором других более мелких. Например, нам может потребоваться разбить значение типа 'size_t' на байты для реализации табличного алгоритма подсчета количества нулевых битов в байте.
union SizetToBytesUnion {
size_t value;
struct {
unsigned char b0, b1, b2, b3;
} bytes;
} u;
SizetToBytesUnion u;
u.value = value;
size_t zeroBitsN = TranslateTable[u.bytes.b0] +
TranslateTable[u.bytes.b1] +
TranslateTable[u.bytes.b2] +
TranslateTable[u.bytes.b3];
Здесь допущена принципиальная алгоритмическая ошибка, заключающаяся в предположении, что тип 'size_t' состоит из 4 байт. Возможность автоматического поиска алгоритмических ошибок на данном этапе развития статических анализаторов не возможна, но осуществляет поиск всех объединений, в которых присутствуют memsize типы. Просмотрев список таких потенциально опасных объединений, пользователь может обнаружить логические ошибки. Найдя приведенное в примере объединение, пользователь может обнаружить алгоритмическую ошибку и переписать код следующим образом.
union SizetToBytesUnion {
size_t value;
unsigned char bytes[sizeof(value)];
} u;
SizetToBytesUnion u;
u.value = value;
size_t zeroBitsN = 0;
for (size_t i = 0; i != sizeof(u.bytes); ++i)
zeroBitsN += TranslateTable[u.bytes[i]];
Родственным диагностическим сообщением является V122.
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциальную ошибку, связанную с использованием опасного выражения, являющегося фактическим аргументом для функции malloc. Ошибка может заключаться в некорректных представлениях о размерах типов, заданных в виде числовых констант.
Анализатор считает подозрительными те выражения, в которых присутствуют константные литералы кратные четырем, но отсутствует оператор sizeof().
Пример первый.
Некорректный код выделения памяти для матрицы размером 3x3 из элементов типа size_t может выглядеть следующим образом:
size_t *pMatrix = (size_t *)malloc(36); // V118
Хотя такой код мог долгое время надежно работать в 32-битной системе, использование магического числа 36 некорректно. При компиляции 64-битной версии необходимо выделить уже 72 байта памяти. Исправление заключается в использовании оператора sizeof ():
size_t *pMatrix = (size_t *)malloc(9 * sizeof(size_t));
Второй пример.
Некорректен для 64-битной системы и следующий код, основанный на предположении, что размер структуры Item равен 12 байт:
struct Item {
int m_a;
int m_b;
Item *m_pParent;
};
Item *items = (Item *)malloc(GetArraySize() * 12); // V118
Исправление также заключается в использовании оператора sizeof() для корректного вычисления размера структуры:
Item *items = (Item *)malloc(GetArraySize() * sizeof(Item));
Приведенные ошибки просты и легки в исправлении. Но от этого они не менее опасны и сложны для поиска в больших приложениях. Именно поэтому диагностика подобных ошибок реализована в виде отдельного правила.
Наличие константы в выражении, являющегося параметром для функции malloc(), вовсе не означает, что на него всегда выдается предупреждение V118. Если в выражении участвует оператор sizeof(), то такая конструкция считается безопасной. Пример кода, считающийся анализатором безопасным:
int *items = (int *)malloc(sizeof(int) * 12);
Дополнительные материалы по данной теме:
Анализатор обнаружил опасное арифметическое выражение, содержащее в себе несколько операторов sizeof(). Подобные выражения потенциально могут содержать ошибки, связанные с некорректным вычислением размеров структур без учета выравнивания полей.
Пример:
struct MyBigStruct {
unsigned m_numberOfPointers;
void *m_Pointers[1];
};
size_t n2 = 1000;
void *p;
p = malloc(sizeof(unsigned) + n2 * sizeof(void *));
Для расчета размеры структуры, которая будет содержать 1000 указателей используется на первый взгляд корректное арифметическое выражение. Размеры базовых типов определяются через операторы sizeof(). Это хорошо, но недостаточно, чтобы правильно вычислить необходимый объем памяти. Дополнительно необходимо учитывать выравнивание полей.
Приведенный пример будет корректно работать в 32-битном режиме, так как размер указателей и типа unsigned совпадает. Оба типа имеют размер 4 байта. Выравниваются указатели и тип unsigned также кратно 4 байтам. И размер необходимой памяти будет вычислен корректно.
В 64-битном коде размер указателя составляет 8 байт. Также указатели выравниваются в памяти по границе 8 байт. Это приводит к тому, что после переменной m_numberOfPointers будут располагаться 4 дополнительных байта для выравнивания указателей по границе 8 байт.
Для вычисления корректного размера необходимо использовать функцию offsetof:
p = malloc(offsetof(MyBigStruct, m_Pointers) +
n * sizeof(void *));
Во многих случаях использование нескольких операторов sizeof() в рамках одного выражения корректно и анализатор игнорирует подобные конструкции. Пример безопасных выражений с несколькими операторами sizeof:
int MyArray[] = { 1, 2, 3 };
size_t MyArraySize =
sizeof(MyArray) / sizeof(MyArray[0]);
assert(sizeof(unsigned) < sizeof(size_t));
size_t strLen = sizeof(String) - sizeof(TCHAR);
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку при работе с классами, в которых имеется operator[].
Классы с перегруженным operator[] обычно представляют собой разновидность массива и аргументом operator[] является индекс запрашиваемого элемента. Если operator[] имеет формальный аргумент 32-битного типа, а в качестве фактического аргумента используется memsize-тип, то это может свидетельствовать об ошибке. Рассмотрим пример, приводящий к сообщению V120:
class MyArray {
int m_arr[10];
public:
int &operator[](unsigned i) { return m_arr[i]; }
} Object;
size_t k = 1;
Object[k] = 44; //V120
Данный пример не содержит ошибки, но может свидетельствовать об архитектурной недоработке. Необходимо или работать с классом MyArray используя 32-битные индексы или модифицировать operator[], чтобы он принимал аргумент типа size_t. Второй вариант предпочтительней, так как использование memsize-типов не только делает программу более надежной, но и в ряде случаев позволяет компилятору построить более эффективный код.
Родственными диагностическими сообщениями являются V108 и V302.
Анализатор обнаружил потенциально возможную ошибку, связанную с вызовом оператора new. В качестве аргумента оператору new передается значение не memsize-типа. Оператор new принимает значения типа size_t и передача 32-битного фактического аргумента может свидетельствовать о наличии ошибки переполнения при вычислении объема выделяемой памяти.
Рассмотрим пример.
unsigned a = 5;
unsigned b = 1024;
unsigned c = 1024;
unsigned d = 1024;
char *ptr = new char[a*b*c*d]; //V121
В данном случае происходит переполнение при вычислении выражения "a*b*c*d" и в результате программа выделит меньшее количество памяти, чем планировалось. Исправление заключается в использовании типа size_t:
size_t a = 5;
size_t b = 1024;
size_t c = 1024;
size_t d = 1024;
char *ptr = new char[a*b*c*d]; //Ok
Ошибка не будет диагностироваться, если значение задано безопасным 32-битным константным значением. Пример безопасного кода:
char *ptr = new char[100];
const int size = 3*3;
char *p2 = new char[size];
Родственным диагностическим сообщением являются V106.
Дополнительные материалы по данной теме:
В некоторых случаях бывает необходимо найти все поля в структурах, имеющих memsize-тип. Такие поля можно обнаружить, используя диагностическое правило V122.
Необходимость просмотреть все memsize-поля может возникнуть при переносе программы, в которой присутствует сериализация структур, например в файл. Рассмотрим пример:
struct Header
{
unsigned m_version;
size_t m_bodyLen;
};
...
size_t size = fwrite(&header, 1, sizeof(header), file);
...
Данный код записывает в файл различное количество байт, в зависимости от того скомпилирован он в режиме Win32 или Win64. Это может нарушить совместимость формата файлов или приводить к иным ошибкам.
Автоматизировать выявление подобных ошибок представляется практически не решаемой задачей. Однако, если имеются предпосылки считать, что код содержит подобные ошибки, то разработчики один раз могут проверить все структуры, которые участвую в сериализации. Для этого и может потребоваться проверка правила V122. По умолчанию оно отключено, так как дает ложное предупреждение в более чем 99% случаев.
В приведенном выше примере сообщение V122 будет выдано на строку "size_t m_bodyLen;". Исправление кода может заключаться в использовании типов фиксированного размера:
struct Header
{
My_UInt32 m_version;
My_UInt32 m_bodyLen;
};
...
size_t size = fwrite(&header, 1, sizeof(header), file);
...
Рассмотрим другие примеры, где будет выдано сообщение V122:
class X
{
int i;
DWORD_PTR a; //V122
DWORD_PTR b[3]; //V122
float c[3][4];
float *ptr; //V122
};
Родственным диагностическим сообщением является V117.
Примечание. Если есть уверенность, что структуры, содержавшие указатели, никогда не сериализуются, то можно использовать следующий комментарий:
//-V122_NOPTR
Он подавит все предупреждения, которые относятся к указателям.
Этот комментарий следует вписать в заголовочный файл, который включается во все другие файлы. Например, таким файлом может быть "stdafx.h". Если вписать этот комментарий в "*.cpp" файл, то он будет действовать только для этого файла.
Анализатор обнаружил потенциально возможную ошибку, связанную с операцией выделения памяти. При вычислении размера выделяемой памяти используется оператор sizeof(X). Результат, возвращаемый функцией выделения памяти, приводится не к "(X *)", а к другому типу "(Y *)". Это может свидетельствовать о выделении недостаточного или излишнего количества памяти.
Рассмотрим первый пример:
int **ArrayOfPointers = (int **)malloc(n * sizeof(int));
Здесь из-за опечатки в 64-битной программе будет выделяться в два раза меньше памяти, чем это необходимо. В 32-битной программе размер типа int и указателя на тип int совпадает и программа, несмотря на опечатку, успешно работает.
Исправленный вариант кода:
int **ArrayOfPointers = (int **)malloc(n * sizeof(int *));
Рассмотрим другой пример, где происходит выделение большего количества памяти, чем это необходимо:
unsigned *p = (unsigned *)malloc(len * sizeof(size_t));
Программа, содержащая подобный код, скорее всего, будет корректно работать как в 32-битном, так и в 64-битном варианте. Но в 64-битном варианте программа выделяет больше памяти, чем ей необходимо. Корректный вариант:
unsigned *p = (unsigned *)malloc(len * sizeof(unsigned));
В ряде случаев, анализатор не выдает предупреждение, хотя типы X и Y не совпадают. Пример подобного корректного кода:
BYTE *simpleBuf = (BYTE *)malloc(n * sizeof(float));
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что размер записываемых или читаемых данных явно задан константой.
При компиляции в 64-битном режиме изменяется размер некоторых типов, а также границы их выравнивания. Размеры основных типов и границы их выравнивания показаны на рисунке:
Анализатор обращает внимание на те места в коде, где размер записываемых/читаемых данных указан явным образом. Эти места программисту следует проверить. Пример кода:
size_t n = fread(buf, 1, 40, f_in);
Константа 40 может являться некорректным значением в рамках 64-битной системы. Возможно, более правильно будет написать:
size_t n = fread(buf, 1, 10 * sizeof(size_t), f_in);
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в 64-битном коде присутствует определение зарезервированных типов. При этом они определяются как 32-битные.
Пример:
typedef unsigned size_t;
typedef __int32 INT_PTR;
Подобное определение типов может привести к различным ошибкам, так как в разных частях программы и библиотеках эти типы будут иметь различный размер. Следует использовать специальные заголовочные файлы, в которых корректно определены эти типы. Например, тип size_t определен в заголовочном файле stddef.h для языка C и в файле cstddef для языка C++.
Дополнительные ресурсы:
Данное диагностическое сообщение позволяет найти все типы 'long' используемые в программе.
Конечно, наличие типа 'long' в программе не является ошибкой. Однако иногда бывает полезно просмотреть все места в тексте программы, где используется этот тип. Такое может понадобиться при создании переносимого 64-битного кода, который должен функционировать в Windows и в Linux.
Windows и Linux используют различные модели данных для 64-битной архитектуры. Модель данных это соотношения размеров базовых типов данных, таких как int, float, указатель и так далее. Windows использует модель данных LLP64. В Linux используется модель данных LP64. В этих моделях различается размер типа 'long'.
В Windows (LLP64) размер типа 'long' равен 4 байта.
В Linux (LP64) размер типа 'long' равен 8 байт.
Различие размера типа 'long' может привести к несовместимости форматов файлов или ошибкам при разработке кода, выполняемого в Linux и Windows. При желании, используя PVS-Studio мы сможете просмотреть все участки кода, где используется тип 'long'.
Дополнительные ресурсы:
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в длинном цикле может переполниться 32-битная переменная.
Анализатор, конечно, не может определить все возможные ситуации, когда происходит переполнение переменных в циклах. Но он поможет найти ряд типовых ошибочных конструкций.
Рассмотрим пример:
int count = 0;
for (size_t i = 0; i != N; i++)
{
if ((A[i] & MASK) != 0)
count++;
}
Приведенный код корректно работает в 32-битной программе. Переменной типа 'int' достаточно, чтобы посчитать количество каких-то элементов в массиве. Однако в 64-битной программе количество таких элементов может превысить INT_MAX и произойдет переполнение переменной 'count'. Об этом и предупреждает анализатор, выдавая сообщение V127. Корректный вариант:
size_t count = 0;
for (size_t i = 0; i != N; i++)
{
if ((A[i] & MASK) != 0)
count++;
}
Также анализатор содержит ряд дополнительных проверок, чтобы сократить количество ложных срабатываний. Например, сообщение V127 не будет выдано, если мы имеем дело с коротким циклом. Пример кода, который анализатор считает безопасным:
int count = 0;
for (size_t i = 0; i < 100; i++)
{
if ((A[i] & MASK) != 0)
count++;
}
Анализатор обнаружил потенциальную ошибку, связанную с несовместимостью данных, которыми обменивается 32-битная и 64-битная версия программы. В поток записываются или из потока читаются переменные memsize-типа. Ошибка может заключаться в том, что данные записанные в бинарный файл в 32-х битном приложении будут некорректно прочитаны 64-х битном приложением.
Рассмотрим пример такого кода:
std::vector<int> v;
....
ofstream os("myproject.dat", ios::binary);
....
os << v.size();
Функция 'size()' возвращает значение с типом size_t. Его размер различен в 32-х и 64-х битных приложениях. Соответственно, в файл будет записано различное количество байт.
Способов избежать несовместимости данных достаточно много. Самый простой и грубый способ, это жестко задать размер читаемых и записываемых типов. Пример такого кода:
std::vector<int> v;
....
ofstream os("myproject.dat", ios::binary);
....
os << static_cast<__int64>(v.size());
Естественно, жёсткий переход на 64-битные типы данных нельзя назвать хорошим решением. Например, этот способ не позволит читать данные, записанные старой 32-битной программой. Если читать и записывать данные как 32-битные значения, возникнет другая проблема. В этом случае, 64-битная программа не сможет записать информацию о массивах, состоящих более чем из 2^32 элементов. Это может быть неприятным ограничением. Ведь часто 64-битные программы создаются именно для того, чтобы работать с огромными массивами данных.
Выходом из этой ситуации может стать внесения понятия версии сохраненных данных. Например, 32-битная программа сможет открывать файлы, созданные только 32-битной версией. А 64-битная программа будет работать с данными, сгенерированными как 32-битной, так и 64-битной версией программы.
Ещё один вариант решения проблемы совместимости данных это хранение их в текстовом виде, или использовании XML формата данных.
Следует отметить, что далеко не во всех программах имеет место проблема совместимости данных. Если программа не создает проекты и иные файлы, которые нужно открывать на других компьютерах, то диагностику V128 можно просто отключить.
Также, нет смысла беспокоиться, если поток используется для распечатки значений на экране. PVS-Studio, по возможности, пытается определить такие ситуации и не выдавать сообщения. Однако, появление ложных сообщений весьма вероятно. В этом случае, вы можете воспользоваться одним из механизмов подавления ложных предупреждений, описанных в документации.
Дополнительные возможности
По просьбе пользователей реализована возможность самостоятельно указать, какие из функций используются для сохранения/чтения данных. Если в эти функции передаются memsize-типы, то это считается опасным кодом.
Формат расширения следующий: возле прототипа функции (или возле её реализации, или в общем заголовочном файле) пишется комментарий специального вида. Начнём с примера использования:
//+V128, function:write, non_memsize:2
void write(string name, char);
void write(string name, int32);
void write(string name, int64);
foo()
{
write("zz", array.size()); // warning V128
}
Формат:
Уровень диагностики в случае срабатывания на пользовательской функции – всегда первый.
Напоследок дадим наиболее полный пример использования:
// Предупреждать, когда в метод C класса B
// из пространства имён A во второй или третий
// аргумент была помещена переменная типа memsize:
//+V128,namespace:A,class:B,function:C,non_memsize:3,non_memsize:2
Предупреждение информирует о наличии явного приведения 32-битного целочисленного типа к memsize типу, что может маскировать одну из следующих ошибок: V101, V102, V104, V105, V106, V108, V109. Вы можете обратиться к приведенному списку предупреждений, чтобы выявить причину появления диагностирующего сообщения V204.
Ранее предупреждение V201 распространялось и на приведение 32-битных целочисленных типов к указателю. Такие приведения весьма опасны. Поэтому они были выделены в отдельное диагностическое правило V204.
Учтите, что, скорее всего, большинство предупреждений этого типа будет выдано на корректный код. Приведем несколько примеров корректного и некорректного кода, на который будет выдано данное предупреждение.
Примеры некорректного кода.
int i;
ptrdiff_t n;
...
for (i = 0; (ptrdiff_t)(i) != n; ++i) { //V201
...
}
unsigned width, height, depth;
...
size_t arraySize = size_t(width * height * depth); //V201
Примеры корректного кода.
const size_t seconds = static_cast<size_t>(60 * 60); //V201
unsigned *array;
...
size_t sum = 0;
for (size_t i = 0; i != n; i++) {
sum += static_cast<size_t>(array[i] / 4); //V201
}
unsigned width, height, depth;
...
size_t arraySize =
size_t(width) * size_t(height) * size_t(depth); //V201
Оно информирует о наличии явного приведения целочисленного memsize типа к 32-битному типу, что может маскировать одну из следующих ошибок: V103, V107, V110. Вы можете обратиться к приведенному списку предупреждений, чтобы выявить причину появления диагностирующего сообщения V202.
Ранее предупреждение V202 распространялось и на приведение указателя к 32-битному целочисленному типу. Такие приведения весьма опасны. Поэтому они были выделены в отдельное диагностическое правило V205.
Учтите, что, скорее всего, большинство предупреждений этого типа будет выдано на корректный код. Приведем несколько примеров корректного и некорректного кода, на который будет выдано данное предупреждение.
Примеры некорректного кода.
size_t n;
...
for (unsigned i = 0; i != (unsigned)n; ++i) { //V202
...
}
UINT_PTR width, height, depth;
...
UINT arraySize = UINT(width * height * depth); //V202
Примеры корректного кода.
const unsigned bits =
unsigned(sizeof(object) * 8); //V202
extern size_t nPane;
extern HICON hIcon;
BOOL result =
SetIcon(static_cast<int>(nPane), hIcon); //V202
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку, связанную с явным преобразованием memsize типа в 'double' тип или наоборот. Потенциальная ошибка может заключаться в невозможности хранения всего диапазона значений memsize типа в переменных типа 'double'.
Эта ошибка полностью аналогична ошибке V113. Отличие заключается только в том, что используется явное приведение типов, как например, показано ниже:
SIZE_T size = SIZE_MAX;
double tmp = static_cast<double>(size);
size = static_cast<SIZE_T>(tmp); // x86: size == SIZE_T
// x64: size != SIZE_T
Для ознакомления с данным видом ошибок смотри описание ошибки V113.
Дополнительные материалы по данной теме:
Предупреждение информирует о наличии явного приведения 32-битного целочисленного типа к указателю. Ранее подобную ситуацию можно было выявить с помощью диагностического правила V201. Однако, явное приведение типа 'int' к указателю, гораздо опаснее, чем приведение 'int' к типу 'intptr_t'. Поэтому было создано отдельное правило для поиска явного приведения типов при работе с указателями.
Пример некорректного кода.
int n;
float *ptr;
...
ptr = (float *)(n);
В 64-битной программе тип 'int' имеет размер 4 байта и не может вместить в себя указатель размером 8 байт. Приведение типа, как показано в примере, практически всегда свидетельствует о наличии ошибки.
Отметим, что такие ошибки очень неприятны тем, что могут не сразу проявить себя. Программа может хранить указатели в 32-битных переменных и некоторое время корректно работать, пока все создаваемые в программе объекты располагаются в младших адресах оперативной памяти.
Если по каким-то причинам необходимо хранить указатель в переменной целочисленного типа, то для этого следует использовать memsize-типы данных. Например: size_t, ptrdiff_t, intptr_t, uintptr_t.
Пример корректного кода:
intptr_t n;
float *ptr;
...
ptr = (float *)(n);
Есть все-таки специфическая ситуация, когда указатель допустимо хранить в 32-битных типах. Речь идет о дескрипторах (handles), которые используются в Windows для работы с различными системными объектами. Примеры таких типов: HANDLE, HWND, HMENU, HPALETTE, HBITMAP и так далее. По сути, эти типы являются указателями. Например, HANDLE объявляется в заголовочных файлах как "typedef void *HANDLE;".
Хотя дескрипторы являются 64-битными указателями, для большей совместимости (например, для возможности взаимодействия между 32-битынми и 64-битными процессами) в них используется только младшие 32-бита. Подробнее смотри "Microsoft Interface Definition Language (MIDL): 64-Bit Porting Guide" (USER and GDI handles are sign extended 32b values).
Такие указатели можно хранить в 32-битным типам данных (например, int, DWORD). Для преобразования таких указателей к 32-битным типам и обратно используются специальные функции:
void * Handle64ToHandle( const void * POINTER_64 h )
void * POINTER_64 HandleToHandle64( const void *h )
long HandleToLong ( const void *h )
unsigned long HandleToUlong ( const void *h )
void * IntToPtr ( const int i )
void * LongToHandle ( const long h )
void * LongToPtr ( const long l )
void * Ptr64ToPtr ( const void * POINTER_64 p )
int PtrToInt ( const void *p )
long PtrToLong ( const void *p )
void * POINTER_64 PtrToPtr64 ( const void *p )
short PtrToShort ( const void *p )
unsigned int PtrToUint ( const void *p )
unsigned long PtrToUlong ( const void *p )
unsigned short PtrToUshort ( const void *p )
void * UIntToPtr ( const unsigned int ui )
void * ULongToPtr ( const unsigned long ul )
Дополнительные материалы по данной теме:
Предупреждение информирует о наличии явного приведения указателя к 32-битному целочисленному типу. Ранее подобную ситуацию можно было выявить с помощью диагностического правила V202. Однако, явное приведение указателя к типу 'int', гораздо опаснее, чем приведение 'intptr_t' к типу 'int'. Поэтому было создано отдельное правило для поиска явного приведения типов при работе с указателями.
Пример некорректного кода.
int n;
float *ptr;
...
n = (int)ptr;
В 64-битной программе тип 'int' имеет размер 4 байта и не может вместить в себя указатель размером 8 байт. Приведение типа, как показано в примере, практически всегда свидетельствует о наличии ошибки.
Отметим, что такие ошибки очень неприятны тем, что могут не сразу проявить себя. Программа может хранить указатели в 32-битных переменных и некоторое время корректно работать, пока все создаваемые в программе объекты располагаются в младших адресах оперативной памяти.
Если по каким-то причинам необходимо хранить указатель в переменной целочисленного типа, то для этого следует использовать memsize-типы данных. Например: size_t, ptrdiff_t, intptr_t, uintptr_t.
Пример корректного кода:
intptr_t n;
float *ptr;
...
n = (intptr_t)ptr;
Есть все-таки специфическая ситуация, когда указатель допустимо хранить в 32-битных типах. Речь идет о дескрипторах (handles), которые используются в Windows для работы с различными системными объектами. Примеры таких типов: HANDLE, HWND, HMENU, HPALETTE, HBITMAP и так далее. По сути, эти типы являются указателями. Например, HANDLE объявляется в заголовочных файлах как "typedef void *HANDLE;".
Хотя дескрипторы являются 64-битными указателями, для большей совместимости (например, для возможности взаимодействия между 32-битынми и 64-битными процессами) в них используется только младшие 32-бита. Подробнее смотри "Microsoft Interface Definition Language (MIDL): 64-Bit Porting Guide" (USER and GDI handles are sign extended 32b values).
Такие указатели можно хранить в 32-битным типам данных (например, int, DWORD). Для преобразования таких указателей к 32-битным типам и обратно используются специальные функции:
void * Handle64ToHandle( const void * POINTER_64 h )
void * POINTER_64 HandleToHandle64( const void *h )
long HandleToLong ( const void *h )
unsigned long HandleToUlong ( const void *h )
void * IntToPtr ( const int i )
void * LongToHandle ( const long h )
void * LongToPtr ( const long l )
void * Ptr64ToPtr ( const void * POINTER_64 p )
int PtrToInt ( const void *p )
long PtrToLong ( const void *p )
void * POINTER_64 PtrToPtr64 ( const void *p )
short PtrToShort ( const void *p )
unsigned int PtrToUint ( const void *p )
unsigned long PtrToUlong ( const void *p )
unsigned short PtrToUshort ( const void *p )
void * UIntToPtr ( const unsigned int ui )
void * ULongToPtr ( const unsigned long ul )
Рассмотрим пример:
HANDLE h = Get();
UINT uId = (UINT)h;
Анализатор не выдаёт здесь сообщение, хотя HANDLE есть не что иное, как указатель. Значения этого указателя всегда помещаются в 32-бита. Будьте только аккуратны в дальнейшем. Учтите, что невалидные дескрипторы (handles) объявлены следующим образом:
#define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1)
Поэтому некорректно будет ниже написать так:
if (HANDLE(uID) == INVALID_HANDLE_VALUE)
Т.к. переменная 'uID' беззнаковая, то значение указателя будет равно не 0xFFFFFFFFFFFFFFFF, а 0x00000000FFFFFFFF.
Для подозрительной проверки, где unsigned будет превращаться в указатель, будет выдано предупреждение V204.
Дополнительные материалы по данной теме:
Предупреждение информирует о наличии явного приведения типа 'void *', 'byte *' к указателю на функцию или к указателю на 32/64-битные целочисленные типы данных. Или наоборот.
Конечно, само по себе такое приведение типа не является ошибкой. Давайте разберёмся для чего реализована такая диагностика.
Достаточно часто указатель на какой-то буфер памяти передаются в другую часть программы с помощью указателя на байт или void. Причины написать такой код могут быть различны. Часто это свидетельствует о плохом дизайне, но этот вопрос выходит за рамки документации. Указатели на функцию тоже часто хранятся как void *.
Итак, в каком-то месте программы указатель на массив/функцию сохраняется как void *, а в другом происходит обратное приведение типа. При портировании, такой код чреват ошибками. В одном месте, тип данных может быть изменён, а в другом нет.
Рассмотрим пример:
size_t array[20];
void *v = array;
....
unsigned* sizes = (unsigned*)(v);
Этот код корректно работает в 32-битном режиме, так как размеры типов 'unsigned' и 'size_t' совпадают. В 64-битном режиме, размер типов отличается и поведение программы начнет отличаться от ожидаемого. См. также: паттерн 6 - изменение типа массива.
Анализатор укажет на строку с явным приведением типа и изучив код можно обнаружить ошибку. Исправленный вариант кода может выглядеть так:
unsigned array[20];
void *v = array;
....
unsigned* sizes = (unsigned*)(v);
Или так:
size_t array[20];
void *v = array;
....
size_t* sizes = (size_t*)(v);
Аналогичная ошибку можно допустить при работе с указателями на функцию.
void Do(void *ptr, unsigned a)
{
typedef void (*PtrFoo)(DWORD);
PtrFoo f = (PtrFoo)(ptr);
f(a);
}
void Foo(DWORD_PTR a) { /*... */ }
void Call()
{
Do(Foo, 1);
}
Исправленный вариант кода:
typedef void (*PtrFoo)(DWORD_PTR);
Примечание. Анализатор знает о множестве случаев, когда явное приведение типа безопасно. Например, он не обращает внимание, на явное приведение указателя типа void *, который вернула функция malloc():
int *p = (int *)malloc(sizeof(int) * N);
Как было отмечено в начале, явное приведение типа само по себе не является ошибкой. Поэтому не смотря на большое количество исключений из правила, анализатор всё равно генерирует много ложных предупреждений V206. Анализатор не знает, есть ли в программе другие места, где неправильно используются эти указатели. Поэтому он выдаёт предупреждения на все потенциально опасные приведения типов.
Например, выше были приведены два примера некорректного кода и способы их исправления. Но анализатор, будет продолжать выдавать предупреждения и на уже исправленный код.
Можно предложить следующий способ работы с этим предупреждением. Вы один раз внимательно просматриваете все сообщения V206 после чего отключаете эту диагностику в настройках. Если ложных предупреждений не много, то можно воспользоваться один из методов подавления ложных предупреждений.
Предупреждение информирует о наличии явного приведения 32-битной целочисленной переменной к типу ссылки на указатель.
Рассмотрим простой синтетический пример:
int A;
(int *&)A = pointer;
По какой-то причине в переменную типа 'int' следует поместить указатель. Чтобы это сделать, можно привести тип переменной 'A' к типу 'int *&' (ссылка на указатель).
Этот код волне работоспособен в 32-битной системе, так как размеры переменной 'int' и указателей совпадают. В 64-битной системе произойдёт запись за границу памяти, которую занимает переменная 'A', и дальнейшее поведение программы будет неопределено.
Чтобы исправить ошибку, следует использовать один из memsize-типов данных. Например, можно использовать тип intptr_t:
intptr_t A;
(intptr_t *&)A = pointer;
Рассмотрим более сложный пример. Пример, составленный на основе кода, взятого из реального приложения:
enum MyEnum { VAL1, VAL2 };
void Get(void*& data) {
static int value;
data = &value;
}
void M() {
MyEnum e;
Get((void*&)e);
....
}
Есть функция, которая возвращает значения, имеющие тип указателя. В одном месте полученное значение помещается в переменную типа 'enum'. Оставим в стороне, почему так было сделано. Нам важно, что этот код корректно работал, когда был 32-битным. Однако 64-битный вариант программы ведёт себя некорректно. Функция Get() меняет не только переменную 'e', но и память по соседству.
Предупреждение информирует о наличии странной последовательности приведений типа. Memsize-тип явно приводится к 32-битному целочисленному типу. А затем тут же вновь явно или неявно приводится к memsize-типу. Такая последовательность приведений приводит к потере значений старших бит. Как правило, это свидетельствует о наличии серьезной ошибки.
Рассмотрим пример:
char *p1;
char *p2;
ptrdiff_t n;
...
n = int(p1 - p2);
Здесь присутствует лишнее приведение к типу 'int'. Оно не нужно и может послужить причиной сбоя, если в 64-битной программе указатели p1 и p2 будут отстоять друг от друга больше чем на INT_MAX элементов.
Корректный вариант кода:
char *p1;
char *p2;
ptrdiff_t n;
...
n = p1 - p2;
Рассмотрим другой пример:
BOOL SetItemData(int nItem, DWORD_PTR dwData);
...
CItemData *pData = new CItemData;
...
CListCtrl::SetItemData(nItem, (DWORD)pData);
Этот код приведёт к ошибке, если объект типа CItemData будет создан за пределами четырех младших гигабайт памяти. Корректный вариант кода:
BOOL SetItemData(int nItem, DWORD_PTR dwData);
...
CItemData *pData = new CItemData;
...
CListCtrl::SetItemData(nItem, (DWORD_PTR)pData);
Следует учитывать, что анализатор не выдаёт предупреждение, если конвертации подвергается такие типы данных, как HANDLE, HWND, HCURSOR и так далее. Хотя, по сути, это указатели (void *), их значения всегда вмещаются в младшие 32-бита. Это сделано специально, чтобы эти дескрипторы (handles) можно было передавать между 32-битными и 64-битными процессами. Подробнее: Как корректно привести указатель к int в 64-битной программе?
Рассмотрим пример:
typedef void * HANDLE;
HANDLE GetHandle(DWORD nStdHandle);
int _open_osfhandle(intptr_t _OSFileHandle, int _Flags);
....
int fh = _open_osfhandle((int)GetHandle(sh), 0);
Здесь имеет место преобразование вида:
HANDLE -> int -> intptr_t
Т.е. вначале указатель превращается в 32-битный тип 'int', а замет расширяется до 'intptr_t'. Это не очень красиво. Лучше было бы написать "(intptr_t)GetHandle(STD_OUTPUT_HANDLE)". Однако, никакой ошибки здесь и нет, так как значения типа HANDLE помещаются в 'int'. Поэтому анализатор промолчит.
Если будет написано:
int fh = _open_osfhandle((unsigned)GetHandle(sh), 0);
То анализатор выдаст предупреждение. Смешивание знаковых и беззнаковых типов всё портит. Представим, что GetHandle() вернёт INVALID_HANDLE_VALUE. Это значение задано в системных заголовочных файлах следующим образом.
#define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1)
Посмотрим, что получитcя при преобразовании (intptr_t)(unsigned)((HANDLE)(LONG_PTR)-1):
-1 -> 0xffffffffffffffff -> HANDLE -> 0xffffffffu -> 0x00000000fffffffff
Значение -1 превратилось в 4294967295. Это может быть не учтено программистом и программа может работать некорректно, если функция GetHandle() вернёт INVALID_HANDLE_VALUE. Поэтому, для второго случая анализатор выдаст предупреждение.
Предупреждение информирует о наличии странной последовательности приведений типа. Указатель явно приводится к memsize-типу. А затем вновь явно или неявно приводится к 32-битному целочисленному типу. Такая последовательность приведений приводит к потере значений старших бит. Как правило, это свидетельствует о наличии серьезной ошибки.
Рассмотрим пример:
int *p = Foo();
unsigned a, b;
a = size_t(p);
b = unsigned(size_t(p));
В обоих случаях указатель преобразовывается в тип 'unsigned'. При этом старшая часть указателя будет потеряна. Если затем переменные 'a' или 'b' вновь превратить в указатель, эти указатели могут оказаться некорректными.
Различие между переменными 'a' и 'b' только в том, что второй случай тяжелей обнаружить. В первом случае, компилятор предупредит о потери значащих бит. Во втором случае он будет молчать, так как используется явное приведение типа.
Исправление ошибки заключается в том, чтобы хранить указатели только memsize-типах, например, в переменных типа size_t:
int *p = Foo();
size_t a, b;
a = size_t(p);
b = size_t(p);
Иногда некоторое недопонимание вызывает предупреждение анализатора на код следующего вида:
BOOL Foo(void *ptr)
{
return (INT_PTR)ptr;
}
Тип BOOL это не что иное как 32-битный тип 'int'. Поэтому возникает цепочка преобразований вида:
pointer -> INT_PTR -> int.
Может показаться, что здесь ошибки нет. Ведь нам только важно, что указатель равен или не раен нулю. Но на самом деле ошибка есть. Не следует путать как ведёт себя тип BOOL и bool.
Пусть у нас есть 64-битная переменная, значение которой равно 0x000012300000000. Тогда при приведении к bool и BOOL мы получим разные результаты:
int64_t v = 0x000012300000000ll;
bool b = (bool)(v); // true
BOOL B = (BOOL)(v); // FALSE
В случае 'BOOL' просто будут отброшены старшие биты. И ненулевое значение превратится в 0 (FALSE).
Аналогичная ситуация с указателем. При явном приведении указателя к BOOL отпросятся старшие биты и ненулевой указатель превратится в целочисленный 0 (FALSE). Вероятность такого события мала, но есть. Поэтому такой код неверен.
Для того чтобы код стал корректным, можно поступить двумя способами. Первый вариант - использовать тип 'bool':
bool Foo(void *ptr)
{
return (INT_PTR)ptr;
}
Хотя конечно лучше и проще написать так:
bool Foo(void *ptr)
{
return ptr != nullptr;
}
Показанный выше вариант не всегда возможен. Например, в языке Си нет типа 'bool'. Второй вариант исправления ошибки:
BOOL Foo(void *ptr)
{
return ptr != NULL;
}
Следует учитывать, что анализатор не выдаёт предупреждение, если конвертации подвергается такие типы данных, как HANDLE, HWND, HCURSOR и так далее. Хотя по сути это указатели (void *), их значения всегда вмещаются в младшие 32-бита. Это сделано специально, чтобы эти дескрипторы (handles) можно было передавать между 32-битными и 64-битными процессами. Подробнее: Как корректно привести указатель к int в 64-битной программе?
Анализатор обнаружил потенциально возможную ошибку, связанную с изменением поведения перекрытых виртуальных функций.
Пример изменения поведения виртуальных функций.
class CWinApp {
...
virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
...
};
class CSampleApp : public CWinApp {
...
virtual void WinHelp(DWORD dwData, UINT nCmd);
...
};
Перед вами классический пример, с которым может столкнуться разработчик, переносящий свое приложение на 64-битную архитектуру. Проследим жизненный цикл разработки некоторого приложения. Пусть первоначально оно разрабатывалось под Visual Studio 6.0. когда, функция 'WinHelp' в классе 'CWinApp' имела следующий прототип:
virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);
Совершенно корректно было осуществить перекрытие виртуальной фукции в классе 'CSampleApp', как показано в примере. Затем проект был перенесен в Visual Studio 2005, где прототип функции в классе 'CWinApp' претерпел изменения, заключающиеся в смене типа 'DWORD' на тип 'DWORD_PTR'. На 32-битной платформе такая программа продолжит совершенно корректно работать, так как здесь типы 'DWORD' и 'DWORD_PTR' совпадают. Неприятности проявят себя при компиляции данного кода под 64-битную платформу. Получатся две функции с одинаковыми именами, но с различными параметрами, в результате чего перестанет вызываться пользовательский код.
Анализатор позволяет обнаруживать подобные ошибки, исправление которых не представляет сложности. Достаточно изменить прототип функции в классе наследника, как показано ниже:
class CSampleApp : public CWinApp {
...
virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
...
};
Дополнительные материалы по данной теме:
Анализатор обнаружил потенциально возможную ошибку при работе с классами, в которых имеется operator[]. Классы с перегруженным operator[] обычно представляют собой разновидность массива и аргументом operator[] является индекс запрашиваемого элемента. Если operator[] имеет аргумент 32-битного типа, то это может свидетельствовать об ошибке.
Рассмотрим пример, приводящий к сообщению V302:
class MyArray {
std::vector<float> m_arr;
...
float &operator[](int i) //V302
{
DoSomething();
return m_arr[i];
}
} A;
...
int x = 2000;
int y = 2000;
int z = 2000;
A[x * y * z] = 33;
Если класс спроектирован для работы с большим количеством аргументов, то такая реализация operator[] является некорректной, так как не позволяет обращаться к элементам с порядковыми номерами более UINT_MAX. Диагностировать ошибку в приведенном примере можно только указав на потенциально опасный operator[]. Выражение "x * y * z" не выглядит подозрительным, так как отсутствует неявное приведение типа. Когда мы исправим operator[] следующим образом:
float &operator[](ptrdiff_t i);
анализатор PVS-Studio предупредит о возможной ошибке в строке "A[x * y * z] = 33;" и мы сможем сделать код программы окончательно корректным. Пример исправленного кода:
class MyArray {
std::vector<float> m_arr;
...
float &operator[](ptrdiff_t i) //V302
{
DoSomething();
return m_arr[i];
}
} A;
...
ptrdiff_t x = 2000;
ptrdiff_t y = 2000;
ptrdiff_t z = 2000;
A[x * y * z] = 33;
Родственными диагностическими сообщениями являются V108 и V120.
Ряд функций при переносе приложения на 64-битные системы лучше заменить на их новые варианты. В противном случае работа 64-битного приложения может быть некорректна. Анализатор предупреждает об использовании в коде устаревшей функции и предлагает вариант для замены.
Рассмотрим несколько примеров устаревших функций:
Цитата из MSDN: To control whether a 64-bit application enumerates 32-bit modules, 64-bit modules, or both types of modules, use the EnumProcessModulesEx function.
Цитата из MSDN: This function has been superseded by the SetWindowLongPtr function. To write code that is compatible with both 32-bit and 64-bit versions of Windows, use the SetWindowLongPtr function.
Цитата из MSDN: When lpFileSizeHigh is NULL, the results returned for large files are ambiguous, and you will not be able to determine the actual size of the file. It is recommended that you use GetFileSizeEx instead.
Будьте аккуратны, если захотите заменить функцию 'lstrlen' на 'strlen'. Функция 'lstrlen' не сможет корректно подсчитать длину строки, если эта строка состоит более чем из 'INT_MAX' символов. Однако, на практике вряд ли можно ожидать встречу со столь длинными строками. Зато функция 'lstrlen' в отличии 'strlen' корректно обрабатывает ситуацию, когда ей на вход подаётся нулевой указатель: "If lpString is NULL, the function returns 0".
Если просто заменить 'lstrlen' на 'strlen', то программа может начать работать некорректно. Поэтому обычно менять 'lstrlen' на вызов какой-то другой функции вообще не стоит.
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит логическую ошибку. В тексте программы имеется оператор (<, >, <=, >=, ==, !=, &&, ||, -, /), слева и справа от которого расположены одинаковые подвыражения.
Рассмотрим пример:
if (a.x != 0 && a.x != 0)
В данном случае оператор '&&' окружен одинаковыми подвыражениями "a.x != 0", что позволяет обнаружить ошибку, допущенную по невнимательности. Корректный код, который не вызовет подозрений у анализатора, будет выглядеть так:
if (a.x != 0 && a.y != 0)
Рассмотрим другой пример ошибки, обнаруженный анализатором в коде реального приложения:
class Foo {
int iChilds[2];
...
bool hasChilds() const { return(iChilds > 0 || iChilds > 0); }
...
}
В данном случае, хотя код успешно и без предупреждений компилируется, он не имеет смысла. Корректный код должен был выглядеть следующим образом:
bool hasChilds() const { return(iChilds[0] > 0 || iChilds[1] > 0);}
Анализатор выдает предупреждение не во всех случаях, когда слева и справа от оператора находятся одинаковые подвыражения.
Первое исключение относится к конструкциям, где используются оператор инкремента ++, декремента --, а также += и -=. Пример взятый из реального приложения:
do {
} while (*++scan == *++match && *++scan == *++match &&
*++scan == *++match && *++scan == *++match &&
*++scan == *++match && *++scan == *++match &&
*++scan == *++match && *++scan == *++match &&
scan < strend);
Данный код анализатор считает безопасным.
Второе исключение относится к сравнению двух одинаковых чисел. Этот прием часто используется программистами, чтобы выключить определенные ветки программы. Пример:
#if defined(_OPENMP)
#include <omp.h>
#else
#define omp_get_thread_num() 0
...
#endif
...
if (0 == omp_get_thread_num()) {
Последнее исключение относится к сравнению, где используются макросы:
#define _WINVER_NT4_ 0x0004
#define _WINVER_95_ 0x0004
...
UINT winver = g_App.m_pPrefs->GetWindowsVersion();
if(winver == _WINVER_95_ || winver == _WINVER_NT4_)
Следует понимать, что в ряде случаев анализатор может выдать предупреждение на корректную конструкцию. Например, анализатор не учитывает побочные эффекты (side effects) при вызове функций:
if (wr.takeChar() == '\0' && wr.takeChar() == '\0')
Другой пример ложного срабатывания анализатора был замечен на юнит-тестах одного проекта, в той его части, где проверялось корректность работы перегруженного оператора '==':
CHECK(VDStringA() == VDStringA(), true);
CHECK(VDStringA("abc") == VDStringA("abc"), true);
Диагностическое сообщение не выдается, если сравниваются два идентичных выражения типа float или double. Такое сравнение позволяет определить, является ли значение NaN. Пример кода, реализующего подобную проверку:
bool isnan(double X) { return X != X; }
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V501. |
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит логическую ошибку. В тексте программы имеется выражение, содержащее тернарный оператор '?:' и которое может вычисляться не так, как планировал программист.
Оператор '?:' имеет более низкий приоритет по сравнению с операторами ||, &&, |, ^, &, !=, ==, >=, <=, >, <, >>, <<, -, +, %, /, *. Это можно случайно забыть и написать ошибочный код, подобный приведенному ниже:
bool bAdd = ...;
size_t rightLen = ...;
size_t newTypeLen = rightLen + bAdd ? 1 : 0;
Забыв, что оператор '+' более приоритетный, чем оператор '?:' программист ожидает, что код будет эквивалентен: "rightLen + (bAdd ? 1 : 0)". Но на самом деле код эквивалентен выражению: "(rightLen + bAdd) ? 1 : 0".
Анализатор диагностирует возможность существования ошибку, выполняя следующие проверки:
1) Слева от оператора '?:' находится переменная или подвыражение имеющие тип bool.
2) Это подвыражение сравнивается/складывается/умножается/... с переменной с типом отличной от bool.
Если эти условия выполняются, то высока вероятность наличия ошибки и анализатор выдает рассматриваемое предупреждение.
Рассмотрим еще некоторые примеры некорректного кода:
bool b;
int x, y, z, h;
...
x = y < b ? z : h;
x = y + (z != h) ? 1 : 2;
Очевидно, что программистом, скорее всего, задумывался следующий корректный код:
bool b;
int x, y, z, h;
...
x = y < (b ? z : h);
x = y + ((z != h) ? 1 : 2);
Если слева от оператора '?:' находится тип отличный от bool, то анализатор считает, что код написан в Си стиле (где нет bool), или с использование объектов классов и не может выявить, опасен данный код или нет.
Пример корректного кода в стиле языка Си, который анализатор также считает корректным:
int conditions1;
int conditions2;
int conditions3;
...
char x = conditions1 + conditions2 + conditions3 ? 'a' : 'b';
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V502. |
Анализатор обнаружил фрагмент кода, содержащий бессмысленное сравнение. С большой вероятностью данный код содержит логическую ошибку.
Пример кода:
class MyClass {
public:
CObj *Find(const char *name);
...
} Storage;
if (Storage.Find("foo") < 0)
ObjectNotFound();
Кажется практически невероятным, что подобный код может присутствовать в программе. Однако причина его появления может быть достаточно проста. Предположим, что в программе имеется следующий код:
class MyClass {
public:
// Если объект не найден, то функция
// Find возвращает значение -1.
ptrdiff_t Find(const char *name);
CObj *Get(ptrdiff_t index);
...
} Storage;
...
ptrdiff_t index = Storage.Find("ZZ");
if (index >= 0)
Foo(Storage.Get(index));
...
if (Storage.Find("foo") < 0)
ObjectNotFound();
Это корректный, но не изящный код. В процессе рефакторинга класс MyClass может быть переписан следующим образом:
class MyClass {
public:
CObj *Find(const char *name);
...
} Storage;
После такой модернизации класса, необходимо исправить все места в программе, использующие функцию Find(). Первый участок кода пропустить невозможно, так как он не скомпилируется и следовательно он обязательно будет исправлен:
CObj *obj = Storage.Find("ZZ");
if (obj != nullptr)
Foo(obj);
Второй фрагмент кода успешно скомпилируется и его легко пропустить, тем самым создав рассматриваемую ошибку:
if (Storage.Find("foo") < 0)
ObjectNotFound();
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V503. |
Анализатор обнаружил фрагмент кода, в котором возможно пропущена точка с запятой ';'.
Пример кода, который приводит к выдаче диагностического сообщения V504:
void Foo();
void Foo2(int *ptr)
{
if (ptr == NULL)
return
Foo();
...
}
В данном коде планировалось завершить работу функции, если указатель ptr == NULL. Однако, после оператора return забыта точка с запятой ';', что приводит к вызову функции Foo(). Функция Foo() и Foo2() ничего не возвращают и поэтому данный код компилируется без ошибок и предупреждений.
Скорее всего, программист планировал написать:
void Foo();
void Foo2(int *ptr)
{
if (ptr == NULL)
return;
Foo();
...
}
Если изначальный код все-таки корректен, то его лучше переписать следующим образом:
void Foo2(int *ptr)
{
if (ptr == NULL)
{
Foo();
return;
}
...
}
Анализатор считает код безопасным, если отсутствует оператор "if" или вызов функции находится на той же строке, что и оператор "return". Такой код достаточно часто можно встретить в программах. Примеры безопасного кода:
void CPagerCtrl::RecalcSize()
{
return
(void)::SendMessageW((m_hWnd), (0x1400 + 2), 0, 0);
}
void Trace(unsigned int n, std::string const &s)
{ if (n) return TraceImpl(n, s); Trace0(s); }
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V504. |
Анализатор обнаружил использование функции 'alloca' внутри цикла.
Функция 'alloca' выделяет память для заданного буфера внутри фрейма вызывающей функции. Эта область памяти будет очищена только вместе с уничтожением этого фрейма в момент её завершения.
Рассмотрим пример:
void foo ()
{
char *buffer = nullptr;
buffer = (char *) alloca(256); // <= (1)
// using buffer
....
} // <= (2)
Пример синтетический, но на нём можно увидеть принцип работы функция 'alloca'. На строчке, помеченной как (1), происходит выделение блока памяти размеров в 256 байт на фрейме стека функции 'foo', который будет создан при её вызове. Фрейм функции будет уничтожен на строчке (2), когда она вернёт поток управления вызывающему коду. Это освободит всю выделенную под него память на стеке и позволит избежать утечек памяти.
Однако неаккуратное использование этой функции может привести к проблеме. Память стека исполняемой программы ограничена, и нужно следить за её переполнением.
Рассмотрим пример:
void bar(int n)
{
for (size_t i = 0; i < n; ++i)
{
char *buffer = nullptr;
if (buffer = (char*) alloca(256)) // <=
{
// using buffer
....
}
}
}
Функция 'alloca' вызывается в цикле. Проблема заключается в том, что между его итерациями выделенная память не будет освобождена. При этом, если количество итераций 'n' будет достаточно большим, то стек программы может переполниться.
Исправить некорректный код в данном случае достаточно просто. Для этого можно перенести вызов функции 'alloca' за пределы цикла и использовать 'buffer' повторно на каждой итерации:
void bar(int n)
{
char *buffer = (char*)alloca(256);
for (size_t i = 0; i < n; ++i)
{
// using buffer
....
}
}
Ещё один пример опасного кода:
// A2W defined in ATL using alloca
#define A2W(lpa) ....
void AtlExample()
{
....
size_t n = ....;
wchar_t** strings = { '\0' };
LPCSTR* pszSrc = { '\0' };
for (size_t i = 0; i < n; ++i)
{
if (wcscmp(strings[i], A2W(pszSrc[i])) == 0) // <=
{
....
}
}
}
Макрос 'A2W' определён в библиотеке 'ATL' версии 3.0. Внутри него используется функция 'alloca'. Приведёт ли данный код к ошибке или нет, будет зависеть от длины обрабатываемых строк, их количества и размера доступного стека. Исправить опасный код можно использовав класс 'CA2W', определённый в библиотеке 'ATL' версии 7.0. В отличии от макроса, он выделяет память на стеке только для маленьких строк. Для длинных она выделяется через 'malloc'. Кроме того, память, выделенная на стеке, будет освобождена при выходе из области видимости объявления переменной. Следовательно, будет освобождаться после сравнения с 'strings[i]'.
Исправленный пример:
// using ATL 7.0
....
for (size_t i = 0; i < n; ++i)
{
if (wcscmp(strings[i], CA2W(pszSrc[i])) == 0) // <=
{
....
}
}
Подробнее про функции библиотеки ATL можно почитать в документации.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V505. |
Анализатор обнаружил потенциально возможную ошибку, связанную с хранением указателя на локальную переменную. Предупреждение выдается в том случае, если время жизни объекта меньше, чем время жизни указателя на него.
Первый пример:
class MyClass
{
size_t *m_p;
void Foo() {
size_t localVar;
...
m_p = &localVar;
}
};
В данном случае адрес локальной переменной сохраняется внутри класса в переменную m_p и может затем быть по ошибке использован в другой функции, когда переменная localVar будет уже уничтожена.
Второй пример:
void Get(float **x)
{
float f;
...
*x = &f;
}
Функция Get() вернет указатель на локальную переменную, которая уже в этот момент не будет существовать.
Это сообщение подобно сообщению V507.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V506. |
Анализатор обнаружил потенциально возможную ошибку, связанную с хранением указателя на локальный массив. Предупреждение выдается в том случае, если время жизни массива меньше, чем время жизни указателя на него.
Первый пример:
class MyClass1
{
int *m_p;
void Foo()
{
int localArray[33];
...
m_p = localArray;
}
};
Массив localArray создается в стеке и массив localArray перестанет существовать по завершению функции Foo(). Однако указатель на этот массив будет сохранен в переменной m_p и может по неаккуратности использоваться, что приведет к ошибке.
Второй пример:
struct CVariable {
...
char name[64];
};
void CRendererContext::RiGeometryV(int n, char *tokens[])
{
for (i=0;i<n;i++)
{
CVariable var;
if (parseVariable(&var, NULL, tokens[i])) {
tokens[i] = var.name;
}
}
В этом примере указатель на массив, находящийся в переменной типа CVariable, сохраняется во внешнем массиве. В результате массив "tokens" после завершения функции RiGeometryV будет содержать указатели на уже несуществующие объекты.
Предупреждение V507 не всегда свидетельствует о наличии ошибки. Приведем сокращенный фрагмент кода, который анализатор считает опасным, но на самом деле этот код корректен:
png_infop info_ptr = png_create_info_struct(png_ptr);
...
BYTE trans[256];
info_ptr->trans = trans;
...
png_destroy_write_struct(&png_ptr, &info_ptr);
В данном коде время жизни объекта info_ptr совпадает с временем жизни trans. Объект создается внутри png_create_info_struct (), а уничтожается внутри png_destroy_write_struct(). Анализатор не может разобрать данный случай и предполагает, что объект png_ptr поступает извне. Пример кода, где анализатор был бы прав:
void Foo()
{
png_infop info_ptr;
info_ptr = GetExternInfoPng();
BYTE trans[256];
info_ptr->trans = trans;
}
Это сообщение подобно сообщению V506.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V507. |
Анализатор обнаружил код, который может содержать опечатку и, как следствие, ошибку. Происходит динамическое создание одного единственного объекта целочисленного типа и его инициализация. Высока вероятность, что из-за опечатки используются круглые скобки вместо квадратных.
Пример:
int n;
...
int *P1 = new int(n);
Происходит выделение памяти для одного объекта типа int. Это достаточно странно. Вероятно, корректный код должен выглядеть следующим образом:
int n;
...
int *P1 = new int[n];
Анализатор выдает предупреждение только в том случае, если память выделяется для простых объектов. При этом аргумент в скобках должен иметь целочисленный тип. В результате, анализатор не будут выдавать предупреждения на следующий корректный код:
float f = 1.0f;
float *f2 = new float(f);
MyClass *p = new MyClass(33);
Данная диагностика классифицируется как:
Если в программе возникает исключение, начинается свертывание стека, в ходе которого объекты разрушаются путем вызова деструкторов. Если деструктор объекта, разрушаемого при свертывании стека, бросает еще одно исключение и это исключение покидает деструктор, библиотека C++ немедленно аварийно завершает программу, вызывая функцию terminate(). Из этого следует, что деструкторы никогда не должны распространять исключения. Исключение, брошенное внутри деструктора, должно быть обработано внутри того же деструктора.
Анализатор обнаружил деструктор, содержащий оператор throw вне блока try..catch. Пример:
LocalStorage::~LocalStorage()
{
...
if (!FooFree(m_index))
throw Err("FooFree", GetLastError());
...
}
Данный код следует переписать таким образом, чтобы сообщить об ошибке, возникшей в деструкторе без использования механизма исключений. Если ошибка не критична, то ее можно игнорировать:
LocalStorage::~LocalStorage()
{
try {
...
if (!FooFree(m_index))
throw Err("FooFree", GetLastError());
...
}
catch (...)
{
assert(false);
}
}
Так же исключения могут возникать при вызове оператора 'new'. При невозможности выделения памяти будет сгенерировано исключение 'bad_alloc'. Пример:
A::~A()
{
...
int *localPointer = new int[MAX_SIZE];
...
}
Появление исключения возможно при использовании dynamic_cast<Type> при работе с ссылками. При невозможности приведения типов будет сгенерировано исключение 'bad_cast'. Пример:
B::~B()
{
...
UserType &type = dynamic_cast<UserType&>(baseType);
...
}
Для исправления данных ошибок следует переписать код таким образом, чтобы 'new' или 'dynamic_cast' были помещены в блок 'try{...}'.
Начиная с C++11 функции могут быть размечены как 'noexcept'. Если такие функции выбрасывают исключение, это приводит к аварийному завершению программы. Анализатор обнаруживает вызовы, которые потенциально могут выбросить исключение, в 'noexcept' функциях. Пример:
int noexceptWithNew() noexcept
{
return *(new int{42});
}
Анализатор выдаст предупреждение, так как оператор 'new' может выбросит исключение. Вызов 'new' в этом случае нужно обернуть в блок 'try..catch'.
Анализатор также обнаруживает вызовы функций, не размеченных как 'noexcept', из деструкторов и 'noexcept' функций. Такие вызовы потенциально опасны, так как они могут приводить к исключениям. Рассмотрим пример:
int allocate_memory()
{
return *(new int{ 42 });
}
int noexceptFunc() noexcept
{
return allocate_memory();
}
Анализатор выдаст предупреждение на строке с вызовом функции 'allocate_memory'.
Обратите внимание, что даже если вызываемая функция не размечена как 'noexcept', но анализатор не нашел в ее коде операций, способных выбросить исключение, предупреждение на ее вызов выдано не будет.
Дополнительные материалы по данной теме:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V509. |
Вариативная функция (функция, последним формальным параметром которой является эллипсис) принимает в качестве фактического аргумента, являющегося частью эллипсиса, объект классового типа, что может свидетельствовать о логической ошибке. В качестве фактического параметра для эллипсиса могут выступать только POD-типы.
POD – это аббревиатура от "Plain Old Data", что можно перевести как "Простые данные в стиле C". Начиная с C++11, к POD-типам относятся:
Если эллипсису функции в качестве параметра передается объект не POD-типа, это практически всегда свидетельствует о наличии ошибки в программе. Согласно стандарту C++11:
Passing a potentially-evaluated argument of class type having a non-trivial copy constructor, a non-trivial move constructor, or a non-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics.
Пример кода с ошибкой:
void bar(size_t count, ...);
void foo()
{
std::string s1 = ....;
std::string s2 = ....;
std::string s3 = ....;
bar(3, s1, s2, s3);
}
Начиная с C++11, для исправления ошибки можно воспользоваться вариативными шаблонами, благодаря которым информация о типах переданных аргументов будет сохранена:
template <typename T, typename ...Ts>
void bar(T &&arg, Ts &&...args);
void foo()
{
std::string s1 = ....;
std::string s2 = ....;
std::string s3 = ....;
bar(s1, s2, s3);
}
Анализатор не будет выдавать предупреждение, если передача объекта не POD-типа происходит в невычисляемом контексте (например, внутри операторов 'sizeof' / 'alignof'):
int bar(size_t count, ...);
void foo()
{
auto res = sizeof(bar(2, std::string {}, std::string {}));
}
На практике диагностическое правило V510 помогает выявлять ошибки при передаче аргументов в функции форматного ввода/вывода из C:
void foo(const std::wstring &ws)
{
wchar_t buf[100];
swprintf(buf, L"%s", ws);
}
Вместо указателя на строку в стек попадает содержимое объекта. Такой код приведет к формированию в буфере "абракадабры" или к аварийному завершению программы.
Корректный вариант кода должен выглядеть так:
wchar_t buf[100];
std::wstring ws(L"12345");
swprintf(buf, L"%s", ws.c_str());
Вместо printf-подобных функций в C++ рекомендуется использовать более безопасные аналоги. Например, 'boost::format', 'fmt::format', 'std::format' (C++20) и т.п.
Примечание. Диагностическое правило V510 также рассматривает объекты POD-типов при их передаче в функции форматного ввода/вывода. Несмотря на то, что такая передача безопасна, дальнейшая работа функции с такими аргументами может привести к непредвиденным результатам.
Если ложные срабатывания диагностического правила доставляют неудобства, их можно подавить на конкретной функции, вставив в код комментарий специального вида:
//-V:MyPrintf:510
Ошибку, аналогичную приведенной выше, мы должны наблюдать и в следующем коде:
void foo()
{
CString s;
CString arg(L"OK");
s.Format(L"Test CString: %s\n", arg);
}
Корректный вариант кода должен выглядеть так:
s.Format(L"Test CString: %s\n", arg.GetString());
Или, как предлагается в MSDN, для получения указателя на строку можно использовать оператор явного приведения к 'LPCTSTR', реализованный в классе 'CString':
void foo()
{
CString kindOfFruit = "bananas";
int howmany = 25;
printf("You have %d %s\n", howmany, (LPCTSTR)kindOfFruit);
}
Однако первый вариант 's.Format(L"Test CString: %s\n", arg);' также является корректным, как и остальные. Подробнее эта тема обсуждается в статье "Большой брат помогает тебе".
Разработчики MFC реализовали тип 'CString' специальным образом, чтобы его можно было передавать в функции вида 'printf' и 'Format'. Сделано это достаточно хитро, и те, кто интересуется, могут ознакомиться с реализацией класса 'CStringT'.
Таким образом, анализатор делает исключение для типа 'CString' и считает следующий код корректным:
void foo()
{
CString s;
CString arg(L"OK");
s.Format(L"Test CString: %s\n", arg);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования форматной строки. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V510. |
Оператор 'sizeof' возвращает размер указателя, а не массива, для случаев, когда массив был передан в функцию по копии.
Есть особенность языка, о которой легко забыть и допустить ошибку. Рассмотрим фрагмент кода:
char A[100];
void Foo(char B[100])
{
}
В этом коде объект A является массивам и выражение sizeof(A) вернет значение 100.
Объект B является просто указателем. Значение 100 в квадратных скобках подсказывает программисту, что он работает с массивом из ста элементов. Но в функцию передается вовсе не массив из ста элементов, а только указатель. Таким образом выражение sizeof(B) будет возвращать значение 4 или 8 (размер указателя в 32-битной/64-битной системе).
Предупреждение V511 выдается в том случае, когда вычисляется размер указателя переданного в качестве аргумента в формате "имя_типа имя_массива[N]". Такой код с высокой вероятностью содержит ошибку. Рассмотрим пример:
void Foo(float array[3])
{
size_t n = sizeof(array) / sizeof(array[0]);
for (size_t i = 0; i != n; i++)
array[i] = 1.0f;
}
Функция заполнит значением 1.0f не весь массив, а только 1 или 2 элемента, в зависимости от разрядности системы.
В Win32: sizeof(array) / sizeof(array[0]) = 4/4 = 1.
В Win64: sizeof(array) / sizeof(array[0]) = 8/4 = 2.
Для предотвращения подобных ошибок необходимо явно передавать размер массива. Корректный код:
void Foo(float *array, size_t arraySize)
{
for (size_t i = 0; i != arraySize; i++)
array[i] = 1.0f;
}
Другой вариант, это использовать ссылку на массив:
void Foo(float (&array)[3])
{
size_t n = sizeof(array) / sizeof(array[0]);
for (size_t i = 0; i != n; i++)
array[i] = 1.0f;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V511. |
Анализатор обнаружил потенциально возможную ошибку, связанную с заполнением, копированием или сравнением буферов памяти. Ошибка может приводить к переполнению буфера (buffer overflow).
Примечание: ранее данная диагностика содержала в себе дополнительный функционал, но позже было решено вынести его в отдельную диагностику V1086. О причинах и последствиях такого решения можно прочитать в специальной заметке.
Это достаточно распространённый вид ошибки, возникающий из-за опечаток или невнимательности. В результате может произойти чтение или запись в область памяти, которая занята другими данными. Такой ошибкой могут воспользоваться злоумышленники для выполнения вредоносного программного кода, чтения чувствительной информации или вызова сбоев в работе операционной системы. Неприятность подобных ошибок заключается в том, что программа долгое время может работать стабильно.
Рассмотрим пример N1:
#define BYTES_COUNT 5
struct Example
{
unsigned char id[BYTES_COUNT];
unsigned char extended[BYTES_COUNT - 2];
unsigned char data[20];
};
void ClearID(Example *data)
{
memset(&data->id, 0, BYTES_COUNT);
memset(&data->extended, 0, BYTES_COUNT);
}
В данном примере в функцию 'ClearID' передаётся указатель на объект типа 'Example'. Внутри функции происходит очистка полей 'id' и 'extended' с помощью функции 'memset'. Из-за невнимательного использования макроса 'BYTES_COUNT' при очистке поля 'extended' произойдёт переполнение буфера, которое приведёт к перезаписи соседнего поля 'data'.
Также переполнение буфера можно получить при неправильном приведении типов, как в примере N2:
struct MyTime
{
int timestamp;
....
};
MyTime s;
time((time_t*)&s.timestamp);
Данный пример, на первый взгляд, не содержит в себе опасностей и даже будет корректно работать, пока совпадает размер типов 'int' и 'time_t'. Проблема проявится при использовании стандартной библиотеки, где тип 'time_t' может быть 64-битным. При этом переменная 'int' имеет размер 32 бита.
В таком случае при вызове функции 'time' произойдёт запись в переменную 'timestamp', а также в область памяти после неё. Корректный вариант:
struct MyTime
{
time_t time;
....
};
MyTime s;
time(&s.time);
Ранее диагностическое правило содержало в себе дополнительный функционал, который был перенесён в диагностику V1086. Новая диагностика выявляет случаи, когда буфер обработан не полностью.
До разделения была возможность тонко настроить поведение диагностики и отключить неактуальную часть с помощью специальных комментариев. В целях обеспечения обратной совместимости, осталась возможность отключить диагностику V512 с помощью специального комментария:
//-V512_OVERFLOW_OFF
Этот комментарий можно вписать в заголовочный файл, который включается во все другие файлы. Например, это может быть "stdafx.h". Если вписать этот комментарий в "*.cpp" файл, то он будет действовать только для этого файла.
Поскольку теперь диагностика V512 ищет только переполнение буфера, то этот комментарий стал эквивалентен полному отключению диагностики (//-V::512).
Иногда анализатор при работе с форматной строкой может не знать точное значение аргумента – к примеру, когда он пришёл из параметра функции:
void foo(int someVar)
{
char buf[2];
sprintf(buf, "%d", someVar);
....
}
По умолчанию здесь предупреждение выдаваться не будет. Чтобы включить его выдачу, добавьте следующий комментарий:
//V_512_WARN_ON_UNKNOWN_FORMAT_ARGS
В этом случае для работы анализатор будет использовать диапазон значений из типа аргумента.
Неоднократно к нам в поддержку обращались пользователи, считая, что анализатор выдаёт ложное предупреждение на подобный код:
char buf[5];
strncpy(buf, "X", 100);
На первый взгляд может показаться, что функция должна скопировать только 2 байта (символ 'X' и терминальный ноль). Но, на самом деле, здесь действительно произойдёт выход за пределы массива. Причиной тому является важное свойство функции 'strncpy':
Если требуемое количество символов (третий аргумент функции) больше длины исходной строки, то целевая строка будет дополнена нулевыми символами до требуемого размера.
Более подробно с этим или другими свойствами функции 'strncpy' можно ознакомиться на cppreference.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V512. |
В программе обнаружено использование функции CreateThread или ExitThread. Если в параллельных потоках используются функции CRT (C run-time library), то вместо CreateThread/ ExitThread следует вызывать функции _beginthreadex/_endthreadex.
Приведем выдержки из 6-ой главы книги Джеффри Рихтера "Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows" / Пер. с англ. - 4-е изд.
"CreateThread - это Windows-функция, создающая поток. Но никогда не вызывайте ее, если Вы пишете код на С/С++. Вместо нее Вы должны использовать функцию _beginthreadex из библиотеки Visual С++.
Чтобы многопоточные программы, использующие библиотеку С/С++ (CRT), работали корректно, требуется создать специальную структуру данных и связать ее с каждым потоком, из которого вызываются библиотечные функции. Более того, они должны знать, что, когда Вы к ним обращаетесь, нужно просматривать этот блок данных в вызывающем потоке чтобы не повредить данные в каком-нибудь другом потоке.
Так откуда же система знает, что при создании нового потока надо создать и этот блок данных? Ответ очень прост - не знает и знать не хочет Вся ответственность - исключительно на Вас. Если Вы пользуетесь небезопасными в многопоточной среде функциями, то должны создавать потоки библиотечной функцией _beginthreadex, а не Windows-функцией CreateThread .
Заметьте, что функция _beginthreadex существует только в многопоточных версиях библиотеки С/С++. Связав проект с однопоточной библиотекой, Вы получите от компоновщика сообщение об ошибке "unresolved external symbol". Конечно, это сделано специально, потому что однопоточная библиотека не может корректно работать в многопоточном приложении. Также обратите внимание на то, что при создании нового проекта Visual Studio по умолчанию выбирает однопоточную библиотеку. Этот вариант не самый безопасный, и для многопоточных приложений Вы должны сами выбрать одну из многопоточных версий библиотеки С/С++."
Соответственно, для уничтожения потока, созданного с помощью функции _beginthreadex, необходимо использовать функцию _endthreadex.
Дополнительные материалы по данной теме:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V513. |
Анализатор обнаружил потенциально возможную ошибку, связанную с делением размера указателя на некоторое значение. Деление размера указателя является странной операцией, так как не имеет практического смысла и, скорее всего, свидетельствует о наличии в коде ошибки или опечатки.
Рассмотрим пример:
const size_t StrLen = 16;
LPTSTR dest = new TCHAR[StrLen];
TCHAR src[StrLen] = _T("string for V514");
_tcsncpy(dest, src, sizeof(dest)/sizeof(dest[0]));
В выражении "sizeof(dest)/sizeof(dest[0])" происходит деление размера указателя на размер элемента, на который ссылается указатель. В результате мы можем получить различное количество скопированных байтов в зависимости от размера указателя и типа TCHAR, но не то количество, что планировал программист.
С учетом того, что функция _tcsncpy сама по себе не является безопасный, корректный и более безопасный код может выглядеть следующим образом:
const size_t StrLen = 16;
LPTSTR dest = new TCHAR[StrLen];
TCHAR src[StrLen] = _T("string for V514");
_tcsncpy_s(dest, StrLen, src, StrLen);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V514. |
В коде оператор delete применяется не к указателю, а к объекту класса. С большой вероятностью это является ошибкой.
Рассмотрим пример кода:
CString str;
...
delete str;
Оператор 'delete' можно применить к объекту типа CString,так как класс CString может быть автоматически приведен к указателю. Подобный код может привести к исключению или неопределенному поведению программы.
Корректный код, возможно должен был выглядеть так:
CString *pstr = new CString;
...
delete pstr;
В некоторых случаях применение оператора 'delete' к объектам класса не является ошибкой. Подобный код, например, можно встретить при работе с классом QT::QBasicAtomicPointer. Анализатор игнорирует вызов операnора 'delete' для объектов этого типа. Если Вы знаете другие подобные классы, применение к которым оператора 'delete' является стандартной практикой, то сообщите нам о них. Мы добавим их в исключения.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
В коде имеется конструкция сравнения ненулевого указателя на функцию с нулем. Скорее всего, это означает, что в коде присутствует опечатка - забыты круглые скобки.
Рассмотрим пример:
int Foo();
void Use()
{
if (Foo == 0)
{
//...
}
}
Условие "Foo == 0" не имеет смысла. Адрес функции 'Foo' всегда не равен нулю, а следовательно результатом сравнение всегда будет значение 'false'. В рассматриваемом коде случайно пропущены круглые скобки. Корректный вариант кода:
if (Foo() == 0)
{
//...
}
Если в коде явно написано взятие адреса функции, то такой код считается корректным. Пример:
int Foo();
void Use()
{
if (&Foo != NULL)
//...
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V516. |
Анализатор обнаружил потенциально возможную ошибку в конструкции, состоящей из условных операторов.
Рассмотрим пример:
if (a == 1)
Foo1();
else if (a == 2)
Foo2();
else if (a == 1)
Foo3();
В данном примере функции 'Foo3()' никогда не получит управления. Вероятно, мы имеем дело с логической ошибкой и корректный код должен выглядеть так:
if (a == 1)
Foo1();
else if (a == 2)
Foo2();
else if (a == 3)
Foo3()
На практике подобная ошибка может выглядеть следующим образом:
if (radius < THRESH * 5)
*yOut = THRESH * 10 / radius;
else if (radius < THRESH * 5)
*yOut = -3.0f / (THRESH * 5.0f) * (radius - THRESH * 5.0f) + 3.0f;
else
*yOut = 0.0f;
Трудно сказать, как должно выглядеть корректное условие сравнения, но наличие в коде ошибки очевидно.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V517. |
Анализатор обнаружил потенциально возможную ошибку, связанную с выделением недостаточного количества памяти. В коде подсчитывается длина строки и выделяется буфер памяти данного размера, но не учитывается наличие терминального '\0'.
Рассмотрим пример:
char *p = (char *)malloc(strlen(src));
strcpy(p, src);
В данном случае просто забыто про +1. Правильный код:
char *p = (char *)malloc(strlen(src) + 1);
strcpy(p, src);
Приведем другой пример некорректного кода, обнаруженный анализатором в одном из приложений:
if((t=(char *)realloc(next->name, strlen(name+1))))
{
next->name=t;
strcpy(next->name,name);
}
Здесь по невнимательности неправильно поставлена правая скобочка ')'. В результате мы выделим на 2 байта меньше памяти, чем необходимо. Исправленный вариант:
if((t=(char *)realloc(next->name, strlen(name)+1)))
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V518. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что одной и той же переменной дважды подряд присваивается значение. Причем между этими присваиваниями сама переменная не используется.
Рассмотрим пример:
A = GetA();
A = GetB();
То, что переменной 'A' два раза присваивается значение, может свидетельствовать о наличии ошибки. Высока вероятность, что код должен выглядеть следующим образом:
A = GetA();
B = GetB();
Если переменная между присваиваниями используется, то этот код считается анализатором корректным:
A = 1;
A = A + 1;
A = Foo(A);
Рассмотрим, как подобная ошибка может выглядеть на практике. Следующий код взят из реального приложения, где был реализован свой собственный класс CSize:
class CSize : public SIZE
{
...
CSize(POINT pt) { cx = pt.x; cx = pt.y; }
Корректный вариант должен был выглядеть так:
CSize(POINT pt) { cx = pt.x; cy = pt.y; }
Рассмотрим еще один пример. Вторая строка была написана для отладки или для того, чтобы посмотреть, как будет смотреться текст другого цвета. И, видимо, потом вторую строку забыли удалить:
m_clrSample = GetSysColor(COLOR_WINDOWTEXT);
m_clrSample = RGB(60,0,0);
Иногда анализатор выдает ложные предупреждения, когда запись в переменные используется для отладочных целей. Пример подобного кода:
status = Foo1();
status = Foo2();
В данной ситуации можно подавить ложные срабатывания, используя комментарий "//-V519". Можно убрать из кода ничего не значащие присваивания. И последнее. Возможно этот код все же некорректен, и необходимо проверять значение переменной 'status'.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V519. |
Анализатор обнаружил потенциальную ошибку, которая может являться следствием опечатки. В качестве индекса при работе с массивом используется выражение, содержащий оператор запятая ','.
Пример подозрительного кода:
float **array_2D;
array_2D[getx() , gety()] = 0;
Скорее всего, здесь имелось в виду:
array_2D[ getx() ][ gety() ] = 0;
Подобные ошибки могут быть допущены после работы с языком программирования, где индексы массивов разделяются запятыми.
Рассмотрим пример ошибки, найденный анализатором в одном из проектов:
float **m;
TextOutput &t = ...
...
t.printf("%10.5f, %10.5f, %10.5f,\n%10.5f, %10.5f, %10.5f,\n%10.5f,
%10.5f, %10.5f)",
m[0, 0], m[0, 1], m[0, 2],
m[1, 0], m[1, 1], m[1, 2],
m[2, 0], m[2, 1], m[2, 2]);
Так как функция printf из класса TextOutput работает с переменным количеством аргументов, то она не может проверить, что место значений типа float ей будут переданы указатели. В результате мы распечатаем мусор вместо значений элементов матрицы. Корректный вариант:
t.printf("%10.5f, %10.5f, %10.5f,\n%10.5f, %10.5f, %10.5f,\n%10.5f,
%10.5f, %10.5f)",
m[0][0], m[0][1], m[0][2],
m[1][0], m[1][1], m[1][2],
m[2][0], m[2][1], m[2][2]);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V520. |
Оператор запятая ',' используется для выполнения стоящих по обе стороны от него выражений в порядке слева направо и получения значения правого выражения.
Анализатор обнаружил в коде программы выражение, в котором подозрительным образом используется оператор ','. Высока вероятность, что текст программы содержит опечатку.
Рассмотрим пример:
float Foo()
{
double A;
A = 1,23;
float f = 10.0f;
return 3,f;
}
В данном коде переменной A будет присвоено значение 1, а вовсе не 1.23. Согласно правилам языка Си/Си++ выражение "A = 1,23" эквивалентно "(A = 1),23". Также функция Foo() вернет значение 10.0f, а не 3.0f. В обоих случаях ошибка связана с использованием символа запятая ',' вместо символа точки '.'.
Исправленный вариант кода:
float Foo()
{
double A;
A = 1.23;
float f = 10.0f;
return 3.f;
}
Примечание. Были случаи, когда анализатор не мог разобраться в коде и выдавал предупреждения V521 на совершенно безобидные конструкции. Обычно это связано с использованием шаблонных классов или сложных макросов. Если при работе с анализатором вы заметили подобное ложное срабатывание, то просим сообщить о нем разработчикам. Для подавления ложных срабатываний можно использовать комментарий вида "//-V521".
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V521. |
Анализатор обнаружил фрагмент кода, который может привести к использованию нулевого указателя.
Рассмотрим несколько примеров, для которых анализатор выдает диагностическое сообщение V522:
if (pointer != 0 || pointer->m_a) { ... }
if (pointer == 0 && pointer->x()) { ... }
if (array == 0 && array[3]) { ... }
if (!pointer && pointer->x()) { ... }
Во всех условиях допущена логическая ошибка, которая приведет к разыменованию нулевого указателя. Ошибка может быть допущена при рефакторинге кода или из-за случайной опечатки.
Корректные варианты:
if (pointer == 0 || pointer->m_a) { ... }
if (pointer != 0 && pointer->x()) { ... }
if (array != 0 && array[3]) { ... }
if (pointer && pointer->x()) { ... }
Конечно, это очень простые ситуации. На практике проверка указателя и его использование может находиться в разных местах. Если анализатор выдал предупреждение V522, изучите код расположенный выше и попробуйте понять, почему указатель может быть нулевым.
Пример кода, где проверка и использование указателя находятся в разных строках
if (ptag == NULL) {
SysPrintf("SPR1 Tag BUSERR\n");
psHu32(DMAC_STAT)|= 1<<15;
spr1->chcr = ( spr1->chcr & 0xFFFF ) |
( (*ptag) & 0xFFFF0000 );
return;
}
Анализатор предупредит, об опасности в строке "( (*ptag) & 0xFFFF0000 )". Здесь или некорректно написано условие, или вместо 'ptag' должна использоваться другая переменная.
Иногда в тестовых целях программисты сознательно используют разыменование нулевого указателя. Например, анализатор будет генерировать предупреждение там, где используется вот такой макрос:
/// This generate a coredump when we need a
/// method to be compiled but not usabled.
#define elxFIXME { char * p=0; *p=0; }
Лишние предупреждения можно отключить, используя комментарий "//-V522" в тех строках, где используется макрос 'elxFIXME'. Альтернативный вариант, это написать рядом с макросом комментарий специального вида:
//-V:elxFIXME:522
Комментарий может быть написан как до, так и после макроса. Это не имеет значения. Подробнее с методами подавления ложных предупреждений можно познакомиться здесь.
Частой причиной появления предупреждения является использование указателя, возвращённого такой функцией как 'malloc' и аналогичной ей, без предварительной проверки. Некоторые программисты считают, что проверять указатель необязательно. В случае ошибки выделения памяти программа всё равно не работоспособна, и её аварийное завершение в следствии записи по нулевому указателю является приемлемым сценарием.
Однако, всё гораздо сложнее и опаснее, чем может казаться на первый взгляд. Предлагаем ознакомиться с публикацией: "Почему важно проверять, что вернула функция malloc".
Если по каким-то причинам всё равно не планируете проверять такие указатели, далее рассказано о специализированной настройке анализатора.
Данная диагностика учитывает информацию, может ли тот или иной указатель быть нулевым. В ряде случаев, эта информация берется из таблиц разметки функций, которые находятся внутри самого анализатора.
Примером может служить функция 'malloc'. Эта функция может вернуть 'NULL'. Соответственно, если использовать указатель, который вернула функция 'malloc', без предварительной проверки, это может привести к разыменованию нулевого указателя.
Иногда у наших пользователей возникает желание изменить поведение анализатора и заставить его считать, что, например, функция 'malloc' не может вернуть 'NULL'. Пользователь может использовать системные библиотеки, в которых ситуации нехватки памяти обрабатываются особым образом.
Также может возникнуть желание подсказать анализатору, что определённая функция может вернуть нулевой указатель.
В этом случае вы можете воспользоваться дополнительными настройками, которые описаны в разделе "Как указать анализатору, что функция может или не может возвращать nullptr".
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки разыменования нулевого указателя. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V522. |
Анализатор обнаружил ситуацию, когда истинная и ложная ветка оператора 'if' полностью совпадают. Часто это свидетельствует о наличии логической ошибки.
Пример:
if (X)
Foo_A();
else
Foo_A();
Будет условие X ложно или истинно, все равно произойдет вызов функции Foo_A().
Корректный вариант кода:
if (X)
Foo_A();
else
Foo_B();
Пример подобной ошибки, взятый из реального приложения:
if (!_isVertical)
Flags |= DT_BOTTOM;
else
Flags |= DT_BOTTOM;
Наличие двух пустых веток считается корректной и безопасной ситуацией. Подробные конструкции можно часто встретить при использовании макросов. Пример безопасного кода:
if (exp) {
} else {
}
Также анализатор считает подозрительным, если оператор 'if' не содержит блок 'else', а следующий за ним код идентичен блоку условного оператора. При этом блок кода заканчивается оператором return, break и т.п.
Подозрительный фрагмент кода:
if (X)
{
doSomething();
Foo_A();
return;
}
doSomething();
Foo_A();
return;
Возможно программист забыл исправить скопированный фрагмент кода, либо написал лишний код.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V523. |
Данное предупреждение выдается в том случае, если анализатор обнаружил две функции, реализованные идентичным образом. Наличие двух одинаковых функций само по себе не является ошибкой, но является поводом обратить на них внимание.
Смысл данной диагностики в обнаружении следующей разновидности ошибок:
class Point
{
...
float GetX() { return m_x; }
float GetY() { return m_x; }
};
Из-за допущенной опечатки две разные по смыслу функции выполняют одинаковые действия. Корректный вариант:
float GetX() { return m_x; }
float GetY() { return m_y; }
В приведенном примере идентичность тел функций GetX() и GetY() явно свидетельствует о наличии ошибки. Однако если выдавать предупреждения на все одинаковые функции, то процент ложный срабатываний будет крайне большим. Поэтому анализатор руководствуется целым рядом исключений, когда не стоит предупреждать об одинаковых телах функций. Перечислим некоторые из них:
Однако все равно в ряде случаев анализатор не может понять, что одинаковые тела функций не являются подозрительной ситуацией. Вот код, который диагностируется как опасный, но по сути таковым не являющимся:
PolynomialMod2 Plus(const PolynomialMod2 &b) const
{return Xor(b);}
PolynomialMod2 Minus(const PolynomialMod2 &b) const
{return Xor(b);}
Бороться с ложными срабатываниями можно несколькими способами. Если ложные срабатывания относятся к файлам внешних библиотек, то эту библиотеку (путь до нее) можно добавить в исключения. Если предупреждения относятся к вашему коду, то вы можете использовать комментарий вида "//-V524", который приведет к подавлению предупреждений. Если ложных срабатываний много, то вы можете в настройках анализатора полностью отключить использование данной проверки. Также вы можете модифицировать код таким образом, чтобы одна функция вызывала другую с тем же самым кодом.
Последний вариант часто является наиболее оптимальным, поскольку, во-первых, это сокращает объем кода, а во вторых делает его более простым в поддержке. Правки достаточно вносить только в одну, а не в две функции. Приведем пример реального кода, где вызов одной функции из другой будет полезен:
static void PreSave(void) {
int x;
for(x=0;x<TotalSides;x++) {
int b;
for(b=0; b<65500; b++)
diskdata[x][b] ^= diskdatao[x][b];
}
}
static void PostSave (void) {
int x;
for(x=0;x<TotalSides;x++) {
int b;
for(b=0; b<65500; b++)
diskdata[x][b] ^= diskdatao[x][b];
}
}
Разумно заменить этот код на следующий вариант:
static void PreSave(void) {
int x;
for(x=0;x<TotalSides;x++) {
int b;
for(b=0; b<65500; b++)
diskdata[x][b] ^= diskdatao[x][b];
}
}
static void PostSave (void) {
PreSave();
}
В этом коде не была исправлена ошибка. Но после рефакторинга предупреждение V524 исчезло, а код стал проще.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V524. |
Анализатор обнаружил код, который возможно содержит опечатку. Этот код можно разделить на более мелкие и похожие между собой фрагменты кода. Хотя фрагменты кода похожи, но все же различны. Высока вероятность, что подобный код был создан с использованием подхода Copy-Paste. Сообщение V525 будет выдано в том случае, если есть подозрение, что один из элементов не исправлен в скопированном тексте. Ошибка может находиться в одной из строк, номера которых содержатся в сообщении V525.
Недостатки сообщения V525:
1) Данное диагностическое правило основано на эвристических методах и нередко дает ложное срабатывание.
2) Реализация эвристического алгоритма сложна и занимает более 1000 строк кода на Си++. Поэтому его затруднительно описать в рамках документации. Как следствие пользователю может быть трудно понять, почему выдано то или иное сообщение V525.
3) Диагностическое сообщение относится к нескольким строкам, а не к одной. Указать только одну строку невозможно, так как ошибка может быть в любой из них.
Преимущества сообщения V525:
1) Можно обнаружить ошибки, которые крайне сложно заметить при обзоре кода (code review).
Рассмотрим вначале искусственный пример:
...
float rgba[4];
rgba[0] = object.GetR();
rgba[1] = object.GetG();
rgba[2] = object.GetB();
rgba[3] = object.GetR();
Массив 'rgba' представляет собой цвет и прозрачность некоего объекта. При написании кода, заполняющего массив, вначале была написана строчка "rgba[0] = object.GetR();". Затем это строка была несколько раз скопирована и изменена. Однако в последней строке изменения сделаны не до конца и вместо функции 'GetA()' вызывается 'GetR()'. Анализатор выдает на данный код следующее предупреждение:
V525: The code containing the collection of similar blocks. Check items 'GetR', 'GetG', 'GetB', 'GetR' in lines 12, 13, 14, 15.
Просмотрев строки 12, 13, 14 и 15 можно обнаружить ошибку. Исправленный вариант кода:
rgba[3] = object.GetA();
Теперь рассмотрим несколько примеров, взятых из кода реальных приложений. Первый пример:
tbb[0].iBitmap = 0;
tbb[0].idCommand = IDC_TB_EXIT;
tbb[0].fsState = TBSTATE_ENABLED;
tbb[0].fsStyle = BTNS_BUTTON;
tbb[0].dwData = 0;
tbb[0].iString = -1;
...
tbb[6].iBitmap = 6;
tbb[6].idCommand = IDC_TB_SETTINGS;
tbb[6].fsState = TBSTATE_ENABLED;
tbb[6].fsStyle = BTNS_BUTTON;
tbb[6].dwData = 0;
tbb[6].iString = -1;
tbb[7].iBitmap = 7;
tbb[7].idCommand = IDC_TB_CALC;
tbb[7].fsState = TBSTATE_ENABLED;
tbb[7].fsStyle = BTNS_BUTTON;
tbb[6].dwData = 0;
tbb[7].iString = -1;
Фрагмент кода приведен далеко не полностью. Вырезано более чем половина. Фрагмент писался методом копирования и правки кода. Неудивительно, что в таком большом фрагменте затерялся неверный индекс. Анализатор выдает следующее диагностическое сообщение: "The code containing the collection of similar blocks. Check items '0', '1', '2', '3', '4', '5', '6', '6' in lines 589, 596, 603, 610, 617, 624, 631, 638". Просмотрев данные строки, мы можем исправить дважды повторяющийся индекс '6'. Исправленный вариант кода:
tbb[7].iBitmap = 7;
tbb[7].idCommand = IDC_TB_CALC;
tbb[7].fsState = TBSTATE_ENABLED;
tbb[7].fsStyle = BTNS_BUTTON;
tbb[7].dwData = 0;
tbb[7].iString = -1;
Второй пример:
pPopup->EnableMenuItem(
ID_CONTEXT_EDITTEXT,MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_CLOSEALL, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_CLOSE, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_SAVELAYOUT, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_RESIZE, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_REFRESH, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_EDITTEXT, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_SAVE, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_EDITIMAGE,MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_CLONE,MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
Рассматривая данный код, очень сложно найти в нем ошибку. Однако ошибка здесь действительно присутствует. Дважды модифицируется состояние одного и того же пункта меню 'ID_CONTEXT_EDITTEXT'. Выделим две повторяющиеся строки:
------------------------------
pPopup->EnableMenuItem(
ID_CONTEXT_EDITTEXT,MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
------------------------------
pPopup->EnableMenuItem(
ID_CONTEXT_CLOSEALL, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_CLOSE, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_SAVELAYOUT, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_RESIZE, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_REFRESH, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
------------------------------
pPopup->EnableMenuItem(
ID_CONTEXT_EDITTEXT, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
------------------------------
pPopup->EnableMenuItem(
ID_CONTEXT_SAVE, MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_EDITIMAGE,MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
pPopup->EnableMenuItem(
ID_CONTEXT_CLONE,MF_GRAYED|MF_DISABLED|MF_BYCOMMAND);
Возможно это несерьезная погрешность, и одна из строк просто лишняя. А возможно, здесь забыли изменить состояние какого-то другого элемента меню.
К сожалению, анализатор, осуществляя данный вид диагностики, часто ошибается и генерирует ложные срабатывания. Пример кода, приводящий к ложному срабатыванию:
switch (i) {
case 0: f1 = 2; f2 = 3; break;
case 1: f1 = 0; f2 = 3; break;
case 2: f1 = 1; f2 = 3; break;
case 3: f1 = 1; f2 = 2; break;
case 4: f1 = 2; f2 = 0; break;
case 5: f1 = 0; f2 = 1; break;
}
Здесь анализатору не нравится корректный столбец цифр: 2, 0, 1, 1, 2, 0. В подобных ситуациях можно воспользоваться подавлением предупреждений, вписав комментарий //-V525 в конце строки:
switch (i) {
case 0: f1 = 2; f2 = 3; break; //-V525
case 1: f1 = 0; f2 = 3; break;
case 2: f1 = 1; f2 = 3; break;
case 3: f1 = 1; f2 = 2; break;
case 4: f1 = 2; f2 = 0; break;
case 5: f1 = 0; f2 = 1; break;
}
Если ложных предупреждений слишком много, то можно отключить данное диагностическое правило в настройках анализатора. Также мы будем благодарны, если вы напишите нам в поддержку о тех ситуациях, когда происходят ложные срабатывания, и мы постараемся улучшить алгоритм диагностики. Просьба в письме приводить соответствующие фрагменты кода.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V525. |
Данное сообщение носит рекомендательный характер. Оно редко диагностирует логическую ошибку, но помогает сделать код более читабельным для молодых специалистов.
Анализатор обнаружил конструкцию сравнения двух строк, которую рационально записать более понятным способом. Такие функции как strcmp, strncmp, wcsncmp возвращают 0, если строки совпадает. Это может приводить к ошибкам в логике программы. Рассмотрим пример кода:
if (strcmp(s1, s2))
Это условие выполнится в том случае, если строки НЕ СОВПАДАЮТ. Возможно, вы хорошо помните, что возвращает strcmp(). Однако человек, редко работающий со строковыми функциями может подумать, что функция strcmp() возвращает значение типа 'bool'. Тогда он прочитает этот код так: "условие истинно, если строки совпадают".
Лучше не экономить на лишних символах в тексте программы и написать так:
if (strcmp(s1, s2) != 0)
Подобная запись подсказывает человеку, что функция strcmp() возвращает не тип bool, а некое числовое значение. Так снижается вероятность, что код будет неверно понят.
Если вам не нравится получать данное диагностическое сообщение, вы можете выключить его в настройках анализатора.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V526. |
Подобная ошибка возникает в двух схожих ситуациях.
1) Анализатор обнаружил потенциально возможною ошибку, связанную с тем, что указателю на тип bool присваивается значение false. Высока вероятность, что забыта операция разыменования указателя. Пример:
float Get(bool *retStatus)
{
...
if (retStatus != nullptr)
retStatus = false;
...
}
В данном коде забыт оператор '*'. Вместо возвращения статуса произойдет обнуление указателя retStatus. Корректный вариант кода:
if (retStatus != nullptr)
*retStatus = false;
2) Анализатор обнаружил потенциально возможною ошибку, связанную с тем, что указателю на тип char/wchar_t присваивается значение '\0' или L'\0'. Высока вероятность, что забыта операция разыменования указателя. Пример:
char *cp;
...
cp = '\0';
Корректный вариант:
char *cp;
...
*cp = '\0';
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V527. |
Подобная ошибка возникает в двух схожих ситуациях.
1) Анализатор обнаружил потенциально возможною ошибку, связанную с тем, что указатель на тип bool сравнивается со значением false. Высока вероятность, что забыта операция разыменования указателя. Пример:
bool *pState;
...
if (pState != false)
...
В данном коде забыт оператор '*'. И получается, что мы сравниваем значение указателя pState с нулевым указателем nullptr. Корректный вариант кода:
bool *pState;
...
if (*pState != false)
...
2) Анализатор обнаружил потенциально возможною ошибку, связанную с тем, что указатель на тип char/wchar_t сравнивается со значением '\0' или L'\0'. Высока вероятность, что забыта операция разыменования указателя. Пример:
char *cp;
...
if (cp != '\0')
Корректный вариант:
char *cp;
...
if (*cp != '\0')
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V528. |
Анализатор обнаружил потенциально возможною ошибку, связанную с наличием точки с запятой ';' после оператора if, for или while.
Приведем пример:
for (i = 0; i < n; i++);
{
Foo(i);
}
Корректный вариант:
for (i = 0; i < n; i++)
{
Foo(i);
}
Использование точки с запятой ';' сразу после оператора for или while само по себе не является ошибкой и часто встречается в коде. Поэтому анализатор отсевает многие случаи, руководствуясь рядом дополнительных факторов. Например, следующий пример кода считается анализатором безопасным:
for (depth = 0, cur = parent; cur; depth++, cur = cur->parent)
;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V529. |
Вызов некоторых функций не имеет смысла, если результат их работы не используется.
Рассмотрим первый пример:
void VariantValue::Clear()
{
m_vtype = VT_NULL;
m_bvalue = false;
m_ivalue = 0;
m_fvalue = 0;
m_svalue.empty();
m_tvalue = 0;
}
Этот код очистки значений взят из реального приложения. Ошибка заключается в том, что вместо функции 'clear' объекта 'std::string' случайно вызывается функция 'empty', и содержимое строки остается неизменным. Ошибка диагностируется на основании того, что результат работы функции 'empty' обязательно должен быть использован. Например, результат должен быть с чем-то сравнен или записан в переменную.
Исправленный вариант кода:
void VariantValue::Clear()
{
m_vtype = VT_NULL;
m_bvalue = false;
m_ivalue = 0;
m_fvalue = 0;
m_svalue.clear();
m_tvalue = 0;
}
Второй пример:
void unregisterThread() {
Guard<TaskQueue> g(_taskQueue);
std::remove(_threads.begin(), _threads.end(),
ThreadImpl::current());
}
Функция 'std::remove' не удаляет элементы из контейнера. Она только сдвигает элементы и возвращает итератор на начало мусора. Пусть мы имеем контейнер 'vector<int>', содержащий элементы 1,2,3,1,2,3,1,2,3. Если выполнить код "remove( v.begin(), v.end(), 2 )", то контейнер будет содержать элементы 1,3,1,3,?,?,?, где ? - некий мусор. При этом функция вернет итератор на первый мусорный элемент, и если мы хотим удалить эти мусорные элементы, то должны написать код: "v.erase(remove(v.begin(), v.end(), 2), v.end())".
Как видно из объяснения, результат 'std::remove' должен быть обязательно использован. Корректный код:
void unregisterThread() {
Guard<TaskQueue> g(_taskQueue);
auto trash = std::remove(_threads.begin(), _threads.end(),
ThreadImpl::current());
_threads.erase(trash, _threads.end());
}
Функций, результат которых должен быть обязательно использован, огромное количество. К ним можно отнести: 'malloc', 'realloc', 'fopen', 'isalpha', 'atof', 'strcmp' и многие, многие другие функции. Неиспользуемый результат свидетельствует об ошибке, чаще всего связанной с допущенной опечаткой. Однако анализатор предупреждает только об ошибках, связанных с использованием стандартной библиотеки. На это имеется две причины:
1) Допустить ошибку, не используя результат такой функции, как 'fopen' намного сложней, чем спутать 'std::clear' и 'std::empty'.
2) Данная функциональность будет дублировать возможности Code Analysis for C/C++, входящий в состав некоторых редакций Visual Studio (смотри предупреждение C6031). Однако в Visual Studio эти предупреждения не реализованы для функций стандартной библиотеки.
Если вы хотите предложить расширить список функций, поддерживаемых анализатором, то обратитесь в поддержку. Мы будем благодарны за интересные примеры и советы.
Безопасность
Помимо явных ошибок и опечаток, следует рассмотреть тему безопасности. Существуют функции, связанные с разграничением доступа. В качества примера назовём только функции LogonUser и SetThreadToken, но на самом деле их много. Нужно обязательно проверять статусы, которые возвращают эти функции. Неиспользование значений, которые вернули эти функции, является грубой ошибкой и потенциальной уязвимостью. Именно поэтому для таких функций анализатор также выдает предупреждение V530.
Дополнительные возможности
Можно указать имена пользовательских функций, для которых следует выполнять проверку, используется ли возвращаемое функцией значение.
Для этого используется специальная пользовательская аннотация. Пример использования:
//+V530, function: MyNamespace::MyClass::MyFunc
namespace MyNamespace {
class MyClass {
int MyFunc();
}
....
obj.MyFunc(); // warning V530
}
Формат:
В проектах с особыми требованиями качества может понадобиться найти все функции, возвращаемое значение которых не используется. Для этого можно воспользоваться пользовательской аннотацией 'RET_USE_ALL'. Подробнее об этом можно прочитать в документации по пользовательским аннотациям.
Примечание. По умолчанию пользовательские аннотации не применяются к виртуальным функциям. О том, как включить данный функционал, вы можете прочитать здесь.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V530. |
Код, в котором значение, возвращаемое оператором sizeof(), умножается на другой оператор sizeof(), практически всегда свидетельствует о наличии ошибки. Бессмысленно умножать размер одного объекта на размер другого объекта. Чаще всего подобные ошибки встречаются при работе со строками.
Рассмотрим реальный пример кода:
TCHAR szTemp[256];
DWORD dwLen =
::LoadString(hInstDll, dwID, szTemp,
sizeof(szTemp) * sizeof(TCHAR));
Функция LoadString в качестве последнего аргумента принимает размер буфера в символах. В Unicode версии приложения мы сообщим функции, что размер буфера больше, чем он есть на самом деле. Это может привести к переполнению буфера. Заметим, что следующее исправление вовсе не является корректным:
TCHAR szTemp[256];
DWORD dwLen =
::LoadString(hInstDll, dwID, szTemp, sizeof(szTemp));
Приведем на эту темы выдержку из MSDN:
"Using this function incorrectly can compromise the security of your application. Incorrect use includes specifying the wrong size in the nBufferMax parameter. For example, if lpBuffer points to a buffer szBuffer which is declared as TCHAR szBuffer[100], then sizeof(szBuffer) gives the size of the buffer in bytes, which could lead to a buffer overflow for the Unicode version of the function. Buffer overflow situations are the cause of many security problems in applications. In this case, using sizeof(szBuffer)/sizeof(TCHAR) or sizeof(szBuffer)/sizeof(szBuffer[0]) would give the proper size of the buffer."
Корректный вариант кода:
TCHAR szTemp[256];
DWORD dwLen =
::LoadString(hInstDll, dwID, szTemp,
sizeof(szTemp) / sizeof(TCHAR));
Другой корректный вариант:
const size_t BUF_LEN = 256;
TCHAR szTemp[BUF_LEN];
DWORD dwLen =
::LoadString(hInstDll, dwID, szTemp, BUF_LEN);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V531. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в коде присутствует разыменование указателя, но значение, на которое указывает указатель, никак не используется.
Рассмотрим пример:
int *p;
...
*p++;
Выражение "*p++" выполняет следующие действия. Указатель "p" будет увеличен на единицу, но прежде этого из памяти будет извлечено значение типа "int". Это значение никак не используется, что странно. Получается, что операция разыменования "*" является лишней. Возможны следующие варианты, как следует поступить с кодом:
1) Удалить лишнее разыменование. Высказывание "*p++;" эквивалентно "p++;":
int *p;
...
p++;
2) На самом деле хотели увеличить не указатель, а значение. Тогда следует написать:
int *p;
...
(*p)++;
Если результат выражения "*p++" используется, то анализатор считает код корректным. Пример безопасного кода:
while(*src)
*dest++ = *src++;
Рассмотрим пример взятый из реального приложения:
STDMETHODIMP CCustomAutoComplete::Next(
ULONG celt, LPOLESTR *rgelt, ULONG *pceltFetched)
{
...
if (pceltFetched != NULL)
*pceltFetched++;
...
В данном случае забыты круглые скобки. Корректный вариант:
if (pceltFetched != NULL)
(*pceltFetched)++;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V532. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' увеличивается переменная, относящаяся к внешнему циклу.
В самом простом виде эта ошибка выглядит следующим образом:
for (size_t i = 0; i != 5; i++)
for (size_t j = 0; j != 5; i++)
A[i][j] = 0;
Во внутреннем цикле происходит увеличение переменной 'i' вместо 'j'. В реальном приложении подобная ошибка может быть не так хорошо заметна. Корректный вариант кода:
for (size_t i = 0; i != 5; i++)
for (size_t j = 0; j != 5; j++)
A[i][j] = 0;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V533. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' увеличивается переменная, относящаяся к внешнему циклу.
В самом простом виде эта ошибка выглядит следующим образом:
for (size_t i = 0; i != 5; i++)
for (size_t j = 0; j != 5; i++)
A[i][j] = 0;
Во внутреннем цикле происходит увеличение переменной 'i' вместо 'j'. В реальном приложении подобная ошибка может быть не так хорошо заметна. Корректный вариант кода:
for (size_t i = 0; i != 5; i++)
for (size_t j = 0; j != 5; j++)
A[i][j] = 0;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V533. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' в условии используется переменная, относящаяся к внешнему циклу.
В самом простом виде эта ошибка выглядит следующим образом:
for (size_t i = 0; i != 5; i++)
for (size_t j = 0; i != 5; j++)
A[i][j] = 0;
Во внутреннем цикле происходит сравнение 'i != 5' вместо 'j != 5'. В реальном приложении подобная ошибка может быть не так хорошо заметна. Корректный вариант кода:
for (size_t i = 0; i != 5; i++)
for (size_t j = 0; j != 5; j++)
A[i][j] = 0;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V534. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что вложенный цикл организован с использованием переменной, которая также используется и во внешнем цикле.
Схематически эта ошибка выглядит следующим образом:
size_t i, j;
for (i = 0; i != 5; i++)
for (i = 0; i != 5; i++)
A[i][j] = 0;
Конечно, это искусственный пример и в реальном приложении ошибка может быть не так очевидна. Корректный вариант кода:
size_t i, j;
for (i = 0; i != 5; i++)
for (j = 0; j != 5; j++)
A[i][j] = 0;
Использование одной переменной для внешнего и внутреннего цикла не всегда является ошибкой. Рассмотрим пример корректного кода, где анализатор не будет выдавать предупреждение:
for(c = lb; c <= ub; c++)
{
if (!(xlb <= xlat(c) && xlat(c) <= ub))
{
Range * r = new Range(xlb, xlb + 1);
for (c = lb + 1; c <= ub; c++)
r = doUnion(
r, new Range(xlat(c), xlat(c) + 1));
return r;
}
}
В этом коде внутренний цикл "for (c = lb + 1; c <= ub; c++)" организован при помощи переменной "c". Внешний цикл также использует переменную "c". Но ошибки здесь нет. После того, как выполнится внутренний цикл, сразу произойдет выход из функции при помощи оператора "return r".
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V535. |
Использование констант в восьмеричной системе счисления само по себе не является ошибкой. Эта система удобна при работе с битами и используется в коде, взаимодействующем с сетью или внешними устройствами. Однако средний программист редко использует эту систему счисления и поэтому может написать перед числом 0, забыв, что это превращает значение в восьмеричное.
Анализатор предупреждает о наличии восьмеричных констант, если рядом нет других восьмеричных констант. Такие "одиночные" восьмеричные константы часто являются ошибкой.
Рассмотрим пример, взятый из реального приложения. Пример достаточно большой, но хорошо демонстрирует суть проблемы.
inline
void elxLuminocity(const PixelRGBf& iPixel,
LuminanceCell< PixelRGBf >& oCell)
{
oCell._luminance = 0.2220f*iPixel._red +
0.7067f*iPixel._blue +
0.0713f*iPixel._green;
oCell._pixel = iPixel;
}
inline
void elxLuminocity(const PixelRGBi& iPixel,
LuminanceCell< PixelRGBi >& oCell)
{
oCell._luminance = 2220*iPixel._red +
7067*iPixel._blue +
0713*iPixel._green;
oCell._pixel = iPixel;
}
Рассматривая подобный код непросто заметить ошибку, но она есть. Первая функция elxLuminocity корректна и работает со значения типа float. В коде имеются следующие константы: 0.2220f, 0.7067f, 0.0713f. Вторая функция аналогична первой, но работает с целыми значениями. Все целые значения умножены на 10000. Вот эти значения: 2220, 7067, 0713. Ошибка в том, что последняя константа "0713" задана в восьмеричной системе счисления и имеет значение вовсе не 713, а 459. Исправленный вариант кода:
oCell._luminance = 2220*iPixel._red +
7067*iPixel._blue +
713*iPixel._green;
Как было сказано выше, предупреждение о восьмеричных константах выдается только в том случае, если вблизи от них нет других восьмеричных констант. Поэтому следующий пример кода считается анализатором безопасным и для него предупреждений выдано не будет:
static unsigned short bytebit[8] = {
01, 02, 04, 010, 020, 040, 0100, 0200 };
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V536. |
Анализатор обнаружил потенциально возможную опечатку в коде, связанную с неверным использованием схожих имен.
Данное правило пытается эвристическим методом обнаружить ошибку схожую со следующей:
int x = static_cast<int>(GetX()) * n;
int y = static_cast<int>(GetX()) * n;
Во второй строке вместо функции 'GetY' используется 'GetX'. Корректный код:
int x = static_cast<int>(GetX()) * n;
int y = static_cast<int>(GetY()) * n;
Для обнаружения этого подозрительного места анализатор следовал следующей логике. Мы имеем строку, где используется имя, включающее в себя фрагмент 'x'. Рядом с ней есть строка, где используется похожее имя, содержащая 'y'. Но при этом во второй строке также есть и 'X'. Так как выполнилось это и еще некоторые условия, данную конструкцию следует считать нуждающейся в проверке программистом. Если бы, например, слева не было переменных 'x' и 'y', то такой код не считался бы опасным. Пример кода, на который анализатор не обратит внимания:
array[0] = GetX() / 2;
array[1] = GetX() / 2;
К сожалению, данное правило часто дает ложное срабатывание, так как анализатор не имеет представления об устройстве программы и предназначении кода. Пример ложного срабатывания:
halfWidth -= borderWidth + 2;
halfHeight -= borderWidth + 2;
Анализатор предположил, что возможно второй строкой должно быть иное выражение, например: 'halfHeight -= borderHeight + 2'. На самом деле никакой ошибки нет. Размер границы ('border') одинаков по вертикали и горизонтали. Такой константы как 'borderHeight' просто не существует. Однако подобные высокоуровневые абстракции не доступны анализатору. Чтобы убрать предупреждение вы можете вписать в код комментарий '//-V537'.
Можно использовать и другой приём для предотвращения ложных срабатываний. Возьмём вот такой фрагмент кода:
bsdf->alpha_x = closure->alpha_x;
bsdf->alpha_y = bsdf->alpha_x;
Этот код корректен, но выглядит подозрительно, причём не только с точки зрения анализатора. Человеку, который будет сопровождать код, будет сложно понять, ошибочен он или нет. Если переменным 'alpha_x' и 'alpha_y' хочется присвоить одно и тоже значение, то можно написать так:
bsdf->alpha_y = bsdf->alpha_x = closure->alpha_x;
Этот код не вызовет вопроса у человека, а анализатор не выдаст предупреждение.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V537. |
В тексте программы встретились управляющие ASCII символы.
Таким символом может быть:
0x0B - LINE TABULATION (vertical tabulation) - Перемещает позицию печати к следующей позиции вертикальной табуляции. На терминалах этот символ обычно эквивалентен переводу строки.
Наличие подобных символов в тексте программы допустимо и такой текст успешно компилируется в Visual C++. Однако, скорее всего эти символы попали в текст программы случайно и лучше от них избавиться. Для этого есть 2 причины:
1) Если подобный управляющий символ находится в первых строках файла, то среда Visual Studio не может понять формат файла и открывает его не с помощью встроенного редактора, а в программе Notepad.
2) Некоторые внешние инструменты, работающие с текстами программ, могут некорректно обработать файлы, содержащие перечисленные управляющие символы.
Символы 0x0B не видны в редакторе Visual Studio 2010. Чтобы найти их в строке и удалить, можно открыть файл в программе Notepad или в другом редакторе, отображающим подобные управляющие символы.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V538. |
Анализатор обнаружил код, выполняющий действия над контейнерами, который с большой вероятностью содержит ошибку. Рекомендуется внимательно проверить данный фрагмент кода.
Рассмотрим несколько примеров, демонстрирующих ситуации, когда будет выдано данное предупреждение:
Пример 1.
void X(std::vector<int> &X, std::vector<int> &Y)
{
std::for_each (X.begin(), X.end(), SetValue);
std::for_each (Y.begin(), X.end(), SetValue);
}
В функции происходит заполнение двух массивов некими значениями. Из-за опечатки при втором вызове функции "std::for_each" ей передаются итераторы от различных контейнеров, что приведет к ошибке на этапе исполнения программы. Корректный код:
std::for_each (X.begin(), X.end(), SetValue);
std::for_each (Y.begin(), Y.end(), SetValue);
Пример 2.
std::includes(a.begin(), a.end(), a.begin(), a.end());
Данный код странен, и скорее всего планировалось обрабатывать две различные последовательности, а не одну. Корректный вариант кода:
std::includes(a.begin(), a.end(), b.begin(), b.end());
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V539. |
В Windows API есть структуры, в которых указатели на строки должны заканчиваться двойным нулем.
В качестве примера можно привести член lpstrFilter в структуре OPENFILENAME.
Описание lpstrFilter в MSDN:
"LPCTSTR
A buffer containing pairs of null-terminated filter strings. The last string in the buffer must be terminated by two NULL characters."
Как следует из этого описания, в конце строки мы должны обязательно написать дополнительный ноль. Пример: lpstrFilter = "All Files\0*.*\0";
Однако многие забывают о дополнительном нуле в конце строки. Пример некорректного кода обнаруженный в одном из приложений:
lofn.lpstrFilter = L"Equalizer Preset (*.feq)\0*.feq";
Подобный код приведет к тому, что в диалоге работы с файлом в поле фильтров мы можем увидеть мусор. Исправленный код:
lofn.lpstrFilter = L"Equalizer Preset (*.feq)\0*.feq\0";
Мы явно в конце строки написали 0, и еще один ноль добавит компилятор. Некоторые для большей наглядности пишут так:
lofn.lpstrFilter = L"Equalizer Preset (*.feq)\0*.feq\0\0";
Но здесь мы получим в конце не два, а три нуля. Это излишне, но зато хорошо заметно программисту.
Есть и другие структуры, помимо OPENFILENAME, в которых можно допустить схожие ошибки. Например, двойным нулем должны заканчиваться строки lpstrGroupNames, lpstrCardNames в структурах OPENCARD_SEARCH_CRITERIA, OPENCARDNAME.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V540. |
Анализатор обнаружил потенциально возможную ошибку, связанную с печатью строки в саму себя. Результат подобного действия может быть весьма неожиданным.
Рассмотрим пример:
char s[100] = "test";
sprintf(s, "N = %d, S = %s", 123, s);
В примере буфер 's' одновременно используется как буфер под новую строку и как один из элементов, из которых формируется текст. В результате работы этого кода хочется получить строку:
N = 123, S = test
Но на практике в буфере будет сформирована строка:
N = 123, S = N = 123, S =
В других ситуациях аналогичный код может привести не только к выводу некорректного текста, но и к переполнению буфера или аварийному завершению программы. Код может быть исправлен, если использовать для сохранения результата новый буфер.
Корректный код:
char s1[100] = "test";
char s2[100];
sprintf(s2, "N = %d, S = %s", 123, s1);
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования форматной строки, Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V541. |
Анализатор обнаружил очень подозрительное явное приведение типов. Это приведение типов может свидетельствовать о наличии ошибки. Рекомендуется проверить данный фрагмент кода.
Пример:
typedef unsigned char Byte;
void Process(wchar_t ch);
void Process(wchar_t *str);
void Foo(Byte *buf, size_t nCount)
{
for (size_t i = 0; i < nCount; ++i)
{
Process((wchar_t *)buf[i]);
}
}
Мы имеем функцию Process, которая умеет обрабатывать как отдельные символы, так и строки. Также мы имеем функцию 'Foo', которая на вход получает указатель на буфер. Этот буфер обрабатывается как массив символов типа wchar_t. Но код содержит ошибку и анализатор предупреждает о том, что тип 'char' явно приводится к типу ' wchar_t *'. Причина в том, что выражение "(wchar_t *)buf[i]" эквивалентно "(wchar_t *)(buf[i])". В начале из массива извлекается значение типа 'char', а затем оно превращается в указатель. Исправленный вариант кода:
Process(((wchar_t *)buf)[i]);
Однако не всегда, странные приведения типов являются ошибкой. Рассмотрим пример безопасного кода, взятого из реального приложения:
wchar_t *destStr = new wchar_t[len+1];
...
for (int j = 0 ; j < nbChar ; j++)
{
if (Case == UPPERCASE)
destStr[j] =
(wchar_t)::CharUpperW((LPWSTR)destStr[j]);
...
Здесь присутствует явное приведение типа 'wchar_t ' к 'LPWSTR' и обратно от типа 'LPWSTR' к 'wchar_t '. Дело в том, что Windows API функция CharUpperW может работать с входным значением, и как с указателем, и как с символом. Прототип функции:
LPTSTR WINAPI CharUpperW(__inout LPWSTR lpsz);
Если старшая часть указателя равна 0, то входное значение считается символом. В противном случае функция обрабатывает строку.
Анализатор знает про поведение функции CharUpperW и считает данный код безопасным. Однако в другой подобной ситуации анализатор может выдать ложное срабатывание.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V542. |
Анализатор обнаружил потенциальную ошибку при работе с переменной типа HRESULT.
HRESULT — это 32-разрядное значение, разделенное на три различных поля: код серьезности ошибки, код устройства и код ошибки. Для работы со значением HRESULT служат специальные константы, такие как S_OK, E_FAIL, E_ABORT и так далее. А для проверки значений тип HRESULT предназначены такие макросы как SUCCEEDED, FAILED.
Предупреждение V543 выдается в том случае, если в переменную типа HRESULT пытаются записать значение -1, true или false. Рассмотрим пример:
HRESULT h;
...
if (bExceptionCatched)
{
ShowPluginErrorMessage(pi, errorText);
h = -1;
}
Запись значения "-1" некорректна. Если хочется сообщить о какой-то непонятной ошибке, то следует использовать значение 0x80004005L (Unspecified failure). Эта и аналогичные константы, описаны в "WinError.h". Корректный код:
if (bExceptionCatched)
{
ShowPluginErrorMessage(pi, errorText);
h = E_FAIL;
}
Дополнительные ресурсы:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V543. |
Анализатор обнаружил потенциальную ошибку при работе с переменной типа HRESULT.
HRESULT- это 32-разрядное значение, разделенное на три различных поля: код серьезности ошибки, код устройства и код ошибки. Для работы со значением HRESULT служат специальные константы, такие как S_OK, E_FAIL, E_ABORT и так далее. А для проверки значений тип HRESULT предназначены такие макросы как SUCCEEDED, FAILED.
Предупреждение V544 выдается в том случае, если переменную типа HRESULT пытаются сравнить с -1, true или false. Рассмотрим пример:
HRESULT hr;
...
if (hr == -1)
{
}
Сравнение со значением "-1" некорректно. Коды ошибок могут быть различны. Например, это может быть 0x80000002L (Ran out of memory), 0x80004005L (unspecified failure), 0x80070005L (General access denied error) и так далее. Для проверки значения HRESULT в данном случае необходимо использовать макрос FAILED, объявленный в "WinError.h". Корректный вариант кода:
if (FAILED(hr))
{
}
Дополнительные ресурсы:
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку при работе с переменной типа HRESULT.
HRESULT - это 32-разрядное значение, разделенное на три различных поля: код серьезности ошибки, код устройства и код ошибки. Для работы со значением HRESULT служат специальные константы, такие как S_OK, E_FAIL, E_ABORT и так далее. А для проверки значений тип HRESULT предназначены такие макросы как SUCCEEDED, FAILED.
Предупреждение V545 выдается в том случае, если переменная типа HRESULT используется в операторе 'if' как переменная типа bool. Пример:
HRESULT hr;
...
if (hr)
{
}
HRESULT и тип bool это совершенно разные по смыслу типы. Показанный пример сравнения некорректен. Тип HRESULT имеет множество состояний. Это может быть 0L (S_OK), 0x80000002L (Ran out of memory), 0x80004005L (unspecified failure), и так далее. Обратите внимание, что состояние S_OK кодируется как 0.
Для проверки значения HRESULT необходимо использовать макрос SUCCEEDED или FAILED, объявленные в "WinError.h". Корректные варианты кода:
if (FAILED(hr))
{
}
if (SUCCEEDED(hr))
{
}
Дополнительные ресурсы:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V545. |
Анализатор обнаружил опечатку, когда член класса инициализируется самим собой.
Рассмотрим пример конструктора:
C95(int field) : Field(Field)
{
...
}
Здесь имя параметра и название члена класса отличается только одной буквой. Из-за этого допущена опечатка и член 'Field' останется неинициализированным. Исправленный вариант кода:
C95(int field) : Field(field)
{
...
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V546. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что условие всегда истинно или ложно. Подобные условия не всегда означают наличие ошибки, но эти фрагменты кода следует обязательно проверить.
Пример кода:
LRESULT CALLBACK GridProc(HWND hWnd,
UINT message, WPARAM wParam, LPARAM lParam)
{
...
if (wParam<0)
{
BGHS[SelfIndex].rows = 0;
}
else
{
BGHS[SelfIndex].rows = MAX_ROWS;
}
...
}
Здесь ветка "BGHS[SelfIndex].rows = 0;" никогда не будет выполнена. Дело в том, что переменная wParam имеет беззнаковый тип WPARAM, который объявлен как "typedef UINT_PTR WPARAM".
Этот код или содержит логическую ошибку, или может быть сокращен до одной строки: "BGHS[SelfIndex].rows = MAX_ROWS;".
Теперь рассмотрим пример кода, который не является ошибочным, но он потенциально опасен и имеет бессмысленное сравнение:
unsigned int a = _ttoi(LPCTSTR(str1));
if((0 > a) || (a > 255))
{
return(FALSE);
}
Программист хотел реализовать следующий алгоритм.
1) Превратить строку в число.
2) Если число лежит вне диапазона [0..255] то вернуть статус ошибки (return FALSE).
Ошибка заключается в использовании типа 'unsigned'. Если функция _ttoi вернет отрицательное значение, то оно превратится в большое положительное значение. Например, значение "-3" превратится в 4294967293. Сравнение '0 > a' всегда вернёт false. Программа корректно работает из-за того, что диапазон значений [0..255] проверяется условием 'a > 255'.
Данный фрагмент кода будет диагностирован так: "V547 Expression '0 > a' is always false. Unsigned type value is never < 0."
Этот фрагмент лучше исправить следующим образом:
int a = _ttoi(LPCTSTR(str1));
if((0 > a) || (a > 255))
{
return(FALSE);
}
Рассмотрим один специальный случай. Анализатор выдает предупреждение:
V547 Expression 's == "Abcd"' is always false. To compare strings you should use strcmp() function.
на следующий код:
const char *s = "Abcd";
void Test()
{
if (s == "Abcd")
cout << "TRUE" << endl;
else
cout << "FALSE" << endl;
}
Однако это не совсем верно. Этот код может все-таки распечатать "TRUE", если переменная 's' и функция Test() объявлены в одном модуле. Компилятор не плодит множество одинаковых константных строк, а использует одну. В результате, иногда кажется, что код вполне работоспособен. Однако надо понимать, что это очень плохой код и следует использовать специальные функции для сравнения.
Следующий пример:
if (lpszHelpFile != 0)
{
pwzHelpFile = ((_lpa_ex = lpszHelpFile) == 0) ?
0 : Foo(lpszHelpFile);
...
}
Этот код работает вполне корректно, но он излишне запутан. Условие "((_lpa_ex = lpszHelpFile) == 0)" всегда ложно, так как указатель lpszHelpFile всегда не равен нулю. Этот код сложен для чтения, и его лучше переписать.
Упрощенный вариант кода:
if (lpszHelpFile != 0)
{
_lpa_ex = lpszHelpFile;
pwzHelpFile = Foo(lpszHelpFile);
...
}
Следующий пример:
SOCKET csd;
csd = accept(nsd, (struct sockaddr *) &sa_client, &clen);
if (csd < 0)
....
Функция accept в заголовочных файлах Visual Studio возвращает значение, имеющее беззнаковый тип SOCKET. Поэтому проверка 'csd < 0' недопустима, ведь её результат всегда ложь (false). Возвращенные значения надо явно сравнивать с различными константами, например, с SOCKET_ERROR:
if (csd == SOCKET_ERROR)
Анализатор предупреждает далеко не про все условия, которые всегда ложны или истинны. Он диагностирует только те ситуации, где высока вероятность наличия ошибки. Рассмотрим некоторые примеры, которые анализатор считает абсолютно корректными:
// 1) Вечный цикл
while (true)
{
...
}
// 2) Развернутый в Release версии макрос
// MY_DEBUG_LOG("X=", x);
0 && ("X=", x);
// 3) assert(false);
if (error) {
assert(false);
return -1;
}
Примечание. Время от времени в поддержку приходят однотипные письма, в которых люди пишут, что не понимают предупреждение V547. Попробуем внести ясность. Рассмотрим обобщенный текст письма, которое присылают люди: "Анализатор выдаёт предупреждение "Expression 'i == 1' is always true". Но ведь это не так. Переменная может быть равна не только единице, но и нулю. Наверное, надо исправить поведение анализатора."
for (int i = 0; i <= 1; i++)
{
if(i == 0)
A();
else if(i == 1) // V547
B();
}
Даём пояснение. Сообщение анализатора не говорит, что переменная 'i' всегда равна 1. Анализатор говорит, что 'i' равно 1 в конкретном месте, и указывает на эту строчку в программе.
Когда выполняется проверка 'if (i == 1)' точно известно, что переменная 'i' будет равна 1. Других вариантов нет. Конечно, такой код не обязательно содержит ошибку. Однако, это то место программы, которое стоит внимательно проверить.
Как видите, анализатор выдаёт здесь предупреждение совершенно правомерно. Если вы столкнулись с подобным видом предупреждения, есть следующие варианты как с ним поступить:
Упрощенный код:
for (int i = 0; i <= 1; i++)
{
if(i == 0)
A();
else
B();
}
Если это избыточная проверка, но не хочется изменять код, то вы можете воспользоваться одним из методов подавления ложных срабатываний.
Рассмотрим еще один пример, в этот раз связанный с перечисляемыми типами.
enum state_t { STATE_A = 0, STATE_B = 1 }
state_t GetState()
{
if (someFailure)
return (state_t)-1;
return STATE_A;
}
state_t state = GetState();
if (state == STATE_A) // <= V547
Автор хотел вернуть значение -1, если что-то пошло не так в процессе выполнения функции 'GetState'.
Анализатор выдаёт здесь предупреждение "V547 Expression 'state == STATE_A' is always true". На первый взгляд может показаться, что это ложное срабатывание, ведь мы не можем заранее предсказать, что нам вернёт функция. На самом деле, так сказывается на работе анализатора наличие в коде неопределённого поведения.
В 'state_t' не определена именованная константа со значением -1, и на самом деле 'return (state_t)-1' может вернуть из функции всё что угодно, так как здесь возникает неопределённое поведение. Кстати, анализатор предупредит о возникновении неопределённого поведения с помощью предупреждения "V1016 The value '-1' is out of range of enum values. This causes unspecified or undefined behavior', выданного на строку 'return (state_t)-1".
Итак, поскольку 'return (state_t)-1;' это undefined behavior, то анализатор не учитывает -1 как возможное значение, возвращаемое функцией. С точки зрения анализатора функция 'GetState' возвращает только значение 'STATE_A'. Это и является причиной выдачи сообщения V547.
Чтобы исправить ситуацию, следует добавить в перечисление константу, символизирующую ошибочный результат:
enum state_t { STATE_ERROR = -1, STATE_A = 0, STATE_B = 1 }
state_t GetState()
{
if (someFailure)
return STATE_ERROR;
return STATE_A;
}
Теперь одновременно исчезнет предупреждение V547 и V1016.
Дополнительные ссылки:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V547. |
Анализатор обнаружил потенциально возможную ошибку, связанную с явным приведением типа. В программе массив, объявленный условно как "type Array[3][4]", приводится к типу "type **". Это приведение типа, скорее всего, не имеет смысла.
Типы "type[a][b]" и "type **" представляют собой разные структуры данных. Type[a][b] это единый участок памяти с которым можно работать как с двумерным массивом. Type ** - это массив указателей на какие-то участки памяти.
Пример:
void Foo(char **names, size_t count)
{
for(size_t i=0; i<count; i++)
printf("%s\n", names[i]);
}
void Foo2()
{
char names[32][32];
...
Foo((char **)names, 32); //Crash
}
Исправленный вариант:
void Foo2()
{
char names[32][32];
...
char *names_p[32];
for(size_t i=0; i<32; i++)
names_p[i] = names[i];
Foo(names_p, 32); //OK
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V548. |
Анализатор обнаружил потенциальную ошибку в программе, связанную с тем, что совпадают два фактических аргумента функции. Передача одного и того же значения в качестве двух аргументов для многих функций является нормальной ситуацией. Но если речь идет о таких функциях как memmove, memcpy, strstr, strncmp, то это очень подозрительная ситуация.
Пример из реального приложения:
#define str_cmp(s1, s2) wcscmp(s1, s2)
...
v = abs(str_cmp(a->tdata, a->tdata));
Из-за опечатки функция wcscmp сравнивает строку саму с собой. Корректным вариантом кода должно было быть:
v = abs(str_cmp(a->tdata, b->tdata));
Анализатор выдаст предупреждение в случае работы с функциями: memcpy, memmove, memcmp, _memicmp, strstr, strspn, strtok, strcmp, strncmp, wcscmp, _stricmp, wcsncmp и так далее. Если вы обнаружили подобную ошибку, которая не диагностируется анализатором, то просим сообщить нам имя функции, которая также не должна принимать в качестве первого и второго аргумента одинаковые значения.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V549. |
Анализатор обнаружил потенциальную ошибку связанную с тем, что для сравнения чисел с плавающей точкой используется оператор == или !=. Нередко точное сравнение может быть источником ошибки.
Рассмотрим пример:
double a = 0.5;
if (a == 0.5) //OK
x++;
double b = sin(M_PI / 6.0);
if (b == 0.5) //ERROR
x++;
Первое сравнение 'a == 0.5' истинно. Второе сравнение 'b == 0.5' может быть как истинно, так и ложно. Результат выражения 'b == 0.5' зависит от используемого процессора, версии и настроек компилятора. Например, значение переменной 'b' было равно 0.49999999999999994 при использовании компилятора Visual C++ 2010. Более корректно этот код можно написать следующим образом:
double b = sin(M_PI / 6.0);
if (fabs(b - 0.5) < DBL_EPSILON)
x++;
В данном случае сравнение с погрешностью DBL_EPSILON верно, так как результат функции sin() лежит в диапазоне [-1, 1]. Однако, если мы работаем со значениями больше нескольких единиц, то такие погрешности как FLT_EPSILON, DBL_EPSILON будут слишком малы. И наоборот при работе со значениями типа 0.00001 эти погрешности слишком велики. Каждый раз следует выбирать погрешность адекватную диапазону возможных значений.
Возникает вопрос. Как же все-таки сравнить две переменных типа double?
double a = ...;
double b = ...;
if (a == b) // how?
{
}
Одного единственно-правильного ответа нет. В большинстве случаев можно сравнит две переменных типа double, написав код следующего вида:
if (fabs(a-b) <= DBL_EPSILON * fmax(fabs(a), fabs(b)))
{
}
Только осторожней с этой формулой, она работает только для чисел с одинаковым знаком. Также в ряде с большим количеством вычислений постоянно набегает ошибка, и константа 'DBL_EPSILON' может оказаться слишком маленьким значением.
А можно ли все-таки точно сравнивать значения в формате с плавающей точкой?
В некоторых случаях да. Но эти ситуации весьма ограничены. Сравнивать можно в том случае, если это и есть, по сути, одно и то же значение.
Пример, где допустимо точное сравнение:
// -1 - признак, что значение переменной не было установлено
float val = -1.0f;
if (Foo1())
val = 123.0f;
if (val == -1.0f) //OK
{
}
В данном случае сравнение со значением "-1" допустимо, так как именно точно таким же значением мы инициализировали переменную ранее.
В рамках документации невозможно более подробно раскрыть тему сравнения float/double типов, поэтому мы отсылаем вас к внешнем источникам информации приведенных в конце.
Анализатор может только указать на потенциально опасные участки кода, где сравнение может не дать желаемого результата. Действительно ли код содержит ошибку, может понять только программист. Также мы не можем заранее дать в документации точные рекомендации, так как задачи, в которых используются типы с плавающей точкой слишком различны.
Диагностическое сообщение не выдается, если сравниваются два идентичных выражения типа float или double. Такое сравнение позволяет определить, является ли значение NaN. Пример кода, реализующего подобную проверку:
bool isnan(double X) { return X != X; }
Впрочем, такой код нельзя назвать хорошим и лучше использовать стандартную функцию 'std::isnan'.
Дополнительные ресурсы:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V550. |
Анализатор обнаружил потенциальную ошибку связанную с тем, что одна из ветвей оператора switch() никогда не получит управление. Причина этого в том, что аргумент оператора switch() не может принять значение, которое прописано в операторе case.
Рассмотрим пример:
char ch = strText[i];
switch (ch)
{
case '<':
strHTML += "<";
bLastCharSpace = FALSE;
nNonbreakChars++;
break;
case '>':
strHTML += ">";
bLastCharSpace = FALSE;
nNonbreakChars++;
break;
case 0xB7:
case 0xBB:
strHTML += ch;
strHTML += "<wbr>";
bLastCharSpace = FALSE;
nNonbreakChars = 0;
break;
...
}
Здесь ветка расположенная после "case 0xB7:" и "case 0xBB:" никогда не получит управление. Переменная 'ch' имеет тип 'char', а, следовательно, диапазон её значений лежит в пределах [-128..127]. Результатом сравнения "ch == 0xB7" и "ch==0xBB" всегда будет ложь (false). Чтобы код был корректен переменная 'ch' должна иметь тип 'unsigned char'. Исправленный код:
unsigned char ch = strText[i];
switch (ch)
{
...
case 0xB7:
case 0xBB:
strHTML += ch;
strHTML += "<wbr>";
bLastCharSpace = FALSE;
nNonbreakChars = 0;
break;
...
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V551. |
Анализатор обнаружил потенциально опасную конструкция в коде, где происходит инкремент переменной типа bool.
Рассмотрим пример:
bool bValue = false;
...
bValue++;
Во-первых, стандарт языка Си++ говорит:
The use of an operand of type bool with the postfix ++ operator is deprecated.
Это значит, что подобную конструкцию лучше не использовать.
Во-вторых, лучше явно присвоить переменной значение типа true. Это более понятный код:
bValue = true;
В-третьих, возможно имеется опечатка и на самом деле хотелось увеличить другую переменную. Пример:
bool bValue = false;
int iValue = 1;
...
if (bValue)
bValue++;
Случайно использовали не ту переменную и на самом деле должно быть:
bool bValue = false;
int iValue = 1;
...
if (bValue)
iValue++;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V552. |
Анализатор обнаружил в программе объявление класса или тело функции, которые занимают более 2000 строк. Этот класс или функция не обязательно содержат ошибки, но вероятность этого очень высокая. Чем больше функция, тем легче допустить в ней ошибку и тем сложнее ее отлаживать. Чем больше класс, тем сложнее разобраться в его интерфейсах.
Это сообщение хороший повод постараться все-таки найти время для рефакторинга кода. Да, всегда нужно делать что-то срочное. Но чем больше по размеру будут функции и классы, тем больший процент времени будет уходить не на написание новой функциональности, а на поддержание старого кода и устранение в нем ошибок.
Дополнительные материалы по данной теме:
Анализатор обнаружил ситуацию, когда использование умного указателя может привести к неопределенному поведению, в частности, к повреждению кучи, аварийному завершению программы или неполному разрушению объектов. Ошибка заключается в том, что память выделяется и освобождается различными методиками.
Рассмотрим первый пример:
void Foo()
{
struct A
{
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
};
std::unique_ptr<A> p(new A[3]);
}
По умолчанию класс unique_ptr использует для освобождения памяти оператор 'delete'. Поэтому будет разрушен только один объект класса 'A' и на экране будет распечатан следующий текст:
A()
A()
A()
~A()
Чтобы исправить ошибку необходимо указать, что необходимо использовать оператор 'delete []'. Корректный вариант кода:
std::unique_ptr<A[]> p(new A[3]);
Теперь будет вызываться равное количество конструкторов и деструкторов и будет распечатан текст:
A()
A()
A()
~A()
~A()
~A()
Рассмотрим второй пример:
std::unique_ptr<int []> p((int *)malloc(sizeof(int) * 5));
Память выделяется с использованием функции 'malloc()', а освобождается при помощи оператора 'delete []'. Это некорректно и следует указать, что освобождать память надо используя функцию 'free()'. Корректный вариант кода может выглядеть следующим образом:
int *d =(int *)std::malloc(sizeof(int) * 5);
unique_ptr<int, void (*)(void*)> p(d, std::free);
Дополнительные материалы по данной теме:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V554. |
Анализатор обнаружил потенциальную ошибку в выражении вида "A - B > 0". Высока вероятность, что условие написано неверно, если подвыражение "A - B" имеет беззнаковый тип.
Условие "A - B > 0" выполняется во всех случаях, когда значение 'A' не равно значению 'B'. Это значит, что вместо выражения "A - B > 0" можно написать "A != B". Но скорее всего программист задумывал совсем другое.
Рассмотрим пример:
unsigned int *B;
...
if (B[i]-70 > 0)
Программист хотел проверить, что i-тый элемент массива B больше значения 70. Это можно было записать так: "B[i] > 70". Исходя из каких-то своих соображений, программист записал эту проверку так: "B[i]-70 > 0". И допустил ошибку. Он забыл, что элементы массива 'B' имеют тип 'unsigned'. Это значит, что и выражение "B[i]-70" будет иметь тип 'unsigned'. Получается, что условие всегда истинно, за исключением случая, когда элемент 'B[i]' будет равен 70.
Поясним эту ситуацию.
Если 'B[i]' больше 70, то "B[i]-70" будет больше 0.
Если 'B[i]' меньше 70, то произойдет переполнение типа unsigned и мы получим очень большое значение. Пусть B[i] == 50. Тогда "B[i]-70" = 50u - 70u = 0xFFFFFFECu = 4294967276. Естественно, что 4294967276 > 0.
Демонстрационный пример:
unsigned A;
A = 10; cout << "A=10 " << (A-70 > 0) << endl;
A = 70; cout << "A=70 " << (A-70 > 0) << endl;
A = 90; cout << "A=90 " << (A-70 > 0) << endl;
// Будет распечатано
A=10 1
A=70 0
A=90 1
Первый вариант исправленного кода:
unsigned int *B;
...
if (B[i] > 70)
Второй вариант исправленного кода:
int *B;
...
if (B[i]-70 > 0)
Отметим, что выражение вида "A - B > 0" далеко не всегда означает наличие ошибки. Рассмотрим код, где анализатор выдаст ложное предупреждение:
// Функции GetLength() и GetPosition() возвращают
// значение типа size_t.
while ((inStream.GetLength() - inStream.GetPosition()) > 0)
{ ... }
Здесь GetLength() всегда больше или равно GetPosition(). Поэтому код корректен. Чтобы избавиться от ложного срабатывания можно использовать комментарий //-V555 или переписать код следующим образом:
while (inStream.GetLength() != inStream.GetPosition())
{ ... }
Вот еще один случай, когда ошибки не возникнет.
__int64 A;
__uint32 B;
...
if (A - B > 0)
Здесь подвыражение "A - B" имеет знаковый тип __int64 и ошибки не возникнет. Анализатор не выдает предупреждения в таких ситуациях.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V555. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в коде сравниваются enum значения, имеющие разные типы.
Пример:
enum ErrorTypeA { E_OK, E_FAIL };
enum ErrorTypeB { E_ERROR, E_SUCCESS };
void Foo(ErrorTypeB status) {
if (status == E_OK)
{ ... }
}
В сравнении случайно использовано неверное имя, и логика работы программы будет нарушена. Корректный вариант:
void Foo(ErrorTypeB status) {
if (status == E_SUCCESS)
{ ... }
}
Сравнение значений различных типов перечислений (enum) не обязательно является ошибкой. Но подобный код нуждается в проверке (code review).
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V556. |
Анализатор обнаружил потенциально возможный доступ к памяти за границами массива. Самым распространенным случаем является ошибка при записи символа '\0' после последнего элемента массива.
Рассмотрим соответствующий пример:
struct IT_SAMPLE
{
unsigned char filename[14];
...
};
static int it_riff_dsmf_process_sample(
IT_SAMPLE * sample, const unsigned char * data)
{
memcpy( sample->filename, data, 13 );
sample->filename[ 14 ] = 0;
...
}
Последний элемент массива имеет индекс 13, а не 14. Поэтому корректный код должен иметь вид:
sample->filename[13] = 0;
Конечно, в подобных случаях лучше использовать не явное значение индекса, а выражение использующее оператор sizeof(). Однако следует помнить, что и в этом случае можно допустить ошибку. Рассмотрим пример:
typedef wchar_t letter;
letter name[30];
...
name[sizeof(name) - 1] = L'\0';
На первый взгляд выражение "sizeof(name) - 1" верно. Но здесь программист забыл, что он работает с типом 'wchar_t', а не 'char'. В результате символ '\0' будет записано далеко за пределами массива. Корректный вариант:
name[sizeof(name) / sizeof(*name) - 1] = L'\0';
Для упрощения записи таких конструкций можно использовать специальный макрос:
#define str_len(arg) ((sizeof(arg) / sizeof(arg[0])) - 1)
name[str_len(name)] = L'\0';
Анализатор выявляет некоторые ошибки, когда индексом является переменная, значение которой может выйти за пределами массива. Пример:
int buff[25];
for (int i=0; i <= 25; i++)
buff[i] = 10;
Корректный вариант:
int buff[25];
for (int i=0; i < 25; i++)
buff[i] = 10;
Следует учитывать, что при работе с подобными диапазонами значений анализатор может ошибаться и выдавать ложные срабатывания.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V557. |
Анализатор обнаружил ситуацию, когда функция возвращает указатель на локальный объект. Этот объект будет уничтожен при выходе из функции, и использовать указатель на него будет нельзя.
В самом простом виде данное диагностическое сообщение будет выдано на следующий код:
float *F()
{
float f = 1.0;
return &f;
}
Конечно, в таком виде ошибка вряд ли будет существовать. Рассмотрим более реальный пример кода.
int *Foo()
{
int A[10];
// ...
if (err)
return 0;
int *B = new int[10];
memcpy(B, A, sizeof(A));
return A;
}
Мы работали с временным массивом A. При некотором условии мы должны вернуть указатель на новый массив B. Однако из-за опечатки мы возвращаем массив A, что повлечет за собой непредсказуемое поведение программы или её аварийное завершение. Исправленный вариант кода:
int *Foo()
{
...
int *B = new int[10];
memcpy(B, A, sizeof(A));
return B;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V558. |
Анализатор обнаружил ситуацию, когда в условии оператора 'if' или 'while' присутствует оператор присваивания '='. Подобная конструкция часто свидетельствует о наличии ошибок. Высока вероятность, что вместо оператора '=' планировалось использовать оператор '=='.
Рассмотрим пример:
const int MAX_X = 100;
int x;
...
if (x = MAX_X)
{ ... }
В коде допущена опечатка: вместо сравнения переменной 'x' c константной MAX_X произойдет изменение значения переменной 'x'. Корректный вариант кода:
if (x == MAX_X)
{ ... }
Конечно, присваивание внутри условия часто ошибкой не является. Такой приём используется многими программистами для сокращения размера кода. Стоит отметить, что это плохая практика, так как работая с подобным кодом всегда подолгу приходится изучать его, чтобы понять: является ли он следствием опечатки или желанием сократить программу.
Мы предлагаем отказаться от присваивания внутри условия и использовать присваивание в качестве отдельной операции. Другой вариант - использовать дополнительные скобки вокруг присваивания:
while ((x = Foo()))
{
...
}
Подобный код анализатор и многие компиляторы воспринимают как безопасный. Заодно, это подсказка человеку, что код не содержит ошибку.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V559. |
Анализатор обнаружил потенциально возможную ошибку внутри логического условия. Часть логического выражения всегда истинно и оценено как опасное.
Рассмотрим пример:
#define REO_INPLACEACTIVE (0x02000000L)
...
if (reObj.dwFlags && REO_INPLACEACTIVE)
m_pRichEditOle->InPlaceDeactivate();
Программист хотел проверить состояние определенного бита в переменной dwFlags. Но из-за опечатки он написал оператор '&&', вместо оператора '&'. Корректный код:
if (reObj.dwFlags & REO_INPLACEACTIVE)
m_pRichEditOle->InPlaceDeactivate();
Рассмотрим другой пример:
if (a = 10 || a == 20)
Случайно вместо оператора сравнения '==' написан оператор присваивания '='. С точки зрения языка Си++, это выражение будет идентично выражению вида "if (a = (10 || a == 20))".
Выражение "10 || a == 20" анализатор считает опасным, так как левая его часть представляет собой константу. Корректный код:
if (a == 10 || a == 20)
Иногда предупреждение V560 выявляет не ошибку, а просто избыточный код. Рассмотрим пример:
if (!mainmenu) {
if (freeze || winfreeze ||
(mainmenu && gameon) ||
(!gameon && gamestarted))
drawmode = normalmode;
}
Анализатор предупредит, что в подвыражении (mainmenu && gameon) переменная mainmenu всегда равна 0. То, что переменная mainmenu равна нулю, следует из вышестоящей проверки " if (!mainmenu)". Этот код может быть вполне корректен. Однако он избыточен, и лучше его упростить. Это сделает программу более простой для понимания другими разработчиками.
Упрощенный вариант кода:
if (!mainmenu) {
if (freeze || winfreeze ||
(!gameon && gamestarted))
drawmode = normalmode;
}
Рассмотрим более интересный случай.
int16u Integer = ReadInt16u(Liste);
int32u Exponent=(Integer>>10) & 0xFF;
if (Exponent==0 || Exponent==0xFF) // V560
return 0;
Пользователь, приславший этот пример, был озадачен, почему анализатор выдаёт предупреждение, в котором утверждается, что подвыражение 'Exponent==0xFF' всегда ложное. Давайте разберемся. Для этого нам надо внимательно посчитать.
16-битная беззнаковая переменная 'Integer' имеет диапазон возможных значений [0..0b1111111111111111] или [0..0xFFFF].
При сдвиге вправо на 10 бит, диапазон возможных значений уменьшается: [0..0b111111] или [0..0x3F].
Далее выполняется операция '& 0xFF'.
В результате, никак невозможно получить значение '0xFF'. Максимум, это будет '0x3F'.
Ряд конструкций на языке Си++ анализатор считает безопасными, даже если в них часть выражения представляется константой. Примеры некоторых ситуаций, когда анализатор не считает код опасными:
Особые настройки диагностики V560
По дополнительной просьбе пользователей появилась возможность управлять поведением диагностики V560. В общем заголовочном файле или pvsconfig-файле можно написать комментарий специального вида:
//+V560 ENABLE_PEDANTIC_WARNINGS
Режим 'ENABLE_PEDANTIC_WARNINGS' ослабляет исключения диагностики. Рассмотрим пример:
void foo()
{
bool debugCheck = false; // maybe in macros
if (x)
{
if (debugCheck)
{
....
}
}
}
По умолчанию анализатор не будет считать такой код опасным, так как часто он пишется для отладки. Комментарий позволит ослабить исключение диагностики, чтобы анализатор мог сообщить о проблеме.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V560. |
Анализатор обнаружил потенциально возможную ошибку, найдя в коде переменную, которая объявляется и инициализируется, но далее не используется. При этом во внешней области видимости имеется переменная, имеющая такое же имя и такой же тип. Высока вероятность, что следовало использовать уже имеющуюся переменную, а не объявлять новую.
Рассмотрим пример:
BOOL ret = TRUE;
if (m_hbitmap)
BOOL ret = picture.SaveToFile(fptr);
Программист случайно объявил новую переменную 'ret', в результате чего предыдущая всегда будет иметь значение TRUE, вне зависимости от того успешно будет сохранена картина в файл или нет. Исправленный вариант кода:
BOOL ret = TRUE;
if (m_hbitmap)
ret = picture.SaveToFile(fptr);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V561. |
Анализатор обнаружил ситуацию, когда значение типа bool сравнивается с числом. Скорее всего, это свидетельствует о наличии в коде ошибки.
Рассмотрим пример:
if (0 < A < 5)
Этим кодом программист, недостаточно хорошо знакомый с языком Си++, пытался определить, лежит значение в диапазоне от 0 до 5 или нет. На самом деле, вычисления будут происходить в следующей последовательности: ((0 < A) < 5). Результат выражения "0 < A" имеет тип bool, а значит всегда меньше 5.
Корректный вариант проверки:
if (0 < A && A < 5)
Предыдущий пример, напоминает ошибку, которые делают студенты. Однако от подобных ошибок не застрахованы и профессиональные разработчики.
Рассмотрим второй пример:
if (! (fp = fopen(filename, "wb")) == -1) {
perror("opening image file failed");
exit(1);
}
Здесь сразу 2 ошибки разного плана. Во-первых, функция "fopen" возвращает указатель, и сравнивать возвращаемое значение следует с NULL. Программист спутал функцию "fopen" с "open", которая как раз возвращает "-1" в случае ошибки. Второй недочет в коде связан с тем, что вначале выполняется операция отрицания "!", а только затем сравнение с "-1". Значение типа bool не имеет смысла сравнивать с "-1" и именно поэтому анализатор и обратил внимание на приведенный код.
Корректный вариант:
if ( (fp = fopen(filename, "wb")) == NULL) {
perror("opening image file failed");
exit(1);
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V562. |
Анализатор обнаружил потенциально возможную ошибку в логических условиях. Логика работы кода не совпадает с тем, как этот код отформатирован.
Рассмотрим пример:
if (X)
if (Y) Foo();
else
z = 1;
Форматирование кода сбивает с толку, и кажется, что присваивание "z = 1" произойдет в том случае, если X == false. Однако ветка 'else' относится к ближайшему оператору 'if'. Другими словами приведенный код на самом деле эквивалентен следующему коду:
if (X)
{
if (Y)
Foo();
else
z = 1;
}
Таким образом, код работает не так, как может показаться на первый взгляд.
Если выдано предупреждение V563, то это может означать две вещи:
1) Код плохо отформатирован и ошибки на самом деле нет. Тогда, чтобы предупреждение V563 не выдавалось, а код был более понятен, его нужно отформатировать. Пример корректного форматирования:
if (X)
if (Y)
Foo();
else
z = 1;
2) Найдена логическая ошибка. Тогда, код можно исправить, например, так:
if (X) {
if (Y)
Foo();
} else {
z = 1;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V563. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что операторы '&' и '|' работают со значениями типа bool. Подобные выражения не обязательно являются ошибочными, но часто свидетельствуют об опечатках или об ошибках в условиях.
Рассмотрим пример:
int a, b;
#define FLAG 0x40
...
if (a & FLAG == b)
{
}
Этот пример считается классическим. Очень легко ошибиться в приоритетах операций. Кажется, что последовательность вычислений следующая - "(a & FLAG) == b". Но на самом деле, последовательность вычислений будет следующая - "a & (FLAG == b)". И скорее всего, это ошибка.
Анализатор выдаст здесь предупреждение, так как странно использовать оператор '&' для переменных типа int и bool.
Если выяснится, что код содержит ошибку, то его можно исправить следующим образом:
if ((a & FLAG) == b)
Конечно, код может оказаться корректным и работать именно так, как нужно. Однако и в этом случае рационально переписать его, чтобы он стал проще для понимания. Лучше использовать оператор && или дополнительные скобки:
if (a && FLAG == b)
if (a & (FLAG == b))
После таких исправлений предупреждение V564 более выдаваться не будет, а код станет более прост для чтения.
Рассмотрим другой пример:
#define SVF_CASTAI 0x00000010
if ( !ent->r.svFlags & SVF_CASTAI ) {
...
}
Здесь мы имеем дело с явной ошибкой. В начале будет вычислено подвыражение "!ent->r.svFlags" и получен результат true или fasle. Но это не имеет значения. Будем мы выполнять операцию "true & 0x00000010" или "false & 0x00000010", результат будет одинаков. Условие в данном примере всегда ложно.
Исправленный вариант:
if ( ! (ent->r.svFlags & SVF_CASTAI) )
Примечание. Анализатор не будет выдавать предупреждение, если слава и справа от оператора '&' или '|' находятся значения типа bool. Такой код хотя и не очень красив, но корректен. Пример кода, который анализатор считает безопасным:
bool X, Y;
...
if (X | Y)
{ ... }
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V564. |
Найден обработчик исключения, который ничего не делает.
Пример кода:
try {
...
}
catch (MyExcept &)
{
}
Конечно, такой код вовсе не обязательно ошибочен. Но очень странно просто подавлять исключение, ничего не делая. Такая обработка исключений может скрывать дефекты в программе и усложнить тестирование программы.
Следует, как то реагировать на исключения. Например, рационально вписать хотя бы "assert(false)":
try {
...
}
catch (MyExcept &)
{
assert(false);
}
Иногда подобные конструкции используют, чтобы вернуть управление из множества вложенных циклов или рекурсивных функций. Но это плохая практика, так как исключение очень ресурсоемкая операция. Исключения следует использовать по назначению, а именно при возникновении нештатных ситуаций, которые должны быть обработаны на более высоком уровне.
Единственное место, где допустимо просто подавлять исключения, это деструкторы. Деструктор не должен бросать исключений. Однако, в деструкторах часто не понятно, что делать с исключениями и обработчик вполне может быть пуст. Анализатор не предупреждает о пустых обработчиках внутри деструкторов:
CClass::~ CClass()
{
try {
DangerousFreeResource();
}
catch (...) {
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V565. |
Анализатор обнаружил явное приведение числового значения к типу указателя. Чаще всего это предупреждение выдается для участков кода, где числа используются как статус состояния объекта. Подобные приемы не всегда являются ошибкой, но, как правило, свидетельствуют о плохом дизайне.
Рассмотрим пример:
const DWORD SHELL_VERSION = 0x4110400;
...
char *ptr = (char*) SHELL_VERSION;
...
if (ptr == (char*) SHELL_VERSION)
В указателе сохраняется константное значение, которое отмечает некоторое специальное состояние. Подобный код может долгое время успешно работать. Но если будет создан объект по адресу 0x4110400, то не будет возможность отличить, это магический флаг или просто объект. Если хочется использовать специальный флаг, то лучше написать так:
const DWORD SHELL_VERSION = 0x4110400;
...
char *ptr = (char*)(&SHELL_VERSION);
...
if (ptr == (char*)(&SHELL_VERSION))
Примечание. Чтобы сократить количество ложных срабатываний, сообщение V566 не выдается в целом ряде случаев. Например, если магическим числом являются такие значения, как: -1, 0, 0xcccccccc, 0xdeadbeef. Предупреждение не выдается, если число лежит в диапазоне от 0 до 65535 и приводится к указателю на строку. Это позволяет пропустить корректный код следующего вида:
CString sMessage( (LPCSTR)IDS_FILE_WAS_CHANGED ) ;
Подобный способ загрузки строки из ресурсов достаточно распространен, хотя, конечно, лучше использовать MAKEINTRESOURCE. Есть и другие исключения.
Данная диагностика классифицируется как:
|
Анализатор обнаружил выражение, которое приводит к неопределенному поведению программы. Переменная неоднократно используется между двумя точками следования, при этом ее значение изменяется. В результате невозможно предсказать результат работы такого выражения. Рассмотрим понятие "неопределенное поведение" и "точка следования" более подробно.
Неопределённое поведение (англ. undefined behaviour) — свойство некоторых языков программирования (наиболее заметно в Cи и Си++) в определённых ситуациях выдавать результат, зависящий от реализации компилятора. Другими словами, спецификация не определяет поведение языка в любых возможных ситуациях, а говорит: "при условии А результат операции Б не определён". Допускать такую ситуацию в программе считается ошибкой, даже если на некотором компиляторе программа успешно выполняется, она не будет кроссплатформенной и может отказать на другой машине, в другой ОС и даже на других настройках компилятора.
Точка следования (англ. Sequence point) — в императивном программировании любая точка программы, в которой гарантируется, что все побочные эффекты предыдущих вычислений уже проявились, а побочные эффекты последующих еще отсутствуют.
Их часто упоминают, говоря о языках Си и Си++, поскольку в этих языках особенно просто записать выражение, значение которого может зависеть от неопределённого порядка проявления побочных эффектов. Добавление одной или нескольких точек следования задает порядок более жестко и является одним из методов достижения устойчивого (т.е. корректного) результата.
Стоит заметить, что в C++11 вместо точек следования ввели понятия sequenced before/after, sequenced и unsequenced. Многие выражения, приводящие к неопределённому поведению в C++03, стали определены (например, i = ++i). Эти правила также дополнялись в C++14 и C++17. Анализатор выдаёт срабатывание, независимо от используемого стандарта. Определённость выражений вида i = ++i не служит оправданием к их использованию. Такие выражения лучше переписать, сделав их более понятными коллегам. Также если потребуется поддержать более ранний стандарт, можно получить трудно отлаживаемый баг.
Примеры неопределённого поведение в зависимости от стандартов:
i = ++i + 2; // undefined behavior until C++11
i = i++ + 2; // undefined behavior until C++17
f(i = -2, i = -2); // undefined behavior until C++17
f(++i, ++i); // undefined behavior until C++17,
// unspecified after C++17
i = ++i + i++; // undefined behavior
cout << i << i++; // undefined behavior until C++17
a[i] = i++; // undefined behavior until C++17
n = ++i + i; // undefined behavior
Точки следования необходимы в ситуации, когда одна и та же переменная изменяется в выражении более одного раза. Часто в качестве примера приводят выражение i=i++, в котором происходит присваивание переменной i и её же инкремент. Какое значение примет i? Стандарт языка должен либо указать одно из возможных поведений программы как единственно допустимое, либо указать диапазон допустимых поведений, либо указать, что поведение программы в данном случае совершенно не определено. В языках Си и Си++ вычисление выражения i=i++ приводит к неопределённому поведению, поскольку это выражение не содержит внутри себя ни одной точки следования.
В Cи и Си++ определены следующие точки следования:
Рассмотрим теперь несколько примеров, приводящих к неопределенному поведению:
int i, j;
...
X[i]=++i;
X[i++] = i;
j = i + X[++i];
i = 6 + i++ + 2000;
j = i++ + ++i;
i = ++i + ++i;
Во всех этих случаях невозможно предсказать результат вычислений. Конечно, эти примеры искусственны и опасность в них видна сразу. Рассмотрим пример кода, взятого из реального приложения:
while (!(m_pBitArray[m_nCurrentBitIndex >> 5] &
Powers_of_Two_Reversed[m_nCurrentBitIndex++ & 31]))
{}
return (m_nCurrentBitIndex - BitInitial - 1);
Компилятор может вычислить вначале как левый, так и правый аргумент оператора '&'. Это значит, что переменная m_nCurrentBitIndex может быть уже увеличена на единицу при вычислении "m_pBitArray[m_nCurrentBitIndex >> 5]". А может быть ещё и не увеличена.
Этот код может долго и исправно работать. Однако следует учитывать, что гарантированно корректно он будет себя вести только при сборке определенной версией компилятора с неизменным набором параметров компиляции. Корректный вариант кода:
while (!(m_pBitArray[m_nCurrentBitIndex >> 5] &
Powers_of_Two_Reversed[m_nCurrentBitIndex & 31]))
{ ++m_nCurrentBitIndex; }
return (m_nCurrentBitIndex - BitInitial);
Этот код более не содержит неоднозначностей. Заодно исчезла магическая константа "-1".
Программисты часто считают, что неопределенное поведение может возникать только при использовании постинкремента, в то время как преинкремент безопасен. Это не так. Рассмотрим пример общения на эту тему.
Вопрос:
Скачал ознакомительную версию вашей студии, прогнал свой проект и получил такое предупреждение: V567 Undefined behavior. The 'i_acc' variable is modified while being used twice between sequence points.
Код
i_acc = (++i_acc) % N_acc;
Как мне кажется, здесь нет undefined behavior, так как переменная i_acc не участвует в выражении дважды.
Ответ:
Неопределенное поведение здесь есть. Другое дело, что вероятность проявления ошибки весьма мала. Оператор '=' не является точкой следования. Это значит, что вначале компилятор может поместить значение переменной i_acc в регистр. Затем увеличить значение в регистре. После чего вычислить выражение и записать результат в переменную i_acc. После чего вновь записать в эту переменную регистр с увеличенным значением. В результате мы получим код вида:
REG = i_acc;
REG++;
i_acc = (REG) % N_acc;
i_acc = REG;
Компилятор имеет на это полное право. Конечно, на практике, скорее всего он сразу увеличит значение переменной и тогда всё посчитается так, как ожидает программист. Но полагаться на это нельзя.
Рассмотрим ещё одну ситуацию, связанную с вызовом функций.
Порядок вычисления аргументов функции не определён. Если аргументами является изменяющаяся переменная, то результат будет непредсказуем. Это неуточнённое поведение. Рассмотрим пример:
int A = 0;
Foo(A = 2, A);
Функция 'Foo' может быть вызвана как с аргументами (2, 0), так и с аргументами (2, 2). Порядок вычисления аргументов функции зависит от компилятора и настроек оптимизации.
Дополнительные ресурсы
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V567. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что аргументом оператора sizeof() является подозрительное выражение.
Подозрительные выражения можно разделить на три группы:
1. В выражении пытаются изменить какую-то переменную.
Оператор sizeof() вычисляет тип выражения и возвращает размер этого типа. Но само выражение не вычисляется. Пример подозрительного кода:
int A;
...
size_t size = sizeof(A++);
Данный код не увеличивает переменную 'A'. Если необходимо увеличить 'A' то следует переписать код следующим образом:
size_t size = sizeof(A);
A++;
2. В выражении используются такие операции, как сложение, умножение и так далее.
Сложные выражения являются признаком наличия ошибки. Чаще всего эти ошибки связаны с опечатками. Пример:
SendDlgItemMessage(
hwndDlg, RULE_INPUT_1 + i, WM_GETTEXT,
sizeof(buff - 1), (LPARAM) input_buff);
Вместо "sizeof(buff) - 1" программист случайно написал "sizeof(buff - 1)". Корректный вариант:
SendDlgItemMessage(
hwndDlg, RULE_INPUT_1 + i, WM_GETTEXT,
sizeof(buff) - 1, (LPARAM) input_buff);
Другой пример опечатки в тексте программы:
memset(tcmpt->stepsizes, 0,
sizeof(tcmpt->numstepsizes * sizeof(uint_fast16_t)));
Корректный вариант:
memset(tcmpt->stepsizes, 0,
tcmpt->numstepsizes * sizeof(uint_fast16_t));
3. Аргументом оператора sizeof() является указатель на класс. Чаще всего это означает, что программист забыл разыменовать указатель.
Пример:
class MyClass
{
public:
int a, b, c;
size_t getSize() const
{
return sizeof(this);
}
};
Метод getSize() возвращает размер указателя, а не размер объекта. Корректный вариант:
size_t getSize() const
{
return sizeof(*this);
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V568. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что константное значение усекается при помещении в переменную.
Рассмотрим пример:
int A[100];
unsigned char N = sizeof(A);
Размер массива 'A' (в Win32/Win64) составляет 400 байт. Диапазон значений типа unsigned char: 0..255. Следовательно, переменная 'N' не может хранить размер массива 'A'.
Предупреждение V569 указывает на то, что для хранения размера выбран неверный тип. Или что на самом деле планировалось вычислить количество элементов в массиве, а не размер массива.
Если выбран неверный тип, то код может быть исправлен следующим образом:
size_t N = sizeof(A);
Если планировалось вычислить количество элементов в массиве, то код следует изменить так:
unsigned char N = sizeof(A) / sizeof(*A);
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V569. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что переменная присваивается сама себе.
Рассмотрим пример:
dst.m_a = src.m_a;
dst.m_b = dst.m_b;
Из-за опечатки, значение переменной 'dst.m_b' не изменится. Исправленный вариант кода:
dst.m_a = src.m_a;
dst.m_b = src.m_b;
Анализатор выдаёт предупреждение не только на копирующее присваивание, но и перемещающее. Пример:
dst.m_a = std::move(src.m_a);
Анализатор не всегда выдает предупреждение, если встречает присваивание переменной самой себе. Например, если переменные взяты в скобки. Подобный приём часто используется для подавления предупреждений компилятора. Пример:
int Foo(int foo)
{
UNREFERENCED_PARAMETER(foo);
return 1;
}
Макрос UNREFERENCED_PARAMETER объявлен в файле WinNT.h следующим образом:
#define UNREFERENCED_PARAMETER(P) \
{ \
(P) = (P); \
}
Анализатор знает про такие ситуации и не выдаст предупреждение V570 на присваивание следующего вида:
(foo) = (foo);
Если в вашем проекте такой подход не используется, то можно добавить следующий комментарий, чтобы включить предупреждение:
//V_WARN_ON_ARGUMENT_SELF_ASSIGN
Примечание. Если предупреждение V570 относится к макросам, которые нельзя исправить, то можно воспользоваться механизмом подавления предупреждений в макросах. Достаточно написать специальный комментарий в файле, используемый во всём проекте (например, в StdAfx.h). Пример:
//-V:MY_MACROS:V570
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V570. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что дважды проверяется одно и тоже условие.
Рассмотрим два примера:
// Example N1:
if (A == B)
{
if (A == B)
...
}
// Example N2:
if (A == B) {
} else {
if (A == B)
...
}
В первом случае вторая проверка "if (A==B)" всегда истинна. Во втором случае, вторая проверка всегда ложна.
Высока вероятность, что подобный код содержит ошибку. Например, из-за опечатки используется ошибочное имя переменной. Корректный код:
// Example N1:
if (A == B)
{
if (A == C)
...
}
// Example N2:
if (A == B) {
} else {
if (A == C)
...
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V571. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что объект, созданный с помощью оператора 'new', явным образом приводится к другому типу.
Рассмотрим пример:
T_A *p = (T_A *)(new T_B());
...
delete p;
Возможны три варианта, как появился такой код и что с ним делать.
1) T_B не является наследником от класса T_A.
Скорее всего, это досадная опечатка или грубая ошибка. Способ её исправления зависит от того, что должен делать данный код.
2) T_B наследуется от класса T_A. Класс T_A не имеет виртуального деструктора.
В этом случае приводить T_B к T_A нельзя, так как потом невозможно корректно уничтожить созданный объект. Корректный код:
T_B *p = new T_B();
...
delete p;
3) T_B наследуется от класса T_A. Класс T_A имеет виртуальный деструктор.
В этом случае код корректен, но явное приведение типа не имеет смысла. Можно написать проще:
T_A *p = new T_B();
...
delete p;
Бывают и другие ситуации, когда будет выдано предупреждение V572. Рассмотрим пример кода, взятый из реального приложения:
DWORD CCompRemoteDriver::Open(HDRVR,
char *, LPVIDEO_OPEN_PARMS)
{
return (DWORD)new CCompRemote();
}
Программа для своих нужд работает с указателем, как с дескриптором. Для этого она явно приводит указатель к типу DWORD. Такой код будет корректно работать в 32-битных системах, но может привести к сбою в 64-битной программе. Можно избежать 64-битной ошибки, используя более подходящий тип данных DWORD_PTR:
DWORD_PTR CCompRemoteDriver::Open(HDRVR,
char *, LPVIDEO_OPEN_PARMS)
{
return (DWORD_PTR)new CCompRemote();
}
Иногда, причиной для предупреждения V572 является атавизм, оставшийся со времен, когда код был написан на языке Си. Рассмотрим пример такого кода:
struct Joint {
...
};
joints=(Joint*)new Joint[n]; //malloc(sizeof(Joint)*n);
Комментарий подсказывает нам, что раньше для выделения памяти использовалась функция 'malloc'. Теперь для этого используется оператор 'new'. Но удалить приведение забыли. Код корректен, но приведение типа здесь совершенно излишне. Можно написать более короткий код:
joints = new Joint[n];
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V572. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что объявляемая переменная для инициализации использует саму себя.
Рассмотрим простой синтетический пример:
int X = X + 1;
Переменная X будет инициализирована случайным значениям. Конечно, этот пример надуман, но зато он прост и хорошо демонстрирует смысл предупреждения. На практике подобная ошибка может встречаться в более сложных выражениях. Рассмотрим пример:
void Class::Foo(const std::string &FileName)
{
if (FileName.empty())
return;
std::string FullName = m_Dir + std::string("\\") + FullName;
...
}
Из-за опечатки в выражении случайно используется имя FullName, а не FileName. Корректный вариант кода:
std::string FullName = m_Dir + std::string("\\") + FileName;
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V573. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, переменная используется одновременно как указатель на один объект и как массив.
Рассмотрим пример ошибки, которую анализатор нашёл сам в себе:
TypeInfo *factArgumentsTypeInfo =
new (GC_QuickAlloc) TypeInfo[factArgumentsCount];
for (size_t i = 0; i != factArgumentsCount; ++i)
{
Typeof(factArguments[i], factArgumentsTypeInfo[i]);
factArgumentsTypeInfo->Normalize();
}
Подозрительно, что с переменной factArgumentsTypeInfo мы работаем как с массивом "factArgumentsTypeInfo[i]" и как с указателем на один объект "factArgumentsTypeInfo ->". На самом деле необходимо вызвать функцию Normalize() для всех элементов. Исправленный вариант кода:
TypeInfo *factArgumentsTypeInfo =
new (GC_QuickAlloc) TypeInfo[factArgumentsCount];
for (size_t i = 0; i != factArgumentsCount; ++i)
{
Typeof(factArguments[i], factArgumentsTypeInfo[i]);
factArgumentsTypeInfo[i].Normalize();
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V574. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в качестве фактического аргумента в функцию передаётся очень странное значение.
Рассмотрим пример:
bool Matrix4::operator==(const Matrix4& other) const {
if (memcmp(this, &other, sizeof(Matrix4) == 0))
return true;
...
Здесь мы имеем дело с опечаткой. Круглая скобочка поставлена не там, где нужно. К сожалению, это плохо заметно и такая ошибка может очень долго присутствовать в коде. Из-за опечатки размер сравниваемой памяти вычисляется выражением "sizeof(Matrix4) == 0". Так как результат выражение 'false', то сравнивается ноль байт памяти. Корректный вариант:
bool Matrix4::operator==(const Matrix4& other) const {
if (memcmp(this, &other, sizeof(Matrix4)) == 0)
return true;
...
Другой пример. Диагностика определяет случаи, когда массив, состоящий из enum-элементов заполняется с помощью функции 'memset'. При этом подразумевается, что размер элемента не равен одному байту. Такое заполнение будет некорректным, т.к. в этом случае значением заполнится каждый байт, а не каждый элемент массива.
Некорректный код:
enum E { V0, V1, V2, V3, V4 };
E array[123];
memset(array, V1, sizeof(array));
Если компилятор сделает размер каждого элемента равным, например, 4 байта, то все элементы массива будут равны значению 0x01010101, а вовсе не 0x00000001 (V1), как ожидает программист.
Корректный код для заполнения массива:
for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] = V1;
}
Или:
std::fill(begin(array), end(array), V1);
Примечание. NULL - странный аргумент.
Иногда программисты пытаются вычислить, сколько требуется выделить памяти под буфер, используя код вот такого типа:
const char* format = getLocalizedString(id, resource);
int len = ::vsprintf(NULL, format, args);
char* buf = (char*) alloca(len);
::vsprintf(buf, format, args);
Учтите, что вызов ::vsprintf(NULL, format, args) некорректен. Вот что про это сказано в MSDN:
int vsprintf(*buffer, char *format, va_list argptr);
....
vsprintf and vswprintf return the number of characters written, not including the terminating null character, or a negative value if an output error occurs. If buffer or format is a null pointer, these functions invoke the invalid parameter handler, as described in Parameter Validation. If execution is allowed to continue, these functions return -1 and set errno to EINVAL.
Данная диагностика учитывает информацию, может ли тот или иной указатель быть нулевым. В ряде случаев, эта информация берется из таблиц разметки функций, которые находятся внутри самого анализатора.
Примером может служить функция 'malloc'. Эта функция может вернуть 'NULL'. Соответственно, если использовать указатель, который вернула функция 'malloc', без предварительной проверки, то это может привести к разыменованию нулевого указателя.
Иногда у наших пользователей возникает желание изменить поведение анализатора и заставить его считать, что, например, функция 'malloc' не может вернуть 'NULL'. Пользователь может использовать системные библиотеки, в которых ситуации нехватки памяти обрабатываются особым образом.
Также может возникнуть желание подсказать анализатору, что определённая функция может вернуть нулевой указатель.
В этом случае вы можете воспользоваться дополнительными настройками, которые описаны в разделе "Как указать анализатору, что функция может или не может возвращать nullptr".
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V575. |
Анализатор обнаружил потенциальную ошибку при использовании функций форматного вывода ('printf', 'sprintf', 'wprintf' и так далее). Строка форматирования не соответствует передаваемым в функцию фактическим аргументам.
Рассмотрим простой пример:
int A = 10;
double B = 20.0;
printf("%i %i\n", A, B);
Согласно строке форматирования, функция 'printf' ожидает два фактических аргумента типа 'int'. Однако второй аргумент имеет значение типа 'double'. Подобное несоответствие приводит к неопределённому поведению программы. Например, к распечатке бессмысленных значений.
Корректный вариант:
int A = 10;
double B = 20.0;
printf("%i %f\n", A, B);
Ошибочных вариантов использования функции 'printf' можно привести огромное количество. Рассмотрим только несколько типовых примеров, которые чаще всего можно встретить в программах.
Распечатка адреса
Очень часто значение указателя пытаются распечатать, используя следующий код:
int *ptr = new int[100];
printf("0x%0.8X\n", ptr);
Этот код ошибочен, поскольку будет работать только в тех системах, где размер указателя совпадает с размером типа 'int'. А, например, в Win64 этот код уже распечатает только младшую часть указателя 'ptr'. Корректный вариант кода:
int *ptr = new int[100];
printf("0x%p\n", ptr);
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в качестве фактического аргумента в функцию передаётся очень странное значение.
Неиспользуемые аргументы
Часто в программах можно встретить вызов функций, где часть аргументов не используется. Пример:
int nDOW;
#define KEY_ENABLED "Enabled"
...
wsprintf(cDowKey, L"EnableDOW%d", nDOW, KEY_ENABLED);
Очевидно, что параметр 'KEY_ENABLED' здесь лишний, или код должен был выглядеть следующим образом:
wsprintf(cDowKey, L"EnableDOW%d%s", nDOW, KEY_ENABLED);
Недостаточное количество аргументов
Намного более опасной ситуацией является, когда в функцию передаётся меньше аргументов, чем необходимо. Это может легко привести к ошибке доступа к памяти, переполнению буфера или распечатке мусора. Рассмотрим пример функции выделения памяти, взятой из одной реальной программы:
char* salloc(register int nbytes)
{
register char* p;
p = (char*) malloc((unsigned)nbytes);
if (p == (char *)NULL)
{
fprintf(stderr, "%s: out of memory\n");
exit(1);
}
return (p);
}
Если функция 'malloc' вернёт значение 'NULL', то программа не сможет корректно сообщить о нехватке памяти и завершить свою работу. Она аварийно завершится или распечатает непонятный текст. В любом случае, подобное поведение усложнит анализ причины неработоспособности программы.
Путаница с signed/unsigned
Очень часто программисты используют спецификатор печати знаковых значений (например '%i') для печати переменных типа 'unsigned'. И наоборот. Эта ошибка, как правило, не критична и так сильно распространена, что в анализаторе она имеет низкий приоритет. Во многих случаях подобный код успешно работает и даёт сбой только при больших или отрицательных значениях. Рассмотрим код, который хотя не корректен, но успешно работает:
int A = 10;
printf("A = %u\n", A);
for (unsigned i = 0; i != 5; ++i)
printf("i = %d\n", i);
Хотя здесь имеется несоответствие, это код на практике печатает корректные значения. Конечно, всё равно так лучше не делать и написать корректно:
int A = 10;
printf("A = %d\n", A);
for (unsigned i = 0; i != 5; ++i)
printf("i = %u\n", i);
Ошибка проявит себя в том случае, если в программе имеются большие или отрицательные значения. Пример:
int A = -1;
printf("A = %u", A);
Вместо строки "A = -1" программа распечатает "A = 4294967295". Корректный вариант:
printf("A = %i", A);
Широкие строки (Wide character string)
У Visual Studio есть неприятная особенность, что он нестандартно интерпретирует формат строки для печати широких символов. В результате анализатор помогает диагностировать ошибку, например, в таком коде:
const wchar_t *p = L"abcdef";
wprintf(L"%S", p);
В Visual C++ считается, что '%S' предназначен для печати строки типа 'const char *'. Поэтому с точки зрения Visual C++ правильным является код:
wprintf(L"%s", p);
Начиная с Visual Studio 2015 предлагается решение этой проблемы, чтобы писать переносимый код. Для совместимости с ISO C (C99) следует указать препроцессору макрос _CRT_STDIO_ISO_WIDE_SPECIFIERS.
В этом случае, код:
const wchar_t *p = L"abcdef";
wprintf(L"%S", p);
является правильным.
Анализатор знает про '_CRT_STDIO_ISO_WIDE_SPECIFIERS' и учитывает его при анализе.
Кстати, если вы включили режим совместимости с ISO C (объявлен макрос '_CRT_STDIO_ISO_WIDE_SPECIFIERS'), вы можете в отдельных местах вернуть старое приведение, используя спецификатор формата '%Ts'.
Вся эта история с широкими символами достаточно запутанная и выходит за пределы документации. Чтобы лучше разобраться в вопросе, предлагаем ознакомиться со следующими ссылками:
Дополнительная настройка диагностики
Чтобы самостоятельно указать имена своих собственных функций, для которых следует выполнять проверку формата, можно использовать пользовательские аннотации. Подробнее об этом можно почитать здесь.
Дополнительные ресурсы:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования форматной строки. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V576. |
Анализатор обнаружил потенциальную ошибку внутри оператора switch. Используется метка с именем похожим на 'default'. Возможно это опечатка.
Рассмотрим пример:
int c = 10;
int r = 0;
switch(c){
case 1:
r = 3; break;
case 2:
r = 7; break;
defalt:
r = 8; break;
}
Кажется после того, как этот код отработает, значение переменной 'r' будет 8. Но на самом деле значение 'r' останется равно нулю. Дело в том, что "defalt" это метка, а не оператор "default". Исправленный вариант кода:
int c = 10;
int r = 0;
switch(c){
case 1:
r = 3; break;
case 2:
r = 7; break;
default:
r = 8; break;
}
Эта диагностика срабатывает также, когда имя метки начинается с "case". Есть вероятность, что пропущен пробел. Например, вместо метки "case1:", должно быть написано "case 1:".
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку в выражении, работающем с битами. Часть выражения не имеет смысла или является избыточным. Как правило, такие ошибки возникают из-за опечатки.
Рассмотрим пример:
if (up & (PARAMETER_DPDU | PARAMETER_DPDU | PARAMETER_NG))
Здесь два раза используется константа PARAMETER_DPDU. В корректном коде должны использоваться две разных константы: PARAMETER_DPDU и PARAMETER_DPDV. Буква 'U' похожа на 'V' поэтому и возникла такая опечатка. Исправленный вариант:
if (up & (PARAMETER_DPDU | PARAMETER_DPDV | PARAMETER_NG))
Другой пример. Здесь ошибки нет, но код избыточен:
if (((pfds[i].dwFlags & pPFD->dwFlags) & pPFD->dwFlags)
!= pPFD->dwFlags)
Сокращенный вариант:
if ((pfds[i].dwFlags & pPFD->dwFlags) != pPFD->dwFlags)
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V578. |
Анализатор обнаружил в коде подозрительный вызов функции. В качестве аргументов в функцию передается указатель и размер этого указателя. На самом деле, разработчики часто хотят передать в функцию не размер указателя, а размер буфера.
Рассмотрим, как подобная ошибка может возникнуть в коде. Предположим, в начале имелся код следующего вида:
char buf[100];
...
memset(buf, 0, sizeof(buf));
Код корректен. Функция memset() очищает массив из 100 байт. Затем, код изменился, и буфер стал переменного размера. Код очистки буфера изменить забыли:
char *buf = new char[N];
...
memset(buf, 0, sizeof(buf));
Теперь код некорректен. Оператор sizeof() возвращает размер указателя, а не размер буфера с данными. Как результат, функция memset() очищает только часть массива.
Рассмотрим другой пример, взятый из реального приложения:
apr_size_t ap_regerror(int errcode,
const ap_regex_t *preg, char *errbuf,
apr_size_t errbuf_size)
{
...
apr_snprintf(errbuf, sizeof errbuf,
"%s%s%-6d", message, addmessage,
(int)preg->re_erroffset);
...
}
В таком коде заметить ошибку непросто. Функция apr_snprintf() принимает в качестве аргумента указатель 'errbuf' и размер этого указателя 'sizeof errbuf'. Анализатор считает этот код подозрительным и абсолютно прав. Размер буфера находится в переменной 'errbuf_size' и именно эту переменную следует использовать. Корректный код:
apr_snprintf(errbuf, errbuf_size,
"%s%s%-6d", message, addmessage,
(int)preg->re_erroffset);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V579. |
Анализатор обнаружил подозрительное явное приведение типа. Это может быть ошибкой или потенциальной ошибкой.
Рассмотрим пример:
DWORD errCode = 0;
void* dwErrParams[MAX_MESSAGE_PARAMS];
dwErrParams[0] = *((void**)&errCode);
Код содержит 64-битную ошибку. Тип 'DWORD' превращается в тип 'void *' . Этот код некорректно работает в 64-битных системах, где размер указателя не совпадает с размером типа DWORD. Корректный вариант:
DWORD_PTR errCode = 0;
void* dwErrParams[MAX_MESSAGE_PARAMS];
dwErrParams[0] = (void *)errCode;
Данная диагностика классифицируется как:
|
Анализатор обнаружил код, в котором рядом находятся два оператора 'if' с одинаковыми условиями. Это является потенциальной ошибкой или избыточным кодом.
Рассмотрим пример:
if (strlen(S_1) == SIZE)
Foo(A);
if (strlen(S_1) == SIZE)
Foo(B);
Содержит этот код ошибку или нет, зависит от того, что хотел сделать программист. Если во втором условии нужно вычислить длину другой строки, то это ошибка. Исправленный код:
if (strlen(S_1) == SIZE)
Foo(A);
if (strlen(S_2) == SIZE)
Foo(B);
Код может быть корректен. Но тогда он неэффективен, так как два раза приходится вычислять длину одной и той же строки. Оптимизированный код:
if (strlen(S_1) == SIZE) {
Foo(A);
Foo(B);
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V581. |
Анализатор обнаружил потенциальную ошибку при работе с контейнером фиксированного размера. Эту диагностику нам предложил реализовать один из наших пользователей. Вот как он сформулировал задачу.
Для работы с массивами константного размера мы используем следующий шаблонный класс:
template<class T_, int numElements > class idArray
{
public:
int Num() const { return numElements; };
.....
inline const T_ & operator[]( int index ) const {
idassert( index >= 0 );
idassert( index < numElements );
return ptr[index];
};
inline T_ & operator[]( int index ) {
idassert( index >= 0 );
idassert( index < numElements );
return ptr[index];
};
private:
T_ ptr[numElements];
};
Класс позволяет обнаружить ошибки выхода за пределы массива при запуске DEBUG версии. При этом производительность RELESE версии не снижается. Пример некорректного кода:
idArray<int, 1024> newArray;
newArray[-1] = 0;
newArray[1024] = 0;
Ошибки будут обнаружены при запуске отладочной версии. Однако хочется иметь возможность обнаруживать подобные ошибки с помощью статического анализа ещё на этапе компиляции.
Для выявления подобных ошибочных ситуаций и предназначена диагностика V582. Если в программе используется класс, реализующий функциональность контейнера фиксированного размера, то анализатор пытается проверить, что индекс не выходит за его границу. Пример диагностики:
idArray<float, 16> ArrA;
idArray<float, 8> ArrB;
for (size_t i = 0; i != 16; i++)
ArrA[i] = 1.0f;
for (size_t i = 0; i != 16; i++)
ArrB[i] = 1.0f;
Здесь анализатор выдаст предупреждение:
V582 Consider reviewing the source code which operates the 'ArrB' container. The value of the index belongs to the range: [0..15].
Ошибка в том, что оба цикла обрабатывают 16 элементов, хотя второй массив содержит только 8 элементов. Корректный вариант:
for (size_t i = 0; i != 16; i++)
ArrA[i] = 1.0f;
for (size_t i = 0; i != 8; i++)
ArrB[i] = 1.0f;
Следует отметить, что передача слишком больших или маленьких индексов не всегда означает ошибку в программе. Например, оператор [] может быть реализован следующим образом:
inline T_ & operator[]( int index ) {
if (index < 0) index = 0;
if (index >= numElements) index = numElements - 1;
return ptr[index];
};
Если вы используете подобные классы и получаете много ложных срабатываний, то очевидно вам следует отключить диагностику V582.
Примечание. Анализатор не обладает искусственным интеллектом и поэтому его возможности поиска дефектов при работе с контейнерами ограничены. Мы работаем над усовершенствованием алгоритмов, поэтому если вы заметили явные ложные срабатывания или наоборот ситуации, в которых анализатор не выдал предупреждение, просим написать нам и прислать соответствующий пример кода.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку при использовании тернарного оператора "?:". Независимо от условия, будет выполнено одно и тоже действие. Скорее всего, в коде имеется опечатка.
Рассмотрим самый простой пример:
int A = B ? C : C;
В любом случае переменной A будет присвоено значение переменной C.
Рассмотрим, как подобная ошибка может выглядеть в коде реального приложения:
fovRadius[0] =
tan(DEG2RAD((rollAngleClamped % 2 == 0 ?
cg.refdef.fov_x : cg.refdef.fov_x) * 0.52)) * sdist;
Здесь код отформатирован. В тексте программы это одна строка, и не удивительно, что легко просмотреть опечатку. Ошибка заключается, что два раза используется член структуры "fov_x". Корректный вариант:
fovRadius[0] =
tan(DEG2RAD((rollAngleClamped % 2 == 0 ?
cg.refdef.fov_x : cg.refdef.fov_y) * 0.52)) * sdist;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V583. |
Анализатор обнаружил выражение, которое можно упростить. С большой вероятностью это выражение содержит опечатку.
Рассмотрим пример:
float SizeZ;
if (SizeZ + 1 < SizeZ)
Анализатор считает, что это условие содержит ошибку, так как не имеет практического смысла. Скорее всего, хотелось выполнить совсем другую проверку. Корректный код:
if (SizeZ + 1 < maxSizeZ)
Конечно, иногда программисты используют приемы, которые корректны, но выглядят странно. Анализатор по возможности старается распознать такие ситуации и не выдавать диагностические сообщения. Например, анализатор считает безопасными следующие проверки:
//Тест на переполнение при суммировании
int a, b;
if (a + b < a)
//Проверка, что X не равен 0, +бесконечность или -бесконечность.
double X;
if (X * 0.5f != X)
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V584. |
Анализатор обнаружил попытку освободить память, занятую локальной переменной. Подобные ошибки могут возникать в ходе неаккуратного рефакторинга или опечатки.
Рассмотрим пример некорректного кода:
void Foo()
{
int *p;
...
free(&p);
}
Корректный код:
void Foo()
{
int *p;
...
free(p);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку повторного освобождения какого-то ресурса. При определённых обстоятельствах этот код может стать дефектом безопасности.
Ресурсом может быть память, файл или, например, объект HBRUSH.
Рассмотрим пример некорректного кода:
float *p1 = (float *)malloc(N * sizeof(float));
float *p2 = (float *)malloc(K * sizeof(float));
...
free(p1);
free(p1);
В тексте программы имеется опечатка, из-за которой дважды освобождается одна и та же область памяти. Последствия выполнения такого кода предсказать сложно. Возможно, программа аварийно завершит свою работу. Или, возможно, продолжит свою работу, но возникнет утечка памяти (memory leak).
Более того, подобный код является дефектом безопасности и может привести к возникновению уязвимости. Например, уязвимости подвержена функция 'malloc' ('dmalloc') Дуга Ли (Doug Lea), которая используется в некоторых библиотеках как 'malloc' по умолчанию. Для возникновения уязвимости, связанной с двойным освобождением памяти, требуется несколько условий: блоки памяти, смежные с освобождаемым, не должны быть свободны, а список свободных блоков памяти должен быть пуст. В этом случае возможно создание эксплойта. Несмотря на то что подобные уязвимости сложно использовать из-за необходимости специфичной конфигурации памяти, существуют реальные примеры уязвимого кода, который был успешно взломан.
Корректный вариант:
float *p1 = (float *)malloc(N * sizeof(float));
float *p2 = (float *)malloc(K * sizeof(float));
...
free(p1);
free(p2);
Иногда ошибка двойного освобождения ресурсов не является опасной:
vector<unsigned> m_arrStack;
...
m_arrStack.clear();
m_arrBlock.clear();
m_arrStack.clear();
Случайно два раза очищаем массив. Код работает правильно, но его всё равно следует посмотреть и поправить. В процессе его изучения может выясниться, что забыли очистить другой массив.
Корректный вариант:
vector<unsigned> m_arrStack;
...
m_arrStack.clear();
m_arrBlock.clear();
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V586. |
Анализатор обнаружил потенциальную ошибку, связанную с бессмысленным взаимным присваиванием переменных.
Рассмотрим пример:
int A, B, C;
...
A = B;
C = 10;
B = A;
Здесь присваивание "B = A" не имеет никакого практического смысла. Возможно, это опечатка или просто лишнее действие. Корректный вариант кода:
A = B;
C = 10;
B = A_2;
Рассмотренный выше пример был искусственным. Рассмотрим, как эта ошибка может выглядеть в коде реального приложения:
// Swap; exercises counters
{
RCPFooRef temp = f2;
f2 = f3;
f3 = f2;
}
Корректный вариант:
// Swap; exercises counters
{
RCPFooRef temp = f2;
f2 = f3;
f3 = temp;
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V587. |
Анализатор обнаружил потенциальную ошибку, так как в программе имеется последовательность символов '=+'. Возможно, это опечатка и следует использовать оператор '+='.
Рассмотрим пример:
size_t size, delta;
...
size=+delta;
Этот код может быть корректен. Но с большой вероятностью имеется опечатка и на самом деле, хотели использовать оператор '+='. Исправленный вариант:
size_t size, delta;
...
size+=delta;
Если код корректен, то чтобы убрать предупреждение V588 можно удалить '+' или поставить дополнительный пробел. Вариант корректного кода, где предупреждение не выдается:
size = delta;
size = +delta;
Примечание. Для поиска опечаток вида 'A =- B' используется диагностическая проверка V589. Эта проверка сделана отдельно, так как возможно большое количество ложных срабатываний, и может возникнуть желание отключить её.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V588. |
Анализатор обнаружил потенциальную ошибку, так как в программе имеется последовательность символов '=-'. Возможно, это опечатка и следует использовать оператор '-='.
Рассмотрим пример:
size_t size, delta;
...
size =- delta;
Этот код может быть корректен. Но с большой вероятностью имеется опечатка и на самом деле, хотели использовать оператор '-='. Исправленный вариант:
size_t size, delta;
...
size -= delta;
Если код корректен, то чтобы убрать предупреждение V589 можно использовать дополнительный пробел между символами '=' и '-'. Вариант корректного кода, где предупреждение не выдается:
size = -delta;
Чтобы уменьшить количество ложных срабатываний, для правила V589 действует ряд специфичных исключений. Например, анализатор не будет выдавать предупреждение, если программист не использует пробелов между переменными и операторами. Ряд примеров, код которых анализатор считает безопасным:
A=-B;
int Z =- 1;
N =- N;
Примечание. Для поиска опечаток вида 'A =+ B' используется диагностическая проверка V588.
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, так как в коде имеется избыточное сравнение.
Поясним на простом примере:
if (Aa[42] == 10 && Aa[42] != 3)
Условие будет выполнено в том случае, если 'Aa[42] == 10'. Вторая часть выражения бессмысленна. Проанализировав код, можно прийти к одному из двух выводов:
1) Выражение можно упросить. Исправленный код:
if (Aa[42] == 10)
2) Выражение содержит ошибку. Исправленный код:
if (Aa[42] == 10 && Aa[43] != 3)
Рассмотрим практический пример. Здесь ошибки нет, но выражение избыточно, что может затруднять чтение кода:
while (*pBuff == ' ' && *pBuff != '\0')
pBuff++;
Проверка " *pBuff != '\0' " не имеет смысла. Сокращенный вариант кода:
while (*pBuff == ' ')
pBuff++;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V590. |
Анализатор обнаружил non-void функцию, для которой существует путь исполнения, не возвращающий значение. Вызов такой функции ведёт к неопределённому поведению.
Если при исполнении тела non-void функции достигнут конец её тела без 'return', то произойдёт неопределённое поведение.
Рассмотрим пример:
int GetSign(int arg)
{
if (arg > 0)
{
return 1;
}
else if (arg < 0)
{
return -1;
}
}
Если в функцию 'GetSign' передать 0, то произойдёт неопределённое поведение. Исправленный вариант:
int GetSign(int arg)
{
if (arg > 0)
{
return 1;
}
else if (arg < 0)
{
return -1;
}
return 0;
}
Исключением из правила являются функции 'main' и 'wmain'. Для этих функций достижение конца тела эквивалентно выполнению конструкции 'return 0;', поэтому неопределённого поведения не будет. Рассмотрим такой пример:
....
int main()
{
AnalyzeFile(FILE_NAME);
}
В данном случае имеем дело с функцией 'main'. Здесь не будет неопределённого поведения. Соответственно, не будет и выдано предупреждение анализатора. Фрагмент кода эквивалентен следующему:
....
int main()
{
AnalyzeFile(FILE_NAME);
return 0;
}
Заметим, что неопределённое поведение происходит, только если конец non-void функции действительно достигнут. В частности, если на пути исполнения тела такой функции произошёл бросок исключения, которое не было перехвачено в теле этой же функции, то неопределённого поведения не будет.
Анализатор не выдаст предупреждение на следующем фрагменте кода:
int Calc(int arg);
int Bar(int arg)
{
if (arg > 0)
{
return Calc(arg);
}
throw std::logic_error { "bad arg was passed to Bar" };
}
Также неопределённого поведения не будет, если в ходе выполнения тела такой функции произошел вызов другой функции, которая не возвращает управление. Такие функции обычно помечают '[[noreturn]]'. Поэтому анализатор не выдаст предупреждение на следующем примере:
[[noreturn]] void exit(int exit_code);
int Foo()
{
....
exit(10);
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V591. |
Анализатор обнаружил двойные круглые скобки вокруг выражения. Есть вероятность, что одна из скобок поставлена не там, где надо.
Хочется подчеркнуть, что анализатор не ищет фрагменты кода, где два раза повторяются круглые скобки. Например, анализатор считает проверку "if ((A = B))" безопасной. Здесь дополнительные скобки используются, чтобы подавить предупреждения некоторых компиляторов. В этом выражении невозможно расставить скобки так, чтобы возникла ошибка.
Анализатор пытается обнаружить те ситуации, когда изменив местонахождение одной скобки, можно изменить смысл выражения. Рассмотрим пример:
if((!osx||howmanylevels))
Этот код подозрителен. Непонятно, зачем здесь дополнительные круглые скобки. Возможно, выражение должно выглядеть так:
if(!(osx||howmanylevels))
Если даже выражение корректно, то всё равно лучше убрать лишние круглые скобки. На это есть две причины.
1) Человек читающий код, может усомниться в его корректности, увидев дублирующиеся круглые скобки.
2) Если убрать лишние скобки, то анализатор перестанет выдавать ложное предупреждение.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V592. |
Анализатор обнаружил потенциальную ошибку в выражении, которое, скорее всего, работает не так, как задумывал программист. Данный тип ошибок чаще всего можно встретить в выражениях, где одновременно проверяется результат работы функции и выполняется присваивание.
Рассмотрим простой пример:
if (handle = Foo() != -1)
Создавая такой код, программист, как правило, хочет выполнить действия в следующем порядке:
if ((handle = Foo()) != -1)
Но приоритет оператора '!=' выше, чем приоритет оператора '='. Поэтому выражение вычистится так:
if (handle = (Foo() != -1))
Чтобы исправить ошибку, можно использовать скобки. Ещё лучше не жадничать на количестве строк кода. Текст вашей программы станет более читабелен, если написать так:
handle = Foo();
if (handle != -1)
Рассмотрим, как подобная ошибка может выглядеть на практике:
if (hr = AVIFileGetStream(pfileSilence,
&paviSilence, typeAUDIO, 0) != AVIERR_OK)
{
ErrMsg("Unable to load silence stream");
return hr;
}
Проверка в коде, что произошла ошибка, работает корректно и будет выдано сообщение "Unable to load silence stream". Беда в том, что переменная 'hr' будет хранить не код ошибки, а значение 1. Исправленный вариант кода:
if ((hr = AVIFileGetStream(pfileSilence,
&paviSilence, typeAUDIO, 0)) != AVIERR_OK)
{
ErrMsg("Unable to load silence stream");
return hr;
}
Анализатор не всегда выдает предупреждения, обнаруживая конструкцию вида "if (x = a == b)". Например, анализатор понимает, что данный код безопасен:
char *from;
char *to;
bool result;
...
if (result = from == to)
{}
Примечание. Если анализатор все-таки выдал ложное предупреждение, то есть два способа устранить его:
1) Добавить дополнительные скобки. Пример: "if (x = (a == b))".
2) Использовать комментарий для подавления предупреждения. Пример: "if (x = a == b) //-V593".
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V593. |
Анализатор обнаружил потенциальную ошибку при работе с указателями. В программе есть выражение, при вычислении которого получается, что указатель вышел за границу массива.
Проясним это простым примером:
int A[10];
fill(A, A + sizeof(A), 33);
Мы хотим присвоить всем элементам массива значение 33. Ошибка в том, что указатель "A + sizeof(A)" ссылается далеко за пределы массива. В результате, мы изменим больше ячеек памяти, чем планировалось. Результат этой ошибки непредсказуем.
Правильный вариант кода:
int A[10];
fill(A, A + sizeof(A) / sizeof(A[0]), 33);
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V594. |
Анализатор обнаружил потенциальную ошибку, которая может привести к разыменовыванию нулевого указателя.
Анализатор заметил в коде следующую ситуацию. В начале, указатель используется. А уже затем этот указатель проверяется на значение NULL. Это может означать одно из двух:
1) Возникнет ошибка, если указатель будет равен NULL.
2) Программа всегда работает корректно, так как указатель всегда не равен NULL. Проверка является лишней.
Рассмотрим первый вариант. Ошибка есть.
buf = Foo();
pos = buf->pos;
if (!buf) return -1;
Если указатель 'buf' окажется равен NULL, то выражение 'buf->pos ' приведёт к ошибке. Анализатор выдаст предупреждение на этот код, указав 2 строки. Первая строка - это то место, где используется указатель. Вторая строка - это то место, где указатель сравнивается со значением NULL.
Исправленный вариант кода:
buf = Foo();
if (!buf) return -1;
pos = buf->pos;
Рассмотрим второй вариант. Ошибки нет.
void F(MyClass *p)
{
if (!IsOkPtr(p))
return;
printf("%s", p->Foo());
if (p) p->Clear();
}
Этот код всегда работает корректно. Указатель всегда не равен NULL. Однако анализатор не разобрался в этой ситуации и выдал предупреждение. Чтобы оно исчезло, следует удалить проверку "if (p)". Она не имеет практического смысла и только может запутать программиста, читающего код.
Исправленный вариант:
void F(MyClass *p)
{
if (!IsOkPtr(p))
return;
printf("%s", p->Foo());
p->Clear();
}
В случае если анализатор ошибается, то кроме изменения кода, можно использовать комментарий для подавления предупреждений. Пример: "p->Foo(); //-V595".
Примечание N1.
Некоторые пользователи сообщают, что анализатор выдает предупреждение V595 на корректный код, Пример:
static int Foo(int *dst, int *src)
{
*dst = *src; // V595 !
if (src == 0)
return 0;
return Foo(dst, src);
}
...
int a = 1, b = 2;
int c = Foo(&a, &b);
Да, здесь анализатор выдает ложное срабатывание. Код корректен и указатель 'src' не может быть равен NULL в тот момент, когда выполняется присваивание "*dst = *src". Возможно, в дальнейшем мы реализуем исключение для подобных случаев, но пока не спешим это делать. Хотя здесь нет ошибки, анализатор выявил избыточность кода. Функцию можно сократить. При этом пропадет предупреждение V595, а код станет проще.
Улучшенный вариант:
int Foo(int *dst, int *src)
{
assert(dst && src);
*dst = *src;
return Foo(dst, src);
}
Примечание N2.
Иногда в программах можно встретить код следующего вида:
int *x=&p->m_x; //V595
if (p==NULL) return(OV_EINVAL);
Вычисляется указатель на член класса. Этот указатель не разыменовывается и кажется, что анализатор зря выдаёт диагностическое сообщение V595. Однако, этот код приводит к неопределённому поведению. Если программа работает, это везение и не более того. Нельзя вычислять выражение, если "&p->m_x", если указатель 'p' равен нулю.
С аналогичной ситуацией программист может встретиться, желая отсортировать массив:
int array[10];
std::sort(&array[0], &array[10]); // Undefined behavior
Использование &array[10] приводит к неопределенному поведению, так как элемент array[10] уже лежит за границами массива. Но использование арифметики указателей вполне допустимо. Можно обратиться к указателю, который адресует конечный элемент массива. Поэтому, необходимо написать следующий корректный код:
int array[10];
std::sort(array, array+10); //ok
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки разыменования нулевого указателя. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V595. |
Анализатор обнаружил потенциальную ошибку, связанную с использованием класса std::exception или наследуемого от него класса. Анализатор выдает предупреждение в том случае, если создается объект типа std::exception / CException, но не используется.
Пример:
if (name.empty())
std::logic_error("Name mustn't be empty");
Ошибка заключается в том, что случайно забыто ключевое слово 'throw'. В результате данный код не генерирует исключение в случае ошибочной ситуации. Исправленный вариант кода:
if (name.empty())
throw std::logic_error("Name mustn't be empty");
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V596. |
Анализатор обнаружил потенциальную ошибку, когда массив, содержащий приватную информацию, не будет очищен.
Рассмотрим пример кода.
void Foo()
{
char password[MAX_PASSWORD_LEN];
InputPassword(password);
ProcessPassword(password);
memset(password, 0, sizeof(password));
}
Функция на стеке создает временный буфер, предназначенный для хранения пароля. По окончанию работы с паролем, мы хотим очистить этот буфер. Если это не сделать, пароль останется в памяти, что может привести к неприятным последствиям. На эту тему есть хорошая статья "Перезаписывать память - зачем?".
К сожалению, приведенный код может оставить буфер неочищенным. Обратите внимание, что массив 'password' очищается в конце и более не используется. Поэтому при сборке Release версии программы, компилятор, скорее всего, удалит вызов функции memset(). На это компилятор имеет полное право. Такое изменение не влияет на наблюдаемое поведение, которое описано в Стандарте как последовательность вызова функций ввода-вывода и чтения-записи volatile данных. То есть с точки зрения языка Си/Си++, если удалить вызов функции memset(), это ничего не изменит!
Для очистки буферов содержащих приватную информацию необходимо использовать специальную функцию RtlSecureZeroMemory или memset_s (см. также статью "Безопасная очистка приватных данных").
Исправленный вариант кода:
void Foo()
{
char password[MAX_PASSWORD_LEN];
InputPassword(password);
ProcessPassword(password);
RtlSecureZeroMemory(password, sizeof(password));
}
Кажется, что на практике, компилятор не может удалить вызов такой важной функции memset(). Может возникнуть впечатление, что речь идет о редких экзотических компиляторах. Нет, это не так. Возьмем для примера компилятор Visual C++ 10, входящий в состав Visual Studio 2010.
Рассмотрим две функции.
void F1()
{
TCHAR buf[100];
_stprintf(buf, _T("Test: %d"), 123);
MessageBox(NULL, buf, NULL, MB_OK);
memset(buf, 0, sizeof(buf));
}
void F2()
{
TCHAR buf[100];
_stprintf(buf, _T("Test: %d"), 123);
MessageBox(NULL, buf, NULL, MB_OK);
RtlSecureZeroMemory(buf, sizeof(buf));
}
Функции отличаются тем, как они очищают буфер. Первая использует функцию memset(), а вторая RtlSecureZeroMemory(). Скомпилируем оптимизированный код, указав для компилятора Visual C++ 10 ключ "/O2". Рассмотрим получившийся ассемблерный код:
Рисунок 1. Функция memset() удалена.
Рисунок 2. Функция RtlSecureZeroMemory() заполняет память нулями.
Как видно в ассемблерном коде, функция memset() была удалена компилятором при оптимизации. Функция RtlSecureZeroMemory() выстроилась в код, поэтому теперь массив успешно обнуляется.
Дополнительные ссылки:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V597. |
Анализатор обнаружил, что для работы с объектом класса используются такие низкоуровневые функции, как 'memset', 'memcpy', 'memmove', 'memcmp', 'memchr'. Это недопустимо, если класс содержит указатель на таблицу виртуальных функций (vtable).
Если указатель на объект передается в качестве целевого функциям 'memset', 'memcpy' или 'memmove', то они могут испортить vtable. Если указатель передается в качестве источника функциям 'memcpy' или 'memmove', то результат такого копирования может быть непредсказуемым. В случае с функциями 'memcmp' и 'memchr' сравнение или поиск при наличии vtable также может привести к нежелательному результату.
Рассмотрим пример кода:
class MyClass
{
public:
MyClass();
virtual ~MyClass();
private:
int A, B, C;
char buf[100];
};
MyClass::MyClass()
{
memset(this, 0, sizeof(*this));
}
Обратите внимание, что в классе есть виртуальный деструктор. Это значит, что в классе присутствует vtable. Программист поленился очищать члены класса по отдельности. Для очистки он использовал функцию 'memset'. Это привет к порче vtable, так как функция 'memset' ничего про него не знает.
Корректный код:
MyClass::MyClass() : A(0), B(0), C(0)
{
memset(buf, 0, sizeof(buf));
}
Начиная с C++11, можно переписать этот код следующим образом, если требуется инициализировать поля нулями:
class MyClass
{
public:
MyClass() = default;
virtual ~MyClass() = default;
private:
int A = {}, B = {}, C = {};
char buf[100] = {};
};
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V598. |
Анализатор обнаружил потенциальную ошибку, связанную с отсутствием в базовом классе виртуального деструктора.
Чтобы анализатор выдал предупреждение V599 необходимо, чтобы выполнились следующие условия:
1) Объект класса уничтожается с помощью оператора delete.
2) Класс имеет хотя бы одну виртуальную функцию.
Наличие виртуальных функций говорит о том, что класс могут использовать полиморфно. В этом случае виртуальный деструктор необходим, чтобы корректно разрушить объект.
Рассмотрим пример кода.
class Father
{
public:
Father() {}
~Father() {}
virtual void Foo() { ... }
};
class Son : public Father
{
public:
int* buffer;
Son() : Father() { buffer = new int[1024]; }
~Son() { delete[] buffer; }
virtual void Foo() { ... }
};
...
Father* object = new Son();
delete object; // Call ~Father()!!
Нижеприведённый код является некорректным и приводит к утечке памяти. В момент уничтожения объекта (delete object;) вызывается только деструктор в классе 'Father'. Чтобы вызвать деструктор класса 'Son' необходимо сделать деструктор виртуальным.
Корректный код:
class Father
{
public:
Father() {}
virtual ~Father() {}
virtual void Foo() { ... }
};
Диагностическое сообщение V599 помогает выявить далеко не все проблемы связанные с отсутствием виртуальных деструкторов. Приведем соответствующий пример: "Вы разрабатываете библиотеку. В ней есть класс XXX, в котором есть виртуальные функции, но нет виртуального деструктора. Вы в библиотеке сами не работаете с этим классом, и анализатор не предупредит об опасности. Проблема может возникнуть у программиста, использующего вашу библиотеку и наследующего свои классы от класса XXX."
Намного больше проблем позволяет выявить диагностическое предупреждение C4265: 'class' : class has virtual functions, but destructor is not virtual, реализованная в Visual C++. Это очень полезная диагностика. Но по умолчанию она выключена. Почему эта диагностика отключена я ответить затрудняюсь. Обсуждение этой темы поднималось на сайте Stack Overflow: Why is C4265 Visual C++ warning (virtual member function and no virtual destructor) off by default? К сожалению толкового объяснения дать никто не смог.
Мы предполагаем, что предупреждение C4265 дает много срабатываний в коде, где используется паттерн примесь (подмешивание). При использовании этого паттерна возникает множество интерфейсных классов. Они содержат виртуальные функции, но виртуальный деструктор в них не нужен.
Можно сказать, что диагностическое правило V599 является частным случаем C4265. Оно дает меньше ложных срабатываний, но, к сожалению, позволяет выявить меньшее количество дефектов. Если вы хотите провести более тщательный анализ своего кода, то включите предупреждение C4265.
P. S.
К сожалению, ВСЕГДА объявлять деструктор виртуальным, не является идеальной практикой программирования. Это приводит к дополнительным накладным расходам, так как в классе появляется указатель на таблицу виртуальных методов.
P.P.S.
Родственным диагностическим сообщением являются V689.
Дополнительная информация:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти), Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V599. |
Анализатор обнаружил сравнение адреса массива с нулем. Такое сравнение не имеет смысла и может говорить о наличии ошибки в программе.
Рассмотрим пример кода.
void Foo()
{
short T_IND[8][13];
...
if (T_IND[1][j]==0 && T_IND[5]!=0)
T_buf[top[0]]= top[1]*T_IND[6][j];
...
}
Программа обрабатывает двумерный массив. Код сложен для чтения и ошибка на первый взгляд не заметна. Однако анализатор предупредит, что сравнение "T_IND[5]!=0" не имеет смысла. Указатель "T_IND[5]" всегда не равен нулю.
Изучив предупреждения V600 можно найти ошибки, которые, как правило, связаны с опечатками. Например, может оказаться, что данный код должен выглядеть так:
if (T_IND[1][j]==0 && T_IND[5][j]!=0)
Предупреждение V600 далеко не всегда означает наличие настоящей ошибки. Часто причиной появления V600 является неаккуратный рефакторинг. Рассмотрим наиболее распространенный случай. В начале код выглядел так:
int *p = (int *)malloc(sizeof(int) *ARRAY_SIZE);
...
if (!p)
return false;
...
free(p);
Код менялся. Стало ясно, что значение ARRAY_SIZE небольшое и массив можно создавать на стеке. В результате получился следующий код:
int p[ARRAY_SIZE];
...
if (!p)
return false;
...
Здесь выдается предупреждение V600. Однако код корректно работает. Просто получилось, что поверка "if (!p)" потеряла смысл и её можно удалить.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V600. |
Анализатор обнаружил подозрительное неявное приведение типа. Такое приведение типа может говорить о наличии ошибки или о неаккуратности в коде.
Рассмотрим первый пример.
std::string str;
bool bstr;
...
str = true;
Любой программист удивится, увидев как переменной типа 'std::string' присваивают значение 'true'. Однако это вполне допустимая и работающая конструкция. Здесь программист просто ошибся и написал в коде не ту переменную.
Корректный вариант кода:
std::string str;
bool bstr;
...
bstr = true;
Рассмотрим второй пример:
bool Ret(int *p)
{
if (!p)
return "p1";
...
}
Строковый литерал "p1" превращается в значение 'true' и возвращается из функции. Это очень странный код.
Общие рекомендации по правке подобного кода дать сложно и каждый случай надо рассматривать отдельно.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V601. |
Анализатор обнаружил потенциальную ошибку, которая может возникнуть из-за опечатки. Высока вероятность, что в выражении вместо оператора '<' должен использоваться оператор '<<'.
Рассмотрим пример кода.
void Foo(unsigned nXNegYNegZNeg, unsigned nXNegYNegZPos,
unsigned nXNegYPosZNeg, unsigned nXNegYPosZPos)
{
unsigned m_nIVSampleDirBitmask =
(1 << nXNegYNegZNeg) | (1 < nXNegYNegZPos) |
(1 << nXNegYPosZNeg) | (1 << nXNegYPosZPos);
...
}
Код содержит ошибку, так как случайно в выражении написан оператор '<'. Корректный вариант кода должен выглядеть так:
unsigned m_nIVSampleDirBitmask =
(1 << nXNegYNegZNeg) | (1 << nXNegYNegZPos) |
(1 << nXNegYPosZNeg) | (1 << nXNegYPosZPos);
Примечание.
Анализатор считает подозрительными сравнения ('<', '>'), если полученный результат используется в двоичных операциях, таких как '&', '|' или '^'. Диагностика устроена более сложно, но надеемся, что общая идея понятна. Найдя такие выражения, анализатор выдает предупреждение V602.
Если анализатор выдает ложное срабатывание, то вы можете подавить его, используя комментарий "//-V602". Но чаще всего, этот код лучше переписать. С выражениями, которые имеют тип 'bool' лучше не работать, используя двоичные операторы. Это делает код неочевидным и затрудняет его чтение.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V602. |
Анализатор обнаружил потенциальную ошибку, связанную с неправильным использованием конструктора. Программисты часто ошибаются, пытаясь явно вызвать конструктор для инициализации объекта.
Рассмотрим типичный пример, взятый из реального приложения:
class CSlideBarGroup
{
public:
CSlideBarGroup(CString strName, INT iIconIndex,
CListBoxST* pListBox);
CSlideBarGroup(CSlideBarGroup& Group);
...
};
CSlideBarGroup::CSlideBarGroup(CSlideBarGroup& Group)
{
CSlideBarGroup(Group.GetName(), Group.GetIconIndex(),
Group.GetListBox());
}
В классе есть два конструктора. Для сокращения размера исходного кода, программист решил вызвать один конструктор из другого. Но этот код делает совсем не то, что ожидает разработчик.
Происходит следующее. Создаётся новый неименованный объект типа CSlideBarGroup и тут же разрушается. В результате поля класса остаются неинициализированными.
Правильным вариантом будет создать функцию инициализации и вызывать её из конструкторов. Корректный код:
class CSlideBarGroup
{
void Init(CString strName, INT iIconIndex,
CListBoxST* pListBox);
public:
CSlideBarGroup(CString strName, INT iIconIndex,
CListBoxST* pListBox)
{
Init(strName, iIconIndex, pListBox);
}
CSlideBarGroup(CSlideBarGroup& Group)
{
Init(Group.GetName(), Group.GetIconIndex(),
Group.GetListBox());
}
...
};
Если очень хочется вызвать именно конструктор, то это можно записать следующим образом:
CSlideBarGroup::CSlideBarGroup(CSlideBarGroup& Group)
{
this->CSlideBarGroup::CSlideBarGroup(
Group.GetName(), Group.GetIconIndex(), Group.GetListBox());
}
Другой аналогичный вариант:
CSlideBarGroup::CSlideBarGroup(CSlideBarGroup& Group)
{
new (this) CSlideBarGroup(
Group.GetName(), Group.GetIconIndex(),
Group.GetListBox());
}
Приведенные примеры являются очень опасным кодом, и нужно хорошо понимать, как они работают!
Таким кодом можно нанести больше вреда, чем пользы. Рассмотрим пример, где такой вызов конструктора допустим, а где нет.
class SomeClass
{
int x,y;
public:
SomeClass() { SomeClass(0,0); }
SomeClass(int xx, int yy) : x(xx), y(yy) {}
};
Код содержит ошибку. В конструкторе 'SomeClass() ' создается временный объект. В результате поля 'x' и 'y' остаются неинициализированными. Исправить код можно так:
class SomeClass
{
int x,y;
public:
SomeClass() { new (this) SomeClass(0,0); }
SomeClass(int xx, int yy) : x(xx), y(yy) {}
};
Этот код будет корректно работать. Код безопасен и работает, так как класс содержит простые типы данных и не наследуется от других классов. В этом случае двойной вызов конструктора ничем не грозит.
Рассмотрим другой код, где явный вызов конструктора приводит к ошибке:
class Base
{
public:
char *ptr;
std::vector vect;
Base() { ptr = new char[1000]; }
~Base() { delete [] ptr; }
};
class Derived : Base
{
Derived(Foo foo) { }
Derived(Bar bar) {
new (this) Derived(bar.foo);
}
}
Когда мы вызываем конструктор "new (this) Derived(bar.foo);", объект Base уже создан и поля инициализированы. Повторный вызов конструктора приведет к двойной инициализации. В 'ptr' запишем указатель на вновь выделенный участок памяти. В результате получаем утечку памяти. К чему приведет двойная инициализация объекта типа std::vector, вообще предсказать сложно. Ясно одно. Такой код недопустим.
В заключении ещё раз хочется отметить, что лучше явно не вызывать конструктор, а создать функцию инициализации. Явный вызов конструктора требуется только в крайне редких случаях.
Новый стандарт позволяет вызывать одни конструкторы класса из других (так называемая делегация). Это позволяет писать конструкторы, использующие поведение других конструкторов без внесения дублирующего кода.
Пример корректного кода:
class MyClass {
int m_x;
public:
MyClass(int X) : m_x(X) {}
MyClass() : MyClass(33) {}
};
Конструктор MyClass без аргументов вызывает конструктор того же класса с целочисленным аргументом.
В C++03 объект считается до конца созданным, когда его конструктор завершает выполнение. В C++11 после выполнения хотя бы одного делегирующего конструктора остальные конструкторы будут работать уже над полностью сконструированным объектом. Несмотря на это объекты производного класса начнут конструироваться только после выполнения всех конструкторов базовых классов.
Дополнительная информация
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V603. |
Анализатор обнаружил потенциальную ошибку в конструкции, образующий цикл. Цикл подозрителен тем, что количество итераций, которое он выполняет, равно sizeof(указатель). Высока вероятность, что количество итераций должно соответствовать размеру массива, на который ссылается указатель.
Рассмотрим, как может возникнуть подобная ошибка. В начале, программа выглядела так:
char A[N];
for (size_t i=0; i < sizeof(A); ++i)
A[i] = 0;
Затем, код программы менялся и массив 'A' стал иметь переменный размер. Код стал некорректен:
char *A = (char *)malloc(N);
for (size_t i=0; i < sizeof(A); ++i)
A[i] = 0;
Теперь выражение "sizeof(A)" возвращает размер указателя, а не размер массива.
Корректный вариант кода:
char *A = (char *)malloc(N);
for (size_t i=0; i < N; ++i)
A[i] = 0;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V604. |
Анализатор обнаружил потенциальную ошибку в выражении, где беззнаковая переменная сравнивается с отрицательным числом. Это достаточно редкая ситуация и не всегда такое сравнение является ошибкой. Однако предупреждение V605, это хороший повод, чтобы посмотреть и проверить код.
Пример кода, где будет выдано предупреждение V605:
unsigned u = ...;
if (u < -1)
{ ... }
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V605. |
Анализатор обнаружил потенциальную ошибку, найдя в коде лишнюю лексему. Чаще всего, такие "потерянные" лексемы появляются в коде, когда забыли написать ключевое слово return.
Рассмотрим пример:
bool Run(int *p)
{
if (p == NULL)
false;
...
}
Здесь случайно забыли написать "return". Код компилируется, но не имеет практического смысла.
Исправленный вариант:
bool Run(int *p)
{
if (p == NULL)
return false;
...
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V606. |
Анализатор обнаружил потенциальную ошибку, найдя в коде лишнее выражение. Чаще всего, такие "потерянные" выражения появляются в коде, когда забыли написать ключевое слово return или в процессе неаккуратного рефакторинга кода.
Рассмотрим пример:
void Run(int &a, int b, int c, bool X)
{
if (X)
a = b + c;
else
b - c;
}
Из-за опечатки текст программы не закончен. Код компилируется, но не имеет практического смысла.
Исправленный вариант:
void Run(int &a, int b, int c, bool X)
{
if (X)
a = b + c;
else
a = b - c;
}
Иногда "потерянные" выражения имеют практический смысл. Например, анализатор не будет выдавать предупреждение на следующий код:
struct A {};
struct B : public A {};
...
void Foo(B *p)
{
static_cast<A*>(p);
...
}
Здесь выражение "static_cast<A*>(p);" проверяет, что класс 'B' наследуется от класса 'A'. Если это не так, то произойдет ошибка компиляции.
В качестве другого примера, можно привести подавление предупреждения компилятора о неиспользуемых переменных:
void Foo(int a, int b)
{
a, b;
}
Здесь анализатор не выдаст предупреждение V607.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V607. |
Анализатор обнаружил повторяющиеся последовательности, состоящие из операторов явного приведения типов. Как правило, подобный код появляется из-за опечаток и не приводит к возникновению ошибок. Однако разумно проверить фрагменты кода, где анализатор выдает предупреждение V608. Возможно, ошибка всё-таки есть. Или, по крайней мере, код можно упростить.
Рассмотрим пример:
m_hIcon = AfxGetApp()->LoadStandardIcon(
MAKEINTRESOURCE(IDI_ASTERISK));
Анализатор выдаст предупреждение: V608 "Recurring sequence of explicit type casts: (LPSTR)(ULONG_PTR)(WORD) (LPSTR)(ULONG_PTR)(WORD)."
Давайте разберемся, откуда взялись две цепочки "(LPSTR)(ULONG_PTR)(WORD)".
Константное значение IDI_ASTERISK представляет собой макрос вида:
#define IDI_ASTERISK MAKEINTRESOURCE(32516)
Это значит, что приведенный выше код эквивалентен этому:
m_hIcon = AfxGetApp()->LoadStandardIcon(
MAKEINTRESOURCE(MAKEINTRESOURCE(32516)));
Макрос MAKEINTRESOURCE разворачивается в (LPSTR)((DWORD)((WORD)(i))). В результате получается следующая последовательность:
m_hIcon = AfxGetApp()->LoadStandardIcon(
(LPSTR)((DWORD)((WORD)((LPSTR)((DWORD)((WORD)((32516))))))
);
Этот код будет корректно работать, но он избыточен. Код можно переписать без лишних приведений типов:
m_hIcon = AfxGetApp()->LoadStandardIcon(IDI_ASTERISK);
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, когда может произойти деление на ноль.
Рассмотрим пример:
for (int i = -10; i != 10; ++i)
{
Foo(X / i);
}
В процессе выполнения цикла, переменная 'i' примет значение, равное 0. В этот момент произойдёт деление на 0. Чтобы исправить ситуацию, необходимо специально обработать случай, когда итератор 'i' равен нулю.
Исправленный вариант:
for (int i = -10; i != 10; ++i)
{
if (i != 0)
Foo(X / i);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки деления на ноль. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V609. |
Анализатор обнаружил операцию сдвига, которая приводит к неопределенному или к неуточненному поведению (undefined behaviour/unspecified behavior).
Стандарт Си++11 описывает работу операторов сдвига следующим образом:
"The shift operators << and >> group left-to-right.
shift-expression << additive-expression
shift-expression >> additive-expression
The operands shall be of integral or unscoped enumeration type and integral promotions are performed.
1. The type of the result is that of the promoted left operand. The behavior is undefined if the right operand is negative, or greater than or equal to the length in bits of the promoted left operand.
2. The value of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are zero-filled. If E1 has an unsigned type, the value of the result is E1 * 2^E2, reduced modulo one more than the maximum value representable in the result type. Otherwise, if E1 has a signed type and non-negative value, and E1*2^E2 is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.
3. The value of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a non-negative value, the value of the result is the integral part of the quotient of E1/2^E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined."
Приведем примеры, которые приводят к undefined или unspecified behavior:
int A = 1;
int B;
B = A << -3; // undefined behavior
B = A << 100; // undefined behavior
B = -1 << 5; // undefined behavior
B = -1 >> 5; // unspecified behavior
Конечно, это упрощенные примеры. В реальных программах ситуации сложнее. Рассмотрим практический пример:
SZ_RESULT
SafeReadDirectUInt64(ISzInStream *inStream, UInt64 *value)
{
int i;
*value = 0;
for (i = 0; i < 8; i++)
{
Byte b;
RINOK(SafeReadDirectByte(inStream, &b));
*value |= ((UInt32)b << (8 * i));
}
return SZ_OK;
}
Функция пытается побайтно прочитать 64-битное значение. К сожалению, у неё это не получится, если число было больше 0x00000000FFFFFFFF. Обратите внимание на сдвиг "(UInt32)b << (8 * i)". Размер левого операнда составляет 32 бита. При этом сдвиг происходит от 0 до 56 бит. На практике это приведёт к тому, что старшая часть 64-битного значения останется заполнена нулями. Теоретически здесь вообще имеет место неопределенное поведение и результат непредсказуем.
Корректный код должен выглядеть так:
*value |= ((UInt64)b << (8 * i));
Чтобы получить больше информации по рассмотренному вопросу предлагаю познакомиться со статьёй "Не зная брода, не лезь в воду. Часть третья".
Рассмотрим более подробно ситуацию, когда левый операнд отрицателен. Как правило, создаётся впечатление, что такой код всегда работает правильно. Можно подумать, что хотя это неопределённое поведение (undefined behavior), фактически все компиляторы ведут себя одинаковым образом. Это не так. Правильнее говорить, что большинство компиляторов ведут себя одинаковым образом. Если вас заботит переносимость кода, вы не должны использовать сдвиги отрицательных значений.
Подкрепим свои слова примером. Неожиданный результат можно получить, используя компилятор GCC для микропроцессора MSP430. Здесь описывается подобная ситуация. Хотя программист заявляет, что это ошибка компилятора, фактически мы имеем тот самый случай, когда компилятор ведёт себя по-другому.
Тем не менее, мы понимаем желание программистов отключить предупреждение для случаев, когда левый операнд отрицателен. Для этого можно вписать где-то в текст программы специальный комментарий:
//-V610_LEFT_SIGN_OFF
Этот комментарий следует вписать в заголовочный файл, который включается во все другие файлы. Например, таким файлом может быть "stdafx.h". Если вписать этот комментарий в "*.cpp" файл, то он будет действовать только для этого файла.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V610. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что память может выделятьcя и освобождаться несовместимыми между собой способами.
Например, анализатор предупредит, если память выделена с помощью оператора 'new', а освобождается с помощью функции 'free'.
Рассмотрим пример некорректного кода:
int *p = (int *)malloc(sizeof(int) * N);
...
...
delete [] p;
Исправленный вариант:
int *p = (int *)malloc(sizeof(int) * N);
...
...
free(p);
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V611. |
Анализатор обнаружил подозрительный цикл. В теле цикла используется один из следующих операторов: break, continue, return, goto. Эти операторы выполняются всегда, без каких либо условий.
Рассмотрим соответствующие примеры:
do {
X();
break;
} while (Foo();)
for (i = 0; i < 10; i++) {
continue;
Foo();
}
for (i = 0; i < 10; i++) {
x = x + 1;
return;
}
while (*p != 0) {
x += *p++;
goto endloop;
}
endloop:
Показанные выше примеры циклов конечно искусственны и малоинтересны. Давайте рассмотрим фрагмент кода, найденный в одном из настоящих приложений. Для большей наглядности код функции сокращён.
int DvdRead(....)
{
....
for (i=lsn; i<(lsn+sectors); i++){
....
// switch (mode->datapattern){
// case CdSecS2064:
((u32*)buf)[0] = i + 0x30000;
memcpy_fast((u8*)buf+12, buff, 2048);
buf = (char*)buf + 2064; break;
// default:
// return 0;
// }
}
....
}
Часть строк в функции закомментировано. Беда в том, что забыли закомментировать оператор "break".
Когда комментариев не было, "break" был внутри тела "switch". Когда "switch" закомментировали, оператор "break" стал досрочно завершать цикл. В результате тело цикла выполняется только один раз.
Корректный вариант кода:
buf = (char*)buf + 2064; // break;
Следует отметить, что диагностическое правило V612 достаточно сложно. Учитывается множество ситуаций, когда использование оператора break/continue/return/goto совершенно корректно. Рассмотрим для примера несколько ситуаций, когда предупреждение V612 выводиться не будет.
1) Наличие условия.
while (*p != 0) {
if (Foo(p))
break;
}
2) Специальные приёмы, как правило, используемые в макросах:
do { Foo(x); return 1; } while(0);
3) Обход оператора 'continue' с помощью 'goto':
for (i = 0; i < 10; i++) {
if (x == 7) goto skipcontinue;
continue;
skipcontinue: Foo(x);
}
Возможны и другие приёмы, которые используются на практике, но про которые мы не знаем. Если вы заметили, что анализатор выдаёт ложное предупреждение V612, просим написать нам и прислать соответствующие примеры. Мы изучим их и постараемся реализовать исключения для подобных случаев.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V612. |
Анализатор обнаружил потенциальную ошибку в коде, выделяющем память. Указатель, который возвращается функцией 'malloc' или аналогичной ей, складывается с каким-то числом. Это очень подозрительно и высока вероятность, что в коде имеется опечатка.
Рассмотрим пример:
a = ((int *)(malloc(sizeof(int)*(3+5)))+2);
В выражении много лишних скобок и вероятно программист в них запутался. Давайте упростим этот код для наглядности:
a = (int *)malloc(sizeof(int)*8);
a += 2;
Очень странно, прибавлять к указателю число 2. Даже если так надо и код правильный, он очень опасен. Например, очень легко забыть, что освобождать память надо так: "free(a - 2);".
Корректный вариант кода:
a = (int *)malloc(sizeof(int)*(3+5+2));
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил использование неинициализированной переменной. Использование неинициализированной переменной приводит к непредсказуемым результатам. Опасность подобных дефектов заключается в том, что они могут не проявляться годами, пока благодаря удачному стечению обстоятельств в неинициализированных переменных оказываются подходящие значения.
Рассмотрим простейший пример:
int Aa = Get();
int Ab;
if (Ab) // Ab - uninitialized variable
Ab = Foo();
else
Ab = 0;
Будет или нет вызвана функция Foo(), зависит от стечения различных обстоятельств. Как правило, ошибки использования неинициализированных переменных, возникают из-за опечаток. Например, может оказаться, что в этом месте следовало использовать другую переменную. Корректный вариант кода:
int Aa = Get();
int Ab;
if (Aa) // OK
Ab = Foo();
else
Ab = 0;
Предупреждение V614 выдается не только при использовании простых типов. Анализатор может выдавать предупреждение для переменных типа класс, которые имеют конструктор и, по сути, являются инициализированными. Однако их использование без предварительного присваивания не имеет смысла. Примером таких классов являются умные указатели и итераторы.
Рассмотрим примеры:
std::auto_ptr<CLASS> ptr;
UsePtr(ptr);
std::list<T>::iterator it;
*it = X;
Корректный вариант кода:
std::auto_ptr<CLASS> ptr(Get());
UsePtr(ptr);
std::list<T>::iterator it;
it = Get();
*it = X;
Бывает, что анализатор выдаёт ложные сообщения V614. Но иногда в этом виноват сам программист, написавший коварный код. Рассмотрим пример кода, взятый из реального приложения:
virtual size_t _fread(const void *ptr, size_t bytes){
size_t ret = ::fread((void*)ptr, 1, bytes, fp);
if(ret < bytes)
failbit = true;
return ret;
}
int read32le(uint32 *Bufo, EMUFILE *fp)
{
uint32 buf;
if(fp->_fread(&buf,4)<4) // False alarm: V614
return 0;
....
}
Обратите внимание, что буфер, куда читаются данные из файла, объявлен как "const void *ptr". Чтобы код компилировался, используется явное приведение указателя к типу "(void*)". Неизвестно, что побудило программиста написать такой код. Бессмысленный квалификатор "const" путает анализатор. Анализатор считает, что функция _fread() будет использовать переменную 'buf' только для чтения. Так как переменная 'buf' не инициализирована, анализатор выдаёт предупреждение.
Этот код работает. Однако его нельзя назвать хорошим. Рационально будет его переписать. Во-первых, код станет короче и понятней. Во-вторых, исчезнет предупреждение V614.
Исправленный вариант кода:
virtual size_t _fread(void *ptr, size_t bytes){
size_t ret = ::fread(ptr, 1, bytes, fp);
if(ret < bytes)
failbit = true;
return ret;
}
Есть ещё одна ситуация, когда предупреждение V614 может показаться ложным. Рассмотрим следующий синтетический пример:
std::shared_ptr<foo> GetFoo()
{
std::shared_ptr<foo> Bar;
return Bar; // V614
}
Здесь создается умный указатель 'Bar' типа 'std::shared_ptr', для которого будет вызван конструктор по умолчанию. Поэтому 'Bar' всегда будет инициализирован 'nullptr'. Анализатор считает опасным использование умных указателей, созданных конструктором по умолчанию. Тем не менее, такой код имеет право на жизнь. Есть несколько способов убрать подобные предупреждения анализатора.
Можно поправить код следующим образом:
std::shared_ptr<foo> GetFoo()
{
std::shared_ptr<foo> Bar { nullptr };
return Bar; // no V614
}
Такой код лучше читается, потому что теперь сразу понятно, что функция 'GetFoo' возвращает объект типа 'std::shared_ptr', содержащий нулевой указатель. В этом случае ревьюверу будет более очевидно, что предполагается именно нулевой указатель. Такая запись также является знаком анализатору, что программист знает, что он делает, и нулевой указатель возвращается осознанно, а не по ошибке.
Однако, если анализатор выдал на вашем коде много подобных предупреждений, и вы не хотите их видеть, то можно воспользоваться специальным комментарием:
//-V614_IGNORE_SMART_POINTERS
Этот комментарий следует вписать в заголовочный файл, который включается во все другие файлы. Например, таким файлом может быть "stdafx.h". Если вписать этот комментарий в "*.с" или "*.cpp" файл, то он будет действовать только для этого файла.
В ином случае можно воспользоваться механизмом подавления ложных срабатываний.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V614. |
Анализатор обнаружил подозрительное приведение типов указателей. Подобными подозрительными ситуациями является, когда указатель на float пытаются привести к указателю на double или наоборот. Дело в том, что типы float и double имеют различный размер и подобное приведение типов, скорее всего, свидетельствует об ошибке.
Рассмотрим простейший пример:
float *A;
double* B = (double*)(A);
Несоответствие размеров приводимых типов приведет к тому, что 'B' будет указывать на некорректный для типа double формат числа. Такие ошибки приведения типов указателей возникают из-за опечаток или невнимательности. Например, может оказаться, что в этом месте следует использовать другой тип данных или другой указатель.
Корректный вариант кода:
double *A;
double* B = A;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V615. |
Анализатор обнаружил использование нулевой константы в битовой операции И ( & ). Результатом такого выражения всегда будет нулевое значение. Это может привести к неправильной логике работы программы, если такое выражение используется в условиях или циклах.
Рассмотрим простейший пример:
enum { FirstValue, SecondValue };
int Flags = GetFlags();
if (Flags & FirstValue)
{...}
Выражения в условии оператора 'if' всегда равно нулю. Это приводит к неверной логике выполнения программы. Как правило, ошибки использования нулевых констант в битовых операциях, возникают из-за опечаток или некорректного объявления констант. Например, может оказаться, что в этом месте следовало использовать другую константу. Корректный вариант кода:
enum { FirstValue, SecondValue };
int Flags = GetFlags();
if (Flags & SecondValue)
{...}
Так же корректным может быть вариант, когда константа будет объявлена ненулевой. Пример кода:
enum { FirstValue = 1, SecondValue };
int Flags = GetFlags();
if (Flags & FirstValue)
{...}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V616. |
Анализатор обнаружил использование ненулевой константы в битовой операции ИЛИ ( | ). Результатом такого выражения всегда будет ненулевое значение. Это может привести к неправильной логике работы программы, если данное выражение будет использоваться в условиях или циклах.
Рассмотрим простейший пример:
enum { FirstValue, SecondValue };
int Flags = GetFlags();
if (Flags | SecondValue)
{...}
Выражение в условии оператора 'if' всегда истинно. Как правило, ошибки использования ненулевых констант в битовых операциях, возникают из-за опечаток. Например, может оказаться, что в этом месте следовало использовать другую битовую операцию, например И ( & ). Корректный вариант кода:
enum { FirstValue, SecondValue };
int Flags = GetFlags();
if (Flags & SecondValue)
{...}
Рассмотрим пример кода, найденный анализатором в реальном приложении:
#define PSP_HIDEHEADER 0x00000800
BOOL CResizablePageEx::NeedsRefresh(....)
{
if (m_psp.dwFlags | PSP_HIDEHEADER)
return TRUE;
...
return
CResizableLayout::NeedsRefresh(layout, rectOld, rectNew);
}
Очевидно, что оператор 'if' будет всегда выполнять ветку 'return TRUE;', что не корректно. Исправленный вариант кода:
#define PSP_HIDEHEADER 0x00000800
BOOL CResizablePageEx::NeedsRefresh(....)
{
if (m_psp.dwFlags & PSP_HIDEHEADER)
return TRUE;
...
return
CResizableLayout::NeedsRefresh(layout, rectOld, rectNew);
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V617. |
Анализатор обнаружил, что вызов функции форматного вывода может привести к некорректному результату. Более того, подобный код может стать объектом для атаки (подробнее см. эту статью).
Вывод строки осуществляется напрямую без использования спецификатора "%s". В результате, если в строке случайно или преднамеренно встретиться управляющий символ это приведёт к сбою в работе программы. Рассмотрим простейший пример:
char *p;
...
printf(p);
Вызов функции printf(p) некорректен, поскольку отсутствует форматная строка вида "%s". Если в строке 'p' встретится спецификатор формата, то такой вывод скорее всего окажется некорректным. Безопасный вариант кода:
char *p;
...
printf ("%s", p);
Предупреждение V618 может показаться несущественным. Но на самом деле это очень важный момент в создании качественных и надёжных программ.
Учтите, что совершенно неожиданно в строке вдруг могут встретиться спецификаторы формата (%i, %p и так далее). Это может произойти случайно, когда пользователь введёт некорректные входные данные. Это может произойти умышленно, когда некорректные данные будут поданы специально. Отсутствие спецификатора "%s" может привести к падению программы или в выводе куда-то приватных данных. Прежде чем отключить диагностику V618 ещё раз настаиваем на том, чтобы обязательно прочитать статью "Не зная брода, не лезь в воду. Часть вторая". Исправления в коде будут не так велики, чтобы проигнорировать данный вид дефекта.
Примечание. Анализатор старается не выдавать предупреждение V618, когда вызов функции не может привести к каким либо плохим последствиям. Пример, где анализатор не выдаёт предупреждение:
printf("Hello!");
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.), Ошибки использования форматной строки. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V618. |
Анализатор обнаружил, что к переменной, объявленной как массив данных, применяется оператор '->'. Подобный код может свидетельствовать о некорректном использовании структур данных, приводящее к неправильному заполнению полей структуры.
Рассмотрим пример некорректного кода:
struct Struct {
int r;
};
...
Struct ms[10];
for (int i = 0; i < 10; i++)
{
ms->r = 0;
...
}
Такое использование некорректно, поскольку будет проинициализирован только первый элемент массива. Возможно, произошла опечатка или должна быть использована другая переменная. Корректный вариант кода:
Struct ms[10];
for (int i = 0; i < 10; i++)
{
ms[i].r = 0;
...
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V619. |
Анализатор обнаружил, что к переменной типа указатель прибавляется выражение, содержащее оператор sizeof(T). Такое использование может свидетельствовать о некорректной адресной арифметике.
Рассмотрим простейший пример:
int *p;
size_t N = 5;
...
p = p + sizeof(int)*N;
Такое использование некорректно. Ожидается, что мы переместимся на N элементов структуры данных. Вместо этого происходит смещение на 20 элементов, поскольку sizeof(int) имеет значение 4 в 32-битных программах. В результате мы получим: "p = p + 20;". Возможно допущена опечатка или иная ошибка. Корректный вариант кода:
int *p;
size_t N = 5;
...
p = p + N;
Примечание. Анализатор считает код корректным, если в нём работают с типом char. Рассмотрим пример, где анализатор не выдаёт предупреждение:
char *c;
size_t N = 5;
...
c = c + sizeof(float)*N;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V620. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' используются странные начальное и конечное значения счетчика. Это может приводить к некорректному выполнению цикла и нарушению логики работы программы.
Рассмотрим пример:
signed char i;
for (i = -10; i < 100; i--)
{
...
};
Возможно, произошла опечатка и перепутаны начальные и конечные значения. Так же ошибка может возникнуть, если перепутаны операторы '++' и '--'.
Корректный вариант кода:
for (i = -10; i < 100; i++)
{
...
};
Также корректным будет следующий код:
for (i = 100; i > -10; i--)
{
...
};
Рассмотрим код, найденный анализатором в реальном приложении:
void CertificateRequest::Build()
{
...
uint16 authCount = 0;
for (int j = 0; j < authCount; j++) {
int sz = REQUEST_HEADER + MIN_DIS_SIZE;
...
}
}
Переменная 'authCount' инициализируется неправильным значением или здесь вообще должна быть использована другая переменная.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V621. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в блоке оператора 'switch' первым оператором не является оператор 'case'. Это приводит к тому, что фрагмент кода никогда не получит управление.
Рассмотрим пример:
char B = '0';
int I;
...
switch(I)
{
B = '1';
break;
case 2:
B = '2';
break;
default:
B = '3';
break;
}
Присваивание "B = '1';" никогда не будет выполнено. Корректный вариант кода:
switch(I)
{
case 1:
B = '1';
break;
case 2:
B = '2';
break;
default:
B = '3';
break;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V622. |
Анализатор обнаружил возможную ошибку при работе с тернарным оператором '?:'. Если при работе с оператором '?:' совместно используются объект типа класс и любой другой тип, который может быть преобразован к данному классу, то создаются временные объекты. Временные объекты будут удалены по завершению оператора '?:'. Ошибка возникает, если мы при этом сохраняем результат в переменную типа указатель.
Рассмотрим пример:
CString s1(L"1");
wchar_t s2[] = L"2";
bool a = false;
...
const wchar_t *s = a ? s1 : s2;
В результате выполнения этого кода переменная 's' будет указывать на данные, находящиеся внутри временного объекта. Беда в том, что этот объект уже уничтожен!
Корректный вариант кода:
wchar_t s1[] = L"1";
wchar_t s2[] = L"2";
bool a = false;
...
const wchar_t *s = a ? s1 : s2;
Другой вариант корректного кода:
CString s1(L"1");
wchar_t s2[] = L"2";
bool a = false;
...
CString s = a ? s1 : s2;
Предупреждение V623 требует со стороны программиста повышенного внимания. Беда в том, что ошибки данного типа хорошо прячутся. Код, содержащий подобные ошибки может успешно работать многие годы. Но это только иллюзия, что он работает. На самом деле используется освобождённая память. То, что в памяти находятся корректные данные - это только везение. Поведение программы может поменяться в любой момент. Это может произойти при смене версии компилятора. Это может произойти, после рефакторинга кода, когда появляется новый объект, использующий ту же область памяти. Рассмотрим это на примере.
Напишем, скомпилируем и запустим следующий код:
bool b = false;
CComBSTR A("ABCD");
wchar_t *ptr = b ? A : L"Test OK";
wcout << ptr << endl;
Этот код мы скомпилировали с помощью Visual Studio 2010 и он распечатал "Test OK". Может показаться, что всё работает правильно. Но внесём в код небольшую правку:
bool b = false;
CComBSTR A("ABCD");
wchar_t *ptr = b ? A : L"Test OK";
wchar_t *tmp = b ? A : L"Error!";
wcout << ptr << endl;
Кажется строчка, где инициализируется переменная 'tmp' ничего не изменит. Но это не так. Теперь программа печатает на экран: "Error!".
Дело в том, что новый временный объект использовал ту же области памяти, что и предыдущий. Кстати, следует учитывать, что данный код по стечению обстоятельств вполне может работать. Всё зависит от везения и фазы луны. Предугадать, где будут создаваться временные объекты невозможно. Поэтому не отказывайтесь от правки кода, основываясь на утверждении "этот код правильно работал несколько лет и значит, ошибки в нём нет".
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V623. |
Анализатор обнаружил возможную ошибку при работе с константами типа double. Возможно, для математических расчетов используются константы недостаточной точности или константа записана с ошибкой.
Рассмотрим пример:
double pi = 3.141592654;
Такая запись не совсем корректна и лучше использовать математические константы из заголовочного файла 'math.h'. Корректный вариант кода:
#include <math.h>
...
double pi = M_PI;
Анализатор не считает ошибочной явную запись констант в формате 'float'. Это связано с тем, что тип 'float' имеет меньше значащих разрядов, по сравнению с типом 'double'. Пример кода:
float f = 3.14159f; //ok
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V624. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' совпадают начальное и конечное значения счетчика. Такое использование оператора 'for' приведет к тому, что цикл не будет выполнен ни разу или выполнен только один раз.
Рассмотрим пример:
void beginAndEndForCheck(size_t beginLine, size_t endLine)
{
for (size_t i = beginLine; i < beginLine; ++i)
{
...
}
Тело цикла никогда не выполняется. Скорее всего, произошла опечатка и следует заменить "i < beginLine" на корректное выражение "i < endLine". Корректный вариант кода:
for (size_t i = beginLine; i < endLine; ++i)
{
...
}
Другой пример:
for (size_t i = A; i <= A; ++i)
...
Тело этого цикла будет выполнено только один раз. Скорее всего, это не то, что задумывал программист.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V625. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что вместо точки запятой ';' случайно написана запятая ','. Такая опечатка может привести к неправильной логике выполнения программы.
Рассмотрим пример:
int a;
int b;
...
if (a == 2)
a++,
b = a;
В результате выражение "b = a;" будет выполняться только когда верно условие оператора 'if', скорее всего произошла опечатка и следует заменить ',' на ';'. Корректный вариант кода:
if (a == 2)
a++;
b = a;
Анализатор не будет обнаруживать ошибку, если форматирование фрагмента кода отражает преднамеренное использовать оператор ','. Пример кода:
if (a == 2)
a++,
b = a;
if (a == 2)
a++, b = a;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V626. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что аргументом оператор 'sizeof' является макрос, раскрывающийся в число. Такое использование оператора может привести к выделению некорректного размера памяти и иным ошибкам.
Рассмотрим пример:
#define NPOINT 100
...
char *point = (char *)malloc(sizeof(NPOINT));
В результате выполнения данного кода будет выделен недостаточный объем памяти. Корректный вариант кода:
#define NPOINT 100
...
char *point = (char *)malloc(NPOINT);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V627. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что два последовательно идущих оператора 'if' оказались разделены закомментированной строкой. Высока вероятность, что неаккуратно был закомментирован фрагмент кода. Неаккуратность привела к тому, что существенно изменилась логика работы программы.
Рассмотрим пример:
if(!hwndTasEdit)
//hwndTasEdit = getTask()
if(hwndTasEdit)
{
...
}
Программа потеряла смысл. Условие второго оператора 'if' никогда не выполняется. Корректный вариант кода:
//if(!hwndTasEdit)
//hwndTasEdit = getTask()
if(hwndTasEdit)
{
...
}
Анализатор не выдает ошибки в коде, где форматирование кода соответствует преднамеренному использованию последовательно двух операторов 'if', разделенных строкой комментария. Пример кода:
if (Mail == ready)
// comment
if (findNewMail)
{
...
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V628. |
Анализатор обнаружил потенциальную ошибку в выражении, содержащем операцию сдвига. В программе выполняется сдвиг 32-битного значения. Затем полученный 32-битный результат явно или неявно преобразуется в 64-битный тип.
Рассмотрим пример некорректного кода:
unsigned __int64 X;
X = 1u << N;
Данный код вызывает неопределенное поведение, если значение N больше 32. На практике это означает, что с помощью этого кода не получится записать в переменную 'X' значение более 0x80000000.
Код можно исправить, если тип левого аргумента будет 64-битным.
Исправленный вариант кода:
unsigned __int64 X;
X = 1ui64 << N;
Обратите внимание, что диагностика V629 не относится к 64-битным ошибкам. Под 64-битными ошибками понимаются те ситуации, когда 32-битная версия программы работает корректно, а 64-битная некорректно.
Рассматриваемая здесь ситуация приводит к ошибке как в 32-битной, так и в 64-битной программе. Поэтому диагностика V629 относится к правилам общего назначения.
Анализатор не будет выдавать предупреждение, если результат выражения со сдвигом укладывается в 32-битный тип. Это означает, что значащие биты не потеряны и код корректен.
Пример безопасного кода:
char W = 7;
long long Q = W << 10;
Этот код работает следующим образом. Вначале переменная 'W' расширяется до 32-битного типа 'int'. Затем происходит сдвиг и получается значение 0x00001C00. Это число помещается в 32-битный тип, а значит ошибки не возникнет. На последнем этапе это значение расширяется до 64-битного типа 'long long' и записывается в переменную 'Q'.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V629. |
Анализатор обнаружил потенциальную ошибку, связанную с использованием одной из функций динамического выделения памяти, такой как malloc, calloc, realloc. С выделенной памятью работают как с массивом объектов, имеющих конструктор или деструктор. При таком выделении памяти для класса не будет вызван конструктор. При освобождении памяти с помощью функции free не будет вызван деструктор. Это крайне подозрительно. Подобный код может привести к работе с неинициализированными переменными и другим ошибкам.
Рассмотрим пример некорректного кода:
class CL
{
int num;
public:
CL() : num(0) {...}
...
};
...
CL *pCL = (CL*)malloc(sizeof(CL) * 10);
В результате переменная 'num' будет не инициализирована. Конечно, можно вызвать конструктор для каждого объекта "вручную", но более правильным будет использование оператора 'new'.
Исправленный вариант:
CL *pCL = new CL[10];
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V630. |
Анализатор обнаружил потенциальную ошибку при вызове функции, предназначенную для работы с файлами. В одном из фактических аргументов в функцию передаётся абсолютный путь до файла или директории. Такое использование функции является опасным, поскольку могут встретиться случаи, когда данного пути не будет существовать на компьютере пользователя.
Рассмотрим пример некорректного кода:
FILE *text = fopen("c:\\TEMP\\text.txt", "r");
Более корректным будет вариант получения пути к файлу исходя из определенных условий.
Исправленный вариант кода:
string fullFilePath = GetFilePath() + "text.txt";
FILE *text = fopen(fullFilePath.c_str(), "r");
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V631. |
Анализатор обнаружил потенциальную ошибку, связанную с передачей подозрительного аргумента в функцию. В функцию был передан аргумент в формате числа с плавающей точкой, хотя ожидался целочисленный тип данных. Такое использование является некорректным, поскольку значение аргумента будет приведено к целочисленному типу.
Рассмотрим пример некорректного кода:
double buf[N];
...
memset(buf, 1.0, sizeof(buf));
Программист планировал заполнить массив значениями '1.0'. Однако этот код заполнит массив мусором.
Второй аргумент функции 'memset' имеет целочисленный тип. Этот аргумент определяет, каким значением заполнить каждый байт массива.
Произойдет приведение '1.0' к целочисленному значению '1'. Массив данных 'buf' будет заполнен побайтно единичными значениями. Такой результат отличается от заполнения каждого элемента массива значением '1.0'.
Исправленный вариант кода:
double buf[N];
...
for (size_t i = 0; i != N; ++i)
buf[i] = 1.0;
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку. Возможно вместо оператора '=!' следует написать '!=' или '==!'. Подобные ошибки чаще всего возникают из-за опечатки.
Рассмотрим пример некорректного кода:
int A, B;
...
if (A =! B)
{
...
}
С большой вероятностью здесь должна быть проверка, что переменная 'A' не равна 'B'. Если это так, то корректный вариант кода должен выглядеть следующим образом:
if (A != B)
{
...
}
Анализатор учитывает форматирование в выражении. Поэтому если действительно требуется выполнить присваивание, а не сравнение, необходимо указать, используя скобки или пробелы. Следующие примеры кода считаются анализатором корректными:
if (A = !B)
...
if (A=(!B))
...
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с тем, что операции сложения, вычитания, деления и умножения имеют более высокий приоритет, чем операции сдвига. Про это часто забывают. В результате выражение может давать совсем не тот результат, на который рассчитывал программист.
Рассмотрим пример некорректного кода:
int X = 1<<4 + 2;
Скорее всего, программист ожидал, что результат сдвига '1' на '4' будет сложен с '2'. Но согласно приоритету операций в языке Си/Си++ вначале произойдет сложение, а уже потом сдвиг.
Можно порекомендовать во всех выражениях с редко используемыми вами операторами, писать скобки. Даже если скобки окажутся лишними, это не страшно. Зато код станет более легким для чтения и понимания и будет меньше подвержен ошибкам.
Корректный вариант кода:
int X = (1<<4) + 2;
А как убрать ложное предупреждение, если действительно планировалась последовательность вычислений: сначала сложение, затем сдвиг?
Есть 3 варианта:
1) Самый плохой вариант. Можно использовать комментарий "//-V634" для подавления предупреждения в нужной строке.
int X = 1<<4 + 2; //-V634
2) Можно добавить дополнительные скобки:
int X = 1<<(4 + 2);
3) Можно уточнить ваши намерения, используя пробелы:
int X = 1 << 4+2;
Дополнительные ресурсы:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V634. |
Анализатор обнаружил потенциальную ошибку, связанную c выделением некорректного размера памяти для хранения строки в формате UNICODE.
Как правило, такая ошибка возникает, если для вычисления размера массива используется функция 'strlen' или 'wcslen'. Нередко полученное количество символов забывают умножить на sizeof(wchar_t). В результате в программе может возникнуть доступ за границу массива.
Рассмотрим пример некорректного кода:
wchar_t src[] = L"abc";
wchar_t *dst = (wchar_t *)malloc(wcslen(src) + 1);
wcscpy(dst, src);
В данном случае будет выделено всего 4 байта памяти. Так как тип 'wchar_t' имеет размер 2 или 4 байта в зависимости от модели данных, этой памяти может не хватить. Для исправления ошибки требуется умножить выражение внутри 'malloc' на 'sizeof(wchar_t)'.
Корректный вариант кода:
wchar_t *dst =
(wchar_t *)malloc((wcslen(src) + 1) * sizeof(wchar_t));
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V635. |
В выражении присутствует операция умножения или деления целочисленных типов данных. Полученное значение неявно преобразуется к типу с плавающей точкой. Обнаружив такую ситуацию, анализатор предупреждает о наличии потенциальной ошибки, которая может привести к переполнению или к вычислению неточного результата.
Рассмотрим возможные ошибки на примерах.
Ситуация первая. Переполнение.
int LX = 1000;
int LY = 1000;
int LZ = 1000;
int Density = 10;
double Mass = LX * LY * LZ * Density;
Мы хотим вычислить массу объекта, зная его плотность и размеры. Мы знаем, что результирующее значение может быть большим. Поэтому, переменная 'Mass' имеет тип 'double'. Однако этот код не учитывает, что перемножаются переменные типа 'int'. В результате, в правой части выражения произойдет целочисленное переполнение и результат будет некорректен.
Исправить ситуацию можно двумя способами. Можно изменить типы переменных:
double LX = 1000.0;
double LY = 1000.0;
double LZ = 1000.0;
double Density = 10.0;
double Mass = LX * LY * LZ * Density;
Другой способ - это использовать явное приведение типов:
int LX = 1000;
int LY = 1000;
int LZ = 1000;
int Density = 10;
double Mass = (double)(LX) * LY * LZ * Density;
Достаточно привести к типу 'double' только первую переменную. Поскольку операция умножения относится к лево-ассоциативным операторам, то вычисление будет происходить следующим образом: (((double)(LX) * LY) * LZ) * Density. В результате каждый из операндов перед умножением будет преобразовываться к типу 'double' и мы получим корректный результат.
P.S. Напомним, что вот так, делать неправильно: Mass = (double)(ConstMass) + LX * LY * LZ * Density. Выражение справа от оператора '=' будет иметь тип 'double'. Но перемножаться будут по-прежнему переменные типа 'int'.
Ситуация вторая. Потеря точности.
int totalTime = 1700;
int operationNum = 900;
double averageTime = totalTime / operationNum;
Программист может ожидать, что переменная 'averageTime' будет иметь значение '1.888(8)', однако при выполнении программы будет получен результат равный '1.0'. Это происходит потому, что операция деления выполняется с целочисленными типами и только затем приводится к типу с плавающей точкой.
Как и в предыдущем случае, ошибку можно исправить 2 способами.
Первый способ - изменить типы переменных:
double totalTime = 1700;
double operationNum = 900;
double averageTime = totalTime / operationNum;
Второй способ - использовать явное приведение типов.
int totalTime = 1700;
int operationNum = 900;
double averageTime = (double)(totalTime) / operationNum;
Примечание
Естественно, в некоторых случаях нужно произвести именно целочисленное деление. В таких случаях, чтобы скрыть ложное предупреждение, можно использовать комментарий вида:
//-V636
См. также: Документация. Подавление ложных предупреждений.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V636. |
Анализатор обнаружил возможную логическую ошибку в программе. Ошибка заключается в том, что два условных оператора, идущих последовательно, содержат взаимоисключающие условия.
Примеры взаимоисключающих условий:
Как правило, такая ошибка возникает вследствие опечатки или неудачного рефакторинга. В результате логика выполнения программы нарушается.
Рассмотрим пример некорректного кода:
if (A == B)
if (B != A)
B = 5;
В данном случае высказывание "B = 5;" никогда не будет выполнено. Скорее всего, в первом или втором условиях используется некорректная переменная. Следует посмотреть логику выполнения программы.
Корректный вариант кода:
if (A == B)
if (B != C)
B = 5;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V637. |
Анализатор обнаружил потенциальную ошибку, связанную с наличием внутри строки терминального нулевого символа.
Как правило, такая ошибка возникает вследствие опечатки. Например, последовательность "\0x0A" будет восприниматься как следующая последовательность из четырёх байт: { '\0', 'x', '0', 'A' }.
Если хочется задать код символа в шестнадцатеричном виде, то символ 'x' должен стоять сразу после символа '\'. Если написать "\0", то это будет воспринято как ноль (в формате восьмеричного числа). См. также:
Рассмотрим пример некорректного кода:
const char *s = "string\0x0D\0x0A";
Если попробовать распечатать эту строку, то управляющие символы для перевода строки использованы не будут. Функции вывода остановятся на символе конца строки '\0'. Для устранения этой ошибки следует заменить "\0x0D\0x0A" на "\x0D\x0A".
Корректный вариант кода:
const char *s = "string\x0D\x0A";
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V638. |
Анализатор обнаружил потенциальную ошибку, связанную c подозрительным вызовом функции, за которой следуют запятые и выражения. Возможно, эти выражения должны быть частью вызова функции.
Как правило, такая ошибка возникает, если происходит вызов функции внутри условного оператора и функция имеет аргументы по умолчанию. В таком случае легко по ошибке поставить закрывающую скобку не в том месте, где необходимо. Опасность такого рода ошибок в том, что код компилируется без ошибок и выполняется. Рассмотрим пример некорректного кода:
bool rTuple(int a, bool Error = true);
....
if (rTuple(exp), false)
{
....
}
Неправильно поставленная закрывающаяся скобка приведут сразу к двум ошибкам:
1) Аргумент 'Error' при вызове функции 'rTuple' будет равен 'true', хотя хотели 'false'.
2) Оператор запятая ',' возвращает значение правой части. Значит условие (rTuple(exp), false) будет всегда равно значению 'false'
Корректный вариант кода:
if (rTuple(exp, false))
{
....
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V639. |
Анализатор обнаружил потенциальную ошибку, связанную c тем, что форматирование кода, следующих за условным оператором, не соответствует логике выполнения программы. Высока вероятность, что пропущены открывающиеся и закрывающиеся фигурные скобки.
Рассмотрим пример некорректного кода:
if (a == 1)
b = c; d = b;
В данном случае присваивание 'd = b;' будет выполняться всегда, независимо от условия 'a == 1'.
Если код ошибочен, то ситуацию можно исправить, используя фигурные скобки. Корректный вариант кода:
if (a == 1)
{ b = c; d = b; }
Другой пример некорректного кода:
if (a == 1)
b = c;
d = b;
Для исправления ошибки так же следует использовать фигурные скобки. Корректный вариант кода:
if (a == 1)
{
b = c;
d = b;
}
Если код корректен, то чтобы исчезло предупреждение V640, следует отформатировать код следующим образом:
if (a == 1)
b = c;
d = b;
Нередко данный класс ошибок можно встретить в программах, активно использующих макросы. Рассмотрим ошибку, найденную в реальном приложении:
#define DisposeSocket(a) shutdown(a, 2); closesocket(a)
...
if (sockfd > 0)
(void) DisposeSocket(sockfd);
Вызов функции 'closesocket(a);' будет выполняться всегда. Это приведет к сбою, если переменная 'sockfd' окажется <= 0.
Можно исправить ошибку, используя в макросе фигурные скобки. Но лучше написать полноценную функцию. Код без макросов безопаснее и удобней при отладке.
Корректный пример кода может выглядеть следующим образом:
inline void DisposeSocket(int a) {
shutdown(a, 2);
closesocket(a);
}
...
if (sockfd > 0)
DisposeSocket(sockfd);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V640. |
Анализатор обнаружил потенциальную ошибку, связанную c приведением указателя на исходный буфер к указателю на другой тип, при этом размер исходного буфера некратен размеру одного элемента полученного типа. Рассмотрим два класса ошибок, допускаемых при приведениях типов.
Первый класс ошибок связан с выделением некорректного размера памяти для хранения элементов массива с помощью функций 'malloc', 'calloc', 'alloca', 'realloc' и т.д.
Как правило, такие ошибки возникают, если размер выделяемой памяти задается константой (константами в случае 'calloc'). Для правильного выделения памяти под 'N' элементов массива типа 'T' рекомендуется использовать оператор 'sizeof(T)'. В зависимости от функции, аллоцирующей память, конструкция может иметь следующий вид:
int *p = (int*)malloc(N * sizeof(int));
или
int *p = (int*)calloc(N, sizeof(int));
В результате неправильного выделения памяти в программе может возникнуть выход за границу массива.
Рассмотрим пример некорректного кода с использованием функции 'malloc':
int *p = (int*)malloc(70);
В данном случае будет выделено 70 байт памяти. Обращение к элементу 'p[17]' приведет к неопределенному поведению, поскольку произойдет выход за границу (необходимо 72 байта для корректного чтения 18-го элемента). Корректный вариант кода будет следующим:
p = (int*)malloc(72);
Также возможен случай, когда требуется выделить память для хранения 70 элементов. Корректным будет такой вариант кода:
p = (int*)malloc(70 * sizeof(int));
Рассмотрим пример некорректного кода из реального проекта с использованием функции 'calloc':
int data16len = MultiByteToWideChar(CP_UTF8,
0,
data,
datalen,
NULL,
0);
if (!data16)
{
data16 = (wchar_t*)calloc(data16len + 1, 1);
}
MultiByteToWideChar(CP_UTF8, 0, data, -1, data16, data16len);
В данном случае хотели создать буфер для сохранения широкой (wide) строки после конвертации из UTF-8 строки. Однако размер 'wchar_t' не равен 1 байту (Windows - 2 байта, Linux - 4 байта). Корректный вариант кода будет выглядеть следующим образом:
data16 = (wchar_t*)calloc(data16len + 1, sizeof(wchar_t));
Примечание к функции 'calloc'. Несмотря на то, что прототип функции выглядит следующим образом:
void* calloc(size_t num, size_t size );
некоторые программисты полагают, что размер выделяемой памяти равен num*size и часто меняют аргументы местами. Такой код может приводить к ошибкам. Цитата из документации: "Due to the alignment requirements, the number of allocated bytes is not necessarily equal to num*size.".
Второй класс ошибок связан с преобразованием указателя на объект типа 'A' к указателю на объект типа 'B'. Рассмотрим пример:
struct A
{
int a, b;
float c;
unsigned char d;
};
struct B
{
int a, b;
float c;
unsigned short d;
};
....
A obj1;
B *obj2 = (B*)&obj1; //V641
std::cout << obj2->d;
....
Как видно из примера, две структуры отличаются друг от друга последним полем. Поле 'd' у приведённых структур имеет разный размер типа. При преобразовании указателя 'A*' к 'B*' можно получить неопределенное поведение при обращении к полю 'd'. Отметим, что преобразование указателя 'B*' к 'A*' возможно (хотя это плохой код), и неопределенного поведения не будет.
Анализатор не будет выдавать предупреждения на преобразование указателя 'A*' к 'B*', если два класса (структуры) находятся в иерархии наследования.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V641. |
Анализатор обнаружил потенциальную ошибку, связанную c сохранением результата работы функции в переменную, которая занимает всего 8 или 16 бит. Для некоторых функций, которые возвращают статус типа 'int', это может быть недопустимо. Могут быть потеряны значащие биты.
Рассмотрим пример некорректного кода:
char c = memcmp(buf1, buf2, n);
if (c != 0)
{
...
}
Функция 'memcmp' возвращает следующие значения типа 'int':
Обратите внимание. "Больше 0", означает любые числа, а вовсе не 1. Этими числами могут быть: 2, 3, 100, 256, 1024, 5555, и так далее. Это значит, что этот результат нельзя поместить в переменную типа 'char'. Могут быть отброшены значащие биты, что приведет к нарушению логики выполнения программы.
Опасность такого рода ошибок заключается в том, что возвращаемое значение может зависеть от архитектуры и реализации конкретной функции на данной архитектуре. Например, программа будет корректно работать в 32-битном варианте и некорректно в 64-битном.
Корректный вариант кода:
int c = memcmp(buf1, buf2, n);
if (c != 0)
{
...
}
Возможно, кому-то данная опасность покажется надуманной. Однако такая ошибка послужила причиной серьезной уязвимости в MySQL/MariaDB до версий 5.1.61, 5.2.11, 5.3.5, 5.5.22. Суть в том, что при подключении пользователя MySQL /MariaDB вычисляется токен (SHA от пароля и хэша), который сравнивается с ожидаемым значением функцией 'memcmp'. На некоторых платформах возвращаемое значение может выпадать из диапазона [-128..127]. В итоге, в 1 случае из 256 процедура сравнения хэша с ожидаемым значением всегда возвращает значение 'true', независимо от хэша. В результате, простая команда на bash даёт злоумышленнику рутовый доступ к уязвимому серверу MySQL, даже если он не знает пароль. Причиной этому стал такой код в файле 'sql/password.c':
typedef char my_bool;
...
my_bool check(...) {
return memcmp(...);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V642. |
Анализатор обнаружил потенциальную ошибку, связанную c некорректным прибавлением символьной константы к указателю на строковый литерал.
Как правило, такая ошибка возникает в случае попытки объединения строкового литерала с символом.
Рассмотрим простейший пример некорректного кода:
std::string S = "abcd" + 'x';
Ожидалось, что будет получена строка "abcdx", однако в данном случае к указателю на строку "abcd" будет прибавлено значение 120. Это гарантировано приведет к выходу за границу строкового литерала. Для предотвращения такой ситуации, следует избегать подобных арифметических операций со строковыми и символьными переменными.
Корректный вариант кода:
std::string S = std::string("abcd") + 'x';
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V643. |
Анализатор обнаружил потенциальную ошибку, связанную c неправильным созданием объекта типа 'T'.
Как правило, такая ошибка возникает, если пропущен аргумент вызова конструктора данного типа. В таком случае вместо создания объекта нужного типа, получится объявление функции, возвращающей тип 'T'. Данная ошибка, как правило, возникает при использовании вспомогательных классов, упрощающих блокирование и разблокирование мьютексов. Например, в библиотеке 'Qt' таким классом является 'QMutexLocker', который упрощает работу с классом 'QMutex'.
Рассмотрим пример некорректного кода:
QMutex mutex;
...
QMutexLocker lock();
++objectVarCounter;
Опасность такого рода ошибок в том, что данный код компилируется и выполняется без ошибок. Однако требуемого результата получено не будет. То есть не происходит блокирование других потоков, использующих переменную 'objectVarCounter'. Поэтому выявление таких ошибок может занять много времени и сил.
Корректный вариант кода:
QMutex mutex;
...
QMutexLocker lock(&mutex);
++objectVarCounter;
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную c конкатенацией строк. Ошибка может приводить к переполнению буфера. Неприятность подобных ошибок заключается в том, что программа долгое время может работать стабильно, если на вход функции поступают только короткие строки.
Данному виду уязвимости подвержены такие функции, как 'strncat', 'wcsncat' и так далее [1].
Описание функции 'strncat':
char *strncat(
char *strDest,
const char *strSource,
size_t count
);
Где:
Функция 'strncat', пожалуй, одна из самых опасных строковых функций. Опасность возникает из-за того, что принцип её работы отличается от того, как представляют его себе программисты.
Третий аргумент указывает не размер буфера, а сколько ещё символов в него можно поместить. Вот цитата из описания этой функции в MSDN: "strncat does not check for sufficient space in strDest; it is therefore a potential cause of buffer overruns. Keep in mind that count limits the number of characters appended; it is not a limit on the size of strDest."
К сожалению, про это редко помнят и используют 'strncat' неправильными способами. Можно выделить три типа ошибок:
1) Разработчики считают, что аргумент 'count' — это размер буфера 'strDest'. В результате, можно видеть в программах следующий некорректный код:
char newProtoFilter[2048] = "....";
strncat(newProtoFilter, szTemp, 2048);
strncat(newProtoFilter, "|", 2048);
Программист предполагает, что, передавая в качестве третьего аргумента число 2048, он защищает код от переполнения. Это не так. Он указывает, что к строке можно добавить ещё до 2048 символов!
2) Забывают, что после копирования символов, функция 'strncat' добавит терминальный 0. Пример опасного кода:
char filename[NNN];
...
strncat(filename,
dcc->file_info.filename,
sizeof(filename) - strlen(filename));
На первый взгляд, кажется, что теперь программист защитился от переполнения буфера 'filename'. Но это не так. Он вычел из размера массива длину строки. Это значит, что если вся строка уже заполнена, выражение 'sizeof(filename) - strlen(filename)' вернет единицу. В результате к строке будет прибавлен ещё один символ, а терминальный ноль будет записан уже за границы буфера.
Поясним эту ошибку на более простом примере:
char buf[5] = "ABCD";
strncat(buf, "E", 5 - strlen(buf));
В буфере уже нет места для новых символов. В нём находится 4 символа и терминальный ноль. Выражение "5 - strlen(buf)" равно 1. Функция strncpy() скопирует символ "E" в последний элемент массива 'buf'. Терминальный 0 будет записан уже за пределами буфера!
3) Забывают о факторе целочисленного переполнения. Рассмотрим пример такой ошибки:
struct A
{
....
char consoleText[512];
};
void foo(A a)
{
char inputBuffer[1024];
....
strncat(a.consoleText, inputBuffer,
sizeof(a.consoleText) - strlen(a.consoleText) - 5);
}
Здесь в качестве третьего аргумента используется инфиксное выражение. При невнимательном изучении кода может показаться, что значение выражения "sizeof(a.consoleText) - strlen(a.consoleText) – 5" лежит в диапазоне [0, 507], и код корректен. Однако это не так:
Чтобы исправить приведённые выше примеры, их нужно переписать следующим образом:
// Sample N1
char newProtoFilter[2048] = "....";
strncat(newProtoFilter, szTemp,
2048 - 1 - strlen(newProtoFilter));
strncat(newProtoFilter, "|",
2048 - 1 - strlen(newProtoFilter));
// Sample N2
char filename[NNN];
...
strncat(filename,
dcc->file_info.filename,
sizeof(filename) - strlen(filename) - 1);
// Sample N3
void foo(A a)
{
char inputBuffer[1024];
....
size_t textSize = strlen(a.consoleText);
if (sizeof(a.consoleText) - textSize > 5u)
{
strncat(a.consoleText, inputBuffer,
sizeof(a.consoleText) - textSize - 5);
}
else
{
// ....
}
}
Этот код нельзя назвать красивым или по-настоящему надежным. Гораздо лучшим решением будет отказ от функций типа 'strncat' в пользу более безопасных. Например, можно использовать класс 'std::string' или такие функции, как 'strncat_s', и так далее [2].
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V645. |
Оператор if расположен на той же строке, что и закрывающаяся скобка от предыдущего if. Возможно, в этом месте пропущено ключевое слово 'else' и программа работает не так, как ожидал программист.
Рассмотрим простой пример некорректного кода:
if (A == 1) {
Foo1(1);
} if (A == 2) {
Foo2(2);
} else {
Foo3(3);
}
В случае если переменная 'A' принимает значение 1, то произойдет вызов не только функции 'Foo1', но и вызов функции 'Foo3'. Следует обратить внимание на логику выполнения программы, возможно так и должно быть. В случае ошибки следует добавить ключевое слово 'else'.
Корректный вариант кода:
if (A == 1) {
Foo1(1);
} else if (A == 2) {
Foo2(2);
} else {
Foo3(3);
}
Так же корректным анализатор считает код, в котором в 'then' части первого условного оператора 'if' есть безусловный оператор 'return'. Поскольку в таком случае логика выполнения программы не нарушается, а присутствует только не совсем корректное форматирование кода. Пример такого кода:
if (A == 1) {
Foo1(1);
return;
} if (A == 2) {
Foo2(2);
} else {
Foo3(3);
}
Если ошибки нет, то устранить предупреждение V646 можно, если перенести оператор 'if' на следующую строку. Пример такого кода:
if (A == 1) {
Foo1(1);
}
if (A == 2) {
Foo2(2);
} else {
Foo3(3);
}
В приведенных выше примерах ошибка очевидна, и, кажется, что вряд ли она встретится в реальных программах. Однако, если код сложен, очень легко не заметить отсутствие оператора 'else'. Вот пример этой ошибки, взятый из реального приложения:
if( 1 == (dst->nChannels) ) {
ippiCopy_16s_C1MR((Ipp16s*)pDstCh, dstStep,
(Ipp16s*)pDst, dst->widthStep, roi, pMask, roi.width);
} if( 3 == (dst->nChannels) ) { //V646
ippiCopy_16s_C3R((Ipp16s*)pDst-coi, dst->widthStep,
(Ipp16s*)pTmp, dst->widthStep, roi);
ippiCopy_16s_C1C3R((Ipp16s*)pDstCh, dstStep,
(Ipp16s*)pTmp+coi, dst->widthStep, roi);
ippiCopy_16s_C3MR((Ipp16s*)pTmp, dst->widthStep,
(Ipp16s*)pDst-coi, dst->widthStep, roi, pMask, roi.width);
} else {
ippiCopy_16s_C4R((Ipp16s*)pDst-coi, dst->widthStep,
(Ipp16s*)pTmp, dst->widthStep, roi);
ippiCopy_16s_C1C4R((Ipp16s*)pDstCh, dstStep,
(Ipp16s*)pTmp+coi, dst->widthStep, roi);
ippiCopy_16s_C4MR((Ipp16s*)pTmp, dst->widthStep,
(Ipp16s*)pDst-coi, dst->widthStep, roi, pMask, roi.width);
}
Этот код очень сложен для чтения и понимания. Однако анализатор не теряет бдительности.
В данном примере условия '3 == (dst->nChannels)' и '1 == (dst->nChannels)' не могут выполниться одновременно, и форматирование кода указывает на пропущенное ключевое слово 'else'. Корректный вариант должен выглядеть следующим образом:
if( 1 == (dst->nChannels) ) {
....
} else if( 3 == (dst->nChannels) ) {
....
} else {
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V646. |
Анализатор обнаружил, что выполняется некорректная операция с указателем. В указатель на целочисленный тип записывается целочисленное значение или константа. Скорее всего, в указатель следует поместить адрес переменной. Или следует поместить значение по адресу, который содержит указатель.
Рассмотрим пример некорректного кода:
void foo()
{
int *a = GetPtr();
int b = 10;
a = b; // <=
Foo(a);
}
В данном случае указателю 'a' будет присвоено значение 10. Фактически мы получим не валидный указатель. Для исправления следует разыменовать указатель 'a' или взять адрес переменной 'b'.
Корректный вариант кода:
void foo()
{
int *a = GetPtr();
int b = 10;
*a = b;
Foo(a);
}
Так же корректным будет такой код:
void foo()
{
int *a = GetPtr();
int b = 10;
a = &b;
Foo(a);
}
Анализатор считает безопасным, когда в переменную типа указатель записываются такие магические значения, как: -1, 0xcccccccc, 0xbadbeef, 0xdeadbeef, 0xfeeefeee, 0xcdcdcdcd и т.д. Эти значения часто используются в целях отладки или как специальные маркеры.
Примечание N1.
Подобная ошибка возможна только в языке Си. В Си++ неявное приведение целочисленного значения (кроме 0) к указателю невозможно.
Примечание N2.
Иногда сообщения анализатора могут казаться странными. Рассмотрим такой случай:
char *page_range_split = strtok(page_range, ",");
Анализатор выдаёт предупреждение, что значение типа 'int' помещается в указатель. Но ведь функция 'strtok' возвращает указатель, как же так?
Дело в том, что функция 'strtok' может быть не объявлена ранее! Разработчик мог забыть подключить нужный заголовочный файл. В языке C считается, что по умолчанию функция возвращает тип 'int'. Именно исходя из этих предположений и будет скомпилирован код. Это серьёзная ошибка, которая будет приводить к порче указателей в 64-битных программах. Подробнее эта тема разбирается в статье "Красивая 64-битная ошибка на языке Си".
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V647. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что приоритет логической операций '&&' выше приоритета логической операции '||'. Про это часто забывают. В результате логическое выражение может давать совсем не тот результат, на который рассчитывал программист.
Рассмотрим пример некорректного кода:
if ( c == 'l' || c == 'L' &&
!( token->subtype & TT_LONG ) )
{ .... }
Скорее всего, программист ожидал, что вначале выполнится проверка равенства переменной 'c' значению 'l' или 'L'. И только затем выполнится операция '&&'. Но согласно приоритету операций в языке Си/Си++ вначале произойдет выполнение операции '&&', а уже потом '||'.
Можно порекомендовать во всех выражениях с редко используемыми вами операторами или там где нет уверенности, писать скобки. Даже если скобки окажутся лишними, это не страшно. Зато код станет более легким для понимания и будет меньше подвержен ошибкам.
Корректный вариант кода:
if ( ( c == 'l' || c == 'L' ) &&
!( token->subtype & TT_LONG ) )
А как убрать ложное предупреждение, если действительно планировалась последовательность вычислений: сначала логическое '&&', затем логическое '||'?
Есть несколько вариантов:
1) Плохой вариант. Можно использовать комментарий "//-V648" для подавления предупреждения в нужной строке.
if ( c == 'l' || c == 'L' && //-V648
!( token->subtype & TT_LONG ) )
2) Хороший вариант. Можно добавить дополнительные скобки:
if ( c == 'l' || ( c == 'L' &&
!( token->subtype & TT_LONG ) ) )
Дополнительные скобки помогут вашим коллегам понять, что этот код корректен.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V648. |
Анализатор обнаружил ситуацию, когда 'then' часть оператора 'if' никогда не получит управления. Это происходит из-за того, что ранее уже встречается оператор 'if' с таким же условием, содержащий в 'then' части безусловный оператор 'return'. Это может свидетельствовать как о логической ошибке в программе, так и избыточном втором операторе 'if'.
Рассмотрим пример некорректного кода:
if (l >= 0x06C0 && l <= 0x06CE) return true;
if (l >= 0x06D0 && l <= 0x06D3) return true;
if (l == 0x06D5) return true; // <=
if (l >= 0x06E5 && l <= 0x06E6) return true;
if (l >= 0x0905 && l <= 0x0939) return true;
if (l == 0x06D5) return true; // <=
if (l >= 0x0958 && l <= 0x0961) return true;
if (l >= 0x0985 && l <= 0x098C) return true;
В данном случае условие 'l == 0x06D5' дублируется и для исправления кода достаточно убрать одно из них. Однако возможно, что во втором случае проверяемое значение должно отличаться от первого случая.
Корректный вариант кода:
if (l >= 0x06C0 && l <= 0x06CE) return true;
if (l >= 0x06D0 && l <= 0x06D3) return true;
if (l == 0x06D5) return true;
if (l >= 0x06E5 && l <= 0x06E6) return true;
if (l >= 0x0905 && l <= 0x0939) return true;
if (l >= 0x0958 && l <= 0x0961) return true;
if (l >= 0x0985 && l <= 0x098C) return true;
Предупреждение V649 косвенно может указать на наличие ошибок совсем иного типа. Рассмотрим интересный пример некорректного кода:
AP4_Result AP4_StscAtom::WriteFields(AP4_ByteStream& stream)
{
AP4_Result result;
AP4_Cardinal entry_count = m_Entries.ItemCount();
result = stream.WriteUI32(entry_count);
for (AP4_Ordinal i=0; i<entry_count; i++) {
stream.WriteUI32(m_Entries[i].m_FirstChunk);
if (AP4_FAILED(result)) return result;
stream.WriteUI32(m_Entries[i].m_SamplesPerChunk);
if (AP4_FAILED(result)) return result;
stream.WriteUI32(m_Entries[i].m_SampleDescriptionIndex);
if (AP4_FAILED(result)) return result;
}
return result;
}
Имеющиеся в цикле проверки 'if (AP4_FAILED(result)) return result;' бессмысленны. Переменная 'result' не изменяется при чтении данных из файлов.
Исправленный вариант кода:
AP4_Result AP4_StscAtom::WriteFields(AP4_ByteStream& stream)
{
AP4_Result result;
AP4_Cardinal entry_count = m_Entries.ItemCount();
result = stream.WriteUI32(entry_count);
for (AP4_Ordinal i=0; i<entry_count; i++) {
result = stream.WriteUI32(m_Entries[i].m_FirstChunk);
if (AP4_FAILED(result)) return result;
result = stream.WriteUI32(m_Entries[i].m_SamplesPerChunk);
if (AP4_FAILED(result)) return result;
result = stream.WriteUI32(m_Entries[i].m_SampleDescriptionIndex);
if (AP4_FAILED(result)) return result;
}
return result;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V649. |
Анализатор обнаружил потенциальную ошибку в выражении с адресной арифметикой. Операции сложения/вычитания выполняется с выражением, представляющим собой двойное приведение типов. Это может быть опечаткой - забыли заключить первое приведение типа и операцию сложения в скобки.
Рассмотрим пример некорректного кода:
ptr = (int *)(char *)p + offset_in_bytes;
Скорее всего, программист ожидал, что переменная 'p' будет приведена к типу 'char *' и к ней будет добавлено смещение в байтах. Затем новый указатель будет приведен к типу 'int *'.
Однако пропущенные скобки превращают это выражение в двойное приведение типов и добавление смещения к указателю на 'int'. Полученный результат будет отличаться от ожидаемого результата. Подобная ошибка вполне может привести к выходу за границы массива.
Корректный вариант кода:
ptr = (int *)((char *)p + offset_in_bytes);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V650. |
Анализатор обнаружил потенциальную ошибку в выражении вида 'sizeof(X)/sizeof(X[0])'. Подозрительно то, что объект 'X' является экземпляром класса.
Как правило, выражение 'sizeof(X)/sizeof(X[0]) применяется для определения количества элементов в массиве 'X'. Ошибка может возникнуть в ходе неаккуратного рефакторинга кода. Переменная 'X' изначально была обыкновенным массивом и была заменена на класс-контейнер, но вычисление числа элементов осталось прежним.
Рассмотрим пример некорректного кода:
#define countof( x ) (sizeof(x)/sizeof(x[0]))
Container<int, 4> arr;
for( int i = 0; i < countof(arr); i++ )
{ .... }
Программист ожидал, что будет вычислено число элементов переменной 'arr'. Однако полученное значение будет представлять собой: размер класса, разделённый на размер переменной типа 'int'. Это значение, скорее всего, никак не связано с количеством элементов данных, хранящихся в контейнере.
Пример корректного кода:
const size_t count = 4;
Container<int, count> arr;
for( int i = 0; i < arr.size(); i++ )
{ .... }
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с тем, что одна из операций '!', '~', '-' или '+' повторяется три или более раз. Такая ошибка может произойти в случае опечатки. Такое дублирование операторов бессмысленно и может содержать ошибку.
Рассмотрим пример некорректного кода:
if(B &&
C && !!!
D) { .... }
Скорее всего, такая ошибка возникла вследствие опечатки. Например, были пропущены символы комментария или напечатан лишний символ операции.
Корректный вариант кода:
if (B &&
C && //!!!
D) { .... }
Так же корректным будет следующий вариант кода:
if (B &&
C && !!D) { .... }
Такой приём часто используется для приведения целочисленных типов данных к типу 'bool'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V652. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что в объявлении, состоящем из строковых литералов, две строки объединяются в одну. Ошибка может быть следствием опечатки, когда пропущена запятая между строковыми литералами.
Такая ошибка может долго оставаться незамеченной. Например, она может редко проявлять себя, если массив строковых литералов используется для формирования сообщений об ошибках.
Рассмотрим пример некорректного кода:
const char *Array [] = {
"Min", "Max", "1",
"Begin", "End" "2" };
Между литералами "End" и "2" пропущена запятая, и поэтому они будут объединены в один строковый литерал "End2". Для исправления такой ошибки следует разделить строковые литералы запятой.
Корректный вариант кода:
const char *Array [] = {
"Min", "Max", "1",
"Begin", "End", "2" };
Анализатор не выдает предупреждающих сообщений, если объединённая строка окажется слишком большой (более 50 символов) или состоит более чем из двух фрагментов. Такой прием часто используется при оформлении кода с длинными строковыми литералами.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V653. |
Анализатор обнаружил ситуацию, когда условие в операторе 'for' или 'while' всегда истинно или ложно. Подобная ситуация часто свидетельствует о наличии ошибок. Высока вероятность того, что произошла опечатка и следует обратить внимание на этот фрагмент кода.
Рассмотрим пример некорректного кода:
for (i = 0; 1 < 50; i++)
{ .... }
В коде допущена опечатка. В условии вместо переменной 'i' была напечатана константа '1'. Исправить такой код не представляет больших трудностей. Корректный вариант кода:
for (i = 0; i < 50; i++)
{ .... }
Анализатор не будет выдавать предупреждающее сообщение, если условие задано явно в виде константного выражения '1' или '0', 'true' или 'false'. Пример такого кода:
while (true)
{ .... }
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V654. |
Анализатор обнаружил потенциальную ошибку, найдя в коде неиспользуемое объединение строковых переменных. Тип переменных: std::string, CString, QString, wxString. Чаще всего, такие выражения появляются в коде, когда пропускается оператор присваивания или в процессе неаккуратного рефакторинга кода.
Рассмотрим пример некорректного кода:
void Foo(std::string &s1, const std::string &s2)
{
s1 + s2;
}
Код содержит опечатку. Вместо '+=' написано '+'. Код компилируется, но не имеет практического смысла. Исправленный вариант кода:
void Foo(std::string &s1, const std::string &s2)
{
s1 += s2;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V655. |
Анализатор обнаружил потенциальную ошибку, найдя в коде инициализацию двух различных переменных одинаковыми выражениями. Анализатор считает опасными не все выражения, а только в которых используется вызов функций.
Рассмотрим наиболее простой случай:
x = X();
y = X();
Возможны три варианта действий:
1) Код содержит ошибку. Необходимо исправить ошибку, заменив 'X()' на 'Y()'.
2) Код верен, но работает медленно. Если функция 'X()' требует много вычислений, то лучше написать 'y = x;'.
3) Код верен и работает быстро. Или функция 'X()' читает значение из файла. Тогда чтобы избавиться от ложного срабатывания, можно использовать комментарий "//-V654".
Теперь рассмотрим реальный пример:
while (....)
{
if ( strstr( token, "playerscale" ) )
{
token = CommaParse( &text_p );
skin->scale[0] = atof( token );
skin->scale[1] = atof( token );
continue;
}
}
В данном коде нет ошибки. Однако код не оптимален. Его можно переписать таким образом, что бы исключить лишний вызов функции 'atof'. Учитывая то, что присваивание находится в цикле и может быть вызвано много раз, такое изменение может привести к заметному выигрышу в быстродействии функции. Исправленный вариант кода:
while (....)
{
if ( strstr( token, "playerscale" ) )
{
token = CommaParse( &text_p );
skin->scale[1] = skin->scale[0] = atof( token );
continue;
}
}
Рассмотрим ещё один пример:
String path, name;
SplitFilename(strSavePath, &path, &name, NULL);
CString spath(path.c_str());
CString sname(path.c_str());
В данном примере присутствует явная ошибка. Переменная 'path' используется два раза: для инициализации переменных 'spath' и 'sname'. Однако, по логике программы видно, что для инициализации переменной 'sname' должна быть использована переменная 'name'. Исправленный вариант кода:
....
CString spath(path.c_str());
CString sname(name.c_str());
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V656. |
Анализатор обнаружил странную функцию. Функция не имеет состояния, не изменяет глобальных переменных. При этом она имеет несколько точек возврата, возвращающих одно и то же числовое значение.
Такой код крайне подозрителен и может свидетельствовать о возможной ошибке. Скорее всего, функция должна возвращать различные числовые значения.
Рассмотрим простой пример такого кода:
int Foo(int a)
{
if (a == 33)
return 1;
return 1;
}
Данный код содержит ошибку. Для ее исправления изменим одно из возвращаемых значений. Определить необходимые возвращаемые значения, как правило, можно только зная логику работы всего приложения в целом.
Вариант корректного кода:
int Foo(int a)
{
if (a == 33)
return 1;
return 2;
}
Если код верен, то чтобы избавиться от ложного срабатывания следует использовать комментарий "//-V657".
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с переполнением.
Выполняются следующие действия:
Если при вычитании возможно переполнение, то результат проверки может отличаться от ожиданий программиста.
Рассмотрим наиболее простой случай:
unsigned A = ...;
int B = ...;
if (A - B > 1)
Array[A - B] = 'x';
Такая проверка по замыслу программиста должна защитить от выхода за границу массива. Однако эта проверка не поможет, если переменная A < B.
Пусть: A = 3, B = 5;
Тогда: 0x00000003u - 0x00000005i = FFFFFFFEu
Выражение "A - B" согласно правилам языка Си++ имеет тип "unsigned int". Значит, "A - B" будет равно FFFFFFFEu. Это число больше единицы. В результате произойдёт обращение к памяти за границей массива.
Исправить код можно двумя способами. Во-первых, вычисления можно производить в переменных знаковых типов:
intptr_t A = ...;
intptr_t B = ...;
if (A - B > 1)
Array[A - B] = 'x';
Во-вторых, можно изменить условие. Как именно следует изменить условие, зависит от желаемого результата и входных значений. Если B >= 0, то достаточно будет написать:
unsigned A = ...;
int B = ...;
if (A > B + 1)
Array[A - B] = 'x';
Если код корректен, то можно отключить вывод диагностического сообщения в данной строке, используя комментарий "//-V658".
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V658. |
Анализатор обнаружил в коде две функции с одинаковыми именами. Функции различаются константностью.
Объявления функции могут различаться:
Хотя имена функций совпадают, они работают по-разному. Это может свидетельствовать об ошибке.
Рассмотрим простой случай:
class CLASS {
DATA *m_data;
public:
char operator[](size_t index) const {
if (!m_data || index >= m_data->len)
throw MyException;
return m_data->data[index];
}
char &operator[](size_t index) {
return m_data->data[index];
}
};
В константной функции 'operator[]' существует проверка и генерируется исключение в случае ошибки. В неконстантной функции такой поверки нет. Скорее всего, это недочёт, который должен быть исправлен.
Анализатор учитывает набор различных ситуаций, когда различия в телах функций оправданы. Однако всё исключительные ситуации учесть невозможно. Если анализатор выдал ложное срабатывание, то его можно подавить, используя комментарий "//-V659".
Анализатор обнаружил потенциальную ошибку, когда из-за опечатки написано ':', хотя необходимо '::'.
В коде метода класса найдена неиспользуемая метка. Вслед за этой меткой следует вызов функции. Анализатор считает опасным, если функция с таким именем находится в одном из базовых классов.
Рассмотрим пример:
class Employee {
public:
void print() const {}
};
class Manager: public Employee {
void print() const;
};
void Manager::print() const {
Employee:print();
}
С большой вероятностью строчка 'Employee:print();' является ошибочной. Ошибка заключается в том, что будет вызвана функция не из класса 'Employee', как планировалось, а из своего же класса 'Manager'. Для исправления ошибки достаточно заменить ':' на '::'.
Корректный вариант кода:
void Manager::print() const {
Employee::print();
}
Рассмотрим ещё один пример:
namespace Abcd
{
void Foo() {}
}
class Employee {
void Foo() {}
void X() { Abcd:Foo(); }
};
Здесь ошибка заключается в том, что должна была быть вызвана функция, находящаяся в области видимости 'Abcd'. Ошибку легко исправить:
void X() { Abcd::Foo(); }
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительный фрагмент кода, в котором осуществляется доступ к элементу массива. В качестве индекса массива используется логическое выражение.
Примеры таких выражений: Array[A >= B], Array[A != B]. Возможно, что закрывающаяся квадратная скобка стоит не на своем месте. Подобные ошибки, чаще всего возникают из-за опечатки.
Рассмотрим пример некорректного кода:
if ((bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0 ||
bs->inventory[INVENTORY_ROCKETS < 10]) && <<== ERROR!
(bs->inventory[INVENTORY_RAILGUN] <= 0 ||
bs->inventory[INVENTORY_SLUGS] < 10)) {
return qfalse;
}
Такой код компилируется, но работает неправильно. С большой вероятностью, здесь должно быть написано:
if ((bs->inventory[INVENTORY_ROCKETLAUNCHER] <= 0 ||
bs->inventory[INVENTORY_ROCKETS] < 10) &&
(bs->inventory[INVENTORY_RAILGUN] <= 0 ||
bs->inventory[INVENTORY_SLUGS] < 10)) {
return qfalse;
}
Примечание. Анализатор выдает предупреждения не всегда, когда в квадратных скобках находится логическое выражение. Иногда это оправдано. Например, исключение составляет случай, когда массив состоит всего из двух элементов:
int A[2];
A[x != y] = 1;
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V661. |
Анализатор обнаружил подозрительный цикл. Для инициализации итератора используется контейнер A. Затем, этот итератор сравнивается с концом контейнера B. Высока вероятность, что это опечатка и код некорректен.
Рассмотрим пример, когда будет выдано данное предупреждение:
void useVector(vector<int> &v1, vector<int> &v2)
{
vector<int>::iterator it;
for (it = v1.begin(); it != v2.end(); ++it)
*it = rand();
....
}
В цикле 'for' происходит заполнение массива. Для инициализации итератора и для проверки границы используются различные переменные (v1 и v2). Если на самом деле ссылки v1 и v2 указывают на разные массивы, это приведет к ошибке на этапе исполнения программы.
Исправить ошибку очень просто. Необходимо использовать один и тот же контейнер в обоих случаях. Корректный вариант кода:
void useVector(vector<int> &v1, vector<int> &v2)
{
vector<int>::iterator it;
for (it = v1.begin(); it != v1.end(); ++it)
*it = rand();
....
}
Если переменная v1 и v2 ссылаются на один и тот же контейнер, то код корректен. В этом случае можно использовать механизм подавления ложных срабатываний анализатора. Но скорее всего, более удачным решением является рефакторинг кода. Такой код может запутать не только анализатор, но и программистов, которые будут сопровождать этот код.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V662. |
Анализатор обнаружил потенциальную ошибку, из-за которой может возникнуть бесконечный цикл. При работе с классом 'std::istream' недостаточно вызова функции 'eof()' для завершения цикла. В случае возникновения сбоя при чтении данных, вызов функции 'eof()' будет всегда возвращать значение 'false'. Для завершения цикла в этом случае необходима дополнительная проверка значения, возвращаемого функцией 'fail()'.
Рассмотрим пример некорректного кода:
while (!cin.eof())
{
int x;
cin >> x;
}
Исправить ошибку можно усложнив условие. Корректный вариант кода:
while (!cin.eof() && !cin.fail())
{
int x;
cin >> x;
}
Впрочем, у этого варианта тоже есть недостатки. Наиболее простой и правильный вариант кода выглядит так:
int x;
while(cin >> x) {
....;
}
Подробно вопрос чтения из потока разбирается здесь: Why is iostream::eof inside a loop condition (i.e. 'while (!stream.eof())') considered wrong?
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V663. |
Указатель разыменовывается в списке инициализации конструктора, а затем проверяется в теле конструктора на неравенство нулю. Это может свидетельствовать о скрытой ошибке, которая может не проявляться долгое время.
Рассмотрим пример некорректного кода:
Layer(const Canvas *canvas) :
about(canvas->name, canvas->coord)
{
if (canvas)
{
....
}
}
При разыменовании нулевом указателя возникает неопределенное поведение, то есть дальнейшее нормальное выполнение программы становится невозможным. Для исправления ошибки следует перенести инициализацию в тело конструктора в блок кода, где указатель заведомо неравен нулю. Корректный вариант кода:
Layer(const Canvas *canvas)
{
if (canvas)
{
about.set(canvas->name, canvas->coord);
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки разыменования нулевого указателя. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V664. |
Анализатор обнаружил, что в коде находится некорректная последовательность директив '#pragma warning'.
Программисты часто считают, что после директивы "pragma warning(default : X)" опять начнут действовать предупреждения, отключенные ранее помощью "pragma warning(disable: X)". Это не так. Директива 'pragma warning(default : X)' устанавливает предупреждение с номером 'X' в состояние, которое действует ПО УМОЛЧАНИЮ. Это далеко не одно и то же.
Предположим, что файл компилируется с ключом /Wall. В этом случае, должно выдаваться предупреждение C4061. Если написать "#pragma warning(default : 4061)", то это предупреждение перестанет выдаваться, так как по умолчанию оно является отключенным.
Правильным способом возвращения предыдущего состояние предупреждения является использование "#pragma warning(push[ ,n ])" и "#pragma warning(pop)". С описанием этих директив можно познакомиться в документации к Visual C++: Pragma Directives. Warnings.
Рассмотрим пример некорректного кода:
#pragma warning(disable: 4001)
....
//Корректный код, выдающий предупреждение 4001
....
#pragma warning(default: 4001)
В данном примере предупреждение 4001 будет установлено в значение по умолчанию. Но, скорее всего, планировалось вернуть предыдущее значение, которое было до его отключения. Для этого следует воспользоваться директивой 'pragma warning(push)' до отключения предупреждения и директивой 'pragma warning(pop)' после корректного кода.
Корректный вариант кода:
#pragma warning(push)
#pragma warning(disable: 4001)
....
// Корректный код, выдающий предупреждение 4001
....
#pragma warning(pop)
Особенное внимание предупреждению V665 должны уделять разработчики библиотек. Неаккуратная работа с настройками предупреждений может вызвать массу неприятных моментов у пользователей такой библиотеки.
Хорошая статья по данной теме: "Итак, вы хотите заглушить это предупреждение в Visual C++".
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V665. |
Анализатор предполагает, что в функцию мог быть передан некорректный аргумент. Некорректным считается аргумент, числовое значение которого не совпадает с длиной строки, которая находится в предыдущем аргументе. Анализатор строит такие предположения, анализируя пары аргументов, состоящих из строкового литерала и целочисленной константы. Анализ производится по всем одноименным вызовам функций.
Рассмотрим пример некорректного кода:
if (!_strnicmp(szDir, "My Documents", 11)) // <<== Error!
nFolder = 1;
if (!_strnicmp(szDir, "Desktop", 7))
nFolder = 2;
if (!_strnicmp(szDir, "Network Favorites", 17))
nFolder = 3;
В данном случае в первом вызове функции числовое значение 11 некорректно. Это может привести к тому, что если переменная 'szDir' будет указывать на строковый литерал "My Document", то сравнение пройдет успешно. Для исправления достаточно изменить длину строки на корректное значение, то есть 12.
Корректный вариант кода:
if (!_strnicmp(szDir, "My Documents", 12))
nFolder = 1;
Диагностика V666 носит эмпирический характер. Если вы хотите понять его суть, то, к сожалению, придется прочитать сложное объяснение. Это можно не делать, но тогда просим внимательно проверить аргументы функции. Если вы уверены, что код совершенно корректен, то можно отключить вывод диагностического сообщения, добавив комментарий, вида "//-V666".
Давайте попробуем разобраться, как работает это диагностическое правило. Рассмотрим код:
foo("1234", 1, 4);
foo("123", 2, 3);
foo("321", 2, 2);
Анализатор выберет пары аргументов: строковый литерал и числовое значение. Для них будут проанализированы все вызовы данной функции и построена таблица совпадения длины строки и числового аргумента.
{ { "1234", 1 }, { "1234", 4 } } -> { false, true }
{ { "123", 2 }, { "123", 3 } } -> { false, true }
{ { "321", 2 }, { "321", 2 } } -> { false, false }
Первый столбец не интересен. Не похоже, что это длина строки. А вот второй столбец, кажется, представляет собой длину строки. При этом один из вызовов содержит ошибку.
Конечно, это описание очень схематично, но позволяет понять общий принцип работы. Такой анализ, конечно же, не идеален и будут появляться ложные срабатывания. Однако он позволяет иногда находить интересные ошибки.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V666. |
Анализатор обнаружил, что оператор 'throw' не имеет аргументов и не находится внутри блока 'catch'. Такой код может являться ошибочным. Оператор 'throw' без аргументов используется внутри блока 'catch' для проброса пойманного исключения на верхний уровень. Согласно стандарту, вызов оператора 'throw' без аргумента, если еще не поймано исключение приведет к вызову функции 'std::terminate()'. Это означает, что программа будет завершена.
Рассмотрим пример некорректного кода:
try
{
if (ok)
return;
throw;
}
catch (...)
{
}
Для исправления ошибки необходимо передать оператору 'throw' аргумент.
Корректный вариант кода:
try
{
if (ok)
return;
throw exception("Test");
}
catch (...)
{
}
Однако, вызов оператора 'throw' вне 'catch' блока - это не всегда ошибка. Например, если функция вызывается из блока 'catch' и занимается перебросом исключения на верхний уровень, то ошибки не будет. Однако анализатор не может понять такого поведения программы и все равно будет выдано диагностическое сообщение. Пример такого кода:
void error()
{
try
{
....
if (ok)
return;
throw; <<== на самом деле здесь ошибки нет
}
catch (...)
{
throw;
}
}
void foo()
{
try
{
....
if (ok)
return;
throw exception("Test");
}
catch (...)
{
error();
}
}
В таком случаем вывод диагностического сообщения анализатора можно подавить, используя комментарий вида '//-V667'.
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, когда значение указателя возвращаемого оператором 'new' сравнивается с нулём. Как правило, это значит, что программа при невозможности выделить память будет вести себя не так, как ожидает программист.
Если оператор 'new' не смог выделить память, то согласно стандарту языка Си++, генерируется исключение std::bad_alloc(). Таким образом проверять указатель на равенство нулю не имеет смысла. Рассмотрим простейший случай:
MyStatus Foo()
{
int *p = new int[100];
if (!p)
return ERROR_ALLOCATE;
...
return OK;
}
Указатель 'p' никогда не будет равен нулю. Функция никогда не вернет константное значение ERROR_ALLOCATE. Если выделить память невозможно, то возникнет исключение. Исправим код наиболее простым образом:
MyStatus Foo()
{
try
{
int *p = new int[100];
...
}
catch(const std::bad_alloc &)
{
return ERROR_ALLOCATE;
}
return OK;
}
Отметим, что приведенный пример исправленного кода очень плох. Идеология работы с исключениями совсем иная. Исключения как раз позволяют избавиться от множества проверок и возвращаемых статусов. Лучше позволить исключению выйти за пределы функции 'Foo' и обработать его где-то на более высоком уровне. К сожалению, рассмотрение использования исключений выходит за рамки документации.
Рассмотрим, как подобная ошибка может выглядеть на практике. Вот код, взятый из реального приложения:
// For each processor; spawn a CPU thread to access details.
hThread = new HANDLE [nProcessors];
dwThreadID = new DWORD [nProcessors];
ThreadInfo = new PTHREADINFO [nProcessors];
// Check to see if the memory allocation happenned.
if ((hThread == NULL) ||
(dwThreadID == NULL) ||
(ThreadInfo == NULL))
{
char * szMessage = new char [128];
sprintf(szMessage,
"Cannot allocate memory for "
"threads and CPU information structures!");
MessageBox(hDlg, szMessage, APP_TITLE, MB_OK|MB_ICONSTOP);
delete szMessage;
return false;
}
Пользователь никогда не увидит окошка с сообщением об ошибке. Если не удастся выделить память, программа аварийно завершится или выдаст неподходящее сообщение, обработав исключение в другом месте.
Частой причиной описанных проблем, является изменение в поведении оператора 'new'. Во времена Visual C++ 6.0, оператор 'new' в случае ошибки возвращал NULL. Следующие версии Visual C++ следуют стандарту и генерируют исключение. Помните про это изменение поведения. Таким образом, если вы адаптируете старый проект для сборки современным компилятором, вам стоит уделить повышенное внимание диагностике V668.
Примечание N1. Анализатор не будет выдавать предупреждение, если используется placement new или "new (std::nothrow) T". Пример такого кода:
T * p = new (std::nothrow) T; // OK
if (!p) {
// An error has occurred.
// No storage has been allocated and no object constructed.
...
}
Примечание N2. Есть возможность слинковать проект с nothrownew.obj. В этом случае, оператор new не будет генерировать исключение. Такой возможностью, например, пользуются разработчики драйверов. Подробнее: new and delete operators. В этом случае, просто отключите предупреждение V668.
Дополнительные материалы по данной теме:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V668. |
Анализатор обнаружил, что аргумент передается в функцию по ссылке, но не модифицируется в теле функции. Это может свидетельствовать о наличии ошибки.
Например, виной тому может быть опечатка.
Рассмотрим пример некорректного кода:
void foo(int &a, int &b, int c)
{
a = b == c;
}
Из-за опечатки оператор присваивания ('=') превратился в оператор сравнения ('=='). В результате, переменная 'b' используется только для чтения, хотя это не константная ссылка. Как исправить некорректный код будет зависеть от каждого конкретного случая. Самое главное, такой код требует более подробного изучения.
Корректный вариант кода:
void foo(int &a, int &b, int c)
{
a = b = c;
}
Примечание. Анализатор может ошибаться, пытаясь понять, модифицируется переменная в теле функции или нет. Если вы заметили явно ложное срабатывание, просьба прислать нам соответствующий пример кода.
Вы также можете комментарий "//-V669", чтобы подавить ложное предупреждение в конкретном месте.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V669. |
Анализатор обнаружил возможную ошибку в списке инициализации конструктора класса. Согласно стандарту порядок инициализация членов класса в конструкторе происходит в порядке их объявления в классе. В программе есть конструктор, где инициализация одного члена класса зависит от другого. При этом используемая для инициализации переменная, в этот момент ещё не инициализирована.
Рассмотрим пример такого конструктора:
class Foo
{
int foo;
int bar;
Foo(int i) : bar(i), foo(bar + 1) { }
};
Переменная 'foo' будет инициализироваться первой! В этот момент переменная 'bar' еще не инициализирована. Для исправления ошибки следует перенести объявление члена класса 'foo' выше объявления члена класса 'bar'. Исправленный вариант кода:
class Foo
{
int bar;
int foo;
Foo(int i) : bar(i), foo(bar + 1) { }
};
Если изменить последовательность полей в классе нельзя, то следует изменить выражения для инициализации:
class Foo
{
int foo;
int bar;
Foo(int i) : bar(i), foo(i + 1) { }
};
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V670. |
Анализатор обнаружил потенциальную ошибку, которая может возникнуть при вызове функции с именем 'swap'. В функцию переданы одинаковые фактические аргументы. Передача для этой функции одного и того же аргумента очень подозрительная ситуация. Скорее всего, произошла опечатка.
Пример некорректного кода:
int arg1, arg2;
....
swap(arg1, arg1);
....
Из-за опечатки функция swap() обменивает значение переменной 'arg1' само с собой. Корректным вариантом кода должно было быть:
swap(arg1, arg2);
Так же подозрительным анализатор будет считать следующий код:
MyClass arg1, arg2;
....
arg1.Swap(arg1);
....
Исправить его можно аналогичным образом:
arg1.Swap(arg2);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V671. |
Анализатор обнаружил возможную ошибку в программе, связанную с объявлением переменной, с таким же именем, что и один из аргументов. Если аргумент является ссылкой, то эта ситуация очень подозрительна. Анализатор накладывает и другие условия, чтобы сократить количество ложных срабатываний, но описывать их в документации не имеет смысла.
Что бы лучше понять данный вид ошибки, рассмотрим пример:
bool SkipFunctionBody(Body*& body, bool t)
{
body = 0;
if (t)
{
Body *body = 0;
if (!SkipFunctionBody(body, true))
return false;
body = new Body(body);
return true;
}
return false;
}
Для работы функции с функцией SkipFunctionBody () потребовалась временная переменная. Из-за невнимательности в блоке 'if' повторно объявляется временная переменная с именем 'body'. Это значит что аргумент 'body' не будет модифицирован внутри блока 'if'. Вместо него будет изменена локальная переменная. При выходе из функции значение переменной 'body' всегда будет равно NULL. Ошибка может проявиться не в данном месте программы, а где то далее, когда произойдет разыменование нулевого указателя. Для исправления ошибки следует создавать локальную переменную с другим именем. Корректный вариант кода:
bool SkipFunctionBody(Body*& body, bool t)
{
body = 0;
if (t)
{
Body *tmp_body = 0;
if (!SkipFunctionBody(tmp_body, true))
return false;
body = new Body(tmp_body);
return true;
}
return false;
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V672. |
Анализатор обнаружил потенциальную ошибку в выражении, где используются операции сдвига (shift operations). В процессе сдвига возникает переполнение, и значения старших бит будет потеряно.
Для начала рассмотрим эту ситуацию на простом примере:
std::cout << (77u << 26);
Значение выражения "77u << 26" равно 5167382528 (0x134000000). При этом выражение "77u << 26" имеет тип 'unsigned int'. Это значит, что старшие биты будут отброшены, и на экран будет напечатано значение 872415232 (0x34000000).
Переполнения, возникающие при сдвигах, часто указывают на наличие логической ошибки или просто опечатки. Например, может быть, что число '77u' хотели задать в восьмеричной системе счисления. Тогда корректный код должен выглядеть так:
std::cout << (077u << 26);
Здесь переполнения уже не возникает. Значение выражения "077u << 26" равно 4227858432 (0xFC000000).
Если хочется распечатать на экране значение 5167382528, то число 77 должно быть задано с помощью 64-битного типа. Например, так:
std::cout << (77ui64 << 26);
Перейдём к случаям, с которыми можно встретиться на практике. Для этого рассмотрим две ошибки, обнаруженных в реальных приложениях.
Первый пример.
typedef __UINT64 Ipp64u;
#define MAX_SAD 0x07FFFFFF
....
Ipp64u uSmallestSAD;
uSmallestSAD = ((Ipp64u)(MAX_SAD<<8));
Программист хотел записать в 64-битную переменную uSmallestSAD значение 0x7FFFFFF00. Но на самом деле переменная будет иметь значение 0xFFFFFF00. Старшие биты будут отброшены, так как выражение MAX_SAD<<8 имеет тип 'int'. Программист знал про это, поэтому решил использовать явное приведение типа. Но, к сожалению, ошибся, расставляя скобки. Этот пример хорошо демонстрируют, что подобные ошибки могут возникать из-за простой опечатки. Корректный код:
uSmallestSAD = ((Ipp64u)(MAX_SAD))<<8;
Второй пример.
#define MAKE_HRESULT(sev,fac,code) \
((HRESULT) \
(((unsigned long)(sev)<<31) | \
((unsigned long)(fac)<<16) | \
((unsigned long)(code))) )
*hrCode = MAKE_HRESULT(3, FACILITY_ITF, messageID);
Функция должна сформировать информацию об ошибке в переменной типа HRESULT. Для этого используется макрос MAKE_HRESULT. Но используется он неправильно. Программист посчитал, что первый параметр 'severity' может лежать в приделах от 0 до 3. Видимо он перепутал это со способом формирования кодов ошибки, используемые при работе с функциями GetLastError()/SetLastError().
Макрос MAKE_HRESULT в качестве первого аргумента может принимать только 0 (success) или 1 (failure). Подробнее этот вопрос рассматривался на форуме сайта CodeGuru: Warning! MAKE_HRESULT macro doesn't work.
Так как в качестве первого фактического аргумента используется число 3, то возникает переполнение. Число 3 "превратится" в 1. Благодаря этой случайности ошибка не повлияет на работу программы. Однако мы специально привели этот пример. Хотелось показать, что нередко, код работает исключительно благодаря везению, а не потому что написан правильно.
Правильный код:
*hrCode = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, messageID);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V673. |
Анализатор обнаружил потенциальную ошибку в выражении, где совместно используются целочисленные и real-типы данных. Под real-типами понимаются такие типы, как float/double/long double.
Рассмотрим для начала самый простой случай. Литерал, имеющий тип 'double' неявно приводится к целочисленному типу. Это может указывать на наличии программной ошибки.
int a = 1.1;
Этот код не имеет практического смысла. Скорее всего, переменная должна была быть инициализирована другим значением.
Показанный выше пример надуманный и не интересен. Давайте посмотрим, как проявляет себя эта ошибка на практике.
Пример 1.
int16u object_layer_width;
int16u object_layer_height;
if (object_layer_width == 0 ||
object_layer_height == 0 ||
object_layer_width/object_layer_height < 0.1 ||
object_layer_width/object_layer_height > 10)
Здесь целочисленное значение сравнивается с константой '0.1'. Это очень подозрительно. Предположим, что переменные имеют следующие значения:
Программист ожидает, что при делении этих чисел результат будет равен '0.2'. Это подходящее значение, так оно попадает в диапазон [0.1..10].
На самом деле, результат деления будет равен 0. Деление осуществляется над целочисленными типами данных. Потом, при сравнении с '0.1' произойдет расширение до типа 'double', но будет уже поздно. Чтобы исправить код необходимо заранее использовать явное приведение типов:
if (object_layer_width == 0 ||
object_layer_height == 0 ||
(double)object_layer_width/object_layer_height < 0.1 ||
(double)object_layer_width/object_layer_height > 10.0)
Второй пример.
// be_aas_reach.c
ladderface1vertical =
abs( DotProduct( plane1->normal, up ) ) < 0.1;
Аргумент функции abs() имеет тип 'double'. На первый взгляд, код кажется абсолютно правильным и хочется заявить "анализатор здесь сглупил".
Но давайте разберёмся более детально. Давайте посмотрим, как объявлена функция abs() в заголовочных файлах.
int __cdecl abs( int _X);
#ifdef __cplusplus
extern "C++" {
inline long __CRTDECL abs(__in long _X) { .... }
inline double __CRTDECL abs(__in double _X) { .... }
inline float __CRTDECL abs(__in float _X) { .... }
}
#endif
Да, в Си++ функции abs() перегружены для различных типов. Однако, мы имеем дело с кодом на языке Си (файл be_aas_reach.c).
Это значит, что выражение типа 'float' будет неявно преобразовано к типу 'int'. И результат работы функции abs() тоже будет иметь тип 'int'. Значение типа 'int' нет смысла сравнивать с '0.1'. Об этом и предупреждает анализатор.
В программах на языке Си для правильного вычисления модуля потребуется другая стандартная функция:
double __cdecl fabs(__in double _X);
Исправленный код:
ladderface1vertical =
fabs( DotProduct( plane1->normal, up ) ) < 0.1;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V674. |
Анализатор обнаружил попытку записи в память, которая предназначена только для чтения.
Рассмотрим пример.
char *s = "A_string";
if (x)
s[0] = 'B';
Здесь указатель 's' ссылается на область памяти, предназначенной только для чтения. Изменение этой памяти приведёт к неопределённому поведению программы. Скорее всего, это неопределенное поведение программы будет представлять собой access violation.
Исправленный вариант программы:
char s[] = "A_string";
if (x)
s[0] = 'B';
На стеке создастся массив 's' и в него будет скопирована строка из read-only памяти. Теперь строку 's' можно изменять.
P.S.
Если "A_string" представляет собой "const char *", то почему этот тип неявно приводится к "char *"?
Такое поведение существует из-за соображений совместимости. Было написано ОЧЕНЬ много кода на Си, где используются не константные указатели. Разработчики стандарта/компиляторов не нашли в себе силы в этом месте сломать обратную совместимость с имеющимся кодом.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V675. |
Анализатор обнаружил, что значение типа BOOL сравнивается с константой TRUE (или 1). Это является потенциальной ошибкой, так как значение "истина" может быть любым числом, отличным от нуля.
Для начала вспомним, в чем отличие типа 'bool' и 'BOOL'.
Конструкция вида:
bool x = ....;
if (x == true) ....
совершенно корректна. Тип 'bool' может принимать только два значения: true и false.
В случае с типом BOOL такие проверки недопустимы. Тип BOOL представляет на самом деле тип 'int'. Это значит, что он может хранить значения, отличные от нуля и единицы. Любое значение, отличное от нуля считается "истинным".
Отличные от 1 значения могут возвращаться, например, функциями из Windows SDK.
Константы FALSE/TRUE объявлены следующим образом:
#define FALSE 0
#define TRUE 1
Это значит, что следующее сравнение может дать сбой:
BOOL ret = Some_SDK_Function();
if (TRUE == ret)
{
// do something
}
Нет гарантии, что функция Some_SDK_Function() вернет именно единицу, если будет успешно выполнена. Правильно будет написать:
if (FALSE != ret)
или:
if (ret)
По данной тематике также можно порекомендовать познакомиться с FAQ на сайте CodeGuru: Visual C++ General: What is the difference between 'BOOL' and 'bool'?
В реальном приложении, ошибка может выглядеть следующим образом:
if (CDialog::OnInitDialog() != TRUE )
return FALSE;
В описании функции CDialog::OnInitDialog() сказано: "If OnInitDialog returns nonzero, Windows sets the input focus to the default location, the first control in the dialog box. The application can return 0 only if it has explicitly set the input focus to one of the controls in the dialog box."
Обратите внимание, нигде не сказано про TRUE или про 1. Правильный код:
if (CDialog::OnInitDialog() == FALSE)
return FALSE;
Этот код может долго и успешно работать, но нет никакой гарантии, что так будет всегда.
Относительно ложных срабатываний. Иногда, программист точно знает, что переменная типа BOOL всегда будет иметь значение 0 или 1. В этом случае, можно подавить ложное срабатывание, одним из нескольких методов. Однако, намного лучше все-таки поправить код. Код станет более надежен по отношению к будущим рефакторингам.
Близкой по смыслу к этой диагностике является V642.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V676. |
Анализатор обнаружил, что в программе объявляется один из стандартных типов данных. Это избыточный код, который потенциально может привести к ошибкам. Следует использовать системные файлы, где объявлены соответствующие типы.
Рассмотрим пример некорректного объявления типа:
typedef unsigned *PSIZE_T;
Тип PSIZE_T объявлен как указатель на тип 'unsigned'. Такое объявление может привести к проблемам при попытке собрать 64-битное приложение. Программа не скомпилируется или будет вести себя не так, как задумано. В файле "BaseTsd.h" тип PSIZE_T объявлен следующим образом: "typedef ULONG_PTR SIZE_T, *PSIZE_T;". Однако следует не менять объявление типа, а включить соответствующий заголовочный файл.
Корректный код:
#include <BaseTsd.h>
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V677. |
Анализатор обнаружил вызов нестатической функции-члена класса, которой одним из аргументов передали этот же объект, на котором она была вызвана.
Рассмотрим пример:
A.Foo(A);
Есть вероятность, что этот подозрительный код содержит ошибку. Например, из-за опечатки используется неверное имя переменной. Тогда, корректный код должен был быть таким:
A.Foo(B);
или таким:
B.Foo(A);
Давайте посмотрим, как такие опечатки проявляют себя на практике. Код реального приложения:
CXMLAttribute* pAttr1 =
m_pXML->GetAttribute(CXMLAttribute::schemaName);
CXMLAttribute* pAttr2 =
pXML->GetAttribute(CXMLAttribute::schemaName);
if ( pAttr1 && pAttr2 &&
!pAttr1->GetValue().CompareNoCase(pAttr1->GetValue()))
....
Этот код должен сравнивать два атрибута. Но из-за опечатки, значение "pAttr1->GetValue()" сравнивается само с собой.
Корректный код:
if ( pAttr1 && pAttr2 &&
!pAttr1->GetValue().CompareNoCase(pAttr2->GetValue()))
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V678. |
Анализатор обнаружил ситуацию, когда неинициализированная переменная передаётся в функцию по ссылке или по указателю. При этом функция читает значение из этой неинициализированной переменной.
Рассмотрим пример.
void Copy(int &x, int &y)
{
x = y;
}
void Foo()
{
int x, y;
x = 1;
Copy(x, y);
}
Это конечно очень простой придуманный пример. Однако он хорошо показывает суть ошибки. Переменная 'y' является неинициализированной. Ссылка на эту переменную передаётся в функцию Copy(). Там происходит чтение из этой неинициализированной переменной.
Корректный код, может выглядеть так:
void Copy(int &x, int &y)
{
x = y;
}
void Foo()
{
int x, y;
y = 1;
Copy(x, y);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительную конструкцию, при которой подразумевалось освобождение памяти для произвольного числа указателей, но будет освобождена память только для первого.
Рассмотрим пример:
delete p1, p2;
Такую конструкцию мог написать неопытный программист или человек давно не работавший с языком Си++. На первый взгляд, может показаться, что этот код должен удалить два объекта, адреса которых хранятся в указателях 'p1' и 'p2'. На самом деле, здесь два оператора. Первый - оператор 'delete'. Второй - оператор запятая ','.
В начале, выполнится оператор 'delete'. Затем, оператор запятая ',' вернет значение второго аргумента (а именно, 'p2').
Другими словами, этот код эквивалентен: (delete p1), p2;
Корректный код, должен выглядеть так:
delete p1;
delete p2;
Примечание. Анализатор не выдает сообщение, если оператор запятая используется в практических целях. Пример безопасного кода:
if (x)
delete p, p = nullptr;
После удаления объекта указатель обнуляется. Чтобы не писать фигурные скобки, два действия были объединены с помощью оператора запятая ','.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с последовательностью вызова функций.
Согласно стандарту языка Си++, не определена последовательность вычисления фактических аргументов функции. Для выражения 'A(B(), C())' нельзя сказать, вызовется в начале функция 'B()' или 'C()'. Какая функция будет вызвана первой, зависит от компилятора, параметров компиляции и так далее.
Изредка, это может порождать проблемы. Анализатор выдает предупреждения на код, который выглядит наиболее подозрительно. К сожалению, набор случаев, когда выдаётся предупреждение, сознательно сильно ограничен. Это сделано для того, чтобы он не выдавал ложные срабатывания. Уж очень часто, в качестве фактических аргументов используется вызовы других функций. И в большинстве случаев это безопасно.
Пример кода, для которого PVS-Studio выдаст предупреждение:
Point ReadPoint()
{
return Point(ReadFixed(), ReadFixed());
}
Этот код может привести к тому, что значение X и Y могут быть перепутаны. Неизвестно, какой из аргументов начнет вычисляться первым.
Корректный код:
Point ReadPoint()
{
float x = ReadFixed();
return Point(x, ReadFixed());
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V681. |
Анализатор обнаружил потенциальную ошибку, связанную с использованием прямого слеша.
Легко ошибиться и перепутать прямой и обратный слеш.
Пример:
if (x == '/n')
Программист планировал сравнить переменную 'x' с кодом 0xA (перевод строки). Но он ошибся и использовал прямой слеш. В результате, переменная сравнивается со значением 0x2F6E.
Корректный код:
if (x == '\n')
Такую ошибку можно допустить при работе со следующими управляющими последовательностями символов (escape sequences):
Анализатор обнаружил потенциальную ошибку в цикле. Возможно, из-за опечатки увеличивается/уменьшается не та переменная.
Пример подозрительного кода:
void Foo(float *Array, size_t n)
{
for (size_t i = 0; i != n; ++n)
{
....
}
}
Вместо переменной 'i' увеличивается переменная 'n'. В результате, программа ведёт себя не так, как ожидал программист.
Исправленный вариант кода:
for (size_t i = 0; i != n; ++i)
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительное выражение. Выражение написано для того, чтобы изменить определённые биты в переменной. Но значение переменной останется прежним.
Пример подозрительного кода:
MCUCR&=~(0<<SE);
Код взят из программы для микроконтроллера ATtiny2313. Бит SE должен быть установлен в единицу для того, чтобы по команде SLEEP микроконтроллер перешел в спящий режим. Чтобы избежать случайного перехода в спящий режим, рекомендуется устанавливать бит SE в единичное состояние непосредственно перед вызовом команды SLEEP и сбрасывать его после пробуждения. Именно такой сброс и хотели сделать после пробуждения. Но из-за опечатки значение регистра MCUCR не изменится. Получается, что хотя программа работает, она менее безопасна, чем могла быть.
Исправленный вариант кода:
MCUCR&=~(1<<SE);
Примечание. Иногда возникает множественное ложное предупреждение V684. Как правило, это связано с использованием больших и сложных макросов. Способы подавления ложных срабатываний для макросов описаны в соответствующем разделе документации.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V684. |
Анализатор посчитал подозрительным значение, возвращаемое из функции. В нём используется оператор ',' (запятая). Это не всегда ошибка, но этот код стоит проверить.
Пример подозрительного кода:
int Foo()
{
return 1, 2;
}
Функция вернёт значение 2. Наличие числа 1 здесь избыточно и не оказывает никакого влияния на поведение программы.
Если это просто опечатка, то следует убрать лишнюю единицу:
int Foo()
{
return 2;
}
Но возможно, здесь скрывается настоящая ошибка. И именно поэтому, анализатор обращает внимание на такие конструкции. Например, вдруг в процессе рефакторинга случайно удалили вызов функции.
Тогда, исправленный вариант кода может быть таким:
int Foo()
{
return X(1, 2);
}
Запятая иногда может быть полезна при работе с оператором 'return'. Например, следующий код можно сократить, использую запятую.
Длинный код:
if (A)
{
printf("hello");
return X;
}
Сокращённый код:
if (A)
return printf("hello"), X; // Нет предупреждения
Мы не считаем короткий вариант кода красивым и не рекомендуем его использовать. Однако, так часто пишут и такой код имеет смысл. Поэтому, анализатор не выдаёт предупреждение, если стоящее слева от запятой выражение, оказывает влияние на работу программы.
Данная диагностика классифицируется как:
Анализатор обнаружил выражение, которое можно упросить. Иногда это может означать, что выражение содержит логическую ошибку.
Пример подозрительного кода:
int k,n,j;
...
if (n || (n && j))
Это выражение избыточное. Если "n==0", то условие всегда ложно. Если "n!=0", то условие всегда истинно. Таким образом, условие не зависит от значения переменной 'j' и его можно упростить:
if (n)
Иногда, избыточность означает наличие опечатки. Вдруг, например, на самом деле, условие должно быть таким:
if (k || (n && j))
Рассмотрим более реальный пример, благодаря которому и появилась эта диагностика:
const char *Name = ....;
if (Name || (Name && Name[0] == 0))
Здесь сразу есть и ошибка и избыточность. Условие должно выполняться, если строка, на которую ссылается указатель 'Name', пустая. Пустая строка может обозначаться нулевым указателем.
Из-за ошибки условие выполнится всегда, если Name != nullptr. Исправленный код:
if (!Name || (Name && Name[0] == 0))
Теперь ошибки нет. Но дополнительно можно сократить количество проверок:
if (!Name || Name[0] == 0)
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V686. |
Анализатор обнаружил, что к указателю, прибавляется размер массива. Это подозрительно. Возможно следовало прибавлять значение, равное количеству элементов в массиве, а не его размеру.
Примечание. Безопасным считается работа с массивами, состоящими из байт (char/unsigned char).
Пример ошибки:
int A[10];
...
std::sort(A, A + sizeof(A));
Первый аргумент функции - итератор, указывающий на первый элемент сортируемой последовательности.
Второй аргумент функции - итератор, указывающий на элемент, стоящий после последнего сортируемого элемента.
Вызов функции написан с ошибкой. К указателю прибавляется размер массива, в результате чего, функция попытается отсортировать больше данных, чем следует.
Чтобы исправить ошибку, следует прибавлять количество элементов в массиве:
int A[10];
...
std::sort(A, A + sizeof(A) / sizeof(A[0]));
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что имя локальной переменной совпадает с именем члена класса. Часто, это не ошибка. Однако, такой код ОЧЕНЬ опасен. В процессе рефакторинга такого кода, в нём очень легко возникают ошибки. Программист думает, что работает с членом класса, а на самом деле использует локальную переменную.
Пример подозрительного кода:
class M
{
int x;
void F() { int x = 1; foo(x); }
....
};
Класс содержит член с именем 'x'. С таким же именем объявляется локальная переменная в функции F().
Когда функция маленькая как в этом примере, всё просто и понятно. И диагностика V688 кажется неинтересной. Но когда функции большие, неудачное именование переменных может доставить массу головной боли разработчикам, сопровождающим код.
Можно исправить ситуацию, используя другое имя для локальной переменной:
class M
{
int x;
void F() { int value = 1; foo(value); }
....
};
Или можно использовать прифекс 'm_' для обозначения членов класса:
class M
{
int m_x;
void F() { int x = 1; foo(x); }
....
};
Анализатор выдаёт предупреждение не всегда. Он использует некоторые эвристики, чтобы предотвратить ложные срабатывания. Например, этот код анализатор посчитает безопасным:
class M
{
int value;
void SetValue(int value) { this->value = value; }
....
};
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V688. |
Анализатор обнаружил, что умный указатель может неправильно разрушить объект. Причина ошибки в том, что в базовом классе отсутствует виртуальный деструктор.
Рассмотрим пример:
class Base
{
public:
~Base() { }
};
class Derived : public Base
{
public:
Derived()
{
data = new int[5];
}
~Derived()
{
delete [] data;
}
int* data;
};
void GO()
{
std::auto_ptr<Base> smartPtr(new Derived);
}
Обратите внимание, что создаётся объект класса 'Derived'. Однако, умный указатель хранит ссылку на класс Base. В классе Base деструктор не является виртуальным. В результате возникнет ошибка, когда умный указатель попробует разрушить объект, отданный ему на хранение.
Исправленный вариант класса Base:
class Base
{
public:
virtual ~Base() { }
};
P.S.
Родственным диагностическим сообщением является V599.
Дополнительная информация:
Данная диагностика классифицируется как:
|
Анализатор обнаружил класс, в котором реализован конструктор копирования, но не реализован 'operator =', или наоборот, реализован 'operator =', но не реализован конструктор копирования.
Работать с такими классами очень опасно. Другими словами, нарушен "Закон Большой Двойки". Про этот закон, будет рассказано ниже.
Рассмотрим пример опасного класса. Пример длинный. Но сейчас важно только то, что в классе есть оператор присваивания, но нет конструктора копирования.
class MyArray
{
char *m_buf;
size_t m_size;
void Clear() { delete [] m_buf; }
public:
MyArray() : m_buf(0), m_size(0) {}
~MyArray() { Clear(); }
void Allocate(size_t s)
{ Clear(); m_buf = new char[s]; m_size = s; }
void Copy(const MyArray &a)
{ Allocate(a.m_size);
memcpy(m_buf, a.m_buf, a.m_size * sizeof(char)); }
char &operator[](size_t i) { return m_buf[i]; }
MyArray &operator =(const MyArray &a)
{ Copy(a); return *this; }
};
Оставим в стороне практичность и полезность такого класса. Это всего лишь пример. Нам важно, что вот такой код, будет корректно работать:
{
MyArray A;
A.Allocate(100);
MyArray B;
B = A;
}
Массив успешно копируется с помощью оператора присваивания.
Следующий фрагмент кода приведёт к неопределенному поведению. Приложение упадёт или его работа будете нарушена как-то ещё.
{
MyArray A;
A.Allocate(100);
MyArray C(A);
}
Дело в том, что в классе не реализован конструктор копирования. При создание объекта 'C', указатель на массив будет просто скопирован. Это приведёт к двоёному освобождению памяти при разрушении объектов A и C.
Аналогичная проблема будет, если реализован конструктор копирования, но нет оператора копирования.
Что-бы исправить класс, следует добавить конструктор копирования:
MyArray &operator =(const MyArray &a)
{ Copy(a); return *this; }
MyArray(const MyArray &a) : m_buf(0), m_size(0)
{ Copy(a); }
Если анализатор выдал предупреждение V690, то не ленитесь и реализуйте недостающий метод. Сделайте это, даже если код сейчас работает правильно, и вы помните, об особенностях класса. Пройдёт время, забудется отсутствие operator= или конструктора копирования. И вы или ваш коллега допустите ошибку, которую будет сложно найти. Когда поля класса скопированы автоматически, то часто, такой класс "почти работает". Неприятности проявляют себя позже в сосем другом месте программы.
Как было сказано в начале, диагностика V690 находит классы, в которых нарушен "Закон Большой Двойки". Рассмотрим это подробнее. Но начать надо с "Правило трёх". Обратимся к Wikipedia:
Правило трёх (также известное как "Закон Большой Тройки" или "Большая Тройка") — правило в C++, гласящее, что если класс или структура определяет один из следующих методов, то они должны явным образом определить все три метода:
Эти три метода являются особыми членами-функциями, автоматически создаваемыми компилятором в случае отсутствия их явного объявления программистом. Если один из них должен быть определен программистом, то это означает, что версия, сгенерированная компилятором, не удовлетворяет потребностям класса в одном случае и, вероятно, не удовлетворит в остальных случаях.
Поправка к этому правилу заключается в том, что если используется RAII (от англ. Resource Acquisition Is Initialization), то используемый деструктор может остаться неопределённым (иногда упоминается как "Закон Большой Двойки").
Так как неявно определённые конструкторы и операторы присваивания просто копируют все члены-данные класса, определение явных конструкторов копирования и операторов присваивания копированием необходимо в случаях, когда класс инкапсулирует сложные структуры данных или может поддерживать эксклюзивный доступ к ресурсам. А также в случаях, когда класс содержит константные данные или ссылки.
Сам "Закон Большой Двойки" подробно рассматривается в этой статье: The Law of The Big Two.
Как видите, "Закон Большой Двойки" очень важен и поэтому мы реализовали соответствующую диагностику в анализаторе кода.
Начиная с C++11 появилась семантика перемещения, поэтому это правило расширилось до "Большой пятерки". Список методов, которые нужно определить все, если определён, хотя бы один из них:
Поэтому всё, что справедливо для конструктора/оператора копирования, справедливо и для конструктора/оператора перемещения.
Всегда ли диагностика V690 говорит о наличии проблемы? Нет. Иногда никакой ошибки нет, но есть лишняя функция. Рассмотрим пример из реального приложения:
struct wdiff {
int start[2];
int end[2];
wdiff(int s1=0, int e1=0, int s2=0, int e2=0)
{
if (s1>e1) e1=s1-1;
if (s2>e2) e2=s2-1;
start[0] = s1;
start[1] = s2;
end[0] = e1;
end[1] = e2;
}
wdiff(const wdiff & src)
{
for (int i=0; i<2; ++i)
{
start[i] = src.start[i];
end[i] = src.end[i];
}
}
};
В этом классе есть конструктор копирования, но нет оператора присваивания. Но это не страшно. Массивы 'start' и 'end' состоят из простых типов 'int'. Они будут корректно скопированы компилятором. Что-бы устранить предупреждение V690 нужно удалить бессмысленный конструктор копирования. Компилятор построит код, копирует элементы класса не медленней, а возможно даже быстрее.
Исправленный вариант:
struct wdiff {
int start[2];
int end[2];
wdiff(int s1=0, int e1=0, int s2=0, int e2=0)
{
if (s1>e1) e1=s1-1;
if (s2>e2) e2=s2-1;
start[0] = s1;
start[1] = s2;
end[0] = e1;
end[1] = e2;
}
};
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V690. |
Если анализатор находит два одинаковых строковых литерала, он пытается понять, является ли это последствием неудачного Copy-Paste. Хочется сразу предупредить, диагностика основана на эмпирическом алгоритме и иногда может выдавать странные ложные срабатывания.
Рассмотрим пример ошибки:
static const wchar_t left_str[] = L"Direction: left.";
static const wchar_t right_str[] = L"Direction: right.";
static const wchar_t up_str[] = L"Direction: up.";
static const wchar_t down_str[] = L"Direction: up.";
Код писался с использованием Copy-Paste. В конце забыли изменить строковый литерал "up" на "down". Анализатор заподозрит неладное и укажет на подозрительное слово "up" в последней строчке.
Исправленный код:
static const wchar_t left_str[] = L"Direction: left.";
static const wchar_t right_str[] = L"Direction: right.";
static const wchar_t up_str[] = L"Direction: up.";
static const wchar_t down_str[] = L"Direction: down.";;
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V691. |
Анализатор обнаружил интересный паттерн ошибки. Чтобы записать терминальный ноль в конец строки, вычисляют её длину с помощью функции strlen(). Результат такого действия непредсказуем. Ведь для работы функции strlen() строка уже должна заканчиваться терминальным нулём.
Пример некорректного кода:
char *linkname;
....
linkname[strlen(linkname)] = '\0';
Этот код не имеет смысла. Ноль будет записан как раз в ту ячейку, где был найден 0. При этом, функция strlen() может выйти далеко за пределы буфера, что приведет неопределенному поведению программы.
Чтобы исправить код, надо вычислить длину строки каким-то другим способом:
char *linkname;
size_t len;
....
linkname[len] = '\0';
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V692. |
Анализатор обнаружил опечатку в условии остановки цикла.
Пример:
for (size_t i = 0; v.size(); ++i)
sum += v[i];
Если массив 'v' не является пустым, то возникнет бесконечный цикл.
Исправленный вариант кода:
for (size_t i = 0; i < v.size(); ++i)
sum += v[i];
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V693. |
Анализатор обнаружил очень подозрительное условие. К указателю прибавляется или из указателя вычитается константное значение. Результат сложения/вычитания сравнивается с нулём. Высока вероятность, что в коде имеется опечатка.
Рассмотрим пример со сложением:
int *p = ...;
if (p + 2)
Это условие будет всегда истинным. Единственный случай, когда выражение будет равно 0, если специально записать в указатель магическое число "-2".
Исправленный вариант кода:
int *p = ...;
if (*p + 2)
Теперь рассмотрим пример с подозрительным вычитанием:
char *begin = ...;
char *end = ...;
....
const size_t ibegin = 1;
....
if (end - ibegin)
Из переменной 'end' следовало вычитать переменную 'begin'. Из-за неудачного именования переменных, случайно используется константная переменная 'ibegin', имеющая целочисленный тип.
Исправленный вариант кода:
char *begin = ...;
char *end = ...;
....
if (end - begin)
Примечание. Предупреждение выводится только в том случае, если указатель является "настоящим". Например, он указывает на память, выделенную с помощью функции "malloc()". Если анализатор не знает, чему равен указатель, он не выдаст предупреждение, чтобы сократить число ложных срабатываний. Иногда, в указателях действительно передают "магические числа" и условия вида (ptr - 5 == 0) имеют смысл.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V694. |
Анализатор обнаружил потенциальную ошибку в условии. Программа должна совершать различные действия, в зависимости от того, в какой диапазон значений попадает некая переменная.
Для этого в коде используется следующая конструкция:
if ( MIN_A < X && X < MAX_A ) {
....
} else if ( MIN_B < X && X < MAX_B ) {
....
}
Анализатор выдаёт предупреждение, если диапазоны, проверяемые в условиях, пересекаются. Пример:
if ( 0 <= X && X < 10)
FooA();
else if ( 10 <= X && X < 20)
FooB();
else if ( 20 <= X && X < 300)
FooC();
else if ( 30 <= X && X < 40)
FooD();
Код содержит опечатку. Рука программиста дрогнула, и вместо условия "20 <= X && X < 30" была написано "20 <= X && X < 300". Если переменная X будет хранить, например, значение 35, то будет вызвана функция FooC(), а не FooD().
Исправленный код:
if ( 0 <= X && X < 10)
FooA();
else if ( 10 <= X && X < 20)
FooB();
else if ( 20 <= X && X < 30)
FooC();
else if ( 30 <= X && X < 40)
FooD();
Рассмотрим ещё один пример:
const int nv_ab = 5;
const int nv_bc = 10;
const int nv_re = 15;
const int nv_we = 20;
const int nv_tw = 25;
const int nv_ww = 30;
....
if (n < nv_ab) { AB(); }
else if (n < nv_bc) { BC(); }
else if (n < nv_re) { RE(); }
else if (n < nv_tw) { TW(); } // <=
else if (n < nv_we) { WE(); } // <=
else if (n < nv_ww) { WW(); }
В зависимости от значения переменной 'n' выполняются различные действия. Из-за неудачных названий переменных, легко запутаться. Так и получилось. В начале надо было сравнить переменную 'n' с 'nv_we', а только затем с 'nv_tw'.
Чтобы ошибка стала понятна, подставим значения констант:
if (n < 5) { AB(); }
else if (n < 10) { BC(); }
else if (n < 15) { RE(); }
else if (n < 25) { TW(); }
else if (n < 20) { WE(); } // Условие никогда не выполняется
else if (n < 30) { WW(); }
Исправленный код:
if (n < nv_ab) { AB(); }
else if (n < nv_bc) { BC(); }
else if (n < nv_re) { RE(); }
else if (n < nv_we) { WE(); } // <=
else if (n < nv_tw) { TW(); } // <=
else if (n < nv_ww) { WW(); }
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V695. |
Анализатор обнаружил код, который может ввести в заблуждение программиста. Не все знают, что оператор continue в цикле "do { ... } while(0)" остановит цикл, а не возобновит его.
Вот что говорит стандарт:
§6.6.2 in the standard: "The continue statement (...) causes control to pass to the loop-continuation portion of the smallest enclosing iteration-statement, that is, to the end of the loop." (Not to the beginning.)
Таким образом, после вызова оператора 'continue' будет проверено условие (0), и цикл завершится так как условие ложно.
Рассмотрим пример:
int i = 1;
do {
std::cout << i;
i++;
if(i < 3) continue;
std::cout << 'A';
} while(false);
Программист может ожидать, что программа напечатает "12A". На самом деле будет напечатано "1".
Если именно так задумано и ошибки нет, то код всё равно лучше изменить. Можно воспользоваться оператором 'break':
int i=1;
do {
std::cout << i;
i++;
if(i < 3) break;
std::cout << 'A';
} while(false);
Код стал более понятным. Сразу видно, что если условие (i < 3) выполняется, то цикл будет остановлен. В добавок, анализатор не будет выдавать предупреждение на этот код.
Если код некорректен, то его следует переписать. Здесь нельзя дать точных рекомендаций. Все зависит от логики работы кода. Например, чтобы напечатать "12A" лучше будет написать:
for (i = 1; i < 3; ++i)
std::cout << i;
std::cout << 'A';
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V696. |
Количество элементов в массиве, выделяемого с помощью оператора 'new' равно размеру указателя в байтах. Это очень подозрительный участок кода.
Рассмотрим пример, как в программе может появиться такой код. В начале, в программе был фиксированный массив, состоящий из байт. Необходимо создать массив такого-же размера, но из элементов float. В результате был написан следующий код:
void Foo()
{
char A[10];
....
float *B = new float[sizeof(A)];
....
}
Оставим в стороне качество этого кода. Главное, что в процессе рефакторинга, массив 'A' стал тоже динамическим. Фрагмент, где создаётся массив 'B' изменить забыли. В результате, появляется следующий некорректный код:
void Foo(size_t n)
{
char *A = new char[n];
....
float *B = new float[sizeof(A)];
....
}
Количество элементов в массиве 'B' равно 4 или 8, в зависимости от разрядности платформы. Именно эту ситуацию и выявит анализатор кода.
Исправленный вариант кода:
void Foo(size_t n)
{
char *A = new char[n];
....
float *B = new float[n];
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил в коде сравнение результата функции strcmp() или ей подобной с единицей или с минус единицей. В свою очередь, в спецификации языков C/С++ сказано, что функция strcmp() может вернуть любое положительное или любое отрицательное значение в случае неравенства строк, не только единицу или минус единицу.
В зависимости от реализации, функция strcmp() может в результате неравенства строк возвращать:
Работоспособность конструкций вида strcmp() == 1 зависит от библиотек, компилятора, его настроек, операционной системы, её разрядности и так далее; требуется всегда в таком случае писать strcmp() > 0.
В качестве примера можно привести следующий некорректный код:
std::vector<char *> vec;
....
std::sort(vec.begin(), vec.end(), [](
const char * a, const char * b)
{
return strcmp(a, b) == 1;
});
При смене компилятора, целевой операционной системы или разрядности компилируемого приложения данный код может начать работать некорректно.
Исправленный вариант кода:
std::vector<char *> vec;
....
std::sort(vec.begin(), vec.end(), [](
const char * a, const char * b)
{
return strcmp(a, b) > 0;
});
Анализатором также считает код ошибочным, если в нём сравнивается результат работы двух strcmp() функций. Подобный код встречается крайне редко, однако на него в любом случае стоит обратить внимание.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V698. |
Анализатор обнаружил в коде выражение вида 'foo = bar = baz ? xyz : zzy'. Вполне возможно, что это является ошибкой: имелось в виду выражение вида 'foo = bar == baz ? xyz : zzy', однако по недосмотру программиста вместо сравнения выполняется присваивание.
В качестве примера можно привести следующий некорректный код:
int newID = currentID = focusedID ? focusedID : defaultID;
Программист совершил ошибку и вместо оператора сравнения написал оператор присваивания. Исправленный вариант кода выглядит следующим образом:
int newID = currentID == focusedID ? focusedID : defaultID;
Отметим, что приведённый ниже код не вызовет предупреждения, поскольку выражение перед тернарным оператором явно имеет тип bool, из-за чего анализатор предполагает, что именно так и было задумано.
result = tmpResult = someVariable == someOtherVariable? 1 : 0;
Этот код достаточно очевиден. Он эквивалентен следующему более многословному коду:
if (someVariable == someOtherVariable)
tmpResult = 1;
else
tmpResult = 0;
result = tmpResult;
Данная диагностика классифицируется как:
|
Анализатор обнаружил в коде выражение вида 'T foo = foo = X'. Переменная при инициализаторе сама же участвует в присваивании. В отличие от диагностики V593, переменная foo является инициализированной выражением X, однако этот код является крайне подозрительным: скорее всего, имелось в виду что-то другое.
В качестве примера можно привести следующий некорректный код:
int a = a = 3;
Сложно сказать, что имелось в виду в этом случае. Возможно, корректный код выглядит так:
int a = 3;
Также возможно, что подразумевалось инициализация через присваивание значения некоторой другой переменной:
int a = b = 3;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V700. |
Анализатор обнаружил в коде выражение вида 'foo = realloc(foo, ...)'. Данное выражение является потенциально опасным: рекомендуется результат функции realloc сохранять в другой переменной.
Функция realloc(ptr, ...) производит изменение размера некоторого блока памяти. В случае, если размер блока памяти изменить удалось, не прибегая к перемещению данных, результирующий указатель совпадёт с исходным ptr. В случае, если изменение размера блока памяти осуществить невозможно без его перемещения, функция вернёт указатель на новый блок памяти, а старый блок памяти будет освобождён. Однако в том случае, если изменение размера блока памяти в данный момент вообще невозможно даже с перемещением, функция вернёт нулевой указатель. Такая ситуация может возникнуть в случае выделения большого массива данных, размер которого сопоставим с размером ОЗУ компьютера, а также при сильной сегментации памяти. Этот случай и является потенциально опасным: если realloc(ptr, ...) возвращает нулевой указатель, блок данных по адресу ptr не изменяет своего размера. Главная проблема заключается в том, что при использовании конструкции вида "ptr = realloc(ptr, ...)" указатель ptr на этот блок данных будет утерян.
В качестве примера можно привести следующий некорректный код, взятый из реального приложения:
void buffer::resize(unsigned int newSize)
{
if (capacity < newSize)
{
capacity = newSize;
ptr = (unsigned char *)realloc(ptr, capacity);
}
}
Функция realloc(...) производит изменение размера буфера в случае, если требуемый размер буфера больше текущего. Однако, что случится, если realloc() не сможет выделить память? В результат ptr будет помещён NULL, что само по себе может привести к многочисленным проблемам, но более того: указатель на исходную область памяти будет утерян. Корректный код будет выглядеть следующим образом:
void buffer::resize(unsigned int newSize)
{
if (capacity < newSize)
{
capacity = newSize;
unsigned char * tmp = (unsigned char *)realloc(ptr, capacity);
if (tmp == NULL)
{
/* Handle exception; maybe throw something */
} else
ptr = tmp;
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V701. |
Анализатор обнаружил класс, унаследованный от класса std::exception (или аналогичных классов) через модификатор private или protected. Данное наследование опасно тем, что при непубличном наследовании при попытке поймать исключение std::exception оно будет пропущено.
Ошибка часто возникает из-за того, что забывают указать тип наследования. В соответствии с правилами языка, наследование по умолчанию - приватное. Как результат, обработчики исключений ведут себя не так, как задумывалось.
В качестве примера можно привести следующий некорректный код:
class my_exception_t : std::exception // <=
{
public:
explicit my_exception_t() { }
virtual const int getErrorCode() const throw() { return 42; }
};
....
try
{ throw my_exception_t(); }
catch (const std::exception & error)
{ /* Can't get there */ }
catch (...)
{ /* This code executed instead */ }
Код, который должен будет перехватывать все стандартные и пользовательские исключения "catch (const std::exception & error)", не сможет отработать правильно, потому что приватное наследование исключает неявное преобразование типов.
Для того, чтобы код работал корректно, требуется добавить перед родительским классом std::exception в списке базовых классов модификатор public:
class my_exception_t : public std::exception
{
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V702. |
Анализатор обнаружил в коде класса-наследника поле, совпадающее по типу и по имени с некоторым полем родительского класса. Данное объявление может является ошибочным Технология наследования сама по себе предполагает наличие всех полей родительского класса в дочернем. Объявив же в наследнике поля с таким же именем, мы вносим путаницу и усложняем жизнь программистам, которые будут сопровождать код.
В качестве примера можно привести следующий некорректный код:
class U {
public:
int x;
};
class V : public U {
public:
int x; // <=
int z;
};
Данный код может быть опасен, поскольку теперь в классе V существует две переменных x: собственно 'V::x' и 'U::x'. Последствия, к которым это может привести, могут быть проиллюстрированы следующим кодом:
int main() {
V vClass;
vClass.x = 1;
U *uClassPtr = &vClass;
std::cout << uClassPtr->x << std::endl; // <=
....
}
Здесь будет произведена печать неинициализированной переменной.
Для исправления проблемы достаточно удалить объявление переменной в классе-наследнике. Корректный код приведён ниже:
class U {
public:
int x;
};
class V : public U {
public:
int z;
};
Существует некоторое количество спорных случаев, которые анализатором не считаются ошибочными:
Мы рекомендуем всегда проводить рефакторинг кода, где анализатор выдаёт предупреждение V703. Использование переменной с одним именем в базовом классе и в наследнике далеко не всегда является ошибкой. Но такой код очень опасен. Даже если сейчас программа корректно работает, в дальнейшем при модификации классов очень легко допустить ошибку.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V703. |
Анализатор обнаружил в коде выражение вида 'this == 0'. Данное выражение может оказаться работоспособным в ряде случаев, однако его использование крайне опасно по некоторым соображениям.
Рассмотрим простой пример:
class CWindow {
HWND handle;
public:
HWND GetSafeHandle() const
{
return this == 0 ? 0 : handle;
}
};
Вообще, вызов метода CWindow::GetSafeHandle() для нулевого указателя 'this' по стандарту С++ ведёт к неопределённому поведению. Однако поскольку во время работы метода не производится доступа к полям этого класса, метод может работать. С другой стороны, существует два возможных неблагоприятных сценария выполнения данного кода. Во-первых, согласно стандарту С++, указатель this никогда не может быть нулевым; следовательно, компилятор может оптимизировать вызов метода, упростив его до:
return handle;
Во-вторых, предположим, что существует следующий код:
class CWindow {
.... // CWindow из предыдущего примера
};
class MyWindowAdditions {
unsigned long long x; // 8 bytes
};
class CMyWindow: public MyWindowAdditions, public CWindow {
....
};
....
void foo()
{
CMyWindow * nullWindow = NULL;
nullWindow->GetSafeHandle();
}
Этот код приведёт к чтению из памяти по адресу 0x00000008. В этом можно убедиться, написав следующую строку:
std::cout << nullWindow->handle << std::endl;
На экран на печать будет выведен адрес 0x00000008, поскольку исходный указатель NULL (0x00000000) был скорректирован таким образом, чтобы указывать на начало подобъекта класса CWindow. Для этого его надо сместить на sizeof(MyWindowAdditions) байт.
Самое интересное, что теперь проверка "this == 0" полностью теряет смысл. Указатель 'this' всегда по меньшей мере равен значению 0x00000008.
С другой стороны, ошибка не проявит себя, если поменять местами базовые классы в объявлении CMyWindow:
class CMyWindow: public CWindow, public MyWindowAdditions{
....
};
Всё это может приводить к крайне неочевидным ошибкам.
К сожалению, исправление кода достаточно нетривиально. По идее, корректным в данном случае выходом будет изменить метод класса на статический. Это повлечёт за собой большое количество изменений во всех местах, где встречался вызов метода.
class CWindow {
HWND handle;
public:
static HWND GetSafeHandle(CWindow * window)
{
return window == 0 ? 0 : window->handle;
}
};
Второй вариант – использование паттерна Null Object, что тоже повлечёт за собой значительный объём работ.
class CWindow {
HWND handle;
public:
HWND GetSafeHandle() const
{
return handle;
}
};
class CNullWindow : public CWindow {
public:
HWND GetSafeHandle() const
{
return nullptr;
}
};
....
void foo(void)
{
CNullWindow nullWindow;
CWindow * windowPtr = &nullWindow;
// Выведет 0
std::cout << windowPtr->GetSafeHandle() << std::endl;
}
Стоит добавить, что данный дефект очень опасен тем, что на его обработку почти никогда нет времени, поскольку "всё и так работает", а затраты на рефакторинг велики. Однако то, что работало годами, может неожиданно дать сбой при малейшем изменении условий: сборка под другую ОС, изменение версии компилятора (в том числе и его обновление) и так далее. Стоит привести следующий пример: компилятор GCC, начиная с версии 4.9.0, научился выбрасывать проверку на неравенство нулю разыменованного выше по коду указателя (см. диагностику V595):
int wtf( int* to, int* from, size_t count ) {
memmove( to, from, count );
if( from != 0 ) // <= после оптимизации условие всегда истинно
return *from;
return 0;
}
Примеров проблемного кода из реальных приложений, оказавшегося "сломанным" из-за undefined behavior, достаточно много. Стоит привести несколько из них, чтобы подчеркнуть важность проблемы.
Пример N1. Уязвимость в ядре Linux
struct sock *sk = tun->sk; // initialize sk with tun->sk
....
if (!tun) // <= всегда false
return POLLERR; // if tun is NULL return error
Пример N2. Некорректная работа srandomdev():
struct timeval tv;
unsigned long junk; // <= Не инициализирована специально
gettimeofday(&tv, NULL);
// LLVM: аналог srandom() от неинициализированной переменной,
// т.е. tv.tv_sec, tv.tv_usec и getpid() не учитываются.
srandom((getpid() << 16) ^ tv.tv_sec ^ tv.tv_usec ^ junk);
Пример N3. Синтетический пример, очень наглядно показывающий и возможности компиляторов по агрессивной оптимизации в связи с undefined behavior, и новые возможности "прострелить себе ногу":
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int*)malloc(sizeof(int));
int *q = (int*)realloc(p, sizeof(int));
*p = 1;
*q = 2;
if (p == q)
printf("%d %d\n", *p, *q); // <= Clang r160635: Вывод: 1 2
}
Насколько нам известно, на дату выхода этой диагностики вызов проверки this == 0 ещё не проигнорирован ни одним компилятором, однако это – дело времени, поскольку в стандарте С++ явным образом написано (§9.3.1/1): "If a nonstatic member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.". Иными словами, результат вызова любой нестатической функции для класса с this == 0 не определён. Это лишь дело времени, когда компиляторы начнут вместо (this == 0) подставлять false на этапе компиляции.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V704. |
Данная диагностика похожа на диагностику V628, однако относится к ветви else оператора if. Анализатор обнаружил подозрительное место в коде – возможно, забытый или некорректно закомментированный блок else.
Данную ситуацию лучше всего разобрать на примерах.
if (!x)
t = x;
else
z = t;
В данном примере форматирование кода не совпадает с его логикой: выражение z = t выполнится лишь в случае, если (x == 0) – вряд ли это имелось в виду. Подобная же ситуация может возникнуть при неудачно закомментированном фрагменте кода:
if (!x)
t = x;
else
//t = -1;
z = t;
В данном случае требуется либо исправить форматирование, превратив его в более удобочитаемое, либо исправить логическую ошибку, добавив недостающую ветвь оператора if.
Правда, иногда существуют случаи, в которых тяжело определить, является ли подобный код некорректным или таким образом стилизованным. Анализатор пытается уменьшать количество ложных срабатываний, связанных со стилизацией, путём некоторого эвристического анализа. К примеру, следующий код не приведёт к срабатыванию диагностического правила:
if (x == 1)
t = 42;
else
if (x == 2)
t = 84;
else
#ifdef __extended__x
if (x == 3)
t = 741;
else
#endif
t = 0;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V705. |
Анализатор обнаружил в коде подозрительное деление результата оператора sizeof() на другой оператор sizeof() или число. Причём оператор sizeof() применяется к массиву и размер элемента не совпадает с значением делителя. Скорее всего, в коде присутствует ошибка.
Пример:
size_t A[10];
n = sizeof(A) / sizeof(unsigned);
В режиме сборки 32-битной программы типы unsigned и size_t совпадают по своему размеру и 'n' будет равно десяти. Однако в режиме сборки 64-битной программы тип size_t имеет размер 8 байт, а тип unsigned имеет размер всего 4 байта. В итоге в переменную n будет записано число 20, что вряд ли является тем, чего хотел программист.
Также ошибочным будет считаться код следующего вида:
size_t A[9];
n = sizeof(A) / 7;
Размер массива в случае компиляции 32-битной программы равен 4 * 9 = 36 байтам. Делить 36 на 7 очень странно. Что хотел программист? С эти кодом что-то не так.
Какие-то конкретные рекомендации в этом случае давать сложно, поскольку каждый случай требует индивидуального подхода - возможно, изменился размер типа, размер массива задан с ошибкой и так далее. Часто это ошибка бывает последствием опечатки или простой невнимательности.
Анализатор не будет выдавать предупреждение, если массив имеет тип char или uchar, поскольку подобные массивы часто используются в качестве буферов для хранения каких-либо данных другого типа. Пример кода, который анализатор считает безопасным:
char A[9];
n = sizeof(A) / 3;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V706. |
Анализатор обнаружил в коде глобально объявленную переменную с коротким именем. Это, если и не приведёт к ошибкам, то является плохим стилем программирования и усложняет понимание программы.
Начнём с примера:
int i;
Проблема в коротких именах переменных заключается в том, что достаточно легко ошибиться и использовать вместо локальной переменной глобальную внутри тела функции или метода класса. Например, вместо
void MyFunc()
{
for (i = 0; i < N; i++)
AnotherFunc();
....
}
должно быть:
void MyFunc()
{
for (int i = 0; i < N; i++)
AnotherFunc();
....
}
В данных ситуациях анализатор будет предлагать изменить имя переменной на более длинное. Минимальная длина, которая не является подозрительной для анализатора - три символа. Также анализатор не будет выдавать предупреждения на переменные с именами PI, SI, CR, LF.
Анализатор не выдаёт предупреждения на переменные с коротким именем, если они представляют собой структуры. Хотя это тоже плохой стиль кодирования, но случайно неправильно использовать структуру сложнее. Например, если случайно написать так:
struct T { int a, b; } i;
void MyFunc()
{
for (i = 0; i < N; i++)
AnotherFunc();
....
}
То код просто не скомпилируется.
А вот на константы с короткими именами анализатор ругается. Их нельзя менять, но ничто не мешает сделать неправильную проверку. Пример:
const float E = 2.71828;
void Foo()
{
S *e = X[i];
if (E)
{
e->Foo();
}
....
}
Исправленный код:
const float E = 2.71828;
void Foo()
{
S *e = X[i];
if (e)
{
e->Foo();
}
....
}
Но лучше использовать более длинное имя или обернуть такие константы в специальный namesapce:
namespace Const
{
const float E = 2.71828;
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V707. |
Анализатор обнаружил в коде программы случай неопределённого поведения, связанного с контейнерами типа map или схожими с ним.
Пример некорректного кода:
std::map<size_t, size_t> m;
....
m[0] = m.size();
Этот пример приводит к неопределённому поведению, поскольку порядок вычисления операндов у оператора присваивания не определён. В случае, если в объекте уже существует элемент, ассоциированный с нулём, то проблем не возникнет. Однако в случае его отсутствия действия программы могут пойти по двум различным сценариям в зависимости от версии компилятора, операционной системы и так далее.
Предположим, что компилятор сначала вычисляет правый операнд и лишь потом – левый операнд операции присваивания. Поскольку контейнер пуст, m.size() возвращает ноль. Затем с нулём ассоциируется ноль. В итоге имеем, что m[0] == 0.
Теперь предположим, что компилятор сначала вычисляет левый операнд и лишь потом – правый. Сначала будет взят m[0]. Поскольку с нулём ничего не ассоциировано, будет создана пустая ассоциация. Затем вычисляется m.size(). Поскольку контейнер уже не пуст, m.size() возвращает единицу. После этого единица ассоциируется с нулём. В итоге имеем, что m[0] == 1.
Правильным решением для исправления кода будет использования временной переменной и заранее связать какое-то значение с нулём:
std::map<size_t, size_t> m;
....
m[0] = 0;
const size_t mapSize = m.size();
m[0] = mapSize;
Несмотря на то, что данная ситуация может возникать достаточно редко в реальном коде, она опасна тем, что фрагмент кода, приводящий к неопределённому поведению, часто очень сложно выявить.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V708. |
В коде обнаружено логическое выражение вида a == b == c. К сожалению, иногда программисты забывают, что правила языков С и С++ не совпадают с правилами, принятыми в математике (и на первый взгляд – со здравым смыслом) и таким сравнением можно проверить равенство трёх переменных. Однако на самом деле вместо проверки на равенство будет вычислено несколько не то, что хотелось.
Разберём пример.
if (a == b == c) ....
Предположим, что a == 2, b == 2 и c == 2. Первое сравнение (a == b) окажется истинным, поскольку 2 == 2. В результате данное сравнение возвращает значение true (1). Второе же сравнение (... = c) вернёт уже значение false, поскольку true != 2. Для того, чтобы сравнение трёх (и более) переменных давало корректный результат, следует воспользоваться следующим выражением:
if (a == b && b == c) ....
В данном случае действительно a == b вернёт true, b == c вернёт true, и результат логической операции AND будет также равен true.
Однако часто выражения, подобные ошибочным, пишутся для сокращения количества кода. Анализатор не выдаст предупреждение в случаях, если:
1) Третья переменная имеет тип bool, BOOL и т.п. или сама по себе равна 0, 1, true или false. В этом случае ошибка крайне маловероятна – скорее всего, программист написал правильный код:
bool compare(int a, int b, bool res)
{
return a == b == res;
}
2) В выражении присутствуют скобки. В этом случае очевидно, что программист понимает логику и хочет, чтобы выражение выполнялось именно так, как написано:
if ((a == b) == c) ....
В случае, если анализатор выдал ложное срабатывание V709, рекомендуемым путём его устранения является добавление скобок как в примере выше. Это позволить указать другим программистам, что код корректен.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V709. |
Анализатор обнаружил в коде подозрительный участок кода, где создаётся константная ссылка на числовой литерал. Это действие не имеет практического смысла и скорее всего является последствием какой-то опечатки. Возможно, используется не тот макрос или что-то ещё.
Пара примеров:
const int & u = 7;
const double & v = 4.2;
Данное предупреждение лучше всего не подавить, а исправить путём удаления знака амперсанда, превращая ссылку в обычное константное значение (предварительно, разумеется, проверив, действительно ли так всё и задумывалось):
const int u = 7;
const double v = 4.2;
В теле цикла обнаружено объявление переменной, совпадающей с переменной, используемой для контроля цикла. В то время, как для цикла for и цикла foreach (C++11) это может и не нести серьёзной опасности, всё равно это является нехорошим стилем программирования. С циклами же do {} while и while {} всё намного опаснее. Возможно случайное изменение новой переменной в теле цикла вместо изменения переменной в условии цикла.
Разберём пример.
int ret;
....
while (ret != 0)
{
int ret;
ret = SomeFunctionCall();
while (ret != 0)
{
DoSomeJob();
ret--;
}
ret--;
}
В данной ситуации может возникнуть вечный цикл, поскольку внешняя переменная 'ret' в теле цикла не изменяется вообще. Очевидным решением в данном случае будет изменить имя внутренней переменной:
int ret;
....
while (ret != 0)
{
int innerRet;
innerRet = SomeFunctionCall();
while (innerRet != 0)
{
DoSomeJob();
innerRet--;
}
ret--;
}
Анализатор не всегда выдаёт предупреждение V711 в случае, если видит переменную с тем же именем, что используется в теле цикла. К примеру, следующий код не вызовет подозрений:
int ret;
....
while (--ret != 0)
{
int ret;
ret = SomeFunctionCall();
while (ret != 0)
{
DoSomeJob();
ret--;
}
}
Также анализатор не выдаст предупреждения в случае, если подозрительные переменные имеют явно несоответствующие друг другу типы (к примеру, класс и указатель на int). В таком случае ошибиться намного сложнее.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V711. |
Анализатор обнаружил в коде цикл с пустым телом, который может быть превращён компилятором после оптимизации в вечный цикл или убран из кода программы. Чаще всего подобные циклы используются для ожидания какого-либо события извне.
Пример:
bool AllThreadsCompleted = false; // Глобальная переменная
....
while (!AllThreadsCompleted);
В данной ситуации оптимизирующий компилятор сделает цикл вечным. Посмотрим на ассемблерный код из Debug-версии:
; 8 : AllThreadsCompleted = false;
mov BYTE PTR ?AllThreadsCompleted@@3_NA, 0
; AllThreadsCompleted
$LN2@main:
; 9 :
; 10 : while (!AllThreadsCompleted);
movzx eax, BYTE PTR ?AllThreadsCompleted@@3_NA
; AllThreadsCompleted
test eax, eax
jne SHORT $LN1@main
jmp SHORT $LN2@main
$LN1@main:
В данном случае проверка, очевидно, присутствует. Теперь же обратимся к Release-версии:
$LL2@main:
; 8 : AllThreadsCompleted = false;
; 9 :
; 10 : while (!AllThreadsCompleted);
jmp SHORT $LL2@main
В Release-версии переход был оптимизирован в безусловный. Из-за подобного различия Debug-версии и Release-версии могут возникать сложные и трудно детектируемые ошибки.
Путей корректировки данной ситуации несколько. Если эта переменная и впрямь используется для контроля логики многопоточной программы, то лучше использовать средства синхронизации операционной системы, такие как мьютексы и семафоры. Другим вариантом исправления может стать добавление модификатора 'volatile' к объявлению переменной, запрещающему оптимизацию:
volatile bool AllThreadsCompleted; // Глобальная переменная
....
while (!AllThreadsCompleted);
Соответствующий ассемблерный код в Release-версии:
$LL2@main:
; 9 :
; 10 : while (!AllThreadsCompleted);
movzx eax, BYTE PTR ?AllThreadsCompleted@@3_NC
; AllThreadsCompleted
test al, al
je SHORT $LL2@main
Однако иногда диагностика V712 выдаётся "совсем не там" и указывает на места, где вечного цикла вообще быть не должно. В таком случае, скорее всего, пустой цикл появился случайно из-за опечатки. В подобной ситуации диагностика часто (но не всегда) пересекается с диагностикой V715.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V712. |
Анализатор обнаружил в коде ситуацию, при котором проверка на nullptr некоторого указателя производится после его использования. В отличие от диагностики V595, данная диагностика работает в пределах одного логического выражения.
Ниже представлен некорректный пример.
if (P->x != 0 && P != nullptr) ....
В этом случае вторая проверка бессмысленна. В случае, если 'P' равен nullptr, то произойдёт ошибка доступа к памяти при попытке разыменовывания нулевого указателя. В таком коде что-то явно не так. Наиболее простой рекомендацией будет поменять местами проверки в логическом выражении:
if (P != nullptr && P->x != 0) ....
Однако в подобном случае всегда рекомендуется провести дополнительный обзор кода. Действительно ли имелось в виду именно это? Возможно, указатель сам по себе не может быть равен nullptr и проверка лишняя. Возможно, разыменовывается или проверяется на nullptr не та переменная. Подобные случаи индивидуальны и трудно дать какую-то общую рекомендацию по исправлению кода.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки разыменования нулевого указателя. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V713. |
Анализатор обнаружил в коде подозрительную ситуацию. Присутствует foreach, в котором имеется присваивание контрольной переменной цикла (loop control variable) некоторого значения. В то же время, передача контрольной переменной цикла идёт по значению. Скорее всего, имелась в виду передача по ссылке.
Пример:
for (auto t : myvector)
t = 17;
Это приведёт к копированию переменной 't' на каждой итерации и изменению локальной копии, что вряд ли то, что хотел программист. Скорее всего, хотелось изменить значения в контейнере 'myvector'. Корректно данный участок кода будет выглядеть следующим образом:
for (auto & t : myvector)
t = 17;
Данная диагностика диагностирует лишь самые простые случаи некорректного использования цикла foreach, поскольку в них проще всего совершить ошибку. В случае более сложных конструкций программист наверняка лучше знает, что он делает и следующие ситуации иногда встречаются в реальном коде:
for (auto t : myvector)
{
function(t); // Использовали t по назначению
// Далее t используется как локальная переменная
t = anotherFunction();
if (t)
break;
}
На этот код анализатор не будет выдавать предупреждение V714.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V714. |
Анализатор обнаружил странный код, связанный с наличием оператора while с пустым телом в неожиданном месте. Оператор 'while' находится после закрывающейся скобки, относящейся к телу оператора 'if', 'for' или другого 'while'. Подобные ошибки могут возникать при работе со сложным кодом с высоким уровнем вложенности. Данная диагностика может иногда пересекаться с диагностикой V712.
Пример из реального приложения:
while (node != NULL) {
if ((node->hashCode == code) &&
(node->entry.key == key)) {
return true;
}
node = node->next;
} while (node != NULL);
Пример полностью синтаксически корректен с точки зрения языка C++: первый цикл 'while' заканчивается закрывающей фигурной скобкой, следом за ним идёт второй цикл while с пустым телом. Более того, второй цикл никогда не станет вечным циклом, поскольку по выходу из первого цикла Node уже точно будет не равен NULL. Однако очевидно, что с этим кодом что-то не так. Возможно, программист сначала хотел написать цикл while, а затем передумал и решил сделать цикл do .... while, однако по какой-то причине не поменял первое условие на do. Возможно, сначала был реализован цикл do .... while, а затем не до конца исправлен на while. Вывод здесь может быть только один: требуется провести обзор кода, а затем переписать его таким образом, чтобы устранить бессмысленный цикл while.
В случае же, если код написан так, как и задумывался, мы советуем не размечать ложное срабатывание, а перенести while на следующую строку для того, чтобы явно указать, что while не относится к предыдущему блоку, и тем самым упростить труд других программистов, которые будут потом работать с проектом.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V715. |
Анализатор обнаружил в коде ситуацию, при которой явным или неявным образом производится преобразование типа bool или BOOL к HRESULT или наоборот. В то время, как такая операция вполне допустима с точки зрения языка C++, она не имеет практического смысла. Тип HRESULT предназначен для хранения статуса, имеет достаточно сложный формат и не имеет ничего общего с типом bool или BOOL.
Можно привести следующий пример из реального приложения:
BOOL WINAPI DXUT_Dynamic_D3D10StateBlockMaskGetSetting(....)
{
if( DXUT_EnsureD3D10APIs() &&
s_DynamicD3D10StateBlockMaskGetSetting != NULL )
....
else
return E_FAIL;
}
Опасность заключается в том, что тип HRESULT представляет собой, на самом деле, тип 'long', а тип BOOL – это ни что иное как 'int'. Эти типы легко преобразуются друг в друга, и компилятор не видит ничего подозрительного в приведённом выше коде.
Однако с точки зрения программиста эти типы означают совершенно разное. В то время как BOOL обозначает логическую переменную, тип HRESULT устроен достаточно сложно и должен сигнализировать о том, прошла ли операция успешно, какой результат был возвращён после выполнения операции, в случае ошибки – где произошла ошибка, обстоятельства этой ошибки и так далее.
О типе HRESULT. Первый бит слева (то есть самый старший бит) хранит успешность операции: в случае, если операция прошла успешно, первый бит устанавливается в ноль, иначе – в единицу. Дальнейшие четыре бита характеризуют вид ошибки. Одиннадцать бит далее характеризуют модуль, в котором произошла исключительная ситуация. Последние, самые младшие шестнадцать бит характеризуют статус выполнения операции: в случае ошибки он может указывать на код ошибки, в случае успешного выполнения – статус успешного выполнения. Таким образом, неотрицательные значения обычно сигнализируют об успешном выполнении операции. При этом часто применяется макро константа 'S_OK', равная 0.
Более подробное описание HRESULT можно увидеть в статье на сайте MSDN. А на этой странице список наиболее часто применяемых значений.
Тип BOOL для индикации значения "ложь" должен быть равен нулю, в противном случае он указывает на значение "истина". Иными словами, эти два типа крайне похожи друг на друга в плане типов и их приведение друг к другу не влечёт за собой с точки зрения языка ничего страшного, однако операция приведения лишена смысла. Ведь по первоначальному замыслу тип HRESULT хранит в себе не только информацию об успехе или неудаче (и код ошибки в случае неудачи), но и может сохранять в себе некоторую дополнительную информацию в случае успешного вызова. Особенно подпортить жизнь может значение S_FALSE, равное 0x1. То, что ненулевые значения возвращаются в случае успешного вызова крайне редко, может стать причиной мучительных поисков ошибок, проявляющихся очень иногда и совсем изредка. Зачастую можно встретить конструкцию вида:
HRESULT result = someWinApiFunction(....);
if (!result)
{
// This is an error!
}
Такой код абсолютно ошибочен, поскольку проверка на ошибку сработает, если функция вернёт 0 в случае успеха. При этом код обработки ошибки не сработает, когда функция просигнализирует о проблеме, вернув отрицательное число. При этом подобные неявные преобразования между целочисленным и логическим типом могут происходить в более сложных выражениях, где ошибку сложно заметить невооруженным глазом.
Для контроля возвращаемого значения типа HRESULT настоятельно предлагаем воспользоваться макросами SUCCEEDED и FAILED.
HRESULT someFunction(int x);
....
BOOL failure = FAILED(someFunction(q));
В остальных случаях рефакторинг не так уж и прост, как кажется, и требует по крайней мере вдумчивого чтения и анализа кода.
Ещё раз о главном. Помните, что:
Никогда не смешивайте HRESULT и BOOL. Смешивание этих типов является серьёзной ошибкой в логике работы программы. Для проверки значений типа HRESULT используйте специальные макросы.
Для поиска ситуаций, когда в переменную типа HRESULT помещается true или false, существует родственная диагностика V543.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V716. |
Анализатор заметил в коде ситуацию необычного приведения типов: указатель на объект базового класса жёстко приводится к указателю на производный класс. Причём известно, что указатель на базовый класс действительно указывает на объект базового класса.
Приведение указателей производного класса к базовому – вполне типичная ситуация. А вот приведение базового класса к одному из производных, может иногда быть ошибочным. Если приведение типов выполнено неправильно, то попытка обращения к членам производного класса может привести к Access Violation или ко всему чему угодно.
Иногда программисты допускают ошибки, приводя к производному классу указатель именно на базовый класс. Пример из реального приложения:
typedef struct avatarCacheEntry { .... };
struct CacheNode : public avatarCacheEntry,
public MZeroedObject
{
....
BOOL loaded;
DWORD dwFlags;
int pa_format;
....
};
avatarCacheEntry tmp;
....
CacheNode *cc = arCache.find((CacheNode*)&tmp);
// Далее при попытке обращения, например, к
// cc->loaded, произойдёт ошибка времени выполнения.
К сожалению, в данном случае сложно привести какие-то конкретные рекомендации по устранению некорректной ситуации – скорее всего, потребуется заняться рефакторингом кода с целью его улучшения и увеличения удобочитаемости, а также для предотвращения потенциальных ошибок в будущем. Например, если обращение к новым полям не требуется, то тогда можно подменить указатель на базовый класс на указатель на производный.
Следующий код анализатором считается корректным:
base * foo() { .... }
derived *y = (derived *)foo();
Здесь дело в том, что функция foo() может на самом деле всегда возвращать указатель на один из производных классов, и приведение её результата к производному классу – вполне обыденное явление. Вообще, анализатор выдаёт предупреждение V717 только в том случае, если точно известно, что именно базовый класс приводится к производному. Однако, анализатор не будет выдавать предупреждение V717 в том случае, если в производном классе нет новых нестатических членов (хотя всё равно так делать нехорошо, но это уже ближе к нарушению хорошего стиля кода, нежели к настоящей ошибке):
struct derived : public base
{
static int b;
void bar();
};
....
base x;
derived *y = (derived *)(&x);
Данная диагностика классифицируется как:
Внутри функции DllMain() нельзя вызывать многие функции, так как это может привести к зависанию приложения или иным ошибкам. Именно такой опасный вызов функции и был обнаружен анализатором.
Ситуация с DllMain хорошо описана в статье на сайте MSDN: Dynamic-Link Library Best Practices. Процитируем из неё некоторые фрагменты:
При вызове функции DllMain происходит блокировка загрузчика. По этой причине на функции, которые могут быть вызваны внутри DllMain, накладываются существенные ограничения. Как таковая, функция DllMain предназначена для выполнения задач по минимальной инициализации за счет использования небольшого подмножества Microsoft Windows API. Внутри нее нельзя вызывать функции, которые прямо или косвенно пытаются использовать загрузчик. В противном случае, Вы рискуете создать в программе такие условия, при которых произойдет ее аварийное завершение либо взаимная блокировка потоков. Ошибка в реализации DllMain может подвергнуть опасности весь процесс целиком и все его потоки.
В идеале функция DllMain должна представлять собой всего лишь пустую заглушку. Однако, учитывая сложность многих приложений, данное ограничение было бы слишком строгим. Поэтому на практике при работе с этой функцией следует откладывать инициализацию как можно дольше. Отложенная инициализация повышает надежность работы приложения, поскольку она не происходит, пока загрузчик заблокирован. Кроме того, отложенная инициализация позволяет безопасно использовать Windows API в значительно большем объеме.
Некоторые задачи инициализации не могут быть отложены. Например, DLL-библиотека, которая зависит от файла конфигурации, должна прерывать свою загрузку, если файл оказывается неправильно сформированным или содержит мусор. При таком типе инициализации DLL-библиотеки должны предпринимать попытку выполнить запланированное действие и в случае неудачи сразу же завершаться вместо того, чтобы тратить ресурсы, выполняя какую-то другую работу.
В любом случае никогда не выполняйте следующие задачи в пределах функции DllMain:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V718. |
Анализатор обнаружил подозрительный оператор 'switch'. Выбор варианта осуществляется по переменной enum-типа. При этом рассмотрены не все возможные варианты.
Поясним это на примере:
enum TEnum { A, B, C, D, E, F };
....
TEnum x = foo();
switch (x)
{
case A: Y(11); break;
case B: Y(22); break;
case C: Y(33); break;
case D: Y(44); break;
case E: Y(55); break;
}
Перечисление TEnum содержит 6 именованных констант. Но в операторе 'switch' используется только 5 из них. Высока вероятность, что это ошибка.
Такая ошибка часто возникает в ходе рефакторинга. В 'TEnum' добавили константу 'F". После этого какие-то 'switch' поправили, а про какие-то забыли. В результате, значение 'F' начинает обрабатываться неправильно.
Анализатор предупредит о том, что константа 'F' не используется. И тогда программист может исправить оплошность:
switch (x)
{
case A: Y(11); break;
case B: Y(22); break;
case C: Y(33); break;
case D: Y(44); break;
case E: Y(55); break;
case F: Y(66); break;
}
Анализатор выдает предупреждение далеко не всегда, когда в 'switch' используется не все константы из перечисления. Иначе, было бы слишком много ложных срабатываний. Действует целый ряд исключений эмпирического типа. Основные:
Пользователь может сам задать список имён, которые обозначают последний элемент в перечислении. В этом случае анализатор не использует список имён по умолчанию, такие как "num" или "count". Будут использованы только имена, указанные пользователем. Комментарий, управляющий поведением диагностики V719:
//-V719_COUNT_NAME=ABCD,FOO
Вы можете разметить этот комментарий в одном из файлов, включаемый во все другие. Например, в StdAfx.h.
Описанные исключения являются взвешенным решением, проверенными на практике. Единственное что стоит рассмотреть подробнее, это отсутствие предупреждений, когда есть 'default'. Не всегда это хорошо.
С одной стороны, анализатору нельзя ругаться если константы не используются, но при это есть 'default'. Будет слишком много ложных срабатываний и пользователи просто будут отключать эту диагностику. С другой стороны, весьма типовой ситуацией является, когда в 'switch' следует рассмотреть все варианты, а ветка 'default' используется для отлова аварийных ситуаций. Пример:
enum TEnum { A, B, C, D, E, F };
....
TEnum x = foo();
switch (x)
{
case A: Y(11); break;
case B: Y(22); break;
case C: Y(33); break;
case D: Y(44); break;
case E: Y(55); break;
default:
throw MyException("Ай! Забыли рассмотреть один из вариантов!");
}
Ошибка может быть обнаружена только на этапе исполнения. Естественно есть желание отловить эту ситуацию и с помощью анализатора. В наиболее ответственных местах кода можно поступить следующим образом:
enum TEnum { A, B, C, D, E, F };
....
TEnum x = foo();
switch (x)
{
case A: Y(11); break;
case B: Y(22); break;
case C: Y(33); break;
case D: Y(44); break;
case E: Y(55); break;
#ifndef PVS_STUDIO
default:
throw MyException("Ай! Забыли рассмотреть один из вариантов!");
#endif
}
Используется предопределённый макрос PVS_STUDIO. Этот макрос отсутствует при компиляции. Поэтому при компиляции исполняемого файла ветка 'default' остается на своём месте и в случае ошибки возникнет исключение.
При проверке кода с помощью PVS-Studio макрос PVS_STUDIO определён и поэтому анализатор не увидит default-ветку. Поэтому он проверит 'switch', обнаружит что не используется константа 'F' и выдаст предупреждение.
Исправленный вариант кода:
switch (x)
{
case A: Y(11); break;
case B: Y(22); break;
case C: Y(33); break;
case D: Y(44); break;
case E: Y(55); break;
case F: Y(66); break;
#ifndef PVS_STUDIO
default:
throw MyException("Ай! Забыли рассмотреть один из вариантов!");
#endif
}
Описанный подход не очень красив. Однако, если вы очень переживаете за какой-то 'switch' и хотите максимально защитить его, то этот способ вполне подходит.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V719. |
Анализатор обнаружил, что в программе используется функция SuspendThread() или Wow64SuspendThread(). Сам по себе вызов этих функций не является ошибкой. Однако, разработчики часто используют их не по назначению. Из-за этого, программа может вести себя не так, как ожидает программист.
Функция SuspendThread() должна помогать разрабатывать отладчики и подобные им приложения. Если Вы используете эту функцию в прикладном программном обеспечении для задач синхронизации, то высока вероятность, что в вашей программе есть ошибка.
Суть проблемы неправильного использования функции SuspendThread() изложена в следующих статьях:
Прочитайте их. Если выяснится, что функция SuspendThread() используется неправильно, то необходимо переписать код. Если всё хорошо, то просто отключите диагностику V720 в настройках анализатора.
Статьи в интернете иногда пропадают или меняют своё расположение. Поэтому, мы на всякий случай приводим в документации текст обеих статей.
Примечание. Для российских пользователей мы перевели текст статей. Ознакомиться с оригиналом вы можете, перейдя по ссылкам, приведённым ниже или переключить язык нашего сайта.
Это практически так же плохо, как уничтожение потока.
Вместо того, чтобы просто ответить на вопрос, я задам вопросы вам и посмотрю, сможете ли вы найти на них ответы.
Рассмотрим такую программу на (вздох) C#:
using System.Threading;
using SC = System.Console;
class Program {
public static void Main() {
Thread t = new Thread(new ThreadStart(Program.worker));
t.Start();
SC.WriteLine("Press Enter to suspend");
SC.ReadLine();
t.Suspend();
SC.WriteLine("Press Enter to resume");
SC.ReadLine();
t.Resume();
}
static void worker() {
for (;;) SC.Write("{0}\r", System.DateTime.Now);
}
}
Когда вы запускаете эту программу и жмёте Enter, программа зависает. Но если вы измените функцию worker просто на "for(;;) {}", программа будет работать совершенно нормально. Давайте разберёмся, почему так происходит.
Рабочий поток тратит практически всё время своей работы на вызовы System.Console.WriteLine, поэтому, когда вы зовёте Thread.Suspend(), поток с наибольшей вероятностью и находится в System.Console.WriteLine.
Вопрос: Является ли метод System.Console.WriteLine потокобезопасным?
Хорошо, я сам отвечу на этот: Да. Мне даже не нужно было залезать в документацию, чтобы понять это. Наша программа зовёт его из двух разных потоков без какой-либо синхронизации, так что лучше бы ему быть потокобезопасным, иначе у нас начались бы большие неприятности ещё до того, как мы дошли до приостановки потока.
Вопрос: Каким образом обычно объекты делают потокобезопасными?
Вопрос: Каков результат приостановки потока в момент выполнения им потокобезопасной операции?
Вопрос: Что случится, если вы, в дальнейшем, попытаетесь обратиться к тому же объекту (в данном случае – к консоли) из другого потока?
Такой результат не специфичен именно для C#. Подобная логика применима как к потоковой модели Win32, так и к любой другой. В Win32, структура кучи процесса является потокобезопасным объектом, и, поскольку сложно что-либо сделать в Win32 без обращений к куче, приостановка потока в Win32 имеет большую вероятность привести к блокировке вашего процесса.
Но тогда зачем вообще появилась функция SuspendThread?
Отладчики используют её для "заморозки" всех потоков процесса в момент, когда происходит его отладка. Отладчики также могут использовать её при остановке всех потоков процесса, кроме одного, чтобы вы могли отлаживать потоки по одному. Это не создаёт взаимоблокировок в отладчике, ведь он является отдельным процессом.
Итак, коллега решил игнорировать мой совет потому что он прогонял несколько экспериментов с потоковой безопасностью и взаимоблокирующими операциями, и приостановка потока была удобным способом обнаружения "окон", в которых могут возникать гонки доступа к объектам.
В процессе этих экспериментов, он обнаружил некоторое странное поведение.
LONG lValue;
DWORD CALLBACK IncrementerThread(void *)
{
while (1) {
InterlockedIncrement(&lValue);
}
return 0;
}
// This is just a test app, so we will abort() if anything
// happens we don't like.
int __cdecl main(int, char **)
{
DWORD id;
HANDLE thread = CreateThread(NULL, 0, IncrementerThread,
NULL, 0, &id);
if (thread == NULL) abort();
while (1) {
if (SuspendThread(thread) == (DWORD)-1) abort();
if (InterlockedOr(&lValue, 0) != InterlockedOr(&lValue, 0))
{
printf("Huh? The variable lValue was modified by a suspended
thread?\n");
}
ResumeThread(thread);
}
return 0;
}
Странным здесь является то, что сообщение "Huh?" было распечатано. Разве может приостановленный поток модифицировать переменную? Возможно ли, что InterlockedIncrement начнёт увеличивать значение переменной, затем окажется "замороженным", но каким-то образом всё-же закончит начатое позже?
Но ответ гораздо проще. Функция SuspendThread говорит планировщику заморозить поток, но не ждёт подтверждения от него о том, что "заморозка" уже произошла. На это намекается в документации на SuspendThread, где говорится
Эта функция в основном разработана для использования отладчиками. Она не предназначена для синхронизации потоков.
Вам не следует использовать SuspendThread для синхронизации двух потоков, т.к. не предоставляется гарантий для такой синхронизации. На самом деле, SuspendThread просто сигнализирует планировщику приостановить поток и сразу выходит. Если планировщик занят чем-то другим, то возможно он и не сможет обработать запрос на "заморозку" сразу, поэтому приостанавливаемый поток продолжает работать пока планировщик наконец не обработает запрос, и поток наконец-то не окажется приостановленным на самом деле.
Если вы хотите убедиться в том, что поток на самом деле приостановлен, вам нужно выполнить синхронную операцию, которая зависит от факта приостановки потока. Это принуждает выполнение обработки запроса на приостановку, т.к. этот запрос является необходимым условием для вашей операции, и поскольку ваша операция синхронна, вы знаете, что к моменту, когда она завершится, приостановка потока гарантированно произойдёт.
Традиционный способ для этого – позвать GetThreadContext, т.к. это требует от ядра прочитать из контекста приостановленного потока, что, в свою очередь, требует сохранения контекста, что и потребует приостановки потока.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил, что неправильно используется значение типа VARIANT_BOOL. Дело в том, что значение истина (VARIANT_TRUE) обозначается как -1. Многие программисты не ожидают такого подвоха и используют тип неправильно.
Вот как объявлен тип VARIANT_TRUE и константы для обозначения "истина" и "ложь":
typedef short VARIANT_BOOL;
#define VARIANT_TRUE ((VARIANT_BOOL)-1)
#define VARIANT_FALSE ((VARIANT_BOOL)0)
Рассмотрим несколько примеров неправильной работы с типом VARIANT_TRUE. Во всех случаях программист ожидает, что условие будет истинно. Но на самом деле условие всегда ложно.
Пример N1.
VARIANT_BOOL variantBoolTrue = VARIANT_TRUE;
if (variantBoolTrue == true) // false
Если подставить значение в выражение, то мы получим: ((short)(-1) == true). При вычислении выражения, 'true' превратится в '1'. Условие (-1 == 1) ложно.
Правильный вариант:
if (variantBoolTrue == VARIANT_TRUE)
Пример N2.
VARIANT_BOOL variantBoolTrue = TRUE;
if (variantBoolTrue == VARIANT_TRUE) //false
Здесь программист ошибся и использовал TRUE вместо VARIANT_TRUE. В результате переменной variantBoolTrue будет присвоено значение 1. Это недопустимое значение для переменных типа VARIANT_BOOL.
Если подставить значение в выражение, то мы получим: (1 == (short)(-1)).
Правильный вариант:
VARIANT_BOOL variantBoolTrue = VARIANT_TRUE;
Пример N3.
bool bTrue = true;
if (bTrue == VARIANT_TRUE) //false
Раскроем выражение: (true == (short)(-1)). При вычислении выражения 'true' превратится в '1'. Условие (1 == -1) ложно.
Правильный вариант привести затруднительно. Код в принципе неверен. Нельзя смешивать переменные типа 'bool' и значения типа 'VARIANT_TRUE'.
Подобных примеров можно придумать достаточно много. Например, формальный аргумент функции имеет тип VARIANT_BOOL. А в качестве фактического аргумента будет передано значение 'true'. Ещё вариант - функция возвращает неправильно значение. И так далее.
Самое главное - не смешивать тип VARIANT_BOOL с типами BOOL, bool и BOOLEAN.
Дополнительные ссылки:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V721. |
Анализатор обнаружил подозрительное условие, которое может содержать ошибку.
Диагностика носит эмпирический характер, поэтому проще показать на примере, как она работает, чем объяснить сам принцип работы анализатора.
Рассмотрим реальный пример:
if (obj.m_p == p &&
obj.m_forConstPtrOp == forConstVarOp &&
obj.m_forConstPtrOp == forConstPtrOp)
Из-за того, что имена переменных очень похожи, в коде допущена опечатка. Ошибка находится во второй строке. Переменную 'forConstVarOp' следовало сравнить с 'm_forConstVarOp', а не с 'm_forConstPtrOp'. Даже читая этот текст сложно заметить ошибку. Обратите внимание на 'Var' и 'Ptr' внутри названий переменных.
Правильный вариант:
if (obj.m_p == p &&
obj.m_forConstVarOp == forConstVarOp &&
obj.m_forConstPtrOp == forConstPtrOp)
Если анализатор выдал предупреждение V722, то внимательно изучите соответствующий фрагмент кода. Иногда опечатку бывает сложно заметить.
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, когда функция возвращает указатель на внутренний строковый буфер локального объекта. Такой объект будет автоматически уничтожен вместе со своим буфером при выходе из функции, и использовать указатель на него будет нельзя.
В самом простом виде данное диагностическое сообщение будет выдано на следующий код:
const char* Foo()
{
std::string str = "local";
return str.c_str();
}
Здесь из функции Foo() возвращается C-строка, хранящаяся во внутреннем буфере объекта str, который будет автоматически уничтожен. В итоге мы получим неправильный указатель, использование которого в программе приведет к неопределённому поведению. Исправленный вариант кода:
const char* Foo()
{
static std::string str = "static";
return str.c_str();
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил в коде ситуацию, при которой преобразование указателей или переменных целочисленного типа к типу BOOL может приводить к потере старших разрядов. В результате ненулевое значение, которое по сути означает TRUE, может неожиданно превратиться в FALSE.
В программах тип BOOL (gboolean, UBool и т.д.) определяется как целочисленный тип. Любое отличное от нуля значение интерпретируется как истина, а равное нулю – ложь. Поэтому потеря старших битов при преобразовании типов вызовет ошибку в логике работы программы.
Рассмотрим пример:
typedef long BOOL;
__int64 lLarge = 0x12300000000i64;
BOOL bRes = (BOOL) lLarge;
Здесь отличная от нуля переменная при преобразовании в BOOL срезается до нуля, что означает FALSE.
Рассмотрим другие случаи ошибочного преобразования:
int *p;
size_t s;
long long w;
BOOL x = (BOOL)p;
BOOL y = s;
BOOL z = (BOOL)s;
BOOL q = (BOOL)w;
Для исправления таких ошибок необходимо выполнить проверку на ненулевое значение перед преобразованием в BOOL.
Различные способы исправления:
int *p;
size_t s;
long long w;
BOOL x = p != nullptr;
BOOL y = s != 0;
BOOL z = s ? TRUE : FALSE;
BOOL q = !!w;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V724. |
Анализатор обнаружил опасные приведения указателя "this" к типу "void*" и последующее обратное приведение "void*" к типу класса. Само по себе преобразование "this" к типу "void*" не является ошибкой, но в ряде случаев ошибочным является обратное преобразование - от "void*" к типу указателя на класс. В результате таких преобразований возможно получение некорректного указателя.
Описание диагностики достаточно большое и сложное, но, к сожалению, сделать его проще не получается. Просим внимательно прочитать его целиком.
Рассмотрим пример, где используется приведение "this" к "void*", а после - некорректное обратное приведение к типу класса:
class A
{
public:
A() : firstPart(1){}
void printFirstPart() { std::cout << firstPart << " "; }
private:
int firstPart;
};
class B
{
public:
B() : secondPart(2){}
void* GetAddr() const { return (void*)this; }
void printSecondPart() { std::cout << secondPart << " "; }
private:
int secondPart;
};
class C: public A, public B
{
public:
C() : A(), B(), thirdPart(3){}
void printThirdPart() { std::cout << thirdPart << " "; }
private:
int thirdPart;
};
void func()
{
C someObject;
someObject.printFirstPart();
someObject.printSecondPart();
someObject.printThirdPart();
void *pointerToObject = someObject.GetAddr();
....
auto pointerC = static_cast<C*>(pointerToObject);
pointerC->printFirstPart();
pointerC->printSecondPart();
pointerC->printThirdPart();
}
Можно было бы предположить, что вывод будет следующим:
1 2 3 1 2 3
Однако на самом деле на экране отобразится что-то типа:
1 2 3 2 3 -858993460
В итоге вывод для всех данных после преобразований является некорректным. Проблема кроется в том, что теперь указатель "pointerC" указывает не на начало объекта C, а на блок памяти, выделенной под объект B.
Кажется, что такая ошибка надуманна и допустить её невозможно. Однако ошибка очевидна только из-за того, что пример маленький и простой. В настоящих программах со сложными иерархиями классов можно легко запутаться. Особенно коварно то, что если функцию "GetAddr()" расположить в классе A, то всё работает, а если в классе B, то нет. Это может сбивать с толку. Давайте разберемся в ситуации подробнее.
Чтобы было проще понять, из-за чего возникла ошибка, необходимо знать, как конструируются и располагаются в памяти объекты классов, созданных в результате множественного наследования.
Схематичный пример этого изображён на рисунке 1.
Рисунок 1 - Расположение в памяти объекта класса, полученного путём множественного наследования
Из рисунка 1 видно, что объект класса С (который и получен в результате множественного наследования) состоит из объектов классов A и B, плюс часть объекта C.
Указатель "this" содержит в себе адрес начала выделенного под объект блока памяти. На рисунке 2 изображены указатели "this" для всех трёх объектов.
Рисунок 2 - Указатели "this", и блоки памяти
Так как объект класса C состоит из трёх частей, указатель "this" для него будет указывать не на блок памяти, который добавлен дополнительно к базовым классам, а на начало всего непрерывного блока памяти. То есть в данном случае, указатели "this" для классов A и C совпадут.
Указатель "this" для объекта класса B указывает на начало выделенного под него блока памяти, но при этом адрес начала этого участка памяти будет отличен от адреса начала участка памяти, выделенной под объект класса C.
Таким образом, при вызове метода "GetAddr()", мы получим адрес объекта B, и в результате обратного преобразования полученного указателя к типу "C*" будет получен некорректный указатель.
Т.е. если бы функция "GetAddr()" располагалась в классе A, то всё бы работало так, как и ожидал программист. Но если она расположена в B, то происходит сбой.
Во избежание подобных ошибок необходимо продумать, действительно ли стоит выполнять приведение "this" к "void*", и если всё же стоит - тщательно проверять иерархию наследования, а также дальнейшие операции обратного преобразования от "void*" к типу указателя на класс.
Дополнительные ссылки:
Данная диагностика классифицируется как:
Анализатор обнаружил ошибочный код, в котором осуществляется попытка удаления массива через функцию free() или схожую с ней. При этом, память под этот массив не была выделена при помощи специальных функций, таких как malloc(). Подобные действия являются причиной неопределённого поведения.
Пример:
class A
{
int a[50];
public:
A(){}
~A(){ free(a); }
};
Так как память не была выделена каким-то специальным образом, не стоит также и вызывать каких-либо специальных функций для её очистки – она будет произведена автоматически при уничтожении объекта. Следовательно, в данном случае корректный код должен был выглядеть следующим образом:
class A
{
int a[50];
public:
A(){}
~A(){}
};
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V726. |
Анализатор обнаружил выражение, которое на его взгляд предназначается для вычисления размера буфера в байтах, требуемого для хранения строки. В этом выражении допущена ошибка.
Когда стоит задача получения размера строки типа char, стандартным решением является использование конструкции "strlen(str) + 1". Функция strlen() вычисляет длину некой строки, а единица означает резервирование одного байта для терминального нуля. Но если речь идет о строке типа wchar_t, char16_t или char32_t, то следует не забывать умножать "длину строки + 1" на размер одного символа, то есть на 'sizeof(T)'.
Рассмотрим несколько синтетических примеров ошибок.
Пример N1:
wchar_t *str = L"Test";
size_t size = wcslen(str) + 1 * sizeof(wchar_t);
Из-за пропущенных скобок происходит умножение 'sizeof' на единицу, а затем сложение с функцией, вычисляющей длину строки. Корректный код:
size_t size = (wcslen(str) + 1) * sizeof(wchar_t);
Пример N2:
Выражение может быть записано и в другой последовательности, при которой сначала будет происходить умножение результата функции на 'sizeof', а затем сложение с единицей.
.... = malloc(sizeof(wchar_t) * wcslen(str) + 1);
Порой может случиться такая ситуация, когда программист в процессе написания вспомнил, что длину строки следует умножать на "sizeof(wchar_t)", но по привычке добавил 1. В результате памяти будет выделено на 1 байт меньше, чем необходимо.
Корректные варианты кода:
.... = malloc(wcslen(str) * sizeof(wchar_t) + 1 * sizeof(wchar_t));
.... = malloc((wcslen(str) + 1) * sizeof(wchar_t));
Данная диагностика классифицируется как:
|
Анализатор обнаружил код, который можно упростить. Слева и справа от оператора '||' стоят противоположные по смыслу выражения. Данный код является избыточным, поэтому его можно упростить, сократив количество проверок.
Пример избыточного кода:
if (!Name || (Name && Name[0] == 0))
В выражении "Name && Name[0] == 0" проверка 'Name' является избыточной, так как перед этим проверятся противоположное ему условие '!Name', причём эти выражения разеделены оператором '||'. Следовательно, излишнюю проверку в скобках можно опустить, упростив код:
if (!Name || Name[0] == 0)
Наличие избыточности может свидетельствовать о наличии ошибки. Возможно, что в выражении случайно используется не та переменная. И корректный код на самом деле должен быть, например, таким:
if (!Foo || (Name && Name[0] == 0))
Анализатор выдаёт предупреждения не только на конструкции вида 'x' и '!x', но и на прочие противоположные по смыслу выражения, например:
if (a > 5 || (a <= 5 && b))
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V728. |
Анализатор обнаружил в теле функции метку, на которую не ссылается ни один оператор 'goto'. Возможно, программист ошибся и использует где-то переход не на ту метку.
Синтетический пример некорректного кода:
string SomeFunc(const string &fStr)
{
string str;
while(true)
{
getline(cin,str);
if (str == fStr)
goto retRes;
else if(str == "stop")
goto retRes;
}
retRes:
return str;
badRet:
return "fail";
}
В теле функции есть метка 'badRet' на которую не ссылается ни один оператор 'goto', но при этом присутствует другая метка 'retRes', на которую есть ссылка. Программист ошибся и вместо перехода на метку 'badRet' продублировал переход на метку 'retRes'.
Тогда корректный код мог бы выглядеть так:
string SomeFunc(const string &fStr)
{
string str;
while(true)
{
getline(cin,str);
if (str == fStr)
goto retRes;
else if(str == "stop")
goto badRet;
}
retRes:
return str;
badRet:
return "fail";
}
Другой пример некорректного кода:
int DeprecatedFunc(size_t lhs, size_t rhs, bool cond)
{
if (cond)
return lhs*3+rhs;
else
return lhs*2 + rhs*7;
badLbl:
return -1;
}
В данном случае будет выдано сообщение с низким уровнем опасности, так как метка 'badLbl' осталась после изменения функции, а операторы 'goto', ссылающиеся на неё, были удалены.
Анализатор не будет выдавать предупреждение в случае, если в теле функции есть оператор 'goto', ссылающийся на данную метку, но он закомментирован или этот участок кода не компилируется из-за директивы '#ifdef'.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V729. |
Анализатор обнаружил конструктор, который по всей видимости инициализирует не все члены класса.
Рассмотрим простой синтетический пример:
struct MyPoint
{
int m_x, m_y;
MyPoint() { m_x = 0; }
void Print() { cout << m_x << " " << m_y; }
};
MyPoint Point;
Point.Print();
При создании объекта Point вызовется конструктор, в котором не инициализируется член m_y. Соответственно при вызове функции Print будет использована неинициализированная переменная, и последствия этого непредсказуемы.
Корректный конструктор может выглядеть следующим образом:
MyPoint() { m_x = 0; m_y = 0; }
Мы рассмотрели простой синтетический пример, где сразу всё понятно. Однако в реальном коде всё бывает гораздо сложнее. Поиск неинициализированных членов класса является набором эмпирических алгоритмов. Во-первых, члены классов можно инициализировать разнообразнейшими способами, и анализатор не всегда может понять, инициализирован член класса или нет. Во-вторых, не всегда нужно инициализировать все члены, и сообщения анализатора могут быть ложными, так как он не может угадать задумку программиста.
Поиск неинициализированных членов класса является сложным и неблагодарным занятием. Подробнее этот вопрос рассмотрен в статье: "Поиск неинициализированных членов класса". Поэтому просим в случае ложных срабатываний отнестись к анализатору с пониманием и использовать один из механизмов подавления ложных срабатываний.
Вы можете подавить предупреждение, отметив конструктор комментарием "//-V730". Вы также можете использовать базу данных для разметки ложных предупреждений. В крайнем случае, если ложных срабатываний слишком много, то разумно полностью отключить диагностику V730.
Однако это крайние меры. На практике разумно отключать анализ отдельных членов структур, не требующих инициализации в конструкторе. Рассмотрим искусственный пример:
const size_t MAX_STACK_SIZE = 100;
class Stack
{
size_t m_size;
int m_array[MAX_STACK_SIZE];
public:
Stack() : m_size(0) {}
void Push(int value)
{
if (m_size == MAX_STACK_SIZE)
throw std::exception("overflow");
m_array[m_size++] = value;
}
int Pop()
{
if (m_size == 0)
throw std::exception("underflow");
return m_array[--m_size];
}
};
Этот класс реализует стек. Массив 'm_array' не инициализируется в конструкторе и это корректно, так как изначально стек считается пустым.
Анализатор выдаст предупреждение V730, так как не может понять принцип работы этого класса. Вы можете подсказать анализатору, пометив член 'm_array' комментарием "//-V730_NOINIT". Это укажет анализатору, что массив 'm_array' не требуется обязательно инициализировать.
Теперь, анализируя класс:
class Stack
{
size_t m_size;
int m_array[MAX_STACK_SIZE]; //-V730_NOINIT
public:
Stack() : m_size(0) {}
.....
};
Анализатор не выдаст предупреждение.
Существует способ отключить предупреждения V730 на все поля классов определённого типа.
Рассмотрим пример:
class Field
{
public:
int f;
};
class Test
{
public:
Test() {}
Field field;
};
На этом фрагменте кода будет выдано предупреждение: V730 Not all members of a class are initialized inside the constructor. Consider inspecting: field.
Чтобы исключить все предупреждения поля класса типа 'Field', следует добавить следующий комментарий в код или файл настроек:
//+V730:SUPPRESS_FIELD_TYPE, class:Field
Формат комментария:
//+V730:SUPPRESS_FIELD_TYPE, class:className, namespace:nsName
или
//+V730:SUPPRESS_FIELD_TYPE, class:className.NestedClassName, namespace:nsName
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V730. |
Анализатор обнаружил сравнение переменной типа char с указателем на строку. Такое использование переменной вызвано тем, что были перепутаны одинарные (') и двойные кавычки (").
Рассмотрим пример подобной ошибки:
char ch = 'd';
....
if(ch == "\n")
....
Этим кодом невнимательный программист хотел сравнить переменную 'ch' с символом новой строки, но по ошибке поставил не те кавычки. В результате значение переменной 'ch' сравнивается с адресом, по которому располагается строка "\n". Подобный код компилируется и выполняется в Си, но как правило не имеет смысла. В данном примере корректный код должен содержать одинарные кавычки вместо двойных:
char ch = 'd';
....
if(ch == '\n')
....
Такая же ошибка может быть допущена при инициализации или присваивании переменной, которая в результате будет хранить младший байт адреса присваиваемой строки.
char ch = "d";
В корректном коде должны использоваться одинарные кавычки.
char ch = 'd';
Данная диагностика классифицируется как:
Анализатор обнаружил, что операция унарного минуса применяется к значению типа bool, BOOL, _Bool, и т.п.
Пример ошибки:
bool a;
....
bool b = -a;
Этот код не имеет смысла. Вычисление происходит следующим образом:
Если a == false, тогда 'false' превращается в значение типа int, равное 0. К этому значению применяется оператор '-'. На значение это, естественно, не влияет и в 'b' будет записано 0 (т.е. false).
Если a == true, тогда 'true' превращается в значение типа int, равное 1. К этому значению применяется оператор '-' и получается значение -1. Однако, -1 != 0, а значит при записи -1 в переменную типа bool мы вновь получим 'true'.
Таким образом значение 'false' останется 'false', а 'true' останется 'true'.
В корректном присваивании должен использоваться оператор '!':
bool a;
....
bool b = !a;
Другой пример (BOOL есть не что иное как тип int):
BOOL a;
....
BOOL b = -a;
Унарный минус может изменить численное значение переменной типа BOOL, но не её логическое значение. Всякое ненулевое значение будет обозначать истину, а ноль так и останется ложью.
Корректный вариант:
BOOL a;
....
BOOL b = !a;
Примечание. Некоторые программисты могут умышленно использовать конструкции следующего вида:
int val = Foo();
int s;
s = -(val<0);
Анализатор выдает предупреждение на подобные конструкции. Здесь нет ошибки, но подобный код нельзя рекомендовать к использованию.
В зависимости от значения 'val' переменной "s" хотят присвоить либо 0, либо -1. Применение унарного минуса к логическому выражению только усложняет чтение кода. Более подходящим будет использование тернарного оператора.
s = (val < 0) ? -1 : 0;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V732. |
Анализатор обнаружил потенциальную ошибку в коде, связанную с использованием макросов, раскрывающихся в арифметическое выражение. Обычно ожидается, что подвыражение, переданное как параметр в макрос будет выполняться в конечном выражении первым. Но это может быть не так, что приводит к трудно обнаруживаемым ошибкам.
Рассмотрим пример:
#define RShift(a) a >> 3
....
y = RShift(x & 0xFFF);
Если раскрыть макрос, то мы получим:
y = x & 0xFFF >> 3;
Приоритет операции ">>" выше, чем у "&". Будет вычислено выражение "x & (0xFFF >> 3)", в то время, как программист рассчитывал получить "(x & 0xFFF) >> 3".
Для устранения недостатка требуется взять аргумент 'a' в круглые скобки:
#define RShift(a) (a) >> 3
Однако, стоит сделать ещё одно усовершенствование. Полезно взять всё выражение в макросе ещё в одни скобки. Это является хорошим тоном и может предотвратить некоторые другие ошибки. Улучшенный вариант:
#define RShift(a) ((a) >> 3)
Примечание. Родственной по смыслу диагностикой является V1003. Диагностика V1003 работает менее точно и даёт больше ложных срабатываний, так как анализирует объявление макроса, а не его использование. С другой стороны, не смотря на свои недостатки диагностика V1003 может помочь выявить ошибки, которые диагностика V733 бессильная обнаружить.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V733. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что в выражении ищется более длинная подстрока и более короткая. При этом более короткая строка, является частью более длинной. Получается, что одно из сравнений избыточно или допущена какая-то ошибка.
Рассмотрим пример:
if (strstr(a, "abc") != NULL || strstr(a, "abcd") != NULL)
В случае если подстрока "abc" будет найдена, то дальнейшая проверка не будет выполняться. Если подстрока "abc" не будет найдена, то и поиск более длинной подстроки "abcd" не имеет смысла.
Для исправления ошибки необходимо проверить правильность подстрок или убрать из кода лишние проверки. Пример корректного варианта:
if (strstr(a, "abc") != NULL)
Другой пример:
if (strstr(a, "abc") != NULL)
Foo1();
else if (strstr(a, "abcd") != NULL)
Foo2();
В данном случае функция Foo2() никогда не будет вызвана. Устранить ошибку можно путем замены порядка проверки. То есть сначала следует искать более длинную подстроку, а потом более короткую:
if (strstr(a, "abcd") != NULL)
Foo2();
else if (strstr(a, "abc") != NULL)
Foo1();
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V734. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что в коде задан строковый литерал, в котором содержится разметка HTML с ошибками. Был открыт тег для элемента, которому требуется завершающий тег. Но следующим в строке обнаружен закрывающий тег, который не соответствует открывающему тегу.
Рассмотрим пример:
string html = "<B><I>This is a text, in bold italics.</B>";
В данном случае открывающему тегу "<I>" должен соответствовать закрывающий тег "</I>", но при дальнейшем анализе HTML мы обнаружим закрывающий тег "</B>", что является ошибкой. В таком виде часть HTML кода является невалидной.
Для исправления ошибки необходимо проверить корректность последовательности открывающих и закрывающих тегов и устранить найденные ошибки.
Пример корректного варианта:
string html = "<B><I>This is a text, in bold italics.</I></B>";
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V735. |
Поведение не определено, если выполняется сравнение или арифметические операции над указателями, которые ссылаются на элементы, относящиеся к различным массивам.
Рассмотрим пример:
int a[10], b[20];
fill(a, b);
if (&a[1] > &b[2])
Этот код содержит какую-то ошибку. Например, этот код мог получиться в результате неудачной автозамены фрагментов строк. Предположим, что здесь операторы '&' являются лишними. Тогда корректный код должен выглядеть так:
if (a[1] > b[2])
Данная диагностика классифицируется как:
|
Анализатор заподозрил, что в списке инициализации массива случайно пропущена одна из запятых.
Рассмотрим пример:
int a[3][6] = { { -1, -2, -3
-4, -5, -6 },
{ ..... },
{ ..... } };
После "-3" случайно пропущена запятая. Далее следует значение "-4". Получается выражение "-3-4". В результате код успешно и без предупреждений компилируется, но массив инициализирован неправильно. Значения "-5", и "-6" будут записаны не на свои позиции, а в последний элемент будет записан 0.
В итоге, на самом деле массив инициализируется так:
int a[3][6] = { { -1, -2, -7,
-5, -6, 0 },
..............
Правильный вариант (добавлена пропущенная запятая):
int a[3][6] = { { -1, -2, -3,
-4, -5, -6 },
..............
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V737. |
Анализатор обнаружил, что используется временный анонимный объект, получившийся в результате работы постфиксного оператора ++ или --. Иногда это имеет смысл. Но когда речь заходит о изменении этого временного объекта или взятии его адреса, то это ошибка.
Рассмотрим первый пример:
vector<float>::iterator it = foo();
it++ = x;
Создаётся временная копия итератора. Итератор инкрементируется. После чего к временному объекту применяется оператор присваивания. Этот код не имеет смысла. Автор кода хотел явно сделать что-то другое. Например, он хотел в начале выполнить присваивание, а уже затем воспользоваться инкрементом.
В этом случае корректный код должен выглядеть так:
it = x;
it++;
Однако постфиксная операция для итераторов не эффективна. И будет лучше написать так:
it = x;
++it;
Есть ещё один вариант:
it = x + 1;
Рассмотрим второй пример:
const vector<int>::iterator *itp = &it++;
Указатель 'itp' использовать нельзя. Он указывает на временный неименованный объект, который уже разрушен. Корректный вариант кода:
++it;
const vector<int>::iterator *itp = ⁢
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что константа EOF сравнивается с переменной типа 'char' или 'unsigned char'. Это свидетельствует о том, что некоторые символы будут обрабатываться программой неверно.
Рассмотрим, как объявлен EOF:
#define EOF (-1)
Как видите, EOF есть ни что иное как '-1' типа 'int'. Посмотрим, к каким последствиям это может приводить. Первый пример:
unsigned char c;
while ((c = getchar()) != EOF)
{ .... }
Беззнаковая переменная 'c' никогда не будет равна отрицательному значению '-1'. Поэтому выражение ((c = getchar) != EOF) всегда истинно, и возникает вечный цикл. Такая ошибка будет сразу замечена и исправлена. Поэтому не будем продолжать обсуждать тип 'unsigned char'.
Рассмотрим более интересный случай:
signed char c;
while ((c = getchar()) != EOF)
{ .... }
Функция getchar() возвращает значения типа 'int'. А именно - она может вернуть число от 0 до 255 или -1 (EOF). Прочитанные значение помещаются в переменную типа 'char'. Из-за этого символ со значением 0xFF (255) превращается в -1 и интерпретируется точно также как конец файла (EOF).
Пользователи, использующие Extended ASCII Codes, иногда сталкиваются с ошибкой, когда один из символов их алфавита некорректно обрабатывается программами.
Например, последняя буква русского алфавита в кодировке Windows-1251 как раз имеет код 0xFF и воспринимается некоторыми программами как конец файла.
Исправленный вариант кода:
int c;
while ((c = getchar()) != EOF)
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V739. |
Анализатор обнаружил, что будет сгенерировано исключение, имеющее тип 'int', в то время как программист планировал сгенерировать исключение типа "указатель".
Рассмотрим пример кода:
if (unknown_error)
throw NULL;
В случае возникновения неизвестной ошибки программист решил "бросить" нулевой указатель. Однако он не учёл, что NULL это не что иное, как обыкновенный 0. Вот как определён макрос NULL в C++ программах:
#define NULL 0
Ноль '0' имеет тип 'int'. Поэтому будет сгенерировано исключения типа 'int'.
Оставим в стороне вопрос, что использовать указатели для генерации исключений плохо и опасно. Предположим, что в этом есть настоящая необходимость. Тогда исправленный вариант кода может выглядеть так:
if (unknown_error)
throw nullptr;
Почему не стоит использовать указатели при работе с исключениями хорошо описано в книге:
Стефан К. Дьюхэрст. Скользкие места С++. Как избежать проблем при проектировании и компиляции ваших программ. – М.: ДМК Пресс. – 264 с.: ил. ISBN 5-94074-083-9.
Анализатор обнаружил, что после ключевого слова throw следуют круглые скобки, в которых через запятую перечислены различные значения. Высока вероятность, что забыли указать тип генерируемого исключения.
Рассмотрим пример:
throw ("foo", 123);
Хотя код выглядит странно, он успешно компилируется. В данном случае, результатом выполнения оператора запятая ',' является значение 123. В результате будет сгенерировано исключение типа 'int'.
Другими словами, приведённый код эквивалентен следующему:
throw 123;
Правильный вариант кода:
throw MyException("foo", 123);
Данная диагностика классифицируется как:
Анализатор обнаружил ошибку, связанную с тем, что в функцию, работающую со строкой, передается адрес переменной типа 'char', вместо указателя на буфер символов. Это может привести к ошибкам времени исполнения программы, так как функции, работающие с указателями на буфер символов, ожидают несколько символов и, иногда, терминальный ноль в конце такого буфера.
Рассмотрим пример:
const char a = 'f';
size_t len = strlen(&a);
В данном случае в функцию, которая должна вернуть длину строки передается указатель на переменную 'а'. Функция будет считать строкой всю область памяти, следующую за адресом этой переменной до встречи символа, который будет соответствовать терминальному нулю. Результат выполнения этой функции не определён. Функция может вернуть произвольное значение или возникнет исключение из-за ошибки доступа к памяти.
Описанный тип ошибки встречается крайне редко и обычно связан с неаккуратным редактированием кода или с массовой заменой каких-то подстрок.
Для исправления ошибки необходимо использовать набор данных соответствующий буферу символов или применять функции, обрабатывающие одиночные символы.
Пример корректного кода:
const char a[] = "f";
size_t len = strlen(a);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V742. |
Анализатор обнаружил ошибку, связанную с тем, что в функции memcpy перекрываются области памяти источника и приемника. Использование функции memcpy в таком случае приведет к неопределенному поведению программы во время исполнения [1, 2].
Рассмотрим пример:
void func(int *x){
memcpy(x, x+2, 10 * sizeof(int));
}
В данном случае указатель на источник данных (x+2) смещен относительно приемника данных на 8 байт (sizeof(int) * 2). Попытка копирования 40 байт в приемник из источника приведет к частичному перекрытию области памяти источника.
Для исправления ошибки необходимо использовать специальную функцию для таких операций – memmove(...) или скорректировать заданные смещения источника и приемника, чтобы не происходило перекрытия областей памяти.
Пример корректного кода:
void func(int *x){
memmove(x, x+2, 10 * sizeof(int));
}
Дополнительные ссылки:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V743. |
Анализатор обнаружил ошибку, связанную с тем, что при создании объекта ему забыли дать имя. В этом случае будет сконструирован временный анонимный объект и тут же разрушен. Иногда в этом нет ничего плохого, и именно этого и хотел добиться программист. Но если речь идёт о таких классах как 'CWaitCursor' или 'CMultiLock', то это явная ошибка.
Рассмотрим пример:
void func(){
CMutex mtx;
CSingleLock(&mtx, TRUE);
foo();
}
В данном примере будет создан временный анонимный объект типа 'CSingleLock', которой будет сразу же разрушен, еще до вызова функции foo(). В примере программист хотел обеспечить синхронизацию при исполнении функции foo(), но на самом деле функция будет вызвана без синхронизации, что может привести к серьезным ошибкам.
Для исправления ошибки необходимо дать создаваемому объекту имя.
Пример корректного кода:
void func(){
CMutex mtx;
CSingleLock lock(&mtx, TRUE);
foo();
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил, что со строкой типа "wchar_t *" начинают работать как со строкой типа BSTR. Это очень подозрительно, и код, скорее всего, содержит ошибку. Чтобы лучше понять, в чем заключается опасность, сначала вспомним, что такое BSTR.
На самом деле, мы процитируем статью с сайта MSDN. Читать MSDN документацию не любят, но это нужно сделать. Важно понять, в чем заключается беда, так как предупреждение V745 часто сигнализирует о серьезных ошибках.
typedef wchar_t OLECHAR;
typedef OLECHAR * BSTR;
BSTR (basic string или binary string) – это строковый тип данных, который используется в COM, Automation и Interop функциях. Тип BSTR следует использовать во всех интерфейсах. Представление BSTR:
Тип BSTR является указателем, который указывает на первый символ строки, а не на префикс длины.
Память для BSTR-строк выделяется с помощью функций выделения памяти COM, поэтому они могут возвращаться методами без необходимости контроля над выделением памяти.
Представленный ниже код является неправильным:
BSTR MyBstr = L"I am a happy BSTR";
Данный пример собирается (компилируется и линкуется) успешно, но не будет работать должным образом, поскольку у строки отсутствует префикс длины. Если проверить расположение в памяти данной переменной с помощью отладчика, он покажет отсутствие префикса длины размером 4 байта перед началом строки данных.
Правильный вариант кода должен выглядеть так:
BSTR MyBstr = SysAllocString(L"I am a happy BSTR");
Теперь отладчик покажет наличие префикса длины, который равен значению 34. Оно соответствует 17 символам, которые приводится к wide-character строке с помощью строкового модификатора "L". Отладчик также покажет двухбайтовый символ-ограничитель (0x0000) в конце строки.
Если передать простую Unicode-строку в качестве аргумента функции COM, которая ожидает BSTR-строку, произойдет сбой в работе этой функции.
Надеюсь, процитированного фрагмента MSDN достаточно, чтобы понять, почему следует разделять BSTR и простые строки типа "wchar_t *".
Также надо понимать, что анализатор не может точно предсказать, есть в коде настоящая ошибка или нет. Если неправильная BSTR строка передается куда-то вовне, то произойдёт сбой. Если же BSTR строка превращается обратно в "wchar_t *", то всё хорошо. Имеется в виду код вида:
wchar_t *wstr = Foo();
BSTR tmp = wstr;
wchar_t *wstr2 = tmp;
Да, здесь нет настоящей ошибки. Но это "код с запахом". И его следует поправить. Так он вызовет меньше недоумения у программиста, сопровождающего код, и анализатор не будет выдавать предупреждение. Следует использовать правильные типы данных:
wchar_t *wstr = Foo();
wchar_t *tmp = wstr;
wchar_t *wstr2 = tmp;
Рекомендуем также ознакомиться со ссылками, приведёнными в конце статьи. Они помогут разобраться с BSTR-строками и тем, как их можно конвертировать в строки других типов.
Рассмотрим ещё один пример:
wchar_t *wcharStr = L"123";
wchar_t *foo = L"12345";
int n = SysReAllocString(&wcharStr, foo);
Описание функции SysReAllocString:
INT SysReAllocString(BSTR *pbstr, const OLECHAR *psz);
Выделяет новую BSTR и копирует в нее заданную строку, затем освобождает BSTR, на которую указывает pbstr, и помещает по этому адресу указатель на новую BSTR.
Как видите, функция ожидает в качестве первого аргумента указатель на переменную, содержащую адрес строки в формате BSTR. Но вместо этого ей передают указатель на обыкновенную строку. Так как тип "wchar_t **" с точки зрения компилятора то же самое, что и "BSTR *", то этот код успешно компилируется. Но на практике он не имеет смысла и приведёт к ошибке на этапе исполнения.
Правильный вариант кода:
BSTR wcharStr = SysAllocString(L"123");
wchar_t *foo = L"12345";
int n = SysReAllocString(&wcharStr, foo);
Дополнительно рассмотрим ситуацию, когда используется ключевое слово 'auto'. Анализатор выдаёт предупреждение на следующий безобидный код:
auto bstr = ::SysAllocStringByteLen(foo, 3);
ATL::CComBSTR value;
value.Attach(bstr); // Warning: V745
Да, это ложное срабатывание, но формально анализатор прав, выдавая предупреждение. Переменная 'bstr' имеет тип 'wchar_t *'. Компилятор языка C++ при выведении типа для auto-переменной не учитывает, что функция возвращает значение типа 'BSTR'. При выведении 'auto', тип 'BSTR' - это просто синоним 'whar_t *'. Получается, что написанный код эквивалентен:
wchar_t *bstr = ::SysAllocStringByteLen(foo, 3);
ATL::CComBSTR value;
value.Attach(bstr);
Поэтому анализатор PVS-Studio и выдаёт предупреждение, так как нехорошо хранить указатель на 'BSTR' строку в обыкновенном 'wchar_t *' указателе. Чтобы устранить предупреждение, следует отказаться в данном месте от 'auto' и написать тип явно:
BSTR *bstr = ::SysAllocStringByteLen(foo, 3);
ATL::CComBSTR value;
value.Attach(bstr);
Это интересный случай, когда оператор 'auto' не помогает, а наоборот, теряет информацию о типе и ухудшает ситуацию.
Другой вариант устранить предупреждение - использовать один из механизмов подавления ложных срабатываний, описанных в документации.
Дополнительные ссылки:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V745. |
Анализатор обнаружил потенциальную ошибку, связанную с перехватом исключения по значению. Намного лучше и безопасней перехватывать исключение по ссылке.
Вообще, перехват исключения по значению порождает две разновидности ошибок. Рассмотрим их поочередно.
Проблема N1. Срезка (slicing).
class Exception_Base {
....
virtual void Print() { .... }
};
class Exception_Ex : public Exception_Base { .... };
try
{
if (error) throw Exception_Ex(1, 2, 3);
}
catch (Exception_Base e)
{
e.Print();
throw e;
}
Объявлено 2 класса: исключение базового типа и расширенное исключение, которое наследуется от первого.
Генерируется расширенное исключение. Это исключение планируется перехватить, распечатать о нём информацию и пробросить исключение дальше.
Исключение перехвачено по значению. Это значит, что с помощью конструктора копирования будет сконструирован новый объект 'e' типа Exception_Base. Это порождает сразу 2 ошибки.
Во-первых, часть информации об исключении потеряна. Всё что хранилось Exception_Ex нам более недоступно. Виртуальная функция Print() позволит вывести базовую информацию о проблеме.
Во-вторых, дальше будет проброшено уже новое исключение типа Exception_Base. Таким образом мы передали дальше урезанную информацию о возникшей проблеме.
Правильно будет использовать следующий код:
catch (Exception_Base &e)
{
e.Print();
throw;
}
Теперь функция Print() распечатает всю нужную информацию. Оператор "throw" будет пробрасывать далее уже существующее исключение, и информация не будет потеряна (срезана).
Проблема N2. Изменение временного объекта.
catch (std::string s)
{
s += "Additional info";
throw;
}
Программист хочет перехватить исключение, добавить какую-то дополнительную информацию и пробросить это исключение дальше. Ошибка в том, что изменяется переменная 's', а оператор "throw;" пробрасывает далее исходное исключение. Таким образом мы не изменили информацию о исключении.
Правильный вариант:
catch (std::string &s)
{
s += "Additional info";
throw;
}
Подробнее про преимущества перехвата исключения по ссылке можно прочитать здесь:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V746. |
Анализатор обнаружил подозрительное выражение в скобках. В круглых скобках через запятую перечислены различные переменные и значение. При этом не похоже, что оператор запятая ',' использовался для сокращения кода.
Рассмотрим пример:
if (memcmp(a, b, c) < 0 && (x, y, z) < 0)
При написании текста программы случайно забыли написать имя функции 'memcmp'. Тем не менее код успешно компилируется, хотя работает не так, как задумывалось. В правой части результатом работы двух операторов запятая (comma operator) является переменная 'z'. Именно она сравнивается с нулём. В итоге, приведённый выше код, эквивалентен следующему:
if (memcmp(a, b, c) < 0 && z < 0)
Правильный вариант кода:
if (memcmp(a, b, c) < 0 && memcmp(x, y, z) < 0)
Примечание. Иногда оператор ',' используется для сокращения кода. Поэтому анализатор далеко не всегда ругается на запятые внутри скобок. Например, он считает корректным следующий код:
if (((std::cin >> A), A) && .....)
Мы не рекомендуем писать такие сложные выражения, так как вашим коллегам потом будет тяжело читать такой код. Но и ошибки здесь явно нет. Разработчику просто захотелось совместить в одном выражении и получение значения, и его проверку.
Ещё один аналогичный пример:
if (a)
return (b = foo(), fooo(b), b);
Данная диагностика классифицируется как:
|
Анализатор обнаружил ошибку, связанную с тем, что память для функции getline() была выделена без использования функции malloc()/realloc(). Функция getline() написана таким образом, что если в процессе работы не хватает выделенной ранее памяти, getline() вызовет realloc() для её увеличения (ISO/IEC TR 24731-2). Поэтому выделять память необходимо только при помощи malloc() или realloc().
Рассмотрим пример:
char* buf = new char[count];
getline(&buf, &count, stream);
В данном примере память для функции getline() выделена с помощью оператора new. Если getline() не хватит выделенной памяти, она вызовет realloc(). Результат такого вызова непредсказуем.
Для исправления ошибки необходимо переписать код таким образом, чтобы память для функции getline() выделялась только с использованием malloc() или realloc().
Корректный код:
char* buf = (char*)malloc(count * sizeof(char));
getline(&buf, &count, stream);
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил ошибку, связанную с повторным вызовом деструктора. Если объект создается на стеке, то при выходе за пределы области видимости будет вызван деструктор. Анализатор обнаружил вызов деструктора для такого объекта напрямую.
Рассмотрим пример:
void func(){
X a;
a.~X();
foo();
}
В данном примере для объекта 'a' напрямую вызывается деструктор. Но когда функция 'func' закончит свою работу, деструктор для объекта 'a' будет вызван еще раз.
Для исправления ошибки необходимо удалить ошибочный код или скорректировать код в соответствии с используемой моделью управления памятью.
Пример корректного кода:
void func(){
X a;
foo();
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V749. |
Анализатор обнаружил, что над указателем на строку в формате BSTR выполняются недопустимые операции. Указатель типа BSTR всегда должен ссылаться на первый символ строки. Если мы сдвинем указатель хотя бы на один символ, то получим некорректную BSTR строку.
Имеется ввиду, что вот такой код крайне опасен:
BSTR str = foo();
str++;
Теперь 'str' нельзя использовать как BSTR строку. Если нужно пропустить один символ, следует написать следующий код:
BSTR str = foo();
BSTR newStr = SysAllocString(str + 1);
Если вам не нужна BSTR строка, то можно написать следующий код:
BSTR str = foo();
const wchar_t *newStr = str;
newStr++;
Или так:
BSTR str = foo();
const wchar_t *newStr = str + 1;
Чтобы понять, почему недопустимо менять значение указателя типа BSTR, процитируем статью с сайта MSDN.
typedef wchar_t OLECHAR;
typedef OLECHAR * BSTR;
BSTR (basic string или binary string) – это строковый тип данных, который используется в COM, Automation и Interop функциях. Тип BSTR следует использовать во всех интерфейсах. Представление BSTR:
Тип BSTR является указателем, который указывает на первый символ строки, а не на префикс длины.
Память для BSTR-строк выделяется с помощью функций выделения памяти COM, поэтому они могут возвращаться методами без необходимости контроля над выделением памяти.
Представленный ниже код является неправильным:
BSTR MyBstr = L"I am a happy BSTR";
Данный пример собирается (компилируется и линкуется) успешно, но не будет работать должным образом, поскольку у строки отсутствует префикс длины. Если проверить расположение в памяти данной переменной с помощью отладчика, он покажет отсутствие префикса длины размером 4 байта перед началом строки данных.
Правильный вариант кода должен выглядеть так:
BSTR MyBstr = SysAllocString(L"I am a happy BSTR");
Теперь отладчик покажет наличие префикса длины, который равен значению 34. Оно соответствует 17 символам, которые приводится к wide-character строке с помощью строкового модификатора "L". Отладчик также покажет двухбайтовый символ-ограничитель (0x0000) в конце строки.
Если передать простую Unicode-строку в качестве аргумента функции COM, которая ожидает BSTR-строку, произойдет сбой в работе этой функции.
Надеемся, что процитированного фрагмента MSDN достаточно, чтобы понять, почему нельзя просто так взять и изменить указатель типа BSTR.
Когда мы пишем код:
BSTR str = foo();
str += 3;
Мы портим BSTR строку. Указатель ссылается не на начало строки, а куда-то в середину. Соответственно, если мы попытаемся взять длину строки по отрицательному смещению, то мы прочитаем случайное значение. Вернее, предыдущие символы будут интерпретированы как длина строки.
Дополнительные ссылки:
Анализатор обнаружил подозрительную функцию, один из параметров которой ни разу не используется. При этом другой его параметр используется несколько раз, что, возможно, свидетельствует о наличии ошибки.
Рассмотрим пример:
static bool CardHasLock(int width, int height)
{
const double xScale = 0.051;
const double yScale = 0.0278;
int lockWidth = (int)floor(width * xScale);
int lockHeight = (int)floor(width * yScale);
....
}
Из кода видно, что параметр 'height' ни разу не используется в теле функции, при этом параметр 'width' используется дважды, в том числе при инициализации переменной 'lockHeight'. Скорее всего, здесь допущена ошибка, и код инициализации переменной 'lockHeight' должен был выглядеть следующим образом:
int lockHeight = (int)floor(height * yScale);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V751. |
Анализатор обнаружил, что происходит попытка создать объект с помощью placement new, но размера выделенной для этого памяти не хватит для размещения создаваемого объекта. Будет использована память за пределами выделенного блока, что может привести к аварийному завершению программы или ее неправильному поведению.
Пример некорректного размещения объекта:
struct T { float x, y, z, q; };
char buf[12];
T *p = new (buf) T;
В этом примере объект, размер которого составляет 16 байт, пытаются разместить в буфере 'buf', размер которого 12 байта. При использовании этого объекта произойдёт изменение памяти, лежащей за приделами буфера. Результат непредсказуем.
Для исправления ошибки необходимо скорректировать размер буфера или убедиться, что правильно задано смещение относительно начала буфера.
Вариант правильного кода:
struct T { float x, y, z, q; };
char buf[sizeof(T)];
T *p = new (buf) T;
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти), Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что в результате выполнения побитового оператора "И" значение переменной будет установлено в ноль, что выглядит подозрительно, так как нулевое значение можно получить простым присвоением.
Если же выполняются последовательные вычисления, то возможно данная операция выполняется некорректно, например, из-за опечатки используется не та переменная, что задумывалось или справа используется неправильная константа.
Возможны несколько вариантов возникновения данного предупреждения.
Первый пример - последовательное применение оператора к переменной с неизвестным значением с такими константами, с которыми результирующее значение будет равно нулю:
void foo(int A)
{
A &= 0xf0;
....
A &= 1;
// Теперь 'A' всегда равно 0.
}
Результатом выполнения двух операций будет нулевое значение независимо от изначального значения переменной 'A'. Возможно этот код ошибочен, и программисту необходимо проверить правильность используемых констант.
Второй случай - это применение оператора к переменной, значение которой заведомо известно:
void foo()
{
int C;
....
C = 1;
....
C &= 2;
// C == 0
}
В данном случае при применении оператора к переменной, значение которой известно, также будет получено нулевое значение. Как и в предыдущем случае программисту необходимо проверить правильность используемой константы.
Диагностика может сработать и в следующем, довольно часто использующемся, варианте применения оператора:
void foo()
{
int flags;
....
flags = 1;
....
flags &= ~flags;
....
}
Этот прием программисты иногда используют для сброса набора флагов в нулевое значение. На наш взгляд такой способ не оправдан и может только запутать ваших коллег. Лучше использовать простое присвоение:
void foo()
{
int flags;
....
flags = 1;
....
flags = 0;
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что фактическим аргументом функции является вызов этой-же функции.
Пример некорректного кода:
char lower_ch = tolower(tolower(ch));
В этом примере второй вызов функции избыточен. Возможно в коде допущена опечатка и подразумевался вызов другой функции. Если ошибки нет, то следует убрать лишний вызов, так как подобное выражение выглядит подозрительно:
char lower_ch = tolower(ch);
Другой пример:
if (islower(islower(ch)))
do_something();
Функция 'islower' возвращает значение типа int и может быть использовано в качестве аргумента к самой себе. Данное выражение содержит ошибку и не имеет практического смысла.
Анализатор обнаружил, что в буфер копируются данные из небезопасного источника.
Примерами таких источников являются:
Пример небезопасной работы с аргументами командной строки:
int main(int argc, char *argv[])
{
....
const size_t buf_size = 1024;
char *tmp = (char *) malloc(buf_size);
....
strcpy(tmp, argv[0]);
....
}
В случае, когда размер копируемых данных превысит размер буфера, произойдёт его переполнение. Для того чтобы избежать этого, лучше вычислить требуемый объём памяти заранее:
int main(int argc, char *argv[])
{
....
char buffer[1024];
errno_t err = strncpy_s(buffer, sizeof(buffer), argv[0], 1024);
....
}
Также можно выделять память по мере необходимости, используя функцию 'realloc'. В C++ для работы со строками можно использовать классы, такие как 'std::string'.
До стандарта С++20 была доступна возможность использовать C-строку в качестве буфера-приёмника для стандартных потоков ввода ('std::cin', 'std::ifstream'):
void BadRead(char *receiver)
{
std::cin >> receiver;
}
К счастью, в С++ 20 такую возможность убрали, и теперь потоки ввода стандартной библиотеки можно использовать только с массивами известного размера. При этом происходит неявное ограничение максимально считываемого количества символов.
void Exception2Cpp20()
{
char *buffer1 = new char[10];
std::cin >> buffer1; // Won't compile since C++20
char buffer2[10];
std::cin >> buffer2; // no overflow
// max 9 chars will be read
}
Более подробно это изменение (с примерами и применением) освещено в предложении P0487R1 к стандарту С++ 20.
Злоумышленники могут манипулировать значением, возвращаемым некоторыми функциями, поэтому следует быть предельно аккуратными при работе с ними:
void InsecureDataProcessing()
{
char oldLocale[50];
strcpy(oldLocale, setlocale(LC_ALL, nullptr));
....
}
В данном примере создаётся буфер фиксированного размера, в который происходит чтение из переменной окружения 'LC_ALL'. Если у злоумышленника имеется возможность повлиять на неё, то чтение может привести к переполнению буфера.
Предупреждение не выдаётся в случаях, если источник данных неизвестен:
void Exception1(int argc, char *argv[])
{
char *src = GetData();
char *tmp = (char *)malloc(1024);
strcpy(tmp, src);
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с тем, что при написании двух и более вложенных циклов 'for', из-за опечатки не используется счётчик одного из циклов.
Рассмотрим синтетический пример некорректного кода:
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += matrix[i][i];
В коде планировали обойти все элементы матрицы и найти их сумму, но случайно написали переменную 'i' вместо 'j' при обращении к матрице.
Корректный вариант кода:
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += matrix[i][j];
В отличии от диагностических правил V533, и V534, здесь анализатор ищет ошибки с использованием индекса только в теле циклов.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V756. |
Анализатор обнаружил потенциальную ошибку, которая может привести к доступу по нулевому адресу.
Анализатор заметил в коде следующую ситуацию. Сначала указатель на базовый класс приводится к указателю на производный класс с помощью 'dynamic_cast'. А затем этот же указатель проверяется на значение 'nullptr', хотя в этом случае, скорее всего, предполагалось проверить на 'nullptr' указатель, полученный в результате приведения.
Рассмотрим следующий пример. Здесь возможна ситуация, когда указатель 'baseObj' не будет ссылаться на экземпляр класса 'Derived'. В этом случае при вызове функции произойдёт разыменование нулевого указателя. Анализатор выдаст предупреждение на этот код, указав две строки. Первая строка - это то место, где указатель проверяется на 'nullptr'. Вторая строка - это то место, где указатель на базовый класс приводится к указателю на производный класс.
Base *baseObj;
....
Derived *derivedObj = dynamic_cast<Derived *>(baseObj);
if (baseObj != nullptr)
{
derivedObj->Func();
}
Скорее всего в этом примере перед использованием предполагалось проверить на 'nullptr' указатель, полученный в результате приведения. Исправленный вариант кода:
Base *baseObj;
....
Derived *derivedObj = dynamic_cast<Derived *>(baseObj)
if (derivedObj != null)
{
derivedObj->Func();
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки разыменования нулевого указателя. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V757. |
Анализатор обнаружил ссылку, которая может стать недействительной. Она ссылается на объект, находящийся под управлением умного указателя или контейнера, который возвращают из функции по значению. После разрушения временного объекта, при возврате из функции, будет разрушен и контролируемый им объект. Ссылка на него станет недействительной. Попытка использования такой ссылки приведет к неопределенному поведению.
Рассмотрим пример с умным указателем 'unique_ptr':
std::unique_ptr<A> Foo()
{
std::unique_ptr<A> pa(new A());
return pa;
}
void Foo2()
{
const A &ra = *Foo();
ra.foo();
}
Ссылка указывает на объект, который находится под управлением умного указателя 'unique_ptr'. После выхода из функции временный объект 'unique_ptr' разрушается, и ссылка становится недействительной.
Чтобы избежать подобных проблем, необходимо отказаться от использования ссылки и переписать функцию 'Foo2()' следующим образом:
void Foo2()
{
A a(*Foo());
a.foo();
}
Теперь мы используем не ссылку, а создаём новый объект типа 'A'. Причем, начиная с C++11, для инициализации переменной 'a' может использоваться конструктор перемещения и никаких потерь в производительности не произойдет.
Также можно использовать следующий вариант:
void Foo2()
{
std::unique_ptr<A> pa = Foo();
pa->foo();
}
В данной ситуации произойдет передача владения объектом типа 'A'.
Рассмотрим пример с контейнером 'std::vector':
std::vector<A> Foo();
void Foo2()
{
const A &ra = Foo()[42];
ra.foo();
}
Проблема точно такая же, что и с 'unique_ptr': временный объект 'vector' разрушается, и ссылка на его элемент становится недействительной.
То же самое относится и к методам, которые возвращают ссылки на элементы внутри контейнера: front(), back() и другие:
void Foo2()
{
const A &ra = Foo().front();
ra.foo();
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил множественные обработчики исключений с нарушенным порядком следования. Обработчик для базового класса исключений размещен перед обработчиком для производного класса. В таком случае все исключения, предназначенные для перехвата обработчиком производного класса, будут перехвачены обработчиком для базового класса.
Рассмотрим пример:
class Exception { .... };
class DerivedException : public Exception { ... };
void foo()
{
throw DerivedException;
}
void bar()
{
try
{
foo();
}
catch (Exception&)
{
// Все исключения, имеющие тип DerivedException попадут сюда
}
catch (DerivedException&)
{
// Код в этом обработчике никогда не выполняется
}
}
Так как класс 'Exception' является базовым для класса 'DerivedException', исключения, которые генерирует функция 'foo()', перехватываются первым обработчиком.
Чтобы исправить ошибку, нужно поменять обработчики исключений местами:
void bar()
{
try
{
foo();
}
catch (DerivedException&)
{
// Перехватывает исключения типа DerivedException
}
catch (Exception&)
{
// Перехватывает исключения типа Exception
}
}
Теперь каждый обработчик перехватывает исключения, предназначенные только для него.
Данная диагностика классифицируется как:
|
Анализатор обнаружил код, который возможно содержит опечатку. Высока вероятность, что подобный код был создан с использованием подхода Copy-Paste. Сообщение V760 выдаётся на два одинаковых блока текста, идущих один за другим. Данная диагностика в основном опирается на эвристический метод и поэтому может выдавать ложные срабатывания.
Рассмотрим пример:
void Example(int *a, int *b, size_t n)
{
....
for (size_t i = 0; i != n; i++)
a[i] = 0;
for (size_t i = 0; i != n; i++)
a[i] = 0;
....
}
Код писался с помощью Copy-Paste. Во втором блоке забыли изменить имя массива. На самом деле, код должен быть таким:
void Example(int *a, int *b, size_t n)
{
....
for (size_t i = 0; i != n; i++)
a[i] = 0;
for (size_t i = 0; i != n; i++)
b[i] = 0;
....
}
Анализатор не выдаёт сообщение, если найдено больше двух блоков текста. Пример:
void Foo();
void Example()
{
....
Foo();
Foo();
Foo();
Foo();
....
}
Иногда может быть непонятно, что не нравится анализатору. Рассмотрим такой пример:
switch(t) {
case '!': InvokeMethod(&obj_Sylia, "!", 1); break;
case '~': InvokeMethod(&obj_Sylia, "~", 1); break;
case '+': InvokeMethod(&obj_Sylia, "+", 1); break;
case '-': InvokeMethod(&obj_Sylia, "-", 1); break;
break;
default:
SCRIPT_ERROR(PARSE_ERROR);
}
Нужно присмотреться. В данном случае мы имеем дело с очень коротким повторяющимся блоком. Этот блок - оператор 'break'. Один из операторов здесь лишний. В данном примере это не приводит к настоящей ошибке, но лишний 'break' стоит удалить:
switch(t) {
case '!': InvokeMethod(&obj_Sylia, "!", 1); break;
case '~': InvokeMethod(&obj_Sylia, "~", 1); break;
case '+': InvokeMethod(&obj_Sylia, "+", 1); break;
case '-': InvokeMethod(&obj_Sylia, "-", 1); break;
default:
SCRIPT_ERROR(PARSE_ERROR);
}
Примечание
Дублирование кода само по себе не является ошибкой. Однако, даже если ошибки нет, предупреждение V760 может служить подсказкой, что одинаковые блоки кода стоит вынести в функцию. См. также диагностику V761.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V760. |
Анализатор обнаружил код, который можно улучшить рефакторингом. Данная диагностика ищет одинаковые блоки текста, которые повторяются три или более раз. Вряд ли такой код содержит ошибку, но лучше постараться вынести одинаковый код в отдельную функцию.
В случае, когда код использует много локальных переменных, облегчить ситуацию могут лямбда-функции, с помощью который можно захватить данные по ссылке.
Диагностика может выдавать большое количество срабатываний на коде, где применяется большое количество ручных оптимизаций (например, ручная развёртка циклов). Если вы считаете, что диагностика V761 не актуальна в вашем проекте, то вы можете её отключить.
Рассмотрим синтетический пример, на который анализатор выдаёт предупреждение:
void process(char *&buf);
void func(size_t n, char *arr)
{
size_t i;
i = n;
while (i--)
arr[i] = 1;
for (i = 0; i != 10; i++)
arr[i] = 'a';
process(arr);
i = n;
while (i--)
arr[i] = 1;
for (i = 0; i != 10; i++)
arr[i] = 'a';
process(arr);
i = n;
while (i--)
arr[i] = 1;
for (i = 0; i != 10; i++)
arr[i] = 'a';
process(arr);
i = n;
while (i--)
arr[i] = 1;
for (i = 0; i != 10; i++)
arr[i] = 'a';
process(arr);
}
Будет полезно вынести общий код в отдельную функцию:
void process(char*& buf);
void func_impl(size_t i, size_t *&arr)
{
while (i--)
arr[i] = 1;
for (i = 0; i != 10; i++)
arr[i] = 'a';
process(arr);
}
void func(size_t n, char *arr)
{
for (size_t i = 0; i < 4; ++i)
func_impl(n, arr);
}
См. также диагностику V760.
Диагностика выявляет ошибки при переопределении виртуальных функций. Рассмотрим два возможных варианта.
Вариант N1. В базовом классе имеется виртуальная функция, в которой один из параметров имеет один тип. В наследнике есть точно такая же функция, но этот параметр имеет другой тип. При этом типы могут быть либо целочисленными типами, либо перечислениями, либо указателями или ссылками на базовый и унаследованный классы.
Эта диагностика помогает выявить ошибки, когда при большом рефакторинге меняют тип функции в одном из классов, но забывают изменить эту функцию в другом классе.
Рассмотрим пример:
struct Q { virtual int x(short) { return 1; } };
struct W : public Q { int x(int) { return 2; } };
На самом деле, код должен быть таким:
struct Q { virtual int x(short) { return 1; } };
struct W : public Q { int x(short) { return 2; } };
Если в базовом классе будет две функции 'x' с аргументами 'int' и 'short', то анализатор предупреждение V762 выдавать не будет.
Вариант N2. Диагностика выдаётся в случае, когда в функции базового класса добавили новый аргумент или наоборот удалили. При этом забыли поменять объявление функции в одном из классов-наследников.
Пример подобной ситуации:
struct Q { virtual int x(int, int=3) { return 1; } };
struct W : public Q { int x(int) { return 2; } };
Корректный код:
struct Q { virtual int x(int, int=3) { return 1; } };
struct W : public Q { int x(int, int) { return 2; } };
Сценарий возникновения ошибки может быть следующим. Есть иерархия классов. В какой-то момент в функцию базового класса или класса-наследника добавляют аргумент. В итоге объявляется новая функция, никак не связанная с функцией из базового класса.
Это выглядит подозрительно и может быть ошибкой. Может быть забыли поправить один из классов, может быть забыли про то, что функция является виртуальной. Но анализатор не может понять по смыслу функции, что данный код корректен. Если же данное поведение предусмотрено и ошибкой не является, можно воспользоваться одним из механизмов подавления ложных срабатываний.
Пример подозрительного кода:
struct CA
{
virtual void Do(int Arg);
};
struct CB : CA
{
virtual void Do(int Arg1, double Arg2);
};
Для того чтобы избежать подобных ошибок при использовании C++11 и выше рекомендуется использовать ключевое слово 'override', которое предотвратит несовпадение сигнатур на этапе компиляции.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V762. |
Анализатор нашёл потенциальную ошибку в теле функции. Один из его параметров перезаписывается перед тем, как используется. Таким образом, значение, пришедшее в функцию, попросту теряется.
Рассмотрим пример кода:
void Foo(Node A, Node B)
{
A = SkipParenthesize(A);
B = SkipParenthesize(A); // <=
AnalyzeNode(A);
AnalyzeNode(B);
}
Здесь допущена опечатка, так как перепутаны параметры 'A' и 'B', из-за чего переменная 'B' примет неверное значение. Исправленный код выглядит так:
void Foo(Node A, Node B)
{
A = SkipParenthesize(A);
B = SkipParenthesize(B);
AnalyzeNode(A);
AnalyzeNode(B);
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V763. |
Анализатор обнаружил подозрительную передачу аргументов в функцию. Некоторые имена аргументов не соответствуют именам параметров, в качестве которых они передаются. Это может свидетельствовать об ошибочной передаче значений в функцию.
Пусть имеется объявление функции следующего вида:
void SetRGB(unsigned r, unsigned g, unsigned b);
Пример ошибочного кода:
void Foo(){
unsigned R = 0, G = 0, B = 0;
....
SetRGB(R, B, G);
....
}
Во время задания цвета объекта, перепутали значения синего и зелёного цветов.
Исправленный вариант кода должен выглядеть следующим образом:
SetRGB(R, G, B);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V764. |
Анализатор нашёл потенциальную ошибку в арифметическом или логическом выражении. В составном присваивании переменная стоит и слева, и справа от оператора.
Рассмотрим пример кода:
void Foo(int x, int y, int z)
{
x += x + y;
....
}
Возможно, что здесь допущена опечатка, и правильный код должен выглядеть так:
void Foo(int x, int y, int z)
{
x = x + y;
....
}
Или так:
void Foo(int x, int y, int z)
{
x += z + y;
....
}
Конечно, подобные выражения используют для того, чтобы хитро умножить число на два. Тем не менее такой код подозрителен, и его стоит перепроверить. Такие операции выглядят довольно запутанно и, возможно, стоит написать гораздо более простой и понятный код:
void Foo(int x, int y, int z)
{
x = x * 2 + y;
....
}
Есть и более подозрительные выражения, работу которых стоит внимательно проверить:
void Foo(int x, int y)
{
x -= x + y;
}
Данное выражение можно упростить следующим образом:
Не понятно, является ли такое поведение преднамеренным или же это опечатка. В любом случае, этот код следует проверить.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V765. |
Анализатор обнаружил подозрительную ситуацию, когда в словарь (контейнеры типа 'map' и т.п.) или в множество (контейнеры типа 'set' и т.п.) добавляются элементы с ключами, уже присутствующими в этих контейнерах. В результате добавление нового элемента будет проигнорировано. Это может свидетельствовать об опечатке и привести к неверному заполнению контейнера.
Рассмотрим пример со словарём:
map<char, int> dict = map<char, int>{
make_pair('a', 10),
make_pair('b', 20),
make_pair('a', 30) // <=
};
В последней строке инициализации была допущена ошибка, так как ключ 'a' уже содержится в словаре. В результате данный словарь будет содержать 2 значения, причём значение, связанное с ключом 'a', будет равно 10.
Исправить ошибку можно, использовав правильное значение ключа:
map<char, int> dict = map<char, int>{
make_pair('a', 10),
make_pair('b', 20),
make_pair('c', 30)
};
Схожую ошибку можно допустить и при инициализации множества:
set<string> someSet = set<string>{
"First",
"Second",
"Third",
"First", // <=
"Fifth"
};
Из-за ошибки вместо ключа 'Fourth' в множество 'someSet' пытаются записать строку 'First', но так как такой ключ уже содержится в множестве, он будет проигнорирован.
Для исправления ошибки необходимо исправить список инициализации:
set<string> someSet = set<string>{
"First",
"Second",
"Third",
"Fourth",
"Fifth"
};
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V766. |
Анализатор обнаружил возможную ошибку, связанную с тем, что в цикле 'for' на каждой итерации к элементу массива или контейнера обращаются по одному и тому же константному индексу.
Рассмотрим пример некорректного кода:
void Foo(vector<size_t> &vect)
{
for (size_t i = 0; i < vect.size(); i++)
vect[0] *= 2;
}
В данной функции хотели изменить все значения, хранящиеся в векторе, однако из-за опечатки доступ к элементам вектора осуществляется не по счётчику цикла 'i', а по константному значению - 0. В итоге будет изменено только одно значение (если вектор не пустой).
Для исправления ошибки необходимо правильно переписать обращение к элементам контейнера:
void Foo(vector<size_t> &vect)
{
for (size_t i = 0; i < vect.size(); i++)
vect[i] *= 2;
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V767. |
Анализатор обнаружил подозрительный фрагмент кода, в котором именованная константа из перечисления либо переменная типа перечисления используется как булевое значение. Скорее всего, это свидетельствует о наличии ошибки в логике программы.
Рассмотрим пример.
enum Offset { left=10, right=15, top=20, bottom=25 };
void func(Offset offset)
{
....
if (offset || i < 10)
{
....
}
}
В данном случае переменная типа перечисления 'offset' используется как булевое значение, но поскольку в перечислении 'Offset' все значения ненулевые, то условие будет выполняться всегда. Предупреждение анализатора в данном случае подсказывает, что выражение написано неверно и его следует исправить, например, так:
void func(Offset offset)
{
....
if (offset == top || i < 10)
{
....
}
}
Рассмотрим еще один пример. Пусть у нас имеется перечисление следующего вида:
enum NodeKind
{
NK_Identifier = 64,
....
};
И класс вида
class Node
{
public:
NodeKind _kind;
bool IsKind(ptrdiff_t kind) const { return _kind == kind; }
};
Тогда ошибка может выглядеть следующим образом:
void foo(Node node)
{
if (node.IsKind(!NK_Identifier))
return;
....
}
Подразумевалось прекращение выполнения функции, если текущий узел не является идентификатором. Но результат выражения '!NK_Identifier' будет равен '0', а перечисление 'NodeKind' не содержит элемента с таким значением. Как итог, метод 'IsKind' всегда будет возвращать значение 'false', и функция продолжит выполнение, независимо от того, чем является текущий узел.
Исправленный вариант кода выглядит так:
void foo(Node node)
{
if (!node.IsKind(NK_Identifier))
return;
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V768. |
Анализатор обнаружил подозрительную операцию, применяемую к нулевому указателю. Полученный указатель не имеет смысла. Скорее всего, это свидетельствует о наличии ошибки в логике программы.
Пример:
void foo(bool isEmpty, char *str)
{
char *begin = isEmpty ? str : nullptr;
char *end = begin + strlen(str);
....
}
Если указатель 'begin' равен nullptr, то выражение "nullptr + len" не имеет смысла: использовать его всё равно нельзя. Возможно, переменная дальше не используется. Тогда стоит провести рефакторинг кода таким образом, чтобы данная операция не осуществлялась над нулевым указателем: это потенциальный источник ошибок, если кто-то об этом забудет и попытается обратиться по указателю.
Например, код можно переписать так:
void foo(bool isEmpty, char *str)
{
char *begin = isEmpty ? str : nullptr;
if (begin != nullptr)
{
char *end = begin + strlen(str);
....
}
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки разыменования нулевого указателя. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V769. |
Анализатор обнаружил потенциально возможную опечатку в коде, связанную с тем, что в условии цикла вместо операторов '<' и '<=' используются '<<' и '<<=', соответственно.
Рассмотрим пример такого кода:
void Foo(std::vector<int> vec)
{
for (size_t i = 0; i << vec.size(); i++) // <=
{
// Something
}
}
Выражение "i << vec.size()" будет равно нулю. Это явно ошибка, поскольку тело цикла ни разу не выполняется. Корректный вариант кода:
void Foo(std::vector<int> vec)
{
for (size_t i = 0; i < vec.size(); i++)
{
// Something
}
}
Примечание. Сдвиги вправо (>>, >>=) считаются нормальной ситуацией, так как они используются в различных алгоритмах, например, вычисление количества бит, равных 1. Пример:
size_t num;
unsigned short var = N;
for (num = var & 1 ; var >>= 1; num += var & 1);
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально возможную ошибку в коде, связанную с тем, что в тернарном операторе '?:' в качестве второго и третьего операндов используются константные значения из разных перечислений.
Рассмотрим пример такого кода:
enum OnlyOdd { Not_Odd, Odd };
enum OnlyEven { Not_Even, Even };
int isEven(int a)
{
return (a % 2) == 0 ? Even : Odd;
}
Функция проверяет число на четность, но в возвращающемся значении используются константы из двух разных перечислений (OnlyEven::Even и OnlyOdd::Odd), приводимые к типу 'int'. В результате, независимо от переданного аргумента 'a', функция всегда будет возвращать 1 (истину). Корректный код должен выглядеть следующим образом:
enum OnlyOdd { Not_Odd, Odd };
enum OnlyEven { Not_Even, Even };
int isEven(int a)
{
return (a % 2) == 0 ? Even : Not_Even;
}
Примечание. Нормальной ситуацией считается применение двух разных перечислений без имени, например:
enum
{
FLAG_FIRST = 0x01 << 0,
FLAG_SECOND = 0x01 << 1,
....
};
enum
{
FLAG_RW = FLAG_FIRST | FLAG_SECOND,
....
};
....
bool condition = ...;
int foo = condition ? FLAG_SECOND : FLAG_RW; // нет V771
....
Анализатор обнаружил потенциально возможную ошибку в коде, связанную с тем, что оператор 'delete' или 'delete[]' применяется для нетипизированного указателя (void*). Согласно стандарту C++20 (п. п. $7.6.2.8/3) такое применение ведет к неопределенному поведению.
Рассмотрим пример такого кода:
class Example
{
int *buf;
public:
Example(size_t n = 1024) { buf = new int[n]; }
~Example() { delete[] buf; }
};
....
void *ptr = new Example();
....
delete ptr;
....
Подобный пример опасен тем, что компилятор в реальности не знает, к каком типу относится указатель 'ptr'. Поэтому, при удалении такого нетипизированного указателя могут произойти различные неприятности, например, может возникнуть утечка памяти: оператор 'delete' не вызовет деструктор объекта типа 'Example', на который ссылается указатель 'ptr'.
Если подразумевалась именно работа с нетипизированным указателем, то перед применением оператора 'delete' ('delete[]') его необходимо привести к изначальному типу, например так:
....
void *ptr = new Example();
....
delete (Example*)ptr;
....
Иначе, во избежание ошибок, рекомендуется использовать только типизированные указатели совместно с оператором 'delete' ('delete[]'):
....
Example *ptr = new Example();
....
delete ptr;
....
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V772. |
Анализатор обнаружил потенциально возможную утечку памяти в коде. Такая ситуация возникает, когда память, выделенная с помощью 'malloc' или 'new', не была далее освобождена.
Рассмотрим пример такого кода:
int *NewInt()
{
int *p = new (std::nothrow) int;
....
return p;
}
bool Test()
{
int *p = NewInt();
int res = *p;
return res;
}
В данном примере, выделение памяти спрятано в вызове другой функции. Соответственно после вызова нужно соответствующим образом освободить память.
Исправленный код, где не возникает утечка памяти:
int *NewInt()
{
int *p = new int;
....
return p;
}
bool Test()
{
int *p = NewInt();
int res = *p;
delete p;
return res;
}
Часто подобные ошибки можно встретить в обработчиках ошибочных ситуаций, так как к ним невнимательно относятся при обзорах кода и слабо тестируют. Пример:
bool Test()
{
int *p = (int*)malloc(sizeof(int));
int *q = (int*)malloc(sizeof(int));
if (p == nullptr || q == nullptr)
{
std::cerr << "No memory";
return -1;
}
int res = *p + *q;
free(p);
free(q);
return res;
}
Может сложиться ситуация, когда указатель 'p' будет хранить указатель на выделенную память, а 'q' будет равен 'nullptr'. Тогда выделенная память освобождена не будет. Кстати, может быть и наоборот. В параллельной программе реальна ситуация, когда первый раз не удастся выделить память, а потом удастся.
Помимо утечек памяти, анализатор может находить утечки ресурсов: незакрытые дескрипторы, файлы и т.д. Такие ошибки не сильно отличаются друг от друга, поэтому к ним относится всё вышесказанное. Приведём небольшой пример:
void LoadBuffer(char *buf, size_t len)
{
FILE* f = fopen("my_file.bin", "rb");
fread(buf, sizeof(char), len, f);
}
Примечание. В современном C++ лучше обходиться без ручного управления ресурсами и использовать умные указатели. Например, можно рекомендовать использовать 'std::unique_ptr'. В этом случае вся память будет освобождена корректно во всех точках выхода функции. Также такое решение будет exception-safe.
У статического анализатора меньше информации об указателях, чем у динамического, поэтому анализатор может выдавать ложные предупреждения, если память освобождается нетривиально или далеко от места, где выделяется. Для отключения таких предупреждений предусмотрен специальный комментарий:
//+V773:SUPPRESS, class:className, namespace:nsName
Параметр 'namespace' является необязательным.
Рассмотрим пример:
void foo()
{
EVENT* event = new EVENT;
event->send();
}
Объект класса 'EVENT' не должен удаляться в этой функции, поэтому отключить все предупреждения V773 на этот класс можно с помощью комментария:
//+V773:SUPPRESS, class:EVENT
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V773. |
Анализатор обнаружил использование указателя, который ссылается на освобождённый участок памяти. Это является неопределённым поведением и может привести к разнообразнейшим последствиям.
Некоторые возможные варианты:
Рассмотрим пример такого кода:
for (node *p = head; p != nullptr; p = p->next)
{
delete p;
}
В данном примере при выполнении выражения 'p = p->next' произойдёт разыменование указателя p, удалённого в теле цикла. Необходимо было сначала вычислить это выражение и только потом освободить память. Исправленный вариант кода:
node *p = head;
while (p != nullptr)
{
node *prev = p;
p = p->next;
delete prev;
}
Неприятное в таких ошибках, что долгое время может казаться, что программа работает корректно. При этом, всё может сломаться после простого рефакторинга кода, добавлении новой переменной, смены компилятора и так далее.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V774. |
Анализатор обнаружил подозрительное сравнение элемента BSTR-типа, при помощи операторов отношения: >, <, >=, <=.
BSTR (basic string или binary string) - это строковый тип данных, который используется в COM, Automation и Interop функциях. Этот тип данных включает в себя префикс длины, строку данных и терминальный ноль.
Тип BSTR является указателем, который всегда указывает на первый символ строки данных, а не на префикс длины. По этой причине, каждый BSTR объект является уникальным и один BSTR не может быть частью другого, как это возможно с обычными строками.
Однако, обычная строка может являться частью BSTR (но не наоборот), поэтому сравнения вида "wchar_t* > BSTR" допустимы.
Рассмотрим пример:
void func(BSTR a, BSTR b)
{
if (a > b)
{
....
}
}
Данный код является ошибочным, так как сравнение указателей 'a' и 'b' не имеет смысла.
Подробнее про BSTR можно прочитать на MSDN.
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально бесконечный цикл, условие выхода из которого зависит от переменной, значение которой никогда не меняется в нем.
Рассмотрим пример:
int Do(int x);
....
int n = Foo();
int x = 0;
while (x < n)
{
Do(x);
}
Условие выхода из цикла зависит от переменной 'x', значение которой в цикле всегда будет равно нулю. Таким образом проверка 'x < 10' всегда будет истинной, что приведет к бесконечному циклу в данном случае. Правильный вариант мог бы выглядеть так:
int Do(int x);
int n = Foo();
int x = 0;
while (x < n)
{
x = Do(x);
}
Рассмотрим еще один пример, когда условие выхода из цикла зависит от переменной, изменение значения этой которой зависит от других переменных, которые никогда не меняются внутри цикла. Пусть у нас имеется метод следующего вида:
int Foo(int a)
{
int j = 0;
while (true)
{
if (a >= 32)
{
return j * a;
}
if (j == 10)
{
j = 0;
}
j++;
}
}
Условие выхода из цикла зависит от параметра 'a'. В случае если параметр 'a' не будет удовлетворять условие проверки 'a >= 32', то цикл будет бесконечным, так как его значение в цикле не изменяется. Корректный вариант мог бы выглядеть так:
int Foo(int a)
{
int j = 0;
while (true)
{
if (a >= 32)
{
return j * a;
}
if (j == 10)
{
j = 0;
a++; // <=
}
j++;
}
}
Таким образом изменение параметра 'a' будет зависеть от локальной переменной 'j'.
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально возможную ошибку в коде, связанную с тем, что массив производных классов адресуется через указатель на базовый класс. При попытке доступа к ненулевому элементу массива через указатель на базовый класс произойдет ошибка.
Рассмотрим пример такого кода:
class Base
{
int buf[10];
public:
virtual void Foo() { ... }
virtual ~Base() { }
};
class Derived : public Base
{
char buf[10];
public:
virtual void Foo() override { ... }
virtual ~Derived() { }
};
....
size_t n = 5;
Base *ptr = new Derived[n]; // <=
....
for (size_t i = 0; i < n; ++i)
(ptr + i)->Foo();
....
В примере объявлены базовый класс "Base" и производный от него "Derived". Каждый объект этих классов будут занимать в памяти 48 и 64 байта соответственно (вследствие выравнивания классов по ширине 8 байт; компилятор MSVC, 64-bit). При "i >= 1" для обращения к ненулевому элементу необходимо каждый раз перемещать указатель на "i * 64" байта, но, поскольку массив адресуется указателем на базовый класс Base, смещение на самом деле будет вычисляться как "i * 48" байт.
Так должно было вычисляться смещение указателя:
Однако вычислено смещение указателя будет так:
Фактически, программа начинает работать с объектами, содержащими случайный набор данных.
Корректный вариант кода:
....
size_t n = 5;
Derived *ptr = new Derived[n]; // <=
....
for (size_t i = 0; i < n; ++i)
(ptr + i)->Foo();
....
Ошибочно также приводить указатель на указатель на производный класс к указателю на указатель на базовый класс:
....
Derived arr[3];
Derived *pDerived = arr;
Class5 **ppDerived = &pDerived;
....
Base **ppBase = (Derived**)ppDerived; // <=
....
Для правильного хранения массива объектов производного класса полиморфически необходимо размещать объекты следующим образом:
Корректный код при этом будет выглядеть следующим образом:
....
size_t n = 5;
Base **ppBase = new Base*[n]; // <=
for (size_t i = 0; i < n; ++i)
ppBase[i] = new Derived();
....
Если мы хотим подчеркнуть, что будем работать только с одним объектом, то можно написать так:
....
Derived *derived = new Derived[n];
Base *base = &derived[i];
....
Такой код считается анализатором безопасным, и он не выдаёт предупреждение.
Не является также ошибкой применение указателя, который адресуется на массив объектов производного класса, содержащий один элемент.
....
Derived arr[1];
Derived *new_arr = new Derived[1];
Derived *malloc_arr = static_cast<Base*>(malloc(sizeof(Derived)));
....
Base *base = arr;
base = new_arr;
base = malloc_arr;
....
Примечание. В случае одинакового размера базового и производного классов допускается адресоваться на массив объектов производного класса указателем на базовый класс, однако так делать не рекомендуется.
Данная диагностика классифицируется как:
|
Анализатор обнаружил код, который, возможно, содержит опечатку. Высока вероятность, что подобный код был создан с использованием подхода Copy-Paste.
Данная диагностика выявляет два схожих по структуре блока текста, идущих один за другим и отличающихся именем одной переменной. Предупреждение V778 предназначено для выявления тех случаев, если второй блок был получен путем копирования первого, при этом во втором блоке были переименованы не все переменные.
Рассмотрим пример:
void Example(int a, int b)
{
....
if (a > 50)
doSomething(a);
else if (a > 40)
doSomething2(a);
else
doSomething3(a);
if (b > 50)
doSomething(b);
else if (a > 40) // <=
doSomething2(b);
else
doSomething3(b);
....
}
Код писался с помощью Copy-Paste. Во втором блоке не уследили и забыли заменить все переменные 'a' на 'b'. На самом деле, код должен быть таким:
void Example(int a, int b)
{
....
if (a > 50)
doSomething(a);
else if (a > 40)
doSomething2(a);
else
doSomething3(a);
if (b > 50)
doSomething(b);
else if (b > 40)
doSomething2(b);
else
doSomething3(b);
....
}
Теперь рассмотрим пример, взятый из реального проекта:
....
if(erendlinen>239) erendlinen=239;
if(srendlinen>erendlinen) srendlinen=erendlinen;
if(erendlinep>239) erendlinep=239;
if(srendlinep>erendlinen) srendlinep=erendlinep; // <=
....
Заметить ошибку не так уж и просто по сравнению с предыдущим примером. Имена переменных похожи друг на друга, и поэтому выявление ошибки усложняется в разы. На самом деле во втором блоке вместо переменной 'erendlinen' должна стоять 'erendlinep'.
Имена переменных 'erendlinen' и 'erendlinep' выбраны явно неудачно. Такую ошибку почти невозможно заметить при Code Review. Да что уж там, даже когда анализатор указывает на строку с ошибкой и то сложно её заметить. Поэтому, встретив предупреждение V778, рекомендуем не спешить и внимательно изучить код.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V778. |
Анализатор обнаружил код, который никогда не будет выполнен. Возможно допущена ошибка в логике программы.
Данная диагностика находит блоки кода, до которых никогда не дойдёт управление.
Рассмотрим пример:
void Error()
{
....
exit(1);
}
FILE* OpenFile(const char *filename)
{
FILE *f = fopen(filename, "w");
if (f == nullptr)
{
Error();
printf("No such file: %s", filename);
}
return f;
}
Функция 'printf(....)' никогда не напечатает сообщение об ошибке, так как функция 'Error()' не возвращает управление. Как правильно исправить код зависит от того, какую логику поведения задумывал программист изначально. Возможно, функция должна возвращать управление. Возможно, нарушен порядок выражений и корректный код должен быть таким:
FILE* OpenFile(const char *filename)
{
FILE *f = fopen(filename, "w");
if (f == nullptr)
{
printf("No such file: %s", filename);
Error();
}
return f;
}
Рассмотрим ещё один пример:
void f(char *s, size_t n)
{
for (size_t i = 0; i < n; ++i)
{
if (s[i] == '\0')
break;
else
return;
s[i] = toupper(s[i]);
}
}
Весь код, стоящий после 'if', не будет выполнен, так как обе ветви не возвращают управление. Скорее всего, нужно внести код в одну из ветвей или убрать noreturn выражение.
Например, можно исправить код следующим образом:
void f(char *s, size_t n)
{
for (size_t i = 0; i < n; ++i)
{
if (s[i] == '\0')
break;
s[i] = toupper(s[i]);
}
}
Если реализация функции находится в другом файле, анализатору требуется подсказка, чтобы понять, что она всегда прекращает работу программы. В противном случае, он может не заметить ошибку. Для подсказки можно воспользоваться аннотациями при объявлении функции:
[[noreturn]] void my_abort(); // C++11
__declspec(noreturn) void my_abort(); // MSVC
__attribute__((noreturn)) void my_abort(); // GCC
Есть некоторые фрагменты кода, на которые анализатор не будет выдавать предупреждение, несмотря на формальное наличие ошибки. Пример:
int test()
{
throw 0;
return 0;
}
Это связано с тем, что такой код часто пишут для подавления предупреждений компилятора или других анализторов.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V779. |
Анализатор обнаружил опасную работу со сложными типами. Если объект не является Passive Data Structure (PDS), то нельзя использовать низкоуровневые функции по работе с памятью: memset, memcpy и т.д. Это может нарушить логику работы класса и привести к утечке памяти, двойному очищению одного ресурса или Undefined Behavior.
Пример классов, над которыми нельзя совершать такие действия: std::vector, std::string и другие подобные контейнеры.
Иногда такая диагностика находит опечатки. Рассмотрим пример:
struct Buffer {
std::vector<char>* m_data;
void load(char *buf, size_t len) {
m_data->resize(len);
memcpy(&m_data[0], buf, len);
}
};
Функция 'memcpy' копирует данные не в содержимое контейнера, а в объект, на который указывает 'm_data'. Такой код нужно переписать следующим образом:
memcpy(&(*m_data)[0], buf, len);
или:
memcpy(m_data->data(), buf, len);
Другой способ ошибиться - это использовать memset/memcpy для структуры, полями которой являются non-PDS объекты. Рассмотрим пример:
struct Buffer {
std::vector<char> m_data;
....
};
void F() {
Buffer a;
memset(&a, 0, sizeof(Buffer));
....
}
Чтобы избежать таких ошибок можно рекомендовать использовать value initialization. Такой подход корректно работает и с POD-данными, и с объектами с нетривиальным конструктором.
Для копирования можно положиться на генерируемый компилятором конструктор копирования или написать свой.
Анализатор также ищет структуры, использование memset/memcpy над которыми может быть опасно, исходя из их логики или представления в памяти. К первому случаю относятся классы, в которых одновременно есть указатели, конструкторы и деструкторы. Если в классе содержится нетривиальная работа с указателями (например, управление памятью или ресурсом), то нельзя использовать для него memcpy/memset. Пример такого класса:
struct Buffer {
char *buf;
Buffer() : buf(new char[16]) {}
~Buffer() { delete[] buf; }
};
Buffer buf1, buf2;
memcpy(&buf1, &buf2, sizeof(Buffer));
Ко второму случаю относятся классы, не являющиеся standard layout:
struct BufferImpl {
virtual bool read(char *, size_t) { return false; }
};
struct Buffer {
BufferImpl impl;
};
Buffer buf1, buf2;
memcpy(&buf1, &buf2, sizeof(Buffer));
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V780. |
Анализатор заметил в коде следующую ситуацию. В начале, значение переменной используется в качестве размера или индекса массива. А уже затем это значение сравнивается с 0 или с размером массива. Это может указывать на наличие логической ошибки в коде или опечатку в одном из сравнений.
Рассмотрим пример
int idx = GetPos(buf);
buf[idx] = 42;
if (idx < 0) return -1;
Если значение 'idx' окажется меньше нуля, то выражение 'buf[idx] ' приведёт к ошибке. Анализатор выдаст предупреждение на этот код, указав 2 строки. Первая строка - это то место, где используется переменная. Вторая строка - это то место, где переменная сравнивается с другим значением.
Исправленный вариант кода:
int idx = GetPos(buf);
if (idx < 0) return -1;
buf[idx] = 42;
Точно также анализатор выдаёт предупреждение, если переменная сравнивается с размером массива:
int buf[10];
buf[idx] = 42;
if (idx < countof(buf)) return -1;
Правильный вариант кода:
int buf[10];
if (idx < countof(buf)) return -1;
buf[idx] = 42;
Помимо индексов, анализатор смотрит на использование переменных в качестве аргументов к функциям, которые принимают неотрицательные значения (memset, malloc и т.д.). Рассмотрим пример:
bool Foo(char *A, int size_A, char *B, int size_B)
{
if (size_A <= 0)
return false;
memset(A, 0, size_A);
....
if (size_A <= 0) // Error
return false;
memset(B, 0, size_B);
....
}
В коде допущена опечатка, которая будет выявлена косвенным образом. В коде всё хорошо с массивом 'A'. Однако, размер массива 'B' проверяется неправильно. В результате возникает ситуация, что 'size_A' проверяется уже после использования.
Корректный код:
bool Foo(char *A, int size_A, char *B, int size_B)
{
if (size_A <= 0)
return false;
memset(A, 0, size_A);
....
if (size_B <= 0) // FIX
return false;
memset(B, 0, size_B);
....
}
Так же анализатор может увидеть проблему, если использование переменной в качестве индекса массива и её проверка находятся в одном выражении:
void f(int *arr, const int size)
{
for (int i = 0; arr[i] < 10 && i < size; ++i)
arr[i] = 0;
}
В этом случае на последней итерации цикла мы будем проверять значение, взятое за границей массива, а это является неопределённым поведением.
Исправленный вариант:
void f(int *arr, const int size)
{
for (int i = 0; i < size && arr[i] < 10; ++i)
arr[i] = 0;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V781. |
Анализатор обнаружил бессмысленный код для вычисления расстояния между элементами разных массивов.
Рассмотрим пример:
ptrdiff_t offset()
{
char path[9] = "test.txt";
char resources[9] = "resr.txt";
return path - resources;
}
Вычитание адресов двух массивов, выделенных на стеке, не имеет никакого практического смысла и скорее всего является ошибкой.
Для рассмотрения всех подозрительных операций с указателями на массивы, разделим типы указателей на две условные группы:
Тогда получим таблицу операций над указателями на массивы, вычисление которых не имеет практического смысла (таблица 1).
Таблица 1 - Бессмысленные операции над указателями.
Данная диагностика классифицируется как:
|
Анализатор обнаружил фрагмент кода, который может привести к использованию невалидного итератора.
Рассмотрим несколько примеров, для которых анализатор выдает данное диагностическое сообщение:
if (iter != vec.end() || *iter == 42) { ... }
if (iter == vec.end() && *iter == 42) { ... }
Во всех условиях допущена логическая ошибка, которая приведет к разыменованию невалидного итератора. Ошибка может быть допущена при рефакторинге кода или из-за случайно опечатки.
Корректные варианты:
if (iter != vec.end() && *iter == 42) { ... }
if (iter == vec.end() || *iter == 42) { ... }
Конечно, это очень простые ситуации. На практике проверка итератора и его использование может находиться в разных местах. Если анализатор выдал предупреждение V783, изучите код расположенный выше и попробуйте понять, почему итератор может быть невалидным.
Пример кода, где проверка и использование итератора находятся в разных строках:
if (iter == vec.end()) {
std::cout << "Error: " << *iter << std::endl;
throw std::runtime_error("foo");
}
Анализатор предупредит об опасности в выражении '*iter'. Здесь или некорректно написано условие, или вместо 'iter' должна использоваться другая переменная.
Анализатор также находит ошибки, когда использование итератора находится до его проверки.
Пример:
std::cout << "Element is " << *iter << std::endl;
if (iter == vec.end()) {
throw std::runtime_error("");
}
Здесь проверка не имеет смысла, так как если итератор невалидный, то выше по коду произойдёт его разыменование. Скорее всего нужно добавить дополнительную проверку итератора:
if (iter != vec.end()) {
std::cout << "Element is " << *iter << std::endl;
}
if (iter == vec.end()) {
throw std::runtime_error("");
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V783. |
Анализатор обнаружил подозрительную операцию при работе с битовыми масками. В качестве битовой маски используется переменная меньшей разрядности, чем другой операнд. Это приведёт к гарантированной потере старших бит.
Рассмотрим несколько примеров, для которых анализатор выдает данное диагностическое сообщение:
unsigned long long x;
unsigned y;
....
x &= ~y;
Посмотрим подробнее, что происходит с битами после каждой операции на примере выражения:
x = 0xffff'ffff'ffff'ffff;
y = 0xff;
x &= ~y;
Как правило это не тот результат, который планировал получить программист:
0xffff’ffff’ffff’ff00 – ожидалось
0x0000’0000’ffff’ff00 – получилось
Код можно поправить, явно приведя переменную 'y' к типу, которая имеет переменная'x':
x &= ~(unsigned long long)y;
В данном случае, сначала произойдёт преобразование типов, а потом отрицание. После операции все старшие биты будут равны единице. Рассмотрим, как изменится пример выше при данном порядке вычислений:
Анализатор также предупреждает о таком коде:
unsigned long long x;
unsigned y;
....
x &= y;
Несмотря на то, что здесь не используются дополнительные операции, этот код всё равно выглядит подозрительно. Лучше явно преобразовать типы, дабы поведение было более очевидным, как для анализатора, так и для ваших коллег.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V784. |
Анализатор обнаружил константное выражение в условии 'switch'. Чаще всего это сигнализирует о логической ошибке.
Рассмотрим синтетический пример:
int i = 1;
switch (i)
{
....
}
В качестве условия 'switch' стоит переменная, значение которой может быть посчитано во время компиляции. Такая ситуация могла возникнуть в результате рефакторинга: раньше был код, который менял значение переменной, а потом его поменяли и оказалось, что переменной больше не присваивается никакое значение.
Анализатор не выдаёт предупреждение, когда переменная константная или в условии используется макросы. Такие конструкции часто используют намеренно для включения/выключения функционала во время компиляции.
Например, могут выполняться различные действия, в зависимости от того для какой операционной системы скомпилирован код:
switch (MY_PROJ_OS)
{
case MY_PROJ_WINDOWS:
....
case MY_PROJ_LINUX:
....
case MY_PROJ_MACOS:
....
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что переменной пытаются присвоить значение, которое не входит в её диапазон возможных значений.
Рассмотрим несколько примеров, для которых анализатор выдает данное диагностическое сообщение:
bool b;
....
b = 100;
Нет никакого смысла присваивать значение 100 переменной типа bool. Возможно, что мы имеем дело с опечаткой и вместо 'b' должна использоваться другая переменная.
Рассмотрим другой пример:
struct S
{
int flag : 1;
}
....
S s;
s.flag = 1;
Битовое поле 'flag' может принимать значения из диапазона [-1, 0], а не [0, 1], как может показаться на первый взгляд. Причина в том, что тип переменной знаковый. Если вам нужно битовое поле с диапазоном в [0, 1], используйте тип 'unsigned':
struct S
{
unsigned flag : 1;
}
....
S s;
s.flag = 1;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V786. |
Анализатор обнаружил, что счётчик в цикле используется в условии выхода из цикла в качестве индекса. Такой код выглядит подозрительно.
Рассмотрим пример:
for (int i = 0; i < n; ++i)
for (int j = 0; j < arr[j]; ++j)
....
Скорее всего, вместо переменной 'j' нужно использовать 'i':
for (int i = 0; i < n; ++i)
for (int j = 0; j < arr[i]; ++j)
....
Анализатор обнаружил подозрительный захват переменной в лямбда функции.
Рассмотрим несколько вариантов срабатывания диагностики.
Пример 1:
int x = 0;
auto f = [x] { };
....
x = 42;
f();
...
Переменную, значение которой может быть точно вычислено в compile-time, захватывают в лямбда функции по значению. Внутри этой функции значение переменной будет таким, которым оно было в момент захвата, а не то, которая имеет эта переменная в момент вызова лямбда функции. Возможно, необходимо захватить переменную по ссылке.
int x = 0;
auto f = [&x] { };
....
x = 42;
f();
...
Также возможно, что в ходе рефакторинга был удалён код, который присваивал переменной какое-либо значение.
int x = 0;
if (condition) x = 42;
else x = 43;
auto f = [x] { };
Если же необходимо захватить некую константу, то лучше явно указать в типе переменной 'const' или 'constexpr'.
constexpr int x = 0;
auto f = [x] { };
Пример 2:
int x;
auto f = [x] { };
Неинициализированная переменная захватывается по значению. Её использование приведёт к Undefined Behavior. Если предполагалось, что вызов функции должен проинициализировать переменную, то её необходимо захватить по ссылке.
int x;
auto f = [&x] { x = 42; };
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил инвалидацию итератора в range-based for цикле.
Пример:
std::vector<int> numbers;
for (int num : numbers)
{
numbers.push_back(num * 2);
}
Данный код аналогичен следующему:
for (auto __begin = begin(numbers), __end = end(numbers);
__begin != __end; ++__begin) {
int num = *__begin;
numbers.push_back(num * 2);
}
Теперь видно, что при операции 'push_back' может произойти инвалидация итераторов '__begin' и '__end', если произойдёт реаллокация памяти внутри вектора.
Если необходимо модифицировать контейнер и одновременно читать из него значения, лучше воспользоваться функциями, которые возвращают новый итератор после модификации, или индексами в случае класса 'std::vector'.
Дополнительные ссылки:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V789. |
Анализатор обнаружил, что оператор присваивания принимает объект по неконстантной ссылке и именно его возвращает.
Пример:
class C {
C& operator = (C& other) {
....
return other;
}
};
Если оператор присваивания будет реализован таким способом, то можно получить неожиданные и неприятные эффекты. Допустим, программист напишет код:
(A = B)++;
Оставим в стороне тот факт, что такой код вообще лучше не писать. Предположим, что этот код нужен. Программист, возможно, ожидает, что код будет работать следующим образом:
A = B;
A++;
Однако из-за неправильного оператора присваивания он работает так:
A = B;
B++;
Чтобы предотвратить появление таких ошибок, лучше передавать аргумент по константной ссылке. Тогда код с реализаций оператора просто бы не скомпилировался.
Корректный код:
class C {
C& operator = (const C& other) {
....
return *this;
}
};
Анализатор выявил цикл, который может содержать ошибку или быть неоптимальным. Используется типичный паттерн кода, когда для всех пар элементов массива выполняется некая операция. При этом, как правило, нет смысла выполнять операцию для пары, состоящей из одного и того-же элемента при 'i == j'.
Пример:
for (int i = 0; i < size; i++)
for (int j = i; j < size; j++)
...
Есть большая вероятность, что правильнее или эффективнее использовать следующий код для обхода массивов:
for (int i = 0; i < size; i++)
for (int j = i + 1; j < size; j++)
...
Далее приведён пример кода из реального приложения, в котором реализовали собственный алгоритм сортировки устройств по приоритету и сделали это не оптимально:
/* Simple bubble sort */
for (i = 0; i < n_devices; ++i) {
for (uint32_t j = i; j < n_devices; ++j) {
if (devices[i]->prio > devices[j]->prio) {
struct device_t *tmp;
tmp = devices[i];
devices[i] = devices[j];
devices[j] = tmp;
}
}
}
Исправленный вариант:
/* Simple bubble sort */
for (i = 0; i < n_devices - 1; ++i) {
for (uint32_t j = i + 1; j < n_devices; ++j) {
if (devices[i]->prio > devices[j]->prio) {
struct device_t *tmp;
tmp = devices[i];
devices[i] = devices[j];
devices[j] = tmp;
}
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V791. |
Анализатор обнаружил возможную опечатку в логическом выражении. Вместо логического оператора (&& или ||) написали битовый оператор (& или |). Это означает, что правая часть будет выполнена независимо от результата левой части.
Пример:
if (foo() | bar()) {}
Если в качестве операндов стоят довольно ресурсоемкие операции, то использование битовых операций не оптимально. Кроме того, такой код может привести к ошибке из-за разных типов выражения и разных приоритетов операций. Также возможна ситуация, когда правая часть не должна быть выполнена, если левая завершилась неудачно. В таком случае может произойти обращение к неинициализированным ресурсам. Также битовые операции не гарантируют порядок вычисления операндов.
Корректный код:
if (foo() || bar()) {}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V792. |
Анализатор обнаружил возможную опечатку в логическом выражении. В качестве условного выражения используется арифметическая операция.
Пример:
int a;
int b;
if (a + b) {}
Несмотря на то, что поведение такого кода может быть понятно его автору, лучше использовать явные проверки. Такой код аналогичен 'a + b != 0'. Человек, который будет читать и сопровождать этот код, постоянно будет задавать себе вопрос: "А не забыли ли сравнить с чем-то полученную сумму?". Возможно, результат операции хотели сравнить с некой константой, например, 42, и корректный код должен был быть таким:
if (a + b == 42) {}
Пример из кода реального проекта:
// verify that time is well formed
if ( ( hh / 24 ) || ( mm / 60 ) ) {
return false;
}
Код работает, как и задумывалось. Но он бы выглядел намного понятнее, если бы использовались операции сравнения.
// verify that time is well formed
if ( hh >= 24 || mm >= 60 ) {
return false;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V793. |
Анализатор обнаружил опасный оператор копирования/перемещения. В нём нет проверки на присвоение объекта самому себе. При этом в таком операторе есть операции, которые могут привести к утечкам памяти, использованию указателей после их освобождения и другим проблемам, если этому оператору передать ссылку на '*this' в качестве аргумента.
Пример:
class C {
char *p;
size_t len;
public:
C& operator = (const C& other) {
delete p;
len = other.len;
p = new char[len];
std::copy(other.p, other.p + len, p);
return *this;
}
};
Ошибка проявится, если 'this == &other', а соответственно 'p == other.p'. В таком случае, при вызове 'std::copy' произойдёт копирование неинициализированного массива самого в себя.
Корректный код:
C& operator = (const C& other) {
if (this == std::addressof(other))
return *this;
delete p;
len = other.len;
p = new char[len];
std::copy(other.p, other.p + len, p);
return *this;
}
Если же такой ситуации не может быть, то есть у этого оператора есть неявный контракт, то лучше формализовать его в виде 'assert':
C& operator = (const C& other) {
MyAssert(this != std::addressof(other));
....
}
Примечание: в проверке на равенство самому себе лучше использовать функцию 'std::addressof' вместо оператора '&'. Такая проверка будет работать корректно, даже если класс содержит перегруженный оператор '&'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V794. |
Анализатор обнаружил, что в проекте используются 32-битные типы данных для хранения времени. Данная проблема затрагивает программы, использующие типы данных, представляющие собой количество секунд, прошедшее с 1 января 1970 года. Таким образом не удастся корректно работать с датами после 19 января 2038. Рекомендуется использовать 64-битные типы данных для хранения времени.
Стандарт языка С/C++ не определяет размер типа 'time_t', поэтому в зависимости от платформы и настроек проекта он может быть определён по-разному:
typedef /* unspecified */ time_t;
В старых версиях Visual C++ тип 'time_t' являлся 32-разрядным типом в 32-разрядных версиях Windows. Начиная с Visual C++ 2005 тип 'time_t' по умолчанию представляет собой 64-разрядное целое число. В современных версиях Visual C++ можно принудительно установить размер типа 'time_t' равным 32 битам с помощью директивы '_USE_32BIT_TIME_T'. По возможности стоит избавиться от этой директивы и использовать 64-битный тип time_t.
В Linux тип 'time_t' является 64-разрядным типом только в 64-разрядных версиях ОС. На 32-битных Linux-системах, к сожалению, пока не существует стандартного способа сделать тип 'time_t' 64-разрядным. Возможно стоит отказаться от использования типа 'time_t' в пользу сторонних решений.
Дополнительные ресурсы:
В операторе 'switch' анализатор обнаружил ветвь, в которой отсутствует оператор 'break'. В таком случае поток управления перейдёт к следующему 'case'. Возможно, допущена опечатка, и необходимо добавить 'break'.
Пример:
for (char c : srcString)
{
switch (c)
{
case 't':
*s++ = '\t';
break;
case 'n':
*s++ = '\n';
break;
case 'f':
*s++ = '\f'; // <=
case '0':
*s++ = '\0';
}
}
Если это ошибка, то следует добавить оператор 'break'. Если ошибки нет, то следует оставить подсказку анализатору и коллегам, которые будут поддерживать код в будущем.
Существует несколько вариантов указать, что данное поведение намеренное. Можно добавить комментарий:
case A:
foo();
// fall through
case B:
bar();
Также поддерживаются атрибуты 'fallthrough':
__attribute__((fallthrough));
[[fallthrough]];
[[gnu::fallthrough]];
[[clang::fallthrough]];
В диагностике также реализовано несколько эвристических правил, чтобы сократить количество ложных срабатываний. Например, в случае разворачивания цикла:
switch(num) {
case 3:
sum += arr[i + 2];
case 2:
sum += arr[i + 1];
case 1:
sum += arr[i];
}
В этом случае диагностическое предупреждение не будет выдано.
Если в 'switch' уже есть комментарии или атрибуты 'fallthrough', то такие исключения не будут срабатывать, т.к. такой код выглядит ещё более подозрительным.
Диагностическое предупреждение не выдаётся, если вместо 'break' уже используются другие операторы, прерывающие работу 'switch' (это 'return', 'throw' и т.д.).
Т.к. анализатор не может однозначно определить, является ли найденное место ошибкой, возможны ложные срабатывания. Для их устранения рекомендуется пользоваться 'fallthrough' комментариями или атрибутами. Оставленные пояснения в первую очередь будут полезны для других разработчиков, которые будут заниматься поддержкой кода. Также такие пометки смогут считывать компиляторы и статические анализаторы.
Если срабатываний слишком много, то можно отключить эту диагностику или воспользоваться механизмом подавления ложных срабатываний.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V796. |
Анализатор обнаружил ошибку при поиске подстрок или символов в строке.
Пример:
std::string s = foo();
if (s.find("42")) { .... }
Функция 'std::string::find' возвращает число типа 'std::string::size_type'. В случае, когда ничего не найдено, возвращается 'std::string::npos', равный '(size_t)-1'. Соответственно, для того чтобы проверить наличие подстроки, необходимо написать следующий код:
if (s.find("42") != std::string::npos) { .... }
Несмотря на то, что код 'if (s.find(...))' компилируется и работает, логика работы такого кода крайне подозрительна. Он проверяет, что искомая подстрока не встречается или она не является началом строки 's'. Если действительно нужно такое поведение, то лучше написать явно:
const auto pos = s.find("42");
if (pos == std::string::npos || pos != 0) { .... }
Также можно воспользоваться функцией 'boost::starts_with' или её аналогом.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V797. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что размер динамического массива может оказаться меньше количества элементов в его инициализаторе. В этом случае при создании массива, в зависимости от того, какая версия оператора 'new' используется, будет либо выброшено исключение 'std::bad_array_new_length', либо возвращен нулевой указатель.
Рассмотрим пример:
int n = 2;
...
int* arr = new int[n] { 1, 2, 3 };
Значение переменной 'n' меньше количества элементов в инициализаторе. Корректный код должен иметь вид:
int n = 3;
...
int* arr = new int[n] { 1, 2, 3 };
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что переменная не используется после того, как для нее была динамически выделена память. Стоит проверить код на наличие ошибки, либо удалить неиспользуемую переменную.
Рассмотрим пример:
void Func()
{
int *A = new int[X];
int *B = new int[Y];
int *C = new int[Z];
Foo(A, X);
Foo(B, Y);
Foo(B, Z); // <=
delete [] A;
delete [] B;
delete [] C;
}
Здесь была допущена опечатка, и вместо массива 'C' третий вызов функции 'Foo' использует массив 'B'. Анализатор обнаруживает здесь аномалию, что выделяется и освобождается память, но при этом она никак не используется. Исправленный вариант кода выглядит следующим образом:
void Func()
{
int *A = new int[X];
int *B = new int[Y];
int *C = new int[Z];
Foo(A, X);
Foo(B, Y);
Foo(C, Z); // <=
delete [] A;
delete [] B;
delete [] C;
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V799. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что перед выходом из функции в локальную переменную присваивается значение, которое потом нигде не используется.
Возможно, эта переменная должна участвовать в последующих операциях или возращена как результат функции, но из-за опечатки используется другая переменная, или программист забыл написать соответствующий код. Рассмотрим примеры.
Пример 1.
bool IsFitRect(TPict& pict)
{
TRect pictRect;
...
pictRect = pict.GetRect();
return otherRect.dx >= 16 && otherRect.dy >= 16;
}
В этом примере в операторе 'return' вместо размеров 'pictRect' по ошибке используются размеры 'otherRect', в то время как переменная 'pictRect' больше нигде не участвует в вычислениях. Правильный код выглядит следующим образом:
bool IsFitRect(TPict& pict)
{
TRect pictRect;
...
pictRect = pict.GetRect();
return pictRect.dx >= 16 && pictRect.dy >= 16;
}
Пример 2.
bool CreateMiniDump()
{
BOOL bStatus = FALSE;
CString errorMsg;
...
if (hDbgHelp == NULL)
{
errorMsg = _T("dbghelp.dll couldn't be loaded");
goto cleanup;
}
...
if (hFile == INVALID_HANDLE_VALUE)
{
errorMsg = _T("Couldn't create minidump file");
return FALSE;
}
...
cleanup:
if (!bStatus)
AddToReport(errorMsg);
return bStatus;
}
В этом примере во всех блоках 'if' кроме одного после создания сообщения об ошибке выполняется переход в конец функции, где эта ошибка добавляется в отчёт. А при обработке одного из условий выполняется выход из функции сразу без добавления сообщения в отчёт, которое в итоге теряется. Корректный код выглядит следующим образом:
bool CreateMiniDump()
{
BOOL bStatus = FALSE;
CString errorMsg;
...
if (hDbgHelp == NULL)
{
errorMsg = _T("dbghelp.dll couldn't be loaded");
goto cleanup;
}
...
if (hFile == INVALID_HANDLE_VALUE)
{
errorMsg = _T("Couldn't create minidump file");
goto cleanup;
}
...
cleanup:
if (!bStatus)
AddToReport(errorMsg);
return bStatus;
}
Иногда при работе с криптографическими функциями программисты очищают в конце переменные, записывая в них нулевое значение. Это неправильный подход, так компилятор скорее всего выбросит такой код при оптимизации, если переменная больше не используется. Например:
void ldns_sha256_update(...)
{
size_t freespace, usedspace;
...
/* Clean up: */
usedspace = freespace = 0;
}
Для очистки памяти следует использовать специальные функции, которые не будут удалены компилятором во время оптимизации:
void ldns_sha256_update(...)
{
size_t freespace, usedspace;
...
/* Clean up: */
RtlSecureZeroMemory(&usedspace, sizeof(usedspace));
RtlSecureZeroMemory(&freespace, sizeof(freespace));
}
Подробнее об этой ошибке можно прочитать в описании диагностики V597.
В некоторых случаях при борьбе с предупреждениями компилятора о неиспользуемых переменных, программисты присваивают им какие-то значения или присваивают значение переменной самой себе. Это не самый лучший способ, так как при отсутствии комментариев может вводить в заблуждение тех, кто будет потом сопровождать этот код:
static stbi_uc *stbi__tga_load(...)
{
// read in the TGA header stuff
int tga_palette_start = stbi__get16le(s);
int tga_palette_len = stbi__get16le(s);
int tga_palette_bits = stbi__get8(s);
...
// the things I do to get rid of an error message,
// and yet keep Microsoft's C compilers happy... [8^(
tga_palette_start = tga_palette_len = tga_palette_bits =
tga_x_origin = tga_y_origin = 0;
// OK, done
return tga_data;
}
Для таких случаев есть более красивые решения, например, можно использовать функцию:
template<class T> void UNREFERENCED_VAR( const T& ) { }
static stbi_uc *stbi__tga_load(...)
{
// read in the TGA header stuff
int tga_palette_start = stbi__get16le(s);
...
UNREFERENCED_VAR(tga_palette_start);
...
// OK, done
return tga_data;
}
Или использовать специальные макросы, объявленные в системных заголовочных файлах. Например, в Visual C++ таким макросом является UNREFERENCED_PARAMETER. В этом случае анализатор также не будет выдавать предупреждения.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1001. |
Анализатор обнаружил потенциальную ошибку, связанную с вызовом конструктора копирования или оператора присваивания, которые сгенерированы автоматически.
Перечислим условия, при выполнении которых такой вызов автоматически сгенерированных компилятором функций считается опасным:
Высока вероятность, что указатель ссылается на буфер памяти, которая выделяется в конструкторе, а затем освобождается в деструкторе. Подобные объекты нельзя копировать с помощью таких функций как 'memcpy' или с помощью автосгенерированных функций (конструктор копирования, оператор присваивания).
Рассмотрим пример:
class SomeClass
{
int m_x, m_y;
int *m_storagePtr;
public:
SomeClass(int x, int y) : m_x(x), m_y(y)
{
m_storagePtr = new int[100];
....
}
....
~SomeClass()
{
delete[] m_storagePtr;
}
};
void Func()
{
SomeClass A(0, 0);
SomeClass B(A); // <=
....
}
В данном примере при копировании объекта 'A' в объект 'B', происходит копирование указателя 'm_storagePtr' из объекта 'A' в объект 'B'. Вероятнее всего, это не является ожидаемым поведением, а программист задумывал, что при копировании объектов произойдет копирование данных, а не просто указателей. Корректный код должен выглядеть следующим образом:
class SomeClass
{
int m_x, m_y;
int *m_storagePtr;
public:
SomeClass(int x, int y) : m_x(x), m_y(y)
{
m_storagePtr = new int[100];
....
}
SomeClass(const SomeClass &other) : m_x(other.m_x), m_y(other.m_y)
{
m_storagePtr = new int[100];
memcpy(m_storagePtr, other.m_storagePtr, 100 * sizeof(int));
}
....
~SomeClass()
{
delete[] m_storagePtr;
}
};
Аналогичным образом диагностика находит потенциальные ошибки, связанные с использованием оператора присваивания, определенного по умолчанию.
Конечно, анализатор может ошибиться и выдать предупреждение на вполне безопасный класс, однако лучше подстраховаться и изучить все предупреждения V1002. Если окажется, что ошибки нет, то лучше всего явно указать, что программист предполагал использование автосгенерированных функций и это безопасно. Для этого следует использовать ключевое слово 'default':
T(const T &x) = default;
SomeClass &operator=(const T &x) = default;
В этом случае, человеку, который будет сопровождать код будет легче понять, что ошибки нет. А анализатор PVS-Studio не будет выдавать ложные предупреждения.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1002. |
Анализатор обнаружил потенциально возможную ошибку в записи макроса.
Пример:
#define sqr(x) x * x
Этот макрос стоит переписать таким образом:
#define sqr(x) ((x) * (x))
В изначальной реализации макроса есть две проблемы, которые могут привезти к ошибке. Во-первых, сам макрос стоит обернуть в скобки. Если этого не сделать, то это приведёт к ошибке при написании такого коде:
double d = 1.0 / sqr(M_PI); // 1.0 / M_PI * M_PI == 1.0
По той же причине стоит поставить в скобки аргументы:
sqr(M_PI + 0.42); // M_PI + 0.42 * M_PI + 0.42
Из-за того, что препроцессор работает на уровне лексем, не всегда получается получить корректное синтаксическое дерево из текста макроса. Рассмотрим пример:
#define FOO(A,B) A * B
В зависимости от контекста, это может быть, как умножением A на B, так и объявлением переменной B, которая будет являться указателем на A. Так как в момент объявления макроса нет никакой информации о его использовании, анализатор может выдать ложное срабатывание на корректны код. В таком случае, для подавления ложного срабатывания, вы можете воспользоваться одним из способов, описанных в документации.
Примечание. Родственной по смыслу диагностикой является V733. Диагностика V733 работает более точно и даёт меньше ложных срабатываний, так как анализирует уже раскрытый макрос, а не его объявлении. С другой стороны, возможности V733 более ограничены и диагностика не предупредит о многих ошибках.
Другой тип искомых ошибок может выглядеть следующим образом:
#if A
doA();
#else doB();
#endif
Во время редактирования код был случайно удалён перенос строки и функция 'doB()' перестала вызываться. При этом код остался компилируемым.
Исправленный вариант кода:
#if A
doA();
#else
doB();
#endif
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1003. |
Анализатор обнаружил потенциальное разыменование нулевого указателя. Сначала этот указатель проверялся на ноль, а потом использовался без проверки.
Пример:
if (p != nullptr)
{
*p = 42;
}
....
*p += 33;
В случае, если указатель 'p' был равен нулю, в выражении '*p += 33' произойдёт разыменование нулевого указателя. Поэтому нужно добавить проверку:
if (p != nullptr)
{
*p = 42;
}
....
if (p != nullptr)
{
*p += 33;
}
Либо в этом коде 'p' всегда ненулевой и тогда проверку стоит удалить:
*p = 42;
....
*p += 33;
Анализатор может выдать ложное срабатывание в следующем случае:
if (p == nullptr)
{
MyExit();
}
....
*p += 42;
Это происходит, потому что анализатор не может понять, возвращает ли функция 'MyExit' управление или нет. Чтобы подсказать ему эту информацию, следует проаннотировать функцию одним из следующих способов:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки разыменования нулевого указателя. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1004. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что ресурс может создаваться и освобождаться несовместимыми между собой способами.
Например, анализатор предупредит, если файл открыт с помощью функции 'fopen_s', а закрыт с помощью функции 'CloseHandle'.
Рассмотрим пример некорректного кода.
FILE* file = nullptr;
errno_t err = fopen_s(&file, "file.txt", "r");
...
CloseHandle(file);
Невозможно предсказать, к каким последствиям приведёт выполнение такого кода. Возможно, функция 'CloseHandle' вернёт статус ошибки и в результате просто произойдёт утечка ресурса (файл не будет закрыт). Возможны и более серьезные последствия. При некорректном вызове некоторых функций возникает неопределённое поведение, что влечёт непредсказуемые последствия, вплоть до аварийного завершения работы приложения.
Исправленный вариант:
FILE* file = nullptr;
errno_t err = fopen_s(&file, "file.txt", "r");
...
fclose(file);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1005. |
Анализатор обнаружил ошибку, связанную с тем, что один и тот же указатель инициализирует сразу несколько объектов типа 'shared_ptr'. Это может привести к неопределённому поведению программы в момент, когда второй объект, имеющий тип 'shared_ptr', попытается освободить память, которая уже была освобождена благодаря первому объекту.
Рассмотрим пример некорректного кода.
void func()
{
S *rawPtr = new S(10, 20);
std::shared_ptr<S> shared1(rawPtr);
std::shared_ptr<S> shared2(rawPtr);
....
}
При выходе из функции, объект 'shared1' освободит указатель 'rawPtr', после чего объект 'shared2' также попытается освободить его еще раз, что приведет к некорректной работе программы.
Корректный код:
void func()
{
std::shared_ptr<S> shared1(new S(10, 20));
std::shared_ptr<S> shared2(new S(10, 20));
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил обращение к значению объекта класса 'optional', который ранее не был проинициализирован, т.е. не хранит никакого значения. Формально это приводит к неопределенному поведению, а также служит источником других ошибок.
Рассмотрим пример некорректного кода:
std::optional<Value> opt;
if (cond)
{
opt->max = 10;
opt->min = 20;
}
if (opt)
{
....
}
В данном примере переменная 'opt' так и не была проинициализирован, что в свою очередь приводит к тому, что код под условием "if (opt)" никогда не выполняется.
Исправленный вариант:
std::optional<Value> opt;
if (cond)
{
opt = Value(10, 20);
}
if (opt)
{
....
}
Также, анализатор умеет обнаруживать обращение к значению потенциально неинициализированного объекта типа optional. Например:
boost::optional<int> opt = boost::none;
opt = interpret(tr);
if (cond)
opt = {};
process(*opt);
Исправленный вариант:
boost::optional<int> opt = boost::none;
opt = interpret(tr);
if (!cond)
process(*opt);
Примечание. Диагностическое правило имеет специальную настройку, которая позволяет выдавать расширенное сообщение: в нём будет содержаться список функций, при помощи которых следует проверить объекта опционального типа перед получением значения. Для вывода расширенного сообщения нужно добавить комментарий следующего вида в файл с исходным кодом или в файл конфигурации диагностических правил (.pvsconfig):
//+V1007 PRINT_CHECKERS
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1007. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' используются странные начальное и конечное значения счетчика. Это может приводить к нарушению логики работы программы.
Рассмотрим пример подозрительного кода:
int c = 0;
if (some_condition)
{
....
c = 1;
}
for (int i = 0; i < c; ++i) {
....
}
В данном случае цикл выполнится 0 или 1 раз. Следовательно, цикл можно заменить на оператор 'if'.
int c = 0;
if (some_condition)
{
....
c = 1;
}
if (c != 0)
{
....
}
Возможно, что в коде допущена ошибка при вычислении значения переменной, с которой сравнивается счётчик. Например, возможно требовалось написать такой код:
int c = 0;
if (some_condition)
{
....
c = 1 + n;
}
for (int i = 0; i < c; ++i)
{
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1008. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что при объявлении массива указано значение только для одного элемента. Таким образом, остальные элементы будут инициализированы неявно нулём или конструктором по умолчанию.
Рассмотрим пример подозрительного кода:
int arr[3] = {1};
Возможно, программист ожидал, что 'arr' будет состоять из одних единиц, но это не так. Массив будет состоять из значений 1, 0, 0.
Корректный код:
int arr[3] = {1, 1, 1};
Подобная путаница может произойти из-за схожести с конструкцией "arr = {0}", которая инициализирует весь массив нулями.
Если в вашем проекте активно используются подобные конструкции, вы можете отключить эту диагностику.
Также не рекомендуется пренебрегать наглядностью кода.
Например, код для кодирования значений цвета записан следующим образом:
int White[3] = { 0xff, 0xff, 0xff };
int Black[3] = { 0x00 };
int Green[3] = { 0x00, 0xff };
Благодаря неявной инициализации, все цвета заданы правильно, но лучше переписать код более наглядно:
int White[3] = { 0xff, 0xff, 0xff };
int Black[3] = { 0x00, 0x00, 0x00 };
int Green[3] = { 0x00, 0xff, 0x00 };
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1009. |
Анализатор обнаружил использование данных, полученных извне, без предварительной проверки. Такой сценарий излишнего доверия может привести к различным негативным последствиям, в том числе - стать причиной уязвимостей.
На данный момент диагностическое правило V1010 выявляет ошибки по нескольким паттернам:
Примечание. Начиная с версии 7.32, пользователь может самостоятельно разметить необходимые функции как источники и приёмники недостоверных данных. Примеры аннотаций можно увидеть здесь.
Рассмотрим все паттерны более подробно.
Пример подозрительного кода при использовании в индексе недостоверных данных.
size_t index = 0;
....
if (scanf("%zu", &index) == 1)
{
....
DoSomething(arr[index]); // <=
}
Данный код может привести к доступу за границу массива 'arr' в случае, если пользователем будет введено значение отрицательное или превышающее максимально допустимый индекс массива 'arr'.
Безопасный код проверяет полученное значение:
if (index < ArraySize)
DoSomething(arr[index]);
Пример подозрительного кода при использовании недостоверных данных в аргументе функции.
char buf[1024];
char username [256];
....
if (scanf("%255s", username) == 1)
{
if (snprintf(buf, sizeof(buf) - 1, commandFormat, username) > 0)
{
int exitCode = system(buf); // <=
....
}
....
}
Этот код является уязвимым, т.к. пользовательский ввод передаётся командному интерпретатору без проверки полученных данных. Например, введя "&cmd", на Windows можно получить доступ к командному интерпретатору.
Правильный код должен осуществлять дополнительную проверку считанных данных:
if (IsValid(username))
{
if (snprintf(buf, sizeof(buf) - 1, commandFormat, username) > 0)
{
int exitCode = system(buf);
....
}
....
}
else
{
printf("Invalid username: %s", username);
....
}
Пример подозрительного кода, связанного с порчей указателя.
size_t offset = 0;
int *pArr = arr;
....
if (scanf("%zu", &offset) == 1)
{
pArr += offset; // <=
....
DoSomething(pArr);
}
В данном случае портится значение указателя 'pArr', т.к. в результате прибавления непроверенного значения 'offset' указатель может начать ссылаться за пределы массива. В результате можно испортить какие-то данные (на которые будет ссылаться 'pArr') с непредсказуемыми последствиями.
Правильный код проверяет допустимое смещение:
if (offset <= allowableOffset)
{
pArr += offset;
....
DoSomething(pArr);
}
Пример подозрительного кода с делением на недостоверные данные:
if (fscanf(stdin, "%zu", &denominator) == 1)
{
targetVal /= denominator;
}
Этот код может привести к делению на 0, если соответствующее значение будет введено пользователем.
Корректный код выполняет проверку допустимости значений:
if (fscanf(stdin, "%zu", &denominator) == 1)
{
if (denominator > MinDenominator && denominator < MaxDenominator)
{
targetVal /= denominator;
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1010. |
Анализатор обнаружил использование функции 'std::async', поведение которой может отличаться от того, которое было задумано программистом. 'std::async' принимает в качестве аргументов функцию, аргументы функции и, опционально, флаг, который влияет на политику вызова 'std::async'. Возвращаемым значением 'std::async' является 'std::future', значение которого будет выставлено по завершению функции.
Поведение 'std::async', зависит от переданных флагов следующим образом:
1) 'std::launch::async' - будет немедленно создан объект класса 'thread', с функцией и её аргументами в качестве аргументов нового потока. Т.е. 'std::async' инкапсулирует создание потока, получение 'std::future' и предоставляет однострочную запись для выполнения такого кода.
2) 'std::launch::deferred' - поведение функции поменяется, т.к. никакого асинхронного вызова не произойдёт. Вместо исполнения функции в новом потоке, она, вместе с аргументами, будет сохранена в 'std::future', чтобы быть вызванными позже. Это позже наступит тогда, когда кто-либо вызовет метод 'get' или 'wait' на 'future', которое вернул 'std::async'. При этот вызываемый объект выполнится в потоке, который вызывал 'get/wait'! Это поведение есть ни что иное, как отложенный вызов функции.
3) Флаг не выставлен (std::launch::async | std::launch::deferred ) - в этом случае будет выбрано одно из двух поведений описанных выше. Какое из двух? Неизвестно и зависит от имплементации.
Если используется функция 'std::async' без указания политики, то используется как раз третий случай. Чтобы избежать возможной неизвестности, анализатор выявляет такие случаи использования функции.
Future<int> foo = std::async(MyFunction, args...);
После такого вызова функции есть вероятность, что на разных компьютерах с разными реализациями библиотек возможны отличные поведения функции.
Советуем обратить на это внимание и разрешить потенциальную неопределенность, указав первым параметром конкретную реализацию поведения функции. Надёжный вариант явного задания политики выполнения:
Future<int> foo = std::async(launch::async, MyFunction, args...)
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил неправильный способ определения переполнения, которое может возникать при сложении переменных типов 'unsigned short' или переменных типов 'unsigned char'.
Пример некорректно написанного кода:
bool IsValidAddition(unsigned short x, unsigned short y)
{
if (x + y < x)
return false;
return true;
}
При сложении двух переменных типов 'unsigned short', обе переменные приводятся к типу 'int'. Результат сложения также будет иметь тип 'int'. Поэтому, независимо от того, какие значения будут в переменных 'x' и 'y', в результате их сложения переполнения никогда не возникнет. Далее выполняется операция сравнения. При этом правый операнд (переменная 'x') вновь расширяется до типа 'int'. В итоге, приведённый выше код эквивалентен этому:
bool IsValidAddition(unsigned short x, unsigned short y)
{
if ((int)(x) + (int)(y) < (int)(x))
return false;
return true;
}
Получается, что выражение "x + y < x" всегда будет ложно. Вероятнее всего, компилятор оптимизирует функцию, подставив в места ее вызова значение 'true'. Итог: функция ничего на самом деле не проверяет и не защищает от переполнения.
Примечание: Если вы используете модель данных, где размер типа 'short' и типа 'int' совпадают, то такая проверка будет работать корректно и анализатор не будет выдавать предупреждение.
Чтобы получить корректно работающий код, нужно явно выполнить приведение результата сложения к типу 'unsigned short':
if ((unsigned short)(x + y) < x)
{
...
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит опечатку. В цепочке однотипных сравнений членов класса имеется выражение не похожее на остальные тем, что в нем сравниваются члены с разными именами, в то время как остальные выражения в цепочке сравнивают одноименные члены.
Рассмотрим пример:
if (a.x == b.x && a.y == b.y && a.z == b.y)
В данном случае выражение 'a.z == b.y' отличается от остальных выражений в цепочке. Скорее всего, это выражение является ошибочным из-за опечатки при редактировании скопированного участка текста. Корректный код, который не вызовет подозрений у анализатора, будет выглядеть так:
if (a.x == b.x && a.y == b.y && a.z == b.z)
Анализатор выдает предупреждение в тех случаях, когда длина цепочки сравнений более двух звеньев.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1013. |
Анализатор обнаружил подозрительный способ сравнения двух структур, которые содержат в себе члены типа float или double.
Пример:
struct Object
{
int Length;
int Width;
int Height;
float Volume;
};
bool operator == (const Object &p1, const Object &p2)
{
return memcmp(&p1, &p2, sizeof(Object)) == 0;
}
Так как структура 'Object' содержит числа с плавающей точкой, то их сравнение через 'memcmp' может привести к неожиданному результату. Например, числа -0.0 и 0.0 эквивалентны, но имеют разное битовое представление. Два NaN имеют одинаковое представление, но они не эквивалентны. Возможно стоит использовать оператор == или сравнивать эти переменные с определённой точностью.
Допустим, мы хотим сравнивать члены классы с помощью оператора ==. В этом случае можно вообще удалить 'operator ==', так как компилятор сделает сам всё правильно, реализуя оператор сравнения по умолчанию. Однако, допустим мы хотим написать именно свою функцию, где будем сравнивать члены 'Volume' с определённой точностью. Тогда, исправленный вариант может выглядеть так:
bool operator == (const Object &p1, const Object &p2)
{
return p1.Length == p2.Length
&& p1.Width == p2.Width
&& p1.Height == p2.Height
&& fabs(p1.Volume - p2.Volume) <= FLT_EPSILON;
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительное выражение, в котором смешаны логические и битовые операции. Возможно, в одной из них допущена опечатка.
Пример:
void write(int s);
void write(unsigned char a, unsigned char b,
unsigned char c, unsigned char d)
{
write((a << 24) | (b << 16) || (c << 8) | d);
}
Это явная опечатка. В одном месте, вместо оператора '|' случайно воспользовались оператором '||'. Корректный код:
void write(unsigned char a, unsigned char b,
unsigned char c, unsigned char d)
{
write((a << 24) | (b << 16) | (c << 8) | d);
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил опасное приведение числа к перечислению. Указанное число может не входить в диапазон значений 'enum'.
Примечание 1: Данное правило актуально только для языка C++. В качестве нижележащего типа 'enum' в языке C всегда используется тип 'int'.
Примечание 2: Данное правило актуально только для C++ компиляторов, рассчитывающих фактический размер 'enum' согласно стандарту. Например, такими компиляторами являются GCC и Clang. MSVC не является таким компилятором, так как рассчитывает размер 'enum' в целях обратной совместимости по правилам языка C и в качестве нижележащего типа всегда использует тип 'int', если специально не указан иной тип.
Результат приведения числа, чьё значение не входит в диапазон элементов 'enum', является неуточненным поведением до C++17 и неопределенным поведением, начиная с C++17.
Если у 'enum' указан нижележащий тип, то к типу этого 'enum' можно приводить все значения, которые можно уместить в этот тип.
Пример 1:
enum class byte : unsigned char {}; // Range: [0; 255]
byte b1 = static_cast<byte>(255); // ok
Число 256 уже не вмещается в тип 'char', и этот код некорректен:
byte b2 = static_cast<byte>(256); // UB
Если не указан нижележащий тип, то, согласно стандарту, компилятор пытается уместить значения в зависимости от инициализатора в следующие типы:
int -> unsigned int -> long -> unsigned long ->
long long -> unsigned long long
При этом внутри выбранного типа компилятор использует минимально необходимое число бит (n), способное уместить максимальное число в перечислении. В такой 'enum' можно уместить диапазон значений [- (2^n) / 2; 2^n / 2 - 1] для 'enum' со знаковым нижележащим типом и [0; 2^n - 1] для 'enum' с беззнаковым нижележащим типом. Выход за границы этого диапазона является неуточненным поведением (до C++17) или неопределенным поведением (с C++17).
Пример 2:
enum foo { a = 0, b = UINT_MAX }; // Range: [0; UINT_MAX]
foo x = foo(-1); // UB
На первый взгляд, такой код является корректным, но на самом деле он может приводить к проблемам. Нижележащим типом 'enum' выбирается 'unsigned int'. Число '-1' не попадает в диапазон этого типа, поэтому такое присвоение может привести к неуточнённому или неопределённому поведению.
Пример 3:
enum EN { low = 2, high = 4 }; // Uses 3 bits, range: [0; 7]
EN a1 = static_cast<EN>(7); // ok
Согласно стандарту нижележащим типом для этого enum выберется 'int'. Внутри этого типа компилятор использует минимальное количество битовых полей, которое сможет вместить в себя все значения enum-констант.
В данном случае для вмещения всех значений (2 = 0b010 и 4 = 0b100) понадобится минимум 3 бита, поэтому переменная типа EN может вместить в себя числа от 0 (0b000) до 7 (0b111) включительно. Число 8 занимает уже четыре бита (0b1000), поэтому в тип EN оно уже не вмещается:
EN a2 = static_cast<EN>(8); // UB
UndefinedBehaviorSanitizer также находит ошибку в этом примере: https://godbolt.org/z/GGYo7z.
При этом, если для EN указать нижележащий тип, например 'unsigned char', то это уже будет корректный вариант кода:
enum EN : unsigned char { low = 2, high = 4 }; // Range: [0; 255]
EN a2 = static_cast<EN>(8); // ok
Данная диагностика классифицируется как:
|
Анализатор обнаружил инициализацию экземпляра класса 'std::string_view' временным объектом или присваивание экземпляру класса 'std::string_view' временного объекта.
Рассмотрим пример:
std::string hello = "Hello, ";
std::string_view helloWorldPtr = hello + "world\n";
std::cout << helloWorldPtr;
В данном случае, во второй строке будет создан временный объект типа 'std::string', указатель на который будет скопирован при инициализации экземпляра класса 'std::string_view'. Далее, после вычисления выражения инициализации, временный объект будет уничтожен и в третьей строке произойдет использование указателя на освобожденную память.
Правильный вариант кода:
std::string hello = "Hello, ";
const std::string helloWorld = hello + "world\n";
std::string_view helloWorldPtr = helloWorld;
std::cout << helloWorldPtr;
Данная диагностика классифицируется как:
Анализатор обнаружил ошибку при использовании обёртки над мьютексом (std::unique_lock и т.п.).
Рассмотрим пример:
std::unique_lock<std::mutex> lck;
lck.lock();
В данном случае, во второй строке будет брошено исключение 'std::system_error', так как с обёрткой не связан мьютекс. Необходимо передать его в конструкторе:
std::unique_lock<std::mutex> lck(m, std::defer_lock);
lck.lock();
или инициализировать методом 'swap()':
std::unique_lock<std::mutex> lck_global(mtx, std::defer_lock);
....
std::unique_lock<std::mutex> lck;
lck.swap(lck_global);
lck.lock();
Также диагностика ищет случаи, когда критическая секция объявлена, но блокировка мьютекса по какой-то причине не выполняется:
std::unique_lock<std::mutex> lck(m, std::defer_lock);
//lck.lock();
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил использование составного оператора присваивания внутри выражения, имеющего тип 'bool'.
Рассмотрим пример:
if (adj_number == (size_t)(roving->adj_count - 1) &&
(total_quantity += quantity_delta) < 0)
{
/* ... */
}
В данном случае переменная 'total_quantity' изменяется внутри блока условия оператора 'if'. Дополнительную подозрительность этому коду придает тот факт, что изменение переменной 'total_quantity' произойдет только в том случае, если будет выполнено условие слева от оператора '&&'. Возможно, имеет место опечатка, и вместо составного оператора присваивания '+=' предполагается оператор сложения '+':
if (adj_number == (size_t)(roving->adj_count - 1) &&
(total_quantity + quantity_delta) < 0)
{
/* ... */
};
Даже если первоначальный код был верен, всё равно крайне не рекомендуется писать такие сложные выражения. В логике подобного кода сложно разобраться и очень легко допустить ошибку, модифицируя подобный код.
Анализатор не может однозначно определить, содержит ли выявленный с помощью данной диагностики код настоящую ошибку, или программист перестарался, сокращая код. Мы изучили большое количество открытых проектов и выдели ряд паттернов программирования, когда используются рассмотренные конструкции кода и это не является ошибкой. С целью сокращения числа ложных срабатываний, предупреждение не выдаётся в следующих случаях:
Если в вашем проекте срабатываний слишком много, то можно отключить эту диагностику или воспользоваться механизмом подавления ложных срабатываний.
Данная диагностика классифицируется как:
|
Анализатор обнаружил в теле функции путь выполнения, начинающийся и заканчивающийся вызовом функций, имена которых содержат слова-антагонисты. При этом, между вызовами есть условие, которое приводит к возврату из анализируемой функции без вызова функции-эпилога.
Рассмотрим пример:
int pthread_cond_destroy(pthread_cond_t * cond)
{
EnterCriticalSection(&ptw32_cond_list_lock);
/* ... */
if (sem_wait (&(cv->semBlockLock)) != 0)
{
return errno; // <= V1020 Warning
}
/* ... */
LeaveCriticalSection(&ptw32_cond_list_lock);
return 0;
}
В данном случае, в начале и конце функции присутствуют вызовы 'EnterCriticalSection' и 'LeaveCriticalSection' с соответствующими словами - антагонистами 'Enter' и 'Leave'. Между вызовами есть возврат из функции, перед которым по ошибке пропущен вызов 'LeaveCriticalSection'. Корректный код будет выглядеть следующим образом:
int pthread_cond_destroy(pthread_cond_t * cond)
{
EnterCriticalSection(&ptw32_cond_list_lock);
/* ... */
if (sem_wait (&(cv->semBlockLock)) != 0)
{
LeaveCriticalSection(&ptw32_cond_list_lock);
return errno;
}
/* ... */
LeaveCriticalSection(&ptw32_cond_list_lock);
return 0;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1020. |
Анализатор обнаружил подозрительное присвоение в цикле, который может привести к бесконечному циклу.
Рассмотрим пример:
static void f(Node *n)
{
for (Node *it = n; it != nullptr; it = n->next)
....
}
Это типичный код для обхода списков. В случае, когда 'n' не модифицируется,
этот цикл либо не выполнится ни разу, либо будет выполняться бесконечно.
Корректный код:
static void f(Node *n)
{
for (Node *it = n; it != nullptr; it = it->next)
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил исключение, брошенное по указателю. Чаще всего принято бросать исключения по значению, а перехватывать по ссылке. Бросание указателя может привести к тому, что исключение не будет поймано, так как перехватывать его будут по ссылке. Также использование указателя вынуждает перехватывающую сторону вызвать оператор 'delete' для уничтожения созданного объекта, чтобы не возникали утечки памяти.
Пример:
throw new std::runtime_error("error");
Корректный код:
throw std::runtime_error("error");
Конечно, само по себе бросание исключения по указателю не является ошибкой. Такое исключение можно правильно перехватить и обработать. Однако на практике это неудобно и провоцирует ошибки. Недостатки использования указателя:
Таким образом, бросание исключения по указателю можно считать ошибочным паттерном и такой код следует переписать.
Дополнительные ссылки:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1022. |
Анализатор обнаружил код добавления в контейнер умных указателей с помощью 'emplace_back(new X)'. Этот код может привести к утечке памяти.
Пример:
std::vector<std::unique_ptr<int>> pointers;
pointers.emplace_back(new int(42));
В случае, если вектору понадобится реаллокация и он не сможет выделить память под новый массив, то он бросит исключение и указатель будет потерян.
Корректный код:
pointers.push_back(std::unique_ptr<int>(new int(42)));
pointers.push_back(std::make_unique<int>(42));
Давайте разберёмся с этим видом ошибки более подробно.
Для добавлении элемента в конец контейнера типа 'std::vector<std::unique_ptr<X>>' нельзя просто написать 'v.push_back(new X)', так как нет неявного преобразования из 'X*' в 'std::unique_ptr<X>'.
Распространенным решением является написание 'v.emplace_back(new X)', так как он компилируется: метод 'emplace_back' конструирует элемент непосредственно из аргументов и поэтому может использовать явные конструкторы.
Тем не менее, это не безопасно. Если вектор полон, то происходит перевыделение памяти. Операция перевыделения памяти может закончиться неудачей, в результате чего будет сгенерировано исключение 'std::bad_alloc'. В этом случае указатель будет потерян, и созданный объект никогда не будет удален.
Безопасным решением является создание 'unique_ptr', который будет владеть указателем до того, как вектор попытается перевыделить память:
v.push_back(std::unique_ptr<X>(new X))
Начиная с C++14 можно использовать 'std::make_unique':
v.push_back(std::make_unique<X>())
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1023. |
Анализатор обнаружил возможное использование некорректных данных при их чтении.
Пример неправильного кода:
while (!in.eof()) {
in >> x;
foo(x);
}
В случае, если операция чтения закончится неудачей, переменная 'x' будет содержать некорректные данные. При этом функция 'foo' всё равно будет вызвана. Необходимо либо добавить ещё одну проверку перед использованием переменной 'x', либо переписать цикл так, как показано дальше.
Корректный код:
while (in >> x) {
foo(x);
}
Дополнительные ссылки:
Данная диагностика классифицируется как:
|
Анализатор обнаружил неправильное использование класса 'std::unique_lock', что может приводить к состоянию гонки.
Пример кода с ошибкой:
class C {
std::mutex m_mutex;
void foo() {
std::unique_lock <std::mutex>(m_mutex);
}
};
В данном примере создаётся новая переменная с именем 'm_mutex', инициализированная по умолчанию. Соответственно, мьютекс захвачен не будет.
Корректный код:
void foo() {
std::unique_lock <std::mutex> var(m_mutex);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил возможное переполнение знаковой переменной в цикле. Переполнение знаковых переменных приводит к неопределённому поведению.
Пример:
int checksum = 0;
for (....) {
checksum += ....;
}
Перед нами абстрактный алгоритм подсчёта контрольной суммы. Алгоритм подразумевает возможность переполнения переменной 'checksum'. Однако переменная имеет знаковый тип, а, следовательно, её переполнение приводит к неопределённому поведению. Код некорректен и должен быть модифицирован.
Следует использовать беззнаковые типы, семантика переполнения которых определена.
Корректный код:
unsigned checksum = 0;
for (....) {
checksum += ...
}
Некоторые программисты считают, что в переполнении знаковых переменных ничего страшного нет и они могут предсказать, как программа будет работать. Это не так. Может происходить всё что угодно.
Давайте рассмотрим, как могут проявляться ошибки этого типа на практике. Разработчик на форуме жалуется, что GCC глючит и неправильно компилирует его код в режиме оптимизации. Он приводит код следующей функции для подсчёта хеша строки:
int foo(const unsigned char *s)
{
int r = 0;
while(*s) {
r += ((r * 20891 + *s *200) | *s ^ 4 | *s ^ 3) ^ (r >> 1);
s++;
}
return r & 0x7fffffff;
}
Претензия разработчика в том, что компилятор не генерирует код для оператора побитового И (&). Из-за этого функция возвращает отрицательные значения, хотя не должна.
Разработчик считает, что это глюк в компиляторе. Но это не так, неправ программист, который написал такой код. Функция работает неправильно из-за того, что в ней возникает неопределённое поведение.
Компилятор видит, что в переменной 'r' считается некоторая сумма. Согласно стандартам языков C и C++, переполнения знаковой переменной 'r' произойти не может. Иначе в программе содержится неопределённое поведение, которое компилятор никак не должен рассматривать и учитывать.
Итак, компилятор считает, что раз переменная 'r' после окончания цикла не переполняется, то она не сможет стать отрицательной. Следовательно, операция 'r & 0x7fffffff' для сброса знакового бита является лишней, и компилятор её удаляет. Он просто возвращает из функции значение переменной 'r'.
Диагностика V1026 как раз и предназначена для выявления подобных ошибок. Чтобы исправить код, достаточно считать хеш, используя для этого беззнаковую переменную.
Исправленный вариант кода:
int foo(const unsigned char *s)
{
unsigned r = 0;
while(*s) {
r += ((r * 20891 + *s *200) | *s ^ 4 | *s ^ 3) ^ (r >> 1);
s++;
}
return (int)(r & 0x7fffffff);
}
Дополнительные ссылки:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительное приведение типов. Указатель на класс приводится к указателю на другой класс, при этом классы никак не связаны между собой наследованием.
Подозрительный код:
struct A {};
struct B {};
struct C : B {};
void f(A *a, B *b) {
C *c = (C*)a;
}
Возможно, нужно было привести другую переменную. Исправленный код:
void f(A *a, B *b) {
C *c = (C*)b;
}
Если же такое поведение является ожидаемым, то следует использовать 'reinterpret_cast':
void f(A *a, B *b) {
C *c = reinterpret_cast<C*>(a);
}
В этом случае анализатор не будет выдавать предупреждение.
Примечание. В некоторых проектах, особенно выполняющих низкоуровневые операции, можно встретить очень много приведений между различными структурами, не связанных между собой наследованием, но связанных логически. Другими словами, такие приведения типов используются сознательно и необходимы. В этом случае предупреждения анализатора будут только создавать ненужный шум и будет рационально просто отключить диагностику V1027.
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительное приведение типов. Результат бинарной операции над 32-битными числами приводят к 64-битному типу.
Пример кода с ошибкой:
unsigned a;
unsigned b;
....
uint64_t c = (uint64_t)(a * b);
Такое преобразование избыточно. Тип 'unsigned' и так бы автоматически расширился до типа 'uint64_t' при присваивании.
Скорее всего, программист хотел защититься от переполнения, но не достиг цели. При перемножении переменных типа 'unsigned' всё равно произойдёт переполнение, и только уже бессмысленный результат умножения будет явно расширен до типа 'uint64_t'.
Следовало привести один из аргументов к этому типу, чтобы избежать переполнения. Корректный код:
uint64_t c = (uint64_t)a * b;
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1028. |
Анализатор обнаружил, что длину контейнера или строки кладут в 16-битную или 8-битную переменную. Это опасно, так как даже на не очень больших данных может произойти ошибка из-за того, что размер не помещается в заданную переменную.
Некорректный код:
std::string str;
....
short len = str.length();
Необходимо использовать тип 'size_t', который точно вместит в себя размер любой строки/контейнера:
size_t len = str.length();
Или, если быть более точными и педантичными, то тип 'std::string::size_type':
std::string::size_type len = str.length();
Или можно воспользоваться ключевым словом auto:
auto len = str.length();
Рассмотренная ошибка может показаться безобидной. Программист может исходить из предположения, что строка просто не может быть длинной при любых адекватных сценариях работы программы. Но программист не учитывает, что подобные усечения данных могут сознательно использоваться как уязвимости. Т.е. злоумышленник может неким образом подать некорректные входные данные, чтобы получить очень длинные строки. Некорректная обработка очень длинных строк может открыть ему возможность как-то влиять на поведение программы. Другими словами, данная ошибка может представлять собой потенциальную уязвимость и должна быть обязательно исправлена.
Некоторые пользователи говорят, что анализатор неправ, выдавая предупреждение на следующий код:
size = static_cast<unsigned short>(array->size());
Они считают, что раз есть 'static_cast', то всё хорошо и программист знает, что делает. Однако возможно, что с помощью приведения типа кто-то боролся с предупреждением компилятора. Таким образом, 'static_cast' только маскирует, а не устраняет проблему.
Если в проекте много кода, где используются подобные приведения типа, и вы доверяете этому коду, то можно просто отключить диагностику 1029. Другой вариант - это отключить предупреждения именно там, где используется 'static_cast'. Для этого вы можете написать в одном из глобальных заголовочных файлов или файле конфигурации диагностик (.pvsconfig) следующий комментарий:
//-V:static_cast:1029
Примечание. Диагностика V1029 не ищет случаи, когда размер кладут в 32-битную переменную. Поиском подобных ситуаций занимаются диагностики для выявления паттернов 64-битных ошибок. См. в документации набор диагностик "Diagnosis of 64-bit errors (Viva64, C++)".
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что переменная используется после того, как она была перемещена.
std::string s1;
std::string s2;
....
s2 = std::move(s1);
return s1.size();
После перемещения переменная существует в неуточнённом, но валидном для вызова деструктора состоянии. Полагаться на такое состояние объектов в своих программах опасно. Исключением является ряд классов, например, 'std::unique_ptr', поведение которых указано.
Данная диагностика классифицируется как:
|
Анализатор обнаружил в файле использование функции без её предварительного объявления. В языке Си это допустимо, но такая возможность опасна, т.к. может стать причиной некорректной работы программы.
Рассмотрим простой пример.
char *CharMallocFoo(int length)
{
return (char*)malloc(length);
}
Поскольку заголовочный файл <stdlib.h> не подключен, компилятор языка Си посчитает, что функция 'malloc' вернет тип 'int'. Неверная интерпретация возвращаемого значения компилятором может привести к проблемам во время выполнения программы, в том числе и к аварийному завершению.
Если программа 64-битная, то, скорее всего, будут потеряны старшие 32-бита возвращаемого адреса. Поэтому программа некоторое время будет работать корректно. Однако, когда свободная память в 4-х младших гигабайтах адресного пространства закончится или будет сильно фрагментирована, то будет выделен буфер за пределами 4-х младших гигабайт. Так как будут потеряны старшие биты указателя, то последствия будут крайне неприятны и непредсказуемы. Подробнее эта ситуация рассматривается в статье "Красивая 64-битная ошибка на языке Си".
Исправленный вариант кода:
#include <stdlib.h>
....
char *CharMallocFoo(int length)
{
return (char*)malloc(length);
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил преобразование указателя из одного типа в другой, которое приводит к неопределённому поведению. Разные типы объектов могут быть по-разному выравнены, и после приведения типа указателя выравнивание может быть изменено на некорректное. Если неправильный указатель будет разыменован, то программа может аварийно завершиться. Также операции с таким указателем могут приводить к потере информации.
Рассмотрим пример.
void foo(void) {
char ch = '1';
int *int_ptr = (int *)&ch;
char *char_ptr = (char *)int_ptr;
}
При взятии адреса у переменой 'ch' и записи его в указатель типа 'int' возможна потеря данных. При обратном преобразовании выравнивание может быть изменено.
Обезопасить себя можно, например, используя одинаковые типы для операций:
void func(void) {
char ch = '1';
int i = ch;
int *int_ptr = &i;
}
или вручную задав выравнивание:
#include <stdalign.h>
void func(void) {
alignas(int) char ch = '1';
int *int_ptr = (int *)&ch;
char * char_ptr = (char *)int_ptr;
}
Рассмотрим другой случай, который можно встретить в коде приложений. На стеке выделен буфер байт, и его хотят использовать для хранения структуры. Такое бывает при работе с такими структурами, как BITMAPINFO. Вот как устроена эта структура:
typedef struct tagBITMAPINFOHEADER {
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;
....
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO, *LPBITMAPINFO, *PBITMAPINFO;
Как видите, структура содержит переменные типа DWORD, LONG и т.д., которые должны быть правильно выравнены. Также, 'bmiColors' на самом деле это массив вовсе не из одного элемента. Элементов будет столько, сколько нужно, именно поэтому для создания этой структуры может использоваться массив байт. В результате, в приложениях можно встретить вот такой опасный код:
void foo()
{
BYTE buffer[sizeof(BITMAPINFOHEADER) + 3 * sizeof(RGBQUAD)] = {0};
BITMAPINFO *pBMI = (BITMAPINFO*)buffer;
....
}
Скорее всего, буфер на стеке будет выравнен по границе 8 байт, и этот код будет работать. Однако этот код очень ненадёжен! Достаточно добавить в начало функции одну переменную, и можно всё сломать.
Неправильный код:
void foo()
{
char x;
BYTE buffer[sizeof(BITMAPINFOHEADER) + 3 * sizeof(RGBQUAD)] = {0};
BITMAPINFO *pBMI = (BITMAPINFO*)buffer;
....
}
Теперь велик шанс, что 'buffer' будет начинаться с невыравненного адреса. Размер переменной 'x' и размер элементов массива 'buffer' равны. Следовательно, буфер можно расположить в стеке сразу после переменной 'x' без какого-либо отступа (выравнивания).
Конечно, всё зависит от компилятора. И программисту может вновь повезти, и код будет работать хорошо. Однако надеемся, что нам удалось объяснить, почему лучше так не делать.
Решить проблему можно, создавая массив в динамической памяти. Выделенный блок памяти всегда будет выравнен под любой тип.
Корректный код:
void foo()
{
char x;
BITMAPINFO *pBMI = (BITMAPINFO *)
calloc(sizeof(BITMAPINFOHEADER) + 3 * sizeof(RGBQUAD),
sizeof(BYTE));
....
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1032. |
Анализатор обнаружил переменную, объявленную с помощью ключевого слова 'auto' в языке Си. Ключевое слово 'auto' может запутать человека, привыкшего работать с C++11 и более старшими версиями языка C++. Вместо выведения типа компилятор будет интерпретировать ключевое слово 'auto' как тип 'int'.
Это может привести к неожиданным результатам для программиста, например, в арифметических операциях. Рассмотрим простой пример:
float d = 3.14f;
int i = 1;
auto sum = d + i;
Значение переменной 'sum' будет равно '4', а не '4.14', как мог ожидать разработчик. В программах на языке Си необходимо явно объявить тип переменной:
float d = 3.14f;
int i = 1;
float sum = d + i;
Первый пример полностью корректен для языка Си++. Эта диагностика поможет не ошибиться в проекте, который разрабатывается с использованием двух языков.
Анализатор обнаружил переменную вещественного типа в качестве счётчика цикла 'for'. Поскольку числа с плавающей точкой не могут точно отобразить все действительные числа, использование таких переменных в цикле может дать непостоянное количество итераций.
Рассмотрим пример:
void foo(void) {
for (float A = 0.1f; A <= 10.0f; A += 0.1f) {
....
}
}
Количество итераций в этом цикле может быть 99 или 100. Точность операций с вещественными числами может зависеть от компилятора, режима оптимизации и многого другого.
Лучше переписать цикл следующим образом:
void foo(void) {
for (int count = 1; count <= 100; ++count) {
float A = count / 10.0f;
}
}
Этот цикл выполнит ровно 100 итераций, а переменную 'A' можно использовать для необходимых вычислений.
Данная диагностика классифицируется как:
|
Согласно стандарту языка Си, вызов функции 'fsetpos' с аргументом, полученным не из функции 'fgetpos', приводит к неопределённому поведению (UB, Undefined behavior).
Рассмотрим пример:
int foo()
{
FILE * pFile;
fpos_t position;
pFile = fopen("file.txt", "w");
memset(&position, 0, sizeof(position));
fputs("That is a sample", pFile);
fsetpos(pFile, &position);
fputs("This", pFile);
fclose(pFile);
return 0;
}
Значение переменной 'position' не было получено функцией 'fgetpos', что может привести к сбою программы.
Исправленный вариант:
int foo()
{
FILE * pFile;
fpos_t position;
pFile = fopen("file.txt", "w");
fgetpos(pFile, &position);
fputs("That is a sample", pFile);
fsetpos(pFile, &position);
fputs("This", pFile);
fclose(pFile);
return 0;
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с небезопасным использованием паттерна "блокировки с двойной проверкой" (double checked locking). Блокировка с двойной проверкой - это паттерн, предназначенный для уменьшения накладных расходов получения блокировки. Сначала проверяется условие блокировки без синхронизации. И только если условие выполняется, поток попытается получить блокировку. Таким образом, блокировка будет выполнена только если она действительно была необходима.
Рассмотрим пример с ошибкой:
static std::mutex mtx;
class TestClass
{
public:
void Initialize()
{
if (!initialized)
{
std::lock_guard lock(mtx);
if (!initialized) // <=
{
resource = new SomeType();
initialized = true;
}
}
}
/* .... */
private:
bool initialized = false;
SomeType *resource = nullptr;
};
}
В этом примере, оптимизация компилятором порядка назначений переменных 'resource' и 'initialized' может привести к ошибке. Т.е. в начале переменной 'initialized' будет присвоено значение 'true', а уже потом будет выделена память под объект типа 'SomeType' и проинициализирована переменная 'resource'.
Такая перестановка может привести к ошибке при доступе к объекту из параллельного потока. Получается, что переменная 'resource' будет еще не проинициализирована, а флаг 'intialized' уже будет выставлен в 'true'.
Одна из опасностей таких ошибок состоит в том, что часто кажется, будто программа работает корректно. Это происходит из-за того, что рассмотренная ситуация будет возникать не очень часто, в зависимости от архитектуры используемого процессора.
Дополнительные ссылки:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1036. |
Анализатор обнаружил ситуацию, когда в операторе switch разные метки case содержат одинаковые фрагменты кода. Часто это свидетельствует об избыточном коде, который можно улучшить объединением меток. Но нередко одинаковые фрагменты кода могут быть причиной copy-paste программирования и являться настоящими ошибками.
Рассмотрим пример с избыточным кодом:
switch (wParam)
{
case WM_MOUSEMOVE:
::PostMessage(hWndServer, wParam, 0, 0);
break;
case WM_NCMOUSEMOVE:
::PostMessage(hWndServer, wParam, 0, 0);
break;
....
default:
break
}
Действия для нескольких событий мыши действительно могут быть одинаковыми, поэтому код можно написать более компактно:
switch (wParam)
{
case WM_MOUSEMOVE:
case WM_NCMOUSEMOVE:
::PostMessage(hWndServer, wParam, 0, 0);
break;
....
default:
break
}
Рассмотрим пример из реального приложения, где разработчик допустил ошибку из-за опечатки:
GLOBAL(void)
jpeg_default_colorspace (j_compress_ptr cinfo)
{
switch (cinfo->in_color_space) {
case JCS_GRAYSCALE:
jpeg_set_colorspace(cinfo, JCS_GRAYSCALE);
break;
case JCS_RGB:
jpeg_set_colorspace(cinfo, JCS_YCbCr);
break;
case JCS_YCbCr:
jpeg_set_colorspace(cinfo, JCS_YCbCr);
break;
....
}
....
}
В коде метки JCS_RGB допущена опечатка. Следовало передавать в функцию значение JCS_RGB, а не JCS_YCbCr.
Исправленный код:
GLOBAL(void)
jpeg_default_colorspace (j_compress_ptr cinfo)
{
switch (cinfo->in_color_space) {
case JCS_GRAYSCALE:
jpeg_set_colorspace(cinfo, JCS_GRAYSCALE);
break;
case JCS_RGB:
jpeg_set_colorspace(cinfo, JCS_RGB);
break;
case JCS_YCbCr:
jpeg_set_colorspace(cinfo, JCS_YCbCr);
break;
....
}
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1037. |
Анализатор обнаружил ситуацию, когда к символьному литералу прибавляется указатель. Скорее всего, это ошибка.
Например, к строке, имеющей тип 'const char*', программист хочет дописать символ:
const char* Foo()
{
return "Hello world!\n";
}
int main()
{
const char* bar = 'g' + Foo();
printf("%s", bar);
return 0;
}
В этом случае произойдет прибавление к указателю числового значения символа 'g', в результате чего возможен выход за границы строки. Для исправления этой ошибки следует использовать, по возможности, специальный класс 'std::string' или производить операции с памятью:
const char* Foo()
{
return "Hello world!\n";
}
int main()
{
std::string bar = 'g' + std::string(Foo());
printf("%s", bar.c_str());
return 0;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Анализатор обнаружил код, в котором multicharacter-литерал содержит одновременно символы и escape-коды.
Multicharacter-литерал является implementation-defined, поэтому различные компиляторы могут кодировать эти литералы по-разному. К примеру, GCC и Clang задают значение основываясь на порядке символов в литерале, тогда как MSVC перемещает их в зависимости от типа символа (обычный или escape).
Рассмотрим пример. Код ниже, скомпилированный различными компиляторами, будет вести себя по-разному:
#include <stdio.h>
void foo(int c)
{
if (c == 'T\x65s\x74') // <= V1039
{
printf("Compiled with GCC or Clang.\n");
}
else
{
printf("It's another compiler (for example, MSVC).\n");
}
}
int main(int argc, char** argv)
{
foo('Test');
return 0;
}
Программа, скомпилированная разными компиляторами, может напечатать разные сообщения на экран.
Для проекта, использующего определенный компилятор, это не будет заметно, однако при портировании могут возникнуть проблемы, поэтому следует заменить такие литералы простыми числовыми константами, к примеру, 'Test' поменять на '0x54657374'.
Чтобы продемонстрировать разницу между компиляторами, можно взять последовательности из 3-х и 4-х символов, например, 'GHIJ' и 'GHI', и вывести на экран их представление в памяти после компиляции.
Вывод утилиты, скомпилированной Visual C++:
Hex codes are: G(47) H(48) I(49) J(4A)
'GHIJ' : JIHG
'\x47\x48\x49\x4A' : GHIJ
'G\x48\x49\x4A' : HGIJ
'GH\x49\x4A' : JIHG
'G\x48I\x4A' : JIHG
'GHI\x4A' : JIHG
'GHI' : IHG
'\x47\x48\x49' : GHI
'GH\x49' : IHG
'\x47H\x49' : HGI
'\x47HI' : IHG
Вывод утилиты, скомпилированной GCC или Clang:
Hex codes are: G(47) H(48) I(49) J(4A)
'GHIJ' : JIHG
'\x47\x48\x49\x4A' : JIHG
'G\x48\x49\x4A' : JIHG
'GH\x49\x4A' : JIHG
'G\x48I\x4A' : JIHG
'GHI\x4A' : JIHG
'GHI' : IHG
'\x47\x48\x49' : IHG
'GH\x49' : IHG
'\x47H\x49' : IHG
'\x47HI' : IHG
Анализатор обнаружил возможную опечатку в записи предопределенного макроса.
Пример:
#if defined (__linux__) || defined (__APPLE_)
Это выражение без ошибки выглядит следующим образом:
#if defined (__linux__) || defined (__APPLE__)
Ещё один пример возможной опечатки:
#ifdef __WLN32__
Исправленный вариант:
#ifdef __WIN32__
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1040. |
Диагностика выявляет случаи инициализации членов класса "плохим" указателем или ссылкой.
Пример:
class Foo {
int *x;
int &y;
Foo(int a, int b);
};
Foo::Foo(int a, int b) :
x(&a), // <=
y(b) // <=
{};
Переменная 'x' является указателем, при инициализации ей присваивается адрес переменной 'a'. Переменная 'a' - аргумент и является локальной для конструктора, поэтому область ее видимости короче, чем у 'x'. После выхода из конструктора 'x' будет иметь неверный указатель.
То же самое верно и для ссылки 'y'.
Правильный вариант будет выглядеть таким образом:
class Foo {
int *x;
int &y;
Foo(int *a, int &b) :
x(a),
y(b)
{}
};
Foo::Foo(int *a, int &b) :
x(a),
y(b),
{}
};
Теперь конструктору передаются адрес и ссылка напрямую, их область видимости все так же ограничена областью видимости конструктора, но область видимости их значений - нет.
Данная диагностика классифицируется как:
|
Анализатор обнаружил в файле copyleft лицензию, которая обязывает открыть остальной исходный код. Это может быть неприемлемо для многих коммерческих проектов.
Если вы разрабатываете открытый проект, то можно просто игнорировать это предупреждение и отключить его.
Пример комментария, на который анализатор выдаст предупреждение:
/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Если в закрытый проект добавить файл с такой лицензией (GPL3 в данном случае), то остальной исходный код необходимо будет открыть, из-за особенностей данной лицензии.
Такой тип copyleft лицензий называют "вирусными" лицензиями, из-за их свойства распространяться на остальные файлы проекта. Проблема в том, что использование хотя бы одного файла с подобной лицензией в закрытом проекте автоматически делает весь исходный код открытым и обязывает распространять его вместе с бинарными файлами.
Диагностика занимается поиском следующих "вирусных" лицензий:
Есть следующие варианты, как вы можете поступить, обнаружив в закрытым проекте использование файлов с copyleft лицензий:
Мы понимаем, что данная диагностика неуместна для открытых проектов. Команда PVS-Studio способствует развитию открытых проектов, помогая исправлять в них ошибки и предоставляя бесплатные варианты лицензий. Однако наш продукт является B2B решением, и поэтому данная диагностика по умолчанию включена.
Если же ваш код распространяется под одной из указанных выше copyleft лицензий, то вы можете отключить данную диагностику следующими способами:
Если вам известны ещё типы "вирусных" лицензий, которые в данный момент не выявляет инструмент, то вы можете сообщить нам о них через форму обратной связи. И мы добавим их выявление в следующем релизе.
Данная диагностика классифицируется как:
Анализатор обнаружил в объявление константного экземпляра класса в заголовочном файле. При включении такого файла через 'include' произойдет создание множественных копий объекта. Если в классе есть конструктор, его код выполнится при каждом включении заголовочного файла, что может привести к нежелательным побочным эффектам.
Пример:
//some_header.h
class MyClass
{
int field1;
int field2;
MyClass (int a, int b)
{
// ....
}
};
// ....
const MyClass object{1, 2}; // <=
Диагностика не распространяется на классы и структуры, у которых не определены конструкторы. Следующий код не приведет к срабатыванию:
//some_header.h
struct MyStruct
{
int field1;
int field2;
};
// ....
const MyStruct object{1, 2};
Также, чтобы избежать ошибки, можно объявить переменную как 'inline' (начиная с C++17) или 'extern'. В этом случае инициализация и вызов конструктора произойдет один раз.
Корректный пример:
//some_header.h
class MyClass
{
// ....
};
// ....
inline const MyClass object{1, 2};
Примечание: использование ключевого слова 'constexpr' вместо 'const' при объявлении переменной не изменяет данное поведение. Согласно стандарту С++17, только constexpr-функции и constexpr-статические поля классов/структур объявляются со спецификатором inline неявно.
Более подробно данная проблема рассмотрена в статье "What Every C++ Developer Should Know to (Correctly) Define Global Constants".
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1043. |
Анализатор обнаружил цикл, условия выхода которого не зависят от количества итераций. Такой цикл может выполнится 0, 1, либо бесконечное количество раз.
Пример такого цикла:
void sq_settop(HSQUIRRELVM v, SQInteger newtop)
{
SQInteger top = sq_gettop(v);
if(top > newtop)
sq_pop(v, top - newtop);
else
while(top < newtop) sq_pushnull(v); // <=
}
Ошибка здесь в цикле while - переменные, которые находятся в условии, никак не меняют своих значений, поэтому цикл никогда не завершится, либо никогда не запустится (если их значения равны).
Цикл может выполнятся всегда только один раз, если его первая итерация меняет условие:
while (buf != nullptr && buf != ntObj)
{
ntObj = buf;
}
Если такое поведение является корректным, то цикл лучше заменить на if:
if (buf != nullptr && buf != ntObj)
{
ntObj = buf;
}
Еще один пример:
#define PEEK_WORD(ptr) *((uint16*)(ptr))
....
for(;;)
{
// Verify the packet size
if (dwPacketSize >= 2)
{
dwMsgLen = PEEK_WORD(pbytePacket);
if ((dwMsgLen + 2) == dwPacketSize)
break;
}
throw CString(_T("invalid message packet"));
}
Любой путь этого цикла ведет к выходу из него, а переменные, используемые в цикле, не меняются. Такой цикл, скорее всего, содержит ошибку, либо его просто стоит убрать.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1044. |
Анализатор обнаружил код, выбрасывающий и не перехватывающий исключение в теле функции DllMain.
При загрузке динамической библиотеки в процесс эта функция принимает значение DLL_PROCESS_ATTACH в качестве аргумента 'fwdReason'. Если DllMain завершается с ошибкой, она обязана вернуть значение FALSE. В этом случае загрузчик вызывает эту функцию снова со значением DLL_PROCESS_DETACH в 'fwdReason', что приводит к выгрузке DLL. Если функция DllMain завершается в результате исключения, выгрузка библиотеки не происходит.
Пример:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
....
throw 42;
....
}
Ошибку следует обработать в блоке try...catch и корректно вернуть FALSE.
Корректный пример:
BOOL __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
try
{
....
throw 42;
....
}
catch(...)
{
return FALSE;
}
}
Также исключения могут возникать при вызове оператора 'new'. При невозможности выделения памяти будет сгенерировано исключение 'bad_alloc'. Пример:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
....
int *localPointer = new int[MAX_SIZE];
....
}
Появление исключения возможно при использовании dynamic_cast<Type> при работе со ссылками. При невозможности приведения типов будет сгенерировано исключение 'bad_cast'. Пример:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
....
UserType &type = dynamic_cast<UserType&>(baseType);
....
}
Для исправления данных ошибок следует переписать код таким образом, чтобы 'new' или 'dynamic_cast' были помещены в блок try...catch.
Анализатор также обнаруживает в DllMain вызовы функций, которые потенциально могут выбросить исключение. Пример:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
....
potentiallyThrows();
....
}
Если анализатор не нашел в коде вызываемой функции операций, способных выбросить исключение, предупреждение на ее вызов выдано не будет.
Аналогично предыдущим примерам, вызовы функций, потенциально бросающих исключения, следует обернуть в блок try...catch.
Данная диагностика классифицируется как:
Анализатор обнаружил небезопасное использование типа 'bool' и целочисленного типа вместе в операции побитового И. Проблема в том, что побитовое И вернет 'false' для четных чисел, так как младший разряд всегда равен нулю. Если же привести вручную численный тип к 'bool', то всё будет в порядке.
Рассмотрим синтетический пример:
int foo(bool a)
{
return a ? 0 : 2;
}
....
bool var = ....;
var &= foo(false);
Независимо от начального значения переменной 'var', после выполнения операции '&=' эта переменная будет иметь значение 'false'. Допустим, функция 'foo' вернёт значение 2. Тогда операция 'var & 2' всегда даёт в результате 0, так как возможные значения 'var' это только 0 или 1.
Этот код следует исправить так:
var &= foo(false) != 0;
или же можно изменить и возвращаемое функцией значение:
int foo(bool a)
{
return a ? 0 : 1;
}
Если, например, функция будет возвращать только значения из диапазона [0;1], то всё в порядке, так как мы можем привести их без потерь к bool.
Пример ошибки, найденной с помощью этой диагностики в реальном проекте:
template<class FuncIterator>
bool SetFunctionList( FuncIterator begin, FuncIterator end) {
bool ret = true;
for (FuncIterator itr = begin; itr != end; ++itr) {
const ROOT::Math::IMultiGenFunction *f = *itr;
ret &= AddFunction(*f);
}
return ret;
}
int AddFunction(const ROOT::Math::IMultiGenFunction & func) {
ROOT::Math::IMultiGenFunction *f = func.Clone();
if (!f) return 0;
fFunctions.push_back(f);
return fFunctions.size();
}
Предполагалось, что функция 'SetFunctionList' проверяет валидность переданных ей итераторов и возвращает 'false', если хоть один из них невалидный, иначе 'true'. Но программист допустил ошибку при работе с оператором '&='. Правый аргумент – функция, которая возвращает целое число в диапазоне от 0 до SIZE_MAX. При возвращении чётных чисел из функции 'AddFunction', переменная 'ret' будет обнуляться, хотя должна это делать только для невалидных итераторов.
Исправленная функция 'SetFunctionList', где результат функции 'AddFunction' предварительно приводится к типу 'bool':
template<class FuncIterator>
bool SetFunctionList( FuncIterator begin, FuncIterator end) {
bool ret = true;
for (FuncIterator itr = begin; itr != end; ++itr) {
const ROOT::Math::IMultiGenFunction *f = *itr;
ret &= (bool)AddFunction(*f);
}
return ret;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1046. |
Анализатор обнаружил подозрительный захват переменной в лямбда функции.
Рассмотрим варианты срабатывания диагностики:
Пример 1:
function lambda;
{
auto obj = dummy<int>{ 42 };
lambda = [&obj]() { .... };
}
Переменную, которая будет уничтожена при выходе из блока, захватывают по ссылке. Время жизни лямбда функции при этом превышает время жизни объекта. Это приведет к использованию ссылки на уничтоженный объект при вызове лямбда функции.
Вероятно, объект нужно захватить по значению:
function lambda;
{
auto obj = dummy<int>{ 42 };
lambda = [obj]() { .... };
}
Пример 2:
function lambda;
{
auto obj1 = dummy<int>{ 42 };
auto obj2 = dummy<int>{ 42 };
lambda = [&]() { .... };
}
В этом случае диагностика определит, что обе переменные захвачены по ссылке и выдаст предупреждение на каждую из них.
Также возможно, что функция возвращает лямбду, захватившую локальную переменную по ссылке.
Пример 3:
auto obj = dummy<int>{ 42 };
return [&obj]() { .... };
В этом случае вызывающий код получит из функции лямбду, вызов которой приведет к использованию невалидной ссылки.
Данная диагностика классифицируется как:
|
Анализатор обнаружил присваивание переменной значения, которое уже содержится в ней. Скорее всего, это логическая ошибка.
Рассмотрим пример:
int i = foo();
if (i == 0)
{
i = 0; // <=
}
Отмеченное присваивание не изменяет значение переменной, и код, скорее всего, содержит ошибку.
Анализатор также может находить случаи, когда точные значения переменных неизвестны:
void foo(int x, int y)
{
if (x == y)
{
x = y; // <=
}
}
Хотя переменные 'x' и 'y' могут принимать любые значения, присваивание всё равно не имеет смысла из-за имеющейся выше проверки на равенство.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1048. |
Анализатор обнаружил объявление одного и того же include guard в разных заголовочных файлах, включенных в одну единицу трансляции. Это приводит к тому, что произойдёт "вставка" тела только одного файла, включенного первым.
Это диагностическое правило применяется к проектам на языке C.
Рассмотрим пример заголовочного файла header1.h:
// header1.h
#ifndef _HEADER_H_
#define _HEADER_H_
....
#endif
и header2.h:
// header2.h
#ifndef _HEADER_H_
#define _HEADER_H_ // <=
....
#endif
При создании второго заголовочного файла, программист скопировал код из первого файла и забыл переименовать макрос '_HEADER_H_'.
Таким образом, при компиляции следующего фрагмента, код из header2.h не будет включён в итоговый файл:
....
#include "header1.h"
#include "header2.h"
...
На первый взгляд кажется, что проблемы нет, так как должны возникать ошибки компиляции. Но на самом деле файл может успешно компилироваться.
Язык C позволяет вызывать необъявленные функции. В таких случаях предполагается, что функция возвращает значение типа 'int', и аргументы тоже имеют тип 'int'. Если заголовочный файл с объявлениями функций был исключен из компиляции, сборка проекта может пройти успешно, но во время выполнения программа будет работать некорректно. Подобная ситуация, например, описана в статье "Красивая 64-битная ошибка на языке Си".
Чтобы исправить ошибку, необходимо использовать уникальные имена include guard в заголовочных файлах.
См. также родственную диагностику: V1031. Function is not declared. The passing of data to or from this function may be affected.
Анализатор обнаружил, что вызов базового конструктора в списке инициализации выполняется с использованием неинициализированных полей дочернего класса.
Рассмотрим пример:
struct C : public Base
{
C(int i) : m_i(i), Base(m_i) {};
....
int m_i;
};
Согласно стандарту, сперва инициализируются базовые классы в порядке, указанном в объявлении. На момент вызова конструктора 'Base' переменная 'm_i' ещё не инициализирована, поэтому исправить код можно, например, так:
struct C : public Base
{
C(int i) : m_i(i), Base(i) {};
....
int m_i;
};
Также анализатор может обнаружить использование неинициализированных переменных, находящихся не только внутри класса, но и внутри базовых классов:
struct Base1
{
Base1(int i) : m_base1(i) { };
virtual ~Base1() = default;
....
int m_base1;
};
struct Base2
{
Base2(int i) : m_base2(i) { };
virtual ~Base2() = default;
....
int m_base2;
};
struct C : public Base1, public Base2
{
C(int i) : m_i(i), Base1(m_base2), Base2(i) {};
....
int m_i;
};
Если нужно инициализировать один из базовых классов полем другого, то нужно убедиться в том, что они инициализируются в нужном порядке:
struct C : public Base2, public Base1
{
C(int i) : m_i(i), Base1(m_base2), Base2(i) {};
....
int m_i;
};
Похожие проблемы способна искать диагностика V670. Однако она занимается поиском проблем, связанных с порядком инициализации полей одного класса, когда порядок инициализации переменных зависит от порядка их объявления в классе.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, когда проверка переменной может быть пропущена в следующем 'if' после присваивания или инициализации.
К примеру, ошибочным можно считать такой код:
int ret = syscall(....);
if (ret != -1) { .... }
....
int ret2 = syscall(....);
if (ret != -1) { .... } // <=
Часто бывают случаи, когда надо проверить возвращаемое значение какой-либо функции. Однако можно допустить ошибку, указав другую переменную внутри условия 'if'. Чаще всего подобная ошибка возникает, когда фрагмент кода копируется, но в нём забывают заменить имя переменной в условии. В рассмотренном примере в условии забыли заменить имя 'ret' на 'ret2'.
Исправленный вариант:
int ret2 = syscall(....);
if (ret2 != -1) { .... }
Или же можно допустить ошибку в таком случае:
obj->field = ....;
if (field) ....;
Переменная и член класса имеют одинаковые имена, из-за чего их легко перепутать.
Диагностика является эвристической. Во время своей работы диагностика разбивает имена переменных на составные части, сравнивает их и на основании этого делает предположения о наличии опечатки. Также производится базовая проверка типов. Это сделано для того, чтобы сократить число ложных срабатываний.
Часто диагностика может выдавать срабатывание на подобные случаи:
var->m_abc = ....;
var->m_cba = ....;
if (var->m_abc) // <=
{
....
}
Как правило это корректный код и ошибки нет. Можно либо подавить диагностику, либо поменять переменные местами, чтобы присваивание проверяемой переменной находилось перед 'if':
var->m_cba = ....;
var->m_abc = ....;
if (var->m_abc)
{
....
}
Такая запись может сделать код более читаемым, т.к. присваивание и проверка находятся рядом.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1051. |
Анализатор обнаружил присутствие виртуального метода в классе, помеченном как 'final'.
После рефакторинга или вследствие ошибочного проектирования класса могло получиться так, что класс был объявлен как завершенный ('final'), но при этом в него были добавлены непереопределенные виртуальные методы.
Такая структура класса не имеет смысла, и стоит проверить, не была ли нарушена логика наследования самого класса. Кроме этого, создание такого класса может приводить к хранению лишнего указателя на виртуальную таблицу методов и падению производительности.
Анализатор выдаст предупреждение для следующего класса:
struct Cl final // <= V1052
{
virtual ~Cl() {}
};
struct BaseClass
{
virtual void foo(int);
};
struct DerivedClass final : BaseClass // <= V1052
{
virtual void bar(float);
};
Если виртуальный метод / деструктор завершенного класса переопределяет виртуальный метод / деструктор базового класса, предупреждение выдано не будет:
struct BaseClass
{
virtual void foo();
virtual ~BaseClass();
};
struct DerivedClass final : BaseClass // ok
{
virtual void foo() override;
virtual ~DerivedClass();
};
Анализатор обнаружил вызов виртуальной функции в конструкторе или деструкторе класса.
Рассмотрим пример:
struct Base
{
Base()
{
foo();
}
virtual ~Base() = default;
virtual void foo() const;
};
Сам по себе вызов виртуального метода 'foo' в конструкторе класса 'Base' может не являться ошибкой, однако проблемы могут проявить себя в производных классах.
struct Child : Base
{
Child() = default;
virtual ~Child() = default;
virtual void foo() const override;
};
Во время создания объекта типа 'Child' будет вызван метод 'Base::foo()' из конструктора базового класса, но не переопределенный метод 'Child::foo()' из производного класса.
Для исправления этой проблемы нужно уточнить вызов метода. Например, для класса 'Base':
struct Base
{
Base()
{
Base::foo();
}
virtual ~Base() = default;
virtual void foo() const;
};
Теперь одного взгляда на код достаточно чтобы понять, какой именно метод будет вызван.
Также отметим, что использование указателя на себя 'this' при вызове виртуального метода не решает исходную проблему, и при использовании 'this' все также необходимо уточнить, из какого класса следует позвать виртуальную функцию:
struct Base
{
Base()
{
this->foo(); // bad
this->Base::foo(); // good
}
virtual ~Base() = default;
virtual void foo() const;
};
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1053. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что объект производного класса был скопирован в объект базового класса, т.е. произошла срезка типа.
Если базовый и производный классы являются полиморфными (т.е. содержат виртуальные функции), то при таком копировании информация о переопределенных виртуальных функциях в производных классах будет утеряна. Это может привести к нарушению полиморфного поведения.
Другим фактом является то, что объект базового класса теряет информацию о полях производного класса, если конструктор копирования сгенерирован компилятором неявно (возможно даже и в случае определенного пользователем).
Рассмотрим следующий пример:
struct Base
{
int m_i;
Base(int i) : m_i { i } { }
virtual int getN() { return m_i; }
};
struct Derived : public Base
{
int m_j;
Derived(int i, int j) : Base { m_i }, m_j { j } { }
virtual int getN() { return m_j; }
};
void foo(Base obj) { std::cout << obj.getN() << "\n"; }
void bar()
{
Derived d { 1, 2 };
foo(d);
}
При передаче переменной 'd' в функцию 'foo' произойдет копирование, а функция 'getN' будет вызвана из класса 'Base'.
Чтобы избежать проблем, связанных со срезкой типа, стоит использовать указатели/ссылки:
void foo(Base &obj) { std::cout << obj.getN() << "\n"; }
В этом случае копирования не произойдет, и 'getN' будет вызвана из класса 'Derived'.
Если срезка типа все же необходима, то желательно определить явную операцию, которая это делает. Это спасет читателей кода от путаницы:
struct Base
{
....
};
struct Derived : public Base
{
....
Base copy_base();
....
};
void foo(Base obj);
void bar()
{
Derived d { .... };
foo(d.copy_base());
}
Анализатор не выдает срабатывание в случае, если иерархия классов не содержит виртуальных функций и все нестатические поля располагаются в базовом классе:
struct Base
{
int m_i;
int m_j;
Base(int i, int j) : m_i { i }, m_j { j } { }
int getI() { return m_i; }
int getJ() { return m_j; }
};
struct Derived : public Base
{
Derived(int i, int j) : Base(i, j) { }
virtual int getN() { return m_j; }
};
Данная диагностика классифицируется как:
|
Анализатор обнаружил в качестве аргумента оператора 'sizeof' переменную типа "STL-подобный контейнер".
Рассмотрим следующий пример:
#include <string>
void foo(const std::string &name)
{
auto len = sizeof(name) / sizeof(name[0]);
....
}
Выражение 'sizeof(name)' возвращает не суммарный размер элементов в контейнере в байтах (или просто количество элементов), а именно размер типа контейнера, отведенный для имплементации. Например, типичная имплементация 'std::string' может содержать 3 указателя (стандартная библиотека libc++, 64-битная система), т.е. 'sizeof(name) == 24'. Размер же реальной хранимой строки чаще всего отличается от этого значения.
Ошибка подобного рода может появиться в проекте в результате рефакторинга старого кода:
#define MAX_LEN(str) ( sizeof((str)) / sizeof((str)[0]) - 1 )
typedef char MyString[256];
void foo()
{
MyString str { .... };
....
size_t max_len = MAX_LEN(str);
}
При замене псевдонима 'MyString' с типа 'char[256]' на 'std::string' вычисление максимально допустимой длины строки станет некорректным.
Для получения реального размера STL-подобных контейнеров стоит использовать публичную функцию-член '.size()':
#include <string>
void foo(const std::string &name)
{
auto len = name.size();
}
Если подразумевалось именно вычисление размера имплементации контейнера, то для лучшего понимания кода предпочтительнее передавать в качестве операнда оператора 'sizeof' тип контейнера, прямым способом или через оператор 'decltype' (C++11) для переменных:
#include <string>
void foo(const std::string &str)
{
auto string_size_impl1 = sizeof(std::string);
auto string_size_impl2 = sizeof(decltype(str));
}
Диагностика также знает о существовании контейнера 'std::array' и не выдает на нем срабатывание при его использовании в качестве операнда оператора 'sizeof':
template <typename T, size_t N>
void foo(const std::array<T, N> &arr)
{
auto size = sizeof(arr) / sizeof(arr[0]); // ok
}
Начиная со стандарта C++17, рекомендуется использовать свободную функцию 'std::size()', которая умеет работать как встроенными массивами, так и всеми типами контейнеров, имеющими публичную функцию-член '.size()':
#include <vector>
#include <string>
#include <set>
#include <list>
void foo()
{
int arr[256] { .... };
std::vector vec { .... };
std::string str { .... };
std::set set { .... };
std::list list { .... };
auto len1 = std::size(arr);
auto len2 = std::size(vec);
auto len3 = std::size(str);
auto len4 = std::size(set);
auto len5 = std::size(list);
}
Данная диагностика классифицируется как:
Анализатор обнаружил использование идентификатора '__func__' в теле перегруженного оператора '()'.
Рассмотрим пример:
class C
{
void operator()(void)
{
std::cout << __func__ << std::endl;
}
};
void foo()
{
C c;
c();
}
При запуске будет выведено 'operator()'. На первый взгляд, это вполне ожидаемое поведение для подобного кода, поэтому перейдем к более неочевидному примеру:
void foo()
{
auto lambda = [] () { return __func__; };
std::cout << lambda() << std::endl;
}
Важно учитывать тот факт, что '__func__' не является переменной в привычном смысле этого слова, поэтому следующие варианты не сработают, и по-прежнему будет выведено 'operator()':
void fooRef()
{
auto lambda = [&] () { return __func__; };
std::cout << lambda() << std::endl;
}
void fooCopy()
{
auto lambda = [=] () { return __func__; };
std::cout << lambda() << std::endl;
}
Чтобы исправить это в случае с лямбдой, необходимо передать '__func__' явно через список захвата:
void foo()
{
auto lambda = [func = __func__] () { return func; };
std::cout << lambda() << std::endl;
}
Для более полноценного вывода имени функции даже внутри перегруженного 'operator()' или тела лямбды можно воспользоваться специфичными для платформы/компилятора макросами. Так, компилятор MSVC предоставляет следующие три макроса:
Для компиляторов Clang и GCC доступны следующие макросы:
Анализатор обнаружил подозрительный код, инициализирующий генератор псевдослучайных чисел константным значением.
// C
srand(0);
// C++
std::mt19937 engine(1);
Числа, сгенерированные таким генератором, можно предугадать, либо же они будут воспроизводиться снова и снова при каждом запуске программы.
Чтобы этого избежать, стоит использовать какое-либо случайное число. К примеру, можно воспользоваться текущим системным временем:
srand(time(0));
Но такой подход может вызвать проблемы в многопоточных программах – 'time(0)' может вернуть одинаковые значения в разных потоках. Также стоит учесть, что время может меняться пользователем.
Начиная с C++11, можно воспользоваться генератором 'std::random_device', реализующим интерфейс к генератору истинно случайных чисел:
std::random_device rd;
std::mt19937 engine(rd());
Однако если таковой в системе отсутствует, то будет использован обычный генератор псевдослучайных чисел.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительное сравнение адресов двух функций.
Пример:
namespace MyNamespace
{
int one() noexcept;
int two() noexcept;
}
using namespace MyNamespace;
void SomeFunction()
{
if (one != two)
{
// do something
}
....
}
Сравнение в данном случае всегда вернет 'true', так как 'one' и 'two' – имена функций с совместимыми сигнатурами, объявленных в пространстве имен 'MyNamespace'. Вероятно, что в этом случае предполагалось сравнение возвращаемых значений, но автор кода забыл добавить к именам функций круглые скобки:
namespace MyNamespace
{
int one() noexcept;
int two() noexcept;
}
using namespace MyNamespace;
void SomeFunction()
{
if (one() != two())
{
// do something
}
}
Также подобный код может быть результатом неудачного рефакторинга. Если внутри функции сравнивались локальные переменные, которые в какой-то момент были удалены из кода, при этом само условие сохранилось, то вполне возможно, что будет происходить сравнение функций с такими же именами.
Пример кода до рефакторинга:
namespace MyNamespace
{
int one() noexcept;
int two() noexcept;
}
using namespace MyNamespace;
void SomeFunction(int one, int two)
{
if (one != two)
{
// do something
}
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил макрос, имя которого перекрывает ключевое слово или зарезервированный стандартом идентификатор.
Пример:
#define sprintf std::printf
Здесь из-за коллизии имен вызовы стандартной функции 'sprintf' после препроцессирования заменятся на вызовы 'printf'. Такая замена приведет к некорректной работе программы.
Диагностика также сообщает об удалении предопределенных макросов.
Пример:
#undef assert
Диагностика игнорирует переопределения ключевых слов, если они идентичны с точки зрения семантики.
Примеры:
#define inline __inline
#define inline __forceinline
#define template extern template
Данная диагностика классифицируется как:
|
Анализатор обнаружил передачу строки типа 'BSTR' в функцию 'SysAllocString'.
BSTR FirstBstr = ....;
BSTR SecondBstr = SysAllocString(FirstBstr);
Если необходимо скопировать 'BSTR' строку, то ее передача в функцию 'SysAllocString' может привести к логической ошибке.
BSTR (basic string или binary string) – это строковый тип данных, который используется в COM, Automation и Interop функциях. Представление BSTR:
Тип BSTR является указателем, который указывает на первый символ строки, а не на префикс длины.
Функция 'SysAllocString' работает с 'BSTR' строкой также, как с обычной широкой C-строкой. Это значит, что если строка содержит множественные вложенные нулевые символы, то 'SysAllocString' создаст урезанную строку. Избежать непредвиденного поведения можно, переписав код, используя классы-обёртки над 'BSTR', такие как 'CComBSTR' или '_bstr_t'.
Например, правильно скопировать одну 'BSTR' строку в другую можно так:
CComBstr firstBstr(L"I am a happy BSTR.");
BSTR secoundBstr = firstBstr.Copy();
или так:
_bstr_t firstBstr(L"I am a happy BSTR too.");
BSTR secoundBstr = firstBstr.copy();
Анализатор обнаружил расширение пространства имён 'std' или 'posix'. Несмотря на то, что такая программа успешно компилируется и исполняется, модификация данных пространств имён может привести к неопределённому поведению программы, если иное не указано стандартом.
Содержимое пространства имен 'std' определяется исключительно комитетом стандартизации, и стандарт запрещает добавлять в него:
Стандарт разрешает добавлять следующие специализации шаблонов, определенных в пространстве имен 'std', если они зависят хотя бы от одного определенного в программе типа (program-defined type):
Однако, специализации шаблонов, лежащих внутри классов или шаблонов классов, запрещены.
Наиболее частым вариантом, когда пользователь расширяет пространство имен 'std', является добавление своей перегрузки функции 'std::swap' и полной/частичной специализации шаблона класса 'std::hash'.
Рассмотрим фрагмент кода с добавлением перегрузки 'std::swap':
template <typename T>
class MyTemplateClass
{
....
};
class MyClass
{
....
};
namespace std
{
template <typename T>
void swap(MyTemplateClass<T> &a, MyTemplateClass<T> &b) noexcept // UB
{
....
}
template <>
void swap(MyClass &a, MyClass &b) noexcept // UB since C++20
{
....
};
}
Первый шаблон функции не является специализацией 'std::swap', и такая декларация ведет к неопределенному поведению. Второй шаблон функции является специализацией, и до C++20 поведение программы определено. Однако, в данном случае можно поступить иначе: можно вынести обе функции из пространства имен 'std' и поместить их в то пространство имен, где определены классы:
template <typename T>
class MyTemplateClass
{
....
};
class MyClass
{
....
};
template <typename T>
void swap(MyTemplateClass<T> &a, MyTemplateClass<T> &b) noexcept
{
....
}
void swap(MyClass &a, MyClass &b) noexcept
{
....
};
Теперь, когда необходимо написать шаблон функции, который применяет функцию swap для двух объектов типа T, можно написать следующий код:
template <typename T>
void MyFunction(T& obj1, T& obj2)
{
using std::swap; // make std::swap visible for overload resolution
....
swap(obj1, obj2); // best match of 'swap' for objects of type T
....
}
Теперь, компилятор выберет нужную перегрузку функции на основе поиска с учетом аргументов (argument-dependent lookup, ADL) – пользовательские функции 'swap' для класса 'MyClass' и для шаблона класса 'MyTemplateClass' и стандартную версию 'std::swap' для остальных типов.
Разберем следующий пример со специализацией шаблона класса 'std::hash':
namespace Foo
{
class Bar
{
....
};
}
namespace std
{
template <>
struct hash<Foo::Bar>
{
size_t operator()(const Foo::Bar &) const noexcept;
};
}
С точки зрения стандарта этот код является валидным, и анализатор в этой ситуации не выдает предупреждение. Однако, начиная с C++11, можно и в этом случае поступить иначе, написав специализацию шаблона класса за пределами пространства имен 'std':
template <>
struct std::hash<Foo::Bar>
{
size_t operator()(const Foo::Bar &) const noexcept;
};
В отличие от пространства имен 'std', стандарт C++ запрещает абсолютно любую модификацию пространства имён 'posix':
namespace posix
{
int x; // UB
}
Дополнительная информация:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1061. |
Это диагностическое правило основано на пункте R.15 CppCoreGuidelines.
В классе определен оператор 'new' или 'delete', но при этом не определен обратный оператор.
Пример:
class SomeClass
{
....
void* operator new(size_t s);
....
};
Для динамической аллокации объекта такого класса будет использоваться перегруженный оператор 'new', а для удаления – оператор 'delete', определенный по умолчанию.
Для симметрии операций аллокации/деаллокации следует определить также оператор 'delete':
class SomeClass
{
....
void* operator new(size_t s);
void operator delete(void*);
....
};
Также операторы можно пометить как удаленные ('= delete'), если по какой-то причине необходимо запретить аллокацию или деаллокацию для объектов класса. При этом желательно одновременно запретить и аллокацию и деаллокацию, чтобы вызов любого из этих операторов приводил к ошибке времени компиляции, а не к трудно уловимому багу:
#include <cstddef>
class AutoTransaction
{
public:
/// Mark 'operator new' as deleted to prevent heap allocation
void* operator new (std::size_t) = delete;
};
void foo()
{
auto ptr = new AutoTransaction; // code doesn't compile
}
void bar()
{
AutoTransaction obj;
delete &obj; // code compiles, but contains an error
}
Если также запретить деаллокацию, то компилятор не позволит совершить ошибку:
class SomeClass
{
....
void* operator new(size_t s) = delete;
void operator delete(void*) = delete;
....
};
void foo()
{
auto ptr = new AutoTransaction; // code doesn't compile
}
void bar()
{
AutoTransaction obj;
delete &obj; // code doesn't compile
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки управления динамической памятью (выделения, освобождения, использования освобожденной памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1062. |
Анализатор обнаружил странное выражение, в котором производится деление по модулю 1. Результатом такого выражения всегда будет 0.
Частым паттерном данной ошибки является проверка делимости числа на что-то без остатка. Для этого необходимо поделить число по модулю и сравнить результат с 0 или 1. В этом месте легко сделать опечатку, так как раз ожидается значение 1, то кажется, что и поделить надо на 1. Пример:
if (x % 1 == 1)
{
....
}
Здесь переменную 'x' поделили по модулю '1', и теперь вне зависимости от значения 'x' выражение 'x % 1' всегда будет равно 0. Следовательно, условие всегда будет ложным. Наиболее вероятно, что переменную 'x' следовало поделить по модулю '2':
if (x % 2 == 1)
{
....
}
Рассмотрим пример из реального проекта (stickies):
void init (....)
{
srand(GetTickCount() + rand());
updateFreq1 = (rand() % 1) + 1;
updateFreq2 = (rand() % 1) + 1;
updateFreq3 = (rand() % 1) + 1;
updateFreq4 = (rand() % 1) + 1;
waveFreq1 = (rand() % 15);
waveFreq2 = (rand() % 3);
waveFreq3 = (rand() % 16);
waveFreq4 = (rand() % 4);
// ....
}
Здесь переменные 'updateFreq1', 'updateFreq2', 'updateFreq3' и 'updateFreq4' будут всегда инициализированы значением 1. Возможно, что каждая переменная вероятно должна была быть проинициализирована каким-то псевдослучайным числом. И скорее всего, это [1..2]. Тогда корректный код должен быть таким:
updateFreq1 = (rand() % 2) + 1;
updateFreq2 = (rand() % 2) + 1;
updateFreq3 = (rand() % 2) + 1;
updateFreq4 = (rand() % 2) + 1;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1063. |
Анализатор обнаружил подозрительное выражение, в котором производится целочисленное деление, при котором левый операнд всегда меньше правого. Результат такого выражения всегда будет равен нулю.
Давайте рассмотрим пример:
if ( nTotal > 30 && pBadSource->m_nNegativeVotes / nTotal > 2/3 )
{
....
}
Так как литералы '2' и '3' имеют целочисленный тип, результат их деления тоже будет целочисленным и, соответственно, равен нулю. А значит, данное условие эквивалентно следующему:
if ( nTotal > 30 && pBadSource->m_nNegativeVotes / nTotal > 0 )
{
....
}
Правильным в данной ситуации будет явно привести один из операндов к типу с плавающей точкой. Примеры исправленного кода:
if ( nTotal > 30 && pBadSource->m_nNegativeVotes / nTotal >
static_cast<float>(2)/3 )
{
....
}
Или:
if ( nTotal > 30 && pBadSource->m_nNegativeVotes / nTotal > 2.0f/3 )
{
....
}
Также анализатор выдаст предупреждение, если он обнаружил подозрительное выражение, в котором производится взятие остатка по модулю, при котором делимое всегда меньше делителя. Результат такого выражения всегда будет равен значению делимого.
Давайте рассмотрим пример:
void foo()
{
unsigned int r = 12;
const unsigned int r3a = (16 + 5 - r) % 16;
}
Здесь выражение '16+5-r' будет равно 9. Это значение меньше делителя '16'. Поэтому взятие остатка по модулю в данном случае не имеет смысла, результатом будет 9.
Рассмотрим более сложный пример:
int get_a(bool cond)
{
return cond ? 3 : 5;
}
int get_b(bool cond)
{
return cond ? 7 : 9;
}
int calc(bool cond1, bool cond2)
{
return get_a(cond1) % get_b(cond2);
}
В функции 'calc' производится взятие остатка по модулю. Делимое принимает значения 3 или 5. Делитель принимает значения 7 или 9. Получаем четыре варианта вычислений операции взятия остатка по модулю: '3 % 7', '5 % 7', '3 % 9', '5 % 9'. В каждом из вариантов делимое меньше делителя. Значит, операция бессмысленна.
Если анализатор выдал предупреждение на вашем коде, рекомендуем изучить этот фрагмент кода на наличие логических ошибок. Возможно, вычисление одного из операндов взятия остатка по модулю происходит не так, как ожидалось. Также может быть, что вместо операции взятия остатка по модулю нужно использовать другую операцию.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1064. |
Анализатор обнаружил подозрительное выражение, в котором операнды могут быть сокращены. Это может свидетельствовать о наличии в коде логической ошибки или опечатки.
Рассмотрим простой синтетический пример:
void Foo(int A, int B, int C)
{
if (A - A + 1 < C)
Go(A, B);
}
Из-за опечатки получилось избыточное выражение, которое можно сократить до '1 < C'. На самом деле, планировалось написать, например, так:
void Foo(int A, int B, int C)
{
if (A - B + 1 < C)
Go(A, B);
}
В других случаях могут быть выявлены выражения, которые, хотя и не содержат ошибку, будет полезно упростить. Это может сделать выражения более простыми и понятными. Пример:
if ((rec.winDim.left + (rec.winDim.right - rec.winDim.left)) < inset) // <=
{
rec.winDim.left = -((rec.winDim.right – rec.winDim.left) - inset);
rec.winDim.right = inset;
}
if ((rec.winDim.top + (rec.winDim.bottom – rec.winDim.top)) < inset) // <=
{
rec.winDim.top = -((rec.winDim.bottom – rec.winDim.top) - inset);
rec.winDim.bottom = inset;
}
В обоих условиях выражения могут быть упрощены путем устранения операндов 'rec.winDim.left' и 'rec.winDim.top' соответственно. Сокращённый вариант кода:
if (rec.winDim.right < inset)
{
rec.winDim.left = -((rec.winDim.right – rec.winDim.left) - inset);
rec.winDim.right = inset;
}
if (rec.winDim.bottom < inset)
{
rec.winDim.top = -((rec.winDim.bottom – rec.winDim.top) - inset);
rec.winDim.bottom = inset;
}
Примечание. Иногда избыточность вовсе не уменьшает, а увеличивает читабельность кода. Например, избыточность может нести поясняющий смысл в математических формулах. В этом случае есть смысл не сокращать выражение, а использовать один из механизмов подавления ложных срабатываний.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1065. |
Анализатор обнаружил вызов функции 'SysFreeString' для объекта, тип которого отличен от 'BSTR'.
Функция 'SysFreeString' предназначена только для работы с типом 'BSTR'. Нарушение этого условия может привести к проблемам с освобождением памяти.
Рассмотрим простой синтетический пример:
#include <atlbase.h>
void foo()
{
CComBSTR str { L"I'll be killed twice" };
// ....
SysFreeString(str); //+V1066
}
Здесь в функцию 'SysFreeString' передаётся объект типа 'CComBSTR'. Этот класс является обёрткой над типом 'BSTR' и имеет перегруженный оператор неявного преобразования 'operator BSTR()', который возвращает указатель на обёрнутую BSTR-строку. Поэтому приведённый выше код скомпилируется без предупреждений.
Однако, этот код содержит ошибку. После того, как функция 'SysFreeString' освободит ресурс, принадлежащий объекту 'str', произойдет выход из области видимости, и будет вызван деструктор объекта 'str'. Деструктор произведёт повторное освобождение уже освобождённого ресурса, что приведет к неопределённому поведению.
Иногда подобное поведение может случиться даже при передаче в 'SysFreeString' именно объекта типа 'BSTR'. Например, PVS-Studio также выдаст предупреждение на такой код:
#include <atlbase.h>
void foo()
{
CComBSTR str = { L"a string" };
BSTR bstr = str;
str.Empty();
SysFreeString(bstr); //+V1066
}
Так как 'CComBSTR::operator BSTR()' возвращает указатель на собственное поле, после присвоения 'BSTR bstr = str;' оба объекта будут владеть одним и тем же ресурсом. Вызов метода 'str.Empty();' освободит этот ресурс, а 'SysFreeString(bstr)' попытается освободить уже освобождённый ресурс.
Одним из способов избежать общего владения ресурсом является создание копии или использование метода 'CComBSTR::Detach()'. Например, анализатор не будет выдавать предупреждение на следующий код:
#include <atlbase.h>
void foo()
{
CComBSTR ccombstr = { L"I am a happy CComBSTR" };
BSTR bstr1 = ccombstr.Copy();
SysFreeString(bstr1); // OK
BSTR bstr2;
ccombstr.CopyTo(&bstr2);
SysFreeString(bstr2); // OK
BSTR bstr3 = ccombstr.Detach();
SysFreeString(bstr3); // OK
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил конструктор исключения, из которого может быть брошено другое исключение. Использование такого класса может привести к неожиданному поведению программы при обработке исключений.
Рассмотрим синтетический пример:
#include <stdexcept>
class divide_by_zero_error : public std::invalid_argument
{
public:
divide_by_zero_error() : std::invalid_argument("divide_by_zero")
{
....
if (....)
{
throw std::runtime_error("oops!"); // <=
}
}
};
void example(int a, int b)
{
try
{
if (b == 0)
throw divide_by_zero_error ();
....
}
catch (const divide_by_zero_error &e)
{
....
}
// my_exception thrown and unhandled
}
В коде функции 'example' программист пытается обработать возникшее исключение 'divide_by_zero_error', однако вместо этого будет сформировано исключение 'std::runtime_error' и не перехвачено последующим 'catch'-блоком. Это приведет к тому, что исключение покинет функцию 'example', что может привести к следующим ситуациям:
При разработке и использовании собственных классов исключений нужно проявлять особую бдительность, поскольку исключения в их конструкторах могут возникнуть в неожиданных местах, например при вызове других функций. В следующем примере при создании логирующего исключения может возникнуть второе исключение из функции 'Log':
#include <ios>
static void Log(const std::string& message)
{
....
// std::ios_base::failure may be thrown by stream operations
throw std::ios_base::failure("log file failure");
}
class my_logging_exception : public std::exception
{
public:
explicit my_logging_exception(const std::string& message)
{
Log(message); // <=
}
};
Данная диагностика классифицируется как:
Анализатор обнаружил анонимное пространство имен, объявленное в заголовочном файле. Такой заголовочный файл создаёт копии символов с "внутренним связыванием" (internal linkage) в каждой единице трансляции, включающей этот заголовочный файл. Это приводит к "раздуванию" объектных файлов, что может быть нежелательным поведением.
Рассмотрим простой пример заголовочного файла с анонимным пространством имен:
// utils.hpp
#pragma once
#include <iostream>
namespace
{
int global_variable;
void set_global_variable(int v)
{
std::cout << global_variable << std::endl;
global_variable = v;
}
}
Каждая единица трансляции при включении заголовочного файла 'utils.hpp' получит свой экземпляр переменной 'global_variable', не связанной с другими экземплярами и не доступной из других единиц трансляции. Также будет сгенерировано несколько избыточных функций 'set_global_variable'. До стандарта C++17 такой код мог встречаться в header-only библиотеках для того, чтобы не нарушать One Definition Rule при включении заголовочных файлов в несколько единиц трансляции. Также подобный код может появиться из-за неаккуратного рефакторинга, например, при переносе анонимного пространства имен из компилируемого файла в заголовочный файл.
Стоит отметить, что данное правило распространяется и на безымянные пространства имен, вложенные в другие пространства имен:
namespace my_namespace
{
int variable1; // namespace-scope non-const variable
// 'variable1' has external linkage
namespace // <=
{
int variable2; // unnamed namespace applies 'static'
// 'variable2' has internal linkage
}
}
Если необходимо создать ровно один экземпляр символа для header-only библиотеки, то можно воспользоваться спецификатором 'inline' (начиная с C++17, действует и для переменных):
// utils.hpp
#pragma once
#include <iostream>
inline int global_variable; // ok since C++17
inline void set_global_variable(int v)
{
std::cout << global_variable << std::endl;
global_variable = v;
}
Если используется более ранняя версия стандарта, но библиотека не header-only, то можно объявить символы как 'extern' в заголовочном файле и определить их в одном из юнитов трансляции:
// utils.hpp
#pragma once
extern int global_variable;
void set_global_variable(int v); // functions implicitly
// have external linkage ('extern')
// utils.cpp
#include "utils.hpp"
#include <iostream>
int global_variable;
void set_global_variable(int v)
{
std::cout << global_variable << std::endl;
global_variable = v;
}
В том случае, когда используется более старая версия стандарта, но библиотека должна быть header-only, срабатывание можно подавить комментарием:
// utils.hpp
#pragma once
#include <iostream>
namespace //-V1068
{
int global_variable;
void set_global_variable(int v)
{
std::cout << global_variable << std::endl;
global_variable = v;
}
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1068. |
Анализатор обнаружил фрагмент кода, в котором происходит конкатенация строк с разными префиксами кодировок.
Рассмотрим синтетический пример:
// Until C99/C++11
L"Hello, this is my special "
"string literal with interesting behavior";
До стандартов C11/C++11 в языках C и C++ существовало лишь два типа строковых литералов:
Конкатенация таких строковых литералов разных типов до C99 и C++11 приводит к неопределенному поведению, и анализатор выдает предупреждение первого уровня на такой случай. Корректный код будет выглядеть так:
// Until C99/C++11
L"Hello, this is my special "
L"string literal with defined behavior";
Начиная с C99 и C++11, если один из строковых литералов имеет префикс, а второй – нет, то поведение определено, и результирующий строковый литерал будет иметь тот же тип, что и строковый литерал с префиксом. В этом случае анализатор не выдает предупреждений:
// Since C99/C++11
L"Hello, this is my special "
"string literal with "
"defined behavior";
Начиная со стандарта C11/C++11 в языки были добавлены следующие префиксированные строковые литералы:
Конкатенация UTF-8 и любого "широкого" строкового литерала ведет к ошибке этапа компиляции, и поэтому анализатор не выдает предупреждений:
L"Hello, this is my special "
u8"string literal that won't compile"; // compile-time error
Любые другие комбинации префиксированных строковых литералов ведут к неуточненному поведению. Анализатор в таких случаях выдает предупреждения второго уровня:
// Until C11/C++11
L"Hello, this is my special "
u"string literal with implementation-defined behavior";
L"Hello, this is my special "
U"string literal with implementation-defined behavior";
u"Hello, this is my special "
U"string literal with implementation-defined behavior";
Анализатор также выдает предупреждения третьего уровня для случаев, когда конкатенируются 3 и более строковых литералов, "узкие" и один из префиксированных:
template <typename T>
void foo(T &&val) { .... }
....
void bar()
{
foo("This" L"is" "strange");
foo(L"This" "is" L"strange");
}
Несмотря на то, что в современных стандартах поведение определено, такой код выглядит странно и, возможно, потребует дополнительного внимания. Другими словами, такой код провоцирует ошибки, и полезно рассмотреть вопрос о его рефакторинге.
Анализатор обнаружил ситуацию, при которой результат тернарного оператора, операнды которого имеют целые типы, различные по знаку, сохраняется в целый тип большего размера любой знаковости. При таком преобразовании отрицательное значение станет положительным.
Рассмотрим синтетический пример:
long long foo(signed int a, unsigned int b, bool c)
{
return c ? a : b;
}
Согласно правилам преобразования в C++, если второй и третий операнды тернарного оператора содержат разные типы, и размер беззнакового операнда не меньше размера знакового, то компилятор преобразует их к беззнаковому типу.
Таким образом, знаковая переменная с отрицательным значением (например, -1) будет приведена к беззнаковому типу. В случае 32-битного типа 'int' итоговое значение будет '0xFFFFFFFF'. Затем этот результат будет преобразован в целый тип большего размера (64-битный тип 'long long'), однако исходная знаковость уже была потеряна, и финальный результат так и останется в виде положительного числа.
Проблема сохраняется также для случая, когда результат тернарного оператора преобразуется в беззнаковый тип большей размерности:
unsigned long long f(signed int i, unsigned int ui, bool b)
{
return b ? i : ui;
}
Если переменная 'i' имеет отрицательное значение (например, -1), то результатом тернарного оператора будет значение '0xFFFFFFFF'. Затем оно будет преобразовано в беззнаковый тип большего размера, и его значение станет '0x00000000FFFFFFFF'. Скорее всего программист ожидал, что результатом будет значение '0xFFFFFFFFFFFFFFFF'.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что возвращаемое значение функции игнорируется. При этом в большинстве случаев результат функции каким-либо образом используется.
Диагностика призвана помочь в тех случаях, когда функция или ее возвращающий тип не помечены стандартным атрибутом '[[nodiscard]]' (C23/C++17) или его аналогами. Если в большинстве случаев результат функции использовался каким-либо способом, и при этом в определенных ситуациях игнорируется, то это может свидетельствовать о возможной ошибке.
Рассмотрим синтетический пример:
int foo();
....
auto res = foo();
....
if (foo() == 42) { .... }
....
while (foo() != 42) { .... }
....
return foo();
....
foo();
....
Здесь результат функции 'foo' используется четырьмя различными способами, а затем игнорируется в одном. Если результат не используется менее чем в 10% случаев от общего количества вызовов, анализатор выдаст предупреждение.
В некоторых ситуациях такой код мог быть написан намеренно. Например, если функция содержит некоторые побочные эффекты - операции с потоком, чтение/запись 'volatile'-переменных и т.п., и при этом результатом можно пренебречь.
Чтобы помочь программисту понять, что такое поведение было задумано, рекомендуется явно проигнорировать возвращаемое значение, приведя его к типу 'void':
....
(void) foo(); // or static_cast<void>(foo());
....
Анализатор не выдает срабатывания для следующих случаев:
Анализатор обнаружил потенциальную ошибку, когда буфер, содержащий приватную информацию, не будет очищен.
Рассмотрим синтетический пример:
int f()
{
char password[size];
if (!use1(password))
return -1;
use2(password);
memset_s(password, sizeof(password), 0, sizeof(password));
return 0;
}
Эта ситуация аналогична утечке памяти. Несмотря на то, что буфер очищается при помощи безопасной функции 'memset_s', если произойдет выход из функции под условием, данные останутся в памяти.
Чтобы избежать ошибки, буфер следует очищать на всех путях выполнения.
Исправленный пример:
int f()
{
char password[size];
if (use1(password))
{
use2(password);
memset_s(password, sizeof(password), 0, sizeof(password));
return 0;
}
return -1;
}
Аналогичная ситуация произойдет, если функция выбросит исключение прежде, чем вызовется очистка буфера.
Пример:
int f()
{
char password[size];
if (!use1(password))
throw Exception{};
RtlSecureZeroMemory(password, size);
return 0;
}
Возможный вариант исправления:
int f()
{
char password[size];
if (use1(password))
{
RtlSecureZeroMemory(password, size);
return 0;
}
throw Exception{};
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил возможную ошибку, связанную с тем, что блок кода ('{ .... }'), идущий после конструкции 'if', к ней не относится.
Рассмотрим первый синтетический пример:
if (a == 1) nop(); // <=
{
nop2();
}
При беглом обзоре кода может показаться, что блок выполнится, если условие истинно, но на самом деле это не так. Блок будет выполняться всегда, независимо от условия. Это может ввести программиста в заблуждение.
Рассмотрим другие примеры кода, на которые анализатор выдаст срабатывание:
if (a == 2) nop(); else nop2(); // <=
{
nop3();
}
if (a == 3) nop();
else nop2(); // <=
{
nop3();
}
Стоит отметить, что сам по себе такой паттерн может не являться ошибкой и встречается в коде. Поэтому анализатор отсеивает случаи, когда конструкция 'if' записана в одну строку, и в ее теле выполняется одна из следующих конструкций: 'return', 'throw', 'goto'. Например:
if (a == 4) return; // ok
{
nop();
}
if (a == 5) throw; // ok
{
nop();
}
....
label:
....
if (a == 6) goto label; // ok
{
nop();
}
Также анализатор не выдаст срабатывание, если строки с конструкцией 'if' и несвязанным с ней блоком кода несмежные:
if (a == 7) nop();
// this is a block for initializing MyClass fields
{
....
}
Если вы получили такое срабатывание, и оно ложное, вы можете подсказать об этом анализатору, добавив пустую строку между 'if' и блоком.
Также диагностика не выдаст срабатывание в том случае, когда тело 'if' содержит пустую конструкцию (';'), за это отвечает диагностическое правило V529.
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительную ситуацию внутри строкового или символьного литерала, при которой escape-последовательность, в конце которой стоит буква, не отделена от следующей за ней печатаемой буквы. Такая запись может привести к путанице. Возможно, это опечатка, и литерал записан некорректно.
Рассмотрим пример:
const char *str = "start\x0end";
Предполагается, что символы внутри строки разделены нуль-терминалом. Однако на самом деле после 'start' идет символ с кодом '0xE', а затем 2 остальных символа – 'nd'.
Чтобы исправить проблему, можно:
Например, код выше можно переписать таким образом:
const char *str = "start\x0" "end";
Escape-последовательность можно оставить обособленной от других частей строки:
const char *str = "start" "\x0" "end";
Или ограничить другим спецсимволом, например табуляцией:
const char *str = "start\x0\tend";
Анализатор обнаружил ситуацию, при которой файл был открыт в одном режиме, но вызываемая функция ожидает, что он будет находиться в другом.
Например, файл был открыт в режиме только для записи, но он используется для чтения:
bool read_file(void *ptr, size_t len)
{
FILE *file = fopen("file.txt", "wb"); // <=
if (file != NULL)
{
bool ok = fread(ptr, len, 1, file) == 1;
fclose(file);
return ok;
}
return false;
}
Скорее всего, это опечатка. Для исправления следует использовать правильный режим:
bool read_file(void *ptr, size_t len)
{
FILE *file = fopen("file.txt", "rb"); // <=
if (file != NULL)
{
bool ok = fread(ptr, len, 1, file) == 1;
fclose(file);
return ok;
}
return false;
}
Также возможна ситуация, когда происходит запись в закрытый файл:
void do_something_with_file(FILE* file)
{
// ....
fclose(file);
}
void foo(void)
{
FILE *file = fopen("file.txt", "w");
if (file != NULL)
{
do_something_with_file(file);
fprintf(file, "writing additional data\n");
}
}
Следует проверить корректность такого использования ресурсов в программе и исправить проблему.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1075. |
Анализатор обнаружил в тексте программы символы, которые могут ввести программиста в заблуждение. Эти символы могут не отображаться и изменять видимое представление кода в среде разработки. Комбинации таких символов могут привести к тому, что человек и компилятор будут интерпретировать код по-разному.
Это может быть сделано специально. Такой вид атаки называется Trojan Source. Подробнее:
Анализатор выдаст предупреждение, если найдет один из следующих символов:
Обозначение |
Код |
Название |
Описание |
---|---|---|---|
LRE |
U+202A |
LEFT-TO-RIGHT EMBEDDING |
Текст после символа LRE интерпретируется как вставленный и отображается слева направо. Действие LRE прерывается символом PDF или символом перевода строки. |
RLE |
U+202B |
RIGHT-TO-LEFT EMBEDDING |
Текст после символа RLE интерпретируется как вставленный и отображается справа налево. Действие RLE прерывается символом PDF или символом перевода строки. |
LRO |
U+202D |
LEFT-TO-RIGHT OVERRIDE |
Текст после символа LRO принудительно отображается слева направо. Действие LRO прерывается символом PDF или символом перевода строки. |
RLO |
U+202E |
RIGHT-TO-LEFT OVERRIDE |
Текст после символа RLO принудительно отображается справа налево. Действие RLO прерывается символом PDF или символом перевода строки. |
|
U+202C |
POP DIRECTIONAL FORMATTING |
Символ PDF прерывает действие одного из символов LRE, RLE, LRO или RLO, встреченного ранее. Прерывает ровно один, последний из встреченных, символ. |
LRI |
U+2066 |
LEFT‑TO‑RIGHT ISOLATE |
Текст после символа LRI отображается слева направо и интерпретируется как изолированный. Это означает, что другие управляющие символы не влияют на отображение этого фрагмента текста. Действие LRI прерывается символом PDI или символом перевода строки. |
RLI |
U+2067 |
RIGHT‑TO‑LEFT ISOLATE |
Текст после символа RLI отображается справа налево и интерпретируется как изолированный. Это означает, что другие управляющие символы не влияют на отображение этого фрагмента текста. Действие RLI прерывается символом PDI или символом перевода строки. |
FSI |
U+2068 |
FIRST STRONG ISOLATE |
Направление текста после символа FSI задается первым управляющим символом, не входящим в этот фрагмент текста. Другие управляющие символы не влияют на отображение этого текста. Действие FSI прерывается символом PDI или символом перевода строки. |
PDI |
U+2069 |
POP DIRECTIONAL ISOLATE |
Символ PDI прерывает действие одного из символов LRI, RLI или FSI, встреченного ранее. Прерывает ровно один, последний из встреченных, символ. |
LRM |
U+200E |
LEFT-TO-RIGHT MARK |
Текст после символа LRM отображается слева направо. Действие LRM прерывается символом перевода строки. |
RLM |
U+200F |
RIGHT-TO-LEFT MARK |
Текст после символа RLM отображается справа налево. Действие RLM прерывается символом перевода строки. |
ALM |
U+061C |
ARABIC LETTER MARK |
Текст после символа ALM отображается справа налево. Действие ALM прерывается символом перевода строки. |
ZWSP |
U+200B |
ZERO WIDTH SPACE |
Неотображаемый пробельный символ. Использование символа ZWSP привести к тому, что разные строки будут отображаться одинаково. Например, 'str[ZWSP]ing' отображается как 'string'. |
Рассмотрим следующий фрагмент кода:
#include <iostream>
int main()
{
bool isAdmin = false;
/*[RLO] } [LRI] if (isAdmin)[PDI] [LRI] begin admins only */ // (1)
std::cout << "You are an admin.\n";
/* end admins only [RLO]{ [LRI]*/ // (2)
return 0;
}
Изучим детально строку (1).
[LRI] if (isAdmin)[PDI]
Здесь символ [LRI] действует до символа [PDI]. Строка 'if (isAdmin)' будет отображаться слева направо и считается изолированной, получаем 'if (isAdmin)'.
[LRI] begin admins only */
Здесь символ [LRI] действует до конца строки. Получаем изолированную строку: 'begin admins only */'
[RLO] {пробел1}, '}', {пробел2}, 'if (isAdmin)', 'begin admins only */'
Здесь символ [RLO] действует до конца строки и отображает текст справа налево. Каждая из полученных в предыдущих пунктах изолированных строк рассматривается как отдельный неделимый символ. Получаем такую последовательность:
'begin admins only */', 'if (isAdmin)', {пробел2}, '{', {пробел1}
Обратите внимание, что символ закрывающей фигурной скобки теперь отображается как '{' вместо '}'.
Итоговый вид строки (1), который может быть отображен в редакторе:
/* begin admins only */ if (isAdmin) {
Похожие преобразования затронут и строку (2), которая отобразится так:
/* end admins only */ }
Финальный вид кода, который может отобразиться в редакторе:
#include <iostream>
int main()
{
bool isAdmin = false;
/* begin admins only */ if (isAdmin) {
std::cout << "You are an admin.\n";
/* end admins only */ }
return 0;
}
Ревьювер может посчитать, что в коде выполняется некоторая проверка перед выводом сообщения. Он проигнорирует комментарии и подумает, что код должен выполняться так:
#include <iostream>
int main()
{
bool isAdmin = false;
if (isAdmin) {
std::cout << "You are an admin.\n";
}
return 0;
}
Однако, на самом деле, проверки нет. Для компилятора рассмотренный код выглядит так:
#include <iostream>
int main()
{
bool isAdmin = false;
std::cout << "You are an admin.\n";
return 0;
}
Теперь рассмотрим более простой и в то же время более опасный пример использования неотображаемых символов:
#include <string>
#include <string_view>
enum class BlockCipherType { DES, TripleDES, AES, /*....*/ };
constexpr BlockCipherType
StringToBlockCipherType(std::string_view str) noexcept
{
if (str == "AES[ZWSP]")
return BlockCipherType::AES;
else if (str == "TripleDES[ZWSP]")
return BlockCipherType::TripleDES;
else
return BlockCipherType::DES;
}
Функция 'StringToBlockCipherType' производит конвертацию строки в одно из значений перечисления 'BlockCipherType'. По коду можно сделать вывод, что функция возвращает три разных значения, однако это не так. Из-за того, что в конце каждого строкового литерала дописан неотображаемый пробельный символ [ZWSP], проверки на равенство со строками 'AES' и 'TriplesDES' будут ложными. В итоге из трех ожидаемых возвращаемых значений функция будет возвращать лишь 'BlockCipherType::DES'. В то же время код в редакторе может отображаться следующим образом:
#include <string>
#include <string_view>
enum class BlockCipherType { DES, TripleDES, AES, /*....*/ };
constexpr BlockCipherType
StringToBlockCipherType(std::string_view str) noexcept
{
if (str == "AES")
return BlockCipherType::AES;
else if (str == "TripleDES")
return BlockCipherType::TripleDES;
else
return BlockCipherType::DES;
}
Если анализатор выдал предупреждение о неотображаемых символах на вашем коде, включите отображение невидимых символов в вашем редакторе и убедитесь, что они не изменяют логику выполнения программы.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1076. |
Анализатор обнаружил конструктор, после выполнения которого могут остаться потенциально неинициализированные поля класса.
Рассмотрим простой синтетический пример:
struct Cat
{
int age;
Cat(bool isKitten)
{
if (isKitten)
{
age = 3;
}
}
};
Если при конструировании объекта типа 'Cat' в качестве фактического параметра будет передано значение 'false', то нестатическое поле класса 'age' не будет проинициализировано. Последующий доступ к этому полю приведет к неопределённому поведению:
#include <iostream>
void Cat()
{
Cat instance { false };
std::cout << instance.x << std::endl; // UB
}
Корректный конструктор может выглядеть следующим образом:
Cat(bool isKitten) : age { 0 }
{
if (isKitten)
{
age = 3;
}
}
Если допускается, что какой-либо член класса может остаться неинициализированным после выполнения конструктора, то можно подавить для них предупреждения специальным комментарием "//-V1077_NOINIT":
struct Cat
{
int age; //-V1077_NOINIT
Cat(bool isKitten)
{
if (isKitten)
{
age = 3; // ok
}
}
};
Вы можете подавить предупреждение, отметив конструктор комментарием "//-V1077". Вы также можете применить механизм массового подавления для устранения ложных срабатываний.
Диагностика также поддерживает возможность отключить предупреждения на все поля классов определенного типа. Для этого используется тот же комментарий, что и в диагностике V730 (поиск неинициализированных членов класса в конструкторах).
Рассмотрим формат комментария:
//+V730:SUPPRESS_FIELD_TYPE, class:className, namespace:nsName
Если указать в качестве аргумента параметра 'class' класс с названием 'className', то поля этого типа будут рассматриваться как исключения в диагностике V1077 и V730. Рассмотрим пример:
//+V730:SUPPRESS_FIELD_TYPE, class:Field
struct Field
{
int f;
};
class Test
{
Field someField;
public:
Test(bool cond, int someValue)
{
if (cond)
{
someField.f = someValue; // ok
}
}
};
При указании специального комментария анализатор не будет выдавать предупреждение на поля, имеющие тип 'Field' ('someField' в нашем случае).
Для вложенных классов используется следующий синтаксис:
//+V730:SUPPRESS_FIELD_TYPE, class:className.NestedClassName,
namespace:nsName
Каждый вложенный класс отделяется точкой: "className.NestedClassName".
Мы не стали вводить отдельный комментарий для V1077 по следующим причинам. Если тип отмечен комментарием для V730, значит предполагается, что его экземпляры могут быть не проинициализированы вообще, а значит выдавать для него V1077 тоже бессмысленно. К тому же, если у вас уже есть разметка для V730, она будет работать и для V1077.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, в которой производится попытка обхода пустого контейнера. В результате не произойдет ни одной итерации цикла, что может свидетельствовать об ошибке.
Рассмотрим следующий пример, который может возникнуть вследствие неудачного рефакторинга:
#include <vector>
#include <string_view>
std::vector<std::string_view> GetSystemPaths()
{
std::vector<std::string_view> paths;
#ifdef _WIN32
paths.emplace_back("C:/Program files (x86)/Windows Kits");
paths.emplace_back("C:/Program Files (x86)/Microsoft Visual Studio");
#elif defined(__APPLE__)
paths.emplace_back("/Applications");
paths.emplace_back("/Library");
paths.emplace_back("/usr/local/Cellar");
#elif defined(__linux__)
// TODO: Don't forget to add some specific paths
#endif
return paths;
}
bool IsSystemPath(std::string_view path)
{
static const auto system_paths = GetSystemPaths();
for (std::string_view system_path : system_paths)
{
if (system_path == path)
{
return true;
}
}
return false;
}
Наполнение контейнера 'system_paths' зависит от операционной системы, под которую компилируется приложение. Для операционной системы семейства Linux и всех остальных, кроме Windows и macOS, в результате раскрытия директив препроцессора будет получен пустой контейнер.
В контексте этого примера это нежелательное поведение функции 'GetSystemPaths'. В случае с Linux, для исправления предупреждения, нужно добавить необходимые пути. При компиляции на новую операционную систему (например, FreeBSD) разработчику, возможно, стоит выдать ошибку при помощи static_assert. Возможное исправление кода:
#include <vector>
#include <string_view>
std::vector<std::string_view> GetSystemPaths()
{
std::vector<std::string_view> paths;
#ifdef _WIN32
....
#elif defined(__APPLE__)
....
#elif defined(__linux__)
paths.emplace_back("/usr/include/");
paths.emplace_back("/usr/local/include");
#else
static_assert(false, "Unsupported OS.");
#endif
return paths;
}
В общем случае, если итерирование пустого контейнера было специально задумано программистом, то предупреждение можно подавить.
Диагностическое правило сигнализирует о том, что функция принимает параметр типа 'std::stop_token' и никак не использует его. Такой код потенциально может привести к проблемам.
Начиная со стандарта C++20 в стандартной библиотеке появился класс 'std::jthread'. Это альтернатива классу 'std::thread', которая имеет две новых возможности. Первая — автоматическое присоединение, которое достигается за счёт вызова функций 'request_stop' и 'join' в деструкторе. Вторая — возможность прерывания этого потока при помощи объекта класса 'std::stop_token'. Рассмотрим синтетический пример:
#include <thread>
#include <vector>
struct HugeStruct { .... };
HugeStruct LoadHugeData(std::string_view key);
void worker(std::stop_token st, ....)
{
auto keys = ....;
for (auto key : keys)
{
auto data = LoadHugeData(key);
// Do something with data
}
}
void foo()
{
using namespace std::literals;
std::jthread thread { worker };
// ....
}
Функция производит последовательную загрузку данных большого размера. Реализация имеет возможность прерывания такой операции, однако параметр 'st' для получения сигнала об остановке не используется. Такой код выглядит подозрительно и помечается анализатором как место потенциальной ошибки.
В качестве исправления можно предложить следующий вариант:
#include <thread>
#include <vector>
struct HugeStruct { .... };
HugeStruct LoadHugeData(std::string_view key);
void worker(std::stop_token st, ....)
{
auto keys = ....;
for (auto key : keys)
{
if (st.stop_requested())
{
// Stop execution here
}
auto data = LoadHugeData(key);
// Do something with data
}
}
void foo()
{
using namespace std::literals;
std::jthread thread { worker };
// ....
}
Теперь последовательная загрузка данных может быть прервана. Функция 'worker' прекратит загрузку элементов в случае получения запроса отмены операции (функции 'request_stop') из другого потока.
Диагностическое правило сигнализирует о том, что возвращаемое значение функции 'std::is_constant_evaluated' никогда не изменится. Это может привести к недостижимому коду.
В рамках данного правила будут рассматриваться две ситуации:
Рассмотрим пример: мы хотим реализовать функцию, в которой будет две версии одного алгоритма, для времени компиляции и для времени выполнения. Для разграничения реализаций будет использоваться функция 'std::is_constant_evaluated'.
#include <type_traits>
constexpr void foo()
{
constexpr auto context = std::is_constant_evaluated();
//....
if (context)
{
// compile-time logic
}
else
{
// runtime logic
}
}
В данном случае функция 'std::is_constant_evaluated' всегда возвращает 'true'. В 'else'-ветке находится недостижимый код.
Обратная ситуация может возникнуть, если мы уберём спецификатор 'constexpr' с переменной 'context' и функции 'foo'.
#include <type_traits>
void foo()
{
auto context = std::is_constant_evaluated();
//....
if (context)
{
// compile-time logic
}
else
{
// runtime logic
}
}
Здесь переменная 'context' всегда 'false', и код недостижим уже в 'then'-ветке.
Функция 'std::is_constant_evaluated' всегда вернёт 'true', если вызов происходит:
Функция 'std::is_constant_evaluated' всегда вернёт 'false', если вызов происходит:
Данная диагностика классифицируется как:
Наименьшее отрицательное значение знакового целочисленного типа не имеет соответствующей положительной величины. В результате вычисления модуля числа от такого значения функциями 'abs', 'labs', 'llabs' возникает переполнение. Это приводит к неопределённому поведению.
Рассмотрим пример:
#include <iostream>
#include <cmath>
#include <limits.h>
int main()
{
int min = INT_MIN;
// error: abs(-2147483648) = -2147483648
std::cout << "abs(" << min << ") = "
<< abs(min); // <=
return 0;
}
Минимальное значение 32-битного знакового типа 'int' имеет значение 'INT_MIN', равное -2147483648. При этом максимальное значение 'INT_MAX' равно 2147483647, что на единицу меньше, чем настоящий модуль числа 'INT_MIN'. В данном случае в результате вычисления модуля получилось отрицательное число, равное исходному значению аргумента. Это может привести к ошибке в пограничном случае, когда код не рассчитан на обработку отрицательных чисел, полученных после вычисления модуля числа.
Для остальных значений функция вычисления модуля будет вести себя ожидаемым образом:
int main()
{
int notQuiteMin = INT_MIN + 1;
// ok: abs(-2147483647) = 2147483647
std::cout << "abs(" << notQuiteMin << ") = "
<< abs(notQuiteMin);
return 0;
}
Возможно, перед расчётом модуля стоит сделать специальную проверку значения аргумента, чтобы не допустить возникновения пограничного случая:
void safe_abs_call(int value)
{
if (value == INT_MIN)
return;
std::cout << "abs(" << value << ") = " << abs(value);
}
Срабатывание диагностики можно подавить в случаях, если де-факто диапазон передаваемых функциям 'abs', 'labs' и 'llabs' значений не может достигать минимальной величины.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил функцию, обозначенную как невозвращающую управление, которая тем не менее может вернуть управление на какой-либо из веток выполнения.
Рассмотрим в начале корректный пример:
[[ noreturn ]] void q() {
throw "error"; // OK
}
Функция 'q' прервет своё выполнение броском исключения. Дальнейшее выполнение программы перейдет обработчику исключения, а не вызвавшему эту функцию коду. При этом компилятор понимает, что следующий за вызовом функции 'q' код будет недостижим и его можно оптимизировать.
[[ noreturn ]] void f(int i) { // behavior is undefined
// if called with an argument <= 0
if (i > 0)
throw "positive";
}
То же самое случится при вызове функции 'f' с положительным значением аргумента. Однако если в процессе выполнения программы в функцию 'f' будет передано отрицательное значение или ноль, то согласно стандарту C++ возникнет неопределенное поведение:
9.12.9 Noreturn attribute [dcl.attr.noreturn]
2. If a function f is called where f was previously declared with the noreturn attribute and f eventually returns, the behavior is undefined.
Данная диагностика классифицируется как:
|
Анализатор обнаружил арифметическое выражение, в котором может произойти переполнение знакового числа.
Рассмотрим пример:
long long foo()
{
long longOperand = 0x7FFF'FFFF;
long long y = longOperand * 0xFFFF;
return y;
}
По правилам C и C++ результирующим типом выражения 'longOperand * 0xFFFF' будет 'long'. При использовании компилятора MSVC на Windows тип 'long' имеет размер 4 байта. Максимальное значение, которое может быть представлено этим типом, равно 2'147'483'647 в десятичной системе счисления или 0x7FFF'FFFF в шестнадцатеричной. При умножении переменной 'longOperand' на 0xFFFF (65 535) ожидается результат 0x7FFF'7FFF'0001. Однако согласно стандарту C (см. стандарт С18 пункт 6.5 параграф 5) и C++ (см. стандарт С++20 пункт 7.1 параграф 4) переполнение знаковых чисел приводит к неопределённому поведению.
Исправить этот код можно несколькими способами в зависимости от того, чего хочет программист.
Если требуется произвести корректные вычисления, необходимо использовать типы, размеры которых будут достаточны для отображения чисел. Если число не помещается в машинное слово, то можно воспользоваться одной из библиотек по длинной арифметике. Например, GMP, MPRF, cnl.
Пример выше можно исправить следующим образом:
long long foo()
{
long longOperand = 0x7FFF'FFFF;
long long y = static_cast<long long>(longOperand) * 0xFFFF;
return y;
}
Если переполнение знаковых чисел – это неожидаемое поведение, и его требуется обработать каким-либо образом, то можно воспользоваться специальными библиотеками для безопасной работы с числами. Например, boost::safe_numerics или Google Integers.
Если требуется реализовать циклическую арифметику для знаковых чисел с определённым по стандарту поведением, то для расчётов можно воспользоваться беззнаковыми числами. В случае их переполнения происходит "оборачивание" числа по модулю '2 ^ n', где n – количество бит в числе.
Рассмотрим одно из возможных решений на основе 'std::bit_cast' (C++20):
#include <concepts>
#include <type_traits>
#include <bit>
#include <functional>
namespace detail
{
template <std::signed_integral R,
std::signed_integral T1,
std::signed_integral T2,
std::invocable<std::make_unsigned_t<T1>,
std::make_unsigned_t<T2>> Fn>
R safe_signed_wrapper(T1 lhs, T2 rhs, Fn &&op)
noexcept(std::is_nothrow_invocable_v<Fn,
std::make_unsigned_t<T1>,
std::make_unsigned_t<T2>>)
{
auto uLhs = std::bit_cast<std::make_unsigned_t<T1>>(lhs);
auto uRhs = std::bit_cast<std::make_unsigned_t<T2>>(rhs);
auto res = std::invoke(std::forward<Fn>(op), uLhs, uRhs);
using UR = std::make_unsigned_t<R>;
return std::bit_cast<R>(static_cast<UR>(res));
}
}
Функция 'std::bit_cast' приводит 'lhs' и 'rhs' к соответствующим беззнаковым представлениям. Далее на двух преобразованных операндах выполняется некоторая арифметическая операция. Затем результат расширяется или сужается до нужного результирующего типа и превращается в знаковый.
При таком подходе знаковые числа будут повторять семантику беззнаковых в арифметических операциях, что в свою очередь не будет приводить к неопределённому поведению.
Например, по этой ссылке можно убедиться, что компилятор имеет право оптимизировать код, если видит, что может произойти переполнение знакового числа. Рассмотрим его подробнее:
bool is_max_int(int32_t a)
{
return a + 1 < a;
}
Если 'a' равно 'MAX_INT', то условие 'a + 1 < a' будет равно 'false'. Таким образом часто проверяют не произошло ли переполнение. Однако компилятор генерирует такой код:
is_max_int(int): # @is_max_int(int)
xor eax, eax
ret
Инструкция ассемблера 'xor eax, eax' обнуляет результат выполнения функции 'is_max_int'. В результате последняя всегда возвращает 'true' вне зависимости от значения 'a'. В данном случае это результат неопределённого поведения при переполнении.
В случае применения беззнакового представления такого не происходит:
is_max_int(int): # @is_max_int(int)
cmp edi, 2147483647
sete al
ret
Компилятор сгенерировал код, который честно проверяет условие.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1083. |
Анализатор обнаружил странное сравнение переменной перечисления с числом. Указанное число не входит в диапазон значений перечисления, поэтому такое сравнение не имеет смысла.
Если у перечисления указан нижележащий тип, то с переменной такого перечисления имеет смысл сравнивать только значения, которые можно уместить в этот тип.
Рассмотрим следующий пример:
enum byte : unsigned char {}; // Range: [0; 255]
void foo(byte b1)
{
if (b1 == 256) // logic error : always false
{
//....
}
}
Перечисление 'byte' имеет нижележащий тип 'unsigned char'. Число 256 не вмещается в тип 'unsigned char', поэтому сравнение 'b1 == 256' всегда ложное.
Пример корректного сравнения:
enum byte : unsigned char {}; // Range: [0; 255]
void foo(byte b1)
{
if (b1 == 255) // ok
{
//....
}
}
Более сложным случаем является перечисление без явного указания нижележащего типа.
Для языка C компилятор всегда использует тип 'int' в качестве нижележащего типа. Диапазоном значений перечисления будет весь диапазон 'int'.
Для языка C++ компилятор подставит в качестве нижележащего типа 'int' для строго типизированных перечислений (scoped enum). Диапазоном значений такого перечисления также будет весь диапазон 'int'.
Для обычных перечислений вычисление диапазона значений и нижележащего типа перечисления происходит особым образом. Согласно стандарту С++, компилятор выведет нижележащий тип на основе значений констант перечисления, пытаясь уместить их в следующие типы:
int -> unsigned int -> long -> unsigned long ->
long long -> unsigned long long
При этом внутри выбранного типа компилятор использует минимально необходимое число бит (n), способное уместить весь диапазон констант в перечислении. Такие перечисления смогут обрабатывать диапазон значений [- (2 ^ n) / 2; (2 ^ n) / 2 - 1] для знакового и [0; (2 ^ n) - 1] для беззнакового нижележащего типа соответственно.
Поэтому следующий код на языке C++ содержит ошибку, если используется компилятор, отличный от MSVC (например, GCC или Clang):
enum EN { low = 2, high = 4 }; // Uses 3 bits, range: [0; 7]
void foo(EN en1)
{
if (en1 != 8) // logic error : always true
{
//....
}
}
Согласно стандарту C++, нижележащим типом для этого перечисления выберется 'int'. Внутри этого типа компилятор использует минимальное количество битовых полей, которое сможет вместить в себя все значения enum-констант.
В данном случае для вмещения всех значений (2 = 0b010 и 4 = 0b100) понадобится минимум 3 бита, поэтому переменная типа 'EN' может вместить в себя числа от 0 (0b000) до 7 (0b111) включительно. Число 8 занимает уже четыре бита (0b1000), поэтому в тип 'EN' оно уже не вмещается. Чтобы исправить ошибку, можно явно указать нижележащий тип:
enum EN : int32_t { low = 2, high = 4 };
// Now range is: [−2 147 483 648, 2 147 483 647]
void foo(EN en1)
{
if (en1 != 8) // ok
{
//....
}
}
Не все C++ компиляторы рассчитывают фактический размер перечисления согласно стандарту. Например, MSVC при компиляции C++ кода отходит от стандарта и рассчитывает размер перечисления в целях обратной совместимости по правилам языка C. Поэтому в качестве нижележащего типа MSVC всегда использует тип 'int', если специально не указан иной тип. В таком случае диапазоном значений перечисления будет диапазон 'int'. Поэтому в рассмотренном выше примере нет ошибки, если вы используете MSVC:
enum EN { low = 2, high = 4 };
// MSVC will use int as underlying type
// range is: [−2 147 483 648, 2 147 483 647]
void foo(EN en1)
{
if (en1 != 8) // no logic error
{
//....
}
}
Однако писать такой код не стоит, потому что он будет непереносим на другие компиляторы. Следует явно указать 'int' в качестве нижележащего типа.
Если вы используете только компилятор MSVC и вас не интересует переносимость на другие компиляторы, то вы можете отключить предупреждения диагностики о непереносимости кода с помощью комментария:
//-V1084_TURN_OFF_ON_MSVC
Предупреждения V1084, актуальные для MSVC, останутся.
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, в которой в арифметическом выражении происходит конвертация отрицательного числа к беззнаковому типу. Согласно правилам неявного преобразования C/C++, знаковое число той же размерности, что и беззнаковое, превращается в беззнаковое. При приведении отрицательного числа к беззнаковому типу происходит его "оборачивание" по модулю '(2 ^ n) + 1', где n – количество бит в числе. Такая ситуация не приводит к неопределённому поведению, но может привести к неожиданным результатам.
Рассмотрим пример:
void foo()
{
char *p = (char *) 64;
int32_t a = -8;
uint32_t b = 8;
p = p + a * b;
}
На 32-битной системе в указателе получается 0x0. На 64-битной – 0x0000'0001'0000'0000, что может быть неожиданно для программиста. Давайте разберёмся, почему так происходит.
Переменная 'a' имеет знаковый тип 'int32_t'. Это означает, что её размер — 4 байта и она может принимать значения в диапазоне от -2'147'483'648 до 2'147'483'647. Переменная 'b' имеет тип 'uint32_t'. Она также имеет размер в 4 байта, но, в отличие от переменной 'a', может принимать значения в диапазоне от 0 до 4'294'967'295. Так происходит потому, что старший бит в знаковом числе зарезервирован под знак. Из-за этого ограничения максимальное значение знакового числа вдвое меньше, чем у беззнакового.
По правилам языка C++, если в бинарной операции операнды имеют типы с одинаковым рангом и один из операндов имеет знаковый тип, а другой — беззнаковый, то операнд, который имеет знаковый тип, неявно приводится к беззнаковому.
В выражении 'a * b' типы операндов ('int32_t' и 'uint32_t') имеют одинаковый ранг. Следовательно, операнд 'a', который хранит в себе значение '-8' неявно приводится к беззнаковому типу 'uint32_t'. В результате такого приведения его значение становится равным 4'294'967'288. Далее происходит умножение на переменную 'b', которая хранит значение '8'. Полученный результат, который равен 34'359'738'304, выходит за рамки диапазона возможных значений переменной типа 'uint32_t' и будет обернут по модулю '2 ^ 32'. Таким образом, результат выражения 'a * b' будет равен 34'359'738'304 % 4'294'967'296 = 4'294'967'232.
У оператора сложения 'p + a * b' типы операндов 'char *' и 'uint32_t' соответственно. Согласно стандарту C++, результирующий тип будет 'char *', а результат – сумма левого и правого операндов. При сложении 64 и 4'294'967'232 результат равен 4'294'967'296.
На 32-битной платформе размер указателя равен 4 байтам. Следовательно, его максимальное значение равно 4'294'967'295. Так как число 4'294'967'296 больше, то результат оборачивается по модулю '2 ^ 32', как и в предыдущей операции сложения, и будет равен 4'294'967'296 % 4'294'967'296 = 0. В итоге результат выражения 'p + a * b' равен нулю.
На 64-битной платформе размер указателя равен 8 байтам. И в отличие от 32-битной платформы его максимальное значение куда больше, чем 4'294'967'296. Так как оборачивания не будет происходить, то результат выражения 'p + a * b' равен 4'294'967'296 в десятичной системе или 0x0000'0001'0000'0000 в шестнадцатеричной.
Исправить приведённый выше пример можно, использовав знаковые типы для вычислений:
void foo()
{
char *p = (char *) 64;
int32_t a = -8;
uint32_t b = 8;
p = p + a * static_cast<int32_t>(b);
}
Диагностика не будет сообщать обо всех конвертациях знаковых типов к беззнаковым. Это будет происходить только в тех выражениях, результат вычислений которых будет отличаться от такового при использовании только знаковых типов. Рассмотрим пример:
void foo()
{
unsigned num = 1;
unsigned res1 = num + (-1); // ok
unsigned res5 = num + (-2); //+V1085
unsigned res2 = num - (-1); // ok
unsigned res3 = num * (-1); //+V1085
unsigned res4 = num / (-1); //+V1085
unsigned res6 = num / (-2); // ok
unsigned num2 = 2;
unsigned res7 = num2 / (-1); //+V1085
}
В строчках, отмеченных комментарием 'ok', не будет предупреждения V1085. Приведём результаты вычислений каждого выражения со знаковыми и беззнаковыми вариантами:
num + (signed)(-1) => 1 + (-1) => 0
num + (unsigned)(-1) => 1 + 4294967295 = 0
num + (signed)(-2) => 1 + (-2) => -1
num + (unsigned)(-2) => 1 + 4294967294 = 4294967295
num - (signed)(-1) => 1 – (-1) => 2
num - (unsigned)(-1) => 1 – (4294967295) => 2
num * (signed)(-1) => 1 * (-1) => -1
num * (unsigned)(-1) => 1 * (4294967295) => 4294967295
num / (signed)(-1) => 1 / (-1) => -1
num / (unsigned)(-1) => 1 / 4294967295 => 0
num / (signed)(-2) => 1 / (-2) => 0
num / (unsigned)(-2) => 1 / 4294967294 => 0
num2 / (signed)(-2) => 2 / (-2) => -1
num2 / (unsigned)(-2) => 2 / 4294967294 => 0
В тех местах, где результаты совпадают, предупреждение будет отсутствовать.
Примечание. Рассмотренные проблемы пересекаются с темой переноса приложений с 32-битных на 64-битные системы. См. статью: "Коллекция примеров 64-битных ошибок в реальных программах".
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциально возможную ошибку, связанную с заполнением, копированием или сравнением буферов памяти. Ошибка может приводить к неполной обработке буфера (buffer underflow).
Примечание: ранее данная диагностика была частью другой диагностики – V512, но позже мы решили разделить их. О причинах и последствиях такого решения можно прочитать в специальной заметке.
Это достаточно распространённый вид ошибки, возникающий из-за опечаток или невнимательности. В результате может произойти неполная очистка данных и, как следствие, в дальнейшем использование неинициализированной/повреждённой памяти. Неприятность подобных ошибок заключается в том, что программа долгое время может работать стабильно.
Рассмотрим два примера, взятых из реальных приложений.
Пример N1:
MD5Context *ctx;
....
memset(ctx, 0, sizeof(ctx));
Здесь из-за опечатки нулями заполняется не вся структура, а только её часть. Ошибка в том, что вычисляется размер указателя, а не структуры 'MD5Context'. Корректный вариант кода:
MD5Context *ctx;
....
memset(ctx, 0, sizeof(*ctx));
Пример N2:
#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];
memset(_iContMap, -1, CONT_MAP_MAX);
В данном примере также неверно указан размер заполняемого буфера. Корректный вариант:
#define CONT_MAP_MAX 50
int _iContMap[CONT_MAP_MAX];
memset(_iContMap, -1, CONT_MAP_MAX * sizeof(int));
Ранее диагностическое правило было частью другой диагностики – V512. В целях обеспечения обратной совместимости осталась возможность отключить данную диагностику с помощью специального комментария:
//-V512_UNDERFLOW_OFF
Этот комментарий может быть вписан в заголовочный файл, который включается во все другие файлы. Например, это может быть "stdafx.h". Если вписать этот комментарий в "*.cpp" файл, то он будет действовать только для этого файла.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки использования неинициализированных переменных. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1086. |
Анализатор обнаружил ситуацию, когда верхняя граница диапазона в метке 'case' меньше, чем его нижняя граница. Возможно, это опечатка, из-за которой часть кода может быть недостижима.
В компиляторах GCC и Clang существует расширение Case Ranges, которое позволяет вместо единственного значения метки 'case' указать диапазон константных значений. Такой диапазон будет аналогичен последовательности меток 'case', включая граничные значения:
switch (....)
{
case 1 ... 3:
// Do something
break;
}
// Similar to the previous 'switch' statement
switch (....)
{
case 1:
case 2:
case 3:
// Do something
break;
}
Однако если верхняя граница указанного диапазона будет меньше, чем его нижняя граница, то диапазон будет считаться пустым. Если указать такой диапазон, то при проверке условия управление никогда не сможет быть передано метке. Следовательно, ветка кода может быть недостижимой.
Рассмотрим синтетический пример:
void foo(int i)
{
switch (i)
{
case 1 ... 3:
// First case
break;
case 6 ... 4: // <=
// Second case
break;
case 7 ... 9:
// Third case
break;
}
}
Здесь во второй метке были перепутаны местами константы '4' и '6', из-за чего управление никогда не будет передано метке. Исправленный пример:
void foo(int i)
{
switch (i)
{
case 1 ... 3:
// First case
break;
case 4 ... 6: // <=
// Second case
break;
case 7 ... 9:
// Third case
break;
}
}
Ошибка такого рода может возникнуть при неверном использовании именованных констант или значений, возвращаемых 'constexpr'-функциями. Рассмотрим синтетический пример:
constexpr int for_yourself_min() noexcept { return 1; }
constexpr int for_yourself_max() noexcept { return 3; }
constexpr int for_neighbors_min() noexcept { return 4; }
constexpr int for_neighbors_max() noexcept { return 6; }
void distributeCats(int count)
{
switch (count)
{
case for_yourself_min() ... for_yourself_max():
// Keep for yourself
break;
case for_neighbors_max() ... for_neighbors_min(): // <=
// Give cats to neighbors
break;
default:
// Give cats to a cattery
break;
}
}
Во второй метке из-за опечатки вызовы функций были перепутаны местами, и управление никогда не будет передано метке. Исправленный пример:
constexpr int for_yourself_min() noexcept { return 1; }
constexpr int for_yourself_max() noexcept { return 3; }
constexpr int for_neighbors_min() noexcept { return 4; }
constexpr int for_neighbors_max() noexcept { return 6; }
void distributeCats(int count)
{
switch (count)
{
case for_yourself_min() ... for_yourself_max():
// Keep for yourself
break;
case for_neighbors_min() ... for_neighbors_max(): // <=
// Give cats to neighbors
break;
default:
// Give cats to a cattery
break;
}
}
Однако неправильный диапазон не всегда ведет к недостижимому коду. Если в метке 'case' выше отсутствует 'break', то после выполнения его ветки управление будет передано в 'case' с пустым диапазоном. Синтетический пример:
void foo(int i)
{
switch (i)
{
case 0: // no break
case 3 ... 1:
// First and second case
break;
case 4:
// Third case
default:
// Do something
}
}
Несмотря на достижимость кода, пустой диапазон выглядит странно и бессмысленно. Это может быть опечатка или неправильное раскрытие макросов. Поэтому отсутствие 'break' в метке выше не является исключением для диагностики, а предупреждение будет выдано.
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, при которой объект типа 'std::scoped_lock' конструируется без переданных ему аргументов, т.е. без захвата объектов блокировки. Это может привести к проблемам в многопоточном приложении: состоянию гонки, гонке данных и т.д.
Начиная с C++17, в стандартной библиотеке присутствует шаблон класса 'std::scoped_lock'. Он был внедрен в качестве удобной альтернативы 'std::lock_guard', когда требуется захватить произвольное число объектов блокировки за раз. При этом используется алгоритм, позволяющий избежать взаимных блокировок.
Однако дизайн нового типа содержит определенные недостатки. Рассмотрим объявление одного из его конструкторов:
template <class ...MutexTypes>
class scoped_lock
{
// ....
public:
explicit scoped_lock(MutexTypes &...m);
// ....
};
Конструктор принимает произвольное число аргументов типа 'MutexTypes' (parameter pack). Возможны ситуации, когда parameter pack 'MutexTypes' может быть пустым. Вследствие этого возможно создание RAII-объекта без блокировок:
void bad()
{
// ....
std::scoped_lock lock;
// ....
}
Для исправления стоит инициализировать 'std::scoped_lock' объектом блокировки:
std::mutex mtx;
void good()
{
// ....
std::scoped_lock lock { mtx };
// ....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Диагностическое правило основано на пункте CP.42 CppCoreGuidelines.
Анализатор обнаружил ситуацию, в которой одна из нестатических функций-членов класса 'std::condition_variable' – 'wait', 'wait_for' или 'wait_until' – вызывается без предиката. Это может привести к проблемам: ложному пробуждению потока или его зависанию.
Рассмотрим пример N1, приводящий к потенциальному зависанию:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cond;
void consumer()
{
std::unique_lock<std::mutex> lck { mtx };
std::cout << "Waiting... " << std::endl;
cond.wait(lck); // <=
std::cout << "Working..." << std::endl;
}
void producer()
{
{
std::lock_guard<std::mutex> _ { mtx };
std::cout << "Preparing..." << std::endl;
}
cond.notify_one();
}
int main()
{
std::thread c { consumer };
std::thread p { producer };
c.join();
p.join();
}
В примере есть состояние гонки. Программа может зависнуть, если она выполнится в следующем порядке:
Для исправления следует модифицировать код следующим образом:
Исправленный пример:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cond;
bool pendingForWorking = false; // <=
void consumer()
{
std::unique_lock<std::mutex> lck { mtx };
std::cout << "Waiting... " << std::endl;
cond.wait(lck, [] { return pendingForWorking; }); // <=
std::cout << "Working..." << std::endl;
}
void producer()
{
{
std::lock_guard<std::mutex> _ { mtx };
pendingForWorking = true; // <=
std::cout << "Preparing..." << std::endl;
}
cond.notify_one();
}
int main()
{
std::thread c { consumer };
std::thread p { producer };
c.join();
p.join();
}
Рассмотрим пример N2, в котором может произойти ложное пробуждение:
#include <iostream>
#include <fstream>
#include <sstream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
std::queue<int> queue;
std::mutex mtx;
std::condition_variable cond;
void do_smth(int);
void consumer()
{
while (true)
{
int var;
{
using namespace std::literals;
std::unique_lock<std::mutex> lck { mtx };
if (cond.wait_for(lck, 10s) == std::cv_status::timeout) // <=
{
break;
}
var = queue.front();
queue.pop();
}
do_smth(var);
}
}
void producer(std::istream &in)
{
int var;
while (in >> var)
{
{
std::lock_guard<std::mutex> _ { mtx };
queue.push(var);
}
cond.notify_one();
}
}
void foo(std::ifstream &fin, std::istringstream &sin)
{
std::thread p1 { &producer, std::ref(fin) };
std::thread p2 { &producer, std::ref(sin) };
std::thread p3 { &producer, std::ref(std::cin) };
std::thread c1 { &consumer };
std::thread c2 { &consumer };
std::thread c3 { &consumer };
p1.join(); p2.join(); p3.join();
c1.join(); c2.join(); c3.join();
}
Ложное пробуждение – явление, при котором ожидающий поток пробуждается и обнаруживает, что условие, которое он ожидал, не выполнено. Это может произойти в двух сценариях:
В примере N2 ложное пробуждение может произойти в потоках 'c1', 'c2' и 'c3'. В результате такого пробуждения очередь может оказаться пустой, и доступ к ней приведёт к неопределенному поведению.
Для исправления следует также вызвать перегрузку 'std::condition_variable::wait_for', принимающую предикат. Внутри него нужно проверить, пуста очередь или нет:
void consumer()
{
while (true)
{
int var;
{
using namespace std::literals;
std::unique_lock<std::mutex> lck { mtx };
bool res = cond.wait_for(lck,
10s,
[] { return !queue.empty(); }); // <=
if (!res)
{
break;
}
// no spurious wakeup
var = queue.front();
queue.pop();
}
do_smth(var);
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1089. |
Анализатор обнаружил вызов функции 'std::uncaught_exception'. Применение этой функции может привести к неверной логике программы. Начиная с C++17, она признана устаревшей и должна быть заменена на функцию 'std::uncaught_exceptions'.
Функция 'std::uncaught_exception' обычно применяется для того, чтобы понять, вызывается ли код при раскрутке стека. Рассмотрим пример:
constexpr std::string_view defaultSymlinkPath = "system/logs/log.txt";
class Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName }
{
}
void Log(std::string_view);
~Logger()
{
fileStream.close();
if (!std::uncaught_exception())
{
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};
class Calculator
{
public:
int64_t Calc(const std::vector<std::string> ¶ms);
// ....
~Calculator()
{
try
{
Logger logger("log.txt");
Logger.Log("Calculator destroyed");
}
catch (...)
{
// ....
}
}
}
int64_t Process(const std::vector<std::string> ¶ms)
{
try
{
Calculator calculator;
return Calculator.Calc(params);
}
catch (...)
{
// ....
}
}
В деструкторе класса 'Logger' вызывается функция 'std::filesystem::create_symlink', которая может бросить исключение. Например, если для использования пути 'system/logs/log.txt' у программы недостаточно прав. Если деструктор 'Logger' будет вызван напрямую в результате раскрутки стека, то бросать исключения из этого деструктора нельзя – программа будет аварийно прервана через 'std::terminate'. Поэтому перед вызовом функции программист сделал дополнительную проверку 'if (!std::uncaught_exception())'.
Однако такой код содержит ошибку. Предположим, что функция 'Calc' бросила исключение. Тогда перед выполнением catch-clause произойдёт вызов деструктора 'Calculator'. В нём будет создан экземпляр класса 'Logger', в лог запишется сообщение. Затем будет вызван деструктор 'Logger'. Внутри него произойдёт вызов функции 'std::uncaught_exception'. Эта функция вернёт 'true', потому что исключение, брошенное функцией 'Calc', ещё не перехвачено. Поэтому символическая ссылка для файла с логом не будет создана.
Однако в данном случае можно попробовать создать символическую ссылку. Дело в том, что деструктор 'Logger' будет вызван не напрямую в результате раскрутки стека, а из деструктора 'Calculator'. Поэтому из деструктора 'Logger' можно бросить исключение — нужно только перехватить его до выхода из деструктора 'Calculator'.
Для исправления необходимо воспользоваться функцией 'std::uncaught_exceptions' из C++17:
class Logger
{
std::string m_fileName;
std::ofstream m_fileStream;
int m_exceptions = std::uncaught_exceptions(); // <=
Logger(const char *filename)
: m_fileName { filename }
, m_fileStream { m_fileName }
{
}
~Logger()
{
fileStream.close();
if (m_exceptions == std::uncaught_exceptions())
{
std::filesystem::create_symlink(m_fileName, defaultSymlinkPath);
}
}
};
Теперь при создании объекта класса 'Logger' в поле 'm_exceptions' сохранится текущее количество неперехваченных исключений. Если между созданием объекта и вызовом его деструктора не было брошено новых исключений, то условие будет истинным. Поэтому программа попробует создать символическую ссылку для файла с логом. Если при этом будет брошено исключение, то оно будет перехвачено и обработано в деструкторе 'Calculator', и программа продолжит выполнение.
Данная диагностика классифицируется как:
Анализатор обнаружил приведение указателя к интегральному типу большего размера. Результат может отличаться от ожидаемого программистом.
Согласно стандартам C и C++, результат такого выражения зависит от реализации. Ожидаемый программистом результат в большинстве реализаций будет совпадать тогда, когда указатель приводится к интегральному типу того же размера.
Рассмотрим следующий синтетический пример:
void foo()
{
const void *ptr = reinterpret_cast<const void *>(0x80000000);
uint64_t ui64 = reinterpret_cast<uint64_t>(ptr); // <=
}
В примере указатель 'ptr' преобразуется в тип 'uint64_t', имеющий размер 8 байт. На 32-битной платформе размер указателя равен 4 байтам. Результат такого преобразования зависит от реализации компилятора.
Так, если используется компилятор GCC или MSVC, то в переменную 'ui64' будет записано число 0xffff'ffff'8000'0000. В то же время Clang запишет число 0x0000'0000'8000'0000.
Чтобы преобразовать 32-битный указатель в 64-битное число и избежать поведения, определённого реализацией, нужно сделать следующее:
Для исправления сначала преобразуем указатель к 'uintptr_t'. Это целочисленный беззнаковый тип, размер которого всегда равен размеру указателя. Затем полученное 32-битное число преобразуем к 64-битному. Исправленный код:
void foo()
{
const void *ptr = reinterpret_cast<const void*>(0x80000000);
uint64_t ui64 = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(ptr));
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, в которой при инициализации переменной со static storage duration или thread storage duration происходит цепочка вызовов, приводящая к рекурсии. Согласно стандарту C++, это приводит к неопределенному поведению.
Рассмотрим пример:
int foo(int i)
{
static int s = foo(2*i); // <= undefined behavior
return i + 1;
}
При инициализации переменной 's' происходит рекурсивный вызов функции 'foo'. Анализатор в таком случае выдаст предупреждение V1092.
Чаще к рекурсии может привести цепочка вывозов, как в следующем примере:
int foo(int i);
int bar(int i)
{
return foo(i); // <=
}
int foo(int i)
{
static int s = bar(2*i); // <= V1092
return i + 1;
}
Цепочка вызовов, приводящая к рекурсии, проходит через 'foo -> bar -> foo'.
Анализатор не будет выдавать предупреждение в случае, если цепочка вызовов проходит через недостижимый код. Рассмотрим пример:
int foo();
int bar()
{
if (false)
{
return foo(); // <= unreachable
}
return 0;
}
int foo()
{
static int x = bar(); // <= ok
return x;
}
Цепочка вызовов так же проходит через 'foo -> bar -> foo'. Однако, путь от 'bar' к 'foo' недостижим.
Анализатор обнаружил бессмысленное действие: левый операнд побитово сдвигается вправо на такое количество бит, что в результате всегда получает ноль.
Рассмотрим пример:
void Metazone_Get_Flag(unsigned short* pFlag, int index)
{
unsigned char* temp = 0;
unsigned char flag = 0;
if (index >= 8 && index < 32)
{
temp = (u8*)pFlag;
flag = (*temp >> index) & 0x01; // <=
}
// ....
}
Если обратить внимание на условие оператора 'if', то можно увидеть, что значение переменной 'index' будет лежать в диапазоне [8 .. 31]. Под указателем 'temp' лежит значение типа 'unsigned char'. При операции сдвига левый операнд типа 'unsigned char' вследствие integral promotion расширится до 'int', и старшие биты будут заполнены нулевыми значениями. Соответственно, при сдвиге вправо на большее количество бит, чем было в числе до его преобразования, результатом операции будет 0.
Это значит, что приведённый выше код не имеет практического смысла и скорее всего содержит логическую ошибку или опечатку.
Примечание
Подобное предупреждение может выдаваться для макросов, которые раскрыты для краевых/вырожденных случаев. Другими словами, такие макросы не содержат ошибку и 0 будет являться вполне ожидаемым результатом выражения. Если вы пишете подобный код и не хотите, чтобы анализатор выдавал на него срабатывания, то вы можете подавить их при помощи специального комментария, который содержит имя вашего макроса и номер данного диагностического правила:
//-V:YOUR_MACRO_NAME:1093
Полезные ссылки:
Анализатор обнаружил символьный или строковый литерал, который содержит условную экранирующую последовательность. В такой последовательности за символом обратной косой черты ('\') следует символ, который не принадлежит набору стандартных экранирующих последовательностей.
Пример:
FILE* file = fopen("C:\C\Names.txt", "r");
Программист хочет открыть файл "C:\C\Names.txt". Однако в качестве разделителя директорий используется неэкранированная обратная косая черта, которая в свою очередь начинает экранирующие последовательности '\C' и '\N'. Начиная с версии стандарта C++23, представление таких символов зависит от реализации компилятора. Например, экранирующий '\' может быть проигнорирован, и тогда будет использован следующий за ним символ. В результате получится некорректный путь "C:CNames.txt".
Чтобы исправить такой код, нужно продублировать косую черту:
FILE* file = fopen("C:\\C\\Names.txt", "r");
Другие варианты последовательностей могут иметь специальное значение у разных компиляторов. Или вызывать предупреждения в процессе сборки, например, у Clang и GCC:
warning: unknown escape sequence: '\C'
Подобное поведение, определяемое реализацией, может вызывать проблемы с переносимостью кода. А до C++23 этот момент не был описан в стандарте.
Заметить подобные последовательности глазами трудно, при этом их очень просто допустить из-за опечатки при copy-paste:
....
{ARM_EXT_V6, 0x06500f70, ...., "uqsubaddx%c\t%12-15r, %16-19r, %0-3r"},
{ARM_EXT_V6, 0x06500ff0, ...., "usub16%c\t%12-15r, %16-19r, %0-3r"},
{ARM_EXT_V6, 0x06500f50, ...., "usub8%c\t%12-15r, %16-19r, %0-3r"},
{ARM_EXT_V6, 0x06500f50, ...., "usubaddx%c\t%12-15r, %16-19r, %0-3r"},
{ARM_EXT_V6, 0x06bf0f30, ...., "rev%c\t\%12-15r, %0-3r"}, // <=
{ARM_EXT_V6, 0x06bf0fb0, ...., "rev16%c\t\%12-15r, %0-3r"}, // <=
{ARM_EXT_V6, 0x06ff0fb0, ...., "revsh%c\t\%12-15r, %0-3r"}, // <=
{ARM_EXT_V6, 0xf8100a00, ...., "rfe%23?id%24?ba\t\%16-19r%21'!"}, // <=
{ARM_EXT_V6, 0x06bf0070, ...., "sxth%c\t%12-15r, %0-3r"},
{ARM_EXT_V6, 0x06bf0470, ...., "sxth%c\t%12-15r, %0-3r, ror #8"},
....
В этом примере есть последовательности '\%'.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1094. |
Анализатор обнаружил, что в вызываемую функцию передаётся невалидный дескриптор, имеющий отрицательное значение. Данная диагностика используется только на POSIX-совместимых платформах, поскольку в Windows дескрипторы являются указателями и для них используется диагностика V575.
Рассмотрим синтетический пример:
void Process()
{
int fd = open("path/to/file", O_WRONLY | O_CREAT | O_TRUNC);
char buf[32];
size_t n = read(fd, buf, sizeof(buf)); // <=
// ....
}
Программист забыл проверить результат функции 'open'. Если файл невозможно открыть, то функция 'open' вернёт значение -1, и это некорректное значение дескриптора будет передано в функцию 'read'.
Исправленный пример:
void Process()
{
int fd = open("path/to/file", O_WRONLY | O_CREAT | O_TRUNC);
if (fd < 0)
{
return;
}
char buf[32];
size_t n = read(fd, buf, sizeof(buf));
// ....
}
Рассмотрим другой пример:
static intoss_setformat(ddb_waveformat_t *fmt)
{
// ....
if (fd)
{
close (fd);
fd = 0;
}
fd = open (oss_device, O_WRONLY);
// ....
}
В функцию 'close' может быть передан неверный дескриптор из-за неаккуратной проверки. В таком случае дескриптор со значением 0 тоже может быть валидным и должен быть освобождён. Здесь же мы закрываем дескриптор, если значение 'fd' не равно нулю. Такая ошибка может быть допущена, например, после рефакторинга кода или вследствие незнания программистом того, что неверный дескриптор имеет значение '-1', а не '0'.
Исправим приведённый фрагмент кода:
static intoss_setformat(ddb_waveformat_t *fmt)
{
// ....
if (fd >= 0)
{
close (fd);
fd = -1;
}
fd = open (oss_device, O_WRONLY);
// ....
}
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, в которой внутри 'inline' функции, определённой в заголовочном файле, объявлена статическая переменная. Это может привести к нарушению правила ODR.
Рассмотрим пример:
// sample.h
class MySingleton
{
public:
static MySingleton& GetInstance()
{
static MySingleton Instance; // <=
return Instance;
}
};
Класс 'MySingleton' содержит функцию-член 'GetInstance', который возвращает экземпляр статической переменной 'Instance'. Так как функция определена там же, где и объявлена, компилятор неявно пометит её как 'inline'. В таком случае компоновщик объединит определения функции 'GetInstance' во всех единицах трансляции.
Однако этого не произойдёт в случае объединения определений между модулем исполняемой программы и динамической библиотекой. В результате при выполнении программы будет создано уже два экземпляра статической переменной 'Instance', что нарушает правило ODR.
Чтобы исправить данную ситуацию, нужно разделить объявление и определение метода между заголовочным файлом и файлом с исходным кодом.
Исправленный пример:
// sample.h
class MySingleton
{
public:
static MySingleton& GetInstance();
};
// sample.cpp
MySingleton& MySingleton::GetInstance()
{
static MySingleton Instance;
return Instance;
}
Анализатор обнаружил перенос линии исходного кода (символ '\' в конце строки), который при помощи синтаксиса universal-character-name образует символ Unicode. Согласно стандарту, такой код приводит к неопределённому поведению.
Пример:
void error()
{
auto p = "\\
u0041"; // maybe const char[2] "A" ?
}
В указатель 'p' присваивается строковый литерал, который при помощи последовательности \u0041, образует символ заглавной латинской 'A'. При этом между знаками '\' и 'u' идёт перенос строки, который объединяется ещё одним символом '\'.
В результате второй фазы трансляции линии исходного кода, которые оканчиваются символом '\', должны быть объединены в одну. Это может применяться для повышения читаемости кода, если необходимо разбить на несколько строк макрос или длинный строковый литерал. В результате объединения строки могут сформировать экранированные последовательности. Однако стандарт явно декларирует появление неопределённого поведения, если таким образом формируется universal-character-name:
Except for splices reverted in a raw string literal, if a splice results in a character sequence that matches the syntax of a universal-character-name, the behavior is undefined.
Чтобы избежать такой ситуации, последовательность нужно полностью оставить на одной линии, или перенести на другую:
void ok1()
{
auto p = "\u0041"; // const char[2] "A"
}
void ok2()
{
auto p = "\
\u0041";
}
Анализатор обнаружил потенциально опасное перемещение объекта в ассоциативный контейнер 'std::set' / 'std::map' / 'std::unordered_map' посредством вызова функции 'emplace' / 'insert' . Если элемент с указанным ключом уже существует в контейнере, перемещение может привести к преждевременному освобождению ресурсов.
Рассмотрим следующий пример:
using pointer_type = std::unique_ptr<void, void (*)(void *)>;
std::unordered_map<uintmax_t, pointer_type> Cont;
// Unique pointer should be moved only if
// there is no element in the container by the specified key
bool add_entry(uintmax_t key, pointer_type &&ptr)
{
auto [it, inserted] = Cont.emplace(key, std::move(ptr));
if (!inserted)
{
// dereferencing the potentially null pointer 'ptr' here
}
return inserted;
}
В примере в функцию 'add_entry' передается умный указатель на некоторый ресурс и соответствующий ему ключ. По задумке программиста, умный указатель должен перемещаться в ассоциативный контейнер лишь в том случае, если ранее не было вставки с тем же ключом. Если вставки не произошло, то далее с ресурсом будет произведена некоторая работа через умный указатель.
Однако такой код содержит две проблемы:
Рассмотрим возможные способы исправления проблем.
Начиная со стандарта С++17, для контейнеров 'std::map' и 'std::unordered_map' была добавлена функция 'try_emplace'. Она гарантирует, что если элемент с указанным ключом уже существует, то аргументы функции не будут скопированы или перемещены. Поэтому для контейнеров 'std::map' и 'std::unordered_map' рекомендуется использовать именно эту функцию вместо 'emplace' и 'insert'.
Исправленный код:
using pointer_type = std::unique_ptr<void, void (*)(void *)>;
std::unordered_map<uintmax_t, pointer_type> Cont;
bool add_entry(uintmax_t key, pointer_type &&ptr)
{
auto [it, inserted] = Cont.try_emplace(key, std::move(ptr));
if (!inserted)
{
// dereferencing the 'ptr' here
// 'ptr' is guaranteed to be non-null
}
return inserted;
}
Если функция 'try_emplace' недоступна, то для ассоциативных упорядоченных контейнеров ('std::set', 'std::map') поиск и вставку можно разделить на две операции:
Рассмотрим предыдущий пример, заменив контейнер на 'std::map' и воспользовавшись функциями 'lower_bound' и 'emplace_hint':
using pointer_type = std::unique_ptr<void, void (*)(void *)>;
std::map<uintmax_t, pointer_type> Cont;
// Unique pointer should be moved only if
// there is no element in the container by the specified key
bool add_entry(uintmax_t key, pointer_type &&ptr)
{
bool inserted;
auto it = Cont.lower_bound(key);
if (it != Cont.end() && key == it->first)
{
// key exists
inserted = false;
}
else
{
// key doesn't exist
it = Cont.emplace_hint(it, key, std::move(ptr));
inserted = true;
}
if (!inserted)
{
// dereferencing the 'ptr' here
// 'ptr' is guaranteed to be non-null
}
return inserted;
}
Анализатор может выдавать срабатывания на похожий код:
using pointer_type = std::unique_ptr<void, void (*)(void *)>;
std::map<uintmax_t, pointer_type> Cont;
// Unique pointer should be moved only if
// there is no element in the container by the specified key
bool add_entry(uintmax_t key, pointer_type &&ptr)
{
bool inserted;
auto it = Cont.find(key);
if (it == Cont.end())
{
std::tie(it, inserted) = Cont.emplace(key, std::move(ptr)); // <=
}
else
{
inserted = false;
}
if (!inserted)
{
// dereferencing the 'ptr' here
// 'ptr' is guaranteed to be non-null
}
return inserted;
}
В примере нет ошибки: если элемента по заданному ключу не существует, то гарантированно произойдет вставка. Однако код не оптимален: сначала ведется поиск элемента по ключу, затем поиск повторяется для нахождения позиции вставки внутри функции 'emplace'. Поэтому рекомендуется оптимизировать код одним из описанных ранее способов.
Диагностика имеет два уровня достоверности. Первый уровень выдаётся для move-only объектов, т.е. когда пользовательский тип не имеет конструкторов и операторов копирования. Это значит, при неудачной вставке ресурс может быть освобождён раньше времени. Например, это актуально для типов 'std::unique_ptr' и 'std::unique_lock'. Иначе будет выдан второй уровень достоверности.
Диагностика не работает с типами, у которых отсутствует конструктор перемещения, т.к. в таком случае объекты будут копироваться.
Данная диагностика классифицируется как:
Анализатор обнаружил использование нестатической функции-члена производного класса при инициализации базового класса. Согласно стандарту, такой код приводит к неопределённому поведению.
Рассмотрим пример:
struct Base
{
Base(int);
};
struct Derived : public Base
{
int FuncFromDerived();
Derived() : Base(FuncFromDerived()) {} // <=
};
Конструктор структуры 'Derived' вызывает конструктор базового класса 'Base' в списке инициализации. При этом, в качестве аргумента конструктора передаётся результат функции 'FuncFromDerived', которая принадлежит производной структуре. При создании объекта типа 'Derived' инициализация будет произведена в следующем порядке:
В результате произойдёт вызов функции у структуры, которая не была инициализирована, что нарушает правило стандарта:
Member functions (including virtual member functions, [class.virtual]) can be called for an object under construction.
Similarly, an object under construction can be the operand of the typeid operator ([expr.typeid]) or of a dynamic_cast ([expr.dynamic.cast]).
However, if these operations are performed in a ctor-initializer (or in a function called directly or indirectly from a ctor-initializer) before all the mem-initializers for base classes have completed, the program has undefined behavior.
Данная диагностика классифицируется как:
|
Анализатор обнаружил объявление нестатического поля класса в виде указателя на тип, унаследованный от 'UObject', внутри класса/структуры, который не унаследован от типа 'UObject'. Сборщик мусора Unreal Engine может уничтожить объект, адресуемый этим указателем.
Рассмотрим пример:
class SomeClass
{
UObject *ptr;
};
Одним из ключевых инструментов, используемых в управлении памятью в Unreal Engine, является автоматическая сборка мусора на основе механизма подсчета ссылок. Для этого система рефлексии Unreal Engine отслеживает все классы, унаследованные от класса 'UObject', на наличие жёстких ссылок внутри них.
Жёсткими ссылками в Unreal Engine считаются:
Если класс, не унаследованный от 'UObject', содержит указатель на тип, унаследованный от 'UObject', то сборщик мусора не будет рассматривать его как жёсткую ссылку и может удалить объект в ненужный момент. При этом сборщик мусора не обновит указатель, и тот, в свою очередь, станет висячим.
Для исправления проблемы нужно определить тип связи между объектами – владение или использование – и выбрать нужный тип поля.
Владение. Если класс может наследоваться от 'UObject', пометьте указатель атрибутом 'UPROPERTY()' или воспользуйтесь шаблоном класса 'TSharedObjectPtr'. В противном случае замените указатель на объект типа 'TSharedObjectPtr<....>':
// Approach N1
class SomeClass : public UObject
{
UPROPERTY()
UObject *ptr;
};
// Approach N2
Class SomeClass
{
TSharedObjectPtr<UObject> ptr;
};
Использование. Если связь не подразумевает отношение владения, замените указатель на объект типа 'TWeakObjectPtr<....>':
Class SomeClass
{
TWeakObjectPtr<UObject> ptr;
};
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
Анализатор нашёл виртуальную функцию, имеющую параметр со значением по умолчанию. При этом значения по умолчанию определены в базовом и производном классах и различаются. Такое изменение значения по умолчанию параметра виртуальной функции само по себе не является ошибкой, но может привести к неожиданным результатам при использовании этих классов.
Рассмотрим пример:
struct Base
{
virtual void foo(int i = 0) const noexcept
{
std::cout << "Base::foo() called, i = " << i << std::endl;
}
};
struct Derived : Base
{
void foo(int i = 10) const noexcept override
{
std::cout << "Derived::foo() called, i = " << i << std::endl;
}
};
В базовом классе 'Base' определена виртуальная функция 'foo' с одним параметром 'i' имеющим значение по умолчанию '0'. В классе 'Derived', унаследованном от 'Base', переопределяется виртуальная функция 'foo' и изменяется значение по умолчанию параметра 'i' на '10'.
Давайте разберёмся, к каким проблемам может привести такое переопределение. Допустим, мы используем код следующим образом:
int main()
{
Derived obj;
Base *ptr = &obj;
ptr->foo();
}
В результате выполнения функции 'main' будет выведена неожиданная строка "Derived::foo() called, i = 0". При формировании вызова функции 'foo' компилятор берёт статический тип объекта под указателем 'ptr' - 'Base'. Поэтому в вызов функции будет подставлено значение по умолчанию '0' из базового класса. В то же время переменная 'ptr' в реальности указывает на объект типа 'Derived'. Поэтому будет выполнена виртуальная функция из производного класса.
Для того чтобы избежать подобного поведения, рекомендуется придерживаться одной из стратегий:
Исправленный пример:
struct Base
{
virtual void foo(int i = 0) const noexcept
{
std::cout << "Base::foo() called, i = " << i << std::endl;
}
};
struct Derived : Base
{
void foo(int i) const noexcept override
{
std::cout << "Derived::foo() called, i = " << i << std::endl;
}
};
Замечание. Анализатор не выдает предупреждений на следующий код:
struct Base
{
virtual void foo(int i = 0) const noexcept
{
std::cout << "Base::foo() called, i = " << i << std::endl;
}
};
struct Derived : Base
{
void foo(int i = 0) const noexcept override
{
std::cout << "Derived::foo() called, i = " << i << std::endl;
}
};
Однако мы не рекомендуем писать такой код, потому что его сложнее поддерживать.
Данная диагностика классифицируется как:
Анализатор обнаружил объявление сущности, несоответствующей соглашениям о наименованиях для проектов, основанных на Unreal Engine. Соответствие этому соглашению требуется для корректной работы Unreal Header Tool.
Примечание. Анализатор применяет диагностическое правило только на тех анализируемых файлах, в которых обнаружено включение заголовочных файлов из Unreal Engine. Если вы хотите принудительно применить правило для произвольного файла, воспользуйтесь следующим механизмом.
Далее приведен перечень соглашений, поддерживаемых этим диагностическим правилом.
Имена классов, наследуемых от 'UObject', следует начинать с префикса 'U':
class USomeClass : public UObject
{
....
};
Имена классов, наследуемых от 'AActor', следует начинать с префикса 'A':
class ASomeActor : public AActor
{
....
};
Имена классов, наследуемых от 'SWidget', следует начинать с префикса 'S':
class SSomeWidget : public SWidget
{
....
};
Имена абстрактных классов / интерфейсов следует начинать с префикса 'I':
class IAbstractClass
{
public:
virtual void DoSmth() = 0;
};
Перечисления следует начинать с префикса 'E':
enum class ESomeEnum
{
....
};
Имена шаблонов классов следует начинать с префикса 'T':
template <typename T>
class TClassTemplate
{
....
};
Остальные классы следует начинать с префикса 'F':
class FSimpleClass
{
....
};
Префикс типа и соответствующего ему псевдонима должны совпадать. Псевдоним инстанцированного шаблона следует помечать как конкретную сущность:
// usings
using UGameUIPolicy = USomeClass;
using AAIController = ASomeActor;
using SActorCanvas = SSomeWidget;
using EColorBits = ESomeEnum;
using FArrowSlot = FSimpleClass;
template <typename T>
using TMyArray = TClassTemplate<T>;
using FMyArrayFloat = TClassTemplate<float>;
using FMyArrayInt = TMyArray<int>;
// typedefs
typedef USomeClass UGameUIPolicy;
typedef ASomeActor AAIController;
typedef SSomeWidget SActorCanvas;
typedef ESomeEnum EColorBits;
typedef FSimpleClass FArrowSlot;
typedef TClassTemplate<int> FMyArrayInt;
typedef TClassTemplate<float> FMyArrayFloat;
Анализатор будет выдавать предупреждение на любое нарушение указанных выше соглашений:
class GameUIPolicy: public UObject { .... };
class BoxActor : public AActor { .... };
class WidgetButton : public SWidget { .... };
class Weapon
{
public:
virtual void Shoot() = 0;
};
enum class Texture { .... };
class Enemy { .... };
template <typename T>
class DoubleLinkedList { .... };
typedef DoubleLinkedList<Enemy> EnemyList;
Исправленные примеры:
class UGameUIPolicy: public UObject { .... };
class ABoxActor : public AActor { .... };
class SWidgetButton : public SWidget { .... };
class IWeapon
{
public:
virtual void Shoot() = 0;
};
enum class ETexture { .... };
class FEnemy { .... };
template <typename T>
class TDoubleLinkedList { .... };
typedef DoubleLinkedList<Enemy> FEnemyList;
Анализатор обнаружил фрагмент кода, в котором сравниваются объекты структур, содержащие байты выравнивания.
Рассмотрим синтетический пример:
struct Foo
{
unsigned char a;
int i;
};
void bar()
{
Foo obj1 { 2, 1 };
Foo obj2 { 2, 1 };
auto result = std::memcmp(&obj1, &obj2, sizeof(Foo)); // <=
}
Чтобы понять суть проблемы, надо рассмотреть расположение объектов класса 'C' в памяти:
[offset 0] unsigned char
[offset 1] padding byte
[offset 2] padding byte
[offset 3] padding byte
[offset 4] int, first byte
[offset 5] int, second byte
[offset 6] int, third byte
[offset 7] int, fourth byte
Для того, чтобы корректно и эффективно работать с объектами в памяти, компилятор применяет выравнивание данных. На типовых моделях данных выравнивание типа 'unsinged char' равно 1, а типа 'int' – 4. Это означает, адрес поля 'Foo::i' должен быть кратен 4. Чтобы сделать это, компилятор вставит 3 байта выравнивания после поля 'Foo::a'.
Стандарты C и C++ не уточняют, будут ли занулены байты выравнивания при инициализации объекта. Следовательно, при попытке побайтового сравнения двух объектов с одинаковыми значениями полей при помощи функции 'memcmp' результат может не всегда равняться 0.
Исправить проблему можно несколькими способами.
Способ N1 (предпочтительный). Написать компаратор и сравнивать объекты при помощи него.
Для языка C:
struct Foo
{
unsigned char a;
int i;
};
bool Foo_eq(const Foo *lhs, const Foo *rhs)
{
return lhs->a == rhs->a && lhs->i == rhs->i;
}
Для языка C++:
struct Foo
{
unsigned char a;
int i;
};
bool operator==(const Foo &lhs, const Foo &rhs) noexcept
{
return lhs.a == rhs.a && lhs.i == rhs.i;
}
bool operator!=(const Foo &lhs, const Foo &rhs) noexcept
{
return !(lhs == rhs);
}
Начиная с C++20, код можно упростить, указав компилятору самостоятельно сгенерировать компаратор:
struct Foo
{
unsigned char a;
int i;
auto operator==(const Foo &) const noexcept = default;
};
Способ N2. Предварительно занулять объекты.
struct Foo
{
unsigned char a;
int i;
};
bool Foo_eq(const Foo *lhs, const Foo *rhs)
{
return lhs->a == rhs->a && lhs->i == rhs->i;
}
void bar()
{
Foo obj1;
memset(&obj1, 0, sizeof(Foo));
Foo obj2;
memset(&obj2, 0, sizeof(Foo));
// initialization part
auto result = Foo_eq(&obj1, &obj2);
}
Однако этот способ имеет недостатки:
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с тем, что приоритет операций побитового сдвига выше, чем приоритет побитовых операций '&', '|' и '^'. В результате выражение может давать совсем не тот результат, на который рассчитывал программист.
Рассмотрим пример некорректного кода:
unsigned char foo(unsigned char byte2, unsigned char disp)
{
disp |= byte2 & 0b10000000 >> 6;
return disp;
}
Согласно правилам приоритета операций в языках C и C++, выражение будет вычисляться следующим образом:
( disp |= ( byte2 & ( 0b10000000 >> 6 ) ) )
Побитовый сдвиг вправо маски '0b10000000' в этом случае выглядит подозрительно. Скорее всего, программист ожидал, что результат побитового "И" будет сдвинут на 6.
Корректный вариант:
unsigned char f(unsigned char byte2, unsigned char disp)
{
disp |= (byte2 & 0b10000000) >> 6;
return disp;
}
Общая рекомендация: если в сложном выражении не понятен приоритет операций, то лучше обернуть часть выражения в скобки (CERT EXP00-C, ES.41 CppCoreGuidelines). Даже если скобки окажутся лишними, это нестрашно. Код станет более простым для понимания и будет меньше подвержен ошибкам.
Если вы считаете, что срабатывание ложное, то можно либо подавить его при помощи комментария '//-V1104', либо обернуть в скобки выражение:
// first option
disp |= byte2 & 0b10000000 >> 6; //-V1104
// second option
disp |= byte2 & (0b10000000 >> 6);
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительный код: строковая переменная типа 'std::basic_string' модифицируется с помощью оператора '+='. При этом правым операндом является выражение арифметического типа. Из-за неявных преобразований, проходимых перед вызовом оператора, может быть получен неожиданный результат.
Рассмотрим пример:
void foo()
{
std::string str;
str += 1000; // N1
str += ' ';
str += 4.5; // N2
str += ' ';
str += 400.52; // N3
}
Разработчик хотел сформировать строку, состоящую из трёх чисел. Однако в результате исполнения этого кода получится следующее:
Примечание. Обратите внимание, хоть оба значения — 1000 и 400.52 — не помещаются в 'char', последствия будут разными. В случае 1000 мы имеем дело с сужающим преобразованием. Такой код компилируется, однако может быть некорректен. В свою очередь, преобразование числа с плавающей точкой — 400.52 к типу 'char', согласно стандарту языка, является неопределённым поведением.
Во всех подобных случаях необходимо воспользоваться соответствующими функциями для явного преобразования. Например, функцией 'std::to_string' для конвертации чисел в строки:
void foo()
{
std::string str;
str += std::to_string(1000);
str += ' ';
str += std::to_string(4.5);
str += ' ';
str += std::to_string(400.52);
}
Если же разработчик изначально планировал добавить символ в строку через его числовое представление, то читаемость такого кода однозначно ухудшается. Рекомендуется переписать такой код с применением символьного литерала, содержащего или нужный символ, или escape-последовательность:
void foo()
{
std::string str;
// first option
str += '*';
// second option
str += '\x2A';
}
Диагностическое правило выдаёт срабатывание:
Анализатор обнаружил класс, унаследованный от 'QObject', который не содержит ни одного конструктора, принимающего указатель на 'QObject' в качестве параметра.
Объекты, производные от типа 'QObject', организуются в деревья со связями "родитель – ребёнок". При создании очередного объекта передаётся указатель на объект-родитель. В объекте-родителе создающийся объект добавляется в список дочерних. Благодаря этому, когда произойдёт удаление родительского объекта, также удалятся и все его дочерние объекты.
Поэтому при написании своих классов на основе библиотеки Qt считается хорошей практикой добавлять перегрузку, принимающую указатель на 'QObject'. Анализатор выдаст предупреждение, если не найдёт ни одного конструктора, который принимает такой указатель.
Рассмотрим пример:
class MyCounter : public QObject
{
Q_OBJECT;
public:
MyCounter (int startValue);
};
В классе 'MyCounter' отсутствуют конструкторы, принимающие указатель на 'QObject'. Это можно исправить данным способом:
сlass MyCounter : public QObject
{
Q_OBJECT;
public:
MyCounter (int startValue, QObject *parent = nullptr); // ok
};
Примечание. В силу специфики некоторых проектов анализатор не будет выдавать предупреждение, если будет найден хотя бы один конструктор, принимающий родителя.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
Анализатор обнаружил декларацию функции с неуточнённым количеством параметров и её вызов с ненулевым количеством аргументов. Такой вызов может свидетельствовать об ошибке в коде. Возможно, планировалось вызвать другую функцию с похожим именем.
В языке С существует возможность объявить функцию с неуточнённым количеством параметров:
void foo();
Может показаться, что такая декларация объявляет функцию, не принимающую параметров, как в C++. Однако это не так, и следующий код успешно скомпилируется:
void foo();
void bar()
{
foo("%d %d %d", 1, 2, 3); // No compiler checks
}
При декларации функции 'foo' программист мог подразумевать одно из следующих поведений.
Вариант N1. Функция 'foo' не должна была принимать параметров, и компилятор должен был выдать ошибку. В таком случае до C23 в декларации функции следует явно указать 'void' в списке параметров:
void foo(void);
void bar()
{
foo("%d %d %d", 1, 2, 3); // Compile-time error
}
Вариант N2. Функция 'foo' является вариативной и может принимать переменное количество параметров. В таком случае следует явно во время декларации указать эллипсис ('...').
void foo1(const char *, ...); // since C89
void foo2(...); // since C23
void bar()
{
foo1("%d %d %d", 1, 2, 3); // ok since C89
foo2("%d %d %d", 1, 2, 3); // ok since C23
}
Примечание. Начиная с C23, компиляторы обязаны рассматривать следующие декларации как декларации функций, не принимающих параметров:
void foo(); // Takes no parameters
void bar(void); // Takes no parameters
Анализатор знает об этом поведении и не выдаёт срабатывания для таких деклараций, начиная с C23.
Анализатор обнаружил нарушение ограничений, наложенных пользователем на параметр функции.
Механизм пользовательских аннотаций в формате JSON позволяет предоставить анализатору больше информации о типах и функциях. В том числе, позволяет установить ограничения на параметры аннотируемой функции.
Например, если вы хотите, чтобы анализатор сообщал вам, когда в функцию передаётся отрицательное значение или ноль, то ваша аннотация может выглядеть так:
{
"version": 1,
"annotations": [
{
"type": "function",
"name": "my_constrained_function",
"params": [
{
"type": "int",
"constraint": {
"disallowed": [ "..0" ]
}
}
]
}
]
}
При подключении файла с такой аннотацией на следующем коде появится срабатывание V1108:
void my_constrained_function(int);
void caller(int i)
{
if (i < 0)
{
return;
}
my_constrained_function(i); // <=
}
В данном случае программист совершил ошибку — перепутал оператор '<' с оператором '<='. Однако благодаря ограничениям в аннотации анализатор знает, что в функцию 'my_constrained_function' не должны передаваться отрицательные значения или ноль.
Исправленный код:
void my_constrained_function(int);
void caller(int i)
{
if (i <= 0)
{
return;
}
my_constrained_function(i);
}
Анализатор обнаружил вызов устаревшей функции. Такая функция может не поддерживаться или быть убрана в следующих версиях библиотеки.
Рассмотрим вызов функции 'UpdateTraceW' из WinAPI, которая обновляет параметр свойства указанного сеанса трассировки событий:
....
status = UpdateTraceW((TRACEHANDLE)NULL, LOGGER_NAME, pSessionProperties);
....
Согласно документации, эта функция устарела, и её следует заменить на 'ControlTraceW':
....
status = ControlTraceW((TRACEHANDLE)NULL, KERNEL_LOGGER_NAME,
pSessionProperties, EVENT_TRACE_CONTROL_QUERY);
....
Данное диагностическое правило имеет информацию об устаревших функциях из следующих библиотек: WinAPI, GLib.
Если вам необходимо самостоятельно разметить нежелательные функции, то вы можете воспользоваться механизмом аннотирования функций и диагностическим правилом V2016.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V1109. |
Анализатор обнаружил унаследованный от 'QObject' класс, конструктор которого принимает в качестве параметра указатель на 'QObject', но не использует его для организации связи "родитель — ребёнок".
Производные от типа 'QObject' объекты организуются в деревья со связями "родитель — ребёнок". При создании очередного объекта передаётся указатель на объект-родитель. В объекте-родителе создающийся объект добавляется в список дочерних. Благодаря этому, когда произойдёт удаление родительского объекта, также удалятся и все его дочерние объекты.
Рассмотрим пример:
class BadExample : public QObject
{
public:
BadExample(QObject *parent) : ui(new UI::BadExample)
{
ui->setupUi(this);
};
};
В конструктор класса 'BadExample' передаётся указатель на родительский объект. Однако он не передаётся в конструктор базового класса и не используется внутри тела конструктора.
Исправленный пример:
class GoodExample1 : public QObject
{
public:
GoodExample1(QObject *parent)
: QObject (parent), ui(new UI::GoodExample)
{
/*....*/
};
};
Анализатор не выдаст предупреждение в следующих случаях.
Случай N1. Параметр безымянный. Это значит, что программист не хочет отдавать управление объектом в систему отслеживания Qt:
class GoodExample2 : public QObject
{
public:
GoodExample2(QObject *) { /* .... */ };
};
Случай N2. Параметр сознательно не используется для организации связи "родитель — ребёнок". В конструктор родительского класса явно передаётся нулевой указатель:
class GoodExample3 : public QObject
{
public:
GoodExample3(QObject *parent) : QObject { nullptr } { /* .... */ };
};
Случай N3. Параметр передаётся в функцию QObject::setParent:
class GoodExample4 : public QObject
{
public:
GoodExample4(QObject *parent)
{
setParent(parent);
};
};
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки утечек памяти, незакрытых файловых дескрипторов и дескрипторов сетевых соединений. |
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, которая может привести к выходу за границу массива. В коде ранее присутствуют проверки индекса, однако на указанной строке контейнер использует индекс без проверки.
Рассмотрим синтетический пример:
#define SIZE 10
int buf[SIZE];
int do_something(int);
int some_bad_function(int idx)
{
int res;
if (idx < SIZE)
{
res = do_something(buf[idx]);
}
// ....
res = do_something(buf[idx]); // <=
return res;
}
В данном примере, если в функцию придёт значение больше или равное 'SIZE', то, несмотря на проверку, после неё произойдёт выход за границу массива.
Следует как минимум добавить дополнительную проверку:
int some_good_function(int idx)
{
int res;
if (idx < SIZE)
{
res = do_something(buf[idx]);
}
// ....
if (idx < SIZE)
{
res = do_something(buf[idx]); //ok
}
return res;
}
Примечание: в диагностическом правиле реализованы несколько исключений, которые добавлены для уменьшения количества ложных срабатываний. Для выдачи предупреждения должны быть выполнены следующие условия:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительное сравнение, в котором типы выражений имеют одинаковый ранг, но разные знаки. При этом ранги типов меньше, чем у 'int'. Из-за неявного преобразования таких выражений к типу 'int' или 'unsigned int' сравнение может приводить к неожиданным результатам.
Рассмотрим синтетический пример:
bool foo(char lhs, unsigned char rhs)
{
return lhs == rhs; // <=
}
В примере присутствует сравнение переменных с типами разной знаковости: 'lhs' типа 'char' и 'rhs' типа 'unsigned char'. Будем считать, что нижележащий тип 'char' — это 'signed char' (например, на архитектуре x86_64). Тип 'unsigned char' может отразить диапазон значений от [0 .. 255], при этом тип 'char' – [-128 .. 127]. Согласно стандартам C и C++, перед сравнением значений переменных должно произойти неявное преобразование типов (integral promotion), в результате которого и может возникнуть проблема.
Компилятор превращает код со сравнением в следующий:
return (int) lhs == (int) rhs;
Такое преобразование он делает, если тип 'int' может отобразить диапазон значений 'char' и 'unsigned char', иначе вместо 'int' выберется 'unsigned int'. На большинстве современных платформ тип 'int' занимает 4 байта и с лёгкостью может отобразить эти диапазоны.
В случае, если 'lhs' имел отрицательное значение, то оно же и сохранится в результате преобразования в левом операнде. При этом значение правого операнда после преобразования 'rhs' всегда будет неотрицательным, т.к. исходный тип был беззнаковым. В итоге результат сравнения будет вычисляться как 'false'. Возможна и обратная ситуация. Если в переменной 'rhs' находится значение в диапазоне [128 .. 255], то в этой ситуации результат сравнения будет также 'false'.
Такая ошибка может неожиданно проявить себя при смене компилятора или настроек, когда ранее тип 'char' был беззнаковым, а стал знаковым, и наоборот. Например, в таком случае при вызове функции 'foo' с аргументами '\xEE' и '\xEE' будет считаться, что переданы неравные значения. Хотя такое поведение логично с точки зрения стандарта, оно может быть неожиданным для разработчика.
Ошибки можно избежать двумя способами.
Способ N1. Преобразовать выражения к общему типу по знаку:
if ((unsigned char) lhs == rhs)
Способ N2. Воспользоваться семейством функций 'std::cmp_*' (С++20) или их аналогами для сравнения выражений, типы которых имеют различную знаковость:
if (std::cmp_equal(lhs, rhs))
Примечание. В диагностическом правиле реализованы несколько исключений, которые добавлены для уменьшения количества ложных срабатываний. Анализатор выдаёт срабатывание лишь в том случае, если он смог доказать, что диапазон значений одного из операндов не может быть отображён типом другого операнда. Если же требуется выявить все места в коде, где происходит такое сравнение операндов, разных по знаковости, можно воспользоваться следующим комментарием:
//+V1112, ENABLE_ON_UNKNOWN_VALUES
По этой причине анализатор не выдаёт срабатывание на приведённом ранее синтетическом примере без этой настройки.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительный код: в функцию 'memset' передаётся адрес указателя, который ссылается на динамически выделенную память. Такой код может привести к утечке памяти после использования функции 'memset'.
Рассмотрим следующую ситуацию. Допустим, что в проекте существовал следующий корректно работающий код:
void foo()
{
constexpr size_t count = ....;
char array[count];
memset(&array, 0, sizeof(array));
....
}
На стеке создаётся массив, а затем его содержимое зануляется при помощи функции 'memset'. В исходном примере нет ошибок: первым аргументом передаётся адрес массива, а третьим аргументом – реальный размер массива в байтах.
Чуть позже программист по каким-либо причинам заменил аллокацию буфера со стека на кучу:
void foo()
{
constexpr size_t count = ....;
char *array = (char*) malloc(count * sizeof(char));
....
memset(&array, 0, sizeof(array)); // <=
....
}
При этом он не изменил вызов функции 'memset'. Это означает, что теперь первым аргументом передаётся адрес указателя на стеке функции, а третьим аргументом — его размер. Это приводит к занулению указателя вместо содержимого массива и утечке памяти.
Пример исправленного кода:
void PointerFixed()
{
....
constexpr size_t count = ....;
char *array = (char*) malloc(count * sizeof(char));
....
memset(array, 0, count * sizeof(char));
....
}
Теперь первым параметром передаётся адрес участка памяти на куче, а третьим параметром — его размер.
Анализатор обнаружил подозрительное использование оператора преобразования типов при работе с COM-интерфейсами.
При такой работе может возникнуть ряд проблем. Вот некоторые из них:
Правильным способом работы c COM-интерфейсами является использование функции 'QueryInterface', которая специально предназначена для этого. Причина в том, что COM – независимая от языка программирования технология. COM-объекты могут быть реализованы на языках программирования, отличных от C++, а также находиться в адресных пространствах, отличных от адресного пространства вызывающего процесса.
Функция 'QueryInterface' должна:
Рассмотрим синтетический пример ошибки. Допустим, в коде есть COM-интерфейсы 'IDraw' и 'IShape', которые отвечают за работу с некой геометрической фигурой:
interface IDraw : public IUnknown
{
....
virtual HRESULT Draw() = 0;
....
};
interface IShape : public IUnknown
{
....
virtual HRESULT GetArea(double *area) = 0;
....
};
Также у нас есть COM-объект 'Circle', который имплементирует интерфейсы 'IDraw' и 'IShape':
class Circle : public IDraw, public IShape
{
....
};
Теперь рассмотрим пример неправильной работы с нашим COM-объектом через интерфейс 'IDraw':
void foo(IDraw *ptrIDraw)
{
IShape *ptrIShape = dynamic_cast<IShape*>(ptrIDraw);
....
if (ptrIShape)
ptrIShape->GetArea(area);
....
}
В примере выше не произойдет увеличение счётчика ссылок на объект типа 'Circle'. Для увеличения и уменьшения счётчика должны зваться функции 'AddRef' и 'Release' соответственно. Поэтому будет правильно воспользоваться функцией 'QueryInterface', которая сама контролирует механизм счётчика ссылок.
Таким образом, корректный код выглядит так:
void foo(IDraw *ptrIDraw)
{
IShape *ptrIShape = nullptr;
....
if (SUCCEEDED(ptrIDraw->QueryInterface(IID_IShape, &ptrIShape))
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил функцию, проаннотированную как чистая, которая таковой не является.
Функции могут быть проаннотированы следующими способами:
Функция является чистой, если соответствует следующим требованиям:
Наиболее частые случаи нарушения чистоты функций:
Рассмотрим пример нечистой функции, но проаннотированной как чистая:
[[gnu::pure]] void foo()
{
int *x = new int;
....
}
Функция 'foo' проаннотирована в коде атрибутом 'gnu::pure'. При этом функция выделяет динамическую память в программе и нарушает требование об отсутствии побочных эффектов.
Для исправления необходимо либо убрать атрибут 'pure', либо исправить функцию следующим образом:
[[gnu::pure]] void foo()
{
int x;
....
}
Анализатор обнаружил создание объекта исключения без поясняющего сообщения. Такой подход может привести к недостаточному логированию и затруднить процесс выявления ошибок.
Рассмотрим следующий пример:
void DoSomething(const char *val)
{
if (!val) throw std::runtime_error { "" };
// do something...
}
void foo()
{
const char *val = ....;
try
{
DoSomething(val);
}
catch(std::runtime_error &err)
{
std::cerr << err.what() << std::endl;
}
}
В функции 'DoSomething' выбрасывается исключение 'std::runtime_error', которое будет обработано в функции 'foo'. В процессе обработки ожидается, что в 'std::cerr' попадёт информация о причине создания исключения. Вместо этого в 'std::cerr' будет выдана пустая строка, что затруднит процесс обнаружения проблемы.
Корректный вариант может выглядеть так:
void DoSomething(const char *val)
{
if (!val)
{
throw std::runtime_error{ "[DoSomething]: "
"the poiner 'val' is nullptr." };
}
// do something...
}
Анализатор сгенерирует предупреждение V1116, если обнаружит создание стандартного исключения без поясняющего сообщения. Если вы хотите обнаруживать подобные ситуации для пользовательских исключений, то можете воспользоваться механизмом пользовательских аннотаций.
Анализатор обнаружил в коде на языке C определение типа функции с использованием квалификаторов const
или volatile
. Согласно пункту 10 параграфа 6.7.4.1 стандарта С23, при использовании таких типов поведение не определено.
Пример кода, на котором анализатор сгенерирует предупреждения:
typedef int fun_t(void);
typedef const fun_t const_qual_fun_t; // V1117
typedef const fun_t * ptr_to_const_qual_fun_t; // V1117
void foo()
{
const fun_t c_fun_t; // V1117
const fun_t * ptr_c_fun_t; // V1117
}
Для правильной работы программы следует убрать квалификатор const
при определении типа функции. Таким образом, корректный код выглядит так:
typedef int fun_t(void);
typedef fun_t const_qual_fun_t; // ok
typedef fun_t * ptr_to_const_qual_fun_t; // ok
void foo()
{
fun_t c_fun_t; // ok
fun_t * ptr_c_fun_t; // ok
}
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. В функцию передается объект, представляющий собой класс или структуру. Этот объект передается по значению, но при этом не модифицируется, так как имеется ключевое слово const. Возможно, рационально передавать этот объект с помощью константной ссылки в языке Си++. Или с помощью указателя в языке Си.
Пример:
bool IsA(const std::string s)
{
return s == A;
}
При вызове этой функции произойдет вызов конструктора копирования для класса std::string. Если подобное копирование объектов происходит часто, то это может существенно снижать производительность приложения. Данный код можно легко оптимизировать, добавив ссылку:
bool IsA(const std::string &s)
{
return s == A;
}
Анализатор не выдаёт предупреждение, если по значению передаётся простая структура данных (POD), размером не больше размера указателя. Если передавать такую структуру по ссылке, то никакого выигрыша не будет.
Дополнительные ресурсы:
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. В коде программы имеется структура данных, которая может приводить к неэффективному использованию оперативной памяти.
Рассмотрим пример структуры, о которой анализатор сообщит как о неэффективной:
struct LiseElement {
bool m_isActive;
char *m_pNext;
int m_value;
};
Данная структура в 64-битном коде займет 24 байта, что связано с выравниванием данных. Но если поменять последовательность полей, то ее размер составит всего 16 байт. Оптимизированный вариант структуры будет выглядеть так:
struct LiseElement {
char *m_pNext;
int m_value;
bool m_isActive;
};
Конечно, перестановка полей не всегда возможна или необходима. Но если подобные структуры используются миллионами, то есть смысл задуматься об оптимизации расходуемой памяти. Дополнительно сокращение размера структур может повысить производительность приложения, так как при таком же количестве элементов, будет требоваться меньшее количество обращений к памяти.
Отметим, что приведенная структура всегда занимает 12 байт в 32-битной программе, в какой бы последовательности не располагались поля. Поэтому, при проверке 32-битной конфигурации, сообщение V802 выдано не будет.
Естественно бывают и обратные ситуации, когда размер структуры можно оптимизировать в 32-битной конфигурации и нельзя в 64-битной. Рассмотрим пример такой структуры:
struct T_2
{
int *m_p1;
__int64 m_x;
int *m_p2;
}
Эта структура в 32-битной программе из-за выравнивания будет занимать 24 байта. Если же переставить поля, как показано ниже, то она будет занимать только 16 байт.
struct T_2
{
__int64 m_x;
int *m_p1;
int *m_p2;
}
В 64-битной программе расположение полей в структуре 'T_2' не имеет значения. В любом случае она займет 24 байта.
Метод сокращения объема структур достаточно прост. Достаточно расположить поля в порядке убывания их размера. В этом случае поля начнут располагаться без лишних зазоров. Например, возьмем следующую структуру размером 40 байт в 64-битной программе:
struct MyStruct
{
int m_int;
size_t m_size_t;
short m_short;
void *m_ptr;
char m_char;
};
Простой сортировкой последовательности полей по убыванию размера:
struct MyStructOpt
{
void *m_ptr;
size_t m_size_t;
int m_int;
short m_short;
char m_char;
};
мы получим из нее структуру размером 24 байт.
Анализатор не всегда выдает сообщение о неэффективности структур, так как старается сократить количество излишних предупреждений. Например, анализатор не выдает предупреждение на сложные классы, являющимися наследниками, поскольку такие объекты обычно создаются в малом количестве. Пример:
class MyWindow : public CWnd {
bool m_isActive;
size_t m_sizeX, m_sizeY;
char m_color[3];
...
};
Размер данной структуры может быть сокращен, но это не имеет практического смысла.
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. В коде программы итератор изменяется посредством постфиксного оператора инкремента/декремента. Так как предыдущее значение итератора не используется, то постфиксный итератор можно заменить префиксным. В ряде случаев префиксный итератор будет работать быстрее постфиксного, особенно в Debug-версиях программы.
Пример:
std::vector<size_t>::const_iterator it;
for (it = a.begin(); it != a.end(); it++)
{ ... }
Более быстрый вариант:
std::vector<size_t>::const_iterator it;
for (it = a.begin(); it != a.end(); ++it)
{ ... }
Префиксный оператор инкремента изменяет состояние объекта и возвращает себя в уже изменённом виде. Оператор префиксного инкремента в классе итератора для работы с std::vector может выглядеть так:
_Myt& operator++()
{ // preincrement
++_Myptr;
return (*this);
}
В случае постфиксного инкремента ситуация сложнее. Состояние объекта должно измениться, но при этом возвращено предыдущее состояние. Возникает дополнительный временный объект:
_Myt operator++(int)
{ // postincrement
_Myt _Tmp = *this;
++*this;
return (_Tmp);
}
Если мы хотим только увеличить значение итератора, то получается, что префиксная форма предпочтительна. Поэтому, один из советов по микро-оптимизации программ писать "for (it = a.begin(); it != a.end(); ++it)" вместо "for (it = a.begin(); it != a.end(); it++)". В последнем случае происходит создание ненужного временного объекта, что снижает производительность.
Более подробно все это можно почитать в книге Скотта Мейерса "Наиболее эффективное использование С++. 35 новых рекомендаций по улучшению ваших программ и проектов" (Правило 6. Различайте префиксную форму операторов инкремента и декремента) [1].
Также в заметке "Есть ли практический смысл использовать для итераторов префиксный оператор инкремента ++it, вместо постфиксного it++?" [2] можно познакомиться с примерами замера скорости.
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. В одном выражении дважды вычисляется длина одной и той же строки. Для вычисления длины используются такие функции, как strlen, lstrlen, _mbslen и так далее. Если данное выражение вычисляется много раз или строки имеют большую длину, то данный участок кода рационально оптимизировать.
Для оптимизации можно предварительно вычислить длину строки и поместить её во временную переменную.
Рассмотрим пример:
if ((strlen(directory) > 0) &&
(directory[strlen(directory)-1] != '\\'))
Скорее всего, данный код обрабатывает только одну строку и его оптимизировать не надо. Но если код вызывается очень часто, то его следует переписать. Улучшенный вариант кода:
size_t directoryLen = strlen(directory);
if ((directoryLen > 0) && (directory[directoryLen-1] != '\\'))
Иногда предупреждение V804 помогает выявить гораздо более критические ошибки. Рассмотрим пример:
if (strlen(str_1) > 4 && strlen(str_1) > 8)
Здесь используется некорректное имя переменной. Код должен выглядеть следующим образом:
if (strlen(str_1) > 4 && strlen(str_2) > 8)
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. Для того чтобы определить строка пустая или нет, используется функция strlen или аналогичная ей.
Пример кода:
if (strlen(strUrl) > 0)
Этот код корректен, однако если он используется внутри длинного цикла или если мы работаем с длинными строками, то такая проверка может быть неэффективна. Для того чтобы проверить, пустая строка или нет, нам достаточно сравнить первый символ строки с 0. Оптимизированный вариант:
if (strUrl[0] != '\0')
Иногда предупреждение V805 помогает выявить избыточный код. В одном из приложений, был найден приблизительно такой код:
string path;
...
if (strlen(path.c_str()) != 0)
Скорее всего, такой код появился в ходе неаккуратного рефакторинга, когда тип переменной path был заменен с простого указателя на std::string. Упрощенный и быстрый вариант:
if (!path.empty())
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. Длина строки, находящейся в контейнере, вычисляется с помощью функции strlen() или аналогичной ей. Это лишнее действие, поскольку контейнер имеет специальную функцию для получения длины строки.
Рассмотрим пример:
static UINT GetSize(const std::string& rStr)
{
return (strlen(rStr.c_str()) + 1 );
}
Этот код взят из реального приложения. Обычно такие забавные фрагменты кода получаются в процессе неаккуратного рефакторинга. Этот код медленен и более, того, он возможно вообще не нужен. Можно просто в нужных местах писать выражение "string::length() + 1".
Если всё-таки хочется сделать специальную функцию, для получения длины строки вместе с терминальным нулём, то она должна выглядеть так:
inline size_t GetSize(const std::string& rStr)
{
return rStr.length() + 1;
}
Следует помнить, что действия "strlen(MyString.c_str())" и "MyString.length()" не всегда дают одинаковый результат. Различия будут в том случае, если строка содержит нулевые символы помимо терминального нуля. Однако подобные ситуации можно считать плохим дизайном и сообщение V806 является поводом задуматься над рефакторингом. Даже если программист, написавший такой код, хорошо знает принципы его работы, этот код будет сложен для понимания его коллегам. Коллеги будут гадать, почему написано именно так и могут заменить вызов функции "strlen()" на "length()", внеся ошибку в программу. Следует не лениться и сделать код таким, чтобы принципы его работы были понятны стороннему программисту. Например, если в строке могут быть нулевые символы, то, скорее всего, это вовсе не строка, а массив байт. И тогда нужно использовать класс std::vector или завести свой собственный класс.
Анализатор обнаружил код, который потенциально можно оптимизировать. В коде присутствуют однотипные цепочки вызовов (message chains) для доступа, к какому-то объекту.
Под цепочкой вызовов понимаются следующие конструкции:
Если цепочка вызовов повторяется более двух раз, то возможно следует провести рефакторинг кода.
Рассмотрим пример:
Some->getFoo()->doIt1();
Some->getFoo()->doIt2();
Some->getFoo()->doIt3();
Если функция 'getFoo()' работают медленно или если этот код находится внутри цикла, то тогда код стоит переписать. Например, можно создать временный указатель:
Foo* a = Some->getFoo();
a->doIt1();
a->doIt2();
a->doIt3();
Конечно, так написать можно не всегда. И тем более, не всегда такой рефакторинг даст выигрыш в производительности. Слишком много различных вариантов существует, чтобы давать общие рекомендации.
Однако, наличие цепочек вызовов как правило говорит о неаккуратном коде. Для улучшения такого кода можно использовать несколько вариантов рефакторинга:
Анализатор обнаружил код, который можно упростить. В коде функции присутствуют локальные переменные, которые нигде не используются.
Анализатор выдает предупреждение в следующих случаях:
Анализатор не выдаёт предупреждения, если создаются переменные простых типов. С этим хорошо справляется компилятор. Также это помогает избежать множества ложных срабатываний.
Рассмотрим пример:
void Foo()
{
int A[100];
string B[100];
DoSomething(A);
}
Массив элементов типа 'string' объявлен, но не используется. Под массив выделяется память, вызываются конструкторы и деструкторы. Для оптимизации достаточно просто удалить объявление неиспользуемой локальной переменной или массива. Пример корректного кода:
void Foo()
{
int A[100];
DoSomething(A);
}
Анализатор обнаружил код, который можно упростить. Функция 'free()' и оператор 'delete' корректно обрабатывают нулевой указатель. Поэтому можно удалить проверку, в которой проверяется указатель.
Рассмотрим пример:
if (pointer != 0)
delete pointer;
В данном случае проверка является избыточной, поскольку оператор 'delete' корректно обработает нулевой указатель. Пример исправленного кода:
delete pointer;
Конечно, данное исправление нельзя назвать настоящей оптимизацией. Однако это позволяет удалить лишнюю строку, что сделает код короче и легче для понимания.
Есть только один случай, когда проверка указателя имеет смысл. Если функция 'free()' или оператор 'delete' вызывается ОЧЕНЬ много раз. И при этом, ПОЧТИ ВСЕГДА указатель равен нулю. Если в пользовательском коде будет проверка, то не произойдет вызов системных функций. Это может даже чуть сократить время работы.
Но на практике, нулевой указатель почти всегда свидетельствует о какой-то ошибке. В нормальном режиме работы программы указатели в 99.99% случаев будут ненулевыми. Поэтому проверку лучше удалить.
Стоит отметить, что данное предупреждение применимо и для других функций, которые корректно обрабатывают переданный нулевой указатель, например 'CoTaskMemFree'.
Анализатор обнаружил код, который потенциально можно оптимизировать. В коде присутствует вызов функции, которой в качестве аргументов передаются несколько вызовов одной и той же функции с одинаковыми аргументами.
Рассмотрим пример:
....
init(cos(-roty), sin(-roty),
-sin(-roty), cos(-roty));
....
Вызов такой функции может работать медленно и эффект будет усиливаться, если этот код находится внутри цикла, то тогда код стоит переписать. Например, можно создать временную переменную. Рассмотрим вариант такого кода:
....
double cos_r = cos(-roty);
double sin_r = sin(-roty);
init(cos_r, sin_r, -sin_r, cos_r);
....
Конечно, так изменить код можно не всегда. И тем более, не всегда такой рефакторинг может дать выигрыш в производительности. Однако иногда такие оптимизации могут быть весьма полезны.
Анализатор обнаружил код, который потенциально можно оптимизировать. В коде присутствует избыточное создание объекта типа 'std::string', от которого можно избавиться.
У объекта класса 'std::string' с помощью функции 'c_str()' берется указатель на массив символов. Затем из этих символов конструируется новый объект типа 'std::string'. Например, это возможно, если неоптимальное выражение является:
Рассмотрим пример для случая с вызовом функции:
void foo(const std::string &s)
{
....
}
....
void bar()
{
std::string str;
....
foo(str.c_str());
}
Улучшить код очень просто - достаточно убрать вызов метода 'c_str()'. Рассмотрим пример упрощенного кода:
....
void bar()
{
std::string str;
....
foo(str);
}
Пример некорректного кода для оператора присваивания будет иметь вид:
std::string str;
....
std::string s = str.c_str();
Пример некорректного кода для оператора 'return' будет иметь вид:
std::string foo(const std::string &str)
{
....
return str.c_str();
}
Исправить ошибку в этих случаях следует аналогично случаю для вызова функции.
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. Вызов функции 'count' или 'count_if' из стандартной библиотеки сравнивается с нулем. Это может быть потенциально медленно, потому что этим функциям необходимо обработать весь контейнер, чтобы посчитать количество нужных элементов. Если значение, которое вернула функция, сравнивается с нулем, то нам интересно есть ли хотя бы 1 такой элемент или их нет совсем. Такого же эффекта можно достичь более эффективно, если осуществить вызов функции 'find' или 'find_if'.
Пример неоптимального кода:
void foo(const std::multiset<int> &ms)
{
if (ms.count(10) != 0)
{
....
}
}
Что бы сделать код быстрее, следует заменить неоптимальное выражение аналогичным, использую более подходящую функцию, в данном случае - 'find'. Пример оптимизированного кода:
void foo(const std::multiset<int> &ms)
{
if (ms.find(10) != ms.end())
{
....
}
}
Так же неоптимальным будет и следующий код:
void foo(const std::vector<int> &v)
{
if (count(v.begin(), v.end(), 10) != 0)
{
....
}
}
Провести оптимизацию можно аналогично предыдущему примеру. Тогда оптимизированный код примет такой вид:
void foo(const std::vector<int> &v)
{
if (find(v.begin(), v.end(), 10) != v.end())
{
....
}
}
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. Аргумент, представляющий собой структуру или класс, передается в функцию по значению. Анализатор, проверяя тело функции, пришел к выводу, что этот аргумент не модифицируется. С целью оптимизации такой аргумент можно передавать как константную ссылку. Это может ускорить выполнение программы, поскольку при вызове функции будет скопирован только адрес, а не весь объект класса. Особенно заметна оптимизация, если класс содержит внутри себя большой объем данных.
Рассмотрим пример:
void foo(Point p)
{
float x = p.x;
float y = p.y;
float z = p.z;
float k = p.k;
float l = p.l;
.... далее аргумент 'p' никак не используется....
}
Исправить такой код очень легко - достаточно изменить объявление функции:
void foo(const Point &p)
{
float x = p.x;
float y = p.y;
float z = p.z;
float k = p.k;
float l = p.l;
.... далее аргумент 'p' никак не используется....
}
Анализатор не выдаёт предупреждение, если структуры очень маленькие.
Примечание N1. Пользователь может сам указать размер структур, начиная с которого следует выдавать предупреждения.
Например, чтобы не выдавать сообщения для структур размером 32 байта и менее, можно написать комментарий:
//-V813_MINSIZE=33
Число 33 задаёт размер структуры, начиная с которого следует выдавать предупреждения.
Можно разместить комментарий в одном из глобальных файлов (например в StdAfx.h), чтобы комментарий начал оказывать влияние на весь проект.
Значение по умолчанию: 17.
Примечание N2. Анализатор может ошибаться, пытаясь понять, модифицируется переменная в теле функции или нет. Если вы заметили явно ложное срабатывание, просьба прислать нам соответствующий пример кода.
Если код корректен, то избавиться от ложного сообщения об ошибке можно добавив комментарий "//-V813".
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. При каждой итерации цикла вызывается функция strlen(S) или аналогичная ей. Строка 'S' не меняется, и значит, её длину можно вычислить заранее. В некоторых случаях это может дать существенный прирост производительности.
Рассмотрим первый пример.
for (;;) {
{
....
segment = next_segment + strlen("]]>");
....
}
Внутри цикла много раз вычисляется длина строки "]]>". Конечно, строка короткая и функция strlen() Работает быстро. Но если цикл выполняет миллионы итераций, можно получить замедление "на ровном месте". Этот недочет легко исправить:
const size_t suffixLen = strlen("]]>");
for (;;) {
{
....
segment = next_segment + suffixLen;
....
}
Ещё лучше использовать макрос, на подобии этого:
#define LiteralStrLen(S) (sizeof(S) / sizeof(S[0]) - 1)
....
segment = next_segment + LiteralStrLen("]]>");
Если это Си++, то можно сделать шаблонную функцию:
template <typename T, size_t N>
char (&ArraySizeHelper(T (&array)[N]))[N];
template <typename T, size_t N>
size_t LiteralStrLen(T (&array)[N]) {
return sizeof(ArraySizeHelper(array)) - 1;
}
....
segment = next_segment + LiteralStrLen("]]>");
Рассмотрим второй пример.
for(j=0; j<(int)lstrlen(text); j++)
{
if(text[j]=='\n')
{
lines++;
}
}
Показанный фрагмент кода считает количество строк в тексте. Этот пример не надуманный, а взят из реального приложения.
Если текст будет большим, этот алгоритм будет крайне неэффективен. На каждой итерации цикла, будет вычисляться его длина, чтобы сравнить с ней значение переменной 'j'.
Быстрый вариант:
const int textLen = lstrlen(text);
for(j=0; j<textLen; j++)
{
if(text[j]=='\n')
{
lines++;
}
}
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. В классах строк, реализованы операторы, которые позволяют более эффективно очистить строку или проверить что строка является пустой.
Рассмотрим пример:
bool f(const std::string &s)
{
if (s == "")
return false;
....
}
Этот код можно немного улучшить. Объект класса 'std::string' знает длину строки, которую он хранит. Но не известно с какой строкой его хотят сравнить. Поэтому, запускается цикл для сравнения строк. Намного проще, просто проверить, что длина строки равна 0. Для этого следует использовать функцию 'empty()':
if (s.empty())
return false;
Аналогичная ситуация. Требуется очистить строку. Следующий код можно немного улучшить:
wstring str;
...
str = L"";
Улучшенный вариант:
wstring str;
...
str.clear();
Примечание. Описанные рекомендации спорны. Выигрыш от такой оптимизации крайне мал, но при этом возрастает риск допустить опечатку и использовать не ту функцию. Причина - неудачное именование функций. Например, в классе 'std::string' функция 'empty()' проверяет, что строка пустая. В классе 'CString', функция 'Empty()' очищает строку. Одно название, но разные действия. Поэтому, для улучшения читаемости кода вполне допустимо использовать конструкции: = "", == "", != "".
Выбор за вами. Если диагностика V815 вам не нравится, вы можете отключить её в настройках.
Анализатор обнаружил конструкцию, которую потенциально можно оптимизировать. Перехватывать исключение эффективнее по ссылке, а не по значению. В этом случае, не будет происходить копирование объекта.
Рассмотрим пример:
catch (MyException x)
{
Dump(x);
}
Этот код можно немного улучшить. Сейчас при перехвате исключения конструируется новый объект типа MyExeption. Этого можно избежать, если перехватить исключение по ссылке. Особенно в этом есть смысл, если объект является "тяжёлым".
Улучшенный вариант кода:
catch (MyException &x)
{
Dump(x);
}
Перехватывать исключение по ссылке рекомендуется не только с точки зрения оптимизации. Это позволяет избегать некоторых других ошибок. Например, так можно избежать проблему "срезки" (slice). Однако данная темы выходит за рамки описания данной диагностики. Для борьбы со срезкой существует диагностика V746.
Подробнее про преимущества перехвата исключения по ссылке можно прочитать здесь:
Данная диагностика классифицируется как:
|
Анализатор обнаружил код, который предназначен для поиска символа в строке и который потенциально можно оптимизировать.
Рассмотрим пример неэффективного кода:
bool isSharpPresent(const std::string& str)
{
return str.find("#") != std::string::npos;
}
В таком случае лучше использовать перегруженную версию функции 'find', которая принимает вместо строки символ.
Улучшенный вариант кода:
bool isSharpPresent(const std::string& str)
{
return str.find('#') != std::string::npos;
}
Приведем еще пример неэффективного кода, который можно потенциально оптимизировать:
const char* GetSharpSubStr(const char* str)
{
return strstr(str, "#");
}
В таком случае, эффективнее использовать функцию 'strchr()', которая ищет символ:
const char* GetSharpSubStr(const char* str)
{
return strchr(str, '#');
}
Анализатор обнаружил, что конструктор реализован не оптимально и можно оптимизировать инициализацию членов класса.
Рассмотрим пример:
class UserInfo
{
std::string m_name;
public:
UserInfo(const std::string& name)
{
m_name = name;
}
};
Сначала член 'm_name' инициализируется как пустая строка, а затем уже в этот член копируется строка из переменной 'name'. В C++03 это приведет к лишней аллокации для пустой строки. Как минимум, этот код можно улучшить, сразу вызвав конструктор копирования через список инициализации.
UserInfo(const std::string& name) : m_name(name)
{
}
Если вы используете C++11, то можно пойти дальше. Посмотрим, как можно сконструировать объект UserInfo:
std::string name = "name";
UserInfo u1(name); // 1 copy
UserInfo u2("name"); // 1 ctor, dtor + 1 copy
UserInfo u3(GetSomeName()); // 1 copy
Если строки достаточно длинные и не попадают под Small String Optimization, то здесь произойдут лишние аллокации памяти и лишние копирования. Чтобы этого избежать, сделаем передачу аргумента по значению:
UserInfo(std::string name) : m_name(std::move(name))
{
}
Теперь все временные значения не будут порождать лишних копий, так как сработает move конструктор.
std::string name = "name";
UserInfo u1(name); // 1 copy + 1 move
UserInfo u2("name"); // 1 ctor, dtor + 1 move
UserInfo u3(GetSomeName()); // 2 move
UserInfo u4(std::move(name)); // 2 move
Анализатор обнаружил конструкцию, которую можно оптимизировать. Блок памяти одного и того же размера выделяется и освобождается в теле цикла множество раз. В этом случае можно вынести выделение и освобождение памяти из цикла, что повысит производительность программы.
Рассмотрим пример неэффективного кода:
for (int i = 0; i < N; i++)
{
int *arr = new int[1024 * 1024];
SetValues(arr);
val[i] = GetP(arr);
delete [] arr;
}
Здесь можно вынести выделение и освобождение памяти из тела цикла.
Улучшенный вариант кода:
int *arr = new int[1024 * 1024];
for (int i = 0; i < N; i++)
{
SetValues(arr);
val[i] = GetP(arr);
}
delete [] arr;
Аналогично анализатор может предложить оптимизировать код при выделении памяти с помощью функции 'malloc' и т.п.
Анализатор обнаружил, что переменная не используется после того, как её скопировали в другую переменную. Такой код можно оптимизировать, избавившись от лишнего копирования.
Рассмотрим несколько ситуаций.
Пример 1:
class UserInfo
{
std::string m_name;
public:
void SetName(std::string name)
{
m_name = name;
}
};
В приведённом примере произойдёт два копирования: первое при вызове функции 'SetName()', второе при копировании 'name' в 'm_name'. Можно избавиться от копирования, использовав move присвоение:
void SetName(std::string name)
{
m_name = std::move(name);
}
Если объект не move assignable, то можно изменить сигнатуру функции 'SetName()', сделав переменную 'name' константной ссылкой. В таком случае копирование произойдёт только при присваивании.
void SetName(const std::string &name)
{
m_name = name;
}
Пример 2:
bool GetUserName(int id, std::string &outName)
{
std::string tmp;
if (db->GetUserName(id, tmp))
{
outName = tmp;
return true;
}
return false;
}
В данном случае есть локальная переменная 'tmp', которая копируется в 'outName' и дальше не используется. С точки зрения производительности эффективнее использовать 'move' или 'swap'.
bool GetName(int id, std::string &outName)
{
std::string tmp;
if (db->GetUserName(id, tmp))
{
outName = std::move(tmp);
return true;
}
return false;
}
Пример 3:
void Foo()
{
std::vector<UserInfo> users = GetAllUsers();
{
std::vector<UserInfo> users1 = users;
DoSomethingWithUsers1(users1);
}
{
std::vector<UserInfo> users2 = users;
DoSomethingWithUsers2(users2);
}
}
Иногда копирование можно заменить на ссылку, если вариант с swap/move для какого-то класса недоступен. Это может быть не лучшим решением с точки зрения красоты кода, но это будет лучше по производительности.
void Foo()
{
std::vector<UserInfo> users = GetAllUsers();
{
std::vector<UserInfo> users1 = users;
DoSomethingWithUsers1(users1);
}
{
std::vector<UserInfo> &users2 = users;
DoSomethingWithUsers2(users2);
}
}
Анализатор обнаружил, что переменная может быть создана в меньшей области видимости. Изменив место создания объекта, можно оптимизировать код по скорости и количеству потребляемой памяти.
Например, если выделяется память под большой массив или создаётся "тяжёлый" объект класса, и при этом такая переменная используется только при некотором условии, то её лучше перенести в блок условного оператора.
Рассмотрим пример:
void SetToolInfoText(ToolInfo &info, int nResource, int nItem)
{
Text data(80); // <=
if (nResource)
{
info.text = GetResource(nResource);
}
else
{
GetDataForItem(data, nItem);
info.text = data.text();
}
}
В приведенном примере имеет смысл конструировать объект 'data' только в ветке 'else'.
Улучшенный вариант кода:
void SetToolInfoText(ToolInfo &info, int nResource, int nItem)
{
if (nResource)
{
info.text = GetResource(nResource);
}
else
{
Text data(80); // <=
GetDataForItem(data, nItem);
info.text = data.test();
}
}
Также анализатор может обнаружить динамическое выделение памяти, которое можно убрать в меньшую область видимости. Например:
void func(bool condition)
{
int *arr = new int[1000];
if (condition)
{
use(arr);
}
delete[] arr;
}
В данном примере имеет смысл выделять (и освобождать) память только если выполняется условие.
Улучшенный вариант кода:
void func(bool condition)
{
if (condition)
{
int *arr = new int[1000];
use(arr);
delete[] arr;
}
}
Стоит отметить, что анализатор старается выдавать предупреждения лишь в тех случаях, когда перенос переменной в меньшую область видимости даст реальный прирост производительности или экономию памяти. По этой причине предупреждения не выдаются для отдельных переменных базовых типов. Также предупреждения не выдаются для объектов, которые создаются конструктором без аргументов (как показали эксперименты, почти всегда для такого кода предупреждение является ложным срабатыванием).
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что скорее всего хотели создать ссылку на объект, а не новый объект. Создание нового ненужного объекта может отнимать время и память.
Рассмотрим код, где справа привели переменную к ссылке, но при этом слева в объявлении её не указали:
auto test = static_cast<NotPOD &>(npod);
В приведенном примере в правой части содержится приведение к ссылке на тип NotPOD, однако в левой части его нет. Из-за этого мы копируем объект вместо ожидаемой передачи по ссылке.
Есть два варианта как можно улучшить этот код. Первый – это заменить auto на decltype(auto):
decltype(auto) test = static_cast<NotPOD &>(npod);
Теперь квалификатор '&' будет выводиться в соответствии с правой частью, но это достаточно громоздко, поэтому можно вручную добавить квалификатор:
auto &test = static_cast<NotPOD &>(npod);
Если же копирование было действительно необходимо, тогда стоит убрать квалификатор из правой части, чтобы увеличить читаемость кода:
auto test = static_cast<NotPOD>(npod);
Анализатор обнаружил потенциальное использование менее эффективного метода. При вставке временного объекта в контейнер при использовании методов 'insert' / 'push_*' он конструируется снаружи контейнера и затем перемещен/скопирован внутрь контейнера.
Методы 'emplace' / 'emplace_*' в свою очередь позволяют устранить один избыточный вызов конструктора перемещения / копирования и создают объект "по месту" внутри контейнера, идеально передавая параметры функции конструктору объекта.
Анализатор предлагает следующие замены:
Пример кода:
std::string str { "Hello, World" };
std::vector<std::string> vec;
std::forward_list<std::string> forward_list;
std::list<std::string> list;
std::map<std::string, std::string> map;
....
vec.push_back(std::string { 3, 'A' });
forward_list.push_front(std::string { str.begin(), str.begin() + 6 });
list.push_front(str.substr(7));
list.push_back(std::string { "Hello, World" });
map.insert(std::pair<std::string, std::string> { "Hello", "World" });
Оптимизированный код:
std::vector<std::string> vec;
std::forward_list<std::string> forward_list;
std::list<std::string> list;
std::map<std::string, std::string> map;
....
vector.emplace_back(3, 'A');
forward_list.emplace_front(string.begin(), string.begin() + 6);
list.emplace_front(str.begin() + 7, str.end());
list.emplace_back("Hello, World");
map.emplace("Hello", "World");
В некоторых случаях такая замена может привести к потере базовой гарантии безопасности исключений. Рассмотрим пример:
std::vector<std::unique_ptr<int>> vectUniqP;
vectUniqP.push_back(std::unique_ptr<int>(new int(0)));
auto *p = new int(1);
vectUniqP.push_back(std::unique_ptr<int>(p));
В таком случае замена 'push_back' на 'emplace_back' может привести к утечке памяти, если 'emplace_back' бросит исключение в связи с отсутствием памяти. Анализатор не выдает срабатывания на подобные случаи и не предлагает замену. Если же код был изменен на ошибочный, анализатор выдаст предупреждение V1023.
Иногда замена вызовов методов 'insert' / 'push_*' на их аналог 'emplace' / 'emplace_*' не принесет оптимизации:
std::string foo()
{
std::string res;
// doing some heavy stuff
return res;
}
std::vector<std::string> vec;
....
vec.push_back(foo());
В этом примере метод 'emplace_back' будет иметь такую же эффективность, что и вставка элемента через 'push_back'. Однако, предупреждение всё равно выдаётся для единообразия. Во всех подобных ситуациях рационально сделать замену, чтобы код смотрелся единообразно и при его просмотре не приходилось каждый раз задумываться, стоит использовать 'emplace*' или нет. Если пользователь не согласен с таким подходом, он может рассматривать подобные предупреждения как ложные и подавлять их.
Примечание. К описанной здесь рекомендации стоит подходить разумно, а не формально. Например, замена
widgets.push_back(Widget(foo, bar, baz));
// на
widgets.emplace_back(Widget(foo, bar, baz));
не даёт никакого выигрыша в скорости работы программы. Более того, использование 'emplace_back' может замедлить скорость компиляции кода. Подробнее эта тема освящена в статье "Don't blindly prefer emplace_back to push_back". Наша команда выражает Arthur O'Dwyer благодарность за эту публикацию.
Анализатор рекомендует создать умный указатель не путем вызова конструктора, принимающего "сырой" указатель на ресурс, а вызовом функции 'make_unique' / 'make_shared'.
Использование этих функций позволяет:
Рассмотрим пример кода:
void foo(std::unique_ptr<int> a, std::unique_ptr<int> b)
{
....
}
void bar()
{
foo( std::unique_ptr<int> { new int { 0 } },
std::unique_ptr<int> { new int { 1 } });
}
Поскольку стандартом не регламентируется порядок вычисления аргументов функции, компилятор в целях оптимизации может сделать это в такой последовательности:
В этом случае, если второй вызов 'new' бросит исключение, произойдет утечка памяти – ресурс, выделенный первым вызовом 'new', никогда не будет освобожден. Создание указателя при помощи 'make_unique' решает эту проблему, гарантируя освобождение памяти в случае броска исключения.
Оптимизированный код:
void foo(std::unique_ptr<int> a, std::unique_ptr<int> b)
{
....
}
void bar()
{
foo( std::make_unique<int>(0), std::make_unique<int>(1));
}
Начиная с C++17, хотя порядок вычисления аргументов остается неуточненным, вводятся дополнительные гарантии. Все побочные эффекты от аргумента функции должны быть вычислены до того, как произойдет вычисление следующего аргумента. Это снижает риски в случае исключений, но использовать 'make_unique' все равно предпочтительно.
Замечание по поводу 'make_shared'. При использовании этой функции контрольный блок указателя размещается в памяти рядом с объектом. Это уменьшает количество динамических аллокаций и оптимизирует использование кэша процессора.
Объект удаляется, когда счетчик ссылок становится нулевым, но контрольный блок существует до тех пор, пока существуют слабые ссылки на указатель. Если контрольный блок и объект были созданы при помощи 'make_shared' (т.е. размещены в одной области памяти), это приводит к тому, что память не может быть освобождена до тех пор, пока счетчик ссылок нулевой и на объект ссылается хотя бы один 'weak_ptr'. Для больших объектов такое поведение может быть нежелательным. Если функция 'make_shared' не используется сознательно, чтобы избежать размещения контрольного блока в одной области памяти с объектом, срабатывание диагностики можно подавить.
Ограничение, связанное с разными версиями стандарта C++: так как возможности функций 'make_unique' и 'make_shared' менялись, начиная с C++11, диагностика зависит от версии стандарта следующим образом:
Анализатор обнаружил фрагмент кода, в котором совместно используются функции 'std::unique_ptr::reset' и 'std::unique_ptr::release'.
Рассмотрим простой пример кода:
void foo()
{
auto p = std::make_unique<int>(10);
....
std::unique_ptr<int> q;
q.reset(p.release());
....
}
Формально, такой вызов эквивалентен перемещению умного указателя:
void foo()
{
auto p = std::make_unique<int>(10);
....
auto q = std::move(p);
....
}
В данном случае, предложение анализатора заменить цепочку вызовов 'q.reset(p.release())' на 'q = std::move(p) ' улучшит прозрачность кода. Однако, может возникнуть ситуация, когда перемещение умного указателя будет являться обязательным. Например, при использовании пользовательского функционального объекта для освобождения ресурса:
class Foo { .... };
struct deleter
{
bool use_free;
template<typename T>
void operator()(T *p) const noexcept
{
if (use_free)
{
p->~T();
std::free(p);
}
else
{
delete p;
}
}
};
Рассмотрим два небольших примера, первый с перемещением умного указателя с пользовательским функциональным объектом для освобождения ресурса, при помощи паттерна 'reset' - 'release':
void bar1()
{
std::unique_ptr<Foo, deleter> p { (int*) malloc(sizeof(Foo)),
deleter { true } };
new (p.get()) Foo { .... };
std::unique_ptr<Foo, deleter> q;
q.reset(p.release()); // 1
}
и второй пример, с помощью функции 'std::move':
void bar2()
{
std::unique_ptr<Foo, deleter> p { (int*) malloc(sizeof(Foo)),
deleter { true } };
new (p.get()) Foo { .... };
std::unique_ptr<Foo, deleter> q;
q = std::move(p); // 2
}
В втором примере при перемещении указателя 'p' в 'q' функция 'std::move' позволит переместить также и функциональный объект типа 'deleter' для освобождения ресурса. В первом примере цепочка вызовов 'q.reset(p.release())' этого не сделает. Это приведет к тому, что исходный объект типа 'Foo', аллоцированный на куче через вызов 'malloc' и сконструированный оператором 'placement new', будет неверно освобожден путем вызова оператора 'delete'. Такой код неминуемо приведёт к неопределённому поведению программы.
Анализатор обнаружил контейнер из стандартной библиотеки C++, который можно заменить на другой контейнер в целях оптимизации.
Для определения, какой именно контейнер лучше подойдет в конкретном случае, применяется эвристика, основанная на типах операций, которые производят с контейнером. Кроме этого, анализатор вычисляет алгоритмическую сложность всех операций и предлагает контейнер, у которого она будет ниже.
В сообщении указывается причина замены:
Пример:
void f()
{
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for (auto value : v)
{
std::cout << value << ' ';
}
}
Анализатор выдает следующее сообщение:
V826. Consider replacing the 'v' std::vector with std::array. The size is known at compile time.
Здесь размер вектора известен во время компиляции. Если использовать вместо него 'std::array', можно избежать динамической аллокации памяти. Оптимизированный код:
void f()
{
std::array a{1, 2, 3};
}
Анализатор не предлагает такую замену, если суммарный размер элементов превышает 16 килобайт, а также если вектор приходит в функцию снаружи, возвращается из функции, или передается в другую функцию параметром.
В следующем фрагменте кода сообщение не выдается несмотря на то, что размер контейнера известен:
std::vector<int> f()
{
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
return v;
}
Следующий пример, который можно оптимизировать:
void f(int n)
{
std::vector<int> v;
for (int i = 0; i < n; ++i)
{
v.push_back(i);
}
for (int i = 0; i < n; ++i)
{
std::cout << v.back() << ' ';
v.pop_back();
}
}
Анализатор выдает следующее сообщение:
V826. Consider replacing the 'v' std::vector with std::stack. Elements are added, read and erased only from front/back.
Здесь элементы добавляются в хвост вектора, а затем происходит их последовательное чтение и удаление. Вектор используется, как 'std::stack'. Можно произвести замену. Оптимизированный код:
void f(int n)
{
std::stack<int> v;
for (int i = 0; i < n; ++i)
{
v.push(i);
}
for (int i = 0; i < n; ++i)
{
std::cout << v.top() << ' ';
v.pop();
}
}
Следующий пример, который можно оптимизировать:
void f(int n)
{
std::deque<int> d;
for (int i = 0; i < n; i++)
{
d.push_back(i);
}
for (auto value : d)
{
std::cout << value << ' ';
}
}
Анализатор выдает следующее сообщение:
V826. Consider replacing the 'd' std::deque with std::vector. Contiguous placement of elements in memory can be more efficient.
В этом случае 'std::deque' и 'std::vector' не отличаются с точки зрения алгоритмической сложности. Однако, в случае с вектором элементы в памяти будут расположены последовательно. Это может увеличить производительность, так как последовательный доступ к памяти позволяет эффективно использовать кэш процессора. Оптимизированный код:
void f(int n)
{
std::vector<int> d;
for (int i = 0; i < n; i++)
{
d.push_back(i);
}
for (auto value : d)
{
std::cout << value << ' ';
}
}
Анализатор обнаружил 'std::vector', максимальный размер которого известен на этапе компиляции. При этом перед заполнением вектора не вызывался метод 'reserve'.
Пример:
void f()
{
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
}
В этом случае вызовы 'push_back' могут приводить к реаллокации внутреннего буфера в векторе и перемещении элементов в новую область памяти.
Чтобы уменьшить накладные расходы, можно заранее выделить буфер необходимого размера:
void testVectOK()
{
std::vector<int> v;
v.reserve(6);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);
}
В сообщении анализатора указывается количество элементов, которое следует передать в метод 'reserve'.
В некоторых случаях анализатор не может точно вычислить размер контейнера, например, из-за того, что элементы добавляются под условием:
void f(bool half)
{
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
if (!half)
{
v.push_back(4);
v.push_back(5);
v.push_back(6);
}
}
Количество элементов в контейнере здесь варьируется от 3 до 6 в зависимости от условия. В таких случаях анализатор будет ориентироваться на максимальный размер.
Анализатор обнаружил ситуацию, когда локальная переменная, временный объект или параметр функции возвращаются из функции посредством вызова 'std::move'.
Рассмотрим пример:
struct T { .... };
T foo()
{
T t;
// ....
return std::move(t);
}
На первый взгляд может показаться, что такой код более оптимизирован, так как первым будет гарантировано выбран конструктор перемещения, однако это не так. Использование 'std::move' в контексте возвращаемого выражения может запретить компилятору устранить вызов конструктора копирования / перемещения (copy elision, C++17) и применить RVO/NRVO для локальных объектов.
До появления семантики перемещения (C++11) компиляторы старались производить так называемую оптимизацию возвращаемого значения ([Named] Return Value Optimization) в обход вызова конструктора копирования, при которой возвращаемый объект создавался непосредственно в стеке вызывающей функции, а затем инициализировался вызванной функцией.
Такую оптимизацию компилятор может сделать лишь в том случае, если возвращаемый тип функции не является ссылкой, а операндом оператора 'return' является имя локальной не-'volatile' переменной, его тип должен совпадает с возвращаемым типом функции (игнорируя 'const' / 'volatile' квалификаторы).
Начиная с C++11, при возвращении из функции не-'volatile' локальной переменной компилятор попробует применить RVO/NRVO, затем конструктор перемещения, и лишь затем конструктор копирования. Поэтому, следующий код работает медленнее, чем ожидается:
struct T { .... };
T foo()
{
T t;
// ....
return std::move(t); // <= V828, pessimization
}
В случае не-'volatile' формального параметра компилятор не может применить RVO/NRVO из-за технических ограничений, но попробует выбрать сначала конструктор перемещения, а затем конструктор копирования. Поэтому, следующий код содержит избыточный вызов функции 'std::move', который можно опустить:
struct T { .... };
T foo(T param)
{
T t;
// ....
return std::move(param); // <= V828, redundant 'std::move' call
}
Также, начиная с C++17, если возвращаемое выражение имеет категорию prvalue (например, результат выполнения функции, которая возвращает не ссылку), то компилятор обязан оптимизировать код, удалив вызов конструктора копирования / перемещения (copy elision). Поэтому, следующий код работает медленнее, чем ожидается:
struct T { .... };
T bar();
T foo()
{
return std::move(bar()); // <= V828, pessimization
}
Во всех представленных случаях рекомендуется удалить вызов функции 'std::move' в целях оптимизации или устранения лишнего кода.
Дополнительная информация:
Это диагностическое правило основано на пункте R.5 CppCoreGuidelines (Prefer scoped objects, don't heap-allocate unnecessarily).
Память для хранения локальной переменной выделяется динамически и освобождается перед выходом из функции. В этом случае переменную можно разместить на стеке, чтобы избежать накладных расходов, связанных с выделением и освобождением памяти.
Пример:
class Object { .... };
void DoSomething()
{
auto obj = new Object;
....
delete obj;
}
Так как переменная существует только в рамках текущей области видимости, аллокацию памяти в большинстве случаев можно убрать.
Исправленный пример:
void DoSomething()
{
Object obj;
....
}
Предупреждение не выдается, если выделенная память не освобождается, или в случаях утечки адреса наружу. На этом фрагменте кода указатель возвращают через выходной параметр функции:
void DoSomething(Object** ppObj)
{
auto obj = new Object;
if (obj->good())
{
*ppObj = obj;
return;
}
delete obj;
}
Анализатор обнаружил фрагмент кода, в котором используется доступ к содержимому заведомо инициализированного 'std::optional' с помощью метода 'std::optional::value()'.
Рассмотрим пример:
inline void LuaBlockLoader::loadColorMultiplier(
BlockState &state, const sol::table &table) const
{
std::optional<sol::table> colorMultiplier = table["color_multiplier"];
if (colorMultiplier != std::nullopt) {
state.colorMultiplier(gk::Color{
colorMultiplier.value().get<u8>(1),
colorMultiplier.value().get<u8>(2),
colorMultiplier.value().get<u8>(3),
colorMultiplier.value().get<u8>(4)
});
}
}
Данный метод добавляет накладные расходы на проверку содержимого: если объект класса 'std::optional' равен 'std::nullopt', то будет выброшено исключение 'std::bad_optional_access'. Если известно, что объект инициализирован, можно упростить и ускорить код при помощи перегруженных операторов 'std::optional::operator*' или 'std::optional::operator->':
inline void LuaBlockLoader::loadColorMultiplier(
BlockState &state, const sol::table &table) const
{
std::optional<sol::table> colorMultiplier = table["color_multiplier"];
if (colorMultiplier != std::nullopt) {
state.colorMultiplier(gk::Color{
colorMultiplier->get<u8>(1),
colorMultiplier->get<u8>(2),
colorMultiplier->get<u8>(3),
colorMultiplier->get<u8>(4)
});
}
}
Анализатор обнаружил фрагмент кода, в котором используется доступ к элементу последовательного контейнера (std::array, std::vector или std::deque) с помощью метода 'at', при этом известно, что индекс является валидным.
Метод 'at' получает элемент контейнера по заданному индексу. Перед этим он проверяет находится ли индекс в допустимых пределах, и в случае выхода за границу контейнера генерирует исключение 'std::out_of_range'. Если известно, что при обращении по индексу выход за границу не случится, использование метода 'at' можно безопасно заменить на 'operator[]', который не выполняет дополнительных проверок. Удаление избыточной проверки ускорит выполнение кода.
Рассмотрим пример:
std::vector<std::string> namelessIds;
....
if (!namelessIds.empty()) {
LIST<char> userIds(1);
for (std::string::size_type i = 0; i < namelessIds.size(); i++) {
userIds.insert(mir_strdup(namelessIds.at(i).c_str())); // <=
}
....
}
При обходе элементов вектора в цикле, выбираемый номер элемента не может стать больше номера самого последнего элемента. Таким образом, код можно упростить если заменить 'at' на квадратные скобки:
std::vector<std::string> namelessIds;
....
if (!namelessIds.empty()) {
LIST<char> userIds(1);
for (std::string::size_type i = 0; i < namelessIds.size(); i++) {
userIds.insert(mir_strdup(namelessIds[i].c_str()));
}
....
}
Если в классе специальные функции объявляются с '= default', класс остается тривиально копируемым. Это потенциально поможет копировать и инициализировать такой класс более оптимально.
Правила формирования специальных функций сложные. Поэтому при написании классов/структур лучшим решением будет определить явно некоторые из них для лучшего понимания кода. Вот примеры таких специальных функций: конструктор по умолчанию, конструктор копирования, оператор копирования, деструктор, конструктор перемещения, оператор перемещения.
struct MyClass
{
int x;
int y;
MyClass() {}
~MyClass() {}
};
или так:
// header
struct MyClass
{
int x;
int y;
};
// cpp-file
MyClass::MyClass() {}
MyClass::~MyClass() {}
Такие функции (конструктор по умолчанию и деструктор в примере) программист определяет с пустым телом. Однако при таком подходе класс может стать нетривиально копируемым, из-за чего компилятор не всегда сможет сгенерировать более оптимальный код. Поэтому C++11 вводит синтаксис '= default' для специальных функций:
struct MyClass
{
int x;
int y;
MyClass() = default;
~MyClass() = default;
};
Помимо того, что компилятор сам сгенерирует тела специальных функций, он сможет вывести спецификаторы 'constexpr' и 'noexcept' для них автоматически.
Стоит отметить, что при переносе определения специальных функций из тела класса компилятор считает их определенными пользователем. Это может привести к пессимизации, поэтому '= default' лучше по возможности добавлять непосредственно в теле класса.
Срабатывание не выдается, если:
Определения больших классов внутри заголовочного файла могут многократно увеличивать время компиляции проекта. Чтобы его сократить, можно вынести реализацию класса в отдельный компилируемый файл, а в заголовочном файле оставить только объявления методов и указатель на реализацию класса. Такой подход носит название PIMPL. Вот пример такого класса:
#include <memory>
// header
class MyClass
{
class impl;
std::unique_ptr<impl> pimpl;
public:
void DoSomething();
~MyClass();
};
// cpp-file
class MyClass::impl
{
public:
impl()
{
// does nothing
}
~impl()
{
// does nothing
}
void DoSomething()
{
// ....
}
};
void MyClass::DoSomething()
{
pimpl->DoSomething();
}
MyClass::~MyClass() {}
Добавление '= default' к деструктору в теле класса приведет к ошибкам компиляции, т.к. деструктор класса 'MyClass::impl' на данном этапе неизвестен (деструктор нужен для 'std::unique_ptr'). Поэтому при таком подходе реализация специальных функций переносится в компилируемый файл.
При переносе определения специальных функций из тела класса их пустые тела также можно заменить на '= default'. Это не даст прироста производительности, однако сделает код чище и проще для восприятия:
MyClass::~MyClass() = default;
Анализатор обнаружил ситуацию, когда семантика перемещения не сработает, что приведёт к замедлению производительности.
Рассмотрим пример:
#include <string>
#include <vector>
void foo()
{
std::vector<std::string> fileData;
const std::string alias = ....;
....
fileData.emplace_back(std::move(alias));
....
}
Данный фрагмент кода cработает не так, как ожидает программист. Семантика перемещения невозможна для константных объектов. В результате компилятор выберет конструктор копирования для 'std::string' и желаемая оптимизация не произойдёт.
В данном случае код можно поправить, просто убрав константность с локальной переменной:
#include <string>
#include <vector>
void foo()
{
std::vector<std::string> fileData;
std::string alias = ....;
....
fileData.emplace_back(std::move(alias));
....
}
Диагностика выдает срабатывания также и для случаев, когда 'std::move' применяется для формального параметра функции:
#include <string>
void foo(std::string);
void bar(const std::string &str)
{
....
foo(std::move(str));
....
}
Дать универсальную рекомендацию по исправлению такого кода сложно, но можно применить следующие подходы.
Первый вариант
Можно дописать перегрузку функции, принимающую rvalue-ссылку:
#include <string>
void foo(std::string);
void bar(const std::string &str)
{
....
foo(str); // copy here
....
}
void bar(std::string &&str) // new overload
{
....
foo(std::move(str)); // move here
....
}
Второй вариант
Можно переписать функцию в виде шаблона функции, принимающей forward-ссылку. При этом необходимо ограничить шаблонный параметр нужным типом. Затем применить на шаблонном аргументе функцию 'std::forward':
#include <string>
#include <type_traits> // until C++20
#include <concepts> // since C++20
void foo(std::string);
// ------------ Constraint via custom trait (since C++11) ------------
template <typename T>
struct is_std_string
: std::bool_constant<std::is_same<std::decay_t<T>,
std::string>::value>
{};
template <typename T,
std::enable_if_t<is_std_string<T>::value, int> = 0>
void bar(T &&str)
{
....
foo(std::forward<T>(str));
....
}
// -------------------------------------------------------------------
// ------------ Constraint via custom trait (since C++14) ------------
template <typename T>
static constexpr bool is_std_string_v =
std::is_same<std::decay_t<T>, std::string>::value;
template <typename T, std::enable_if_t<is_std_string_v<T>, int> = 0>
void bar(T &&str)
{
....
foo(std::forward<T>(str));
....
}
// -------------------------------------------------------------------
// ------------------ Constraint via C++20 concept -------------------
template <typename T>
void bar(T &&str) requires std::same_as<std::remove_cvref_t<T>,
std::string>
{
....
foo(std::forward<T>(str));
....
}
// -------------------------------------------------------------------
Третий вариант
Если ранее описанные или другие приёмы невозможны, то следует убрать вызов 'std::move'. Диагностическое правило также сработает в случаях, когда результат функции 'std::move' передаётся в функцию, принимающую lvalue-ссылку на константу. Рассмотрим пример:
#include <string>
std::string foo(const std::string &str);
void bar(std::string str, ....)
{
....
auto var = foo(std::move(str));
....
}
Хоть 'std::move' отработает и вернёт нам xvalue-объект, он всё равно будет скопирован, поскольку формальный параметр функции — lvalue-ссылка на константу. В данном случае результат вызова 'std::move' будет находиться в контексте, в котором вызов конструктора перемещения невозможен. Однако, если дописать перегрузку функции, принимающую rvalue-ссылку, или шаблон функции с forwarding-ссылкой, компилятор выберет её, и код отработает ожидаемым образом:
#include <string>
std::string foo(const std::string &str);
std::string foo(std::string &&str);
void bar(std::string str, ....)
{
....
auto var = foo(std::move(str));
....
}
Теперь давайте рассмотрим случай, когда 'std::move' на ссылку на константу сработает:
template <typename T>
struct MoC
{
MoC(T&& rhs) : obj (std::move(rhs)) {}
MoC(const MoC& other) : obj (std::move(other.obj)) {}
T& get() { return obj; }
mutable T obj;
};
Здесь представлена реализация идиомы MoC (Move on Copy). В конструкторе копирования выполняется перемещение. В данном случае это возможно потому, что нестатическое поле 'obj' имеет спецификатор 'mutable', и это явно говорит компилятору работать с ним не как с константным объектом.
Анализатор обнаружил ситуацию, в которой на каждой итерации цикла происходит неявное копирование элементов контейнера. При этом программист предполагал, что переменная цикла ссылочного типа будет связываться с элементами контейнера без копирования. Это происходит из-за несоответствия типов переменной цикла и элементов контейнера.
Рассмотрим пример:
void foo(const std::unordered_map<int, std::string>& map)
{
for (const std::pair<int, std::string> &i : map)
{
std::cout << i.second;
}
}
В данном фрагменте кода разработчик хочет перебрать в цикле все элементы контейнера 'std::unordered_map' и напечатать значения в поток вывода. Однако тип элементов не 'std::pair<int, std::string>', как ожидалось разработчиком, а 'std::pair<const int, std::string>'. Несоответствие аргументов шаблона 'std::pair' приводит к тому, что каждый элемент контейнера будет неявно конвертироваться ко временному объекту типа 'std::pair<const int, std::string>', а затем ссылка привязывается к нему.
Решить эту проблему можно двумя способами:
Первый способ. Использовать правильный тип переменной цикла. В общем случае достаточно посмотреть, значение какого типа возвращает итератор используемого контейнера при разыменовании (operator *).
void foo(const std::unordered_map<int, std::string> &map)
{
for (const std::pair<const int, std::string> &i : map)
{
std::cout << i.second;
}
}
Второй способ. Использовать тип 'auto' для автоматического вывода типа элементов контейнера.
void foo(const std::unordered_map<int, std::string> &map)
{
for (const auto &i : map)
{
std::cout << i.second;
}
}
Очевидно, что второй способ является более удобным, так как сокращает количество кода и исключает возможность написания неправильного типа.
Анализатор обнаружил функцию, которая принимает параметр по ссылке на константный объект, когда эффективнее это делать по копии.
Рассмотрим два примера для 64-битных систем.
В первом — функция принимает объекты типа 'const std::string_view &':
uint32_t foo_reference(const std::string_view &name) noexcept
{
return static_cast<uint32_t>(8 + name.size()) + name[0];
}
Ассемблерный код:
foo_reference(std::basic_string_view<char, std::char_traits<char> > const&):
mov eax, dword ptr [rdi] // <= (1)
mov rcx, qword ptr [rdi + 8] // <= (2)
movsx ecx, byte ptr [rcx]
add eax, ecx
add eax, 8
ret
В нем при каждом чтении данных из объекта типа 'const std::string_view &' происходит разыменование. Это инструкции 'mov eax, dword ptr [rdi]' (1) и 'mov rcx, qword ptr [rdi + 8] ' (2).
Во втором — функция принимает объекты типа 'std::string_view':
uint32_t foo_value(std::string_view name) noexcept
{
return static_cast<uint32_t>(8 + name.size()) + name[0];
}
Ассемблерный код:
foo_value(std::basic_string_view<char, std::char_traits<char> >):
movsx eax, byte ptr [rsi]
add eax, edi
add eax, 8
ret
Компилятор сгенерировал меньше кода для второго примера. Так происходит потому, что объект полностью помещается в регистры процессора и нет необходимости в адресации для доступа к нему.
Давайте разберёмся, какие объекты выгоднее передавать по копии, а какие по ссылке.
Обратимся к документу "System V Application Binary Interface AMD64 Architecture Processor Supplement". В нём описаны соглашения о вызовах функций для Unix-подобных систем. Пункт 3.2.3 описывает передачу параметров. Для каждого из них определяется свой класс. Если параметр имеет класс MEMORY, то он будет передаваться через стек. В противном случае параметр передаётся через регистры процессора, как в приведённом выше примере. Согласно подпункту 5 (C), если размер объекта превышает 16 байт, то он имеет класс MEMORY. Исключение составляют агрегатные типы размером до 64 байтов, первое поле которых имеют класс SSE, а все остальные SSEUP. Это означает, что объекты, имеющие больший размер, будут размещаться на стеке вызова функции, и для доступа к ним также необходима адресация.
Давайте рассмотрим ещё два примера для 64-битных систем.
В третьем — по копии принимается объект размером в 16 байт:
struct SixteenBytes
{
int64_t firstHalf; // 8-byte
int64_t secondHalf; // 8-byte
}; // 16-bytes
uint32_t foo_16(SixteenBytes obj) noexcept
{
return obj.firstHalf + obj.secondHalf;
}
Ассемблерный код:
foo_16(SixteenBytes): # @foo_16(SixteenBytes)
lea eax, [rsi + rdi]
ret
Компилятор сгенерировал эффективный код, разместив структуру в двух 64-битных регистрах.
Во четвертом примере по копии принимается структура размером в 24 байта:
struct MoreThanSixteenBytes
{
int64_t firstHalf; // 8-byte
int64_t secondHalf; // 8-byte
int32_t yetAnotherStuff; // 4-byte
}; // 24-bytes
uint32_t foo_more_than_16(MoreThanSixteenBytes obj) noexcept
{
return obj.firstHalf + obj.secondHalf + obj.yetAnotherStuff;
}
Ассемблерный код:
foo_more_than_16(MoreThanSixteenBytes):
mov eax, dword ptr [rsp + 16]
add eax, dword ptr [rsp + 8]
add eax, dword ptr [rsp + 24]
ret
Согласно соглашению о вызовах, компилятор вынужден разместить структуру на стеке. Это приводит к тому, что доступ к ней происходит косвенно, через адрес, который вычисляется с помощью регистра 'rsp'. В таком случае будет выдано предупреждение V813.
На Windows аналогичные правила вызовов функций. Подробнее можно почитать в документации.
Диагностика отключена на 32-битной платформе x86, так как на ней правила вызова функций отличаются в силу того, что не хватает регистров процессора для передачи аргументов.
У диагностики возможны ложные срабатывания. У ссылок на константные объекты могут быть необычные применения. Например, функция, в которую передаётся некий объект по ссылке, может сохранить её в глобальное хранилище. При этом сам объект, на который она ссылается, может изменяться.
Рассмотрим пример:
struct RefStorage
{
const int &m_value;
RefStorage(const int &value)
: m_value { value }
{}
RefStorage(const RefStorage &value)
: m_value { value.m_value }
{}
};
std::shared_ptr<RefStorage> rst;
void SafeReference(const int &ref)
{
rst = std::make_shared<RefStorage>(ref);
}
void PrintReference()
{
if (rst)
{
std::cout << rst->m_value << std::endl;
}
}
void foo()
{
int value = 10;
SafeReference(value);
PrintReference();
++value;
PrintReference();
}
Функция 'foo' вызывает функцию 'SafeReference' и передаёт ей в качестве параметра переменную 'value' по ссылке. Далее эта ссылка сохраняется в глобальное хранилище 'rst'. При этом переменная 'value' может изменяться, так как она сама не константная.
Приведённый код достаточно неестественный и плохо написан. В реальных проектах могут быть и более сложные случаи. Если программист знает, что делает, то диагностику можно подавить специальным комментарием '//-V835'.
Если в вашем проекте много таких мест, можно полностью отключить диагностику, добавив комментарий '//-V::835' в предкомпилированный заголовок или '.pvsconfig' файл. Подробнее о подавлении ложных предупреждений можно прочитать в документации.
Анализатор обнаружил ситуацию, в которой возможно лишнее копирование при объявлении переменной.
Рассмотрим пример:
void foo(const std::vector<std::string> &cont)
{
for (auto item : cont) // <=
{
std::cout << item;
}
}
В цикле 'for' происходит обход контейнера, содержащего элементы типа 'std::string'. Согласно правилам вывода результирующий тип переменной 'item' будет 'std::string'. Из-за этого на каждой итерации будет происходить копирование элемента контейнера. Можно также заметить, что в теле цикла не происходит модификаций переменной 'item'. Следовательно, лишнего копирования можно избежать. Для этого надо заменить тип 'auto' на 'const auto &'.
Исправленный пример:
void foo(const std::vector<std::string> &containter)
{
for (const auto &item : containter) // <=
{
std::cout << item;
}
}
Рассмотрим второй пример:
void use(const std::string &something);
void bar(const std::string &name)
{
auto myName = name;
use(myName);
}
В этом случае также лучше заменить 'auto' на 'const auto &', так как 'myName' в теле функции не модифицируется. Исправленный пример:
void use(const std::string &something);
void bar(const std::string &name)
{
const auto &myName = name;
use(myName);
}
Анализатор обнаружил использование функции 'emplace' / 'insert' ассоциативного контейнера стандартной библиотеки ('std::map', 'std::unordered_map'), у которого существует функция 'try_emplace'. Функция 'emplace' / 'insert' может привести к копированию или перемещению аргументов, даже если вставки не произойдёт (элемент с указанным ключом уже присутствует в контейнере). Это может привести к замедлению программы, а в случае перемещения аргумента — к преждевременному освобождению ресурсов.
В зависимости от реализации стандартной библиотеки, функция 'emplace' / 'insert' перед проверкой наличия элемента с указанным ключом может создать временный объект типа 'std::pair', в который аргументы функции будут скопированы или перемещены. Начиная со стандарта С++17, для контейнеров 'std::map' и 'std::unordered_map' была добавлена функция 'try_emplace'. Она гарантирует, что если элемент с указанным ключом уже существует, то аргументы функции не будут скопированы или перемещены.
Рассмотрим пример кода:
class SomeClass
{
std::string name, surname, descr;
public:
// User-defined constructor
SomeClass(std::string name, std::string surname, std::string descr);
// ....
};
std::map<size_t, SomeClass> Cont;
bool add(size_t id,
const std::string &name,
const std::string &surname,
const std::string &descr)
{
return Cont.emplace(id, SomeClass { name, surname, descr })
.second;
}
В примере в некоторый контейнер 'Cont' производят вставку объекта типа 'SomeClass' по ключу 'id'. Если объект по такому ключу уже ранее был добавлен, могут быть произведены следующие лишние операции:
Используя функцию 'try_emplace' вместо 'emplace', можно избежать лишних операций по формированию временного объекта типа 'std::pair<const size_t, SomeClass>':
bool add(size_t id,
const std::string &name,
const std::string &surname,
const std::string &descr)
{
return Cont.try_emplace(id, SomeClass { name, surname, descr })
.second;
}
Использование 'try_emplace' позволяет также конструировать объекты "по месту" внутри ассоциативного контейнера. В примере тип 'SomeClass' не является агрегатным и содержит определённый пользователем конструктор, поэтому можно избежать также и 3 вызовов конструкторов копирования строк:
bool add(size_t id,
const std::string &name,
const std::string &surname,
const std::string &descr)
{
return Cont.try_emplace(id, name, surname, descr)
.second;
}
Начиная с C++20, функция 'try_emplace' работает также и с агрегатными типами:
struct SomeClass
{
std::string name, surname, descr;
};
bool add(size_t id,
const std::string &name,
const std::string &surname,
const std::string &descr)
{
return Cont.try_emplace(id, name, surname, descr)
.second;
}
Анализатор обнаружил вызов функции поиска в ассоциативном упорядоченном контейнере ('std::set', 'std::miltiset', 'std::map' или 'std::multimap') c аргументом, тип которого отличается от типа ключа контейнера. В результате такого вызова произойдет создание временного объекта типа ключа из переданного аргумента.
Если конвертация типов является дорогой операцией (например, 'const char *' в 'std::string'), это может отразиться на производительности программы.
Начиная с C++14, можно избежать создания временного объекта. Для это нужно, чтобы компаратор упорядоченного ассоциативного контейнера поддерживал гетерогенный поиск. Для этого должны быть выполнены следующие условия:
Анализатор выдает предупреждение в случае, если в компараторе не объявлено имя 'is_transparent'.
Рассмотрим пример кода:
void foo(const char *str)
{
static std::set<std::string> cont;
auto it = cont.find(str); // <=
if (it != cont.end())
{
// do smth
}
}
В примере контейнер 'cont' по умолчанию объявлен с компаратором типа 'std::less<std::string>'. Этот компаратор не поддерживает гетерогенный поиск. Поэтому при каждом вызове функции 'find' происходит создание временного объекта 'std::string' из 'const char *'.
Чтобы избежать создания временного объекта типа 'std::string', нужно использовать контейнер с компаратором, поддерживающим гетерогенный поиск и умеющим сравнивать 'std::string' c 'const char *'. Например, можно использовать 'std::set' с компаратором 'std::less<>':
void foo(const char *str)
{
static std::set<std::string, std::less<>> cont;
auto it = cont.find(str);
if (it != cont.end())
{
// do smth
}
}
Теперь при вызове функции 'find' временный объект типа 'std::string' не создается, и аргумент типа 'const char *' напрямую сравнивается с ключами.
Это диагностическое правило основано на пункте F.49 CppCoreGuidelines.
Анализатор обнаружил объявление функции, которая возвращает значения константного типа. Несмотря на то, что квалификатор 'const' по задумке автора специально не позволяет модифицировать временные объекты, такое поведение может препятствовать семантике перемещения. В результате возможно замедление из-за дорогостоящего копирования больших объектов.
Рассмотрим синтетический пример:
class Object { .... };
const std::vector<Object> GetAllObjects() {...};
void g(std::vector<Object> &vo)
{
vo = GetAllObjects();
}
Из-за наличия квалификатора 'const' у возвращаемого типа функции 'GetAllObjects' при выборе перегрузки оператора '=' компилятор не будет рассматривать оператор перемещения и выберет оператор копирования. Собственно, будет выполнено копирования вектора объектов, возвращенного функцией.
Чтобы оптимизировать код, достаточно удалить квалификатор 'const':
class Object { .... };
std::vector<Object> GetAllObjects() {...};
void g(std::vector<Object> &vo)
{
vo = GetAllObjects();
}
Теперь при вызове функции 'GetAllObjects' возвращаемый вектор объектов будет перемещен.
Обращаем внимание, что диагностическое правило применяется только для кода, написанного на C++11 или выше. Это поведение связано с семантикой перемещения, которая была добавлена в C++11.
Рассмотрим следующий код (реализация пользовательского типа для длинной арифметики):
class BigInt
{
private:
....
public:
BigInt& operator++();
BigInt& operator++(int);
BigInt& operator--();
BigInt& operator--(int);
friend const BigInt operator+(const BigInt &,
const BigInt &) noexcept;
// other operators
};
void foo(const BigInt &lhs, const BigInt &rhs)
{
auto obj = ++(lhs + rhs); // compile-time error
// can't call operator++()
// on const object
}
Такой паттерн использовался в прошлом, когда семантика перемещения еще не была внедрена в C++, и при этом хотелось запретить вызывать модифицирующие операции на временном объекте.
Если возвращать из перегруженного оператора '+' не константный объект, то можно будет вызывать перегруженный оператор префиксного инкремента на временном объекте. Для встроенных арифметических типов такая семантика запрещена.
Начиная с C++11 код можно переделать при помощи ref-квалификаторов нестатических функций-членов и дать возможность компилятору применять семантику перемещения:
class BigInt
{
....
public:
BigInt& operator++() & noexcept;
BigInt& operator++(int) & noexcept;
BigInt& operator--() & noexcept;
BigInt& operator--(int) & noexcept;
friend BigInt operator+(const BigInt &,
const BigInt &) noexcept;
// other operators
};
void foo(const BigInt &lhs, const BigInt &rhs)
{
auto obj = ++(lhs + rhs); // compile-time error
// can't call BigInt::operator++()
// on prvalue
}
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор позволяет обнаружить вызов функций, у которых существует "расширенный" аналог. Под термином "расширенные" понимаются функции, имеющие суффикс Ex, такие как VirtualAllocEx, SleepEx, GetDCEx, LoadLibraryEx, FindResourceEx.
Рассмотрим исходный код:
void foo();
void fooEx(float x);
void foo2();
...
void test()
{
foo(); // V2001
foo2(); // OK
}
В месте вызова функции "foo", будет выдано диагностическое сообщение V2001, так как имеется функция с тем же именем, но оканчивающаяся на "Ex". Функция "foo2" не имеет альтернативного варианта, и диагностическое сообщение выдаваться не будет.
Сообщение V2001 будет также выдано для случая:
void fooA(char *p);
void fooExA(char *p, int x);
...
void test()
{
fooA(str); // V2001
}
Родственным диагностическим сообщением является V2002.
Данная диагностика классифицируется как:
|
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор позволяет обнаружить вызов функций, у которых существует 'Ptr' аналог. Имеются в виду функции, имеющие в составе своего названия суффикс 'Ptr', такие как: 'SetClassLongPtr', 'DSA_GetItemPtr'.
Рассмотрим исходный код:
void foo(int a);
void fooPtr(int a, bool b);
....
void test()
{
foo(1); // V2002
}
В месте вызова функции 'foo', будет выдано предупреждение V2002, так как имеется функция с тем же именем, но оканчивающаяся на 'Ptr'. Функция 'foo2' не имеет альтернативного варианта, и предупреждение выдаваться не будет.
Сообщение V2002 будет также выдано для случая:
void fooA(char *p);
void fooPtrA(char *p, int x);
....
void test()
{
fooA(str); // V2002
}
Предупреждение не выдается в случае, когда вызов одноименной функции происходит из её расширенной версии:
class A
{
....
void foo() { .... };
void fooPtr()
{
foo(); // ok
}
....
};
Родственным диагностическим сообщением является V2001.
Данная диагностика классифицируется как:
|
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор позволяет обнаружить все явные приведения типов с плавающих точкой к целочисленным знаковым типам.
Примеры конструкций, на которые анализатор выдаст данное диагностическое сообщение:
float f;
double d;
long double ld;
int i;
short s;
...
i = int(f); // V2003
s = static_cast<short>(d); // V2003
i = (int)ld; // V2003
Родственным диагностическим сообщением является V2004.
Данная диагностика классифицируется как:
|
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор позволяет обнаружить все явные приведения типов с плавающих точкой к целочисленным беззнаковым типам.
Примеры конструкций, на которые анализатор выдаст данное диагностическое сообщение:
float f;
double d;
long double ld;
unsigned u;
size_t s;
...
u = unsigned(f); // V2004
s = static_cast<size_t>(d); // V2004
u = (unsigned)ld; // V2004
Родственным диагностическим сообщением является V2003.
Данная диагностика классифицируется как:
|
Данное диагностическое предупреждение добавлено по просьбе пользователей.
Анализатор позволяет обнаружить явные приведения типов в программе на C++, написанные в старом стиле языка C. В языке C++ более безопасно явно приводить типы с использованием операторов 'static_cast', 'const_cast' и 'reinterpret_cast'.
Диагностическое правило V2005 помогает выполнить рефакторинг кода, поменять старый стиль приведения типов на новый. Иногда это позволяет выявить ошибки.
Примеры конструкций, на которые анализатор выдаст данное диагностическое сообщение:
int i;
double d;
size_t s;
void *p;
....
i = int(p); //+V2005
d = (double)i; //+V2005
s = (size_t)(d); //+V2005
Диагностическое сообщение V2005 не выдается в следующих случаях:
1. Это программа на языке C.
2. Осуществляется приведение к типу 'void'. Такое приведение типа никакой опасности в себе не несёт и используется, чтобы подчеркнуть, что некий результат никак не используется. Пример:
(void)fclose(f);
3. Приведение типа находится в макросе. Если выдавать предупреждения для макросов, то будет огромное количество срабатываний при использовании различных системных констант и макросов. Причем, поправить их все равно нет никакой возможности. Примеры:
#define FAILED(hr) ((HRESULT)(hr) < 0)
#define SRCCOPY (DWORD)0x00CC0020
#define RGB(r,g,b)\
((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))\
|(((DWORD)(BYTE)(b))<<16)))
По умолчанию срабатывания V2005 диагностики имеют второй уровень достоверности (Medium). Если подозрительный код находится в шаблоне, а приведение происходит к типу шаблонного параметра, то уровень предупреждения снижается до третьего (Low).
Рассмотрим синтетический пример:
template <typename TemplateParam>
void foo(const std::vector<SomeType>& vec)
{
auto a = (TemplateParam)(vec[0]); //+V2005 //3rd level
auto b = TemplateParam(vec[3]); //+V2005 //3rd level
// ....
auto i = (int)a; //+V2005 //2 level
auto i = int(a); //+V2005 //2 level
// ....
}
Без инстанцирований шаблона понять, что за тип скрывается под 'TemplateParam', сложно. Если же приведение происходит к известному типу внутри шаблона, то анализатор будет по-прежнему выдавать срабатывания второго уровня. В случае, если предупреждения третьего уровня не имеют практической пользы для вас, их можно подавить при помощи специального комментария:
//-V::2005:3
По дополнительной просьбе пользователей появилась возможность управлять поведением диагностики V2005. В общем заголовочном файле или pvsconfig-файле пишется комментарий специального вида. Пример использования:
//+V2005 ALL
Существует три режима:
а) Режим по умолчанию: на каждое преобразование типов в стиле C выдаётся предупреждающее сообщение, которое гласит: используйте вместо преобразования типов конструкции вида 'static_cast', 'const_cast' и 'reinterpret_cast'.
б) ALL - на каждое преобразование типов в стиле C анализатор выдаёт рекомендацию о том, какое ключевое слово (ключевые слова) требуется использовать вместо него. Изредка возможны единичные неверные рекомендации, связанные с преобразованием сложных шаблонных типов. Также изредка возможна такая ситуация, что анализатор не сможет определить тип преобразования и выдал обыкновенное сообщение без точного указания типа приведения.
//+V2005 ALL
в) NO_SIMPLE_CAST - аналогично предыдущему, но сообщение показывается только если в преобразовании участвует хотя бы один указатель или требуется преобразование сложнее чем static_cast.
//+V2005 NO_SIMPLE_CAST
Дополнительные ссылки:
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор позволяет обнаружить все неявные приведения enum-типов к целочисленным типам.
Диагностическое правило V2006 помогает выполнить рефакторинг кода и иногда выявить ошибки.
Пример конструкции, на которую анализатор выдаст данное диагностическое сообщение:
enum Orientation {
Horizontal = 0x1,
Vertical = 0x2
};
Orientation orientation = Horizontal;
int pos = orientation; // V2006
Диагностическое сообщение V2006 не выдается в следующих случаях.
Первое. Анализатор не предупреждает о сравнении именованной константы перечисления с переменной имеющий тип, например 'int'. Хотя перед началом сравнения константа неявно приводится к типу 'int', такое встречается слишком часто, чтобы выдавать на это предупреждение.
int pos = foo();
if (pos == Vertical) // Ok
{
....
}
Второе. Сравниваются две константы перечисляемого типа:
enum E
{
ZERO, ONE, TWO
};
void foo(E e1, E e2)
{
if (e1 == e2) // ok
....
else if (e1 > e2) // ok
....
else if (e1 != e2) // ok
....
}
Третье. Неявное преобразование типа происходит при сдвиге именованной константы, чтобы инициализировать другую именованную константу или элементы массива:
enum E
{
FIRST_BIT = 1,
SECOND_BIT = FISRT_BIT << 1, // ok
THIRD_BIT = FISRT_BIT << 2, // ok
....
};
int A[3] = {
FIRST_BIT,
FIRST_BIT << 1, // ok
FIRST_BIT << 2 // ok
};
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор позволяет обнаружить ряд мест с подозрительными бинарными операциями, где можно упростить код для повышения его читаемости.
Подозрительные бинарные операции:
Диагностическое правило V2007 помогает выполнять рефакторинг кода и иногда выявлять ошибки.
Примеры конструкций, на которые анализатор выдаст данное диагностическое сообщение:
int X = 1 ^ 0;
int Y = 2 / X;
Приведённый код можно упростить. Пример корректного кода:
int X = 1;
int Y = 2;
Для сокращения количества ложных сообщений, есть несколько исключений. Например, диагностическое сообщение V2007 не выдаётся в случае, если подозрительное выражение находится в макросе или является индексом массива.
Данная диагностика классифицируется как:
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор рассчитывает и выводит значения "Цикломатической сложности" для функций. Цикломатическая сложность - это одна из метрик исходного кода, используемая для оценки сложности программы.
Слишком большое значение цикломатической сложности указывает на необходимость обратить внимание на код функции, для которой было выдано диагностическое сообщение. Велика вероятность того, что необходим рефакторинг этих функций.
Сообщения выводятся только для тех функций, цикломатическая сложность которых превысила пороговое значение. По умолчанию оно равно 50.
Изменить пороговое значение можно добавив в код своей программы такой комментарий:
//-V2008_CYCLOMATIC_COMPLEXITY=N
N - пороговое значение для вывода цикломатической сложности. Значение должно быть больше 1. Комментарий действует в пределах единицы компиляции. Поэтому, если вы хотите задать пороговое значение для всего проекта, то поместите этот комментарий в один из базовых заголовочных файлов. Таким файлом, например, может быть stdafx.h.
Есть ещё одна дополнительная опция, включающая модифицированный вариант расчёта цикломатической сложности:
//-V2008_MODIFIED_CYCLOMATIC_COMPLEXITY
Этот комментарий говорит анализатору, считать цикломатическую сложность оператора switch() за единицу. Количество "case x:" значения не имеет.
Данная диагностика классифицируется как:
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор предполагает, что формальный параметр в виде указателя/ссылки может ссылаться на константный объект.
Данное предупреждение может быть выдано на следующий код:
Эта диагностика может помочь при рефакторинге кода или предотвращении программных ошибок в будущем.
Рассмотрим пример такого кода:
void foo(int *a)
{
int b = a[0] + a[1] + a[2];
.... переменная 'a' больше не используется
}
Переменную 'a' лучше сделать указателем на константное значение. Таким образом, сразу станет понятно, что аргумент используется только для чтения.
Исправленный код:
void foo(const int *a)
{
int b = a[0] + a[1] + a[2];
.... переменная 'a' больше не используется
}
Примечание. Анализатор может ошибаться, пытаясь понять, модифицируется переменная в теле функции или нет. Если вы заметили явно ложное срабатывание, просьба прислать нам соответствующий пример кода.
Иногда выдаваемые анализатором сообщения могут показаться несколько странными. Давайте рассмотрим один из таких случаев более подробно:
typedef struct tagPOINT {
int x, y;
} POINT, *PPOINT;
void foo(const PPOINT a, const PPOINT b) {
a->x = 1; // Данные можно изменить
a = b; // Ошибка компиляции
}
Анализатор предлагает сделать тип, на который ссылается указатель, константным. Это может показаться странным, так как здесь присутствует ключевое слово 'const'. Дело в том, что 'const' означает константность указателя. Объекты, на которую ссылаются указатели, доступна для модификации.
Для того, чтобы сделать сами объекты константными, следует поступить так:
....
typedef const POINT *CPPOINT;
void foo(const CPPOINT a, const CPPOINT b) {
a->x = 1; // Ошибка компиляции
a = b; // Ошибка компиляции
}
Данное диагностическое правило добавлено по просьбе пользователей.
Обработчики для разных типов исключений выполняют одно и то же действие. Возможно это ошибка или код можно упростить.
Рассмотрим пример:
try
{
....
}
catch (AllocationError &e)
{
WriteLog("Memory Allocation Error");
return false;
}
catch (IOError &e)
{
WriteLog("Memory Allocation Error");
return false;
}
Код писался с помощью Copy-Paste и поэтому при ошибке чтения из файла, в лог запишется неверное сообщение. На самом деле, код должен был выглядеть, например, так:
try
{
....
}
catch (AllocationError &e)
{
WriteLog("Memory Allocation Error");
return false;
}
catch (IOError &e)
{
WriteLog("IO Error: %u", e.ErrorCode());
return false;
}
Рассмотрим другой пример. Это корректный код, но его можно упростить:
try
{
....
}
catch (std::exception &)
{
Disconnect();
}
catch (CException &)
{
Disconnect();
}
catch (...)
{
Disconnect();
}
Поскольку все обработчики одинаковы и перехватываются все виду исключений, то можно написать короче:
try
{
....
}
catch (...)
{
Disconnect();
}
Рассмотрим ещё один пример.
class DBException : public std::exception { ... };
class SocketException : public DBException { ... };
class AssertionException : public DBException { ... };
....
try
{
....
}
catch (SocketException& e){
errorLog.push_back(e.what());
continue;
}
catch (AssertionException& e) {
errorLog.push_back(e.what());
continue;
}
catch(std::exception& e){
errorLog.push_back(e.what());
continue;
}
Есть несколько классов, которые наследуются от класса 'std::exception'. Все обработчики исключений совпадают. Обратите внимание, что в том числе, перехватывается и исключение типа 'std::exception'. Этот код избыточен. Можно оставить только обработчик для 'std::exception'. Остальные исключения будут так-же перехвачены и обработаны, так как наследуются от 'std::exception'. Метод 'what()' является виртуальным, поэтому в 'errorLog' будет сохранена правильная информация о типе ошибки.
Упрощенный код:
try
{
....
}
catch(std::exception& e){
errorLog.push_back(e.what());
continue;
}
Данное диагностическое правило добавлено по просьбе пользователей.
Диагностика выявляет следующую ситуацию. В базовом классе имеется виртуальная функция, в которой один из параметров имеет знаковый тип. В наследнике есть точно такая-же функция, но этот параметр является беззнаковым. Или ситуация противоположная: в базовом классе беззнаковый тип, в наследнике - знаковый.
Эта диагностика помогает выявить ошибки, когда при большом факторинге меняют тип функции в одном из классов, но забывают изменить эту функцию в другом классе.
Рассмотрим пример:
struct Q { virtual int x(unsigned) { return 1; } };
struct W : public Q { int x(int) { return 2; } };
На самом деле, код должен быть таким:
struct Q { virtual int x(unsigned) { return 1; } };
struct W : public Q { int x(unsigned) { return 2; } };
Если в базовом классе будет две функции 'x' с аргументами 'int' и "unsigned', то анализатор предупреждение V2011 выдавать не будет.
Данное диагностическое правило добавлено по просьбе пользователей.
В коде обнаружен класс, унаследованный от 'std::unary_function' или 'std::binary_function'. При этом среди параметров его шаблона находятся классы, передаваемые по значению. Очевидно, передача по значению объекта класса (особенно "тяжёлого", с большим количеством полей или сложным конструктором) может повлечь за собой дополнительные расходы по времени и памяти. Конечно, передача объекта по значению не всегда плоха. Передача по значению имеет смысл, если надо сохранить исходный объект и работать с изменённой копией. Но иногда код, в котором происходит передача объекта по значению, получился случайно и является плохим решением.
Рассмотрим пример. Данный функтор при каждом вызове будет копировать два объекта типа 'std::string' вместо того, чтобы передать их по значению:
class example : public std::binary_function
<std::string, std::string, bool>
{
public:
result_type operator()(
first_argument_type first,
second_argument_type second)
{
return first == second;
};
};
Разумеется, простейшим решением в данном случае может послужить передача параметров шаблона по ссылке, а не по значению:
class example : public std::binary_function
<const std::string &, const std::string &, bool> ....
Анализатор также не выдаст предупреждение, если в теле функции все аргументы, переданные не по ссылке, изменяются:
class example : public std::binary_function
<std::string, std::string, bool>
{
public:
result_type operator()(
first_argument_type first,
second_argument_type second)
{
std::replace(first.begin(), first.end(), 'u', 'v');
std::replace(second.begin(), second.end(), 'a', 'b');
return first == second;
};
};
Данное диагностическое правило добавлено по просьбе пользователей. Оно весьма специфично и сделано для решения определённой задачи, которая вряд ли может быть интересна широкому кругу пользователей.
Бывает полезно отследить все вызовы COM-интерфейсов, где указатель на какой-то класс явно приводится к указателю целочисленного типа или просто к целочисленному типу. Некоторые пользователи хотят иметь возможность проверить, правильно ли обрабатываются переданные данные на стороне COM-сервера.
Предположим, имеется какой-то контейнер, содержащий в себе массив элементов типа unsigned. Он передаётся в функцию, которая интерпретирует его как массив элементов типа 'size_t'. Этот код будет корректно работать в 32-битной системе, но в 64-битной программе данные начнут интерпретироваться неправильно. Пример:
MyVector<unsigned> V;
pInterface->Foo((unsigned char *)(&V));
....
void IMyClass::Foo(unsigned char *p)
{
MyVector<size_t> *V = (V *)(p);
....
}
Фактически, это 64-битная ошибка. Мы не стали включать в набор 64-битных диагностик, так как они слишком специфична. Диагностика позволяет найти потенциально опасные вызовы, а уже программист должен глазами изучить методы, принимающие данные и разобраться, имеет место ошибка или нет.
Данная диагностика классифицируется как:
Данное диагностическое правило добавлено по просьбе пользователей. Эта диагностика весьма специфична и предназначена для проверки кода библиотек.
Есть функции, которые прерывают или могут прерывать выполнение программы. Использовать их в своей программе хоть и некрасиво, но вполне можно, т.к. автор программы знает, чего он хочет и что делает.
А вот использовать эти функции в библиотеках нельзя! Неизвестно, как и где библиотека будет использоваться. Будет плохо, если библиотека завершит программу, и пользователь потеряет данные. Библиотеки в случае ошибок должны возвращать статус ошибки или кидать исключение, но не прекращать работу программы.
Рассмотрим пример:
char *CharMallocFoo(size_t length)
{
char *result = (char*)malloc(length);
if (!result)
abort();
return result;
}
Функция 'CharMallocFoo' прервёт выполнение программы в случае неудачной попытки выделить динамическую память. Как вариант, лучше возращать из фукнции нулевой указатель для обработки такой ситации пользователем библиотеки.
Данная диагностика классифицируется как:
|
Данное диагностическое правило добавлено по просьбе пользователей.
Идентификатор, объявленный в области видимости и не отличающийся по имени от другого идентификатора, объявленного в обрамляющей области видимости, "скрывает" внешний идентификатор. Это может привести к путанице или программной ошибке.
Например, такая коллизия имен может привести к логической ошибке, как в примере ниже:
int foo(int param)
{
int i = 0;
if (param > 0)
{
int i = var + 1;
}
return i;
}
При прочтении этого кода на первый взгляд кажется, что когда в функцию 'foo' передается положительное значение параметра, то результатом вычисления будет это значение, увеличенное на '1'. Однако это не так, и на самом деле функция всегда возвращает '0'. Чтобы показать, что происходит в действительности, исключим из кода коллизию идентификаторов:
int foo(int param)
{
int i_outer = 0;
if (param > 0)
{
int i_inner = var + 1;
}
return i_outer;
}
Теперь видно, что присваивание внутри ветки 'if'в переменную 'i' не влияет на результат вычисления функции 'foo'. Внутренний идентификатор 'i' ('i_inner') скрывает внешний идентификатор 'i' ('i_outer'), что приводит к ошибке.
Данная диагностика классифицируется как:
|
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор обнаружил вызов функции, которая помечена пользователем как опасная/запрещенная.
Часто использование некоторых функций в проекте может быть запрещено. Например, в соответствии со стилем кодирования, который принят в проекте. Анализатор может находить использование таких функций, если они были размечены пользовательской аннотацией следующего вида:
//+V2016, function:foo
//+V2016, class:SomeClass, function:foo
//+V2016, namespace:SomeNamespace, class:SomeClass, function:foo
Например, пользователь отметил функцию 'malloc' следующим образом:
//+V2016, function:malloc
Теперь, если анализатор встретит вызов функции 'malloc', то выдаст предупреждение 1 уровня:
struct SomeStruct { .... };
void foo()
{
struct SomeStruct *p = (SomeStruct *) malloc(....);
}
Пользователь может проаннотировать функцию, лежащую в любом пространстве имен и/или классе:
class A
{
// ....
void a(int);
// ....
};
namespace BN
{
class B
{
// ....
void b(double);
// ....
};
}
//+V2016, class:A, function:a
//+V2016, namespace:BN, class:B, function:b
void foo()
{
A a;
a.a(); // <=
BN::B b;
b.b(); // <=
}
Примечание. По умолчанию пользовательские аннотации не применяются к виртуальным функциям. О том, как включить данный функционал, вы можете прочитать здесь.
Данная диагностика классифицируется как:
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор обнаружил подозрительное выражение со строковым литералом, текст которого совпадает с именем переменной строкового типа. Такое выражение может содержать опечатку, которую сложно выявить как на этапе ревью кода, так и при сборке.
Рассмотрим следующий синтетический пример:
bool CheckCredentials(const std::string& username,
const std::string& password)
{
return users[username].password == "password";
}
Данная функция должна проверять, совпадает ли переданный пароль с тем, что был записан в хранилище данных пользователей. При работе с данными строковых типов зачастую можно случайно набрать лишние кавычки. Поскольку такое сравнение синтаксически корректно, оно будет успешно скомпилировано, хотя очевидно, что функция не работает как планировалось. Если такой код окружён однотипными выражениями для проверки других данных, то ревьювер может посчитать его слишком тривиальным и не уделить проверке должного внимания.
Еще один пример опечатки, найденный в реальном проекте:
qboolean QGL_Init( const char *dllname ) {
....
// NOTE: this assumes that 'dllname' is lower case (and it should be)!
if ( strstr( dllname, _3DFX_DRIVER_NAME ) ) {
if ( !GlideIsValid() ) {
ri.Printf( PRINT_ALL,
"...WARNING: missing Glide installation, assuming no 3Dfx available\n" );
return qfalse;
}
}
if ( dllname[0] != '!' && strstr( "dllname", ".dll" ) == NULL ) { // <=
Com_sprintf( libName, sizeof( libName ), "%s\\%s", systemDir, dllname );
} else
{
Q_strncpyz( libName, dllname, sizeof( libName ) );
}
....
}
Фрагмент выражения 'strstr( "dllname", ".dll" ) == NULL' всегда будет иметь значение true, поскольку в строке "dllname" не содержится подстроки ".dll". На самом деле автор этого кода хотел проверить содержимое переменной 'dllname'.
К сожалению, эта диагностика часто выдаёт ложные срабатывания, поскольку сложно надёжно определить логику обработки тех или иных переменных. Например, переменные и текстовые литералы часто пересекаются при работе с key-value контейнерами, предназначенными для связывания данных с их именами. Впрочем, такие предупреждения не занимают много времени при просмотре отчёта и код можно быстро отрефакторить или подавить лишние предупреждения.
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор обнаружил ситуацию с удалением 'const'-квалификатора. Изменение объекта, объявленного с квалификатором 'const', через указатель/ссылку на не-'const' тип ведет к неопределенному поведению. Также зачастую такой код свидетельствует о плохом дизайне приложения.
Рассмотрим пример:
void read_settings(const char *buf);
const char* get_settings_file_name();
bool settings_present();
// ....
void init_settings()
{
const char name[MAX_PATH] = "default.cfg";
if (settings_present())
{
strcpy((char *)name, get_settings_file_name());
}
read_settings(name);
}
Для того, чтобы избежать неопределённого поведения, нужно отказаться от константности при объявлении локальной переменной:
void read_settings(const char *buf);
const char* get_settings_file_name();
bool settings_present();
// ....
void init_settings()
{
char name[MAX_PATH] = "default.cfg";
if (settings_present())
{
strcpy(name, get_settings_file_name());
}
read_settings(name);
}
Данное диагностическое правило добавлено по просьбе пользователей.
Анализатор обнаружил ситуацию с удалением 'volatile'-квалификатора. Доступ к объекту, объявленному с квалификатором 'volatile', через указатель/ссылку на не-'volatile' тип ведет к неопределенному поведению.
Пример кода, на который анализатор выдаст предупреждение:
int foo(int &value)
{
while (value)
{
// do some stuff...
}
return 0;
}
int main()
{
volatile int value = 1;
return foo((int &) value);
}
Другой пример кода, на который анализатор выдаст предупреждение:
#include <utility>
int foo()
{
int x = 30;
volatile int y = 203;
using std::swap;
swap(x, const_cast<int &>(y)); // <=
return x;
}
Данное диагностическое правило добавлено по просьбе пользователей.
Диагностическое правило позволяет обнаружить использование конструкций 'break;' и 'continue;' в телах циклов. Также оно помогает выполнить рефакторинг кода и избежать появления ошибок при изменении старого кода цикла на новый.
Рассмотрим синтетический пример кода:
namespace fs = std::filesystem;
std::vector<fs::path> existingPaths;
void SaveExistingPaths(const std::vector<fs::path> &paths)
{
for (auto &&path: paths)
{
if (!fs::exists(path))
{
break; // <=
}
existingPaths.emplace_back(path);
}
}
В приведённом фрагменте кода задумывалось, что все существующие пути будут сохранены в отдельный контейнер. Однако на первом же несуществующем пути цикл прервётся из-за использования 'break;'. Вместо него здесь должна была использоваться конструкция 'continue;'. Ещё более правильным решением было бы совсем избавиться от 'break;' или 'continue;' в цикле:
namespace fs = std::filesystem;
std::vector<fs::path> existingPaths;
void SaveExistingPaths(const std::vector<fs::path> &paths)
{
for (auto &&path: paths)
{
if (fs::exists(path))
{
existingPaths.emplace_back(path);
}
}
}
Примечание. Данная диагностика отключена по умолчанию, чтобы не выдавать большое количество срабатываний.
Для того, чтобы включить диагностическое правило на анализируемом файле, можно воспользоваться следующим комментарием:
//+V::2020
Также этот комментарий можно расположить в общем заголовочном файле. Тогда диагностическое правило будет активировано на всех файлах, которые включают этот заголовочный файл. Например, для этого подходит 'stdafx.h'.
Для того, чтобы включить диагностику только на определённом блоке кода, можно воспользоваться директивой '#pragma pvs':
namespace fs = std::filesystem;
std::vector<fs::path> existingPaths;
#pragma pvs(push)
#pragma pvs(enable: 2020)
void SaveExistingPaths(const std::vector<fs::path> &paths)
{
for (auto &&path: paths)
{
if (!fs::exists(path))
{
break; // <= V2020
}
existingPaths.emplace_back(path);
}
}
#pragma pvs(pop)
Данное диагностическое правило добавлено по просьбе пользователей.
Диагностическое правило позволяет обнаружить в коде вызов макроса, который может привести к аварийному завершению программы. Таким макросом может быть стандартный 'assert'. Несмотря на то, что его использование позволяет устранять ошибки и снизить вероятность возникновения уязвимостей, его вызов может быть недопустим в различных сценариях. Одним из таких вариантов может быть написание библиотечного кода.
Рассмотрим следующий код:
[[noreturn]] void assertHandler();
#define ASSERT(expr) (!!(expr) || (assertHandler(), 0))
void foo(int i)
{
if (i < 0)
{
ASSERT(false && "The 'i' parameter must be non-negative");
}
}
В примере приведена пользовательская реализация макроса 'assert', которая вызывает функцию, не возвращающую поток управления вызывающей функции. Требуется, чтобы анализатор выдал срабатывание на вызов этого макроса. Для этого надо внести небольшие изменения в код:
[[noreturn]] void assertHandler(); // N1
#define ASSERT(expr) (!!(expr) || (assertHandler(), 0))
//V_PVS_ANNOTATIONS annotations.json // N2
//V_ASSERT_CONTRACT, assertMacro:ASSERT // N3
void foo(int i)
{
if (i < 0)
{
ASSERT(false); // <= V2021
}
}
На этом примере рассмотрим, как настраивается механизм распознавания пользовательского макроса.
Разметка функции как noreturn. Функция 'assertHandler', расположенная внутри макроса 'ASSERT', должна быть размечена как 'noreturn' (строка N1). Это можно сделать как при помощи стандартных атрибутов (C23 и C++11):
[[noreturn]] void assertHandler(); // since C23 or C++11
Так и специфичными для компилятора атрибутами (например, MSVC или GCC / Clang):
__declspec(noreturn) void assertHandler(); // MSVC
__attribute__((noreturn)) void assertHandler(); // GCC, Clang
Если нет возможности изменить исходный код и разметить функцию атрибутом, то можно сделать это c помощью системы пользовательских аннотаций в формате JSON. Для этого нужно создать файл формата JSON со следующим содержимым:
{
"version": 1,
"annotations":
[
{
"type": "function",
"name": "assertHandler",
"parameters": [],
"attributes": [ "noreturn" ]
},
....
]
}
Затем этот файл должен быть включён во время анализа одним из описанных способов. В примере это происходит в строке N2.
Разметка макроса. Необходимо, чтобы анализатор учитывал, что из-за макроса 'ASSERT' выполнение кода может прерваться. Делается это при помощи комментария на строке N3. Более подробно об этом механизме можно узнать здесь.
Пользователь может отключить диагностическое правило для функции, если он уверен, что использование макроса безопасно в этом контексте. Сделать это можно с помощью разметки функции, в которой вызывается макрос, с помощью комментария:
//-V2021_IGNORE_ASSERT_IN_FUNCTION, function: My::Qualified::Name
Примечание. Данная диагностическое правило срабатывает также и на стандартный 'assert', и поэтому отключено по умолчанию, чтобы не выдавать большое количество срабатываний.
Для того, чтобы включить диагностику, можно воспользоваться механизмом включения через комментарий или директивой '#pragma pvs'.
Данная диагностика классифицируется как:
|
Данное диагностическое правило добавлено по просьбе пользователей.
Правило позволяет найти все неявные приведения целочисленных типов к типам перечислений. Это помогает выполнить рефакторинг кода и иногда выявить ошибки.
Данное правило актуально только для языка C.
Рассмотрим примеры конструкций, на которые анализатор выдаст предупреждение.
Пример N1:
typedef enum
{
Horizontal = 0x1,
Vertical = 0x2
} Orientation;
void foo1()
{
int pos = 1;
Orientation orientation = pos; // V2022
}
В этом примере происходят неявные приведения целочисленного значения 'pos' к переменной типа 'Orientation'.
Пример N2:
Orientation GetOrientation (int pos)
{
return pos; // V2022
}
Функция 'GetOrientation' возвращает значение типа 'Orientation' и неявно приводит значение 'pos' к этому типу.
Пример N3:
Orientation GetOrientation (bool b)
{
int posOne = 1;
int posTwo = 2;
return b ? posOne : posTwo; // V2022
}
В этом примере функция использует условный оператор (?:) для выбора между двумя целочисленными переменными 'posOne' и 'posTwo', что также приводит к неявному приведению.
Пример N4:
void foo2(Orientation o){}
void foo3()
{
foo2(Horizontal); //ok
foo2(Horizontal | Vertical); // V2022
}
В результате операции 'Horizontal | Vertical' получается целое число, представляющее побитовый результат. При передаче этого значения в функцию 'foo2', принимающую аргумент типа 'Orientation', происходит неявное приведение типов, что может привести к передаче недопустимого значения.
Анализатор обнаружил операцию упаковки внутри часто выполняемого метода. Упаковка является дорогим процессом, требующим выделения памяти в управляемой куче. Как следствие, большое количество операций упаковки может негативно сказаться на производительности приложения.
Рассмотрим пример:
Vector3 _value;
....
void OnGUI()
{
GUILayout.Label(string.Format(...., _value));
}
В проектах, использующих игровой движок Unity, функция 'OnGUI' используется для отрисовки интерфейса и обработки связанных с ним событий. Она вызывается как минимум один раз за кадр, то есть её код выполняется достаточно часто.
В этом примере вызывается метод 'string.Format', последним аргументом которого является поле значимого типа ('Vector3'). При таком вызове используется перегрузка 'string.Format(string, object)'. Так как ожидается аргумент типа 'object', значение '_value' будет упаковано.
Упаковки можно избежать, вызвав у поля '_value' метод 'ToString':
Vector3 _value;
....
void OnGUI()
{
GUILayout.Label(string.Format(...., _value.ToString()));
}
Рассмотрим ещё один пример:
struct ValueStruct { int a; int b; }
ValueStruct _previousValue;
void Update()
{
....
ValueStruct newValue = ....
....
if (CheckValue (newValue)
....
}
bool CheckValue(ValueStruct value)
{
....
if(_previousValue.Equals(value))
....
}
Метод 'Update' также широко используется в проектах на Unity. Его код выполняется каждый кадр.
В коде 'Update' вызывается метод 'CheckValue'. В нём происходит неявная упаковка, так как значение 'value' передаётся в метод 'Equals' (параметр стандартного 'Equals' имеет тип 'object').
Одним из вариантов решения может быть добавление к типу 'ValueStruct' метода 'Equals', принимающего параметр типа 'ValueStruct':
struct ValueStruct
{
int a;
int b;
public bool Equals(ValueStruct other)
{
....
}
}
В таком случае в методе 'CheckValue' будет использоваться перегрузка 'Equals(ValueStruct)', что позволит избежать упаковки.
Данная диагностика классифицируется как:
Анализатор обнаружил возможность оптимизации операций конкатенации внутри часто выполняемого метода.
Конкатенация приводит к созданию нового объекта строки, а значит и к дополнительному выделению памяти в управляемой куче. В целях повышения производительности этого следует избегать в часто выполняемом коде. В случае если к значению строки нужно несколько раз добавить различные фрагменты, вместо конкатенации разработчиками Unity рекомендуется использовать тип 'StringBuilder'.
Рассмотрим пример:
[SerializeField] Text _stateText;
....
void Update()
{
....
string stateInfo = ....;
....
stateInfo += ....;
stateInfo += ....;
....
stateInfo += ....;
_stateText.text = stateInfo;
....
}
Здесь реализовано построение строки 'stateInfo' путем нескольких операций конкатенации. Выполнение этого кода в методе 'Update' (вызывается несколько десятков раз в секунду), приведет к быстрому накоплению 'мусора' в памяти и к более частому вызову сборщика мусора для его очистки. Последнее может негативно отразиться на производительности. Избежать лишнего выделения памяти можно с помощью объекта 'StringBuilder':
[SerializeField] Text _stateText;
....
StringBuilder _stateInfo = new StringBuilder();
void Update()
{
_stateInfo.Clear();
....
_stateInfo.AppendLine(....);
_stateInfo.AppendLine(....);
....
_stateInfo.AppendLine(....);
_stateText.text = _stateInfo.ToString();
....
}
Метод 'Clear' очищает содержимое 'StringBuilder', но при этом не освобождает выделенную память. Таким образом, дополнительное выделение памяти потребуется только в том случае, если уже используемой будет недостаточно для хранения нового текста.
Рассмотрим другой пример:
[SerializeField] Text _text;
....
List<string> _messages = new();
....
void LateUpdate()
{
....
string message = BuildMessage();
_text.text = message;
_messages.Clear();
}
string BuildMessage()
{
string result = "";
foreach (var msg in _messages)
result += msg + "\n";
return result;
}
В этом примере сообщение, выводимое на интерфейс, формируется с помощью метода 'BuildMessage'. Так как он вызывается внутри 'LateUpdate' (так же часто, как и внутри 'Update'), его тоже стоит оптимизировать:
StringBuilder _message = new StringBuilder();
string BuildMessage()
{
_message.Clear();
foreach (var msg in _messages)
_message.AppendLine(msg);
return _message.ToString();
}
Анализатор обнаружил захват переменной в лямбда-выражении внутри часто выполняемого метода. Захват переменных может приводить к снижению производительности из-за дополнительного выделения памяти.
Рассмотрим пример:
void Update()
{
....
List<int> numbers = GetNumbers();
int divisor = GetDivisor();
var result = numbers.Select(x => x / divisor);
....
}
Здесь 'Update' — метод Unity, выполняющий покадровое обновление. Метод 'Update' — часто вызываемый, и его не рекомендуется нагружать лишними операциями.
В приведенном примере используется лямбда-выражение с захватом переменной 'divisor'. Как упоминалось ранее, захват переменной из внешнего контекста приводит к дополнительному созданию объекта.
Таким образом, представленный участок кода создает дополнительную нагрузку на GC.
Оптимальная реализация метода может выглядеть следующим образом:
void Update()
{
....
List<int> numbers = GetNumbers();
int divisor = GetDivisor();
var result = new List<int>(numbers.Count);
for (int i = 0; i < numbers.Count; i++)
{
result.Add(numbers[i]/divisor);
}
....
}
Использование собственной реализации, аналогичной 'Select', позволяет избавиться от дополнительного выделения памяти и тем самым снизить нагрузку на GC.
Рассмотрим еще один пример:
void Update()
{
....
List<int> numbers = GetNumbers();
int divisor = GetDivisor();
if (AreAllMultipleOf(numbers, divisor))
....
}
bool AreAllMultipleOf(List<int> lst, int divisor)
{
return lst.All(elem => elem % divisor == 0);
}
Здесь из метода 'Update' вызывается метод 'AreAllMultipleOf', который определяет, являются ли все полученные числа кратными значению 'divisor'. Так же, как и ранее: 'Update' — часто вызываемый метод, выполняющий покадровое обновление в Unity.
В представленном случае метод 'AreAllMultipleOf' регулярно выполняется внутри 'Update', а значит, также является часто вызываемым.
Метод 'AreAllMultipleOf' для выполнения проверки использует лямбда-выражение с захватом переменных. Это приводит к дополнительному выделению памяти, которое может негативно сказаться на производительности приложения.
Оптимальная реализация может выглядеть следующим образом:
void Update()
{
....
List<int> numbers = GetNumbers();
int divisor = GetDivisor();
if (AreAllMultipleOf(numbers, divisor))
....
}
bool AreAllMultipleOf(List<int> lst, int divisor)
{
foreach (int num in lst)
{
if (num % divisor != 0)
return false;
}
return true;
}
Здесь мы в очередной раз воспользовались собственной реализацией, что позволило избежать дополнительного выделения памяти и снизить нагрузку на сборщик мусора.
Анализатор обнаружил, что в часто выполняемом коде производятся обращения к свойствам или методам, которые создают новый массив.
Все составляющие API Unity, возвращающие массивы, создают новую коллекцию при каждом обращении. Это приводит к выделению памяти в управляемой куче. Частое обращение к таким свойствам или методам может негативно сказаться на производительности приложения.
Рассмотрим пример:
void Update()
{
for (int i = 0; i < Camera.allCameras.Length; i++)
{
SetDepth(Camera.allCameras[i].depth);
SetName(Camera.allCameras[i].name);
SetMask(Camera.allCameras[i].eventMask);
....
}
}
В методе 'Update' производится запись значений, соответствующих конкретной камере. Данный метод вызывается на каждый кадр. При обращении к свойству 'Camera.allCameras' создаётся новый массив. Таким образом, новая коллекция будет создаваться по четыре раза на каждой итерации 'for' (один раз при проверке условия цикла, три раза в теле).
Множественного создания новых коллекций можно избежать, если получить значение 'Camera.allCameras' перед циклом:
void Update()
{
var cameras = Camera.allCameras;
for (int i = 0; i < cameras.Length; i++)
{
SetDepth(cameras[i].depth);
SetName(cameras[i].name);
SetMask(cameras[i].eventMask);
....
}
}
В данном случае обращение к 'Camera.allCameras' производится единожды. Значение этого свойства присваивается переменной 'cameras'. Далее для записи параметров камеры используется эта переменная, а не свойство 'Camera.allCameras'.
Рассмотрим ещё один пример:
void Update()
{
SetNecessaryGameObjectParameters();
}
void SetNecessaryGameObjectParameters()
{
for (int i = 0; i < GameObject.FindGameObjectsWithTag("Enemy").Length; i++)
{
SetName(GameObject.FindGameObjectsWithTag("Enemy")[i].name);
SetTag(GameObject.FindGameObjectsWithTag("Enemy")[i].tag);
....
}
}
В методе 'SetNecessaryGameObjectParameters' записывается информация об объектах типа 'GameObject'. Объекты будут получены путём вызова 'GameObject.FindGameObjectsWithTag'. При каждом вызове этого метода создаётся новая коллекция. 'SetNecessaryGameObjectParameters' используется в методе 'Update', который является часто вызываемым.
Чтобы избавиться от множественного создания новых коллекций, можно вынести вызов метода за цикл:
void Update()
{
SetNecessaryGameObjectParameters();
}
void SetNecessaryGameObjectParameters()
{
var enemies = GameObject.FindGameObjectsWithTag("Enemy");
for (int i = 0; i < enemies.Length; i++)
{
SetName(enemies[i].name);
SetTag(enemies[i].tag);
....
}
}
Теперь вызов 'GameObject.FindGameObjectsWithTag' производится единожды. Возвращаемое значение этого метода записывается в переменную 'enemies'. Далее информация об объектах типа 'GameObject' будет получена с помощью этой переменной, а не путём вызова 'GameObject.FindGameObjectsWithTag'.
Анализатор обнаружил, что в часто выполняемом коде производятся обращения к ресурсоёмким свойствам или методам.
Согласно документации Unity, ряд методов и свойств из API Unity производят ресурсоёмкие операции при обращении. Частое обращение к таким свойствам или методам может негативно сказаться на производительности приложения.
Рассмотрим пример:
public void Update()
{
foreach (var cameraHandler in CameraHandlers)
{
cameraHandler(Camera.main);
}
....
}
В методе 'Update' производится обработка камеры. Данный метод вызывается на каждый кадр. При обращении к свойству 'Camera.main' осуществляется поиск в кэше, что создаёт нагрузку на процессор. Таким образом, ресурсоёмкая операция будет выполняться на каждой итерации цикла.
Множественного обращения к свойству 'Camera.main' можно избежать, если записать его значение в переменную:
public void Update()
{
var camera = Camera.main;
foreach (var cameraHandler in CameraHandlers)
{
cameraHandler(camera);
}
....
}
В данном случае обращение к 'Camera.main' производится единожды. Значение этого свойства присваивается переменной 'camera'. Далее для обработки камеры используется эта переменная, а не свойство 'Camera.main'.
Рассмотрим ещё один пример:
public void Update()
{
ProcessCamera();
....
}
private void ProcessCamera()
{
if (GetComponent<Camera>() == null)
return;
var cameraDepth = GetComponent<Camera>().depth;
var cameraName = GetComponent<Camera>().name;
var cameraEvent = GetComponent<Camera>().eventMask;
}
В методе 'ProcessCamera' записывается информация о камере. Сама камера будет получена путём вызова 'GetComponent<Camera>'. Этот метод осуществляет поиск объекта, соответствующего типу 'Camera'. Данная операция является ресурсоёмкой. При каждом вызове 'ProcessCamera', метод 'GetComponent<Camera>' выполнится четыре раза. 'ProcessCamera' используется в методе 'Update', который является часто вызываемым.
Чтобы избавиться от множественного выполнения ресурсоёмкой операции, можно записать результат 'GetComponent<Camera>' в переменную:
private void ProcessCamera()
{
var camera = GetComponent<Camera>();
if (camera == null)
return;
var cameraDepth = camera.depth;
var cameraName = camera.name;
var cameraEvent = camera.eventMask;
....
}
Теперь вызов 'GetComponent<Camera>' производится единожды. Возвращаемое значение этого метода записывается в переменную 'camera'. Далее информация о камере будет получена с помощью этой переменной, а не путём вызова 'GetComponent<Camera>'.
Анализатор обнаружил возможность оптимизировать математическую операцию, что может положительно сказаться на производительности, если операция выполняется часто.
Рассмотрим синтетический пример:
[SerializedField] float _speed;
void Update()
{
....
Vector3 input = ....;
var move = input * _speed * Time.deltaTime;
....
}
В методе 'Update' вычисляется значение переменной 'move' — вектор перемещения некоторого персонажа за один кадр. В ходе этого будет выполнено шесть операций умножения, ведь при умножении 'Vector3' на число выполняется умножение каждой из его трёх компонент (x, y, z) на это же число. Частота вызова 'Update' непостоянна и зависит от частоты смены кадров в секунду. Допустим, в нашем случае этот метод вызывается в среднем 60 раз в секунду. Тогда только для вычисления перемещения потребуется 60 * 6 = 360 операций в секунду.
Это значение можно уменьшить, сначала перемножив обычные числа между собой, а потом полученный результат умножив на вектор:
var move = input * (_speed * Time.deltaTime);
Теперь для одного вычисления перемещения будет выполнено четыре операции умножения за один вызов 'Update' и 240 операций в секунду соответственно.
Стоит отметить, что чем больше обычных чисел будет участвовать в таком выражении, тем значительнее будет эффект от данной оптимизации.
Анализатор обнаружил создание Unity-объекта в часто выполняемом методе.
Регулярное создание/уничтожение игровых объектов не только нагружает центральный процессор, но и приводит к увеличению частоты вызовов сборщика мусора. Это негативно отражается на производительности.
Рассмотрим синтетический пример:
CustomObject _instance;
void Update()
{
if (....)
{
CreateCustomObject();
....
}
else if (....)
{
....
Destroy(_instance.gameObject); // <=
}
}
void CreateCustomObject()
{
var go = new GameObject(); // <=
_instance = go.AddComponent<CustomObject>();
....
}
Здесь в методе 'Update' создаётся и уничтожается некоторый игровой объект '_instance'. Т. к. 'Update' выполняется при каждом обновлении кадров, по возможности рекомендуется избегать в нём этих операций.
Данный код можно оптимизировать. Для этого нужно один раз инициализировать '_instance', например, в методе 'Start', после чего в методе 'Update' использовать метод '_instance.gameObject.SetActive' для включения/отключения объекта вместо создания/уничтожения.
Пример реализации:
CustomObject _instance;
void Start()
{
CreateCustomObject();
_instance.gameObject.SetActive(false);
....
}
void Update()
{
if (....)
{
....
_instance.gameObject.SetActive(true);
}
else if (....)
{
....
_instance.gameObject.SetActive(false);
}
}
void CreateCustomObject()
{
var go = new GameObject();
_instance = go.AddComponent<CustomObject>();
....
}
Такая же оптимизация актуальна и для компонентов 'MonoBehaviour'. В этом случае следует использовать свойство 'enabled' компонента для его включения/отключения.
Нередко требуется создавать/уничтожать множество временных объектов, например, снарядов. Такие случаи также можно оптимизировать заменой указанных выше операций на включение/отключение, но с применением пулов. Для таких случаев Unity предоставляет готовый набор универсальных пулов, находящихся в пространстве имён 'UnityEngine.Pool', например, 'ObjectPool<T>'. Подробности о принципе работы с этим классом можно найти на сайте Unity: тут или тут.
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило различается для C и C++. В C восьмеричные числовые литералы не должны использоваться. В C++ помимо восьмеричных числовых литералов не должны использоваться экранирующие последовательности.
Использование восьмеричных литералов может затруднить восприятие кода, особенно при быстром просмотре. Неправильная интерпретация фактического числового значения может приводить к разнообразным ошибкам.
Пример кода, на который анализатор выдаст предупреждение:
if (val < 010)
{
....
}
При быстром просмотре кода можно упустить из виду, что значение числового литерала – 8, а не 10. Чтобы анализатор не выдавал предупреждения, литерал стоит переписать в десятичной или шестнадцатеричной форме:
if (val < 8)
{
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Использование оператора 'goto' может привести к нарушению структуры программы и усложнению понимания кода. Данное диагностическое правило выявляет места использования оператора 'goto'.
Пример кода, на который анализатор выдаст предупреждение:
int foo(int value)
{
....
if (value==0)
goto bad_arg;
....
return OK;
bad_arg:
return BAD_ARG;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Анализатор обнаружил неуникальное значение среди неявно проинициализированных элементов перечисления, приводящее к дублированию значений констант.
Пример некорректного кода:
enum Suits
{
SUITS_SPADES = 1,
SUITS_CLUBS,
SUITS_DIAMONDS,
SUITS_HEARTS,
SUITS_UNKNOWN = 4
};
Подобная инициализация привела к тому, что элементы 'SUITS_HEARTS' и 'SUITS_UNKNOWN' имеют одинаковые значения. Может быть неочевидно, сделано это специально, или код содержит ошибку.
Во избежание подобной ошибки следует явно инициализировать все элементы перечисления, имеющие неуникальные значения:
enum Suits
{
SUITS_SPADES = 1,
SUITS_CLUBS,
SUITS_DIAMONDS,
SUITS_HEARTS = 4,
SUITS_UNKNOWN = 4
};
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Явное указание размера массива улучшает читаемость и понимание кода, уменьшая вероятность возникновения ошибок, связанных с выходом за границы массива из-за незнания его размера.
Анализатор выдаст предупреждение, если встретит объявление массива со спецификатором 'extern', при условии, что размер массива не задан явно:
extern int arr[];
Для исправления следует явно указать размер массива:
extern int arr[12];
Если размер массива можно вывести из инициализатора, предупреждение выдано не будет:
int arr1[] = { 1, 2, 3 };
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Использование оператора 'goto', осуществляющего переход к метке, находящейся выше по коду, ухудшает читаемость кода и как следствие – усложняет его поддержку.
Пример кода, на который анализатор выдаст предупреждение:
void init(....)
{
....
again:
....
if (....)
if (....)
goto again;
....
}
Чтобы анализатор не выдавал предупреждение, необходимо отказаться от использования оператора 'goto' или переписать код таким образом, чтобы метка, на которую ссылается 'goto', находилась ниже него.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Функция должна содержать единственный оператор 'return'. Этот оператор должен быть последним в теле функции. Множественное использование операторов 'return' может усложнить чтение и понимание кода, а также его дальнейшую модификацию.
Пример кода, на который анализатор выдаст предупреждение.
obj foo (....)
{
....
if (condition) {
return a;
}
....
if (other_condition) {
return b;
}
....
}
Исправленный вариант кода имеет одну точку выхода в конце функции:
obj foo (....)
{
....
if (condition) {
result = a;
} else {
....
if (other_condition) {
result = b;
}
}
....
return result;
}
Подобный код также будет легче поддерживать, если понадобится добавить, например, предварительную обработку возвращаемого значения, так как сделать это придётся только в одном месте:
obj foo (....)
{
....
return cache(result);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Тела операторов 'while', 'do-while', 'for', 'if', 'if-else', 'switch' должны быть заключены в фигурные скобки.
Использование фигурных скобок четко определяет, какие выражения составляют тело, повышает читаемость кода и снижает вероятность возникновения ряда ошибок. Например, при отсутствии фигурных скобок программист может ошибочно воспринять выравнивание или не заметить символ ';', случайно поставленный после оператора.
Пример 1:
void example_1(....)
{
if (condition)
if (otherCondition)
DoSmt();
else
DoSmt2();
}
Форматирование данного кода не соответствует логике его выполнения, что может сбивать с толку. Расстановка фигурных скобок устраняет возможную неоднозначность:
void example_1(....)
{
if (condition)
{
if (otherCondition)
{
DoSmt();
}
else
{
DoSmt2();
}
}
}
Пример 2:
void example_2(....)
{
while (count < 10)
DoSmt1(); DoSmt2();
}
Форматирование данного кода также не соответствует логике его исполнения, так как к циклу относится только выражение 'DoSmt1()'.
Исправленный вариант:
void example_2(....)
{
while (count < 10)
{
DoSmt1();
DoSmt2();
}
}
Примечание: 'if', следующий сразу после 'else' не обязательно должен быть заключен в фигурные скобки. На фрагмент кода, приведённый ниже, анализатор не будет выдавать предупреждение.
if (condition1)
{ .... }
else if (condition2)
{ .... }
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций: 'atof', 'atoi', 'atol', 'atoll'.
Неправильное использование этих функций может привести к возникновению неопределённого поведения. Это может произойти, если аргумент функции не является валидной С-строкой или если результирующее значение выходит за границы возвращаемого типа.
Пример кода, на который анализатор выдаст предупреждение:
void Foo(const char *str)
{
int val = atoi(str);
....
}
Для C анализатор также выдаст предупреждение, если обнаружит определение макроса с одним из соответствующих имён.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций 'abort', 'exit', 'getenv', 'system'.
Поведение перечисленных выше функций зависит от реализации. Кроме того, использование таких функций как 'system' может являться причиной возникновения уязвимостей.
Пример кода, на который анализатор выдаст предупреждение:
void Foo(FILE *pFile)
{
if (pFile == NULL)
{
abort();
}
....
}
Для C анализатор также выдаст предупреждение, если обнаружит определение макроса с одним из соответствующих имён.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Анализатор выдаст предупреждение, если встретит использование одной из следующих функций: 'qsort', 'bsearch'.
Неправильное использование этих функций может стать причиной возникновения неопределённого поведения. Подробнее об этом можно прочесть в документации к функциям 'qsort' и 'bsearch'.
Пример кода, на который анализатор выдаст предупреждение:
qsort(arr, cnt, sizeof(int), comp);
Также анализатор будет выдавать предупреждения на определения макросов с соответствующими именами.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если встретит использование функций и операторов выделения \ освобождения динамической памяти, таких как 'malloc'; 'realloc'; 'calloc'; 'free'; 'new'; 'delete'.
Функции работы с динамической памятью являются потенциальным источником проблем, так как их неправильное использование может привести к утечкам памяти, неопределённому поведению и прочим ошибкам. Более того – это может стать причиной возникновения уязвимости в приложении.
Пример кода, на который анализатор выдаст предупреждение:
int* imalloc(size_t cnt)
{
return (int*)malloc(cnt * sizeof(int));
}
Для C анализатор также выдаст предупреждение, если обнаружит определение макроса с одним из соответствующих имён.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование макроса 'setjmp' или функции 'longjmp', так как их неправильное использование может привести к неопределённому поведению.
Пример кода, на который анализатор выдаст предупреждение:
jmp_buf j_buf;
void foo()
{
setjmp(j_buf);
}
int main()
{
foo();
longjmp(j_buf, 0);
return 0;
}
Функция 'longjmp' вызвана уже после выхода из функции, которая вызвала 'setjmp', результат в таком случае не определен.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. Анализатор выдаст предупреждение, если обнаружит использование следующих функций: 'strcpy'; 'strcmp'; 'strcat'; 'strchr'; 'strspn'; 'strcspn'; 'strpbrk'; 'strrchr'; 'strstr'; 'strtok'; 'strlen'.
Неправильное использование этих функций может стать причиной возникновения неопределенного поведения, так как они не защищены от чтения и записи вне границ буфера.
Пример кода, на который анализатор выдаст предупреждение:
int strcpy_internal(char *dest, const char *source)
{
int exitCode = FAILURE;
if (source && dest)
{
strcpy(dest, source);
exitCode = SUCCESS;
}
return exitCode;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит объявление объединения.
Неправильное использование объединений может приводить к различным проблемам, например, к получению неверных значений или к возникновению неопределённого поведения.
Например, в случае C++, неопределённое поведение может возникнуть, когда значение считывается не из члена, в который производилась запись в последний раз.
Пример кода, на который анализатор выдаст предупреждение:
union U
{
unsigned char uch;
unsigned int ui;
} uni;
....
uni.uch = 'w';
int ui = uni.ui;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если встретит объявление, использующее более двух уровней вложенности указателей. Использование таких указателей ухудшает понимание кода и как следствие – может стать причиной возникновения различных ошибок.
Пример кода, на который анализатор выдаст предупреждение:
void foo(int **ppArr[])
{
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Каждая последовательность 'if ... else if' должна заканчиваться 'else'. При отсутствии завершающего 'else' анализатор выдаст предупреждение. Наличие конечного 'else' показывает, что рассмотрены все возможные варианты, что помогает при чтении и понимании кода.
Пример кода, на который анализатор выдаст предупреждение:
if (condition)
{
....
}
else if (other_condition)
{
....
}
Чтобы анализатор не выдавал предупреждение, а разработчик, читающий код, сразу понял, что рассмотрен случай, когда ни одно условие не выполнилось, стоит добавить 'else'-ветвь. В этой ветви следует разместить необходимые действия или поясняющий комментарий.
if (condition)
{
....
}
else if (other_condition)
{
....
}
else
{
// No action needed
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило отличается для C и C++. В C необходимо использовать суффикс 'L' вместо 'l'. В C++ все буквенные суффиксы должны быть в верхнем регистре.
Использование суффиксов в нижнем регистре мешает визуальному восприятию кода. Например, суффикс 'l' можно спутать с единицей (1), что может стать причиной разнообразных ошибок.
Примеры литералов, на которые анализатор выдаст предупреждение:
12l; 34.0f; 23u;
Форма записи литералов, на которую предупреждение выдано не будет:
12L; 34.0F; 23U;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Метка 'default' должна быть либо первой, либо последней меткой в 'switch'. Следование этому правилу облегчает чтение кода.
Пример кода, на который анализатор выдаст предупреждение:
void example_1(int cond)
{
switch (cond)
{
case 1:
DoSmth();
break;
default:
DoSmth2();
break;
case 3:
DoSmth3();
break;
}
}
Чтобы убрать предупреждение, код можно переписать, например, следующим образом:
void example_1(int cond)
{
switch (cond)
{
case 1:
DoSmth();
break;
case 3:
DoSmth3();
break;
default:
DoSmth2();
break;
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Метка 'default' должна присутствовать в каждом 'switch'.
Следование данному правилу гарантирует, что будут обработаны любые ситуации, в которых значение контролирующей переменной не соответствует ни одной метке.
Такие ситуации требуют принятия определенных мер, поэтому каждая метка 'default' должна содержать (помимо 'break') выражение или комментарий, объясняющий, почему никаких действий не предпринимается.
Пример 1:
enum WEEK
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
} weekDay;
void example_1()
{
int isWorkday;
switch (weekDay)
{
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
isWorkday = 1;
break;
case SATURDAY:
case SUNDAY:
isWorkday = 0;
break;
}
Несмотря на то, что обработаны все значения из перечисления 'WEEK', нет гарантии, что 'weekDay' может принимать только эти значения. Чтобы анализатор не выдавал предупреждение, можно переписать код, например, следующим образом:
enum WEEK
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
} weekDay;
void example_1()
{
int isWorkday;
switch (weekDay)
{
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
isWorkday = 1;
break;
case SATURDAY:
case SUNDAY:
isWorkday = 0;
break;
default:
assert(false);
break;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило различается для C и C++. Для обоих языков каждая метка в 'switch' должна быть завершена оператором 'break', не расположенным внутри условия. А для C++ последним оператором также может быть 'throw'.
Намеренное завершение каждой метки гарантирует, что поток управления не "провалится" в метку, расположенную ниже, и позволяет избежать ошибок при добавлении новых меток.
Исключением из этого правила являются пустые метки, расположенные последовательно.
Пример кода, на который анализатор выдаст предупреждение:
void example_1(int cond, int a)
{
switch (cond)
{
case 1:
case 2:
break;
case 3: // <=
if (a == 42)
{
DoSmth();
}
case 4: // <=
DoSmth2();
default: // <=
;
}
}
Исправленный код:
void example_1(int cond, int a)
{
switch (cond)
{
case 1:
case 2:
break;
case 3:
if (a == 42)
{
DoSmth();
}
break;
case 4:
DoSmth2();
break;
default:
/* No action required */
break;
}
}
Следует отметить, что метки не должны завершаться выражением 'return', так как это нарушает правило V2506.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. Анализатор обнаружил совмещение явной и неявной инициализации элементов перечисления. Это может привести к неожиданному дублированию их значений.
Пример некорректного кода:
enum Suits
{
SUITS_SPADES = 1,
SUITS_CLUBS,
SUITS_DIAMONDS,
SUITS_HEARTS,
SUITS_UNKNOWN = 4
};
Подобная инициализация привела к тому, что элементы 'SUITS_HEARTS' и 'SUITS_UNKNOWN' имеют одинаковые значения. Может быть неочевидно, сделано это специально, или код содержит ошибку.
Анализатор не будет выдавать предупреждение, если:
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C++. Последней меткой в 'switch' должна быть 'default'.
Наличие 'default' в конце каждого 'switch' облегчает чтение кода, а также гарантирует, что будут обработаны любые ситуации, в которых значение контролирующей переменной не соответствует ни одной метке. Так как такие ситуации требуют принятия определенных мер, каждая метка 'default' должна содержать (помимо 'break' или 'throw') выражение или комментарий, объясняющий, почему никаких действий не предпринимается.
Пример 1:
void example_1(int i)
{
switch (i)
{
case 1:
DoSmth1();
break;
default: // <=
DoSmth42();
break;
case 3:
DoSmth3();
break;
}
}
Корректный код:
void example_1(int i)
{
switch (i)
{
case 1:
DoSmth1();
break;
case 3:
DoSmth3();
break;
default:
DoSmth42();
break;
}
}
Пример 2:
enum WEEK
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
} weekDay;
void example_2()
{
int isWorkday;
switch (weekDay)
{
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
isWorkday = 1;
break;
case SATURDAY:
case SUNDAY:
isWorkday = 0;
break;
default: // <=
break;
}
Исправленный код:
enum WEEK
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
} weekDay;
void example_2()
{
int isWorkday;
switch (weekDay)
{
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
isWorkday = 1;
break;
case SATURDAY:
case SUNDAY:
isWorkday = 0;
break;
default:
assert(false);
break;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор обнаружил использование целочисленного беззнакового литерала без суффикса 'U' / 'u' (суффикса 'U' в случае C++). Подобные литералы могут осложнить восприятие кода, так как их тип становится неочевиден. Более того, одни и те же литералы могут иметь разные типы на разных моделях данных.
Использование суффиксов для явного указания типа позволит избежать неоднозначностей при чтении числовых литералов.
Пример кода, на который анализатор выдаст предупреждение (если тип литерала на анализируемой платформе – беззнаковый):
auto typemask = 0xffffffffL;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Область видимости, в которой находится метка, должна быть составным выражением, которое представляет собой тело 'switch'. Это означает, что метка не должна быть вложена ни в какой блок, кроме как в тело 'switch', и это тело должно быть составным выражением.
Следовательно, все метки, принадлежащие одному 'switch', должны находиться в одной области видимости. Следование этому правилу позволяет сохранить читаемость и структурированность кода.
Пример 1:
void example_1(int param, bool b)
{
switch (param)
{
case 1:
DoSmth1();
if (b)
{
case 2: // <=
DoSmth2();
}
break;
default:
assert(false);
break;
}
}
Рассмотренный пример сложен для понимания. Чтобы анализатор не выдавал предупреждения, следует написать так:
void example_1(int param, bool b)
{
switch (param)
{
case 1:
DoSmth1();
if (b)
{
DoSmth2();
}
break;
case 2:
DoSmth2();
break;
default:
assert(false);
break;
}
}
В следующем примере тело 'switch' не является составным, поэтому анализатор также выдаст предупреждение:
void example_2(int param)
{
switch (param)
default:
DoDefault();
}
Исправленный вариант:
void example_2(int param)
{
switch (param)
{
default:
DoDefault();
break;
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило различается для языков C и C++. В C каждый 'switch' должен иметь по крайней мере две непустых метки, которыми могут быть 'case' или 'default'. В C++ каждый 'switch' должен иметь по крайней мере одну непустую метку 'case'.
Конструкции 'switch', не удовлетворяющие этим условиям, являются избыточными и могут являться признаком ошибки программиста.
Пример 1:
void example_1(int param)
{
switch(param)
{
case 0:
default:
Func();
break;
}
}
Подобный 'switch' является излишним и не имеет никакого смысла. Независимо от значения переменной 'param', всегда будет выполняться только тело 'default'.
На следующий пример анализатор не будет выдавать предупреждение:
void example_2(int param)
{
switch(param)
{
case 0:
DoSmth1();
break;
case 1:
DoSmth2();
break;
....
default:
Func();
break;
}
}
Пример кода, на который анализатор выдаст предупреждение только при использовании компилятора для языка C:
void example_3(int param)
{
switch(param)
{
case 10:
case 42:
DoMath();
break;
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций: 'clock'; 'time'; 'difftime'; 'ctime'; 'asctime'; 'gmtime'; 'localtime'; 'mktime'.
Перечисленные функции имеют неуточненное поведение или поведение, определенное реализацией, и поэтому могут выдаваться разные форматы времени и даты (в зависимости от окружения, реализации стандартной библиотеки, и т. д.).
Пример кода, на который анализатор выдаст предупреждения:
const char* Foo(time_t *p)
{
time_t t = time(p);
return ctime(t);
}
Для языка С анализатор также выдаст предупреждение, если обнаружит объявление макроса с одним из соответствующих имён.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Значение булевого типа может быть сконвертировано к целочисленному типу, поэтому его можно использовать в качестве управляющей переменной в выражении 'switch'. В этом случае использование конструкции 'if-else' является предпочтительным, так как оно дает более понятное и явное представление намерений программиста.
Пример:
int foo(unsigned a, unsigned b)
{
while (a != 0 && b != 0)
{
switch (a > b) // <=
{
case 0:
a -= b;
break;
default:
b -= a;
break;
}
}
return a;
}
Вместо этого следует написать:
int foo(unsigned a, unsigned b)
{
while (a != 0 && b != 0)
{
if (a > b)
{
b -= a;
}
else
{
a -= b;
}
}
return a;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Не стоит использовать оператор 'запятая', так как он может привести к путанице при чтении кода.
Рассмотрим следующий пример:
int foo(int x, int y) { .... }
foo( ( 0, 3), 12 );
Код может ввести программиста в заблуждение, если ему до прочтения вызова функции неизвестна ее сигнатура. Может показаться, что в функцию передается три аргумента, однако это не так: оператор 'запятая' в выражении '(0, 3)' вычислит левый и правый аргументы, а затем возвращает результат второго. В итоге, вызов функции на самом деле принимает вид:
foo( 3, 12 );
Диагностика выдает предупреждение и для других случаев, таких как например этот:
int myMemCmp(const char *s1, const char *s2, size_t N)
{
for (; N > 0; ++s1, ++s2, --N) { .... }
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Чрезмерное использование 'goto' приводит к плохой структурированности кода, усложняя его понимание.
Для снижения визуальной сложности кода рекомендуется отказаться от переходов во вложенные блоки или между блоками, расположенными на одном уровне.
Пример 1:
void V2532_pos1()
{
...
goto label;
...
{
label:
...
}
}
Здесь 'goto' ссылается во вложенный блок, что нарушает данное правило.
На следующий пример анализатор не будет выдавать предупреждение:
void V2532_neg1()
{
...
label:
...
{
goto label;
...
}
}
Примечание: тела switch-меток рассматриваются как составные выражения, даже если они не обернуты в фигурные скобки. Поэтому прыжки в тело switch-метки из внешнего кода и прыжки между разными switch-метками нарушают данное правило.
Приведем примеры.
Прыжок в switch-метку из внешнего кода (ошибка):
void V2532_pos2(int param)
{
goto label;
switch (param)
{
case 0:
break;
default:
label:;
break;
}
}
Прыжок между switch-метками (ошибка):
void V2532_pos3(int param)
{
switch (param)
{
case 0:
goto label;
break;
default:
label:
break;
}
}
Прыжок из switch-метки во внешний код (ok):
void V2532_neg2(int param)
{
label:
switch (param)
{
case 0:
goto label;
break;
default:
break;
}
}
Прыжок в пределах одной switch-метки (ok):
void neg3(int param)
{
switch (param)
{
case 0:
{
...
{
goto label;
}
}
label:
break;
default:
break;
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Ограничение количества выходов из цикла позволяет значительно снизить визуальную сложность кода.
Пример, на который анализатор выдаст предупреждение:
int V2534_pos_1(vector<int> ivec)
{
int sum = 0;
for (auto i = ivec.cbegin(); i != ivec.cend(); ++i)
{
if (*i < 0)
break;
sum += *i;
if (sum > 42)
break;
}
return sum;
}
В следующем примере выход из цикла производится с помощью как 'break', так и 'goto':
short V2534_pos_2(string str)
{
short count = 0;
for (auto &c : str)
{
if (isalnum(c))
{
count++;
}
else if (isspace(c))
{
break;
}
else
{
goto error;
}
}
return count;
error:
...
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Значение одного сущностного типа не должно быть приведено явно к значению другого несоответствующего сущностного типа.
Стандарт MISRA определяет следующую модель сущностных типов (Essential type model), в которой переменная может иметь тип:
Указатели в этой модели отсутствуют.
Таблица ситуаций, которые желательно избегать:
Исключения:
Причины для явного преобразования типов:
По некоторым причинам преобразование из одного сущностного типа к другому может быть опасно или лишено смысла, например:
Пример, на который анализатор также выдаст соответствующие предупреждения:
enum A {ONE, TWO = 2};
float foo(int x, char ch)
{
enum A a = (enum A) x; // signed to enum, may lead to
// unspecified behavior
int y = int(x == 4); // Meaningless cast Boolean to signed
return float(ch) + .01; // Meaningless cast character to floating,
// there is no precise mapping between
// two representations
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор обнаружил неявное снятие константности с строкового литерала. Так как любая попытка изменить строковый литерал приводит к неопределенному поведению, его следует присваивать лишь к объектам типа указателя на константный символ (pointer to const-qualified char).
Это правило также актуально для широкого строкового литерала.
Пример кода, на который анализатор выдаст предупреждение:
char* Foo(void)
{
return "Hello, world!";
}
При модификации строкового литерала анализатор также выдает предупреждение:
"first"[1] = 'c';
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило справедливо только для C++.
C-подобное (C-style cast) и приведение типов в функциональном стиле (functional cast) способны осуществлять преобразования между несвязанными типами, что может привести к ошибке.
Следует явно приводить типы с использованием операторов 'static_cast', 'const_cast' и 'reinterpret_cast'.
Примеры, на которые анализатор выдаст срабатывание:
int i;
double d;
size_t s;
void *p;
....
i = int(p); //V2533
d = (double)s; //V2533
s = (size_t)(i); //V2533
p = (void *)(d); //V2533
Исключение: приведение к типу void не несёт в себе никакой опасности и используется, чтобы подчеркнуть, что некий результат никак не используется. Пример:
(void)fclose(f);
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Поскольку числа с плавающей точкой не могут точно отобразить все действительные числа, использование таких переменных в цикле может дать непостоянное количество итераций.
Рассмотрим пример:
void foo(void) {
for (float A = 0.1f; A <= 10.0f; A += 0.1f) {
....
}
}
Количество итераций в этом цикле может быть 99 или 100. Точность операций с вещественными числами может зависеть от компилятора, режима оптимизации и многого другого.
Лучше переписать цикл следующим образом:
void foo(void) {
for (int count = 1; count <= 100; ++count) {
float A = count / 10.0f;
}
}
Этот цикл выполнит ровно 100 итераций, а переменную 'A' можно использовать для необходимых вычислений.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Присутствие недостижимого кода может являться признаком ошибки программиста и усложняет поддержку кода.
Компилятор в целях оптимизации вправе удалить недостижимый код. Недостижимый код, неудаленный компилятором, может расходовать ресурсы. Например, он может увеличивать размер бинарного файла или являться причиной излишнего кэширования инструкций.
Рассмотрим первый пример:
void Error()
{
....
exit(1);
}
FILE* OpenFile(const char *filename)
{
FILE *f = fopen(filename, "w");
if (f == nullptr)
{
Error();
printf("No such file: %s", filename);
}
return f;
}
Функция 'printf(....)' никогда не напечатает сообщение об ошибке, так как функция 'Error()' не возвращает управление. Как правильно исправить код зависит от того, какую логику поведения задумывал программист изначально. Возможно, функция должна возвращать управление. Возможно, нарушен порядок выражений и корректный код должен быть таким:
FILE* OpenFile(const char *filename)
{
FILE *f = fopen(filename, "w");
if (f == nullptr)
{
printf("No such file: %s", filename);
Error();
}
return f;
}
Рассмотрим второй пример:
char ch = strText[i];
switch (ch)
{
case '<':
...
break;
case '>':
...
break;
case 0xB7:
case 0xBB:
...
break;
...
}
Здесь ветка расположенная после "case 0xB7:" и "case 0xBB:" никогда не получит управление. Переменная 'ch' имеет тип 'char', а, следовательно, диапазон её значений лежит в пределах [-128..127]. Результатом сравнения "ch == 0xB7" и "ch==0xBB" всегда будет ложь (false). Чтобы код был корректен, переменная 'ch' должна иметь тип 'unsigned char'. Исправленный код:
unsigned char ch = strText[i];
switch (ch)
{
case '<':
...
break;
case '>':
...
break;
case 0xB7:
case 0xBB:
...
break;
...
}
Рассмотрим третий пример:
if (n < 5) { AB(); }
else if (n < 10) { BC(); }
else if (n < 15) { CD(); }
else if (n < 25) { DE(); }
else if (n < 20) { EF(); } // Это ветвь никогда не выполнится.
else if (n < 30) { FG(); }
Из-за некорректного пересечения диапазонов, находящихся в условиях, одна из ветвей никогда не будет выполнена. Исправленный код:
if (n < 5) { AB(); }
else if (n < 10) { BC(); }
else if (n < 15) { CD(); }
else if (n < 20) { EF(); }
else if (n < 25) { DE(); }
else if (n < 30) { FG(); }
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Наличие в теле функции меток, на которые не ссылается ни один оператор 'goto' может служить признаком допущенной программистом ошибки. Такие метки могут появиться, если программист случайно использовал переход не на ту метку или допустил опечатку при создании case-метки.
Рассмотрим первый пример:
string SomeFunc(const string &fStr)
{
string str;
while (true)
{
getline(cin, str);
if (str == fStr)
goto retRes;
else if (str == "stop")
goto retRes;
}
retRes:
return str;
badRet:
return "fail";
}
В теле функции есть метка 'badRet' на которую не ссылается ни один оператор 'goto', но при этом присутствует другая метка 'retRes', на которую есть ссылка. Программист ошибся, и вместо перехода на метку 'badRet' продублировал переход на метку 'retRes'.
Корректный код может быть таким:
string SomeFunc(const string &fStr)
{
string str;
while(true)
{
getline(cin,str);
if (str == fStr)
goto retRes;
else if(str == "stop")
goto badRet;
}
retRes:
return str;
badRet:
return "fail";
}
Рассмотрим второй пример:
switch (c)
{
case 0:
...
break;
case1: // <=
...
break;
defalt: // <=
...
break;
}
Программист допустил две опечатки при написании тела 'switch', в результате чего появились две метки, на которые не ссылается ни один оператор 'goto'. Помимо того, что эти метки нарушают данное правило, код под ними оказался недостижим.
Исправленный код:
switch (c)
{
case 0:
...
break;
case 1:
...
break;
default:
...
break;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Неиспользованные параметры функции часто появляются после рефакторинга кода. Если сигнатура функции не соответствует её реализации, сложно сразу понять, является ли это ошибкой программиста.
Рассмотрим пример:
static bool CardHasLock(int width, int height)
{
const double xScale = 0.051;
const double yScale = 0.0278;
int lockWidth = (int)floor(width * xScale);
int lockHeight = (int)floor(width * yScale);
....
}
Из кода видно, что параметр 'height' ни разу не используется в теле функции. Скорее всего, здесь допущена ошибка, и код инициализации переменной 'lockHeight' должен выглядеть следующим образом:
int lockHeight = (int)floor(height * yScale);
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Если переменная POD-типа не инициализируется явно и не имеет инициализатора по умолчанию, то её значение будет неопределённым. Использование такого значения приведет к неопределенному поведению.
Простой синтетический пример:
int Aa = Get();
int Ab;
if (Ab) // Ab - uninitialized variable
Ab = Foo();
else
Ab = 0;
Как правило, ошибки использования неинициализированных переменных, возникают из-за опечаток. Например, может оказаться, что в этом месте следовало использовать другую переменную. Корректный вариант кода:
int Aa = Get();
int Ab;
if (Aa) // OK
Ab = Foo();
else
Ab = 0;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило справедливо только для C++. Бросание исключения в деструкторе объекта - плохая практика. Начиная с C++11, бросание исключения в теле деструктора приводит к вызову функции 'std::terminate'. Из этого следует, что исключение, брошенное внутри деструктора, должно быть обработано внутри того же деструктора.
Рассмотрим первый пример:
LocalStorage::~LocalStorage()
{
...
if (!FooFree(m_index))
throw Err("FooFree", GetLastError());
...
}
Анализатор обнаружил деструктор, содержащий оператор throw вне блока try..catch. Данный код следует переписать таким образом, чтобы сообщить об ошибке, возникшей в деструкторе без использования механизма исключений. Если ошибка не критична, то ее можно игнорировать:
LocalStorage::~LocalStorage()
{
try {
...
if (!FooFree(m_index))
throw Err("FooFree", GetLastError());
...
}
catch (...)
{
assert(false);
}
}
Также исключения могут возникать при вызове оператора 'new'. При невозможности выделения памяти будет сгенерировано исключение 'std::bad_alloc'. Рассмотрим второй пример:
A::~A()
{
...
int *localPointer = new int[MAX_SIZE];
...
}
Появление исключения также возможно и при использовании dynamic_cast<Type> при работе с ссылками. При невозможности приведения типов будет сгенерировано исключение 'std::bad_cast'. Рассмотрим третий пример:
B::~B()
{
...
UserType &type = dynamic_cast<UserType&>(baseType);
...
}
Для исправления данных ошибок следует переписать код таким образом, чтобы 'new' или 'dynamic_cast' были помещены в блок 'try-catch'.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Если один или несколько элементов массива инициализируются явно, то все элементы должны быть также явно инициализированы.
При инициализации с помощью списка в фигурных скобках все неинициализированные элементы будут инициализированы по умолчанию (например, нулем). Обеспечение явной инициализации каждого элемента гарантирует, что всем элементам было присвоено желаемое значение.
Исключения:
Несколько примеров, нарушающих правило:
int White[4] = { 0xffff };
int RGBwhite[4] = { 0xffff, 0xffff, 0xffff };
char *a3[100] = { "string", "literals" };
char hi[5] = { 'H', 'i', '!' };
int designated_butNotAll[4] = { [0] = 3,[1] = 1, 0 };
И несколько корректных:
char lpszTemp[5] = { '\0' };
char a1[100] = "string_literal";
char a2[100] = { "string_literal" };
int Black[4] = { 0 };
int CMYKwhite[4] = { 0, 0, 0, 0 };
int CMYKblack[4] = { 0xffff, 0xffff, 0xffff, 0xffff };
int designated_All[4] = { [0] = 3,[1] = 1,[2] = 4 };
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило справедливо только для C. В языке C является допустимым использование функции без её предварительного объявления. Однако такое использование опасно, т. к. может стать причиной некорректной работы программы.
Рассмотрим простой пример.
char *CharMallocFoo(int length)
{
return (char*)malloc(length);
}
Поскольку заголовочный файл <stdlib.h> не подключен, компилятор языка C посчитает, что функция 'malloc' вернет тип 'int'. Неверная интерпретация возвращаемого значения компилятором может привести к проблемам во время выполнения программы, в том числе и к аварийному завершению.
Если программа 64-битная, то, скорее всего, будут потеряны старшие 32-бита возвращаемого адреса. Поэтому программа некоторое время будет работать корректно. Однако, когда свободная память в 4-х младших гигабайтах адресного пространства закончится или будет сильно фрагментирована, то будет выделен буфер за пределами 4-х младших гигабайт. Так как будут потеряны старшие биты адреса, то последствия будут крайне неприятны и непредсказуемы. Подробнее эта ситуация рассматривается в статье "Красивая 64-битная ошибка на языке Си".
Исправленный вариант кода:
#include <stdlib.h>
....
char *CharMallocFoo(int length)
{
return (char*)malloc(length);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор обнаружил функцию с не-void возвращаемым типом, которая не возвращает значение на всех путях выполнения. Согласно стандарту C/C++, это может привести к неопределенному поведению.
Рассмотрим пример, в котором неопределенное значение возвращается только иногда:
BOOL IsInterestingString(char *s)
{
if (s == NULL)
return FALSE;
if (strlen(s) < 4)
return;
return (s[0] == '#') ? TRUE : FALSE;
}
В коде допущена опечатка. Если длина строки меньше 4 символов, то функция вернет неопределенное значение. Корректный вариант:
BOOL IsInterestingString(char *s)
{
if (s == NULL)
return FALSE;
if (strlen(s) < 4)
return FALSE;
return (s[0] == '#') ? TRUE : FALSE;
}
Примечание. Анализатор старается определить ситуации, когда отсутствие возвращаемого значения не является ошибкой. Пример кода, который анализатор сочтет безопасным:
int Foo()
{
...
exit(10);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Значения essential character типа не должны использоваться в арифметических выражениях.
Стандарт MISRA определяет следующую модель сущностных типов (Essential type model), в которой переменная может иметь тип:
Указатели в этой модели отсутствуют.
Согласно модели сущностных типов, значения essential character типа не должны использоваться в арифметических выражениях, так как представлены нечисловым типом.
Рассмотрим список правильных способов использовать переменные символьного типа в арифметических выражениях:
Пример кода, на который анализатором будут выданы предупреждения:
void foo(char ch, unsigned ui, float f, _Bool b, enum A eA)
{
ch + f; // Essential character type should not be used in
// the addition operation with expression
// of the essential floating type
ch + b; // Also relates to the essential Boolean
ch + eA; // Also relates to the essential enum <A> type
(ch + ui) + (ch - 6); // After the expressions in parentheses
// have been executed, both operands of the
// essential character type are used
// in addition operation
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Значения, использующиеся в выражениях, должны иметь соответствующие сущностные типы.
Стандарт MISRA определяет следующую модель сущностных типов (Essential type model), в которой переменная может иметь тип:
Указатели в этой модели отсутствуют.
В языке Си нет ограничений на операции с базовыми типами, но некоторые из этих операций могут иметь неуточненное/неопределенное поведение или вовсе не иметь смысла. Например:
Также могут быть опасными неявные приведения к Boolean, т.к. не все десятичные дроби могут быть представлены в двоичной системе счисления.
void Foo(float f, _Bool other_expr)
{
If (f || other_expr) ....
}
В следующей таблице отмечены знаком 'X' пересечения типов операндов и операций, из которых не следует составлять выражения.
Пример кода, на который будут выданы соответствующие сообщения:
void Foo(float f, _Bool b, int a[], enum E e)
{
if (~a[(e ? 1 : 2) >> (-b * f --> +b) << signed(-24U)]) ....;
}
Исключение: выражение знакового типа с положительным значением может быть использовано в качестве правого операнда оператора сдвига (>>, <<).
void foo(signed vi, unsigned _)
{
assert(vi >= 0);
_ >> vi;
_ << vi;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Приведение указателя одного типа к указателю на другой может привести к неопределённому поведению, если выравнивание этих типов не совпадает.
Пример кода, на который анализатор выдаст предупреждения:
void foo( void )
{
int *pi;
double *pd = ....;
typedef int *PI;
pi = pd; // <=
pi = (int*) pd; // <=
pi = PI(pd); // <=
}
Диагностика также проверяет квалификаторы типов, на которые ссылаются указатели при приведени типов:
void foo( void )
{
double **ppd = ....;
const double **ppcd = (const double **) ppd;
double * const *pcpd = ....;
const volatile double * const *pcvpd =
(const volatile double * const *) pcpd;
}
Исключениями являются приведение указателя к указателю на типы 'char', 'signed char', 'unsigned char', 'void', поскольку стандарт явно определяет такое поведение.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор обнаружил потенциально возможную ошибку в записи макроса. Макрос и его параметры следует заключать в скобки.
В случае, когда параметры или само выражение не заключены в скобки, может быть нарушена задуманная последовательность действий после того, как макрос будет подставлен.
Пример кода, на который анализатор выдаст предупреждение:
#define DIV(x, y) (x / y)
Пример использования макроса, приводящего к ошибке:
Z = DIV(x + 1, y + 2);
В результате получим:
Z =(x + 1 / y + 2);
Чтобы избежать нарушения последовательности действий, этот макрос стоит переписать следующим образом:
#define DIV(x,y) ((x) / (y))
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Существует возможность вызвать non-void функцию и не использовать возвращаемое ею значение. Такое поведение программы может скрывать ошибку.
Результат non-void функции всегда должен быть использован. Пример кода, на который анализатор выдаст предупреждение:
int Foo(int x)
{
return x + x;
}
void Bar(int x)
{
Foo(x);
}
В случае, если потеря возврщаемого значения была задумана разработчиком, можно использовать приведение к 'void' типу. Пример кода, на который анализатор не выдаст предупреждение:
void Bar(int x)
{
(void)Foo(x);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Копирование адреса объекта в указатель/ссылку с большим временем жизни может привести к возникновению "висячего" указателя/ссылки после того, как исходный объект перестанет существовать. Это является нарушением безопасности памяти. Использование данных, на которые указывает "висячий" указатель/ссылка, приводит к неопределенному поведению.
Первый пример кода, на который анализатор выдаст предупреждение:
int& Foo( void )
{
int some_variable;
....
return some_variable;
}
Второй пример кода, на который анализатор выдаст предупреждение:
#include <stddef.h>
void Bar( int **ptr )
{
int some_variable;
....
if (ptr != NULL)
*ptr = &some_variable;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило справедливо только для C. Указатель на стандартный тип FILE не должен быть явно или неявно разыменован. Копирование этого объекта бессмысленно, т. к. копия не даст того же самого поведения. Прямое управление объектом FILE запрещено, т. к. может быть несовместимо с дизайном, который принят для работы с потоками (stream) управления файлами.
Явное разыменование подразумевает обычное разыменование специально отведенными для этого операторами:
Неявное разыменование - вызов функции, которая внутри себя разыменовывает указатель, например, 'memcpy' или 'memcmp'.
Пример кода, на котором анализатор выдаст предупреждения:
void foo()
{
FILE *f = fopen(....);
FILE *d = fopen(....);
....
if (memcmp(f, d, sizeof(FILE)) == 0) { .... } // <=
memset(d, 0, sizeof(*d)); // <=
*d = *f; // <=
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило справедливо только для C++. При сравнении на равенство или неравенство значений вещественных типов в зависимости от используемого процессора и настроек компилятора часто можно получить неожиданный результат.
Пример кода, на который анализатор выдаст предупреждение:
const double PI_div_2 = 1.0;
const double sinValue = sin(M_PI / 2.0);
if (sinValue == PI_div_2) { .... }
Чтобы сравнение значений вещественных типов не содержало ошибок, нужно либо использовать встроенные константы 'std::numeric_limits<float>::epsilon()' или 'std::numeric_limits<double>::epsilon()', либо создать собственную константу 'Epsilon' с заданной точностью.
Исправленный пример кода:
const double PI_div_2 = 1.0;
const double sinValue = sin(M_PI / 2.0);
// equality
if (fabs(a - b) <= std::numeric_limits<double>::epsilon()) { .... };
// inequality
if (fabs(a - b) > std::numeric_limits<double>::epsilon()) { .... };
В некоторых случаях сравнение двух вещественных чисел через оператор '==' или '!=' допустимо. Например, когда переменная сравнивается с заведомо известным значением:
bool foo();
double bar();
double val = foo() ? bar() : 0.0;
if (val == 0.0) { .... }
Анализатор не выдает предупреждений, если сравнивается одно и то же значение с самим собой. Такое сравнение позволяет определить, хранится ли в переменной NaN:
bool isnan(double value) { return value != value; }
Впрочем, более хорошим стилем будет использовать для этой проверки функцию 'std::isnan'.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Переменные следует определять в минимально возможной области видимости. Это позволит избежать возможных ошибок, связанных со случайным использованием переменных вне предусмотренной для них области видимости, а также может помочь сэкономить потребление памяти и увеличить производительность программы.
Пример кода, на который анализатор выдаст предупреждение:
static void RenderThrobber(RECT *rcItem, int *throbbing, ....)
{
....
int width = rcItem->right - rcItem->left;
....
if (*throbbing)
{
RECT rc;
rc.right = width;
....
}
.... // width больше нигде не используется
}
Переменная 'width' используется только внутри блока 'if', следовательно, будет разумным перенести переменную внутрь этого блока. Вычисление 'width' при этом будет происходить только при выполнении условия, что экономит время. Дополнительно это позволит избежать ошибочного использования этой переменной в дальнейшем.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Анализатор обнаружил опасное приведение числа к перечислению. Указанное число может не входит в диапазон значений enum.
Пример:
enum TestEnum { A, B, C };
TestEnum Invalid = (TestEnum)42;
Так как стандарт не указывает базовый тип для enum, то результат приведения числа, чьё значение не входит диапазон элементов enum, является unspecified behavior до C++17 и undefined behavior начиная с C++17.
Для того чтобы этого избежать, необходимо проверять числа перед приведениями. Как вариант, можно явно указать базовый тип для enum или использовать 'enum class', базовый тип которого по умолчанию - 'int'.
Корректный код:
enum TestEnum { A, B, C, Invalid = 42 };
Или:
enum TestEnum : int { A, B, C };
E Invalid = (E)42;
Или:
enum class TestEnum { A, B, C };
TestEnum Invalid = (TestEnum)42;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
При применении унарного минуса к переменной типа 'unsigned int', 'unsigned long' или 'unsigned long long' тип этой переменной не изменится и останется 'unsigned'. Таким образом, подобная операция является бессмысленной.
При применении унарного минуса к меньшему 'unsigned' целочисленному типу в результате целочисленного расширения возможно получение 'signed' числа, однако это не является хорошим подходом, и на такую операцию также будет выдано предупреждение.
Пример кода, на который анализатор выдаст предупреждение:
unsigned int x = 1U;
int y = -x;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Не рекомендуется использование декремента (--) или инкремента (++) в одном выражении с другими операторами. Использование этих операторов в выражении, содержащем сторонние побочные эффекты приводит к ухудшению читаемости кода, а также может привести к возникновению неопределенного поведения (undefined behaviour). Безопасным подходом будет использование рассматриваемых операторов в отдельных выражениях.
Пример кода, на который анализатор выдаст предупреждение:
i = ++i + i--;
Здесь происходит попытка изменить одну переменную в пределах одной точки следования. Это приводит к неопределенному поведению (undefined behaviour).
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
При побитовом сдвиге значение правого операнда должно находиться в диапазоне [0 .. N - 1], где N - количество бит, которое необходимо для представления левого операнда. Несоблюдение этого правила ведет к неопределенному поведению.
На следующий код будут выданы соответствующие предупреждения:
(int32_t) 1 << 128u;
(unsigned int64_t) 2 >> 128u;
int64_X >>= 64u;
any_var << -2u;
Рассмотрим пример из реального приложения, в котором происходит неопределенное поведение вследствие неверного побитового сдвига:
UINT32 m_color1_mask;
UINT32 m_color2_mask;
#define ARRAY_LENGTH(x) (sizeof(x) / sizeof(x[0]))
PALETTE_INIT( montecar )
{
static const UINT8 colortable_source[] =
{
0x00, 0x00, 0x00, 0x01,
0x00, 0x02, 0x00, 0x03,
0x03, 0x03, 0x03, 0x02,
0x03, 0x01, 0x03, 0x00,
0x00, 0x00, 0x02, 0x00,
0x02, 0x01, 0x02, 0x02,
0x00, 0x10, 0x20, 0x30,
0x00, 0x04, 0x08, 0x0c,
0x00, 0x44, 0x48, 0x4c,
0x00, 0x84, 0x88, 0x8c,
0x00, 0xc4, 0xc8, 0xcc
};
....
for (i = 0; i < ARRAY_LENGTH(colortable_source); i++)
{
UINT8 color = colortable_source[i];
if (color == 1)
state->m_color1_mask |= 1 << i; // <=
else if (color == 2)
state->m_color2_mask |= 1 << i; // <=
prom_to_palette(machine, i,
color_prom[0x100 + colortable_source[i]]);
}
....
}
В коде в цикле на i-ой итерации сдвигают единицу на i позиций влево. В результате цикла переменная 'i' будет принимать значения в диапазоне [0 .. 43], что больше допустимого диапазона, начиная с 32-ой итерации цикла (в случае, если int - 32-х битовый тип данных).
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило справедливо только для C. Использование указателя на стандартный тип 'FILE' после закрытия потока, на который он указывает, может привести к ошибкам, т. к. состояние этого объекта будет не определено.
На следующий участок кода будет выдано соответствующее предупреждение.
FILE* f = fopen("/path/to/file.log", "w");
if (f == NULL) { .... }
fprintf(f, "....");
if (....) // something went wrong
{
fclose(f);
fprintf(f, "...."); // Print log information
// after stream has been released.
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Оператор 'sizeof()' не выполняет переданное ему выражение, а только вычисляет тип результирующего выражения и возвращает его размер на этапе компиляции. Следовательно, для любого кода внутри 'sizeof()' компилятор не генерирует ассемблерного кода (невыполняемые контекст), и ожидаемые операции внутри не будут произведены.
Поэтому операнд, передаваемый 'sizeof()', не должен содержать каких-либо сторонних эффектов, чтобы избежать потери операций.
Пример кода, на который анализатор выдаст предупреждение:
int x = ....;
....
size_t s = n * sizeof(x++);
Для получения ожидаемого эффекта нужно переписать код следующим образом:
int x = ....;
....
++x;
size_t s = n * sizeof(x);
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. Анализатор обнаружил, что объект передается в функцию через указатель или ссылку, но не модифицируется в теле функции. Это может свидетельствовать о наличии ошибки. Также в случае, если такой аргумент функции действительно не изменяется, это делает сигнатуру функции неточной. Добавление константности объекту предотвращает потенциальные ошибки и делает определение функции более наглядным.
Пример кода, на который анализатор выдаст предупреждение:
size_t StringEval(std::string &str)
{
return str.size();
}
Здесь переменная 'str' используется только для чтения, хотя она и передается по неконстантной ссылке. Явная константность в сигнатуре функции позволила бы с первого взгляда понять, что функция не изменяет объект, а также предотвратить потенциальные ошибки при изменении самой функции.
Корректный вариант кода:
size_t StringEval(const std::string &str)
{
return str.size();
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Согласно стандарту C/C++ (C11 § 6.5.8 п. 5; C++17 § 8.5.9 п. 3) применение одного из операторов '-', '>', '>=', '<' или '<=' к двум указателям, которые не указывают на элементы одного и того же массива, приводит к неопределенному/неуточненному поведению (undefined/unspecified behaviour). Таким образом, если два указателя указывают на разные объекты массива, тогда эти объекты массива должны входить в один и тот же массив для того, чтобы сравнить их.
Пример кода, на который анализатор выдаст предупреждение:
int arr1[10];
int arr2[10];
int *pArr1 = arr1;
if (pArr1 < arr2)
{
....
}
Также следующий код является ошибочным:
int arr1[10];
int arr2[10];
int *pArr1 = &arr1[1];
int *pArr2 = &arr2[1];
int len = pArr1 - pArr2;
Подробнее о том, почему использование указателей может привести к ошибке можно прочитать в статье: "Указатели в C абстрактнее, чем может показаться".
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. В коде не должно быть ни одной определённой вариативной пользовательской функции (содержащий эллипсис '...'). Аргументы, передаваемые в функцию через эллипсис, не проверяются компилятором на соответствие типов, что может привести к ошибкам. Имея только объявление без определения, можно перепутать тип аргументов. Также передача аргумента non-POD типа ведёт к неопределённому поведению.
Пример кода, на который анализатор выдаст предупреждение:
void foo(int _, ...) // <=
{
va_list ap;
va_start(ap, _);
....
va_end(ap);
}
Стандарт, однако, разрешает объявление вариативных функций для использования существующих библиотечных функций. Допустимый код:
int printf(const char *fmt, ...);
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Использование присвоения в подвыражениях привносит дополнительный побочный эффект, что уменьшает читаемость кода и повышает шанс внесения новой ошибки в код.
Более того, следование данному правилу значительно снижает вероятность путаницы между операторами '=' и '=='.
Пример кода, на который анализатор выдаст предупреждения:
int Inc(int i)
{
return i += 1; // <=
}
void neg(int a, int b)
{
int c = a = b; // <=
Inc(a = 1); // <=
if(a = b) {} // <=
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное правило актуально только для языка C. Указатели не должны использоваться в выражениях '+', '-', '+=' и '-='. При этом допускается использовать операцию индексирования '[]' и инкремент/декремент ('++'/'--').
Использование адресной арифметики усложняет чтение программы и может являться причиной неправильного понимания написанного когда. С другой стороны, использование индексирования является явным и легко читаемым, и даже если при его использовании будет внесена ошибка, её будет легче обнаружить. То же касается и операций инкремента/декремента: они явно дают понять, что мы последовательно обходим область памяти, представляющую собой непрерывную область данных.
Пример нарушения правила:
int arr[] = { 0, 1, 2 };
int *p = arr + 1; //+V2562
p += 1; //+V2562
Корректный вариант кода:
int arr[] = { 0, 1, 2 };
int *p = &arr[1];
++p;
int *q = p[1];
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное правило актуально только для языка C++. Данное правило MISRA рекомендует не использовать адресную арифметику. Единственной формой адресной арифметики, допускаемой к использованию данным правилом, является операция индексирования ('[]'), применённая к сущности, объявленной как массив.
Исключение: допускается использовать инкремент и декремент ('++' и '--').
Использование адресной арифметики усложняет чтение программы и может являться причиной неправильного понимания написанного когда. С другой стороны, использование индексирования является явным и легко читаемым, и даже если при его использовании будет внесена ошибка, её будет легче обнаружить. То же касается и операций инкремента/декремента: они явно дают понять, что мы последовательно обходим область памяти, представляющую собой непрерывную область данных.
Пример нарушения правила:
int arr[] = { 0, 1, 2 };
int *p = arr + 1; //+V2563
p += 1; //+V2563
int *q = p[1]; //+V2563
Корректный вариант кода:
int arr[] = { 0, 1, 2 };
int *p = &arr[1];
++p;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. В коде не должно быть неявных преобразований значений вещественного типа в целочисленный и наоборот.
Под целочисленными типами подразумеваются:
Под типами с плавающей точкой подразумеваются:
При неявном преобразовании значений вещественных типов в целые может быть утеряна информация (например, дробная часть), а также возможно возникновение неопределенного поведения, если значение вещественного типа не может быть представлено целым типом.
Неявное преобразование значения целого типа в вещественный может привести к его неточному представлению, которое не соответствует ожиданиям разработчика.
Пример кода, на который анализатор выдаст предупреждения:
void foo1(int x, float y);
void foo2()
{
float y = 10;
int x = 10.5;
foo1(y, x);
}
Пример правильного с точки зрения этой диагностики кода:
void foo1(int x, float y);
void foo2()
{
float y = static_cast<float>(10);
int x = static_cast<int>(10.5);
foo1(static_cast<int>(y), static_cast<float>(x));
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Функции не должны вызывать себя ни напрямую, ни косвенно. Рекурсия может привести к сложно отлавливаемым ошибкам. Одной из них может быть переполнение стека при очень глубокой рекурсии.
Пример кода, на который анализатор выдаст предупреждения:
#include <stdint.h>
uint64_t factorial(uint64_t n)
{
return n > 1 ? n * factorial(n - 1) : 1;
}
По возможности, стоит заменить рекурсивный вызов циклом. Вот как это можно сделать с предыдущим примером:
#include <stdint.h>
uint64_t factorial(uint64_t n)
{
uint64_t result = 1;
for (; n > 1; --n)
{
result *= n;
}
return result;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Согласно стандарту языка C, в выражениях беззнаковых типов при переполнении происходит перенос значений относительно нуля. Применение этого свойства беззнаковых типов при вычислениях на этапе выполнения является известной практикой (в отличие от знаковых типов, для которых переполнение ведёт к неопределённому поведению).
Однако, перенос значений относительно нуля для выражений, вычисленных на этапе компиляции, может сбивать с толку.
Пример кода, на который анализатор выдаст предупреждения:
#include <stdint.h>
#define C1 (UINT_MAX)
#define C2 (UINT_MIN)
....
void foo(unsigned x)
{
switch(x)
{
case C1 + 1U: ....; break;
case C2 - 1U: ....; break;
}
}
Если в константном выражении беззнакового типа происходит перенос значения относительно нуля, то согласно текущему правилу он может трактовать не как ошибка в случае, если выражение никогда не будет вычислено:
#include <stdint.h>
#define C UINT_MAX
....
unsigned foo(unsigned x)
{
if(x < 0 && (C + 1U) == 0x42) ....;
return x + C;
}
Выражение '(C + 1U)', содержащее переполнение не будет выполнено, т.к. выражение 'x < 0' всегда ложно. Следовательно, второй операнд логического выражения не будет вычислен.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Удаление 'const' / 'volatile' квалификаторов может привести к проблемам.
Например:
inline int foo(bool &flag)
{
while (flag)
{
// do some stuff...
}
return 0;
}
int main()
{
volatile bool flag = true;
return foo(const_cast<bool &>(flag));
}
Другой пример кода, на который анализатор выдаст предупреждения:
void my_swap(const int *x, volatile int *y)
{
auto _x = const_cast<int*>(x);
auto _y = const_cast<int*>(y);
swap(_x, _y);
}
void foo()
{
const int x = 30;
volatile int y = 203;
my_swap(&x, &y); // <=
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Если при преобразовании арифметических типов сущностные типы операндов не совпадают, это может привести к неочевидным проблемам.
Стандарт MISRA определяет следующую модель сущностных типов (Essential type model), в которой переменная может иметь тип:
Указатели в этой модели отсутствуют.
Язык С предоставляет свободу для проведения преобразования арифметических типов, что может повлечь за собой неочевидные проблемы как потеря знака, переполнение или потеря значимости. Несмотря на свою строгость, стандарт MISRA разрешает преобразования арифметических типов в случае, когда сущностные типы операндов совпадают.
Исключение: левый и правый операнды операторов '+', '-', '+=', '-=' могут иметь сущностные 'character' и 'signed' / 'unsigned' типы соответственно.
Пример кода, на который анализатор выдаст предупреждения:
enum { A };
int i;
unsigned u;
void foo()
{
A + u;
0.f - i;
A > (_Bool)0;
}
Пример правильного с точки зрения этой диагностики кода:
void foo(unsigned short x, _Bool b)
{
x + 1UL;
if (b && x > 4U) ....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. Встроенные операторы '&&', '||', '&' (взятие адреса) и ',' имеют определённый порядок вычисления и семантику. После перегрузки этих функций разработчик может не знать о потере особенностей этих операторов.
1) После перегрузки логических операторов теряется сокращенное вычисление. Для встроенных операторов, если первый операнд '&&' оказался 'false' или первый операнд '||' оказался 'true', вычисление второго операнда не происходит. В результате может быть потеряна часть оптимизаций:
class Tribool
{
public:
Tribool(bool b) : .... { .... }
friend Tribool operator&&(Tribool lhs, Tribool rhs) { .... }
friend Tribool operator||(Tribool lhs, Tribool rhs) { .... }
....
};
// Do some heavy weight stuff
bool HeavyWeightFunction();
void foo()
{
Tribool flag = ....;
if (flag || HeavyWeightFunction()) // evaluate all operands
// no short-circuit evaluation
{
// Do some stuff
}
}
Здесь компилятор не сможет произвести оптимизацию, и произойдет "тяжеловесной" функции, чего бы не произошло для встроенного оператора.
2) Перегрузка унарного оператора взятия адреса '&' также может привести к неочевидной ситуации. Рассмотрим пример:
// Example.h
class Example
{
public:
Example* operator&() ;
const Example* operator&() const;
};
// Foo.cc
#include "Example.h"
void foo(Example &x)
{
&x; // call overloaded "operator&"
}
// Bar.cc
class Foobar;
void bar(Example &x)
{
&x; // may call built-in or overloaded "operator&"!
}
Во втором случае, согласно стандарту С++ ($8.3.1.5) такое поведение является неуточненным, и получение адреса объекта 'x' может вызвать как встроенный оператор, так и перегруженный.
3) Встроенный оператор "запятая" выполняет левый операнд и игнорирует результат, затем вычисляет правый операнд и возвращает результат правого операнда. Также, встроенный оператор гарантирует, что все побочные эффекты левого операнда будут выполнены до того, как начнется выполнение второго операнда.
Перегруженный оператор не дает эту гарантию (до стандарта C++17), поэтому нижеприведенный код может напечатать как 'foobar', так и 'barfoo':
#include <iostream>
template <typename T1, typename T2>
T2& operator,(const T1 &lhs, T2 &rhs)
{
return rhs;
}
int main()
{
std::cout << "foo", std::cout << "bar";
return 0;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. Использование логических операторов '!', '&&' и '||' с переменными, имеющими отличный от 'bool' тип, не имеет смысла, вряд ли предназначено для этого и может указывать на присутствие ошибки. Возможно, должны были использоваться побитовые операторы ('&', '|' или '~').
Пример кода, на который анализатор выдаст предупреждение:
void Foo(int x, int y, int z)
{
if ((x + y) && z)
{
....
}
}
void Bar(int *x)
{
if (!x)
{
....
}
}
Примеры правильного кода с точки зрения правила:
void Foo(int x, int y, int z)
{
if ((x + y) & z)
{
....
}
}
void Foo(int x, int y, int z)
{
if ((x < y) && (y < z))
{
....
}
}
void Bar(int *x)
{
if (x == NULL)
{
....
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Приведения между указателями на объект и целочисленными типами могут послужить причиной неопределенного, неуточненного или зависимого от реализации поведения. В связи с этим, MISRA рекомендует не использовать подобные приведения.
Пример нарушения 1 (для языка C):
int *p = (int *)0x0042;
int i = p;
enum en { A, B } e = (enum en) p;
Пример нарушения 2 (для языка C++):
struct S { int16_t i; int16_t j; } *ps = ....;
int i64 = reinterpret_cast<int>(ps);
Пример нарушения 3 (для языка C и С++):
void foo(int i) {}
void wrong_param_type()
{
char *pc = ....;
foo((int) pc);
}
Пример нарушения 4 (для языка C и C++):
int wrong_return_type()
{
double *pd = ....;
return (int) pd;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Язык C предоставляет свободу при проведении присваиваний между объектами различных арифметических типов. Однако подобные неявные преобразования могут приводить к неочевидным проблемам, таким как потеря знака, точности или значимости.
Стандарт MISRA определяет следующую модель сущностных типов (Essential type model), в которой переменная может иметь тип:
Указатели в этой модели отсутствуют.
Используя модель сущностных типов, можно уменьшить количество подобных неочевидных проблем путем присваивания переменным значений одного и того же сущностного типа. При этом разрешается присваивать в переменную более широкого сущностного типа значения более узкого. Запрещается производить неявные преобразования значений из одного сущностного типа в другой.
Исключения:
Пример кода, на который анализатор выдаст предупреждения:
typedef enum ENUM {ONE} ENUM;
void Positive(signed char x)
{
unsigned char uchr = x; // <=
unsigned short usht = x; // <=
unsigned int uit = x; // <=
unsigned long ulg = x; // <=
unsigned long long ullg = x; // <=
long double ld = 0.0;
double d = ld; // <=
float f = d; // <=
ENUM e = x; // <=
}
Пример правильного с точки зрения этой диагностики кода:
enum {ONE = 1, TWO, THREE, FOUR, FIVE, SIX,
MUCH = 123123, MORE = 0x7FFFFFFF-1};
void Negative()
{
signed char c = ONE; // ok
signed short h = TWO; // ok
signed int i = THREE; // ok
signed long long ll = FOUR; // ok
unsigned char uc = FIVE; // ok
unsigned short uh = SIX; // ok
unsigned int ui = MUCH; // ok
unsigned long long ull = MORE; // ok
float f = 0.0f; // ok
double d = f; // ok
long double ld = d; // ok
ENUM e = c; // ok
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Согласно стандарту C++ идентификаторы и имена макросов, содержащие в себе '__', либо начинающиеся на '_[A-Z]', зарезервированы для использования в реализации языка и стандартной библиотеки. В языке C тоже есть такое правило, но '__' должно быть в начале идентификатора.
Объявление таких идентификаторов снаружи стандартной библиотеки может привести к проблемам. К примеру, код ниже может изменить поведение функции 'abs':
#define _BUILTIN_abs(x) (x < 0 ? -x : x)
#include <cmath>
int foo(int x, int y, bool c)
{
return abs(c ? x : y)
}
если функция 'abs' реализована через использование встроенной функции компилятора (интринсик) следующим образом:
#define abs(x) (_BUILTIN_abs(x))
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C++. Функция, объявленная в области видимости блока, будет видна также в пространстве имён, обрамляющем блок.
Пример кода:
namespace Foo
{
void func()
{
void bar(); // <=
bar();
}
}
void Foo::bar() // Function 'bar' is visible here
{
}
Программист хотел сузить область видимости функции, задекларировав её в блоке функции 'func'. Однако функция 'bar' видна также за пределами пространства имен 'Foo'. Поэтому следует задекларировать функцию явно в обрамляющем пространстве имён:
namespace Foo
{
void bar();
void func()
{
bar();
}
}
void Foo::bar() // Function 'bar' is visible
{
}
Также, вследствие неоднозначности грамматики языка C++, декларация функции может выглядеть как декларация объекта:
struct A
{
void foo();
};
int main()
{
A a();
a.foo(); // compile-time error
}
Данная проблема известна как "Most vexing parse": компилятор разрешает данную неоднозначность "задекларировать функцию или объект" в пользу "задекларировать функцию". Поэтому, несмотря на ожидание программиста задекларировать объект класса 'A' и вызвать нестатическую функцию-член класса 'A::foo', компилятор воспримет это как объявление функции 'a', не принимающей параметров и возвращающей тип 'A'.
Чтобы избежать путаницы, анализатор также предупреждает о таких объявлениях.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C++. Объявления в глобальном пространстве, засоряют список доступных идентификаторов. Новые идентификаторы, добавленные в область видимости блока, могут быть схожими с идентификаторами в глобальном пространстве. Это может запутать разработчика и привести к ошибочному выбору идентификатора.
Чтобы гарантировать ожидания разработчика, все идентификаторы должны располагаться внутри соответствующих пространств имен.
Пример кода, на который анализатор выдает предупреждение:
int x1;
void foo();
В соответствии с правилом данный код должен выглядеть следующим образом:
namespace N1
{
int x1;
void foo();
}
Также допустимый вариант c extern "C" может выглядеть так:
extern "C"
{
int x1;
}
extern "C" void bar();
Согласно стандарту MISRA разрешено использовать 'typedef' в глобальном пространстве имен в том случае, если имя псевдонима типа содержит в себе размер его итогового типа.
Пример кода, на который анализатор не выдает предупреждений:
typedef short int16_t;
typedef int INT32;
typedef unsigned long long Uint64;
Пример кода, на который анализатор выдает предупреждения:
typedef std::map<std::string, std::string> TestData;
typedef int type1;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C++. Функция 'main' должна присутствовать только в глобальном пространстве имён, чтобы разработчик точно понимал, что если она есть, то всегда является точкой входа в программу.
Пример кода, на который анализатор выдает предупреждение:
namespace N1
{
int main();
}
Другой пример со срабатыванием анализатора:
namespace
{
int main();
}
Код, переписанный в соответствии с правилом, может выглядеть так:
namespace N1
{
int start();
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Если формальный параметр функции объявлен как массив с фиксированным размером, передаваемый в качестве фактического аргумента массив должен иметь размер не меньше, чем размер массива, принимаемый функцией.
В языке C передача массива в функцию происходит через передачу указателя на его начало, поэтому в такую функцию возможно передать массив любого размера. Однако, использование указателя в качестве формального параметра ухудшает понимание интерфейса функции – становится непонятно, работает она с одним элементом или с массивом.
Чтобы указать, что функция работает именно с некоторым количеством элементов, ее формальный параметр объявляется как массив, при этом размер часто указывается при помощи макроса. Макрос затем используется для обхода элементов массива:
#define ARRAY_SIZE 32
void foo(int arr[ARRAY_SIZE])
{
for (size_t i = 0; i < ARRAY_SIZE; ++i)
{
// Do something with elements
}
}
Надо помнить, что такой массив по-прежнему является указателем. Следовательно, существует возможность передать массив с меньшим числом элементов, что может привести к выходу за его границы, что является неопределенным поведением:
#define ARRAY_SIZE 32
void foo(int arr[ARRAY_SIZE]);
void bar()
{
int array1[32] = { 1, ...., 32 };
int array2[28] = { 1, ...., 28 };
foo(array2); // <=
}
В данном примере функция получила массив неподходящего размера. Правильным вариантом может быть:
#define ARRAY_SIZE 32
void foo(int arr[ARRAY_SIZE]);
void bar()
{
int array1[32] = { 1, ...., 32 };
int array2[28] = { 1, ...., 28 };
foo(array1); // <=
}
Другим вариантом может быть изменение числа элементов переданного в функцию массива и заполнение добавленных элементов значениями по умолчанию:
#define ARRAY_SIZE 32
void foo(int arr[ARRAY_SIZE]);
void bar()
{
int array1[32] = { 1, ...., 32 };
int array2[32] = { 1, ...., 28 }; // <=
foo(array2);
}
Если функция обрабатывает массивы разного размера, то правило разрешает использовать в качестве аргумента функции массив произвольного размера. Подразумевается, что размер массива будет передан иным способом, например так:
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(arr[0]))
void foo(int arr[], size_t count);
void bar()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[] = { 10, 20, 30 };
foo(array1, ARRAY_SIZE(array1));
foo(array2, ARRAY_SIZE(array2));
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C++. Передача массива в функцию по указателю ведёт к потери размера массива. В результате функция может принять в качестве аргумента массив с меньшим количеством элементов, чем ожидается, и в процессе выполнения выйти за его границы, что приведёт к неопределённому поведению.
Чтобы избежать потери информации о размере массива, его стоит передавать только по ссылке. В случае, когда функция должна обрабатывать массивы разной длины, следует использовать класс для инкапсуляции массива и его размера.
Пример кода, несоответствующего правилу:
void foo(int *ptr);
void bar(int arr[5])
void bar(const char chars[30]);
int main
{
int array[5] = { 1, 2, 3 };
foo(array);
bar(array);
}
Допустимый вариант:
void bar(int (&arr)[5]);
int main
{
int array[7] = { 1, 2, 3, 4, 5 };
bar(array);
}
Другой пример кода, несоответствующий правилу:
void bar(const char chars[30]);
int main()
{
bar("something"); //const char[10]
}
Допустимый вариант с использованием класса для инкапсуляции:
template <typename T>
class ArrayView
{
T *m_ptr;
size_t m_size;
public:
template <size_t N>
ArrayView(T (&arr)[N]) : m_ptr(arr), m_size(N) {}
// ....
};
void bar(ArrayView<const char> arr);
int main()
{
bar("something");
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Анализатор обнаружил макрос, имя которого перекрывает ключевое слово.
Примеры:
#define if while
#define int float
#define while(something) for (;(something);)
Изменение смысла ключевых слов сбивает с толку и может привести к некорректно работающему коду.
Исключение – переопределение слова 'inline', если используется стандарт языка C90.
Пример, который допустим в C90, но будет считаться ошибкой в C99 и более поздних стандартах:
#define inline
Диагностика также игнорирует переопределения ключевых слов, если они идентичны с точки зрения семантики, или макрос раскрывается в одноименное с ним ключевое слово.
Пример:
#define const const
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Запрещено использовать спецификатор 'restrict' в декларациях переменных, формальных параметров функций и полей структур/объединений. Несмотря на то, что компилятор может сгенерировать более оптимизированный код, это может привести к ошибкам, если два и более указателей ссылаются на одну и ту же область памяти.
Пример кода, на который анализатор выдаёт предупреждения:
void my_memcpy(void * restrict dest,
const void * restrict src,
size_t bytes)
{
// ...
}
typedef struct
{
void * restrict m_field;
} MyStruct;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Однострочные комментарии не должны заканчиваться токеном продолжения предыдущей строки ('\').
Пример:
// Some comment \
Это может приводить к тому, что следующая за комментарием строка кода будет также закомментирована. В следующем примере условие на самом деле не проверяется, потому что 'if' попадает в комментарий, и блок кода выполняется всегда:
int startsWith(const char *str, const char *prefix);
void foo();
void foobar(const char *str)
{
// Check that the string doesn't start with foo\bar\
if (startsWith(str, "foo\\bar\\") == 0)
{
foo();
}
....
}
Подобный код может успешно компилироваться и не приводить к предупреждениям компилятора.
Если после символа '\' в строке есть любые символы, кроме '\' или перевода строки, то следующая строка не будет считаться комментарием, и анализатор не выдаст предупреждение:
int startsWith(const char *str, const char *prefix);
void foo();
void foobar(const char *str)
{
// Check that the string doesn't start with "foo\bar\"
if (startsWith(str, "foo\\bar\\") == 0)
{
foo();
}
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Память, динамически выделенная при помощи функций 'malloc', 'calloc', 'realloc', должна быть освобождена функцией 'free'. Блок памяти, освобождённый при помощи функции 'free', нельзя передавать в 'free' повторно. Такие действия ведут к неопределённому поведению программы.
Рассмотрим первый пример:
void foo()
{
int arr[50];
// ....
free(arr);
}
Здесь анализатор обнаружил ошибочный код, в котором осуществляется попытка удаления массива через функцию 'free'. Однако память под массив выделена на стеке и будет освобождена автоматически при выходе из функции.
Рассмотрим ещё пример:
void foo()
{
float *p1 = (float *)malloc(N * sizeof(float));
float *p2 = (float *)malloc(K * sizeof(float));
// ....
free(p1);
free(p1);
}
В коде имеется опечатка, из-за которой дважды освобождается одна и та же область памяти по указателю 'p1'. В результате, во-первых, возникает утечка памяти, так как не освобождается буфер, адрес которого хранится в переменной 'p2'. Во-вторых, из-за повторного освобождения одного и того же буфера возникает неопределённое поведение.
Корректный вариант:
void foo()
{
float *p1 = (float *)malloc(N * sizeof(float));
float *p2 = (float *)malloc(K * sizeof(float));
// ....
free(p1);
free(p2);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Директивы препроцессора (строки начинающиеся с '#') могут использоваться для условного включения или исключения кода из компиляции. Некорректно написанные препроцессорные директивы могут привести к некорректному включению или исключению кода, которое заранее не предполагалось. Поэтому все директивы предварительной обработки должны быть синтаксически корректны.
Рассмотрим пример:
// #define CIRCLE
#define SQUARE
float processArea(float x)
{
#ifdef CIRCLE
return 3.14 * x * x;
#elf defined(SQUARE)
return x * x;
#else1
return 0;
#endif
}
Здесь неправильно написаны директивы препроцессора '#elif' и '#else', что приведет к исключению всего кода из тела функции. Корректный вариант:
// #define CIRCLE
#define SQUARE
float processArea(float x)
{
#ifdef CIRCLE
return 3.14 * x * x;
#elif defined(SQUARE)
return x * x;
#else
return 0;
#endif
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. В условиях конструкций 'if' / 'for' / 'while' должны использоваться выражения, имеющие essential Boolean тип.
Стандарт MISRA определяет следующую модель сущностных типов (Essential type model), в которой переменная может иметь тип:
Таким образом, стандарт разрешает следующие виды выражений:
Пример, на который анализатор выдаст предупреждение:
void some_func(int run_it)
{
if (run_it)
{
do_something();
}
// ....
}
Здесь нужно явно проверить, что переменная не равна нулю:
void some_func(int run_it)
{
if (run_it != 0)
{
do_something();
}
// ....
}
Еще один пример:
void func(void *p)
{
if (!p) return;
// ....
}
Для устранения нарушения следует явно сравнить указатель с нулевым:
void func(void *p)
{
if (p == NULL) return;
// ....
}
Анализатор не выдаст предупреждение на подобный код:
void fun(void)
{
while (1)
{
// ....
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Преобразование переменной или литерала арифметического типа к типу 'void *' и наоборот может послужить причиной неопределённого (для чисел с плавающей точкой) или зависимого от платформы поведения (для целочисленных типов).
Рассмотрим первый синтетический пример:
void* foo(void)
{
double pi = 3.14;
return pi;
}
В данном случае переменная 'pi', имеющая тип 'double', неявно приводится к типу 'void *'. Такой код приведёт к неопределённому поведению.
Рассмотрим второй синтетический пример:
void bar(void)
{
int a = 5;
void* b = (void*)a;
}
Здесь переменная типа 'int' явно приводится к типу 'void *'. Дальнейшее разыменование такого указателя может потенциально привести к ошибкам сегментации.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Не рекомендуется определять структуры, содержащие flexible-массив. Такие структуры применяются, если предполагается динамически выделять под них память, и при этом размер хранимых данных заранее неизвестен.
Пример:
typedef struct
{
size_t len;
int data[]; // flexible array
} S;
S* alloc_flexible_array(size_t n)
{
S *obj = malloc(sizeof(S) + (n * sizeof(int)));
obj->len = n;
return obj;
}
При таком объявлении структуры для массива 'data' размер будет определяться во время выполнения в зависимости от фактического объема данных.
Опасность таких структур состоит в том, что вызов 'sizeof' на них даст неверный результат.
Еще одна проблема состоит в том, что попытка создать копию структуры может приводить к неожиданным результатам, даже если размер вычислен верно. Рассмотрим соответствующий пример:
typedef struct
{
size_t len;
int data[];
} S;
S* make_copy(S *s)
{
S *copy = malloc(sizeof(S) + (s->len * sizeof(int)));
*copy = *s;
return copy;
}
Здесь, несмотря на то что выделено правильное количество памяти, в копию структуры попадет только поле 'len'.
Часто для объявления flexible-массивов может использоваться такой некорректный паттерн:
typedef struct
{
size_t len;
int data[1];
} S;
Компилятор может трактовать выход за границу такого массива в один элемент как неопределенное поведение и оптимизировать код непредсказуемым образом.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Внутри комментариев не должна присутствовать последовательность символов, которая обозначает начало комментария. Такое возможно, если блок комментариев не был закрыт последовательностью '*/' либо блок кода был закомментирован построчно через '//'.
Рассмотрим первый пример:
/* this comment is not closed
some_critical_function();
/* We're still inside the comment */
Здесь первый блок комментариев не закрыт, и второй блок комментариев оказывается внутри первого. Такой код может привести к тому, что некоторые строки с критичным для выполнения кодом могут оказаться случайно закомментированными.
Подобная проблема может происходить при использовании однострочных комментариев. Рассмотрим второй пример:
int some_function(int x, int y)
{
return x // /*
+ y
// */
;
}
Однострочный комментарий подавляет многострочный, и из-за этого итоговое выражение будет таким:
return x + y;
вместо:
int x = y;
Анализатор не выдает срабатывание, если последовательность '//' встречается внутри однострочного комментария:
....
// some_unecessary_call_1(); // probably, should not do this
// some_unecessary_call_2(); // probably, should not do this too
....
Такой код мог получиться путем комментирования некоторого блока кода однострочными комментариями, и при этом блок кода ранее уже их содержал в конце строк.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Анализатор обнаружил потенциально возможную утечку памяти или ресурсов, выделенных такими функциями стандартной библиотеки, как: 'malloc', 'calloc', 'realloc' или 'fopen'.
Рассмотрим пример:
void foo()
{
int *a = (int*)malloc(40 * sizeof(int));
int *b = (int*)malloc(80 * sizeof(int));
....
free(a);
}
Здесь в коде динамически выделено два буфера, а при выходе из функции освобожден только первый из них. В результате возникла утечка памяти.
Данный фрагмент можно исправить следующим образом:
void foo()
{
int *a = (int*)malloc(40 * sizeof(int));
int *b = (int*)malloc(80 * sizeof(int));
....
free(a);
free(b);
}
Рассмотрим ещё один пример:
void bar(bool b)
{
FILE *f = fopen("tmp", "r");
if (b)
{
return;
}
....
fclose(f);
}
В данном случае файл открывается для чтения, и на одном из путей выхода из функции он не закрывается. В результате возникает утечка файлового дескриптора.
Корректный вариант кода:
void bar(bool b)
{
FILE *f = fopen("tmp", "r");
if (b)
{
fclose(f);
return;
}
....
fclose(f);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Преобразование выражений нецелочисленных типов к указателю и наоборот может привести к неопределённому поведению.
Стандарт MISRA определяет следующую модель сущностных типов (Essential type model), в которой переменная может иметь тип:
Преобразование сущностного 'Boolean', 'character' или 'enum' типа к указателю может привести к формированию указателя с неверным выравниванием, что ведёт к неопределённому поведению. Пример:
enum Nums
{
ONE,
TWO,
....
};
double* bar(Nums num)
{
....
return (double*)num;
}
Преобразование указателя к вышеописанным сущностным типам может привести к тому, что результирующее значение не может быть представлено в выбранном сущностном типе, что также приводит к неопределённому поведению. Пример:
void foo(void)
{
....
char *a = "something";
char b = a;
....
}
Преобразование сущностного 'floating' типа к указателю или наоборот ведёт к неопределенному поведению. Пример:
void foo(short *p)
{
// ....
float f = (float) p;
// ....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Приведение типов между указателем на функцию и любым другим типом ведёт к неопределённому поведению. Приведение типов между указателями на функцию несоответствующего типа также станет причиной неопределённого поведения при вызове этой функции.
Ниже представлен пример, в котором происходит приведение типов между указателями на функцию несоответствующего типа, и оба приведения являются недопустимыми:
void foo(int32_t x);
typedef void (*fp)(int16_t x);
void bar(void)
{
fp fp1 = (fp)&foo;
int32_t(*fp2)(void) = (int32_t (*)(void))(fp1);
}
Рассмотрим ещё один пример некорректного кода, где указатель на функцию приводится другим типам:
void* vp = (void*)fp1;
int32_t i32 = (int32_t)foo;
fp fp3 = (fp)i32;
Вызов функции через полученные таким образом указатели может потенциально привести к ошибкам сегментации.
Исключения:
Константа нулевого указателя может быть приведена к указателю на функцию:
fp fp3 = NULL;
Указатель на функцию может быть приведен к типу 'void':
(void) fp4;
Функция может быть неявно приведена к указателю на такую же функцию:
(void(*)(int32_t)) foo;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. В зависимости от версии стандарта языка C, битовые поля должны быть объявлены только с подходящими для этого типами. Для C90: 'signed int' или 'unsigned int'. Для C99: 'signed int', 'unsigned int', '_Bool' или другой интегральный тип, разрешённый имплементацией, с явным указанием модификатора 'signed' или 'unsigned'.
Также разрешается использовать псевдоним ('typedef') для допустимого типа.
Битовое поле типа 'int' может быть как 'signed', так и 'unsigned' в зависимости от компилятора. Если для представления битового поля используется 'unsigned int', то все выделенные для поля биты будут значимыми. Такое битовое поле из 'n' битов имеет диапазон значений '[0, 2 ^ n - 1]'.
Если же для представления битового поля используется 'signed int', то 1 бит будет выделен как знаковый. Поэтому для записи значимой части значения битового поля будет использовано на 1 бит меньше выделенного. Такое битовое поле из 'n' битов имеет диапазон значений '[-2 ^ (n - 1), 2 ^ (n - 1) - 1]'.
Исходя из этого, в зависимости от компилятора битовые поля типа 'int' могут иметь разные диапазоны значений. Чтобы избежать ошибок, которые могут возникнуть из-за этого, нужно явно указывать модификатор знаковости.
Пример неправильного использования битового поля:
struct S
{
int b : 3; // <=
};
void foo()
{
S s;
s.b = 5;
if (s.b != 5)
{
Boom();
}
}
В этом примере, если компилятор выберет беззнаковый тип для представления битового поля 'b', тогда все 3 бита, в которые записывается значение 5, будут значимыми. То есть фактически записывается остаток по модулю 8, и код будет работать как ожидается – в битовое поле будет записано значение 5.
Если компилятор выберет знаковый тип для представления битового поля 'b', то оно разбивается на 1 бит знака и 2 бита значимой части. При записи числа 5 в 'b' в значимую часть запишутся только 2 младших бита. В результате в битовое поле будет записано значение 1 вместо значения 5. Поэтому проверка будет пройдена, и будет вызвана функция 'Boom'.
Для исправления нужно явно указывать модификатор знаковости ('signed' / 'unsigned'):
struct S
{
unsigned int b : 3;
};
void foo()
{
S s;
s.b = 5;
if (s.b != 5)
{
Boom();
}
}
Для того, чтобы явно указать, какие типы допускаются в объявлении битовых полей вашим компилятором, поддерживающим стандарт C99, в файл с исходными кодом или pvsconfig-файл можно добавить следующий комментарий:
//V_2591_ALLOWED_C99_TYPES:short,long long
После двоеточия через запятую указываются допустимые типы без модификаторов знаковости:
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Идентификатор, объявленный в области видимости и не отличающийся по имени от другого идентификатора, объявленного в обрамляющей области видимости, "скрывает" внешний идентификатор. Это может привести к путанице или программной ошибке.
Например, такая коллизия имен может привести к логической ошибке, как в примере ниже:
int foo(int param)
{
int i = 0;
if (param > 0)
{
int i = var + 1;
}
return i;
}
При прочтении этого кода на первый взгляд кажется, что когда в функцию 'foo' передается положительное значение параметра, то результатом вычисления будет это значение, увеличенное на '1'. Однако это не так, и на самом деле функция всегда возвращает '0'. Чтобы показать, что происходит в действительности, исключим из кода коллизию идентификаторов:
int foo(int param)
{
int i_outer = 0;
if (param > 0)
{
int i_inner = var + 1;
}
return i_outer;
}
Теперь видно, что присваивание внутри ветки 'if' в переменную 'i' не влияет на результат вычисления функции 'foo'. Внутренний идентификатор 'i' ('i_inner') скрывает внешний идентификатор 'i' ('i_outer'), что приводит к ошибке.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Однобитные битовые поля не стоит объявлять со знаковым типом. Согласно стандарту C99 §6.2.6.2, однобитное битовое поле знакового типа имеет один бит для знака и ноль — для значений. При любом представлении целых чисел ноль значащих бит не могут специализировать какое-либо значимое число.
Несмотря на то, что C90 не имеет такого описания, правило применяется также и для этой версии стандарта.
Рассмотрим пример:
struct S
{
int a : 1;
};
void foo()
{
struct S s;
s.a = 1;
if (s.a > 0)
{
DoWork();
}
}
Несмотря на то, что в битовое поле явно присваивается '1', проверка на то, что оно больше нуля, не сработает. Поскольку в зависимости от реализации компилятора, единица в поле 's.a' может быть интерпретирована как знаковый бит. При привидении типов для сравнения к типу 'int' получится число '-1' (0xFFFFFFFF). В результате функция 'DoWork()' не будет выполнена, поскольку условие '-1 > 0' ложно. Корректный вариант:
struct S
{
unsigned a : 1;
};
void foo()
{
struct S s;
s.a = 1u;
if (s.a > 0u)
{
DoWork();
}
}
Исключением являются безымянные битовые поля, поскольку нельзя использовать значение из такого поля:
struct S
{
int a : 31;
int : 1; // ok
};
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Контролирующее выражение в управляющих конструкциях 'if', '?:', 'while', 'for', 'do', 'switch' не должно быть инвариантно, то есть не должно всегда приводить к выполнению одной и той же ветки кода. Если контролирующее выражение содержит инвариантное значение, то это может свидетельствовать о программной ошибке. Любой код, который недостижим из-за инвариантного выражения, может быть удалён компилятором. Выражения, содержащие 'volatile'-переменные, не являются инвариантными.
Исключения:
Примечание. Допустимыми инвариантами для создания бесконечных циклов считаются:
Рассмотрим пример:
void adjust(unsigned error)
{
if (error < 0)
{
increase_value(-error);
}
else
{
decrease_value(error);
}
}
В данном примере допущена ошибка: из-за того, что функция принимает беззнаковое число, результат проверки условия всегда будет ложным. В результате всегда будет вызываться только функция 'decrease_value', а ветка кода с вызовом функции 'increase_value' может быть удалена компилятором.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Когда для инициализации объектов массива используется назначенная инициализация (designated initialization), неявное указание размера массива может привести к ошибкам, так как изменение инициализаторов объектов будет неявно изменять размер массива. Явное указание количества элементов позволяет точно и быстро определить размер массива.
int arr[] = { [0] = 5, [7] = 5, [19] = 5, [3] = 2 };
Если размер такого массива не указан явно, то определение размера происходит по наибольшему индексу инициализированных объектов. Когда инициализированных объектов много, разработчик не всегда может правильно определить размер массива.
Если размер массива не задан явно, то удаление или добавление инициализации объекта с наивысшим индексом может значительно изменить размер:
int arr[] = { [0] = 5, [7] = 5, [3] = 2 };
Размер массива уменьшился с 20 до 8 элементов. В том месте, где ожидается этот массив с 20 элементами, может произойти выход за его границы.
Безопасное объявление массива в соответствии с правилом может выглядеть так:
int arr[20] = { [0] = 5, [7] = 5, [19] = 5, [3] = 2 };
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Язык C предоставляет свободу при проведении присваиваний между объектами различных арифметических типов. Однако подобные неявные преобразования могут приводить к неочевидным проблемам, таким как потеря знака, точности или значимости.
Рассмотрим следующий фрагмент кода:
void foo()
{
....
uint16_t var_a = 30000;
uint16_t var_b = 40000;
uint32_t var_sum;
var_sum = var_a + var_b; /* var_sum = 70000 or 4464? */
....
}
При вычислении значения, которое будет присвоено в переменную 'var_sum', происходит неявное преобразование значения типа 'uint16_t' к 'int'. Как следствие, результат присваивания зависит от размера типа 'int'.
Если 'int' имеет 32-битный размер, то вычисления будут выполнены по модулю 2^32, и в переменную 'var_sum' будет записано ожидаемое значение '70000'.
Если 'int' имеет 16-битный размер, то вычисления будут выполнены по модулю 2^16, и в переменную 'var_sum' будет записано значение '70000 % 65536 == 4464'.
Для предотвращения подобных ошибок в стандарте MISRA определена модель сущностных типов (Essential type model). В этой модели переменная может иметь тип:
Используя модель сущностных типов, можно уменьшить количество подобных неочевидных проблем. Для этого следует избегать присвоения составных выражений, имеющих меньший сущностный тип, в переменные и аргументы функций, имеющие более широкий сущностный тип.
Код выше можно исправить, используя явное преобразование к 'uint32_t':
void foo()
{
....
uint16_t var_a = 30 000;
uint16_t var_b = 40 000;
uint32_t var_sum;
var_sum = (uint32_t)var_a + var_b; /* var_sum = 70 000 */
....
};
Теперь вычисления происходят по модулю 2^32 независимо от того, какой размер имеет 'int', и ошибка не возникает, даже если 'int' имеет 16-битный размер.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С++. Приведение указателя на функцию к любому другому указателю ведёт к неопределённому поведению. Приведение типов между указателями на функцию несоответствующего типа также станет причиной неопределённого поведения при вызове этой функции.
Пример кода с нарушениями правила, в котором все четыре приведения типа являются недопустимыми:
void foo(int32_t x);
typedef void (*fp)(int16_t x);
void bar()
{
fp fp1 = reinterpret_cast<fp>(&foo);
fp fp2 = (fp)foo;
void* vp = reinterpret_cast<void*>(fp1);
char* chp = (char*)fp1;
}
Вызов функции через такие указатели может потенциально привести к ошибкам сегментации.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Объявление массивов, имеющих неконстантный размер (variable-length array, VLA), может привести к переполнению стека и потенциальным уязвимостям в программе.
Рассмотрим следующий пример:
void foo(size_t n)
{
int arr[n];
// ....
}
Передача большого числа 'n' может привести к переполнению стека, так как массив станет слишком большим и займет больше памяти, чем есть на самом деле.
Логичным решением выглядит ограничение размера массива:
#define MAX_SIZE 128
void foo(size_t n)
{
size_t size = n > MAX_SIZE ? MAX_SIZE : n;
int arr[size];
if (size < n) // error
// ....
}
Однако в таком случае лучше использовать константу для упрощения логики программы:
#define SIZE 128
void foo()
{
int arr[SIZE];
// ....
}
Также это позволит избежать проблем, связанных с VLA, например, вычислением 'sizeof' на таких массивах и их передачей в другие функции.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Функции стандартной библиотеки из заголовочных файлов <signal.h> / <csignal> могут быть опасны. Их поведение зависит от реализации, а их использование может привести к неопределенному поведению.
Причиной неопределенного поведения, к примеру, является использование обработчиков сигналов в многопоточной программе. С другими причинами можно ознакомиться здесь.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций:
Пример кода, на который анализатор выдаст предупреждение:
#include <signal.h>
void handler(int sig) { .... }
void foo()
{
signal(SIGINT, handler);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Функции стандартной библиотеки из заголовочных файлов '<stdio.h>' / '<cstdio>' и '<wchar.h>' могут быть опасны. Их поведение зависит от реализации, а их использование может привести к неопределенному поведению.
Рассмотрим следующий фрагмент кода:
#include <stdio.h>
void InputFromFile(FILE *file); // Read from 'file'
void foo()
{
FILE *stream;
....
InputFromFile(stream);
fflush(stream);
}
В коде сначала происходит чтение из потока 'stream', а затем поток передается в функцию 'fflush'. Такая последовательность операций приводит к неопределенному поведению.
Анализатор выдаст предупреждение, если обнаружит использование любых функций, определенных в заголовочных файлах '<stdio.h>' / '<cstdio>' и '<wchar.h>':
Пример кода, на который анализатор выдаст предупреждение:
#include <stdio.h>
void foo(const char *filename, FILE *oldFd)
{
FILE *newFd = freopen(filename, "r", oldFd);
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Использование объявлений функций в стиле "K&R", а также неименованных параметров функций небезопасно.
Функции, объявленные в старом стиле – "K&R" – не несут в себе информации о типах и количестве параметров, поэтому использование таких функций может привести к ошибкам.
Использование именованных параметров функций дает полезную информацию об интерфейсе функции, а также позволяет отследить ошибку, если имена в объявлении и определении различаются.
Рассмотрим пример:
// header
void draw();
// .c file
void draw(x, y)
double x;
double y;
{
// ....
}
// usage
void foo()
{
draw(1, 2);
}
Декларация функции 'draw' не содержит параметров, так что при вызове функции 'draw' в нее будут переданы 2 параметра типа 'int', а не 'double', что является ошибкой. Объявление функции с использованием прототипа исправит проблему:
// header
void draw(double x, double y);
// .c file
void draw(double x, double y)
{
// ....
}
Если функция не имеет параметров, то объявление с использованием пустых скобок не является правильным, т.к. такое объявление соответствует стилю "K&R":
void foo();
Такая декларация допускает передачу любого числа аргументов. Чтобы явно указать, что функция не имеет параметров, нужно использовать ключевое слово 'void':
void foo(void);
Неименованные параметры делают интерфейс функции менее понятным:
void draw(double, double);
Поэтому следует давать параметрам имена, чтобы избежать ошибок в использовании функции:
void draw(double x, double y);
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Последовательности восьмеричных и шестнадцатеричных чисел внутри строковых и символьных литералов должны быть завершёнными. Это поможет избежать ошибок с определением позиции завершения escape-последовательности.
Рассмотрим пример:
const char *str = "\x0exit";
Строковый литерал в данном примере имеет длину в 4 символа, а не 5, как может показаться на первый взгляд. Последовательность '\x0e' считается за один символ с кодом 0xE, а не символ с нулевым кодом и букву 'e'.
Поэтому escape-последовательность должна быть завершена одним из двух способов:
Следующие 2 примера представляют собой правильный вариант завершения escape-последовательности:
const char *str1 = "\x0" "exit";
const char *str2 = "\x1f\x2f";
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Ключевое слово 'static' не должно использоваться в контексте указания размера массива, принимаемого функцией, так как оно может быть проигнорировано. По сути, оно является лишь указанием компилятору, на основании которого он может сгенерировать более эффективный код.
Рассмотрим пример:
void add(int left[static 10], int right[static 10])
{
for(size_t i = 0U; i < 10U; ++i)
{
left[i] += right[i];
}
}
extern int data1[10];
extern int data2[20];
extern int data3[5];
void foo(void)
{
add(data1, data2);
add(data1, data3);
}
В коде несколько раз вызывается функция 'add', формальными параметрами которой являются два массива размером не менее 10 элементов. Второй вызов функции приведёт к неопределённому поведению, так как фактический размер массива (5) меньше ожидаемого (10).
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Тип 'va_list', а также макросы 'va_arg', 'va_start', 'va_end' и 'va_copy', необходимые для работы с функциями с переменным числом аргументов и определённые в заголовочном файле '<stdarg.h>', не должны использоваться. Их неправильное использование часто становится причиной неопределённого поведения.
Рассмотрим пример:
#include <stdint.h>
#include <stdarg.h>
void foo(va_list args)
{
double y;
y = va_arg(args, int);
}
void bar(uint16_t count, ...)
{
uint16_t x;
va_list ap;
va_start (ap, count); // <=
x = va_arg (ap, int);
foo(ap);
x = va_arg (ap, int);
}
void baz(void)
{
bar(1.25, 10.07);
}
В приведенном выше коде есть несколько проблем, которые могут привести к неопределённому поведению (примечание: в списке перечислены не все опасности, содержащиеся в коде):
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Запрещено использовать функции или макросы из заголовочного файла '<tgmath.h>'. Их использование может привести к неопределённому поведению.
Рассмотрим пример:
void bar(float complex fc)
{
ceil(fc); // undefined behavior
}
Вызов функции 'ceil' с фактическим аргументом типа 'float complex' приведет к неопределенному поведению, так как в стандартной библиотеке отсутствует специализированная версия с таким типом формального параметра.
В случае, когда специализированная функция существует, следует использовать именно ее для того, чтобы избежать подобных ситуаций:
#include <tgmath.h>
float foo(float x)
{
return sin(x);
}
Для функции 'sin' существует специализированная версия с формальным аргументом типа 'float' - 'sinf':
#include <math.h>
float foo(float x)
{
return sinf(x);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Стандарт C не определяет поведение, при котором происходит запись в файл, открытый только для чтения. Поэтому такое поведение некорректно.
Рассмотрим пример:
void foo(void)
{
FILE *file = fopen("file.txt", "r");
if (file != NULL)
{
fputs(file, "I am writing to the read-only file\n");
fclose(file);
}
}
Файл file.txt был открыт в режиме только для чтения, но при этом в него происходит запись. Неизвестно, как поведет себя операционная система в этой ситуации.
Скорее всего, это опечатка, и режим открытия стоит изменить:
void foo(void)
{
FILE *file = fopen("file.txt", "w");
if (file != NULL)
{
fputs(file, "I am writing to the write-only file\n");
fclose(file);
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Функции с квалификатором 'inline' должны быть объявлены с квалификатором 'static'.
Если inline-функция объявлена в некоторой единице трансляции и имеет 'external linkage', но не определена в этой единице трансляции, то это приведет к неопределенному поведению.
Проблема может возникнуть, даже если 'inline'-функция, имеющая 'external linkage', объявлена и вызывается в одной единице трансляции. Такой вызов является неуточненным поведением. Компилятор может как сгенерировать вызов функции, так и встроить тело этой функции на место вызова. Несмотря на то, что это не повлияет на поведение вызванной функции, это может повлиять на время выполнения программы, и, как следствие, повлиять на программу во время ее исполнения.
Пример кода, на который анализатор выдаст предупреждение:
#include <stdint.h>
extern inline int64_t sum(int64_t lhs, int64_t rhs);
extern inline int64_t sum(int64_t lhs, int64_t rhs)
{
return lhs + rhs;
};
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Функция или объект, объявленный однажды с внутренним типом связывания, при повторном объявлении или определении будет также иметь внутреннее связывание. Это может быть неочевидно для разработчика, и поэтому следует явно указывать спецификатор 'static' в каждом объявлении и определении.
Для С++ это правило распространяется только на функции.
Следующий код не соответствует правилу, так как определение не отражает внутренний тип связывания, заданный в объявлении функции 'foo' с помощью ключевого слова 'static':
static void foo(int x); //in header.h
void foo(int x) //in source.cpp
{
....
}
Код в соответствии с правилом должен быть следующим:
static void foo(int x); //in header.h
static void foo(int x) //in source.cpp
{
....
}
В примере, приведенном ниже, определение функции 'foo' со спецификатором класса хранения 'extern' не задает внешний тип связывания, как могло показаться. Тип связывания остается внутренним:
static void foo(int x); //in header.h
extern void foo(int x) //in source.cpp
{
....
}
Такой код разрешен стандартом языка С, но в данном случае вводит в заблуждение разработчика. Правильный вариант с точки зрения MISRA:
extern void foo(int x); //in header.h
extern void foo(int x) //in source.cpp
{
....
}
Подобный пример с глобальной переменной, нарушающий правило MISRA C:
static short y; //in header.h
extern short y = 10; //in source.c
Переменная 'y' будет иметь внутренний тип связывания. Это может быть неочевидным для разработчика. Допустимым вариантом будет:
static short y; //in header.h
static short y = 10; //in source.c
или
extern short y; //in header.h
extern short y = 10; //in source.c
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Если в программе возникает неопределённое поведение, то у программиста нет никаких гарантий относительно её исполнения. Такое поведение недопустимо.
Если в программе возникает критическое неуточнённое поведение, это означает, что в зависимости от компилятора и его конфигурации возможно различное исполнение кода. Такое поведение также недопустимо.
В общем случае нельзя сказать, может ли в программе произойти неопределённое или неуточнённое поведение или нет. Не все случаи неопределённого поведения можно распознать. Поэтому нет и не может быть алгоритма, который гарантировал бы отсутствие неопределённого или неуточнённого поведения в конкретной программе.
Однако существует множество ситуаций, которые могут привести к неопределённому или критическому неуточнённому поведению и которые можно распознать алгоритмически. Рассмотрим некоторые из таких случаев.
Часто можно определить потенциальное разыменование нулевого указателя. Рассмотрим следующий фрагмент кода:
void foo()
{
int len = GetLen();
char *str = (char *) malloc(mlen + 1);
str[len] = '\0';
}
При выполнении этого фрагмента кода может произойти разыменование нулевого указателя. Если функция 'malloc' не сможет выделить память, то она запишет 'nullptr' в переменную 'str'. Тогда в выражении 'str[len]' произойдет разыменование 'nullptr', что является неопределенным поведением. Анализатор выдаст следующее предупреждение:
V2609 Undefined behaviour should not occur. There might be dereferencing of a potential null pointer 'str'. Check lines: 4, 5.
На первый взгляд может показаться, что подобные ошибки сразу приведут к аварийному завершению программы. Во многих операционных системах первые килобайты адресного пространства защищены от записи. И при попытке записи в них возникнет исключение/сигнал. А значит, ошибка не является критичной. Это совсем не так.
Более подробно все это рассмотрено в статье "Почему важно проверять, что вернула функция malloc".
Еще одной ситуацией, которую можно распознать алгоритмически, является модификация переменной между двумя точками следования и неоднократное обращение к ней.
Рассмотрим следующий фрагмент кода:
void foo()
{
int *ptr;
....
*ptr++ = *(ptr + 1);
}
Здесь предполагается, что сначала произойдет увеличение указателя 'ptr' на 1. Затем произойдет вычисление выражения 'ptr + 1', и использоваться будет уже новое значение 'ptr'.
Однако такое предположение программиста неверно. Дело в том, что между двумя точками следования происходит два обращения к переменной 'ptr', одно из которых модифицирует ее значение. В таком случае поведение программы неопределенное.
Анализатор выдаст следующее предупреждение:
V2609 Undefined behaviour should not occur. The 'bufl' variable is modified while being used twice between sequence points.
Также можно определить некорректное использование битовых операторов сдвига. Рассмотрим следующий код:
void foo()
{
int delta = -2;
....
int expr = DoSomeCalculations();
expr <<= delta;
}
Здесь переменная 'expr' сдвигается влево на -2 бита. Сдвиг на отрицательное число битов является некорректной операцией и приводит к неопределенному поведению.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Использование некоторых символов в именах заголовочных файлов может привести к неопределённому поведению.
Правило запрещает использование следующих наборов символов:
Пример:
#include <bad"include.h>
#include "bad'include.h"
Чаще всего диагностика выявляет наличие в пути обратной косой черты (backslash). С точки зрения MISRA, такой код является некорректным:
#include "myLibrary\header.h"
Однако допустимо использовать прямую косую черту (forward slash):
#include "myLibrary/header.h"
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Преобразование с использованием указателя на неполный тип может привести к получению неверно выравненного указателя, что может повлечь за собой неопределённое поведение. Такая же ситуация происходит при преобразовании между указателями на неполный тип и числами с плавающей точкой.
Также указатели на неполный тип иногда используются для сокрытия реализации (идиома PIMPL), а приведение к указателю на объект нарушает эту инкапсуляцию.
Рассмотрим пример:
typedef struct _First First;
typedef struct _Second
{
int someVar;
} Second;
void foo(void)
{
First *f;
Second t;
...
f = &t; // <=
...
}
Second* bar(First *ptr)
{
return (Second*)ptr; // <=
}
В приведённом выше коде объявляются структуры 'First' и 'Second'. При этом тип 'First' является не полным, т. к. отсутствует его определение. Далее в функции 'foo' происходит неявное приведение указателя к неполному типу, а в функции 'bar' – явное, но уже из неполного типа в полный. Оба эти случая могут привести к неопределённому поведению.
При этом у правила есть два исключения:
Оба этих случая можно рассмотреть в функции 'baz':
typedef struct _First First;
First* foo(void);
void baz(void)
{
First *f = NULL;
(void)foo();
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Язык C имеет специальный синтаксис инициализирующих выражений, называемый designated initializer. Он позволяет инициализировать элементы массива или структуры в произвольном порядке.
Например, можно инициализировать конкретные элементы массива:
int arr[4] = {
[1] = 1,
[3] = 2,
};
Этот синтаксис работает и для структур:
struct point
{
int x;
int y;
};
struct point pt1 = {
.x = 1,
.y = 1,
};
Однако при его использовании можно допустить ошибку и инициализировать один и тот же элемент дважды:
int arr[4] = {
[3] = 1,
[3] = 2,
};
struct point pt1 = {
.x = 1,
.x = 1,
};
MISRA запрещает данную конструкцию, т.к. стандарт языка не определяет, должны ли проявляться побочные эффекты в данной ситуации или нет. Скорее всего, такая ситуация возникла из-за опечатки.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Анализатор обнаружил ситуацию, при которой составное выражение, участвующее в арифметической операции, имеет более узкий сущностный тип, чем другой операнд этой операции. Вычисление такого составного выражения может привести к переполнению.
Рассмотрим следующий синтетический пример:
uint16_t w1;
uint16_t w2;
uint32_t dw1;
// ....
return w1 * w2 + dw1;
Несмотря на то, что на типичных платформах (x86/ARM) тип 'uint16_t' соответствует типу 'unsigned short' и при вычислении выражения он расширится до типа 'int', на других платформах (например, 16-битных микроконтроллерах) 'uint16_t' может соответствовать типу 'unsigned int', поэтому расширения до 32 бит не произойдет, из-за чего в умножении возможно переполнение.
Диагностика способна определить такую ситуацию за счет модели сущностных типов (Essential Type Model) – она определяет тип выражения таким образом, как если бы расширения до 'int' (integer promotion) не происходило. В этой модели переменная может иметь тип:
Чтобы исправить ситуацию, нужно привести один из операндов составного выражения к результирующему типу. Например:
return (uint32_t)w1 * w2 + dw1;
Тогда вычисление всего выражения будет происходить в более широком типе 'uint32_t'.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Идентификаторы с внешним типом связывания должны быть различимы в пределах ограничений, накладываемых используемым стандартом.
Ограничения:
Рассмотрим первый пример:
// 123456789012345678901234567890123
extern int shrtfn(void); // OK
extern int longfuncname(void); // Error in С90,
// but ОК in С99
extern int longlonglonglonglongfunctionname1(void); // Error in both
Длинные идентификаторы затрудняют чтение кода, а также могут быть перепутаны с автоматически сгенерированными. Также, в случае если два идентификатора различаются только в незначащих символах, то поведение не определено.
На практике некоторые имплементации (компиляторов и компоновщиков) могут иметь собственные ограничения. Для их уточнения стоит обратиться к документации.
Рассмотрим второй пример:
// 123456789012345678901234567890123
extern int longFuncName1(int);
extern int longFuncName2(int);
extern int AAA;
extern int aaa;
void foo(void)
{
longFuncName2(AAA);
}
В данном коде присутствует сразу несколько ошибок (рассмотрим на примере стандарта C90):
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Не рекомендуется определять объекты и/или функции с внешним связыванием (external linkage) без предварительной декларации.
Смысл этого правила в том, чтобы избегать "ручного" использования внешних сущностей в местах, где они нужны. Декларации внешних объектов и функций стоит делать в заголовочном файле.
Например, следующий код определяет внешнюю переменную и функцию без предварительной декларации:
int foo;
void bar(void) {
// ....
}
Декларации стоит поместить в заголовочный файл. Также стоит включить заголовочный файл в компилируемый:
// file.h
extern int foo;
extern void bar(void);
// file.c
#include "file.h"
int foo;
void bar(void) {
// ....
}
Для объектов и функций с внутренним связыванием (internal linkage) предварительная декларация не нужна:
static int baz;
static void qux(void) {
// ....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Директивы условной компиляции '#else', '#elif' и '#endif' должны находиться в том же файле, что и '#if', '#ifdef' или '#ifndef', к которым они относятся. Несоответствие этому правилу приводит к визуальному усложнению кода и увеличивает вероятность совершить ошибку при редактировании и обслуживании кода.
Замечание: при использовании современных компиляторов допустить данную ошибку невозможно. Неправильное использование директив условной компиляции в них приводит к ошибкам сборки программы.
Рассмотрим примеры:
#define Check_A 10
#ifdef Check_A // <=
#if Check_A > 5
static int a = 5;
#elif Check_A > 2
static int a = 2;
#else
static int a = 0;
#endif // <=
int main(void)
{
return a;
}
В первом примере используется вложенное условие, состоящее из '#ifdef' и '#if', но в конце, закрывается только вторая директива условной компиляции ('#if'), а первая ('#ifdef') остаётся открытой, что может привести к созданию некорректного кода.
Рассмотрим другой пример:
/* File.h */
#ifdef Check_B
#include "SomeOtherFile.h" // <=
/* End of File.h */
В данном примере не закрывается директива условной компиляции. Включение данного файла в другие с помощью директивы препроцессора '#include' может привести к трудноуловимым ошибкам.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Поведение не определено, когда создаются два объекта, которые частично перекрываются в памяти, и один из них присваивается или копируется в другой.
Такое может случиться, например, при использовании функции 'memcpy', когда перекрываются области памяти источника и приемника:
void func(int *x)
{
memcpy(x, x+2, 10 * sizeof(int));
}
В данном случае указатель на источник данных '(x+2)' смещен относительно приемника данных на 8 байт ('sizeof(int) * 2'). Попытка копирования 40 байт в приемник из источника приведет к частичному перекрытию области памяти источника.
Для исправления ошибки необходимо использовать специальную функцию для таких операций – 'memmove' – или скорректировать заданные смещения источника и приемника, чтобы не происходило перекрытия областей памяти.
Пример корректного кода:
void func(int *x)
{
memmove(x, x+2, 10 * sizeof(int));
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Объявление двух идентификаторов в одной области видимости, различающихся только незначимыми символами, приводит к неопределённому поведению. Помимо этого, длинные идентификаторы затрудняют чтение кода, а также могут быть перепутаны с автоматически сгенерированными.
До стандарта С99 значимыми являются только первые 31 символ. Начиная со стандарта С99 – первые 63 символа. Остальные символы считаются незначимыми.
Данное правило не применяется, если оба идентификатора имеют external linkage, так как для этого случая предназначена диагностика V2614.
Данное правило не применяется, если один из идентификаторов - макрос.
Рассмотрим пример для С90:
// 1234567890123456789012345678901***
static int very_long_long_long_long__test_var1; // (1)
extern int very_long_long_long_long__test_var2; // (2)
Идентификаторы 1 и 2 различаются только незначимыми символами 'var1' и 'var2', в то время как значимая часть 'long_long_long_long_long__test_' совпадает. Для того, чтобы избежать неопределенного поведения, необходимо сократить длину идентификатора:
// 1234567890123456789012345678901***
static int not_very_long__test_var1;
extern int not_very_long__test_var2;
Рассмотрим второй пример:
// 1234567890123456789012345678901***
static int long_long_long_long_long__test_var3; // (3)
void foo()
{
// 1234567890123456789012345678901***
int long_long_long_long_long__test_var4; // (4)
}
Здесь идентификаторы 3 и 4 также различаются только незначимыми символами, однако они находятся в разных областях видимости, поэтому нарушения правила здесь нет.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Имена 'typedef' должны быть уникальны для всех пространств имен. Повторное использование имён 'typedef' может запутать разработчика.
Рассмотрим следующий пример:
void foo()
{
{
typedef unsigned char Id;
Id x = 128; // ok
}
{
typedef char Id;
Id x = 128; // error
}
}
При работе с типом 'unsigned char' допускается диапазон значений от 0 до 255, а с типом 'signed char' от -128 до 127. Поработав со вторым 'typedef', программист может забыть, что тип поменялся. Это может привести к ошибке.
Рассмотрим второй пример:
void foo()
{
{
typedef unsigned char uId;
uId x = 128; // ok
}
{
typedef singned char sId;
sId x = 128; // ok
}
}
В этом случае сложнее допустить ошибку, так как имена 'typedef' различаются.
В качестве исключения допускается дублировать имя 'typedef' при объявлении 'struct', 'union' или 'enum', если они ассоциированы с этим 'typedef'.
typedef struct list
{
struct list* next;
int element;
} list; // ok
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Приведение результата составного выражения к сущностному типу другой категории или к более широкому типу может стать причиной потери значений старших битов.
Рассмотрим пример:
int32_t foo(int16_t x, int16_t y)
{
return (int32_t)(x * y);
}
Несмотря на то, что на типичных платформах (x86/ARM) тип 'int16_t' соответствует типу 'short', и при вычислении выражения он расширится до типа 'int', на других платформах (например, 16-битных микроконтроллерах) 'int16_t' может соответствовать типу 'int', поэтому расширения до 32 бит не произойдет, из-за чего в умножении возможно переполнение.
Исправленным вариантом может быть:
int32_t foo(int16_t x, int16_t y)
{
return (int32_t)x * y;
}
В этом случае вычисление всего выражения будет происходить в более широком типе 'int32_t'.
Рассмотрим другой пример:
int32_t sum(float x, float y)
{
return (int32_t)(x + y);
}
Согласно модели сущностных типов, результирующий тип выражения относится к floating категории, а тип 'int32_t' к signed категории сущностных типов. Приведение результата сложения к целому типу ведет к потери точности. Результат сложения двух чисел типа 'float' также может быть больше, чем верхний предел диапазона типа 'int32_t'.
Допустимым вариантом может быть:
float sum(float x, float y)
{
return x + y;
}
Если в дальнейшем требуется привести результат выражения к типу 'int', то необходимо выполнить:
В модели сущностных типов (Essential Type Model) определено 6 категорий:
Тип составного выражения в этой модели определяется таким образом, как если бы расширения до 'int' (integer promotion) не происходило.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. Имена структур, перечислений и объединений должны быть уникальны для всех пространств имён и блоков. Повторное использование имён тегов может запутать разработчика.
Рассмотрим следующий пример:
int foo()
{
{
struct MyStuct
{
unsigned char data; // (1)
};
struct MyStuct sample = { .data = 250 }; // ok
}
// ....
{
struct MyStruct
{
signed char data; // (2)
};
struct MyStruct sample = { .data = 250 }; // error
}
}
При работе с типом 'unsigned char' допускается диапазон значений от 0 до 255, а с типом 'signed char' от -128 до 127. Поработав с первой структурой 'MyStruct', программист может привыкнуть, что член структуры 'data' имеет тип 'unsigned char'. После чего, во втором блоке легко совершить ошибку, записав 'sample.data' значение, которое приведет к переполнению.
Исправленный пример:
int foo()
{
{
struct MyStuctUnsigned
{
unsigned char data; // (1)
};
struct MyStuctUnsigned sample = { .data = 250 }; // ok
}
// ....
{
struct MyStructSigned
{
signed char data; // (2)
};
struct MyStructSigned sample = { .data = 127 }; // ok
}
}
В этом случае сложнее допустить ошибку, так как имена структур различаются.
Псевдоним типа, объявляемый через 'typedef', может дублировать имя при объявлении 'struct', 'union' или 'enum', если они ассоциированы с этим 'typedef':
typedef struct list
{
struct list* next;
int element;
} list; // ok
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для С. У любого объекта или функции с внешним связыванием должно быть ровно одно объявление.
Рассмотрим пример нарушения этого правила:
/* lib1.h */
extern int32_t var; // Declaration
/* lib2.h */
extern int32_t var; // Declaration
/* some.cpp */
#include "lib1.h"
#include "lib2.h"
В данном примере переменная 'var' объявлена дважды: в 'lib1.h' и 'lib2.h'.
Здесь возможно несколько вариантов исправления:
/* lib.h */
extern int32_t var; // Declaration
/* lib1.h */
#include "lib.h"
/* lib2.h */
#include "lib.h"
/* some.cpp */
#include "lib1.h"
#include "lib2.h"
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Данное диагностическое правило актуально только для C. Имя макроса должно быть различимо от имён уже ранее определённых макросов. Также имя параметра макроса должно быть различимо как от имени самого макроса, так и от всех других параметров.
Минимальное требование по различимости имён макросов и их параметров зависит от версии стандарта:
В реальности компиляторы дают большие лимиты. Но правило требует, чтобы имена макросов были различимы в рамках ограничений, налагаемых стандартом.
Нижеперечисленные примеры будут касаться C90.
Пример некорректного кода с именем макроса:
// 1234567890123456789012345678901
#define average_winter_air_temperature_monday awt_m
#define average_winter_air_temperature_tuesday awt_t
Имя первого макроса неотличимо от второго, если брать первые 31 символ. Корректный код:
// 1234567890123456789012345678901
#define average_winter_air_temp_monday awt_m
#define average_winter_air_temp_tuesday awt_t
Пример некорректного кода с неразличимыми именем макроса и его параметрами:
#define average_winter_air_temp(average_winter_air_temp) awt_m
Корректный код:
#define average_winter_air_temp(winter_air_temp) awt_m
Пример некорректного кода с неразличимыми именами параметров макроса:
#define air_temp(winter_air_temp, winter_air_temp) awt_m
Корректный код:
#define air_temp(average_winter_air_temp, winter_air_temp) awt_m
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Оно требует, чтобы инициализаторы для агрегатов и объединений оборачивались в фигурные скобки. Правило применимо как для объектов, так и для всех подобъектов.
Исключения:
Рассмотрим пример:
int num[3][2] = { 1, 2, 3, 4, 5, 6 };
В данном коде представлены инициализаторы массива, которые не оборачиваются в фигурные скобки. Корректный код:
int num[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
Рассмотрим ещё один пример:
struct S1
{
char buf;
int num[3][2];
} s1[4] = {
'a', { 1, 2 }, { 0, 0 }, { 5, 6 },
'b', { 1, 2 }, { 0, 0 }, { 5, 6 },
'c', { 1, 2 }, { 0, 0 }, { 5, 6 },
'd', { 1, 2 }, { 0, 0 }, { 5, 6 }
};
Правило требует, чтобы инициализаторы агрегатов также оборачивались в фигурные скобки. Корректный код будет выглядеть так:
struct S1
{
char buf;
int num [3][2];
} s1[4] = {
{ 'a', { { 1, 2 }, { 0, 0 }, { 5, 6 } } },
{ 'b', { { 1, 2 }, { 0, 0 }, { 5, 6 } } },
{ 'c', { { 1, 2 }, { 0, 0 }, { 5, 6 } } },
{ 'd', { { 1, 2 }, { 0, 0 }, { 5, 6 } } }
};
Рассмотрим вот такой случай:
struct S2 {
char color[8];
int num;
} s2[3] = {
{ "Red", 1 },
{ "Green", 2 },
{ "Blue", 3 }
};
В данном примере символьный литерал также должен оборачиваться в фигурные скобки. Корректный код:
struct S2 {
char color[8];
int num;
} s2[3] = {
{ { "Red" }, 1 },
{ { "Green" }, 2 },
{ { "Blue" }, 3 }
};
Пример кода с назначенными (designated) инициализаторами:
int num[2][2] = { [0][1] = 0, { 0, 1 } };
Примеры кода с инициализатором в виде '{ 0 }':
int num1[3][2] = { 0 };
int num2[3][2] = { { 1, 2 }, { 0 }, { 5, 6 } };
int num3[2][2] = { { 0 }, [1][1] = 1 };
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве MISRA (Motor Industry Software Reliability Association) по разработке программного обеспечения.
Это правило актуально только для C. Идентификатор, имеющий внешний тип связывания, должен быть уникальным в программе. Это имя не должно быть переиспользовано другими идентификаторами, имеющими отличный от исходного тип связывания (нет связывания, внутреннее связывание) внутри функций или других юнитах трансляции. Правило применимо как для объектов, так и для функций.
Примечание. Для поиска неуникальных идентификаторов в разных юнитах трансляции требуется включить режим межмодульного анализа.
Рассмотрим пример:
int var; // external linkage
void foo()
{
short var; // no linkage
}
В данном коде идентификатор 'var' с внешним типом связывания будет скрыт локальной переменной в функции 'foo'. Корректный код будет выглядеть так:
int var; // external linkage
void foo()
{
short temp; // no linkage
}
Рассмотрим ещё один пример, но на основе содержимого двух файлов одного проекта:
// file1.c
int x; // external linkage
static int y; // internal linkage
static void bar(); // internal linkage
// file2.c
void bar() // external linkage
{
int y; // no linkage
}
void foo() // external linkage
{
int x; // no linkage
}
Идентификаторы 'x' из 'file1.c' и 'bar' из 'file2.c' имеют внешний тип связывания и не являются уникальными, поэтому нарушают данное правило. Идентификатор 'y' не является уникальным, но поскольку в 'file1.c' он имеет внутреннее связывание, а в 'file2.c' не имеет связывания, то данное правило не нарушается для этого имени.
Корректный код:
// file1.c
static int x; // internal linkage
static int y; // internal linkage
static void func(); // internal linkage
// file2.c
void bar() // external linkage
{
int y; // no linkage
}
void foo() // external linkage
{
int x; // no linkage
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит логическую ошибку. В тексте программы имеется оператор (<, >, <=, >=, ==, !=, &&, ||, -, /, &, |, ^), слева и справа от которого расположены одинаковые подвыражения.
Рассмотрим пример:
if (a.x != 0 && a.x != 0)
В данном случае оператор '&&' окружен одинаковыми подвыражениями "a.x != 0", что позволяет обнаружить ошибку, допущенную по невнимательности. Корректный код, который не вызовет подозрений у анализатора, будет выгладить так:
if (a.x != 0 && a.y != 0)
Рассмотрим другой пример ошибки, обнаруженный анализатором в коде приложения:
class Foo {
List<int> Childs { get; set; }
...
public bool hasChilds() { return(Childs[0] > 0 || Childs[0] > 0); }
...
}
В данном случае, хотя код успешно и без предупреждений компилируется, он не имеет смысла. Корректный код должен был выглядеть следующим образом:
public bool hasChilds(){ return(Childs[0] > 0 || Childs[1] > 0);}
Анализатор производит сравнение блоков с учетом перестановки частей выражения относительно операторов. Ошибка будет обнаружена и в следующем примере кода:
if (Name.Length > maxLength && maxLength < Name.Length)
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3001. |
Анализатор обнаружил оператор 'switch', в котором выбор варианта осуществляется по переменной enum-типа. При этом в операторе 'switch' используются не все элементы перечисления, что может свидетельствовать о наличии ошибки.
Пример кода, в котором встретилась эта ошибка:
public enum Actions { Add, Remove, Replace, Move, Reset };
public void SomeMethod(Actions act)
{
switch (act)
{
case Actions.Add: Calculate(1); break;
case Actions.Remove: Calculate(2); break;
case Actions.Replace: Calculate(3); break;
case Actions.Move: Calculate(5); break;
}
}
В данном случае перечисление 'Actions' содержит 5 именованных констант, а оператор 'switch', выбор в котором осуществляется по данному перечислению, реализует выбор только по 4 из них. Это место является потенциально ошибочным.
Возможно, в ходе рефакторинга в перечисление добавили новую константу, но забыли реализовать выбор по ней в операторе 'switch', или же, что может быть в случае с большими перечислениями, константу банально могли пропустить. В итоге пропущенное значение обрабатывается неправильно.
Тогда корректный код мог бы выглядеть следующим образом:
public void SomeMethod(Actions act)
{
switch (act)
{
case Actions.Add: Calculate(1); break;
case Actions.Remove: Calculate(2); break;
case Actions.Replace: Calculate(3); break;
case Actions.Move: Calculate(5); break;
case Actions.Reset: Calculate(6); break;
}
}
Или так:
public void SomeMethod(Actions act)
{
switch (act)
{
case Actions.Add: Calculate(1); break;
case Actions.Remove: Calculate(2); break;
case Actions.Replace: Calculate(3); break;
case Actions.Move: Calculate(5); break;
default: Calculate(10); break;
}
}
Анализатор выдает предупреждение далеко не всегда, когда в 'switch' используется не все константы из перечисления. Иначе, было бы слишком много ложных срабатываний. Действует целый ряд исключений эмпирического типа. Основные:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3002. |
Анализатор обнаружил потенциально возможную ошибку в конструкции, состоящей из условных операторов.
Рассмотрим пример:
if (a == 1)
Foo1();
else if (a == 2)
Foo2();
else if (a == 1)
Foo3();
В данном примере метод 'Foo3()' никогда не получит управления. Вероятно, мы имеем дело с логической ошибкой и корректный код должен выглядеть так:
if (a == 1)
Foo1();
else if (a == 2)
Foo2();
else if (a == 3)
Foo3();
На практике подобная ошибка может выглядеть следующим более сложным образом.
Например, анализатором удалось найти следующую ошибочную конструкцию.
....
} else if (b.NodeType == ExpressionType.Or ||
b.NodeType == ExpressionType.OrEqual){
current.Condition = ConstraintType.Or;
} else if(...) {
....
} else if (b.NodeType == ExpressionType.OrEqual ||
b.NodeType == ExpressionType.Or){
current.Condition = ConstraintType.Or |
ConstraintType.Equal;
} else if(....
В данном примере, в вышестоящей инструкции if было проверено условие, что
b.NodeType == ExpressionType.Or ||
b.NodeType == ExpressionType.OrEqual.
А в нижестоящей инструкции if, условие было таким же по логике, но написано в обратном порядке, что является малозаметным с точки зрения человека, но приводит к ошибке выполнения кода.
b.NodeType == ExpressionType.OrEqual ||
b.NodeType == ExpressionType.Or
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3003. |
Анализатор обнаружил подозрительный фрагмент кода, в котором истинная и ложная ветви оператора 'if' полностью совпадают. Часто это свидетельствует о наличии ошибки.
Пример подобного кода:
if (condition)
result = FirstFunc(val);
else
result = FirstFunc(val);
Вне зависимости от значения переменной, будут выполнены одни и те же операции. Понятно, что такой код является ошибочным. Тогда корректный вариант кода мог бы выглядеть следующим образом:
if (condition)
result = FirstFunc(val);
else
result = SecondFunc(val);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3004. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что значение переменной присваивается само себе.
Рассмотрим пример, взятый из реального приложения:
public GridAnswerData(
int questionId, int answerId, int sectionNumber,
string fieldText, AnswerTypeMode typeMode)
{
this.QuestionId = this.QuestionId;
this.AnswerId = answerId;
this.FieldText = fieldText;
this.TypeMode = typeMode;
this.SectionNumber = sectionNumber;
}
Из кода видно, что программист хотел изменить значения свойств объекта в соответствии с принятыми в методе параметрами, но ошибся и присвоил свойству 'QuestionId' вместо значения аргумента 'questionId' значение самого же свойства.
Тогда корректный код должен был бы выглядеть так:
public GridAnswerData(
int questionId, int answerId, int sectionNumber,
string fieldText, AnswerTypeMode typeMode)
{
this.QuestionId = questionId;
this.AnswerId = answerId;
this.FieldText = fieldText;
this.TypeMode = typeMode;
this.SectionNumber = sectionNumber;
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3005. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что создаётся экземпляр класса, унаследованного от 'System.Exception', но при этом никак не используется.
Пример ошибочного кода:
public void DoSomething(int index)
{
if (index < 0)
new ArgumentOutOfRangeException();
else
....
}
В данном коде пропущен оператор 'throw', из-за чего будет только создан экземпляр класса, унаследованного от 'System.Exception', но при этом он никак не будет использоваться, и исключение не будет сгенерировано. Корректный код может выглядеть следующим образом:
public void DoSomething(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException();
else
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3006. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что после одного из операторов 'for', 'while' или 'if' стоит точка с запятой ';'.
Рассмотрим пример:
int i = 0;
....
for(i = 0; i < arr.Count(); ++i);
arr[i] = i;
В данном коде программист хотел выполнить присваивание для всех элементов массива, но по ошибке поставил ';' после закрывающей скобки цикла. Таким образом, операция присваивания выполнится только один раз. К тому же произойдёт выход за границу массива.
Корректный вариант кода:
int i = 0;
....
for(i = 0; i < arr.Count(); ++i)
arr[i] = i;
Конечно, наличие точки с запятой ';' после данных операторов не всегда является ошибкой, и иногда для выполнения необходимых операций не требуется тела цикла. В таком случае наличие точки с запятой оправдано. Пример такого кода:
int i;
for (i = 0; !char.IsWhiteSpace(str[i]); ++i) ;
Console.WriteLine(i);
Анализатор не будет выдавать предупреждение на этот код и в ряде других ситуаций.
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что одной и той же переменной дважды подряд присваивается значение. Причем между этими присваиваниями сама переменная не используется.
Рассмотрим пример:
A = GetA();
A = GetB();
То, что переменной 'A' два раза присваивается значение, может свидетельствовать о наличии ошибки. Высока вероятность, что код должен выглядеть следующим образом:
A = GetA();
B = GetB();
Если переменная между присваиваниями используется, то этот код считается анализатором корректным:
A = 1;
A = Foo(A);
Рассмотрим, как подобная ошибка может выглядеть на практике. Следующий код взят из реального приложения:
....
if (bool.TryParse(setting, out value))
_singleSignOn = value;
_singleSignOn = false;
....
Корректный вариант должен был выглядеть так:
....
if (bool.TryParse(setting, out value))
_singleSignOn = value;
else
_singleSignOn = false;
....
Иногда анализатор выдает ложные предупреждения, когда запись в переменные используется для отладочных целей. Пример подобного кода:
status = Foo1();
status = Foo2();
В данной ситуации:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3008. |
Анализатор обнаружил странный метод. Метод не имеет состояния, не изменяет глобальных переменных. При этом он имеет несколько точек возврата, возвращающих одно и то же числовое, строковое, константное, enum значение, или то же самое значение поля, которое предназначено только для чтения.
Такой код крайне подозрителен и может свидетельствовать о возможной ошибке. Скорее всего, метод должна возвращать различные числовые значения.
Рассмотрим простой пример такого кода:
int Foo(int a)
{
if (a == 33)
return 1;
return 1;
}
Данный код содержит ошибку. Для ее исправления изменим одно из возвращаемых значений. Определить необходимые возвращаемые значения, как правило, можно только зная логику работы всего приложения в целом.
Вариант корректного кода:
int Foo(int a)
{
if (a == 33)
return 1;
return 2;
}
Если код верен, то чтобы избавиться от ложного срабатывания следует использовать комментарий "//-V3009".
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3009. |
Анализатор обнаружил подозрительный вызов метода, возвращаемое значение которого не учитывается. Вызов некоторых методов не имеет смысла без использования их возвращаемого значения.
Рассмотрим пример такого кода:
public List<CodeCoverageSequencePoint> SequencePoints
{ get; private set; }
....
this.SequencePoints.OrderBy(item => item.Line);
Из кода видно, что для коллекции 'SequencePoints' вызывается метод-расширение 'OrderBy'. Данный метод выполняет сортировку коллекции по заданному критерию и возвращает отсортированную коллекцию. Так как метод 'OrderBy' не меняет коллекцию 'SequencePoints', то нет смысла вызывать его, не сохраняя при этом коллекцию, возвращённую методом.
Тогда корректный код мог бы выглядеть так:
var orderedList = this.SequencePoints.OrderBy(
item => item.Line).ToList();
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3010. |
Анализатор обнаружил возможную логическую ошибку, связанную с тем, что два условных оператора, идущих последовательно, содержат взаимоисключающие условия.
Примеры таких условий:
Такая ошибка может возникнуть в результате опечатки или неудачного рефакторинга.
Пример некорректного кода:
if (x == y)
if (y != x)
DoSomething(x, y);
В данном случае метод 'DoSomething' никогда не будет вызван, так как при истинности первого условия второе всегда будет ложным. Возможно, в сравнении используется некорректная переменная. Например, во втором условии следовало использовать не 'x', а 'z':
if (x == y)
if (y != z)
DoSomething(x, y);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3011. |
Анализатор обнаружил потенциальную ошибку при использовании тернарного оператора "?:". Независимо от условия, будет выполнено одно и тоже действие. Скорее всего, в коде имеется опечатка.
Рассмотрим самый простой пример:
int A = B ? C : C;
В любом случае переменной A будет присвоено значение переменной C.
Рассмотрим, как подобная ошибка может выглядеть в коде реального приложения:
fovRadius[0] = Math.Tan((rollAngleClamped % 2 == 0 ?
cg.fov_x : cg.fov_x) * 0.52) * sdist;
Здесь код отформатирован. В тексте программы это может быть одной строкой, и неудивительно, что легко просмотреть опечатку. Ошибка в том, что два раза используется член класса "fov_x". Корректный вариант:
fovRadius[0] = Math.Tan((rollAngleClamped % 2 == 0 ?
cg.fov_x : cg.fov_y) * 0.52) * sdist;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3012. |
Данное предупреждение выдается в том случае, если анализатор обнаружил две функции, реализованные идентичным образом. Наличие двух одинаковых функций само по себе не является ошибкой, но является поводом обратить на них внимание.
Смысл данной диагностики в обнаружении следующей разновидности ошибок:
class Point
{
....
float GetX() { return m_x; }
float GetY() { return m_x; }
};
Из-за допущенной опечатки две разные по смыслу функции выполняют одинаковые действия. Корректный вариант:
float GetX() { return m_x; }
float GetY() { return m_y; }
В приведенном примере идентичность тел функций GetX() и GetY() явно свидетельствует о наличии ошибки. Однако если выдавать предупреждения на все одинаковые функции, то процент ложный срабатываний будет крайне большим. Поэтому анализатор руководствуется целым рядом исключений, когда не стоит предупреждать об одинаковых телах функций. Перечислим некоторые из них:
Бороться с ложными срабатываниями можно несколькими способами. Если ложные срабатывания относятся к файлам внешних библиотек или тестов, то путь до файлов или каталога с ними можно добавить в исключения. Если предупреждения относятся к вашему коду, то вы можете использовать комментарий вида "//-V3013", который приведет к подавлению предупреждений. Если ложных срабатываний много, то вы можете в настройках анализатора полностью отключить использование данной проверки. Также вы можете модифицировать код таким образом, чтобы одна функция вызывала другую.
Приведем пример кода из реального приложения, где разные по смыслу функции реализованы одинаково:
public void Pause(FrameworkElement target)
{
if (Storyboard != null)
{
Storyboard.Pause(target);
}
}
public void Stop(FrameworkElement target)
{
if (Storyboard != null)
{
Storyboard.Stop(target);
}
}
public void Resume(FrameworkElement target)
{
if (Storyboard != null)
{
Storyboard.Pause(target);
}
}
Сделав несколько копий одной функции, забыли исправить последнюю функцию Resume().
Исправленный фрагмент кода:
public void Resume(FrameworkElement target)
{
if (Storyboard != null)
{
Storyboard.Resume(target);
}
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3013. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' увеличивается переменная, относящаяся к внешнему циклу.
В самом простом виде эта ошибка выглядит следующим образом:
for (int i = 0; i < 5; i++)
for (int j = 0; j < 5; i++)
A[i][j] = 0;
Во внутреннем цикле происходит увеличение переменной 'i' вместо 'j'. В реальном приложении подобная ошибка может быть не так хорошо заметна. Корректный вариант кода:
for (int i = 0; i < 5; i++)
for (int j = 0; j < 5; j++)
A[i][j] = 0;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3014. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' в условии используется переменная, относящаяся к внешнему циклу.
В самом простом виде эта ошибка выглядит следующим образом:
for (int i = 0; i < 5; i++)
for (int j = 0; i < 5; j++)
A[i][j] = 0;
Во внутреннем цикле происходит сравнение 'i < 5' вместо 'j < 5'. В реальном приложении подобная ошибка может быть не так хорошо заметна. Корректный вариант кода:
for (int i = 0; i < 5; i++)
for (int j = 0; j < 5; j++)
A[i][j] = 0;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3015. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что вложенный цикл организован с использованием переменной, которая также используется и во внешнем цикле.
Схематически эта ошибка выглядит следующим образом:
int i = 0, j = 0;
for (i = 0; i < 5; i++)
for (i = 0; i < 5; i++)
A[i][j] = 0;
Конечно, это искусственный пример и в реальном приложении ошибка может быть не так очевидна. Корректный вариант кода:
int i = 0, j = 0;
for (i = 0; i < 5; i++)
for (j = 0; j < 5; j++)
A[i][j] = 0;
Использование одной переменной для внешнего и внутреннего цикла не всегда является ошибкой. Рассмотрим пример корректного кода, где анализатор не будет выдавать предупреждение:
for(c = lb; c <= ub; c++)
{
if (!(xlb <= xlat(c) && xlat(c) <= ub))
{
Range r = new Range(xlb, xlb + 1);
for (c = lb + 1; c <= ub; c++)
r = DoUnion(r, new Range(xlat(c), xlat(c) + 1));
return r;
}
}
В этом коде внутренний цикл "for (c = lb + 1; c <= ub; c++)" организован при помощи переменной "c". Внешний цикл также использует переменную "c". Но ошибки здесь нет. После того, как выполнится внутренний цикл, сразу произойдет выход из функции при помощи оператора "return r".
Данная диагностика классифицируется как:
Анализатор обнаружил выражение, которое можно упростить. Иногда за такой избыточностью кроются логические ошибки.
Рассмотрим пример подозрительного кода:
bool firstCond, secondCod, thirdCond;
....
if (firstCond || (firstCond && thirdCond))
....
Это выражение является избыточным. В случае, если 'firstCond == true', значение этого условия будет всегда истинным, независимо от значения 'thirdCond', если же 'firstCond == false', то значение выражения будет всегда ложно, опять же, независимо от значения переменной 'thirdCond'.
Возможно, что программист ошибся и написал не ту переменную во втором подвыражении. Тогда корректный код мог бы выглядеть так:
if (firstCond || (secondCod && thirdCond))
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3017. |
Анализатор обнаружил фрагмент кода, в котором оператор 'if' расположен на той же строке, что и закрывающая скобка предыдущего оператора 'if'. Возможно, что в этом месте пропущено ключевое слово 'else', из-за чего программа работает не так, как ожидал программист.
Рассмотрим пример подобного кода:
if (cond1) {
Method1(val);
} if (cond2) {
Method2(val);
} else {
Method3(val);
}
В том случае, если истинно условие 'cond1', будет вызван не только метод 'Method1', но и 'Method2' или 'Method3'. Если подразумевалась именно такая логика, следует исправить форматирование и перенести второй оператор 'if' на следующую строку:
if (cond1) {
Method1(val);
}
if (cond2) {
Method2(val);
} else {
Method3(val);
}
Такой код будет для большинства программистов более привычен и не вызовет подозрения на наличие ошибки. Также анализатор перестанет выдавать лишнее предупреждение.
Если же в такого поведения не подразумевалось и в логике работы программы допущена ошибка, необходимо добавить ключевое слово 'else'. Тогда корректный код будет выглядеть так:
if (cond1) {
Method1(val);
} else if (cond2) {
Method2(val);
} else {
Method3(val);
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3018. |
Анализатор обнаружил потенциальную ошибку, которая может привести к доступу по нулевой ссылке.
Анализатор заметил в коде следующую ситуацию. Сначала объект базового класса приводится к производному классу с помощью оператора 'as'. А затем этот же объект проверяется на значение null, хотя в этом случае скорее всего предполагалось проверить на null объект производного класса.
Рассмотрим следующий пример. Здесь возможна ситуация, когда объект baseObj не будет являться экземпляром класса Derived. В этом случае при вызове функции Func программа упадёт с NullReferenceException. Анализатор выдаст предупреждение на этот код, указав две строки. Первая строка - это то место, где объект базового класса проверяется на null. Вторая строка - это то место, где объект базового класса приводится к объекту производного класса.
Base baseObj;
Derived derivedObj = baseObj as Derived;
if (baseObj != null)
{
derivedObj.Func();
}
Скорее всего в этом примере предполагалось проверить на null объект производного класса перед использованием. Исправленный вариант кода:
Base baseObj;
Derived derivedObj = baseObj as Derived;
if (derivedObj != null)
{
derivedObj.Func();
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3019. |
Анализатор обнаружил подозрительный цикл, в котором используется один из следующих операторов: continue, break, return, goto, throw. Эти операторы выполняются всегда, без каких-либо условий.
Пример ошибочного кода:
while (k < max)
{
if (k == index)
value = Calculate(k);
break;
++k;
}
В данном коде оператор 'break' не принадлежит к оператору 'if', из-за чего выполняться будет вне зависимости от истинности условия 'k == index' и для тела цикла будет выполнена только одна итерация. Тогда корректный код мог бы выглядеть так:
while (k < max)
{
if (k == index)
{
value = Calculate(k);
break;
}
++k;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3020. |
Анализатор обнаружил ситуацию, когда 'then' часть оператора 'if' никогда не получит управления. Это происходит из-за того, что ранее уже встречается оператор 'if' с таким же условием, содержащий в 'then' части безусловный оператор 'return'. Это может свидетельствовать как о логической ошибке в программе, так и избыточном втором операторе 'if'.
Рассмотрим пример некорректного кода:
if (l >= 0x06C0 && l <= 0x06CE) return true;
if (l >= 0x06D0 && l <= 0x06D3) return true;
if (l == 0x06D5) return true; // <=
if (l >= 0x06E5 && l <= 0x06E6) return true;
if (l >= 0x0905 && l <= 0x0939) return true;
if (l == 0x06D5) return true; // <=
if (l >= 0x0958 && l <= 0x0961) return true;
if (l >= 0x0985 && l <= 0x098C) return true;
В данном случае условие 'l == 0x06D5' дублируется и для исправления кода достаточно убрать одно из них. Однако возможно, что во втором случае проверяемое значение должно отличаться от первого случая.
Корректный вариант кода:
if (l >= 0x06C0 && l <= 0x06CE) return true;
if (l >= 0x06D0 && l <= 0x06D3) return true;
if (l == 0x06D5) return true;
if (l >= 0x06E5 && l <= 0x06E6) return true;
if (l >= 0x0905 && l <= 0x0939) return true;
if (l >= 0x0958 && l <= 0x0961) return true;
if (l >= 0x0985 && l <= 0x098C) return true;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3021. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что условие всегда истинно или ложно. Подобные условия не всегда означают наличие ошибки, но эти фрагменты кода следует обязательно проверить.
Пример кода:
string niceUrl = GetUrl();
if (niceUrl != "#" || niceUrl != "") {
Process(niceUrl);
} else {
HandleError();
}
Анализатор выдает предупреждение:
"V3022 Expression 'niceUrl != "#" || niceUrl != ""' is always true. Probably the '&&' operator should be used here. "
Здесь ветка else никогда не будет выполнена. Дело в том, что какое бы значение ни приняла переменная niceUrl, одно из сравнений со строкой всегда будет истинно. Чтобы исправить эту ошибку, следует использовать оператор && вместо оператора ||. Корректный вариант кода:
string niceUrl = GetUrl();
if (niceUrl != "#" && niceUrl != "") {
Process(niceUrl);
} else {
HandleError();
}
Теперь рассмотрим пример кода, в котором имеется бессмысленное сравнение. Возможно этот код не является ошибочным, но на него обязательно следует обратить внимание:
byte type = reader.ReadByte();
if (type < 0)
recordType = RecordType.DocumentEnd;
else
recordType = GetRecordType(type);
Ошибка заключается в сравнении переменной беззнакового типа с нулём. Данный фрагмент кода будет диагностирован так: "V3022 Expression 'type < 0' is always false. Unsigned type value is always >= 0.". В этом примере либо сравнение лишнее, либо случай достижения конца документа обработан неправильно.
Анализатор предупреждает не про все условия, которые всегда ложны или истинны. Он диагностирует только те ситуации, где высока вероятность наличия ошибки. Рассмотрим некоторые примеры, которые анализатор считает абсолютно корректными:
// 1) Временно убранный из компиляции блок кода
if (false && CheckCondition())
{
...
}
// 2) Выражения внутри Debug.Assert()
public enum Actions { None, Start, Stop }
...
Debug.Assert(Actions.Start > 0);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3022. |
Анализатор обнаружил подозрительный фрагмент кода, в котором имеется избыточное сравнение. Этот код может содержать лишнюю проверку, тогда выражение можно упростить, или ошибку, тогда код необходимо исправить.
Рассмотрим пример подобного кода:
if (firstVal == 3 && firstVal != 5)
Данный код избыточен, так как условие будет истинным, если 'firstVal == 3'. Вторая часть выражения бессмысленна.
В данном случае возможны 2 варианта:
1) Вторая проверка просто является избыточной и выражение можно упростить. Тогда корректный код будет выглядеть так:
if (firstVal == 3)
2) Выражение содержит ошибку и вместо переменной 'firstVal' подразумевалась какая-то другая. Тогда корректный код мог бы выглядеть так:
if (firstVal == 3 && secondVal != 5)
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3023. |
Анализатор обнаружил подозрительный фрагмент кода, где для сравнения чисел с плавающей точкой используется оператор '==' или '!='. Такие участки кода могут служить источником ошибок.
Рассмотрим для начала корректный пример кода (на который тем не менее будет выдано предупреждение):
double a = 0.5;
if (a == 0.5) //ok
++x;
В данном случае сравнение можно считать верным. Перед сравнением переменная 'a' явно инициализируется значением '0.5'. С этим же значением производится сравнение. Результатом выражения будет 'истина'.
Итак, в некоторых случаях точные сравнения допустимы. Но часто так сравнивать нельзя. Рассмотрим пример ошибочного кода:
double b = Math.Sin(Math.PI / 6.0);
if (b == 0.5) //err
++x;
Условие 'b == 0.5' при проверке оказалось ложным из-за того, что значение выражения 'Math.Sin(Math.PI / 6.0)' равно 0.49999999999999994. Это число очень близко к '0.5', но ему не равно.
Одним из вариантов решения является сравнение разности значений с каким-то значением (погрешностью, в данном случае - переменная 'epsilon'):
double b = Math.Sin(Math.PI / 6.0);
if (Math.Abs(b - 0.5) < epsilon) //ok
++x;
Необходимо выбирать адекватную погрешность в зависимости от того, какие величины сравниваются.
Анализатор указывает на участки кода, где в сравнении чисел с плавающей точкой используются операторы '!=' или '=='. Является это сравнение ошибочным или нет, может решить только программист.
Дополнительные ресурсы:
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3024. |
Анализатор обнаружил потенциальную ошибку при использовании функций форматирования: String.Format, Console.WriteLine, Console.Write и т.п. Строка форматирования не соответствует передаваемым в функцию фактическим аргументам.
Рассмотрим простые примеры:
Неиспользуемые аргументы.
int A = 10, B = 20;
double C = 30.0;
Console.WriteLine("{0} < {1}", A, B, C);
Не указан формат {2}, поэтому переменная 'С' не будет использована.
Возможные варианты исправленного кода:
//Удалим лишний аргумент
Console.WriteLine("{0} < {1}", A, B);
//Исправим строку форматирования
Console.WriteLine("{0} < {1} < {2}", A, B, C);
Недостаточное количество аргументов.
int A = 10, B = 20;
double C = 30.0;
Console.WriteLine("{0} < {1} < {2}", A, B);
Console.WriteLine("{1} < {2}", A, B);
Намного более опасной ситуацией является, когда в функцию передаётся меньше аргументов, чем необходимо. Это приводит к исключению FormatException.
Возможные варианты исправленного кода:
//Добавим недостающий аргумент
Console.WriteLine("{0} < {1} < {2}", A, B, C);
//Исправим индексы в строке форматирования
Console.WriteLine("{0} < {1}", A, B);
Анализатор не выдаёт предупреждение если...
int row = 10;
Console.WriteLine("Line: {0}; Index: {0}", row);
Пример ошибки в реальном приложении может выглядеть следующим образом :
var sql = string.Format(
"SELECT {0} FROM (SELECT ROW_NUMBER() " +
" OVER (ORDER BY {2}) AS Row, {0} FROM {3} {4}) AS Paged ",
columns, pageSize, orderBy, TableName, where);
В функцию передаются 5 объектов форматирования, но переменная 'pageSize' не используется, т.к. пропущен формат {1}.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3025. |
Анализатор обнаружил, что для математических расчетов используются константы недостаточной точности.
Рассмотрим пример:
double pi = 3.141592654;
Такая запись не совсем корректна и лучше использовать математические константы из статического класса Math. Корректный вариант кода:
double pi = Math.PI;
Анализатор не считает ошибочной явную запись констант в формате 'float'. Это связано с тем, что тип 'float' имеет меньше значащих разрядов по сравнению с типом 'double'. Поэтому на следующий код предупреждение выдано не будет:
float f = 3.14159f; //ok
Данная диагностика классифицируется как:
Анализатор обнаружил в коде ситуацию, при которой проверка переменной на равенство 'null' осуществляется после её использования (вызов метода, обращение к свойству и т.п.). Данная диагностика ищет подобные ситуации в пределах одного логического выражения.
Рассмотрим пример некорректного кода:
if (rootDoc.Text.Trim() == documentName.Trim() && rootDoc != null)
Из данного примера видно, что сначала идёт обращение к свойству 'Text' (и даже более - вызову метода 'Trim' у этого свойства), а лишь затем ссылка 'rootDoc' проверяется на равенство 'null'. В случае, если ссылка 'rootDoc' имеет значение 'null', будет сгенерировано исключение типа 'NullReferenceException'. Возможным способом исправления ошибки будет сначала проверить ссылку на равенство 'null', а лишь затем обращаться к свойству объекта:
if (rootDoc != null && rootDoc.Text.Trim() == documentName.Trim())
Приведённый выше способ исправления ошибки - наиболее простой из возможных. При обнаружении подобных случаев программисту стоит внимательно изучить код, чтобы понять, как корректно исправить данную ошибку.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3027. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' совпадают начальное и конечное значения счетчика. Такое использование оператора 'for' приведет к тому, что цикл не будет выполнен ни разу или выполнен только один раз.
Рассмотрим пример:
void BeginAndEndForCheck(int beginLine, int endLine)
{
for (int i = beginLine; i < beginLine; i++)
{
...
}
Тело цикла никогда не выполняется. Скорее всего, произошла опечатка и следует заменить "i < beginLine" на корректное выражение "i < endLine". Корректный вариант кода:
for (int i = beginLine; i < endLine; i++)
{
...
}
Другой пример:
for (int i = A; i <= A; i++)
...
Тело этого цикла будет выполнено только один раз. Скорее всего, это не то, что задумывал программист.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3028. |
Анализатор обнаружил код, в котором рядом находятся два оператора 'if' с одинаковыми условиями. Это является потенциальной ошибкой или избыточным кодом.
Рассмотрим пример:
public void Logging(string S_1, string S_2)
{
if (!String.IsNullOrEmpty(S_1))
Print(S_1);
if (!String.IsNullOrEmpty(S_1))
Print(S_2);
}
Этот код содержит ошибку во втором условии. В нём повтороно проверяется переменная 'S_1', а необходимо проверить переменную с именем 'S_2'.
Исправленный вариант:
public void Logging(string S_1, string S_2)
{
if (!String.IsNullOrEmpty(S_1))
Print(S_1);
if (!String.IsNullOrEmpty(S_2))
Print(S_2);
}
Данная диагностика не всегда указывает на ошибку. Часто код просто избыточен:
public void Logging2(bool toFile, string S_1, string S_2)
{
if(toFile)
Print(S_1);
if (toFile)
Print(S_2);
}
Этот код корректен. Но он немного неэффективен, так как два раза приходится проверять значение одной и той же логической переменной. Предлагаем переписать код следующим образом:
public void Logging2(bool toFile, string S_1, string S_2)
{
if(toFile)
{
Print(S_1);
Print(S_2);
}
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3029. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что дважды проверяется одно и тоже условие.
Рассмотрим два примера:
// Example N1:
if (A == B)
{
if (A == B)
....
}
// Example N2:
if (A == B) {
} else {
if (A == B)
....
}
В первом случае вторая проверка "if (A == B)" всегда истинна. Во втором случае вторая проверка всегда ложна.
Высока вероятность, что подобный код содержит ошибку. Например, из-за опечатки используется ошибочное имя переменной. Корректный код:
// Example N1:
if (A == B)
{
if (A == C)
....
}
// Example N2:
if (A == B) {
} else {
if (A == C)
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3030. |
Анализатор обнаружил код, который можно упростить. Слева и справа от оператора '||' стоят противоположные по смыслу выражения. Данный код является избыточным, поэтому его можно упростить, сократив количество проверок.
Пример избыточного кода:
if (str == null || (str != null && str == "Unknown"))
В выражении "str != null && str == "Unknown"" проверка "str != null" является избыточной, так как перед этим проверятся противоположное ему условие " str == null ", причём эти выражения разделены оператором '||'. Следовательно, излишнюю проверку в скобках можно опустить, упростив код:
if (str == null || str == "Unknown"))
Избыточность кода может свидетельствовать о наличии ошибки. Возможно, что в выражении случайно используется не та переменная. И корректный код на самом деле должен быть, например, таким:
if (cond || (str != null && str == "Unknown"))
Иногда условие записано в обратном порядке и на первый взгляд его упрощать нельзя:
if ((s != null && s == "Unknown") || s == null)
Кажется, что здесь нельзя избавиться ни от проверки (s!=null), ни от проверки (s==null). Это не так. Это выражение, как и случай выше можно упростить:
if (s == null || s == "Unknown")
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3031. |
Анализатор обнаружил в коде цикл, который может превратиться после оптимизации в вечный цикл. Чаще всего подобные циклы используются для ожидания какого-либо события извне.
Приведём пример:
private int _a;
public void Foo()
{
var task = new Task(Bar);
task.Start();
Thread.Sleep(10000);
_a = 0;
task.Wait();
}
public void Bar()
{
_a = 1;
while (_a == 1);
}
Если выполнить данный код скомпилированный в Debug конфигурации, программа завершится корректно. Однако, если скомпилировать этот же код под Release, программа зависнет на цикле while. Причина такого поведения в том, что компилятор "закэширует" значение переменной '_a'.
Из-за подобного различия Debug-версии и Release-версии могут возникать сложные и трудно детектируемые ошибки. Путей корректировки данной ситуации несколько. Если эта переменная и впрямь используется для контроля логики многопоточной программы, то лучше использовать специализированные средства синхронизации, такие как мьютексы и семафоры. Другим вариантом исправления может стать добавление модификатора 'volatile' к объявлению переменной:
private volatile int _a;
...
Обратите внимание на то, что даже после такой правки код примера не станет полностью безопасным, т.к. нет гарантии, что Bar() начнёт выполняться раньше, чем переменной '_a' будет присвоено значение 0. Мы привели данный пример лишь чтобы продемонстрировать потенциально опасную ситуацию, связанную с оптимизациями компилятора. Для того, чтобы сделать код примера полностью безопасным, необходимо добавить, например, дополнительную синхронизацию перед выражением _a = 0, которая гарантирует, что выражение _a = 1 было выполнено.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3032. |
Анализатор обнаружил потенциально возможную ошибку в логических условиях. Логика работы кода не совпадает с тем, как этот код отформатирован.
Рассмотрим пример:
if (X)
if (Y) Foo();
else
z = 1;
Форматирование кода сбивает с толку, и кажется, что присваивание "z = 1" произойдет в том случае, если X == false. Однако ветка 'else' относится к ближайшему оператору 'if'. Другими словами приведенный код на самом деле эквивалентен следующему коду:
if (X)
{
if (Y)
Foo();
else
z = 1;
}
Таким образом, код работает не так, как может показаться на первый взгляд.
Если выдано предупреждение V3033, то это может означать две вещи:
1) Код плохо отформатирован и ошибки на самом деле нет. Тогда, чтобы предупреждение V3033 не выдавалось, а код был более понятен, его нужно отформатировать. Пример корректного форматирования:
if (X)
if (Y)
Foo();
else
z = 1;
2) Найдена логическая ошибка. Тогда код можно исправить, например, так:
if (X) {
if (Y)
Foo();
} else {
z = 1;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3033. |
Анализатор обнаружил потенциальную ошибку. Возможно вместо оператора '=!' следует написать '!=' или '== !'. Подобные ошибки чаще всего возникают из-за опечатки.
Рассмотрим пример некорректного кода:
bool a, b;
...
if (a =! b)
{
...
}
С большой вероятностью здесь должна быть проверка, что переменная 'a' не равна 'b'. Если это так, то корректный вариант кода должен выглядеть следующим образом:
if (a != b)
{
...
}
Анализатор учитывает форматирование в выражении. Поэтому если действительно требуется выполнить присваивание, а не сравнение, необходимо указать, используя скобки или пробелы. Следующие примеры кода считаются анализатором корректными:
if (a = !b)
...
if (a=(!b))
...
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, так как в программе имеется последовательность символов '=+'. Возможно, это опечатка и следует использовать оператор '+='.
Рассмотрим пример:
int size, delta;
...
size=+delta;
Этот код может быть корректен. Но с большой вероятностью имеется опечатка и на самом деле, хотели использовать оператор '+='. Исправленный вариант:
int size, delta;
...
size+=delta;
Если код корректен, то чтобы убрать предупреждение V3035 можно удалить '+' или поставить дополнительный пробел. Вариант корректного кода, где предупреждение не выдается:
size = delta;
size = +delta;
Примечание. Для поиска опечаток вида 'A =- B' используется диагностическая проверка V3036. Эта проверка сделана отдельно, так как возможно большое количество ложных срабатываний, и может возникнуть желание отключить её.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3035. |
Анализатор обнаружил потенциальную ошибку, так как в программе имеется последовательность символов '=-'. Возможно, это опечатка и следует использовать оператор '-='.
Рассмотрим пример:
int size, delta;
...
size =- delta;
Этот код может быть корректен. Но с большой вероятностью имеется опечатка и на самом деле, хотели использовать оператор '-='. Исправленный вариант:
int size, delta;
...
size -= delta;
Если код корректен, то чтобы убрать предупреждение V3036 можно использовать дополнительный пробел между символами '=' и '-'. Вариант корректного кода, где предупреждение не выдается:
size = -delta;
Чтобы уменьшить количество ложных срабатываний, для правила V3036 действует ряд специфичных исключений. Например, анализатор не будет выдавать предупреждение, если программист не использует пробелов между переменными и операторами. Ряд примеров, код которых анализатор считает безопасным:
A=-B;
int Z =- 1;
N =- N;
Примечание. Для поиска опечаток вида 'A =+ B' используется диагностическая проверка V3035.
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с бессмысленным взаимным присваиванием переменных.
Рассмотрим пример:
int a, b, c;
...
a = b;
c = 10;
b = a;
Здесь присваивание "B = A" не имеет никакого практического смысла. Возможно, это опечатка или просто лишнее действие. Корректный вариант кода:
a = b;
c = 10;
b = a_2;
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3037. |
Анализатор обнаружил потенциально возможную ошибку в программе, в связи с тем, что метод принимает два одинаковых аргумента при вызове. Передача одного и того же значения в качестве двух аргументов для многих методов является нормальной ситуацией, поэтому при разработке диагностики был предпринят ряд ограничений.
Диагностика сработает в случае если аргументы и параметры метода имеют общий паттерн, по которому их можно описать. Для примера рассмотрим данный код:
void Do(int mX, int mY, int mZ)
{
// Some action
}
void Foo(Vecor3i vec)
{
Do(vec.x, vec.y, vec.y);
}
Если обратить внимание на сигнатуру метода 'Do' и его вызов, то можно заметить, что аргумент 'vec.y' передается в него дважды, и скорее всего в данной ситуации для параметра 'mZ' ожидался аргумент 'vec.z'. Корректный вариант мог бы выглядеть так:
Do(vec.x, vec.y, vec.z);
Диагностика спрогнозирует возможные варианты исправления аргумента и в случае, если предложенная переменная присутствует в зоне видимости в месте вызова метода, будет выдано предупреждение о возможной опечатке с вариантом исправления.
V3038 The 'vec.y' argument was passed to 'Do' method several times. It is possible that the 'vec.z' argument should be passed to 'mZ' parameter.
Так же если речь идет о таких функциях как Math.Min, Math.Max, string.Equals и т.д., то передача одинаковых аргументов - это очень подозрительная ситуация.
Рассмотрим пример:
int count, capacity;
....
size = Math.Max(count, count);
Из-за опечатки функция Math.Max сравнивает переменную саму с собой. Корректным вариантом кода должно было быть:
size = Math.Max(count, capacity);
Если Вы обнаружили подобную ошибку, которая не диагностируется анализатором, то просим сообщить нам имя функции, которая не должна принимать в качестве некоторых аргументов одинаковые переменные.
Рассмотрим другой пример ошибки, обнаруженный анализатором в коде реального приложения:
return invariantString
.Replace(@"\", @"\\")
.Replace("'", @"\'")
.Replace("\"", @"""");
Скорее всего из-за незнания особенностей строковых литералов, перед которыми стоит символ '@', произошла трудно заметная ошибка при написании @"""". Судя по коду, хотели добавить два символа кавычек подряд. Но из-за ошибки, одна кавычка будет заменена той же одной кавычкой. Данную ошибку можно поправить двумя способами:
.Replace("\"", "\"\"")
Или
.Replace("\"", @"""""")
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3038. |
Анализатор обнаружил потенциальную ошибку при вызове функции, предназначенную для работы с файлами. В одном из фактических аргументов в функцию передаётся абсолютный путь до файла или директории. Такое использование функция является опасным, поскольку могут встретиться случаи, когда данного пути не будет существовать на компьютере пользователя.
Рассмотрим пример некорректного кода:
String[] file = File.ReadAllLines(
@"C:\Program Files\MyProgram\file.txt");
Более корректным будет вариант получения пути к файлу исходя из определенных условий.
Исправленный вариант кода:
String appPath = Path.GetDirectoryName(
Assembly.GetExecutingAssembly().Location);
String[] fileContent = File.ReadAllLines(
Path.Combine(appPath, "file.txt"));
В таком случае будет осуществлён поиск файла в директории, в которой находится приложение.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку в выражении, где совместно используются целочисленные и real-типы данных. Под real-типами понимаются такие типы, как 'float' и 'double'.
Рассмотрим пример из реального приложения:
public long ElapsedMilliseconds { get; }
....
var minutes = watch.ElapsedMilliseconds / 1000 / 60;
Assert.IsTrue(minutes >= 0.95 && minutes <= 1.05);
Здесь переменная 'minutes' имеет тип 'long', и сравнение этой переменной со значениями 0.95 и 1.05 не имеет практического смысла. Единственное целочисленное значение, которое попадает в этот диапазон, это 1.
Возможно, программист ожидал при делении целочисленных типов результат будет иметь тип 'double'. Это не так. Здесь происходит целочисленное деление в переменную 'minutes' записывается значение целого типа.
Возможным вариантом решения будет явно преобразовать количество миллисекунд к типу 'double', до деления:
var minutes = (double)watch.ElapsedMilliseconds / 1000 / 60;
Assert.IsTrue(minutes >= 0.95 && minutes <= 1.05);
Теперь результат деления будет более точным, а переменная 'minutes' будет имеет тип 'double'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3040. |
В выражении присутствует операция деления целочисленных типов данных. Полученное значение неявно преобразуется к типу с плавающей точкой. Обнаружив такую ситуацию, анализатор предупреждает о наличии потенциальной ошибки, которая может привести к вычислению неточного результата.
Рассмотрим пример:
int totalTime = 1700;
int operationNum = 900;
double averageTime = totalTime / operationNum;
Программист может ожидать, что переменная 'averageTime' будет иметь значение '1.888(8)', однако при выполнении программы будет получен результат равный '1.0'. Это происходит потому, что операция деления выполняется с целочисленными типами и только затем приводится к типу с плавающей точкой.
Как и в предыдущем случае, ошибку можно исправить 2 способами.
Первый способ - изменить типы переменных:
double totalTime = 1700;
double operationNum = 900;
double averageTime = totalTime / operationNum;
Второй способ - использовать явное приведение типов.
int totalTime = 1700;
int operationNum = 900;
double averageTime = (double)(totalTime) / operationNum;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3041. |
Анализатор обнаружил два различных способа обращения к членам одного объекта, через операторы "?." и ".". Как следствие, при обращении к части выражения через "?." предполагается, что предстоящий член выражения может быть равен null. Отсюда следует, что попытка доступа члену выражения через "." приведет к падению программы.
Рассмотрим пример:
if (A?.X == X || A.X == maxX)
...
Из-за невнимательности, возникает ситуация в которой, если "A" будет null, первое условие вернет false, а при проверке второго условия возникнет исключение NullReferenceException. Исправленный вариант программы должен выглядеть так:
if (A?.X == X || A?.X == maxX)
...
Рассмотрим другой пример ошибки, взятый из реального приложения:
return node.IsKind(SyntaxKind.IdentifierName) &&
node?.Parent?.FirstAncestorOrSelf<....>() != null;
Как видно из примера, во второй части условия допускается, что "node" может быть null "node?.Parent", но при вызове функции "IsKind" данной проверки не производиться.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3042. |
Анализатор обнаружил потенциальную ошибку, связанную c тем, что форматирование кода, следующего за условным оператором, не соответствует логике выполнения программы. Высока вероятность, что пропущены открывающиеся и закрывающиеся фигурные скобки.
Рассмотрим пример некорректного кода:
if (a == 1)
b = c; d = b;
В данном случае присваивание 'd = b;' будет выполняться всегда, независимо от условия 'a == 1'.
Если код ошибочен, то ситуацию можно исправить, используя фигурные скобки. Корректный вариант кода:
if (a == 1)
{ b = c; d = b; }
Другой пример некорректного кода:
if (a == 1)
b = c;
d = b;
Для исправления ошибки так же следует использовать фигурные скобки. Корректный вариант кода:
if (a == 1)
{
b = c;
d = b;
}
Если код корректен, то чтобы исчезло предупреждение V3043, следует отформатировать код следующим образом:
if (a == 1)
b = c;
d = b;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3043. |
Анализатор обнаружил потенциальную ошибку в программе, связанную с регистрацией свойств зависимостей [Dependency Properties]. При регистрации свойств зависимостей было ошибочно задано свойство, через которое происходит запись/чтение свойств.
class A : DependencyObject
{
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime", ....);
public static readonly DependencyProperty OtherProperty =
DependencyProperty.Register("Other", ....);
public DateTime CurrentTime {
get { return (DateTime)GetValue(CurrentTimeProperty); }
set { SetValue(OtherProperty, value); } }
}
....
Из-за копирования возникла следующая ситуация. В определении методов доступа get и set, свойства CurrentTime, используются соответственно методы GetValue и SetValue с различные DependencyProperty. Таким образом при чтении свойства CurrentTime значение будет получено из свойства зависимости CurrentTimeProperty, а при записи в свойство CurrentTime, значение будет записано в 'OtherProperty'.
Корректным обращением к DependencyProperty будет подобный вариант:
public DateTime CurrentTime {
get { return (DateTime)GetValue(CurrentTimeProperty); }
set { SetValue(CurrentTimeProperty, value); } }
}
Анализатор обнаружил потенциальную ошибку в программе, связанную с регистрацией свойства зависимости [Dependency Property]. При регистрации свойства зависимости было ошибочно задано имя свойства, через которое происходит обращение к свойству зависимости.
class A : DependencyObject
{
public static readonly DependencyProperty ColumnRulerPenProperty =
DependencyProperty.Register("ColumnRulerBrush", ....);
public DateTime ColumnRulerPen {
get { return (DateTime)GetValue(ColumnRulerPenProperty); }
set { SetValue(ColumnRulerPenProperty, value); }
}
....
Из-за переименования возникла ситуация, при которой в свойстве, через которое происходит запись в свойство зависимости ColumnRulerPenProperty, было ошибочно задано имя свойства. В приведённом примере из реального приложения это ColumnRulerPen, вместо ColumnRulerBrush (согласно параметрам функции Register).
Подобное реализация свойств зависимости таит в себе потенциальную проблему. При первом обращении из XAML разметки к свойству ColumnRulerPen значение будет прочитано, но при изменении ColumnRulerPen значение не будет обновляться.
Корректным заданием свойства будет такой вариант:
public DateTime ColumnRulerBrush {
get { return (DateTime)GetValue(CurrentTimeProperty); }
set { SetValue(CurrentTimeProperty, value); }
}
В реальных примерах встречается и следующий вариант ошибочного задания имени свойства зависимости:
public static readonly DependencyProperty WedgeAngleProperty =
DependencyProperty.Register("WedgeAngleProperty", ....);
Предполагался, что слово "Property" будет отсутствовать в строковом литерале:
public static readonly DependencyProperty WedgeAngleProperty =
DependencyProperty.Register("WedgeAngle", ....);
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3045. |
Анализатор обнаружил потенциальную ошибку в программе, связанную с регистрацией свойства зависимости [Dependency Property]. При регистрации свойства зависимости был ошибочно задан тип значения, который должен быть записан в свойство зависимости.
В приведённом далее примере, это CurrentTimeProperty:
class A : DependencyObject
{
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime", typeof(int),....);
public DateTime CurrentTime
{
get { return (DateTime)GetValue(CurrentTimeProperty); }
set { SetValue(CurrentTimeProperty, value); }
}
....
Из-за копирования возникла ситуация, в которой при регистрации свойства зависимости было указано, что данное свойство хранит значение типа int. При попытке записи или чтения в CurrentTimeProperty из свойства CurrentTime произойдёт ошибка.
Корректный вариант регистрации DependencyProperty выглядит следующим образом:
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime", typeof(DateTime),....);
Данная диагностика также проверят совпадение типов регистрируемого свойства зависимости и типа значения, заданного по умолчанию.
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime", typeof(DateTime),
typeof(A),
new FrameworkPropertyMetadata(132));
В данном примере значением по умолчанию является 132, при этом указано, что само свойство зависимости должно содержать значение типа DateTime.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3046. |
Анализатор обнаружил потенциальную ошибку в программе, связанную с регистрацией свойства зависимости [Dependency Property]. При регистрации свойства зависимости был задан тип владельца свойства, отличный от класса, в котором объявлено само свойство зависимости.
class A : DependencyObject { .... }
class B : DependencyObject
{
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime", typeof(DateTime),
typeof(A));
....
Из-за копирования возникла ситуация, в которой при регистрации свойства зависимости было указано, что данное свойство имеет в качестве владельца класс 'A', хотя на самом деле оно определено в классе 'B'.
Корректный вариант регистрации DependencyProperty выглядит следующим образом:
class B : DependencyObject
{
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime", typeof(DateTime),
typeof(B));
Анализатор обнаружил потенциальную ошибку в программе, связанную с регистрацией свойств зависимостей [Dependency Properties]. При пределах одного класса были зарегистрированы два свойства зависимостей на одно и то же имя.
class A : DependencyObject
{
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime",....);
public static readonly DependencyProperty OtherProperty =
DependencyProperty.Register("CurrentTime",....);
....
Из-за копирования возникла ситуация, в которой, при регистрации свойства зависимости OtherProperty, было указано, что данному свойству нужно обращаться из XAML разметки по имени 'CurrentTime', а не по 'Other', как предполагалось разработчиком.
Корректный вариант регистрации свойств зависимостей выглядит следующим образом:
public static readonly DependencyProperty CurrentTimeProperty =
DependencyProperty.Register("CurrentTime",....);
public static readonly DependencyProperty OtherProperty =
DependencyProperty.Register("Other",....);
Анализатор обнаружил потенциальную ошибку в программе, связанную со свойствами зависимости [Dependency Property]. Свойство зависимости было объявлено, но не было проинициализировано, что приведет к ошибке при попытке доступа к нему через SetValue / GetValue.
class A : DependencyObject
{
public static readonly DependencyProperty CurrentTimeProperty;
static A(){ /* CurrentTimeProperty не инициализируется */ }
....
После неаккуратного рефакторинга или из-за неудачного Copy-Paste возможна ситуация, когда свойство зависимости остается не зарегистрированным. Корректный вариант кода:
class A : DependencyObject
{
public static readonly DependencyProperty CurrentTimeProperty;
static A()
{
CurrentTimeProperty =
DependencyProperty.Register("CurrentTime", typeof(DateTime),
typeof(A));
}
....
Анализатор обнаружил потенциальную ошибку, связанную с тем, что в коде задан строковый литерал, в котором содержится разметка HTML с ошибками. Был открыт тег для элемента, которому требуется завершающий тег. Но следующим в строке обнаружен закрывающий тег, который не соответствует открывающему тегу.
Рассмотрим пример:
string html = "<B><I>This is a text, in bold italics.</B>";
В данном случае открывающему тегу "<I>" должен соответствовать закрывающий тег "</I>", но при дальнейшем анализе HTML мы обнаружим закрывающий тег "</B>", что является ошибкой. В таком виде часть HTML кода является невалидной.
Для исправления ошибки необходимо проверить корректность последовательности открывающих и закрывающих тегов и устранить найденные ошибки.
Пример корректного варианта:
string html = "<B><I>This is a text, in bold italics.</I></B>";
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3050. |
В выражении присутствует избыточное использование оператора 'as' или 'is'. Нет смысла приводить объект к собственному типу или выполнять проверку на совместимость с ним же. Часто данный код просто является избыточным, но порой подобные ситуации могут свидетельствовать о наличии ошибки в коде.
Для того, чтобы лучше понять, о чём речь, рассмотрим несколько примеров.
Искусственный пример:
public void SomeMethod(String str)
{
var localStr = str as String;
....
}
При инициализации переменной 'localStr' выполняется явное приведение объекта 'str' к типу 'String' при том, что явное приведение с использованием оператора 'as' избыточно, так как 'str' уже является объектом типа 'String'.
Тогда упрощённый код мог бы выглядеть так:
public void SomeMethod(String str)
{
String localStr = str;
....
}
Вместо явного указания типа объекта 'localStr' можно было бы оставить ключевое слово 'var', но явное указание типа добавляет ясности программе.
Рассмотрим более интересный пример:
public object FindName(string name, FrameworkElement templatedParent);
....
lineArrow = (Grid)Template.FindName("lineArrow", this) as Grid;
if (lineArrow != null);
....
Рассмотрим строку с приведениями более внимательно и проследим, что происходит:
Если посмотреть на следующую строку, видно, что предполагается, что 'lineArrow' может иметь значение 'null', следовательно, подразумевается именно использование оператора 'as'. Как объяснялось выше, 'lineArrow' не может принять значение 'null' в случае неудачного приведения. Следовательно, это не просто избыточное приведение, а явная ошибка.
Решением может послужить удаление лишней операции приведения из кода:
lineArrow = Template.FindName("lineArrow", this) as Grid;
if (lineArrow != null);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3051. |
Анализатор обнаружил ситуацию, когда исходный объект перехваченного исключения не был использован приемлемым образом при повторной генерации исключения из блока catch. Из-за этого ряд ошибок превращается в трудноотлаживаемые, так как теряется стек оригинального исключения.
Рассмотрим несколько примеров некорректного кода. Первый пример:
public Asn1Object ToAsn1Object()
{
try
{
return Foo(_constructed, _tagNumber);
}
catch (IOException e)
{
throw new ParsingException(e.Message);
}
}
В данном случае перехваченное исключение ввода/вывода хотели трансформировать в другое исключение типа 'ParsingException'. При этом передали только сообщение из первого исключения, тем самым сократив количество полезной информации.
Корректный вариант кода:
public Asn1Object ToAsn1Object()
{
try
{
return Foo(_constructed, _tagNumber);
}
catch (IOException e)
{
throw new ParsingException(e.Message, e);
}
}
В исправленном варианте исходное исключение передается в качестве внутреннего, что полностью сохраняет информацию об исходной ошибке.
Рассмотрим второй пример:
private int ReadClearText(byte[] buffer, int offset, int count)
{
int pos = offset;
try
{
....
}
catch (IOException ioe)
{
if (pos == offset) throw ioe;
}
return pos - offset;
}
В данном случае перехваченное исключение ввода/вывода генерируется повторно и полностью "затирает" стек оригинальной ошибки. Чтобы этого избежать, достаточно сделать переброс оригинального исключения.
Корректный вариант кода:
private int ReadClearText(byte[] buffer, int offset, int count)
{
int pos = offset;
try
{
....
}
catch (IOException ioe)
{
if (pos == offset) throw;
}
return pos - offset;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3052. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что в выражении ищется более длинная подстрока и более короткая. При этом более короткая строка, является частью более длинной. Получается, что одно из сравнений избыточно или допущена какая-то ошибка.
Рассмотрим пример:
if (str.Contains("abc") || str.Contains("abcd"))
В случае если подстрока "abc" будет найдена, то дальнейшая проверка не будет выполняться. Если подстрока "abc" не будет найдена, то и поиск более длинной подстроки "abcd" не имеет смысла.
Для исправления ошибки необходимо проверить правильность подстрок или убрать из кода лишние проверки. Пример корректного варианта:
if (str.Contains("abc"))
Другой пример:
if (str.Contains("abc"))
Foo1();
else if (str.Contains("abcd"))
Foo2();
В данном случае метод Foo2() никогда не будет вызван. Устранить ошибку можно путем замены порядка проверки. То есть сначала следует искать более длинную подстроку, а потом более короткую:
if (str.Contains("abcd"))
Foo2();
else if (str.Contains("abc"))
Foo1();
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3053. |
Анализатор обнаружил потенциальную ошибку, связанную с небезопасным использованием шаблона "блокировки с двойной проверкой" (double checked locking). Блокировка с двойной проверкой - это шаблон, предназначенный для уменьшения накладных расходов получения блокировки. Сначала проверяется условие блокировки без синхронизации. И только если условие выполняется, поток попытается получить блокировку. Таким образом, блокировка будет выполнена только если она действительно была необходима.
Рассмотрим пример небезопасной реализации данного шаблона на языке C#:
private static MyClass _singleton = null;
public static MyClass Singleton
{
get
{
if(_singleton == null)
lock(_locker)
{
if(_singleton == null)
{
MyClass instance = new MyClass();
instance.Initialize();
_singleton = instance;
}
}
return _singleton;
}
}
В данном примере шаблон используется для реализации "ленивой инициализации" - инициализация откладывается до тех пор, пока значение переменной не понадобится. Данный код будет корректно работать в программе, использующей объект '_singleton' из одного потока. Для обеспечения безопасной инициализации в многопоточной программе обычно используется конструкция 'lock', однако в нашем примере этого оказывается недостаточно.
Обратите внимание на вызов метода 'Initialize()' у объекта 'Instance'. В Release версии программы, компилятор может оптимизировать данный код и порядок назначения переменной '_singleton' и метода 'Initialize()' могут поменяться. Таким образом, другой поток, обратившись к 'Singleton' одновременно с инициализирующим потоком, может получить доступ к объекту до того, как инициализация будет завершена.
Рассмотрим другой пример использования шаблона блокировки с двойной проверкой:
private static MyClass _singleton = null;
private static bool _initialized = false;
public static MyClass Singleton;
{
get
{
if(!_initialized)
lock(_locker)
{
if(!_initialized)
{
_singleton = new MyClass();
_initialized = true;
}
}
return _singleton;
}
}
Мы видим, что, как и в предыдущем примере, оптимизация компилятором порядка назначений переменных '_singleton' и '_initialized' может привести к ошибке. Т.е. в начале переменной '_initialized' будет присвоено значение 'true', а уже потом создастся новый объект типа 'MyClass' и ссылка не него будет записана в '_singleton'.
Такая перестановка может привести к ошибке при доступе к объекту из параллельного потока. Получается, что переменная '_singleton' будет ещё не назначена, а флаг '_intialize' уже будет выставлен в 'true'.
Одна из опасностей таких ошибок состоит в том, что часто кажется, будто программа работает корректно. Это происходит из-за того, что рассмотренная ситуация будет возникать не очень часто, в зависимости от архитектуры используемого процессора, версии CLR и т.п.
Есть несколько способов обеспечить потоко-безопасность для данного шаблона. Самым простым будет пометить проверяемую в условии if переменную ключевым словом volatile:
private static volatile MyClass _singleton = null;
public static MyClass Singleton
{
get
{
if(_singleton == null)
lock(_locker)
{
if(_singleton == null)
{
MyClass instance = new MyClass();
instance.Initialize();
_singleton = instance;
}
}
return _singleton;
}
}
Использование ключевого слова 'volatile' предотвратит для переменной возможные оптимизации компилятора, связанные с перестановками инструкций записи\чтения и кэшированием её значения в регистрах процессора.
Из соображений производительности не всегда желательно объявлять переменную как 'volatile'. В этом случае можно организовать доступ к переменной с помощью методов: 'Thread.VolatileRead', 'Thread.VolatileWrite' и 'Thread.MemoryBarrier'. Эти методы создадут барьеры по чтению\записи памяти только там, где это необходимо.
Наконец, для реализации "ленивой инициализации" можно воспользоваться специально предназначенным для этого классом 'Lazy<T>', доступным начиная с .NET 4.
См. также статью: Выявление неправильной блокировки с двойной проверкой с помощью диагностики V3054.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3054. |
Анализатор обнаружил ситуацию, когда в условии оператора if/while/do while/for выполняется оператор присваивания '=' для операндов булевского типа. Есть вероятность, что вместо оператора '=' планировалось использовать оператор '=='.
Рассмотрим пример:
void foo(bool b1, bool b2)
{
if (b1 = b2)
....
В коде допущена опечатка. Вместо сравнения переменных b1 и b2, в коде произойдет изменение значения переменной b1. Корректный вариант кода:
if (b1 == b2)
Если в конструкции 'if' необходимо сделать присвоение, для сокращения кода, то рекомендуется оборачивать присвоение в круглые скобки. Обертывание присваивания в дополнительные скобки - это распространенный паттерн программирования, описываемый в книгах и распознаваемый различными компиляторами и анализаторами кода.
Условие с дополнительными скобками подсказывает программисту и анализаторам кода, что никой ошибки нет:
if ((b1 = b2))
Более того, дополнительное обертывание в скобки не только делают код более читабельным, но и убережет от ошибок приоритета операций, как в следующим примере:
if ((a = b) || a == c)
{ }
Если бы не было скобок, то, согласно приоритету операций, сначала выполнилась бы часть 'b || a == c', а после результат выполнения выражения присвоился бы переменной 'a'. Это поведение могло оказаться вовсе не таким, как задумывал программист.
Данная диагностика классифицируется как:
Анализатор выявляет случаи, когда для условных переменных 'x' и 'y' дважды использовался условный метод 'GetX', вместо 'GetX' и 'GetY' соответственно.
Рассмотрим пример:
int x = GetX() * n;
int y = GetX() * n;
Во второй строке вместо функции GetY() используется GetX(). Корректный код:
int x = GetX() * n;
int y = GetY() * n;
Для обнаружения этого подозрительного места анализатор следовал следующей логике. Мы имеем строку, где используется имя, включающее в себя фрагмент "X". Рядом с ней есть строка, где используется имя - антипод, содержащая "Y". Но при этом во второй строке также есть и "X". Если выполнилось это и ещё некоторые условия, то данная конструкцию считается опасной и анализатор предлагает программисту её проверить. Если бы, например, слева не было переменных "x" и "y" то такой код считался бы безопасным. Пример кода, на который анализатор не обратит внимания:
array[0] = GetX() / 2;
array[1] = GetX() / 2;
К сожалению, данное правило имеет ложные срабатывания, так как анализатор не имеет представления об устройстве программы и предназначении кода. Например, в коде теста:
var t1 = new Thread { Name = "Thread 1" };
var t2 = new Thread { Name = "Thread 2" };
var m1 = new Message { Name = "Thread 1: Message 1", Thread = t1};
var m2 = new Message { Name = "Thread 1: Message 2", Thread = t1};
var m3 = new Message { Name = "Thread 2: Message 1", Thread = t2};
Анализатор предположил, что при объявлении переменной 'm2' использовалось копирование кода и возникла ошибка: используется переменная 't1', а не 't2'. На самом деле ошибки нет. Исходя из текста сообщений, можно сделать вывод, что тест проверят вывод сообщений 'm1', 'm2' из одного потока 't1', а вывод сообщения 'm3' из потока 't2'. Для подобных случаев ошибочного анализа, есть возможность убрать предупреждение, вписав в код комментарий "//-V3056" или использовать другие методы подавления ложных предупреждений.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3056. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в качестве фактического аргумента в функцию передаётся очень странное значение.
Рассмотрим примеры:
Недопустимые символы в пути
string GetLogPath(string root)
{
return System.IO.Path.Combine(root, @"\my|folder\log.txt");
}
В функцию 'Combine()' передаётся путь, содержащий недопустимый символ '|'. В результате чего функция сгенерирует исключение 'ArgumentException'.
Корректный вариант:
string GetLogPath(string root)
{
return System.IO.Path.Combine(root, @"\my\folder\log.txt");
}
Странный аргумент функции форматирования
string.Format(mask, 1, 2, mask);
Функция 'string.Format()' заменяет один или более элементов формата в указанной строке. Анализатор считает подозрительным, если в строку форматирования пытаются записать эту же строку.
Недопустимый индекс
var pos = mask.IndexOf('\0');
if (pos != 0)
asciiname = mask.Substring(0, pos);
'IndexOf()' возращает позицию искомого аргумента. Если аргумент не найден, то функция возвращает значение '-1'. А если передать отрицательный индекс в функцию 'Substring()', то возникнет 'ArgumentOutOfRangeException'.
Корректный вариант:
var pos = mask.IndexOf('\0');
if (pos > 0)
asciiname = mask.Substring(0, pos);
Обратите внимание, что анализатор может также выдать предупреждение, когда в метод передаётся корректный аргумент, но соответствующий параметр внутри метода может принять недопустимое значение.
static void Bar(string[] data, int index, int length)
{
if (index < 0)
throw new Exception(....);
if (data.Length < index + length)
length = data.Length - index; // <=
....
Array.Copy(data, index, result, 0, length); // <=
}
static void Foo(string[] args)
{
Bar(args, 4, 2); // <=
....
}
В данном случае анализатор выдаст предупреждение, что при вызове метода 'Bar' соответствующий аргументу '2' параметр ('length') внутри метода может принять отрицательное значение, которое дальше используется при вызове метода 'Array.Copy'. Это приведёт к возникновению исключения 'ArgumentOutOfRangeException'.
И действительно, если размер массива 'args' ('data' внутри метода 'Bar') будет меньше 4, внутри метода 'Bar' в 'length' будет записано отрицательное значение несмотря на то, что в метод передаётся положительное значение - 2. Как следствие - при вызове 'Array.Copy' будет выброшено исключение.
В метод 'Bar' следует добавить проверку нового значения 'length' и необходимую обработку отрицательных значений:
if (data.Length < index + length)
length = data.Length - index;
if (length < 0)
.... // Error handling
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3057. |
Анализатор обнаружил ситуацию, когда в словарь добавляются значения для ключа, который уже присутствует в словаре. На этапе выполнения это приведет к генерации исключения ArgumentException с сообщением: "An item with the same key has already been added.".
Рассмотрим пример некорректного кода:
var mimeTypes = new Dictionary<string, string>();
mimeTypes.Add(".aif", "audio/aiff");
mimeTypes.Add(".aif", "audio/x-aiff"); // ArgumentException
В данном случае, при повторной попытке добавить значение по ключу ".aif", будет сгенерировано исключение ArgumentException.
Корректный вариант кода предполагает устранение дубликатов ключей при заполнении словаря:
var mimeTypes = new Dictionary<string, string>();
mimeTypes.Add(".aif", "audio/aiff");
Данная диагностика классифицируется как:
Анализатор посчитал подозрительным перечисление, элементы которого участвуют в битовых операциях или имеют значения, равные степеням 2. Но при этом само перечисление не отмечено атрибутом [Flags].
Если выполняется одно из этих условий, то для использования перечислений как битовых флагов, необходимо отметить перечисление атрибутом [Flags], что даст ряд преимуществ при работе с этим перечислением в коде.
Для того, чтобы лучше понять, как изменяет поведение программы применение атрибута [Flags] к перечислимому типу, рассмотрим несколько примеров:
enum Suits { Spades = 1, Clubs = 2, Diamonds = 4, Hearts = 8 }
// en1: 5
var en1 = (Suits.Spades | Suits.Diamonds);
Без атрибута [Flags] результатом применения битовой операции OR для членов со значениями '1' и '4' будет значение '5'.
Поведение меняется, если для перечисления задан атрибут [Flags]:
[Flags]
enum SuitsFlags { Spades = 1, Clubs = 2, Diamonds = 4, Hearts = 8 }
// en2: SuitsFlags.Spades | SuitsFlags.Diamonds;
var en2 = (SuitsFlags.Spades | SuitsFlags.Diamonds);
В таком случае результат операции OR рассматривается не как единственное целочисленное значение, а как набор битов, содержащих значения 'SuitsFlags.Spades' и 'SuitsFlags.Diamonds'.
Если у объектов 'en1' и 'en2' вызвать метод 'ToString', его результат также будет различаться. Данный метод попытается преобразовать числовое значение в символьный эквивалент, но у значения '5' нет символьного эквивалента. Однако обнаружив у типа атрибут [Flags], метод 'ToString' рассматривает числовое значение как набор битовых флагов. Следовательно, вызов метода 'ToString' для объектов 'en1' и 'en2' даст следующий вывод:
String str1 = en1.ToString(); // "5"
String str2 = en2.ToString(); // "SuitsFlags.Spades |
// SuitsFlags.Diamonds"
По аналогии работают и получения числовых значений из строки при использовании статических методов 'Parse' и 'TryParse' класса 'Enum'.
Дополнительным преимуществом использования [Flags] является упрощение процесса отладки. Значение переменной 'en2' будет отображаться как набор именованных констант, а не как просто число:
Дополнительная информация:
Анализатор обнаружил подозрительное битовое выражение. Выражение написано для того, чтобы изменить определённые биты в переменной. Но значение переменной останется прежним.
Пример подозрительного кода:
A &= ~(0 << Y);
A = A & ~(0 << Y);
Программист планировал сбросить определенный бит в переменной, но допустил ошибку. написав 0, вместо 1.
Обе строки идентичны по результату, поэтому рассмотрим вторую строку, как более наглядный пример. Предположим, что имеется следующие значение переменных в битовом представлении:
A = 0..0101
A = 0..0101 & ~(0..0000 << 0..00001)
Сдвиг влево на один бит значения 0 - ни к чему не приведет. В итоге получится следующие выражение:
A = 0..0101 & ~0..0000
Далее будет выполнена операции побитового отрицания, что приведет к следующему выражению:
A = 0..0101 & 11111111
После операции побитового "и", получится, что исходное и результирующее выражение равны:
A = 0..0101
Исправленный вариант кода должен выглядеть, следующим образом:
A &= ~(1 << Y);
A = A & ~(1 << Y);
Данная диагностика классифицируется как:
Анализатор нашёл потенциальную ошибку в теле метода. Один из его параметров перезаписывается перед тем, как используется. Таким образом значение, пришедшее в метод, попросту теряется.
Эта ошибка имеет разные проявления. Рассмотрим пример кода:
void Foo1(Node A, Node B)
{
A = SkipParenthesize(A);
B = SkipParenthesize(A);
// do smt...
}
Здесь допущена опечатка, из-за чего объект 'B' примет неверное значение. Исправленный код выглядит так:
void Foo1(Node A, Node B)
{
A = SkipParenthesize(A);
B = SkipParenthesize(B);
// do smt...
}
Но данная ошибка может проявить себя и более интересно:
void Foo2(List<Int32> list, Int32 count)
{
list = new List<Int32>(count);
for (Int32 i = 0; i < count; ++i)
list.Add(GetElem(i));
}
Данный метод должен был инициализировать список некоторыми значениями. В данном случае происходит копирование ссылки ('list'), хранящей адрес блока памяти в куче, в котором расположен список (или 'null', если память не была выделена). Поэтому, когда мы вновь выделяем память под список, адрес блока памяти записывается в локальную копию ссылки, а исходная (за пределами метода) остаётся неизменной. Таким образом выполняется лишняя работа для выделения памяти, инициализации списка и последующей уборки мусора.
Ошибка заключается в пропущенном модификаторе 'out'. Корректный вариант кода:
void Foo2(out List<Int32> list, Int32 count)
{
list = new List<Int32>(count);
for (Int32 i = 0; i < count; ++i)
list.Add(GetElem(i));
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3061. |
Анализатор обнаружил вызов метода, в который в качестве аргумента передаётся сам объект, у которого вызывается метод. Вероятно, данный код содержит ошибку, и в метод должен передаваться другой объект.
Рассмотрим пример:
A.Foo(A);
Здесь из-за опечатки используется неверное имя переменной. Тогда корректный код должен был быть таким:
A.Foo(B);
или таким:
B.Foo(A);
Пример кода из реального приложения:
private bool CanRenameAttributePrefix(....)
{
....
var nameWithoutAttribute =
this.RenameSymbol.Name.GetWithoutAttributeSuffix(isCaseSensitive:
true);
var triggerText = GetSpanText(document,
triggerSpan,
cancellationToken);
// nameWithoutAttribute, triggerText - String
return triggerText.StartsWith(triggerText);
}
Результатом возвращаемого значения в данном случае всегда будет 'true', из-за того, что метод, проверяющий, что строка начинается с подстроки, принимает в качестве аргумента саму строку ('triggerText'). Скорее всего подразумевалась следующая проверка:
return triggerText.StartsWith(nameWithoutAttribute);
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3062. |
Анализатор обнаружил потенциально возможную ошибку внутри логического условия. Часть логического выражения всегда истинно/ложно и оценено, как подозрительное.
Рассмотрим пример:
uint i = length;
while ((i >= 0) && (n[i] == 0)) i--;
Выражение "i >= 0" всегда истинно, т.к. переменная 'i' имеет тип uint. Поэтому, если значение 'i' дойдёт до нуля, то цикл while не будет остановлен, и 'i' примет максимальное значение типа uint. Попытка дальнейшего доступа к массиву 'n' приведёт к исключению OverflowException.
Корректный код:
int i = length;
while ((i >= 0) && (n[i] == 0)) i--;
Рассмотрим другой пример:
public static double Cos(double d)
{
// -9223372036854775295 <= d <= 9223372036854775295
bool expected = !performCheck ||
!(-9223372036854775295 <= d || // <=
d <= 9223372036854775295);
if (!expected)
....
Программист хотел проверить, что переменная d попадает в заданный диапазон (это видно по комментарию перед проверкой). Но из-за опечатки он написал оператор '||', вместо оператора '&&'. Корректный код:
bool expected = !performCheck ||
!(-9223372036854775295 <= d &&
d <= 9223372036854775295);
Иногда предупреждение V3063 выявляет не ошибку, а просто избыточный код. Рассмотрим пример:
if (@char < 0x20 || @char > 0x7e) {
if (@char > 0x7e
|| (@char >= 0x01 && @char <= 0x08)
|| (@char >= 0x0e && @char <= 0x1f)
|| @char == 0x27
|| @char == 0x2d)
Анализатор предупредит, что подвыражения @char == 0x27 и @char == 0x2d всегда ложны из-за предшествующего оператора if. Этот код может быть вполне корректен. Однако он избыточен, и лучше его упростить. Это сделает программу более простой для понимания другими разработчиками.
Упрощенный вариант кода:
if (@char < 0x20 || @char > 0x7e) {
if (@char > 0x7e
|| (@char >= 0x01 && @char <= 0x08)
|| (@char >= 0x0e && @char <= 0x1f))
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3063. |
Анализатор обнаружил ситуацию, когда может произойти деление на ноль.
Рассмотрим пример:
if (maxHeight >= 0)
{
fx = height / maxHeight;
}
В условии проверяется, что значение переменной maxHeight неотрицательно. Если эта переменная будет равна нулю, то внутри условия произойдёт деление на 0. Чтобы исправить ситуацию, необходимо выполнять деление только в том случае, когда maxHeight положительно.
Исправленный вариант:
if (maxHeight > 0)
{
fx = height / maxHeight;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3064. |
Анализатор обнаружил подозрительный метод, один из параметров которого ни разу не используется. При этом другой его параметр используется несколько раз, что, возможно, свидетельствует о наличии ошибки.
Рассмотрим пример:
private static bool CardHasLock(int width, int height)
{
const double xScale = 0.051;
const double yScale = 0.0278;
int lockWidth = (int)Math.Round(height * xScale);
int lockHeight = (int)Math.Round(height * yScale);
....
}
Из кода видно, что параметр 'width' ни разу не используется в теле метода, при этом параметр 'height' используется дважды, в том числе при инициализации переменной 'lockWidth'. Скорее всего, здесь допущена ошибка, и код инициализации переменной 'lockWidth' должен был выглядеть следующим образом:
int lockWidth = (int)Math.Round(width * xScale);
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3065. |
Анализатор обнаружил подозрительную передачу аргументов в метод. Возможно некоторые аргументы перепутаны местами.
Пример подозрительного кода:
void SetARGB(byte a, byte r, byte g, byte b)
{ .... }
void Foo(){
byte A = 0, R = 0, G = 0, B = 0;
....
SetARGB(A, R, B, G);
....
}
Во время задания цвета объекта, перепутали синий и зелёный цвет.
Исправленный вариант кода должен выглядеть следующим образом:
SetARGB(A, R, G, B);
Рассмотри более сложный пример опечатки из реального проекта:
public virtual string Qualify(string catalog,
string schema,
string table)
{ .... }
public Table AddDenormalizedTable(....) {
string key = subselect ??
dialect.Qualify(schema, catalog, name);
....
}
Согласно логике, можем предположить, что код должен выглядеть следующим образом:
public Table AddDenormalizedTable(....) {
string key = subselect ??
dialect.Qualify(catalog, schema, name);
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3066. |
Анализатор обнаружил подозрительное место в коде - возможно, забытый или некорректно закомментированный блок else.
Данную ситуацию лучше всего разобрать на примерах.
if (!x)
t = x;
else
z = t;
В этом примере форматирование кода не совпадает с его логикой: выражение "z = t;" выполнится лишь в случае, если (x == false) - вряд ли это имелось в виду. Подобная же ситуация может возникнуть при неудачно закомментированном фрагменте кода:
if (!x)
t = x;
else
//t = -1;
z = t;
В данном случае требуется либо исправить форматирование, превратив его в более удобочитаемое, либо исправить логическую ошибку, добавив недостающую ветвь оператора if.
Иногда встречаются случаи, в которых тяжело определить, является ли подобный код некорректным или таким образом стилизованным. Анализатор пытается уменьшать количество ложных срабатываний, связанных со стилизацией, не выдавая предупреждение, если код отформатирован с помощью и пробелов, и символов табуляции. Причём символов табуляции в разных строках содержится разное количество.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3067. |
Анализатор обнаружил потенциально возможную ошибку внутри конструктора класса - вызов перегружаемого (виртуального или абстрактного) метода.
Приведём пример, когда такая ситуация может привести к ошибке:
abstract class Base
{
protected Base()
{
Initialize();
}
protected virtual void Initialize()
{
...
}
}
class Derived : Base
{
Logger _logger;
public Derived(Logger logger)
{
_logger = logger;
}
protected override void Initialize()
{
_logger.Log("Initializing");
base.Initialize();
}
}
В нашем примере, в конструкторе абстрактного класса Base присутствует вызов виртуального метода Initialize. В классе Derived, унаследованном от класса Base, мы переопределяем метод Initialize, и используем в переопределённом методе поле _logger, которое инициализируется в конструкторе класса Derived.
Однако, при создании экземпляра класса Derived сначала будет выполнен конструктор самого базового класса в цепочке наследования (в нашем случае это Base). Но при вызове из конструктора Base метода Initialize, будет выполнен метод Initialize непосредственно у того объекта, который мы создаём в runtime - т.е. метод Initialize у класса Derived. Обратите внимание, что при выполнении метода Initialize поле _logger окажется ещё неинициализированным - создание экземпляра класса Derived из нашего примера приведёт к исключению NullReferenceException.
Таким образом, вызов переопределяемых методов в конструкторе потенциально может примести к выполнению методов объекта, инициализация которого ещё не завершена.
Для того, чтобы исправить предупреждение PVS-Studio, пометьте вызываемый метод (или класс, его содержащий), как sealed, либо снимите его пометку, как виртуального.
В случае, если вы точно уверены, что вам нужно описанное поведение при инициализации объекта и вы хотите скрыть предупреждение от анализатора, пометьте сообщение как ложнопозитивное. Информация о том, как это сделать, вы можете посмотреть в нашей документации.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3068. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что два последовательно идущих оператора 'if' оказались разделены закомментированной строкой. Высока вероятность, что неаккуратно был закомментирован фрагмент кода. Неаккуратность привела к тому, что существенно изменилась логика работы программы.
Рассмотрим пример:
if(!condition)
//condition = GetCondition();
if(condition)
{
...
}
Программа потеряла смысл. Условие второго оператора 'if' никогда не выполняется. Корректный вариант кода:
//if(!condition)
//condition = GetCondition();
if(condition)
{
...
}
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что член класса будет инициализирован не тем значением, которое предполагалось.
Рассмотрим пример:
class AClass {
static int A = B + 1;
static int B = 10;
}
В данном случае поле 'A' будет инициализировано значением '1', а не '11', как мог предположить программист. Дело в том, что на момент инициализации поля 'A', значение поля 'B' будет равно '0'. Это обусловлено тем, что изначально все члены типа (класса или структуры) инициализируются значениями по умолчанию ('0' для числовых типов, 'false' - для логического, и 'null' для ссылочных). И только после этого будет выполняться инициализация, установленная программистом. Решением проблемы будет изменение порядка следования полей:
class AClass {
static int B = 10;
static int A = B + 1;
}
В таком случае на момент инициализации поля 'A', 'B' будет содержать в себе значение '10', как и предполагалось.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3070. |
Анализатор обнаружил возврат из функции объекта, который используется в операторе using.
Пример подозрительного кода:
public FileStream Foo(string path)
{
using (FileStream fs = File.Open(path, FileMode.Open))
{
return fs;
}
}
Т.к. переменная была проинициализирована в блоке using, то перед возвратом из метода у данной переменной будет вызван метод Dispose. Вследствие этого, использовать объект, который вернет функция, может быть небезопасно.
Вызов метода Dispose произойдет по причине того, что вышеописанный код будет преобразован компилятором таким образом:
public FileStream Foo(string path)
{
FileStream fs = File.Open(path, FileMode.Open)
try
{
return fs;
}
finally
{
if (fs != null)
((IDisposable)fs).Dispose();
}
}
Исправленный вариант кода может выглядеть следующим образом:
public FileStream Foo(string path)
{
return File.Open(path, FileMode.Open)
}
Анализатор обнаружил в классе, не реализующим интерфейс 'IDisposable', поля или свойства, которые имеют тип реализующий интерфейс 'IDisposable'. Подобный код говорит о том, что программист, возможно, забывает очищать некоторые ресурсы после использования объекта своего класса.
Пример подозрительного кода:
class Logger
{
FileStream fs;
public Logger() {
fs = File.OpenWrite("....");
}
}
В данном случае, класс-обёртка, позволяющий писать лог в файл, не реализует интерфейс 'IDisposable'. При этом он содержит переменную типа 'FileStream', позволяющую осуществлять запись в файл. В данном случае переменная 'fs' будет держать файл до тех пор, пока не вызовется метод Finalize у объекта 'fs' (это произойдёт, когда объект будет очищаться сборщиком мусора). Как следствие, мы получим плавающую ошибку доступа, к примеру, когда попытаемся открыть этот же файл из другого потока.
Исправить проблему можно несколькими способами. Самым правильным является следующий:
class Logger : IDisposable
{
FileStream fs;
public Logger() {
fs = File.OpenWrite("....");
}
public void Dispose() {
fs.Dispose();
}
}
Но не всегда логика программы позволяет реализовать в классе 'Logger' интерфейс 'IDisposable'. Анализатор проверят множество ситуаций и сокращает число ложных срабатываний. Например, в данном случае, будет достаточно закрыть 'FileStream', который пишет в файл в отдельной функции.
class Logger
{
FileStream fs;
public Logger() {
fs = File.OpenWrite("....");
}
public void Close() {
fs.Close();
}
}
При проверке класса, реализующего интерфейс 'IDisposable', была выявлена потенциальная ошибка. В методе 'Dispose', данного класса, не для всех полей, тип которых реализует интерфейс 'IDisposable', производится вызов метода 'Dispose'. Высока вероятность, что разработчик забыл очистить некоторые ресурсы после их использования.
Пример подозрительного кода:
class Logger : IDisposable
{
FileStream fs;
public Logger() {
fs = File.OpenWrite("....");
}
public void Dispose() { }
}
В приведенном примере присутствует класс-обёртка 'Logger', реализующий интерфейс 'IDisposable', который позволяет писать информацию в лог-файл. Данный класс содержит переменную 'fs' с помощью которой и осуществляется запись в сам файл. Вследствие того, что в методе 'Dispose' класса 'Logger' забыли вызвать методы 'Dispose' или 'Close' возможно возникновение следующей ошибки.
Допустим, объект класс 'Logger' был создан в блоке 'using':
using(Logger logger = new Logger()){
....
}
Вследствие этого, перед выходом из блока 'using' будет вызван метод 'Dispose' для объекта 'logger'.
Данное использование предполагает, что все ресурсы, используемые объектом класса 'Logger', освобождены и можно повторно к ним обращаться.
Но в нашем случае, поток 'fs', который пишет в файл, не будет закрыт. И при попытке повторного доступа к этому файлу, например, из другого потока, возможно возникновение ошибка доступа.
Данная ошибка будет иметь "плавающий" характер, поскольку объект 'fs', при очистке его сборщиком мусора, высвободит открытый файл. Однако, сборка объекта является недетерминированным событием, она не произойдёт гарантированно после выхода объекта logger из блока using. Ошибка доступа к файлу возникнет, если файл будет открыт до того, как сборщик мусора успеет очистить объект 'fs'.
Для исправления подобной проблемы, достаточно вызвать 'fs.Dispose()' в методе 'Dispose' класса 'Logger':
class Logger : IDisposable
{
FileStream fs;
public Logger() {
fs = File.OpenWrite("....");
}
public void Dispose() {
fs.Dispose();
}
}
Это гарантирует в приведённом ранее примере, что файл, открытый объектом 'fs', будет освобождён на момент выхода из блока using.
Анализатор обнаружил в классе, не реализующем интерфейс 'IDisposable', метод с именем 'Dispose'. В данном случае, возможны два варианта развития событий.
Наиболее распространённым случаем является, простое несоответствие соглашениям о написании кода Microsoft. Метод c данным именем является реализацией стандартного интерфейса 'IDisposable' и применяется для детерминированной очистки ресурсов, в том числе unmanaged (неуправляемых) ресурсов.
Пример подозрительного кода:
class Logger
{
....
public void Dispose()
{
....
}
}
Согласно соглашениям, метод 'Dispose' используется для очистки ресурсов. И его наличие предполагает, что сам класс реализует интерфейс IDisposable. В данном случае есть два варианта решения проблемы.
1) Дописать в объявлении класса реализацию интерфейса IDisposable:
class Logger : IDisposable
{
....
public void Dispose()
{
....
}
}
Это позволит вам использовать объекты класса 'Logger' в блоке 'using', гарантирующем вызов метода Dispose при выходе из блока.
using(Logger logger = new Logger()){
....
}
2) Исправить имя метода на нейтральное. Например, на 'Close':
class Logger
{
....
public void Close()
{
....
}
}
Второй случай, где срабатывает данное предупреждение, таит в себе опасность неверного вызова метода, в случае приведения класса к интерфейсу 'IDisposable'.
Пример подозрительного кода:
class A : IDisposable
{
public void Dispose()
{
Console.WriteLine("Dispose A");
}
}
class B : A
{
public new void Dispose()
{
Console.WriteLine("Dispose B");
}
}
В случае приведения объекта класса 'B' к интерфейсу 'IDisposable' или его использование в блоке 'using'. Например, так:
using(B b = new B()){
....
}
Будет вызвана функция 'Dispose' из класса 'A', т.е. освобождение ресурсов класса 'B' не произойдёт.
Для вызова верной функции из класса 'B', нужно дополнительно реализовать в классе 'B' интерфейс 'IDisposable'. В таком случае, при приведении объекта класса 'B' к интерфейсу 'IDisposable' или его использование в блоке 'using', будет вызвана функция 'Dispose' именно из него.
Пример исправленного кода:
class B : A, IDisposable
{
public new void Dispose()
{
Console.WriteLine("Dispose B");
base.Dispose();
}
}
Анализатор обнаружил потенциальную ошибку, связанную с тем, что одна из операций '!', '~', '-' или '+' повторяется два или более раз. Такая ошибка может произойти в случае опечатки. Такое дублирование операторов бессмысленно и может содержать ошибку.
Рассмотрим пример некорректного кода:
if (!(( !filter )))
{
....
}
Скорее всего, такая ошибка возникла после проведения рефакторинга кода. Например, была удалена часть сложного логического выражения, а отрицание всего результата осталось. В итоге, получилось противоположное по смыслу выражение.
Корректный вариант кода:
if ( filter )
{
....
}
Или:
if ( !filter )
{
....
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3075. |
Анализатор обнаружил сравнение переменной типа float или double с float.NaN или double.NaN. В соответствии с документацией, если два значения double.NaN сравниваются с помощью оператора == , то результатом будет false. Таким образом какое бы значение типа double ни сравнивали с double.NaN, в результате всегда будет false.
Рассмотрим пример:
void Func(double d) {
if (d == double.NaN) {
....
}
}
Проверка на NaN с помощью операторов == и != некорректна. Вместо этого следует использовать методы float.IsNaN() или double.IsNaN(). Исправленный вариант:
void Func(double d) {
if (double.IsNaN(d)) {
....
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3076. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что в аксессорах свойств и событий не используется параметр 'value'.
Рассмотрим пример некорректного кода:
private bool _visible;
public bool IsVisible
{
get { return _visible; }
set { _visible = true; }
}
Скорее всего, при задании нового значения свойству "IsVisible" планировали сохранить результат в переменную "_visible", но допустили ошибку, из-за чего изменение свойства никак не будет влиять на состояние объекта.
Корректный вариант кода:
public bool IsVisible
{
get { return _visible; }
set { _visible = value; }
}
Анализатор может выдавать предупреждения на код следующего вида:
public bool Unsafe {
get { return (flags & Flags.Unsafe) != 0; }
set { flags |= Flags.Unsafe; }
}
В этом случае метод 'set' используется для изменения состояния флага и в коде ошибки нет, но использование такого свойства может вводить в заблуждение разработчика, т.к. результат при присваиваниях "myobj.Unsafe = true" и "myobj.Unsafe = false" будет одинаковый.
Для сброса состояния внутренней переменной лучше воспользоваться функцией, а не свойством:
public bool Unsafe
{
get { return (flags & Flags.Unsafe) != 0; }
}
public void SetUnsafe()
{
flags |= Flags.Unsafe;
}
Если от использования такого свойства нельзя отказаться, то можно пометить это место комментарием специального вида " //-V3077" и анализатор в дальнейшем не будет выдавать предупреждение для этого свойства:
public bool Unsafe {
get { return (flags & Flags.Unsafe) != 0; }
set { flags |= Flags.Unsafe; } //-V3077
}
Все способы подавления ложных предупреждений описаны в документации.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3077. |
Анализатор обнаружил потенциальную ошибку: два раза подряд вызываются методы 'OrderBy' или 'OrderByDescending'. Результат такой сортировки может отличаться от ожидаемого.
Рассмотрим пример:
var seq = points.OrderBy(item => item.Primary)
.OrderBy(item => item.Secondary);
Допустим, программист хотел отсортировать коллекцию так, чтобы элементы были сгруппированы и отсортированы по 'Primary', а внутри каждая группа была отсортирована по 'Secondary'.
Но на самом деле элементы коллекции будут сгруппированы и отсортированы по 'Secondary', а внутри каждая группа будет отсортирована по 'Primary'.
Для получения ожидаемого поведения второй вызов 'OrderBy' следует заменить на вызов метода 'ThenBy'.
var seq = points.OrderBy(item => item.Primary)
.ThenBy(item => item.Secondary);
Используя два вызова 'OrderBy', получить предполагаемое поведение также можно, но для этого необходимо поменять вызовы местами:
var seq = points.OrderBy(item => item.Secondary)
.OrderBy(item => item.Primary);
Аналогичную ошибку можно допустить при написании кода с использованием синтаксиса запроса:
var seq = from item in points
orderby item.Primary
orderby item.Secondary
select item;
Код можно исправить следующим образом:
var seq = from item in points
orderby item.Primary, item.Secondary
select item;
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3078. |
Анализатор обнаружил подозрительное объявление нестатического поля, к которому применяется атрибут 'ThreadStatic'.
Применение данного атрибута к статическому полю позволяет устанавливать индивидуальное значение этого поля для каждого потока. К тому же это запрещает совместное использование данного поля в разных потоках, что исключает возможность блокировок при обращении к полю. Однако при применении данного атрибута к нестатическому полю, атрибут будет проигнорирован.
Рассмотрим пример:
[ThreadStatic]
bool m_knownThread;
Скорее всего данное поле является флагом, который должен иметь своё значение для каждого потока. Но так как поле не является статическим, применение данного атрибута не имеет смысла. Если все же логика работы программы основывается на факте, что значение поля должно быть уникальным для каждого потока (исходя из названия и наличия атрибута 'ThreadStatic'), возможна ошибка.
Для исправления ошибки необходимо добавить модификатор 'static' к объявлению поля:
[ThreadStatic]
static bool m_knownThread;
Дополнительная информация:
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3079. |
Анализатор обнаружил фрагмент кода, который может привести к доступу по нулевой ссылке.
Рассмотрим несколько примеров, для которых анализатор выдает диагностическое сообщение V3080:
if (obj != null || obj.Func()) { ... }
if (obj == null && obj.Func()) { ... }
if (list == null && list[3].Func()) { ... }
Во всех условиях допущена логическая ошибка, которая приведет к доступу по нулевой ссылке. Ошибка может быть допущена при рефакторинге кода или из-за случайно опечатки.
Корректные варианты:
if (obj == null || obj.Func()) { .... }
if (obj != null && obj.Func()) { .... }
if (list != null && list[3].Func()) { .... }
Конечно, это очень простые ситуации. На практике проверка объекта на null и его использование может находиться в разных местах. Если анализатор выдал предупреждение V3080, изучите код расположенный выше и попробуйте понять, почему ссылка может быть нулевой.
Пример кода, где проверка и использование объекта находятся в разных строках
if (player == null) {
....
var identity = CreateNewIdentity(player.DisplayName);
....
}
Анализатор предупредит, об опасности в строке внутри блока 'if'. Здесь или некорректно написано условие, или вместо 'player' должна использоваться другая переменная.
Программисты иногда забывают о том, что при проверке двух объектов на null один из них может оказаться нулевым, а второй нет, в результате чего будет вычислено всё условие и произойдёт доступ по нулевой ссылке. Например,
if ((text == null && newText == null) || text.Equals(newText)) {
....
}
Это условие можно переписать, например, так
if ((text == null && newText == null) ||
(text != null && newText != null && text.Equals(newText))) {
....
}
Ещё один способ допустить ошибку заключается в использовании оператора логического AND (&) вместо условного AND (&&). Нужно помнить о том что, во-первых, при использовании логического AND всегда вычисляются обе части выражения, а во-вторых приоритет у логического AND выше чем у условного AND.
Пример:
public static bool HasCookies {
get {
var context = HttpContext;
return context != null
&& context.Request != null & context.Request.Cookies != null
&& context.Response != null && context.Response.Cookies != null;
}
}
В этом примере обращение к context.Request.Cookies будет выполнено даже если context.Request равен null.
Также опасной является ситуация с разыменованием параметра, для которого значением по умолчанию является 'null'. Пример:
public NamedBucket(string name, List<object> items = null)
{
_name = name;
foreach (var item in items)
{
....
}
}
Конструктор принимает коллекцию 'items' в качестве необязательного параметра. Однако если при вызове значение для 'items' не будет передано, то при попытке обхода коллекции в 'foreach' будет выброшено исключение типа 'NullReferenceException'.
В зависимости от ситуации решение этой проблемы может отличаться. Например, можно производить обход коллекции только в случае, если она не равна 'null'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3080. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что при написании двух и более вложенных циклов 'for', из-за опечатки не используется счётчик одного из циклов.
Рассмотрим синтетический пример некорректного кода:
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += matrix[i, i];
В коде планировали обойти все элементы матрицы и найти их сумму, но случайно написали переменную 'i' вместо 'j' при обращении к матрице.
Корректный вариант кода:
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += matrix[i, j];
В отличие от диагностических правил V3014, V3015 и V3016, здесь анализатор ищет ошибки с использованием индекса только в теле циклов.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3081. |
Анализатор обнаружил подозрительный фрагмент кода, в котором создаётся объект типа 'Thread', но при этом созданный поток не запускается.
Рассмотрим пример:
void Foo(ThreadStart action)
{
Thread thread = new Thread(action);
thread.Name = "My Thread";
}
В данном коде создаётся объект типа 'Thread', ссылка на который записывается в переменную 'thread'. Но при этом поток не запускается и никуда не передаётся. Таким образом, созданный объект просто будет удалён при следующей уборке мусора, при этом никак не использовавшись.
Для исправления ситуации у объекта необходимо вызвать метод 'Start', который запустит поток на выполнение. Тогда исправленный код может выглядеть так:
void Foo(ThreadStart action)
{
Thread thread = new Thread(action);
thread.Name = "My Thread";
thread.Start();
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально небезопасный вызов обработчика события (event). Возможно возникновение исключения 'NullReferenceException'.
Рассмотрим пример опасного кода:
public event EventHandler MyEvent;
void OnMyEvent(EventArgs e)
{
if (MyEvent != null)
MyEvent(this, e);
}
В данном примере происходит проверка на 'null' поля 'MyEvent', и затем происходит вызов данного события. Проверка на 'null' позволит избежать исключения в случае, если на событие никто не подписан на момент его вызова (в такой ситуации 'MyEvent' будет иметь значение 'null').
Однако, представим, что у события 'MyEvent' есть один подписчик. И в момент между проверкой на null и непосредственными вызовом обработчика события по 'MyEvent()' существует вероятность, что будет произведена отписка от события, например, в другом потоке:
MyEvent -= OnMyEventHandler;
Теперь, если обработчик 'OnMyEventHandler' был единственным подписчиком события 'MyEvent', поле 'MyEvent' примет значение 'null'. Но, т.к., в другом потоке, где событие должно будет быть вызвано, в нашем гипотетическом примере проверка на 'null' уже произошла, будет выполнена следующая строка: 'MyEvent()'. Это приведёт к возникновению исключения 'NullReferenceException'.
Как мы видим, одной проверки на 'null' недостаточно, чтобы гарантировать безошибочное выполнение вызова события. Существует несколько способов, позволяющих избежать описанной выше потенциальной ошибки. Рассмотрим их.
Первый способ заключается в создании временной локальной переменной, в которую мы сохраняем ссылку на обработчики нашего события:
public event EventHandler MyEvent;
void OnMyEvent(EventArgs e)
{
EventHandler handler = MyEvent;
if (handler != null)
handler(this, e);
}
Это позволит выполнить обработчики события без возникновения исключения. Даже если между проверкой 'handler' на 'null' и вызовом обработчика произойдёт отписка от события, как в нашем первом примере, переменная 'handler' всё равно будет содержать ссылку на первоначальный обработчик. И он будет корректно вызван несмотря на то, событие 'MyEvent' уже не содержит данный обработчик.
Другой способ избежать ошибки - изначально присвоить событию пустой обработчик в виде анонимного метода или лямбда выражения:
public event EventHandler MyEvent = (sender, args) => {};
Такой код гарантирует, что поле 'MyEvent' никогда не будет иметь значения 'null', т.к. такой анонимный метод невозможно будет отписать (если он конечно не сохранён в отдельной переменной), что также позволяет избавиться от проверки на null перед вызовом события.
Наконец, начиная с версии языка C# 6.0 (Visual Studio 2015), можно обеспечить безопасный вызов события с помощью оператора '?.':
MyEvent?.Invoke(this, e);
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3083. |
Анализатор обнаружил потенциально ошибочную отписку от события с использованием анонимных функций.
Приведём пример некорректного кода:
public event EventHandler MyEvent;
void Subscribe()
{
MyEvent += (sender, e) => HandleMyEvent(e);
}
void UnSubscribe()
{
MyEvent -= (sender, e) => HandleMyEvent(e);
}
В приведённом примере объявлены методы 'Subscribe' и 'UnSubscribe' для, соответственно, подписки и отписки от события 'MyEvent'. В качестве обработчика события используется лямбда выражение. Подписка на событие в методе 'Subscribe' будет успешно выполнена - обработчик (анонимная функция) будет добавлен в событие.
Однако, метод 'UnSubscribe' не выполнит отписку ранее назначенного в методе 'Subscribe' обработчика. После выполнения данного метода, событие 'MyEvent' по-прежнему будет содержать обработчик, назначенный в 'Subscribe'.
Такое поведение обусловлено тем, что каждое объявление анонимной функции приводит к созданию отдельного экземпляра делегата, в нашем случае, с типом EventHandler. Таким образом, в методе 'Subscribe' была произведена подписка 'делегата 1', а в методе Unsubscribe производится отписка уже для 'делегата 2', несмотря на то, что тела данных делегатов идентичны. Так как наше событие на момент отписки содержит только 'делегат 1', отписка от 'делегата 2' никак не изменит значения 'MyEvent'.
Для того, чтобы корректно использовать анонимные функции при подписке на события (если потребуется дальнейшая отписка), можно сохранять обработчик-лямбду в отдельную переменную, и использовать её как при подписке, так и при отписке от события:
public event EventHandler MyEvent;
EventHandler _handler;
void Subscribe()
{
_handler = (sender, e) => HandleMyEvent(sender, e);
MyEvent += _handler;
}
void UnSubscribe()
{
MyEvent -= _handler;
}
Анализатор обнаружил во вложенном классе поле или свойство, которое имеет такое же имя, что и статическое/константное поле или свойство во внешнем классе.
Пример подозрительного кода:
class Outside
{
public static int index;
public class Inside
{
public int index; // <= Одноименное поле
public void Foo()
{
index = 10;
}
}
}
Использование подобной конструкции может привести к ошибочной работе программы. Наиболее опасен следующий сценарий. Допустим, изначально в классе 'Inside' не было поля 'index'. Это значит, что функция 'Foo' изменяла именно статическую переменную 'index' во внешнем классе 'Outside'. Когда мы добавили поле 'index' в класс 'Inside', то, без явного указания имени внешнего класса, функция 'Foo' станет изменять поле 'index' во внутреннем классе. Естественно, данный код начнёт работать не так, как первоначально было задумано разработчиком, хотя и не вызовет нареканий со стороны компилятора.
Исправить ошибку можно переименованием переменной:
class Outside
{
public static int index;
public class Inside
{
public int insideIndex;
public void Foo()
{
index = 10;
}
}
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3085. |
Анализатор обнаружил потенциальную ошибку, найдя в коде инициализацию двух различных переменных одинаковыми выражениями. Анализатор считает опасными не все выражения, а только в которых используется вызов функций (либо слишком длинное выражение).
Рассмотрим наиболее простой случай:
x = X();
y = X();
Возможны три варианта действий:
Теперь рассмотрим реальный пример:
string frameworkPath =
Path.Combine(tmpRootDirectory, frameworkPathPattern);
string manifestFile =
Path.Combine(frameworkPath, "sdkManifest.xml");
string frameworkPath2 =
Path.Combine(tmpRootDirectory, frameworkPathPattern2);
string manifestFile2 =
Path.Combine(frameworkPath, "sdkManifest.xml");
В данном коде закралась ошибка copy-paste, которую сложно заметить на первый взгляд. На самом деле, при получении сроки 'manifestFile2', ошибочно передали в функцию 'Path.Combine' первую часть пути. Согласно логике, в коде должна использоваться переменная 'frameworkPath2', а не 'frameworkPath', которая там сейчас находится.
Исправленный код должен выглядеть так:
string manifestFile2 =
Path.Combine(frameworkPath2, "sdkManifest.xml");
Анализатор обнаружил потенциальную ошибку в цикле 'foreach'. Существует вероятность возникновения исключения InvalidCastException при обходе коллекции 'IEnumarable<T>'.
Рассмотрим пример:
List<object> numbers = new List<object>();
....
numbers.Add(1.0);
....
foreach (int a in numbers)
Console.WriteLine(a);
В данном случае, шаблон коллекции 'numbers' инициализирован типом 'object', тем самым позволяя добавлять в неё объекты любого типа.
При обходе этой коллекцией, в цикле задано, что её члены должны иметь тип 'int'. Если в коллекции окажется объект другого типа, будет выполнено приведение и возможно возникновение исключения 'InvalidCastException'. В нашем примере исключение возникнет из-за невозможности распаковки (Unboxing) значения типа 'double', упакованного в элементе типа 'object' коллекции, в тип 'int'.
Для исправления данной ошибки можно привести тип шаблона коллекции и тип элемента в цикле 'foreach' к одному типу:
Вариант 1:
List<object> numbers = new List<object>();
....
foreach (object a in numbers)
Вариант 2:
List<int> numbers = new List<int>();
....
foreach (int a in numbers)
Часто подобная ошибка наблюдается, когда имеется коллекция с элементами базового интерфейса, а в цикле указывают тип одного из интерфейсов или классов, реализующих данный базовый интерфейс:
void Foo1(List<ITrigger> triggers){
....
foreach (IOperableTrigger trigger in triggers)
....
}
void Foo2(List<ITrigger> triggers){
....
foreach (IMutableTrigger trigger in triggers)
....
}
Для обхода в коллекции объектов только одного конкретного типа, можно предварительно отфильтровать её с помощью функции 'OfType':
void Foo1(List<ITrigger> triggers){
....
foreach (IOperableTrigger trigger in
triggers.OfType<IOperableTrigger>())
....
}
void Foo2(List<ITrigger> triggers){
....
foreach (IMutableTrigger trigger in
triggers.OfType<IMutableTrigger>())
....
}
Такой код гарантирует, что цикл foreach будет работать только с объектами нужного типа, и исключит возможность возникновения исключения 'InvalidCastException'.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3087. |
Анализатор обнаружил двойные круглые скобки вокруг выражения. Есть вероятность, что одна из скобок поставлена не там, где надо.
Хочется подчеркнуть, что анализатор не просто ищет фрагменты кода, где два раза повторяются круглые скобки, а пытается обнаружить те ситуации, когда, изменив местонахождение одной скобки, можно изменить смысл выражения. Рассмотрим пример:
if((!isLowLevel|| isTopLevel))
Этот код подозрителен. Непонятно, зачем здесь дополнительные круглые скобки. Возможно, выражение должно выглядеть так:
if(!(isLowLevel||isTopLevel))
Если даже выражение корректно, то всё равно лучше убрать лишние круглые скобки. На это есть две причины:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3088. |
Анализатор обнаружил подозрительный фрагмент кода, в котором поле, отмеченное атрибутом [ThreadStatic] инициализируется при объявлении или в статическом конструкторе.
Если выполняется инициализация при объявлении, поле будет проинициализировано этим значением только у первого обратившегося потока. В остальных потоках это поле будет содержать значение, предусмотренное по умолчанию.
Схожая ситуация со статическим конструктором – он выполняется только один раз, и поле будет проинициализировано только в потоке, в котором статический конструктор выполнится.
Рассмотрим пример ситуации с инициализацией при объявлении:
class SomeClass
{
[ThreadStatic]
public static Int32 field = 42;
}
class EntryPoint
{
static void Main(string[] args)
{
new Task(() => { var a = SomeClass.field; }).Start(); // a == 42
new Task(() => { var a = SomeClass.field; }).Start(); // a == 0
new Task(() => { var a = SomeClass.field; }).Start(); // a == 0
}
}
При обращении первого потока к полю 'field' оно будет проинициализировано заданным программистом значением. Таким образом, переменная 'a' будет иметь значение '42', равно как и поле 'field'.
При запуске последующих потоков и обращении к полю, оно уже будет проинициализировано значением по умолчанию ('0' в данном случае), поэтому во всех последующих потоков значение 'a' будет равно '0'.
Как упоминалось ранее, статический конструктор не является решением проблемы – он будет вызван 1 раз, при инициализации типа, поэтому проблема остаётся актуальной.
Решением может послужить использование свойства в качестве обёртки над полем, куда можно дописать дополнительную логику по инициализации поля. Это решает проблему, но опять же, не полностью – при обращении к полю, а не свойству (например, внутри класса), остаётся вероятность получить некорректное значение.
class SomeClass
{
[ThreadStatic]
private static Int32 field = 42;
public static Int32 Prop
{
get
{
if (field == default(Int32))
field = 42;
return field;
}
set
{
field = value;
}
}
}
class EntryPoint
{
static void Main(string[] args)
{
new Task(() => { var a = SomeClass.Prop; }).Start(); // a == 42
new Task(() => { var a = SomeClass.Prop; }).Start(); // a == 42
new Task(() => { var a = SomeClass.Prop; }).Start(); // a == 42
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3089. |
Анализатор обнаружил фрагмент кода с использованием небезопасной блокировки.
Данная диагностика срабатывает на ряд случаев:
Первые три случая могут привести к возникновению взаимной блокировки, два последних - к отсутствию желаемой синхронизации. Общая суть первых трёх случаев состоит в том, что к объекту, используемому для блокировки, имеется общий доступ. Такой объект может быть использован для блокировки в другом месте без ведома разработчика, использовавшего объект для блокировки в первый раз. Это, в свою очередь, создаёт вероятность возникновения взаимоблокировки на один и тот же объект.
Блокировка с использованием 'this' небезопасна, если класс не является приватным. Тогда в другом месте программист может установить блокировку на этот же объект после создания его экземпляра.
Аналогичная ситуация с публичными членами классов.
Для решения описанных выше ситуаций достаточно использовать, например, приватное поле класса в качестве объекта блокировки.
Приведём пример небезопасного кода с использованием оператора 'lock' и 'this' в качестве объекта блокировки:
class A
{
void Foo()
{
lock(this)
{
// do smt
}
}
}
Для того, чтобы избежать возможных взаимных блокировок, в качестве объекта блокировки стоит использовать, например, приватное поле:
class A
{
private Object locker = new Object();
void Foo()
{
lock(locker)
{
// do smt
}
}
}
С экземплярами класса 'Type', 'MemberInfo', 'ParameterInfo' ситуация несколько опаснее, так как здесь вероятность взаимной блокировки выше. Используя оператор 'typeof' или метод 'GetType', метод 'GetMember' и т.п. для разных экземпляров одного типа, результат будет одинаков - один и тот же экземпляр данного класса.
Отдельно стоят объекты типов 'String' и 'Thread'.
Доступ к объектам этих типов можно получить из любого места в программе, и даже из другого домена приложения (Application Domain), что ещё больше увеличивает опасность взаимной блокировки. Решение - не использовать в качестве объектов блокировки экземпляры этих типов.
Рассмотрим пример возникновения взаимной блокировки. Пусть имеется следующий код в каком-нибудь приложении (Sample.exe):
static void Main(string[] args)
{
var thread = new Thread(() => Process());
thread.Start();
thread.Join();
}
static void Process()
{
String locker = "my locker";
lock (locker)
{
....
}
}
В другом приложении есть код следующего вида:
String locker = "my locker";
lock (locker)
{
AppDomain domain = AppDomain.CreateDomain("test");
domain.ExecuteAssembly(@"C:\Sample.exe");
}
Результатом выполнения этого кода будет взаимная блокировка, возникшая в результате использования в качестве объекта блокировки экземпляра класса 'String'.
Мы создаём новый домен в рамках того же процесса и пытаемся выполнить в нём сборку, содержащуюся в другом файле (Sample.exe). В итоге возникает ситуация, когда оба оператора 'lock' используют в качестве объекта блокировки одинаковый строковый литерал. Для строковых литералов срабатывает механизм интернирования строк, за счёт которого в обоих случаях будут получены ссылки на один и тот же объект в памяти. Как следствие - оба оператора 'lock' выполняют блокировку по одному и тому же объекту, что и привело к взаимоблокировке.
Эта ошибка могла бы проявить себя и при исполнении в рамках одного домена.
Аналогичная ситуация с типом 'Thread', экземпляр которого можно легко получить, например, с помощью свойства 'Thread.CurrentThread'.
Решение - не использовать в качестве объектов блокировки объекты типов 'Thread' и 'String'.
Блокировка с использованием объекта значимого типа приведёт к тому, что синхронизация потоков осуществляться не будет. Стоит отметить, что конструкция 'lock' не позволяет выполнять блокировку на объектах значимого типа, но класс 'Monitor' с его методами 'Enter' и 'TryEnter' от этого не застрахован.
Методы 'Enter' и 'TryEnter' ожидают в качестве параметра объект типа 'Object', поэтому, если в метод передаётся объект значимого типа, будет выполнена его 'упаковка'. Это значит, что каждый раз для блокировки будет создаваться новый объект, следовательно - блокировка будет устанавливаться (и сниматься) по этому новому объекту. Результат - отсутствие желаемой синхронизации.
Рассмотрим пример ошибочного кода:
sealed class A
{
private Int32 m_locker = 10;
void Foo()
{
Monitor.Enter(m_locker);
// Do smt...
Monitor.Exit(m_locker);
}
}
Программист хотел установить блокировку по приватному полю 'm_locker'. На самом же деле блокировка будет устанавливаться (и сниматься) не по желаемому полю, а по вновь созданным объектам, полученным в результате 'упаковки'.
Для исправления данной ошибки достаточно изменить тип поля 'm_locker' на допустимый ссылочный, например - 'Object'. Тогда пример корректного кода выглядел бы так:
sealed class A
{
private Object m_locker = new Object();
void Foo()
{
Monitor.Enter(m_locker);
// Do smt...
Monitor.Exit(m_locker);
}
}
Схожая ошибка проявится и при использовании конструкции 'lock', если выполняется упаковка объекта в результате приведения:
Int32 val = 10;
lock ((Object)val)
{ .... }
В этом коде блокировка будет устанавливаться по объектам, полученным в результате упаковки. Так как в результате упаковки будут создаваться новые объекты, синхронизации потоков не будет.
Ошибочным является блокировка по вновь создаваемым объектам. Пример подобного кода может выглядеть так:
lock (new Object())
{ .... }
или так
lock (obj = new Object())
{ .... }
Так как при выполнении этого кода каждый раз создаются новые объекты, блокировка также будет осуществляться по разным объектам, следовательно, потоки не будут синхронизироваться.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3090. |
Если анализатор находит два одинаковых строковых литерала, он пытается понять, является ли это последствием неудачного Copy-Paste. Хочется сразу предупредить, диагностика основана на эмпирическом алгоритме и иногда может выдавать странные ложные срабатывания.
Рассмотрим пример ошибки:
string left_str = "Direction: left.";
string right_str = "Direction: right.";
string up_str = "Direction: up.";
string down_str = "Direction: up.";
Код писался с использованием Copy-Paste. В конце забыли изменить строковый литерал "up" на "down". Анализатор заподозрит неладное и укажет на подозрительное слово "up" в последней строчке.
Исправленный код:
string left_str = "Direction: left.";
string right_str = "Direction: right.";
string up_str = "Direction: up.";
string down_str = "Direction: down.";
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3091. |
Анализатор обнаружил потенциальную ошибку в условии. Программа должна совершать различные действия, в зависимости от того, в какой диапазон значений попадает некая переменная.
Для этого в коде используется следующая конструкция:
if ( MIN_A < X && X < MAX_A ) {
....
} else if ( MIN_B < X && X < MAX_B ) {
....
}
Анализатор выдаёт предупреждение, если диапазоны, проверяемые в условиях, пересекаются. Пример:
if ( 0 <= X && X < 10)
FooA();
else if ( 10 <= X && X < 20)
FooB();
else if ( 20 <= X && X < 300)
FooC();
else if ( 30 <= X && X < 40)
FooD();
Код содержит опечатку. Рука программиста дрогнула, и вместо условия "20 <= X && X < 30" была написано "20 <= X && X < 300". Если переменная X будет хранить, например, значение 35, то будет вызвана функция FooC(), а не FooD().
Исправленный код:
if ( 0 <= X && X < 10)
FooA();
else if ( 10 <= X && X < 20)
FooB();
else if ( 20 <= X && X < 30)
FooC();
else if ( 30 <= X && X < 40)
FooD();
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3092. |
Анализатор обнаружил возможную ошибку, связанную с тем, что в логическом выражении перепутаны операторы & и && или | и ||.
Условные операторы И (&&) / ИЛИ (||) вычисляют второй операнд при необходимости (см. Short circuit), в то время как операторы & и | всегда вычисляют оба операнда. Возможно, программист не планировал такое выполнение кода.
Рассмотрим пример некорректного кода:
if ((i < a.m_length) & (a[i] % 2 == 0))
{
sum += a[i];
}
Пусть объект 'a' это некий контейнер. Количество элементов в этом контейнере хранится в члене 'm_length'. Необходимо вычислить суммы чётных элементов, при этом надо контролировать, чтобы не выйти за границу массива.
Из-за опечатки, использовали не оператор '&&', а '&'. В результате, если индекс 'i' будет больше или равен 'a.m_length', произойдёт выход за границу массива при вычислении подвыражения (a[i] % 2 == 0). Независимо от того, истина или ложна левая часть выражения, правая часть в любом случае вычисляется.
Корректный вариант кода:
if ((i < a. m_length) && (a[i] % 2 == 0))
{
sum += a[i];
}
Ещё один пример некорректного кода может выглядеть следующим образом:
if (x > 0 | BoolFunc())
{
....
}
Вызов функции 'BoolFunc()' будет выполнен всегда, даже если условие '(x > 0)' будет истинным.
Корректный вариант кода:
if (x > 0 || BoolFunc())
{
....
}
Участки кода, выявленные с помощью диагностического правила V3093, не всегда могут содержать серьёзные ошибки, но часто выражения являются неоптимальными с точки зрения производительности (особенно, если в выражении вызываются сложные функции).
Если же условное выражение записано так, как был задумано разработчиком, то можно пометить это место комментарием специального вида "//-V3093" и анализатор в дальнейшем не будет выдавать это предупреждение:
if (x > 0 | BoolFunc()) //-V3093
{
....
}
Все способы подавления ложных предупреждений описаны в документации.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3093. |
Анализатор обнаружил подозрительный класс, реализующий интерфейс 'ISerializable', но не реализующий конструктор сериализации.
Конструктор сериализации применяется при десериализации объекта и принимает 2 параметра, имеющих типы 'SerializationInfo' и 'StreamingContext'. Наследование от данного интерфейса обязует программиста реализовать метод 'GetObjectData', но не обязует реализовывать конструктор сериализации. Однако его отсутствие приведёт к возникновению исключения типа 'SerializationException'.
Рассмотрим пример ошибочного кода. Пусть где-то определен метод, выполняющий сериализацию и десериализацию объекта:
static void Foo(MemoryStream ms, BinaryFormatter bf, C1 obj)
{
bf.Serialize(ms, obj);
ms.Position = 0;
obj = (C1)bf.Deserialize(ms);
}
А сам класс 'C1' определён следующим образом:
[Serializable]
sealed class C1 : ISerializable
{
public C1()
{ }
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("field", field, typeof(String));
}
private String field;
}
При попытке десериализовать объект будет сгенерировано исключение типа 'SerializationException'. Для того, чтобы объект типа 'C1' корректно проходил десериализацию, необходимо реализовать специальный конструктор. Тогда исправленное объявление класса будет выглядеть так:
[Serializable]
sealed class C1 : ISerializable
{
public C1()
{ }
private C1(SerializationInfo info, StreamingContext context)
{
field = (String)info.GetValue("field", typeof(String));
}
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("field", field, typeof(String));
}
private String field;
}
Примечание. Для данной диагностики доступна дополнительная настройка, задающаяся в конфигурационном файле (*.pvsconfig), имеющая следующий синтаксис:
//+V3094:CONF:{ IncludeBaseTypes: true }
В таком случае анализатор просматривает не только непосредственно реализацию интерфейса 'ISerializable' самим классом, но и каким-либо из базовых. По умолчанию эта настройка отключена.
Дополнительную информацию о конфигурационных файлах можно прочитать здесь.
Анализатор обнаружил потенциальную ошибку, которая может привести к доступу по нулевой ссылке.
Анализатор заметил в коде следующую ситуацию. В начале, объект используется. А уже затем этот объект проверяется на null. Это может означать одно из двух:
1) Возникнет ошибка, если объект будет равен null.
2) Программа всегда работает корректно, так как объект всегда не равен null. Проверка является лишней.
Рассмотрим первый вариант. Ошибка есть.
obj = Foo();
result = obj.Func();
if (obj == null) return -1;
Если объект 'obj' окажется равен null, то выражение 'obj.Func()' приведёт к ошибке. Анализатор выдаст предупреждение на этот код, указав 2 строки. Первая строка - это то место, где используется объект. Вторая строка - это то место, где объект сравнивается со значением null.
Исправленный вариант кода:
obj = Foo();
if (obj == null) return -1;
result = obj.Func();
Рассмотрим второй вариант. Ошибки нет.
Stream stream = CreateStream();
while (stream.CanRead)
{
....
}
if (stream != null)
stream.Close();
Этот код всегда работает корректно. Объект stream всегда не равен null. Однако анализатор не разобрался в этой ситуации и выдал предупреждение. Чтобы оно исчезло, следует удалить проверку "if (stream != null)". Она не имеет практического смысла и только может запутать программиста, читающего код.
Исправленный вариант:
Stream stream = CreateStream();
while (stream.CanRead)
{
....
}
stream.Close();
В случае если анализатор ошибается, то кроме изменения кода, можно использовать комментарий для подавления предупреждений. Пример: "obj.Foo(); //-V3095".
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3095. |
Анализатор обнаружил тип, реализующий интерфейс 'ISerializable', но не отмеченный атрибутом [Serializable]. При попытке сериализации экземпляров данного типа будет сгенерировано исключение типа 'SerializationException'. Реализации интерфейса 'ISerializable' недостаточно для того, чтобы во время выполнения программы было известно, что тип сериализуем. Для этого его необходимо декорировать атрибутом [Serializable].
Рассмотрим пример. Пусть имеется метод, выполняющий сериализацию и десериализацию объекта:
static void Foo(MemoryStream ms, BinaryFormatter bf, C1 obj)
{
bf.Serialize(ms, obj);
ms.Position = 0;
obj = (C1)bf.Deserialize(ms);
}
Объявление класса 'C1' выглядит следующим образом:
sealed class C1 : ISerializable
{
public C1()
{ }
private C1(SerializationInfo info, StreamingContext context)
{
field = (String)info.GetValue("field", typeof(String));
}
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("field", field, typeof(String));
}
private String field = "Some field";
}
При попытке выполнить сериализацию экземпляря данного типа будет сгенерировано исключение типа 'SerializationException'. Для решения проблемы необходимо декорировать данный класс атрибутом [Serializable]. Тогда корректное определение класса будет выглядеть так:
[Serializable]
sealed class C1 : ISerializable
{
public C1()
{ }
private C1(SerializationInfo info, StreamingContext context)
{
field = (String)info.GetValue("field", typeof(String));
}
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("field", field, typeof(String));
}
private String field = "Some field";
}
Примечание. Для данной диагностики доступна дополнительная настройка, задающаяся в конфигурационном файле (*.pvsconfig), имеющая следующий синтаксис:
//+V3096:CONF:{ IncludeBaseTypes: true }
В таком случае анализатор просматривает не только непосредственно реализацию интерфейса 'ISerializable' самим классом, но и каким-либо из базовых. По умолчанию эта функция анализа отключена.
Дополнительную информацию о конфигурационных файлах можно прочитать здесь.
Анализатор обнаружил подозрительный класс, декорированный атрибутом [Serializable] и имеющий члены несериализуемых типов (т.е. типов, которые сами не декорированы этим атрибутом). При этом, несериализуемые члены не помечены атрибутом [NonSerialized]. Наличие таких членов может, при использовании некоторых стандартных классов для сериализации, привести к возникновению исключения типа 'SerializationException' при попытке сериализации экземпляра данного класса.
Рассмотрим пример. Предположим, что у нас есть метод, выполняющий сериализацию и десериализацию объекта:
static void Foo(MemoryStream ms, BinaryFormatter bf, C1 obj)
{
bf.Serialize(ms, obj);
ms.Position = 0;
obj = (C1)bf.Deserialize(ms);
}
Имеются следующие определения классов 'C1' и 'NonSerializedClass':
sealed class NonSerializedClass { }
[Serializable]
class C1
{
private Int32 field1;
private NotSerializedClass field2;
}
При попытке сериализации экземпляра класса 'C1' возникнет исключение типа 'SerializationException', так как декорирование класса атрибутом [Serializable] подразумевает, что все поля будут сериализованы. Но тип поля 'field2' не сериализуем, что приведёт к возникновению исключения. Для решения проблемы поле 'field2' необходимо декорировать атрибутом [NonSerialized]. Тогда корректное определение класса 'C1' может выглядеть так:
[Serializable]
class C1
{
private Int32 field1;
[NonSerialized]
private NotSerializedClass field2;
}
Со свойствами ситуация несколько отличается. Рассмотрим пример ошибочного класса:
[Serializable]
class C2
{
private Int32 field1;
public NonSerializedClass Prop { get; set; }
}
К свойствам нельзя применить атрибут [NonSerialized]. Тем не менее, при попытке сериализовать такой класс, например, с использованием 'BinaryFormatter', будет сгенерировано исключение. Дело в том, что автоматически реализуемые свойства компилятором раскрываются в поле и методы получения значения этого свойства и, возможно, записи. В этом случае будет выполняться сериализация не самого свойства, а поля, сгенерированного компилятором. Эта ситуация аналогична описанному ранее примеру с полем.
Ошибку можно исправить, явно реализовав свойство через какое-то поле. Тогда корректный код будет выглядеть так:
[Serializable]
class C2
{
private Int32 field1;
[NonSerialized]
private NonSerializedClass nsField;
public NonSerializedClass Prop
{
get { return nsField; }
set { nsField = value; }
}
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3097. |
Анализатор обнаружил код, который может ввести в заблуждение программиста. Не все знают, что оператор continue в цикле "do { ... } while(false)" остановит цикл, а не возобновит его.
Таким образом, после вызова оператора 'continue' будет проверено условие '(false)', и цикл завершится так как условие ложно.
Рассмотрим пример:
int i = 1;
do
{
Console.Write(i);
i++;
if (i < 3)
continue;
Console.Write('A');
} while (false);
Программист может ожидать, что программа напечатает '12A'. На самом деле будет напечатано '1'.
Если именно так задумано и ошибки нет, то код всё равно лучше изменить. Можно воспользоваться оператором 'break':
int i=1;
do {
Console.Write(i);
i++;
if(i < 3)
break;
Console.Write('A');
} while(false);
Код стал более понятным. Сразу видно, что если условие (i < 3) выполняется, то цикл будет остановлен. В добавок, анализатор не будет выдавать предупреждение на этот код.
Если код некорректен, то его следует переписать. Здесь нельзя дать точных рекомендаций. Все зависит от логики работы кода. Например, чтобы напечатать '12A' лучше будет написать:
for (i = 1; i < 3; ++i)
Console.Write(i);
Console.Write('A');
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительную реализацию метода 'GetObjectData', в котором сериализуются не все сериализуемые члены типа. Это может привести к неверной десериализации объекта или возникновению исключения типа 'SerializationException'.
Рассмотрим пример. Пусть имеется метод, выполняющий сериализацию и десериализацию объекта.
static void Foo(BinaryFormatter bf, MemoryStream ms, Derived obj)
{
bf.Serialize(ms, obj);
ms.Position = 0;
obj = (Derived)bf.Deserialize(ms);
}
Объявление класса 'Base':
abstract class Base
{
public Int32 Prop { get; set; }
}
Объявление класса 'Derived':
[Serializable]
sealed class Derived : Base, ISerializable
{
public String StrProp { get; set; }
public Derived() { }
private Derived(SerializationInfo info,
StreamingContext context)
{
StrProp = info.GetString(nameof(StrProp));
}
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue(nameof(StrProp), StrProp);
}
}
В данном коде разработчик класса 'Derived' забыл сериализовать свойство базового класса 'Prop', из-за чего в результате сериализации объекта его состояние не будет сохранено полностью. При десериализации значение свойства 'Prop' будет установлено в значение по умолчанию, в данном случае, равное 0.
Для того, чтобы сохранить состояние объекта в результате сериализации, необходимо изменить код, добавив в реализацию метода 'GetObjectData' сохранение значения свойства 'Prop'в объекте типа 'SerializationInfo', а в конструкторе сериализации - его извлечение.
Тогда исправленный код реализации метода 'GetObjectData' и конструктора сериализации класса 'Derived' может выглядеть так:
private Derived(SerializationInfo info,
StreamingContext context)
{
StrProp = info.GetString(nameof(StrProp));
Prop = info.GetInt32(nameof(Prop));
}
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue(nameof(StrProp), StrProp);
info.AddValue(nameof(Prop), Prop);
}
В примере, рассмотренном выше, разработчик базового класса не предусмотрел его сериализацию. Если же эта возможность предусмотрена и тип реализует интерфейс 'ISerializable', то для корректной сериализации членов базового класса необходимо вызвать метод 'GetObjectData' базового класса из производного:
public override void GetObjectData(SerializationInfo info,
StreamingContext context)
{
base.GetObjectData(info, context);
....
}
Дополнительная информация:
Данная диагностика классифицируется как:
Анализатор обнаружил код, который может привести к возникновению исключения NullReferenceException в деструкторе (финализаторе) класса.
Тело деструктора класса является критичным местом в программе. Начиная с версии .NET 2.0, возникновения неперехваченного исключения в теле деструктора приводит к полной остановке программы. Перехватить исключение, выпущенное из деструктора, в дальнейшем невозможно.
Из этого следует, что при обращении к объектам внутри деструктора, следует предварительно проверять их на null во избежание падения.
Рассмотрим пример:
class A
{
public List<int> numbers { get; set; }
~A()
{
if (numbers.Count > 0) {
....
}
}
}
Т.к. изначально коллекция 'numbers' не была проинициализирована при объявлении, мы не можем утверждать, что при финализации объекта класса 'A', поле 'numbers' будет содержать ссылку на объект. Поэтому, стоит дополнительно проверить коллекцию на null, либо обернуть обращение к полю в блок try/catch.
Безопасный вариант кода может выглядеть следующим образом:
~A()
{
if (numbers != null)
{
if (numbers.Count > 0)
{
....
}
}
}
Начиная с версии языка C# 6.0, с помощью оператора '?.', можно сократить проверку до:
~A()
{
if (numbers?.Count > 0) {
....
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительный деструктор, содержащий потенциально некорректное "воскрешение" объекта.
Деструктор объекта вызывается сборщиком мусора .NET непосредственно перед тем, как объект будет удалён. Объявление деструктора объекта не является обязательным в языках .NET Framework - объект будет удалён сборщиком мусора и без явного определения деструктора. Обычно деструктор используется для освобождения одновременно с удалением самого .NET объекта неуправляемых ресурсов, которые этот объект использует. Например, это могут быть дескрипторы файловой системы. Такие неуправляемые ресурсы не будут освобождены сборщиком мусора автоматически.
Однако, в момент непосредственно перед удалением объекта, пользователь может (сознательно или несознательно) "воскресить" такой объект перед тем, как тот будут очищен сборщиком мусора. Напомним, что сборщик мусора очищает объекты, ставшие недоступными. Т.е. на такие объекты нигде не осталось ссылок. Однако, если присвоить ссылку на такой объект из его деструктора, например, в глобальную статическую переменную, то объект снова станет видим и из других частей программы, и, соответственно, "воскреснет". Заметим, что такую операцию можно производить неограниченное количество раз.
Далее приведём пример такого "воскрешения":
class HeavyObject
{
private HeavyObject()
{
HeavyObject.Bag.Add(this);
}
...
public static ConcurrentBag<HeavyObject> Bag;
~HeavyObject()
{
if (HeavyObject.Bag != null)
HeavyObject.Bag.Add(this);
}
}
Предположим, что у нас есть объект HeavyObject, создание которого является очень ресурсозатратным дейстивем. При этом такой объект нельзя использовать из нескольких мест параллельно. Предположим также, что мы можем создать всего несколько экземпляров таких объектов. В нашем примере тип HeavyObject имеет открытое статическое поле Bag - коллекцию, в которую будут добавлены (в конструкторе) все созданные нами экземпляры объектов HeavyObject. Это позволит получить из любого места в программе экземпляр типа HeavyObject:
HeavyObject heavy;
HeavyObject.Bag.TryTake(out heavy);
Метод TryTake удалит экземпляр heavy из коллекции Bag. Таким образом, в программе возможно будет использовать только ограниченное число заранее созданных экземпляров типа HeavyObject (его конструктор является закрытым). Далее, представим, что экземпляр heavy, полученный с помощью метода TryTake, стал более не нужен, и все ссылки на этот объект оказались удалены. Тогда для этого объекта, через какое-то время, сборщиком мусора будет вызван его деструктор, где этот объект снова будет добавлен в коллекцию Bag, т.е. "воскрешён", и снова станет доступен для пользователей программы, без необходимости его пересоздавать.
Однако, приведённый пример содержит ошибку, из-за которой он не будет работать так, как было описано выше. Ошибка эта - в предположении, что у "воскрешённого" объекта будет вызываться деструктор каждый раз, когда он перестаёт быть виден в программе (на него не остаётся ссылок). На самом деле, в приведённом примере деструктор будет вызван лишь один раз, т.е. мы "потеряем" объект при его повторной (второй) уборке сборщиком.
Для обеспечения корректной работы деструктора при "воскрешении" объекта, такой объект необходимо перерегистрировать с помощью метода GC.ReRegisterForFinalize:
~HeavyObject()
{
if (HeavyObject.Bag != null)
{
GC.ReRegisterForFinalize(this);
HeavyObject.Bag.Add(this);
}
}
Это обеспечит вызов деструктора каждый раз перед очисткой объекта сборщиком мусора.
Анализатор обнаружил возможную ошибку, связанную с тем, что в цикле 'for' на каждой итерации к элементу массива или списка обращаются по одному и тому же константному индексу.
Рассмотрим пример некорректного кода:
ParameterInfo[] parameters = method.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
Type parameterType = parameters[0].ParameterType;
....
}
Здесь на каждой итерации цикла планировали сохранять некое значение i-го элемента массива 'parameters' в переменную 'parameterType', но допустили опечатку, и на каждой итерации работают с одним и тем же - первым - элементом массива. Возможно также, что при написании кода программист отлаживался с использованием нулевого элемента, а потом забыл изменить значение индекса.
Корректный вариант кода:
ParameterInfo[] parameters = method.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
Type parameterType = parameters[i].ParameterType;
....
}
Рассмотрим ещё один пример с ошибкой, взятый из реального приложения:
if (method != null && method.SequencePoints.Count > 0)
{
CodeCoverageSequence firstSequence = method.SequencePoints[0];
int line = firstSequence.Line;
int column = firstSequence.Column;
for (int i = 1; i < method.SequencePoints.Count; ++i)
{
CodeCoverageSequence sequencePoint = method.SequencePoints[0];
if (line > sequencePoint.Line)
{
line = sequencePoint.Line;
column = sequencePoint.Column;
}
}
// ....
}
В этом фрагменте кода разработчик реализовал отдельную обработку нулевого элемента списка 'method.SequencePoints', а обход остальных элементов сделал в цикле. К сожалению, программист скопировал в тело цикла строку с доступом к нулевому элементу и переименовал только имя переменной с 'firstSequence' на 'sequencePoint', а изменить индекс забыл.
Корректный вариант кода:
if (method != null && method.SequencePoints.Count > 0)
{
CodeCoverageSequence firstSequence = method.SequencePoints[0];
int line = firstSequence.Line;
int column = firstSequence.Column;
for (int i = 1; i < method.SequencePoints.Count; ++i)
{
CodeCoverageSequence sequencePoint = method.SequencePoints[i];
if (line > sequencePoint.Line)
{
line = sequencePoint.Line;
column = sequencePoint.Column;
}
}
// ....
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3102. |
Анализатор обнаружил странный модификатор доступа у конструктора сериализации.
Странными можно считать следующие ситуации:
Конструктор сериализации вызывается при десериализации объекта и не должен вызываться извне типа (исключение – вызов конструктора классом-наследником), поэтому не стоит объявлять его с модификатором 'public' или 'internal'.
Если конструктор объявлен с модификатором 'private', но класс не является запечатанным, классы-наследники не смогут вызывать этот конструктор, что приведёт к невозможности десериализации членов базового типа.
Рассмотрим пример:
[Serializable]
class C1 : ISerializable
{
....
private C1(SerializationInfo info, StreamingContext context)
{
....
}
....
}
Класс 'C1' является незапечатанным, но при этом конструктор сериализации объявлен с модификатором доступа 'private', что приведёт к невозможности вызова этого конструктора из дочерних классов и, как следствие, к неверной десериализации объекта. Для исправления ситуации необходимо исправить модификатор доступа на 'protected':
[Serializable]
class C1 : ISerializable
{
....
protected C1(SerializationInfo info, StreamingContext context)
{
....
}
....
}
Примечание. Для данной диагностики доступна дополнительная настройка, задающаяся в конфигурационном файле (*.pvsconfig), имеющая следующий синтаксис:
//+V3103:CONF:{ IncludeBaseTypes: true }
В таком случае анализатор просматривает не только непосредственно реализацию интерфейса 'ISerializable' самим классом, но и каким-либо из базовых. По умолчанию эта настройка отключена.
Дополнительную информацию о конфигурационных файлах можно прочитать здесь.
Анализатор обнаружил незапечатанный класс, реализующий интерфейс 'ISerializable', но не реализующий виртуальный метод 'GetObjectData'. Это может привести к ошибкам сериализации в производных классах.
Рассмотрим пример. Пусть у нас имеются объявления базового и производного классов следующего вида:
[Serializable]
class Base : ISerializable
{
....
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
}
}
[Serializable]
sealed class Derived : Base
{
....
public new void GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
}
}
Где-то есть код, выполняющий сериализацию объекта:
void Foo(BinaryFormatter bf, MemoryStream ms)
{
Base obj = new Derived();
bf.Serialize(ms, obj);
ms.Position = 0;
Derived derObj = (Derived)bf.Deserialize(ms);
}
В таком случае сериализация выполнится неправильно из-за того, что будет вызван метод 'GetObjectData' не производного, а базового класса. Следовательно, члены производного класса не будут сериализованы. Если при десериализации из объекта 'SerializationInfo' будут извлекаться значения членов, добавляемых в методе 'GetObjectData' производного класса, будет сгенерировано исключение, так как объект 'SerializationInfo' не будет содержать запрашиваемых значений.
Для решения проблемы необходимо определить метод 'GetObjectData' в базовом классе с модификатором 'virtual', а в производном – 'override'. Тогда корректный код может выглядеть так:
[Serializable]
class Base : ISerializable
{
....
public virtual void GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
}
}
[Serializable]
sealed class Derived : Base
{
....
public override void GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
}
}
Если в базовом классе определена только явная реализация интерфейса, стоит также добавить неявную реализацию виртуального метода 'GetObjectData'. Рассмотрим пример. Предположим, что у нас имеются определения классов следующего вида
[Serializable]
class Base : ISerializable
{
....
void ISerializable.GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
}
}
[Serializable]
sealed class Derived : Base, ISerializable
{
....
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
}
}
В таком случае из производного класса будет невозможно обратиться к методу 'GetObjectData' базового класса, а значит, часть членов не будет сериализована. Для исправления ошибки помимо явной реализации в базовый класс необходимо добавить неявную реализацию виртуального метода 'GetObjectData'. Тогда исправленный код может выглядеть так:
[Serializable]
class Base : ISerializable
{
....
void ISerializable.GetObjectData(SerializationInfo info,
StreamingContext context)
{
GetObjectData(info, context);
}
public virtual void GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
}
}
[Serializable]
sealed class Derived : Base
{
....
public override void GetObjectData(SerializationInfo info,
StreamingContext context)
{
....
base.GetObjectData(info, context);
}
}
Если же подразумевается, что у класса не должно быть наследников, следует добавить модификатор 'sealed' к объявлению класса, что сделает его запечатанным.
Данная диагностика предупреждает вас о возможном возникновении 'NullReferenceException' при выполнении программы. Предупреждение возникает в том случае, если происходит обращение к полю переменной без проверки её на 'null'. Ключевым моментом является то, что значение переменной вычисляется с помощью выражения, где используется оператор null-conditional.
Рассмотрим пример:
public int Foo (Person person)
{
string parentName = person?.Parent.ToString();
return parentName.Length;
}
В данном случае при инициализации объекта 'parentName' мы предполагаем, что 'person' может быть 'null'. В таком случае функция 'ToString()' не будет выполнена, а в переменную 'parentName' запишется 'null'. При попытке чтения свойства 'Length' из переменной 'parentName' возникнет исключение 'NullReferenceException'.
Исправленный код функции может быть следующим:
public int Foo (Person person)
{
string parentName = person?.Parent.ToString();
return parentName?.Length ?? 0;
}
Теперь, если в переменной 'parentName' не было 'null', то мы вернем длину строки, а если был 'null', то 0.
Ошибка может возникнуть, если значение, полученное с помощью null-conditional, без проверки передаётся в метод, конструктор или присваивается свойству.
Рассмотрим пример:
void UsersProcessing(Users users)
{
IEnumerable<User> usersList = users?.GetUsersCollection();
LogUserNames(usersList);
}
void LogUserNames(IEnumerable<User> usersList)
{
foreach (var user in usersList)
{
....
}
}
Переменная 'usersList' передаётся в качестве аргумента методу 'LogUserNames'. Она может иметь значение 'null', так как оно получено с помощью оператора null-conditional. Внутри 'LogUserNames' производится обход переданной коллекции. Для этого используется 'foreach' и, следовательно, у коллекции будет вызван метод 'GetEnumerator'. Если 'userList' имеет значение 'null', то будет выброшено исключение типа 'NullReferenceException'.
Вариант с исправлениями может выглядеть следующим образом:
void UsersProcessing(Users users)
{
IEnumerable<User> usersList = users?.GetUsersCollection();
LogUserNames(usersList ?? Enumerable.Empty<User>());
}
void LogUserNames(IEnumerable<User> usersList)
{
foreach (var user in usersList)
{
....
}
}
Результат выполнения 'users?.GetUsersCollection()' присваивается переменной 'usersList'. Если значение этой переменной — 'null', то в метод 'LogUserNames' будет передаваться пустая коллекция. Это поможет избежать исключения типа 'NullReferenceException' при обходе 'usersList' в 'foreach'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3105. |
При доступе по индексу к переменной типа массив, список или строка может возникнуть исключение IndexOutOfRangeException, если значение индекса оказывается за пределами допустимого диапазона. Анализатор способен обнаружить некоторые ошибки такого рода.
Например, это может произойти во время обхода массива в цикле:
int[] buff = new int[25];
for (int i = 0; i <= 25; i++)
buff[i] = 10;
Нужно помнить о том, что первый элемент массива имеет индекс 0, а последний – на единицу меньше размера массива. Корректный вариант:
int[] buff = new int[25];
for (int i = 0; i < 25; i++)
buff[i] = 10;
Похожую ошибку можно сделать не только в цикле, но и при неправильной проверке индекса в условии:
void ProcessOperandTypes(ushort opCodeValue, byte operandType)
{
var OneByteOperandTypes = new byte[0xff];
if (opCodeValue < 0x100)
{
OneByteOperandTypes[opCodeValue] = operandType;
}
...
}
Корректный вариант:
void ProcessOperandTypes(ushort opCodeValue, byte operandType)
{
var OneByteOperandTypes = new byte[0xff];
if (opCodeValue < 0xff)
{
OneByteOperandTypes[value] = operandType;
}
...
}
Также можно допустить ошибку при доступе к конкретному элементу массива или списка.
void Initialize(List<string> config)
{
...
if (config.Count == 16)
{
var result = new Dictionary<string, string>();
result.Add("Base State", config[0]);
...
result.Add("Sorted Descending Header Style", config[16]);
}
...
}
В этом примере допущена ошибка в количестве записей в списке config. Исправленный вариант выглядит следующим образом:
void Initialize(List<string> config)
{
...
if (config.Count == 17)
{
var result = new Dictionary<string, string>();
result.Add("Base State", config[0]);
...
result.Add("Sorted Descending Header Style", config[16]);
}
...
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3106. |
Анализатор обнаружил одинаковые подвыражения в левой и правой части составного оператора присваивания (compound assignment operator). Возможно эта операция содержит ошибку, не имеет смысла, либо может быть упрощена.
Рассмотрим пример. Пусть у нас имеется выражение следующего вида:
x += x + 5;
Возможно программист хотел просто прибавить к переменной 'x' значение 5. Тогда корректный код может выглядеть так:
x = x + 5;
Либо же программист хотел прибавить 5, но случайно добавил лишнюю переменную 'x' в выражение. Тогда корректный код может выглядеть так:
x += 5;
Впрочем, возможно код написан правильно. Однако согласитесь, читать такое выражения сложно и лучше переписать его. Более читабельный вариант может выглядеть так:
x = x * 2 + 5;
Рассмотрим следующий пример:
x += x;
Данная операция эквивалентна операции умножения на два. Более понятное выражение будет иметь следующий вид:
x *= 2;
Рассмотрим еще одно выражение:
y += top - y;
Мы пытаемся прибавить переменной 'y' разницу между переменной 'top' и переменной 'y'. Давайте разложим данное выражение:
y = y + top – y;
Данное выражение можно упростить, так как переменная 'y' вычитается сама из себя, что является бессмысленным. Тогда простой и корректный код будет выглядеть так:
y = top;
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3107. |
Анализатор обнаружил, что в переопределенном методе 'ToString()' возвращается значение null или выбрасывается исключение.
Рассмотрим пример. Пусть у нас имеется метод следующего вида:
public override string ToString()
{
return null;
}
Есть вероятность, что в дальнейшем при работе программы или ее отладке будет вызван данный метод для получения текстового представления объекта. Т.к. разработчик скорее всего не станет проверять результат работы этой функции на null, дальнейшее его использование может повлечь за собой исключение 'NullReferenceException'. Если необходимо вернуть пустое или неизвестное значение текстового представление объекта, рекомендуется использовать для этого пустую строку:
public override string ToString()
{
return string.Empty;
}
Другой пример плохой практики при реализации метода 'ToString()' - выбрасывание исключения из этого метода. Рассмотрим следующий пример:
public override string ToString()
{
if(hasError)
throw new Exception();
....
}
Высока вероятность, что данный метод будет вызван пользователем класса в месте, не подразумевающем возможности возникновения и обработки исключений, например в деструкторе.
Если необходимо выдать сообщение об ошибке при генерации текстового представления объекта, лучше будет вернуть его текст в виде строки, либо каким-то образом залогировать эту ошибку:
public override string ToString()
{
if(hasError)
{
LogError();
return "Error encountered";
}
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3108. |
Анализатор обнаружил одинаковые подвыражения в левой и правой части оператора сравнения (comparison operator). Возможно, эта операция содержит ошибку, не имеет смысла, либо может быть упрощена.
Рассмотрим пример. Допустим у нас имеется выражение следующего вида:
if ((x – y) >= (x - z)) {};
Очевидно, что использование переменной 'x' в данном фрагменте кода является избыточным и переменная может быть сокращена в левой и правой частях выражения. Тогда упрощенный код может выглядеть так:
if (y <= z) {};
Рассмотрим следующий пример:
if (x1 == x1 + 1) {};
Данное выражение уже содержит явную ошибку, так как будет ложным при любом значении переменной 'x1'. Возможно, здесь присутствует опечатка и корректный код мог бы выглядеть так:
if (x2 == x1 + 1) {};
Рассмотрим еще одно выражение:
if (x < x * y) {};
В данном выражении также может быть сокращена переменная 'x'. При этом упрощенное и более понятное выражение могло бы иметь вид:
if (y > 1) {};
Анализатор обнаружил, что может возникать бесконечная рекурсия. Скорее всего это приведет к переполнению стека вызовов и возникновению исключения Stack Overflow.
Рассмотрим пример. Пусть у нас имеется свойство 'MyProperty' и поле '_myProperty', связанное с ним. Из-за опечатки можно допустить следующую ошибку:
private string _myProperty;
public string MyProperty
{
get { return MyProperty; } // <=
set { _myProperty = value; }
}
При указании возвращаемого значения в методе доступа свойства, вместо поля '_myProperty' было указано свойство 'MyProperty', что приводится к возникновению бесконечной рекурсии при получении значения у свойства. Корректный код должен выглядеть так:
private string _myProperty;
public string MyProperty
{
get { return _myProperty; }
set { _myProperty = value; }
}
Рассмотрим второй пример:
class Node
{
Node parent;
public void Foo()
{
// some code
parent.Foo(); // <=
}
}
Вероятнее всего программист хотел рекурсивно обойти все поля 'parent', но не предусмотрел условия выхода из рекурсии. Эта ситуация более интересная: здесь может возникнуть не только переполнение стека, но и доступ по нулевой ссылке, когда мы дойдём до самой верхней родительской сущности. Корректный код мог бы выглядеть так:
class Node
{
Node parent;
public void Foo()
{
// some code
if (parent != null)
parent.Foo();
}
}
Рассмотрим третий пример. Пусть у нас имеется метод с конструкцией 'try - catch - finally'.
void Foo()
{
try
{
// some code;
return;
}
finally
{
Foo(); // <=
}
}
Вероятнее всего программист не учел, что блок 'finally' будет выполнен как в случае возникновения исключения внутри блока 'try', так и в случае выхода из метода оператором 'return'. Таким образом блок 'finally' всегда будет выполнять рекурсивный вызов метода 'Foo'. Для корректного выполнения рекурсии необходимо реализовать условие перед выполнением метода. К примеру корректный код мог бы выглядеть так:
void Foo()
{
try
{
// some code;
return;
}
finally
{
if (condition)
Foo();
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3110. |
Анализатор обнаружил, что значение, имеющее обобщённый (generic) тип, сравнивается с 'null'. Если обобщённый тип никак не ограничен, то он может быть инстанциирован как значимым, так и ссылочным типом. В случае если он окажется значимым, проверка на 'null' всегда будет возвращать 'false', так как значимые типы не могут принимать нулевое (null) значение.
Рассмотрим пример.
class Node<T>
{
T value;
void LazyInit(T newValue)
{
if (value == null) // <=
{
value = newValue;
}
}
}
В случае если тип 'T' будет определен как значимый, тело условия 'if' никогда не выполнится. В нашем примере это приведёт к тому, что переменная 'value' не будет проинициализирована переданным значением - её значение всегда останется равным 'default' значению 'T'.
Если необходимо обрабатывать объекты только ссылочного типа, то используйте ограничители. К примеру, generic тип 'T' из примера выше может быть ограничен только для ссылочных типов:
class Node<T> where T : class // <=
{
T value;
void LazyInit(T newValue)
{
if (value == null)
{
value = newValue;
}
}
}
Если же generic тип должен принимать как значимые, так и ссылочные типы, и проверка должна осуществляться для обоих видов значений, то тогда необходимо использовать проверку на значение данного типа по умолчанию (default), вместо проверки на 'null':
class Node<T>
{
T value;
void LazyInit(T newValue)
{
if (object.Equals(value, default(T))) // <=
{
value = newValue;
}
}
}
В данном случае проверка будет корректно работать как для ссылочных, так и для значимых типов. Если же подразумевалось, что данная проверка должна срабатывать только на ссылочные типы с нулевым значением (без ограничения типа 'T'), то тогда мы предлагаем использовать такой вариант.
class Node<T>
{
T value;
void LazyInit(T newValue)
{
if (typeof(T).IsClass && // <=
object.Equals(value, default(T)))
{
value = newValue;
}
}
}
Метод 'IsClass' вернет 'true' если generic был инстанциирован ссылочным типом. Таким образом, проверка на значение по умолчанию будет происходить только для ссылочных типов, по аналогии с предыдущим примером.
Анализатор обнаружил подозрительное условие, которое может содержать ошибку.
Диагностика носит эмпирический характер, поэтому проще показать на примере, как она работает, чем объяснить сам принцип работы анализатора.
Рассмотрим пример:
if (m_a != a ||
m_b != b ||
m_b != c) // <=
{
....
}
Из-за того, что имена переменных очень похожи, в коде допущена опечатка. Ошибка находится в третей строке. Переменную 'c' следовало сравнить с 'm_c', а не с 'm_b'. Даже читая этот текст сложно заметить ошибку. Обратите внимание на окончания в названии переменных.
Правильный вариант:
if (m_a != a ||
m_b != b ||
m_c != c) // <=
{
....
}
Если анализатор выдал предупреждение V3112, то внимательно изучите соответствующий фрагмент кода. Иногда опечатку бывает сложно заметить.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3112. |
Анализатор обнаружил, что в разделе итератора (iterator section) оператора 'for' производится инкремент или декремент переменной, не являющейся счетчиком.
Рассмотрим пример. Допустим, у нас имеется выражение следующего вида:
for (int i = 0; i != N; ++N)
Вероятно, данный фрагмент кода содержит ошибку и вместо переменной 'N' в выражении инкремента '++N' необходимо использовать переменную 'i'. Корректный вариант кода должен выглядеть так:
for (int i = 0; i != N; ++i)
Рассмотрим следующий пример:
for (int i = N; i >= 0; --N)
Данный фрагмент кода также ошибочен. В выражении декремента '‑‑N' должна быть использована переменная 'i':
for (int i = N; i >= 0; --i)
Данная диагностика классифицируется как:
Чтобы понять смысл данной диагностики, давайте для начала немного вспомним теорию.
Сборщик мусора автоматически освобождает память, связанную с контролируемым объектом, если он больше не используется и на него не осталось видимых ссылок. Тем не менее, невозможно предсказать, когда именно произойдёт сборка мусора (если не вызывать её вручную). Кроме того, сборщик мусора не имеет информации о таких неуправляемых ресурсах, как дескрипторы, окна или открытые файлы и потоки. Метод 'Dispose' обычно используется, чтобы освобождать такие неуправляемые ресурсы.
Понимая это, анализатор обнаружил локальную переменную, объект которой реализует интерфейс 'IDisposable' и не передается за пределы зоны видимости локальной переменной. При этом после использования данного объекта не был вызван метод 'Dispose' освобождающий принадлежащие ему неуправляемые ресурсы.
Если предположить, что данный объект содержит какой-либо дескриптор (например, файл), то он будет оставаться в памяти до следующей сборки мусора, которая произойдет через неопределенный промежуток времени вплоть до момента завершения работы программы. В результате файл может неопределённое время оставаться заблокированным, мешая нормальной работе других программ или операционной системе.
Рассмотрим пример такой ситуации:
string Foo()
{
var stream = new StreamReader(@"С:\temp.txt");
return stream.ReadToEnd();
}
В данном случае объект 'StreamReader' будет хранить в себе дескриптор открытого файла даже после выхода из метода 'Foo', тем самым блокируя его для других программ и операционной системы до тех пор, пока сборщик мусора не очистит его.
Чтобы избежать этого - своевременно освобождайте ресурсы, используя метод 'Dispose' как в примере ниже:
string Foo()
{
var stream = new StreamReader(@"С:\temp.txt");
var result = stream.ReadToEnd();
stream.Dispose();
return result;
}
Однако, для более надёжного освобождения ресурсов рекомендуется использовать конструкцию 'using'. Она обеспечит автоматическую очистку ресурсов используемого объекта после завершения:
string Foo()
{
using (var stream = new StreamReader(@"С:\temp.txt"))
{
return stream.ReadToEnd();
}
}
Компилятор раскроет блок 'using' в блок 'try finally', и в 'finally' вставит вызов метода 'Dispose' - это гарантирует очистку объекта даже в случае возникновения исключений.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3114. |
Анализатор обнаружил, что в переопределенном методе 'Equals(object obj)' возможно возникновение исключения.
Рассмотрим пример. Пусть у нас имеется метод следующего вида:
public override bool Equals(object obj)
{
return obj.GetType() == this.GetType();
}
В том случае, если аргумент 'obj' будет нулевым (null) - это повлечет за собой исключение 'NullReferenceException'. Вероятнее всего программист не предусмотрел подобную ситуацию при реализации данного метода. Для корректной работы можно использовать проверку на 'null':
public override bool Equals(object obj)
{
if (obj == null)
return false;
return obj.GetType() == this.GetType();
}
Другой пример плохой практики при реализации метода 'Equals(object obj)' - явное выбрасывание исключения из этого метода. Рассмотрим следующий пример:
public override bool Equals(object obj)
{
if (obj == null)
throw new InvalidOperationException("Invalid argument.");
return obj == this;
}
Высока вероятность, что данный метод будет вызван пользователем в месте, не подразумевающем возможности возникновения и обработки исключений.
Если один из объектов не соответствует условиям сравнения, лучше будет вернуть 'false':
public override bool Equals(object obj)
{
if (obj == null)
return false;
return obj == this;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3115. |
Анализатор обнаружил, что в операторе 'for' используются некорректные границы для итератора.
Рассмотрим пример:
for (int i = 0; i < 100; --i)
Очевидно, что данный фрагмент кода содержит ошибку. Значение переменной 'i' будет всегда меньше 100. По крайней мере до тех пор, пока не произойдёт её переполнение. Но вряд ли такое поведение задумывал программист. Для исправления ошибки необходимо в указанном выражении либо заменить оператор декремента '‑‑i' на оператор инкремента '++i':
for (int i = 0; i < 100; ++i)
либо указать корректные границы для переменной 'i', использовав при этом оператор сравнения '>' или '!= ':
for (int i = 99; i >= 0; --i)
for (int i = 99; i != -1; --i)
Выбор наиболее подходящего варианта должен осуществлять автор кода на основе анализа конкретной ситуации.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3116. |
Анализатор обнаружил, что один из параметров конструктора не используется.
Рассмотрим пример.
public class MyClass
{
protected string _logPath;
public String LogPath { get { return _logPath; } }
public MyClass(String logPath) // <=
{
_logPath = LogPath;
}
}
В данном случае программист скорее всего опечатался, написав 'LogPath' вместо 'logPath', в результате чего параметр конструктора нигде не используется. Корректный код должен выглядеть так:
public class MyClass
{
protected string _logPath;
public String LogPath { get { return _logPath; } }
public MyClass(String logPath) // <=
{
_logPath = logPath;
}
}
Рассмотрим ещё один пример.
public class MyClass
{
public MyClass(String logPath) // <=
{
//_logPath = logPath;
}
}
В случае, если программист умышленно не использует параметр конструктора, мы предлагаем пометить конструктор атрибутом 'Obsolete'.
public class MyClass
{
[Obsolete]
public MyClass(String logPath) // <=
{
//_logPath = logPath;
}
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3117. |
Анализатор обнаружил обращение к свойству 'Milliseconds', 'Seconds', 'Minutes' или 'Hours', у объекта типа 'TimeSpan', который является временным интервалом между несколькими датами или другими временными интервалами.
Возможно данное выражение содержит ошибку, так как в случае, если вы ожидаете суммарное значение временной единицы за весь интервал времени, данное свойство вернет только часть интервала.
Рассмотрим пример:
var t1 = DateTime.Now;
await SomeOperation(); // 2 минуты 10 секунд
var t2 = DateTime.Now;
Console.WriteLine("Execute time: {0}sec", (t2 - t1).Seconds);
// Результат - "Execute time: 10sec"
В переменную 't1' мы записываем дату и время до выполнения операции. В переменную 't2' мы записываем дату и время после выполнения операции. Предположим, что метод 'SomeOperation' выполняется ровно 2 минуты 10 секунд. Далее мы выводим в консоль разницу в секундах между переменными, тем самым получаем временной интервал выполнения операции. В данном примере временной интервал будет равен 130 секунд, но свойство 'Seconds' вернет только 10 секунд. Корректный пример будет выглядеть так:
var t1 = DateTime.Now;
await SomeOperation(); // 2 минуты 10 секунд
var t2 = DateTime.Now;
Console.WriteLine("Execute time: {0}sec", (t2 - t1).TotalSeconds);
// Результат - "Execute time: 130sec"
Необходимо использовать свойство 'TotalSeconds', чтобы получить суммарное количество секунд в данном временном интервале.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3118. |
Анализатор обнаружил использование виртуального или переопределенного события. В случае, если данное событие будет переопределено в производном классе, это может привести к непредсказуемому поведению. MSDN не рекомендует использование переопределенных виртуальных событий: "Do not declare virtual events in a base class and override them in a derived class. The C# compiler does not handle these correctly and it is unpredictable whether a subscriber to the derived event will actually be subscribing to the base class event". https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/how-to-raise-base-class-events-in-derived-classes.
Рассмотрим пример:
class Base
{
public virtual event Action MyEvent;
public void FooBase() { MyEvent?.Invoke(); }
}
class Child: Base
{
public override event Action MyEvent;
public void FooChild() { MyEvent?.Invoke(); }
}
static void Main()
{
var child = new Child();
child.MyEvent += () => Console.WriteLine("Handler");
child.FooChild();
child.FooBase();
}
Несмотря на то, что производится вызов обоих методов 'FooChild()' и 'FooBase()', результатом работы метода 'Main()' будет вывод на консоль одной строки:
Handler
Используя отладчик или тестовый вывод можно убедиться в том, что при вызове 'child.FooBase()' значение переменной 'MyEvent' будет равно 'null'. Таким образом, подписывание на событие 'MyEvent' для класса 'Child', наследника 'Base', переопределяющего это событие, не привело к подписыванию на событие 'MyEvent' базового класса. На первый взгляд, такое поведение не согласуется с поведением, например, виртуальных методов, однако его можно объяснить особенностью реализации событий в C#. При объявлении события компилятор автоматически создаёт для него 2 метода-аксессора 'add' и 'remove', и поле-делегат, в которое происходит добавление\удаление делегатов при подписке\отписке на события. В случае виртуального события, базовый и дочерний классы будут иметь индивидуальные (не виртуальные) поля, связанные с данным событием.
Данной проблемы можно избежать, объявив для события его аксессоры в явном виде:
class Base
{
public virtual Action _myEvent { get; set; }
public virtual event Action MyEvent
{
add
{
_myEvent += value;
}
remove
{
_myEvent -= value;
}
}
public void FooBase() { _myEvent?.Invoke(); }
}
Мы настоятельно не рекомендуем использовать виртуальные или переопределенные события описанным в первом примере образом. В случае, если вы все же вынуждены использовать переопределенные события (например, при наследовании от абстрактного класса), используйте их с осторожностью, учитывая возможность неопределенного поведения. Используйте явное определение аксессоров 'add' и 'remove' либо используйте при объявлении класса или события ключевое слово 'sealed'.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3119. |
Анализатор обнаружил потенциально бесконечный цикл, условие выхода из которого зависит от переменной, значение которой никогда не меняется в нем.
Рассмотрим пример:
int x = 0;
while (x < 10)
{
Do(x);
}
Условие выхода из цикла зависит от переменной 'x', значение которой в цикле всегда будет равно нулю. Таким образом проверка 'x < 10' всегда будет истинной, что приведет к бесконечному циклу в данном случае. Правильный вариант мог бы выглядеть так:
int x = 0;
while (x < 10)
{
x = Do(x);
}
Рассмотрим еще один пример, когда условие выхода из цикла зависит от переменной, изменение значения этой которой зависит от других переменных, которые никогда не меняются внутри цикла. Пусть у нас имеется метод следующего вида:
int Foo(int a)
{
int j = 0;
while (true)
{
if (a >= 32)
{
return j * a;
}
if (j == 10)
{
j = 0;
}
j++;
}
}
Условие выхода из цикла зависит от параметра 'a'. В случае если параметр 'a' не будет удовлетворять условие проверки 'a >= 32', то цикл будет бесконечным, так как его значение в цикле не изменяется. Корректный вариант мог бы выглядеть так:
int Foo(int a)
{
int j = 0;
while (true)
{
if (a >= 32)
{
return j * a;
}
if (j == 10)
{
j = 0;
a++; // <=
}
j++;
}
}
Таким образом изменение параметра 'a' будет зависеть от локальной переменной 'j'.
Данная диагностика классифицируется как:
Анализатор обнаружил использование атрибута 'Flags' (System.FlagsAttribute) при объявлении перечисления. При этом для констант в перечислении не задано ни одного значения, переопределяющего значения по умолчанию.
Рассмотрим пример:
[Flags]
enum DeclarationModifiers
{
Static,
New,
Const,
Volatile
}
После указания атрибута 'Flags' перечисление будет вести себя не просто как набор именованных взаимоисключающих констант, а как битовое поле, то есть набор флагов. Значения флагов в этом случае обычно задают как степени числа 2, а перечисление используют, комбинируя значения при помощи битовой операции OR:
DeclarationModifiers result = DeclarationModifiers.New |
DeclarationModifiers.Const;
При использовании перечисления с атрибутом 'Flags', для которого не заданы инициализаторы значений (используются значения по умолчанию), возможно перекрытие значений при их комбинировании. Приведенный выше пример, скорее всего, содержит ошибку. Она может быть исправлена следующим образом:
[Flags]
enum DeclarationModifiers
{
Static = 1,
New = 2,
Const = 4,
Volatile = 8
}
Теперь перечисление удовлетворяет всем требованиям для битового поля.
Тем не менее, возможна ситуация, когда программист умышленно оставил значения констант по умолчанию в перечислении с атрибутом 'Flags'. Но при этом должны быть учтены все возможные комбинации значений. Пример такого перечисления:
[Flags]
enum Colors
{
None, // = 0 by default
Red, // = 1 by default
Green, // = 2 by default
Red_Green // = 3 by default
}
В данном случае программист учел перекрывающиеся значения: в результате комбинации 'Colors.Red' и 'Colors.Green' будет получено ожидаемое значение 'Colors.Red_Green'. Здесь нет ошибки, но установить данный факт может лишь автор программы.
Проиллюстрируем отличие в работе перечислений с атрибутом 'Flags' без использования инициализации значений, а также с использованием инициализации значений:
[Flags]
enum DeclarationModifiers
{
Static, // = 0 by default
New, // = 1 by default
Const, // = 2 by default
Volatile // = 3 by default
}
[Flags]
enum DeclarationModifiers_Good
{
Static = 1,
New = 2,
Const = 4,
Volatile = 8
}
static void Main(....)
{
Console.WriteLine(DeclarationModifiers.New |
DeclarationModifiers.Const);
Console.WriteLine(DeclarationModifiers_Good.New |
DeclarationModifiers_Good.Const);
}
Результат выполнения программы:
Volatile
New, Const
Так как в перечислении 'DeclarationModifiers' используются значения по умолчанию, то комбинация констант 'DeclarationModifiers.New' и 'DeclarationModifiers.Const' даст в результате значение 3, перекрывающее константу 'DeclarationModifiers.Volatile'. Это может стать неожиданностью для программиста. В то же время, для перечисления 'DeclarationModifiers_Good' получено правильное значение, являющееся именно комбинацией флагов 'DeclarationModifiers_Good.New ' и DeclarationModifiers_Good.Const'.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3121. |
Анализатор обнаружил, что производится сравнение двух строк, которые имеют заведомо разный регистр символов.
Рассмотрим пример:
void Some(string s)
{
if (s.ToUpper() == "abcde")
{
....
}
}
После того, как значение переменной 's' приводится к верхнему регистру, результат сравнивается со строкой, все символы которой в нижнем регистре. Так как такое сравнение будет всегда ложным, фрагмент кода содержит ошибку, которая может быть исправлена следующим образом:
void Some(string s)
{
if (s.ToLower() == "abcde")
{
....
}
}
Рассмотрим еще один пример:
void Some()
{
string s = "abcde";
....
if (s.Contains("AbCdE"))
{
....
}
}
Переменная 's' содержит символы только в нижнем регистре. При этом, для данной строки производится попытка поиска ее подстроки, содержащей символы в смешанном регистре. Очевидно, что в данном случае результатом работы метода 'Contains' всегда будет ложь. Приведенный фрагмент кода также содержит ошибку.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3122. |
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит логическую ошибку. В тексте программы имеется выражение, содержащее оператор '??' или '?:', которое может вычисляться не так, как планировал программист.
Операторы '??' и '?:' имеют более низкий приоритет по сравнению с операторами ||, &&, |, ^, &, !=, ==, +, -, %, /, *. Это можно случайно забыть и написать ошибочный код, подобный приведенному ниже:
public bool Equals(Edit<TNode> other)
{
return _kind == other._kind
&& (_node == null) ? other._node == null :
node.Equals(other._node);
}
Так как приоритет оператора '&&' выше чем оператора '?:', то сначала будет выполнено выражение '_kind == other._kind && (_node == null)'. Чтобы избежать таких ошибок, следует заключать всё выражение, содержащее оператор '?:', в скобки:
public bool Equals(Edit<TNode> other)
{
return _kind == other._kind
&& ((_node == null) ? other._node == null :
node.Equals(other._node));
}
Рассмотрим другой пример с оператором '??':
public override int GetHashCode()
{
return ValueTypes.Aggregate(...)
^ IndexMap?.Aggregate(...) ?? 0;
}
Приоритет оператора '^' выше чем приоритет оператора '??', поэтому если 'IndexMap' равен 'null', то и левая часть оператора '??' тоже будет иметь значение 'null', и значит функция всегда будет возвращать 0 вне зависимости от того что содержится в коллекции 'ValueTypes'.
Как и в случае оператора '?:', выражение с оператором '??' лучше заключать в скобки
public override int GetHashCode()
{
return ValueTypes.Aggregate(...)
^ (IndexMap?.Aggregate(...) ?? 0);
}
Теперь функция 'GetHashCode()' будет возвращать разные значения в зависимости от содержимого коллекции 'ValueTypes' и в том случае когда 'IndexMap' будет равен 'null'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3123. |
Анализатор обнаружил подозрительный фрагмент кода, в котором производится проверка на наличие ключа в одном словаре, после чего новый элемент добавляется в другой словарь. Возможно, допущена опечатка или логическая ошибка.
Рассмотрим пример:
Dictionary<string, string> dict = new Dictionary<string, string>();
Dictionary<string, string> _dict = new Dictionary<string, string>();
....
void Add(string key, string val)
{
if (!dict.ContainsKey(key))
_dict.Add(key, val);
}
Здесь, вероятно, допущены сразу две ошибки. Первая - добавление элемента не в тот словарь, что может исказить логику работы программы. Вторая ошибка связана с тем, что проверка наличия ключа 'key' выполняется для словаря 'dict', а не для '_dict'. Если словарь '_dict' уже содержит значение для ключа 'key', в момент выполнения '_dict.Add(key, val)' будет выброшено исключение 'ArgumentException'. В данном случае возможны два варианта исправления данной конструкции (в обоих случаях на наличие ключа проверяется тот словарь, куда производится добавление нового элемента):
Dictionary<string, string> dict = new Dictionary<string, string>();
Dictionary<string, string> _dict = new Dictionary<string, string>();
....
void Add1(string key, string val)
{
if (!_dict.ContainsKey(key))
_dict.Add(key, val);
}
...
void Add2(string key, string val)
{
if (!dict.ContainsKey(key))
dict.Add(key, val);
}
Анализатор обнаружил потенциальную ошибку, которая может привести к доступу по нулевой ссылке.
Анализатор заметил в коде следующую ситуацию. Сначала объект проверяется на null, а потом используется уже без проверки на null. Это может означать одно из двух:
1) Возникнет исключение, если объект будет равен null.
2) Программа всегда работает корректно, так как объект всегда не равен null. Проверка является лишней.
Рассмотрим первый вариант. Здесь может возникнуть исключение.
obj = Foo();
if (obj != null)
obj.Func1();
obj.Func2();
Если объект 'obj' окажется равен null, то выражение 'obj.Func2()' приведёт к ошибке. Анализатор выдаст предупреждение на этот код, указав 2 строки. Первая строка - это то место, где используется объект. Вторая строка - это то место, где объект сравнивается со значением null.
Исправленный вариант кода:
obj = Foo();
if (obj != null) {
obj.Func1();
obj.Func2();
}
Рассмотрим второй вариант. Проход по списку безопасный, проверка лишняя
List<string> list = CreateNotEmptyList();
if (list == null || list.Count == 0) { .... }
foreach (string item in list) { .... }
Этот код всегда работает корректно. Список list всегда не пуст. Однако анализатор не разобрался в этой ситуации и выдал предупреждение. Чтобы оно исчезло, следует удалить проверку "if (list == null || list.Count == 0)". Она не имеет практического смысла и может только запутать программиста, читающего код.
Исправленный вариант:
List<string> list = CreateNotEmptyList();
foreach (string item in list) { .... }
Ещё один вариант сообщения анализатора - когда проверка и использование расположены в разных ветках if\else или switch выражений. Например:
if (lines.Count == 1)
{
if (obj != null)
obj.Func1();
}
else
{
lines.Clear();
obj.Func2();
}
В такой ситуации, несмотря на то, что обе ветки никогда не выполнятся одновременно, а будет выбрана только одна из веток, проверка на null в одной из них косвенно свидетельствует о возможности принятия переменной значения null и в другой ветке. Тогда, если управление придёт во вторую ветку, возникнет исключение.
Исправленный вариант:
if (lines.Count == 1)
{
if (obj != null)
obj.Func1();
}
else
{
lines.Clear();
if (obj != null)
obj.Func2();
}
В случае если анализатор ошибается, то кроме изменения кода, можно использовать комментарий для подавления предупреждений. Пример: "obj.Foo(); //-V3125".
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3125. |
Анализатор обнаружил пользовательский тип, реализующий интерфейс 'IEquatable<T>'. При этом тип не переопределяет метод 'GetHashCode'.
Это может привести к ошибочным результатам при использовании типа, например, с методами из 'System.Linq.Enumerable', такими как: 'Distinct', 'Except', 'Intersect' или 'Union'.
Рассмотрим пример с использованием 'Distinct':
class Test : IEquatable<Test>
{
private string _data;
public Test(string data)
{
_data = data;
}
public override string ToString()
{
return _data;
}
public bool Equals(Test other)
{
return _data.Equals(other._data);
}
}
static void Main()
{
var list = new List<Test>();
list.Add(new Test("ab"));
list.Add(new Test("ab"));
list.Add(new Test("a"));
list.Distinct().ToList().ForEach(item => Console.WriteLine(item));
}
В результате работы программы на консоль будет выведено:
ab
ab
a
Как видим, несмотря на то, что тип 'Test' реализует интерфейс 'IEquatable<Test>' (объявлен метод 'Equals'), этого недостаточно. В ходе выполнения программы нам не удалось получить ожидаемого результата, и коллекция содержит повторяющиеся элементы. Для устранения этой проблемы в объявление типа 'Test' необходимо добавить переопределение метода 'GetHashCode':
class Test : IEquatable<Test>
{
private string _data;
public Test(string data)
{
_data = data;
}
public override string ToString()
{
return _data;
}
public bool Equals(Test other)
{
return _data.Equals(other._data);
}
public override int GetHashCode()
{
return _data.GetHashCode();
}
}
static void Main()
{
var list = new List<Test>();
list.Add(new Test("ab"));
list.Add(new Test("ab"));
list.Add(new Test("a"));
list.Distinct().ToList().ForEach(item => Console.WriteLine(item));
}
Вновь выполним программу. В результате на консоль будет выведено:
ab
a
Мы получили корректный результат: коллекция содержит только уникальные элементы.
Анализатор обнаружил код, который, возможно, содержит опечатку. Высока вероятность, что подобный код был создан с использованием подхода Copy-Paste.
Данная диагностика выявляет два схожих по структуре блока кода, идущих один за другим и отличающихся переменной, которая несколько раз встречалась в первом блоке кода, но во втором встречается только один раз. Из этого можно сделать вывод, что переменная возможно была забыта и не заменена. Предупреждение V3127 предназначено для выявления тех случаев, если второй блок был получен путем копирования первого, при этом во втором блоке были переименованы не все переменные.
Рассмотрим пример:
if (x > 0)
{
Do1(x);
Do2(x);
}
if (y > 0)
{
Do1(y);
Do2(x); // <=
}
Вероятнее всего во втором блоке вместо переменной 'x' должна идти переменная 'y'. Корректный вариант мог бы выглядеть так:
if (x > 0)
{
Do1(x);
Do2(x);
}
if (y > 0)
{
Do1(y);
Do2(y);
}
Рассмотрим еще один пример, но уже более сложный.
....
if(erendlinen>239) erendlinen=239;
if(srendlinen>erendlinen) srendlinen=erendlinen;
if(erendlinep>239) erendlinep=239;
if(srendlinep>erendlinen) srendlinep=erendlinep; // <=
....
Заметить ошибку не так уж и просто. Имена переменных похожи друг на друга, и поэтому выявление ошибки усложняется в разы. На самом деле во втором блоке вместо переменной 'erendlinen' должна стоять 'erendlinep'.
Корректный вариант будет выглядеть так:
....
if(erendlinen>239) erendlinen=239;
if(srendlinen>erendlinen) srendlinen=erendlinen;
if(erendlinep>239) erendlinep=239;
if(srendlinep>erendlinep) srendlinep=erendlinep; // <=
....
Имена переменных 'erendlinen' и 'erendlinep' выбраны явно неудачно. Такую ошибку почти невозможно заметить при Code Review. Да что уж там, даже когда анализатор указывает на строку с ошибкой и то сложно её заметить. Поэтому, встретив предупреждение V3127, рекомендуем не спешить и внимательно изучить код.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3127. |
Анализатор обнаружил использование поля (свойства) до его инициализации в конструкторе класса.
Рассмотрим пример:
class Test
{
List<int> mylist;
Test()
{
int count = mylist.Count; // <=
....
mylist = new List<int>();
}
}
В конструкторе класса 'Test' производится доступ к свойству 'Count' списка 'mylist'. При этом инициализация списка производится позже. В результате выполнения приведенного фрагмента кода произойдет доступ по нулевой ссылке. В данном случае необходимо обеспечить предварительную инициализацию списка, например, при его объявлении:
class Test
{
List<int> mylist = new List<int>();
Test()
{
int count = mylist.Count;
....
}
}
Рассмотрим еще один пример:
class Test2
{
int myint;
Test2(int param)
{
Foo(myint); // <=
....
myint = param;
}
}
В данном случае в метод 'Foo' передают поле 'myint', которое имеет значение по умолчанию 0. Возможно, так и задумывалось, и ошибки нет. Однако, в некоторых случаях это может приводить к неожиданному поведению. Правильным решением будет явная инициализация поля 'myint', даже значением по умолчанию 0:
class Test2
{
int myint = 0;
Test2(int param)
{
Foo(myint);
....
myint = param;
}
}
Теперь коллегам и анализатору очевидно, что программист не забыл про инициализацию поля 'myint'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3128. |
Анализатор обнаружил потенциально возможную ошибку, связанную с замыканием в анонимную функцию переменной, которая используется как итератор цикла. На этапе компиляции захваченная переменная будет обернута в анонимный класс-контейнер, и в дальнейшем только один экземпляр этого класса будет передан во все анонимные функции для каждой итерации цикла. Вероятнее всего, программист будет ожидать разные значения итератора внутри каждой анонимной функции, вместо последнего, что является неочевидным поведением и может привести к ошибке.
Давайте рассмотрим данную ситуацию более подробно на примере:
void Foo()
{
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
actions.Add(() => Console.Write(i)); // <=
}
// SOME ACTION
actions.ForEach(x => x());
}
Многие подумают, что в результате выполнения метода 'Foo' в консоль будут выведены числа от 0 до 9, так как по логике при замыкании переменной 'i' в анонимную функцию после компиляции будет создан анонимный класс-контейнер, а значение переменной 'i' скопировано в одно из его полей. Но на самом деле в консоль будет 10 раз выведено число 10. Это связано с тем, что анонимный класс-контейнер создается сразу после объявления переменной 'i', а не перед объявлением анонимной функции. Таким образом, для всех экземпляров анонимных функций при каждой итерации цикла замыкается не текущее значение итератора, а ссылка на анонимный класс-контейнер, содержащий в себе последнее значение итератора. Также стоит отметить, что при компиляции объявление переменной 'i' будет вынесено перед циклом.
Во избежание данной ошибки необходимо следить, чтобы в анонимную функцию замыкалась локальная переменная для текущей итерации цикла. Корректный вариант мог бы выглядеть так:
void Foo()
{
var actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
var curIndex = i;
actions.Add(() => Console.Write(curIndex)); // <=
}
// SOME ACTION
actions.ForEach(x => x());
}
Таким образом, мы копируем значение итератора при каждой итерации в локальную переменную, и как уже нам известно, анонимный класс-контейнер будет создан при объявлении замыкаемой переменной, в данной случае - при объявлении переменной 'curIndex' со значением текущего итератора.
Давайте еще рассмотрим подозрительный код из проекта 'CodeContracts':
var tasks = new Task<int>[assemblies.Length];
Console.WriteLine("We start the analyses");
for (var i = 0; i < tasks.Length; i++)
{
tasks[i] = new Task<int>(() => CallClousotEXE(i, args)); // <=
tasks[i].Start();
}
Console.WriteLine("We wait");
Task.WaitAll(tasks);
Несмотря на то, что создание и запуск задачи (Task) идет в пределах одной итерации цикла, свою работу она начнет не сразу, поэтому велика вероятность того, что это произойдет уже после текущей итерации, что в результате приведет к выше упомянутой ошибке.
К примеру, в данной ситуации на синтетическом примере все задачи стартовали уже после завершения работы цикла, и как следствие, переменная 'i' во всех задачах была равна последнему значению итератора (10).
Корректный вариант кода мог бы выглядеть так:
var tasks = new Task<int>[10];
Console.WriteLine("We start the analyses");
for (var i = 0; i < tasks.Length; i++)
{
var index = i;
tasks[i] = new Task<int>(() => CallClousotEXE(index, args));
tasks[i].Start();
}
Console.WriteLine("We wait");
Task.WaitAll(tasks);
Анализатор обнаружил потенциальную ошибку, связанную с тем, что приоритет логического оператора '&&' выше приоритета логического оператора '||'. Про это часто забывают. В результате логическое выражение может давать совсем не тот результат, на который рассчитывал программист.
Рассмотрим пример некорректного кода:
if (c == 'l' || c == 'L' && !token.IsKeyword)
{ .... }
Скорее всего, программист ожидал, что вначале выполнится проверка равенства переменной 'c' значению 'l' или 'L'. И только затем выполнится операция '&&'. Но согласно приоритету операторов, в языке C# вначале произойдет выполнение операции '&&', а уже потом '||'.
Можно порекомендовать во всех выражениях с редко используемыми вами операторами или там где нет уверенности, писать скобки. Даже если скобки окажутся лишними, это не страшно. Зато код станет более легким для понимания и будет меньше подвержен ошибкам.
Корректный вариант кода:
if ((c == 'l' || c == 'L') && !token.IsKeyword)
{ .... }
А как убрать ложное предупреждение, если действительно планировалась последовательность вычислений: сначала логическое '&&', затем логическое '||'?
Есть несколько вариантов:
1) Плохой вариант. Можно использовать комментарий "//-V3130" для подавления предупреждения в нужной строке.
if (c == 'l' || c == 'L' && !token.IsKeyword) //-V3130
{ .... }
2) Хороший вариант. Можно добавить дополнительные скобки:
if (c == 'l' || (c == 'L' && !token.IsKeyword))
{ .... }
Дополнительные скобки помогут вашим коллегам понять, что этот код корректен.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3130. |
Анализатор обнаружил потенциально возможную ошибку, связанную с проверкой выражения на принадлежность к одному типу и приведением этого выражения в блоке проверки к другому типу.
Давайте рассмотрим данную ситуацию более подробно на примере:
if (obj is A)
{
return (B)obj;
}
Скорее всего программист ошибся, так как подобное приведение типов с большой вероятностью приведет к ошибке, и вероятнее всего подразумевалась либо проверка выражения на принадлежность к другому типу, либо приведение выражения к другому типу.
Корректный вариант мог бы выглядеть так:
if (obj is B)
{
return (B)obj;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3131. |
Анализатор обнаружил потенциальную ошибку, связанную с наличием внутри строки терминального нулевого символа.
Как правило, такая ошибка возникает вследствие опечатки. Например, последовательность "\0x0A" будет восприниматься как следующая последовательность из четырёх байт: { '\0', 'x', '0', 'A' }.
Если хочется задать код символа в шестнадцатеричном виде, то символ 'x' должен стоять сразу после символа '\'. Если написать "\0", то это будет воспринято как ноль (в формате восьмеричного числа). См. также:
Рассмотрим пример некорректного кода:
String s = "string\0x0D\0x0A";
Если попробовать распечатать эту строку, то управляющие символы для перевода строки использованы не будут. Функции вывода остановятся на символе конца строки '\0'. Для устранения этой ошибки следует заменить "\0x0D\0x0A" на "\x0D\x0A".
Корректный вариант кода:
String s = "string\x0D\x0A";
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с бессмысленным использованием постфиксного инкремента или декремента в выражении присвоения в эту же переменную.
Давайте рассмотрим пример:
int i = 5;
// Some code
i = i++;
В данном случае инкремент будет бессмысленным, и после выполнения данного кода переменная 'i' будет иметь значение '5'.
Это связано с тем, что постфиксный инкремент и декремент выполняются после вычисления правого операнда оператора присваивания, а результат вычисления временно кэшируется, и после выполнения операций постфиксного инкремента или декремента присваивается левой части выражения. Таким образом результат выполнения постфиксного инкремента или декремента перезаписывается результатом всего выражения.
Для более глубокого понимания природы данного поведения давайте рассмотрим IL код вышеупомянутого примера:
-======- НАЧАЛО ОПЕРАЦИИ "int i = 5" -======-
// Объявление локальной переменной 'i'
// Текущий стек => []
.locals init ([0] int32 i)
// Передача числа 5 на верх стека
// Текущий стек => [5]
IL_0001: ldc.i4.5
// Присвоение переменной 'i' числа 5 со стека
// Текущий стек => []
IL_0002: stloc.0
-======- КОНЕЦ ОПЕРАЦИИ "int i = 5" -======-
-======- НАЧАЛО ОПЕРАЦИИ "i = i++" -======-
// Передача на верх стека значения переменной 'i'
// Текущий стек => [5]
IL_0003: ldloc.0
-======- НАЧАЛО ОПЕРАЦИИ "i++" -======-
// Копия верхнего значения на стеке
// Текущий стек => [5, 5]
IL_0004: dup
// Передача числа 1 на верх стека
// Текущий стек => [1, 5, 5]
IL_0005: ldc.i4.1
// Операция сложения 2 верхних чисел со стека (5 + 1)
// Результат вычисления (6) передается на верх стека
// Текущий стек => [6, 5]
IL_0006: add
// Присвоение переменной 'i' числа 6 со стека
// Текущий стек => [5]
IL_0007: stloc.0
-======- КОНЕЦ ОПЕРАЦИИ "i++" -======-
// Присвоение переменной 'i' числа 5 со стека
// Текущий стек => []
IL_0008: stloc.0
-======- КОНЕЦ ОПЕРАЦИИ "i = i++" -======-
Корректный же пример может иметь разный вид в зависимости от изначальной задачи.
Это может быть опечатка, и на самом деле программист случайно написал 2 раза переменную 'i' в выражении присвоения. Тогда корректный вариант мог бы выглядеть так:
int i = 5;
// Some code
q = i++;
Либо же программист не знал, что оператор постфиксного инкремента прибавляет единицу к значению переменной, но возвращает начальное ее значение. Тогда операция присвоения будет излишней, и корректный вариант мог бы выглядеть так:
int i = 5;
// Some code
i++;
На первый взгляд может показаться, что это синтетический тест, и никто так не пишет, но на самом деле данную ошибку можно встретить и в серьезных проектах. Давайте рассмотрим аналогическую ошибку, найденную в проекте 'MSBuild'.
_parsePoint =
ScanForPropertyExpressionEnd(expression, parsePoint++);
Инкремент переменной '_parsePoint' будет бессмысленным, так как будет вычислен после передачи изначального значения этой переменной в метод ' ScanForPropertyExpressionEnd', и никак не повлияет на результат выполнения данного метода. Вероятнее всего программист перепутал постфиксный инкремент с префиксным инкрементом. Тогда корректный вариант мог бы выглядеть следующим образом:
_parsePoint =
ScanForPropertyExpressionEnd(expression, ++_parsePoint);
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную со сдвигом целого числа на 'N' бит, при этом 'N' больше размера этого числового типа в битах.
Давайте рассмотрим пример:
UInt32 x = ....;
UInt32 y = ....;
UInt64 result = (x << 32) + y;
В данном случае хотели собрать 64-битное число из 2-х 32-битных, сдвинув 'x' на 32 бита и сложив старшую и младшую часть. Но поскольку 'x' в момент сдвига является 32-битным числом, то сдвиг на 32 бита эквивалентен сдвигу на 0 бит, что приведет к некорректному результату.
Корректный вариант может выглядеть так:
UInt32 x = ....;
UInt32 y = ....;
UInt64 result = ((UInt64)x << 32) + y;
Также давайте рассмотрим пример из реального проекта:
static long GetLong(byte[] bits)
{
return ((bits[0] & 0xff) << 0)
| ((bits[1] & 0xff) << 8)
| ((bits[2] & 0xff) << 16)
| ((bits[3] & 0xff) << 24)
| ((bits[4] & 0xff) << 32)
| ((bits[5] & 0xff) << 40)
| ((bits[6] & 0xff) << 48)
| ((bits[7] & 0xff) << 56);
}
В методе 'GetLong' выполняется преобразование массива байт в 64-битное число. Поскольку операторы битового сдвига определены только для 32 и 64-битных чисел, то каждый байт будет неявно преобразован в 'Int32'. Диапазон битового сдвига 32-битного числа - [0..31], поэтому преобразование будет выполняться корректно только для первых 4 байт массива.
Если предположить, что массив байт был сформирован из 64-битного числа (К примеру из 'Int64.MaxValue'), то при обратном преобразовании из массива байт в Int64 данным методом возможна ошибка, в случае, если изначальное число не находилось в диапазоне [Int32.MinValue....Int32.MaxValue].
Для большей наглядности давайте рассмотрим работу данного кода для числа '289077008695033855'. Данное число после преобразования в массив байт будет иметь вид:
289077008695033855 => [255, 255, 255, 255, 1, 2, 3, 4]
Передав этот массив байт в метод 'GetLong' перед операцией сдвига каждый байт будет неявно преобразован в Int32. Давайте выполним каждую операцию сдвига отдельно, чтобы понять в чем проблема.
Как видим, сдвиг выполняется для 32-битного числа, что в итоге приводит к пересечению диапазонов и как следствие к неправильному результату. Это происходит потому, что при попытке сдвинуть 32-битное число более чем на 32 бита мы начинаем сдвигать биты по кругу (сдвиг на 32, 40, 48 и 56 бит идентичен сдвигу на 0, 8, 16 и 24 соответственно)
Исправленный вариант данного метода мог бы выглядеть так:
static long GetLong(byte[] bits)
{
return ((long)(bits[0] & 0xff) << 0)
| ((long)(bits[1] & 0xff) << 8)
| ((long)(bits[2] & 0xff) << 16)
| ((long)(bits[3] & 0xff) << 24)
| ((long)(bits[4] & 0xff) << 32)
| ((long)(bits[5] & 0xff) << 40)
| ((long)(bits[6] & 0xff) << 48)
| ((long)(bits[7] & 0xff) << 56);
}
Рассмотрев каждую операцию сдвига отдельно, мы увидим, что теперь сдвиг выполняется для 64-битного числа, что в итоге препятствует пересечению диапазонов.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3134. |
Анализатор выявил цикл, который может содержать ошибку или быть неоптимальным. Используется типичный паттерн кода, когда для всех пар элементов массива выполняется некая операция. При этом, как правило, нет смысла выполнять операцию для пары, состоящей из одного и того-же элемента при 'i == j'.
Пример:
for (int i = 0; i < size; i++)
for (int j = i; j < size; j++)
...
Есть большая вероятность, что правильнее или эффективнее использовать следующий код для обхода массивов:
for (int i = 0; i < size; i++)
for (int j = i + 1; j < size; j++)
...
Данная диагностика классифицируется как:
Анализатор обнаружил константное выражение в условии 'switch'. Чаще всего это сигнализирует о логической ошибке.
Рассмотрим синтетический пример:
int i = 1;
switch (i)
{
....
}
В качестве условия 'switch' стоит переменная, значение которой может быть посчитано во время компиляции. Такая ситуация могла возникнуть в результате рефакторинга: раньше был код, который менял значение переменной, а потом его поменяли и оказалось, что переменной больше не присваивается никакое значение.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3136. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что локальной переменной присваивается значение, но переменная далее нигде не используется до выхода из метода.
Рассмотрим фрагмент кода:
private string GetDisplayName(string name)
{
MyStringId tmp = MyStringId.GetOrCompute(name);
string result;
if (!MyTexts.TryGet(tmp, out result))
result = name;
return name;
}
Программист хотел, чтобы результатом метода была переменная 'result', которая инициализируется в зависимости от выполнения 'TryGet'. К сожалению, из-за опечатки метод всегда возвращает переменную 'name'. Правильный код должен выглядеть следующим образом:
private string GetDisplayName(string name)
{
MyStringId tmp = MyStringId.GetOrCompute(name);
string result;
if (!MyTexts.TryGet(tmp, out result))
result = name;
return result;
}
Рассмотрим еще один фрагмент кода:
protected DateTimeOffset? GetFireTimeAfter()
{
DateTimeOffset sTime = StartTimeUtc;
DateTimeOffset? time = null;
....
if (....)
{
....
time = sTime;
}
else if (....)
{
....
time = sTime;
}
....
//apply the timezone before we return the time.
sTime = TimeZoneUtil.ConvertTime(time.Value, this.TimeZone);
return time;
}
В нескольких блоках 'if' в переменную 'time' записывается значение 'sTime', хранящее некоторое стартовое время, увеличенное на некоторый интервал. В конце метода переменная 'time' возвращается. Перед возвращением 'time', судя по комментарию в коде, программист хочет скорректировать представление времени, учитывая временную зону. По ошибке корректируется временная зона переменной 'sTime', которая более нигде не используется. Правильный код, скорее всего, должен выглядеть следующим образом:
protected DateTimeOffset? GetFireTimeAfter()
{
DateTimeOffset sTime = StartTimeUtc;
DateTimeOffset? time = null;
....
//apply the timezone before we return the time.
time = TimeZoneUtil.ConvertTime(time.Value, this.TimeZone);
return time;
}
Часто встречается код, в котором при объявлении переменной присваивается какое-то значение, а затем эта переменная более нигде не используется. Обычно это не является ошибкой. Например, это может быть сделано в соответствии с принятым в компании стандартом программирования, при котором требуется всегда сохранять в переменной результат работы любого метода, даже если этот результат никак не используется. Например:
void SomeMethod()
{
....
int result = DoWork();
....
}
Для таких ситуаций в анализаторе сделано исключение и диагностика не выдаст предупреждение.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3137. |
Анализатор обнаружил строку, которая могла бы содержать интерполированное выражение, но символ интерполяции '$' у литерала отсутствует.
Рассмотрим фрагмент кода:
string test = "someText";
....
Console.WriteLine("{test}");
Из-за пропущенного знака '$' перед объявлением строки в консоль будет выведено имя переменной. Корректная запись будет выглядеть так:
string test = "someText";
....
Console.WriteLine($"{test}");
Также потенциальными ошибками считаются строки, содержащие выражения:
int a = 1;
int b = 1;
string test = "{a:c} test";
string test1 = "{a+b} test1 {{{ a + b }}}";
Исключениями считаются строковые литералы, передаваемые в методы в качестве аргумента, когда другими аргументами этих методов являются переменные, содержащиеся в этом литерале.
string test1 = ReplaceCustom("someText {test}", "{test}", test);
В этом случае выражение в литерале часто бывает меткой для замены на значение переменной, также передаваемой в метод.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3138. |
Анализатор обнаружил ситуацию, когда в операторе switch разные метки case содержат одинаковые фрагменты кода. Часто это свидетельствует об избыточном коде, который можно улучшить объединением меток. Но нередко одинаковые фрагменты кода могут быть причиной copy-paste программирования и являться настоящими ошибками.
Рассмотрим пример с избыточным кодом:
switch (switcher)
{
case 0: Console.Write("0"); return;
case 1: Console.Write("0"); return;
default: Console.Write("default"); return;
}
Действия для нескольких значений 'switcher' действительно могут быть одинаковыми, поэтому код можно написать более компактно:
switch (switcher)
{
case 0:
case 1: Console.Write("0"); return;
default: Console.Write("default"); return;
}
Если вы используете 'case expression', то объединить такие выражения под одно условие у вас не получится:
private static void ShowCollectionInformation(object coll, bool cond)
{
switch (coll)
{
case Array arr:
if(cond)
{
Console.WriteLine (arr.ToString());
}
break;
case IEnumerable<int> arr:
if(cond)
{
Console.WriteLine (arr.ToString());
}
break;
}
}
В таком случае, вы можете вынести общий код в метод, что облегчит дальнейшее редактирование и отладку.
Теперь, рассмотрим пример из реального приложения, где разработчик допустил ошибку:
switch (status.BuildStatus)
{
case IntegrationStatus.Success:
snapshot.Status = ItemBuildStatus.CompletedSuccess;
break;
case IntegrationStatus.Exception:
case IntegrationStatus.Failure:
snapshot.Status = ItemBuildStatus.CompletedSuccess;
break;
}
В присвоение статуса попала ошибка: у перечисления 'ItemBuildStatus' есть элемент 'CompletedFailed', который следовало присвоить в случае ошибки или исключения.
Исправленный код:
switch (status.BuildStatus)
{
case IntegrationStatus.Success:
snapshot.Status = ItemBuildStatus.CompletedSuccess;
break;
case IntegrationStatus.Exception:
case IntegrationStatus.Failure:
snapshot.Status = ItemBuildStatus. CompletedFailed;
break;
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3139. |
Анализатор обнаружил свойство, использующее разные поля в get и set методах доступа. Это может быть следствием опечатки или копирования тела другого свойства.
Рассмотрим пример из реального приложения, где разработчик допустил подобную ошибку:
String _heading; String _copyright;
public string Heading
{
get { return this._heading; }
set { this._heading = value; }
}
public string Copyright
{
get { return this._heading; }
set { this._copyright = value; }
}
Метод доступа get свойства 'Copyright' должен был возвращать поле '_copyright', a не поле '_heading'.
Исправленный код будет выглядеть следующим образом:
String _heading; String _copyright;
public string Heading
{
get { return this._heading; }
set { this._heading = value; }
}
public string Copyright
{
get { return this._copyright; }
set { this._copyright = value; }
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3140. |
Анализатор обнаружил передачу потенциального null значения в throw выражение. В случае передачи null в выражение throw, исполняемая среда .NET генерирует исключение разыменования нулевой ссылки (NullReferenceException), хотя фактического разыменования в вашем коде произведено не будет.
Например, в таком коде:
private Exception GetException(String message)
{
if (message == null)
return null;
return new Exception(message);
}
....
throw GetException(message);
произойдёт передача в выражение throw значения null, если параметр 'message' имеет значение null.
С точки зрения дальнейшей работы с исключением, такое поведение может быть неочевидным или нежелательным. Во-первых, стек исключения NullReferenceException, сгенерированный на throw, будет указывать на сам throw, а не на причину, к нему приводящую (т.е. на возврат значения null из метода 'GetException'). Во-вторых, сама генерация в данной ситуации NullReferenceException выглядит неправильной, т.к. фактически разыменования нулевой ссылки не произошло, и это в дальнейшем может затруднить отладку.
Чтобы сделать последующую отладку подобной ситуации более удобной, лучше либо проверить перед выбрасыванием исключения значение, возвращаемое методом 'GetException', либо вместо возврата null в этом методе бросить более конкретное исключение, которое точнее будет описывать возникшую ситуацию с неожидаемым значением, приходящим в метод.
Исправленный код может выглядеть следующим образом:
private Exception GetException(String message)
{
if (message == null)
throw new ArgumentException();
return new Exception(message);
}
Анализатор обнаружил код, который никогда не будет выполнен. Возможно, допущена ошибка в логике программы.
Данная диагностика находит блоки кода, до которых никогда не дойдёт управление.
Рассмотрим пример:
static void Foo()
{
Environment.Exit(255);
Console.WriteLine("Hello World!");
}
Функция 'Console.WriteLine (....)' недостижима, так как функция 'Exit()' не возвращает управление. Как правильно исправить код, зависит от того, какую логику поведения задумывал программист изначально. Возможно, нарушен порядок выражений и корректный код должен быть таким:
static void Foo()
{
Console.WriteLine("Hello World!");
Environment.Exit(255);
}
Рассмотрим ещё один пример:
static void ThrowEx()
{
throw new Exception("Programm Fail");
}
public void SetResponse(int response)
{
ThrowEx();
Debug.Assert(false); //should never reach here
}
В данном примере межпроцедурный анализ проверяет метод 'ThrowEx' и предупреждает, что код после вызова метода недостижим. Если такое поведение ожидаемо - вы можете отметить предупреждение как ложное.
public void SetResponse(int response)
{
ThrowEx();
Debug.Assert(false); //should never reach here //-V3142
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3142. |
Анализатор обнаружил присвоение некоторого значения параметру 'value' внутри setter'а свойства. При этом, после этого присвоения параметр 'value' больше не использовался в теле этого setter'а, что возможно свидетельствует об ошибке или опечатке.
Рассмотрим пример из реального проекта:
public LoggingOptions Options
{
get { return m_loggingOptions; }
set { value = m_loggingOptions; }
}
Здесь значение параметра 'value' перезаписывается сразу после входа в setter метод свойства. При этом изначальное значение параметра 'value' будет потеряно. Возможно, что разработчик опечатался и перепутал 'value' и 'm_loggingOptions' местами. Если же разработчик не хотел давать возможность записи в это свойство, то его можно было бы объявить с private set методом или вообще не объявлять setter.
Исправленный вариант может выглядеть так:
public LoggingOptions Options
{
get { return m_loggingOptions; }
set { m_loggingOptions = value; }
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3143. |
Анализатор обнаружил в файле copyleft лицензию, которая обязывает открыть остальной исходный код. Это может быть неприемлемо для многих коммерческих проектов.
Если вы разрабатываете открытый проект, то можно просто игнорировать это предупреждение и отключить его.
Пример комментария, на который анализатор выдаст предупреждение:
/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Если в закрытый проект добавить файл с такой лицензией (GPL3 в данном случае), то остальной исходный код необходимо будет открыть, из-за особенностей данной лицензии.
Такой тип copyleft лицензий называют "вирусными" лицензиями, из-за их свойства распространяться на остальные файлы проекта. Проблема в том, что использование хотя бы одного файла с подобной лицензией в закрытом проекте автоматически делает весь исходный код открытым и обязывает распространять его вместе с бинарными файлами.
Диагностика занимается поиском следующих "вирусных" лицензий:
Есть следующие варианты, как вы можете поступить, обнаружив в закрытым проекте использование файлов с copyleft лицензий:
Мы понимаем, что данная диагностика неуместна для открытых проектов. Команда PVS-Studio способствует развитию открытых проектов, помогая исправлять в них ошибки и предоставляя бесплатные варианты лицензий. Однако наш продукт является B2B решением, и поэтому данная диагностика по умолчанию включена.
Если же ваш код распространяется под одной из указанных выше copyleft лицензий, то вы можете отключить данную диагностику следующими способами:
Если вам известны ещё типы "вирусных" лицензий, которые в данный момент не выявляет инструмент, то вы можете сообщить нам о них через форму обратной связи. И мы добавим их выявление в следующем релизе.
Анализатор обнаружил небезопасное использование объекта типа 'WeakReference', которое может привести к разыменованию нулевой ссылки.
Рассмотрим пример:
string Foo(WeakReference weak)
{
return weak.Target.ToString();
}
Так как объект, на который 'WeakReference' хранит ссылку, может быть в любой момент очищен сборщиком мусора, всегда существует вероятность, что свойство 'Target' вернёт значение 'null'.
В таком случае, при попытке вызова метода 'ToString' может произойти разыменование нулевой ссылки и будет выброшено исключение 'NullReferenceException'. Один из вариантов защиты от потенциального удаления объекта – запись его в локальную переменную, на время работы с ним. Исправленный код будет выглядеть так:
string Foo(WeakReference weak)
{
var weakTarget = weak.Target;
return weakTarget != null ? weakTarget.ToString() : String.Empty;
}
После записи ссылки в локальную переменную, сборщик мусора уже не сможет удалить этот объект, пока ссылка на него находится на стеке. Тем не менее, после записи объекта в локальную переменную её обязательно нужно проверить на 'null', т.к. в момент доступа к свойству 'Target' объект, на который 'Target' указывал, уже мог быть удалён сборщиком мусора.
Проверка на существование объекта внутри 'WeakReference' с помощью свойства 'IsAlive' также не защитит от возникновения 'NullReferenceException', так как между проверкой 'IsAlive' и разыменованием свойства 'Target' объект также может быть удалён сборщиком мусора:
char Foo(WeakReference weak)
{
if (weak.IsAlive)
return (weak.Target as String)[0];
return ' ';
}
Корректное использование свойства 'IsAlive' может выглядеть так:
char Foo(WeakReference weak)
{
var str = weak.Target as String;
if (weak.IsAlive)
return str[0];
return ' ';
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3145. |
Анализатор обнаружил небезопасное использование результата вызова одного из методов библиотеки System.Enumerable, которые могут вернуть 'default' значение.
Примеры таких методов: 'FirstOrDefault', 'LastOrDefault', 'SingleOrDefault' и 'ElementAtOrDefault'. Эти методы возвращают значение по умолчанию, если в массиве нет ни одного объекта, удовлетворяющего предикату поиска. Значением по умолчанию для ссылочных типов является пустая ссылка (null). Соответственно, прежде чем использовать полученную ссылку, её следует проверить.
Пример опасного разыменования:
public void TestMemberAccess(List<string> t)
{
t.FirstOrDefault(x => x == "Test message").ToString();
}
В этом случае стоит добавить проверку возвращаемого элемента на null:
public void TestMemberAccess(List<string> t)
{
t.FirstOrDefault(x => x == "Test message")?.ToString();
}
Повышенную опасность методы, возвращающие default значения, представляют в цепочках вызовов. Пример из открытого проекта:
public IViewCompiler GetCompiler()
{
....
_compiler = _services
.GetServices<IViewCompilerProvider>()
.FirstOrDefault()
.GetCompiler();
}
....
return _compiler;
}
Если вы уверены, что при обработке массива в нём есть нужный вам элемент – мы советуем использовать метод, не возвращающий default значение:
public IViewCompiler GetCompiler()
{
....
_compiler = _services
.GetServices<IViewCompilerProvider>()
.First()
.GetCompiler();
}
....
return _compiler;
}
В этом случае в случае ошибки вы получите не 'NullReferenceException', а 'InvalidOperationException' с более понятным сообщением: "Sequence contains no elements".
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3146. |
Анализатор обнаружил неатомарное изменение 'volatile' переменной, которое может привести к состоянию гонки.
Известно, что использование модификатора 'volatile' гарантирует, что все потоки будут видеть актуальное значение соответствующей переменной. Модификатор 'volatile' используется для того, чтобы указать CLR, что все операции присвоения этой переменной и все операции чтения из неё должны быть атомарными.
Можно посчитать, что пометки переменных как 'volatile' будет достаточно, чтобы безопасно использовать все возможные операции присвоения в многопоточном приложении.
Помимо простых операций присвоения, существуют также операции, изменяющие значение переменной перед записью. К таким операциям можно отнести:
Такая запись выглядит как одна операция, но в действительности это целая последовательность операций чтения-изменения-записи.
Рассмотрим использование 'volatile' переменной в качестве счетчика (counter++).
class Counter
{
private volatile int counter = 0;
....
public void increment()
{
counter++; // counter = counter + 1
}
....
}
В IL коде операция инкремента раскрывается в следующие команды:
IL_0001: ldarg.0
IL_0002: ldarg.0
IL_0003: volatile.
IL_0005: ldfld int32
modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)
VolatileTest.Test::val
IL_000a: ldc.i4.1
IL_000b: add
IL_000c: volatile.
IL_000e: stfld int32
modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)
VolatileTest.Test::val
Здесь и кроется состояние гонки. Предположим, что 2 потока одновременно работают с одним и тем же экземпляром объекта типа Counter и выполняют инкремент переменной 'counter', изначально проинициализированной значением 10. При этом, оба потока будут работать одновременно, выполняя промежуточные действия над переменной counter, каждый на своём собственном стеке (назовём эти промежуточные значения temp1 и temp2):
[counter == 10, temp1 == 10] Поток N1 считывает значение 'counter' на свой стек. (операция ldfld в IL)
[counter == 10, temp1 == 11] Поток N1 изменяет значение temp1 на своём стеке. (операция add в IL)
[counter == 10, temp2 == 10] Поток N2 считывает значение 'counter' на свой стек. (операция ldfld в IL)
[counter == 11, temp1 == 11] Поток N1 записывает temp1 в 'counter'. (операция stfld в IL)
[counter == 11, temp2 == 11] Поток N2 изменяет значение temp2 на своём стеке. (операция add в IL)
[counter == 11, temp2 == 11] Поток N2 записывает temp2 в 'counter'. (операция stfld в IL)
Ожидалось значение переменной 'counter' равное 12 (а не 11), так как 2 потока выполнили инкремент над одной и той же переменной. Также возможна ситуация, когда потоки выполнят инкремент друг за другом, и в таком случае все будет так, как и ожидалось.
Чтобы избежать подобного поведения неатомарных операций для разделяемых переменных, можно использовать:
Пример корректного кода:
class Counter
{
private volatile int counter = 0;
....
public void increment()
{
Interlocked.Increment(ref counter);
}
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил небезопасное приведение ссылки, потенциально содержащей значение 'null', к значимому типу. Хотя в данном случае не происходит прямого разыменования, такое приведение типа приводит к выбросу 'NullReferenceException'.
Простейший пример получения такого исключения:
void Foo()
{
object b = null;
var c = (bool)b;
}
Исправленный код будет выглядеть так:
void Foo()
{
object b = null;
var c = (bool)(b ?? false);
}
Также предупреждение будет сгенерировано при приведении переменной с потенциальным 'null' значением к любому значимому типу, например, к структуре:
protected override void ProcessMessageAfterSend(....)
{
....
(DateTime)msg.GetMetadata(....);
....
}
public object GetMetadata(string tag)
{
object data;
if (metadata != null && metadata.TryGetValue(tag, out data))
{ return data; }
return null;
}
Диагностика проверила результат вызова метода 'GetMetadata' и обнаружила там потенциальный 'null'. В данном случае также стоило добавить проверку на null перед приведением типа:
protected override void ProcessMessageAfterSend(....)
{
....
(DateTime)(msg.GetMetadata(....) ?? new DateTime());
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3148. |
Анализатор обнаружил небезопасное разыменование результата приведения типа через оператор 'as'.
Рассмотрим пример:
void Foo()
{
BaseItem a = GetItem();
var b = a as SpecificItem;
b.Bar();
}
Возвращаемое методом значение может иметь тип, отличный от типа, к которому мы его приводим. Тогда при приведении типа с помощью оператора 'as' в переменную 'b' будет записано значение null. Хотя при самом приведении типа ошибки не будет, при последующем использовании этой переменной без проверки произойдёт выброс исключения 'NullReferenceException'. Исправленный код будет выглядеть так:
void Foo()
{
BaseItem a = GetItem();
var b = a as SpecificItem;
b?.Bar();
}
Если вы уверены, что runtime тип значения у переменной, приводимой с помощью оператора 'as', всегда может быть успешно приведён к указанному типу, то лучше воспользоваться оператором явного приведения типа:
void Foo()
{
BaseItem a = GetItem();
var b = (SpecificItem)a;
b.Bar();
}
В этом случае, если в будущем поведение программы поменяется и метод 'GetItem' перестанет всегда возвращать значение, гарантированно приводимое к заданному типу, в месте приведения будет сгенерировано исключение 'InvalidCastException', что позволит сразу идентифицировать проблемное место в коде. Напротив, используя оператор 'as' в такой ситуации, можно получить 'NullReferenceException' далее по коду, когда приведённая неуспешно переменная, содержащая null, будет разыменована. Причём это может произойти далеко от места, где производится приведение типов, например в другом методе, что затруднит локализацию и исправление такой ошибки.
Также диагностика указывает на возможные опечатки при проверках типа:
void Foo()
{
IDisposable a = GetItem();
if(a is NonSpecificItem)
{
var b = a as SpecificItem;
b.Bar();
}
}
В данном примере типы SpecificItem и NonSpecificItem не связаны друг с другом и результатом приведения будет нулевой указатель. Для защиты от таких опечаток можно использовать возможности C# 7.0 и сделать проверку через Is Type Pattern синтаксис:
void Foo()
{
IDisposable a = GetItem();
if(a is NonSpecificItem item)
{
item.Bar();
}
}
Приведём пример работы диагностики на коде из открытого проекта:
....
FuelDefinition = MyDefinitionManager.Static.GetPhysicalItemDefinition(FuelId);
MyDebug.AssertDebug(FuelDefinition != null);
....
String constraintTooltip = FuelDefinition.DisplayNameText;
В данном примере метод 'GetPhysicalItemDefinition' возвращает объект типа MyPhysicalItemDefinition, полученный из массива объектов базового типа 'MyDefinitionBase':
public MyPhysicalItemDefinition GetPhysicalItemDefinition(MyDefinitionId id)
{
....
return m_definitions.m_definitionsById[id] as MyPhysicalItemDefinition;
}
При этом после вызова метода 'GetPhysicalItemDefinition' присутствует проверка результата приведения на null (MyDebug.AssertDebug), что указывает на потенциальную возможность получения из метода объекта неподходящего типа. Однако данная проверка будет работать только в Debug версии приложения. В Release версии неудачное приведение приведёт к разыменованию нулевой ссылки далее по ходу выполнения программы (FuelDefinition.DisplayNameText).
Данная диагностика классифицируется как:
Анализатор обнаружил цикл, условия выхода которого не зависят от количества итераций. Такой цикл может выполнится 0, 1, либо бесконечное количество раз.
Пример такого цикла:
void Foo(int left, int right)
{
while(left < right)
{
Bar();
}
}
Ошибка здесь в цикле while - переменные, которые находятся в условии, никак не меняют своих значений, поэтому цикл никогда не завершится, либо никогда не запустится.
Рассмотрим другой пример кода, на который сработает данная диагностика. Цикл может стать бесконечным, если в блоке 'try-catch' забыть перебросить исключение дальше по стеку:
while (condition)
{
try {
if(Foo())
{
throw new Exception();
}
}
catch (Exception ex)
{
....
}
}
Чтобы такой цикл прервался выбросом исключения, можно, например, перебросить это исключение из catch секции с помощью выражения throw:
while (condition)
{
try {
if(Foo())
{
throw new Exception();
}
}
catch (Exception ex)
{
....
throw;
}
}
Анализатор обнаружил потенциальную ошибку, которая может привести к делению на ноль.
Анализатор заметил в коде следующую ситуацию. Сначала происходит деление на некоторую переменную. Затем эта переменная сравнивается с нулём. Это может означать одно из двух:
1) Возникнет ошибка, если число будет равно 0.
2) Программа всегда работает корректно, так как число всегда не равно 0. Проверка является лишней.
Рассмотрим пример кода, на который выдаётся предупреждение анализатора.
int Foo(int num)
{
result = 1 / num;
if (num == 0) return -1;
....
}
Если число 'num' окажется равно нулю, то выражение '1 / num' приведёт к ошибке. Анализатор выдаст предупреждение на этот код, указав 2 строки. Первая строка - это то место, где происходит деление на число. Вторая строка - это то место, где число сравнивается с нулём.
Исправленный вариант кода:
int Foo(int num)
{
if (num == 0) return -1;
result = 1 / num;
....
}
Рассмотрим второй вариант срабатывания, в котором нет ошибки, а проверка на ноль избыточна.
int num = MyOneTenRandom();
result = 1 % num;
if (num == 0) return -1;
Этот код всегда работает корректно. Функция 'MyOneTenRandom' написана так, что никогда не возвращает ноль. Однако анализатор не разобрался в этой ситуации (например, если этот метод виртуальный и межпроцедурный анализ не смог выбрать, какая именно в runtime'е реализация метода будет вызвана), и выдал предупреждение. Чтобы оно исчезло, следует удалить проверку "if (num == 0)". Она не имеет практического смысла и может только запутать программиста, читающего данный код.
Исправленный вариант:
int num = MyOneTenRandom();
result = 1 % num;
В случае если анализатор ошибается, то, кроме изменения кода, можно использовать комментарий для подавления предупреждений. Пример: "1 % num; //-V3151".
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, которая может привести к делению на ноль.
Анализатор заметил в коде следующую ситуацию. В начале вы сравниваете некоторое число с нулём, а потом делите на него уже без проверки. Это может означать одно из двух:
1) Возникнет ошибка, если число будет равно 0.
2) Программа всегда работает корректно, так как число всегда не равно 0. Проверка является лишней.
Рассмотрим первый вариант. Здесь может возникнуть исключение.
int num = Foo();
if (num != 0)
variable1 = 3 / num;
variable2 = 5 / num;
Если значение переменной 'num' окажется равно нулю, то выполнение выражения '5 / num ' приведёт к ошибке. Анализатор выдаст предупреждение на этот код, указав на 2 строки. Первая строка - это то место, где происходит деление на переменную. Вторая строка - это то место, где переменная сравнивается с нулём.
Исправленный вариант кода:
int num = Foo();
if (num != 0)
{
variable1 = 3 / num;
variable2 = 5 / num;
}
Рассмотрим второй вариант. Деление безопасное, проверка лишняя:
List<string> list = CreateNonEmptyList();
var count = list.Count;
if (count == 0) {....}
var variable = 10 / count;
Предположим, что метод 'CreateNonEmptyList' возвращает список 'list' всегда с ненулевым числом элементов. Тогда приведённый выше код всегда будет работать корректно, а деления на 0 никогда не случится
Примечание: в данном примере не всегда будет сгенерировано предупреждение V3152 - если анализатор сможет понять, что данный метод возвращает всегда непустой список, то он выдаст на проверку 'list.Count == 0' предупреждение V3022 "данное выражение всегда истинно". Если не сможет (например, из-за сложной последовательности переприсвоений переменных, когда этот метод виртуальный, и т.п.) - будет сгенерировано предупреждение V3152. Тип сгенерированного предупреждения будет зависеть от реализации метода 'CreateNonEmptyList'.
Чтобы предупреждение исчезло, можно удалить проверку 'if (list == null || list.Count == 0)'. В данном случае она не имеет практического смысла и может только запутать программиста, читающего код.
Исправленный вариант:
List<string> list = CreateNonEmptyList();
var variable = 10 / list.Count;
Ещё один вариант сообщения анализатора - когда проверка и использование расположены в разных ветках if\else или switch выражений. Например:
if (lines.Count == 1)
{
if (num != 0)
variable = 10 / num;
}
else
{
variable = 10 / num;
}
В такой ситуации, несмотря на то что обе ветки никогда не выполняются одновременно, а будет выбрана только одна из веток, сравнение с нулём в одной из них косвенно свидетельствует о возможности принятия переменной значения ноль и в другой ветке. Тогда, если управление придёт во вторую ветку, произойдёт деление на ноль.
Исправленный вариант:
if (lines.Count == 1)
{
if (num != 0)
variable = 10 / num;
}
else
{
if (num != 0)
variable = 10 / num;
}
В случае если анализатор ошибается, то кроме изменения кода, можно использовать комментарий для подавления предупреждений. Например:
variable = 10 / num; //-V3152
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, которая может привести к разыменованию нулевой ссылки. Результат работы оператора '?.' сразу явно или неявно разыменовывается.
Разыменование может произойти в нескольких случаях:
Сценарии выше могут привести к:
Рассмотрим первый вариант. Здесь может возникнуть исключение:
var t = (obj?.ToString()).GetHashCode();
Если объект 'obj' окажется равен 'null', то в выражении 'obj?.ToString()' метод 'ToString()' не будет вызван. Это принцип работы оператора условного доступа. Но метод 'GetHashCode()' всё равно будет вызван, поскольку его вызов уже не будет относиться к оператору условного доступа.
Исправленный вариант кода:
var t = obj?.ToString().GetHashCode();
Здесь, кроме отсутствия опасного разыменования, переменная 't' теперь будет иметь тип 'Nullable<int>', что корректно отражает её содержимое как потенциально содержащее значение 'null'.
Рассмотрим теперь случай, когда разыменование безопасное, а проверка лишняя:
object obj = GetNotNullString();
....
var t = ((obj as String)?.Length).GetHashCode();
Этот код всегда работает корректно. Объект 'obj' всегда имеет тип 'String', а значит проверка после приведения типа избыточна.
Исправленный вариант:
var t = ((String)obj).Length.GetHashCode();
Рассмотрим пример с циклом 'foreach':
void DoSomething(string[] args)
{
foreach (var str in args?.Where(arg => arg != null))
....
}
В данном случае, если параметр 'args' равен 'null', то результат выражения 'args?.Where(....)' также будет равен 'null' из-за оператора '?.'. В таком случае будет выброшено исключение типа 'NullReferenceException' при попытке перебора коллекции в 'foreach'. Это произойдёт из-за того, что у результата выражения 'args?.Where(....)' неявно будет вызван метод 'GetEnumerator()', что приведет к разыменованию нулевой ссылки.
Исправленный вариант кода может быть таким:
void DoSomething(string[] args)
{
foreach (var str in args?.Where(arg => arg != null)
?? Enumerable.Empty<string>())
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3153. |
Операция получения остатка от деления, выполненная на целочисленном типе, всегда возвращает 0. Это связано с тем, что справа от оператора находится значение 1 (-1). Скорее всего была допущена опечатка в правом операнде.
Частым паттерном данной ошибки является проверка делимости числа на 2 без остатка. Для этого необходимо поделить число по модулю и сравнить результат с 0 или 1. В этом месте легко сделать опечатку, так как раз ожидается значение 1, то кажется, что и поделить надо на 1. Пример:
if ((x % 1) == 1)
{
....
}
Здесь вместо 2 написали 1 и теперь вне зависимости от значения 'x' выражение 'x % 1' всегда будет равно 0. Следовательно, условие всегда будет ложным.
Исправленный вариант:
if ((x % 2) == 1)
{
....
}
Рассмотрим диагностику на реальном примере:
const int SyncReportFrequency = 1;
....
private void TimerOnElapsed(object sender, ElapsedEventArgs e)
{
if (_reportId % SyncReportFrequency == 0)
{
WriteSyncReport();
}
}
Переменная 'SyncReportFrequency ' является константой равной единице. Поэтому при любом значении переменной '_reportId' условие в операторе 'if' всегда будет истинным.
Возможно, в данном фрагменте кода разработчик совершил опечатку или условие является излишним. Пример исправленного варианта кода:
const int SyncReportFrequency = 2;
....
private void TimerOnElapsed(object sender, ElapsedEventArgs e)
{
if (_reportId % SyncReportFrequency == 0)
{
WriteSyncReport();
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительное выражение, результат которого всегда будет равен одному из его операндов. Такие выражения могут являются избыточными по задумке автора, и должны передать какую-то идею тем, кто будет сопровождать код. А возможно это просто ошибка.
Рассмотрим простой вариант, как может выглядеть ошибка:
var a = 11 - b + c * 1;
Переменная 'c' умножается на '1', что является избыточным. Возможно, была допущена опечатка и вместо 1 должно быть написано, например 10:
var a = 11 - b + c * 10;
Давайте теперь рассмотрим случай из реального проекта, когда избыточность была добавлена сознательно, но неудачно:
detail.Discount = i * 1 / 4M;
Программист имел в виду, что переменная 'i' должна умножаться на значение одна четвёртая. Да, можно было сразу написать 0.25, но 1/4 лучше может передавать смысл алгоритма.
Однако, получилось не очень удачно. В начале выполняется умножение переменной 'i' на 1 и только затем происходит деление. Да, результат получится один и тот же. Но такой код может сбить с толку человека. И по этой причине он не нравится и анализатору, который выдаст здесь предупреждение.
Чтобы код стал более читаемым, а анализатор перестал выдавать предупреждение, лучше добавить скобки:
detail.Discount = i * (1 / 4M);
Еще один пример из реального проекта:
public virtual ValueBuffer GetIncludeValueBuffer(int queryIndex)
{
return queryIndex == 0
? _activeQueries[_activeIncludeQueryOffset + queryIndex].Current
: _activeIncludeQueries[queryIndex - 1].Current;
}
В данном случае '_activeIncludeQueryOffset' всегда будет складываться с нулем, так как выше находится проверка 'queryIndex == 0'. Скорее всего это не ошибка, но код можно упростить:
public virtual ValueBuffer GetIncludeValueBuffer(int queryIndex)
{
return queryIndex == 0
? _activeQueries[_activeIncludeQueryOffset].Current
: _activeIncludeQueries[queryIndex - 1].Current;
}
Примечание. Анализатор не ругается на случаи, когда рядом с подозрительным выражением находится подобное ему выражение. Пример:
A[i+0]=1;
A[i+1]=10;
A[i+2]=100;
A[i+3]=1000;
A[i+4]=10000;
Выражение 'i + 0' является избыточным. Но рядом располагаются подобные выражения вида 'i + литерал'. Поэтому можно сделать вывод, что сложение с 0 было сделано сознательно, для соблюдения общего стиля написания.
Данная диагностика классифицируется как:
Анализатор обнаружил возможную ошибку, связанную с тем, что в метод в качестве аргумента, не ожидающего 'null', может передаваться значение 'null'.
Это может привести, например, к возникновению исключения или неправильному исполнению вызываемого метода.
При написании кода бывает сложно отследить, были ли добавлены проверки на 'null' во всех необходимых местах. Это особенно актуально, когда переменная, которая может иметь значение 'null', передается в метод, внутри которого используется как аргумент другого метода, не ожидающего значение 'null'.
Рассмотрим небольшой синтетический пример:
void Method(string[] args)
{
var format = args.Length != 0 ? args[0] : null;
....
var message = string.Format(format, _value);
// do something
}
Если массив 'args' не содержит элементов, в переменную 'format' будет записано значение 'null'. Следовательно, в метод 'string.Format' в качестве первого аргумента будет передано значение 'null', что приведет к возникновению исключения. Исправить это можно следующим образом:
void Method(string[] args)
{
var format = args.Length != 0 ? args[0] : null;
....
if (format == null)
{
// process an error
return;
}
var message = string.Format(format, _value);
// do something
}
Немного усложним пример выше:
void Method(string[] args)
{
var format = args.Length != 0 ? args[0] : null;
....
WriteInfo(format);
}
void WriteInfo(string format)
{
Console.Write(format, _value);
}
Переменная 'format' все так же зависит от 'args.Length' и потенциально может быть 'null'. В данном случае считаем, что 'format == null'. Соответственно, в метод 'WriteInfo' также будет передано значение 'null'. Далее это же значение будет передано в качестве первого аргумента в метод 'Console.WriteLine', что приведёт к возникновению исключения типа 'ArgumentNullException'.
Исправим это аналогичным образом:
void Method(string[] args)
{
var format = args.Length != 0 ? args[0] : null;
....
WriteInfo(format);
}
void WriteInfo(string format)
{
if (format == null)
{
// process an error
return;
}
Console.Write(format, _value);
}
Теперь рассмотрим реальный пример:
private static string HandleSuffixValue(object val, StringSegment suffixSegment)
{
....
var res = string.Format(suffixSegment.Value, val).TrimEnd(']');
return res == "" ? null : res;
}
Первый аргумент метода 'string.Format' не должен быть равен 'null'. Давайте посмотрим, что возвращает 'suffixSegment.Value':
public string Value
{
get
{
if (HasValue)
{
return Buffer.Substring(Offset, Length);
}
else
{
return null;
}
}
}
Если 'HasValue' будет 'false', то 'Value' вернет 'null'. Получается, что потенциально вызов метода 'string.Format' в данном случае может выбросить исключение. Пример исправленного варианта:
private static string HandleSuffixValue(object val, StringSegment suffixSegment)
{
....
if (suffixSegment.Value == null)
{
return null;
}
var res = string.Format(suffixSegment.Value, val).TrimEnd(']');
return res == "" ? null : res;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3156. |
Анализатор обнаружил одну из двух операций с целыми числами - деление или взятие остатка от деления - в которой значение левого операнда по модулю всегда меньше значения по модулю правого.
Результаты таких операций:
Скорее всего, такое выражение содержит ошибку или является избыточным.
Рассмотрим синтетический пример:
public void Method()
{
int a = 10;
int b = 20;
var c = a / b;
....
}
В данном примере результатом выражения 'a / b' всегда будет 0, так как 'a < b'. В этом случае необходимо преобразовать тип переменной 'a' для выполнения вещественного деления:
public void Method()
{
int a = 10;
int b = 20;
var c = (double)a / b;
....
}
Теперь рассмотрим реальный пример:
public override Shipper CreateInstance(int i)
{
....
return new Shipper
{
....
DateCreated = new DateTime(i + 1 % 3000, // <=
(i % 11) + 1,
(i % 27) + 1,
0,
0,
0,
DateTimeKind.Utc),
....
};
}
Здесь допущена ошибка в приоритете операций. В выражении 'i + 1 % 3000' первым делом вычисляется выражение '1 % 3000', результатом которого всегда будет 1. Следовательно, 'i' всегда складывается с 1. Исправленный вариант может выглядеть следующим образом:
public override Shipper CreateInstance(int i)
{
....
return new Shipper
{
....
DateCreated = new DateTime((i + 1) % 3000, // <=
(i % 11) + 1,
(i % 27) + 1,
0,
0,
0,
DateTimeKind.Utc),
....
};
}
Рассмотрим еще один реальный пример:
private void ValidateMultiRecords(StorageEnvironment env,
IEnumerable<string> trees,
int documentCount,
int i)
{
for (var j = 0; j < 10; j++)
{
foreach (var treeName in trees)
{
var tree = tx.CreateTree(treeName);
using (var iterator = tree.MultiRead((j % 10).ToString())) // <=
{
....
}
}
}
}
В данном примере переменная 'j' изменяется в пределах [0..9]. Получается, что выражение 'j % 10' всегда будет равно 'j'. Упрощенный вариант может выглядеть следующим образом:
private void ValidateMultiRecords(StorageEnvironment env,
IEnumerable<string> trees,
int documentCount,
int i)
{
for (var j = 0; j < 10; j++)
{
foreach (var treeName in trees)
{
var tree = tx.CreateTree(treeName);
using (var iterator = tree.MultiRead(j.ToString())) // <=
{
....
}
}
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3157. |
Анализатор обнаружил одну из двух операций с целыми числами - деление или взятие остатка от деления - в которой абсолютное значение левого операнда всегда равно значению правого.
Результаты таких операций:
Скорее всего, такое выражение содержит ошибку или является избыточным.
Рассмотрим пример:
const int MinDimensionValue = 42;
const int MaxDimensionValue = 146;
static int GetSidesRatio(int width, int height)
{
if (width < MinDimensionValue || width > MinDimensionValue)
throw new ArgumentException(/*....*/);
if (height < MinDimensionValue || height > MinDimensionValue)
throw new ArgumentException(/*....*/);
return width / height;
}
Анализатор предупреждает, что результат выражения 'width / height' всегда будет равен 1. Действительно, программа дойдет до выполнения операции деления только в том случае, если значение у 'width' будет строго равно 'MinDimensionValue'. При любых других значениях будет выброшено исключение. С 'height' ситуация аналогична.
В данном фрагменте кода была допущена опечатка, и вместо проверки 'width > MinDimensionValue' должна быть 'width > MaxDimensionValue' (аналогично и с 'height'). Исправленный вариант:
const int MinDimensionValue = 42;
const int MaxDimensionValue = 146;
static int GetSidesRatio(int width, int height)
{
if (width < MinDimensionValue || width > MaxDimensionValue)
throw new ArgumentException(/*....*/);
if (height < MinDimensionValue || height > MaxDimensionValue)
throw new ArgumentException(/*....*/);
return width / height;
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что значение постфиксной или префиксной операции инкремента / декремента не используется. Скорее всего, или операция избыточна, или вместо постфиксной операции следует использовать префиксную.
Пример:
int CalculateSomething()
{
int value = GetSomething();
....
return value++;
}
В данном примере имеется локальная переменная 'value'. Из метода возвращается ее инкрементированное значение. Однако постфиксная операция создаст копию 'value', после этого увеличит значение 'value' и вернет копию. Получается, что оператор '++' никак не повлияет на значение, которое вернет функция 'CalculateSomething'. Возможный исправленный вариант:
int CalculateSomething()
{
int value = GetSomething();
....
return ++value;
}
Следующий вариант исправления кода ещё лучше подчёркивает, что следует вернуть значение на единицу больше:
int CalculateSomething()
{
int value = GetSomething();
....
return value + 1;
}
Мы рекомендуем использовать второй вариант, так как его проще понять.
Рассмотрим ещё один синтетический пример:
void Foo()
{
int value = GetSomething();
Do(value++);
Do(value++);
Do(value++);
}
Каждый раз функция 'Do' вызывается с аргументом на единицу больше. Последний инкремент не имеет смысла, так как увеличенное значение переменной далее не используется. Однако ошибки здесь нет, так как последний инкремент написан просто для красоты. Анализатор это поймет и не будет выдавать предупреждение. Предупреждение не выдаётся, если переменная последовательно инкрементируется более двух раз подряд.
Однако, мы все равно рекомендуем писать вот так:
void Foo()
{
int value = GetSomething();
Do(value++);
Do(value++);
Do(value);
}
Или так:
void Foo()
{
int value = GetSomething();
Do(value + 0);
Do(value + 1);
Do(value + 2);
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3159. |
Анализатор обнаружил, что тип объекта, вызывающего метод 'Enum.HasFlag', отличается от типа передаваемого аргумента. В результате такого вызова будет выброшено исключение типа 'ArgumentException'.
Рассмотрим синтетический пример:
bool DoSomethingIfAttachedToParent(TaskContinuationOptions options)
{
if (options.HasFlag(TaskCreationOptions.AttachedToParent))
{
// ....
return true;
}
else
{
return false;
}
}
Обратите внимание: в данном примере 'HasFlag' вызывается у объекта типа 'TaskContinuationOptions', а в качестве аргумента передаётся объект типа 'TaskCreationOptions'. Выполнение этого кода приведёт к выбрасыванию исключения типа 'ArgumentException'.
Заметить такую ошибку может быть непросто, так как названия перечислений схожи, а элемент 'AttachedToParent' присутствует в обоих типах.
Чтобы поправить данный фрагмент, необходимо поменять тип аргумента или вызывающего объекта:
bool DoSomethingIfAttachedToParent(TaskContinuationOptions options)
{
if (options.HasFlag(TaskContinuationOptions.AttachedToParent))
{
// ....
return true;
}
else
{
return false;
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительный вызов метода 'Object.ReferenceEquals': один или оба переданных аргумента могут иметь значимый тип. Так как метод принимает параметры типа 'Object', при передаче значимых типов будет произведена упаковка. Таким образом, в 'Object.ReferenceEquals' будет передана ссылка на объект, созданный в куче при упаковке. Так как полученная ссылка не будет равна никакой другой, из этого следует, что метод будет возвращать 'false' независимо от значения переданного аргумента.
Рассмотрим простой синтетический пример:
void SyntheticMethod(Point x, object a)
{
if (Object.ReferenceEquals(x, a))
....
}
В качестве аргументов в 'Object.ReferenceEquals' передаются 'x' и 'a'. Переменная 'x' имеет тип 'Point', следовательно, её значение будет упаковано при преобразовании в 'Object'. Таким образом, вызов 'Object.ReferenceEquals' всегда будет возвращать 'false' вне зависимости от того, какие аргументы переданы в 'SyntheticMethod'.
В качестве исправления можно заменить вызов метода на непосредственную проверку равенства значений:
void SyntheticMethod(Point x, object a)
{
if (a is Point pointA && pointA == x)
....
}
Правило также применяется и к аргументам, типы которых являются generic-параметрами. Если значения такого параметра не ограничены ссылочными типами, то анализатор будет выдавать предупреждение. Пример:
void SomeFunction<T>(T genericObject,
object someObject)
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Здесь 'genericObject' может быть экземпляром как ссылочного, так и значимого типа. Для значимых типов 'Object.ReferenceEquals' всегда будет возвращать 'false'. Такое поведение может оказаться неожиданным и нежелательным. Чтобы быть уверенным в том, что в метод не будут передаваться объекты значимых типов, нужно добавить соответствующее ограничение параметра:
void SomeFunction<T>(T genericObject,
object someObject) where T : class
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Теперь на месте параметра может быть только ссылочный тип, и анализатор не будет выдавать предупреждение.
Рассмотрим ситуацию, в которой параметр ограничен интерфейсом:
void SomeFunction<T>(T genericObject,
object someObject) where T : ICloneable
{
if (Object.ReferenceEquals(genericObject, someObject))
....
}
Анализатор выдаст предупреждение на данный код, так как объекты значимых типов могут реализовывать интерфейсы. Таким образом, ограничение 'where T : ICloneable' не защищает от вызова метода для структур, и результат такого вызова может оказаться неожиданным.
Отдельно рассмотрим одну особенность, связанную с nullable-типами. Они являются значимыми, следовательно, их преобразование в 'Object' приведёт к упаковке. Однако упаковка объектов таких типов производится особым образом. Если переменной присвоено значение, то при упаковке в кучу попадёт не объект nullable-типа, а само значение. Если же значение не задано, то при упаковке будет получена нулевая ссылка.
В следующем примере 'x' имеет тип 'Nullable<Point>'. Вполне возможно, что в результате упаковки 'x' будет возвращена нулевая ссылка. В таком случае вызов 'Object.ReferenceEquals' вернёт 'true', если аргумент 'a' также будет равен 'null'.
void SyntheticMethod(Point? x, object a)
{
if (Object.ReferenceEquals(x, a))
....
}
Тем не менее, на этот код будет выдано предупреждение. Использование такого способа для проверки двух элементов на равенство 'null' выглядит подозрительно. Уместнее будет проверять наличие значений непосредственным сравнением с 'null' или с помощью свойства 'HasValue':
void SyntheticMethod(Point? x, object a)
{
if (!x.HasValue && a == null)
....
}
Дополнительные материалы по теме особенностей nullable-типов:
Данная диагностика классифицируется как:
Анализатор обнаружил, что выражение 'return', всегда возвращает пустую коллекцию, которая была определена как локальная переменная. Чаще всего это происходит из-за того, что в коллекцию забыли добавить элементы.
Пример:
List<string> CreateDataList()
{
List<string> list = new List<string>();
string data = DoSomething();
return list;
}
В данном примере забыли добавить элемент 'data' в 'list'. Поэтому метод всегда будет возвращать пустую коллекцию. Исправленный вариант:
List<string> CreateDataList()
{
List<string> list = new List<string>();
string data = DoSomething();
list.Add(data);
return list;
}
Иногда программисты создают метод, который только и делает, что создает и возвращает коллекцию. Например, так:
List<List<CustomClass>> CreateEmptyDataList()
{
var list = new List<List<CustomClass>>();
return list;
}
Или так:
List<List<CustomClass>> CreateEmptyDataList()
{
return new List<List<CustomClass>>();
}
Это используется для некоторых паттернов программирования, или если тип коллекции имеет очень длинное название. Анализатор понимает такие ситуации и не выдаёт на них предупреждения.
Анализатор обнаружил пустой блок обработки исключения ('catch' или 'finally'). Отсутствие корректной обработки исключений может привести к снижению уровня надёжности приложения.
В некоторых случаях отсутствие корректной обработки исключительных ситуаций может стать причиной возникновения уязвимости. Недостаточное логирование и мониторинг выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A10:2017-Insufficient Logging & Monitoring.
Пример кода с пустым 'catch':
try
{
someCall();
}
catch
{
}
Конечно, такой код вовсе не обязательно ошибочен. Но очень странно просто подавлять исключение, ничего не делая, так как такая обработка исключений может скрывать дефекты в программе.
В качестве обработки исключения можно использовать, например, логгирование. Это по крайней мере не позволит исключительной ситуации остаться незамеченной:
try
{
someCall();
}
catch (Exception e)
{
Logger.Log(e);
}
Не менее подозрительным моментом является наличие в коде пустого блока 'finally'. Это может свидетельствовать о том, что какая-то логика, необходимая для надёжной работы приложения, не реализована. Например:
try
{
someCall();
}
catch
{ .... }
finally
{
}
Подобный код с большой вероятностью свидетельствует об ошибке или попросту избыточен. В отличие от пустого блока 'catch', который может быть использован для подавления исключения, у пустого блока 'finally' нет какого-либо практического применения.
Данная диагностика классифицируется как:
Анализатор обнаружил класс исключения, недоступного для других сборок. Если такое исключение будет выброшено, внешний код будет вынужден отлавливать объекты ближайшего доступного предка или вообще базового класса всех исключений – 'Exception'. В этом случае усложняется обработка конкретных исключительных ситуаций, ведь код других сборок не может чётко идентифицировать возникшую проблему.
Отсутствие чёткой идентификации возникшей проблемы несёт дополнительные риски с точки зрения безопасности, так как для каких-то определённых исключительных ситуаций может понадобиться специфичная обработка, а не общая. Недостаточное логирование и мониторинг (в том числе, обнаружение проблем) выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A10:2017-Insufficient Logging & Monitoring.
Простой пример из реального проекта:
internal sealed class ResourceException : Exception
{
internal ResourceException(string? name, Exception? inner = null)
: base(name, inner)
{
}
}
Чтобы можно было корректно обработать конкретную исключительную ситуацию, необходимо задать в объявлении класса модификатор доступности 'public':
public sealed class ResourceException : Exception
{
internal ResourceException(string? name, Exception? inner = null)
: base(name, inner)
{
}
}
Теперь другие сборки смогут отлавливать данное исключение и обрабатывать конкретную ситуацию.
Следует учитывать, что для вложенных классов модификатора 'public' у объявления исключения может быть недостаточно. Например:
namespace SomeNS
{
class ContainingClass
{
public class ContainedException : Exception {}
....
}
}
В данном примере класс исключения вложен в 'ContainingClass', который неявно объявлен как 'internal'. Вследствие этого 'ContainedException' также будет видим только в пределах текущей сборки, хотя он и помечен модификатором 'public'. Анализатор обнаруживает такие случаи и выдаёт соответствующие предупреждения.
Данная диагностика классифицируется как:
Анализатор обнаружил возможную ошибку при вызове конструктора или метода. В качестве одного из аргументов используется выражение типа 'char', которое неявно преобразуется к другому типу, в то время как обнаружена подходящая перегрузка, в которой соответствующий параметр представлен типом 'String'. Возможно, для вызова правильной перегрузки необходимо было использовать выражение типа 'String' вместо 'char'.
Рассмотрим пример:
public static string ToString(object[] a)
{
StringBuilder sb = new StringBuilder('['); // <=
if (a.Length > 0)
{
sb.Append(a[0]);
for (int index = 1; index < a.Length; ++index)
{
sb.Append(", ").Append(a[index]);
}
}
sb.Append(']');
return sb.ToString();
}
Разработчик хотел, чтобы строка, хранящаяся в экземпляре типа 'StringBuilder', начиналась с квадратной скобки. Однако из-за опечатки будет получен объект ёмкостью под 91 элемент, не содержащий символов.
Это произошло из-за того, что вместо двойных кавычек использовались одинарные, что привело к вызову не той перегрузки конструктора:
....
public StringBuilder(int capacity);
public StringBuilder(string? value);
....
При вызове конструктора символьный литерал '[' будет неявно приведен к соответствующему значению типа 'int' (91 в Unicode), из-за чего будет вызван конструктор с параметром типа 'int', задающим начальную вместимость, вместо конструктора, задающего начало строки.
Для исправления ошибки следует заменить символьный литерал на строковый, что позволит вызвать правильную перегрузку конструктора:
public static string ToString(object[] a)
{
StringBuilder sb = new StringBuilder("[");
....
}
Данное диагностическое правило учитывает не только литералы, но и выражения, так что на следующий код также будет выдано предупреждение:
public static string ToString(object[] a)
{
var initSmb = '[';
StringBuilder sb = new StringBuilder(initSmb);
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что вызов метода 'SingleOrDefault' без предиката может быть выполнен на коллекции, содержащей более одного элемента. Данное действие приведет к возникновению исключения типа 'System.InvalidOperationException'.
Такое поведение может быть неочевидным из-за поведения других 'OrDefault' методов. Например, методы 'FirstOrDefault', 'LastOrDefault', 'ElementAtOrDefault' возвращают 'default' значение типа элементов коллекции, если не удалось выполнить соответствующую операцию (если коллекция пуста, под предикат не попал ни один элемент и т. п.). При вызове метода 'SingleOrDefault' для пустой коллекции также будет возвращено значение 'default', но, если коллекция содержит более 1 элемента, будет выброшено исключение. Эта особенность может быть неочевидной.
Рассмотрим пример:
IEnumerable<State> GetStates()
{
var states = new List<State>();
if (usualCondition)
states.Add(GetCustomState());
if (veryRareCondition)
states.Add(GetAnotherState());
return states;
}
void AnalyzeState()
{
....
var state = GetStates().SingleOrDefault();
....
}
Не зная особенностей поведения 'SingleOrDefault', разработчик хотел записать в переменную 'state' значение, полученное из метода 'GetStates', если в коллекции содержится только один элемент, или 'default' значение в остальных случаях (элементы отсутствуют или их больше одного). Однако, если одновременно сработают обычное и очень редкое условие (переменные 'usualCondition' и 'veryRareCondition'), метод 'GetStates' вернёт коллекцию из двух элементов. В этом случае вместо записи в 'state' значения 'default' будет выброшено исключение.
Возможный вариант исправления метода 'AnalyzeState':
void AnalyzeState()
{
....
var states = GetStates();
var state = states.Count() == 1 ? states.First()
: default;
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что принимаемый методом параметр типа 'CancellationToken' не используется.
Объекты этого типа обычно применяются в случаях, когда может возникнуть необходимость прерывания операции, выполняемой в параллельном потоке. Метод, принимающий 'CancellationToken' в качестве параметра, может обращаться к нему во время выполнения, чтобы при необходимости завершить работу досрочно. Иногда токен также передаётся в качестве аргумента при вызове других методов, чтобы предоставить возможность досрочного завершения их работы.
Случаи, когда метод принимает 'CancellationToken', но не использует, выглядят подозрительно. Возможно, при разработке предполагалось использование этого параметра для обеспечения возможности отмены операции, но была допущена ошибка, из-за которой параметр оказался "лишним". Из-за этого может, к примеру, возникнуть ситуация, когда приложение не реагирует на запросы отмены от пользователя своевременно.
Рассмотрим пример:
public List<SomeStruct> LoadInfo(string[] keys, CancellationToken token)
{
List<SomeStruct> result = new List<SomeStruct>();
foreach (string key in keys)
{
SomeStruct item = LoadHugeData(key);
result.Add(item);
}
return result;
}
Данный метод производит последовательную загрузку данных большого размера. Реализация возможности прерывания такой операции имеет смысл, однако, несмотря на это, параметр 'token' не используется. Такой код выглядит подозрительно и помечается анализатором как место потенциальной ошибки. В качестве исправления можно предложить следующий вариант:
public List<SomeStruct> LoadInfo(string[] keys, CancellationToken token)
{
List<SomeStruct> result = new List<SomeStruct>();
foreach (string key in keys)
{
if(token.IsCancellationRequested)
break;
SomeStruct item = LoadHugeData(key);
result.Add(item);
}
return result;
}
Теперь последовательная загрузка данных может быть прервана. В случае получения запроса отмены операции метод 'LoadInfo' прекратит загрузку элементов и вернёт то, что было загружено до отмены операции.
Рассмотрим другой пример:
void ExecuteQuery(CancellationToken token = default)
{ .... }
public void ExecuteSomeActions(CancellationToken token)
{
....
ExecuteQuery();
....
}
Метод 'ExecuteQuery' в качестве аргумента может принимать значение типа 'CancellationToken'. Однако при вызове в методе 'ExecuteSomeActions' параметр 'token' не передаётся в метод 'ExecuteQuery'. Как следствие, такой код может несвоевременно реагировать на действия отмены.
Для исправления проблемы необходимо передавать 'token' в качестве аргумента метода 'ExecuteQuery':
ExecuteQuery(token);
Стоит отметить, что анализатор не будет выдавать предупреждения в случаях, когда метод помечен модификатором 'override' или является частью реализации интерфейса. Это исключение связано с тем, что при наследовании и переопределении сигнатура метода обязана содержать параметр типа 'CancellationToken' вне зависимости от того, предусматривает ли конкретная реализация его использование. Похожее исключение связано с virtual-методами: 'CancellationToken' в объявлении может быть предназначен не для стандартной реализации, а для наследников, которые будут данный метод переопределять.
Также предупреждение не будет выдаваться на лямбда-выражения, так как они достаточно часто используются в качестве обработчиков событий или передаются в качестве аргументов. В этих случаях функция будет вынуждена принимать определённый набор параметров даже в случаях, когда для её работы они не требуются.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил фрагмент, в котором оператор 'await' используется с выражением, значением которого может быть нулевая ссылка. При использовании 'await' с 'null' будет выброшено исключение типа 'NullReferenceException'.
Рассмотрим пример:
async void Broadcast(....)
{
await waiter.GetExplorerBehavior()?.SaveMatches();
....
}
ExplorerBehavior GetExplorerBehavior()
{
return _state == NodeState.HandShaked ? _Node.Behavior : null;
}
В методе 'Broadcast' оператор 'await' используется с выражением, которое в определённых случаях может иметь значение 'null'. Дело в том, что метод 'GetExplorerBehaviour' при некоторых обстоятельствах возвращает 'null', который впоследствии попадёт в 'Broadcast'.
В результате, при использовании оператора 'await' с выражением, имеющим значение 'null', будет выброшено исключение типа 'NullReferenceException'.
В качестве исправления можно добавить в метод 'Broadcast' дополнительную проверку на 'null':
async void Broadcast(....)
{
var task = waiter.GetExplorerBehavior()?.SaveMatches();
if (task != null)
await task;
....
}
Анализатор также предупреждает о случаях, когда потенциально нулевая ссылка передаётся в метод, конструктор или свойство, внутри которого она может быть использована с 'await'. Пример:
void ExecuteActionAsync(Action action)
{
Task task = null;
if (action != null)
task = new Task(action);
ExecuteTask(task); // <=
....
}
async void ExecuteTask(Task task)
{
....
await task;
}
В данном фрагменте 'await' используется с параметром 'task', в который при вызове передаётся потенциально нулевая ссылка. Ниже представлен исправленный вариант метода 'ExecuteActionAsync':
void ExecuteActionAsync(Action action)
{
Task task = null;
if (action != null)
{
task = new Task(action);
ExecuteTask(task);
}
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3168. |
Анализатор обнаружил, что в выражении 'return' возвращается локальная переменная, значением которой всегда будет 'null'. Подобное может происходить как из-за того, что переменной забыли присвоить нужное значение, так и из-за потенциальной логической ошибки.
Рассмотрим пример с логической ошибкой, вследствие которой метод 'GetRootNode' всегда возвращает 'null':
public Node GetRootNode(Node node)
{
Node parentNode = node.Parent == null ? node : node.Parent;
while (parentNode != null)
{
parentNode = parentNode.Parent;
}
return parentNode;
}
Условие в операторе 'while' будет выполняться до тех пор, пока переменная 'parentNode' не станет равной 'null'. Для того, чтобы исправить поведение метода, в условии оператора 'while' следует выполнять сравнение с 'null' не самой переменной 'parentNode', а её свойства 'Parent'. Исправленный вариант метода 'GetRootNode' будет выглядеть следующим образом:
public Node GetRootNode(Node node)
{
Node parentNode = node.Parent == null ? node : node.Parent;
while (parentNode.Parent != null)
{
parentNode = parentNode.Parent;
}
return parentNode;
}
Анализатор обнаружил, что оба операнда, использующиеся в операторе '??' или '??=', одинаковы. Скорее всего, эта операция содержит ошибку. Подобные ошибки могут появиться в результате опечатки или невнимательного copy-paste.
Рассмотрим пример подобной ошибки при использовании оператора '??':
string SomeMethod()
{
String expr1 = Foo();
String expr2 = Bar();
....
return expr1 ?? expr1;
}
Выражение 'expr1 ?? expr1' в методе 'SomeMethod' не имеет смысла, потому что данный метод будет возвращать одно и то же значение вне зависимости от того, содержит ли переменная 'expr1' значение 'null' или нет. Скорее всего, произошла опечатка, и правильный вариант выражения должен выглядеть так:
return expr1 ?? expr2;
Подобная ошибка может быть допущена и при использовании оператора '??=':
void SomeMethod()
{
String expr1 = Foo();
String expr2 = Bar();
....
expr1 ??= expr1;
....
DoSmt(expr1);
}
В данном случае была допущена ошибка, аналогичная описанной в предыдущем примере. Исправленный код:
expr1 ??= expr2;
Данная диагностика классифицируется как:
Анализатор обнаружил, что при создании массива в качестве значения для задания его длины может использоваться переменная или выражение, имеющие отрицательное значение.
Рассмотрим пример:
void ProcessBytes(byte[] bytes)
{
int length = BitConverter.ToUInt16(bytes, 0);
int[] newArr = new int[length - 2];
....
}
Значение, возвращаемое методом 'ToUInt16' и присваиваемое переменной 'length', может оказаться равным нулю. Это произойдёт в том случае, если первые два байта в массиве 'bytes' равны нулю. Тогда при создании массива 'newArr' в качестве его длины будет выступать отрицательное значение. Это приведёт к выбросу исключения типа 'OverflowException'.
Исправленный вариант метода 'ProcessBytes' с дополнительной проверкой может выглядеть следующим образом:
void ProcessBytes(byte[] bytes)
{
int length = BitConverter.ToUInt16(bytes, 0);
if (length < 2)
return;
int[] newArr = new int[length - 2];
....
}
Приведём ещё один пример кода. В нём при вызове метода 'SubArray' с определённым набором входных аргументов в качестве длины массива может быть использовано отрицательное значение:
public byte[] GetSubArray(byte[] bytes)
{
return bytes.SubArray(4, 2);
}
public static T[] SubArray<T>(this T[] arr, int index, int length)
{
if (length < 0)
throw new Exception($"Incorrect length value: {length}.");
if (index < 0)
throw new Exception($"Incorrect index value: {index}.");
if (arr.Length < index + length)
length = arr.Length - index;
var subArr = new T[length];
Array.Copy(arr, index, subArr, 0, length);
return subArr;
}
Проблема метода 'SubArray' заключается в том, что он не учитывает случаи, когда длина массива 'arr' может оказаться меньше значения, записанного в переменной 'index'. В этом случае переменная 'length' получит отрицательное значение. Предположим, что длина массива 'arr' равна 3, а значение переменной 'index' - 4. Переменная 'length' в ходе выполнения метода получит значение равное -1, и будет произведена попытка создания массива с отрицательным размером.
Исправленный вариант метода 'SubArray' может выглядеть следующим образом:
public static T[] SubArray<T>(this T[] arr, int index, int length)
{
if (length < 0)
throw new Exception($"Incorrect length value: {length}.");
if (index < 0 || arr.Length <= index)
throw new Exception($"Incorrect index value: {index}.");
if (arr.Length < index + length)
length = arr.Length - index;
var subArr = new T[length];
Array.Copy(arr, index, subArr, 0, length);
return subArr;
}
Данная диагностика классифицируется как:
Анализатор обнаружил возможную ошибку, связанную с тем, что блок кода ('{ .... }'), идущий после выражения 'if/if-else/for/while/foreach', к нему не относится.
Рассмотрим синтетический пример:
if (a == 1) DoSmt();
{
DoSmt2();
}
При беглом обзоре кода может показаться, что блок выполнится, если условие истинно, но на самом деле это не так. Блок будет выполняться всегда, независимо от условия. Это может ввести программиста в заблуждение.
Рассмотрим ещё несколько примеров кода, на которые анализатор выдаст срабатывания:
if (a == 2) Nop(); else Nop2();
{
Nop3();
}
if (a == 3) Nop();
else Nop2();
{
Nop3();
}
foreach (var item in query) DoSmt1();
{
DoSmt2();
}
Анализатор не выдаст срабатывание, если строки с оператором и несвязанным с ним блоком кода несмежные:
if (a == 7) DoSmt();
// this is a block for initializing MyClass fields
{
....
}
Если вы получили такое срабатывание, и оно ложное, вы можете подсказать об этом анализатору, добавив пустую строку между оператором и блоком.
Также диагностика не выдаст срабатывание в том случае, когда тело оператора содержит пустую конструкцию (';'), за это отвечает диагностическое правило V3007.
if (a == 3) ;
{
DoSmt();
}
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с тем, что пропущена инициализация переменной или свойства.
Рассмотрим это на примере:
class A
{
int field1;
string field2;
string field3;
....
public void foo(int value)
{
field1 = value;
field2 = // <=
field3 = GetInitialization(value);
}
}
В данном примере разработчик отложил инициализацию поля 'field2' в надежде на то, что этот код не скомпилируется, и тем самым напомнит о пропущенной инициализации. Но в данном случае код будет успешно скомпилирован. Полю 'field2' будет присвоено значение поля 'field3'.
Похожая ситуация может возникнуть во время декларации, например:
int b, c;
int a =
b = c = 2;
Этот код выглядит очень подозрительно. Непонятно чем хотели инициализировать переменную 'a'.
Анализатор не будет выдавать предупреждение в ряде случаев, когда понятно, что код написан подобным образом специально.
Например, есть отступы относительно первой строки:
var1 =
var2 = 100;
Другой пример, если нет отступа, но подобным образом значение записывается в несколько переменных:
var1 =
var2 =
var3 =
var4 = 100;
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит опечатку. В цепочке однотипных сравнений членов класса имеется выражение, не похожее на остальные. В нем сравниваются члены с разными именами, в то время как остальные выражения в цепочке сравнивают одноименные члены.
Рассмотрим пример:
public void Foo(TestClass a, TestClass b)
{
if (a.x == b.x && a.y == b.y && a.z == b.y)
{
....
}
}
В данном случае выражение 'a.z == b.y' отличается от остальных звеньев в цепочке. Скорее всего, оно является ошибочным из-за опечатки при редактировании скопированного участка текста. Корректный код, который не вызовет подозрений у анализатора, будет выглядеть так:
public void Foo(TestClass a, TestClass b)
{
if (a.x == b.x && a.y == b.y && a.z == b.z)
{
....
}
}
Анализатор выдает предупреждение в тех случаях, когда длина цепочки сравнений более двух звеньев.
Данная диагностика классифицируется как:
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит ошибку. В критической секции, образованной вызовами методов класса 'Monitor', используется оператор 'await'. Это может привести к выбрасыванию исключения типа 'SynchronizationLockException'.
Рассмотрим пример:
static object _locker = new object();
public async void Foo()
{
Monitor.Enter(_locker);
await Task.Delay(TimeSpan.FromSeconds(5));
Monitor.Exit(_locker);
}
Метод 'Monitor.Enter' получает блокировку объекта, переданного ему в качестве параметра. Блокировка позволяет ограничить доступ к коду, находящемуся после вызова этого метода. Ограничение распространяется на все потоки, кроме того, в котором была получена блокировка. Чтобы снять блокировку, необходимо вызвать метод 'Monitor.Exit', который освободит заблокированный объект и позволит получить доступ следующему потоку. Ограниченный подобным образом фрагмент кода называют критической секцией.
В данном примере после вызова 'Monitor.Enter' используется оператор 'await'. Скорее всего, выполнение кода после асинхронной операции будет производиться в потоке, отличном от того, в котором был вызван 'Monitor.Enter'. Таким образом, открытие и закрытие критической секции может производиться разными потоками. В таком случае будет выброшено исключение типа 'SynchronizationLockException'.
Корректный код, который не вызовет подозрений у анализатора, может выглядеть так:
static SemaphoreSlim _semaphore = new SemaphoreSlim(1);
private static async void Foo()
{
_semaphore.Wait();
await Task.Delay(TimeSpan.FromSeconds(1));
_semaphore.Release();
}
Механизм блокировки реализован при помощи внутреннего счётчика экземпляра класса 'SemaphoreSlim'. Вызов 'Wait' приводит к уменьшению его значения на 1. Если счётчик равен 0, следующие вызовы 'Wait' будут блокировать вызывающий поток до тех пор, пока значение счётчика не станет положительным. Увеличение значения счётчика происходит при вызове 'Release' — при этом не важно, из какого потока он был вызван.
Если при создании объекта типа 'SemaphoreSlim' передать в конструктор 1, то между вызовами 'Wait' и 'Release' будет образован аналог критической секции. Использование оператора 'await' внутри образованной области кода не приведёт к выбрасыванию исключения типа 'SynchronizationLockException'.
Данная диагностика классифицируется как:
Анализатор обнаружил фрагмент кода, в котором правый операнд оператора '&=' или '|=' всегда имеет одно и то же значение.
Рассмотрим пример:
void Foo(bool status)
{
....
bool currentStatus = ....;
....
if(status)
currentStatus &= status;
....
}
В блоке 'then' конструкции 'if' выполняется операция составного присваивания. Переменная 'currentStatus' примет значение, равное результату логической операции 'currentStatus & status'. Исходя из условия, 'status' всегда будет 'true', что делает бессмысленным использование оператора '&=', ведь после выполнения присваивания значение 'currentStatus' не изменится.
Если же правый операнд всегда равен 'false', использование оператора '&=' также является бессмысленным. В таких случаях он может быть заменён на обычное присваивание.
Использование оператора '|=' также не имеет смысла, если его правый операнд всегда имеет одно и то же значение:
Все описанные выше случаи могут свидетельствовать как об избыточном использовании операторов '&=' и '|=', так и о наличии ошибки в логике работы приложения.
Например, корректный вариант вышеописанного метода может выглядеть следующим образом:
void Foo(bool status)
{
....
bool currentStatus = ....;
....
if(status)
currentStatus = status;
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил фрагмент кода, который, возможно, содержит логическую ошибку. В условном выражении логический литерал находится между '??' и другим оператором с более высоким приоритетом.
Оператор '??' имеет приоритет ниже, чем операторы '||', '&&', '|', '^', '&', '!=', '=='. В случае отсутствия скобок, определяющих порядок проверки условия, может возникнуть ошибка, подобная приведенной ниже:
class Item
{
....
public bool flag;
....
}
void CheckItem(Item? item)
{
if (item?.flag ?? true || GetNextCheck(item))
{
....
}
return;
}
Так как приоритет оператора '??' ниже чем '||', в первую очередь будет выполнена проверка выражения 'true || GetNextCheck()', которая всегда возвращает 'true', при этом метод 'GetNextCheck' никак не влияет на полученный результат.
В данном случае проблему можно решить, взяв часть выражения в скобки:
class Item
{
....
public bool flag;
....
}
void CheckItem(Item? item)
{
if ((item?.flag ?? true) || GetNextCheck(item))
{
....
}
return;
}
В исправленном варианте условия сначала выполнится выражение 'item?.flag ?? true', и только потом отработает оператор '||'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3177. |
Анализатор обнаружил вызов метода или обращение к свойству объекта, у которого ранее был вызван метод 'Dispose' или его аналог. Подобные действия могут привести к выбрасыванию исключения.
Рассмотрим пример:
public void AppendFileInformation(string path)
{
FileStream stream = new FileStream(path,
FileMode.Open,
FileAccess.Read);
....
stream.Close();
....
if (stream.Length == stream.Position)
{
Console.WriteLine("End of file has been reached.");
....
}
....
}
В условии проверяется, что файл был прочитан полностью. Для проверки сравнивают текущую позицию потока и его длину. Проблема в том, что при обращении к свойству 'Length' будет выброшено исключение типа 'ObjectDisposedException'. Это связано с тем, что перед условием у переменной 'stream' вызывается метод 'Close'. Для класса 'FileStream' этот метод является аналогом 'Dispose'. Следовательно, ресурсы 'stream' будут освобождены.
Рассмотрим корректную реализацию 'AppendFileInformation':
public void AppendFileInformation(string path)
{
using (FileStream stream = new FileStream(path,
FileMode.Open,
FileAccess.Read))
{
....
if (stream.Length == stream.Position)
{
Console.WriteLine("End of file has been reached.");
}
....
}
....
}
Для корректной работы метода стоит использовать выражение 'using'. В этом случае:
Ещё один вариант ошибки выглядит следующим образом:
public void ProcessFileStream(FileStream stream)
{
....
bool flag = CheckAndCloseStream(stream);
AppendFileInformation(stream);
....
}
public bool CheckAndCloseStream(FileStream potentiallyInvalidStream)
{
....
potentiallyInvalidStream.Close();
....
}
public void AppendFileInformation(FileStream streamForInformation)
{
....
if (streamForInformation.Length == streamForInformation.Position)
{
Console.WriteLine("End of file has been reached.");
}
....
}
После вызова 'CheckAndCloseStream' в методе 'ProcessFileStream' ресурсы объекта, на который ссылается переменная 'stream', будут освобождены. Далее эта переменная передаётся в метод 'AppendFileInformation'. Внутри него производится обращение к свойству 'Length', из-за чего будет выброшено исключение типа 'ObjectDisposedException'.
Корректная реализация 'ProcessFileStream' будет выглядеть следующим образом:
public void ProcessFileStream(FileStream stream)
{
....
AppendFileInformation(stream);
bool flag = CheckAndCloseStream(stream);
....
}
Был изменён порядок вызовов методов 'CheckAndCloseStream' и 'AppendFileInformation'. Вследствие этого ресурсы будут освобождаться уже после выполнения действий над потоком. Следовательно, исключение выброшено не будет.
Данная диагностика классифицируется как:
Анализатор обнаружил, что у потенциально пустой коллекции вызван метод, выбрасывающий исключение, если в коллекции нет элементов.
Чтобы лучше разобраться в проблеме, рассмотрим пример:
public static bool ComparisonWithFirst(List<string> list,
string strForComparison)
{
string itemForComparison = null;
if (list != null && !list.Any())
{
itemForComparison = list.First();
}
....
}
При попытке получить первый элемент из коллекции будет выброшено исключение типа 'InvalidOperationException'. Внутри then-ветви коллекция будет пустой, так как выполняется проверка, что в 'list' нет элементов.
Исправленный вариант:
public static bool ComparisonWithFirst(List<string> list,
string strForComparison)
{
string itemForComparison = null;
if (list != null && list.Any())
{
itemForComparison = list.First();
}
....
}
Подобная ошибка может возникать и при передаче пустой коллекции в метод, который не ожидает этого.
public static void ProcessList(List<string> list)
{
if (list.Any())
return;
CompareFirstWithAll(list);
}
public static void CompareFirstWithAll(List<string> list)
{
string itemForComparison = list.First();
....
}
Из-за опечатки в методе 'ProcessList' коллекция 'list' передаётся в метод 'CompareFirstWithAll' пустой. Метод 'CompareFirstWithAll' не ожидает передачи в него пустой коллекции.
Исправленный вариант:
public static void ProcessList(List<string> list)
{
if (!list.Any())
return;
CompareFirstWithAll(list);
}
Данная диагностика классифицируется как:
Анализатор обнаружил вызов метода 'HasFlag', который всегда возвращает 'true' из-за аргумента, равного '0'.
Рассмотрим пример:
public enum RuntimeEvent
{
Initialize = 1,
BeginRequest = 2,
BeginSessionAccess = 4,
ExecuteResource = 8,
EndSessionAccess = 16,
EndRequest = 32
}
public void FlagsTest()
{
....
RuntimeEvent support = GetSupportEvent();
....
Assert.True(support.HasFlag( RuntimeEvent.EndRequest
& RuntimeEvent.BeginRequest),
"End|Begin in End|SessionEnd");
....
}
'support' – экземпляр перечисления типа 'RuntimeEvent'. Значение переменной – результат вызова метода 'GetSupportEvent'. После инициализации у 'support' проверяют наличие флага со значением побитового 'И' для 'EndRequest' и 'BeginRequest'.
Подобная проверка не имеет смысла, так как выражение '32 & 2' равно нулю. Если аргумент 'HasFlag' – ноль, то результатом вызова метода всегда будет 'true'. Получается, что тест проходит, независимо от значения 'support'. Такой код выглядит подозрительно.
Корректная реализация проверки может выглядеть следующим образом:
public void FlagsTest()
{
....
RuntimeEvent support = GetSupportEvent();
....
Assert.True(support.HasFlag( RuntimeEvent.EndRequest
| RuntimeEvent.BeginRequest),
"End|Begin in End|SessionEnd");
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3180. |
Анализатор обнаружил, что выполняется побитовая операция 'AND' (&) с операндом, равным 0. Возможно, использован неподходящий оператор или операнд.
Рассмотрим пример:
public enum TypeAttr
{
NotPublic = 0x0,
Public = 0x1,
NestedPublic = 0x2,
NestedPrivate = 0x3
}
public static bool IsNotPublic(TypeAttr type)
{
return (type & TypeAttr.NotPublic) == TypeAttr.NotPublic;
}
Метод 'IsNotPublic' проверяет наличие флага 'NotPublic' у аргумента типа 'TypeAttr'.
Подобный метод проверки не имеет смысла так как флаг 'TypeAttr.NotPublic' имеет нулевое значение, а значит использование его как операнда оператора '&' приводит к всегда нулевому значению результата. Таким образом в представленной реализации мы всегда получаем истинное условие.
Корректная реализация проверки может выглядеть следующим образом:
public static bool IsNotPublic(TypeAttr type)
{
return type == TypeAttr.NotPublic;
}
Также анализатор выдаст срабатывание на использование нулевого операнда с оператором '&='. Такой код также выглядит подозрительно, так как равенство нулю одного из операндов, означает, что и результат выражения будет равен нулю.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3181. |
Анализатор обнаружил использование побитового 'AND' (&) с операндами, при которых результат операции всегда равен 0. Возможно, использован неподходящий оператор или операнд.
Рассмотрим пример:
public enum FlagType : ulong
{
Package = 1 << 1,
Import = 1 << 2,
Namespace = 1 << 3,
....
}
....
FlagType bitMask = FlagType.Package & FlagType.Import;
Здесь 'bitMask' – объект типа перечисления 'FlagType', в котором создаётся битовая маска.
Подобный метод объединения флагов перечисления является некорректным. Побитовое 'AND' (&) между значениями 'FlagType.Package' и 'FlagType.Import' равно нулю, так как эти битовые флаги не содержат единиц в соответствующих разрядах.
Корректная реализация объединения флагов может выглядеть следующим образом:
FlagType bitMask = FlagType.Package | FlagType.Import;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3182. |
Анализатор обнаружил инструкцию, относящуюся к оператору 'if'. При этом форматирование не соответствует логике исполнения: код может содержать ошибку.
Рассмотрим пример:
string GetArgumentPositionStr(Argument argument)
{
if (argument.first)
return "first";
if (argument.second)
if (argument.third)
return "third";
return String.Empty;
}
В данном примере пропущена 'then' ветвь условной конструкции 'if (argument.second)'. Из-за этого ошибочный фрагмент кода будет работать так же, как и приведённый ниже:
if (argument.second)
{
if (argument.third)
return "third";
}
Исправленный вариант:
string GetArgumentPositionStr(Argument argument)
{
if (argument.first)
return "first";
if (argument.second)
return "second";
if (argument.third)
return "third";
return String.Empty;
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что в метод передаётся индекс, который больше или равен количеству элементов коллекции. Это может привести к выбрасыванию исключения.
Чтобы лучше разобраться в проблеме, рассмотрим пример:
public static void Foo()
{
List<string> list = new List<string>(20) { "0", "1", "2" };
list.RemoveAt(list.Count);
}
При попытке удалить элемент будет выброшено исключение. Метод 'RemoveAt' выбрасывает исключение, если первый аргумент больше или равен количеству элементов коллекции.
Исправленный вариант:
public static void Foo()
{
List<string> list = new List<string>(20) { "0", "1", "2" };
list.RemoveAt(list.Count - 1);
}
Рассмотрим более сложный пример с ошибкой:
public static void ProcessList()
{
List<string> list = new List<string>(20) { "0", "1", "2" };
string str = GetStringOrNull(list, list.Count); // <=
}
public static string GetStringOrNull(List<string> list, int index)
{
if (index > list.Count)
return null;
return list.ElementAt(index);
}
Коллекция 'list' передаётся в метод 'GetStringOrNull', который содержит ошибку в условии оператора 'if'. При подобных аргументах метод должен вернуть 'null', но из-за ошибки выбрасывает исключение.
Исправленный вариант:
public static void ProcessList()
{
List<string> list = new List<string>(20) { "0", "1", "2" };
string str = GetStringOrNull(list, list.Count);
}
public static string GetStringOrNull(List<string> list, int index)
{
if (index >= list.Count)
return null;
return list.ElementAt(index);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Анализатор обнаружил странный аргумент, переданный в метод в качестве пути к файлу. Возможно, он был перепутан местами с другим аргументом этого метода.
Рассмотрим пример:
void AppendText(FileInfo file, string fileContent)
{
var filePath = file.FullName;
File.AppendAllText(fileContent, filePath);
}
Приведённый выше метод 'AppendText' предназначен для добавления строки 'fileContent' в файл. В переменную 'filePath' записывается путь к файлу из 'file.FullName'. После этого 'filePath' и 'fileContent' используются в качестве аргументов метода 'File.AppendAllText', выполняющего добавление текста в файл. В качестве первого аргумента этот метод принимает путь к файлу, а в качестве второго – строку для записи. Однако в приведённом выше примере эти два аргумента перепутаны местами. Результат использования такого метода зависит от содержимого 'fileContent':
Для решения этой проблемы нужно переставить аргументы метода 'File.AppendAllText' в правильном порядке:
void AppendText(FileInfo file, string fileContent)
{
var filePath = file.FullName;
File.AppendAllText(filePath, fileContent);
}
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительный вызов метода, выполняющего операции над фрагментом коллекции. Переданные в метод аргументы некорректны: их использование предполагает взаимодействие с элементами за границей коллекции. Это приводит к выбросу исключения.
Рассмотрим пример:
int[] arr = new int[] { 0, 1, 3, 4 };
var indexOfOdd = Array.FindIndex(arr,
startIndex: 2,
count: 3,
x => x % 2 == 1);
Метод 'FindIndex' принимает следующие аргументы:
Метод возвращает либо индекс первого элемента, соответствующего предикату, либо '-1'.
Массив 'arr' состоит из четырех элементов, значит индекс последнего элемента — '3'. Здесь же разработчик пытается получить доступ к элементу с индексом '4'. В таком случае будет выброшено соответствующее исключение.
Корректная реализация поиска индекса элемента может выглядеть следующим образом:
int[] arr = new int[] { 0, 1, 3, 4 };
var indexOfOdd = Array.FindIndex(arr,
startIndex: 2,
count: 2,
x => x % 2 == 1);
Или же вы можете использовать перегрузку метода, не указывающую параметр 'count'. В таком случае обход всегда будет заканчиваться на последнем элементе коллекции:
int[] arr = new int[] { 0, 1, 3, 4 };
var indexOfOdd = Array.FindIndex(arr,
startIndex: 2,
x => x % 2 == 1);
Также аргументы могут формировать секцию в обратную сторону. Например:
int[] arr = new int[] { 0, 1, 3, 4 };
var lastEvenInd = Array.FindLastIndex(arr,
startIndex: 1,
count: 3,
x => x % 2 == 0);
Здесь аргументы выполняют те же самые роли, однако элементы будут просматриваться в обратном порядке. Область поиска здесь формируется из элемента с индексом '1' и двух элементов, стоящих перед ним. Таким образом, методу нужно будет обратиться по индексу '-1'. Так как такое поведение некорректно, будет выброшено исключение.
В этом случае корректная реализация поиска индекса может выглядеть так:
int[] arr = new int[] { 0, 1, 3, 4 };
var lastEvenInd = Array.FindLastIndex(arr,
startIndex: 1,
count: 2,
x => x % 2 == 0);
Рассмотрим более сложный пример:
var list = new List<int> { 2, 3, 5, 7 };
var index = GetFirstEvenIndex(list, 1, list.Count);
....
public int GetFirstEvenIndex(List<int> lst, int startIndex, int count)
{
return lst.FindIndex(startIndex, count, x => x % 2 == 0);
}
Параметры метода 'GetFirstEvenIndex':
Здесь формируется диапазон поиска от элемента с индексом '1' до элемента с индексом 'list.Count' включительно. Таким образом, методу нужно будет обратиться по индексу, лежащему за пределами коллекции. Так как такое поведение некорректно, будет выброшено исключение.
Корректный код передачи аргументов методу может выглядеть следующим образом:
var list = new List<int> { 2, 3, 5, 7 };
var startIndex = 1;
var index = GetFirstEvenIndex(list, startIndex, list.Count - startIndex);
Данная диагностика классифицируется как:
Анализатор обнаружил, что в SQL-запросе, вероятно, пропущен пробел или какой-либо другой символ-разделитель между словами. Эта опечатка могла быть допущена в результате конкатенации, интерполяции или вызова метода 'String.Format' для получения строки SQL-запроса.
Рассмотрим пример:
public Customer GetCustomerData(ulong id)
{
string query = "SELECT c.Email, c.Phone, " +
"c.firstName, c.lastName FROM customers c" + // <=
$"WHERE customers.id = {id}";
var sqlCommand = new SqlCommand(query);
....
}
Тут создается SQL-запрос на получение данных клиента по его ID. Обратите внимание, что этот запрос был получен путем конкатенации двух строк, в месте соединения которых отсутствует пробел. Из-за этой ошибки SQL-запрос будет содержать опечатку — "сWHERE" — что сделает его некорректным.
Чтобы исправить проблему, нужно подставить недостающий пробел:
public Customer GetCustomerData(ulong id)
{
string query = "SELECT c.Email, c.Phone, " +
"c.firstName, c.lastName FROM customers c " +
$"WHERE customers.id = {id}";
var sqlCommand = new SqlCommand(query);
....
}
Анализатор обнаружил разыменование потенциально уничтоженного или имеющего значение 'null' объекта. Это может стать причиной выбрасывания исключения.
Рассмотрим пример:
void ProcessTransform(UnityEngine.Transform transform)
{
if (transform == null)
{
var position = transform.position;
....
}
}
В данном примере обращение к свойству 'position' производится, когда 'transform' либо является уничтоженным объектом, либо равен 'null'. Оба варианта приводят к выбрасыванию исключения.
В данном случае проблему можно исправить, изменив оператор сравнения:
void ProcessTransform(UnityEngine.Transform transform)
{
if (transform != null)
{
var position = transform.position;
....
}
}
Анализатор обнаружил, что члену 'readonly' поля присваивается значение, и при этом поле может иметь значимый тип. Если поле будет иметь значимый тип, то изменения члена поля не произойдёт.
Подобная ошибка возникает из-за того, что типы значений непосредственно содержат свои данные. Если тип поля явно определён как значимый, то подобную ошибку найдёт компилятор. Однако если типом поля является универсальный параметр, то код успешно скомпилируется. Из-за этого возможна ситуация, когда в член 'readonly' поля происходит запись, но его значение не изменяется.
Рассмотрим пример:
private interface ITable
{
int Rows { get; set; }
}
private class Table<T> where T : ITable
{
private readonly T _baseTable;
public void SetRows(int x)
{
_baseTable.Rows = x; // <=
}
}
Класс имеет поле '_baseTable', типом которого является универсальный параметр. В методе 'SetRows' свойству 'Rows' данного поля присваивается значение аргумента.
Ниже представлен пример использования этого класса:
private struct RelationTable : ITable
{
public int Rows { get; set; }
}
....
static void DoSomething()
{
Table<RelationTable> table = new Table<RelationTable>();
table.SetRows(10);
}
В данном случае вызов 'SetRows' никак не повлияет на значение свойства 'Rows'. Для того чтобы обезопасить код от подобных ошибок, нужно добавить ограничения типа:
private interface ITable
{
int Rows { get; set; }
}
private class Table<T> where T : class, ITable
{
private readonly T _baseTable;
public void SetRows(int x)
{
_baseTable.Rows = x;
}
}
Анализатор обнаружил возможную ошибку в коде: несколько потоков без синхронизации изменяют общий ресурс.
Рассмотрим пример:
ConcurrentBag<String> GetNamesById(List<String> ids)
{
String query;
ConcurrentBag<String> result = new();
Parallel.ForEach(ids, id =>
{
query = $@"SELECT Name FROM data WHERE id = {id}";
result.Add(ProcessQuery(query));
});
return result;
}
Метод 'GetNamesById' возвращает имена в соответствии с переданным списком идентификаторов. Для этого методом 'Parallel.ForEach' обрабатываются все элементы коллекции 'ids': для каждого из них составляется и исполняется SQL-запрос.
Проблема в том, что захваченная локальная переменная 'query' является общим разделяемым ресурсом потоков, исполняющихся в 'Parallel.ForEach'. Разные потоки будут производить несинхронизированный доступ к одному объекту. Это может привести к некорректному поведению программы.
Ниже приведено описание возможной проблемной ситуации:
Корректная реализация метода может выглядеть следующим образом:
ConcurrentBag<String> GetNamesById(List<String> ids)
{
ConcurrentBag<String> result = new();
Parallel.ForEach(ids, id =>
{
String query = $@"SELECT Name FROM data WHERE id = {id}";
result.Add(ProcessQuery(query));
});
return result;
}
Здесь каждый поток работает с собственной переменной 'query'. В таком случае проблем не будет, так как нет разделяемого между потоками ресурса.
Рассмотрим ещё один пример:
int CountFails(List<int> ids)
{
int count = 0;
Parallel.ForEach(ids, id =>
{
try
{
DoSomeWork(id);
}
catch (Exception ex)
{
count++;
}
});
return count;
}
Метод 'CountFails' считает количество исключений при выполнении операций над элементами коллекции 'ids'. Этот код также содержит проблему несинхронизированного доступа к общему ресурсу. Операции инкремента и декремента не являются атомарными, поэтому корректный подсчёт количества исключений в этом случае не гарантирован.
Корректная реализация метода может выглядеть следующим образом:
int CountFails(List<int> ids)
{
int count = 0;
Parallel.ForEach(ids, id =>
{
try
{
DoSomeWork(id);
}
catch (Exception ex)
{
Interlocked.Increment(ref count);
}
});
return count;
}
Здесь для корректного подсчета используется метод 'Interlocked.Increment', предоставляющий атомарную операцию инкремента переменной.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил попытку обхода пустой коллекции. Такая операция не имеет смысла: скорее всего, код содержит ошибку.
Рассмотрим пример:
private List<Action> _actions;
....
public void Execute()
{
var remove = new List<Action>();
foreach (var action in _actions)
{
try
{
action.Invoke();
}
catch (Exception ex)
{
Logger.LogError(string.Format("Error invoking action:\n{0}", ex));
}
}
foreach (var action in remove)
_actions.Remove(action);
}
Метод 'Execute' поочередно вызывает делегаты из списка '_actions', а также ловит и логирует возникающие в процессе выполнения ошибки. Помимо основного цикла, в конце метода есть еще один, который должен удалять из списка '_actions' делегаты, хранящиеся в коллекции 'remove'.
Проблема здесь в том, что коллекция 'remove' всегда будет пустой. Она создается в самом начале метода, но не заполняется в процессе. Таким образом, последний цикл никогда не будет выполнен.
Корректная реализация метода может выглядеть следующим образом:
public void Execute()
{
var remove = new List<Action>();
foreach (var action in _actions)
{
try
{
action.Invoke();
}
catch (Exception ex)
{
Logger.LogError(string.Format("Error invoking action:\n{0}", ex));
remove.Add(action);
}
}
foreach (var action in remove)
_actions.Remove(action);
}
Теперь добавляем в коллекцию 'remove' делегаты, при исполнении которых возникли исключения, чтобы впоследствии удалить их.
Анализатор также может выдать предупреждение на вызов метода, выполняющего обход коллекции.
Рассмотрим пример:
int ProcessValues(int[][] valuesCollection,
out List<int> extremums)
{
extremums = new List<int>();
foreach (var values in valuesCollection)
{
SetStateAccordingToValues(values);
}
return extremums.Sum();
}
Метод 'ProcessValues' принимает массивы чисел для обработки. В данном случае нас интересует коллекция 'extremums': она создается пустой и не заполняется в процессе выполнения метода. При этом 'ProcessValues' возвращает результат вызова метода 'Sum' на коллекции 'extremums'. Код выглядит ошибочным, так как вызов 'Sum' всегда возвращает 0.
Корректная реализация метода может выглядеть следующим образом:
int ProcessValues(int[][] valuesCollection,
out List<int> extremums)
{
extremums = new List<int>();
foreach (var values in valuesCollection)
{
SetStateAccordingToValues(values);
extremums.Add(values.Max());
}
return extremums.Sum();
}
Анализатор обнаружил возможную ошибку, связанную с тем, что один из членов класса не используется в методе 'Equals' и при этом используется в методе 'GetHashCode'.
Рассмотрим пример:
public class UpnpNatDevice
{
private EndPoint hostEndPoint;
private string serviceDescriptionUrl;
private string controlUrl;
public override bool Equals(object obj)
{
if (obj is UpnpNatDevice other)
{
return hostEndPoint.Equals(other.hostEndPoint)
&& serviceDescriptionUrl == other.serviceDescriptionUrl;
}
return false;
}
public override int GetHashCode()
{
return hostEndPoint.GetHashCode()
^ controlUrl.GetHashCode()
^ serviceDescriptionUrl.GetHashCode();
}
}
В данном примере поле 'controlUrl' не используется в методе 'Equals', но при этом присутствует в 'GetHashCode'. Это возможно в двух ситуациях:
Обе ситуации приводят к одной проблеме – метод 'GetHashCode' может возвращать разные значения для двух эквивалентных объектов. Исходя из документации Microsoft, метод 'GetHashCode' должен возвращать одинаковый хэш код для любых двух объектов, для которых вызов 'Equals' возвращает 'True'.
В данном случае для двух объектов с одинаковыми полями 'hostEndPoint' и 'serviceDescriptionUrl' метод 'Equals' вернёт 'True'. При этом результат 'GetHashCode' зависит ещё и от 'controlUrl'. Это может свидетельствовать об ошибке. Также подобная реализация может негативно сказаться на корректности работы с коллекциями 'Hashtable', 'Dictionary<TKey,TValue>' и другими.
Исправленный вариант:
public override bool Equals(object obj)
{
if (obj is UpnpNatDevice other)
{
return hostEndPoint.Equals(other.hostEndPoint)
&& serviceDescriptionUrl == other.serviceDescriptionUrl
&& controlUrl == other.controlUrl;
}
return false;
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что результаты обработки вывода процесса могут быть использованы до завершения всех операций их формирования. В этом случае приложение будет использовать выходные данные процесса в некорректном или неполном виде.
Рассмотрим пример:
public void Run()
{
var process = new Process();
process.StartInfo.FileName = GetProcessFile();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
StringBuilder data = new StringBuilder();
process.OutputDataReceived +=
(sender, args) => data.AppendLine(args.Data); // <=
process.Start();
process.BeginOutputReadLine();
WriteData(data.ToString()); // <=
}
Код в данном примере запускает процесс, сохраняет его вывод в переменную 'data', после чего собранные результаты передаются в метод 'WriteData'. Здесь возможна ситуация, при которой вызов 'data.ToString' будет выполнен до полной обработки всего вывода процесса. К примеру, если процесс выводит несколько строк, то к моменту вызова 'ToString' не все из них могут быть добавлены в переменную 'data'.
Для решения проблемы необходимо убедиться, что обработка всего вывода процесса была завершена. Для этого можно вызвать метод 'WaitForExit' без аргументов:
public void Run()
{
var process = new Process();
process.StartInfo.FileName = GetProcessFile();
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
StringBuilder data = new StringBuilder();
process.OutputDataReceived +=
(sender, args) => data.AppendLine(args.Data);
process.Start();
process.BeginOutputReadLine();
process.WaitForExit();
WriteData(data.ToString());
}
Такой вызов 'WaitForExit' возвращает управление только после того, как обработка вывода будет завершена. Обратите внимание, что вызов перегрузки 'WaitForExit(Int32)' не обладает данной особенностью. Поэтому следующий код может отрабатывать некорректно:
public void Run()
{
var process = new Process();
....
StringBuilder data = new StringBuilder();
process.OutputDataReceived +=
(sender, args) => data.AppendLine(args.Data); // <=
process.Start();
process.BeginOutputReadLine();
if (process.WaitForExit(3000))
{
WriteData(data.ToString()); // <=
}
else
{
.... // throw timeout error
}
}
В данном примере получение значения из переменной 'data' производится после того, как процесс завершил работу. Тем не менее, обработка вывода процесса может быть не завершена к моменту вызова 'ToString'. Такое поведение описано в документации к методу 'WaitForExit'. Для того чтобы обработка была гарантированно завершена, необходимо дополнительно вызвать метод без аргументов:
public void Run()
{
var process = new Process();
....
StringBuilder data = new StringBuilder();
process.OutputDataReceived +=
(sender, args) => data.AppendLine(args.Data);
process.Start();
process.BeginOutputReadLine();
if (process.WaitForExit(3000))
{
process.WaitForExit();
WriteData(data.ToString());
}
else
{
.... // throw timeout error
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, когда в результате вызова 'OfType' будет возвращена пустая коллекция. Данное поведение обусловлено невозможностью преобразования типа элементов коллекции к типу, по которому фильтрует 'OfType'.
Рассмотрим пример:
public struct SyntaxToken {....}
public class InvocationExpressionSyntax : ExpressionSyntax {....}
public List<SyntaxToken> GetAllTokens() {....}
public List<ExpressionSyntax> GetAllExpressions() {....}
void ProcessInvocationExpressions()
{
var result = GetAllTokens().OfType<InvocationExpressionSyntax>();
....
}
В 'ProcessInvocationExpressions' должны обрабатываться объекты типа 'InvocationExpressionSyntax'. Для этого производится фильтрация коллекции с помощью метода 'OfType'. Однако данный метод вызывается на коллекцию элементов типа 'SyntaxToken'. В результате фильтрации будет получена пустая коллекция, так как экземпляры структуры 'SyntaxToken' точно не могут являться 'InvocationExpressionSyntax'.
В вышеописанном примере был перепутан метод для получения коллекции объектов типа 'ExpressionSyntax'. Вместо 'GetAllTokens' должен использоваться 'GetAllExpressions'. Корректная реализация 'ProcessInvocationExpressions' будет выглядеть следующим образом:
void ProcessInvocationExpressions()
{
var result = GetAllExpressions().OfType<InvocationExpressionSyntax>();
....
}
При такой реализации будет отфильтрована коллекция с элементами типа 'ExpressionSyntax'. Данный тип является базовым для 'InvocationExpressionSyntax'. Следовательно, преобразование из 'ExpressionSyntax' в 'InvocationExpressionSyntax' представляется возможным. В результате вызова 'OfType' может быть получена непустая коллекция.
Данная диагностика классифицируется как:
Анализатор обнаружил, что в результате инициализации коллекции будет выброшено исключение типа 'NullReferenceException'. Это может произойти, если коллекция является свойством/полем, которое инициализируется на этапе создания объекта.
Рассмотрим пример:
class Container
{
public List<string> States { get; set; }
}
void Process(string? message)
{
var container = new Container
{
States = { "Red", "Yellow", "Green" }
};
}
В методе 'Process' создаётся объект типа 'Container'. На этапе создания объекта инициализируется список 'States'. При его инициализации будет выброшено исключение типа 'NullReferenceException'. Данное поведение обусловлено тем, что конструкция 'States = { "Red", "Yellow", "Green" }' раскрывается в три вызова метода 'Add' у свойства 'States'. По умолчанию объект типа 'List<string>' имеет значение 'null'. Следовательно, в данном случае метод 'Add' будет вызван у свойства, которое имеет значение 'null'.
Чтобы избежать возникновения исключения, можно присвоить значение свойству на этапе объявления:
class Container
{
public List<string> States { get; set; } = new List<string>();
}
Теперь при инициализации на этапе создания объекта класса исключения не будет.
Рассмотрим ещё один способ:
void Process(string? message)
{
var container = new Container
{
States = new() { "Red", "Yellow", "Green" }
};
}
В данном случае сначала будет создан объект списка с помощью 'new()', после чего в него будут добавлены элементы.
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительный код, в котором один из параметров никак не используется. При этом в теле функции/конструктора используется значение переменной, поля или свойства с похожим названием. Возможно, это значение было по ошибке использовано вместо значения параметра метода.
Рассмотрим пример:
public GridAnswerData(int questionId, ....)
{
this.QuestionId = QuestionId;
....
}
В данном конструкторе свойству присваивается собственное же значение. Очевидно, что это опечатка, и свойству 'this.QuestionId' должно быть присвоено значение параметра 'questionId'. В результате решение проблемы выглядит следующим образом:
public GridAnswerData(int questionId, ....)
{
this.QuestionId = questionId;
....
}
Рассмотрим ещё один пример:
public async void Save(string filePath = null)
{
using(var writer = new StreamWriter(FilePath))
{
....
await writer.WriteAsync(Data);
}
}
В данном случае в метод 'Save' в качества аргумента 'filePath' передаётся путь к файлу, в который должны быть сохранены какие-то данные. Однако вместо этого сохранение выполняется в другой файл, путь к которому возвращает свойство 'FilePath'. Можно предположить, что свойство 'FilePath' должно быть использовано только в случае, если аргумент 'filePath' имеет значение 'null'. В итоге исправленный код может выглядеть следующим образом:
public async void Save(string filePath = null)
{
var path = filePath ?? FilePath;
using(var writer = new StreamWriter(path))
{
....
await writer.WriteAsync(Data);
}
}
Даже если это предположение ошибочно, наличие неиспользуемого параметра в сигнатуре метода может запутать и потенциально привести к ошибкам при использовании этого метода в будущем.
Анализатор обнаружил возможную ошибку в переопределённом методе 'Equals', связанную с проверкой неверного типа.
Рассмотрим пример:
private class FirstClass
{
....
public override bool Equals(object obj)
{
SecondClass other = obj as SecondClass; // <=
if (other == null)
{
return false;
}
return Equals(other);
}
public bool Equals(FirstClass other)
{
....
}
}
В переопределённом методе 'Equals' класса 'FirstClass' допущена ошибка при проверке типа 'obj': вместо 'SecondClass' должен быть 'FirstClass'.
В результате ошибки, если в переопределённый метод 'Equals' будет передан объект типа 'FirstClass', метод будет всегда возвращать 'false'.
Более того, если передать в качестве параметра объект типа 'SecondClass', тогда будет вызван этот же переопределённый метод 'Equals'. Это приведёт к рекурсии и выбрасыванию исключения типа 'StackOverflowException'.
Исправленный вариант:
private class FirstClass
{
....
public override bool Equals(object obj)
{
FirstClass other = obj as FirstClass;
if (other == null)
{
return false;
}
return Equals(other);
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V3197. |
Анализатор обнаружил ситуацию, когда переменной присваивается значение, которое она уже имеет.
Рассмотрим пример:
public long GetFactorial(long it)
{
long currentValue = 1;
for (int i = 1; i <= it; i++)
{
currentValue = currentValue * currentValue;
}
return currentValue;
}
Метод 'GetFactorial' должен возвращать значение факториала, соответствующее параметру. Однако этот метод всегда будет возвращать единицу. Данная ситуация возникает из-за того, что на каждой итерации цикла переменной 'currentValue' присваивается значение, которое она уже имеет.
Для исправления нужно заменить один из множителей на 'i':
for (int i = 1; i <= it; i++)
{
currentValue = currentValue * i;
}
Существует возможность игнорировать возвращаемые значения методов и свойств для данного диагностического правила. Для этого нужно добавить в файл '.pvsconfig' следующую команду:
//V_3198_IGNORE_RETURN_VALUE:NamespaseName.TypeName.MethodName
При использовании команды из примера, данное диагностическое правило не будет выдавать предупреждения, если переменной присваивается возвращаемое значение метода 'NamespaseName.TypeName.MethodName'.
Аналогичным образом можно размечать свойства.
Данная диагностика классифицируется как:
Анализатор обнаружил доступ к элементу коллекции с помощью оператора '^' со значением, которое меньше или равно 0. Это приводит к исключению типа 'IndexOutOfRangeException'.
Рассмотрим пример:
T GetLastItem<T>(T[] array)
{
return array[^0];
}
Оператор '^' указывает, что индекс учитывается с конца последовательности. Может быть не очевидно, что '^0' равносильно 'array.Length'. При попытке получения последнего элемента коллекции с помощью '^0' будет выброшено исключение, как и при использовании 'array[array.Length]'.
Исправленный код:
T GetLastItem<T>(T[] array)
{
return array[^1];
}
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительное приведение типов. Результат бинарной операции приводится к типу с большим диапазоном.
Рассмотрим пример:
long Multiply(int a, int b)
{
return (long)(a * b);
}
Такое преобразование избыточно. Тип 'int' и так бы автоматически расширился до типа 'long'.
Скорее всего, подобный паттерн приведения используется для защиты от переполнения, но он неправильный. При перемножении переменных типа 'int' всё равно произойдёт переполнение, и только уже бессмысленный результат умножения будет явно расширен до типа 'long'.
Для корректной защиты от переполнения можно привести один из аргументов к типу 'long'. Исправленный код:
long Multiply(int a, int b)
{
return (long)a * b;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально-ошибочное игнорирование возвращаемого значения метода, которое в большинстве остальных случаев используется.
Рассмотрим синтетический пример:
Audio _currentMusic = null;
void Foo1(....)
{
....
_currentMusic = PlayMusic();
}
void Foo2()
{
if (....)
_currentMusic = PlayMusic();
}
....
void Foo10()
{
....
PlayMusic(); // <=
}
В данном примере возвращаемое значение метода 'PlayMusic' используется всегда, кроме одного случая. Анализатор выдаёт предупреждение, если возвращаемое значение метода игнорируется не более чем в 10% случаев. При этом отсутствуют признаки того, что оно намеренно не используется.
В некоторых ситуациях возвращаемое значение действительно не требуется как-то использовать. Например, если метод имеет побочные эффекты (изменение свойств, полей, запись/чтение файла и прочее), и возвращаемым значением можно пренебречь. Для улучшения читаемости кода рекомендуется явно обозначить это, присвоив результат метода discard-переменной:
_ = PlayMusic();
В этом случае предупреждения не будет.
Анализатор обнаружил потенциальную ошибку, связанную с тем, что одна или несколько ветвей оператора 'switch' никогда не выполнятся. Причина этого в том, что сравниваемое выражение не может принять значение, записанное после оператора 'case'.
Рассмотрим синтетический пример:
switch (random.Next(0, 3))
{
case 0:
case 1:
Console.WriteLine("1");
break;
case 2:
Console.WriteLine("2");
break;
case 3: // <=
Console.WriteLine("3");
break;
default:
break;
}
В данном случае код в 'case 3' никогда не выполнится. Дело в том, что в 'random.Next(0, 3)' верхняя граница не входит в возвращаемый диапазон значений. В результате выражение в 'switch' никогда не примет значение 3, и 'case 3' не выполнится.
Исправить эту ошибку можно двумя способами. В первом варианте можно просто избавиться от мертвого кода, удалив секцию 'case 3', которая не входит в диапазон 'random.Next(0, 3)':
switch (random.Next(0, 3))
{
case 0:
case 1:
Console.WriteLine("1");
break;
case 2:
Console.WriteLine("2");
break;
}
Либо же можно увеличить верхнюю границу в методе 'next' - 'random.Next(0, 4)':
switch (random.Next(0, 4))
{
case 0:
case 1:
Console.WriteLine("1");
break;
case 2:
Console.WriteLine("2");
break;
case 3:
Console.WriteLine("3");
break;
}
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с тем, что один или несколько параметров метода не были использованы.
Рассмотрим пример:
private List<uint> TranslateNgramHashesToIndexes(Language language, ....)
{
....
//var offset = (uint)Data.LanguageOffset[language];
....
if (Data.SubwordHashToIndex.TryGetValue(hashes[i]/* + offset*/,
out int index))
....
else if (....)
{
....
Data.SubwordHashToIndex.Add(hashes[i]/* + offset*/, index);
}
....
}
Параметр 'language' используется только в закомментированном коде. В такой ситуации стоит убедиться, что этот код действительно должен быть закомментирован, а не случайно оставлен таким после отладки.
Рассмотрим ещё один пример:
private void DoConnect(EndPoint address)
{
ReportConnectFailure(() =>
{
_channel = DatagramChannel.Open();
_channel.ConfigureBlocking(false);
var socket = _channel.Socket;
....
_channel.Connect(_connect.RemoteAddress);
});
}
В данном примере единственный параметр 'address' не используется. Это может привести к путанице при использовании этого метода, а в худшем случае и вовсе свидетельствует о наличии ошибки в реализации.
В случае, если параметр является устаревшим, можно пометить метод атрибутом 'Obsolete'. Если параметр намеренно не используется по какой-то другой причине, рекомендуется дать ему имя следующего вида: '_', '_1', '_2' и т. д.
Анализатор обнаружил не работающую из-за неявного приведения типа проверку на переполнение.
Рассмотрим пример:
bool IsValidAddition(ushort x, ushort y)
{
if (x + y < x)
return false;
return true;
}
Цель данного метода — проверить, произойдет ли переполнение при сложении двух положительных чисел. В случае переполнения результат суммы должен оказаться меньше любого из её операндов.
Однако проверка не выполнит свою задачу, поскольку оператор '+' не имеет перегрузки для сложения чисел с типом 'ushort'. В результате оба числа будут в начале приведены к типу 'int', после чего выполнится их сложение. Так как складываются значения типа 'int', никакого переполнения не произойдёт.
Для исправления проверки нужно явно привести результат суммы к типу 'ushort':
bool IsValidAddition(ushort x, ushort y)
{
if ((ushort)(x + y) < x)
return false;
return true;
}
Кромe типа 'ushort', перегрузка оператора суммы отсутствует и для чисел с типом 'byte'. Перед сложением эти числа также будут неявно приведены к 'int' типу.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
Анализатор обнаружил нежелательное создание экземпляра классов 'MonoBehaviour' или 'ScriptableObject' с помощью оператора 'new'. Объекты, созданные таким образом, не будут связаны с движком, поэтому такие специфичные Unity-методы, как 'Update', 'Awake', 'OnEnable' и прочие, вызываться не будут.
Ниже приведён пример:
class ExampleSO: ScriptableObject
....
class ExampleComponent: MonoBehaviour
....
void Awake
{
var scriptableObject = new ExampleSO();
var component = new ExampleComponent();
}
Чтобы избежать потенциальных проблем, вместо оператора 'new' для создания экземпляров этих классов следует использовать один из следующих методов:
Исправленный код:
class ExampleSO: ScriptableObject
....
class ExampleComponent: MonoBehaviour
....
void Awake
{
var scriptableObject = ScriptableObject.CreateInstance<ExampleSO>();
var component = this.gameObject.AddComponent<ExampleComponent>();
}
Анализатор обнаружил подозрительный вызов в Unity-скрипте метода, похожего на coroutine, возвращаемое значение которого не используется. Для запуска coroutine нужно использовать метод 'StartCoroutine'.
Рассмотрим пример:
class CustomComponent: MonoBehaviour
{
IEnumerator ExampleCoroutine()
{
....
yield return null;
....
}
void Start()
{
....
ExampleCoroutine();
....
}
}
В данном случае код coroutine 'ExampleCoroutine' выполняться не будет, т. к. возвращаемый в результате вызова объект 'IEnumerator' никак не используется. Чтобы решить проблему, нужно передать его в метод 'MonoBehaviour.StartCoroutine':
void Start()
{
....
StartCoroutine(ExampleCoroutine());
....
}
Дополнительные ссылки
Анализатор обнаружил фрагмент кода, который, возможно, содержит логическую ошибку. В условном выражении был обнаружен паттерн 'is not * or *'. Приоритет оператора 'not' выше, чем у оператора 'or'. Вследствие этого отрицание не применяется к правой части 'or'.
Рассмотрим пример:
private void ShowWordDetails(string key)
{
if (key is not "" or null)
{
PanelReferenceBox.Controls.Clear();
CurrentWord = Words.Find(x => x.Name == key);
....
}
}
Логика выражения 'key is not "" or null' нарушена. Если 'key' – null, то результатом логического выражения будет 'true', когда подразумевался 'false'.
Ошибка возникает, если разработчик не учитывает, что оператор 'not' имеет приоритет выше, чем 'or'. В данном паттерне второе подвыражение оператора 'or', как правило, оказывается бессмысленным. Например, в выражении 'key is not "" or null', если 'key' – null, при проверке на не пустую строку будет получен результат 'true'. Получается, что вторая часть выражения не будет влиять на конечный результат.
Для корректного порядка выполнения операторов стоит использовать скобки после оператора 'not'.
Исправленный код:
private void ShowWordDetails(string key)
{
if (key is not ("" or null))
{
PanelReferenceBox.Controls.Clear();
CurrentWord = Words.Find(x => x.Name == key);
....
}
}
Теперь код работает, как ожидалось. В условии проверяется, что строка 'key' не пустая и не 'null', вместо проверки только на пустую строку.
Данная диагностика классифицируется как:
Анализатор обнаружил использование экземпляра класса 'UnityEngine.Object' или его подкласса вместе с 'System.WeakReference'. Из-за неявного использования экземпляра самим движком поведение слабой ссылки может отличаться от ожидаемого.
Рассмотрим пример:
WeakReference<GameObject> _goWeakRef;
void UnityObjectWeakReference()
{
var go = new GameObject();
_goWeakRef = new WeakReference<GameObject>(go);
}
В данном примере создается слабая ссылка на объект класса 'GameObject'. Даже если вы не создавали сильных ссылок на этот объект, сборщик мусора не сможет очистить его. Свойство 'IsAlive' у 'WeakReference' или результат метода 'TryGetTarget' у 'WeakReference<T>' будут иметь значение 'true' даже после уничтожения объекта при помощи метода 'Destroy' или 'DestroyImmediate'.
Больше информации об этом можно прочитать в документации Unity.
При обнаружении подобного использования 'WeakReference' стоит произвести рефакторинг кода для исключения использования слабой ссылки вместе с 'UnityEngine.Object' или его подклассами.
Так же анализатор выдаёт предупреждение, когда при помощи метода 'WeakReference.SetTarget' в качестве аргумента передают экземпляр класса 'UnityEngine.Object' или его подкласса.
Анализатор обнаружил более одного использования одного и того же 'UnityEngine.Awaitable' объекта с оператором 'await'. В целях оптимизации 'Awaitable' объекты хранятся в пуле-объектов. При await-вызове объект 'Awaitable' возвращается в пул. После этого при повторном применении 'await' к этому же объекту будет выброшено исключение, в некоторых случаях также возможна взаимная блокировка.
Рассмотрим синтетический пример:
async Awaitable<bool> AwaitableFoo() { .... }
async Awaitable ExampleFoo()
{
Awaitable<bool> awaitable = AwaitableFoo();
if (await awaitable)
{
var result = await awaitable;
....
}
}
В этом коде будет выброшено исключение (или возникнет взаимная блокировка) при инициализации переменной 'result' значением, полученным с помощью 'await' вызова 'awaitable'. Это произойдёт из-за того, что ранее 'await' уже применялся к 'awaitable' в условии условной конструкции.
Чтобы обезопасить этот код, следует избегать записи 'Awaitable' в переменную. Вместо этого можно переиспользовать значение, полученное при await-вызове 'AwaitableFoo()':
async Awaitable<bool> AwaitableFoo() { .... }
async Awaitable ExampleFoo()
{
bool value = await AwaitableFoo();
if (value)
{
var result = value;
....
}
}
Или повторно выполнять await-вызов непосредственно метода 'AwaitableFoo' там, где это требуется:
async Awaitable<bool> AwaitableFoo() { .... }
async Awaitable ExampleFoo()
{
if (await AwaitableFoo())
{
var result = await AwaitableFoo();
....
}
}
Такое решение также является корректным, ведь при каждом вызове 'AwaitableFoo()' будет возвращён новый объект 'Awaitable'.
Стоит отметить, что возможны и менее очевидные случаи возникновения проблемы. Например, когда повторный await-вызов значения 'Awaitable' происходит внутри другого метода, которому это значение было передано в качестве аргумента:
async Awaitable<bool> AwaitableFoo() { .... }
async Awaitable<Result> GetResult(Awaitable<bool> awaitable)
{
if (await awaitable){ .... } // <=
else { .... }
}
async Awaitable ExampleFoo()
{
Awaitable<bool> awaitable = AwaitableFoo();
if (await awaitable) // <=
{
....
}
var result = await GetResult(awaitable); // <=
}
Решения проблемы в таких случаях полностью аналогичны решениям, описанным ранее.
Анализатор обнаружил, что для вызова метода 'Destroy' или 'DestroyImmediate' класса 'UnityEngine.Object' используется аргумент типа 'UnityEngine.Transform'. Это приводит к ошибке во время вызова метода. Удаление компонента 'Transform' из игрового объекта не допускается в Unity.
Рассмотрим пример:
using UnityEngine;
class Projectile : MonoBehaviour
{
public void Update()
{
if (....)
{
Destroy(transform);
}
....
}
}
Свойство 'transform' из базового класса 'MonoBehaviour' возвращает экземпляр класса 'Transform', который передается в качестве аргумента методу 'Destroy'. При таком вызове метода Unity выдаст сообщение об ошибке, а сам компонент не будет уничтожен.
Сообщение Unity:
Can't destroy Transform component of 'Projectile'. If you want to destroy the game object, please call 'Destroy' on the game object instead. Destroying the transform component is not allowed.
Один из вариантов исправления ошибочного кода:
using UnityEngine;
class Projectile : MonoBehaviour
{
public void Update()
{
if (....)
{
Destroy(gameObject);
}
....
}
}
В этом случае будет уничтожен весь игровой объект, включая привязанный к нему компонент 'Transform'.
Данная диагностика классифицируется как:
Анализатор обнаружил пустой блок обработки исключения ('catch' или 'finally'). Отсутствие корректной обработки исключений может привести к снижению уровня надёжности приложения.
В некоторых случаях отсутствие корректной обработки исключительных ситуаций может стать причиной возникновения уязвимости. Недостаточное логирование и мониторинг выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A10:2017-Insufficient Logging & Monitoring.
Пример кода с пустым 'catch':
try
{
someCall();
}
catch (Exception e)
{
}
Конечно, такой код вовсе не обязательно ошибочен. Но очень странно просто подавлять исключение, ничего не делая, так как такая обработка исключений может скрывать дефекты в программе.
В качестве обработки исключения можно использовать, например, логгирование. Это по крайней мере не позволит исключительной ситуации остаться незамеченной:
try
{
someCall();
}
catch (Exception e)
{
logger.error("Message", e);
}
Не менее подозрительным моментом является наличие в коде пустого блока 'finally'. Это может свидетельствовать о том, что какая-то логика, необходимая для надёжной работы приложения, не реализована. Например:
try
{
someCall();
}
catch (Exception e)
{ .... }
finally
{
}
Подобный код с большой вероятностью свидетельствует об ошибке или попросту избыточен. В отличие от пустого блока 'catch', который может быть использован для подавления исключения, у пустого блока 'finally' нет какого-либо практического применения.
Данная диагностика классифицируется как:
|
Анализатор обнаружил класс исключения, недоступный для внешних классов. Если такое исключение будет выброшено, внешний код будет вынужден отлавливать объекты ближайшего доступного предка или вообще базового класса всех исключений – 'Throwable'. В этом случае усложняется обработка конкретных исключительных ситуаций, ведь внешний код не сможет чётко идентифицировать возникшую проблему.
Отсутствие чёткой идентификации возникшей проблемы несёт дополнительные риски с точки зрения безопасности, так как для каких-то определённых исключительных ситуаций может понадобиться специфичная обработка, а не общая. Недостаточное логирование и мониторинг (в том числе, обнаружение проблем) выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A10:2017-Insufficient Logging & Monitoring.
Простой пример из реального проекта:
public class TxnLogToolkit implements Closeable
{
static class TxnLogToolkitException extends Exception
{
....
private int exitCode;
TxnLogToolkitException(int exitCode, ....)
{
super(....);
this.exitCode = exitCode;
}
int getExitCode()
{
return exitCode;
}
}
....
}
Чтобы можно было корректно обработать конкретную исключительную ситуацию, необходимо задать в объявлении класса модификатор доступности 'public':
public class TxnLogToolkit implements Closeable
{
public static class TxnLogToolkitException extends Exception
{
....
private int exitCode;
public TxnLogToolkitException(int exitCode, ....)
{
super(....);
this.exitCode = exitCode;
}
public int getExitCode()
{
return exitCode;
}
}
....
}
Теперь код внешних классов сможет отлавливать данное исключение и обрабатывать конкретную ситуацию.
Следует учитывать, что для вложенных классов модификатора 'public' у объявления исключения может быть недостаточно. Например:
class OperatorHelper
{
public static class OpCertificateException extends CertificateException
{
private Throwable cause;
public OpCertificateException(String msg, Throwable cause)
{
super(msg);
this.cause = cause;
}
public Throwable getCause()
{
return cause;
}
}
}
В данном примере класс исключения вложен в класс 'OperatorHelper', который неявно объявлен как 'package-private'. Вследствие этого исключение 'OpCertificateException' также будет видимо только в пределах текущего пакета, несмотря на то, что имеет модификатор доступа 'public'. Анализатор обнаруживает такие случаи и выдаёт соответствующие предупреждения.
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с тем, что создаётся экземпляр класса исключения, но при этом никак не используется.
Пример ошибочного кода:
int checkIndex(int index)
{
if (index < 0)
new IndexOutOfBoundsException("Index Out Of Bounds!!!");
return index;
}
В данном коде пропущен оператор 'throw', из-за чего будет только создан экземпляр класса, но при этом он никак не будет использоваться, и исключение не будет сгенерировано. Корректный код может выглядеть следующим образом:
int checkIndex(int index)
{
if (index < 0)
throw new IndexOutOfBoundsException("Index Out Of Bounds!!!");
return index;
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с небезопасным использованием паттерна "блокировка с двойной проверкой" (double-checked locking).
Блокировка с двойной проверкой - это паттерн, предназначенный для уменьшения накладных расходов получения блокировки. Сначала проверяется условие блокировки без синхронизации. И только если условие выполняется, поток попытается получить блокировку. Таким образом, блокировка будет выполнена только в том случае, когда она действительно была необходима.
Основной ошибкой при реализации этого паттерна является публикация объекта перед его инициализацией:
class TestClass
{
private static volatile Singleton singleton;
public static Singleton getSingleton()
{
if (singleton == null)
{
synchronized (TestClass.class)
{
if (singleton == null)
{
singleton = new Singleton();
singleton.initialize(); // <=
}
}
}
return singleton;
}
}
При многопоточном выполнении один из потоков может увидеть, что объект уже был создан и воспользоваться им, даже если инициализация этого объекта еще не произошла.
Похожая ошибка случится, когда в блоке синхронизации объект переприсваивается в зависимости от тех или иных условий. После первого присваивания какой-либо другой поток вполне может начать с ним работать, не подозревая, что далее в программе будет использоваться другой объект.
Исправление таких ошибок производится путем создания временной переменной:
class TestClass
{
private static volatile Singleton singleton;
public static Singleton getSingleton()
{
if (singleton == null)
{
synchronized (TestClass.class)
{
if (singleton == null)
{
Singleton temp = new Singleton();
temp.initialize();
singleton = temp;
}
}
}
return singleton;
}
}
Другой распространённой ошибкой при реализации этого паттерна является пропуск модификатора 'volatile' в декларации поля, к которому производится доступ:
class TestClass
{
private static Singleton singleton;
public static Singleton getSingleton()
{
if (singleton == null)
{
synchronized (TestClass.class)
{
if (singleton == null)
{
Singleton temp = new Singleton();
temp.initialize();
singleton = temp;
}
}
}
return singleton;
}
}
Объект класса 'Singleton' может быть создан несколько раз из-за того, что проверка 'singleton == null' увидит значение 'null', закешированное в потоке. Кроме того, компилятор может изменить порядок выполнения операций с не-volatile полями, из-за чего, например, вызов метода инициализации объекта и запись ссылки на этот объект в поле могут произойти в обратном порядке, что опять же приведет к использованию объекта, которому только предстоит пройти процедуру инициализации.
Одна из опасностей таких ошибок состоит в том, что в большинстве случаев программа работает корректно. В данном случае некорректное поведение программы может проявиться в зависимости от используемой JVM, уровня конкуретности, решений планировщика потоков и прочих факторов. Воспроизвести такие условия вручную крайне сложно.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил в коде данные, которые могут являться конфиденциальными.
В качестве таких данных могут выступать, например, пароли. Их хранение в исходном коде может привести к нарушению контроля доступа к данным и возможностям, не предназначенным для публичного использования. Имея байт-код, любой пользователь сможет извлечь все строковые литералы, которые в нем используются. В случае open-source проектов всё ещё проще, так как злоумышленник может изучать непосредственно исходный код.
Таким образом, все секретные данные могут оказаться публично доступными. Уязвимости, связанные с недостаточной защищённостью конфиденциальных данных, выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A2:2017-Broken Authentication.
Рассмотрим пример:
public static void main(String[] arg)
{
....
JSch jsch = new JSch();
Session session = jsch.getSession(user, host, 22);
session.setPassword("123fj");
....
}
В указанном примере пароль хранится в коде. Следовательно, злоумышленник может легко получить эти данные.
Вместо хранения секретных данных в коде лучше использовать, например, хранилища, в которых данные будут храниться в зашифрованном виде, и к которым у обычных пользователей не будет прямого доступа.
В таком случае код может выглядеть, например, так:
public static void main(String[] arg)
{
....
JSch jsch = new JSch();
Session session = jsch.getSession(user, host, 22);
session.setPassword(dataStorage.getPassword);
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, когда оригинальная информация об исключении была утеряна при повторной генерации из блока 'catch'. Из-за этого ошибки превращаются в трудноотлаживаемые.
Отсутствие чёткой идентификации возникшей проблемы несёт дополнительные риски с точки зрения безопасности. Недостаточное логирование и мониторинг (в том числе обнаружение проблем) выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A10:2017-Insufficient Logging & Monitoring.
Рассмотрим пример некорректного кода:
try {
sb.append((char) Integer.parseInt(someString));
....
} catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
В данном случае перехваченное исключение хотели перебросить и не передали полезную информацию в виде сообщения и stacktrace.
Корректный вариант кода:
try {
sb.append((char) Integer.parseInt(someString));
....
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
В исправленном варианте исходное исключение передаётся в качестве внутреннего, что полностью сохраняет информацию об исходной ошибке.
Другим вариантом исправления может являться выбрасывание исключения с сообщением пользователю:
try {
sb.append((char) Integer.parseInt(someString));
....
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"String " + someString + " is not number"
);
}
В данной ситуации стек оригинальной ошибки был утерян, но описанная информация в новом исключении внесёт большую ясность при отладке этого кода.
Если потеря информации об исключении является ожидаемым поведением, то вы можете заменить имена 'catch'-параметров на "ignore" или "expected", и исключение выдаваться не будет.
Данная диагностика классифицируется как:
|
Данное диагностическое правило выявляет случаи использования псевдорандомного генератора случайных чисел, которые могут привести к недостаточно случайному распределению или предсказуемости генерируемого числа.
Случай 1
Создание нового объекта типа 'Random' каждый раз, когда требуется случайное значение. Это неэффективно и может привести к получению чисел, которые не являются достаточно случайными, в зависимости от JDK.
Рассмотрим пример:
public void test() {
Random rnd = new Random();
}
Для большей эффективности и случайности распределения создайте один экземпляр класса 'Random', сохраните его и используйте повторно.
static Random rnd = new Random();
public void test() {
int i = rnd.nextInt();
}
Случай 2
Анализатор обнаружил подозрительный код, инициализирующий генератор псевдослучайных чисел константным значением.
public void test() {
Random rnd = new Random(4040);
}
Числа, сгенерированные таким генератором, можно предугадать — они будут воспроизводиться снова и снова при каждом запуске программы. Чтобы этого избежать, не стоит использовать константное число. К примеру, можно воспользоваться текущим системным временем:
static Random rnd = new Random(System.currentTimeMillis());
public void test() {
int i = rnd.nextInt();
}
Данная диагностика классифицируется как:
Анализатор обнаружил подозрительное приведение типов. Результат бинарной операции приводится к типу с большим диапазоном.
Рассмотрим пример:
long multiply(int a, int b) {
return (long)(a * b);
}
Такое преобразование избыточно. Тип 'int' и так бы автоматически расширился до типа 'long'.
Скорее всего, подобный паттерн приведения используется для защиты от переполнения, но он неправильный. При перемножении переменных типа 'int' всё равно произойдёт переполнение, и только уже бессмысленный результат умножения будет явно расширен до типа 'long'.
Для корректной защиты от переполнения можно привести один из аргументов к типу 'long'. Исправленный код:
long multiply(int a, int b) {
return (long)a * b;
}
Данная диагностика классифицируется как:
Анализатор обнаружил создание SQL-команды из данных, полученных из внешнего источника, без предварительной проверки. Это может стать причиной возникновения SQL-инъекции в случае, если данные будут скомпрометированы.
SQL-инъекции выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A1:2017-Injection.
Рассмотрим пример:
public static void getFoo(Connection conn, String bar) throws SQLException {
var st = conn.createStatement();
var rs = st.executeQuery("SELECT * FROM foo WHERE bar = '" + bar + "'");
// ....
}
В данном случае при создании SQL-команды используется значение переменной 'bar', полученной из публичного метода. Так как метод публичный, туда могут попасть непроверенные данные из внешнего источника (контроллеров, форм и пр.). Использование данных без какой-либо проверки является опасным, так как открывает злоумышленникам разные способы внедрения команд.
Например, вместо ожидаемого значения имени пользователя злоумышленник может ввести специальную команду. Тогда из базы произойдёт извлечение данных всех пользователей, для которых будет запущена дальнейшая обработка.
Пример такой скомпрометированной строки для запроса выше:
' OR '1'='1
Для защиты от подобных запросов следует использовать параметризированные команды:
public static void getFoo(Connection conn, String bar) throws SQLException {
var sql = "SELECT * FROM foo WHERE bar = ?";
var st = conn.prepareStatement(sql);
st.setString(1, bar);
var rs = st.executeQuery();
// ....
}
Также безопасной альтернативой будет использование ORM, если нет необходимости в прямом исполнении SQL-запросов.
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Восьмеричные числовые литералы и экранирующие последовательности не должны использоваться. Использование восьмеричных литералов может затруднить восприятие кода, особенно при быстром просмотре. Неправильная интерпретация фактического числового значения может приводить к разнообразным ошибкам.
Пример кода, на который анализатор выдаст предупреждение:
if (val < 010)
{
....
}
При быстром просмотре кода можно упустить из виду, что значение числового литерала – 8, а не 10. Чтобы анализатор не выдавал предупреждения, литерал стоит переписать в десятичной или шестнадцатеричной форме:
if (val < 8)
{
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Явное указание размера массива улучшает читаемость и понимание кода, уменьшая вероятность возникновения ошибок, связанных с выходом за границы массива из-за незнания его размера.
Анализатор выдаст предупреждение, если встретит объявление массива со спецификатором 'extern', при условии, что размер массива не задан явно:
extern int arr[];
Для исправления следует явно указать размер массива:
extern int arr[12];
Если размер массива можно вывести из инициализатора, предупреждение выдано не будет:
int arr1[] = {1, 2, 3};
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Использование оператора 'goto', осуществляющего переход к метке, находящейся выше по коду, ухудшает читаемость кода и как следствие – усложняет его поддержку.
Пример кода, на который анализатор выдаст предупреждение:
void init(....)
{
....
again:
....
if (....)
if (....)
goto again;
....
}
Чтобы анализатор не выдавал предупреждение, необходимо отказаться от использования оператора 'goto' или переписать код таким образом, чтобы метка, на которую ссылается 'goto', находилась ниже него.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Тела операторов 'while', 'do-while', 'for', 'if', 'if-else', 'switch' должны быть заключены в фигурные скобки.
Использование фигурных скобок четко определяет, какие выражения составляют тело, повышает читаемость кода и снижает вероятность возникновения ряда ошибок. Например, при отсутствии фигурных скобок программист может ошибочно воспринять выравнивание или не заметить символ ';', случайно поставленный после оператора.
Пример 1:
void example_1(....)
{
if (condition)
if (otherCondition)
DoSmt();
else
DoSmt2();
}
Форматирование данного кода не соответствует логике его выполнения, что может сбивать с толку. Расстановка фигурных скобок устраняет возможную неоднозначность:
void example_1(....)
{
if (condition)
{
if (otherCondition)
{
DoSmt();
}
else
{
DoSmt2();
}
}
}
Пример 2:
void example_2(....)
{
while (count < 10)
DoSmt1(); DoSmt2();
}
Форматирование данного кода также не соответствует логике его исполнения, так как к циклу относится только выражение 'DoSmt1()'.
Исправленный вариант:
void example_2(....)
{
while (count < 10)
{
DoSmt1();
DoSmt2();
}
}
Примечание: 'if', следующий сразу после 'else' не обязательно должен быть заключен в фигурные скобки. На фрагмент кода, приведённый ниже, анализатор не будет выдавать предупреждение.
if (condition1)
{ .... }
else if (condition2)
{ .... }
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций: 'atof'; 'atoi'; 'atol'; 'atoll'.
Неправильное использование этих функций может привести к возникновению неопределённого поведения. Это может произойти, если аргумент функции не является валидной С-строкой или если результирующее значение выходит за границы возвращаемого типа.
Пример кода, на который анализатор выдаст предупреждение:
void Foo(const char *str)
{
int val = atoi(str);
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций: 'abort'; 'exit'; 'getenv'; 'system'.
Поведение перечисленных выше функций зависит от реализации. Кроме того, использование таких функций как 'system' может являться причиной возникновения уязвимостей.
Пример кода, на который анализатор выдаст предупреждение:
void Foo(FILE *pFile)
{
if (pFile == NULL)
{
abort();
}
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование имен 'setjmp' или 'longjmp', так как их неправильное использование может привести к неопределённому поведению.
Пример кода, на который анализатор выдаст предупреждение:
jmp_buf j_buf;
void foo()
{
setjmp(j_buf);
}
int main()
{
foo();
longjmp(j_buf, 0);
return 0;
}
Функция 'longjmp' вызвана уже после выхода из функции, которая вызвала 'setjmp', результат в таком случае не определен.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций: 'strcpy'; 'strcmp'; 'strcat'; 'strchr'; 'strspn'; 'strcspn'; 'strpbrk'; 'strrchr'; 'strstr'; 'strtok'; 'strlen'.
Неправильное использование этих функций может стать причиной возникновения неопределённого поведения, так как они не защищены от чтения и записи вне границ буфера.
Пример кода, на который анализатор выдаст предупреждение:
int strcpy_internal(char *dest, const char *source)
{
int exitCode = FAILURE;
if (source && dest)
{
strcpy(dest, source);
exitCode = SUCCESS;
}
return exitCode;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит объявление объединения.
Неправильное использование объединений может приводить к различным проблемам, например, к получению неверных значений или к возникновению неопределённого поведения.
Например, неопределённое поведение может возникнуть, когда значение считывается не из члена, в который производилась запись в последний раз.
Пример кода, на который анализатор выдаст предупреждение:
union U
{
unsigned char uch;
unsigned int ui;
} uni;
....
uni.uch = 'w';
int ui = uni.ui;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если встретит объявление, использующее более двух уровней вложенности указателей. Использование таких указателей ухудшает понимание кода и как следствие – может стать причиной возникновения различных ошибок.
Пример кода, на который анализатор выдаст предупреждение:
void foo(int **ppArr[])
{
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Каждая последовательность 'if ... else if' должна заканчиваться 'else'. При отсутствии завершающего 'else' анализатор выдаст предупреждение. Наличие конечного 'else' показывает, что рассмотрены все возможные варианты, что помогает при чтении и понимании кода.
Пример кода, на который анализатор выдаст предупреждение:
if (condition)
{
....
}
else if (other_condition)
{
....
}
Чтобы анализатор не выдавал предупреждение, а разработчик, читающий код, сразу понял, что рассмотрен случай, когда ни одно условие не выполнилось, стоит добавить 'else'-ветвь. В этой ветви следует разместить необходимые действия или поясняющий комментарий.
if (condition)
{
....
}
else if (other_condition)
{
....
}
else
{
// No action needed
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Все буквенные суффиксы должны быть в верхнем регистре.
Использование суффиксов в нижнем регистре мешает визуальному восприятию кода. Например, суффикс 'l' можно спутать с единицей (1), что может стать причиной разнообразных ошибок.
Примеры литералов, на которые анализатор выдаст предупреждение:
12l; 34.0f; 23u;
Форма записи литералов, на которую предупреждение выдано не будет:
12L; 34.0F; 23U;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Каждая метка в 'switch' должна быть завершена оператором 'break' или выражением 'throw', не расположенным внутри условия.
Намеренное завершение каждой метки гарантирует, что поток управления не "провалится" в метку, расположенную ниже, и позволяет избежать ошибок при добавлении новых меток.
Исключением из этого правила являются пустые метки, расположенные последовательно.
Пример кода, на который анализатор выдаст предупреждение:
void example_1(int cond, int a)
{
switch (cond)
{
case 1:
case 2:
break;
case 3: // <=
if (a == 42)
{
DoSmth();
}
case 4: // <=
DoSmth2();
default: // <=
;
}
}
Исправленный код:
void example_1(int cond, int a)
{
switch (cond)
{
case 1:
case 2:
break;
case 3:
if (a == 42)
{
DoSmth();
}
break;
case 4:
DoSmth2();
break;
default:
/* No action required */
break;
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Последней меткой в 'switch' должна быть 'default'.
Наличие 'default' в конце каждого 'switch' облегчает чтение кода, а также гарантирует, что будут обработаны любые ситуации, в которых значение контролирующей переменной не соответствует ни одной метке. Так как такие ситуации требуют принятия определенных мер, каждая метка 'default' должна содержать (помимо 'break' или 'throw') выражение или комментарий, объясняющий, почему никаких действий не предпринимается.
Пример 1:
void example_1(int i)
{
switch (i)
{
case 1:
DoSmth1();
break;
default: // <=
DoSmth42();
break;
case 3:
DoSmth3();
break;
}
}
Корректный код:
void example_1(int i)
{
switch (i)
{
case 1:
DoSmth1();
break;
case 3:
DoSmth3();
break;
default:
DoSmth42();
break;
}
}
Пример 2:
enum WEEK
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
} weekDay;
void example_2()
{
int isWorkday;
switch (weekDay)
{
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
isWorkday = 1;
break;
case SATURDAY:
case SUNDAY:
isWorkday = 0;
break;
default: // <=
break;
}
Исправленный код:
enum WEEK
{
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY
} weekDay;
void example_2()
{
int isWorkday;
switch (weekDay)
{
case MONDAY:
case TUESDAY:
case WEDNESDAY:
case THURSDAY:
case FRIDAY:
isWorkday = 1;
break;
case SATURDAY:
case SUNDAY:
isWorkday = 0;
break;
default:
assert(false);
break;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор обнаружил использование целочисленного беззнакового литерала без суффикса 'U'. Подобные литералы могут осложнить восприятие кода, так как их тип становится неочевиден. Более того, одни и те же литералы могут иметь разные типы на разных моделях данных.
Использование суффиксов для явного указания типа позволит избежать неоднозначностей при чтении числовых литералов.
Пример кода, на который анализатор выдаст предупреждение (если тип литерала на анализируемой платформе – беззнаковый):
auto typemask = 0xffffffffL;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Область видимости, в которой находится метка, должна быть составным выражением, которое представляет собой тело 'switch'. Это означает, что метка не должна быть вложена ни в какой блок, кроме как в тело 'switch', и это тело должно быть составным выражением.
Следовательно, все метки, принадлежащие одному 'switch', должны находиться в одной области видимости. Следование этому правилу позволяет сохранить читаемость и структурированность кода.
Пример 1:
void example_1(int param, bool b)
{
switch (param)
{
case 1:
DoSmth1();
if (b)
{
case 2: // <=
DoSmth2();
}
break;
default:
assert(false);
break;
}
}
Рассмотренный пример сложен для понимания. Чтобы анализатор не выдавал предупреждения, следует написать так:
void example_1(int param, bool b)
{
switch (param)
{
case 1:
DoSmth1();
if (b)
{
DoSmth2();
}
break;
case 2:
DoSmth2();
break;
default:
assert(false);
break;
}
}
В следующем примере тело 'switch' не является составным, поэтому анализатор также выдаст предупреждение:
void example_2(int param)
{
switch (param)
default:
DoDefault();
}
Исправленный вариант:
void example_2(int param)
{
switch (param)
{
default:
DoDefault();
break;
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций: 'clock'; 'time'; 'difftime'; 'ctime'; 'asctime'; 'gmtime'; 'localtime'; 'mktime'.
Перечисленные функции имеют неуточнённое поведение или поведение, определённое реализацией, и поэтому могут выдаваться разные форматы времени и даты (в зависимости от окружения, реализации стандартной библиотеки и т. д.).
Пример кода, на который анализатор выдаст предупреждения:
const char* Foo(time_t *p)
{
time_t t = time(p);
return ctime(t);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Значение булевого типа может быть сконвертировано к целочисленному типу, поэтому его можно использовать в качестве управляющей переменной в выражении 'switch'. В этом случае использование конструкции 'if-else' является предпочтительным, так как оно дает более понятное и явное представление намерений программиста.
Пример:
int foo(unsigned a, unsigned b)
{
while (a != 0 && b != 0)
{
switch (a > b) // <=
{
case 0:
a -= b;
break;
default:
b -= a;
break;
}
}
return a;
}
Вместо этого следует написать:
int foo(unsigned a, unsigned b)
{
while (a != 0 && b != 0)
{
if (a > b)
{
b -= a;
}
else
{
a -= b;
}
}
return a;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Не стоит использовать оператор 'запятая', так как он может привести к путанице при чтении кода.
Рассмотрим следующий пример:
int foo(int x, int y) { .... }
foo( ( 0, 3), 12 );
Код может ввести программиста в заблуждение, если ему до прочтения вызова функции неизвестна ее сигнатура. Может показаться, что в функцию передается три аргумента, однако это не так: оператор 'запятая' в выражении '(0, 3)' вычислит левый и правый аргументы, а затем возвращает результат второго. В итоге, вызов функции на самом деле принимает вид:
foo( 3, 12 );
Диагностика выдает предупреждение и для других случаев, таких как например этот:
int myMemCmp(const char *s1, const char *s2, size_t N)
{
for (; N > 0; ++s1, ++s2, --N) { .... }
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Чрезмерное использование 'goto' приводит к плохой структурированности кода, усложняя его понимание.
Для снижения визуальной сложности кода рекомендуется отказаться от переходов во вложенные блоки или между блоками, расположенными на одном уровне.
Пример 1:
void V2532_pos1()
{
...
goto label;
...
{
label:
...
}
}
Здесь 'goto' ссылается во вложенный блок, что нарушает данное правило.
На следующий пример анализатор не будет выдавать предупреждение:
void V2532_neg1()
{
...
label:
...
{
goto label;
...
}
}
Примечание: тела switch-меток рассматриваются как составные выражения, даже если они не обернуты в фигурные скобки. Поэтому прыжки в тело switch-метки из внешнего кода и прыжки между разными switch-метками нарушают данное правило.
Приведем примеры.
Прыжок в switch-метку из внешнего кода (ошибка):
void V2532_pos2(int param)
{
goto label;
switch (param)
{
case 0:
break;
default:
label:;
break;
}
}
Прыжок между switch-метками (ошибка):
void V2532_pos3(int param)
{
switch (param)
{
case 0:
goto label;
break;
default:
label:
break;
}
}
Прыжок из switch-метки во внешний код (ok):
void V2532_neg2(int param)
{
label:
switch (param)
{
case 0:
goto label;
break;
default:
break;
}
}
Прыжок в пределах одной switch-метки (ok):
void neg3(int param)
{
switch (param)
{
case 0:
{
...
{
goto label;
}
}
label:
break;
default:
break;
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Поскольку числа с плавающей точкой не могут точно отобразить все действительные числа, использование таких переменных в цикле может дать непостоянное количество итераций.
Рассмотрим пример:
void foo(void) {
for (float A = 0.1f; A <= 10.0f; A += 0.1f) {
....
}
}
Количество итераций в этом цикле может быть 99 или 100. Точность операций с вещественными числами может зависеть от компилятора, режима оптимизации и многого другого.
Лучше переписать цикл следующим образом:
void foo(void) {
for (int count = 1; count <= 100; ++count) {
float A = count / 10.0f;
}
}
Этот цикл выполнит ровно 100 итераций, а переменную 'A' можно использовать для необходимых вычислений.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Присутствие недостижимого кода может являться признаком ошибки программиста и усложняет поддержку кода.
Компилятор в целях оптимизации вправе удалить недостижимый код. Недостижимый код, неудаленный компилятором, может расходовать ресурсы. Например, он может увеличивать размер бинарного файла или являться причиной излишнего кэширования инструкций.
Рассмотрим первый пример:
void Error()
{
....
exit(1);
}
FILE* OpenFile(const char *filename)
{
FILE *f = fopen(filename, "w");
if (f == nullptr)
{
Error();
printf("No such file: %s", filename);
}
return f;
}
Функция 'printf(....)' никогда не напечатает сообщение об ошибке, так как функция 'Error()' не возвращает управление. Как правильно исправить код зависит от того, какую логику поведения задумывал программист изначально. Возможно, функция должна возвращать управление. Возможно, нарушен порядок выражений и корректный код должен быть таким:
FILE* OpenFile(const char *filename)
{
FILE *f = fopen(filename, "w");
if (f == nullptr)
{
printf("No such file: %s", filename);
Error();
}
return f;
}
Рассмотрим второй пример:
char ch = strText[i];
switch (ch)
{
case '<':
...
break;
case '>':
...
break;
case 0xB7:
case 0xBB:
...
break;
...
}
Здесь ветка расположенная после "case 0xB7:" и "case 0xBB:" никогда не получит управление. Переменная 'ch' имеет тип 'char', а, следовательно, диапазон её значений лежит в пределах [-128..127]. Результатом сравнения "ch == 0xB7" и "ch==0xBB" всегда будет ложь (false). Чтобы код был корректен, переменная 'ch' должна иметь тип 'unsigned char'. Исправленный код:
unsigned char ch = strText[i];
switch (ch)
{
case '<':
...
break;
case '>':
...
break;
case 0xB7:
case 0xBB:
...
break;
...
}
Рассмотрим третий пример:
if (n < 5) { AB(); }
else if (n < 10) { BC(); }
else if (n < 15) { CD(); }
else if (n < 25) { DE(); }
else if (n < 20) { EF(); } // Это ветвь никогда не выполнится.
else if (n < 30) { FG(); }
Из-за некорректного пересечения диапазонов, находящихся в условиях, одна из ветвей никогда не будет выполнена. Исправленный код:
if (n < 5) { AB(); }
else if (n < 10) { BC(); }
else if (n < 15) { CD(); }
else if (n < 20) { EF(); }
else if (n < 25) { DE(); }
else if (n < 30) { FG(); }
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Неиспользованные параметры функции часто появляются после рефакторинга кода. Если сигнатура функции не соответствует её реализации, сложно сразу понять, является ли это ошибкой программиста.
Рассмотрим пример:
static bool CardHasLock(int width, int height)
{
const double xScale = 0.051;
const double yScale = 0.0278;
int lockWidth = (int)floor(width * xScale);
int lockHeight = (int)floor(width * yScale);
....
}
Из кода видно, что параметр 'height' ни разу не используется в теле функции. Скорее всего, здесь допущена ошибка, и код инициализации переменной 'lockHeight' должен выглядеть следующим образом:
int lockHeight = (int)floor(height * yScale);
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Если переменная POD-типа не инициализируется явно и не имеет инициализатора по умолчанию, то её значение будет неопределённым. Использование такого значения приведет к неопределенному поведению.
Простой синтетический пример:
int Aa = Get();
int Ab;
if (Ab) // Ab - uninitialized variable
Ab = Foo();
else
Ab = 0;
Как правило, ошибки использования неинициализированных переменных, возникают из-за опечаток. Например, может оказаться, что в этом месте следовало использовать другую переменную. Корректный вариант кода:
int Aa = Get();
int Ab;
if (Aa) // OK
Ab = Foo();
else
Ab = 0;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор обнаружил функцию с не-void возвращаемым типом, которая не возвращает значение на всех путях выполнения. Согласно стандарту C++, это может привести к неопределенному поведению.
Рассмотрим пример, в котором неопределенное значение возвращается только иногда:
BOOL IsInterestingString(char *s)
{
if (s == NULL)
return FALSE;
if (strlen(s) < 4)
return;
return (s[0] == '#') ? TRUE : FALSE;
}
В коде допущена опечатка. Если длина строки меньше 4 символов, то функция вернет неопределенное значение. Корректный вариант:
BOOL IsInterestingString(char *s)
{
if (s == NULL)
return FALSE;
if (strlen(s) < 4)
return FALSE;
return (s[0] == '#') ? TRUE : FALSE;
}
Примечание. Анализатор старается определить ситуации, когда отсутствие возвращаемого значения не является ошибкой. Пример кода, который анализатор сочтет безопасным:
int Foo()
{
...
exit(10);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор обнаружил потенциально возможную ошибку в записи макроса. Макрос и его параметры следует заключать в скобки.
В случае, когда параметры или само выражение не заключены в скобки, может быть нарушена задуманная последовательность действий после того, как макрос будет подставлен.
Пример кода, на который анализатор выдаст предупреждение:
#define DIV(x, y) (x / y)
Пример использования макроса, приводящего к ошибке:
Z = DIV(x + 1, y + 2);
В результате получим:
Z =(x + 1 / y + 2);
Чтобы избежать нарушения последовательности действий, этот макрос стоит переписать следующим образом:
#define DIV(x,y) ((x) / (y))
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Существует возможность вызвать non-void функцию и не использовать возвращаемое ею значение. Такое поведение программы может скрывать ошибку.
Результат non-void функции всегда должен быть использован. Пример кода, на который анализатор выдаст предупреждение:
int Foo(int x)
{
return x + x;
}
void Bar(int x)
{
Foo(x);
}
В случае, если потеря возврщаемого значения была задумана разработчиком, можно использовать приведение к 'void' типу. Пример кода, на который анализатор не выдаст предупреждение:
void Bar(int x)
{
(void)Foo(x);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Копирование адреса объекта в указатель/ссылку с большим временем жизни может привести к возникновению "висячего" указателя/ссылки после того, как исходный объект перестанет существовать. Это является нарушением безопасности памяти. Использование данных, на которые указывает "висячий" указатель/ссылка, приводит к неопределенному поведению.
Первый пример кода, на который анализатор выдаст предупреждение:
int& Foo( void )
{
int some_variable;
....
return some_variable;
}
Второй пример кода, на который анализатор выдаст предупреждение:
#include <stddef.h>
void Bar( int **ptr )
{
int some_variable;
....
if (ptr != NULL)
*ptr = &some_variable;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
При сравнении на равенство или неравенство значений вещественных типов в зависимости от используемого процессора и настроек компилятора часто можно получить неожиданный результат.
Пример кода, на который анализатор выдаст предупреждение:
const double PI_div_2 = 1.0;
const double sinValue = sin(M_PI / 2.0);
if (sinValue == PI_div_2) { .... }
Чтобы сравнение значений вещественных типов не содержало ошибок, нужно либо использовать встроенные константы 'std::numeric_limits<float>::epsilon()' или 'std::numeric_limits<double>::epsilon()', либо создать собственную константу 'Epsilon' с заданной точностью.
Исправленный пример кода:
const double PI_div_2 = 1.0;
const double sinValue = sin(M_PI / 2.0);
// equality
if (fabs(a - b) <= std::numeric_limits<double>::epsilon()) { .... };
// inequality
if (fabs(a - b) > std::numeric_limits<double>::epsilon()) { .... };
В некоторых случаях сравнение двух вещественных чисел через оператор '==' или '!=' допустимо. Например, когда переменная сравнивается с заведомо известным значением:
bool foo();
double bar();
double val = foo() ? bar() : 0.0;
if (val == 0.0) { .... }
Анализатор не выдает предупреждений, если сравнивается одно и то же значение с самим собой. Такое сравнение позволяет определить, хранится ли в переменной NaN:
bool isnan(double value) { return value != value; }
Впрочем, более хорошим стилем будет использовать для этой проверки функцию 'std::isnan'.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Переменные следует определять в минимально возможной области видимости. Это позволит избежать возможных ошибок, связанных со случайным использованием переменных вне предусмотренной для них области видимости, а также может помочь сэкономить потребление памяти и увеличить производительность программы.
Пример кода, на который анализатор выдаст предупреждение:
static void RenderThrobber(RECT *rcItem, int *throbbing, ....)
{
....
int width = rcItem->right - rcItem->left;
....
if (*throbbing)
{
RECT rc;
rc.right = width;
....
}
.... // width больше нигде не используется
}
Переменная 'width' используется только внутри блока 'if', следовательно, будет разумным перенести переменную внутрь этого блока. Вычисление 'width' при этом будет происходить только при выполнении условия, что экономит время. Дополнительно это позволит избежать ошибочного использования этой переменной в дальнейшем.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор обнаружил опасное приведение числа к перечислению. Указанное число может не входит в диапазон значений enum.
Пример:
enum TestEnum { A, B, C };
TestEnum Invalid = (TestEnum)42;
Так как стандарт не указывает базовый тип для enum, то результат приведения числа, чьё значение не входит диапазон элементов enum, является unspecified behavior до C++17 и undefined behavior начиная с C++17.
Для того чтобы этого избежать, необходимо проверять числа перед приведениями. Как вариант, можно явно указать базовый тип для enum или использовать 'enum class', базовый тип которого по умолчанию - 'int'.
Корректный код:
enum TestEnum { A, B, C, Invalid = 42 };
Или:
enum TestEnum : int { A, B, C };
E Invalid = (E)42;
Или:
enum class TestEnum { A, B, C };
TestEnum Invalid = (TestEnum)42;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
При применении унарного минуса к переменной типа 'unsigned int', 'unsigned long' или 'unsigned long long' тип этой переменной не изменится и останется 'unsigned'. Таким образом, подобная операция является бессмысленной.
При применении унарного минуса к меньшему 'unsigned' целочисленному типу в результате целочисленного расширения возможно получение 'signed' числа, однако это не является хорошим подходом, и на такую операцию также будет выдано предупреждение.
Пример кода, на который анализатор выдаст предупреждение:
unsigned int x = 1U;
int y = -x;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Не рекомендуется использование декремента (--) или инкремента (++) в одном выражении с другими операторами. Использование этих операторов в выражении, содержащем сторонние побочные эффекты приводит к ухудшению читаемости кода, а также может привести к возникновению неопределенного поведения (undefined behaviour). Безопасным подходом будет использование рассматриваемых операторов в отдельных выражениях.
Пример кода, на который анализатор выдаст предупреждение:
i = ++i + i--;
Здесь происходит попытка изменить одну переменную в пределах одной точки следования. Это приводит к неопределенному поведению (undefined behaviour).
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
При побитовом сдвиге значение правого операнда должно находиться в диапазоне [0 .. N - 1], где N - количество бит, которое необходимо для представления левого операнда. Несоблюдение этого правила ведет к неопределенному поведению.
На следующий код будут выданы соответствующие предупреждения:
(int32_t) 1 << 128u;
(unsigned int64_t) 2 >> 128u;
int64_X >>= 64u;
any_var << -2u;
Рассмотрим пример из реального приложения, в котором происходит неопределенное поведение вследствие неверного побитового сдвига:
UINT32 m_color1_mask;
UINT32 m_color2_mask;
#define ARRAY_LENGTH(x) (sizeof(x) / sizeof(x[0]))
PALETTE_INIT( montecar )
{
static const UINT8 colortable_source[] =
{
0x00, 0x00, 0x00, 0x01,
0x00, 0x02, 0x00, 0x03,
0x03, 0x03, 0x03, 0x02,
0x03, 0x01, 0x03, 0x00,
0x00, 0x00, 0x02, 0x00,
0x02, 0x01, 0x02, 0x02,
0x00, 0x10, 0x20, 0x30,
0x00, 0x04, 0x08, 0x0c,
0x00, 0x44, 0x48, 0x4c,
0x00, 0x84, 0x88, 0x8c,
0x00, 0xc4, 0xc8, 0xcc
};
....
for (i = 0; i < ARRAY_LENGTH(colortable_source); i++)
{
UINT8 color = colortable_source[i];
if (color == 1)
state->m_color1_mask |= 1 << i; // <=
else if (color == 2)
state->m_color2_mask |= 1 << i; // <=
prom_to_palette(machine, i,
color_prom[0x100 + colortable_source[i]]);
}
....
}
В коде в цикле на i-ой итерации сдвигают единицу на i позиций влево. В результате цикла переменная 'i' будет принимать значения в диапазоне [0 .. 43], что больше допустимого диапазона, начиная с 32-ой итерации цикла (в случае, если int - 32-х битовый тип данных).
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Оператор 'sizeof()' не выполняет переданное ему выражение, а только вычисляет тип результирующего выражения и возвращает его размер на этапе компиляции. Следовательно, для любого кода внутри 'sizeof()' компилятор не генерирует ассемблерного кода (невыполняемые контекст), и ожидаемые операции внутри не будут произведены.
Поэтому операнд, передаваемый 'sizeof()', не должен содержать каких-либо сторонних эффектов, чтобы избежать потери операций.
Пример кода, на который анализатор выдаст предупреждение:
int x = ....;
....
size_t s = n * sizeof(x++);
Для получения ожидаемого эффекта нужно переписать код следующим образом:
int x = ....;
....
++x;
size_t s = n * sizeof(x);
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Анализатор обнаружил, что объект передается в функцию через указатель или ссылку, но не модифицируется в теле функции. Это может свидетельствовать о наличии ошибки. Также в случае, если такой аргумент функции действительно не изменяется, это делает сигнатуру функции неточной. Добавление константности объекту предотвращает потенциальные ошибки и делает определение функции более наглядным.
Пример кода, на который анализатор выдаст предупреждение:
size_t StringEval(std::string &str)
{
return str.size();
}
Здесь переменная 'str' используется только для чтения, хотя она и передается по неконстантной ссылке. Явная константность в сигнатуре функции позволила бы с первого взгляда понять, что функция не изменяет объект, а также предотвратить потенциальные ошибки при изменении самой функции.
Корректный вариант кода:
size_t StringEval(const std::string &str)
{
return str.size();
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Согласно стандарту C++ (C++17 § 8.5.9 п. 3) применение одного из операторов '-', '>', '>=', '<' или '<=' к двум указателям, которые не указывают на элементы одного и того же массива, приводит к неопределенному/неуточненному поведению (undefined/unspecified behaviour). Таким образом, если два указателя указывают на разные объекты массива, тогда эти объекты массива должны входить в один и тот же массив для того, чтобы сравнить их.
Пример кода, на который анализатор выдаст предупреждение:
int arr1[10];
int arr2[10];
int *pArr1 = arr1;
if (pArr1 < arr2)
{
....
}
Также следующий код является ошибочным:
int arr1[10];
int arr2[10];
int *pArr1 = &arr1[1];
int *pArr2 = &arr2[1];
int len = pArr1 - pArr2;
Подробнее о том, почему использование указателей может привести к ошибке можно прочитать в статье: "Указатели в C абстрактнее, чем может показаться".
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Использование присвоения в подвыражениях привносит дополнительный побочный эффект, что уменьшает читаемость кода и повышает шанс внесения новой ошибки в код.
Более того, следование данному правилу значительно снижает вероятность путаницы между операторами '=' и '=='.
Пример кода, на который анализатор выдаст предупреждения:
int Inc(int i)
{
return i += 1; // <=
}
void neg(int a, int b)
{
int c = a = b; // <=
Inc(a = 1); // <=
if(a = b) {} // <=
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Данное правило рекомендует не использовать адресную арифметику. Единственной формой адресной арифметики, допускаемой к использованию данным правилом, является операция индексирования ('[]'), примененная к сущности, объявленной как массив.
Исключение: допускается использовать инкремент и декремент ('++' и '--').
Использование адресной арифметики усложняет чтение программы и может являться причиной неправильного понимания написанного когда. С другой стороны, использование индексирования является явным и легко читаемым, и даже если при его использовании будет внесена ошибка, её будет легче обнаружить. То же касается и операций инкремента/декремента: они явно дают понять, что мы последовательно обходим область памяти, представляющую собой непрерывную область данных.
Пример нарушения правила:
int arr[] = { 0, 1, 2 };
int *p = arr + 1; //+V2563
p += 1; //+V2563
int *q = p[1]; //+V2563
Корректный вариант кода:
int arr[] = { 0, 1, 2 };
int *p = &arr[1];
++p;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
В коде не должно быть неявных преобразований значений вещественного типа в целочисленный и наоборот.
Под целочисленными типами подразумеваются:
Под типами с плавающей точкой подразумеваются:
При неявном преобразовании значений вещественных типов в целые может быть утеряна информация (например, дробная часть), а также возможно возникновение неопределенного поведения, если значение вещественного типа не может быть представлено целым типом.
Неявное преобразование значения целого типа в вещественный может привести к его неточному представлению, которое не соответствует ожиданиям разработчика.
Пример кода, на который анализатор выдаст предупреждения:
void foo1(int x, float y);
void foo2()
{
float y = 10;
int x = 10.5;
foo1(y, x);
}
Пример правильного с точки зрения этой диагностики кода:
void foo1(int x, float y);
void foo2()
{
float y = static_cast<float>(10);
int x = static_cast<int>(10.5);
foo1(static_cast<int>(y), static_cast<float>(x));
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Функции не должны вызывать себя ни напрямую, ни косвенно. Рекурсия может привести к сложно отлавливаемым ошибкам. Одной из них может быть переполнение стека при очень глубокой рекурсии.
Пример кода, на который анализатор выдаст предупреждения:
#include <stdint.h>
uint64_t factorial(uint64_t n)
{
return n > 1 ? n * factorial(n - 1) : 1;
}
По возможности, стоит заменить рекурсивный вызов циклом. Вот как это можно сделать с предыдущим примером:
#include <stdint.h>
uint64_t factorial(uint64_t n)
{
uint64_t result = 1;
for (; n > 1; --n)
{
result *= n;
}
return result;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Согласно стандарту языка C++ в выражениях беззнаковых типов при переполнении происходит перенос значений относительно нуля. Применение этого свойства беззнаковых типов при вычислениях на этапе выполнения является известной практикой (в отличие от знаковых типов, для которых переполнение ведет к неопределенному поведению).
Однако, перенос значений относительно нуля для выражений, вычисленных на этапе компиляции, может сбивать с толку.
Пример кода, на который анализатор выдаст предупреждения:
#include <stdint.h>
#define C1 (UINT_MAX)
#define C2 (UINT_MIN)
....
void foo(unsigned x)
{
switch(x)
{
case C1 + 1U: ....; break;
case C2 - 1U: ....; break;
}
}
Если в константном выражении беззнакового типа происходит перенос значения относительно нуля, то согласно текущему правилу он может трактовать не как ошибка в случае, если выражение никогда не будет вычислено:
#include <stdint.h>
#define C UINT_MAX
....
unsigned foo(unsigned x)
{
if(x < 0 && (C + 1U) == 0x42) ....;
return x + C;
}
Выражение '(C + 1U)', содержащее переполнение не будет выполнено, т.к. выражение 'x < 0' всегда истинно. Следовательно, второй операнд логического выражения не будет вычислен.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Удаление 'const' / 'volatile' квалификаторов может привести к неопределённому поведению.
Компилятор вправе оптимизировать код в случае возникновения неопределённого поведения. Например, в следующем коде компилятор может сделать цикл бесконечным:
inline int foo(bool &flag)
{
while (flag)
{
// do some stuff...
}
return 0;
}
int main()
{
volatile bool flag = true;
return foo(const_cast<bool &>(flag));
}
Другой пример кода, на который анализатор выдаст предупреждения:
void my_swap(const int *x, volatile int *y)
{
auto _x = const_cast<int*>(x);
auto _y = const_cast<int*>(y);
swap(_x, _y);
}
void foo()
{
const int x = 30;
volatile int y = 203;
my_swap(&x, &y); // <=
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Встроенные операторы '&&', '||', '&' (взятие адреса) и ',' имеют определенный порядок вычисления и семантику. После перегрузки этих функций разработчик может не знать о потере особенностей этих операторов.
1) После перегрузки логических операторов теряется сокращенное вычисление. Для встроенных операторов, если первый операнд '&&' оказался 'false' или первый операнд '||' оказался 'true', вычисление второго операнда не происходит. В результате может быть потеряна часть оптимизаций:
class Tribool
{
public:
Tribool(bool b) : .... { .... }
friend Tribool operator&&(Tribool lhs, Tribool rhs) { .... }
friend Tribool operator||(Tribool lhs, Tribool rhs) { .... }
....
};
// Do some heavy weight stuff
bool HeavyWeightFunction();
void foo()
{
Tribool flag = ....;
if (flag || HeavyWeightFunction()) // evaluate all operands
// no short-circuit evaluation
{
// Do some stuff
}
}
Здесь компилятор не сможет произвести оптимизацию, и произойдет "тяжеловесной" функции, чего бы не произошло для встроенного оператора.
2) Перегрузка унарного оператора взятия адреса '&' также может привести к неочевидной ситуации. Рассмотрим пример:
// Example.h
class Example
{
public:
Example* operator&() ;
const Example* operator&() const;
};
// Foo.cc
#include "Example.h"
void foo(Example &x)
{
&x; // call overloaded "operator&"
}
// Bar.cc
class Foobar;
void bar(Example &x)
{
&x; // may call built-in or overloaded "operator&"!
}
Во втором случае, согласно стандарту С++ ($8.3.1.5) такое поведение является неуточненным, и получение адреса объекта 'x' может вызвать как встроенный оператор, так и перегруженный.
3) Встроенный оператор "запятая" выполняет левый операнд и игнорирует результат, затем вычисляет правый операнд и возвращает результат правого операнда. Также, встроенный оператор гарантирует, что все побочные эффекты левого операнда будут выполнены до того, как начнется выполнение второго операнда.
Перегруженный оператор не дает эту гарантию (до стандарта C++17), поэтому нижеприведенный код может напечатать как 'foobar', так и 'barfoo':
#include <iostream>
template <typename T1, typename T2>
T2& operator,(const T1 &lhs, T2 &rhs)
{
return rhs;
}
int main()
{
std::cout << "foo", std::cout << "bar";
return 0;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Использование логических операторов '!', '&&' и '||' с переменными, имеющими отличный от 'bool' тип, не имеет смысла, вряд ли предназначено для этого и может указывать на присутствие ошибки. Возможно, должны были использоваться побитовые операторы ('&', '|' или '~').
Пример кода, на который анализатор выдаст предупреждение:
void Foo(int x, int y, int z)
{
if ((x + y) && z)
{
....
}
}
void Bar(int *x)
{
if (!x)
{
....
}
}
Примеры правильного кода с точки зрения правила:
void Foo(int x, int y, int z)
{
if ((x + y) & z)
{
....
}
}
void Foo(int x, int y, int z)
{
if ((x < y) && (y < z))
{
....
}
}
void Bar(int *x)
{
if (x == NULL)
{
....
}
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Приведения между указателями на объект и целочисленными типами могут послужить причиной неопределенного, неуточненного или зависимого от реализации поведения. В связи с этим, рекомендуется не использовать подобные приведения.
Пример нарушения 1:
struct S { int16_t i; int16_t j; } *ps = ....;
int i64 = reinterpret_cast<int>(ps);
Пример нарушения 2:
void foo(int i) {}
void wrong_param_type()
{
char *pc = ....;
foo((int) pc);
}
Пример нарушения 3:
int wrong_return_type()
{
double *pd = ....;
return (int) pd;
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Согласно стандарту C++ идентификаторы и имена макросов, содержащие в себе '__', либо начинающиеся на '_[A-Z]', зарезервированы для использования в реализации языка и стандартной библиотеки. В языке C тоже есть такое правило, но '__' должно быть в начале идентификатора.
Объявление таких идентификаторов снаружи стандартной библиотеки может привести к проблемам. К примеру, код ниже может изменить поведение функции 'abs':
#define _BUILTIN_abs(x) (x < 0 ? -x : x)
#include <cmath>
int foo(int x, int y, bool c)
{
return abs(c ? x : y)
}
если функция 'abs' реализована через использование встроенной функции компилятора (интринсик) следующим образом:
#define abs(x) (_BUILTIN_abs(x))
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Функция, объявленная в области видимости блока, будет видна также в пространстве имён, обрамляющем блок.
Пример кода:
namespace Foo
{
void func()
{
void bar(); // <=
bar();
}
}
void Foo::bar() // Function 'bar' is visible here
{
}
Программист хотел сузить область видимости функции, задекларировав ее в блоке функции 'func'. Однако функция 'bar' видна также за пределами пространства имен 'Foo'. Поэтому следует задекларировать функцию явно в обрамляющем пространстве имен:
namespace Foo
{
void bar();
void func()
{
bar();
}
}
void Foo::bar() // Function 'bar' is visible
{
}
Также, вследствие неоднозначности грамматики языка C++, декларация функции может выглядеть как декларация объекта:
struct A
{
void foo();
};
int main()
{
A a();
a.foo(); // compile-time error
}
Данная проблема известна как "Most vexing parse": компилятор разрешает данную неоднозначность "задекларировать функцию или объект" в пользу "задекларировать функцию". Поэтому, несмотря на ожидание программиста задекларировать объект класса 'A' и вызвать нестатическую функцию-член класса 'A::foo', компилятор воспримет это как объявление функции 'a', не принимающей параметров и возвращающей тип 'A'.
Чтобы избежать путаницы, анализатор также предупреждает о таких объявлениях.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Объявления в глобальном пространстве, засоряют список доступных идентификаторов. Новые идентификаторы, добавленные в область видимости блока, могут быть схожими с идентификаторами в глобальном пространстве. Это может запутать разработчика и привести к ошибочному выбору идентификатора.
Чтобы гарантировать ожидания разработчика, все идентификаторы должны располагаться внутри соответствующих пространств имен.
Пример кода, на который анализатор выдает предупреждение:
int x1;
void foo();
В соответствии с правилом данный код должен выглядеть следующим образом:
namespace N1
{
int x1;
void foo();
}
Также допустимый вариант c extern "C" может выглядеть так:
extern "C"
{
int x1;
}
extern "C" void bar();
Согласно стандарту AUTOSAR разрешено использовать 'typedef' в глобальном пространстве имен в том случае, если имя псевдонима типа содержит в себе размер его итогового типа.
Пример кода, на который анализатор не выдает предупреждений:
typedef short int16_t;
typedef int INT32;
typedef unsigned long long Uint64;
Пример кода, на который анализатор выдает предупреждения:
typedef std::map<std::string, std::string> TestData;
typedef int type1;
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Функция 'main' должна присутствовать только в глобальном пространстве имен, чтобы разработчик точно понимал, что если она есть, то всегда является точкой входа в программу.
Пример кода, на который анализатор выдает предупреждение:
namespace N1
{
int main();
}
Другой пример со срабатыванием анализатора:
namespace
{
int main();
}
Код, переписанный в соответствии с правилом, может выглядеть так:
namespace N1
{
int start();
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Передача массива в функцию по указателю ведет к потери размера массива. В результате функция может принять в качестве аргумента массив с меньшим количеством элементов, чем ожидается, и в процессе выполнения выйти за его границы, что приведет к неопределенному поведению.
Чтобы избежать потери информации о размере массива, его стоит передавать только по ссылке. В случае, когда функция должна обрабатывать массивы разной длины, следует использовать класс для инкапсуляции массива и его размера.
Пример кода, не соответствующего правилу:
void foo(int *ptr);
void bar(int arr[5])
void bar(const char chars[30]);
int main
{
int array[5] = { 1, 2, 3 };
foo(array);
bar(array);
}
Допустимый вариант:
Void bar(int (&arr)[5]);
int main
{
int array[7] = { 1, 2, 3, 4, 5 };
bar(array);
}
Другой пример кода, несоответствующий правилу:
void bar(const char chars[30]);
int main()
{
bar("something"); //const char[10]
}
Допустимый вариант с использованием класса для инкапсуляции:
template <typename T>
class ArrayView
{
T *m_ptr;
size_t m_size;
public:
template <size_t N>
ArrayView(T (&arr)[N]) noexcept: m_ptr(arr), m_size(N) {}
// ....
};
void bar(ArrayView<const char> arr);
int main()
{
bar("something");
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Приведение указателя на функцию к любому другому указателю ведет к неопределенному поведению. Приведение типов между указателями на функцию несоответствующего типа также станет причиной неопределенного поведения при вызове этой функции.
Пример кода с нарушениями правила, в котором все четыре приведения типа являются недопустимыми:
void foo(int32_t x);
typedef void (*fp)(int16_t x);
void bar()
{
fp fp1 = reinterpret_cast<fp>(&foo);
fp fp2 = (fp)foo;
void* vp = reinterpret_cast<void*>(fp1);
char* chp = (char*)fp1;
}
Вызов функции через такие указатели может потенциально привести к ошибкам сегментации.
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Функции стандартной библиотеки из заголовочных файлов <signal.h> / <csignal> могут быть опасны. Их поведение зависит от реализации, а их использование может привести к неопределенному поведению.
Причиной неопределенного поведения, к примеру, является использование обработчиков сигналов в многопоточной программе. С другими причинами можно ознакомиться здесь.
Анализатор выдаст предупреждение, если обнаружит использование следующих функций:
Пример кода, на который анализатор выдает предупреждение:
#include <csignal>
void handler(int sig) { .... }
void foo()
{
signal(SIGINT, handler);
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Функции стандартной библиотеки из заголовочных файлов '<stdio.h>' / '<cstdio>' и '<wchar.h>' могут быть опасны. Их поведение зависит от реализации, а их использование может привести к неопределенному поведению.
Рассмотрим следующий фрагмент кода:
#include <stdio.h>
void InputFromFile(FILE *file); // Read from 'file'
void foo()
{
FILE *stream;
....
InputFromFile(stream);
fflush(stream);
}
В коде сначала происходит чтение из потока 'stream', а затем поток передается в функцию 'fflush'. Такая последовательность операций приводит к неопределенному поведению.
Анализатор выдаст предупреждение, если обнаружит использование любых функций, определенных в заголовочных файлах '<stdio.h>' / '<cstdio>' и '<wchar.h>':
Пример кода, на который анализатор выдаст предупреждение:
#include <stdio.h>
void foo(const char *filename, FILE *oldFd)
{
FILE *newFd = freopen(filename, "r", oldFd);
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило основано на руководстве AUTOSAR (AUTomotive Open System ARchitecture) по разработке программного обеспечения.
Функция, объявленная однажды с 'internal linkage', при повторном объявлении или определении будет также иметь 'internal linkage'. Это может быть неочевидно для разработчика, и поэтому следует явно указывать спецификатор 'static' в каждом объявлении и определении.
Следующий код не соответствует правилу, так как определение не отражает тип связывания, заданный в объявлении функции 'foo'.
static void foo(int x);
void foo(int x)
{
....
}
Код в соответствии с правилом должен быть следующим:
static void foo(int x);
static void foo(int x)
{
....
}
В примере, приведенном ниже, определение функции 'foo' со спецификатором класса хранения 'extern' не задает 'external linkage', как могло показаться. Тип связывания остается 'internal linkage':
static void foo(int x);
extern void foo(int x)
{
....
}
Правильный вариант:
extern void foo(int x);
extern void foo(int x)
{
....
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил фрагмент кода, в котором возможно пропущена точка с запятой ';'.
Пример кода, который приводит к выдаче диагностического сообщения V5001:
void Foo();
void Foo2(int *ptr)
{
if (ptr == NULL)
return
Foo();
...
}
В данном коде планировалось завершить работу функции, если указатель ptr == NULL. Однако, после оператора return забыта точка с запятой ';', что приводит к вызову функции Foo(). Функция Foo() и Foo2() ничего не возвращают и поэтому данный код компилируется без ошибок и предупреждений.
Скорее всего, программист планировал написать:
void Foo();
void Foo2(int *ptr)
{
if (ptr == NULL)
return;
Foo();
...
}
Если изначальный код все-таки корректен, то его лучше переписать следующим образом:
void Foo2(int *ptr)
{
if (ptr == NULL)
{
Foo();
return;
}
...
}
Анализатор считает код безопасным, если отсутствует оператор "if" или вызов функции находится на той же строке, что и оператор "return". Такой код достаточно часто можно встретить в программах. Примеры безопасного кода:
void CPagerCtrl::RecalcSize()
{
return
(void)::SendMessageW((m_hWnd), (0x1400 + 2), 0, 0);
}
void Trace(unsigned int n, std::string const &s)
{ if (n) return TraceImpl(n, s); Trace0(s); }
Данная диагностика классифицируется как:
|
Примечание. Данное диагностическое правило применимо только к языку C++.
Анализатор обнаружил обработчик исключения, который ничего не делает.
Пример кода:
try {
...
}
catch (MyExcept &)
{
}
Конечно, такой код вовсе не обязательно ошибочный. Но очень странно просто подавлять исключение, ничего не делая. Такая обработка исключений может скрывать дефекты в программе и усложнить тестирование программы.
Следует реагировать на исключения, например, вписать 'assert(false)':
try {
...
}
catch (MyExcept &)
{
assert(false);
}
Иногда подобные конструкции используют, чтобы вернуть управление из множества вложенных циклов или рекурсивных функций. Но исключения — очень ресурсоёмкая операция и их следует использовать по назначению, а именно при возникновении нештатных ситуаций, которые должны быть обработаны на более высоком уровне.
Единственное место, где допустимо просто подавлять исключения — это деструкторы. Деструктор не должен бросать исключений. Однако в деструкторах часто непонятно, что делать с исключениями, и обработчик вполне может быть пуст. Анализатор не предупреждает о пустых обработчиках внутри деструкторов:
CClass::~ CClass()
{
try {
DangerousFreeResource();
}
catch (...) {
}
}
Данная диагностика классифицируется как:
Примечание. Данное диагностическое правило применимо только к языку C++.
Анализатор обнаружил потенциальную ошибку, связанную с использованием класса 'std::exception' или наследуемого от него класса. Анализатор выдаёт предупреждение в том случае, если создаётся объект типа 'std::exception' / 'CException', но не используется.
Пример:
if (name.empty())
std::logic_error("Name mustn't be empty");
Ошибка заключается в том, что случайно забыто ключевое слово 'throw'. В результате данный код не генерирует исключение в случае ошибочной ситуации. Исправленный вариант кода:
if (name.empty())
throw std::logic_error("Name mustn't be empty");
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку в выражении, содержащем операцию сдвига. В программе выполняется сдвиг 32-битного значения. Затем полученный 32-битный результат явно или неявно преобразуется в 64-битный тип.
Рассмотрим пример некорректного кода:
unsigned __int64 X;
X = 1u << N;
Данный код вызывает неопределенное поведение, если значение N больше 32. На практике это означает, что с помощью этого кода не получится записать в переменную 'X' значение более 0x80000000.
Код можно исправить, если тип левого аргумента будет 64-битным.
Исправленный вариант кода:
unsigned __int64 X;
X = 1ui64 << N;
Анализатор не будет выдавать предупреждение, если результат выражения со сдвигом укладывается в 32-битный тип. Это означает, что значащие биты не потеряны и код корректен.
Пример безопасного кода:
char W = 7;
long long Q = W << 10;
Этот код работает следующим образом. Вначале переменная 'W' расширяется до 32-битного типа 'int'. Затем происходит сдвиг и получается значение 0x00001C00. Это число помещается в 32-битный тип, а значит ошибки не возникнет. На последнем этапе это значение расширяется до 64-битного типа 'long long' и записывается в переменную 'Q'.
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с переполнением.
Выполняются следующие действия:
Если при вычитании возможно переполнение, то результат проверки может отличаться от ожиданий программиста.
Рассмотрим наиболее простой случай:
unsigned A = ...;
int B = ...;
if (A - B > 1)
Array[A - B] = 'x';
Такая проверка по замыслу программиста должна защитить от выхода за границу массива. Однако эта проверка не поможет, если 'A < B'.
Пусть: A = 3, B = 5;
Тогда: 0x00000003u - 0x00000005i = FFFFFFFEu
Выражение 'A – B', согласно правилам языков C и C++, имеет тип 'unsigned int'. Значит, 'A – B' будет равно FFFFFFFEu. Это число больше единицы. В результате произойдёт обращение к памяти за границей массива.
Исправить код можно двумя способами. Во-первых, вычисления можно производить в переменных знаковых типов:
intptr_t A = ...;
intptr_t B = ...;
if (A - B > 1)
Array[A - B] = 'x';
Во-вторых, можно изменить условие. Как именно следует изменить условие, зависит от желаемого результата и входных значений. Если 'B >= 0', то достаточно будет написать:
unsigned A = ...;
int B = ...;
if (A > B + 1)
Array[A - B] = 'x';
Если код корректен, то можно отключить вывод диагностического сообщения в данной строке, используя комментарий '//-V5005'.
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку в выражении, где используются операции сдвига (shift operations). В процессе сдвига возникает переполнение, и значения старших бит будет потеряно.
Для начала рассмотрим эту ситуацию на простом примере:
std::cout << (77u << 26);
Значение выражения "77u << 26" равно 5167382528 (0x134000000). При этом выражение "77u << 26" имеет тип 'unsigned int'. Это значит, что старшие биты будут отброшены, и на экран будет напечатано значение 872415232 (0x34000000).
Переполнения, возникающие при сдвигах, часто указывают на наличие логической ошибки или просто опечатки. Например, может быть, что число '77u' хотели задать в восьмеричной системе счисления. Тогда корректный код должен выглядеть так:
std::cout << (077u << 26);
Здесь переполнения уже не возникает. Значение выражения "077u << 26" равно 4227858432 (0xFC000000).
Если хочется распечатать на экране значение 5167382528, то число 77 должно быть задано с помощью 64-битного типа. Например, так:
std::cout << (77ui64 << 26);
Перейдём к случаям, с которыми можно встретиться на практике. Для этого рассмотрим две ошибки, обнаруженных в реальных приложениях.
Первый пример.
typedef __UINT64 Ipp64u;
#define MAX_SAD 0x07FFFFFF
....
Ipp64u uSmallestSAD;
uSmallestSAD = ((Ipp64u)(MAX_SAD<<8));
Программист хотел записать в 64-битную переменную uSmallestSAD значение 0x7FFFFFF00. Но на самом деле переменная будет иметь значение 0xFFFFFF00. Старшие биты будут отброшены, так как выражение MAX_SAD<<8 имеет тип 'int'. Программист знал про это, поэтому решил использовать явное приведение типа. Но, к сожалению, ошибся, расставляя скобки. Этот пример хорошо демонстрируют, что подобные ошибки могут возникать из-за простой опечатки. Корректный код:
uSmallestSAD = ((Ipp64u)(MAX_SAD))<<8;
Второй пример.
#define MAKE_HRESULT(sev,fac,code) \
((HRESULT) \
(((unsigned long)(sev)<<31) | \
((unsigned long)(fac)<<16) | \
((unsigned long)(code))) )
*hrCode = MAKE_HRESULT(3, FACILITY_ITF, messageID);
Функция должна сформировать информацию об ошибке в переменной типа HRESULT. Для этого используется макрос MAKE_HRESULT. Но используется он неправильно. Программист посчитал, что первый параметр 'severity' может лежать в приделах от 0 до 3. Видимо он перепутал это со способом формирования кодов ошибки, используемые при работе с функциями GetLastError()/SetLastError().
Макрос MAKE_HRESULT в качестве первого аргумента может принимать только 0 (success) или 1 (failure). Подробнее этот вопрос рассматривался на форуме сайта CodeGuru: Warning! MAKE_HRESULT macro doesn't work.
Так как в качестве первого фактического аргумента используется число 3, то возникает переполнение. Число 3 "превратится" в 1. Благодаря этой случайности ошибка не повлияет на работу программы. Однако мы специально привели этот пример. Хотелось показать, что нередко, код работает исключительно благодаря везению, а не потому что написан правильно.
Правильный код:
*hrCode = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, messageID);
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку в цикле. Возможно, из-за опечатки увеличивается/уменьшается не та переменная.
Пример подозрительного кода:
void Foo(float *Array, size_t n)
{
for (size_t i = 0; i != n; ++n)
{
....
}
}
Вместо переменной 'i' увеличивается переменная 'n'. В результате, программа ведёт себя не так, как ожидал программист.
Исправленный вариант кода:
for (size_t i = 0; i != n; ++i)
Данная диагностика классифицируется как:
|
Примечание. Данное диагностическое правило применимо только к языку C++.
Анализатор обнаружил класс, унаследованный от класса 'std::exception' (или аналогичных классов) через модификатор 'private' или 'protected'. Данное наследование опасно тем, что при непубличном наследовании при попытке поймать исключение 'std::exception', оно будет пропущено.
Ошибка часто возникает из-за того, что забывают указать тип наследования. В соответствии с правилами языка, наследование по умолчанию приватное. Как результат, обработчики исключений ведут себя не так, как задумывалось.
В качестве примера можно привести следующий некорректный код:
class my_exception_t : std::exception // <=
{
public:
explicit my_exception_t() { }
virtual const int getErrorCode() const throw() { return 42; }
};
....
try
{ throw my_exception_t(); }
catch (const std::exception & error)
{ /* Can't get there */ }
catch (...)
{ /* This code executed instead */ }
Код, который должен будет перехватывать все стандартные и пользовательские исключения (catch (const std::exception & error)), не сможет отработать правильно, потому что приватное наследование исключает неявное преобразование типов.
Для того, чтобы код работал корректно, требуется добавить перед родительским классом 'std::exception' в списке базовых классов модификатор 'public':
class my_exception_t : public std::exception
{
....
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил использование данных, полученных извне, без предварительной проверки. Такой сценарий излишнего доверия может привести к различным негативным последствиям, в том числе - стать причиной уязвимостей.
На данный момент диагностическое правило V5009 выявляет ошибки по нескольким паттернам:
Рассмотрим все паттерны более подробно.
Пример подозрительного кода при использовании в индексе недостоверных данных.
size_t index = 0;
....
if (scanf("%zu", &index) == 1)
{
....
DoSomething(arr[index]); // <=
}
Данный код может привести к доступу за границу массива 'arr' в случае, если пользователем будет введено значение отрицательное или превышающее максимально допустимый индекс массива 'arr'.
Безопасный код проверяет полученное значение:
if (index < ArraySize)
DoSomething(arr[index]);
Пример подозрительного кода при использовании недостоверных данных в аргументе функции.
char buf[1024];
char username [256];
....
if (scanf("%255s", username) == 1)
{
if (snprintf(buf, sizeof(buf) - 1, commandFormat, username) > 0)
{
int exitCode = system(buf); // <=
....
}
....
}
Этот код является уязвимым, т.к. пользовательский ввод передаётся командному интерпретатору без проверки полученных данных. Например, введя "&cmd", на Windows можно получить доступ к командному интерпретатору.
Правильный код должен осуществлять дополнительную проверку считанных данных:
if (IsValid(username))
{
if (snprintf(buf, sizeof(buf) - 1, commandFormat, username) > 0)
{
int exitCode = system(buf);
....
}
....
}
else
{
printf("Invalid username: %s", username);
....
}
Пример подозрительного кода, связанного с порчей указателя.
size_t offset = 0;
int *pArr = arr;
....
if (scanf("%zu", &offset) == 1)
{
pArr += offset; // <=
....
DoSomething(pArr);
}
В данном случае портится значение указателя 'pArr', т.к. в результате прибавления непроверенного значения 'offset' указатель может начать ссылаться за пределы массива. В результате можно испортить какие-то данные (на которые будет ссылаться 'pArr') с непредсказуемыми последствиями.
Правильный код проверяет допустимое смещение:
if (offset <= allowableOffset)
{
pArr += offset;
....
DoSomething(pArr);
}
Пример подозрительного кода с делением на недостоверные данные:
if (fscanf(stdin, "%zu", &denominator) == 1)
{
targetVal /= denominator;
}
Этот код может привести к делению на 0, если соответствующее значение будет введено пользователем.
Корректный код выполняет проверку допустимости значений:
if (fscanf(stdin, "%zu", &denominator) == 1)
{
if (denominator > MinDenominator && denominator < MaxDenominator)
{
targetVal /= denominator;
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил возможное переполнение знаковой переменной в цикле. Переполнение знаковых переменных приводит к неопределённому поведению.
Пример:
int checksum = 0;
for (....) {
checksum += ....;
}
Перед нами абстрактный алгоритм подсчёта контрольной суммы. Алгоритм подразумевает возможность переполнения переменной 'checksum'. Однако переменная имеет знаковый тип, а, следовательно, её переполнение приводит к неопределённому поведению. Код некорректен и должен быть модифицирован.
Следует использовать беззнаковые типы, семантика переполнения которых определена.
Корректный код:
unsigned checksum = 0;
for (....) {
checksum += ...
}
Некоторые программисты считают, что в переполнении знаковых переменных ничего страшного нет и они могут предсказать, как программа будет работать. Это не так. Может происходить всё что угодно.
Давайте рассмотрим, как могут проявляться ошибки этого типа на практике. Разработчик на форуме жалуется, что GCC глючит и неправильно компилирует его код в режиме оптимизации. Он приводит код следующей функции для подсчёта хеша строки:
int foo(const unsigned char *s)
{
int r = 0;
while(*s) {
r += ((r * 20891 + *s *200) | *s ^ 4 | *s ^ 3) ^ (r >> 1);
s++;
}
return r & 0x7fffffff;
}
Претензия разработчика в том, что компилятор не генерирует код для оператора побитового И (&). Из-за этого функция возвращает отрицательные значения, хотя не должна.
Разработчик считает, что это глюк в компиляторе. Но это не так, неправ программист, который написал такой код. Функция работает неправильно из-за того, что в ней возникает неопределённое поведение.
Компилятор видит, что в переменной 'r' считается некоторая сумма. Переполнения переменной 'r' произойти не должно. Иначе это неопределённое поведение, которое компилятор никак не должен рассматривать и учитывать. Итак, компилятор считает, что раз переменная 'r' после окончания цикла не может быть отрицательной, то операция 'r & 0x7fffffff' для сброса знакового бита является лишней и компилятор просто возвращает из функции значение переменной 'r'.
Диагностика V5010 как раз и предназначена для выявления подобных ошибок. Чтобы исправить код, достаточно считать хеш, используя для этого беззнаковую переменную.
Исправленный вариант кода:
int foo(const unsigned char *s)
{
unsigned r = 0;
while(*s) {
r += ((r * 20891 + *s *200) | *s ^ 4 | *s ^ 3) ^ (r >> 1);
s++;
}
return (int)(r & 0x7fffffff);
}
Дополнительные ссылки:
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительное приведение типов. Результат бинарной операции над 32-битными числами приводят к 64-битному типу.
Пример кода с ошибкой:
unsigned a;
unsigned b;
....
uint64_t c = (uint64_t)(a * b);
Такое преобразование избыточно. Тип 'unsigned' и так бы автоматически расширился до типа 'uint64_t' при присваивании.
Скорее всего, программист хотел защититься от переполнения, но не достиг цели. При перемножении переменных типа 'unsigned' всё равно произойдёт переполнение, и только уже бессмысленный результат умножения будет явно расширен до типа 'uint64_t'.
Следовало привести один из аргументов к этому типу, чтобы избежать переполнения. Корректный код:
uint64_t c = (uint64_t)a * b;
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с небезопасным использованием паттерна "блокировки с двойной проверкой" (double checked locking). Блокировка с двойной проверкой - это паттерн, предназначенный для уменьшения накладных расходов получения блокировки. Сначала проверяется условие блокировки без синхронизации. И только если условие выполняется, поток попытается получить блокировку. Таким образом, блокировка будет выполнена только если она действительно была необходима.
Рассмотрим пример с ошибкой:
static std::mutex mtx;
class TestClass
{
public:
void Initialize()
{
if (!initialized)
{
std::lock_guard lock(mtx);
if (!initialized) // <=
{
resource = new SomeType();
initialized = true;
}
}
}
/* .... */
private:
bool initialized = false;
SomeType *resource = nullptr;
};
}
В этом примере, оптимизация компилятором порядка назначений переменных 'resource' и 'initialized' может привести к ошибке. Т.е. в начале переменной 'initialized' будет присвоено значение 'true', а уже потом будет выделена память под объект типа 'SomeType' и проинициализирована переменная 'resource'.
Такая перестановка может привести к ошибке при доступе к объекту из параллельного потока. Получается, что переменная 'resource' будет еще не проинициализирована, а флаг 'intialized' уже будет выставлен в 'true'.
Одна из опасностей таких ошибок состоит в том, что часто кажется, будто программа работает корректно. Это происходит из-за того, что рассмотренная ситуация будет возникать не очень часто, в зависимости от архитектуры используемого процессора.
Дополнительные ссылки:
Данная диагностика классифицируется как:
|
Анализатор обнаружил в коде данные, которые могут являться конфиденциальными. В качестве таких данных могут выступать учётные данные.
Хранение учетных данных в исходном коде может привести к нарушению контроля доступа к данным и возможностям, не предназначенным для публичного использования. Имея доступ к сборке, злоумышленник при помощи дизассемблера сможет увидеть все строковые литералы, которые в ней используются. В случае open-source проектов всё ещё проще, так как злоумышленник может изучать непосредственно исходный код.
Таким образом, все секретные данные могут оказаться публично доступными. Уязвимости, связанные с недостаточной защищённостью конфиденциальных данных, выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A2:2017-Broken Authentication.
Рассмотрим пример:
bool LoginAsAdmin(const std::string &userName,
const std::string &password)
{
if (userName == "admin" && password == "sRbHG$a%")
{
....
return true;
}
return false;
}
В указанном примере пароль, используемый для входа в систему с правами администратора, хранится в коде. И злоумышленник может легко получить данные для авторизации, а, следовательно, все возможности администратора системы.
Вместо хранения секретных данных в коде лучше использовать, например, классы-хранилища, в которых данные будут храниться в зашифрованном виде и к которым у обычных пользователей нет прямого доступа. В таком случае код может выглядеть, например, так:
bool LoginAsAdmin(const DataStorage &secretStorage,
const std::string &userName,
const std::string &password)
{
var adminData = secretStorage.GetAdminData();
if ( userName == adminData.UserName
&& password == adminData.Password)
{
....
return true;
}
return false;
}
Данная диагностика классифицируется как:
Анализатор обнаружил вызов устаревшей криптографической функции. Использование такой функции может повлечь за собой проблемы с безопасностью.
Рассмотрим следующий пример:
BOOL ImportKey(HCRYPTPROV hProv, LPBYTE pbKeyBlob, DWORD dwBlobLen)
{
HCRYPTKEY hPubKey;
if (!CryptImportKey(hProv, pbKeyBlob, dwBlobLen, 0, 0, &hPubKey))
{
return FALSE;
}
if (!CryptDestroyKey(hPubKey))
{
return FALSE;
}
return TRUE;
}
Согласно документации Microsoft функции 'CryptoImportKey' и 'CryptoDestroyKey' устарели. Их следует заменить на безопасные аналоги из Cryptography Next Generation ('BCryptoImportKey' и 'BCryptoDestroyKey'):
BOOL ImportKey(BCRYPT_ALG_HANDLE hAlgorithm,
BCRYPT_ALG_HANDLE hImportKey,
BCRYPT_KEY_HANDLE* phKey,
PUCHAR pbInput,
ULONG cbInput,
ULONG dwFlags)
{
if (!BCryptImportKey(
hAlgorithm,
hImportKey,
BCRYPT_AES_WRAP_KEY_BLOB,
phKey,
NULL,
0,
pbInput,
cbInput,
dwFlags))
{
return FALSE;
}
if (!BCryptDestroyKey(phKey))
{
return FALSE;
}
return TRUE;
}
Данное диагностическое правило применяется к устаревшим криптографическим функциям Windows API, Linux Kernel Crypto API и GnuPG Made Easy.
Если вам необходимо самостоятельно разметить нежелательные функции, то вы можете воспользоваться механизмом аннотирования функций и диагностическим правилом V2016.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил в коде данные, которые могут являться конфиденциальными.
В качестве таких данных могут выступать, например, пароли. Их хранение в исходном коде может привести к нарушению контроля доступа к данным и возможностям, не предназначенным для публичного использования. Имея доступ к сборке, любой пользователь сможет увидеть все строковые литералы, которые в ней используются. Для этого достаточно рассмотреть метаданные или изучить IL-код, используя утилиту ildasm. В случае open-source проектов всё ещё проще, так как злоумышленник может изучать непосредственно исходный код.
Таким образом, все секретные данные могут оказаться публично доступными. Уязвимости, связанные с недостаточной защищённостью конфиденциальных данных, выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A2:2017-Broken Authentication.
Рассмотрим пример:
bool LoginAsAdmin(string userName, string password)
{
if (userName == "admin" && password == "sRbHG$a%")
{
....
return true;
}
return false;
}
В указанном примере пароль, используемый для входа в систему с правами администратора, хранится в коде. Изучив метаданные сборки или IL-код, злоумышленник может легко получить данные для авторизации, а следовательно - все возможности администратора системы.
Вместо хранения секретных данных в коде лучше использовать, например, хранилища, в которых данные будут храниться в зашифрованном виде и к которым у обычных пользователей нет прямого доступа. В таком случае код может выглядеть, например, так:
bool LoginAsAdmin(DataStorage secretStorage,
string userName,
string password)
{
var adminData = secretStorage.GetAdminData();
if ( userName == adminData.UserName
&& password == adminData.Password)
{
....
return true;
}
return false;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, связанную с тем, что создаётся экземпляр класса, унаследованного от 'System.Exception', но при этом никак не используется.
Пример ошибочного кода:
public void DoSomething(int index)
{
if (index < 0)
new ArgumentOutOfRangeException();
else
....
}
В данном коде пропущен оператор 'throw', из-за чего будет только создан экземпляр класса, унаследованного от 'System.Exception', но при этом он никак не будет использоваться, и исключение не будет сгенерировано. Корректный код может выглядеть следующим образом:
public void DoSomething(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException();
else
....
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, когда исходный объект перехваченного исключения не был использован приемлемым образом при повторной генерации исключения из блока catch. Из-за этого ряд ошибок превращается в трудноотлаживаемые, так как теряется стек оригинального исключения.
Рассмотрим несколько примеров некорректного кода. Первый пример:
public Asn1Object ToAsn1Object()
{
try
{
return Foo(_constructed, _tagNumber);
}
catch (IOException e)
{
throw new ParsingException(e.Message);
}
}
В данном случае перехваченное исключение ввода/вывода хотели трансформировать в другое исключение типа 'ParsingException'. При этом передали только сообщение из первого исключения, тем самым сократив количество полезной информации.
Корректный вариант кода:
public Asn1Object ToAsn1Object()
{
try
{
return Foo(_constructed, _tagNumber);
}
catch (IOException e)
{
throw new ParsingException(e.Message, e);
}
}
В исправленном варианте исходное исключение передается в качестве внутреннего, что полностью сохраняет информацию об исходной ошибке.
Рассмотрим второй пример:
private int ReadClearText(byte[] buffer, int offset, int count)
{
int pos = offset;
try
{
....
}
catch (IOException ioe)
{
if (pos == offset) throw ioe;
}
return pos - offset;
}
В данном случае перехваченное исключение ввода/вывода генерируется повторно и полностью "затирает" стек оригинальной ошибки. Чтобы этого избежать, достаточно сделать переброс оригинального исключения.
Корректный вариант кода:
private int ReadClearText(byte[] buffer, int offset, int count)
{
int pos = offset;
try
{
....
}
catch (IOException ioe)
{
if (pos == offset) throw;
}
return pos - offset;
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с небезопасным использованием шаблона "блокировки с двойной проверкой" (double checked locking). Блокировка с двойной проверкой - это шаблон, предназначенный для уменьшения накладных расходов получения блокировки. Сначала проверяется условие блокировки без синхронизации. И только если условие выполняется, поток попытается получить блокировку. Таким образом, блокировка будет выполнена только если она действительно была необходима.
Рассмотрим пример небезопасной реализации данного шаблона на языке C#:
private static MyClass _singleton = null;
public static MyClass Singleton
{
get
{
if(_singleton == null)
lock(_locker)
{
if(_singleton == null)
{
MyClass instance = new MyClass();
instance.Initialize();
_singleton = instance;
}
}
return _singleton;
}
}
В данном примере шаблон используется для реализации "ленивой инициализации" - инициализация откладывается до тех пор, пока значение переменной не понадобится. Данный код будет корректно работать в программе, использующей объект '_singleton' из одного потока. Для обеспечения безопасной инициализации в многопоточной программе обычно используется конструкция 'lock', однако в нашем примере этого оказывается недостаточно.
Обратите внимание на вызов метода 'Initialize()' у объекта 'Instance'. В Release версии программы, компилятор может оптимизировать данный код и порядок назначения переменной '_singleton' и метода 'Initialize()' могут поменяться. Таким образом, другой поток, обратившись к 'Singleton' одновременно с инициализирующим потоком, может получить доступ к объекту до того, как инициализация будет завершена.
Рассмотрим другой пример использования шаблона блокировки с двойной проверкой:
private static MyClass _singleton = null;
private static bool _initialized = false;
public static MyClass Singleton;
{
get
{
if(!_initialized)
lock(_locker)
{
if(!_initialized)
{
_singleton = new MyClass();
_initialized = true;
}
}
return _singleton;
}
}
Мы видим, что, как и в предыдущем примере, оптимизация компилятором порядка назначений переменных '_singleton' и '_initialized' может привести к ошибке. Т.е. в начале переменной '_initialized' будет присвоено значение 'true', а уже потом создастся новый объект типа 'MyClass' и ссылка не него будет записана в '_singleton'.
Такая перестановка может привести к ошибке при доступе к объекту из параллельного потока. Получается, что переменная '_singleton' будет ещё не назначена, а флаг '_intialize' уже будет выставлен в 'true'.
Одна из опасностей таких ошибок состоит в том, что часто кажется, будто программа работает корректно. Это происходит из-за того, что рассмотренная ситуация будет возникать не очень часто, в зависимости от архитектуры используемого процессора, версии CLR и т.п.
Есть несколько способов обеспечить потоко-безопасность для данного шаблона. Самым простым будет пометить проверяемую в условии if переменную ключевым словом volatile:
private static volatile MyClass _singleton = null;
public static MyClass Singleton
{
get
{
if(_singleton == null)
lock(_locker)
{
if(_singleton == null)
{
MyClass instance = new MyClass();
instance.Initialize();
_singleton = instance;
}
}
return _singleton;
}
}
Использование ключевого слова 'volatile' предотвратит для переменной возможные оптимизации компилятора, связанные с перестановками инструкций записи\чтения и кэшированием её значения в регистрах процессора.
Из соображений производительности не всегда желательно объявлять переменную как 'volatile'. В этом случае можно организовать доступ к переменной с помощью методов: 'Thread.VolatileRead', 'Thread.VolatileWrite' и 'Thread.MemoryBarrier'. Эти методы создадут барьеры по чтению\записи памяти только там, где это необходимо.
Наконец, для реализации "ленивой инициализации" можно воспользоваться специально предназначенным для этого классом 'Lazy<T>', доступным начиная с .NET 4.
См. также статью: Выявление неправильной блокировки с двойной проверкой с помощью диагностики V3054.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциально небезопасный вызов обработчика события (event). Возможно возникновение исключения 'NullReferenceException'.
Рассмотрим пример опасного кода:
public event EventHandler MyEvent;
void OnMyEvent(EventArgs e)
{
if (MyEvent != null)
MyEvent(this, e);
}
В данном примере происходит проверка на 'null' поля 'MyEvent', и затем происходит вызов данного события. Проверка на 'null' позволит избежать исключения в случае, если на событие никто не подписан на момент его вызова (в такой ситуации 'MyEvent' будет иметь значение 'null').
Однако, представим, что у события 'MyEvent' есть один подписчик. И в момент между проверкой на 'null' и непосредственными вызовом обработчика события по 'MyEvent()' существует вероятность, что будет произведена отписка от события, например, в другом потоке:
MyEvent -= OnMyEventHandler;
Теперь, если обработчик 'OnMyEventHandler' был единственным подписчиком события 'MyEvent', поле 'MyEvent' примет значение 'null'. Но, т.к., в другом потоке, где событие должно будет быть вызвано, в нашем гипотетическом примере проверка на 'null' уже произошла, будет выполнена следующая строка: 'MyEvent()'. Это приведёт к возникновению исключения 'NullReferenceException'.
Как мы видим, одной проверки на 'null' недостаточно, чтобы гарантировать безошибочное выполнение вызова события. Существует несколько способов, позволяющих избежать описанной выше потенциальной ошибки. Рассмотрим их.
Первый способ заключается в создании временной локальной переменной, в которую мы сохраняем ссылку на обработчики нашего события:
public event EventHandler MyEvent;
void OnMyEvent(EventArgs e)
{
EventHandler handler = MyEvent;
if (handler != null)
handler(this, e);
}
Это позволит выполнить обработчики события без возникновения исключения. Даже если между проверкой 'handler' на 'null' и вызовом обработчика произойдёт отписка от события, как в нашем первом примере, переменная 'handler' всё равно будет содержать ссылку на первоначальный обработчик. И он будет корректно вызван несмотря на то, событие 'MyEvent' уже не содержит данный обработчик.
Другой способ избежать ошибки - изначально присвоить событию пустой обработчик в виде анонимного метода или лямбда выражения:
public event EventHandler MyEvent = (sender, args) => {};
Такой код гарантирует, что поле 'MyEvent' никогда не будет иметь значения 'null', т.к. такой анонимный метод невозможно будет отписать (если он конечно не сохранён в отдельной переменной), что также позволяет избавиться от проверки на 'null' перед вызовом события.
Наконец, начиная с версии языка C# 6.0 (Visual Studio 2015), можно обеспечить безопасный вызов события с помощью оператора '?.':
MyEvent?.Invoke(this, e);
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил пустой блок обработки исключения ('catch' или 'finally'). Отсутствие корректной обработки исключений может привести к снижению уровня надёжности приложения.
В некоторых случаях отсутствие корректной обработки исключительных ситуаций может стать причиной возникновения уязвимости. Недостаточное логирование и мониторинг выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A10:2017-Insufficient Logging & Monitoring.
Пример кода с пустым 'catch':
try
{
someCall();
}
catch
{
}
Конечно, такой код вовсе не обязательно ошибочен. Но очень странно просто подавлять исключение, ничего не делая, так как такая обработка исключений может скрывать дефекты в программе.
В качестве обработки исключения можно использовать, например, логгирование. Это по крайней мере не позволит исключительной ситуации остаться незамеченной:
try
{
someCall();
}
catch (Exception e)
{
Logger.Log(e);
}
Не менее подозрительным моментом является наличие в коде пустого блока 'finally'. Это может свидетельствовать о том, что какая-то логика, необходимая для надёжной работы приложения, не реализована. Например:
try
{
someCall();
}
catch
{ .... }
finally
{
}
Подобный код с большой вероятностью свидетельствует об ошибке или попросту избыточен. В отличие от пустого блока 'catch', который может быть использован для подавления исключения, у пустого блока 'finally' нет какого-либо практического применения.
Данная диагностика классифицируется как:
Анализатор обнаружил класс исключения, недоступного для других сборок. Если такое исключение будет выброшено, внешний код будет вынужден отлавливать объекты ближайшего доступного предка или вообще базового класса всех исключений – 'Exception'. В этом случае усложняется обработка конкретных исключительных ситуаций, ведь код других сборок не может чётко идентифицировать возникшую проблему.
Отсутствие чёткой идентификации возникшей проблемы несёт дополнительные риски с точки зрения безопасности, так как для каких-то определённых исключительных ситуаций может понадобиться специфичная обработка, а не общая. Недостаточное логирование и мониторинг (в том числе, обнаружение проблем) выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A10:2017-Insufficient Logging & Monitoring.
Простой пример из реального проекта:
internal sealed class ResourceException : Exception
{
internal ResourceException(string? name, Exception? inner = null)
: base(name, inner)
{
}
}
Чтобы можно было корректно обработать конкретную исключительную ситуацию, необходимо задать в объявлении класса модификатор доступности 'public':
public sealed class ResourceException : Exception
{
internal ResourceException(string? name, Exception? inner = null)
: base(name, inner)
{
}
}
Теперь другие сборки смогут отлавливать данное исключение и обрабатывать конкретную ситуацию.
Следует учитывать, что для вложенных классов модификатора 'public' у объявления исключения может быть недостаточно. Например:
namespace SomeNS
{
class ContainingClass
{
public class ContainedException : Exception {}
....
}
}
В данном примере класс исключения вложен в 'ContainingClass', который неявно объявлен как 'internal'. Вследствие этого 'ContainedException' также будет видим только в пределах текущей сборки, хотя он и помечен модификатором 'public'. Анализатор обнаруживает такие случаи и выдаёт соответствующие предупреждения.
Данная диагностика классифицируется как:
|
Анализатор обнаружил создание SQL-команды из данных, полученных из внешнего источника, без предварительной проверки. Это может стать причиной возникновения SQL-инъекции в случае, если данные будут скомпрометированы.
SQL инъекции выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A1:2017-Injection.
Рассмотрим пример:
void ProcessUserInfo()
{
using (SqlConnection connection = new SqlConnection(_connectionString))
{
....
String userName = Request.Form["userName"];
using (var command = new SqlCommand()
{
Connection = connection,
CommandText = "SELECT * FROM Users WHERE UserName = '" + userName + "'",
CommandType = System.Data.CommandType.Text
})
{
using (var reader = command.ExecuteReader())
....
}
}
}
В данном случае при формировании SQL-команды используется значение переменной 'userName', полученной из внешнего источника — 'Request.Form'. Использование данных без какой-либо проверки является опасным, так как открывает злоумышленникам разные способы внедрения команд.
Например, вместо ожидаемого значения имени пользователя злоумышленник может ввести специальную команду. Тогда из базы произойдёт извлечение данных всех пользователей, для которых будет запущена дальнейшая обработка.
Пример такой скомпрометированной строки:
' OR '1'='1
Для защиты от подобных запросов следует проводить валидацию входных данных или, например, использовать параметризованные команды.
Пример кода с использованием параметризированных команд:
void ProcessUserInfo()
{
using (SqlConnection connection = new SqlConnection(_connectionString))
{
....
String userName = Request.Form["userName"];
using (var command = new SqlCommand()
{
Connection = connection,
CommandText = "SELECT * FROM Users WHERE UserName = @userName",
CommandType = System.Data.CommandType.Text
})
{
var userNameParam = new SqlParameter("@userName", userName);
command.Parameters.Add(userNameParam);
using (var reader = command.ExecuteReader())
....
}
}
}
Анализатор также считает небезопасными источниками параметры методов, доступных из других сборок. Пользователи таких сборок могут рассчитывать на то, что валидация данных будет происходить внутри вызываемого метода. Однако, так как ни пользователь библиотеки, ни её разработчик не провели валидацию входных данных, это может стать причиной возникновения уязвимостей при использовании скомпрометированных данных.
Рассмотрим пример:
public class DBHelper
{
public void ProcessUserInfo(String userName)
{
....
var command = "SELECT * FROM Users WHERE userName = '" + userName + "'";
ExecuteCommand(command);
....
}
private void ExecuteCommand(String rawCommand)
{
using (SqlConnection connection = new SqlConnection(_connectionString))
{
....
using (var sqlCommand = new SqlCommand(rawCommand, connection))
{
using (var reader = sqlCommand.ExecuteReader())
....
}
}
}
}
Класс 'DBHelper' предоставляет метод 'ProcessUserInfo' для внешнего использования, так как он доступен из других сборок. Однако параметр этого метода - 'userName' - никак не проверяется перед использованием. Значение, полученное извне, напрямую используется для формирования команды (переменная 'command'). Далее полученная команда передаётся в метод 'ExecuteCommand', где без проверки используется для создания объекта типа 'SQLCommand'.
В данном случае анализатор выдаст предупреждение на вызов метода 'ExecuteCommand', принимающего в качестве аргумента заражённую строку.
Теперь рассмотрим возможный вариант использования метода 'ProcessUserInfo':
static void TestHelper(DBHelper helper)
{
var userName = Request.Form["userName"];
helper.ProcessUserInfo(userName);
}
Разработчик, написавший подобный код, может не иметь доступа к коду класса 'DBHelper' и рассчитывать на то, что валидация входных данных будет происходить внутри метода 'ProcessUserInfo'. Так как ни текущий код, ни код метода 'ProcessUserInfo' не провёл валидацию данных, этот код будет уязвимым для SQL инъекций.
Хотя подобные случаи могут привести к возникновению уязвимости, явного внешнего источника данных при анализе кода 'DBHelper' нет. Поэтому анализатор выдаст предупреждение с низким уровнем достоверности, если источником входных данных считаются параметры методов, доступных внешним сборкам.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что данные, полученные из внешнего источника, используются в качестве путей к файлам или папкам без предварительной проверки. Это может стать причиной уязвимости приложения к атакам path traversal.
Атаки этого типа выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A5:2017-Broken Access Control.
Рассмотрим пример:
HttpResponse response;
HttpRequest request;
private void SendUserFileContent(string userDirectory)
{
....
string userFileRelativePath = request.QueryString["relativePath"];
string fullPath = Path.Combine(userDirectory,
userFileRelativePath);
var content = File.ReadAllText(fullPath);
....
response.Write(content);
}
Данный метод производит отправку содержимого некоторого файла из заданной папки пользователя. Предполагается, что пользователь должен иметь возможность получения содержимого только файлов, хранящихся внутри этой папки.
В качестве относительного пути здесь используется значение, полученное из внешнего источника – 'Request.QueryString'. Отсутствие каких-либо проверок позволяет злоумышленнику получить доступ к содержимому любых файлов в системе.
Например, в папке каждого пользователя может храниться файл userInfo.xml, в котором содержится различная (в том числе конфиденциальная) информация (при этом код выполняется на системе Windows). Тогда для получения доступа к данным пользователя 'admin' злоумышленник может передать в 'Request. QueryString["relativePath"]' следующую строку:
..\admin\userInfo.xml
Для защиты от такой атаки недостаточно простой проверки на наличие '..\' в начале строки. К примеру, для получения тех же данных может передаваться и строка
someFolder\..\..\admin\userInfo.xml
Ещё одна возможность для атаки – передача абсолютного пути вместо относительного. Если один из аргументов 'Path.Combine' представляет собой абсолютный путь, то все ранее записанные аргументы игнорируются. К примеру, метод может быть вызван следующим образом:
Path.Combine("folder", "childFolder", "C:\Users\Admin\secret.txt")
В результате будет возвращена строка 'C:\Users\Admin\secret.txt '. Таким образом, отсутствие проверки входных данных позволяет злоумышленнику получить доступ к любому файлу в системе. Более подробно атаки типа path traversal и способы их проведения описаны на официальном сайте OWASP по ссылке.
Для обеспечения защиты от path traversal необходимо применять различные средства проверки входных данных. В примере, описанном выше, можно использовать проверку на наличие подстрок ":" и "..\":
HttpResponse response;
HttpRequest request;
private void SendUserFileContent(string userDirectory)
{
....
string userFileRelativePath = request.QueryString["relativePath"];
if ( !userFileRelativePath.Contains(":")
&& !userFileRelativePath.Contains(@"..\"))
{
string fullPath = Path.Combine(userDirectory,
userFileRelativePath);
var content = File.ReadAllText(fullPath);
....
response.Write(content);
}
else
{
....
}
}
Анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Эта тема подробно раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Рассмотрим пример кода, выполняющегося на ОС Windows:
public class UserFilesManager
{
private const string UsersDirectoryAbsolutePath = ....;
private HttpResponse response;
private string userName;
public void WriteUserFile(string relativePath)
{
string path = Path.Combine(UsersDirectoryAbsolutePath,
userName,
relativePath);
WriteFile(path);
}
private void WriteFile(string absolutePath)
{
response.Write(File.ReadAllText(absolutePath));
}
}
Анализатор сформирует предупреждение низкого уровня достоверности на вызов метода 'WriteFile' внутри 'WriteUserFile'. После вызова 'Path.Combine' небезопасные данные из 'relativePath' передаются в 'path', после чего выступают в качестве аргумента вызова 'WriteFile', где используются в качестве пути. Таким образом, пользовательский ввод может попасть в метод 'File.ReadAllText' без проверки, из-за чего данный код уязвим к path traversal.
Для защиты от атаки необходимо проводить валидацию параметров. В данном случае важно провести её до вызова 'Path.Combine', так как возвращаемое им значение в любом случае будет абсолютным путём:
public void WriteUserFile(string relativePath)
{
if (relativePath.Contains(":") || relativePath.Contains(@"..\"))
{
....
return;
}
string path = Path.Combine(UsersDirectoryAbsolutePath,
userName,
relativePath);
WriteFile(path);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V5609. |
Анализатор обнаружил ситуацию, при которой данные, полученные из внешнего источника, могут быть использованы для запуска вредоносного скрипта. Подобный код может быть уязвим к XSS.
XSS выделен в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A7:2017-Cross-Site Scripting (XSS).
К XSS чаще всего уязвимы аргументы строки HTTP запроса, тело HTTP запроса и поля ввода HTML страниц.
Рассмотрим простейший пример XSS через параметры URL запроса:
void OpenHtmlPage()
{
WebBrowser.Navigate(TargetSiteUrl.Text);
}
В данном случае пользователь в интерфейсе программы вводит в текстовое поле 'TargetSiteUrl.Text' строку и нажимает на кнопку. После нажатия кнопки в интерфейсе приложения открывается страница сайта, указанного в 'TargetSiteUrl.Text'.
Допустим, на веб странице отображается строка, указанная в URL параметре 'inputData'.
В таком случае, если в URL параметре 'inputData' в строке запроса к странице указать элемент <script>alert("XSS Injection")</script>, то при рендеринге страницы этот код будет выполнен, и в отдельном окне отобразится строка, использованная в скрипте:
Выполнение JavaSсript кода, переданного в URL параметре, является XSS атакой. Благодаря социальной инженерии злоумышленник может убедить пользователя вставить подобный запрос с вредоносным скриптом в поле 'TargetSiteUrl.Text'. В результате выполнения скрипта у злоумышленника может оказаться информация для доступа к аккаунту пользователя. Например, cookie, которые хранились в браузере. Воспользовавшись ей, он сможет похитить конфиденциальные данные или выполнить вредоносные действия от имени пользователя.
Данная XSS атака была бы изначально неудачной, если бы строка, которую ввел пользователь в поле 'TargetSiteUrl.Text', перед использованием в методе 'WebBrowser.Navigate' кодировала спец. символы HTML:
void OpenHtmlPage()
{
var encodedUrl = System.Net.WebUtility.HtmlEncode(TargetSiteUrl.Text);
WebBrowser.Navigate(encodedUrl);
}
В таком случае код вредоносного скрипта не выполнился бы, а просто отобразился на странице:
Также XSS возможен и через поля ввода на сайте. Например, функционал сайта предоставляет возможность оставлять комментарии и просматривать комментарии других посетителей сайта, если вы авторизовались на сайте. Добавленные комментарии сохраняются в базу данных сайта и отображаются на странице комментариев, которая видна только авторизированным пользователям. Если перед отображением комментариев в них не кодируются спец. символы HTML, то возможна интересная XSS атака.
Злоумышленник зарегистрируется на сайте и оставит комментарий, в котором содержится вредоносный скрипт. Комментарий со скриптом сохранится в базу данных. После этого все авторизированные пользователи смогут просмотреть этот комментарий. Если при отображении комментария на странице в нем не кодируются спец. символы HTML, то вредоносный скрипт будет выполнен в браузере каждого авторизированного пользователя, который решил посмотреть комментарии. Таким образом злоумышленник сможет получить cookie не одного, а нескольких пользователей сайта. Если в cookie сохраняется токен авторизации на сайте, то он некоторое время будет иметь возможность войти в аккаунты других пользователей сайта.
Пример кода, отображающего комментарии пользователей на странице сайта:
using (var sqlConnection = ....)
{
var command = ....;
....
var reader = command.ExecuteReader();
while (reader.Read())
{
....
var userComment = reader.GetString(1);
....
Response.Write("<p>");
Response.Write(userComment);
Response.Write("</p>");
....
}
}
Для защиты от подобной XSS атаки необходимо обрабатывать данные либо перед записью в базу данных, либо перед отображением на странице.
Исправленный код отображения комментариев из базы данных (данные кодируются перед отображением) может быть таким:
using (var sqlConnection = ....)
{
var command = ....;
....
var reader = command.ExecuteReader();
while (reader.Read())
{
....
var userComment = reader.GetString(1);
var encodedComment = WebUtility.HtmlEncode(userComment);
....
Response.Write("<p>");
Response.Write(encodedComment);
Response.Write("</p>");
....
}
}
Анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Рассмотрим пример кода:
public class UriHelper
{
WebBrowser WebBrowser = new WebBrowser();
private string _targetSite = "http://mysite.com";
public void ProcessUrlQuery(string urlQuery)
{
var urlRequest = _targetSite + "?" + urlQuery;
OpenWebPage(urlRequest);
}
private void OpenWebPage(string urlWithQuery)
{
WebBrowser.Navigate(urlWithQuery);
}
}
В данном случае анализатор выдаст предупреждение низкого уровня достоверности при анализе исходного кода метода 'ProcessUrlQuery' на вызов метода 'OpenWebPage'. Анализатор отследил передачу небезопасных данных из параметра 'urlQuery' в метод 'Navigate'.
Параметр 'urlQuery' используется при конкатенации строк, из-за чего переменная 'urlRequest' также будет содержать небезопасные данные. Далее 'urlRequest' передаётся в метод 'OpenWebPage', где выступает в качестве аргумента метода 'Navigate'. Таким образом, пользовательский ввод может попасть в метод 'Navigate' без проверки, из-за чего данный код уязвим к XSS.
Защититься от XSS в этом коде возможно тем же способом, что и в примере ранее - просто кодировать строку запроса перед передачей в метод 'Navigate':
public class UriHelper
{
WebBrowser WebBrowser = new WebBrowser();
private string _targetSite = "http://mysite.com";
public void ProcessUrlQuery(string urlQuery)
{
var urlRequest = _targetSite + "?" + urlQuery;
OpenWebPage(urlRequest);
}
private void OpenWebPage(string urlWithQuery)
{
var encodedUrlWithQuery =
System.Net.WebUtility.HtmlEncode(urlWithQuery);
WebBrowser.Navigate(encodedUrlWithQuery);
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, при которой данные, полученные из внешнего источника, могут быть использованы для создания объекта при десериализации. Подобный код может быть причиной различных уязвимостей.
Небезопасная десериализация выделена в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A8:2017-Insecure Deserialization.
Рассмотрим синтетический пример:
[Serializable]
public class User
{
....
public bool IsAdmin { get; private set; }
....
}
private static User GetUserFromFile(string filePath)
{
User user = null;
using (var fileStream = new FileStream(filePath, FileMode.Open))
{
var soapFormatter = new SoapFormatter();
user = (User) soapFormatter.Deserialize(fileStream);
}
return user;
}
static void Main(string[] args)
{
Console.WriteLine("Please provide the path to the file.");
var userInput = Console.ReadLine();
User user = GetUserFromFile(userInput);
if (user?.IsAdmin == true)
// Performs actions with elevated privileges
else
// Performs actions with limited privileges
}
При запуске метода 'Main' консольное приложение запросит у пользователя путь до файла. После указания пути до файла, содержимое файла будет десериализовано в объект типа 'User'. Если получилось провести десериализацию объекта из файла и его свойство 'IsAdmin' равно 'true', то дальше будут произведены действия с повышенными привилегиями. В противном случае будут произведены действия с ограниченными привилегиями. Учитывая то, что данные из файла десериализуются SOAP сериализатором в объект типа 'User', имеется возможность увидеть структуру объекта, находящегося в файле:
<SOAP-ENV:Envelope xmlns:xsi=....
xmlns:xsd=....
xmlns:SOAP-ENC=....
xmlns:SOAP-ENV=....
xmlns:clr=....
SOAP-ENV:encodingStyle=....>
<SOAP-ENV:Body>
<a1:Program_x002B_User id="ref-1" xmlns:a1=....>
<_x003C_UserId_x003E_k__BackingField>1</_x003C_UserId_x003E_k__BackingField>
<_x003C_UserName_x003E_k__.... id="ref-3">Name</_x003C_UserName_x003E_k__....>
<_x003C_IsAdmin_x003E_k__....>false</_x003C_IsAdmin_x003E_k__....>
</a1:Program_x002B_User>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Благодаря этой информации злоумышленник может, например, изменить значение свойства 'IsAdmin' с приватным сеттером на 'true' вместо 'false':
<_x003C_IsAdmin_x003E_k__....>true</_x003C_IsAdmin_x003E_k__....>
Это позволит злоумышленнику при десериализации объекта из файла получить повышенные привилегии для десериализованного объекта. В результате, в программе будут выполнены действия, которые изначально не были доступны объекту из файла. Например, злоумышленник сможет украсть конфиденциальные данные или совершить вредоносные действия, которые до модификации объекта из файла были ему не доступны.
Чтобы избавится от этой уязвимости, необходимо убедиться, что злоумышленник при получении доступа к файлу не сможет узнать структуру объекта. Для этого следует использовать шифрование данных, записывающихся в файл. В C# имеется класс 'CryptoStream', который поможет в этом:
private static void SerializeAndEncryptUser(User user,
string filePath,
byte[] key,
byte[] iv)
{
using (var fileStream = new FileStream(filePath, FileMode.CreateNew))
{
using (Rijndael rijndael = Rijndael.Create())
{
rijndael.Key = key;
rijndael.IV = iv;
var encryptor = rijndael.CreateEncryptor(rijndael.Key, rijndael.IV);
using (var cryptoStream = new CryptoStream(fileStream,
encryptor,
CryptoStreamMode.Write))
{
var soapFormatter = new SoapFormatter();
soapFormatter.Serialize(cryptoStream, user);
}
}
}
}
Этот код зашифрует данные, полученные при сериализации объекта 'User', перед тем, как записать их в файл. При обработке содержимого файла в методе 'GetUserFromFile' перед десериализацией необходимо будет дешифровать данные, также используя 'CryptoStream':
private static User GetUserFromFile(string filePath, byte[] key, byte[] iv)
{
User user = null;
using (var fileStream = new FileStream(filePath, FileMode.Open))
{
using (Rijndael rijndael = Rijndael.Create())
{
rijndael.Key = key;
rijndael.IV = iv;
var decryptor = rijndael.CreateDecryptor(rijndael.Key,
rijndael.IV);
using (var cryptoStream = new CryptoStream(fileStream,
decryptor,
CryptoStreamMode.Read))
{
var soapFormatter = new SoapFormatter();
user = (User) soapFormatter.Deserialize(cryptoStream);
}
}
}
return user;
}
Таким образом структура и содержимое объекта из файла останутся неизвестны злоумышленнику, и он не сможет получить повышенные привилегии при помощи изменения значения свойства 'IsAdmin' в файле. Следовательно, в описанном примере будет устранена проблема небезопасной десериализации.
Для более надежной защиты от уязвимостей этого типа в дополнение к шифрованию сериализованных данных стоит придерживаться еще нескольких правил, которые перечислены в соответствующем разделе OWASP Top 10.
Анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Рассмотрим пример:
public class DeserializationHelper
{
public T DesrializeFromStream<T>(Stream stream)
{
T deserializedObject = default;
using(var streamReader = new StreamReader(stream))
{
deserializedObject = DeserializeXml<T>(streamReader);
}
return deserializedObject;
}
private T DeserializeXml<T>(StreamReader streamReader)
{
return (T) new XmlSerializer(typeof(T)).Deserialize(streamReader);
}
}
В данном случае анализатор выдаст предупреждение низкого уровня достоверности при анализе исходного кода метода 'DesrializeFromStream' на вызов метода 'DeserializeXml'. Анализатор отследил передачу небезопасных данных из параметра 'stream' в конструктор 'StreamReader', а передачу объекта 'streamReader' - в метод 'Deserialize'.
Защититься от небезопасной десериализации в этом коде возможно тем же способом, что и в примере ранее, при помощи класса 'CryptoStream':
public class DeserializationHelper
{
public T DesrializeFromFile<T>(Stream stream, ICryptoTransform transform)
{
T deserializedObject = default;
using (var cryptoStream = new CryptoStream(stream,
transform,
CryptoStreamMode.Read))
{
using (var streamReader = new StreamReader(cryptoStream))
{
deserializedObject = DeserializeXml<T>(streamReader);
}
}
return deserializedObject;
}
private T DeserializeXml<T>(StreamReader streamReader)
{
return (T) new XmlSerializer(typeof(T)).Deserialize(streamReader);
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V5611. |
Анализатор обнаружил использование в коде устаревших версий протоколов SSL/TLS. Это может сделать приложение уязвимым к таким атакам, как man-in-the-middle, BEAST, и т.п.
Проблемы, связанные с применением устаревших протоколов, могут быть отнесены к двум категориям OWASP Top Ten 2017:
Рассмотрим пример:
public void Run(ApplePushChannelSettings settings)
{
....
var certificates = new X509CertificateCollection();
....
var stream = new SslStream(....);
stream.AuthenticateAsClient(settings.FeedbackHost,
certificates,
SslProtocols.Tls, // <=
false);
....
}
В указанном фрагменте используется значение 'SslProtocols.Tls', представляющее протокол TLS версии 1.0. Данная версия является устаревшей и не рекомендуется к использованию, так как TLS 1.0 уязвим к ряду атак, в числе которых упоминавшаяся ранее BEAST.
Рекомендуется использовать более новые версии протоколов, например - TLS 1.2:
public void Run(ApplePushChannelSettings settings)
{
....
var certificates = new X509CertificateCollection();
....
var stream = new SslStream(....);
stream.AuthenticateAsClient(settings.FeedbackHost,
certificates,
SslProtocols.Tls12,
false);
....
}
Версии протоколов, являющиеся более старыми, чем TLS 1.2, не рекомендуются к использованию из-за возможных проблем с безопасностью. К таким протоколам относятся SSL версий 2.0 и 3.0, а также TLS версий 1.0 и 1.1.
Стоит отметить, что использование значения 'SslProtocols.Default' также не рекомендуется, так как оно соответствует разрешению использования протоколов SSL 3.0 или TLS 1.0, которые являются устаревшими.
Как правило, наиболее подходящими значениями являются 'SslProtocols.None' и 'SecurityProtocolType.SystemDefault', предоставляющие выбор протокола передачи данных операционной системе. Если по каким-либо причинам данные значения не подходят, то рекомендуется задавать самую новую версию из доступных.
Анализатор также сформирует предупреждение в случае, если непосредственное использование устаревших протоколов происходит внутри вызванного метода:
SslStream _sslStream;
public string TargetHost { get; set; }
public X509CertificateCollection Certificates { get; set; }
private void PrepareSslStream()
{
....
var protocol = SslProtocols.Ssl3 | SslProtocols.Tls12;
Authenticate(protocol); // <=
....
}
private void Authenticate(SslProtocols protocol)
{
_sslStream.AuthenticateAsClient(TargetHost,
Certificates,
protocol,
true);
}
В качестве аргумента метода 'Authenticate' передаётся значение, представляющее протоколы SSL 3.0 и TLS 1.2. Внутри метода оно используется для задания протоколов, используемых стандартным методом 'AuthenticateAsClient'. Так как SSL 3.0 является устаревшим, его использование может привести к появлению уязвимостей, поэтому анализатор отобразит соответствующее предупреждение.
Исправление в данном случае то же, что и раньше – необходимо исключить небезопасный протокол из списка доступных:
private void PrepareSslStream()
{
....
var protocol = SslProtocols.Tls12;
Authenticate(protocol);
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что в приложении используется устаревший алгоритм шифрования или хеширования. Применение таких алгоритмов может привести к раскрытию конфиденциальных данных, утечке ключей, нарушению аутентификации и т. д.
Уязвимости, связанные с использованием небезопасных алгоритмов шифрования, могут быть отнесены к следующим категориям OWASP Top Ten 2017:
Рассмотрим пример:
private static string CalculateSha1(string text, Encoding enc)
{
var buffer = enc.GetBytes(text);
using var cryptoTransformSha1 = new SHA1CryptoServiceProvider(); // <=
var hash = BitConverter.ToString(cryptoTransformSha1.ComputeHash(buffer))
.Replace("-", string.Empty);
return hash.ToLower();
}
При проверке фрагмента анализатор сформирует предупреждение о том, что использование алгоритма SHA1 не рекомендуется. В данном случае проблема алгоритма состоит в наличии широко известных проблем с коллизией. Таким образом, его использование не является безопасным.
Вместо устаревших алгоритмов следует использовать более современные. В примере, представленном выше, одним из решений может быть замена SHA1 на SHA256:
private static string CalculateSha256(string text, Encoding enc)
{
var buffer = enc.GetBytes(text);
using var cryptoTransformSha256 = new SHA256CryptoServiceProvider();
var hash = BitConverter.ToString(cryptoTransformSha256.ComputeHash(buffer))
.Replace("-", string.Empty);
return hash.ToLower();
}
На сайте Microsoft доступна документация по стандартным реализациям различных алгоритмов шифрования. Как правило, классы, реализующие устаревшие алгоритмы, отмечены в документации специальным предупреждением. Ниже приведены некоторые из них:
Использование классов, наследующих указанные, также не рекомендуется.
На официальном сайте OWASP по ссылке представлены различные методики проверки приложения на наличие потенциальных уязвимостей, связанных с использованием небезопасных алгоритмов шифрования.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил использование небезопасно сконфигурированного XML-парсера, который обрабатывает данные, полученные из внешнего источника. Это может сделать приложение уязвимым к XXE-атаке.
XXE-атаки выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2017: A4:2017 – XML External Entities (XXE) и включены в категорию A05:2021 – Security Misconfiguration OWASP Top 10 2021.
Стандарт XML предусматривает использование DTD (document type definition). DTD даёт возможность определять и использовать XML-сущности. Сущности могут быть как полностью определены внутри документа (например, представлять собой какую-то строку), так и ссылаться на какой-то внешний ресурс. Отсюда и происходит название XXE-атаки: XML eXternal Entities.
Внешние сущности могут быть определены через URI, вследствие чего XML-парсер может обработать этот URI и подставить полученное содержимое в XML-документ.
Пример XML-документа, в котором определена внешняя сущность:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file://D:/XXETarget.txt">
]>
<foo>&xxe;</foo>
Здесь определена сущность '&xxe;'. При обработке этого XML парсер подставит вместо '&xxe;' содержимое файла 'D:\XXETarget.txt'.
Таким образом, атака возможна, если:
Как следствие, злоумышленник может, например, раскрыть содержимое данных с машины, на которой исполняется приложение и которая занимается парсингом XML-файла.
PVS-Studio выдаст предупреждение, если обнаружит в коде небезопасно сконфигурированный XML-парсер, который обрабатывает данные, которые могут быть получены из внешнего источника.
Рассмотрим простой пример. Есть приложение, которое принимает запросы в виде XML-файлов и обрабатывает товары с соответствующим идентификатором. Если идентификатор задан неверно, приложение сообщает об этом.
Формат XML-файла, с которым работает приложение:
<?xml version="1.0" encoding="utf-8" ?>
<shop>
<itemID>62</itemID>
</shop>
Допустим, обработкой занимается следующий код:
static void ProcessItemWithID(String pathToXmlFile)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
XmlResolver = new XmlUrlResolver(),
DtdProcessing = DtdProcessing.Parse
};
using (var fileReader = File.OpenRead(pathToXmlFile))
{
using (var reader = XmlReader.Create(fileReader, settings))
{
while (reader.Read())
{
if (reader.Name == "itemID")
{
var itemIDStr = reader.ReadElementContentAsString();
if (long.TryParse(itemIDStr, out var itemIDValue))
{
// Process item with the 'itemIDValue' value
Console.WriteLine(
$"An item with the '{itemIDValue}' ID was processed.");
}
else
{
Console.WriteLine($"{itemIDStr} is not valid 'itemID' value.");
}
}
}
}
}
}
Для приведённого выше XML-файла приложение распечатает следующую строку:
An item with the '62' ID was processed.
Если вместо номера в ID будет записано что-то другое (например, строка "Hello world"), приложение сообщит об ошибке:
"Hello world" is not valid 'itemID' value.
Несмотря на то, что код делает то, что от него ожидается, он уязвим к XXE-атакам за счёт соблюдения всех перечисленных ранее факторов:
Ниже представлен XML-файл, через который можно скомпрометировать данный код:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file://D:/MySecrets.txt">
]>
<shop>
<itemID>&xxe;</itemID>
</shop>
В этом файле объявляется внешняя сущность 'xxe', которая будет обработана парсером. Вследствие этого содержимое файла 'D:/MySecrets.txt' (например, такое: 'This is an XXE attack target.'), находящегося на машине, где запущено приложение, будет выдано пользователю:
This is an XXE attack target. is not valid 'itemID' value.
Для того, чтобы обезопаситься от подобной атаки, можно запретить обработку внешних сущностей (присвоить свойству 'XmlResolver' значение 'null'), а также запретить или игнорировать обработку DTD (записать в свойство 'DtdProcessing' значение 'Prohibit' или 'Ignore' соответственно).
Пример безопасных настроек:
XmlReaderSettings settings = new XmlReaderSettings()
{
XmlResolver = null,
DtdProcessing = DtdProcessing.Prohibit
};
Обратите внимание, что при использовании различных типов, уязвимость к XXE-атакам может выглядеть по-разному. Например, следующий код также уязвим к XXE-атаке:
static void ProcessXML(String pathToXmlFile)
{
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.XmlResolver = new XmlUrlResolver();
using (var xmlStream = File.OpenRead(pathToXmlFile))
{
xmlDoc.Load(xmlStream);
Console.WriteLine(xmlDoc.InnerText);
}
}
Здесь загрузка XML происходит через экземпляр типа 'XmlDocument'. При этом для 'XmlResolver' явно выставлено опасное значение, а обработка DTD включена неявно. Чтобы запретить обработку внешних сущностей, достаточно записать в свойство 'XmlResolver' значение 'null'.
Анализатор также учитывает и межпроцедурные вызовы. Рассмотрим пример:
static FileStream GetXmlFileStream(String pathToXmlFile)
{
return File.OpenRead(pathToXmlFile);
}
static XmlDocument GetXmlDocument()
{
XmlDocument xmlDoc = new XmlDocument()
{
XmlResolver = new XmlUrlResolver()
};
return xmlDoc;
}
static void LoadXmlInternal(XmlDocument xmlDoc, Stream input)
{
xmlDoc.Load(input);
Console.WriteLine(xmlDoc.InnerText);
}
static void XmlDocumentTest(String pathToXmlFile)
{
using (var xmlStream = GetXmlFileStream(pathToXmlFile))
{
var xmlDoc = GetXmlDocument();
LoadXmlInternal(xmlDoc, xmlStream);
}
}
В данном случае анализатор выдаст предупреждение на вызов метода 'LoadXmlInternal', так как отследит, что:
Стоит отметить, что анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Например, на следующий код будет выдано предупреждение низкого уровня достоверности, так как источник небезопасных данных – параметр публично-доступного метода:
public static void XmlDocumentTest(Stream xmlStream)
{
XmlDocument doc = new XmlDocument()
{
XmlResolver = new XmlUrlResolver()
};
doc.Load(xmlStream);
Console.WriteLine(doc.InnerText);
}
Обратите внимание, что настройки некоторых XML-парсеров менялись в разных версиях .NET Framework.
Рассмотрим следующий фрагмент кода:
static void XmlDocumentTest(String pathToXml)
{
var xml = File.ReadAllText(pathToXml);
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
Console.WriteLine(doc.InnerText);
}
Данный код является устойчивым к XXE-атакам в .NET Framework 4.5.2 и более новых версиях, так как свойство 'XmlResolver' по умолчанию имеет значение 'null'. Как следствие, внешние сущности не будут обработаны.
В версиях .NET Framework 4.5.1 и более старых данный код уязвим к XXE-атакам, так как по умолчанию 'XmlResolver' не равен 'null' и обрабатывает внешние сущности.
PVS-Studio также учитывает дефолтные настройки парсеров в зависимости от того, какая версия .NET Framework / .NET используется в анализируемом проекте.
Общая рекомендация по защите от XXE-атак – следить за тем, чтобы обработка DTD была отключена; обработка внешних сущностей – запрещена. В разных XML-парсерах настройки могут немного отличаться, но, как правило, за это отвечают свойства 'DtdProcessing' ('ProhibitDtd' в более старых версиях .NET Framework) и 'XmlResolver'.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил использование небезопасно сконфигурированного XML-парсера для обработки данных, полученных из внешнего источника. Это может сделать приложение уязвимым к XEE-атаке (альтернативные названия: 'billion laughs'-атака или атака с помощью XML-бомб).
XEE-атаки включены в категории OWASP Top 10 2017: A4:2017 – XML External Entities (XXE) и OWASP Top 10 2021: A05:2021 – Security Misconfiguration.
Стандарт XML предусматривает использование DTD (document type definition). DTD даёт возможность определять и использовать XML-сущности. Сущности могут как ссылаться на какой-то внешний ресурс, так и быть полностью определены внутри документа. В последнем случае они могут быть представлены какой-то строкой или, например, другими сущностями.
XML-файл с примерами подобных сущностей:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY lol "lol">
<!ENTITY lol1 "&lol;&lol;">
]>
<foo>&lol1;</foo>
Здесь определены сущности 'lol' и 'lol1', причём первая определена через строку, а вторая – через другие сущности. Как следствие, значением сущности 'lol1' будет строка 'lollol'.
Вложенность и количество сущностей можно увеличивать. Например, так:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE foo [
<!ENTITY lol "lol">
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
]>
<foo>&lol2;</foo>
Сущность 'lol2' раскрывается следующим образом:
lollollollollollollollollollollollollollollollollollollollollollollol
lollollollollollollollollollollollollollollollollollollollollollollol
lollollollollollollollollollollollollollollollollollollollollollollol
lollollollollollollollollollollollollollollollollollollollollollollol
lollollollollollollollol
По подобному принципу, путём увеличения количества вложенных сущностей, создаются так называемые XML-бомбы. Они представляют собой небольшие по размеру файлы, которые при раскрытии сущностей разрастаются до огромных размеров. Отсюда же происходят различные названия такого типа атак:
Таким образом, DoS-атака на приложение с использованием XML-бомб возможна, если:
Интересный реальный пример уязвимости приложения к XEE описан в статье "Как Visual Studio 2022 съела 100 Гб памяти и при чём здесь XML бомбы?".
Рассмотрим пример:
static void XEETarget(String pathToXml)
{
XmlReaderSettings settings = new XmlReaderSettings()
{
DtdProcessing = DtdProcessing.Parse,
MaxCharactersFromEntities = 0
};
using (var xml = File.OpenRead(pathToXml))
{
using (var reader = XmlReader.Create(xml, settings))
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Text)
Console.WriteLine(reader.Value);
}
}
}
}
В данном примере с помощью XML-парсера 'reader' происходит чтение XML-файла. Однако данный парсер уязвим к XML-бомбам, т.к. он создан с небезопасными настройками, в которых:
Как следствие, парсер может зависнуть в попытке разобрать XML-бомбу, начать потреблять большое количество памяти и т.п.
Стоит отметить, что обрабатываемые данные приходят из внешнего источника (считываются из файла по пути 'pathToXml'). Обнаружив сочетание всех перечисленных факторов, анализатор выдаст предупреждение.
Чтобы сделать парсер устойчивым к XEE-атакам, достаточно:
Пример настроек, в которых разрешена обработка DTD, но при этом ограничен максимальный размер сущностей:
XmlReaderSettings settings = new XmlReaderSettings()
{
DtdProcessing = DtdProcessing.Parse,
MaxCharactersFromEntities = 1024
};
Если в ходе разбора XML-файла будет превышен установленный лимит, парсер сгенерирует исключение типа 'XmlException'.
Анализатор также отслеживает межпроцедурные вызовы. Изменим предыдущий пример:
static XmlReaderSettings GetDefaultSettings()
{
var settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Parse;
settings.MaxCharactersFromEntities = 0;
return settings;
}
public static void XEETarget(String pathToXml)
{
using (var xml = File.OpenRead(pathToXml))
{
using (var reader = XmlReader.Create(xml, GetDefaultSettings()))
{
ProcessXml(reader);
}
}
}
static void ProcessXml(XmlReader reader)
{
while (reader.Read())
{
// Process XML
}
}
В данном случае анализатор выдаст предупреждение на вызов метода 'ProcessXml', так как отследит, что:
Кроме того, анализатор укажет на фрагменты кода, соответствующие перечисленным выше действиям.
Стоит отметить, что анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Пример:
public class XEETest
{
public static void XEETarget(Stream xmlStream)
{
var rs = new XmlReaderSettings()
{
DtdProcessing = DtdProcessing.Parse,
MaxCharactersFromEntities = 0
};
using (var reader = XmlReader.Create(xmlStream, rs))
{
while (reader.Read())
{
// Process XML
}
}
}
}
Анализатор выдаст предупреждение низкого уровня достоверности на данный код, так как источник небезопасных данных – параметр публично-доступного метода – используется в опасно сконфигурированном XML-парсере.
Обратите внимание, что в разных версиях .NET Framework настройки по умолчанию могут отличаться. Как следствие, один и тот же код может либо быть уязвимым к XEE-атакам, либо устойчивым.
Пример подобного кода:
static void XEETarget(String pathToXml)
{
using (var xml = File.OpenRead(pathToXml))
{
var settings = new XmlReaderSettings()
{
DtdProcessing = DtdProcessing.Parse
};
using (var reader = XmlReader.Create(xml, settings))
{
while (reader.Read())
{
// Process XML
}
}
}
}
Данный код уязвим к XEE-атакам в версиях .NET Framework 4.5.1 и более старых, так как не устанавливает ограничения на размер сущностей (значение свойства 'MaxCharactersFromEntities' - 0). В версиях .NET Framework 4.5.2 и более новых по умолчанию установлено ограничение на размер сущностей, как следствие – данный код в них будет устойчив к XEE.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил создание команды уровня операционной системы из непроверенных данных, полученных из внешнего источника. Это может стать причиной возникновения уязвимости command injection.
В OWASP Top 10 Application Security Risks инъекции команд относятся к следующим категориям:
Рассмотрим пример:
HttpRequest _request;
string _pathToExecutor;
private void ExecuteOperation()
{
....
String operationNumber = _request.Form["operationNumber"];
Process.Start("cmd", $"/c {_pathToExecutor} {operationNumber}");
....
}
В представленном коде из запроса считывается номер операции, которую должен выполнить вызываемый процесс. Таким образом, набор операций чётко ограничен. Тем не менее, злоумышленник может передать в качестве значения параметра "operationNumber" специальную строку, которая позволит выполнить дополнительные несанкционированные действия. К примеру, в "operationNumber" может храниться следующая строка:
0 & del /q /f /s *.*
Допустим, что в '_pathToExecutor' записан путь 'executor.exe'. Тогда в результате вызова 'Process.Start' будет выполнена следующая команда:
cmd /c executor.exe 0 & del /q /f /s *.*
Символ '&' здесь будет интерпретирован как разделитель команд. Инструкция 'del' с такими аргументами приведёт к удалению всех файлов в текущей и вложенных папках (за исключением тех, для которых у приложения нет достаточных прав доступа). Таким образом, правильно подобранное значение в параметре "operationNumber" позволяет совершить вредоносные действия.
Во избежание появления уязвимости рекомендуется всегда проверять входные данные. Конкретный способ проверки зависит от ситуации. В указанном выше примере будет достаточно убедиться, что в переменной 'operationNumber' записано число:
private void ExecuteOperation()
{
String operationNumber = _request.Form["operationNumber"];
if (uint.TryParse(operationNumber, out uint number))
Process.Start("cmd", $"/c {_pathToExecutor} {number}");
}
Параметры методов, доступных из других сборок, также являются источниками небезопасных данных, хотя предупреждение для таких источников будет выдано с низким уровнем достоверности. Подробное объяснение данной позиции приведено в заметке "Почему важно проверять значения параметров общедоступных методов".
В качестве примера рассмотрим следующий код:
private string _userCreatorPath;
public void CreateUser(string userName, bool createAdmin)
{
string args = $"--name {userName}";
if (createAdmin)
args += " --roles ADMIN";
RunCreatorProcess(args); // <=
}
private void RunCreatorProcess(string arguments)
{
Process.Start(_userCreatorPath, arguments).WaitForExit();
}
В данном коде пользователь создаётся с помощью процесса, запускаемого методом 'RunCreatorProcess'. Этот пользователь должен получить права администратора только в том случае, если флаг 'createAdmin' имеет значение 'true'.
Код из библиотеки, зависимой от текущей, может вызывать метод 'CreateUser' для создания пользователя. В параметр 'userName' может быть передано, к примеру, значение какого-нибудь параметра запроса. При этом высока вероятность того, что никаких проверок в вызывающем коде не производится, так как разработчик будет рассчитывать на их наличие в методе 'CreateUser'. Таким образом, и в библиотеке, и в использующем её коде отсутствует валидация 'userName'.
В результате правильно подобранное имя позволит злоумышленнику создать пользователя с правами администратора вне зависимости от значения флага 'createAdmin', который, очевидно, будет равен 'false' в большинстве случаев. Допустим, что в параметр 'userName' записана следующая строка:
superHacker --roles ADMIN
После подстановки строка аргументов будет выглядеть так же, как если бы в 'createAdmin' было записано значение 'true':
--name superHacker --roles ADMIN
Таким образом, даже не имея прав на создание администратора, злоумышленник сможет создать пользователя с правами администратора и использовать его далее в своих целях.
В данном случае для защиты нужно проверять имя пользователя на отсутствие запрещённых символов. Например, можно разрешить использование только латинских букв и цифр:
public void CreateUser(string userName, bool createAdmin)
{
if (!Regex.IsMatch(userName, @"^[a-zA-Z0-9]+$"))
{
// error handling
return;
}
string args = $"--name {userName}";
if (createAdmin)
args += " --roles ADMIN";
RunCreatorProcess(args);
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил код, в котором производится установка бесконечного или очень большого срока действительности сессии. Это может привести к проблемам, связанным с безопасностью данных аутентифицированного пользователя.
Ошибки, связанные с установкой некорректного срока действительности сессии, относятся к следующим категориям OWASP Top 10 Application Security Risks:
Первый пример:
public void ConfigureSession(HttpContext current, ....)
{
HttpSessionState session = current.Session;
session.Timeout = -1;
....
}
Значение, присваиваемое свойству 'HttpSessionState.Timeout', указывает срок действительности сессии в минутах.
Присваивание свойству 'Timeout' отрицательного значения приводит к установке потенциально бесконечного срока действительности сессии. Если пользователь не выполнил корректный выход из системы, то его данные будут в опасности. К примеру, следующий пользователь этой системы может получить доступ к данным предыдущего, потому что сессия последней аутентификации на сервере ещё активна.
В другом случае, злоумышленник, укравший токен аутентификации, может иметь больше времени на совершение неправомерного доступа. Токен аутентификации может украден, например, посредством проведения XSS-атаки.
Второй пример:
public void ConfigureSession(HttpContext current, ....)
{
HttpSessionState session = current.Session;
session.Timeout = 120;
....
}
Опасность и эксплуатация уязвимости второго примера аналогична первому.
Пороговым значением, когда анализатор считает код корректным, является срок действия менее двух часов:
public void ConfigureSession(HttpContext current, ....)
{
HttpSessionState session = current.Session;
session.Timeout = 30;
....
}
Большинство библиотек и фреймворков по умолчанию предлагают срок не более чем 30 минут.
Предупреждение для слишком большого присваиваемого значения имеет уровень Medium, а предупреждение о бесконечном сроке – уровень High.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил обращение к удаленному ресурсу без проверки предоставленного пользователем URL-адреса. Использование непроверенных внешних данных для формирования адреса может стать причиной возникновения Server-Side Request Forgery.
Уязвимости типа Server-Side Request Forgery выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2021: A10:2021-Server-Side Request Forgery.
Приведём пример:
void ServerSideRequestForgery()
{
var url = Request.QueryString["url"];
WebRequest request = WebRequest.Create(url);
WebResponse response = request.GetResponse();
using (Stream stream = response.GetResponseStream())
{
using (StreamReader reader = new StreamReader(stream))
{
....
}
}
response.Close();
}
В данном примере 'url' может содержать заражённые данные, так как они приходят из внешнего источника. Из этого адреса формируется запрос, который будет выполнен от имени сервера. Он может быть отправлен на любой веб-ресурс или на сам сервер.
Таким образом, злоумышленник получает возможность совершать вредоносные действия, отправляя запросы к ресурсам, прямой доступ к которым у него отсутствует.
Пример скомпрометированных данных:
http://localhost/admin/delete?username=testSSRF
При выполнении запроса с таким адресом злоумышленник может удалить пользователя.
Также стоит заметить, что при борьбе с SSRF не стоит использовать запрещённый список или регулярные выражения. Злоумышленник может легко обойти эти ограничения:
http://2130706433/ = http://127.0.0.1
http://0x7f000001/ = http://127.0.0.1
Пример борьбы с SSRF с использованием проверки предоставленных данных:
string ServerSideRequestForgery()
{
var url = Request.QueryString["url"];
if (!whiteList.Contains(url))
return "Forbidden URL";
WebRequest request = WebRequest.Create(url);
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил логирование данных из внешнего источника без предварительной проверки. Это может нарушить процессы логирования или скомпрометировать содержание логов.
Ошибки, связанные с логированием данных, относятся к категории A09:2021 – Security Logging and Monitoring Failures списка OWASP Top 10 Application Security Risks.
Если логирование пользовательского ввода производится без проверок, то злоумышленник может внедрить произвольные данные в лог.
Рассмотрим пример. Предположим, что логи хранятся в простом текстовом формате. Узнать формат хранения логов можно разными способами. Например, если проект имеет открытый исходный код или с помощью какой-либо другой атаки. Возможный лог может иметь такой вид:
INFO: User 'SomeUser' entered value: '2022'.
INFO: User 'SomeUser' logged out.
Код, осуществляющий логирование, может выглядеть так:
public class InputHelper
{
HttpRequest Request {get; set;}
Logger logger;
string UserName;
void ProcessUserInput()
{
string userInput = Request["UserInput"];
string logMessage = "INFO: User '"
+ UserName
+ "' entered value: '"
+ userInput + "'.";
logger.Log(logMessage);
....
}
}
В таком случае злоумышленник может внедрить произвольные данные о событиях, которые никогда не происходили.
Например, злоумышленник вводит такой текст:
2022/r/nINFO: User 'Admin' logged out.
Тогда в логах появится такая запись, которая может ввести человека, анализирующего логи, в заблуждение:
INFO: User 'SomeUser' entered value: '2022'.
INFO: User 'Admin' logged out.
Рассмотрим другой вид атаки. Например, если логи хранятся в формате XML, то злоумышленник может внедрить такие данные, которые сделают содержимое отчёта некорректным. Также последующий процесс парсинга готовых логов может выдать неправильные данные или завершиться ошибкой. Пример уязвимого кода:
public class InputHelper
{
HttpRequest Request {get; set;}
Logger logger;
void ProcessUserInput()
{
string userID = Request["userID"];
logger.Info(userID); // <=
....
}
}
Злоумышленник может внедрить незакрытый тег и сделать невозможным парсинг XML-файла.
Список возможных уязвимостей зависит от архитектуры и настроек ввода, логгера, вывода логов или иных частей системы логирования. Например, атака на логи в формате XML может иметь следующие последствия:
Некоторых атак можно избежать экранированием символов, чтобы они не рассматривались как часть XML-синтаксиса. Например, начальный символ тега "<" следует экранировать как "<". Некоторые стандартные методы .NET для работы с XML, например, наследники 'XNode', реализуют экранирование записываемых в XML-дерево данных. Также .NET предоставляет отдельные от инфраструктуры XML классы для обеспечения безопасности. Пример более безопасного кода, использующего кодирование:
public class InputHelper
{
HttpRequest Request {get; set;}
Logger logger;
string EscapeCharsForXmlLog(string userInput)
{
return SecurityElement.Escape(userInput);
}
void ProcessUserInput()
{
string userInput = Request["userID"];
userInput = EscapeCharsForXmlLog(userInput);
logger.Info(userInput); // <=
....
}
}
Ещё один пример: стандарт JSON запрещает наличие null-символов ("\0") в файлах. Если злоумышленник внедрит этот символ, это может сломать процесс сохранения или просмотра готовых логов. Null-символ следует экранировать как "\u0000".
Другой пример: если логи хранятся в реляционной СУБД, использующей SQL, отсутствие проверок входных данных может привести к SQL-инъекции (подробнее: диагностика V5608).
Также анализатор считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов". Рассмотрим пример:
public class InputHelper
{
Logger logger;
public void ProcessInput(string input)
{
Log("Input logged:" + input);
}
private void Log(string input)
{
logger.Log(LogLevel.Information, input);
}
}
В этом случае анализатор выдаст предупреждение низкого уровня достоверности на вызов метода 'Log'. Заражённый результат конкатенации строк используется для логирования.
Сделать этот код более безопасным можно также, как в примере выше — кодированием строки:
public class InputHelper
{
Logger logger;
public void ProcessInput(string input)
{
Log(SecurityElement.Escape("Input logged:" + input));
}
private void Log(string input)
{
logger.Log(LogLevel.Information, input);
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что потенциально заражённые данные используются для формирования фильтра LDAP-запроса. Это может стать причиной LDAP-инъекции в случае, если данные будут скомпрометированы. По своей сути данная атака похожа на SQL-инъекции.
Уязвимости типа LDAP-инъекции относятся к категории рисков OWASP Top 10 Application Security Risks 2021: A3:2021-Injection.
Рассмотрим пример:
public void Search()
{
....
string user = textBox.Text;
string password = pwdBox.Password;
DirectoryEntry de = new DirectoryEntry();
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = $"(&(userId={user})(UserPassword={password}))";
search.PropertiesToLoad.Add("mail");
search.PropertiesToLoad.Add("telephonenumber");
SearchResult sresult = search.FindOne();
if(sresult != null)
{
....
}
....
}
В данном примере формируется фильтр поиска для предоставления некоторых личных данных пользователю, обладающему подходящими логином и паролем. В фильтре используются значения переменных 'user' и 'password', полученные из внешнего источника. Использование данных подобным образом опасно, так как даёт злоумышленнику возможность подделки фильтра поиска.
Для лучшего понимания атаки приведём несколько примеров.
Если в 'user' будет записано "PVS", а в 'password' – "Studio", то получится следующий запрос:
LDAP query: (&(userId=PVS)(UserPassword=Studio))
В этом случае мы получили от пользователя ожидаемые данные и если такая комбинация пользователя и пароля существует, то будет предоставлен доступ.
Но допустим, что в переменных 'user' и 'password' будут записаны следующие значения:
user: PVS)(userId=PVS))(|(userId=PVS)
password: Any
При подстановке этих строк в шаблон получится следующий фильтр:
LDAP query: (&(userId=PVS)(userId=PVS))(|(userId=PVS)(UserPassword=Any))
При использовании такого фильтра поиска доступ будет предоставлен в любом случае, даже если злоумышленник введёт неверный пароль. Это произойдёт из-за того, что LDAP будет обрабатывать только первый фильтр, а (|(userId=PVS)(UserPassword=Any)) просто проигнорирует.
Чтобы защититься от подобной атаки, стоит проводить валидацию всех входных данных или экранировать все специальные символы в данных, которые приходят от пользователей. Существуют методы, которые автоматически экранируют все небезопасные значения.
Пример кода с использованием метода автоматического экранирования из пространства имён 'Microsoft.Security.Application.Encoder':
public void Search()
{
....
string user = textBox.Text;
string password = pwdBox.Password;
DirectoryEntry de = new DirectoryEntry();
DirectorySearcher search = new DirectorySearcher(de);
user = Encoder.LdapFilterEncode(user);
password = Encoder.LdapFilterEncode(password);
search.Filter = $"(&(userId={user})(UserPassword={password}))";
search.PropertiesToLoad.Add("mail");
search.PropertiesToLoad.Add("telephonenumber");
SearchResult sresult = search.FindOne();
if (sresult != null)
{
....
}
....
}
Анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Рассмотрим пример:
public class LDAPHelper
{
public void Search(string userName)
{
var filter = "(&(objectClass=user)(employeename=" + userName + "))";
ExecuteQuery(filter);
}
private void ExecuteQuery(string filter)
{
DirectoryEntry de = new DirectoryEntry();
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = filter;
search.PropertiesToLoad.Add("mail");
search.PropertiesToLoad.Add("telephonenumber");
SearchResult sresult = search.FindOne();
if (sresult != null)
{
....
}
}
}
В данном случае анализатор выдаст предупреждение низкого уровня достоверности при анализе исходного кода метода 'Search' на вызов 'ExecuteQuery'. PVS-Studio отследил передачу небезопасных данных из параметра 'userName' в переменную 'filter' и после в 'ExecuteQuery'.
Защита в таком случае не отличается от приведённой ранее.
public class LDAPHelper
{
public void Search(string userName)
{
userName = Encoder.LdapFilterEncode(userName);
var filter = "(&(objectClass=user)(employeename=" + userName + "))";
ExecuteQuery(filter);
}
private void ExecuteQuery(string filter)
{
DirectoryEntry de = new DirectoryEntry();
DirectorySearcher search = new DirectorySearcher(de);
search.Filter = filter;
....
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил раскрытие потенциально чувствительных данных, содержащихся в сообщении об ошибке. К таким данным относятся сообщения и стеки исключений.
Ошибки, связанные с неявным раскрытием чувствительных данных, относятся к категории A04:2021 – Insecure Design списка OWASP Top 10 Application Security Risks.
Рассмотрим пример уязвимости:
public void Foo(string value)
{
try
{
int intVal = int.Parse(value);
....
}
catch (Exception e)
{
Console.WriteLine(e.StackTrace); // <=
}
}
Не рекомендуется показывать пользователям стеки исключений. Это может привести к раскрытию информации о деталях реализации проекта. Например, могут быть раскрыты названия используемых в проекте библиотек, а они могут содержать известные уязвимости. Злоумышленник может использовать эту информацию для атаки на проект.
Также стек исключения для стандартных классов исключений .NET может быть раскрыт через метод 'ToString':
public void Foo(string value)
{
try
{
int intVal = int.Parse(value);
....
}
catch (Exception e)
{
Console.WriteLine(e.ToString()); // <=
}
}
Следует помнить, что 'ToString' вызывается внутри методов вывода, принимающих 'object' в качестве аргумента:
Console.WriteLine(e);
Решением этой проблемы архитектуры может быть предотвращение вывода чувствительной информации пользователю. Например, можно использовать ресурсы, явно связанные с исключениями, но не содержащие чувствительной информации. Простой пример с использованием 'enum':
enum ErrorCode
{
/// <summary>
/// ArgumentNull exception occurred
/// </summary>
ArgumentNull,
....
Unknown
}
public void Foo(string value)
{
try
{
int intVal = int.Parse(value);
....
}
catch (Exception e)
{
ErrorCode errCode = e switch
{
ArgumentNullException => ErrorCode.ArgumentNull,
....
_ => ErrorCode.Unknown
};
Console.WriteLine("An error has occurred: " + errCode); // <=
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что для формирования выражения на языке XPath используются непроверенные данные из внешнего источника. Это может стать причиной возникновения XPath-инъекции.
Уязвимости, связанные с инъекциями, относятся к категории A03:2021 – Injection списка OWASP Top 10 Application Security Risks.
Рассмотрим пример:
class UserData
{
HttpRequest request;
XPathNavigator navigator;
void RetrieveUserData()
{
string username = request.Form["username"];
string password = request.Form["password"];
string hashedPassword = Hash(password);
string query = $@"//users/user[
username/text() = '{username}' and
passwordHash/text() = '{hashedPassword}']
/data/text()";
object res = navigator.Evaluate(query);
....
}
}
В этом примере XPath-выражение используется для получения данных пользователя из XML-файла. Имя пользователя хранится "как есть", а пароль хранится в зашифрованном виде.
Злоумышленник может передать в качестве имени пользователя и пароля любые данные. Проверка будет скомпрометирована, если во входных данных передать выражение, которое сделает XPath-условие всегда истинным. Так как пароль хранится в зашифрованном виде, то внедрять опасное выражение нужно вместе с именем пользователя.
Для примера пусть имя пользователя будет 'john'. Добавим к нему выражение такого вида:
' or ''='
Вместо пароля может быть введён любой набор символов. Тогда XPath-выражение будет иметь такой вид:
[
username/text()='john' or ''='' and
passwordHash/text() = '750084105bcbe9d2c89ba9b'
]
Теперь выражение содержит оператор 'or'. Рассмотрим, как оно вычисляется:
Таким образом, результатом XPath-запроса будут данные пользователя 'john' независимо от того, правильный пароль был введён или нет. Это может привести к утечке данных.
Не следует использовать непроверенные внешние данные в XPath-выражениях. Для повышения безопасности стоит экранировать потенциально опасные символы во внешних данных. Примерами таких символов являются "<", ">" и "'". Экранирование может быть произведено с помощью метода 'SecurityElement.Escape':
class UserData
{
HttpRequest request;
XPathNavigator navigator;
void RetrieveUserData()
{
string username = request.Form["username"];
string password = request.Form["password"];
username = SecurityElement.Escape(username);
string hashedPassword = Hash(password);
string query = $@"//users/user[
username/text()= '{username}' and
passwordHash/text() ='{hashedPassword}']
/data/text()";
object res = navigator.Evaluate(query);
....
}
}
Существуют другие возможности для предотвращения XPath-инъекций. Например, Microsoft предлагает возможность реализации класса-резолвера. Его можно использовать в методах класса 'XPathNavigator', которые принимают строку XPath-выражения и объект, реализующий интерфейс 'IXmlNamespaceResolver'.
Это позволяет определить свои собственные переменные и функции внутри XPath-выражения, которые будут обработаны резолвером. Само по себе это не является решением проблемы XPath-инъекции, но определение своиx переменных позволяет использовать подход, похожий на параметризацию SQL-запросов.
Также анализатор считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов". Рассмотрим пример:
public class UserData
{
XPathNavigator navigator;
public object RetrieveUserData(string username,
string password)
{
string hashedPassword = Hash(password);
string query = $@"//users/user[
username/text()= '{username}' and
passwordHash/text() = '{hashedPassword}']
/data/text()";
return EvaluateXpath(query);
}
private object EvaluateXpath(string xpath)
{
object res = navigator.Evaluate(xpath);
....
}
}
В этом примере метод 'RetrieveUserData' доступен из других сборок. Параметры этого метода 'username' и 'password' не проверяются перед внедрением в XPath-запрос. Полученное выражение в переменной 'query' передаётся в метод 'EvaluateXpath', где используется без проверки. В таком случае анализатор выдаст предупреждение с низким уровнем достоверности.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил перенаправление с одного ресурса на другой. Причём URL-адрес для перенаправления был получен из внешнего источника и не был проверен. Это может стать причиной возникновения уязвимости типа open redirect, если адрес будет скомпрометирован.
Уязвимости типа open redirect относятся к категории рисков OWASP Top 10 Application Security Risks 2021: A1:2021- Broken Access Control.
Приведём пример:
void Foo()
{
string url = Request.QueryString["redirectUrl"];
....
if (loggedInSuccessfully)
Response.Redirect(url);
}
В этом примере 'url' может содержать заражённые данные, так как они получены из внешнего источника. Эти данные используются для перенаправления клиента на адрес, который записан в 'url'. Подобная логика работы программы упрощает проведение фишинговых атак для кражи данных пользователя.
Пример скомпрометированного адреса:
URL: http://mySite.com/login?redirectUrl=http://attacker.com/
Возможный сценарий проведения атаки:
Главная опасность open redirect состоит в том, что ссылка, полученная от злоумышленника, фактически ведёт на сайт, которому пользователь доверяет. Следовательно, жертва с большей вероятностью перейдёт по ней.
Для защиты open redirect стоит проверять, что перенаправление осуществляется на локальный адрес или на адрес из белого списка.
Приведём пример борьбы с уязвимостью типа open redirect. Используя метод 'IsLocalUrl' из пространства имён 'Microsoft.AspNet.Membership.OpenAuth' можно проверить, что адрес является локальным:
void Foo()
{
string url = Request.QueryString["url"];
if (OpenAuth.IsLocalUrl(url))
Response.Redirect(url);
else
throw ....;
}
Происходит проверка, что полученный URL-адрес является локальным и только в этом случае осуществляется переход по нему.
Анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Рассмотрим пример:
public class UriHelper
{
public void ProcessUrlQuery(HttpResponse resp, string url)
{
RedirectUrl(url, resp);
}
private void RedirectUrl(string redirectUrl, HttpResponse resp)
{
resp.Redirect(redirectUrl);
}
}
Анализатор обнаружит, что небезопасные данные из параметра 'url' передаются в метод 'RedirectUrl', внутри которого они без проверки используются для перенаправления.
Защититься в данном случае можно тем же способом, что был приведён ранее.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что для конфигурации используются данные из внешнего источника. Это может привести к возникновению дефекта безопасности.
Уязвимости такого типа относятся к категории рисков OWASP Top 10 Application Security Risks 2021: A5:2021 - Security Misconfiguration.
Приведём пример:
public void ExecuteSqlQuery(....)
{
....
string catalog = Request.QueryString["catalog"];
using (SqlConnection dbConnection = IO.GetDBConnection())
{
dbConnection.ConnectionString = $"Data Source=....; " +
$"Initial Catalog={catalog}; " +
$"User ID=....; " +
$"Password=....;";
....
}
....
}
В этом примере формируется строка подключения к БД. В параметр 'Initial Catalog' записываются данные, не прошедшие никакой валидации, благодаря чему злоумышленник может передать любое название каталога. Подобным образом он может получить информацию, доступ к которой не был предусмотрен.
Для защиты от подобных атак стоит производить валидацию входных данных. Пример корректного формирования строки подключения:
public void ExecuteSqlQuery(...., HashSet<string> validCatalogNames)
{
....
string catalog = Request.QueryString["catalog"];
if(!validCatalogNames.Contains(catalog))
return;
using(SqlConnection dbConnection = IO.GetDBConnection())
{
dbConnection.ConnectionString = $"Data Source=....; " +
$"Initial Catalog={catalog}; " +
$"User ID=....; " +
$"Password=....;";
....
}
....
}
В данном случае проверяется, что 'catalog' содержится в коллекции 'validCatalogNames'. Таким образом, пользователь будет иметь доступ лишь к определённому перечню каталогов, что не позволит получить приватную информацию.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил, что в проекте используются сборки, которые могут содержать известные уязвимости. Использование таких зависимостей может привести к появлению уязвимостей в самом приложении.
Данной проблеме посвящена категория A6: Vulnerable and Outdated Components из списка OWASP Top Ten 2021.
К примеру, в проекте может использоваться сборка log4net версии 1.2.13. Это единственная сборка в пакете log4net 2.0.3, который содержит уязвимость CVE-2018-1285. Следовательно, работа с функционалом этой зависимости может привести к появлению уязвимостей и в использующем её проекте.
Анализатор также исследует и транзитивные зависимости, то есть зависимости библиотек, от которых зависит проект.
К примеру, одной из зависимостей пакета RepoDb.MySql 1.1.4 является MySql.Data версии 8.0.22 и выше. В свою очередь MySql.Data 8.0.22 зависит от пакетов Google.Protobuf (версии 3.11.4 и выше) и SSH.NET (версии 2016.1.0 и выше).
Анализатору известно, что:
Таким образом, любой проект, использующий RepoDb.MySql 1.1.4, может транзитивно зависеть от уязвимых сборок SSH.NET и Google.Protobuf. Уязвимости в таких зависимостях также могут привести к различным проблемам в работе приложения. Тем не менее, вероятность этого меньше, чем в случае с прямыми зависимостями, поэтому уровень достоверности предупреждения будет ниже.
Если зависимость проекта содержит уязвимость, то для обеспечения безопасности необходимо как-то от неё избавиться или защитить себя от возможных рисков. Для этого в первую очередь необходимо определить, каким образом проект связан с уязвимой библиотекой или пакетом.
Если проект ссылается на зависимость напрямую, то её название (и, возможно, версия) будет явно прописано в списке зависимостей проекта. К примеру, в случае с пакетами такую зависимость можно будет найти в окне NuGet Package Manager на вкладке списка установленных пакетов. В Visual Studio это окно выглядит следующим образом:
Для непрямых (транзитивных) зависимостей может понадобиться отследить цепочку пакетов или библиотек, которые "соединяют" проект и уязвимую зависимость. Для этого можно использовать различные программные средства. К примеру, в среде Visual Studio 2022 есть возможность поиска среди внешних компонентов:
Используя эту возможность, можно найти полную цепочку зависимостей от проекта до уязвимого пакета. Похожий функционал поиска присутствует и в среде JetBrains Rider.
Для получения иерархии зависимостей сборок могут быть полезны такие средства, как JetBrains dotPeek. В нём нужно открыть интересующую сборку и в контекстном меню выбрать пункт References Hierarchy:
На открывшейся панели будет отображено дерево зависимостей выбранной сборки:
Ниже представлены возможные варианты решения проблем с найденными уязвимыми зависимостями.
Как правило, уязвимость присутствует лишь в некоторых версиях пакета или сборки. Если проект зависит от уязвимого компонента напрямую, то возможно, стоит использовать другую его версию.
Отдельно стоит рассмотреть ситуацию, когда проект использует уязвимую зависимость не напрямую. К примеру, проект зависит от сборки "A", зависящей от уязвимой сборки "B".
Если у "B" есть безопасные версии, то можно попробовать одно из следующих решений:
Похожим образом можно решать проблемы и с более "глубокими" зависимостями – к примеру, если проект зависит от сборки "A", которая зависит от сборки "B", зависящей от уязвимой сборки "C".
Если безопасные версии пакета/сборки отсутствуют, или их обновление невозможно по каким-то другим причинам, то, возможно, стоит рассмотреть использование другой библиотеки.
Ситуация несколько сложнее, если проект зависит от уязвимой библиотеки не напрямую. В этом случае замене подлежит прямая зависимость, из-за которой проект становится зависимым от уязвимой библиотеки. То есть если проект зависит от библиотеки "A", которая зависит от уязвимой библиотеки "B", то заменить придётся библиотеку "A".
Если предыдущие варианты решения не подходят, то необходимо понять, в чём состоит уязвимость используемой библиотеки, и как она может повлиять на работу приложения. Используя эту информацию, нужно внести в код приложения правки, защищающие от эксплуатации уязвимости. Например, можно добавить дополнительную валидацию входных данных или отказаться от использования части функционала зависимости в пользу более безопасных методов.
Скорее всего, в этом случае предупреждения диагностического правила будет нужно подавить. Данной теме посвящён следующей раздел.
Так как V5625 является диагностикой уровня проекта, то её срабатывания не связаны с каким-то конкретным фрагментом кода. Поэтому предупреждения этой диагностики нельзя пометить как ложные с помощью добавления в код комментария типа "//-V5625". Также на данный момент отсутствует возможность baselining-а (подавления) таких срабатываний с помощью suppress-файлов.
Для подавления сообщений V5625 используйте файл конфигурации диагностик – текстовый файл с расширением pvsconfig, добавляемый в проект либо solution.
Например, в Visual Studio для добавления файла конфигурации нужно выделить проект или solution и в контекстном меню выбрать пункт 'Add New Item...'. В появившемся окне нужно выбрать 'PVS-Studio Filters File'.
Файл конфигурации, добавленный в проект, действует на все файлы данного проекта. Файл конфигурации, добавленный в solution, действует на все файлы всех проектов, добавленных в данный solution.
Для подавления срабатывания диагностики V5625 на конкретную библиотеку можно добавить в pvsconfig строку следующего вида:
//-V::5625::{Google.Protobuf 3.6.1}
Тогда анализатор перестанет выдавать срабатывания V5625, содержащие в сообщении подстроку "Google.Protobuf 3.6.1".
Также есть возможность указать уровень:
//-V::5625:2:{Google.Protobuf 3.6.1}
В этом случае диагностическое правило не будет выдавать срабатывание, если оно имеет второй уровень (Medium), и его сообщение содержит подстроку "Google.Protobuf 3.6.1".
Более подробное описание pvsconfig файлов приведено в документации.
Данная диагностика классифицируется как:
Анализатор обнаружил применение потенциально опасного регулярного выражения для обработки данных из внешнего источника. Это может сделать приложение уязвимым к ReDoS-атаке.
ReDoS – отказ в обслуживании, причиной которого стало уязвимое регулярное выражение. Цель злоумышленника при проведении ReDoS-атаки – передать в регулярное выражение строку, оценка которой потребует максимального количества времени.
Регулярное выражение является уязвимым, если соответствует следующим условиям:
Таким образом, при получении предупреждения данной диагностики, следует проверить регулярное выражение на наличие подвыражений вида:
Здесь 'a', 'b', 'c' могут быть:
Также важно, чтобы после этих подвыражений было хотя бы одно подвыражение, не помеченное кванторами '?' или '*'. Например: '(x+)+y', '(x+)+$', '(x+)+(...)', ' (x+)+[...]' и т. д.
Разберем проблему этих выражений на примере '(x+)+y'. В этом выражении шаблону 'x+' может соответствовать любое количество символов 'x'. Строка, которая соответствует шаблону '(x+)+y', состоит из любого количества подстрок, сопоставленных с 'x+'. Как следствие, появляется большое множество вариантов сопоставлений одной и той же строки с регулярным выражением.
Несколько вариантов сопоставлений строки 'xxxx' с шаблоном '(x+)+y' продемонстрированы в таблице ниже:
Каждый раз, когда регулярному выражению не удаётся найти символ 'y' в конце строки, оно начинает проверку следующего варианта. Лишь проверив их все, регулярное выражение даст ответ – совпадений не найдено. Однако время выполнения этого процесса может оказаться катастрофически большим в зависимости от длины подстроки, соответствующей уязвимому паттерну.
График ниже отражает зависимость времени вычисления регулярного выражения (x+)+y от количества символов во входных строках вида 'xx....xx':
Рассмотрим пример кода:
Regex _datePattern = new Regex(@"^(-?\d+)*$");
public bool IsDateCorrect(string date)
{
if (_datePattern.IsMatch(date))
....
}
В этом примере дата проверяется с помощью регулярного выражения. Если дата корректна, регулярное выражение отработает так, как и ожидалось. Ситуация изменится, если в качестве даты приложение получит следующую строку:
3333333333333333333333333333333333333333333333333333333333333 Hello ReDoS!
В этом случае обработка регулярным выражением займёт очень много времени. Поступление нескольких запросов с подобными данными может создать сильную нагрузку на приложение.
Возможное решение – ограничить время обработки регулярным выражением входной строки:
Regex _datePattern = new Regex(@"^(-?\d+)*$",
RegexOptions.None,
TimeSpan.FromMilliseconds(10));
Рассмотрим ещё один пример. В регулярном выражении намеренно добавлено подвыражение '(\d|[0-9]?)', чтобы показать суть проблемы.
Regex _listPattern = new Regex(@"^((\d|[0-9]?)(,\s|\.))+$(?<=\.)");
public void ProcessItems(string path)
{
using (var reader = new StreamReader(path))
{
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
if (line != null && _listPattern.IsMatch(line))
....
}
}
}
Здесь данные считываются из файла и проверяются регулярным выражением на соответствие следующему паттерну: строка должна представлять собой список, каждый элемент которого является цифрой или пустой строкой. Корректный ввод может выглядеть так:
3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4, 5, 3, 4.
При обработке таких данных регулярное выражение отработает за нормальное время. Однако, если передать ту же строку, но без точки в конце, приложение затратит на обработку данных намного больше времени.
В регулярном выражении используются подвыражения '\d' и '[0-9]?', которые могут сопоставляться с одними и теми же значениями. Обратите внимание, что ко второму подвыражению применяется квантор '? ', а к родительскому подвыражению '((\d|[0-9]?)(,\s|\.))' – квантор '+'. Это приводит к появлению большого количества возможных сопоставлений в строке. Если бы не было хотя бы одного из этих двух кванторов, ReDoS-уязвимости не возникло бы.
В данном примере для устранения ReDoS-уязвимости достаточно убрать лишнее сопоставление:
Regex _listPattern = new Regex(@"^([0-9]?(,\s|\.))+$(?<=\.)");
Еще больше узнать о ReDoS-уязвимостях можно, к примеру, на сайте OWASP.
Устранить ReDoS-уязвимость можно несколькими способами. Рассмотрим их на примере регулярного выражения '^(-?\d+)*$'.
Способ 1. Добавить ограничение на время обработки строки регулярным выражением. Это можно сделать, задав параметр 'matchTimeout' при создании объекта 'Regex' или при вызове статического метода:
RegexOptions options = RegexOptions.None;
TimeSpan timeout = TimeSpan.FromMilliseconds(10);
Regex datePattern = new Regex(@"^(-?\d+)*$", options, timeout);
Regex.IsMatch(date, @"^(-?\d+)*$", options, timeout);
Способ 2. Использовать атомарные группы '(?>...)'. Атомарные группы отключают поиск всех возможных комбинаций символов, соответствующих подвыражению, ограничиваясь лишь одной:
Regex datePattern = new Regex(@"^(?>-?\d+)*$");
Способ 3. Переписать регулярное выражение, убрав опасный паттерн. Предположим, что выражение '^(-?\d+)*$' предназначено для поиска даты вида '27-09-2022', в этом случае его можно заменить на более надёжный аналог:
Regex datePattern = new Regex (@"^(\d{2}-\d{2}-\d{4})$");
В этом варианте любая подстрока сопоставляется не более чем с одним подвыражением из-за обязательной проверки символа '-' между шаблонами '\d{...}'.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил использование непроверенных данных из внешнего источника для формирования запроса к NoSQL базе данных. Это может стать причиной NoSQL-инъекции в случае, если данные скомпрометированы.
Инъекции выделены в отдельную категорию рисков в OWASP Top 10 Application Security Risks 2021: A3:2021-Injection.
Рассмотрим пример:
public IFindFluent<BsonDocument, BsonDocument> Authentication()
{
String log = Request.Form["login"];
String pass = Request.Form["password"];
String filter = "{$where: \"function() {" +
$"return this.login=='{log}' && this.password=='{pass}'"+
";}\"}";
return collection.Find(filter);
}
Метод 'Authentication' ищет запись пользователя в NoSQL базе данных MongoDB по логину и паролю. Для этого создается строка 'filter', содержащая JavaScript код. С её помощью будут фильтроваться результаты поиска. Аналогом этой операции в SQL будет следующий запрос: SELECT * FROM collection WHERE login = @log AND password = @pass.
Для формирования фильтра используются значения строк 'log' и 'pass', полученные из внешнего источника. Подобное использование непроверенных данных позволяет злоумышленнику внедрить вредоносный код внутрь запроса.
Например, здесь злоумышленник может отправить вместо ожидаемого значения 'pass' строку следующего вида:
"-1' || this.login == 'admin"
Тогда обращение к базе данных может выглядеть так:
{$where: "function()
{
return this.login == 'doesn't matter'
&& this.password == '-1'
|| this.login == 'admin';
}"}
В таком случае запрос вернёт данные аккаунта администратора.
Для защиты от инъекций NoSQL базы предоставляют инструменты параметризированного создания запросов.
Пример создания безопасного запроса:
public IFindFluent<BsonDocument, BsonDocument> Authentication()
{
String log = Request.Form["login"];
String pass = Request.Form["password"];
var filter = Builders<BsonDocument>.Filter.Eq("login", log)
& Builders<BsonDocument>.Filter.Eq("password", pass);
return collection.Find(filter);
}
Здесь фильтр создается при помощи специального класса 'Builders'. За счёт этого запрос будет параметризованным и внешние данные не смогут повлиять на логику фильтрации.
Анализатор также считает источниками небезопасных данных параметры методов, доступных из других сборок. Более подробно эта тема раскрыта в заметке "Почему важно проверять значения параметров общедоступных методов".
Рассмотрим пример:
public class MongoDBRep
{
public void DeleteItemsByCounter(string count)
{
DeleteMany(count);
}
private void DeleteMany(string count)
{
var filter = "{$where:\"function(){return this.count == "+count+";}\"}";
collection.DeleteMany(filter);
}
}
Здесь потенциально заражённые данные из параметра 'count' передаются в метод 'DeleteMany', внутри которого они без проверки используются для удаления записей из базы данных.
Злоумышленник может сформировать запрос следующего вида:
{$where: "function()
{
return this.count == -999
|| 1 == 1;
}"}
Исполнение этого запроса приведёт к удалению всех документов базы данных, независимо от значения поля 'count'.
В этом случае рекомендуется защититься тем же способом, что был приведён ранее.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил операцию извлечения файла по непроверенному пути, включающему имя файла. В случае наличия в имени файла "dot-dot-slash" последовательностей эта операция приведет к возникновению уязвимости Zip Slip в приложении.
Zip Slip проводится через передачу в приложение архива с вредоносными файлами внутри: они содержат в имени "dot-dot-slash" последовательности ("../../evil.csx"). Спровоцировав распаковку такого архива, злоумышленник может переписать любые файлы, к которым у приложения есть доступ.
В большинстве архиваторов или операционных систем создать файл с названием вида '../../evil.csx' не получится из-за встроенных ограничений. Тем не менее, существуют инструменты, допускающие эту операцию. Из-за этого атака Zip Slip и становится возможной.
Рассмотрим пример небезопасного кода:
public void ExtractArchive(ZipArchive archive, string destinationDirectory)
{
var entries = archive.Entries;
foreach (var entry in entries)
{
var extractPath = Path.Combine(destinationDirectory, entry.FullName);
entry.ExtractToFile(extractPath, true);
}
}
Здесь внутри цикла файлы поочередно извлекаются из архива в директорию, расположенную по пути 'destinationDirectory'. Для каждого файла создаётся путь распаковки с помощью метода 'Path.Combine', после чего результат записывается в переменную 'extractPath'. Далее 'extractPath' используется в качестве аргумента метода 'entry.ExtractToFile', выполняющего разархивирование файла по заданному пути.
Предположим, что архив должен быть извлечён в директорию 'C:\ApplicationFiles\UserFiles'. Однако если свойство 'entry.FullName' вернёт строку вида '\..\config.ini', файл попадёт в корневой каталог приложения 'C:\ApplicationFiles'. В случае совпадения имени извлекаемого файла и, например, файла конфигурации приложения, будет выполнена перезапись последнего.
Обезопасить код в предыдущем примере можно, например, следующим образом:
public void ExtractArchive(ZipArchive archive, string destinationDirectory)
{
var destinationDirectoryFullPath = Path.GetFullPath(destinationDirectory);
foreach (var entry in archive.Entries)
{
var extractPath = Path.Combine(destinationDirectory, entry.FullName);
var extractFullPath = Path.GetFullPath(extractPath);
if (!extractFullPath.StartsWith(destinationDirectoryFullPath))
{
throw new IOException("Zip Slip vulnerability");
}
entry.ExtractToFile(extractFullPath);
}
}
Здесь в переменную 'extractFullPath' записывается результат обработки пути 'extractPath' методом 'Path.GetFullPath'. В ходе этой операции путь, содержащий "dot-dot-slash" последовательности, будет заменен на аналогичный, не включающий их.
После этого с помощью метода 'extractFullPath.StartsWith' проверяется, не изменилась ли директория для распаковки файла в результате предыдущей операции. В случае если произошла подмена директории, выбрасывается исключение.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки непроверенного использования чувствительных данных (ввода пользователя, файлов, сети и пр.). |
Данная диагностика классифицируется как:
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит логическую ошибку. В тексте программы имеется оператор (<, >, <=, >=, ==, !=, &&, ||, -, /, &, |, ^), слева и справа от которого расположены одинаковые подвыражения.
Рассмотрим пример:
if (a.x != 0 && a.x != 0)
В данном случае оператор '&&' окружен одинаковыми подвыражениями "a.x != 0", что позволяет обнаружить ошибку, допущенную по невнимательности. Корректный код, который не вызовет подозрений у анализатора, будет выгладить так:
if (a.x != 0 && a.y != 0)
Анализатор производит сравнение блоков с учетом перестановки частей выражения относительно операторов. Ошибка будет обнаружена и в следующем примере кода:
if (a.x > a.y && a.y < a.x)
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6001. |
Анализатор обнаружил оператор 'switch', в котором выбор варианта осуществляется по переменной enum-типа. При этом в операторе 'switch' используются не все элементы перечисления, что может свидетельствовать о наличии ошибки.
Пример кода, в котором встретилась эта ошибка:
enum Fruit { APPLE, BANANA, PEAR, PINEAPPLE, ORANGE }
int SomeMethod(Fruit fruit)
{
int res = 0;
switch (fruit)
{
case APPLE:
res = calculate(10); break;
case BANANA:
res = calculate(20); break;
case PEAR:
res = calculate(30); break;
case PINEAPPLE:
res = calculate(40); break;
}
// code
return res;
}
В данном случае перечисление 'Fruit' содержит 5 именованных констант, а оператор 'switch', выбор в котором осуществляется по данному перечислению, реализует выбор только по 4 из них. Это место является потенциально ошибочным.
Возможно, в ходе рефакторинга в перечисление добавили новую константу, но забыли реализовать выбор по ней в операторе 'switch', или же, что может быть в случае с большими перечислениями, константу банально могли пропустить. В итоге пропущенное значение обрабатывается неправильно.
Тогда корректный код мог бы выглядеть следующим образом:
int SomeMethod(Fruit fruit)
{
int res = 0;
switch (fruit)
{
case APPLE:
res = calculate(10); break;
case BANANA:
res = calculate(20); break;
case PEAR:
res = calculate(30); break;
case PINEAPPLE:
res = calculate(40); break;
case ORANGE:
res = calculate(50); break;
}
// code
return res;
}
Анализатор выдает предупреждение далеко не всегда, когда в 'switch' используется не все константы из перечисления. Иначе, было бы слишком много ложных срабатываний. Действует целый ряд исключений эмпирического типа. Основные:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6002. |
Анализатор обнаружил потенциально возможную ошибку в конструкции, состоящей из условных операторов.
Рассмотрим пример:
if (a == 1)
Foo1();
else if (a == 2)
Foo2();
else if (a == 1)
Foo3();
В данном примере метод 'Foo3()' никогда не получит управления. Вероятно, мы имеем дело с логической ошибкой и корректный код должен выглядеть так:
if (a == 1)
Foo1();
else if (a == 2)
Foo2();
else if (a == 3)
Foo3();
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6003. |
Анализатор обнаружил подозрительный фрагмент кода, в котором истинная и ложная ветви оператора 'if' полностью совпадают. Часто это свидетельствует о наличии ошибки.
Пример подобного кода:
if (condition)
result = FirstFunc(val);
else
result = FirstFunc(val);
Вне зависимости от значения переменной, будут выполнены одни и те же операции. Понятно, что такой код является ошибочным. Тогда корректный вариант кода мог бы выглядеть следующим образом:
if (condition)
result = FirstFunc(val);
else
result = SecondFunc(val);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6004. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что значение переменной присваивается само себе.
Рассмотрим пример:
void change(int width, int height, int length)
{
this.mWidth = width;
this.mHeight = height;
this.mLength = this.mLength;
}
Из кода видно, что предполагалось изменить значения свойств объекта в соответствии с принятыми в методе параметрами, но произошла ошибка и свойству 'mLength' вместо значения аргумента 'length' присвоилось значение самого же свойства.
Тогда корректный код должен был бы выглядеть так:
void change(int width, int height, int length)
{
this.mWidth = width;
this.mHeight = height;
this.mLength = length;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6005. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что создаётся экземпляр класса исключения, но при этом никак не используется.
Пример ошибочного кода:
int checkIndex(int index)
{
if (index < 0)
new IndexOutOfBoundsException("Index Out Of Bounds!!!");
return index;
}
В данном коде пропущен оператор 'throw', из-за чего будет только создан экземпляр класса, но при этом он никак не будет использоваться, и исключение не будет сгенерировано. Корректный код может выглядеть следующим образом:
int checkIndex(int index)
{
if (index < 0)
throw new IndexOutOfBoundsException("Index Out Of Bounds!!!");
return index;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6006. |
Анализатор выявляет некорректные условные выражения, которые при вычислении всегда являются истинными или ложными.
Случай 1.
Некорректно сформированное условие является всегда истинным или ложным.
Подобные условия не всегда означают наличие ошибки, но эти фрагменты кода следует обязательно проверить.
Пример некорректного кода:
String str = ...;
if (!str.equals("#") || !str.isEmpty()){
...
} else {
...
}
Здесь ветка 'else' никогда не будет выполнена. Дело в том, что какое бы значение ни приняла переменная 'str', одно из сравнений со строкой всегда будет истинно. Чтобы исправить эту ошибку, следует использовать оператор && вместо оператора ||.
Корректный вариант кода:
String str = ...;
if (!str.equals("#") && !str.isEmpty()){
...
} else {
...
}
Случай 2.
Два условных оператора, идущих последовательно, содержат взаимоисключающие условия.
Примеры взаимоисключающих условий:
Такая ошибка может возникнуть в результате опечатки или неудачного рефакторинга.
Пример некорректного кода:
if (x == y)
if (y != x)
DoSomething(x, y);
В данном случае метод 'DoSomething' никогда не будет вызван, так как при истинности первого условия второе всегда будет ложным. Возможно, в сравнении используется некорректная переменная. Например, во втором условии следовало использовать не 'x', а 'z':
if (x == y)
if (y != z)
DoSomething(x, y);
Случай 3.
В выражении ищется более длинная подстрока и более короткая. При этом, более короткая строка является частью более длинной. Получается, что одно из сравнений избыточно или содержит какую-нибудь ошибку.
Рассмотрим пример:
if (str.contains("abc") || str.contains("abcd"))
В случае, если подстрока "abc" будет найдена, то дальнейшая проверка не будет выполняться. Если подстрока "abc" не будет найдена, то и поиск более длинной подстроки "abcd" не имеет смысла.
Для исправления ошибки необходимо проверить правильность подстрок или убрать из кода лишние проверки. Пример корректного варианта:
if (str.contains("abc"))
Другой пример:
if (str.contains("abc"))
Foo1();
else if (str.contains("abcd"))
Foo2();
В данном случае метод 'Foo2' никогда не будет вызван. Устранить ошибку можно путем замены порядка проверки. То есть сначала следует искать более длинную подстроку, а потом более короткую:
if (str.contains("abcd"))
Foo2();
else if (str.contains("abc"))
Foo1();
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6007. |
Анализатор обнаружил фрагмент кода, который может привести к доступу по нулевой ссылке.
Рассмотрим несколько примеров, для которых анализатор выдает диагностическое сообщение V6008:
if (obj != null || obj.isEmpty()) { ... }
if (obj == null && obj.isEmpty()) { ... }
Во всех условиях допущена логическая ошибка, которая приведет к доступу по нулевой ссылке. Ошибка может быть допущена при рефакторинге кода или из-за случайной опечатки.
Корректные варианты:
if (obj == null || obj.isEmpty()) { .... }
if (obj != null && obj.isEmpty()) { .... }
Конечно, это очень простые ситуации. На практике проверка объекта на null и его использование может находиться в разных местах. Если анализатор выдал предупреждение V6008, изучите код расположенный выше и попробуйте понять, почему ссылка может быть нулевой.
Пример кода, где проверка и использование объекта находятся в разных строках
if (player == null) {
....
String currentName = player.getName();
....
}
Анализатор предупредит, об опасности в строке внутри блока 'if'. Здесь или некорректно написано условие, или вместо 'player' должна использоваться другая переменная.
Программисты иногда забывают о том, что при проверке двух объектов на null один из них может оказаться нулевым, а второй нет, в результате чего будет вычислено всё условие и произойдёт доступ по нулевой ссылке. Например,
if ((text == null && newText == null) && text.equals(newText)) {
....
}
Это условие можно переписать, например, так
if ((text == null && newText == null) ||
(text != null && newText != null && text.equals(newText))) {
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6008. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в качестве фактического аргумента в функцию передаётся очень странное значение.
Рассмотрим примеры:
Недопустимый индекс
String SomeMethod(String mask, char ch)
{
String name = mask.substring(0, mask.indexOf(ch));
...
return name;
}
IndexOf() возращает позицию искомого аргумента. Если аргумент не найден, то функция возвращает значение '-1'. А если передать отрицательный индекс в функцию substring(), то возникнет 'StringIndexOutOfBoundsException'.
Корректный вариант:
String SomeMethod(String mask, char ch)
{
int pos = mask.indexOf(ch);
if (pos < 0) return "error";
String name = mask.substring(0, pos);
...
return name;
}
Null аргумент
String[] SplitFunc(String s, String d) {
...
if (d == null) {
return s.split(d);
}
return null;
}
По ряду причин была произведена некорректная проверка, из-за чего в функцию split() передается аргумент равный null. В результате чего функция сгенерирует исключение 'NullPointerException'.
Корректный вариант:
String[] SplitFunc(String s, String d) {
...
if (d != null) {
return s.split(d);
}
return null;
}
Сравнение с самим собой
...
return obj.equals(obj);
Фрагмент кода возвращает результат сравнения некоторого объекта с самим собой. Не странно ли? Скорее всего имело место сравнение с каким-то другим объектом:
...
return obj.equals(obj2);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6009. |
Анализатор обнаружил подозрительный вызов метода, возвращаемое значение которого не учитывается. Вызов некоторых методов не имеет смысла без использования их возвращаемого значения.
Рассмотрим пример такого кода:
String prepare(String base, int a, double d)
{
String str = base + " /\\ " +
String.valueOf(a) + " /\\ " +
String.valueOf(d);
...
str.replace("/", "\\");
...
return str;
}
Из кода видно, что строка вызывает метод 'replace', но при этом результат вызова игнорируется. Как известно, метод 'replace' возвращает новую измененную строку, при этом не меняет содержимое строки, что вызвала этот метод. Следовательно, строка с необходимой заменой так и не будет использована. Поэтому, чтобы изменения были учтены, необходимо подкорректировать код следующим образом:
String prepare(String base, int a, double d)
{
String str = base + " /\\ " +
String.valueOf(a) + " /\\ " +
String.valueOf(d);
...
str = str.replace("/", "\\");
...
return str;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6010. |
Анализатор обнаружил потенциальную ошибку в выражении, где совместно используются целочисленные и real-типы данных. Под real-типами понимаются такие типы, как 'float' и 'double'.
Давайте посмотрим, как может проявить себя ошибка на практике.
boolean IsInRange(int object_layer_width, int object_layer_height)
{
return object_layer_height != 0 &&
object_layer_width/object_layer_height > 0.1 &&
object_layer_width/object_layer_height < 10;
}
Здесь целочисленное значение сравнивается с константой '0.1'. Это очень подозрительно. Предположим, что переменные имеют следующие значения:
Программист ожидает, что при делении этих чисел результат будет равен '0.2'. Это подходящее значение, так оно попадает в диапазон (0.1..10).
На самом деле, результат деления будет равен 0. Деление осуществляется над целочисленными типами данных. Потом, при сравнении с '0.1' произойдет расширение до типа 'double', но будет уже поздно. Чтобы исправить код, необходимо заранее использовать явное приведение типов:
boolean IsInRange(int object_layer_width, int object_layer_height)
{
return object_layer_height != 0 &&
(double)object_layer_width/object_layer_height > 0.1 &&
(double)object_layer_width/object_layer_height < 10;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6011. |
Анализатор обнаружил потенциальную ошибку при использовании тернарного оператора "?:". Независимо от условия, будет выполнено одно и тоже действие. Скорее всего, в коде имеется опечатка.
Рассмотрим самый простой пример:
int A = B ? C : C;
В любом случае переменной A будет присвоено значение переменной C.
Рассмотрим пример, где уже не так легко заметить подобную ошибку:
double calc(Box bx, int angle, double scale )
{
return Math.tan((angle % 2 == 0 ?
bx.side_x : bx.side_x) * 0.42) * scale;
};
Здесь код отформатирован. В тексте программы это может быть одной строкой, и неудивительно, что легко просмотреть опечатку. Ошибка в том, что два раза используется член класса "side_x". Корректный вариант:
double calc(Box bx, int angle, double scale )
{
return Math.tan((angle % 2 == 0 ?
bx.side_x : bx.side_y) * 0.42) * scale;
};
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6012. |
Анализатор обнаружил ситуацию, когда строки/массивы/коллекции сравниваются через оператор '=='. Скорее всего подразумевалось сравнение содержимого, а на самом деле происходит непосредственное сравнение ссылок на объекты.
Рассмотрим пример некорректного сравнения строк:
if (str1 == "example") {}
if (str1 == str2) {}
В рассматриваемых случаях, если содержимое 'str1' и 'str2' будет совпадать и равно "example", то условия будут ложными, так как оператор '==' сравнивает адреса объектов, а не содержимое строк. Если необходимо сравнивать строки по содержимому, то корректный вариант кода примет следующий вид:
if (str1.equals("example")) {}
if (str1.equals(str2)) {}
Рассмотрим пример некорректного сравнения массивов:
int[] a = ...;
int[] b = ...;
...
if (a.equals(b)) { ... }
Для массивов вызов метода 'equals' тоже самое, что и оператор '=='. Сравниваются адреса объектов, а не содержимое. Для того, чтобы сравнивать массивы по содержимому, необходимо код переписать следующим образом:
if (Arrays.equals(a ,b){ ... }
С коллекциями оператор '==' ведет себя также, как с массивами и строками.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6013. |
Анализатор обнаружил странный метод. Метод не имеет состояния, не изменяет глобальных переменных. При этом он имеет несколько точек возврата, возвращающих одно и то же числовое, строковое, константное, enum значение, или то же самое значение поля, которое предназначено только для чтения.
Такой код крайне подозрителен и может свидетельствовать о возможной ошибке. Скорее всего, метод должна возвращать различные числовые значения.
Рассмотрим простой пример такого кода:
int Foo(int a)
{
if (a == 33)
return 1;
return 1;
}
Данный код содержит ошибку. Для ее исправления изменим одно из возвращаемых значений. Определить необходимые возвращаемые значения, как правило, можно только зная логику работы всего приложения в целом.
Вариант корректного кода:
int Foo(int a)
{
if (a == 33)
return 1;
return 2;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6014. |
Анализатор обнаружил потенциальную ошибку, связанную со странным использованием пары операторов ('=!', '=-', '=+'). Скорее всего их использование ошибочно, и подразумевался один из следующих операторов: '!=', '-=', '+='.
Оператор '=!'
Пример подозрительного кода:
boolean a = ... ;
boolean b = ... ;
...
if (a =! b)
{
...
}
С большой вероятностью здесь должна быть проверка, что переменная 'a' не равна 'b'. Если это так, то корректный вариант кода должен выглядеть следующим образом:
if (a != b)
{
...
}
Анализатор учитывает форматирование в выражении. Поэтому, если действительно требуется выполнить присваивание, а не сравнение, необходимо указать, используя скобки или пробелы. Следующие примеры кода считаются анализатором корректными:
if (a = !b)
...
if (a=(!b))
...
Оператор '=-'
Пример подозрительного кода:
int size = ... ;
int delta ... ;
...
size =- delta;
Этот код может быть корректен. Но с большой вероятностью имеется опечатка, и на самом деле хотели использовать оператор '-='. Исправленный вариант:
size -= delta;
Если код корректен, то, чтобы убрать предупреждение V6015 можно использовать дополнительный пробел между символами '=' и '-'. Вариант корректного кода, где предупреждение не выдается:
size = -delta;
Оператор '=+'
Пример подозрительного кода:
int size = ... ;
int delta ... ;
...
size =+ delta;
Исправленный вариант:
size+=delta;
Если код корректен, то чтобы убрать предупреждение V6015 можно удалить '+' или поставить дополнительный пробел. Вариант корректного кода, где предупреждение не выдается:
size = delta;
size = +delta;
Данная диагностика классифицируется как:
Анализатор обнаружил возможную ошибку, связанную с тем, что в цикле 'for' на каждой итерации к элементу массива или списка обращаются по одному и тому же константному индексу.
Рассмотрим пример некорректного кода:
void transform(List<Integer> parameters, ...)
{
for (int i = 0; i < parameters.size(); i++)
{
int element = parameters.get(0);
...
}
...
}
Здесь на каждой итерации цикла планировали сохранять некое значение i-го элемента массива 'parameters' в переменную 'element', но допустили опечатку, и на каждой итерации работают с одним и тем же - первым - элементом массива. Возможно также, что при написании кода программист отлаживался с использованием нулевого элемента, а потом забыл изменить значение индекса.
Корректный вариант кода:
void transform(List<Integer> parameters, ...)
{
for (int i = 0; i < parameters.size(); i++)
{
int element = parameters.get(i);
...
}
...
}
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6016. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что при написании двух и более вложенных циклов 'for', из-за опечатки не используется счётчик одного из циклов.
Рассмотрим синтетический пример некорректного кода:
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += matrix[i][i];
В коде планировали обойти все элементы матрицы и найти их сумму, но случайно написали переменную 'i' вместо 'j' при обращении к матрице.
Корректный вариант кода:
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
sum += matrix[i][j];
Анализатор обнаружил константное выражение в условии 'switch'. Чаще всего это сигнализирует о логической ошибке.
Рассмотрим синтетический пример:
int i = 1;
switch (i)
{
....
}
В качестве условия 'switch' стоит переменная, значение которой может быть посчитано во время компиляции. Такая ситуация могла возникнуть в результате рефакторинга: раньше был код, который менял значение переменной, а потом его поменяли и оказалось, что переменной больше не присваивается никакое значение.
Данная диагностика классифицируется как:
|
Анализатор обнаружил код, который никогда не будет выполнен. Возможно допущена ошибка в логике программы.
Данная диагностика находит блоки кода, до которых никогда не дойдёт управление.
Рассмотрим пример:
void printList(List<Integer> list) {
if (list == null) {
System.exit(-999);
System.err.println("Error!!! Output empty!!! list == null");
}
list.forEach(System.out::println);
}
Функция 'prinln(....)' никогда не напечатает сообщение об ошибке, так как функция 'System.exit(...)' не возвращает управление. Как правильно исправить код зависит от того, какую логику поведения задумывал программист изначально. Возможно, функция должна возвращать управление. Возможно, нарушен порядок выражений и корректный код должен быть таким:
void printList(List<Integer> list) {
if (list == null) {
System.err.println("Error!!! Output empty!!! list == null");
System.exit(-999);
}
list.forEach(System.out::println);
}
Рассмотрим ещё один пример:
void someTransform(int[] arr, int n, boolean isErr, int num, int den)
{
if (den == 0 || isErr)
{
return;
}
...
for (int i = 0; i < n; ++i)
{
if (!isErr || arr[i] <= 0)
continue;
arr[i] += 2 * num/den;
}
...
}
В данном фрагменте так и не выполнится код 'arr[i] += 2 * num/den;'. Проверки переменной 'isErr' в начале метода и в цикле противоречат друг другу, а изменения этой переменной между проверками нет. Вследствие чего будет выполняться оператор 'continue' на каждом шаге цикла. Скорее всего это произошло по причине рефакторинга.
Пример корректного код:
void someTransform(int[] arr, int n, boolean isErr, int num, int den)
{
if (den == 0 || isErr)
{
return;
}
...
for (int i = 0; i < n; ++i)
{
if (arr[i] <= 0)
continue;
arr[i] += 2 * num/den;
}
...
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6019. |
Анализатор обнаружил ситуацию, когда может произойти деление на ноль.
Рассмотрим пример:
if (maxHeight >= 0)
{
fx = height / maxHeight;
}
В условии проверяется, что значение переменной maxHeight неотрицательно. Если эта переменная будет равна нулю, то внутри условия произойдёт деление на 0. Чтобы исправить ситуацию, необходимо выполнять деление только в том случае, когда maxHeight положительно.
Исправленный вариант:
if (maxHeight > 0)
{
fx = height / maxHeight;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6020. |
Данное диагностическое правило выявляет случаи, когда некоторое значение присваивается переменной, которая далее перетирается новым значением или вовсе не используется.
Случай 1.
Одной и той же переменной дважды подряд присваивается значение. Причем между этими присваиваниями сама переменная не используется.
Рассмотрим пример:
A = GetA();
A = GetB();
То, что переменной 'A' два раза присваивается значение, может свидетельствовать о наличии ошибки. Высока вероятность, что код должен выглядеть следующим образом:
A = GetA();
B = GetB();
Если переменная между присваиваниями используется, то этот код считается анализатором корректным:
A = 1;
A = Foo(A);
Случай 2.
Локальной переменной присваивается значение, но переменная далее нигде не используется до выхода из метода.
Рассмотрим фрагмент кода:
String GetDisplayName(Titles titles, String name)
{
String result = null;
String tmp = normalize(name);
if (titles.isValidName(name, tmp)){
result = name;
}
return name;
}
Программист хотел, чтобы результатом метода была переменная 'result', которая инициализируется в зависимости от выполнения 'isValidName'. К сожалению, из-за опечатки метод всегда возвращает переменную 'name'. Правильный код должен выглядеть следующим образом:
String GetDisplayName(Titles titles, String name)
{
String result = null;
String tmp = normalize(name);
if (titles.isValidName(name, tmp)){
result = name;
}
return result;
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6021. |
Анализатор обнаружил подозрительный метод, один из параметров которого ни разу не используется. При этом другой его параметр используется несколько раз, что, возможно, свидетельствует о наличии ошибки.
Рассмотрим пример:
private static bool CardHasLock(int width, int height)
{
const double xScale = 0.051;
const double yScale = 0.0278;
int lockWidth = (int)Math.Round(height * xScale);
int lockHeight = (int)Math.Round(height * yScale);
....
}
Из кода видно, что параметр 'width' ни разу не используется в теле метода, при этом параметр 'height' используется дважды, в том числе при инициализации переменной 'lockWidth'. Скорее всего, здесь допущена ошибка, и код инициализации переменной 'lockWidth' должен был выглядеть следующим образом:
int lockWidth = (int)Math.Round(width * xScale);
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6022. |
Анализатор нашёл потенциальную ошибку в теле метода. Один из его параметров перезаписывается перед тем, как используется. Таким образом значение, пришедшее в метод, попросту теряется.
Эта ошибка имеет разные проявления. Рассмотрим пример кода:
void Foo1(int A, int B)
{
A = Calculate(A);
B = Calculate(A);
// do smt...
}
Здесь допущена опечатка, из-за чего объект 'B' примет неверное значение. Исправленный код выглядит так:
void Foo1(int A, int B)
{
A = Calculate(A);
B = Calculate(B);
// do smt...
}
Рассмотрим еще один пример данной ошибки:
void Foo2(List<Integer> list, int count)
{
list = new ArrayList<Integer>(count);
for (int i = 0; i < count; ++i)
list.add(MyRnd(i));
}
Данный метод должен был инициализировать список некоторыми значениями. В данном случае происходит копирование ссылки ('list'), хранящей адрес блока памяти в куче, в котором расположен список (или 'null', если память не была выделена). Поэтому, когда мы вновь выделяем память под список, адрес блока памяти записывается в локальную копию ссылки, а исходная (за пределами метода) остаётся неизменной. Таким образом выполняется лишняя работа для выделения памяти, инициализации списка и последующей уборки мусора.
Возможно, следует переписать метод следующим образом:
List<Integer> Foo2(int count)
{
List<Integer> list = new ArrayList<>(count);
for (int i = 0; i < count; ++i)
list.add(MyRnd(i));
}
...
list = Foo2(count);
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6023. |
Анализатор обнаружил код, который может ввести в заблуждение программиста. Не все знают, что оператор continue в цикле "do { ... } while(false)" остановит цикл, а не возобновит его.
Таким образом, после вызова оператора 'continue' будет проверено условие '(false)', и цикл завершится так как условие ложно.
Рассмотрим пример:
int i = 1;
do
{
System.out.print(i);
i++;
if (i < 3)
continue;
System.out.print('A');
} while (false);
Программист может ожидать, что программа напечатает '12A'. На самом деле будет напечатано '1'.
Если именно так задумано и ошибки нет, то код всё равно лучше изменить. Можно воспользоваться оператором 'break':
int i = 1;
do
{
System.out.print(i);
i++;
if (i < 3)
break;
System.out.print('A');
} while (false);
Код стал более понятным. Сразу видно, что если условие (i < 3) выполняется, то цикл будет остановлен. В добавок, анализатор не будет выдавать предупреждение на этот код.
Если код некорректен, то его следует переписать. Здесь нельзя дать точных рекомендаций. Все зависит от логики работы кода. Например, чтобы напечатать '12A' лучше будет написать:
for (int i = 1; i < 3; ++i)
System.out.print(i);
System.out.print('A');
Данная диагностика классифицируется как:
При доступе по индексу к переменной типа массив, список или строка может возникнуть исключение IndexOutOfBoundsException, если значение индекса оказывается за пределами допустимого диапазона. Анализатор способен обнаружить некоторые ошибки такого рода.
Например, это может произойти во время обхода массива в цикле:
int[] buff = new int[25];
for (int i = 0; i <= 25; i++)
buff[i] = 10;
Нужно помнить о том, что первый элемент массива имеет индекс 0, а последний – на единицу меньше размера массива. Корректный вариант:
int[] buff = new int[25];
for (int i = 0; i < 25; i++)
buff[i] = 10;
Похожую ошибку можно сделать не только в цикле, но и при неправильной проверке индекса в условии:
void ProcessOperandTypes(int opCodeValue, byte operandType)
{
byte[] OneByteOperandTypes = new byte[0xff];
if (opCodeValue < 0x100)
{
OneByteOperandTypes[opCodeValue] = operandType;
}
...
}
Корректный вариант:
void ProcessOperandTypes(int opCodeValue, byte operandType)
{
byte[] OneByteOperandTypes = new byte[0xff];
if (opCodeValue < 0xff)
{
OneByteOperandTypes[opCodeValue] = operandType;
}
...
}
Также можно допустить ошибку при доступе к конкретному элементу массива или списка.
private Map<String, String> TransformListToMap(List<String> config)
{
Map<String, String> map = new HashMap<>();
if (config.size() == 10)
{
map.put("Base State", config.get(0));
...
map.put("Sorted Descending Header Style", config.get(10));
}
...
return map;
}
В этом примере допущена ошибка в количестве записей в списке config. Исправленный вариант выглядит следующим образом:
private Map<String, String> TransformListToMap(List<String> config)
{
Map<String, String> map = new HashMap<>();
if (config.size() == 11)
{
map.put("Base State", config.get(0));
...
map.put("Sorted Descending Header Style", config.get(10));
}
...
return map;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6025. |
Анализатор обнаружил потенциальную ошибку, связанную с бессмысленным взаимным присваиванием переменных.
Рассмотрим пример:
int a, b, c;
...
a = b;
c = 10;
b = a;
Здесь присваивание "b = a" не имеет никакого практического смысла. Возможно, это опечатка или просто лишнее действие. Корректный вариант кода:
a = b;
c = 10;
b = a_2;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6026. |
Анализатор обнаружил потенциальную ошибку, найдя в коде инициализацию двух различных переменных одинаковыми выражениями. Анализатор считает опасными не все выражения, а только в которых используется вызов функций (либо слишком длинное выражение).
Рассмотрим наиболее простой случай:
sz1 = s1.length();
sz2 = s1.length();
Двум разным переменным присваивается один и тот же размер строки. Глядя на переменные 'sz1' и 'sz2' можно сделать вывод, что произошла опечатка. Корректный фрагмент кода будет выглядеть следующим образом:
sz1 = s1.length();
sz2 = s2.length();
Если анализатор выдал сообщение на фрагмент кода:
x = expression;
y = expression;
Варианты действий следующие:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6027. |
Анализатор обнаружил одинаковые подвыражения в левой и правой части составного оператора присваивания (compound assignment operator). Возможно эта операция содержит ошибку, не имеет смысла, либо может быть упрощена.
Рассмотрим пример. Пусть у нас имеется выражение следующего вида:
x += x + 5;
Возможно программист хотел просто прибавить к переменной 'x' значение 5. Тогда корректный код может выглядеть так:
x = x + 5;
Либо же программист хотел прибавить 5, но случайно добавил лишнюю переменную 'x' в выражение. Тогда корректный код может выглядеть так:
x += 5;
Впрочем, возможно код написан правильно. Однако согласитесь, читать такое выражения сложно и лучше переписать его. Более читабельный вариант может выглядеть так:
x = x * 2 + 5;
Рассмотрим следующий пример:
x += x;
Данная операция эквивалентна операции умножения на два. Более понятное выражение будет иметь следующий вид:
x *= 2;
Рассмотрим еще одно выражение:
y += top - y;
Мы пытаемся прибавить переменной 'y' разницу между переменной 'top' и переменной 'y'. Давайте разложим данное выражение:
y = y + top – y;
Данное выражение можно упростить, так как переменная 'y' вычитается сама из себя, что является бессмысленным. Тогда простой и корректный код будет выглядеть так:
y = top;
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6028. |
Анализатор обнаружил подозрительную передачу аргументов в метод. Возможно некоторые аргументы перепутаны местами.
Пример подозрительного кода:
void SetARGB(short A, short R, short G, short B) { .... }
void Foo(){
short A = 0, R = 0, G = 0, B = 0;
....
SetARGB(A, R, B, G);
....
}
Во время задания цвета объекта, перепутали синий и зелёный цвет.
Исправленный вариант кода должен выглядеть следующим образом:
SetARGB(A, R, G, B);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6029. |
Анализатор обнаружил возможную опечатку в логическом выражении. Вместо логического оператора (&& или ||) написали битовый оператор (& или |). Это означает, что правая часть будет выполнена независимо от результата левой части.
Пример:
if (foo() | bar()) {}
Если в качестве операндов стоят довольно ресурсоемкие операции, то использование битовых операций не оптимально. Кроме того, такой код может привести к ошибке из-за разных типов выражения и разных приоритетов операций. Также возможна ситуация, когда правая часть не должна быть выполнена, если левая завершилась неудачно. В таком случае может произойти обращение к неинициализированным ресурсам. Также битовые операции не гарантируют порядок вычисления операндов.
Корректный код:
if (foo() || bar()) {}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6030. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что вложенный цикл организован с использованием переменной, которая также используется и во внешнем цикле.
Схематически эта ошибка выглядит следующим образом:
int i = 0, j = 0;
for (i = 0; i < 5; i++)
for (i = 0; i < 5; i++)
arr[i][j] = 0;
Конечно, это искусственный пример и в реальном приложении ошибка может быть не так очевидна. Корректный вариант кода:
int i = 0, j = 0;
for (i = 0; i < 5; i++)
for (j = 0; j < 5; j++)
arr[i][j] = 0;
Использование одной переменной для внешнего и внутреннего цикла не всегда является ошибкой. Рассмотрим пример корректного кода, где анализатор не будет выдавать предупреждение:
for (c = lb; c <= ub; c++)
{
if (!(xlb <= calc(c) && calc(c) <= ub))
{
Range r = new Range(xlb, xlb + 1);
for (c = lb + 1; c <= ub; c++)
r = DoUnion(r, new Range(calc(c), calc(c) + 1));
return r;
}
}
В этом коде внутренний цикл "for (c = lb + 1; c <= ub; c++)" организован при помощи переменной "c". Внешний цикл также использует переменную "c". Но ошибки здесь нет. После того, как выполнится внутренний цикл, сразу произойдет выход из функции при помощи оператора "return r".
Данная диагностика классифицируется как:
|
Данное предупреждение выдается в том случае, если анализатор обнаружил две функции, реализованные идентичным образом. Наличие двух одинаковых функций само по себе не является ошибкой, но является поводом обратить на них внимание.
Смысл данной диагностики в обнаружении следующей разновидности ошибок:
class Point
{
....
int GetX() { return mX; }
int GetY() { return mX; }
};
Из-за допущенной опечатки две разные по смыслу функции выполняют одинаковые действия. Корректный вариант:
int GetX() { return mX; }
int GetY() { return mY; }
В приведенном примере идентичность тел функций GetX() и GetY() явно свидетельствует о наличии ошибки. Однако, если выдавать предупреждения на все одинаковые функции, то процент ложных срабатываний будет крайне большим. Поэтому анализатор руководствуется целым рядом исключений, когда не стоит предупреждать об одинаковых телах функций. Перечислим некоторые из них:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6032. |
Анализатор обнаружил подозрительную ситуацию, когда в словарь (контейнеры типа 'map' и т.п.) или в множество (контейнеры типа 'set' и т.п.) добавляются элементы с ключами, уже присутствующими в этих контейнерах. В результате добавление нового элемента будет проигнорировано. Это может свидетельствовать об опечатке и привести к неверному заполнению контейнера.
Рассмотрим пример со словарём:
Map<String, Integer> map = new HashMap<String, Integer>() {{
put("a", 10);
put("b", 20);
put("a", 30); // <=
}};
В последней строке инициализации была допущена ошибка, так как ключ 'a' уже содержится в словаре. В результате данный словарь будет содержать 2 значения.
Исправить ошибку можно, использовав правильное значение ключа:
Map<String, Integer> map = new HashMap<String, Integer>() {{
put("a", 10);
put("b", 20);
put("c", 30);
}};
Схожую ошибку можно допустить и при инициализации множества:
HashSet<String> someSet = new HashSet<String>(){{
add("First");
add("Second");
add("Third");
add("First"); // <=
add("Fifth");
}};
Из-за ошибки вместо ключа 'Fourth' в множество 'someSet' пытаются записать строку 'First', но так как такой ключ уже содержится в множестве, он будет проигнорирован.
Для исправления ошибки необходимо исправить список инициализации:
HashSet<String> someSet = new HashSet<String>(){{
add("First");
add("Second");
add("Third");
add("Fourth");
add("Fifth");
}};
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6033. |
Анализатор обнаружил потенциальную ошибку, связанную со сдвигом целого числа на 'N' бит, при этом 'N' больше размера этого числового типа в битах.
Давайте рассмотрим пример:
long convert(int x, int y, int shift)
{
if (shift < 0 || shift > 32) {
return -1;
}
return (x << shift) + y;
}
В данном случае хотели собрать 64-битное число из 2-х 32-битных, сдвинув 'x' на некоторое количество бит 'shift' и сложив старшую и младшую части. Есть вероятность того, что 'shift' может иметь значение равное 32. Поскольку 'x' в момент сдвига является 32-битным числом, то сдвиг на 32 бита эквивалентен сдвигу на 0 бит, что приведет к некорректному результату.
Корректный вариант может выглядеть так:
long convert(int x, int y, int shift)
{
if (shift < 0 || shift > 32) {
return -1;
}
return ((long)x << shift) + y;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6034. |
Анализатор обнаружил потенциальную ошибку, связанную с тем, что происходит двойное отрицание переменной. Такое дублирование сбивает с толку и, скорее всего, содержит ошибку.
Рассмотрим пример некорректного кода:
if (!(( !filter )))
{
...
}
Скорее всего, такая ошибка возникла после проведения рефакторинга кода. Например, была удалена часть сложного логического выражения, а отрицание всего результата осталось. В итоге, получилось противоположное по смыслу выражение.
Корректный вариант кода:
if ( filter )
{
...
}
Или:
if ( !filter )
{
...
}
Анализатор обнаружил обращение к объекту Optional, который в свое время является потенциально пустым. В таком случае будет исключение 'NoSuchElementException'.
Рассмотрим пример:
PClient getClient(boolean cond, String name, String company, /*args*/)
{
Optional<PClient> optClient = cond ?
Optional.of(new PClient(name, company)) : Optional.empty();
...
PClient pClient = optClient.get();
...
return pClient;
}
После выполнения первой строки, объект 'optClient' в зависимости от условия может инициализироваться пустым Optional. В таком случае, строчка 'optClient.get()' без проверки вызовет исключение. Это могло возникнуть по невнимательности или после рефакторинга. Как вариант, можно исправить код следующим образом:
PClient getClient(boolean cond, String name, String company, /*args*/)
{
Optional<PClient> optClient = cond ?
Optional.of(new PClient(name, company)) : Optional.empty();
...
if (optClient.isPresent())
{
PClient pClient = optClient.get();
...
return pClient;
}
else
{
return null;
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6036. |
Анализатор обнаружил подозрительный цикл, в котором используется один из следующих операторов: continue, break, return, goto, throw. Эти операторы выполняются всегда, без каких-либо условий.
Пример ошибочного кода:
for(int k = 0; k < max; k++)
{
if (k == index)
value = Calculate(k);
break;
}
В данном коде оператор 'break' не принадлежит к оператору 'if', из-за чего выполняться будет вне зависимости от истинности условия 'k == index' и для тела цикла будет выполнена только одна итерация. Тогда корректный код мог бы выглядеть так:
for(int k = 0; k < max; k++)
{
if (k == index)
{
value = Calculate(k);
break;
}
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6037. |
Анализатор обнаружил сравнение переменной типа float или double с Float.NaN или Double.NaN. В соответствии с документацией (15.21.1), если два значения Double.NaN сравниваются с помощью оператора '==' , то результатом будет false. Таким образом, какое бы значение типа 'double' ни сравнивали с Double.NaN, в результате всегда будет false.
Рассмотрим пример:
void Func(double d) {
if (d == Double.NaN) {
....
}
}
Проверка на NaN с помощью операторов '==' и '!= ' некорректна. Вместо этого следует использовать методы Float.isNaN() или Double.isNaN(). Исправленный вариант:
void Func(double d) {
if (Double.isNaN(d)) {
....
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, когда 'then' часть оператора 'if' никогда не получит управления. Это происходит из-за того, что ранее уже встречается оператор 'if' с таким же условием, содержащий в 'then' части безусловный оператор 'return'. Это может свидетельствовать как о логической ошибке в программе, так и избыточном втором операторе 'if'.
Рассмотрим пример некорректного кода:
if (l >= 0x06C0 && l <= 0x06CE) return true;
if (l >= 0x06D0 && l <= 0x06D3) return true;
if (l == 0x06D5) return true; // <=
if (l >= 0x06E5 && l <= 0x06E6) return true;
if (l >= 0x0905 && l <= 0x0939) return true;
if (l == 0x06D5) return true; // <=
if (l >= 0x0958 && l <= 0x0961) return true;
if (l >= 0x0985 && l <= 0x098C) return true;
В данном случае условие 'l == 0x06D5' дублируется и для исправления кода достаточно убрать одно из них. Однако возможно, что во втором случае проверяемое значение должно отличаться от первого случая.
Корректный вариант кода:
if (l >= 0x06C0 && l <= 0x06CE) return true;
if (l >= 0x06D0 && l <= 0x06D3) return true;
if (l == 0x06D5) return true;
if (l >= 0x06E5 && l <= 0x06E6) return true;
if (l >= 0x0905 && l <= 0x0939) return true;
if (l >= 0x0958 && l <= 0x0961) return true;
if (l >= 0x0985 && l <= 0x098C) return true;
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6039. |
Анализатор обнаружил потенциальную ошибку, связанную c тем, что форматирование кода, следующего за условным оператором, не соответствует логике выполнения программы. Высока вероятность, что пропущены открывающиеся и закрывающиеся фигурные скобки.
Рассмотрим пример некорректного кода:
if (a == 1)
b = c; d = b;
В данном случае присваивание 'd = b;' будет выполняться всегда, независимо от условия 'a == 1'.
Если код ошибочен, то ситуацию можно исправить, используя фигурные скобки. Корректный вариант кода:
if (a == 1)
{ b = c; d = b; }
Другой пример некорректного кода:
if (a == 1)
b = c;
d = b;
Для исправления ошибки так же следует использовать фигурные скобки. Корректный вариант кода:
if (a == 1)
{
b = c;
d = b;
}
Если код корректен, то чтобы исчезло предупреждение V6040, следует отформатировать код следующим образом:
if (a == 1)
b = c;
d = b;
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, когда в условии оператора 'if'/ 'while'/'do...while' присутствует оператор присваивания '='. Подобная конструкция часто свидетельствует о наличии ошибок. Высока вероятность, что вместо оператора '=' планировалось использовать оператор '=='.
Рассмотрим пример:
void func(int x, boolean skip, ...)
{
if (skip = true) {
return;
}
...
if ((x > 50) && (x < 150)) {
...
}
...
}
В коде допущена опечатка: вместо проверки переменной 'skip' на истинность происходит изменение значения переменной 'skip'. В итоге, из-за опечатки условие будет истинно, и всегда будет выполняться оператор 'return'. Корректный вариант кода:
if (skip == true){
return;
}
или
if (skip){
return;
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциально возможную ошибку, связанную с проверкой выражения на принадлежность к одному типу и приведением этого выражения в блоке проверки к другому типу.
Давайте рассмотрим данную ситуацию более подробно на примере:
if (a instanceof A)
{
return (B)a;
}
Скорее всего программист ошибся, так как подобное приведение типов с большой вероятностью приведет к ошибке, и вероятнее всего подразумевалась либо проверка выражения на принадлежность к другому типу, либо приведение выражения к другому типу.
Корректный вариант мог бы выглядеть так:
if (a instanceof B)
{
return (B)a;
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6042. |
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что в операторе 'for' совпадают начальное и конечное значения счетчика. Такое использование оператора 'for' приведет к тому, что цикл не будет выполнен ни разу или выполнен только один раз.
Рассмотрим пример:
void BeginAndEndForCheck(int beginLine, int endLine)
{
for (int i = beginLine; i < beginLine; i++)
{
...
}
...
}
Тело цикла никогда не выполняется. Скорее всего, произошла опечатка и следует заменить "i < beginLine" на корректное выражение "i < endLine". Корректный вариант кода:
for (int i = beginLine; i < endLine; i++)
{
...
}
Другой пример:
for (int i = n; i <= n; i++)
...
Тело этого цикла будет выполнено только один раз. Скорее всего, это не то, что задумывал программист.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6043. |
Анализатор обнаружил потенциальную ошибку, связанную с бессмысленным использованием постфиксного инкремента или декремента в выражении присвоения в эту же переменную.
Давайте рассмотрим пример:
int i = 5;
// Some code
i = i++;
В данном случае инкремент будет бессмысленным, и после выполнения данного кода переменная 'i' будет иметь значение '5'.
Это связано с тем, что постфиксный инкремент и декремент выполняются после вычисления правого операнда оператора присваивания, а результат вычисления временно кэшируется, и после выполнения операций постфиксного инкремента или декремента присваивается левой части выражения. Таким образом результат выполнения постфиксного инкремента или декремента перезаписывается результатом всего выражения.
Корректный же пример может иметь разный вид в зависимости от изначальной задачи.
Это может быть опечатка, и на самом деле программист случайно написал 2 раза переменную 'i' в выражении присвоения. Тогда корректный вариант мог бы выглядеть так:
int i = 5;
// Some code
q = i++;
Либо же программист не знал, что оператор постфиксного инкремента прибавляет единицу к значению переменной, но возвращает начальное ее значение. Тогда операция присвоения будет излишней, и корректный вариант мог бы выглядеть так:
int i = 5;
// Some code
i++;
Данная диагностика классифицируется как:
|
Анализатор обнаружил фрагмент кода, который, скорее всего, содержит опечатку. В цепочке однотипных сравнений членов класса имеется выражение не похожее на остальные тем, что в нем сравниваются члены с разными именами, в то время как остальные выражения в цепочке сравнивают одноименные члены.
Рассмотрим пример:
if (a.x == b.x && a.y == b.y && a.z == b.y)
В данном случае выражение 'a.z == b.y' отличается от остальных выражений в цепочке. Скорее всего, это выражение является ошибочным из-за опечатки при редактировании скопированного участка текста. Корректный код, который не вызовет подозрений у анализатора, будет выгладить так:
if (a.x == b.x && a.y == b.y && a.z == b.z)
Анализатор выдает предупреждение в тех случаях, когда длина цепочки сравнений более двух звеньев.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6045. |
Анализатор обнаружил потенциальную ошибку при использовании функций форматирования: String.format, System.out.format, System.err.format и т.п. Строка форматирования не соответствует передаваемым в функцию фактическим аргументам.
Рассмотрим простые примеры:
Неиспользуемые аргументы.
int A = 10, B = 20;
double C = 30.0;
System.out.format("%1$s < %2$s", A, B, C);
Не указан формат '%3$s', поэтому переменная 'С' не будет использована.
Возможные варианты исправленного кода:
//Удалим лишний аргумент
System.out.format("%1$s < %2$s", A, B);
//Исправим строку форматирования
System.out.format("%1$s < %2$s < %3$s", A, B, C);
Недостаточное количество аргументов.
int A = 10, B = 20;
double C = 30.0;
System.out.format("%1$s < %2$s < %3$s", A, B);
Намного более опасной ситуацией является, когда в функцию передаётся меньше аргументов, чем необходимо. Это приводит к исключению.
Возможные варианты исправленного кода:
//Добавим недостающий аргумент
System.out.format("%1$s < %2$s < %3$s", A, B, C);
//Исправим индексы в строке форматирования
System.out.format("%1$s < %2$s", A, B);
Анализатор не выдаёт предупреждение, если...
int row = 10;
System.out.format("Line: %1$s; Index: %1$s", row);
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6046. |
Анализатор обнаружил потенциально возможную ошибку в логических условиях. Логика работы кода не совпадает с тем, как этот код отформатирован.
Рассмотрим пример:
if (X)
if (Y) Foo();
else
z = 1;
Форматирование кода сбивает с толку, и кажется, что присваивание "z = 1" произойдет в том случае, если X == false. Однако ветка 'else' относится к ближайшему оператору 'if'. Другими словами, приведенный код на самом деле эквивалентен следующему коду:
if (X)
{
if (Y)
Foo();
else
z = 1;
}
Таким образом, код работает не так, как может показаться на первый взгляд.
Если выдано предупреждение V6047, то это может означать две вещи:
1) Код плохо отформатирован и ошибки на самом деле нет. Тогда, чтобы предупреждение V6047 не выдавалось, а код был более понятен, его нужно отформатировать. Пример корректного форматирования:
if (X)
if (Y)
Foo();
else
z = 1;
2) Найдена логическая ошибка. Тогда код можно исправить, например, так:
if (X) {
if (Y)
Foo();
} else {
z = 1;
}
Данная диагностика классифицируется как:
Анализатор позволяет обнаружить подозрительные операции, такие как '+', '-', '<<', '>>', где один из операндов равен 0, или операции '*', '/', '%' с операндом, равным 1.
Диагностическое правило V6048 помогает выполнить рефакторинг кода и иногда выявить ошибки.
Примеры конструкций, на которые анализатор выдаст данное диагностическое сообщение:
int y = 1;
...
int z = x * y;
Приведённый код можно упростить. Пример корректного кода:
int z = x;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6048. |
Анализатор обнаружил пользовательский тип, который переопределяет метод 'equals', но не переопределяет метод 'hashCode', и наоборот. Это может привести к неправильному функционированию пользовательского типа в сочетании с такими коллекциями как HashMap, HashSet и Hashtable, так как они активно используют 'hashCode' и 'equals' в своей работе.
Рассмотрим пример с использованием HashSet:
public class Employee {
String name;
int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getFullInfo() {
return this.name + " - " + String.valueOf(age);
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Employee))
return false;
Employee employee = (Employee) obj;
return employee.getAge() == this.getAge()
&& employee.getName() == this.getName();
}
}
public static void main(String[] args)
{
HashSet<Employee> employees = new HashSet<>();
employees.add(new Employee("OLIVER", 25));
employees.add(new Employee("MUHAMMAD", 54));
employees.add(new Employee("OLIVER", 25));
employees.forEach(arg -> System.out.println(arg.getFullInfo()));
}
В результате работы программы на консоль будет выведено:
OLIVER - 25
MUHAMMAD - 54
OLIVER - 25
Как видим, несмотря на то, что тип 'Employee' переопределяет метод 'equals', этого недостаточно. В ходе выполнения программы нам не удалось получить ожидаемого результата, и коллекция содержит повторяющиеся элементы. Для устранения этой проблемы в объявление типа 'Employee' необходимо добавить переопределение метода 'hashCode':
public class Employee {
...
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Employee))
return false;
Employee employee = (Employee) obj;
return employee.getAge() == this.getAge()
&& employee.getName() == this.getName();
}
@Override
public int hashCode() {
int result=17;
result=31*result+age;
result=31*result+(name!=null ? name.hashCode():0);
return result;
}
}
public static void main(String[] args)
{
HashSet<Employee> employees = new HashSet<>();
employees.add(new Employee("OLIVER", 25));
employees.add(new Employee("MUHAMMAD", 54));
employees.add(new Employee("OLIVER", 25));
employees.forEach(arg -> System.out.println(arg.getFullInfo()));
}
Вновь выполним программу. В результате на консоль будет выведено:
MUHAMMAD - 54
OLIVER – 25
Мы получили корректный результат: коллекция содержит только уникальные элементы.
Данная диагностика классифицируется как:
|
Данное диагностическое правило выявляет случаи неправильного порядка объявления статических полей внутри класса, а также случаи зависимостей статических полей от других классов. Такие случаи могут привести к сложному поддержанию кода или к ошибочной инициализации класса.
Случай 1.
Использование статического поля до того, как оно будет проинициализировано.
Рассмотрим синтетический пример некорректного кода:
public class Purse
{
static private Purse reserve = new Purse(10);
static private int scale = 5 + (int) (Math.random() * 5);
private int deposit;
Purse() {
deposit = 0;
}
Purse(int initial) {
deposit = initial * scale;
}
...
}
Как известно, статические поля будут инициализироваться в порядке объявления самыми первыми при первом использовании класса. Следовательно, в данном примере сначала инициализируется 'reserve', а потом 'scale'.
Рассмотрим инициализацию статического поля 'reserve':
Вследствие чего, поле 'deposit' у объекта 'reserve' будет проинициализировано не так, как задумывалось.
Для того, чтобы исправить ситуацию, необходимо поменять порядок объявления статических полей:
public class Purse
{
static private int scale = 5 + (int) (Math.random() * 5);
static private Purse reserve = new Purse(10);
private int deposit;
Purse() {
deposit = 0;
}
Purse(int initial) {
deposit = initial * scale;
}
...
}
Случай 2.
Взаимная зависимость статических полей разных классов.
Рассмотрим синтетический пример некорректного кода:
public class A {
public static int scheduleWeeks = B.scheduleDays / 7 + 1;
....
}
....
public class B {
public static int scheduleDays = A.scheduleWeeks * 7 + 7;
....
}
Ситуация здесь такова, что статическое поле 'A.scheduleWeeks' зависит от статического поля 'B.scheduleDays', и наоборот. Порядок инициализации классов может меняться, отсюда меняется и порядок инициализации статических полей. Если инициализируется первым класс 'A', то 'A.scheduleWeeks' будет иметь значение 2, а 'B.scheduleDays' – 7. Если первым будет класс 'B', то 'A.scheduleWeeks' будет равен 1, а 'B.scheduleDays' – 14. Вряд ли такое поведение будет приятно наблюдать у себя в коде. В таком случае необходимо пересмотреть инициализацию этих полей и убрать зависимость друг от друга.
Например, можно одно из статических полей инициализировать константой, и эта зависимость исчезнет:
public class A {
public static int scheduleWeeks = B.scheduleDays / 7 + 1;
....
}
....
public class B {
public static int scheduleDays = 14;
....
}
В таком случае 'B.scheduleDays' всегда будет равен 14, а 'A.scheduleWeeks' – 3.
Случай 3.
Инициализация статического поля в одном классе, используя статический метод второго класса, который в свою очередь использует статический метод или поле первого класса.
Рассмотрим синтетический пример некорректного кода:
public class A {
public static int scheduleWeeks = B.getScheduleWeeks();
public static int getScheduleDays() { return 21; }
....
}
....
public class B {
public static int getScheduleWeeks() {return A.getScheduleDays()/7;}
....
}
Независимо от того, какой из классов первым будет инициализирован, поле 'A.scheduleWeeks' примет значение равное 3. Несмотря на это, такой способ инициализации сбивает с толку при чтении и поддержке такого кода.
Например, можно подкорректировать код следующим образом:
public class A {
public static int scheduleWeeks = B.getScheduleWeeks();
....
}
....
public class B {
public static int getScheduleDays() { return 21; }
public static int getScheduleWeeks() {return B.getScheduleDays()/7;}
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6050. |
Анализатор выявил использование операторов return/break/continue и т.п. внутри блока 'finally'. Их использование может привести к потере необработанного исключения, возникшего в блоках 'try' или 'catch'. Согласно JLS [14.20.2], в случае такого завершения блока 'finally', все сгенерированные исключения в 'try' или 'catch' будут подавлены.
Рассмотрим пример, когда анализатор выдаст предупреждение:
int someMethod(int a, int b, int c) throws SomeException
{
int value = -1;
...
try
{
value = calculateTransform(a, b, c);
...
}
finally
{
System.out.println("Result of someMethod()");
return value; // <=
}
}
В данном примере, несмотря на то, что в сигнатуре метода указано, что может возникнуть исключение, метод 'someMethod' никогда не выбросит его, так как применение оператора 'return' подавит исключение, и оно не выйдет наружу.
Возможно, это намеренный способ подавления исключений. В таком случае, чтобы анализатор не ругался, достаточно убрать исключение из сигнатуры метода.
int someMethod(int a, int b, int c)
{
int value = -1;
...
try
{
value = calculateTransform(a, b, c);
...
}
finally
{
System.out.println("Result of someMethod()");
return value;
}
}
Немного изменим предыдущий пример:
int someMethod(int a, int b, int c) throws SomeException
{
int value = -1;
...
try
{
value = calculateTransform(a, b, c);
...
}
catch (SomeException se)
{
...
throw se;
}
finally
{
System.out.println("Result of someMethod()");
return value; // <=
}
}
Здесь анализатор тоже выдаст предупреждение. Есть обработчик исключения 'SomeException', который что-то делает и пробрасывает это исключение дальше. После чего отработает блок 'finally' и функция вернет значение 'value'. А что же исключение? А исключение, что было проброшено дальше в обработчике, так и не выйдет за пределы метода.
Чтобы исправить ситуацию, можно исправить код следующим образом:
int someMethod(int a, int b, int c) throws SomeException
{
int value = -1;
...
try
{
value = calculateTransform(a, b, c);
...
}
catch (SomeException se)
{
...
throw se;
}
finally
{
System.out.println("Result of someMethod()");
}
return value;
}
В таком случае, если произошло исключение, то оно будет обязательно проброшено наружу, что и говорит сигнатура метода 'someMethod'.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6051. |
Выявлен случай использования метода в родительском конструкторе, который, в свою очередь, переопределяется в дочернем классе. Такое использование может привести к тому, что переопределенный метод будут использовать неинициализированные поля класса.
В JLS [12.5] описан порядок инициализации нового класса, и если пренебречь им, то может наблюдаться такое поведение.
Рассмотрим пример, который приведет к такому случаю:
public class Parent {
private String parentStr = "Black";
public Parent () {
printInfo();
}
public void printInfo () {
System.out.println("Parent::printInfo");
System.out.println("parentStr: " + parentStr);
System.out.println("-----------------");
}
....
}
public class Child extends Parent {
private int childInt;
private String childStr;
public Child() {
super();
this.childInt = 25;
this.childStr = "White";
}
public void printInfo () {
super.printInfo();
System.out.println("Child::printInfo");
System.out.println("childInt: "+childInt+";childStr: "+childStr);
System.out.println("-----------------");
}
....
}
Если выполнить следующую строку кода:
Child obj = new Child();
то на экран будет выведен такой текст:
Parent::printInfo
parentStr: Black
-----------------
Child::printInfo
childInt: 0 ; childStr: null
-----------------
По распечатанному фрагменту видно, что был вызван переопределенный метод 'printInfo' из родительского конструктора класса 'Parent', когда дочерний класс 'Child' не был полностью проинициализирован. Отсюда и получается, что поля 'childInt' и 'childStr' распечатаны со значениями по умолчанию, а не с теми, что мы задали.
Вывод: никогда не используйте методы в конструкторе, которые могут быть в дальнейшем переопределены в дочерних классах. Если же в конструкторе используется метод класса, то он должен быть либо final, либо private.
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6052. |
Анализатор обнаружил, что коллекция, не предназначенная для параллельного изменения, модифицируется во время итерации по ней. Это может привести к возникновению исключения 'ConcurrentModificationException'.
Рассмотрим несколько примеров кода с ошибкой.
Пример 1:
List<Integer> mylist = new ArrayList<>();
....
for (Integer i : mylist)
{
if (cond)
{
mylist.add(i * 2);
}
}
Пример 2:
List<Integer> myList = new ArrayList<>();
....
Iterator iter = myList.iterator();
while (iter.hasNext())
{
if (cond)
{
Integer i = (Integer) iter.next();
myList.add(i * 2);
}
}
Пример 3:
Set<Integer> mySet = new HashSet<>();
....
mySet.stream().forEach(i -> mySet.add(i * 2));
При этом, анализатор не будет выдавать предупреждение если коллекция позволяет параллельное изменение:
List<Integer> mylist = new CopyOnWriteArrayList<>();
....
for (Integer i : mylist)
{
if (cond)
{
mylist.add(i + 1);
}
}
Кроме того, предупреждение не будет выдано если сразу после модификации осуществляется выход из цикла:
List<Integer> mylist = new ArrayList<>();
....
for (Integer i : mylist)
{
if (cond)
{
mylist.add(i + 1);
break;
}
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6053. |
Анализатор обнаружил ситуацию, когда сравнение классов осуществляется по имени. Такое сравнение является некорректным, т.к. согласно спецификации JVM классы имеют уникальное имя только внутри пакета.
Помимо логических ошибок, такой код может в некоторых случаях привести к различным уязвимостям, поскольку не известно, какое поведение у непроверенного класса.
Пример:
if (obj.getClass().getSimpleName().equals("Plane"))
{
....
}
Такой код следует переписать следующим образом:
if(obj.getClass().equals(ArrayList.class))
{
....
}
либо:
if (obj instanceof Plane)
{
....
}
или же:
if (obj.getClass().isAssignableFrom(Plane.class))
{
....
}
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6054. |
Анализатор выявил случай, когда в 'assert' присутствует метод, который меняет состояние объекта. Вызов таких методов будет зависеть от настроек Java Virtual Machine, что может являться непредвиденным поведением для разработчика.
Рассмотрим пример такого случая:
void someFunction(List<String> listTokens)
{
....
assert "<:>".equals(listTokens.remove(0));
....
}
Здесь нас интересует вызов 'listTokens.remove(0)'. Данный метод удаляет первый элемент в 'listTokens', тем самым модифицируя коллекцию. Далее возвращенная удаленная строка сравнивается с некоторой ожидаемой строкой. Проблема тут в том, что если в проекте будут отключены assert'ы, то выражение не будет вычистяться, и, следовательно, первый элемент в коллекции не будет удален, что может сказаться на дальнейшем поведении программы.
Чтобы избежать таких ситуаций, из assert'a следует убрать вызов функций, меняющих содержимое объектов.
Пример исправленного кода:
void someFunction(List<String> listTokens)
{
....
boolean isFirstStr = "<:>".equals(listTokens.remove(0));
assert isFirstStr;
....
}
Данная диагностика классифицируется как:
|
Анализатор выявил случай, когда метод 'compareTo' перегружает одноименный метод родительского класса, который реализует интерфейс 'Comparable<T>'. Скорее всего, перегрузка метода родительского класса не то, что подразумевалось разработчиком.
Рассмотрим пример класса, который вместо переопределения метода осуществляет перегрузку:
public class Human implements Comparable<Human>
{
private String mName;
private int mAge;
....
Human(String name, int age)
{
mName = name;
mAge = age;
}
....
public int compareTo(Human human)
{
int result = this.mName.compareTo(human.mName);
if (result == 0)
{
result = Integer.compare(this.mAge, human.mAge);
}
return result;
}
}
public class Employee extends Human
{
int mSalary;
....
public Employee(String name, int age, int salary) {
super(name, age);
mSalary = salary;
}
....
public int compareTo(Employee employee)
{
return Integer.compare(this.mSalary, employee.mSalary);
}
}
Итак, у нас есть два класса: базовый 'Human' и дочерний 'Employee'. 'Human' реализует интерфейс 'Comparable<Human>' и определяет метод 'compareTo'. Далее дочерний класс 'Employee' расширяет базовый класс и перегружает метод 'compareTo'. Результат метода сравнения таков:
Из этого может выйти следующий эффект:
1) Если мы представим объекты 'Employee' через ссылку на базовый класс, то вызвав метод 'compareTo' мы не достигнем нужного сравнения объектов:
Human emp1 = new Employee("Andrew", 25, 33000);
Human emp2 = new Employee("Madeline", 29, 31000);
System.out.println(emp1.compareTo(emp2));
На экран будет выведено -12, и получается, что объект emp1 логически меньше emp2. Ведь это не так. Скорее всего, программистом подразумевалось, что эти объекты будут сравниваться на основе поля 'mSalary', и в таком случае, результат был бы противоположным. Это произошло из-за того, что программист перегрузил метод сравнения, а не переопределил. И когда произошел вызов метода, то вызвался метод из класса 'Human'.
2) Известно, что списки элементов, которые реализуют интерфейс 'Comparable<T>', могут автоматически быть отсортированы с помощью 'Collections.sort'/'Arrays.sort', а также такие элементы могут быть использованы в качестве ключей отсортированных коллекций, без явного указания компоратора. И в случае такой перегрузки сортировка будет осуществляться не так, как задумывалась, судя по реализации сравнения в дочернем классе. Опасность в том, что в таких случаях происходит неявный вызов метода сравнения, и сразу ошибку можно не найти.
Выполним следующий код:
List<Human> listEmployees = new ArrayList<>();
listEmployees.add(new Employee("Andrew", 25, 33000));
listEmployees.add(new Employee("Madeline", 29, 31000));
listEmployees.add(new Employee("Hailey", 45, 55000));
System.out.println("Before: ");
listEmployees.forEach(System.out::println);
Collections.sort(listEmployees);
System.out.println("After: ");
listEmployees.forEach(System.out::println);
На экран будет выведен следующий текст:
Before:
Name: Andrew; Age: 25; Salary: 33000
Name: Madeline; Age: 29; Salary: 31000
Name: Hailey; Age: 45; Salary: 55000
After:
Name: Andrew; Age: 25; Salary: 33000
Name: Hailey; Age: 45; Salary: 55000
Name: Madeline; Age: 29; Salary: 31000
Как видно из вывода, сортировка была осуществлена не по полю 'mSalary'. И все происходит по той же самой причине.
Решение этой проблемы сводится к тому, что метод сравнения должен быть переопределен, а не перегружен. Например:
public class Employee extends Human
{
....
public int compareTo(Human employee)
{
if (employee instanceof Employee)
{
return Integer.compare(this.mSalary,
((Employee)employee).mSalary);
}
return -1;
}
....
}
И тогда будет все работать так, как задумывалось изначально.
В первом случае вывод будет 1 (emp1 логически больше emp2).
Во втором случае вывод будет следующим:
Name: Andrew; Age: 25; Salary: 33000
Name: Madeline; Age: 29; Salary: 31000
Name: Hailey; Age: 45; Salary: 55000
After:
Name: Madeline; Age: 29; Salary: 31000
Name: Andrew; Age: 25; Salary: 33000
Name: Hailey; Age: 45; Salary: 55000
Анализатор обнаружил избыточное сравнение, которое может быть потенциальной ошибкой.
Поясним на простом примере:
if (arr[42] == 10 && arr[42] != 3)
Условие будет выполнено в том случае, если 'Aa[42] == 10'. Вторая часть выражения бессмысленна. Проанализировав код, можно прийти к одному из двух выводов:
1) Выражение можно упросить. Исправленный код:
if (arr[42] == 10)
2) Выражение содержит ошибку. Исправленный код:
if (arr[42] == 10 && arr[43] != 3)
Рассмотрим еще пример с условием, которое вызывает подозрение:
if ((3 < value) && (value > 10))
Условие выполнится только тогда, когда 'value > 10'. Скорее всего это будет означать ошибку, и подразумевалось, что 'value' примет значение в интервале (3;10):
if ((3 < value) && (value < 10))
Статья, в которой рассмотрены подобные ситуации и даны советы, как избежать таких ошибок: "Логические выражения в C/C++. Как ошибаются профессионалы".
Данная диагностика классифицируется как:
|
Анализатор обнаружил потенциальную ошибку, связанную с вызовом функции сравнения для несовместимых по типу объектов. Анализатор выдает предупреждения на некорректное использование таких функций как equals, assertEquals, assertArrayEquals и т.д.
Разберем ошибочные случаи сравнения:
Пример 1:
String param1 = ...;
Integer param2 = ...;
...
if (param1.equals(param2))
{...}
Пример 2:
List<String> list = Arrays.asList("1", "2", "3");
Set<String> set = new HashSet<>(list);
if (list.equals(set))
{...}
В обоих примерах сравниваются объекты с несовместимыми типами. Результат их сравнения всегда будет false, так как в реализациях 'equals' есть проверка на соответствие пришедшего объекта с текущим.
Для String:
public boolean equals(Object anObject)
{
...
if (anObject instanceof String)
{
...
}
return false;
}
Для List:
public boolean equals(Object o)
{
...
if (!(o instanceof List))
return false;
...
}
Если V6058 в вашем коде, то скорее всего это ошибка, и сравнивать надо другие объекты.
Пример 1:
...
String param1 = ...;
String param3 = ...;
...
if (param1.equals(param3))
{...}
Пример 2:
...
List<String> list = Arrays.asList("1", "2", "3");
List<String> list2 = ...;
...
if (list.equals(list2))
{...}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6058. |
Анализатор обнаружил странное регулярное выражение, использовав которое, программист получит результат, отличный от ожидаемого, либо при использовании которого полученный результат будет отличаться от ожидаемого.
Рассмотрим пример:
String[] arr = "Hot. Cool. Yours".split(".");
После выполнения этой строчки кода в массиве не будет ожидаемого {"Hot", " Cool", " Yours"}, а будет пустой массив. Это связано с тем, что точка является специальным символом в регулярном выражении, у которой есть свое предназначение. Чтобы сделать точку разделителем в вашей строке, нужно использовать:
String[] arr = "Hot. Cool. Yours".split("\\.");
Также анализатор предупредит, если ваше регулярное выражение будет состоять только из следующих символов:
Данная диагностика классифицируется как:
Анализатор обнаружил потенциальную ошибку, которая может привести к использованию нулевого объекта.
Анализатор заметил в коде следующую ситуацию. Вначале объект используется. А уже затем этот объект проверяется на значение 'null'. Это может означать одно из двух:
1) Возникнет ошибка, если объект будет равен 'null'.
2) Программа всегда работает корректно, так как объект всегда не равен null. Проверка является лишней.
Рассмотрим фрагмент кода, на котором анализатор выдаст предупреждение:
boolean isSomething(String str)
{
String prepareStr = prepare(str);
if (prepareStr.contains("*?*"))
{
...
}
...
return prepareStr == null ? false : prepareStr.contains("?*?");
}
Итак, в вышеприведенном примере потенциальная ошибка или лишнее сравнение? Сначала с переменной 'prepareStr' идут какие-то вычисления, а в конце она проверяется на null. Здесь имеет место быть возможен как первый, так и второй случай. Если все же метод 'prepare' может вернуть null, то необходимо внести изменения в код:
boolean isSomething(String str)
{
String prepareStr = prepare(str);
if (prepareStr == null) {
return false;
}
if (prepareStr.contains("*?*"))
{
//...
}
//...
return prepareStr.contains("?*?");
}
Если же 'prepare' не возвращает 'null', то просто можно убрать сравнение с null, чтобы не путать ваших коллег-разработчиков.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6060. |
Анализатор выявил подозрительное использование константы в восьмеричной системе счисления. Анализатор предупреждает о наличии восьмеричных констант, если рядом нет других восьмеричных констант. Такие "одиночные" восьмеричные константы часто являются ошибкой.
Использование констант в восьмеричной системе счисления само по себе не является ошибкой. Эта система удобна при работе с битами и используется в коде, взаимодействующем с сетью или внешними устройствами. Однако средний программист редко использует эту систему счисления и поэтому может написать перед числом 0, забыв, что это превращает значение в восьмеричное.
Рассмотрим ошибочный пример:
void compute(int red, int blue, int green)
{
int color = 2220 * red +
7067 * blue +
0713 * green;
// ...
}
Рассматривая подобный код непросто заметить ошибку, но она есть. Ошибка в том, что последняя константа "0713" задана в восьмеричной системе счисления и имеет значение вовсе не 713, а 459. Исправленный вариант кода:
void compute(int red, int blue, int green)
{
int color = 2220 * red +
7067 * blue +
713 * green;
// ...
}
Как было сказано выше, предупреждение о восьмеричных константах выдается только в том случае, если вблизи от них нет других восьмеричных констант. Поэтому следующий пример кода считается анализатором безопасным и для него предупреждений выдано не будет:
short bytebit[] = {01, 02, 04, 010, 020, 040, 0100, 0200 };
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что может возникать бесконечная рекурсия. Скорее всего, это приведет к переполнению стека вызовов и возникновению исключения 'StackOverflow'.
Рассмотрим пример из реального проекта, который приводил к такой ситуации:
@Override
public void glGenTextures(IntBuffer textures) {
gl.glGenTextures(textures);
checkError();
}
@Override
public void glGenQueries(int num, IntBuffer ids) {
glGenQueries(num, ids); // <=
checkError();
}
Программист допустил опечатку и вызвал не тот метод, что стало причиной возникновения бесконечной рекурсии. Метод 'glGenQueries' надо было вызывать у объекта 'gl', как это делается в других фукнциях.
Исправленный пример:
@Override
public void glGenTextures(IntBuffer textures) {
gl.glGenTextures(textures);
checkError();
}
@Override
public void glGenQueries(int num, IntBuffer ids) {
gl.glGenQueries(num, ids);
checkError();
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6062. |
Анализатор обнаружил потенциально возможную ошибку, связанную с наличием точки с запятой ';' после оператора if, for или while.
Приведем пример:
int someMethod(int value, int a, int b, int c, ...)
{
int res = -1;
....
if (value > (a - b)/c);
{
....
res = calculate(value);
}
....
return res;
}
Корректный вариант:
int someMethod(int value, int a, int b, int c, ...)
{
int res = -1;
....
if (value > (a - b)/c)
{
....
res = calculate(value);
}
....
return res;
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительный прямой вызов Thread.run(). Такой запуск потока может ввести в заблуждение. Когда метод run() объекта Thread вызывается напрямую, все действия в методе run() будут выполняться в текущем потоке, а не вновь созданном.
Рассмотрим некорректный вариант:
private class Foo implements Runnable
{
@Override
public void run() {/*...*/}
}
....
Foo foo = new Foo();
new Thread(foo).run();
....
В данном примере выполнение тела метода run() будет в текущем потоке. Так ли задумывалось? Для того, чтобы тело метод run() выполнилось в новом потоке, необходимо использовать метод start().
Корректный вариант:
private class Foo implements Runnable
{
@Override
public void run() {/*...*/}
}
....
Foo foo = new Foo();
new Thread(foo).start();
....
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил сериализацию объекта, в котором нет реализации интерфейса 'java.io.Serializable'. Для того, чтобы корректно выполнялись операции сериализации и десериализации, необходимо реализовать в классе объекта данный интерфейс.
Рассмотрим некорректный вариант:
class Dog
{
String breed;
String name;
Integer age;
....
}
....
Dog dog = new Dog();
....
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(dog);
....
Если дойдет дело до сериализации объекта 'dog', то будет выдано исключение 'java.io.NotSerializableException'. Для того, чтобы все успешно отработало необходимо реализовать интерфейс 'java.io.Serializable' в классе 'Dog'.
Корректный вариант:
class Dog implements Serializable
{
String breed;
String name;
Integer age;
....
}
....
Dog dog = new Dog();
....
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(dog);
....
Анализатор обнаружил потенциальную ошибку, связанную с вызовом метода коллекции, в который передали объект, тип которого не совпадает с типом коллекции. Анализатор выдает предупреждения на такие функции как remove, contains, removeAll, containsAll, retainAll и т.д.
Давайте рассмотрим пример с ошибочным использованием метода 'remove':
List<String> list = ...;
Integer index = ...;
...
list.remove(index);
Здесь хотели удалить объект из списка по индексу, но не учли, что индекс не примитивный целый тип, а объект типа 'Integer'. Будет использоваться перегруженный метод 'remove', который ожидает на вход объект, а не 'int'. Объекты 'Integer' и 'String' несовместимы, и поэтому использование метода будет ошибочным.
Если нет возможности изменить тип переменной "index", то можно исправить код следующим образом:
List<String> list = ...;
Integer index = ...;
...
list.remove(index.intValue());
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6066. |
Анализатор обнаружил ситуацию, когда в операторе switch разные метки case содержат одинаковые фрагменты кода. Часто это свидетельствует об избыточном коде, который можно улучшить объединением меток. Но нередко одинаковые фрагменты кода могут быть причиной copy-paste программирования и являться настоящими ошибками.
Рассмотрим пример с избыточным кодом:
public static String getSymmetricCipherName(SymmetricKeyAlgorithmTags tag)
{
switch (tag)
{
case DES:
return "DES";
case AES_128:
return "AES";
case AES_192:
return "AES";
case AES_256:
return "AES";
case CAMELLIA_128:
return "Camellia";
case CAMELLIA_192:
return "Camellia";
case CAMELLIA_256:
return "Camellia";
case TWOFISH:
return "Twofish";
default:
throw new IllegalArgumentException("....");
}
}
В реальных проектах для различных case'ов возможны случаи, когда необходимо выполнить одинаковые действия. И для того, чтобы выглядело читабельнее, код можно написать более компактно:
public static String getSymmetricCipherName(SymmetricKeyAlgorithmTags tag)
{
switch (tag)
{
case DES:
return "DES";
case AES_128:
case AES_192:
case AES_256:
return "AES";
case CAMELLIA_128:
case CAMELLIA_192:
case CAMELLIA_256:
return "Camellia";
case TWOFISH:
return "Twofish";
default:
throw new IllegalArgumentException("....");
}
}
Рассмотрим пример из реального приложения, где разработчик допустил ошибку из-за опечатки:
protected boolean condition(Actor actor) throws ....
{
....
if (fieldValue instanceof Number)
{
....
switch (tokens[2])
{
case "=":
case "==":
passing = (Double) fieldValue
==
Double.parseDouble(secondValue);
break;
case "!":
case "!=":
passing = (Double) fieldValue
==
Double.parseDouble(secondValue);
break;
case "<=":
passing = ((Number) fieldValue).doubleValue()
<=
Double.parseDouble(secondValue);
break;
....
}
....
}
....
}
В коде меток '!' и '!=' допущена опечатка, которая возникла по всей видимости из-за copy-paste. Просмотрев остальные ветви case, можно прийти к выводу, что следовало использовать оператор сравнения '!=' вместо '=='.
Исправленный код:
protected boolean condition(Actor actor) throws ....
{
....
if (fieldValue instanceof Number)
{
....
switch (tokens[2])
{
case "=":
case "==":
passing = (Double) fieldValue
==
Double.parseDouble(secondValue);
break;
case "!":
case "!=":
passing = (Double) fieldValue
!=
Double.parseDouble(secondValue);
break;
case "<=":
passing = ((Number) fieldValue).doubleValue()
<=
Double.parseDouble(secondValue);
break;
....
}
....
}
....
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6067. |
Анализатор обнаружил использование класса BigDecimal, которое может вести себя не так, как ожидал разработчик.
Анализатор выдает предупреждения в следующих ситуациях:
1. Вызов конструктора, аргументом которого является число с плавающей точкой.
Пример такого использования:
BigDecimal bigDecimal = new BigDecimal(0.6);
Про использование таким образом конструктора есть несколько примечаний в документации класса.
Если не вдаваться в подробности, то созданный таким образом объект будет иметь значение 0.59999999999999997779553950749686919152736663818359375 вместо 0,6. Это связано с невозможностью точного представления числа с плавающей точкой.
А ведь BigDecimal прежде всего необходим для вычислений с крайне высокими требованиями к точности. И примеру, есть ПО, в котором от точности вычислений может зависеть человеческая жизнь (ПО для самолетов, ракет или для медицинского оборудования). Поэтому погрешность даже в 30 разряде после запятой может сыграть свою роль.
Чтобы этого избежать, необходимо создавать объект BigDecimal одним из следующих способов:
BigDecimal bigDecimal1 = BigDecimal.valueOf(0.6);
BigDecimal bigDecimal2 = new BigDecimal("0.6");
2. Вызов метода 'equals', так как вполне вероятно имелся ввиду метод 'compareTo'.
Принято считать, что для сравнения объектов нужно вызывать метод 'equals'. Тут не поспорить!
И когда разработчик работает с объектом этого класса, то может считать, что он просто работает с объектом, который может содержать очень большое вещественное число. И вызывая метод 'equals', подразумевает эквивалентность сравниваемых чисел.
В таком случае, следующий фрагмент кода может удивить разработчика:
BigDecimal bigDecimal1 = BigDecimal.valueOf(0.6);
BigDecimal bigDecimal2 = BigDecimal.valueOf(0.60);
....
if (bigDecimal1.equals(bigDecimal2)) // false
{
// code
}
Нюанс кроется в том, что при сравнении через метод 'equals' сравнивается не только значение, но и количество значащих чисел после запятой. Как раз этого разработчик может и не ожидать. Чтобы сравнивать числа без учета значащих чисел после запятой необходимо использовать метод 'compareTo':
BigDecimal bigDecimal1 = BigDecimal.valueOf(0.6);
BigDecimal bigDecimal2 = BigDecimal.valueOf(0.60);
....
if (bigDecimal1.compareTo(bigDecimal2) == 0) // true
{
// code
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил случай, когда над возможно отрицательным числом типа 'byte' или 'short' применяется беззнаковый сдвиг вправо с присваиванием (>>>=). Результат такого сдвига может отличаться от ожидаемого.
Часто требуется, чтобы при сдвиге вправо расширение знакового разряда не происходило, а освобождающиеся левые разряды независимо от знака старшего бита заполнялись бы нулями. С этой целью используется оператор беззнакового сдвига вправо >>>.
Также этот сдвиг можно совмещать со знаком равенства (>>>=). Однако при этом может возникнуть неочевидное поведение при использовании данного оператора с типами 'byte' или 'short'. Проблема заключается в том, что они сначала будут неявно преобразованы к типу 'int' и сдвинуты вправо, а затем обрезаны при возвращении к исходному типу.
Если Вы попробуете скомпилировать код со следующим содержимым:
void test(byte byteValue, boolean isFlag)
{
....
if (isFlag)
{
byteValue = byteValue >>> 5;
}
....
}
, то получите ошибку:
error: incompatible types:
possible lossy conversion from int to byte
byteValue = byteValue >>> 5;
^
Это подтверждает вышесказанные слова о расширении до 'int', и далее компилятор не даст Вам присвоить 'int' к 'byte' без явного указания. Это означает, что Вы знаете что делаете. Если скомпилировать тот же файл с небольшим исправлением:
....
byteValue >>>= 5;
....
, то все в порядке. При выполнении этого кода произойдет расширение, смещение и сужение до исходного типа.
Из-за этого в таком сдвиге с присвоением кроется поведение, которое может не ожидаться разработчиком. В случае сдвига положительного числа будет работать так, как и ожидалось. А что с отрицательным числом?
Давайте разберем синтетический случай, когда смещается с присвоением значение -1 типа 'byte':
byte byteValue = -1; // 0xFF or 0b1111_1111
byteValue >>>= 4;
assertTrue(byteValue == 0x0F); // byteValue == 0b0000_1111
Итак, разработчик думал, что у него есть 8 бит (byte), и, беззнаково смещая на 4 бита вправо, ожидает увидеть только 4 младших бита. И тут на удивление разработчика 'assertTrue' не отрабатывает!
Это происходит как раз из-за того, что 'byteValue' неявно расширяется до 'int', сдвигается и обрезается до 'byte':
byteValue == 0xFF (byte): 11111111
Расширение до 'int' : 11111111 11111111 11111111 11111111
Смещение на 4 : 00001111 11111111 11111111 11111111
Преобразование в 'byte' : 11111111
По завершении операции может сложиться ощущение, что беззнаковый сдвиг (>>>=) работает некорректно. Но всё логично и правильно. Просто существует нюанс, который следует учитывать при работе с типами 'byte' или 'short'.
Анализатор обнаружил синхронизацию по объекту, которая может привести к скрытым проблемам параллелизма из-за того, что синхронизированный объект может неявно использоваться в других логически несвязанных частях программы.
Проблема заключается в том, что если производить синхронизацию по:
то это может приводить к потенциальным тупикам и недетерминированному поведению.
Причиной этому может служить то, что вышеперечисленные объекты могут повторно использоваться в разных частях программы.
Суть проблемы синхронизации по приведенным объектам состоит в том, что к объекту, используемому для блокировки, имеется общий доступ. Такой объект может быть использован для блокировки в другом месте без ведома разработчика, использовавшего объект для блокировки в первый раз. Это, в свою очередь, и создаёт вероятность возникновения взаимоблокировки на один и тот же объект.
Приведём синтетический пример взаимоблокировки при синхронизации по 'this':
class SynchroThis
{
void doSmt()
{
synchronized(this)
{
// do smt
}
}
}
....
SynchroThis obj = new SynchroThis();
synchronized(obj)
{
Thread t = new Thread(() -> obj.doSmt());
t.start();
t.join();
}
....
В результате программа никогда не завершится, т.к. происходит deadlock по экземпляру класса SynchroThis (первая блокировка в основном потоке по 'obj', вторая - в потоке 't' по 'this').
Для того, чтобы избежать возможных взаимных блокировок, в качестве объекта блокировки стоит использовать, например, приватное поле:
class A
{
private final Object lock = new Object();
void foo()
{
synchronized(lock)
{
// do smt
}
}
}
Рассмотрим синтетический пример синхронизации по объекту типа Byte:
class FirstClass
{
private final Byte idLock;
....
public FirstClass(Byte id, ....)
{
idLock = id;
....
}
....
public void calculateFromFirst(....)
{
synchronized (idLock) // <=
{
....
}
}
}
class SecondClass
{
private final Byte idLock;
....
public SecondClass(Byte id, ....)
{
idLock = id;
....
}
....
public void calculateFromSecond(....)
{
synchronized (idLock) // <=
{
....
}
}
}
Обусловим, что поток N1 оперирует объектом класса 'FirstClass', а поток N2 - 'SecondClass'.
Теперь давайте рассмотрим сценарий:
Итак, у нас 2 разных потока выполняют совершенно разную логику программы для разных объектов. Что же получится? А получится то, что поток N2 будет находиться в состоянии ожидания до тех пор, пока поток N1 не закончит работу в синхронизированном блоке по объекту 'idLock'. Почему же так получается?
Как и все объекты, переменные созданные с помощью классов оберток будут храниться в куче. У каждого такого объекта будет свой адрес в куче. Но есть небольшой нюанс, который нужно всегда учитывать. Целочисленные классы обертки, полученные при помощи автоупаковки, со значением в диапазоне [-128..127] кэшируются JVM. Поэтому такие обертки с одинаковыми значениями в этом диапазоне будут являться ссылками на один объект.
Так и получается в нашем случае. Синхронизация производится по одному и тому же объекту в памяти, чего вовсе и не ожидалось.
Также, помимо целочисленных классов оберток, не следует использовать для синхронизации объекты классов:
Использование синхронизации по таким объектам небезопасно. Рекомендуется использовать вышеописанный способ с приватным полем, но если по каким-либо причинам Вам это не подходит, то создавайте объекты явно при помощи конструктора. Такой способ гарантирует, что у объектов будут разные адреса. Пример безопасного кода:
class FirstClass
{
private final Byte idLock;
....
public FirstClass(Byte id, ....)
{
idLock = new Byte(id);
....
}
....
public void calculateFromFirst(....)
{
synchronized (idLock)
{
....
}
}
}
....
Дополнительную информацию можно посмотреть здесь.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил в файле copyleft лицензию, которая обязывает открыть остальной исходный код. Это может быть неприемлемо для многих коммерческих проектов.
Если вы разрабатываете открытый проект, то можно просто игнорировать это предупреждение и отключить его.
Пример комментария, на который анализатор выдаст предупреждение:
/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Если в закрытый проект добавить файл с такой лицензией (GPL3 в данном случае), то остальной исходный код необходимо будет открыть, из-за особенностей данной лицензии.
Такой тип copyleft лицензий называют "вирусными" лицензиями, из-за их свойства распространяться на остальные файлы проекта. Проблема в том, что использование хотя бы одного файла с подобной лицензией в закрытом проекте автоматически делает весь исходный код открытым и обязывает распространять его вместе с бинарными файлами.
Диагностика занимается поиском следующих "вирусных" лицензий:
Есть следующие варианты, как вы можете поступить, обнаружив в закрытым проекте использование файлов с copyleft лицензий:
Мы понимаем, что данная диагностика неуместна для открытых проектов. Команда PVS-Studio способствует развитию открытых проектов, помогая исправлять в них ошибки и предоставляя бесплатные варианты лицензий. Однако наш продукт является B2B решением, и поэтому данная диагностика по умолчанию включена.
Если же ваш код распространяется под одной из указанных выше copyleft лицензий, то вы можете отключить данную диагностику следующими способами:
Если вам известны ещё типы "вирусных" лицензий, которые в данный момент не выявляет инструмент, то вы можете сообщить нам о них через форму обратной связи. И мы добавим их выявление в следующем релизе.
Анализатор обнаружил код, который, возможно, содержит опечатку. Высока вероятность, что подобный код был создан с использованием подхода Copy-Paste.
Данная диагностика выявляет два схожих по структуре блока кода, идущих один за другим и отличающихся переменной, которая несколько раз встречалась в первом блоке кода, но во втором встречается только один раз. Из этого можно сделать вывод, что переменная возможно была забыта и не заменена. Предупреждение V6072 предназначено для выявления тех случаев, если второй блок был получен путем копирования первого, при этом во втором блоке были переименованы не все переменные.
Рассмотрим пример:
if (x > 0)
{
Do1(x);
Do2(x);
}
if (y > 0)
{
Do1(y);
Do2(x); // <=
}
Вероятнее всего во втором блоке вместо переменной 'x' должна идти переменная 'y'. Корректный вариант мог бы выглядеть так:
if (x > 0)
{
Do1(x);
Do2(x);
}
if (y > 0)
{
Do1(y);
Do2(y);
}
Рассмотрим еще один пример, но уже более сложный.
....
if(erendlinen>239) erendlinen=239;
if(srendlinen>erendlinen) srendlinen=erendlinen;
if(erendlinep>239) erendlinep=239;
if(srendlinep>erendlinen) srendlinep=erendlinep; // <=
....
Заметить ошибку не так уж и просто. Имена переменных похожи друг на друга, и поэтому выявление ошибки усложняется в разы. На самом деле во втором блоке вместо переменной 'erendlinen' должна стоять 'erendlinep'.
Корректный вариант будет выглядеть так:
....
if(erendlinen>239) erendlinen=239;
if(srendlinen>erendlinen) srendlinen=erendlinen;
if(erendlinep>239) erendlinep=239;
if(srendlinep>erendlinep) srendlinep=erendlinep; // <=
....
Имена переменных 'erendlinen' и 'erendlinep' выбраны явно неудачно. Такую ошибку почти невозможно заметить при Code Review. Да что уж там, даже когда анализатор указывает на строку с ошибкой и то сложно её заметить. Поэтому, встретив предупреждение V6072, рекомендуем не спешить и внимательно изучить код.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6072. |
Анализатор обнаружил, что переопределенный метод 'toString' или 'clone' может вернуть значение 'null' или выбросить исключение.
Использование методов 'toString' / 'clone' на объекте должно всегда возвращать строку / объект соответственно. Возврат же недействительного значения противоречит неявному контракту метода.
Давайте поясним сказанное на примерах некорректного переопределения метода 'toString':
@Override
public String toString()
{
return null;
}
Есть вероятность, что в дальнейшем при работе программы или ее отладке будет вызван данный метод для получения текстового представления объекта. Т.к. разработчик скорее всего не станет проверять результат работы этой функции на null, дальнейшее его использование может повлечь за собой исключение 'NullPointerException'. Если необходимо вернуть пустое или неизвестное значение текстового представление объекта, рекомендуется использовать для этого пустую строку:
@Override
public String toString()
{
return "";
}
Другой пример плохой практики при реализации метода 'toString' – выбрасывание исключения. Рассмотрим следующий синтетический пример:
@Override
public String toString()
{
if(hasError)
{
throw new IllegalStateException("toString() method error encountered");
}
....
}
Высока вероятность, что данный метод будет вызван пользователем класса в месте, не подразумевающем возможности возникновения и обработки исключений.
Если необходимо выдать сообщение об ошибке при генерации текстового представления объекта, лучше будет вернуть его текст в виде строки, либо каким-то образом залогировать эту ошибку:
....
@Override
public String toString()
{
if(hasError)
{
logger.warn("toString() method error encountered");
return "Error encountered";
}
....
}
С методом 'clone' аналогичная ситуация. Работая с этим методом, разработчик в голове может держать только 2 возможных исхода:
Но он явно не ожидает того, что при работе с методом может произойти подобное:
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6073. |
Анализатор обнаружил неатомарное изменение 'volatile' переменной, которое может привести к состоянию гонки.
Известно, что использование модификатора 'volatile' гарантирует, что все потоки будут видеть актуальное значение соответствующей переменной. К этому можно добавить, что модификатор 'volatile' используется для того, чтобы указать JVM, что все операции присвоения этой переменной и все операции чтения из неё должны быть атомарными.
Можно посчитать, что пометки переменных как 'volatile' будет достаточно, чтобы безопасно их использовать в многопоточном приложении! Но что будет, если изменять 'volatile' переменную, будущее значение которой зависит от текущего?
К таким операциям можно отнести:
Рассмотрим использование 'volatile' переменной в качестве счетчика (counter++).
class Counter
{
private volatile int counter = 0;
....
public void increment()
{
counter++; // counter = counter + 1
}
....
}
Такая операция выглядит как одна операция, но в действительности это целая последовательность операций чтения-изменения-записи. Здесь и кроется состояние гонки.
Предположим, что 2 потока одновременно работают с объектом класса Counter и выполняют инкремент переменной 'counter' (10):
[counter == 10, temp == 10] Поток N1 считывает значение 'counter'во временную переменную.
[counter == 10, temp == 11] Поток N1 изменяет временную переменную.
[counter == 10, temp == 10] Поток N2 считывает значение 'counter'во временную переменную.
[counter == 11, temp == 11] Поток N1 записывает временную переменную в 'counter'.
[counter == 11, temp == 11] Поток N2 изменяет временную переменную.
[counter == 11, temp == 11] Поток N2 записывает временную переменную в 'counter'.
Ожидалось значение переменной 'counter' равное 12 (а не 11), так как 2 потока выполнили инкремент над одной и той же переменной. Также возможна ситуация, когда потоки выполнят инкремент друг за другом, и в таком случае все будет так, как и ожидалось. Как итог, раз на раз не приходится!
Чтобы избежать подобного поведения неатомарных операций для разделяемых переменных, можно использовать:
Пример корректного кода:
class Counter
{
private volatile int counter = 0;
....
public synchronized void increment()
{
counter++;
}
....
}
или
class Counter
{
private final AtomicInteger counter = new AtomicInteger(0);
....
public void increment()
{
counter.incrementAndGet();
}
....
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6074. |
Анализатор обнаружил пользовательский метод сериализации, который не соответствует требованиям интерфейса. В случае несоответствия пользовательская сериализация будет проигнорирована средствами Serialization API.
Если поведения сериализации по умолчанию для пользовательского класса недостаточно, то есть возможность его изменить, реализовав специальные методы:
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
private void readObjectNoData()
throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace()
throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve()
throws ObjectStreamException;
Однако реализовывать эти методы необходимо строго по тем требованиям, которые формируются из сигнатур вышеуказанных методов. Если этого не сделать, то пользовательская реализация сериализации будет проигнорирована в пользу сериализации по умолчанию. Дополнительно можно почитать здесь.
Сложность заключается в том, что интерфейс 'java.io.Serializable' представляет собой пустой интерфейс и является лишь маркером для механизма сериализации. И в случае реализации своей логики, неправильно определенные методы не обнаруживаются при помощи, например, компилятора, так как они представляют собой обычные пользовательские методы.
Рассмотрим синтетический пример, который вполне имеет право на существование в реальном проекте:
class Base implements Serializable
{
....
}
class Example extends Base
{
....
void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
throw new NotSerializableException("Serialization is not supported!");
}
void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
throw new NotSerializableException("Deserialization is not supported!");
}
}
Был базовый класс, который мог участвовать в сериализации. Со временем понадобилось создать дочерний класс, но сериализация была не нужна. Программист реализовал соответствующие методы-заглушки и продолжил писать код дальше. Через какое-то время обнаруживается, что наш дочерний класс, вопреки желанию автора кода, сериализуется! Это произошло именно из-за того, что реализованные методы не соответствуют требованиям. Для исправления ситуации необходимо default модификатор изменить на private:
class Base implements Serializable
{
....
}
class Example extends Base
{
....
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
{
throw new NotSerializableException("Serialization is not supported!");
}
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException
{
throw new NotSerializableException("Deserialization is not supported!");
}
....
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил ситуацию, когда объект однажды уже записанный в поток, спустя какое-то время записывается в него снова с измененным состоянием. Из-за нюансов 'java.io.ObjectOuputStream' это приведет к тому, что измененное состояние сериализуемого объекта будет проигнорировано в пользу исходного.
Класс 'java.io.ObjectOuputStream', который используется для сериализации, кэширует записываемые объекты. Это означает, что один и тот же объект не будет сериализоваться дважды. Один раз класс сериализует объект, а во второй раз просто запишет в поток ссылку на тот же самый первый объект. В этом и кроется подводный камень. Если мы сериализуем объект, изменим его, а после снова сериализуем, то 'java.io.ObjectOuputStream' об изменениях объекта ничего не узнает, поэтому сочтет объект тем же самым, который был сериализован ранее.
Рассмотрим некорректный синтетический пример, в котором будет проигнорировано изменение состояния объекта при сериализации:
ObjectOutputStream out = new ObjectOutputStream(....);
SerializedObject obj = new SerializedObject();
obj.state = 100;
out.writeObject(obj); // сохраняет объект с состоянием = 100
obj.state = 200;
out.writeObject(obj); // сохраняет объект с состоянием = 100 (а ожидалось 200)
out.close();
Чтобы избежать подобного поведения, есть два способа.
Самым простым и надежным решением является создание нового экземпляра этого объекта с необходимым состоянием. Рассмотрим пример:
ObjectOutputStream out = new ObjectOutputStream(....);
SerializedObject obj = new SerializedObject();
obj.state = 100;
out.writeObject(obj);
obj = new SerializedObject();
obj.state = 200;
out.writeObject(obj);
out.close();
Следующий способ менее тривиален. Он заключается в использовании метода 'reset' класса 'java.io.ObjectOuputStream'. Использовать данный способ рекомендуется только тогда, когда вы хорошо понимаете, что делаете и зачем, так как 'reset' обнулит информацию обо всех объектах, ранее записанных в поток. Рассмотрим пример:
ObjectOutputStream out = new ObjectOutputStream(....);
SerializedObject obj = new SerializedObject();
obj.state = 100;
out.writeObject(obj);
out.reset();
obj.state = 200;
out.writeObject(obj);
out.close();
Анализатор обнаружил потенциальную ошибку внутри оператора switch. Используется метка с именем похожим на 'default'. Возможно, это опечатка.
Рассмотрим пример:
int c = getValue();
double weightCoefficient = 0;
switch(c){
case 1:
weightCoefficient += 3 * (/*math formula #1*/);
case 2:
weightCoefficient += 7 * (/*math formula #2*/);
defalt:
weightCoefficient += 0.42;
}
Кажется, после того, как этот код отработает, значение переменной 'weightCoefficient' будет 0.42. Но на самом деле значение 'weightCoefficient' останется равным нулю. Дело в том, что 'defalt' это метка, а не оператор 'default'. Исправленный вариант кода:
int c = getValue();
double weightCoefficient = 0;
switch(c){
case 1:
weightCoefficient += 3 * (/*math formula #1*/);
case 2:
weightCoefficient += 7 * (/*math formula #2*/);
default:
weightCoefficient += 0.42;
}
Эта диагностика срабатывает также, когда имя метки начинается с 'case'. Есть вероятность, что пропущен пробел. Например, вместо метки 'case1:', должно быть написано 'case 1:'.
Данная диагностика классифицируется как:
|
Данное диагностическое правило позволяет обнаружить Java SE API в вашем коде, которые будут удалены или помечены как устаревшие в более новых версиях Java SE.
Когда выходят новые версии Java SE, то они, как правило, обратно совместимы с более ранними версиями, то есть, например, приложение, разработанное на основе Java SE 8, должно без проблем запуститься на 11 версии Java. Однако, между различными версиями API Java SE могут быть небольшие несовместимости. Эти несовместимости заключаются в том, что некоторые API претерпевают изменения: удаляются методы и классы, меняется их поведение, методы помечаются как устаревшие, и так далее.
Если у вас в компании строго с предупреждениями компилятора, то часть проблем можно не откладывая решить - например, не использовать метод или класс, который помечен как устаревший. Это полезно делать, так как есть вероятность, что при использовании вновь вышедшей версии Javа ваше приложение поведет себя неожидаемым образом или вовсе упадет.
Также в комплекте JDK есть инструмент 'jdeps', который поможет вам обнаружить зависимость вашего приложения от внутренних API JDK. Но такой инструмент запускают, как правило, когда встает вопрос о миграции вашего приложения на более свежую версию Java. А этим вопросом хорошо бы озадачиться еще тогда, когда вы просто пишете код, и не завязываться на API, которое будет удалено в следующих выпусках Java SE.
Диагностика V6078 заранее предупредит вас о том, что ваш код зависим от некоторых функций и классов Java SE API, которые на следующих версиях Java могут доставить вам трудности. Например, вы столкнётесь с такими трудностями, когда обнаружится, что ваше приложение падает у вашего клиента на более свежей версии Java. С большой вероятностью вам рано или поздно в какой-то момент придётся очищать код от использования старых API - лучше делать это регулярно, а не накапливать технический долг на будущее.
Диагностическое правило выдает предупреждения в следующих случаях:
Правило на данный момент позволяет проанализировать совместимость Oracle Java SE c 8-ой по 14-ую версии. Диагностика V6078 по умолчанию выключена., Чтобы она заработала, её необходимо активировать и настроить.
В IntelliJ IDEA плагине вам необходимо во вкладке Settings > PVS-Studio > API Compatibility Issue Detection включить правило и указать параметры, а именно:
Используя gradle плагин, вам необходимо сконфигурировать настройки анализатора в build.gardle:
apply plugin: com.pvsstudio.PvsStudioGradlePlugin
pvsstudio {
....
compatibility = true
sourceJava = /*version*/
targetJava = /*version*/
excludePackages = [/*pack1, pack2, ...*/]
}
Используя maven плагин, вам необходимо сконфигурировать настройки анализатора в pom.xml:
<build>
<plugins>
<plugin>
<groupId>com.pvsstudio</groupId>
<artifactId>pvsstudio-maven-plugin</artifactId>
....
<configuration>
<analyzer>
....
<compatibility>true</compatibility>
<sourceJava>/*version*/</sourceJava>
<targetJava>/*version*/</targetJava>
<excludePackages>/*pack1, pack2, ...*/</excludePackages>
</analyzer>
</configuration>
</plugin>
</plugins>
</build>
Если вы используете анализатор напрямую через командную строку, то, чтобы активировать анализ совместимости выбранных Java SE API, необходимо использовать следующие параметры:
java -jar pvs-studio.jar /*other options*/ --compatibility
--source-java /*version*/ --target-java /*version*/
--exclude-packages /*pack1 pack2 ... */
Давайте предположим, что мы разрабатываем приложение на базе Java SE 8 и у нас есть класс со следующим содержимым:
/* imports */
import java.util.jar.Pack200;
public class SomeClass
{
/* code */
public static void someFunction(Pack200.Packer packer, ...)
{
/* code */
packer.addPropertyChangeListener(evt -> {/* code */});
/* code */
}
}
Запустив статический анализ с различными параметрами настройки диагностического правила, мы будем наблюдать следующую картину:
Сначала в Java SE 9 был удален метод 'addPropertyChangeListener' в классе 'Pack200.Packer'. В 11 версии к этому добавился тот факт, что класс 'Pack200' пометили как устаревший. А в 14 версии и вовсе этот класс был удален.
Поэтому, запустив приложение на Java 11, вы получите 'java.lang.NoSuchMethodError', а если запустите на Java 14 – 'java.lang.NoClassDefFoundError'.
Зная эту информацию, при разработке своего приложения, вы заранее будете знать о потенциальных проблемах при использовании такого API, и сможете сразу рассмотреть возможность использовать альтернативное API для решения поставленной задачи.
Анализатор заметил в коде следующую ситуацию. В начале, значение переменной или выражения используется в качестве индекса массива / коллекции. А уже затем это значение сравнивается с 0 или с размером массива / коллекции. Это может указывать на наличие логической ошибки в коде или опечатку в одном из сравнений.
Рассмотрим пример
int idx = getPosition(buf);
buf[idx] = 42;
if (idx < 0) return -1;
Если значение 'idx' окажется меньше нуля, то выражение 'buf[idx] ' приведёт к ошибке. Анализатор выдаст предупреждение на этот код, указав 2 строки. Первая строка - это то место, где переменная сравнивается с 0. Вторая строка - это то место, где до этой проверки использовалась переменная 'idx'.
Исправленный вариант кода:
int idx = getPosition(buf);
if (idx < 0) return -1;
buf[idx] = 42;
Точно так же анализатор выдаёт предупреждение, если переменная сравнивается с размером массива:
int[] buf = getArrayValue(/*params*/);
buf[idx] = 42;
if (idx < buf.length) return;
Правильный вариант кода:
int[] buf = getArrayValue(/*params*/);
if (idx < buf.length) return;
buf[idx] = 42;
Также анализатор может увидеть проблему, если использование переменной в качестве индекса массива и её проверка находятся в одном выражении:
void f(int[] arr)
{
for (int i = 0; arr[i] < 10 && i < arr.length; i++)
{
System.out.println("arr[i] = " + arr[i]);
}
}
В этом случае, если все элементы массива меньше 10, то на последней итерации цикла мы будем проверять значение, взятое за границей массива. А это ArrayIndexOutOfBoundsException!
Исправленный вариант:
void f(int[] arr)
{
for (int i = 0; i < arr.length && arr[i] < 10; i++)
{
System.out.println("arr[i] = " + arr[i]);
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки переполнения буфера (записи или чтения за пределами выделенной для буфера памяти). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6079. |
Анализатор обнаружил ситуацию, когда проверка переменной может быть пропущена в следующем 'if' после присваивания или инициализации.
К примеру, ошибочным можно считать такой код:
int ret = foo(....);
if (ret != -1) { .... }
....
int ret2 = bar(....);
if (ret != -1) { .... } // <=
Часто бывают случаи, когда надо проверить возвращаемое значение какой-либо функции. Однако, можно допустить ошибку, указав другую переменную внутри условия 'if'. Чаще всего подобная ошибка возникает, когда фрагмент кода копируется, но в нём забывают заменить имя переменной в условии. В рассмотренном примере в условии забыли заменить имя 'ret' на 'ret2'.
Исправленный вариант:
int ret2 = bar(....);
if (ret2 != -1) { .... }
Или же можно допустить ошибку в таком случае:
this.data = calculate(data, ....);
if (data != -1) ....;
Переменная и поле имеют одинаковые имена, из-за чего их легко перепутать.
Диагностика является эвристической. Во время своей работы она сравнивает имена переменных и на основании этого делает предположения о наличии опечатки. Также производится базовая проверка типов для уменьшения количества ложных срабатываний.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6080. |
Данное диагностическое правило позволяет обнаружить ошибочные ситуации, в которых при помощи Reflection API пытаются выявить наличие аннотаций, не имеющих политики удержания 'RUNTIME'.
Когда реализовывается аннотация, то ей необходимо прописать мета-аннотацию 'Retention', которая позволяет указать жизненный цикл аннотации:
Если вы 'Retention' не упоминали, то будет значение по умолчанию: 'CLASS'.
Используя Reflection API для получения информации о присутствующих аннотациях, необходимо учесть то, что только аннотации с политикой удержания 'RUNTIME' будут доступны механизму рефлексии. Любая попытка получить информацию о аннотации с политикой удержания 'SOURCE' или 'CLASS' ничего вам не даст.
Рассмотрим синтетический пример. В проекте реализована следующая аннотация:
package my.package;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ....})
public @interface MyAnnotation {
int field_id() default -1;
String field_name() default "";
....
}
Разработчик, пытаясь определить наличие этой аннотации у определенного метода через Reflection API:
void runMethod(Method method, ....)
{
....
if (method.isAnnotationPresent(MyAnnotation.class))
{
....
}
....
}
всегда будет получать false. Это происходит из-за того, что при определении аннотации не использовали мета-аннотацию 'Retention'. А как ранее было уже сказано, что в случае, если ее не указывают, значение применяется по умолчанию: 'CLASS'.
Чтобы до вашей аннотации можно было достучаться через Reflection API, вам нужно будет явно об этом позаботиться, указав 'RUNTIME':
package my.package;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ....})
public @interface MyAnnotation {
int field_id() default -1;
String field_name() default "";
....
}
Помимо метода 'isAnnotationPresent', диагностическое правило проверяет: getAnnotation, getAnnotationsByType, getDeclaredAnnotation, getDeclaredAnnotationsByType.
Анализатор обнаружил потенциальную ошибку, связанную с небезопасным использованием паттерна "блокировка с двойной проверкой" (double-checked locking).
Блокировка с двойной проверкой - это паттерн, предназначенный для уменьшения накладных расходов получения блокировки. Сначала проверяется условие блокировки без синхронизации. И только если условие выполняется, поток попытается получить блокировку. Таким образом, блокировка будет выполнена только в том случае, когда она действительно была необходима.
Основной ошибкой при реализации этого паттерна является публикация объекта перед его инициализацией:
class TestClass
{
private static volatile Singleton singleton;
public static Singleton getSingleton()
{
if (singleton == null)
{
synchronized (TestClass.class)
{
if (singleton == null)
{
singleton = new Singleton();
singleton.initialize(); // <=
}
}
}
return singleton;
}
}
При многопоточном выполнении один из потоков может увидеть, что объект уже был создан и воспользоваться им, даже если инициализация этого объекта еще не произошла.
Похожая ошибка случится, когда в блоке синхронизации объект переприсваивается в зависимости от тех или иных условий. После первого присваивания какой-либо другой поток вполне может начать с ним работать, не подозревая, что далее в программе будет использоваться другой объект.
Исправление таких ошибок производится путем создания временной переменной:
class TestClass
{
private static volatile Singleton singleton;
public static Singleton getSingleton()
{
if (singleton == null)
{
synchronized (TestClass.class)
{
if (singleton == null)
{
Singleton temp = new Singleton();
temp.initialize();
singleton = temp;
}
}
}
return singleton;
}
}
Другой распространённой ошибкой при реализации этого паттерна является пропуск модификатора 'volatile' в декларации поля, к которому производится доступ:
class TestClass
{
private static Singleton singleton;
public static Singleton getSingleton()
{
if (singleton == null)
{
synchronized (TestClass.class)
{
if (singleton == null)
{
Singleton temp = new Singleton();
temp.initialize();
singleton = temp;
}
}
}
return singleton;
}
}
Объект класса 'Singleton' может быть создан несколько раз из-за того, что проверка 'singleton == null' увидит значение 'null', закешированное в потоке. Кроме того, компилятор может изменить порядок выполнения операций с не-volatile полями, из-за чего, например, вызов метода инициализации объекта и запись ссылки на этот объект в поле могут произойти в обратном порядке, что опять же приведет к использованию объекта, которому только предстоит пройти процедуру инициализации.
Одна из опасностей таких ошибок состоит в том, что в большинстве случаев программа работает корректно. В данном случае некорректное поведение программы может проявиться в зависимости от используемой JVM, уровня конкуретности, решений планировщика потоков и прочих факторов. Воспроизвести такие условия вручную крайне сложно.
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6082. |
Данное диагностическое правило позволяет обнаружить несоответствие последовательностей сериализации и десериализации полей объекта.
При использовании интерфейса 'java.io.Serializable' весь контроль над сериализацией достается JVM. При всём удобстве этого подхода, часто оказывается, что он является недостаточно гибким или производительным.
Альтернативным способом сериализации, предоставляемым JVM, является использование интерфейса 'java.io.Externalizable' с переопределением методов 'writeExternal' и 'readExternal'. При этом очень легко перепутать порядок записи и чтения полей, что может привести к появлению трудноуловимой ошибки.
Рассмотрим пример:
public class ExternalizableTest implements Externalizable
{
public String name;
public String host;
public int port;
....
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
out.writeInt(port); // <=
out.writeUTF(name);
out.writeUTF(host);
}
@Override
public void readExternal(ObjectInput in) throws IOException
{
this.name = in.readUTF(); // <=
this.host = in.readUTF();
this.port = in.readInt();
}
}
В данном примере порядок сериализации полей объекта: port, name, host, type. Порядок десериализации: name, host, port, type. При сериализации первым полем идет целочисленное значение, а при десериализации – строка. Такое нарушение порядка десериализации приведет к исключению 'java.io.EOFException'. Можно сказать, что "повезло", так как эта ошибка проявит себя при первой же попытке десериализовать объект.
А что если чуть-чуть не "повезет"? Например, так:
public class ExternalizableTest implements Externalizable
{
public String name;
public String host;
public int port;
....
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
out.writeInt(port);
out.writeUTF(name); // <=
out.writeUTF(host);
}
@Override
public void readExternal(ObjectInput in) throws IOException
{
this.port = in.readInt();
this.host = in.readUTF(); // <=
this.name = in.readUTF();
}
}
Последовательность десериализации опять не соответствует последовательности сериализации: перепутаны местами строковые поля 'name' и 'host'. В этом случае падения никакого не будет и объект успешно восстановится, но значения этих полей будут перепутаны. Такое обнаружить уже будет не так легко.
Исправленный вариант:
public class ExternalizableTest implements Externalizable
{
public String name;
public String host;
public int port;
....
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
out.writeInt(port);
out.writeUTF(name);
out.writeUTF(host);
}
@Override
public void readExternal(ObjectInput in) throws IOException
{
this.port = in.readInt();
this.name = in.readUTF();
this.host = in.readUTF();
}
}
Анализатор обнаружил выражение 'return', возвращающее всегда пустую коллекцию, которая была определена как локальная переменная.
Чаще всего это происходит из-за того, что в коллекцию забыли добавить элементы:
public List<Property> getProperties()
{
List<Property> properties = new ArrayList<>();
Property p1 = new Property();
p1.setName("property1");
p1.setValue(42);
return properties;
}
Объект подходящего типа был создан, но разработчик пропустил вызов 'properties.add(p1)', из-за чего метод 'getProperties' возвращает некорректные данные. Исправленный фрагмент кода:
public List<Property> getProperties()
{
List<Property> properties = new ArrayList<>();
Property p1 = new Property();
p1.setName("property1");
p1.setValue(42);
properties.add(p1); // <=
return properties;
}
В случае, когда необходимо вернуть пустую коллекцию, лучше сделать это явно:
public List<Property> getMutableProperties()
{
return new ArrayList<>();
}
public List<Property> getImmutableProperties()
{
return Collections.emptyList();
}
Анализатор обнаружил подозрительное условие, которое может содержать copy-paste ошибку.
Диагностика носит эмпирический характер, поэтому проще показать на примере, как она работает, чем объяснить сам принцип работы анализатора. Рассмотрим пример:
if (m_a != a ||
m_b != b ||
m_b != c) // <=
{
....
}
Из-за того, что имена переменных очень похожи, в коде допущена опечатка. Ошибка находится в третьей строке. Переменную 'c' следовало сравнить с 'm_c', а не с 'm_b'. Даже читая этот текст сложно заметить ошибку. Обратите внимание на окончания в названии переменных.
Правильный вариант:
if (m_a != a ||
m_b != b ||
m_c != c)
{
....
}
Если анализатор выдал предупреждение V6085, то внимательно изучите соответствующий фрагмент кода. Иногда опечатку бывает сложно заметить.
Данная диагностика классифицируется как:
Анализатор обнаружил фрагмент кода, в котором оператор 'if' расположен на той же строке, что и закрывающая скобка предыдущего оператора 'if'. Возможно, что в этом месте пропущено ключевое слово 'else', из-за чего программа работает не так, как ожидал программист.
Рассмотрим пример подобного кода:
public void fillTrialMessage(User user, Response response)
{
if (user.getTrialTime() > 7) {
// Do not set message
} if (user.getTrialTime() > 0) { // <=
response.setTrialMessage("Trial ends soon");
} else {
response.setTrialMessage("Trial has ended");
}
}
Здесь разработчик подразумевал, что сообщение в ответе заполнится только в случае, когда триальный период у пользователя подходит к концу или уже закончился. Однако, из-за пропущенного слова 'else', пользователь будет оповещён даже если до конца этого периода ещё несколько месяцев. После исправления фрагмент кода будет выглядеть следующим образом:
public void fillTrialMessage(User user, Response response)
{
if (user.getTrialTime() > 7) {
// Do not set message
} else if (user.getTrialTime() > 0) {
response.setTrialMessage("Trial ends soon");
} else {
response.setTrialMessage("Trial has ended");
}
}
Если же использование конструкции else-if не подразумевалось, следует исправить форматирование и перенести второй 'if' на следующую строку:
public void doSomething()
{
if (condition1) {
foo();
}
if (condition2) {
bar();
} else {
baz();
}
}
Такой код будет более привычен для большинства программистов и не вызовет подозрений. Кроме того, анализатор перестанет выдавать лишнее предупреждение.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6086. |
Анализатор обнаружил ситуацию, когда отсутствие доступного конструктора по умолчанию при десериализации может привести к 'java.io.InvalidClassException'.
При использовании интерфейса 'java.io.Serializable' весь контроль над сериализацией достается JVM. Для выполнения десериализации под объект выделяется память, после чего его поля заполняются значениями из потока байтов без вызова конструктора. Стоит учесть, что если у сериализуемого класса имеется несериализуемый родитель, то механизм десериализации будет запускать его конструктор по умолчанию. В случае его отсутствия возникнет исключение 'java.io.InvalidClassException'.
Рассмотрим синтетический пример:
class Parent {
private String parentField;
public Parent(String field) {
this.parentField = field;
}
// ....
}
class Child extends Parent implements Serializable {
public String childField;
public Child() {
super("");
}
public Child(String field1, String field2) {
super(field1);
this.childField = field2;
}
// ....
}
Так как родительский класс не является сериализуемым, то при десериализации объекта класса 'Child' встроенный механизм десериализации попытается вызвать конструктор по умолчанию и выбросит исключение при его отсутствии.
Чтобы все корректно проинициализировалось, достаточно в родительском классе определить доступный конструктор по умолчанию:
class Parent {
private String parentField;
public Parent() {
this.parentField = "";
}
public Parent(String field) {
this.parentField = field;
}
// ....
}
При реализации интерфейса 'java.io.Externalizable' вызывается пользовательская логика: способ сериализации и десериализации описывается в методах 'writeExternal' и 'readExternal'. Во время десериализации всегда вызывается публичный конструктор по умолчанию, а потом уже на созданном объекте вызывается метод 'readExternal'. Если такого конструктора не будет, то возникнет 'java.io.InvalidClassException'.
class Parent implements Externalizable
{
private String field;
public Parent(String field) {
this.field = field;
}
public void writeExternal(ObjectOutput arg0) throws .... {
// serializable logic
}
public void readExternal(ObjectInput in) throws .... {
// deserializable logic
}
// ....
}
Подходящий конструктор в данном классе отсутствует. Если сериализовать объект этого класса, то никаких проблем не возникнет. А вот если сериализованный ранее объект захотят восстановить, то возникнет исключение.
Для исправления ситуации достаточно прописать доступный конструктор без параметров:
class Parent implements Externalizable
{
private String field;
public Parent() {
this.field = "";
}
public Parent(String field) {
this.field = field;
}
// ....
}
Данное правило находит тернарные операторы, в которых происходит неявное приведение численных типов. Это может нарушить логику работы программы из-за неожиданной смены типа объекта.
Рассмотрим пример подобной ошибки:
public void writeObject(Serializer serializer, Object o)
{
....
else if (o instanceof Integer)
{
serializer.writeInt((Integer) o);
}
else if (o instanceof Double)
{
serializer.writeDouble((Double) o);
}
....
}
public void serialize(Serializer serializer)
{
Object data = condition ? 5 : 0.5; // <=
writeObject(serializer, data);
}
В данном случае фактическим аргументом метода 'writeObject' всегда будет являться число типа 'double': 5.0 или 0.5. Из-за этого внутри 'writeObject' будет выполнена не та ветка конструкции if-else-if, которая подразумевалась программистом. Исправление этой ошибки заключается в замене тернарного оператора на блок if-else:
public void serialize(Serializer serializer)
{
if (condition)
{
writeObject(serializer, 5);
}
else
{
writeObject(serializer, 0.5);
}
// or
Object data;
if (condition)
{
data = 5;
}
else
{
data = 0.5;
}
writeObject(serializer, data);
}
Особенность этой ошибки состоит в том, что взаимозаменяемые условный и тернарный операторы в реальности могут иметь различное поведение.
Анализатор обнаружил потенциально возможную ошибку, связанную с тем, что неаккуратно закомментированный фрагмент кода приводит к изменению логики выполнения программы.
Диагностическое правило ищет комментарии, похожие на код, между оператором 'if (...)' и его 'then'-ветвью, причем 'then'-ветвь отформатирована подозрительным образом. В этом случае можно предположить, что текущая 'then'-ветвь стала результатом неудачного рефакторинга.
Рассмотрим пример:
if (hwndTaskEdit == null)
// hwndTaskEdit = getTask(...);
if (hwndTaskEdit != null)
{
...
}
Программа потеряла смысл: условие второго оператора 'if' никогда не выполняется.
Корректный вариант кода:
// if (hwndTaskEdit == null)
// hwndTaskEdit = getTask(...);
if (hwndTaskEdit != null)
{
...
}
Анализатор не будет выдавать предупреждение, если форматирование кода будет соответствовать логике программы.
Пример кода:
if (isReady)
// some comment
if (isSmt)
{
...
}
Данная диагностика классифицируется как:
|
Анализатор обнаружил в конструкторе класса обращение к полю, которое ещё не было инициализировано.
Все поля в Java неявно инициализируются значениями по умолчанию соответствующих типов. Для ссылочных типов этим значением является 'null'.
Рассмотрим пример подобной ошибки:
public class Test
{
private Object data;
private DataProvider dataProvider;
public Test(DataProvider provider)
{
this.data = dataProvider.get();
this.dataProvider = dataProvider;
}
}
Здесь производится обращение к полю класса, а не параметру конструктора, из-за чего возникает 'NullPointerException' при каждом вызове этого конструктора. Исправленный вариант:
public class Test
{
private Object data;
private DataProvider dataProvider;
public Test(DataProvider provider)
{
this.data = provider.get();
this.dataProvider = provider;
}
}
Другой ошибкой, связанной с использованием неинициализированных ссылочных полей, является их сравнение с 'null'. В этом случае условия являются всегда истинными или всегда ложными, что показывает ошибку в построении логики программы.
public class Test
{
private DataProvider dataProvider;
public Test()
{
if (dataProvider != null)
{
dataProvider = new DataProvider();
}
}
}
Анализатор не будет выдавать данное предупреждение, если поле было явно проинициализировано. В том числе, и значением 'null'.
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6090. |
Анализатор обнаружил геттер/сеттер, который обращается к полю, отличному от указанного в названии.
Такие ошибки обычно появляются вследствие невнимательности, неаккуратного использования автодополнения или copy-paste.
Рассмотрим пример, на котором анализатор выдаст предупреждение:
public class Vector2i
{
private int x;
private int y;
public void setX(int x)
{
this.x = x;
}
public int getX()
{
return x;
}
public void setY(int y)
{
this.y = y;
}
public int getY()
{
return x; // <=
}
}
Корректный вариант кода:
public class Vector2i
{
private int x;
private int y;
public void setX(int x)
{
this.x = x;
}
public int getX()
{
return x;
}
public void setY(int y)
{
this.y = y;
}
public int getY()
{
return y;
}
}
Для реализации таких методов лучше использовать возможности IDE или кодогенерацию, предоставляемую библиотекой Lombok.
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6091. |
Анализатор обнаружил возвращение из метода 'AutoCloseable' объекта, который используется в выражении try-with-resources.
Выражение try-with-resources автоматически закрывает все ресурсы при выходе из него, то есть возвращённый ресурс всегда будет уже закрытым. В подавляющем большинстве случаев закрытые ресурсы не имеют применения, а вызов их методов почти всегда приведёт к возникновению 'IOException'.
public InputStream getStreamWithoutHeader() throws IOException
{
try (InputStream stream = getStream())
{
stream.skip(HEADER_LENGTH);
return stream;
}
}
В данном случае 'stream' будет закрыт перед передачей управления вызывающему методу и произвести какие-либо действия с этим потоком будет невозможно.
Вариант исправления:
public InputStream getStreamWithoutHeader() throws IOException
{
InputStream stream = getStream();
stream.skip(HEADER_LENGTH);
return stream;
}
Анализатор обнаружил фрагмент кода, в котором может произойти автоматическая распаковка null-значения, что приведёт к возникновению 'NullPointerException'.
Данную ошибку достаточно часто можно встретить внутри операторов сравнения. Например, 'Boolean' можно использовать как флаг с тремя значениями: ложным, истинным и неустановленным. Тогда при проверке того, что какой-либо флаг явно выставлен в нужное значение, можно написать:
public void doSomething()
{
Boolean debugEnabled = isDebugEnabled();
if (debugEnabled == true)
{
...
}
}
Однако, при сравнении примитива с упакованным значением всегда происходит автоматическая распаковка, что и вызывает 'NullPointerException'. Исправить данный фрагмент можно несколькими способами:
public void doSomething()
{
Boolean debugEnabled = isDebugEnabled();
if (debugEnabled != null && debugEnabled == true)
{
...
}
// or
if (Objects.equals(debugEnabled, true))
{
...
}
}
В отличие от большинства операторов, тернарный оператор позволяет смешивать в выражении примитивы и обёртки, производя автоматическую упаковку при приведении к общему типу. Это позволяет достаточно легко сделать опечатку:
boolean x = httpRequest.getAttribute("DEBUG_ENABLED") != null
? (boolean) httpRequest.getAttribute("DEBUG_ENABLED")
: null;
Здесь выражение тернарного оператора имеет тип 'Boolean' как общий для его операндов, после чего распаковывается обратно в примитив при присвоении в переменную 'x'. После исправления:
boolean x = httpRequest.getAttribute("DEBUG_ENABLED") != null
? (boolean) httpRequest.getAttribute("DEBUG_ENABLED")
: false;
Данная диагностика классифицируется как:
|
В выражении присутствует операция деления целочисленных типов данных. Полученное значение неявно преобразуется к типу с плавающей точкой. Обнаружив такую ситуацию, анализатор предупреждает о наличии потенциальной ошибки, которая может привести к вычислению неточного результата.
Рассмотрим пример:
int totalTime = 1700;
int operationNum = 900;
double averageTime = totalTime / operationNum;
Программист может ожидать, что переменная 'averageTime' будет иметь значение '1.888(8)', однако при выполнении программы будет получен результат равный '1.0'. Это происходит потому, что операция деления выполняется с целочисленными типами и только затем приводится к типу с плавающей точкой.
Как и в предыдущем случае, ошибку можно исправить 2 способами.
Первый способ - изменить типы переменных:
double totalTime = 1700;
double operationNum = 900;
double averageTime = totalTime / operationNum;
Второй способ - использовать явное приведение типов.
int totalTime = 1700;
int operationNum = 900;
double averageTime = (double)(totalTime) / operationNum;
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6094. |
Анализатор обнаружил фрагмент кода, в котором внутри синхронизированной функции или блока синхронизации выполнен вызов 'Thread.sleep(....)'.
При вызове 'Thread.sleep(....)' текущий поток засыпает, продолжая удерживать захваченный монитор объекта. Это приводит к тому, что другие потоки, пытающиеся выполнить синхронизацию на этом объекте, будут простаивать в ожидании пробуждения потока. Это может привести к понижению производительности, а в некоторых случаях даже к взаимной блокировке потоков.
Рассмотрим пример подобного кода:
private final Object lock = new Object();
public void doSomething() {
synchronized(lock) {
....
Thread.sleep(1000);
....
}
}
Вместо 'Thread.sleep()' лучше использовать метод 'lock.wait(....)', который переведет текущий поток в ожидание на указанное время с освобождением монитора, что позволит другим потокам не простаивать. Однако необходимо учитывать, что поток в этом случае может быть ''разбужен'' ранее указанного времени. Поэтому обычно следует проверять какое-либо условие, подтверждающее, что пробуждение потока произошло не ранее, чем планировалось разработчиком:
private final Object lock = new Object();
public void doSomething() {
synchronized(lock) {
....
while(!ready()) {
lock.wait(1000)
}
....
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
|
Анализатор обнаружил подозрительный фрагмент кода, где для сравнения чисел с плавающей точкой используется оператор '==' или '!='. Такие участки кода могут служить источником ошибок.
Рассмотрим для начала корректный пример кода:
double a = 0.5;
if (a == 0.5) //ok
++x;
В данном случае сравнение можно считать верным. Перед сравнением переменная 'a' явно инициализируется значением '0.5'. С этим же значением производится сравнение. Результатом выражения будет 'истина'.
Итак, в некоторых случаях точные сравнения допустимы. Но часто так сравнивать нельзя. Рассмотрим пример ошибочного кода:
double b = Math.sin(Math.PI / 6.0);
if (b == 0.5) //err
++x;
Условие 'b == 0.5' при проверке оказалось ложным из-за того, что значение выражения 'Math.sin(Math.PI / 6.0)' равно 0.49999999999999994. Это число очень близко к '0.5', но ему не равно.
Одним из вариантов решения является сравнение разности значений с каким-то значением (погрешностью, в данном случае - переменная 'epsilon'):
double b = Math.sin(Math.PI / 6.0);
if (Math.abs(b - 0.5) < epsilon) //ok
++x;
Необходимо выбирать адекватную погрешность в зависимости от того, какие величины сравниваются.
Анализатор указывает на участки кода, где в сравнении чисел с плавающей точкой используются операторы '!=' или '=='. Является это сравнение ошибочным или нет, может решить только программист.
Дополнительные ресурсы:
Данная диагностика классифицируется как:
Анализатор обнаружил фрагмент кода, в котором литерал типа 'long' объявлен с маленькой буквой 'l' в конце.
Рассмотрим пример:
long value = 1111l;
В данном случае маленькую букву 'l' легко спутать с цифрой '1'. Все зависит от текущего шрифта, так как в некоторых случаях разница между данными символами вовсе незаметна, что повышает вероятность ошибочного восприятия значения литерала. Чтобы не произошла путаница, рекомендуется объявлять 'long' литералы с большой буквой 'L' на конце:
long value = 1111L;
Диагностическое правило выдает предупреждение далеко не во всех случаях. Оно имеет ряд исключений, например, если:
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что метод не переопределяет метод базового класса или интерфейса, однако сигнатуры этих методов очень похожи.
Рассмотрим синтетический пример:
public class Base
{
public String someThing()
{
return "Base";
}
}
public class Derived extends Base
{
public String something() // <=
{
return "Derived";
}
}
В приведенном примере, при переопределении метода 'someThing' в классе 'Derived' была допущена опечатка в названии метода. Из-за этой опечатки переопределения метода не произойдет. Эффект от этой ошибки проявится при использовании полиморфизма:
...
List<Base> list = new ArrayList<>();
list.add(new Base());
list.add(new Derived());
StringBuilder builder = new StringBuilder();
for (Base base : list)
{
builder.append(base.someThing());
}
String result = builder.toString();
...
Из-за ошибки переопределения метода в переменной 'result' будет сохранена строка 'BaseBase', а не 'BaseDerived' как ожидалось.
Для защиты от ошибок переопределения метода в Java используется аннотация '@Override'. Если метод помечен этой аннотацией и не переопределяет никакой метод, то компилятор сообщит об ошибке и программа не скомпилируется.
Исправленный вариант кода:
public class Base
{
public String someThing()
{
return "Base";
}
}
public class Derived extends Base
{
@Override
public String someThing() //ok
{
return "Derived";
}
}
Вот еще несколько примеров, когда произойдет срабатывание анализатора.
import first.A;
public class Base
{
public void someThing(A input)
{
...
}
}
import second.A;
public class Derived extends Base
{
public void someThing(A input) // <=
{
...
}
}
Имена типов аргументов 'input' в объявлении методов одинаковые. Однако данные типы расположены в разных пакетах 'first' и 'second', из-за чего произойдет перегрузка метода вместо переопределения.
package first;
public class Base
{
void someThing()
{
...
}
}
package second;
import first.Base;
public class Derived extends Base
{
void someThing() // <=
{
...
}
}
В этом примере классы 'Base' и 'Derived' расположены в разных пакетах, 'first' и 'second' соответственно. Метод 'someThing' в классе 'Base' объявлен без каких-либо модификаторов, поэтому имеет уровень доступа по умолчанию (package-private), из-за чего его не видно в классе 'Derived', который находится в другом пакете. Именно поэтому переопределения метода не произойдет, о чем и предупредит анализатор.
Анализатор выявил цикл, который может содержать ошибку или быть неоптимальным. Используется типичный паттерн кода, когда для всех пар элементов массива выполняется некая операция. При этом, как правило, нет смысла выполнять операцию для пары, состоящей из одного и того-же элемента при 'i == j'.
Пример:
for (int i = 0; i < size; i++)
for (int j = i; j < size; j++)
....
Есть большая вероятность, что правильнее или эффективнее использовать следующий код для обхода массивов:
for (int i = 0; i < size; i++)
for (int j = i + 1; j < size; j++)
....
Данная диагностика классифицируется как:
Анализатор обнаружил вызов метода, в который в качестве аргумента передаётся сам объект, у которого вызывается метод. Вероятно, данный код содержит ошибку, и в метод должен передаваться другой объект.
Рассмотрим пример:
a.foo(a);
Здесь из-за опечатки используется неверное имя переменной. Тогда корректный код должен был быть таким:
a.foo(b);
или таким:
b.foo(a);
Пример кода из реального приложения:
public class ByteBufferBodyConsumer {
private ByteBuffer byteBuffer;
....
public void consume(ByteBuffer byteBuffer) {
byteBuffer.put(byteBuffer);
}
}
Здесь в 'byteBuffer' пытаются вставить его собственные значения.
Корректный код должен выглядеть так:
public class ByteBufferBodyConsumer {
private ByteBuffer byteBuffer;
....
public void consume(ByteBuffer byteBuffer) {
this.byteBuffer.put(byteBuffer);
}
}
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6100. |
Анализатор обнаружил в коде сравнение результата метода 'Comparable.compareTo' или ему подобному с конкретным ненулевым значением (в частности, с 1 и -1). В свою очередь, в спецификации языка Java контракт этого метода подразумевает, что может вернуться любое положительное число или любое отрицательное.
Работоспособность конструкции 'compareTo == 1' зависит от реализации этого метода. Поэтому сравнивать результат с конкретным числом считается плохой практикой, которая в некоторых случаях может привести к трудноуловимым ошибкам. В таких случаях корректно писать 'compareTo > 0'.
В качестве примера можно привести следующий некорректный код:
void smt(SomeType st1, SomeType st2, ....)
{
....
if (st1.compareTo(st2) == 1)
{
// some logic
}
....
}
Разработчик долгое время мог работать над другим проектом, где, в силу специфики реализации интерфейса 'Comparable', сравнивать результат 'compareTo' с 1 было корректно. Перейдя на следующий проект, он продолжил так сравнивать, но специфика реализации метода уже отличалась - в зависимости от тех или иных условий возвращались разные положительные числа.
Исправленный вариант кода:
void smt(SomeType st1, SomeType st2, ....)
{
....
if (st1.compareTo(st2) > 0)
{
// some logic
}
....
}
Анализатор также считает код ошибочным, если в нём сравнивается результат работы двух 'compareTo' методов. Подобный код встречается крайне редко, однако на него в любом случае стоит обратить внимание.
Данная диагностика классифицируется как:
|
Анализатор обнаружил поле, к которому производится обращение без синхронизации, хотя большая часть обращений к данному полю производилась в синхронизированном контексте.
Неполная синхронизация является основной причиной возникновения состояния гонки, когда общее состояние изменяется несколькими потоками одновременно, и результат работы программы зависит от того, в каком порядке будет выполняться код в потоках. Это приводит к совершенно различным ошибкам, которые проявляются в непредсказуемые моменты времени, а попытка повторения ошибки в целях отладки со схожими условиями работы может оказаться безуспешной.
Иначе говоря, поля должны либо синхронизироваться во всех случаях использования, либо не синхронизироваться вообще, чтобы не запутать разработчиков, которые будут поддерживать этот код. Поэтому, если анализатор выдает вам предупреждение V6102, рекомендуется удостовериться, что все обращения к полю полностью синхронизированы.
Простой пример из реального проекта, где доступ к полю 'acked' синхронизирован во всех случаях, кроме одного:
public class FixedTupleSpout implements IRichSpout
{
private static final Map<String, Integer> acked = new HashMap<>();
....
public static int getNumAcked(String stormId)
{
synchronized (acked)
{
return get(acked, stormId, 0);
}
}
public static void clear(String stormId)
{
acked.remove(stormId); // <=
....
}
public int getCompleted()
{
synchronized (acked)
{
ackedAmt = acked.get(_id);
}
....
}
public void cleanup()
{
synchronized (acked)
{
acked.remove(_id);
}
....
}
}
Из-за того, что доступ к 'acked' в методе 'clear' несинхронизирован, есть вероятность одновременного доступа к полю из разных потоков. Так как 'acked' непотокобезопасная коллекция HashMap, то велика вероятность испортить внутреннее состояние объекта. Чтобы исправить это, выражение 'acked.remove(stormId)' нужно заключить в блок 'synchronized':
public class FixedTupleSpout implements IRichSpout
{
private static final Map<String, Integer> acked = new HashMap<>();
....
public static int getNumAcked(String stormId)
{
synchronized (acked)
{
return get(acked, stormId, 0);
}
}
public static void clear(String stormId)
{
synchronized (acked))
{
acked.remove(stormId);
}
....
}
public int getCompleted()
{
synchronized (acked)
{
ackedAmt = acked.get(_id);
}
....
}
public void cleanup()
{
synchronized (acked)
{
acked.remove(_id);
}
....
}
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.). |
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6102. |
Анализатор обнаружил, что исключение 'InterruptedException' в блоке 'catch' было проигнорировано. В этом случае теряется информация о прерывании потока, что может подвергнуть риску способность приложения отменять функции или своевременно прекратить работу.
Каждый поток имеет статус прерывания – скрытое булево поле, которое хранит информацию о том, был поток прерван или нет. Для установки этого статуса нужно вызвать метод 'Thread.interrupt()'. По соглашению, любой метод (например, 'Object.wait()', 'Thread.sleep()' и так далее), который может выбросить 'InterruptedException', очищает статус прерывания, когда это происходит. Поэтому, если не обработать исключение должным образом, факт прерывания теряется, что не позволит вызывающей части программы среагировать на отмену выполнения. Однако возможны случаи, когда статус прерывания не будет сбрасываться при выбрасывании 'InterruptedException' (например, пользовательская реализация прерывания, методы сторонних библиотек), но рассчитывать на это не рекомендуется.
Чтобы информация о прерывании потока гарантированно не была потеряна, при поимке 'InterruptedException' следует либо вновь установить флаг прерывания с помощью 'Thread.interrupt()', либо пробросить пойманное исключение дальше без оборачивания его в какое-либо другое. В случае, когда обработка прерывания бессмысленна, данное исключение ловить не следует.
Рассмотрим ошибочный пример кода из реального приложения:
public void disconnect()
{
....
try
{
sendThread.join();
}
catch (InterruptedException ex)
{
LOG.warn("....", ex);
}
....
}
Корректный код должен выглядеть так:
public void disconnect()
{
....
try
{
sendThread.join();
}
catch (InterruptedException ex)
{
Thread.currentThread().interrupt();
LOG.warn("....", ex);
}
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил выражение, которое можно упростить. Иногда за такой избыточностью кроются логические ошибки.
Рассмотрим пример подозрительного кода:
boolean firstCond, secondCond, thirdCond;
....
if (firstCond || (firstCond && thirdCond))
....
Это выражение является избыточным. В случае, если 'firstCond == true', значение этого условия будет всегда истинным, независимо от значения 'thirdCond', если же 'firstCond == false', то значение выражения будет всегда ложно, опять же, независимо от значения переменной 'thirdCond'. Таким образом, выражение 'firstCond || (firstCond && thirdCond)' можно упростить:
if (firstCond)
Возможно, что программист ошибся и написал не ту переменную во втором подвыражении. Тогда корректный код мог бы выглядеть так:
if (firstCond || (secondCond && thirdCond))
Анализатор обнаружил, что в разделе итератора (iterator section) оператора 'for' производится инкремент или декремент переменной, не являющейся счетчиком.
Рассмотрим пример. Допустим, у нас имеется выражение следующего вида:
for (int i = 0; i != N; ++N)
Вероятно, данный фрагмент кода содержит ошибку, и вместо переменной 'N' в выражении инкремента '++N' необходимо использовать переменную 'i'. Корректный вариант кода должен выглядеть так:
for (int i = 0; i != N; ++i)
Рассмотрим следующий пример:
for (int i = N; i >= 0; --N)
Данный фрагмент кода также ошибочен. В выражении декремента '‑‑N' должна быть использована переменная 'i':
for (int i = N; i >= 0; --i)
Данная диагностика классифицируется как:
Анализатор обнаружил, что после явного приведения переменной к одному числовому типу данных осуществляется дальнейшее неявное приведение к другому числовому типу данных. Обычно это говорит о том, что явное приведение либо сделано ошибочно, либо является излишним.
В Java существует несколько видов преобразований над числовыми типами:
Когда в одном контексте встречается последовательность явных и неявных преобразований, то это повод присмотреться к коду повнимательней.
Рассмотрим пример подозрительного приведения типов, встретившийся в одном из реальных проектов:
public void method(...., Object keyPattern, ....)
{
....
if (keyPattern instanceof Integer)
{
int intValue = (Integer) keyPattern;
....
}
else if (keyPattern instanceof Short)
{
int shortValue = (Short) keyPattern;
....
}
....
}
После проверки 'keyPattern instanceof Short' переменную 'keyPattern' явно приводят к типу 'Short'. Но при присвоении значения переменой 'shortValue' происходит неявное приведение ранее сделанного каста к типу 'int', так как переменная 'shortValue' имеет тип 'int'. Компилятор Java не выдает здесь предупреждений, так как оба преобразования являются допустимыми, но программист, скорее всего, хотел указать для переменной 'shortValue' тип 'short'.
Исправленный вариант кода должен выглядеть так:
public void method(...., Object keyPattern, ....)
{
....
if (keyPattern instanceof Integer)
{
int intValue = (Integer) keyPattern;
....
}
else if (keyPattern instanceof Short)
{
short shortValue = (Short) keyPattern;
....
}
....
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что для математических расчётов используются константы недостаточной точности.
Рассмотрим пример:
double pi = 3.141592654;
Такая запись не совсем корректна и лучше использовать математические константы из статического класса Math. Корректный вариант кода:
double pi = Math.PI;
Анализатор не считает ошибочной явную запись констант в формате 'float'. Это связано с тем, что тип 'float' имеет меньше значащих разрядов по сравнению с типом 'double'. Поэтому на следующий код предупреждение выдано не будет:
float f = 3.14159f; //ok
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6107. |
Анализатор обнаружил переменную вещественного типа в качестве счётчика цикла 'for'. Поскольку числа с плавающей точкой не могут точно отобразить все действительные числа, использование таких переменных в цикле может привести к неожиданным результатам, таким как лишние итерации.
Рассмотрим подробнее:
for (double i = 0.0; i < 1.0; i += 0.1) {
....
}
Количество итераций в этом цикле будет равно 11 вместо ожидаемых 10. При выполнении кода без модификатора 'strictfp' в версиях Java ниже 17-ой результат операций с плавающей точкой может также зависеть от платформы. Чтобы избежать возможных проблем, следует использовать целочисленный тип для счётчика, а вычисления производить внутри:
for (var i = 0; i < 10; i++) {
double counter = i / 10.0;
....
}
Дополнительной причиной не использовать действительный тип может быть опасность бесконечного цикла. В качестве примера — следующий фрагмент:
for (float i = 100000000.0f; i <= 100000009.0f; i += 0.5f) {
....
}
Он возникает из-за слишком маленького инкремента относительно количества значащих цифр. Для предотвращения такой ситуации лучше также использовать целочисленный тип счётчика, а во избежание потери точности использовать 'double' для хранения значения:
for (var i = 0; i < 19; i++) {
double value = 100000000.0d + i / 2d;
....
}
Данная диагностика классифицируется как:
|
Данное диагностическое правило выявляет случаи использования псевдорандомного генератора случайных чисел, которые могут привести к недостаточно случайному распределению или предсказуемости генерируемого числа.
Случай 1.
Создание нового объекта типа 'Random' каждый раз, когда требуется случайное значение. Это неэффективно и может привести к получению чисел, которые не являются достаточно случайными, в зависимости от JDK.
Рассмотрим пример:
public void test() {
Random rnd = new Random();
}
Для большей эффективности и случайности распределения создайте один экземпляр класса 'Random', сохраните его и используйте повторно.
static Random rnd = new Random();
public void test() {
int i = rnd.nextInt();
}
Случай 2.
Анализатор обнаружил подозрительный код, инициализирующий генератор псевдослучайных чисел константным значением.
public void test() {
Random rnd = new Random(4040);
}
Числа, сгенерированные таким генератором, можно предугадать — они будут воспроизводиться снова и снова при каждом запуске программы. Чтобы этого избежать, не стоит использовать константное число. К примеру, можно воспользоваться текущим системным временем:
static Random rnd = new Random(System.currentTimeMillis());
public void test() {
int i = rnd.nextInt();
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки некорректного использования системных процедур и интерфейсов, связанных с обеспечением информационной безопасности (шифрования, разграничения доступа и пр.). |
Данная диагностика классифицируется как:
Данное диагностическое правило позволяет обнаружить использование переменных среды, которые можно заменить системным свойством.
Согласно документации, проблема может заключаться в том, что:
По этим причинам с большей вероятностью есть возможность получить непредвиденные побочные эффекты. Следовательно, если переменная среды содержит информацию, доступную другими способами, эту переменную использовать не следует.
Например, если операционная система предоставляет имя пользователя, оно всегда будет доступно в системном свойстве 'user.name'.
Плохая практика:
String user = System.getenv("USER");
Исправление:
String user = System.getProperty("java.name");
Помимо прямых вызовов метода 'System.getenv()', диагностика отслеживает методы по их сигнатурам, которые могут свидетельствовать о возвращении значений переменных среды.
Данная диагностика классифицируется как:
|
Анализатор обнаружил, что при создании массива в качестве значения для задания его длины может использоваться переменная или выражение, имеющие отрицательное значение.
Рассмотрим пример:
void process(boolean isNotCsv) {
String str = "column1,column2";
if (isNotCsv) {
str = "content";
}
var arr = new String[str.indexOf(',')];
....
}
Значение, возвращаемое методом 'indexOf', может оказаться равным -1. Это произойдёт в том случае, если в строке не окажется указанного символа. Тогда при создании массива 'arr' в качестве его длины будет выступать отрицательное значение. Это приведёт к выбросу исключения типа 'NegativeArraySizeException'.
Исправленный вариант метода 'process' может выглядеть следующим образом:
public static void process(boolean isNotCsv) {
String str = "column1,column2";
if (isNotCsv) {
str = "content";
} else {
var arr = new String[str.indexOf(',')];
....
}
....
}
Данная диагностика классифицируется как:
Метод 'getClass' используется для получения типа объекта, у которого данный метод был вызван. Аналогично можно использовать литерал 'class' непосредственно с самим типом, а не объектом.
При использовании метода 'getClass' вместе с литералом 'class' будет получена информация о типе 'Class'. Рассмотрим пример:
var typeInfo = Integer.class.getClass();
В результате вызова данного метода переменная 'typeInfo' будет хранить информацию о типе 'Class'. Это связано с тем, что литерал 'class' хранит информацию типа 'Class<Integer>'. Когда происходит вызов метода 'getClass', то информация будет, соответственно, о типе 'Class', а не о типе 'Integer'. Если нужно получать информацию о типе 'Integer', то достаточно использовать только литерал 'class':
var typeInfo = Integer.class;
Помимо этого, может произойти случайное дублирование вызова 'getClass':
Integer i = 0;
var typeInfo = i.getClass().getClass();
Как и в первом примере, при первом вызове 'getClass' будет получен объект типа 'Class<Integer>'. При повторном вызове 'getClass' будет получена информация о типе 'Class', а не о типе 'Integer'. Чтобы получить информацию именно о типе 'Integer', достаточно вызвать метод один раз:
Integer i = 0;
var typeInfo = i.getClass();
Если требуется информация именно о типе 'Class', то достаточно такой конструкции:
var classType = Class.class;
Допускается использование с 'Class.class' метода 'getClass', так как результат не меняется:
var classType = Class.class.getClass();
Анализатор обнаружил операцию деления или вычисления остатка от деления с целыми числами, в которой абсолютное значение левого операнда всегда меньше абсолютного значения правого операнда. Такое выражение содержит ошибку или является избыточным.
Рассмотрим пример:
int a = 5;
int b = 10;
int result = a / b; // Результат: 0
В результате выполнения данного фрагмента кода переменная 'result' всегда будет равна нулю. Такие операции могут быть логическими ошибками: программист использовал некорректное значение или указал не ту переменную.
Если операнды указаны верно и требуется точное значение результата деления, исправить данный фрагмент кода можно путём явного приведения типов перед выполнением деления:
int a = 5;
int b = 10;
double result = (double)a / b; // Результат: 0.5
Подобная ситуация не будет ошибкой, когда операция деления выполняется с вещественными числами:
double a = 5;
double b = 10;
double result = a / b; // Результат: 0.5
При использовании операции остатка от деления, если абсолютное значение левого операнда меньше правого операнда, результат выражения всегда будет равен левому операнду. Такая операция является избыточной. Пример:
int a = 5;
int b = 10;
int result = a % b; // Результат: 5
Данная диагностика классифицируется как:
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V6113. |
Анализатор обнаружил в классе поля, реализующие интерфейс 'Closeable' (или 'AutoCloseable'), но ни в одном методе анализируемого класса для них не был вызван метод 'close'. Такой код говорит о том, что ресурс может быть не закрыт.
class A {
private FileWriter resource;
public A(String name) throws IOException {
resource = new FileWriter(name);
}
....
}
В данном примере поле 'resource' было инициализировано, но внутри класса 'A' для него не было вызова метода 'close'. Отсутствие вызова метода закрытия приводит к тому, что ресурс не будет освобождён, даже когда ссылка на объект класса 'A' будет утеряна. Из-за этого может произойти нарушение логики работы программы. Например, если ресурс не будет освобождён, то из-за этого будет невозможно получить доступ к нему из другой части кода.
Исправить подобную ситуацию можно несколькими способами. Одним из них является добавление в класс 'A' интерфейса 'Closeable' или 'AutoClosable' с методом 'close', внутри которого будет происходить закрытие ресурса:
class A implements Closeable {
private FileWriter resource;
public A(String name) throws IOException {
resource = new FileWriter(name);
}
public void close() throws IOException {
resource.close();
}
}
Не всегда логика программы позволяет реализовать в классе этот интерфейс. Альтернативным решением будет закрытие ресурса в одном из методов класса 'A':
class A {
private FileWriter resource;
public A(String name) throws IOException {
resource = new FileWriter(name);
}
public void method() throws IOException {
....
resource.close();
....
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил в классе, реализующем интерфейс 'Closeable' (или 'AutoCloseable'), поля (ресурсы), которые также реализуют этот интерфейс, но не освобождаются в методе 'close' анализируемого класса.
сlass A implements Closeable {
private FileWriter resource;
public A(String name) {
resource = new FileWriter(name);
}
public void close() {
// Не освобождается resource
}
}
В данном примере поле 'resource' было инициализировано, но внутри класса 'A' не было вызова 'close' для этого поля. Отсутствие вызова метода закрытия приводит к тому, что ресурс не будет освобожден, даже когда для объекта класса 'A' был вызван метод 'close'. Из-за этого может произойти нарушение логики работы программы. Например, если ресурс не будет освобожден, то из-за этого не получится получить доступ к нему из другой части кода.
Такая ошибка может сохраниться, даже если в одном из методов происходит закрытие ресурса:
сlass A implements Closeable {
private FileWriter resource;
public A(String name) {
resource = new FileWriter(name);
}
public void endWrite() {
resource.close();
}
public void close() {
// Не освобождается resource и не вызывается метод endWrite
}
}
Исправить подобную ситуацию можно несколькими способами. Одним из них является освобождение ресурса внутри метода 'close' анализируемого класса:
сlass A implements Closeable {
private FileWriter resource;
public A(String name) {
resource = new FileWriter(name);
}
public void close() {
resource.close();
}
}
Другой вариант исправления — добавление вызова метода, в котором происходит закрытие ресурса в метод close:
сlass A implements Closeable {
private FileWriter resource;
public A(String name) {
resource = new FileWriter(name);
}
public void endWrite() {
resource.close();
}
public void close() {
endWrite();
}
}
Данная диагностика классифицируется как:
Анализатор обнаружил метод 'close', внутри которого происходит освобождение полей (ресурсов). Однако сам класс не реализует интерфейс 'Closeable' или 'AutoCloseable'.
Такой код может привести к следующим проблемам:
Во всех описанных случаях ресурсы, удерживаемые объектом, не будут освобождены. Из-за этого может произойти нарушение логики работы программы. Например, если ресурс не будет освобождён, то из-за этого не получится получить доступ к нему из другой части кода.
Пример кода, который может привести к ошибкам:
сlass SomeClass {
private FileWriter resource;
public SomeClass(String name) {
resource = new FileWriter(name);
}
public void close() {
resource.close();
}
}
Исправленная версия класса 'SomeClass' будет выглядеть следующим образом:
сlass SomeClass implements Closeable {
private FileWriter resource;
public SomeClass(String name) {
resource = new FileWriter(name);
}
public void close() {
resource.close();
}
}
Может быть ситуация, когда сам класс реализует интерфейс или наследуется от класса, который уже содержит метод 'close':
interface SomeInterface {
public void close();
}
class SomeInterfaceImpl implements SomeInterface {
private FileWriter resource;
public SomeInterfaceImpl(String name) {
resource = new FileWriter(name);
}
public void close() {
resource.close();
}
}
В таком случае имеется три решения. Первое связано с объявлением у класса с методом 'close' интерфейса 'Closeable' (или 'AutoCloseable'):
class SomeInterfaceImpl implements SomeInterface, Closeable {
private FileWriter resource;
public SomeInterfaceImpl(String name) {
resource = new FileWriter(name);
}
public void close() {
resource.close();
}
}
Второе решение связано с расширением интерфейса. В приведённом примере можно объявить у 'SomeInterface' родительский интерфейс 'Closeable' (или 'AutoCloseable'):
interface SomeInterface extends Closeable {
public void close();
}
Если 'close' от 'SomeInterface' имеет реализации, в которых не происходит освобождение ресурсов, или наследовать 'Closeable' или 'AutoCloseable' по каким-то причинам нежелательно, тогда стоит переименовать данный метод, так как такое название характерно для этих интерфейсов:
interface SomeInterface {
public void shut();
}
Анализатор обнаружил подозрительное приведение типов. Результат бинарной операции приводится к типу с большим диапазоном.
Рассмотрим пример:
long multiply(int a, int b) {
return (long)(a * b);
}
Такое преобразование избыточно. Тип 'int' и так бы автоматически расширился до типа 'long'.
Скорее всего, подобный паттерн приведения используется для защиты от переполнения, но он неправильный. При перемножении переменных типа 'int' всё равно произойдёт переполнение, и только уже бессмысленный результат умножения будет явно расширен до типа 'long'.
Для корректной защиты от переполнения можно привести один из аргументов к типу 'long'. Исправленный код:
long multiply(int a, int b) {
return (long)a * b;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
Анализатор обнаружил ситуацию, когда оригинальная информация об исключении была утеряна при повторной генерации из блока 'catch'. Из-за этого ошибки превращаются в трудноотлаживаемые.
Рассмотрим пример некорректного кода:
try {
sb.append((char) Integer.parseInt(someString));
....
} catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
В данном случае перехваченное исключение хотели перебросить и не передали полезную информацию в виде сообщения и stacktrace.
Корректный вариант кода:
try {
sb.append((char) Integer.parseInt(someString));
....
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
В исправленном варианте исходное исключение передается в качестве внутреннего, что полностью сохраняет информацию об исходной ошибке.
Другим вариантом исправления может являться выбрасывание исключения с сообщением пользователю:
try {
sb.append((char) Integer.parseInt(someString));
....
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"String " + someString + " is not number"
);
}
В данной ситуации стек оригинальной ошибки был утерян, но описанная информация в новом исключении внесёт большую ясность при отладке этого кода.
Если потеря информации об исключении является ожидаемым поведением, то вы можете заменить имена 'catch'-параметров на "ignore" или "expected", и исключение выдаваться не будет.
Данная диагностика классифицируется как:
Анализатор обнаружил использование побитового 'AND' (&) с операндами, при которых результат операции всегда равен 0. Возможно, использован неподходящий оператор или операнд.
Пример:
final int ACCESS_READ = 0b001;
final int ACCESS_WRITE= 0b010;
final int adminMask = ACCESS_READ & ACCESS_WRITE; // <=
В данном примере создаётся маска из битовых флагов ('final' переменных) для доступа к операциям с файлом. В результате выполнения операции побитового 'AND' в переменной 'adminMask' все биты будут равны нулю, и маска станет бесполезной.
Корректная реализация создания маски будет иметь вид:
final int adminMask = ACCESS_READ | ACCESS_WRITE;
Данная диагностика классифицируется как:
Анализатор обнаружил, что выполняется побитовая операция 'AND' (&) с операндом, равным 0. Возможно, использован неподходящий оператор или операнд.
Рассмотрим пример:
public class TypeAttribute {
private static final int NOT_PUBLIC = 0x0,
private static final int PUBLIC = 0x1,
private static final int NESTED_PUBLIC = 0x2,
private static final int NESTED_PRIVATE = 0x3
public static boolean isNotPublic(int type) {
return (type & NOT_PUBLIC) == NOT_PUBLIC;
}
}
Метод 'isNotPublic' проверяет наличие флага 'NOT_PUBLIC' у аргумента 'type'.
Подобный метод проверки не имеет смысла, так как у флага 'NOT_PUBLIC' нулевое значение. Это значит, что использование его как операнда оператора '&' приводит к всегда нулевому значению результата. Поэтому в представленной реализации мы всегда получаем истинное условие.
Корректная реализация проверки может выглядеть следующим образом:
public static boolean isNotPublic(int type) {
return type == NOT_PUBLIC;
}
Также анализатор выдаст срабатывание на использование нулевого операнда с оператором '&='. Такой код выглядит подозрительно, так как равенство нулю одного из операндов означает, что и результат выражения будет равен нулю.
Данная диагностика классифицируется как:
Анализатор обнаружил потенциально ошибочное игнорирование возвращаемого значения, которое в большинстве случаев каким-либо образом используется.
Рассмотрим синтетический пример:
class Item {
int getID() {
....
}
}
class ItemController {
int setNewItem(Item lastItem) {
Item newItem = new Item(lastItem.getID());
....
newItem.getID(); // <=
return newItem.getID();
}
}
В данном примере возвращаемое значение метода 'getID' используется во всех случаях, кроме одного. Если результат не используется менее чем в 10% случаев от общего количества вызовов, анализатор выдаст предупреждение.
В некоторых ситуациях возвращаемое значение действительно не требуется использовать. Например, если метод имеет побочные эффекты (изменение свойств, полей, запись/чтение файла и прочее), возвращаемым значением можно пренебречь.
Чтобы помочь программисту понять, что такое поведение было задумано, можно оставить рядом с вызовом комментарий о том, что результат будет проигнорирован:
int updateItem() {
....
return 0;
}
....
void someMethod() {
....
updateItem(); // ignore result
}
Анализатор обнаружил потенциальную ошибку, связанную с использованием спецификатора 'Y' в паттерне форматирования даты. Возможно, предполагалось использовать спецификатор 'y'.
Рассмотрим пример:
Date date = new Date("2024/12/31");
String result = new SimpleDateFormat("dd-MM-YYYY").format(date); //31-12-2025
'Y' литерал в паттерне даты обозначает не текущий год, а год относительно текущей недели.
По стандарту ISO-8601:
Рассмотрим фрагмент календаря на конец 2024 и начало 2025 года:
ПН |
ВТ |
СР |
ЧТ |
ПТ |
СБ |
ВС |
---|---|---|---|---|---|---|
30 |
31 |
1 |
2 |
3 |
4 |
5 |
Приведённая неделя будет считаться первой неделей 2025 года, поскольку соответствует условиям из вышеупомянутого стандарта. Поэтому, используя литерал 'Y' вместо ожидаемого 2024 года, мы получаем 2025.
Ошибочным будет и обратный пример:
Date date = new Date("2027/01/01");
String result =
new SimpleDateFormat("dd-MM-YYYY").format(date); // 01-01-2026
Рассмотрим фрагмент календаря на конец 2026 и начало 2027 года:
ПН |
ВТ |
СР |
ЧТ |
ПТ |
СБ |
ВС |
---|---|---|---|---|---|---|
28 |
29 |
30 |
31 |
1 |
2 |
3 |
Обратите внимание: 1, 2 и 3 января будут относиться к последней неделе декабря. Эта неделя условиям вышеупомянутого стандарта не соответствует.
В случае, если вы желаете отобразить год без учёта недели года, необходимо использовать литерал 'y' в паттерне форматирования даты.
Корректный пример:
Date date = new Date("2027/01/01");
String result = new SimpleDateFormat("dd-MM-yyyy").format(date) // 01-01-2027
Анализатор обнаружил, что значение постфиксной операции не используется. Скорее всего, или операция избыточна, или вместо постфиксной операции следует использовать префиксную.
Пример:
int calculateSomething() {
int value = getSomething();
....
return value++;
}
В данном примере имеется локальная переменная 'value'. Ожидается, что метод вернёт её инкрементированное значение. Однако согласно JLS:
The value of the postfix increment expression is the value of the variable before the new value is stored.
Таким образом оператор '++' никак не повлияет на значение, которое вернёт метод 'calculateSomething'. Возможный исправленный вариант:
int calculateSomething() {
int value = getSomething();
....
return ++value;
}
Следующий вариант исправления кода ещё лучше подчёркивает, что следует вернуть значение на единицу больше:
int calculateSomething() {
int value = getSomething();
....
return value + 1;
}
Мы рекомендуем использовать второй вариант, так как его проще понять.
Рассмотрим ещё один синтетический пример:
void foo() {
int value = getSomething();
bar(value++);
bar(value++);
bar(value++);
}
Каждый раз метод 'bar' вызывается с аргументом на единицу больше. Последний инкремент не имеет смысла, так как увеличенное значение переменной далее не используется. Однако ошибки здесь нет, так как последний инкремент написан из эстетических соображений. Предупреждение не выдаётся, если переменная последовательно инкрементируется более двух раз подряд.
Однако мы всё равно рекомендуем писать следующим образом:
void foo() {
int value = getSomething();
bar(value++);
bar(value++);
bar(value);
}
Или так:
void foo() {
int value = getSomething();
bar(value + 0);
bar(value + 1);
bar(value + 2);
}
Данная диагностика классифицируется как:
Анализатор обнаружил, что переменной целочисленного типа присвоили значение, выходящее за диапазон допустимых значений.
Пример:
public static void test() {
byte a = (byte) 256; // a = 0
short b = (short) 32768; // b = -32768
int c = (int) 2147483648L; // c = -2147483648
}
В данном примере произойдёт переполнение, и в переменных будут храниться не те значения, которые программист попытался присвоить.
Происходит это потому, что под определённый целочисленный тип выделяется фиксированное количество байт. Если значение выходит за рамки того количества байт, которое под него выделили, то лишние биты у значения отсекаются. Опасность заключается в том, что Java даст скомпилировать и запустить такую программу, но при этом из-за допущенной ошибки программист получит не те значения, которые хотел изначально.
Возможно, стоит рассмотреть использование типа, включающего в себя больший диапазон значений:
public static void a() {
short s = (short) 256;
int i = 32768;
long l = 2_147_483_648L;
}
Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки целочисленного переполнения и некорректного совместного использования знаковых и беззнаковых чисел. |
Данная диагностика классифицируется как:
Анализатор обнаружил, что методы 'wait', 'notify', 'notifyAll' могут быть вызваны в несинхронизированном контексте.
public void someMethod() {
notify();
}
public void anotherMethod() throws InterruptedException {
wait();
}
Методы 'wait', 'notify', 'notifyAll' работают с монитором объекта, по которому происходит синхронизация. То есть их вызов корректен только в синхронизированном контексте и только на объекте, по которому происходит синхронизация.
В случае, если методы 'wait', 'notify' или 'notifyAll' вызвать в несинхронизированном контексте или не на том объекте, по которому синхронизация происходит, произойдёт выброс исключения 'IllegalMonitorStateException'.
Пример корректного использования в 'synchronized' блоке:
private final Object lock = new Object();
public void someCorrectMethod() {
synchronized (lock) {
lock.notify();
}
}
Поскольку синхронизация идёт по объекту 'lock', вызов метода 'notify' корректен только на объекте 'lock'.
Корректное использование в 'synchronized' методе:
public synchronized void anotherCorrectMethod() {
notifyAll();
}
Фрагмент выше эквивалентен следующему:
public void anotherCorrectMethod() {
synchronized (this) {
notifyAll();
}
}
Из этого следует, что в данной ситуации метод 'notifyAll' корректно вызывать только на 'this' объекте.
Windows, Visual Studio, Visual C++ are either registered trademarks or trademarks of Microsoft Corporation in the United States and/or other countries.
Other product and company names mentioned herein may be the trademarks of their respective owners.
Portions of PVS-Studio are based in part of OpenC++. Bellow you can read OpenC++ Copyright Notice.
*** Copyright Notice
Copyright (c) 1995, 1996 Xerox Corporation.
All Rights Reserved.
Use and copying of this software and preparation of derivative works based upon this software are permitted. Any copy of this software or of any derivative work must include the above copyright notice of Xerox Corporation, this paragraph and the one after it. Any distribution of this software or derivative works must comply with all applicable United States export control laws.
This software is made available AS IS, and XEROX CORPORATION DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, AND NOTWITHSTANDING ANY OTHER PROVISION CONTAINED HEREIN, ANY LIABILITY FOR DAMAGES RESULTING FROM THE SOFTWARE OR ITS USE IS EXPRESSLY DISCLAIMED, WHETHER ARISING IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR STRICT LIABILITY, EVEN IF XEROX CORPORATION IS ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*** Copyright Notice
Copyright (C) 1997-2001 Shigeru Chiba, Tokyo Institute of Technology.
Permission to use, copy, distribute and modify this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation.
Shigeru Chiba makes no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty.
*** Copyright Notice
Permission to use, copy, distribute and modify this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. Other Contributors make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty.
2001-2003 (C) Copyright by Other Contributors.
PVS-Studio can use Clang as preprocessor. Read Clang/LLVM license:
==============================================================================
LLVM Release License
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2007-2011 University of Illinois at Urbana-Champaign.
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois at Urbana-Champaign, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE.
==============================================================================
The LLVM software contains code written by third parties. Such software will have its own individual LICENSE.TXT file in the directory in which it appears. This file will describe the copyrights, license, and restrictions which apply to that code.
The disclaimer of warranty in the University of Illinois Open Source License applies to all code in the LLVM Distribution, and nothing in any of the other licenses gives permission to use the names of the LLVM Team or the University of Illinois to endorse or promote products derived from this Software.
The following pieces of software have additional or alternate copyrights, licenses, and/or restrictions:
Program Directory
------- ---------
<none yet>
PVS-Studio uses GNU C Library. GNU C Library is licensed under GNU LESSER GENERAL PUBLIC LICENSE Version 2.1. PVS-Studio provides object code in accordance with section 6.a of GNU LESSER GENERAL PUBLIC LICENSE. Bellow you can read GNU C Library License.
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things.
To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others.
Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs.
When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library.
We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances.
For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system.
Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library.
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library.
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful.
(For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.)
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library.
In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices.
Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange.
If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things:
a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place.
e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute.
7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above.
b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License.
11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
MIT License
Copyright (c) 2013-2017 Niels Lohmann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
MIT License
Copyright (c) 2016-2017 Taylor C. Richberger <taywee@gmx.com> and Pavel Belikov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
PVS-Studio uses Compact Encoding Detection C++ Library. Bellow you can read CED License.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
ScintillaNET uses Scintilla. This is Scintilla license:
License for Scintilla and SciTE
Copyright 1998-2003 by Neil Hodgson <neilh@scintilla.org>
All Rights Reserved
Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation.
NEIL HODGSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL NEIL HODGSON BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
PVS-Studio uses SourceGrid control (sourcegrid.codeplex.com). Bellow you can read Source Grid License.
SourceGrid LICENSE (MIT style)
Copyright (c) 2009 Davide Icardi
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
C and C++ Compiler Monitoring UI uses ScintillaNET. This is ScintillaNET license:
The MIT License (MIT)
Copyright (c) 2017, Jacob Slusser, https://github.com/jacobslusser
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
C and C++ Compiler Monitoring UI uses ScintillaNet-FindReplaceDialog. This is ScintillaNet-FindReplaceDialog license:
MIT License
Copyright (c) 2017 Steve Towner
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
C and C++ Compiler Monitoring UI uses DockPanel_Suite. This is DockPanel_Suite license:
The MIT License
Copyright (c) 2007 Weifen Luo (email: weifenluo@yahoo.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
PVS-Studio uses Font Awesome. Bellow you can read Font Awesome License.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
PVS-Studio uses Roslyn. This is Roslyn license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
PVS-Studio uses Command Line Parser Library. This is Command Line Parser Library license:
The MIT License (MIT)
Copyright (c) 2005 - 2015 Giacomo Stelluti Scala & Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The core Protocol Buffers technology is provided courtesy of Google. At the time of writing, this is released under the BSD license. Full details can be found here:
http://code.google.com/p/protobuf/
This .NET implementation is Copyright 2008 Marc Gravell
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
PVS-Studio uses MSBuild. This is MSBuild license:
MSBuild
The MIT License (MIT)
Copyright (c) .NET Foundation and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
PVS-Studio plugin for Jenkins uses jsoup. This is jsoup license:
The MIT License
Copyright © 2009 - 2017 Jonathan Hedley (jonathan@hedley.net)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
PVS-Studio uses Spoon. This is Spoon license:
CeCILL-C FREE SOFTWARE LICENSE AGREEMENT
Notice
This Agreement is a Free Software license agreement that is the result of discussions between its authors in order to ensure compliance with the two main principles guiding its drafting:
The authors of the CeCILL-C (for Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre]) license are:
Commissariat à l'Energie Atomique - CEA, a public scientific, technical and industrial research establishment, having its principal place of
business at 25 rue Leblanc, immeuble Le Ponant D, 75015 Paris, France. Centre National de la Recherche Scientifique - CNRS, a public scientific and technological establishment, having its principal place of business at 3 rue Michel-Ange, 75794 Paris cedex 16, France.
Institut National de Recherche en Informatique et en Automatique INRIA, a public scientific and technological establishment, having its principal place of business at Domaine de Voluceau, Rocquencourt, BP 105, 78153 Le Chesnay cedex, France.
Preamble
The purpose of this Free Software license agreement is to grant users the right to modify and re-use the software governed by this license.
The exercising of this right is conditional upon the obligation to make available to the community the modifications made to the source code of the software so as to contribute to its evolution.
In consideration of access to the source code and the rights to copy, modify and redistribute granted by the license, users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the successive licensors only have limited liability.
In this respect, the risks associated with loading, using, modifying and/or developing or reproducing the software by the user are brought to the user's attention, given its Free Software status, which may make it complicated to use, with the result that its use is reserved for developers and experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the suitability of the software as regards their requirements in conditions enabling the security of their systems and/or data to be ensured and, more generally, to use and operate it in the same conditions of security. This Agreement may be freely reproduced and published, provided it is not altered, and that no provisions are either added or removed herefrom.
This Agreement may apply to any or all software for which the holder of the economic rights decides to submit the use thereof to its provisions.
Article 1 - DEFINITIONS
For the purpose of this Agreement, when the following expressions commence with a capital letter, they shall have the following meaning:
Agreement: means this license agreement, and its possible subsequent versions and annexes.
Software: means the software in its Object Code and/or Source Code form and, where applicable, its documentation, "as is" when the Licensee accepts the Agreement.
Initial Software: means the Software in its Source Code and possibly its Object Code form and, where applicable, its documentation, "as is" when it is first distributed under the terms and conditions of the Agreement.
Modified Software: means the Software modified by at least one Integrated Contribution.
Source Code: means all the Software's instructions and program lines to which access is required so as to modify the Software.
Object Code: means the binary files originating from the compilation of the Source Code.
Holder: means the holder(s) of the economic rights over the Initial Software.
Licensee: means the Software user(s) having accepted the Agreement.
Contributor: means a Licensee having made at least one Integrated Contribution.
Licensor: means the Holder, or any other individual or legal entity, who distributes the Software under the Agreement.
Integrated Contribution: means any or all modifications, corrections, translations, adaptations and/or new functions integrated into the Source Code by any or all Contributors.
Related Module: means a set of sources files including their documentation that, without modification to the Source Code, enables supplementary functions or services in addition to those offered by the
Software.
Derivative Software: means any combination of the Software, modified or not, and of a Related Module.
Parties: mean both the Licensee and the Licensor.
These expressions may be used both in singular and plural form.
Article 2 - PURPOSE
The purpose of the Agreement is the grant by the Licensor to the Licensee of a non-exclusive, transferable and worldwide license for the Software as set forth in Article 5 hereinafter for the whole term of the protection granted by the rights over said Software.
Article 3 - ACCEPTANCE
3.1 The Licensee shall be deemed as having accepted the terms and conditions of this Agreement upon the occurrence of the first of the following events:
3.2 One copy of the Agreement, containing a notice relating to the characteristics of the Software, to the limited warranty, and to the fact that its use is restricted to experienced users has been provided to the Licensee prior to its acceptance as set forth in Article 3.1 hereinabove, and the Licensee hereby acknowledges that it has read and understood it.
Article 4 - EFFECTIVE DATE AND TERM
4.1 EFFECTIVE DATE
The Agreement shall become effective on the date when it is accepted by the Licensee as set forth in Article 3.1.
4.2 TERM
The Agreement shall remain in force for the entire legal term of protection of the economic rights over the Software.
Article 5 - SCOPE OF RIGHTS GRANTED
The Licensor hereby grants to the Licensee, who accepts, the following rights over the Software for any or all use, and for the term of the Agreement, on the basis of the terms and conditions set forth hereinafter.
Besides, if the Licensor owns or comes to own one or more patents protecting all or part of the functions of the Software or of its components, the Licensor undertakes not to enforce the rights granted by these patents against successive Licensees using, exploiting or modifying the Software. If these patents are transferred, the Licensor undertakes to have the transferees subscribe to the obligations set forth in this paragraph.
5.1 RIGHT OF USE
The Licensee is authorized to use the Software, without any limitation as to its fields of application, with it being hereinafter specified that this comprises:
1. permanent or temporary reproduction of all or part of the Software by any or all means and in any or all form.
2. loading, displaying, running, or storing the Software on any or all medium.
3. entitlement to observe, study or test its operation so as to determine the ideas and principles behind any or all constituent elements of said Software. This shall apply when the Licensee carries out any or all loading, displaying, running, transmission or storage operation as regards the Software, that it is entitled to carry out hereunder.
5.2 RIGHT OF MODIFICATION
The right of modification includes the right to translate, adapt, arrange, or make any or all modifications to the Software, and the right to reproduce the resulting software. It includes, in particular, the right to create a Derivative Software.
The Licensee is authorized to make any or all modification to the Software provided that it includes an explicit notice that it is the author of said modification and indicates the date of the creation thereof.
5.3 RIGHT OF DISTRIBUTION
In particular, the right of distribution includes the right to publish, transmit and communicate the Software to the general public on any or all medium, and by any or all means, and the right to market, either in consideration of a fee, or free of charge, one or more copies of the Software by any means.
The Licensee is further authorized to distribute copies of the modified or unmodified Software to third parties according to the terms and conditions set forth hereinafter.
5.3.1 DISTRIBUTION OF SOFTWARE WITHOUT MODIFICATION
The Licensee is authorized to distribute true copies of the Software in Source Code or Object Code form, provided that said distribution complies with all the provisions of the Agreement and is accompanied by:
1. a copy of the Agreement,
2. a notice relating to the limitation of both the Licensor's warranty and liability as set forth in Articles 8 and 9,
and that, in the event that only the Object Code of the Software is redistributed, the Licensee allows effective access to the full Source Code of the Software at a minimum during the entire period of its distribution of the Software, it being understood that the additional cost of acquiring the Source Code shall not exceed the cost of transferring the data.
5.3.2 DISTRIBUTION OF MODIFIED SOFTWARE
When the Licensee makes an Integrated Contribution to the Software, the terms and conditions for the distribution of the resulting Modified Software become subject to all the provisions of this Agreement.
The Licensee is authorized to distribute the Modified Software, in source code or object code form, provided that said distribution complies with all the provisions of the Agreement and is accompanied by:
1. a copy of the Agreement,
2. a notice relating to the limitation of both the Licensor's warranty and liability as set forth in Articles 8 and 9,
and that, in the event that only the object code of the Modified Software is redistributed, the Licensee allows effective access to the full source code of the Modified Software at a minimum during the entire period of its distribution of the Modified Software, it being understood that the additional cost of acquiring the source code shall not exceed the cost of transferring the data.
5.3.3 DISTRIBUTION OF DERIVATIVE SOFTWARE
When the Licensee creates Derivative Software, this Derivative Software may be distributed under a license agreement other than this Agreement, subject to compliance with the requirement to include a notice concerning the rights over the Software as defined in Article 6.4.
In the event the creation of the Derivative Software required modification of the Source Code, the Licensee undertakes that:
1. the resulting Modified Software will be governed by this Agreement,
2. the Integrated Contributions in the resulting Modified Software will be clearly identified and documented,
3. the Licensee will allow effective access to the source code of the Modified Software, at a minimum during the entire period of distribution of the Derivative Software, such that such modifications may be carried over in a subsequent version of the Software; it being understood that the additional cost of purchasing the source code of the Modified Software shall not exceed the cost of transferring the data.
5.3.4 COMPATIBILITY WITH THE CeCILL LICENSE
When a Modified Software contains an Integrated Contribution subject to the CeCILL license agreement, or when a Derivative Software contains a Related Module subject to the CeCILL license agreement, the provisions set forth in the third item of Article 6.4 are optional.
Article 6 - INTELLECTUAL PROPERTY
6.1 OVER THE INITIAL SOFTWARE
The Holder owns the economic rights over the Initial Software. Any or all use of the Initial Software is subject to compliance with the terms and conditions under which the Holder has elected to distribute its work and no one shall be entitled to modify the terms and conditions for the distribution of said Initial Software.
The Holder undertakes that the Initial Software will remain ruled at least by this Agreement, for the duration set forth in Article 4.2.
6.2 OVER THE INTEGRATED CONTRIBUTIONS
The Licensee who develops an Integrated Contribution is the owner of the intellectual property rights over this Contribution as defined by applicable law.
6.3 OVER THE RELATED MODULES
The Licensee who develops a Related Module is the owner of the intellectual property rights over this Related Module as defined by applicable law and is free to choose the type of agreement that shall govern its distribution under the conditions defined in Article 5.3.3.
6.4 NOTICE OF RIGHTS
The Licensee expressly undertakes:
1. not to remove, or modify, in any manner, the intellectual property notices attached to the Software;
2. to reproduce said notices, in an identical manner, in the copies of the Software modified or not;
3. to ensure that use of the Software, its intellectual property notices and the fact that it is governed by the Agreement is indicated in a text that is easily accessible, specifically from the interface of any Derivative Software.
The Licensee undertakes not to directly or indirectly infringe the intellectual property rights of the Holder and/or Contributors on the Software and to take, where applicable, vis-à-vis its staff, any and all measures required to ensure respect of said intellectual property rights of the Holder and/or Contributors.
Article 7 - RELATED SERVICES
7.1 Under no circumstances shall the Agreement oblige the Licensor to provide technical assistance or maintenance services for the Software.
However, the Licensor is entitled to offer this type of services. The terms and conditions of such technical assistance, and/or such maintenance, shall be set forth in a separate instrument. Only theLicensor offering said maintenance and/or technical assistance services shall incur liability therefor.
7.2 Similarly, any Licensor is entitled to offer to its licensees, underits sole responsibility, a warranty, that shall only be binding upon itself, for the redistribution of the Software and/or the Modified Software, under terms and conditions that it is free to decide. Said warranty, and the financial terms and conditions of its application, shall be subject of a separate instrument executed between the Licensor and the Licensee.
Article 8 - LIABILITY
8.1 Subject to the provisions of Article 8.2, the Licensee shall be entitled to claim compensation for any direct loss it may have suffered from the Software as a result of a fault on the part of the relevant Licensor, subject to providing evidence thereof.
8.2 The Licensor's liability is limited to the commitments made under this Agreement and shall not be incurred as a result of in particular: (i) loss due the Licensee's total or partial failure to fulfill its obligations, (ii) direct or consequential loss that is suffered by the Licensee due to the use or performance of the Software, and (iii) more generally, any consequential loss. In particular the Parties expressly agree that any or all pecuniary or business loss (i.e. loss of data, loss of profits, operating loss, loss of customers or orders, opportunity cost, any disturbance to business activities) or any or all legal proceedings instituted against the Licensee by a third party, shall constitute consequential loss and shall not provide entitlement to any or all compensation from the Licensor.
Article 9 - WARRANTY
9.1 The Licensee acknowledges that the scientific and technical state-of-the-art when the Software was distributed did not enable all possible uses to be tested and verified, nor for the presence of possible defects to be detected. In this respect, the Licensee's attention has been drawn to the risks associated with loading, using, modifying and/or developing and reproducing the Software which are reserved for experienced users.
The Licensee shall be responsible for verifying, by any or all means, the suitability of the product for its requirements, its good working order, and for ensuring that it shall not cause damage to either persons or properties.
9.2 The Licensor hereby represents, in good faith, that it is entitled to grant all the rights over the Software (including in particular the rights set forth in Article 5).
9.3 The Licensee acknowledges that the Software is supplied "as is" by the Licensor without any other express or tacit warranty, other than that provided for in Article 9.2 and, in particular, without any warranty as to its commercial value, its secured, safe, innovative or relevant nature.
Specifically, the Licensor does not warrant that the Software is free from any error, that it will operate without interruption, that it will be compatible with the Licensee's own equipment and software configuration, nor that it will meet the Licensee's requirements.
9.4 The Licensor does not either expressly or tacitly warrant that the Software does not infringe any third party intellectual property right relating to a patent, software or any other property right. Therefore, the Licensor disclaims any and all liability towards the Licensee arising out of any or all proceedings for infringement that may be instituted in respect of the use, modification and redistribution of the Software. Nevertheless, should such proceedings be instituted against the Licensee, the Licensor shall provide it with technical and legal assistance for its defense. Such technical and legal assistance shall be decided on a case-by-case basis between the relevant Licensor and the Licensee pursuant to a memorandum of understanding. The Licensor disclaims any and all liability as regards the Licensee's use of the name of the Software. No warranty is given as regards the existence of prior rights over the name of the Software or as regards the existence of a trademark.
Article 10 - TERMINATION
10.1 In the event of a breach by the Licensee of its obligations hereunder, the Licensor may automatically terminate this Agreement thirty (30) days after notice has been sent to the Licensee and has remained ineffective.
10.2 A Licensee whose Agreement is terminated shall no longer be authorized to use, modify or distribute the Software. However, any licenses that it may have granted prior to termination of the Agreement shall remain valid subject to their having been granted in compliance with the terms and conditions hereof.
Article 11 - MISCELLANEOUS
11.1 EXCUSABLE EVENTS
Neither Party shall be liable for any or all delay, or failure to perform the Agreement, that may be attributable to an event of force majeure, an act of God or an outside cause, such as defective functioning or interruptions of the electricity or telecommunications networks, network paralysis following a virus attack, intervention by government authorities, natural disasters, water damage, earthquakes, fire, explosions, strikes and labor unrest, war, etc.
11.2 Any failure by either Party, on one or more occasions, to invoke one or more of the provisions hereof, shall under no circumstances be interpreted as being a waiver by the interested Party of its right to invoke said provision(s) subsequently.
11.3 The Agreement cancels and replaces any or all previous agreements, whether written or oral, between the Parties and having the same purpose, and constitutes the entirety of the agreement between said Parties concerning said purpose. No supplement or modification to the terms and conditions hereof shall be effective as between the Parties unless it is made in writing and signed by their duly authorized representatives.
11.4 In the event that one or more of the provisions hereof were to conflict with a current or future applicable act or legislative text, said act or legislative text shall prevail, and the Parties shall make the necessary amendments so as to comply with said act or legislative text. All other provisions shall remain effective. Similarly, invalidity of a provision of the Agreement, for any reason whatsoever, shall not cause the Agreement as a whole to be invalid.
11.5 LANGUAGE
The Agreement is drafted in both French and English and both versions are deemed authentic.
Article 12 - NEW VERSIONS OF THE AGREEMENT
12.1 Any person is authorized to duplicate and distribute copies of this Agreement.
12.2 So as to ensure coherence, the wording of this Agreement is protected and may only be modified by the authors of the License, who reserve the right to periodically publish updates or new versions of the Agreement, each with a separate number. These subsequent versions may address new issues encountered by Free Software.
12.3 Any Software distributed under a given version of the Agreement may only be subsequently distributed under the same version of the Agreement or a subsequent version.
Article 13 - GOVERNING LAW AND JURISDICTION
13.1 The Agreement is governed by French law. The Parties agree to endeavor to seek an amicable solution to any disagreements or disputes that may arise during the performance of the Agreement.
13.2 Failing an amicable solution within two (2) months as from their occurrence, and unless emergency proceedings are necessary, the disagreements or disputes shall be referred to the Paris Courts having jurisdiction, by the more diligent Party.
Version 1.0 dated 2006-09-05.
PVS-Studio uses Gson. This is Gson license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses picocli. This is picocli license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses fastutil. This is fastutil license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses StreamEx. This is StreamEx license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses Maven Model. This is Maven Model license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses Maven Plugin Tools. This is Maven Plugin Tools license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses Commons IO Library. This is Commons IO Library license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses Apache Commons Lang. This is Apache Commons Lang license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses JetBrains Java Annotations. This is JetBrains Java Annotations license:
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
PVS-Studio uses JUnit Framework. This is JUnit Framework license:
Eclipse Public License - v 1.0
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program.
"Program" means the Contributions distributed in accordance with this Agreement.
"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within the Program.
Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense.
For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such
Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved.
This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.
PVS-Studio uses juniversalchardet. This is juniversalchardet license:
MOZILLA PUBLIC LICENSE
Version 1.1
1. Definitions.
1.0.1. "Commercial Use" means distribution or otherwise making the Covered Code available to a third party.
1.1. "Contributor" means each entity that creates or contributes to the creation of Modifications.
1.2. "Contributor Version" means the combination of the Original Code, prior Modifications used by a Contributor, and the Modifications made by that particular Contributor.
1.3. "Covered Code" means the Original Code or Modifications or the combination of the Original Code and Modifications, in each case including portions thereof.
1.4. "Electronic Distribution Mechanism" means a mechanism generally accepted in the software development community for the electronic transfer of data.
1.5. "Executable" means Covered Code in any form other than Source Code.
1.6. "Initial Developer" means the individual or entity identified as the Initial Developer in the Source Code notice required by Exhibit A.
1.7. "Larger Work" means a work which combines Covered Code or portions thereof with code not governed by the terms of this License.
1.8. "License" means this document.
1.8.1. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.
1.9. "Modifications" means any addition to or deletion from the substance or structure of either the Original Code or any previous Modifications. When Covered Code is released as a series of files, a Modification is: A. Any addition to or deletion from the contents of a file containing Original Code or previous Modifications.
B. Any new file that contains any part of the Original Code or previous Modifications.
1.10. "Original Code" means Source Code of computer software code which is described in the Source Code notice required by Exhibit A as Original Code, and which, at the time of its release under this License is not already Covered Code governed by this License.
1.10.1. "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.
1.11. "Source Code" means the preferred form of the Covered Code for making modifications to it, including all modules it contains, plus any associated interface definition files, scripts used to control compilation and installation of an Executable, or source code differential comparisons against either the Original Code or another well known, available Covered Code of the Contributor's choice. The Source Code can be in a compressed or archival form, provided the appropriate decompression or de-archiving software is widely available for no charge.
1.12. "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License or a future version of this License issued under Section 6.1. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.
2. Source Code License.
2.1. The Initial Developer Grant. The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:
(a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, and/or as part of a Larger Work; and
(b) under Patents Claims infringed by the making, using or selling of Original Code, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Code (or portions thereof).
(c) the licenses granted in this Section 2.1(a) and (b) are effective on the date Initial Developer first distributes Original Code under the terms of this License.
(d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices.
2.2. Contributor Grant. Subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license
(a) under intellectual property rights (other than patent or trademark) Licensable by Contributor, to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof) either on an unmodified basis, with other Modifications, as Covered Code and/or as part of a Larger Work; and
(b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: 1) Modifications made by that Contributor (or portions thereof); and 2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).
(c) the licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first makes Commercial Use of the Covered Code.
(d) Notwithstanding Section 2.2(b) above, no patent license is granted: 1) for any code that Contributor has deleted from the Contributor Version; 2) separate from the Contributor Version; 3) for infringements caused by: i) third party modifications of Contributor Version or ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or 4) under Patent Claims infringed by Covered Code in the absence of Modifications made by that Contributor.
3. Distribution Obligations.
3.1. Application of License. The Modifications which You create or to which You contribute are governed by the terms of this License, including without limitation Section 2.2. The Source Code version of Covered Code may be distributed only under the terms of this License or a future version of this License released under Section 6.1, and You must include a copy of this License with every copy of the Source Code You distribute. You may not offer or impose any terms on any Source Code version that alters or restricts the applicable version of this License or the recipients' rights hereunder. However, You may include an additional document offering the additional rights described in Section 3.5.
3.2. Availability of Source Code. Any Modification which You create or to which You contribute must be made available in Source Code form under the terms of this License either on the same media as an Executable version or via an accepted Electronic Distribution Mechanism to anyone to whom you made an Executable version available; and if made available via Electronic Distribution Mechanism, must remain available for at least twelve (12) months after the date it initially became available, or at least six
(6) months after a subsequent version of that particular Modification has been made available to such recipients. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party.
3.3. Description of Modifications.
You must cause all Covered Code to which You contribute to contain a file documenting the changes You made to create that Covered Code and the date of any change. You must include a prominent statement that the Modification is derived, directly or indirectly, from Original Code provided by the Initial Developer and including the name of the Initial Developer in (a) the Source Code, and (b) in any notice in an Executable version or related documentation in which You describe the origin or ownership of the Covered Code.
3.4. Intellectual Property Matters
(a) Third Party Claims.
If Contributor has knowledge that a license under a third party's intellectual property rights is required to exercise the rights granted by such Contributor under Sections 2.1 or 2.2, Contributor must include a text file with the Source Code distribution titled "LEGAL" which describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If Contributor obtains such knowledge after the Modification is made available as described in Section 3.2, Contributor shall promptly modify the LEGAL file in all copies Contributor makes available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Covered Code that new knowledge has been obtained.
(b) Contributor APIs.
If Contributor's Modifications include an application programming interface and Contributor has knowledge of patent licenses which are reasonably necessary to implement that API, Contributor must also include this information in the LEGAL file.
(c) Representations.
Contributor represents that, except as disclosed pursuant to Section 3.4(a) above, Contributor believes that Contributor's Modifications are Contributor's original creation(s) and/or Contributor has sufficient rights to grant the rights conveyed by this License.
3.5. Required Notices.
You must duplicate the notice in Exhibit A in each file of the Source Code. If it is not possible to put such notice in a particular Source Code file due to its structure, then You must include such notice in a location (such as a relevant directory) where a user would be likely to look for such a notice. If You created one or more Modification(s) You may add your name as a Contributor to the notice described in Exhibit A. You must also duplicate this License in any documentation for the Source Code where You describe recipients' rights or ownership rights relating to Covered Code. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Code. However, You may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear than any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.
3.6. Distribution of Executable Versions. You may distribute Covered Code in Executable form only if the requirements of Section 3.1-3.5 have been met for that Covered Code, and if You include a notice stating that the Source Code version of the Covered Code is available under the terms of this License, including a description of how and where You have fulfilled the obligations of Section 3.2. The notice must be conspicuously included in any notice in an Executable version, related documentation or collateral in which You describe recipients' rights relating to the Covered Code. You may distribute the Executable version of Covered Code or ownership rights under a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable version does not attempt to limit or alter the recipient's rights in the Source Code version from the rights set forth in this License. If You distribute the Executable version under a different license You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or any Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.
3.7. Larger Works.
You may create a Larger Work by combining Covered Code with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Code.
4. Inability to Comply Due to Statute or Regulation.
If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Code due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 3.4 and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it.
5. Application of this License.
This License applies to code to which the Initial Developer has attached the notice in Exhibit A and to related Covered Code.
6. Versions of the License.
6.1. New Versions.
Netscape Communications Corporation ("Netscape") may publish revised and/or new versions of the License from time to time. Each version will be given a distinguishing version number.
6.2. Effect of New Versions.
Once Covered Code has been published under a particular version of the License, You may always continue to use it under the terms of that version. You may also choose to use such Covered Code under the terms of any subsequent version of the License published by Netscape. No one other than Netscape has the right to modify the terms applicable to Covered Code created under this License.
6.3. Derivative Works.
If You create or use a modified version of this License (which you may only do in order to apply it to code which is not already Covered Code governed by this License), You must (a) rename Your license so that the phrases "Mozilla", "MOZILLAPL", "MOZPL", "Netscape", "MPL", "NPL" or any confusingly similar phrase do not appear in your license (except to note that your license differs from this License) and (b) otherwise make it clear that Your version of the license contains terms which differ from the Mozilla Public License and Netscape Public License. (Filling in the name of the Initial Developer, Original Code or Contributor in the notice described in Exhibit A shall not of themselves be deemed to be modifications of this License.)
7. DISCLAIMER OF WARRANTY.
COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
8. TERMINATION.
8.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. All sublicenses to the Covered Code which are properly granted shall survive any termination of this License. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.
8.2. If You initiate litigation by asserting a patent infringement claim (excluding declatory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You file such action is referred to as "Participant") alleging that:
(a) such Participant's Contributor Version directly or indirectly infringes any patent, then any and all rights granted by such Participant to You under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively, unless if within 60 days after receipt of notice You either: (i) agree in writing to pay Participant a mutually agreeable reasonable royalty for Your past and future use of Modifications made by such Participant, or (ii) withdraw Your litigation claim with respect to the Contributor Version against such Participant. If within 60 days of notice, a reasonable royalty and payment arrangement are not mutually agreed upon in writing by the parties or the litigation claim is not withdrawn, the rights granted by Participant to You under Sections 2.1 and/or 2.2 automatically terminate at the expiration of the 60 day notice period specified above.
(b) any software, hardware, or device, other than such Participant's Contributor Version, directly or indirectly infringes any patent, then any rights granted to You by such Participant under Sections 2.1(b) and 2.2(b) are revoked effective as of the date You first made, used, sold, distributed, or had made, Modifications made by that Participant.
8.3. If You assert a patent infringement claim against Participant alleging that such Participant's Contributor Version directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license.
8.4. In the event of termination under Sections 8.1 or 8.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or any distributor hereunder prior to termination shall survive termination.
9. LIMITATION OF LIABILITY.
UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
10. U.S. GOVERNMENT END USERS.
The Covered Code is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" and "commercial computer software documentation," as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Code with only those rights set forth herein.
11. MISCELLANEOUS.
This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by California law provisions (except to the extent applicable law, if any, provides otherwise), excluding its conflict-of-law provisions. With respect to disputes in which at least one party is a citizen of, or an entity chartered or registered to do business in the United States of America, any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California, with venue lying in Santa Clara County, California, with the losing party responsible for costs, including without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License.
12. RESPONSIBILITY FOR CLAIMS.
As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
13. MULTIPLE-LICENSED CODE.
Initial Developer may designate portions of the Covered Code as "Multiple-Licensed". "Multiple-Licensed" means that the Initial Developer permits you to utilize portions of the Covered Code under Your choice of the NPL or the alternative licenses, if any, specified by the Initial Developer in the file described in Exhibit A.
PVS-Studio supports CWE. This is CWE license:
LICENSE
The MITRE Corporation (MITRE) hereby grants you a non-exclusive, royalty-free license to use Common Weakness Enumeration (CWE™) for research, development, and commercial purposes. Any copy you make for such purposes is authorized provided that you reproduce MITRE's copyright designation and this license in any such copy.
DISCLAIMERS
ALL DOCUMENTS AND THE INFORMATION CONTAINED THEREIN ARE PROVIDED ON AN "AS IS" BASIS AND THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS OR IS SPONSORED BY (IF ANY), THE MITRE CORPORATION, ITS BOARD OF TRUSTEES, OFFICERS, AGENTS, AND EMPLOYEES, DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION THEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
CWE is free to use by any organization or individual for any research, development, and/or commercial purposes, per these CWE Terms of Use. MITRE has copyrighted the CWE List, Top 25, CWSS, and CWRAF for the benefit of the community in order to ensure each remains a free and open standard, as well as to legally protect the ongoing use of it and any resulting content by government, vendors, and/or users. MITRE has trademarked ™ the CWE and related acronyms and the CWE and related logos to protect their sole and ongoing use by the CWE effort within the information security arena. Please contact cwe@mitre.org if you require further clarification on this issue.
PVS-Studio поддерживает проверку кроссплатформенных проектов на C и С++, независимо от используемой сборочной системы. Для проверки таких проектов существует специальная утилита. Она имеет различные названия в зависимости от платформы: для Linux и macOS – pvs-studio-analyzer, для Windows – CompilerCommandsAnalyzer.exe. Все примеры запуска, описанные в этой документации, будут использовать имя pvs-studio-analyzer.
Для проверки проектов Visual Studio следует воспользоваться следующий документацией:
На Windows вы также можете использовать сервер мониторинга компиляции.
Примечание: pvs-studio-analyzer и CompilerCommandsAnalyzer.exe являются одной и той же кроссплатформенной утилитой и имеют незначительные отличия. Платформо-зависимые особенности будут описаны в данном документе. Все примеры запуска pvs-studio-analyzer являются кроссплатформенными, если в описании к ним не говорится обратное.
Для работы анализатора необходимо активировать лицензию одним из способов, предложенных в документации.
Если у вас нет лицензии, вы можете запросить её через форму обратной связи.
Для запуска анализа проекта утилите pvs-studio-analyzer необходимо иметь представление о параметрах запуска компиляции для каждой единицы трансляции. Эти параметры могут быть получены из JSON Compilation Database (compile_commands.json) либо из файла трассировки сборки.
Важно: Для анализа проект должен успешно собираться.
Многие сборочные системы (CMake, Ninja и др.) позволяют сгенерировать файл compile_commands.json. Для сборочных систем, не предусматривающих получение compile_commands.json напрямую, существуют разные утилиты (Bear, Text Toolkit, intercept-build и др.), позволяющие сгенерировать его.
Процесс генерации JSON Compilation Database и анализа подробно описан здесь.
Если у вас нет возможности сгенерировать compile_commands.json для своего проекта, вы можете воспользоваться режимом трассировки компиляции. Данный режим работает только на Linux и использует утилиту strace для перехвата вызовов компилятора.
Примечание: для мониторинга компиляции на Windows следует воспользоваться сервером мониторинга компиляции CLMonitor.
Важно: для трассировки компиляции в системе должен быть установлен strace версии 4.11 или старше, и включён системный вызов PTRACE.
Примечание: во многих дистрибутивах PTRACE включен по умолчанию. Однако, бывают исключения. Для включения PTRACE измените значение параметра kernel.yama.ptrace_scope в файле /etc/sysctl.d/10-ptrace.conf на 1.
Результат трассировки записывается в файл с именем strace_out (по умолчанию) в текущей директории, который впоследствии использует анализатор для получения параметров компиляции. При помощи флага -o можно задать произвольный путь, в который будет записана трассировка.
Перед запуском трассировки убедитесь, что в сборочном каталоге нет артефактов предыдущей сборки. Иначе сборочная система может опустить вызовы компилятора для неизменённых файлов, если она использует инкрементальный режим сборки.
Для запуска трассировки компиляции воспользуйтесь следующей командой:
pvs-studio-analyzer trace [-o <FILE>] -- build_command
build_command – команда, используемая для сборки проекта.
Пример:
pvs-studio-analyzer trace -- cmake build .
После формирования JSON Compilation Database или файла трассировки компиляции можно перейти к анализу проекта.
В общем случае для запуска анализа необходимо выполнить команду:
pvs-studio-analyzer analyze [-o /path/to/PVS-Studio.log] \
[-e /path/to/exclude-path]... \
[-j <N>]
Далее будет приведено описание всех флагов запуска анализа.
‑‑cfg [FILE] (-c [FILE]) – задаёт файл конфигурации *.cfg, в который можно поместить некоторые параметры запуска анализатора (например, exclude-path, lic-file и др.). В следующем разделе будет дано описание настроек файла конфигурации. Вы можете использовать файл конфигурации, чтобы вынести туда общие параметры проверки различных проектов.
‑‑lic-file [FILE] (-l [FILE]) – путь до файла с лицензией. Для данного параметра есть соответствующая настройка в файле конфигурации.
‑‑threads [N] (-j [N]) – задаёт число потоков, на которое будет распараллелен анализ.
‑‑output-file [FILE] (-o [FILE]) – имя файла, в который будет записан отчёт анализатора. По умолчанию, если данный флаг не указан, отчёт будет записан в файл PVS-Studio.log в текущей директории. Вы можете задать данный параметр в файле конфигурации (*.cfg).
‑‑exclude-path [DIR] (-e [DIR]) – задаёт путь, по которому следует исключить файлы из анализа. Вы можете задать абсолютный или относительный путь. Также можно использовать шаблоны (glob) для исключения набора файлов. Если есть несколько директорий, которые нужно исключить из проверки, добавьте каждую через данный флаг или пропишите их в файле конфигурации.
‑‑analysis-paths [MODE=PATH|GLOB] – определяет поведение анализа на указанных путях. Доступны следующие режимы:
'skip-analysis' - задаёт директорию, файлы из которой проверять не надо. Обычно это каталоги системных файлов или подключаемых библиотек.
'skip-settings' - игнорирует настройки, расположенные в исходных файлах и файлах '.pvsconfig' по указанному пути.
'skip' - игнорирует настройки, расположенные в исходных файлах и файлах '.pvsconfig' по указанному пути. Также будут отфильтрованы предупреждения, сгенерированные для файлов исходного кода по указанному пути или маске.
‑‑analysis-mode [MODE] (-a [MODE]) – задаёт группу предупреждений, которые будут активированы при анализе вашего проекта.
Подробнее про MISRA, AUTOSAR и OWASP можно прочитать здесь.
Если вы хотите задать несколько групп предупреждений, то следует разделить их через символ ';' или '+'. Например: 'GA;OP;64' или GA+OP+64. Вы можете не указывать кавычки, если используете в качестве разделителя '+'. Если вы используете в качестве разделителя символ ';', то следует обернуть выражение в кавычки или экранировать каждый символ "точка с запятой", т. к. в командной оболочке он обычно обозначает разделитель команд.
По умолчанию используется группа GA.
Также вы можете задать данный параметр в файле конфигурации (*.cfg).
‑‑sourcetree-root [DIR] (-r [DIR]) – флаг указывает, что в отчёте следует заменить корневую часть пути (DIR) на специальный символ. Таким образом путь до файла с предупреждением анализатора станет относительным. По умолчанию при генерации диагностических сообщений PVS-Studio выдаёт абсолютные пути до файлов, на которые анализатор выдал срабатывания. С помощью данной настройки можно задать корневую часть пути, которую анализатор будет автоматически подменять на специальный маркер. Замена произойдет, если путь до файла начинается с заданной корневой части ([DIR]). В дальнейшем отчёт с относительными путями можно использовать для просмотра результатов анализа в окружении с отличающимся расположением исходных файлов.
‑‑disableLicenseExpirationCheck – флаг устанавливает нулевой код возврата, если срок действия лицензии скоро истечёт. Данный флаг следует использовать, если вы встраиваете анализатор в системы непрерывной интеграции (Travis CI, CircleCI, GitLab CI/CD) или автоматизируете проверку коммитов и Pull Requests и срок действия вашей лицензии скоро закончится (осталось менее 30 дней).
Обратите внимание: если после обновления лицензии забыть убрать этот флаг, то pvs-studio-analyzer заменит возможный нулевой код возврата кодом 6.
‑‑file [FILE] (-f [FILE]) – задаёт путь до файла трассировки компиляции или JSON Compilation Database. По умолчанию, если этот флаг не указан, PVS-Studio ищет файл strace_out или compile_commands.json в текущем каталоге. Следует учесть, что PVS-Studio первым ищет файл compile_commands.json и только потом strace_out. Поэтому если у вас в рабочем каталоге лежат два этих файла, то предпочтение будет отдано первому. Если вы используете JSON Compilation DB, то обязательно указывайте расширение файла '.json', иначе он будет считаться как файл трассировки.
Данный флаг следует задавать, если файл трассировки компиляции или JSON Compilation Database сохранён по нестандартному пути.
‑‑quiet – не отображать процесс анализа.
‑‑preprocessor [NAME] – задаёт тип препроцессора, который анализатор будет ожидать при разборе препроцессированных файлов (*.PVS-Studio.i) Возможные значения:
Во время работы препроцессора выполняется раскрытие макросов и подстановка содержимого файлов, включенных через #include, в результирующий препроцессированный файл. Для корректной навигации компилятора и различных утилит (в том числе и PVS-Studio) по такому файлу препроцессор вставляет специальные #line-директивы. Они указывают на файл, содержимое которого было вставлено в данное место.
PVS-Studio нужно знать тип препроцессора для корректной обработки директив #line, специфичных для разных компиляторов.
По умолчанию, если этот флаг не указан, анализатор пытается сам определить тип препроцессора. Однако бывают ситуации, когда анализатор может некорректно определить его. В таком случае препроцессор можно указать явно.
Данный параметр можно задать в файле конфигурации (*.cfg).
‑‑platform [NAME] – флаг позволяет задать целевую платформу, под которую производится компиляция проекта.
Данный флаг ожидает следующие параметры:
Информация о платформе нужна анализатору для корректного вывода модели данных.
По умолчанию, если вы не задали этот флаг, PVS-Studio попытается определить платформу на основе параметров запуска компилятора.
Также данный параметр может быть задан в файле конфигурации.
‑‑ignore-ccache – включает анализ всех исходных файлов, независимо от состояния ccache. Если в вашем проекте для ускорения сборки используется обёртка над вызовом компилятора (ccache), то анализ не найдёт файлы компиляции. Этот флаг позволяет опустить вызов ccache и обработать обёрнутую в него команду компилятора.
--incremental (-i) – флаг включает инкрементальный анализ проекта.
‑‑source-files [FILE] (-S [FILE]) – задаёт список исходных файлов для режима проверки списка файлов. Этот список представляет собой текстовый файл, где путь до каждого файла исходного кода расположен на новой строке. Допустимо использовать абсолютные и относительные пути. Относительные пути следует указывать относительно директории, из которой вы хотите запускать анализ.
Такой подход удобно использовать при анализе коммитов и Pull Request'ов.
‑‑regenerate-depend-info [OPTION] – обновляет информацию о зависимостях компиляции для каждого исходного файла. Информация о зависимостях хранится в файле depend_info.json.
Флаг поддерживает следующие режимы:
Файл зависимостей нужен анализатору для корректной проверки списков файлов и для инкрементального анализа. Подробнее об этом можно прочитать здесь.
‑‑suppress-file [FILE] (-s [FILE]) – задаёт путь до файла с подавленными предупреждениями. Предупреждения, попавшие в файл подавления, игнорируются при формировании отчёта анализатора. Подробнее об этом можно узнать тут. По умолчанию файл подавления имеет имя suppress_file.suppress.json.
‑‑analyze-specified-system-paths — включение в анализ файлов из пользовательских системных каталогов, которые указаны через флаги компиляции: isystem, isysroot, system_include_dir и т. д.
--compiler [COMPILER_NAME[=COMPILER_TYPE]] (-C [COMPILER_NAME[=COMPILER_TYPE]]) – позволяет задать имя и тип компилятора. Данный флаг следует использовать, когда PVS-Studio не может распознать вызовы компилятора (при анализе по файлу трассировки) или запускает компилятор с неправильными флагами препроцессирования, так как вычисляет неверный тип компилятора.
COMPILE_NAME используется для фильтрации команд компилятора при разборе файла трассировки (strace_out).
COMPILE_TYPE – задаёт тип компилятора, что позволяет анализатору правильно запустить команду препроцессирования файла. Возможные значения: gcc, clang, keil5, keil5gnu, keil6, tiarmcgt, cl, clangcl, gccarm, iararm_v7_orolder, iararm, qcc, xc8. Если тип компилятора не указан, то анализатор попытаться вывести его по имени или через информацию о версии. А если не сможет, то будет считать его как GCC (на Linux, macOS) или cl (на Windows).
Например, следующая команда указывает анализатору, что в файле strace_out есть неизвестный компилятор и CustomCompiler его следует воспринимать, как GCC:
pvs-studio-analyzer analyzer -f /path/to/strace_out \
-C CustomCompiler=gcc
--env [VAR=VALUE] (-E [VAR=VALUE]) – задаёт переменную окружения, с которой будет производиться препроцессирование.
--rules-config [FILE] (-R [FILE]) – файл конфигурации диагностик (*.pvsconfig). Подробнее о конфигурации диагностик можно узнать здесь.
‑‑intermodular – включает режим межмодульного анализа. В этом режиме анализатор выполняет более глубокий анализ кода, но тратит на это больше времени.
Файл конфигурации позволяет задать общие параметры запуска анализатора.
Для проекта можно создать отдельный файл конфигурации, в который следует поместить специфические параметры.
Параметры записываются как пара "ключ=значение". Вы можете использовать символ '#' для комментирования строк.
Возможные значения в конфигурационном файле:
exclude-path — задаёт путь (абсолютный или относительный) до файлов или директорий, которые должны быть исключены из анализа. Относительный путь следует указывать относительно директории, содержащей файл конфигурации. Также можно использовать шаблоны командных оболочек (glob) '?' и '*' для указания пути.
analysis-paths — определяет поведение анализа на указанных путях. Доступны следующие режимы:
'skip-analysis' - задаёт директорию, файлы из которой проверять не надо. Обычно это каталоги системных файлов или подключаемых библиотек.
'skip-settings' - игнорирует настройки, расположенные в исходных файлах и файлах '.pvsconfig' по указанному пути.
'skip' - игнорирует настройки, расположенные в исходных файлах и файлах '.pvsconfig' по указанному пути. Также будут отфильтрованы предупреждения, сгенерированные для файлов исходного кода по указанному пути или маске.
timeout — задаёт время (в секундах), по истечении которого будет прерван анализ единицы трансляции. По умолчанию на анализ одного файла отводится 10 минут (600 секунд). Если передать в качестве значения 0, то ограничение на время будет снято. Однако учтите, что снятие временного ограничения может привести к зависанию анализа.
platform – задаёт используемую платформу. Возможные варианты: win32, x64, Itanium, linux32, linux64, macOS, pic8, tms.
preprocessor — задаёт используемый препроцессор. Возможные варианты: visualcpp, clang, gcc, bcc, bcc_clang64, iar, keil5, keil5_gnu, c6000.
lic-file – задаёт абсолютный или относительный путь до файла лицензии. Путь может быть задан относительно директории, содержащей файл конфигурации.
analysis-mode – задаёт тип выдаваемых предупреждений. Тип представляет собой битовую маску. С помощью "побитового ИЛИ" можно задать несколько групп диагностик, которые будут использованы при анализе.
Возможные значения:
output-file – полный или относительный путь к файлу, в который следует записать отчёт работы анализатора. По умолчанию отчёт будет записан в файл 'PVS-Studio.log'. Относительный путь следует указывать относительно каталога, из которого будет произведен запуск анализа. При распараллеливании анализа все процессы ядра PVS-Studio пишут отчёт в один файл. Следовательно, этот файл будет заблокирован пока последний процесс не запишет в него информацию.
funsigned-char — задаёт знаковость типа char. Если true — анализатор трактует char как unsigned char, если false — как знаковый char.
rules-config — задаёт путь до файла конфигурации диагностик (*.pvsconfig). Путь может быть задан относительно директории, содержащей файл конфигурации.
no-noise — позволяет исключить из отчёта все срабатывания 3-го уровня достоверности. Если true — срабатывания с низким уровнем достоверности не попадут в отчёт анализатора. По умолчанию — false.
errors-off — задаёт список деактивированных диагностик. Список задаётся через пробел или запятую: 'V1024 V591' или 'V1024, V591'. Диагностики, перечисленные в этом списке, не будут применены во время анализа.
analyzer-errors — задаёт список активных диагностик. Список может быть задан через пробел или через запятую: 'V1024 V591' или 'V1024, V591'. Во время анализа будут использованы только те диагностики, которые перечислены в этом списке.
Обратите внимание: список деактивированных диагностик, заданный через errors-off, имеет больший приоритет, чем список активированных.
Пример: зададим основные параметры запуска PVS-Studio в файле конфигурации и запустим анализ проекта, предав анализатору наш *.cfg файл.
Файл MyProject.cfg:
lic-file=~/.config/PVS-Studio/PVS-Studio.lic
exclude-path=*/tests/*
exclude-path=*/lib/*
exclude-path=*/third-party/*
platform=linux64
preprocessor=clang
analysis-mode=4
output-file=~/MyProject/MyProject.PVS-Studio.log
Запуск анализа (предполагается, что в текущем каталоге есть strace_out или compile_commands.json):
pvs-studio-analyzer analyze --cfg ./MyProject.cfg ....
Использование файла конфигурации позволяет упростить интеграцию анализатора с системами CI/CD.
В PVS-Studio существует механизм подавления предупреждений, который подходит для следующих сценариев:
Утилита pvs-studio-analyzer позволяет подавить сообщения анализатора и провести фильтрацию отчёта, исключив из него подавленные сообщения.
Для подавления сообщений создаётся специальный файл (по умолчанию имеет имя suppress_file.suppress.json), куда записываются предупреждения анализатора, которые следует игнорировать.
Общий синтаксис запуска режима подавления выглядит следующим образом:
pvs-studio-analyzer suppress [-a <TYPES>] [-f <FILE...>] \
[-v <NUMBER...>] [-o <FILE>] [log]
[log] – путь до отчёта, который был создан анализатором. По умолчанию анализатор будет искать файл PVS-Studio.log в текущем каталоге.
‑‑analyzer [TYPES] (-a [TYPES]) – позволяет указать, предупреждения каких групп диагностик и их уровней достоверности будут перемещены в файл подавления. Параметр принимает строку вида 'Diagnostic group: Diagnostic level [, Diagnostic level]*', где Diagnostic group определяет группу диагностик (возможные группы: GA, 64, OP, CS, MISRA, AUTOSAR, OWASP), а Diagnostic level — уровень достоверности (возможные уровни: 1, 2, 3). Объединение разных групп и их уровней возможно через символ ';' или '+'.
Например: запись вида 'GA:1;OP:1' говорит анализатору, что при подавлении следует использовать только диагностики с первым уровнем достоверности из групп общего назначения и микрооптимизаций. По умолчанию фильтрация идёт по всем группам и уровням.
‑‑file [FILE...] (-f [FILE...]) – позволяет подавить все предупреждения для конкретного файла:
pvs-studio-analyzer suppress -f test.cpp -f test2.cpp /path/to/PVS-Studio.log
или для конкретного файла и строки в нем:
pvs-studio-analyzer suppress -f test.cpp:15 /path/to/PVS=Studio.log
--warning [NUMBER...] (-v[NUMBER...]) – задает номер диагностики, срабатывания которой необходимо подавить из отчета:
pvs-studio-analyzer suppress -v512 -v /path/to/PVS-Studio.log
--output [FILE], (-o[FILE]) – задаёт путь и имя для файла подавления. По умолчанию PVS-Studio записывает всю информацию о подавлении срабатываний в файл suppress_file.suppress.json в текущей директории.
Примечание: флаги ‑‑file, ‑‑warning и ‑‑analyzer можно комбинировать. Например, данная команда подавит все предупреждения V1040 на строке 12:
pvs-studio-analyzer suppress -f test.cpp:12 -v1040 /path/to/PVS-Studio.log
Следующая команда подавляет все диагностики общего назначения 3-его уровня для файла:
pvs-studio-analyzer suppress -f test.cpp -a 'GA:3' /path/to/PVS-Studio.log
Вы можете отфильтровать предупреждения, которые ранее были помещены в файл подавления, из отчёта анализатора. Для этого следует выполнить команду:
pvs-studio-analyzer filter-suppressed [-o <FILE>] [-s <FILE>] [log]
--output [FILE] (-o [FILE]) – имя файла для записи отфильтрованного отчёта. По умолчанию, если флаг не задан, pvs-studio-analyzer перезапишет существующий файл отчёта.
--suppress-file [FILE] (-s [FILE]) – файл подавления сообщений. По умолчанию pvs-studio-analyzer ищет файл suppress_file.suppress.json в каталоге запуска.
[log] — файл отчёта, из которого следует отфильтровать предупреждения.
Утилита pvs-studio-analyzer всегда ищет файл подавления в режиме анализа для создания фильтрованного отчёта. Если файл имеет нестандартный путь, вы можете указать его через флаг -s:
pvs-studio-analyzer analyze -s /path/to/suppress_file.suppress.json ....
Утилита может возвращать следующие значения:
0 – анализ прошел успешно;
1 – разнообразные внутренние ошибки. Например, не удалось препроцессировать файл или при разборе файла трассировки произошла ошибка. Как правило, падение c таким кодом сопровождается описанием ошибки в stdout;
2 – срок действия лицензии истекает менее чем через месяц;
3 – во время анализа некоторых файлов произошла внутренняя ошибка;
5 – срок действия вашей лицензии истёк;
6 – утилита была запущена с флагом –disableLicenseExpirationCheck, и ей была передана новая лицензия сроком действия более 30 дней.
7 – ни одна единица компиляции не была принята к анализу. Например, все файлы были исключены из анализа с помощью настроек пользователя или путём маркировки всех каталогов исходного кода как путей к системным заголовкам.
8 – не было обнаружено ни одного вызова компилятора. Например, используется неизвестный компилятор или некорректно сгенерирован файл структуры проекта (strace_out или база данных команд компиляции).
В режиме trace, анализатор по умолчанию возвращает тот же код, что получил от запускаемой программы. Если же вы хотите, чтобы анализатор игнорировал настоящий код возврата и всегда возвращал 0, то можете воспользоваться флагом -i или -- ignoreTraceReturnCode. Например:
pvs-studio-analyzer trace -i -- ....