MSBuild — популярная платформа сборки с открытым исходным кодом, созданная Microsoft. Разработчики во всем мире используют MSBuild. В 2016 году мы впервые проверили его и обнаружили несколько подозрительных мест. Сможем ли мы найти что-нибудь на этот раз? Давайте посмотрим!

Введение

С момента предыдущей проверки проект сильно вырос. Наш анализатор тоже стал более продвинутым. Это только делает эту задачу более интересной! Несмотря на высокое качество продукта MSBuild и известное имя его создателя, нам снова удалось найти некоторые проблемы в исходном коде MSBuild. Проект почти полностью написан на C#. Посмотреть его можно на GitHub. Мы взяли код из этого коммита.

Для сравнения результатов анализа рассмотрим две диаграммы:

После второй проверки анализатор выдал 839 предупреждений. В прошлый раз их было всего 262. Количество предупреждений среднего уровня увеличилось в четыре раза. Предупреждения такого уровня достоверности преобладают в нашей статье. Количество предупреждений Low-level увеличилось примерно в два с половиной раза. Предупреждения высокого уровня увеличились почти в два раза.

С момента первой проверки прошло шесть лет — и мы, разработчики PVS-Studio, зря времени не теряли :). С момента первой проверки MSBuild мы добавили в анализатор C# 64 GA (общий анализ) и 23 диагностики OWASP. Мы также улучшили существующие правила диагностики. Но не только разработчики C# проделали значительную работу. Если хотите проследить, как менялся анализатор — нажмите здесь.

Рассмотрим самые интересные предупреждения.

Неверный шаг

Проблема 1

Предупреждение PVS-Studio: Постфиксное приращение V3133 для переменной parsePoint бессмысленно, так как эта переменная перезаписывается. Сканер.cs 310

Возможно, разработчик ожидал, что ScanForPropertyExpressionEnd примет увеличенное значение parsePoint в качестве второго аргумента. К сожалению, этого не произойдет. Проблема заключается в использовании постфиксной записи для приращения. В этом случае сначала возвращается текущее значение переменной, и только потом оно увеличивается.

Поэтому в метод передается начальное значение parsePoint. Значение, полученное после выполнения ScanForPropertyExpressionEnd, присваивается переменной parsePoint. Из-за этого увеличенное значение переменной перезаписывается. Итак, операция инкремента ни на что не влияет в этом фрагменте кода.

Эту проблему можно решить, изменив постфиксную нотацию на префиксную:

parsePoint = ScanForPropertyExpressionEnd(expression, ++parsePoint);

Подозрительные логические выражения

Проблема 2

Анализатор выдал три предупреждения на этот фрагмент кода:

  • V3022 Выражение leftConflictReference.IsPrimary && rightConflictReference.IsPrimary всегда ложно. ReferenceTable.cs 2388
  • V3063 Часть условного выражения всегда истинна, если она оценивается: !isNonUnified. ReferenceTable.cs 2389
  • V3063 Часть условного выражения всегда истинна, если она оценивается: !isNonUnified. ReferenceTable.cs 2390

Второе и третье предупреждения являются следствием проблемы, отмеченной первым предупреждением. Давайте посмотрим на условие последнего if. Как мы видим, значения leftConflictReference.IsPrimary и rightConflictReference.IsPrimary в теле if всегда равны false.

Переменная isNonUnified инициализируется значением, полученным после выполнения leftConflictReference.IsPrimary && rightConflictReference.IsPrimary. Обе эти переменные имеют значение false. Поэтому isNonUnified всегда false.

Затем isNonUnified используется как часть выражения для инициализации еще двух переменных:

Следовательно, значение этих переменных зависит только от правого операнда оператора &&. Код можно упростить, заменив тело if следующим:

Скорее всего, код не содержит никаких ошибок, просто какие-то ненужные операции. Однако мы не можем игнорировать предупреждение анализатора — это не ложное срабатывание. Мой тиммейт написал об этом статью, очень рекомендую прочитать.

Проблема 3

Предупреждение PVS-Studio: V3063 Часть условного выражения всегда ложна, если оно оценивается: dllArchitecture == SystemProcessorArchitecture.MSIL. ReferenceTable.cs 2968

Переменная dllArchitecture инициализируется значением SystemProcessorArchitecture.None. Этой переменной можно присвоить другое значение только в теле switch. Если присмотреться, можно заметить, что SystemProcessorArchitecture.MSIL не назначен ни в одном из блоков case. Обратите внимание, что (SystemProcessorArchitecture) 6 не соответствует элементу MSIL. В ветке по умолчанию нет присвоения этой переменной.

Под switch есть проверка того, что dllArchitecture равно SystemProcessorArchitecture.MSIL. Выглядит странно — dllArchitecture не может иметь это значение.

Код также содержит комментарий, поясняющий часть условия: «Если сборка MSIL или отсутствует, она может работать где угодно, поэтому нет необходимости в каких-либо предупреждениях и т. д.». Значит, проверка не случайна. Это делает код очень подозрительным.

Проблема 4

Можете ли вы найти здесь ошибку?

Что-то мне подсказывает, что вы либо не нашли, либо нашли, но часами искали. Давайте немного сократим этот фрагмент кода:

Предупреждение PVS-Studio: V3008 Переменной _toolsetProvider два раза подряд присваиваются значения. Возможно, это ошибка. Проверить строки: 284, 282. BuildParameters.cs 284

Теперь вы можете легко найти проблему здесь. Полю _toolsetProvider дважды присваивается значение. Это абсолютно бессмысленно. Трудно сказать, действительно ли это ошибка. Неясно, должно ли быть что-то еще вместо одного из назначений _toolsetProvider. Возможно, это лишнее назначение, но таких случаев лучше избегать.

Эта проблема — хороший пример того, как может помочь статический анализ. Человеческий глаз почти всегда не сможет найти проблему в таком коде, а статический анализатор — нет.

Перепутанные аргументы

Проблема 5

Предупреждение PVS-Studio: V3066 Возможен неверный порядок передачи аргументов в конструктор SdkResult: sdkResult.Warnings и sdkResult.Errors. InternalEngineHelpers.cs 83

Чтобы понять это предупреждение, нам нужно сначала проверить объявление конструктора SdkResult:

Довольно редкое и интересное предупреждение. Обычно это указывает на серьезную ошибку. Судя по названиям параметров, можно сделать вывод, что второй параметр — это набор ошибок, а третий — набор предупреждений. Теперь понятно, почему анализатор выдал предупреждение. Когда объект создается в методе CloneSdkResult, sdkResult.Warnings передается в качестве второго аргумента, а sdkResult.Errors — в качестве третьего. аргумент. Скорее всего, здесь перепутан порядок аргументов — сложно представить ситуацию, когда предупреждение и ошибка были бы взаимозаменяемы.

Потенциальное нулевое разыменование

Проблема 6

Предупреждение PVS-Studio: V3125 Объект проект использовался после проверки на null. Проверить строки: 2446, 2439. Engine.cs 2446

Переменная project проверяется на null в следующем условии:

if (String.IsNullOrEmpty(toolsVersion) && project != null)

Следующее условие обращается к свойству project.FullFileName. Но project там не проверяется на null — отсюда и проблема. Это странно: разработчик подозревает, что переменная может быть null на семь строк кода выше этой, но не подозревает об этом сейчас.

Стоит отметить, что состояние переменной не может измениться и buildRequest.ProjectFileName никак не связано с project. Разыменование нулевой ссылки приведет к NullReferenceException.

Проблема 7

Предупреждение PVS-Studio: V3125 Объект item был использован после проверки на null. Проверьте строки: 139, 134. BuildItemCacheEntry.cs 139

В теле foreach переменная item проверяется на null. Если item имеет значение null, в поток записывается 0. Затем, без каких-либо условий, в поток записывается 1, а затем… Затем генерируется NullReferenceException. Это произойдет из-за вызова writeToStream элемента item.

Возможно, здесь отсутствует блок else. Ниже приведен возможный способ исправить ошибку:

Проблема 8

Предупреждение PVS-Studio: V3153 Перечисление результата оператора условного доступа null может привести к NullReferenceException. Рассмотрите возможность проверки: properties?.Keys. MockEngine.cs 165

В приведенном выше коде блок foreach перебирает коллекцию. Чтобы получить эту коллекцию, оператор foreach использует оператор ‘?.’. Разработчик мог предположить, что если properties равно null, код в теле foreach просто не будет выполняться. Хотя это правильно, вот проблема — будет выброшено исключение.

Для итерируемой коллекции вызывается метод GetEnumerator. Нетрудно догадаться, к чему приведет вызов этого метода для переменной со значением null.

Более подробный разбор таких вопросов вы можете найти в этой статье.

Проблема 9

Анализатор выдал два предупреждения на этот фрагмент кода:

  • V3125 Объект propertyValue был использован после того, как он был проверен на значение null. Проверить строки: 3301, 3253. Expander.cs 3301
  • V3095 Объект propertyValue использовался до того, как он был проверен на нуль. Проверить строки: 3301, 3324. Expander.cs 3301

На самом деле оба этих предупреждения указывают на одну и ту же проблему. Давайте посмотрим на условие первого if. Часть этого условия проверяет propertyValue на null. Это означает, что разработчик ожидал, что это значение может быть нулевым. Может быть случай, когда propertyValue == null равно true, а вторая часть условия false. Следовательно, будет выполнена ветвь else. В этой ветви ссылка null будет разыменована при вызове метода propertyValue.GetType. Также стоит отметить, что далее, перед вызовом метода, PropertyValue проверяется на null.

Заключение

В этой статье мы описали не только проблемы, диагностики которых не существовало, когда мы впервые проверяли MSBuild, но и предупреждения относительно старых диагностик.

Очевидно, что новая диагностика помогла найти ошибки, которые мы не увидели при первой проверке. В старой диагностике используются основные механизмы. Мы постоянно совершенствуем эти механизмы для достижения качественного анализа, поэтому старые диагностики выдают новые предупреждения.

Вы можете задать вопрос: «Почему вы описали только 9 предупреждений?» Мы хотели показать вам самые интересные из них, не делая статью скучной.

Наконец, что не менее важно, мы хотели бы отметить усердную работу разработчиков MSBuild — они действительно заботятся о качестве проекта.

Мы постоянно работаем над улучшением PVS-Studio: добавляются новые диагностики, модифицируются старые. Это позволяет найти больше фрагментов кода, которые могут быть опасны для вашей программы. Вы можете бесплатно попробовать PVS-Studio и посмотреть, что он может найти в вашем проекте.