对对抗Avalonia UI动物园平台的贡献很小

图2

本文是使用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); } ..... } } 

分析器发现在ItemsRepeaterElementIndexChangedEventArgsUpdate方法中,参数oldIndexnewIndex具有不同的顺序:

 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争取更少平台的贡献

Source: https://habr.com/ru/post/zh-CN481598/


All Articles