Поиск Ошибок В C#-Коде Gui-Фреймворка Eto.forms.

Популярность GUI-фреймворков для .

NET постоянно растет — появляются новые, разрабатываются старые.

Мы решили не оставлять эту тему без внимания и посмотреть на подозрительные места, обнаруженные в C#-коде одного из таких проектов — Eto.Forms.

Поиск ошибок в C#-коде GUI-фреймворка Eto.Forms.



Введение

Eto.Forms (или просто Eto) — это одна из платформ графического интерфейса, использующая для разработки C# и XAML. Сам он также написан на C#.

Важной особенностью Eto является то, что он кроссплатформенный: позволяет создавать приложения с графическим интерфейсом для основных настольных операционных систем: Windows, Linux и macOS. Поддержка мобильных платформ Android и iOS находится в разработке.

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

Кроме, конечно, мобильных платформ :) В данной статье использовался анализатор версии 7.17 и Исходники Это.

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



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



Выпуск 1

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

/// <summary> /// .

/// </summary> /// .

/// <returns>True if successful, /// or false if the value could not be parsed // </returns> public static bool TryParse(string value, out DashStyle style) { if (string.IsNullOrEmpty(value)) { style = DashStyles.Solid; return true; } switch (value.ToUpperInvariant()) { case "SOLID": style = DashStyles.Solid; return true; case "DASH": style = DashStyles.Dash; return true; case "DOT": style = DashStyles.Dot; return true; case "DASHDOT": style = DashStyles.DashDot; return true; case "DASHDOTDOT": style = DashStyles.DashDotDot; return true; } var values = value.Split(','); if (values.Length == 0) { style = DashStyles.Solid; return true; } float offset; if (!float.TryParse(values[0], out offset)) throw new ArgumentOutOfRangeException("value", value); float[] dashes = null; if (values.Length > 1) { dashes = new float [values.Length - 1]; for (int i = 0; i < dashes.Length; i++) { float dashValue; if (!float.TryParse(values[i + 1], out dashValue)) throw new ArgumentOutOfRangeException("value", value); dashes[i] = dashValue; } } style = new DashStyle(offset, dashes); return true; }

Предупреждение PVS-Studio: В3009 Странно, что этот метод всегда возвращает одно и то же значение true. Это DashStyle.cs 56 Анализатор предупредил, что метод возвращает только истинный во всех его многочисленных ответвлениях.

Давайте разберемся, что не так с этим кодом.

Начну с того, что обычно методы с префиксом «TryParse» в названии следуют одноименному шаблону и имеют следующие особенности:

  • возвращаться логическое значение ;
  • Доступность вне -параметр;
  • никаких исключений не создается.

Предполагается, что:
  • когда операция успешна, возвращается истинный , А вне -аргумент получает требуемое значение;
  • в противном случае он возвращается ЛОЖЬ , А вне -аргумент получает по умолчанию -значение.

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

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

В этом методе возврат происходит только в том случае, если входные данные изначально верны — в противном случае выдается исключение.

Эта логика противоположна описанной в паттерне Try-Parse — метод ей не соответствует. Это делает префикс «TryParse» опасным для программистов, знакомых с этим шаблоном.

Кстати, у метода есть XML-комментарий: True в случае успеха или false, если значение не удалось проанализировать.

.

К сожалению, оно несет ложную информацию.



Выпуск 2



public static IEnumerable<IPropertyDescriptor> GetProperties(Type type) { if (s_GetPropertiesMethod != null) ((ICollection)s_GetPropertiesMethod.Invoke(null, new object[] { type })) .

OfType<object>() .

Select(r => Get(r)); // <= return type.GetRuntimeProperties().

Select(r => Get(r)); }

Предупреждение PVS-Studio: В3010 Необходимо использовать возвращаемое значение функции Select. Это PropertyDescriptorHelpers.cs 209 Анализатор обнаружил, что возвращаемое значение метода Выбирать не используется.

Выбирать это один из методов расширения типа LINQ. IEnumerable .

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

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

Здесь уже очевидна ошибка неиспользованного результата.

Если посмотреть на код внимательнее, то окажется, что метод Получать , используемый в лямбде, возвращает IPPropertyDescriptor :

public static IPropertyDescriptor Get(object obj) { if (obj is PropertyInfo propertyInfo) return new PropertyInfoDescriptor(propertyInfo); else return PropertyDescriptorDescriptor.Get(obj); }

Это означает, что тип, возвращаемый методом Выбирать коллекции будут IEnumerable .

Это точно тот же тип, что и возвращаемое значение метода.

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

Скорее всего здесь потерялся возвращаться :

public static IEnumerable<IPropertyDescriptor> GetProperties(Type type) { if (s_GetPropertiesMethod != null) return ((ICollection)s_GetPropertiesMethod.Invoke(null, new object[] { type })) .

OfType<object>() .

Select(r => Get(r)); return type.GetRuntimeProperties().

Select(r => Get(r)); }



Выпуск 3



public override string Text { get { return base.Text; } set { var oldText = Text; var newText = value ?? string.Empty; // <= if (newText != oldText) { var args = new TextChangingEventArgs(oldText, newText, false); Callback.OnTextChanging(Widget, args); if (args.Cancel) return; base.Text = value; if (AutoSelectMode == AutoSelectMode.Never) Selection = new Range<int>(value.Length, // <= value.Length - 1); // <= } }

Предупреждение PVS-Studio: В3125 Объект «значение» использовался после того, как было проверено его соответствие нулю.

Проверить строки: 329, 320. Это.

WinForms(net462) TextBoxHandler.cs 329 Анализатор показывает, что ссылка проверена на наличие нулевой , но впоследствии использовался без проверки.

Что произойдет, если ценить будет равен нулевой ? Значение ценить проверено на нулевой с помощью оператора объединения нулей.

Линия новыйтекст получит значение строка.

Пусто .

Если старыйтекст ранее не содержал пустой строки, то управление перейдет к затем - ветвь.

Назначение выполняется внутри ветки нулевой свойство:

base.Text = value;

Это уже выглядит странно.

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

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

Но это не все.

Давайте посмотрим на код дальше.

Несколькими строками ниже есть разыменование ценить :

Selection = new Range<int>(value.Length, // <= value.Length - 1);

И здесь ценить это все еще может быть нулевой .

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



Выпуск 4



protected virtual void OnChanging(BindingChangingEventArgs e) { if (Changing != null) Changing(this, e); }

Предупреждение PVS-Studio: В3083 Возможен небезопасный вызов события «Изменение», NullReferenceException. Рассмотрите возможность присвоения события локальной переменной перед ее вызовом.

Это Binding.cs 80 Анализатор выдал сообщение, что вызов события небезопасен, так как нет гарантии наличия подписчиков.

Несмотря на наличие проверки если (Изменение! = ноль) , количество подписчиков может меняться между проверкой и вызовом.

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

Само мероприятие анонсируется следующим образом:

public event EventHandler<BindingChangingEventArgs> Changing;

Класс, содержащий событие, также является общедоступным:

public abstract partial class Binding

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

Следует использовать метод Вызов и оператор Элвиса, чтобы вызвать событие:

protected virtual void OnChanging(BindingChangingEventArgs e) { Changing?.

Invoke(this, e); }

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

protected virtual void OnChanging(BindingChangingEventArgs e) { EventHandler<BindingChangingEventArgs> safeChanging = Changing; if (safeChanging != null) safeChanging(this, e); }



Выпуск 5



void UpdateColumnSizing(.

) { .

switch (FixedPanel) { case SplitterFixedPanel.Panel1: SetLength(0, new sw.GridLength(1, sw.GridUnitType.Star)); // <= break; case SplitterFixedPanel.Panel2: SetLength(0, new sw.GridLength(1, sw.GridUnitType.Star)); // <= break; case SplitterFixedPanel.None: SetLength(0, new sw.GridLength(1, sw.GridUnitType.Star)); SetLength(2, new sw.GridLength(1, sw.GridUnitType.Star)); break; } .

}

Предупреждение PVS-Studio: В3139 Две или более ветвей дела выполняют одни и те же действия.

Это.

Wpf(net462) SplitterHandler.cs 357 Анализатор обнаружил фрагмент структуры выключатель , где разные случай -ветки содержат один и тот же код. выключатель охватывает 3 элемента перечисления СплиттерФиксированнаяПанель , два из которых названы Панель1 И Панель2 .

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

void SetLength(int panel, sw.GridLength value)

Значение аргумента панель используется как индекс внутри метода SetLength :

Control.ColumnDefinitions[panel] = .



Другая ветка охватывает элемент Никто .

Я предполагаю, что он объединяет код для обеих панелей.

Наверное, использование магических чисел «0» и «2» вполне корректно, поскольку здесь мы работаем со стандартным элементом управления «SplitContainer».

Число «1» соответствует разделителю, не упомянутому здесь.

Возможно, код должен выглядеть так:

void UpdateColumnSizing(.

) { .

switch (FixedPanel) { case SplitterFixedPanel.Panel1: SetLength(0, new sw.GridLength(1, sw.GridUnitType.Star)); break; case SplitterFixedPanel.Panel2: SetLength(2, new sw.GridLength(1, sw.GridUnitType.Star)); break; case SplitterFixedPanel.None: SetLength(0, new sw.GridLength(1, sw.GridUnitType.Star)); SetLength(2, new sw.GridLength(1, sw.GridUnitType.Star)); break; } .

}



Выпуск 6



public Font SelectionFont { get { .

Pango.FontDescription fontDesc = null; .

foreach (var face in family.Faces) { var faceDesc = face.Describe(); if ( faceDesc.Weight == weight && faceDesc.Style == style && faceDesc.Stretch == stretch) { fontDesc = faceDesc; break; } } if (fontDesc == null) fontDesc = family.Faces[0]?.

Describe(); // <= var fontSizeTag = GetTag(FontSizePrefix); fontDesc.Size = fontSizeTag != null // <= ? fontSizeTag.Size : (int)(Font.Size * Pango.Scale.PangoScale); .

} }

Предупреждение PVS-Studio: В3105 Переменная «fontDesc» использовалась после того, как она была присвоена с помощью условного оператора с нулевым значением.

Возможно исключение NullReferenceException. Это.

Gtk3 RichTextAreaHandler.cs 328 Анализатор предупреждает об использовании переменной без проверки, что может быть нулевой , поскольку при присвоении ему значения используется оператор с нулевым условием.

Переменная FontDesc присваивается по декларации нулевой .

Если новое значение не было присвоено в цикле для каждого , то есть еще одна ветвь, где значение присваивается FontDesc .

Но код присваивания использует оператор с нулевым условием (Элвиса):

fontDesc = family.Faces[0]?.

Describe();

Это означает, что если первый элемент массива содержит нулевой , Что FontDesc будет назначен нулевой .

И затем происходит разыменование:

fontDesc.Size = .



Если FontDesc воля нулевой , затем попытка присвоить значение свойству Размер приведет к выдаче исключения NullReferenceException .

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

Если в семья.

Лица[0] будет расположен нулевой , то выброс NullReferenceException произойдет в цикле для каждого .

Здесь происходит разыменование:

foreach (var face in family.Faces) { var faceDesc = face.Describe(); // <= if ( faceDesc.Weight == weight && faceDesc.Style == style && faceDesc.Stretch == stretch) { fontDesc = faceDesc; break; } }



Выпуск 7



public override NSObject GetObjectValue(object dataItem) { float? progress = Widget.Binding.GetValue(dataItem); // <= if (Widget.Binding != null && progress.HasValue) // <= { progress = progress < 0f ? 0f : progress > 1f ? 1f : progress; return new NSNumber((float)progress); } return new NSNumber(float.NaN); }

Предупреждение PVS-Studio: В3095 Объект Widget.Binding использовался до того, как было проверено его соответствие нулю.

Проверить строки: 42, 43. Eto.Mac64 ProgressCellHandler.cs 42 Анализатор показал, что ссылка разыменовывается перед проверкой на наличие нулевой .

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

Проверьте ниже Виджет.Binding != ноль бесполезно.

Вам следует упростить код, используя уже упомянутый в этой статье оператор Элвиса, и изменить условие.

Более правильный код мог бы быть таким:

public override NSObject GetObjectValue(object dataItem) { float? progress = Widget.Binding?.

GetValue(dataItem); if (progress.HasValue) { progress = progress < 0f ? 0f : (progress > 1f ? 1f : progress); return new NSNumber((float)progress); } return new NSNumber(float.NaN); }



Выпуск 8

Я оставляю вам возможность найти ошибку самостоятельно:

public bool Enabled { get { return Control != null ? enabled : Control.Sensitive; } set { if (Control != null) Control.Sensitive = value; else enabled = value; } }

Где она?

Поиск ошибок в C#-коде GUI-фреймворка Eto.Forms.

Ошибка здесь:

get { return Control != null ? enabled : Control.Sensitive; }

Предупреждение PVS-Studio: В3080 Возможное нулевое разыменование.

Рассмотрите возможность проверки «Контроля».

Eto.Gtk3 RadioMenuItemHandler.cs 143 Анализатор сообщает о возможном разыменовании нулевой ссылки.

Проверка бессмысленна и не защищает от NullReferenceException .

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

Если условие ложно, то вычисляется второе выражение.

Когда Контроль воля нулевой , то условие будет ложным и нулевая ссылка будет разыменована - очевидно NullReferenceException .



Выпуск 9



public NSShadow TextHighlightShadow { get { if (textHighlightShadow == null) { textHighlightShadow = new NSShadow(); textHighlightShadow.ShadowColor = NSColor.FromDeviceWhite(0F, 0.5F); textHighlightShadow.ShadowOffset = new CGSize(0F, -1.0F); textHighlightShadow.ShadowBlurRadius = 2F; } return textHighlightShadow; } set { textShadow = value; } }

Предупреждение PVS-Studio: В3140 Средства доступа к свойствам используют разные поля поддержки.

Это.

Mac64 MacImageAndTextCell.cs 162 Анализатор обнаружил, что установщик и получатель свойств используют разные поля.

Установщик использует текстТень , а в геттере – текстВыделениеТень .

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

Вот его объявление:

public class MacImageListItemCell : EtoLabelFieldCell { .

NSShadow textHighlightShadow; }

Поле текстВыделениеТень инициализируется только внутри свойства ТекстВыделитьТень .

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

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

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

public NSShadow TextShadow { get { if (textShadow == null) { textShadow = new NSShadow(); textShadow.ShadowColor = NSColor.FromDeviceWhite(1F, 0.5F); textShadow.ShadowOffset = new CGSize(0F, -1.0F); textShadow.ShadowBlurRadius = 0F; } return textShadow; } set { textShadow = value; } }

Поскольку в установщике ТекстВыделитьТень поле используется текстТень , то при каждом изменении ТекстВыделитьТень изменится и ТекстТень .

Сомнительно, что разработчик решил реализовать именно такое поведение.



Выпуск 10



public static NSImage ToNS(this Image image, int? size = null) { .

if (size != null) { .

var sz = (float)Math.Ceiling(size.Value / mainScale); // <= sz = size.Value; // <= } .

}

Предупреждение PVS-Studio: В3008 Переменной 'sz' присваиваются значения дважды подряд. Возможно, это ошибка.

Проверить строки: 296, 295. Eto.Mac64 MacConversions.cs 296 Анализатор предупредил, что переменной со значением присваивается другое значение, хотя предыдущее никак не использовалось.

Объявление переменной и инициализация выполняются в одной строке.

сз .

И сразу на следующей строке значение сз перезаписывается, что делает расчет значения инициализации бессмысленным.



Выпуск 11



public static IBinding BindingOfType(.

) { .

var ofTypeMethod = bindingType.GetRuntimeMethods() .

FirstOrDefault(.

); return (IBinding)ofTypeMethod.MakeGenericMethod(toType) .

Invoke(.

); }

Предупреждение PVS-Studio: В3146 Возможное нулевое разыменование ofTypeMethod. «FirstOrDefault» может возвращать нулевое значение по умолчанию.

Это BindingExtensionsNonGeneric.cs 21 Анализатор показывает, что метод ФёрстОрдефолт , который используется для инициализации переменной ofTypeMethod , могу вернуться нулевой .

Разыменование ofTypeMethod без проверки может привести к выдаче исключения NullReferenceException .

Если есть гарантия, что элемент будет найден, следует использовать метод Первый :

var ofTypeMethod = bindingType.GetRuntimeMethods() .

First(r => r.Name == "OfType" && r.GetParameters().

Length == 2);

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

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



Заключение

Когда эталонная реализация .

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

Со временем появились кроссплатформенные Mono, Xamarin и, в конечном итоге, .

NET Core. Одним из первых желаний сообщества было портирование фреймворков с графическим интерфейсом из Windows на новые платформы.

Существует множество хороших подобных фреймворков, использующих для разработки C# и XAML: Avalonia UI, Uno Platform и Eto.Forms. И, если вам известен подобный, не упомянутый проект, напишите, пожалуйста, о нем в комментариях.

Даже как-то странно желать конкуренции этим хорошим проектам, но конкуренция – двигатель прогресса.

PVS-Studio может помочь разработчикам этих проектов улучшить их код. Более того, вы можете использовать анализатор в некоммерческих Open Source проектах.

бесплатно .

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

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

Поиск ошибок в C#-коде GUI-фреймворка Eto.Forms. .

Теги: #C++ #.

NET #pvs-studio #GUI #статический анализ #pvsstudio

Вместе с данным постом часто просматривают: