MSBuild – популярная сборочная платформа с открытым исходным кодом от Microsoft, которую используют разработчики по всему миру. В далёком 2016 году мы уже проверяли проект при помощи PVS-Studio и нашли несколько подозрительных мест. Давайте посмотрим, что удалось обнаружить в коде MSBuild при повторной проверке. С момента предыдущей проверки проект сильно вырос, да и анализатор стал более совершенным. Тем интереснее задача! Несмотря на высокое качество продукта и известное имя разработчика, в этот раз нам снова удалось обнаружить в исходниках MSBuild несколько интересных дефектов. Код проекта практически полностью написан на C# и доступен на GitHub. Состояние кода соответствует коммиту. Для сравнения результатов анализа приведу две диаграммы: Общее число срабатываний при повторной проверке составило 839, в прошлый раз их было всего 262. Больше, чем в четыре раза стало предупреждений уровня Medium, именно они превалируют в статье. На уровне Low их число увеличилось примерно в два с половиной раза. Почти вдвое больше стало срабатываний уровня High. Очевидно, разработчики PVS-Studio не сидели сложа руки эти 6 лет :). За это время в C# анализатор было добавлено 64 GA (General Analysis) и 23 OWASP диагностики. Также модернизировались уже существующие диагностические правила. Но не только C# разработчики проделали существенную работу. С динамикой развития анализатора вы можете ознакомиться здесь. Приступим же к разбору наиболее интересных срабатываний. Предупреждение PVS-Studio: V3133 Postfix increment for variable 'parsePoint' is senseless because this variable is overwritten. Scanner.cs 310 Скорее всего, разработчик ожидал, что метод ScanForPropertyExpressionEnd в качестве второго аргумента примет увеличенное значение parsePoint, но этого не произойдёт. Проблема заключается в использовании постфиксной записи для инкремента. В данном случае сначала будет произведён возврат текущего значения переменной, а уже потом оно увеличится. Следовательно, в метод будет передано изначальное значение parsePoint. Результат выполнения ScanForPropertyExpressionEnd присваивается переменной parsePoint, вследствие чего её увеличенное значение будет перезаписано. Получается, что в данном фрагменте кода операция инкремента вообще ни на что не влияет. Возможным вариантом исправления будет изменение постфиксной записи на префиксную: На данный фрагмент кода анализатор выдал сразу три предупреждения: V3022 Expression 'leftConflictReference.IsPrimary && rightConflictReference.IsPrimary' is always false. ReferenceTable.cs 2388 V3063 A part of conditional expression is always true if it is evaluated: !isNonUnified. ReferenceTable.cs 2389 V3063 A part of conditional expression is always true if it is evaluated: !isNonUnified. ReferenceTable.cs 2390 Второе и третье предупреждения являются следствием проблемы, о которой сигнализирует первое. Рассмотрим условие последнего if. Исходя из него, становится понятно, что в теле условного оператора значения leftConflictReference.IsPrimary и rightConflictReference.IsPrimary всегда будут false. Переменная isNonUnified инициализируется значением, полученным в результате выполнения операции leftConflictReference.IsPrimary && rightConflictReference.IsPrimary. Обе эти переменные имеют значения false. Следовательно, isNonUnified всегда false. Далее isNonUnified используется в качестве части выражения для инициализации ещё двух переменных: Получается, значение этих переменных будет зависеть лишь от правого операнда оператора '&&'. Код можно упростить, изменив тело if на следующее: Скорее всего, код не содержит ошибку, просто разработчик произвёл ряд лишних операций. Но и предупреждение анализатора ложным назвать нельзя. Как раз на этот счёт недавно была написана заметка, рекомендую с ней ознакомиться. Предупреждение PVS-Studio: V3063 A part of conditional expression is always false if it is evaluated: dllArchitecture == SystemProcessorArchitecture.MSIL. ReferenceTable.cs 2968 Переменная dllArchitecture инициализируется значением SystemProcessorArchitecture.None. Другое значение может быть присвоено ей лишь в теле switch. Рассмотрев его, можно понять, что SystemProcessorArchitecture.MSIL не присваивается ни в одном из блоков case. Стоит отметить, что (SystemProcessorArchitecture) 6 не соответствует элементу MSIL. В default-ветке в принципе нет присваивания этой переменной. После switch есть проверка на то, что dllArchitecture равна SystemProcessorArchitecture.MSIL. Выглядит странно, ведь dllArchitecture не может иметь это значение. Также в коде есть комментарий, поясняющий часть условия: "If the assembly is MSIL or none it can work anywhere so there does not need to be any warning ect." Получается, что данная проверка была не случайной, что делает код очень подозрительным. Попробуйте найти ошибку в следующем коде: Что-то мне подсказывает, что обнаружить проблему вам либо не удалось, либо на это было потрачено приличное количество времени. Сократим вышеописанный код: Предупреждение PVS-Studio: V3008 The '_toolsetProvider' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 284, 282. BuildParameters.cs 284 Теперь без труда можно понять, в чём здесь проблема. Полю _toolsetProvider дважды присваивается значение, что в данном случае само по себе бессмысленно. Сложно сказать, действительно ли тут допущена ошибка. Непонятно, должно ли быть вместо одного из присваиваний _toolsetProvider что-то другое. Возможно, данное присваивание является излишним, но таких случаев так же стоит избегать. Этот пример отлично демонстрирует пользу статического анализа. Человеческому глазу практически невозможно обнаружить дефект в подобном коде, чего нельзя сказать об анализаторе. Предупреждение PVS-Studio: V3066 Possible incorrect order of arguments passed to 'SdkResult' constructor: 'sdkResult.Warnings' and 'sdkResult.Errors'. InternalEngineHelpers.cs 83 Чтобы разобраться в данном срабатывании, необходимо рассмотреть определение конструктора SdkResult: Довольно редкое и интересное срабатывание, которое обычно указывает на серьёзную ошибку. Исходя из названий параметров, можно сделать вывод, что второй из них – коллекция ошибок, третий – коллекция предупреждений. Становится понятно, почему анализатор выдал предупреждение. Ведь при создании объекта в методе CloneSdkResult в качестве второго аргумента передаётся sdkResult.Warnings, а в качестве третьего – sdkResult.Errors. Скорее всего, здесь был перепутан порядок аргументов, так как тяжело представить ситуацию, в которой предупреждение и ошибка будут взаимозаменяемы. Предупреждение PVS-Studio: V3125 The 'project' object was used after it was verified against null. Check lines: 2446, 2439. Engine.cs 2446 Переменная project была проверена на null в условии: В следующем условии обращаются к свойству project.FullFileName. Проблема заключается в том, что здесь project на null уже не проверяют. Странно, что буквально семь строчек кода назад для переменной предполагалась возможность значения null, а сейчас – нет. Стоит отметить, что состояние переменной измениться не могло и buildRequest.ProjectFileName с project никак не связаны. Разыменование нулевой ссылки приведёт к выбрасыванию исключения типа NullReferenceException. Предупреждение PVS-Studio: V3125 The 'item' object was used after it was verified against null. Check lines: 139, 134. BuildItemCacheEntry.cs 139 В теле foreach переменная item проверяется на null. В случае, если item имеет значение null, в поток записывается 0. Дальше без какого-либо условия в поток запишется 1, а потом... Потом будет выброшено исключение типа NullReferenceException. Произойдёт это из-за вызова WriteToStream у item. Скорее всего, здесь не хватает блока else. Возможный вариант исправления ошибки: Предупреждение PVS-Studio: V3153 Enumerating the result of null-conditional access operator can lead to NullReferenceException. Consider inspecting: properties?.Keys. MockEngine.cs 165 Обратите внимание на коллекцию, по которой производят итерирование в foreach. Она получена с использованием оператора '?.'. Возможно, разработчик предполагал, что, если properties имеет значение null, код в теле foreach просто не выполнится. Он был прав, но есть некоторая проблема — будет выброшено исключение. У коллекции, по которой производят итерирование, вызывается метод GetEnumerator. Нетрудно понять, к какому исходу приведёт вызов этого метода у переменной, имеющей значение null. Более подробный разбор таких проблем вы можете найти в статье. В данном фрагменте кода анализатор обнаружил сразу две ошибки: V3125 The 'propertyValue' object was used after it was verified against null. Check lines: 3301, 3253. Expander.cs 3301 V3095 The 'propertyValue' object was used before it was verified against null. Check lines: 3301, 3324. Expander.cs 3301 По сути, оба срабатывания указывают на одну проблему. Рассмотрим условие первого if. Его часть состоит из проверки propertyValue на null. Это говорит о том, что разработчик предполагал такое значение для этой переменной. Может возникнуть ситуация, когда propertyValue == null будет true, а другая часть условия – false. Следовательно, выполнится код в else-ветви этого if. В этой ветви произойдет разыменование нулевой ссылки при вызове метода propertyValue.GetType. Также стоит отметить, что и далее перед вызовом метода у propertyValue проверка на null присутствует. В статье отражены как срабатывания диагностик, которых ещё не существовало на момент первой проверки, так и срабатывания относительно старых диагностических правил. Очевидно, что новые диагностики находят ошибки, которые мы не обнаружили при написании предыдущей статьи. Старые же диагностики используют общие механизмы. Эти механизмы постоянно дорабатываются, чтобы сделать анализ более качественным, вследствие чего и появляются новые срабатывания старых диагностик. Может возникнуть вопрос, почему в статье описано лишь девять срабатываний. Связано это с тем, что хотелось рассмотреть наиболее интересные из найденных дефектов и не сделать статью скучной. Также стоит отдать должное разработчикам MSBuild, ведь проект действительно качественный. PVS-Studio постоянно совершенствуется, добавляются новые диагностики, модифицируются старые. Это позволяет находить всё больше опасных мест в коде программ. Вы можете сами оценить работу анализатора, бесплатно попробовав PVS-Studio. Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Panevin. Build to order? Checking MSBuild for the second time.  Программист C#