В нашей компании возникла потребность использования библиотеки для Blazor компонентов. Мы остановились на MudBlazor и перед внедрением проверили качество её кода. В результате нашли ряд странностей и даже воспроизводящееся падение, о чём и расскажем в статье.
MudBlazor — одна из самых популярных библиотек для приложений под Blazor. Во многом наш выбор пал на неё из-за количества компонентов и красоты их реализации. Её исходный код написан на C#, поэтому перед непосредственным использованием мы решили проанализировать его. Для анализа взяли исходники версии v6.1.7. Дальше речь пойдёт о найденных ошибках.
Сомнительный вызов
Issue 1
public EndSlopeSpline(....):base(....)
{
m = new Matrix(n);
gauss = new MatrixSolver(n, m);
a = new double[n];
b = new double[n];
c = new double[n];
d = new double[n];
h = new double[n];
CalcParameters(firstSlopeDegrees, lastSlopeDegrees);
Integrate(); // <=
Interpolate();
}
Предупреждение PVS-Studio: V3010 The return value of function 'Integrate' is required to be utilized. EndSlopeSpline.cs 26
Чтобы разобраться в сути данного срабатывания, необходимо обратиться к реализации метода Integrate:
public double Integrate()
{
double integral = 0;
for (int i = 0; i < h.Length; i++)
{
double termA = a[i] * h[i];
double termB = b[i] * Math.Pow(h[i], 2) / 2.0;
double termC = c[i] * Math.Pow(h[i], 3) / 3.0;
double termD = d[i] * Math.Pow(h[i], 4) / 4.0;
integral += termA + termB + termC + termD;
}
return integral;
}
В методе не изменяется чьё-либо состояние (например, поля или свойства). Исходя из этого, можно сделать вывод, что единственное назначение этого метода — расчёт с последующим возвращением значения переменной integral. Это делает бессмысленным вызов Integrate без использования возвращаемого значения.
Стоит отметить, что методы CalcParameters и Interpolate (вызванные рядом с Integrate) как раз изменяют состояния ряда свойств и полей.
Issue 2
internal void DateValueChanged(DateTime? value)
{
_valueDate = value;
if (value != null)
{
var date = value.Value.Date;
// get the time component and add it to the date.
if (_valueTime != null)
{
date.Add(_valueTime.Value); // <=
}
_filterDefinition.Value = date;
}
else
_filterDefinition.Value = value;
_dataGrid.GroupItems();
}
Предупреждение PVS-Studio: V3010 The return value of function 'Add' is required to be utilized. Filter.cs 140
По своей сути срабатывание похоже на предыдущее. Здесь не было использовано возвращаемое значение метода Add, вызванного у переменной типа DateTime. Скорее всего, разработчик хотел добавить значение _valueTime.Value к date. Однако он не учёл, что метод Add для объекта типа DateTime возвращает результат добавления, а не изменяет исходный объект. В итоге имеем бесполезный вызов, который по задумке наверняка должен был на что-то влиять.
Неожиданный NullReferenceException
Issue 3
public static bool operator ==(ResizeOptions l, ResizeOptions r)
=> l.Equals(r);
public static bool operator !=(ResizeOptions l, ResizeOptions r)
=> !l.Equals(r);
Предупреждения PVS-Studio:
V3115 Passing 'null' to '==' operator should not result in 'NullReferenceException'. ResizeOptions.cs 34
V3115 Passing 'null' to '!=' operator should not result in 'NullReferenceException'. ResizeOptions.cs 35
При перегрузке операторов равенства и неравенства стоит учитывать возможность того, что один из операндов может иметь значение null. Подобные рекомендации даны в документации Microsoft.
Однако в реализации перегрузки операторов '==' и '!=' левый операнд не проверяется на null. Если значение параметра l – null, то при вызове метода Equals будет выброшено исключение типа NullReferenceException. Аргумент метода Equals, кстати, так же разыменовывается без проверки:
public bool Equals(ResizeOptions other)
{
if (ReportRate != other.ReportRate || ....) //<=
{
return false;
}
....
}
Такое поведение может стать весьма неприятным сюрпризом для пользователя, если класс публичный. Именно таким и является ResizeOptions.
Чтобы убедиться в наличии проблемы, попробуем сравнить объект типа ResizeOptions с null.
Вполне ожидаемо получаем исключение типа NullReferenceException. Если правый операнд – null, поведение будет аналогичным.
Необоснованное присваивание
Issue 4
protected override void OnParametersSet()
{
if (....)
{
....
foreach (....)
{
if (firstTime)
{
....
gridValueY = verticalStartSpace; // <=
}
else
{
....
gridValueY = verticalStartSpace; // <=
}
gridValueY = yValue; // <=
....
}
}
else
{
....
}
....
}
Предупреждение PVS-Studio: V3008 The 'gridValueY' variable is assigned values twice successively. Perhaps this is a mistake. Check lines: 191, 189. Line.razor.cs 191
Рассмотрим вложенный if. В обеих ветвях этого оператора переменной gridValueY присваивается некоторое значение. Стоит отметить, что оно одинаково как в первом, так и во втором случае. Получается, что такое присваивание стоило вообще вынести за if. Однако основная проблема заключается не в этом: значение gridValueY изменяется сразу после if, хотя оно не использовалось.
Блок else оператора if верхнего уровня схож с блоком then:
foreach (....)
{
if (firstTime)
{
....
gridValueY = verticalStartSpace;
}
else
{
....
gridValueY = verticalStartSpace;
}
....
gridValueY = boundHeight - (gridValueY + gridValue); // <=
....
}
В данном случае значение gridValueY тоже перезаписывается сразу после if, однако старое значение при этом используется.
Неправильно расставленные приоритеты
Issue 5
internal async Task<bool> StartResizeColumn(....)
{
....
// In case resize mode is column, we have to find any column right
// of the current one that can also be resized and is not hidden.
var nextResizableColumn = _columns.Skip(_....) + 1)
.FirstOrDefault(c =>
c.Resizable ?? true && !c.Hidden); // <=
....
}
Предупреждение PVS-Studio: V3177 The 'true' literal belongs to the '&&' operator with a higher priority. It is possible the literal was intended to belong to '??' operator instead. DataGridColumnResizeService.cs 52
Рассмотрим выражение c.Resizable ?? true && !c.Hidden. Стоит сразу сказать, что приоритет '??' ниже, чем у '&&'. Исходя из этого, если c.Resizable — null, то оператор null-coalescing вернёт результат операции true && !c.Hidden. Операция "&&" с литералом true бессмысленна. Скорее всего, правильный вариант выглядит так:
(c.Resizable ?? true) && !c.Hidden
Разница этих записей проявит себя, если значения c.Resizable и c.Hidden будут равны true.
Есть ещё один момент, указывающий на то, что была допущена ошибка. Строчкой выше написан комментарий о том, что столбец должен иметь возможность изменения размера (информация об этом содержится в c.Resizable) и не быть скрытым (это можно узнать из c.Hidden). Если c.Resizable — true, то логика работы будет нарушена, так как значение c.Hidden никак не повлияет на результат.
Подводя итог
Как мне кажется, каждое из разобранных срабатываний может указывать на ошибку, меняющую логику работы программы. В одном из случаев это удалось доказать. Перед написанием статьи я создал issue на GitHub. Однако, ответ от разработчиков пока не был получен. Надеюсь, что они рассмотрят перечисленные проблемы и смогут повысить качество кода своего проекта :).
Можно попробовать использованный в статье анализатор, скачав его отсюда. Кстати, недавно в PVS-Studio появилась возможность анализа блоков C# кода в Blazor-компонентах. Некоторые найденные в таких компонентах ошибки описаны в этой статье.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Nikita Panevin. Should we check libraries before using them? MudBlazor helps us find the answer.
Комментарии (8)
uhfath
00.00.0000 00:00А уж какое там количество проблем с асинхронностью... Одни только вызовы асинк методов в сеттерах чего стоят. Из-за этого часто натыкался на то, что тот или иной клик по меню просто не реагирует и не ругается. А всё из-за того, что где-то внутри обработчика по пути вызовов была ошибка логики (у меня), но сам обработчик клика асинхронностью, однако "его никто не ждёт".
Коммитил туда несколько раз, но авторы очень тяжко отвечают.
Greenorine
00.00.0000 00:00Спасибо за проделанную работу, интересно. Я надеюсь, вы создали issue по найденным проблемам?)
NikitaPanevin Автор
00.00.0000 00:00+1Да, ссылка на него есть в заключении статьи. Продублирую её здесь.
Greenorine
00.00.0000 00:00Я читал немного сквозь строки. Когда заметил, мне уже не дало отредактировать комментарий, ибо он первый :d
Спасибо! К слову, я игрался в их сандбоксе за пару часов до выхода вашей статьи, не уверен, я ли его положил (после моих экспериментов, сайт перестал работать), но вот до сих пор не подняли :/
Vanirn
00.00.0000 00:00Интересно как обстоят дела с качеством кода в других библиотах компонентов для Blazor.
uhfath
00.00.0000 00:00Ну на самом деле MudBlazor в этом плане довольно показателен. Туда много людей коммитит и, по сути, это такая сборная солянка проблем. О чём, кстати, в упомянутом issue и сказано. К тому же компоненты эти существуют давно, так что вероятно и легаси подходы ещё остались где-то.
Другие популярные наборы делают уже отдельные компании в качестве free версий своих компонент. Там уже дела чуть получше, т.к. нет такого базара и больше контроля.
Didimus
Не попробовали через чатгбт проверять код?
NikitaPanevin Автор
В ближайшее время должна выйти статья, где мы проверим, насколько хорошо ChatGPT ищет ошибки в коде.