Для получения триального ключа
заполните форму ниже
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Бесплатная лицензия PVS-Studio для специалистов Microsoft MVP
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

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

Мне интересно попробовать плагин на:
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

>
>
>
Повторная проверка Unity статическим ан…

Повторная проверка Unity статическим анализатором PVS-Studio

08 Апр 2022

Unity – один из самых популярных игровых движков. С его помощью создаётся множество отличных межплатформенных проектов. С нашей последней проверки его исходного кода прошло почти 4 года. Пришло время узнать, что удастся найти интересного в этот раз.

0932_Unity_ru/image1.png

Введение

Ранее мы уже выпускали статью про проверку Unity. Думаю, вам будет интересно ознакомиться с ней. Это действительно большой проект, который ежедневно используют тысячи разработчиков. Не стоит забывать и про пользователей, которые проводят своё время в играх, разрабатываемых с помощью Unity. Я считаю, что подобного масштаба проекты не стоит оставлять надолго без внимания, ведь ошибки в них могут отразиться на большом количестве людей.

Анализировать будем исходный код движка Unity и редактора версии 2022.1.0b8. Давайте перейдём непосредственно к результатам проверки.

Результаты проверки

Issue 1

private void Draw(Rect windowRect)
{
  var rect = new Rect(....);
  ....
  if (m_NumFilteredVariants > 0)
  {
    ....        
    if (m_NumFilteredVariants > maxFilteredLength)
    {
      GUI.Label(....);
      rect.y += rect.height;
    }
  }
  else
  {
    GUI.Label(rect, "No variants with these keywords");
    rect.y += rect.height;                               // <=
  }

  rect.y = windowRect.height - kMargin - kSpaceHeight – 
    EditorGUI.kSingleLineHeight;                         // <=
  ....
}

V3008 The 'rect.y' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 370, 366. ShaderVariantCollectionInspector.cs 370

Анализатор сообщает о том, что одной и той же переменной rect.y дважды подряд присваивается значение, и при этом между присваиваниями эта переменная не используется. Если присмотреться, то можно увидеть, что сформированное для этой переменной значение чуть выше под условием m_NumFilteredVariants > maxFilteredLength тоже будет теряться.

Получается, что все изменения значения переменной, кроме последнего, не имеют смысла.

Issue 2

public static string FetchBuiltinDescription(....)
{
  return string.IsNullOrEmpty(version?.packageInfo?.description) ?
    string.Format(L10n.Tr(....), version.displayName) :
    version.packageInfo.description.Split(....)[0];
}

V3042 Possible NullReferenceException. The '?.' and '.' operators are used for accessing members of the 'version' object UpmPackageDocs.cs 38

Анализатор обнаружил два способа обращения к членам одного объекта. Если значение version будет null, то метод IsNullOrEmpty вернёт true, и при обращении к displayName будет выброшено исключение NullReferenceException.

Issue 3

public void SetScaleFocused(Vector2 focalPoint,
                            Vector2 newScale,
                            bool lockHorizontal,
                            bool lockVertical)
{
  if (uniformScale)
    lockHorizontal = lockVertical = false;
  else
  {
    if (hZoomLockedByDefault)
      lockHorizontal = !lockHorizontal;

    if (hZoomLockedByDefault)
      lockVertical = !lockVertical;
  }
....
}

V3029 The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 930, 933. ZoomableArea.cs 930

Разработчики осуществляют одинаковую проверку два раза подряд. hZoomLockedByDefault является полем класса и если мы посмотрим на место его объявления, то увидим, что рядом есть поле vZoomLockedByDefault.

internal class ZoomableArea
{
  ....
  // Zoom lock settings
  public bool hZoomLockedByDefault = false;
  public bool vZoomLockedByDefault = false;
  ....
}

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

Issue 4

private void UpdateTextFieldVisibility()
{
  if (showInputField)
  {
    ....
  }
  else if (inputTextField != null && inputTextField.panel != null)
  {
    if (inputTextField.panel != null)                         // <=
      inputTextField.RemoveFromHierarchy();

    inputTextField.UnregisterValueChangedCallback(OnTextFieldValueChange);
    inputTextField.UnregisterCallback<FocusOutEvent>(OnTextFieldFocusOut);
    inputTextField = null;
  }
}

V3022 Expression 'inputTextField.panel != null' is always true. BaseSlider.cs 648

Анализатор сообщает о том, что выражение inputTextField.panel != null всегда истинно.

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

Issue 5

Был найден следующий код:

public enum EventType
{
  ....
  // Mouse button was released.
  MouseUp = 1,
  ....
  // Already processed event.
  Used = 12,
  ....
}
public static void MinMaxScroller(....)
{
  ....
  if (   Event.current.type == EventType.MouseUp 
      && Event.current.type == EventType.Used) 
  {
    scrollControlID = 0;
  }

  ....
}

V3022 Expression is always false. Probably the '||' operator should be used here. EditorGUIExt.cs 141

В данном случае анализатор нашёл выражение, которое всегда ложно. Какое бы значение ни вернуло свойство, одно из сравнений будет ложно.

Возможный вариант исправленного кода:

public static void MinMaxScroller(....)
{
  ....
  if (   Event.current.type == EventType.MouseUp 
      || Event.current.type == EventType.Used) 
  {
    scrollControlID = 0;
  }

  ....
}

Issue 6

private List<T> GetChildrenRecursively(....)
{
  if (result == null)
    result = new List<T>();
  if (m_Children.Any())
  {
    var children = sorted ? (....)m_Children.OrderBy(c => c.key)
                                            .OrderBy(c => c.m_Priority) 
                          : m_Children;
    foreach (var child in children)
      child.GetChildrenRecursively(sorted, result);
  }
  else if (value != null)
    result.Add(value);
  return result;
}

V3078 Original sorting order will be lost after repetitive call to 'OrderBy' method. Use 'ThenBy' method to preserve the original sorting. MenuService.cs 499

Анализатор обнаружил в коде ситуацию, в которой происходит подряд два вызова OrderBy.

Как по мне, так очень интересное предупреждение. Естественно, что два вызова OrderBy не являются паттерном ошибки. Это скорее код, который может привести к ошибке, если неправильно понимать, как он работает. Если программист хотел отсортировать коллекцию сначала по ключу, а потом по приоритету, то в данном случае будет ошибка. Почему?

Давайте разбираться. В данном коде два вызова OrderBy отсортируют коллекцию сначала по приоритету, а после по ключу. Не совсем очевидно, не так ли? Думаю, что вместо второго OrderBy правильнее использовать ThenBy, чтобы сортировка выполнялась не "наоборот". Написание с ThenBy будет лучше читаться и не будет вызывать лишних вопросов. За подробностями предлагаю вам перейти к небольшой заметке.

Кстати, PVS-Studio нашёл ещё одно такое же подозрительное место: V3078 Original sorting order will be lost after repetitive call to 'OrderBy' method. Use 'ThenBy' method to preserve the original sorting. SearchSelector.cs 177

Issue 7

public void IconSectionGUI(NamedBuildTarget namedBuildTarget,....)
{
  ....
  if (platformUsesStandardIcons)
  {
    var selectedDefault = (m_SelectedPlatform < 0);
    // Set default platform variables
    BuildPlatform platform = null;
    namedBuildTarget = NamedBuildTarget.Standalone;
    ....
  }
  ....
}

V3061 Parameter 'namedBuildTarget' is always rewritten in method body before being used. PlayerSettingsIconsEditor.cs 396

Довольно странный фрагмент кода. Первый параметр метода перезаписывается перед тем, как используется. Причём данный параметр используется только внутри условия if (platformUsesStandardIcons). В итоге значение, которое передаётся в метод, всегда теряется.

Issue 8

internal void BeginNamingNewAsset(....)
{
  m_State.m_NewAssetIndexInList = m_LocalAssets.IndexOfNewText(....);
  if (m_State.m_NewAssetIndexInList != -1)
  {
    Frame(instanceID, true, false);
    GetRenameOverlay().BeginRename(newAssetName, instanceID, 0f);
  }
  else
  {
    Debug.LogError("Failed to insert new asset into list");
  }

  Repaint();
}

V3022 Expression 'm_State.m_NewAssetIndexInList != -1' is always true. ObjectListArea.cs 511

Анализатор обнаружил выражение, которое всегда истинно. m_State.m_NewAssetIndexInList присваивается возвращаемое значение метода IndexOfNewText. Давайте взглянем на реализацию этого метода:

public int IndexOfNewText(....)
{
  int idx = 0;
  if (m_ShowNoneItem)
    idx++;

  for (; idx < m_FilteredHierarchy.results.Length; ++idx)
  {
    FilteredHierarchy.FilterResult r = m_FilteredHierarchy.results[idx];
                    
    if (foldersFirst && r.isFolder && !isCreatingNewFolder)
      continue;
                    
    if (foldersFirst && !r.isFolder && isCreatingNewFolder)
      break;
                    
    string propertyPath = AssetDatabase.GetAssetPath(r.instanceID);
    if (EditorUtility.NaturalCompare(....) > 0)
    {
      return idx;
    }
  }
  return idx;
}

Можно заметить, что метод возвращает idx, который всегда больше или равен 0.

Как итог ветвь else никогда не будет выполняться. Возможно, ошибка может быть в методе IndexOfNewText. При его использовании ожидали, что он может вернуть -1.

Issue 9

public static Overlay CreateOverlay(Type type)
{
  ....
  if (overlay == null)
  {
    Debug.LogWarning("Overlay of type {type} can not be instantiated." + ....);
    return null;
  }
  ....
}

V3138 String literal contains potential interpolated expression. Consider inspecting: type. OverlayUtilities.cs 116

PVS-Studio указывает на то, что пропущен символ интерполяции строк. Подобные недочёты часто усложняют поиск проблемы, т. к. полученное сообщение будет содержать неточную информацию.

Issue 10

int GetCurveAtPosition(Vector2 viewPos, out Vector2 closestPointOnCurve)
{
  ....
  for (int i = m_DrawOrder.Count - 1; i >= 0; --i)
  {
    CurveWrapper wrapper = GetCurveWrapperFromID(m_DrawOrder[i]);

    if (wrapper.hidden || wrapper.readOnly || wrapper.curve.length == 0)
      continue;
    ....
  }
}

V3080 Possible null dereference. Consider inspecting 'wrapper'. CurveEditor.cs 1889

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

Метод GetCurveWrapperFromID может возвращать null:

internal CurveWrapper GetCurveWrapperFromID(int curveID)
{
  if (m_AnimationCurves == null)
    return null;

  int index;
  if (curveIDToIndexMap.TryGetValue(curveID, out index))
    return m_AnimationCurves[index];

  return null;
}

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

Issue 11

internal static void MaterialShaderReferences(....)
{
  var material = context.target as Material;
  if (material == null || !material.shader)
    return;

  indexer.AddReference(context.documentIndex, "shader", material.shader);

  if (!indexer.settings.options.properties)
    return;

  var ownerPropertyType = typeof(Shader);
  var shaderName = $"{material.shader.name}/" ?? string.Empty;   // <=
  ....
}

V3022 Expression '$"{material.shader.name}/"' is always not null. The operator '??' is excessive. IndexerExtensions.cs 190

Анализатор сигнализирует о том, что выражение $"{material.shader.name}/" всегда не null. Думаю, что с этим утверждением довольно трудно не согласиться. Следовательно, проверка на null с помощью оператора '??' является бесполезной.

Issue 12

static int CountIntersections(....)
{
  ....
  int hitLength = s_RayCastHits.Length;
  float maxDist = 0;
  if (hitLength > 0)
    maxDist = s_RayCastHits[s_RayCastHits.Length - 1].distance;

  physicsScene.Raycast(....);
  if (s_RayCastHits.Length > 0)
  {
    float len = length - s_RayCastHits[0].distance;
    if (len > maxDist)
    {
      maxDist = len;                                 // <=
    }
  }

  return hitLength + s_RayCastHits.Length;
}

V3137 The 'maxDist' variable is assigned but is not used by the end of the function. TreeAOImporter.cs 142

Анализатор указывает на то, что локальной переменной присваивается значение, но после она нигде не используется. Также можно заметить, что код, начиная с if (s_RayCastHits.Length > 0), не делает ничего осмысленного. Все присваивания в этом фрагменте выполняются локальным переменным, которые никак не влияют на возвращаемое значение.

Issue 13

public override DragAndDropVisualMode DoDrag(....)
{
  var hierarchyTargetItem = targetItem as GameObjectTreeViewItem;

  if (m_CustomDragHandling != null)
  {
    DragAndDropVisualMode dragResult = 
      m_CustomDragHandling(parentItem as GameObjectTreeViewItem,
                           hierarchyTargetItem,
                           ....);
    ....
  }
  DragAndDropVisualMode dragSceneResult =
    DoDragScenes(parentItem as GameObjectTreeViewItem,
                 hierarchyTargetItem,
                 ....);

  if (   targetItem != null 
      && !IsDropTargetUserModifiable(hierarchyTargetItem, dropPos)) // <=
  {
    return DragAndDropVisualMode.Rejected;
  }
  ....
}

V3019 Possibly an incorrect variable is compared to null after type conversion using 'as' keyword. Check variables 'targetItem', 'hierarchyTargetItem'. AssetOrGameObjectTreeViewDragging.cs 153

Анализатор указывает на то, что targetItem приводят к производному типу GameObjectTreeViewItem с использованием оператора as. Однако на равенство с null проверяют не полученную ссылку, а исходную.

В случае если не выйдет преобразовать с помощью as, тогда hierarchyTargetItem будет содержать значение null. И при передаче hierarchyTargetItem в IsDropTargetUserModifiable внутри метода произойдёт выбрасывание всеми любимого NullReferenceException.

Упрощённый код этого метода:

static bool IsDropTargetUserModifiable(GameObjectTreeViewItem targetItem, ....)
{
  if (targetItem.isSceneHeader && !targetItem.scene.isLoaded)
    return false;
  ....
}

Стоит отметить, что hierarchyTargetItem используется ещё раньше в качестве второго аргумента при вызове делегата m_CustomDragHandling и метода DoDragScenes. В первом случае непонятно, на какие методы указывает делегат и, как следствие, возможно ли разыменование нулевой ссылки. Во втором случае в методе DoDragScenes всегда происходит проверка на null, поэтому выбрасывания исключения не произойдёт. Код этого метода можно найти здесь.

Issue 14

static Vector3 ResizeHandlesGUI(....)
{
  ....
  Vector3 scale = Vector3.one; 
  ....
  if (uniformScaling)                                 // <=
  {
    float refScale = (xHandle == 1 ? scale.y : scale.x);
    scale = Vector3.one * refScale;
  }

  if (uniformScaling)                                 // <=
  {
    float refScale = (xHandle == 1 ? scale.y : scale.x);
    scale = Vector3.one * refScale;
  }
  ....
}

V3029 The conditional expressions of the 'if' statements situated alongside each other are identical. Check lines: 722, 728. BuiltinTools.cs 722

Анализатор обратил внимание на подозрительный код, в котором два if с одинаковыми условиями идут друг за другом. Можно было предположить, что второй if – просто лишний код, который ни на что не влияет. Это предположение будет ошибочным, так как при формировании значения refScale используется значение scale. Поэтому второй блок всё же влияет на результат.

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

Заключение

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

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

Вы также можете проверить свой проект анализатором PVS-Studio. Для этого можно взять пробный ключик у нас на сайте.

Популярные статьи по теме
Зачем разработчикам игр на Unity использовать статический анализ?

Дата: 11 Май 2022

Автор: Артём Ровенский

С годами стоимость создания игр стала больше, вырос их масштаб, а следовательно, и их кодовая база. Разработчикам становится всё сложнее уследить за ошибками. А забагованная игра влечёт финансовые и …
Проверка Barotrauma статическим анализатором PVS-Studio

Дата: 31 Мар 2022

Автор: Михаил Евтихевич

Barotrauma – игра, в которой можно поуправлять подлодкой, попрятаться от монстров и даже поиграть на аккордеоне в попытке не пойти ко дну. Посмотрим, как проект, начатый инди-студией Undertow Games и…
Игра с null: проверка MonoGame статическим анализатором PVS-Studio

Дата: 04 Фев 2022

Автор: Вадим Кулешов

Анализатор PVS-Studio уже не раз был использован для анализа кода библиотек, фреймворков и движков для разработки игр. Пришло время добавить к их списку MonoGame – низкоуровневый gamedev-фреймворк, н…
Nintendo Switch: Drop Test исходников эмулятора Ryujinx

Дата: 30 Июн 2021

Автор: Данила Карпов

Такие компании, как Sony, Microsoft и Nintendo, каждое поколение радуют своих потребителей новыми консолями и различными играми на них. Но вот беда, часть игр является эксклюзивами своих платформ, а …
Анализ Unity проектов: The solution file has two projects named "UnityEngine.UI"

Дата: 09 Июн 2021

Автор: Сергей Васильев

При анализе Unity проекта с помощью PVS-Studio может возникнуть ошибка следующего вида: Error was encountered while trying to open solution file '...': The solution file has two projects named "Unity…

Комментарии (0)

Следующие комментарии
Unicorn with delicious cookie
Мы используем куки, чтобы пользоваться сайтом было удобно. Хотите узнать подробнее?
Принять