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

О проекте

Mono — это проект по созданию полноценной реализации .NET Framework, который является бесплатным и с открытым исходным кодом. Основной разработчик Mono — корпорация Xamarin, ранее Novell.

Mono представляет собой набор инструментов, включающий компилятор C#, среду реализации .NET-mono (с поддержкой JIT) и mint (без поддержки JIT), отладчик, набор библиотек, включающий реализации WinForms, ADO.NET и ASP.NET, а также компиляторы smcs (для создания приложений для Moonlight) и vbc (для приложений, написанных на VB.NET).

В рамках этого проекта также предусмотрена привязка графической библиотеки GTK+ к платформе .NET.

Исходный код доступен в репозитории на GitHub. Количество строк кода для анализа из репозитория, загруженного с GitHub, составило около 3,6 млн (без учета пустых строк). Такая большая кодовая база выглядит очень привлекательно — где-то там обязательно должны прятаться ошибки. С другой стороны, анализ такого большого проекта будет полезен самому анализатору, так как послужит отличным стресс-тестом.

Инструмент анализа и особенности проверки

Инструмент анализа — статический анализатор кода PVS-Studio. На данный момент анализатор имеет более 100 диагностических правил, каждое из которых описано в документации, содержащей информацию об ошибке, возможных последствиях и способах ее исправления. С момента релиза мы успели проверить большое количество различных проектов, написанных на C#, таких как Roslyn, Xamarin.Forms, Space Engineers, CoreFX, Code Contracts и другие (можно посмотреть полный список по этой ссылке)

Сам анализатор доступен по этой ссылке. Пробной версии должно быть достаточно, чтобы оценить всю ценность этого инструмента. Если вас заинтересовал этот инструмент, вы можете написать нам и мы предоставим ключ для более близкого знакомства с инструментом, и поможем его настроить.

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

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

Пару слов о том, почему анализ проекта — вещь нетривиальная

В идеальном мире проверка проекта и написание статьи осуществляется по следующему сценарию: найти проект —> построить его —> запустить на нем анализатор —> найти приличное количество багов —> написать статью. Все довольны: ставим галочку напротив проверенного проекта, люди читают новую статью, разработчики узнали о багах в коде, автора хвалят за хорошую работу.

К сожалению, наш мир не идеален. Довольно часто проблемы возникают на различных этапах этого процесса. Если есть подробный мануал по сборке проекта, или можно сделать самому — отлично! Тогда можно смело приступать к проверке проекта и написанию статьи. В противном случае у нас будет сильная головная боль. Именно это и произошло с «Моно». Решение net_4_x.sln, объединяющее проекты C#, не компилируется «из коробки» (т.е. сразу после загрузки из репозитория). Один из скриптов сборки работал некорректно (был неправильный путь (возможно из-за того, что со временем менялась иерархия каталогов)), но и исправление пути не помогло.

Конечно, я не хотел сдаваться; поэтому я экспериментировал с компиляцией даже в свободное время. Но особого результата это не принесло. Наконец, потратив на это немало часов, мы решили написать статью «как есть».

Время от времени я указываю в статьях, что проект должен быть скомпилирован для полноценного анализа — со всеми зависимостями, без каких-либо багов и так далее. Как правило, я стараюсь делать это так; но всегда есть исключения из правил, как в этом случае, например.

Конечно, проверять нескомпилированный проект — плохая идея по нескольким причинам:

  • Анализ не такой качественный, каким мог бы быть. Это факт. Насколько именно снижается качество, зависит от реализации диагностического правила. Вы можете получить ложное срабатывание или, наоборот, полезное предупреждение не будет выдано;
  • следует быть предельно внимательным при просмотре лога, так как есть вероятность (хотя и небольшая) ложных срабатываний, которых можно было бы избежать, если проект был правильно скомпилирован;
  • по мере исчезновения нескольких полезных предупреждений есть шанс пропустить некоторые интересные ошибки, которые могли бы попасть в статью и привлечь внимание разработчиков (впрочем, они могут узнать об этих ошибках сами, если проверят проект);
  • Вот почему мы должны написать такие разделы, как: «Пару слов о том, почему проверка проекта…»

Тем не менее, было много подозрительных фрагментов, некоторые из которых будут описаны ниже.

Результаты анализа

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

К сожалению, в этот раз я не могу привести такую ​​статистику. Во-первых, кода много, а также предупреждений. Если количество анализируемых предупреждений составляет несколько десятков, то их можно просмотреть и дать приблизительную оценку. Когда количество предупреждений достигает нескольких сотен, то задача анализа становится чем-то далеко не тривиальным.

Во-вторых, для полностью скомпилированного проекта эта статистика может варьироваться: число может как увеличиваться, так и уменьшаться. Анализатор может получить больше семантической информации в скомпилированном проекте, а значит, сможет провести более глубокий анализ (исчезнут ложные срабатывания, появятся новые предупреждения). Для тех, кому интересно, как семантическая информация влияет на анализ, и на какие принципы опирается его работа, предлагаю прочитать статью Введение в Roslyn. Использование инструментов статического анализа для разработки программ».

Но вам, наверное, не терпится увидеть, что интересного можно найти в кодовом проекте? Что ж, давайте посмотрим на некоторые фрагменты кода.

Одни и те же подвыражения в одном выражении

Это одна из самых распространенных ошибок; для этого есть много причин. Это может быть копирование-вставка, похожие имена переменных, чрезмерное использование IntelliSense и простая невнимательность. Программист на секунду отвлекся — вот и ошибся.

public int ExactInference (TypeSpec u, TypeSpec v)
{
  ....
  var ac_u = (ArrayContainer) u;
  var ac_v = (ArrayContainer) v;
  ....
  var ga_u = u.TypeArguments;
  var ga_v = v.TypeArguments;
  ....
  if (u.TypeArguments.Length != u.TypeArguments.Length) // <=
    return 0;
  ....
}

Предупреждение PVS-Studio:V3001 Слева и справа от оператора != одинаковые подвыражения u.TypeArguments.Length. универсальный.cs 3135

Теперь, когда код метода нельзя упростить, не составит труда заметить ошибку в операторе if — параметр v, а не u следует использовать как экземпляр типа TypeSpec. Возможно, ошибка была из-за того, что символы u и v выглядят довольно похоже, и их легко спутать, если человек не акцентирует внимание на этом выражении.

Остальная часть кода была дана, чтобы подчеркнуть, что эти параметры обычно используются вместе,

if (u.TypeArguments.Length != v.TypeArguments.Length) 
    return 0;

Случай, который также представляет интерес:

bool BetterFunction (....)
{
  ....
  int j = 0;
  ....
  if (!candidate_params && 
      !candidate_pd.FixedParameters [j - j].HasDefaultValue) { // <=
    return true;
  }
  ....
  if (!candidate_pd.FixedParameters [j - 1].HasDefaultValue &&   
       best_pd.FixedParameters [j - 1].HasDefaultValue)
    return true;
  if (candidate_pd.FixedParameters [j - 1].HasDefaultValue &&     
      best_pd.HasParams)
    return true;
  ....
}

Предупреждение PVS-Studio:V3001 Слева и справа от оператора - одинаковые подвыражения j. ecore.cs 4832

Программист ошибся, написав выражение j — j в одном из выражений для вычисления индекса. Таким образом будет доступ к первому элементу массива. Если это именно то, что здесь нужно, то логичнее было бы использовать целочисленный литерал, равный 0. Другие обращения по индексу к этому массиву: j — 1 доказывают, что это баг . Опять же могу предположить, что ошибка не была замечена из-за некоторого сходства символов j и 1, так что при быстром просмотре кода она может остаться незамеченной.

Примечание коллеги Андрея Карпова. Когда я читал черновик этой статьи, то хотел сделать пометку, что Сергей разместил неверный фрагмент кода. Смотрел код, ошибки не увидел. Только когда начал читать описание, до меня дошло. Подтверждаю, что эту опечатку очень сложно заметить. Наша PVS-Studio великолепна!

Продолжаем ломать голову:

internal void SetRequestLine (string req)
{
  ....
  if ((ic >= 'A' && ic <= 'Z') ||
      (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<'    &&
       c != '<' && c != '>'  && c != '@' && c != ',' && c != ';' &&
       c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' &&
       c != ']' && c != '?'  && c != '=' && c != '{' && c != '}'))
    continue;
  ....
}

Предупреждение PVS-Studio:V3001 Слева и справа от оператора && одинаковые подвыражения ‘c != ‘‹’’. HttpListenerRequest.cs 99

Подвыражение c != ‘‹’ записывается в выражении дважды. Это, наверное, просто лишнее сравнение.

protected virtual bool ShouldSerializeLinkHoverColor ()
{
  return grid_style.LinkHoverColor != grid_style.LinkHoverColor;
}

Предупреждение PVS-Studio:V3001 Слева и справа от оператора != одинаковые подвыражения grid_style.LinkHoverColor. DataGrid.cs 2225

Мне не нужно было упрощать код, чтобы сделать ошибку более очевидной. В сравнении участвуют два похожих подвыражения — grid_style.LinkHoverColor.

То есть код, вероятно, должен был быть таким:

protected virtual bool ShouldSerializeLinkHoverColor ()
{
  return grid_style.LinkHoverColor != default_style.LinkHoverColor;
}

Почему так? В приведенном выше коде есть ряд методов, в которых различные свойства grid_style сравниваются со свойствами объекта default_style. Но в последнем случае программист ослабил бдительность и допустил ошибку. Хм… эффект последней строки?

Ну, эти ошибки просто классические:

static bool AreEqual (VisualStyleElement value1, 
                      VisualStyleElement value2)
{
  return
    value1.ClassName == value1.ClassName && // <=
    value1.Part == value2.Part &&
    value1.State == value2.State;
}

Предупреждение PVS-Studio:V3001 Слева и справа от оператора == одинаковые подвыражения value1.ClassName. ThemeVisualStyles.cs 2141

Подвыражение value1.ClassName было случайно сопоставлено само с собой. Конечно, во втором случае следует использовать объект value2.

Я думаю, если мы будем использовать табличное форматирование для выравнивания кода, ошибку будет сложнее заметить. Это хороший способ предотвратить попадание таких опечаток в код:

static bool AreEqual (VisualStyleElement value1, 
                      VisualStyleElement value2)
{
  return
       value1.ClassName == value1.ClassName
    && value1.Part      == value2.Part
    && value1.State     == value2.State;
}

Код, отформатированный таким образом, намного легче читать и легче заметить, что с одним из столбцов что-то не так. Подробнее см. главу 13 из книги Главный вопрос программирования, рефакторинга и всего остального.

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

Аналогичные условия в конструкции else if

enum TitleStyle {
  None   = 0,
  Normal = 1,
  Tool   = 2
}
internal TitleStyle title_style;
public Point MenuOrigin {
  get {
    ....
    if (this.title_style == TitleStyle.Normal)  {        // <=
      pt.Y += caption_height;
    } else if (this.title_style == TitleStyle.Normal)  { // <=
      pt.Y += tool_caption_height;
    }
    ....
}

Предупреждение PVS-Studio: V3003Обнаружено использование паттерна if (A) {…} else if (A) {…}. Существует вероятность наличия логической ошибки. Проверить строки: 597, 599. Hwnd.cs 597

Одно и то же выражение this.title_style == TitleStyle.Normal проверяется дважды. Судя по всему, в этом коде есть ошибка. Несмотря на значение выражения, приведенного выше, выражение pt.Y += tool_caption_height никогда не будет выполнено. Могу предположить, что во втором случае программист хотел сравнить поле title_style с константой TitleStyle.Tool.

Те же тела "если-то" и "если-иначе"

public static void DrawText (....)
{
  ....
  if (showNonPrint)
    TextRenderer.DrawTextInternal (g, text, font, 
      new Rectangle (new Point ((int)x, (int)y), max_size), color,   
      TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix, false);
  else
    TextRenderer.DrawTextInternal (g, text, font, 
      new Rectangle (new Point ((int)x, (int)y), max_size), color, 
      TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix, false);
  ....
}

Предупреждение PVS-Studio: V3004Утверждение тогда эквивалентно утверждению еще. System.Windows.Forms-net_4_x TextBoxTextRenderer.cs 79

Статический метод DrawTextInternal класса TextRenderer с теми же аргументами будет вызываться независимо от значения переменной showNonPrint. Не исключено, что ошибка была допущена из-за использования копипаста. Вызов метода был скопирован, но аргументы остались забытыми.

Возвращаемое значение метода не используется

public override object ConvertTo(.... object value, 
                                 Type destinationType) 
{
  ....
  if (destinationType == typeof(string)) {
    if (value == null) {
      return String.Empty;
    }
    else {
      value.ToString();
    }
  }
  ....
}

Предупреждение PVS-Studio: V3010Необходимо использовать возвращаемое значение функции ToString. ColumnTypeConverter.cs 91

Это довольно интересная ошибка с явно далеко идущими последствиями. Из кода видно, что есть проверка типа, и если тип string, то есть проверка на null. Затем начинается самая интересная часть; если ссылка value имеет значение null, то возвращается пустая строка, иначе… Скорее всего, ожидалось, что программа вернет строковое представление объекта, но есть нет оператора return. Поэтому возвращаемое значение метода ToString() никак не будет использоваться, а метод ConvertTo будет выполняться в дальнейшем. Таким образом, из-за забытого оператора return изменилась вся логика программы. Я предполагаю, что правильная версия кода должна выглядеть так:

if (value == null) {
  return String.Empty;
}
else {
  return value.ToString();
}

Ошибку, которую мы здесь имеем в виду, вы узнаете позже

Обычно я упрощаю методы, чтобы легче было увидеть ошибку. Давайте на этот раз поиграем. Найдите ошибку в следующем фрагменте кода. Чтобы было интереснее, я не буду называть тип ошибки и не буду упрощать код (здесь я уже привожу только часть метода).

Вы можете нажать на картинку, чтобы увеличить ее.

Ну как дела? Мне почему-то кажется, что многие даже не пробовали. Но я больше не буду тебя дразнить.

Предупреждение PVS-Studio:V3012 Оператор ‘?:’, независимо от его условного выражения, всегда возвращает одно и то же значение: Color.FromArgb (150, 179, 225). ProfessionalColorTable.cs 258

Вот он, злосчастный тернарный оператор:

button_pressed_highlight = use_system_colors ?
                             Color.FromArgb (150, 179, 225) : 
                             Color.FromArgb (150, 179, 225);

Независимо от значения переменной use_system_colors объекту button_pressed_highlight будет присвоено одно и то же значение. Если вы считаете, что такие ошибки иногда бывает сложно отследить, предлагаю посмотреть весь файл (ProfessionalColorTable.cs) и понять, что такие ошибки не просто трудно отследить самостоятельно — это просто невозможно.

Подобных фрагментов было довольно много (аж 32), что заставляет усомниться, что это реальный баг, а какое-то преднамеренное действие. Тем не менее код выглядит странно, поэтому я бы посоветовал его перепроверить. Даже если это не ошибка, а ожидаемая логика, было бы гораздо проще использовать простое присваивание, а не писать странные запутанные тернарные операторы. Остальные предупреждения V3012 приведены в файле.

Использование счетчика другого цикла

public override bool Equals (object obj)
{
  if (obj == null)
    return false;
  PermissionSet ps = (obj as PermissionSet);
  if (ps == null)
    return false;
  if (state != ps.state)
    return false;
  if (list.Count != ps.Count)
    return false;
  for (int i=0; i < list.Count; i++) {
    bool found = false;
    for (int j=0; i < ps.list.Count; j++) {
      if (list [i].Equals (ps.list [j])) {
        found = true;
        break;
      }
    }
    if (!found)
      return false;
  }
  return true; 
}

Предупреждение PVS-Studio:V3015 Вероятно, внутри оператора for сравнивается не та переменная. Попробуйте просмотреть i corlib-net_4_x PermissionSet.cs 607.

Условие выхода из вложенного цикла i ‹ ps.list.Count выглядит подозрительно. Переменная j работает здесь как счетчик цикла, но в условии выхода переменная i используется как счетчик внешнего цикла.

Замысел автора кода вполне понятен — проверить, что коллекции содержат одинаковые элементы. Но если элемента из коллекции list нет в ps.list, то выход из вложенного цикла с помощью оператор разрыва. При этом переменная i внутри этого цикла не меняется, т.е. выражение i ‹ ps.list.Count всегда будет иметь истинное значение. В результате цикл будет выполняться до тех пор, пока индекс коллекции не выйдет за границы (из-за постоянного приращения счетчика j).

Проверка на null неправильной ссылки после ее приведения с использованием оператора as

Оказалось, что это типичная ошибка для C#. Мы находим его практически в каждом проекте, о котором пишем статью. Как правило, V3019 выявляет случаи следующего рода:

var derived = base as Derived;
if (base == null) {
  // do something
}
// work with 'derived' object

Проверка base == null сохранится только в том случае, если base действительно имеет nullзначение, и тогда оно не Неважно, сможем ли мы провести кастинг или нет. Судя по всему, здесь имелась в виду проверка производной ссылки. Затем, если base != null, и программе не удалось выполнить приведение,нодалее происходит обработка членов производногообъекта , мы получим исключение типа NullReferenceException.

Мораль: если вы используете этот шаблон, убедитесь, что вы сверяете правильную ссылку с нулевым значением.

Но это все теория. Посмотрим, что нам удалось найти на практике:

public override bool Equals (object o)
{
  UrlMembershipCondition umc = (o as UrlMembershipCondition);
  if (o == null)
    return false;
  ....
  return (String.Compare (u, 0, umc.Url, ....) == 0); // <=
}

Предупреждение PVS-Studio:V3019 Возможно некорректная переменная сравнивается с null после приведения типа с использованием ключевого слова as. Проверьте переменные o, umc. UrlMembershipCondition.cs 111

Эта схема точно такая же, как описанная выше. Если тип объекта o несовместим с типом UrlMembershipCondition, и в то же время объект o не null, то при попытке доступа к свойству umc.Url мы получим исключение NullReferenceException.

Таким образом, чтобы исправить ошибку, нам нужно исправить проверку:

if (umc == null)
  return false;

Взгляните на еще один баг:

static bool QSortArrange (.... ref object v0, int hi, 
                          ref object v1, ....)
{
  IComparable cmp;
  ....
  cmp = v1 as IComparable;
  if (v1 == null || cmp.CompareTo (v0) < 0) {
    ....
  }
  ....
}

Предупреждение PVS-Studio:V3019 Возможно некорректная переменная сравнивается с null после приведения типа с использованием ключевого слова as. Проверьте переменные v1, cmp. Массив.cs 1487

Эта ситуация аналогична описанной выше. Единственное отличие — в случае неудачного поведения исключение NullReferenceException будет сгенерировано сразу — прямо во время проверки выражения.

Примерно такая же ситуация и в нескольких других фрагментах, поэтому приведу еще 12 предупреждений в текстовом файле.

Безусловное исключение

public void ReadEmptyContent(XmlReader r, string name)
{
  ....
  for (r.MoveToContent(); 
         r.NodeType != XmlNodeType.EndElement; 
           r.MoveToContent())
  {
    if (r.NamespaceURI != DbmlNamespace)
      r.Skip();
    throw UnexpectedItemError(r); // <=
  }
  ....
}

Предупреждение PVS-Studio: V3020Безусловный бросок внутри цикла. System.Data.Linq-net_4_x XmlMappingSource.cs 180

Во время первой итерации мы получим исключение UnexpectedItemError. По крайней мере, это выглядит странно. Кстати, Visual Studio подсвечивает объект r в секции, где происходит изменение счетчика циклов, с подсказкой о недостижимом коде. Возможно, автор кода просто не использовал Visual Studio или не заметил предупреждений, поэтому ошибка осталась в коде.

Подозрительные операторы if

Довольно часто мы видим ошибки, когда в методе есть два одинаковых оператора if, а значение объектов, используемых в условных выражениях этих операторов, не изменяется. Если какое-либо из этих условных выражений истинно, то произойдет выход из тела тела метода. Таким образом, второе «если» никогда не будет выполнено. Давайте посмотрим на фрагмент кода, который содержит именно такую ​​ошибку:

public int LastIndexOfAny (char [] anyOf, int startIndex, int count)
{
  ....
  if (this.m_stringLength == 0)
    return -1;
  ....
  if (this.m_stringLength == 0)
    return -1;
  ....
}

Предупреждение PVS-Studio:V3021 есть два оператора if с одинаковыми условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второй оператор if бессмыслен. corlib-net_4_x String.cs 287

Выполнение метода никогда не дойдет до второго оператора if, указанного в этом фрагменте, потому что еслиthis.m_stringLength == 0, то выход будет выполнен при выполнении первого условное заявление. Мы могли бы оправдать код, если бы значение поля m_stringLength изменилось, но это не так.

Последствия бага зависят от причины, по которой он появился:

  • Если оба условных выражения верны (с точки зрения логики), а второй код просто лишний — в этом нет ничего страшного, но удалить его стоит, чтобы не вводить в заблуждение других людей;
  • Если имелась в виду проверка другого выражения в одном из операторов, или в случае, если имелись в виду другие действия — это более серьезная проблема, указывающая на ошибку в логике программы. Тогда к этому вопросу следует отнестись более серьезно.

Пример более серьезного случая можно увидеть в следующем фрагменте кода (нажмите на картинку для увеличения):

Конечно, найти ошибку в этом коде несложно. Шучу, конечно, это непросто. Не для анализатора. Давайте воспользуемся нашим старым добрым методом упрощения кода, чтобы яснее увидеть ошибку:

private PaperKind GetPaperKind (int width, int height)
{
  ....
  if (width == 1100 && height == 1700)
    return PaperKind.Standard11x17;
  ....
  if (width == 1100 && height == 1700)
    return PaperKind.Tabloid;
  ....
}

Предупреждение PVS-Studio:V3021 есть два оператора if с одинаковыми условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второй оператор if бессмыслен. System.Drawing-net_4_x PrintingServicesUnix.cs 744

Если выражение width == 1100 && height == 1700 истинно, будет выполнено только первое выражение if. Однако значения, возвращаемые этим выражением, если оно истинно, отличаются, поэтому мы не можем просто сказать, что второй оператор if является избыточным. Более того, возможно, в его состоянии должно быть и другое выражение. Очевидно, рабочий процесс программы поврежден.

Наконец, я хотел бы взглянуть на еще один фрагмент кода с этой ошибкой:

private void SerializeCore (SerializationStore store, 
                            object value, bool absolute)
{
  if (value == null)
    throw new ArgumentNullException ("value");
  if (store == null)
    throw new ArgumentNullException ("store");
  CodeDomSerializationStore codeDomStore = 
    store as CodeDomSerializationStore;
  if (store == null)
    throw new InvalidOperationException ("store type unsupported");
  codeDomStore.AddObject (value, absolute);
}

Предупреждение PVS-Studio:V3021 есть два оператора if с одинаковыми условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второй оператор if бессмыслен. System.Design-plaindesign-net_4_x CodeDomComponentSerializationService.cs 562

Это предупреждение имеет много общего с предупреждением V3019, так как у нас есть паттерн проверки на null после приведения с использованием оператора as неправильной ссылки. Неважно, какое предупреждение выдается — ошибка очевидна.

Были и другие подобные предупреждения:

  • V3021 Имеются два оператора if с идентичными условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второе утверждение «если» бессмысленно. Mono.Data.Sqlite-net_4_x SQLiteDataReader.cs 270
  • V3021 Имеются два оператора if с идентичными условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второе утверждение «если» бессмысленно. System.Web-net_4_x HttpUtility.cs 220
  • V3021 Имеются два оператора if с идентичными условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второй оператор if бессмысленен. System.Design-plaindesign-net_4_x CodeDomComponentSerializationService.cs 562
  • V3021 Имеются два оператора if с идентичными условными выражениями. Первый оператор if содержит возврат метода. Это означает, что второе утверждение «если» бессмысленно. Mono.Security.Providers.DotNet-net_4_x DotNetTlsProvider.cs 77

Подозрительные строки формата

Правило диагностики V3025 обнаруживает строки неправильного формата. Это также тип ошибки, который мы находим во многих проектах, которые мы проверяем. Обычно бывают ситуации двух видов:

  • строка формата предполагает большее количество параметров, чем задано;
  • строка формата ожидает меньше параметров, чем задано.

В первом случае будет выброшено исключение типа FormatException, во втором случае неиспользуемые аргументы будут просто проигнорированы. В любом случае такие фрагменты стоит пересматривать и исправлять.

Конечно, я бы не стал говорить об этом диагностическом правиле, если бы не было найдено подобных ошибок.

static IMessageSink GetClientChannelSinkChain(string url, ....)
{
  ....
  if (url != null) 
  {
    string msg = String.Format (
      "Cannot create channel sink to connect to URL {0}. 
       An appropriate channel has probably not been registered.", 
      url); 
    throw new RemotingException (msg);
  }
  else 
  {
    string msg = String.Format (
      "Cannot create channel sink to connect to the remote object. 
       An appropriate channel has probably not been registered.", 
      url); 
    throw new RemotingException (msg);
  }
  ....
}

Предупреждение PVS-Studio:V3025 Неверный формат. При вызове функции Формат ожидается другое количество элементов формата. Аргументы не используются: url. corlib-net_4_x RemotingServices.cs 700

Хочу обратить ваше внимание на вторую строку формата. Это строковый литерал, который не обеспечивает замену аргументов (в отличие от строки формата выше). Однако метод Format принимает объект url в качестве второго аргумента. Из вышесказанного следует, что объект url при формировании новой строки будет просто проигнорирован, и информация о нем не попадет в текст исключения.

В C# 6.0 были добавлены интерполированные строки, что в ряде случаев поможет избежать проблем, связанных с использованием форматных строк, в том числе некорректного количества аргументов.

Рассмотрим еще один ошибочный фрагмент кода:

public override string ToString ()
{
  return string.Format ("ListViewSubItem {{0}}", text);
}

Предупреждение PVS-Studio:V3025 Неверный формат. При вызове функции Формат ожидается другое количество элементов формата. Неиспользуемые аргументы: текст. System.Windows.Forms-net_4_x ListViewItem.cs 1287

По форматной строке можно сделать вывод, что текст в фигурных скобках должен быть записан в результирующей строке. На самом деле результирующая строка будет такой:

"ListViewSubItem {{0}}"
To fix this bug, we could use interpolated strings to rewrite the method:
public override string ToString ()
{
  return $"ListViewSubItem {{{text}}}",;
}

Или, оставаясь верным методу String.Format, мы должны добавить фигурные скобки с каждой стороны. Тогда строка формата будет выглядеть следующим образом:

"ListViewSubItem {{{0}}}"

Вот последний фрагмент со строкой формата. Как всегда самое интересное подается на десерт:

void ReadEntropy ()
{
  if (reader.IsEmptyElement)
    throw new XmlException (
      String.Format ("WS-Trust Entropy element is empty.{2}", 
                      LineInfo ()));
  ....
}

Предупреждение PVS-Studio:V3025 Неверный формат. При вызове функции Формат ожидается другое количество элементов формата. Элементы формата не используются: {2}. Аргументы не используются: 1-й. System.ServiceModel-net_4_x WSTrustMessageConverters.cs 147

Я понятия не имею, как элемент форматирования с индексом «2» попал в строку форматирования, но это приводит к довольно забавной ошибке. Это должно было вызвать исключение с некоторым текстом, созданным строкой формата. И будет выброшено исключение. Исключение типа FormatException, поскольку текущая строка формата требует 3 аргумента (поскольку нужен именно третий), а представлен только один.

Если программист перепутал только номер запрошенного аргумента (при рефакторинге, например), то этот баг будет легко исправить:

"WS-Trust Entropy element is empty.{0}"

В этом файле приведены другие подозрительные фрагменты, обнаруженные правилом V3025.

Доступ по нулевой ссылке

private bool IsContractMethod (string methodName, 
                               Method m, 
                               out TypeNode genericArgument)
{
  ....
  return m.Name != null && m.Name == methodName &&
    (m.DeclaringType.Equals (this.ContractClass)     // <=
     || (m.Parameters    != null && 
         m.Parameters.Count == 3 && 
         m.DeclaringType != null &&                  // <=
         m.DeclaringType.Name != ContractClassName));
}

Предупреждение PVS-Studio: V3027Переменная ‘m.DeclaringType’ использовалась в логическом выражении до того, как она была проверена на нуль в том же логическом выражении. Mono.CodeContracts-net_4_x ContractNodes.cs 211

Перед доступом к свойству Name свойства DeclaringType программист решил перестраховаться и проверить свойство DeclaringType на соответствие null, чтобы он случайно не обратился к нулевой ссылке. Желание сделать это понятно и вполне правомерно. Единственное, это не будет иметь никакого эффекта, потому что далее в коде мы видим, что метод экземпляра Equals для свойства DeclaringType, а это значит, что если DeclaringType == null, мы получим исключение типа NullReferenceException. Чтобы решить эту проблему, мы можем переместить проверку на значение NULL выше в коде или использовать оператор условия NULL ('?.'), который доступен в C# 6.0.

Другой случай.

Node leftSentinel;
....
IEnumerator<T> GetInternalEnumerator ()
{
  Node curr = leftSentinel;
  while ((curr = curr.Nexts [0]) != rightSentinel && curr != null) {
    ....
  }
}

Предупреждение PVS-Studio: V3027Переменная curr использовалась в логическом выражении до того, как она была проверена на нуль в том же логическом выражении. Mono.Parallel-net_4_x ConcurrentSkipList.cs 306

Опять та же ситуация. Если curr == null, то будет выход из цикла. Если curr изначально был нулевым (в момент выполнения кода leftSentinel == null), мы снова получим исключение NullReferenceException .

Излишняя проверка

Время от времени мы видим выражения следующего вида или подобные им:

!aa || (aa && bb)

Их можно упростить до выражения следующего вида:

!aa || bb

В некоторых случаях вы получаете некоторый прирост производительности (хотя и незначительный), но также второй вариант легче читать, когда он логически эквивалентен первому (если подвыражение aa не меняется между вызовами).

Возможно, вместо aa должно было быть другое подвыражение:

!aa || (cc && bb)

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

public void Emit(OpCode opc)
{
  Debug.Assert(opc != OpCodes.Ret || (
               opc == OpCodes.Ret && stackHeight <= 1));
  ....
}

Предупреждение PVS-Studio: V3031Можно упростить лишнюю проверку. Оператор || окружен противоположными выражениями. mcs-net_4_x ILGenerator.cs 456

Избыточный код. Доступ к объекту opc не меняет его значения, поэтому это выражение можно упростить:

Debug.Assert(opc != OpCodes.Ret || stackHeight <= 1));

Другой фрагмент кода:

public bool Validate (bool checkAutoValidate)
{
  if ((checkAutoValidate && (AutoValidate != AutoValidate.Disable)) ||
      !checkAutoValidate)
    return Validate ();
  return true;
}

Предупреждение PVS-Studio: V3031Можно упростить лишнюю проверку. Оператор || окружен противоположными выражениями. System.Windows.Forms-net_4_x ContainerControl.cs 506

Эта ситуация похожа на предыдущую. Выражение можно легко и безболезненно упростить следующим образом:

!checkAutoValidate || (AutoValidate != AutoValidate.Disable)

Некоторые из выбранных мной предупреждений приведены в файле.

Форматирование кода, не соответствующее логике программы

При создании диагностических правил, таких как V3033, мы обсуждали, насколько они актуальны. Дело в том, что диагностика, связанная с форматированием кода, весьма своеобразна, так как большинство редакторов/сред разработки (та же Visual Studio) уже форматирует код по мере его написания. Поэтому вероятность совершить такую ​​ошибку достаточно мала. Я редко вижу ошибки такого рода, но в Моно их было парочка.

public bool this [ string header ] {
  set {
      ....
      if (value)
        if (!fields.Contains (header))
          fields.Add (header, true);
      else
        fields.Remove (header);
  }
}

Предупреждение PVS-Studio:V3033 Возможно, эта ветвь else должна применяться к предыдущему оператору if. HttpCacheVaryByHeaders.cs 159

Код отформатирован таким образом, что может показаться, что else относится к первому оператору if. Но для компилятора не имеет значения, как отформатирован код, потому что он будет интерпретировать этот фрагмент по-своему, связывая else со вторым оператором if, так как он должно быть. Интересный баг. Код, выровненный в соответствии с заданной логикой, должен быть таким:

if (value)
  if (!fields.Contains (header))
    fields.Add (header, true);
  else
    fields.Remove (header);

Снова появилось похожее предупреждение: V3033 Возможно, эта ветвь else должна применяться к предыдущему оператору if. HttpCacheVaryByParams.cs 102

К этой категории можно отнести еще одно диагностическое правило — V3043.

Неверный код:

public void yyerror (string message, string[] expected) {
  ....
  for (int n = 0; n < expected.Length; ++ n)
    ErrorOutput.Write (" "+expected[n]);
    ErrorOutput.WriteLine ();
  ....
}

Предупреждение PVS-Studio: V3043Логика работы кода не соответствует его форматированию. Оператор имеет отступ вправо, но выполняется всегда. Возможно, фигурные скобки отсутствуют. cs-parser.cs 175

Судя по форматированию кода (и забывая о правилах программирования), можно подумать, что оба вызова методов (Write и Writeline)относятся к для заявление. Фактически в цикле будет вызываться только метод Write. С этим кодом определенно что-то не так! Если программист действительно имел в виду такую ​​логику (она может показаться действительно логичной — элементы отображаются, после чего вставляется пустая строка), то зачем нам форматирование, которое действительно вводит в заблуждение? С другой стороны, трудно сразу понять истинную логику высказывания. Не зря программисты придерживаются определенных стилей форматирования.

private string BuildParameters ()
{
  ....
  if (result.Length > 0)
    result.Append (", ");
    if (p.Direction == TdsParameterDirection.InputOutput) // <=
      result.Append (String.Format("{0}={0} output",     
                                   p.ParameterName));
    else
  result.Append (FormatParameter (p));
  ....
}

Предупреждение PVS-Studio: V3043Логика работы кода не соответствует его форматированию. Оператор имеет отступ вправо, но выполняется всегда. Возможно, фигурные скобки отсутствуют. tds50.cs 379

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

public void Restore ()
{
  while (saved_count < objects.Count)
    objects.Remove (objects.Last ().Key);
    referenced.Remove (objects.Last ().Key);
  saved_count = 0;
  referenced.RemoveRange (saved_referenced_count, 
                          referenced.Count - saved_referenced_count);
  saved_referenced_count = 0;
}

Предупреждение PVS-Studio: V3043Логика работы кода не соответствует его форматированию. Оператор имеет отступ вправо, но выполняется всегда. Возможно, фигурные скобки отсутствуют. XamlNameResolver.cs 81

Судя по всему, планировалось удалить из коллекций объекты и ссылочные значения, соответствующие определенному ключу. При этом программист забыл про фигурные скобки, в результате из коллекции ссылка будет удалено только одно значение. Что еще интересно — поставить фигурные скобки здесь будет недостаточно, так как в этом случае при каждой итерации цикла из коллекции referenced будет удаляться объект не по тому ключу, который использовался при удалении из коллекции objects.Это происходит из-за того, что в момент вызова метода Remove для ссылочногоколлекция, коллекция объектов будет изменена, и поэтому метод Last вернет другой элемент.

Появилось больше предупреждений об ошибках, связанных с ошибкой форматирования, не соответствующих логике программы. Вот некоторые из них:

  • V3043 Логика работы кода не соответствует его форматированию. Оператор имеет отступ вправо, но выполняется всегда. Возможно, фигурные скобки отсутствуют. ExpressionParser.cs 92
  • V3043 Логика работы кода не соответствует его форматированию. Оператор имеет отступ вправо, но выполняется всегда. Возможно, фигурные скобки отсутствуют. EcmaUrlParser.cs 80
  • V3043 Логика работы кода не соответствует его форматированию. Оператор имеет отступ вправо, но выполняется всегда. Возможно, фигурные скобки отсутствуют. ILParser.cs 167

Приведение объекта к его типу/проверка совместимости объекта с его типом

За такие ситуации отвечает диагностическое правило V3051. Как правило, он находит лишний код так:

String str;
String str2 = str as String;

or

String str;
if (str is String)

Но иногда мы видим куда более интересные случаи.

Давайте посмотрим на следующий фрагмент кода:

public string GenerateHttpGetMessage (Port port, 
                                      OperationBinding obin, 
                                      Operation oper, 
                                      OperationMessage msg)
{
  ....
  MimeXmlBinding mxb = 
    (MimeXmlBinding) obin.Output
                         .Extensions
                         .Find (typeof(MimeXmlBinding)) 
      as MimeXmlBinding;
  if (mxb == null) return req;
  ....
}

Предупреждение PVS-Studio: V3051Чрезмерное приведение типов. Объект уже имеет тип MimeXmlBinding. SampleGenerator.cs 232

Может показаться, что в лишнем кастинге нет ничего плохого. Чуть ниже мы видим, что mxb сверяется с null, так что если тип не совместим — ничего страшного. Но ничего подобного. Метод Find возвращает экземпляр типа Object, после чего он явно приводится к типу MimeXmlBinding, и только после приведения к того же типа с помощью оператора as. Однако оператор явного приведения, если аргумент имеет значение null или имеет несовместимый тип, не возвращает null (в отличие от оператора as), и создает исключение типа InvalidCastException. В конце концов, проверка mxb == null не поможет, если типы приводятся неправильно.

Остальные предупреждения не столь интересны (например, чрезмерное литье), поэтому приведу их список в текстовом файле.

Перед использованием параметр перезаписывается в тело метода

Сам факт того, что параметр метода немедленно перезаписывается, выглядит подозрительно. Потому что полученное методом значение никак не используется, а теряется/игнорируется. В идеале мы должны исправить сигнатуру метода и сделать параметр локальной переменной. Правда в том, что такой подход не всегда возможен. Например, когда вы реализуете интерфейс или виртуальные функции.

internal static int SetErrorInfo (int dwReserved, 
                                  IErrorInfo errorInfo)
{
  int retVal = 0;
  errorInfo = null;
  ....
  retVal = _SetErrorInfo (dwReserved, errorInfo);
  ....
}

Предупреждение PVS-Studio:параметр V3061 errorInfo всегда перезаписывается в теле метода перед использованием. corlib-net_4_x Marshal.cs 1552

Значение, полученное как параметр errorInfo, никак не используется. Параметр немедленно обнуляется, а затем передается методу. В этом случае было бы логично сделать errorInfo локальной переменной (если есть возможность изменить сигнатуру метода).

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

Неверная инициализация статических членов

class ResXResourceWriter : IResourceWriter, IDisposable
{
  ....
  public static readonly string ResourceSchema = schema;
  ....
  static string schema = ....;
  ....
}

Предупреждение PVS-Studio: V3070При инициализации переменной ResourceSchema используется неинициализированная переменная schema. ResXResourceWriter.cs 59
Программист хотел установить значение общедоступного статического поля ResourceSchema, доступного только для чтения, равным другому статическому полю — schema. Здесь мы не могли обойтись без ошибки. В момент инициализации ResourceSchema поле schema будет инициализировано значением по умолчанию (в данном случае — null). Вряд ли разработчик имел в виду именно это.

Ошибочная инициализация статического поля, украшенного атрибутом [ThreadStatic]

Это редкий и интересный баг. Давайте посмотрим на следующий фрагмент кода:

static class Profiler
{
  [ThreadStatic]
  private static Stopwatch timer = new Stopwatch();
  ....
}

Предупреждение PVS-Studio:V3089 Инициализатор поля, помеченного атрибутом [ThreadStatic], будет вызван один раз в первом доступном потоке. Поле будет иметь значение по умолчанию в разных потоках. System.Data.Linq-net_4_x Profiler.cs 16

Оформление поля атрибутом [ThreadStatic] означает, что в каждом потоке значение этого поля будет уникальным. Выглядит просто. Но дело в том, что такие поля нельзя инициализировать ни при объявлении, ни в статическом конструкторе. Это отличный способ выстрелить себе в ногу (или даже в обе ноги) и получить ошибку, которую будет очень сложно поймать.

На самом деле, если инициализация выполняется во время объявления, файлы будут инициализированы значением только первого потока, обратившегося к ним. Для остальных потоков поле будет иметь значение по умолчанию (в данном случае — null, т.к. Секундомер — это ссылочный тип). Статический конструктор также будет вызываться только один раз, при доступе из первого потока. Следовательно, в остальных потоках файл будет инициализирован значением по умолчанию.

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

Пересекающиеся диапазоны

public void EmitLong (long l)
{
  if (l >= int.MinValue && l <= int.MaxValue) {
    EmitIntConstant (unchecked ((int) l));
    ig.Emit (OpCodes.Conv_I8);
  } else if (l >= 0 && l <= uint.MaxValue) {
    EmitIntConstant (unchecked ((int) l));
    ig.Emit (OpCodes.Conv_U8);
  } else {
    ig.Emit (OpCodes.Ldc_I8, l);
  }
}

Предупреждение PVS-Studio:V3092 пересечения диапазонов возможны в условных выражениях. Пример: if (A › 0 && A ‹ 5) { … } else if (A › 3 && A ‹ 9) { … }. mcs-net_4_x codegen.cs 742

Анализатор обнаружил подозрительное пересечение в выражениях:

  • l ›= int.MinValue && l ‹= int.MaxValue
  • l ›= 0 && l ‹= uint.MaxValue

Диапазон [0, Int32.MaxValue] является общим для обоих этих выражений, поэтому, если переменная l имеет значение в этом диапазоне, то первое условие будет истинным, несмотря на то, что второе тоже может быть правдой.

Похожие предупреждения:

  • V3092 Пересечения диапазонов возможны в условных выражениях. Пример: if (A › 0 && A ‹ 5) { … } else if (A › 3 && A ‹ 9) { … }. I18N.CJK-net_4_x CP51932.cs 437
  • V3092 Пересечения диапазонов возможны в условных выражениях. Пример: if (A › 0 && A ‹ 5) { … } else if (A › 3 && A ‹ 9) { … }. I18N.CJK-net_4_x CP932.cs 552
  • V3092 Пересечения диапазонов возможны в условных выражениях. Пример: if (A › 0 && A ‹ 5) { … } else if (A › 3 && A ‹ 9) { … }. I18N.CJK-net_4_x ISO2022JP.cs 460

Доступ к элементу коллекции по постоянному индексу, осуществляемый внутри цикла

Общепринятой практикой является присвоение счетчикам коротких имен. Нет ничего плохого в вызове счетчика цикла i или j — понятно, что это за переменная (за исключением случаев, когда счетчики требуют более осмысленных имен). Но бывают случаи, когда в качестве индекса используется не счетчик цикла, а числовой литерал. Понятно, что это сделано намеренно, но иногда указывает на ошибку. Диагностическое правило V3102 ищет подобные ошибочные фрагменты.

public void ConvertGlobalAttributes (
  TypeContainer member, 
  NamespaceContainer currentNamespace, 
  bool isGlobal)
{
  var member_explicit_targets = member.ValidAttributeTargets;
  for (int i = 0; i < Attrs.Count; ++i) {
    var attr = Attrs[0];
    if (attr.ExplicitTarget == null)
      continue;
    ....
  }
}

Предупреждение PVS-Studio: V3102Подозрительный доступ к элементу объекта Attrs по постоянному индексу внутри цикла. mcs-net_4_x attribute.cs 1272

На каждой итерации цикла переменная attr инициализируется одним и тем же значением — Attrs[0]. Далее она обрабатывается (вызываются свойства, передается в метод). Я сомневаюсь, что программист намеревался работать с одним и тем же значением во всех итерациях цикла, поэтому я предполагаю, что правильная инициализация должна быть такой:

var attr = Attrs[i];

Аналогичные ошибки были еще в двух фрагментах:

  • V3102 Подозрительный доступ к элементу объекта seq по постоянному индексу внутри цикла. System.Xml-net_4_x XmlQueryRuntime.cs 679
  • V3102 Подозрительный доступ к элементу объекта «состояние» по постоянному индексу внутри цикла. System.Web-net_4_x Login.cs 1223

Ненадежные блокировки

Мы часто видим код lock(this) или lock(typeof(….)) в проверяемых проектах. Это не лучший способ блокировки, так как он может привести к взаимоблокировкам. Но давайте пройдемся по этому шаг за шагом. Во-первых, давайте посмотрим на опасный код:

public RegistryKey Ensure (....)
{
  lock (typeof (KeyHandler)){
    ....
  }
}

Предупреждение PVS-Studio: V3090Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект Тип. corlib-net_4_x UnixRegistryApi.cs 245

Распространенной проблемой возможной взаимоблокировки является тот факт, что блокировка выполняется на одном и том же объекте. Поэтому общий совет, который поможет избавиться от этих проблем — использовать в качестве объекта блокировки объект, недоступный извне — локальную переменную или приватное поле класса.

В чем проблема с оператором typeof? Этот оператор всегда возвращает экземпляр типа Type, одинаковый для одного и того же аргумента, следовательно, нарушается правило, описанное выше, и мы получаем проблему блокировки на одном и том же объекте.

Таких фрагментов в коде было несколько:

  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 245
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 261
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 383
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 404
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 451
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 469
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 683
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». corlib-net_4_x UnixRegistryApi.cs 698
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». Система-net_4_x NetworkChange.cs 66
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». Система-net_4_x NetworkChange.cs 74
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». Система-net_4_x NetworkChange.cs 85
  • V3090 Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект «Тип». System-net_4_x NetworkChange.cs 93

Ситуация с методом GetType() мало чем отличается от описанной выше:

void ConfigureHttpChannel (HttpContext context)
{
  lock (GetType())
  {
    ....
  }
}

Предупреждение PVS-Studio: V3090Небезопасная блокировка типа. Все экземпляры типа будут иметь один и тот же объект Тип. System.Runtime.Remoting-net_4_x HttpRemotingHandlerFactory.cs 61

Метод GetType() также возвращает экземпляр типа Type, поэтому, если блокировка реализована где-то с помощью метода GetType() или оператора typeof, для объекта одного типа — у нас будет вероятность взаимоблокировки.

Теперь я хотел бы рассмотреть блокировку на объектах типа String, потому что здесь ситуация становится намного интереснее, и сделать ошибку становится намного проще и сложнее ее обнаружить.

const string Profiles_SettingsPropertyCollection = 
               "Profiles.SettingsPropertyCollection";
....
static void InitProperties ()
{
  ....
  lock (Profiles_SettingsPropertyCollection) {
  if (_properties == null)
    _properties = properties;
  }
}

Предупреждение PVS-Studio: V3090Небезопасная блокировка объекта типа String. System.Web-net_4_x ProfileBase.cs 95

Суть осталась прежней — блокировка на том же объекте, но детали интереснее. Доступ к объектам типа String может быть получен из другого домена приложения (это связано с механизмом интернирования строк). Представьте, каково это отлаживать взаимоблокировку, возникшую из-за того, что в разных доменах приложения программист использовал одну и ту же строку в качестве объекта блокировки. Совет очень короткий — не используйте объекты типа String (и типа Thread тоже). Описание этих и других проблем, связанных с использованием механизма синхронизации, можно найти в документации к диагностическому правилу V3090.

Неверное сравнение

public bool Equals (CounterSample other)
{
  return
    rawValue == other.rawValue &&
    baseValue == other.counterFrequency &&
    counterFrequency == other.counterFrequency &&
    systemFrequency == other.systemFrequency &&
    timeStamp == other.timeStamp &&
    timeStamp100nSec == other.timeStamp100nSec &&
    counterTimeStamp == other.counterTimeStamp &&
    counterType == other.counterType;
}

Предупреждение PVS-Studio:V3112 Аномалия в подобных сравнениях. Возможно, внутри выражения baseValue == other.counterFrequency присутствует опечатка. Система-net_4_x CounterSample.cs 139

Я намеренно не менял форматирование кода, а скопировал исходную версию. Идея метода понятна и проблем с ее пониманием не возникает — программист сравнивает поле текущего объекта и поле объекта, полученного в качестве аргумента. Но даже такой, казалось бы, простой метод содержит ошибку. Я уверен, что если бы форматирование кода было сделано лучше, его было бы легче заметить. Вот тот же фрагмент кода, но уже отформатированный:

public bool Equals (CounterSample other)
{
  return
    rawValue         == other.rawValue         &&
    baseValue        == other.counterFrequency && // <=
    counterFrequency == other.counterFrequency && // <=
    systemFrequency  == other.systemFrequency  &&
    timeStamp        == other.timeStamp        &&
    timeStamp100nSec == other.timeStamp100nSec &&
    counterTimeStamp == other.counterTimeStamp &&
    counterType      == other.counterType;
}

Отформатировав код таким образом, заметить ошибку гораздо проще — видно, что одно и то же поле сравнивается с разными полями текущего объекта. В результате логика нарушена.
На этом фрагменте хорошо видно, что форматирование играет важную роль! Даже если стиль форматирования не имеет значения для компилятора, он важен для программистов, так как помогает избежать тривиальных ошибок и упрощает понимание кода и его логики.

Как это исправить?

Хорошо, мы нашли ошибки. Множество ошибок. Если считать ошибки, которые описаны в статье, и приведенные в файлах, то это более 167 ошибок! Хочу отметить, что это не все ошибочные/подозрительные фрагменты — я просто выбрал их для статьи (что не составило труда). Большинство ошибок оставлено за рамками этой статьи, потому что она и так довольно длинная.

Может возникнуть резонный вопрос — как исправить их все? Как интегрировать статический анализатор в проект?

Вряд ли будет отдельная команда, которая будет только исправлять баги (хотя это может быть наша команда, как это было с Unreal Engine). Правильно будет отмечать все найденные баги и постепенно исправлять их, стараясь не делать новых.

Для упрощения первой задачи предусмотрен механизм подавления массовых предупреждений. Это поможет дифференцировать старые и новые ошибки, отслеживая количество исправленных и вновь созданных ошибок только в новом коде. «Старые» баги исправляются отдельно.

Режим инкрементного анализа предназначен для решения второй проблемы. Этот режим запускает анализ на машине разработчика сразу после компиляции, позволяя обнаруживать свежие ошибки и исправлять их до того, как они попадут в систему контроля версий (СКВ).

Все равно будут ошибки, которые попадут в VCS. Чтобы обнаруживать баги как можно быстрее после того, как они попадут в VCS, будет неплохо внедрить статический анализ в ночные сборки и использовать результаты анализа. Каким путем? Например, можно уведомить ответственного за проект, а также программиста, допустившего попадание этой ошибки в репозиторий.

Такие ошибки важно исправлять без промедления, чтобы они не загромождали лишний код, и задача не усложнялась еще больше.

Использование приведенных выше советов (интеграция статического анализа на сервере сборки и на машинах разработчиков) уменьшит затраты на исправление ошибок, так как при правильной организации процесса они будут исправлены в кратчайшие сроки (до достижения тестировщики и уж точно не попадающие в релизные версии).

Подробнее об интеграции статического анализа в процесс разработки вы можете прочитать в статье Как быстро интегрировать статический анализ в большой проект?.

Вывод

К одной из статей были комментарии: «Вы пишете, что проверяете open-source продукты своим анализатором, а на самом деле вы проверяете свой анализатор!». В этом есть доля правды.

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

Тем не менее, мы проверяем открытые проекты и, самое главное, находим там настоящие баги. Более того, мы их не просто находим, а сообщаем разработчикам и всем, кому это интересно. Как использовать эту информацию дальше, зависит от человека. Я полагаю, сообщество open-source выигрывает от этих проверок. Не так давно у нас произошло знаменательное событие: Мы нашли более 10000 ошибок в различных open source проектах!

В общем, наша главная цель — популяризация методологии статического анализа в целом и анализатора PVS-Studio в частности. Наши статьи — отличный способ показать, как использовать эту методологию.

Попробуйте PVS-Studio на своем проекте: http://www.viva64.com/ru/pvs-studio/

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