При разработке статического анализатора PVS-Studio мы стараемся развивать его в разных направлениях. Так, наша команда работает над плагинами для IDE (Visual Studio, Rider), над улучшением интеграции с CI и так далее. Повышение эффективности анализа проектов в Unity также является одной из наших приоритетных задач. Мы считаем, что статический анализ позволит программистам, использующим этот игровой движок, улучшить качество своего исходного кода и упростить работу над любыми проектами. Поэтому мы хотим повысить популярность PVS-Studio среди компаний, которые разрабатывают под Unity. Одним из первых шагов в реализации этой идеи было написание аннотаций для методов, определенных в движке. Это позволяет разработчику контролировать правильность кода, связанного с вызовами аннотированных методов.

Введение

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

Например, вызов метода GetComponent выглядит несколько странно, если возвращаемое им значение не используется. Мелкая ошибка? Никоим образом не. Конечно, это может быть просто лишний звонок, всеми забытый и брошенный. Или может случиться так, что какое-то важное задание было пропущено. Аннотации могут помочь анализатору найти похожие и многие другие ошибки.

Конечно, мы уже написали много аннотаций для анализатора. Например, аннотируются методы класса из пространства имен System. Кроме того, есть механизм автоматического аннотирования некоторых методов. Подробно об этом можно прочитать здесь. Обратите внимание, что в этой статье больше рассказывается о той части PVS-Studio, которая отвечает за анализ проектов на C++. Однако нет заметной разницы в том, как работают аннотации для C# и C++.

Написание аннотаций для методов Unity

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

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

Сбор информации

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

Проблема в том, что многие из найденных проектов были достаточно малы по размеру исходного кода. Если в таких проектах и ​​есть ошибки, то их количество невелико. Не говоря уже о том, что в них реже можно найти какие-то предупреждения, связанные с методами из Unity. Изредка попадались проекты, в которых почти не использовались (или не использовались вообще) специфичные для Unity классы, хотя они описывались как так или иначе связанные с движком. Такие находки совершенно не подходили для поставленной задачи.

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

Таким образом, было 20 проектов, использующих возможности движка. Для поиска наиболее часто используемых классов была написана утилита на основе Roslyn, которая считает вызовы методов из Unity. Эту программу, кстати, тоже можно назвать статическим анализатором. Ведь если подумать, то он действительно анализирует исходный код, не запуская сам проект.

Написанный «анализатор» позволил найти классы, средняя частота использования которых в найденных проектах была самой высокой:

  • UnityEngine.Vector3
  • UnityEngine.Mathf
  • UnityEngine.Debug
  • UnityEngine.GameObject
  • UnityEngine.Материал
  • UnityEditor.EditorGUILayout
  • UnityEngine.Компонент
  • UnityEngine.Объект
  • UnityEngine.GUILayout
  • UnityEngine.Кватернион
  • и другие.

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

Аннотирование

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

В ходе этих проверок были обнаружены интересные особенности некоторых методов. Например, запуск кода

MeshRenderer renderer = cube.GetComponent<MeshRenderer>();
Material m = renderer.material;
List<int> outNames = null;
m.GetTexturePropertyNameIDs(outNames);

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

MeshRenderer renderer = cube.GetComponent<MeshRenderer>();
Material m = renderer.material;
string keyWord = null;
bool isEnabled = m.IsKeywordEnabled(keyWord);

Эти проблемы актуальны для редактора Unity 2019.3.10f1.

Сбор результатов

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

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

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

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

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

Например, был обнаружен немного странный вызов GetComponent:

void OnEnable()
{
  GameObject uiManager = GameObject.Find("UIRoot");
  if (uiManager)
  {
    uiManager.GetComponent<UIManager>();
  }
}

Предупреждение анализатора: V3010 Необходимо использовать возвращаемое значение функции GetComponent. — ДОПОЛНИТЕЛЬНО В ТЕКУЩЕМ UIEditorWindow.cs 22

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

Вот еще один пример дополнительных предупреждений анализатора:

public void ChangeLocalID(int newID)
{
  if (this.LocalPlayer == null)                          // <=
  {
    this.DebugReturn(
      DebugLevel.WARNING, 
      string.Format(
        ...., 
        this.LocalPlayer, 
        this.CurrentRoom.Players == null,                // <=
        newID  
      )
    );
  }
  if (this.CurrentRoom == null)                          // <=
  {
    this.LocalPlayer.ChangeLocalID(newID);               // <=
    this.LocalPlayer.RoomReference = null;
  }
  else
  {
    // remove old actorId from actor list
    this.CurrentRoom.RemovePlayer(this.LocalPlayer);
    // change to new actor/player ID
    this.LocalPlayer.ChangeLocalID(newID);
    // update the room's list with the new reference
    this.CurrentRoom.StorePlayer(this.LocalPlayer);
  }
}

Предупреждения анализатора:

  • V3095 Объект this.CurrentRoom использовался до того, как он был проверен на нуль. Строки проверки: 1709, 1712. — ДОПОЛНИТЕЛЬНО В ТЕКУЩЕМ LoadBalancingClient.cs 1709
  • V3125 Объект this.LocalPlayer использовался после того, как он был проверен на значение null. Строки проверки: 1715, 1707. — ДОПОЛНИТЕЛЬНО В ТЕКУЩЕМ LoadBalancingClient.cs 1715

Обратите внимание, что PVS-Studio не обращает внимания на передачу LocalPlayer в string.Format, так как это не вызовет ошибки. И код выглядит так, как будто он был написан намеренно.

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

Дело в том, что метод DebugReturn делает несколько вызовов, что теоретически может повлиять на значение свойства CurrentRoom:

public virtual void DebugReturn(DebugLevel level, string message)
{
  #if !SUPPORTED_UNITY
  Debug.WriteLine(message);
  #else
  if (level == DebugLevel.ERROR)
  {
    Debug.LogError(message);
  }
  else if (level == DebugLevel.WARNING)
  {
    Debug.LogWarning(message);
  }
  else if (level == DebugLevel.INFO)
  {
    Debug.Log(message);
  }
  else if (level == DebugLevel.ALL)
  {
    Debug.Log(message);
  }
  #endif
}

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

Аннотации также содержали информацию о том, что методы, вызываемые внутри DebugReturn, не влияют на значения других переменных. Таким образом, использование переменной перед проверкой ее на null может быть сочтено подозрительным.

Вывод

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

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