本文是使用PVS-Studio静态分析器检查Avalonia UI项目的结果。 Avalonia UI是一个基于XAML的开源,跨平台用户界面平台。 这是.NET历史上具有重大技术意义的项目之一,因为它使您可以基于WPF系统创建跨平台接口。 我希望本文将帮助作者纠正一些错误,并说服他们将来使用静态分析器。
关于Avalonia UI
Avalonia UI项目(以前称为Perspex)提供了创建在Windows,Linux和MacOS上运行的用户界面的功能。 目前,还为Android和iOS提供了实验性支持。 Avalonia UI不是包装器的包装器,而是指本机API。 与Xamarin Forms不同,后者包装Xamarin包装器。 在其中一个演示视频中,我对将控件带入Debian控制台的能力感到震惊。 此外,由于使用了XAML标记,该项目比传统的界面设计器提供了更多的布局和设计功能。
已经使用
Avalonia UI的项目包括
AvalonStudio (用于C#和C / C ++开发的跨平台IDE)和
Core2D (二维图和图的编辑器)。 作为商业项目,您可以携带
Wasabi钱包 (比特币钱包)。
在创建跨平台应用程序时,与需要多个不同的库的斗争非常重要。 我们想为该项目提供帮助,因此我下载了该项目并使用分析仪对其进行了检查。 我希望作者能够关注本文并对代码进行必要的更改,或者在开发过程中引入常规的静态分析。 为此,他们可以利用PVS-Studio的免费许可选项进行开源项目。 定期使用静态分析器有助于避免许多问题,并降低检测和修复许多错误的成本。
验证结果
PVS-Studio警告: V3001在'^'运算符的左侧和右侧有相同的子表达式'
controlFlags '。 WindowImpl.cs 975TwitterClientMessageHandler.cs 52
private void UpdateWMStyles(Action change) { .... var style = (WindowStyles)GetWindowLong(....); .... style = style | controlledFlags ^ controlledFlags; .... }
我将象征性地开始我们的第一个C#诊断程序。 分析器检测到按位OR运算符的奇怪用法。 让我解释一下数字:
表达
1100 0011 | 1111 0000 ^ 1111 0000
与此类似:
1100 0011 | 0000 0000
异或(“ ^”)的优先级高于按位或(“ |”)。 最有可能在这里暗示了不同的操作顺序。 在这种情况下,应将第一个表达式放在括号中:
private void UpdateWMStyles(Action change) { .... style = (style | controlledFlags) ^ controlledFlags; .... }
在接下来的两个警告之前,我必须承认:误报。 这是由于使用了公共API
TransformToVisual方法。 在我们的案例中,
VisualRoot始终是
visual的父级。 该项目的作者在写完文章后告诉我,我在分析响应时并不理解这一点。 因此,本文中提出的修改并不是为了防止实际跌倒,而是为了避免可能破坏此逻辑的修订。
PVS-Studio警告: V3080方法返回值可能会空引用。 考虑检查:TranslatePoint(...)。 VisualExtensions.cs 23
public static Point PointToClient(this IVisual visual, PixelPoint point) { var rootPoint = visual.VisualRoot.PointToClient(point); return visual.VisualRoot.TranslatePoint(rootPoint, visual).Value; }
一种小方法。 分析器认为取消对TranslatePoint的调用结果的引用是不安全的。 看一下这个方法:
public static Point? TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) { var transform = visual.TransformToVisual(relativeTo); if (transform.HasValue) { return point.Transform(transform.Value); } return null; }
确实,有一个返回
null 。
此方法有6个调用。 在三种情况下,将检查该值,在其余情况下,PVS-Studio会检测到潜在的取消引用并发出警告。 我在上面引用了第一个,另外两个警告在这里:
- V3080可能为空的取消引用。 考虑检查“ p”。 VisualExtensions.cs 35
- V3080可能为空的取消引用。 考虑检查“ controlPoint”。 Scene.cs 176
我建议通过在
PointToClient方法内添加
Nullable <Struct> .HasValue检查来类比地
修复它
: public static Point PointToClient(this IVisual visual, PixelPoint point) { var rootPoint = visual.VisualRoot.PointToClient(point); if (rootPoint.HasValue) return visual.VisualRoot.TranslatePoint(rootPoint, visual).Value; else throw ....; }
PVS-Studio警告: V3080方法返回值可能会空引用。 考虑检查:TransformToVisual(...)。 ViewportManager.cs 381
与前面的示例非常相似的情况:
private void OnEffectiveViewportChanged(TransformedBounds? bounds) { .... var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; .... }
TransformToVisual方法如下所示:
public static Matrix? TransformToVisual(this IVisual from, IVisual to) { var common = from.FindCommonVisualAncestor(to); if (common != null) { .... } return null; }
顺便说一句,
FindCommonVisualAncestor方法确实
可以返回
null作为引用类型的默认值:
public static IVisual FindCommonVisualAncestor(this IVisual visual, IVisual target) { Contract.Requires<ArgumentNullException>(visual != null); return ....FirstOrDefault(); }
TransformToVisual方法在9个地方使用;在7个地方有检查。 无需检查即可使用的第一个警告较高,最后一个在这里:
V3080可能为空的取消引用。 考虑检查“转换”。 MouseDevice.cs 80
PVS-Studio警告: V3022表达式始终为true。 可能应在此处使用“ &&”运算符。 NavigationDirection.cs 89
public static bool IsDirectional(this NavigationDirection direction) { return direction > NavigationDirection.Previous || direction <= NavigationDirection.PageDown; }
奇怪的检查。 在
NavigationDirection枚举中,有9种类型,而
PageDown是它们的最后一种。 也许情况并非总是如此,还是可以防止突然出现新的推荐人选。 在我看来,第一张支票就足够了。 我将把决定权留给项目的作者。
警告PVS-Studio: V3066传递给“ SelectionChangedEventArgs”构造函数的参数的可能错误顺序:“ removedSelectedItems”和“ addedSelectedItems”。 数据网格SelectedItemsCollection.cs 338
internal SelectionChangedEventArgs GetSelectionChangedEventArgs() { .... return new SelectionChangedEventArgs (DataGrid.SelectionChangedEvent, removedSelectedItems, addedSelectedItems) { Source = OwningGrid }; }
在这种情况下,分析器建议将构造函数的第二个和第三个参数混淆。 让我们看一下被调用的构造函数:
public SelectionChangedEventArgs(RoutedEvent routedEvent, IList addedItems, IList removedItems) : base(routedEvent) { AddedItems = addedItems; RemovedItems = removedItems; }
可接受两个类型为
IList的容器,易于混合。 从类开头的注释判断,这是从Microsoft复制并在Avalonia下修改的控制代码中的错误。 但是在我看来,纠正该方法的参数顺序是值得的,至少在错误报告到达时不要在自己中查找可能的错误。
分析仪发现了另外三个类似的错误:
警告PVS-Studio: V3066传递给“ SelectionChangedEventArgs”构造函数的参数的可能错误顺序:“已删除”和“已添加”。 AutoCompleteBox.cs 707
OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
相同的构造函数
SelectionChangedEventArgs。PVS-Studio V3066警告 :
- 传递给“ ItemsRepeaterElementIndexChangedEventArgs”构造函数的参数的可能错误顺序:“ oldIndex”和“ newIndex”。 ItemsRepeater.cs 532
- 传递给“更新”方法的参数的可能错误顺序:“ oldIndex”和“ newIndex”。 ItemsRepeater.cs 536
一种事件调用方法中的两种操作。
internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) { if (ElementIndexChanged != null) { if (_elementIndexChangedArgs == null) { _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); } else { _elementIndexChangedArgs.Update(element, oldIndex, newIndex); } ..... } }
分析器发现在
ItemsRepeaterElementIndexChangedEventArgs和
Update方法中,参数
oldIndex和
newIndex具有不同的顺序:
internal ItemsRepeaterElementIndexChangedEventArgs( IControl element, int newIndex, int oldIndex) { Element = element; NewIndex = newIndex; OldIndex = oldIndex; } internal void Update(IControl element, int newIndex, int oldIndex) { Element = element; NewIndex = newIndex; OldIndex = oldIndex; }
也许代码是由不同的程序员编写的,一方面,发生的事情更重要,另一方面,将会发生的事情:)
与前面的情况一样,您不应立即对其进行编辑,而需要检查是否确实存在错误。
PVS-Studio警告: V3004'then '语句等效于'else'语句。 数据网格排序描述.cs 235
public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq) { if (_descending) { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } else { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } }
ThenBy方法的一个非常有趣的实现。 继承
seq参数的
IEnumerable接口具有
ThenBy方法; 我想暗示它的使用。 像这样:
public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq) { if (_descending) { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } else { return seq.ThenBy(o => GetValue(o), InternalComparer); } }
警告PVS-Studio: V3106可能的负索引值。 “索引”索引的值可能达到-1。 Animator.cs 68
protected T InterpolationHandler(double animationTime, T neutralValue) { .... if (kvCount > 2) { if (animationTime <= 0.0) { .... } else if (animationTime >= 1.0) { .... } else { int index = FindClosestBeforeKeyFrame(animationTime); firstKeyframe = _convertedKeyframes[index]; } .... } .... }
分析器认为
索引的值可以为-1。 该变量是从
FindClosestBeforeKeyFrame方法获得的,请看一下它:
private int FindClosestBeforeKeyFrame(double time) { for (int i = 0; i < _convertedKeyframes.Count; i++) if (_convertedKeyframes[i].Cue.CueValue > time) return i - 1; throw new Exception("Index time is out of keyframe time range."); }
如我们所见,在循环中检查条件,并返回迭代器的先前值。 该条件很难验证,我无法确切说明
CueValue是什么,但是根据描述,它的取值范围是0.0到1.0。 我们可以说一些有关
时间的信息 ,这是调用方法中的
animationTime ,它肯定大于零且小于一。 否则,程序执行将转到其他分支。 如果调用这些方法来渲染动画,则情况看起来像是个不错的浮动错误。 如果在这种情况下需要特殊处理,我将为
FindClosestBeforeKeyFrame的结果添加检查。 或者-如果第一个元素不满足某些其他条件,则将其从循环中删除。 不知道这一切如何工作,我将选择第二个选项作为更正示例:
private int FindClosestBeforeKeyFrame(double time) { for (int i = 1; i < _convertedKeyframes.Count; i++) if (_convertedKeyframes[i].Cue.CueValue > time) return i - 1; throw new Exception("Index time is out of keyframe time range."); }
PVS-Studio警告:未使用
V3117构造函数参数“ phones”。 Country.cs 25
public Country(string name, string region, int population, int area, double density, double coast, double? migration, double? infantMorality, int gdp, double? literacy, double? phones, double? birth, double? death) { Name = name; Region = region; Population = population; Area = area; PopulationDensity = density; CoastLine = coast; NetMigration = migration; InfantMortality = infantMorality; GDP = gdp; LiteracyPercent = literacy; BirthRate = birth; DeathRate = death; }
很好的例子说明了分析仪操作和手动代码检查之间的区别。 十三个构造函数参数,一个未使用。 实际上,Visual Studio还记录了一个未使用的参数,但在警告的第三级(它们通常被禁用)。 在这种情况下,这是一个明显的错误,因为该类的每个参数还具有13个属性,并且在
Phones中的任何位置均未分配任何值。 编辑很明显,我不会提出。
PVS-Studio警告: V3080可能会取消引用。 考虑检查“ tabItem”。 TabItemContainerGenerator.cs 22
protected override IControl CreateContainer(object item) { var tabItem = (TabItem)base.CreateContainer(item); tabItem.ParentTabControl = Owner; .... }
分析器认为取消引用调用
CreateContainer的结果很危险。
看一下这个方法:
protected override IControl CreateContainer(object item) { var container = item as T; if (item == null) { return null; } else if (container != null) { return container; } else { .... return result; } }
即使通过一连串的五十个方法传递值,分析器也可以看到对变量的
空值分配。 但是他不能说执行是否至少会在该线程上进行一次。 是的,而且我通常也做不到...方法调用在重写的和虚拟方法之间丢失。 因此,我建议您额外进行检查以确保安全:
protected override IControl CreateContainer(object item) { var tabItem = (TabItem)base.CreateContainer(item); if(tabItem == null) return null; tabItem.ParentTabControl = Owner; .... }
PVS-Studio警告: V3142检测到无法访问的代码。 可能存在错误。 DevTools.xaml.cs 91
我不会在这里写太多代码来制造阴谋,我会马上说:警告是错误的。 分析器看到了对引发无条件异常的方法的调用。 这是:
public static void Load(object obj) { throw new XamlLoadException($"No precompiled XAML found for {obj.GetType()}, make sure to specify x:Class and include your XAML file as AvaloniaResource"); }
不可能不注意有关调用此方法后的无法访问代码的三十五个警告(!)。 我问了一个项目的开发人员:它是如何工作的? 他们告诉我一种使用
Mono.Cecil库将一种方法的调用替换为另一种方法的调用的方法。 它允许您直接在IL代码中替换呼叫。
分析器不支持该库,因此,我们有许多误报,因此最好在此项目上禁用此诊断。 承认这一点有点令人尴尬,是我进行了诊断……但是,像任何工具一样,需要配置静态分析。
例如,我们目前正在开发有关不安全类型转换的诊断程序。 而且它在游戏项目上提供的操作少于一千个,而在游戏项目中,在引擎端执行打字控制。
PVS-Studio警告: V3009奇怪的是,此方法始终返回一个相同的“ true”值。 DataGridRows.cs 412
internal bool ScrollSlotIntoView(int slot, bool scrolledHorizontally) { if (.....) { .... if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot) { return true; } else if (DisplayData.FirstScrollingSlot == slot && slot != -1) { .... return true; } .... } .... return true; }
该方法始终返回
true 。 自编写签名以来,该方法的目的可能已更改,但是很可能这是一个错误。 这也是从Microsoft复制的控件类,根据类开头的注释判断。 我认为
DataGrid通常是最不稳定的控件之一,在我看来,这是值得考虑的,如果滚动不满足条件,是否需要确认滚动?
结论
其中一些错误不是由Avalonia UI开发人员自己引入的,而是由WPF控件复制的代码引入的。 但是,对于界面用户而言,错误的来源通常不起作用。 崩溃的接口或损坏的接口会破坏整个程序的视线。
在我提到的需要配置分析仪的文章中,由于静态分析算法的操作原理,存在不可避免的误报。 任何熟悉
停止问题的人都知道使用其他代码时的数学限制。 但是在这种情况下,我们正在谈论禁用将近一百零五种诊断程序。 因此,我们不是在谈论静态分析中的意义损失(否则这个问题是不值得的)。 此外,这种诊断本来可以做出很好的反应,但在大量误报中却很难找到。
请务必注意高质量的项目代码! 我希望开发人员能够保持代码质量的步伐和水平。 不幸的是,项目越大,其中的错误越多。 减少错误的可能方法之一是通过静态和动态分析的连接来正确配置CI \ CD。 而且,如果您想简化大型项目的工作并减少调试所需的时间,请
下载并尝试使用 PVS-Studio!

如果您想与讲英语的读者分享本文,请使用翻译链接:Alexander Senichkin。
我们对Avalonia UI争取更少平台的贡献 。