مساهمة صغيرة في المعركة ضد منصات حديقة الحيوان Avalonia UI

الشكل 2

هذه المقالة هي نتيجة فحص مشروع Avalonia UI باستخدام محلل ثابت PVS-Studio. Avalonia UI عبارة عن منصة واجهة مستخدم مفتوحة المصدر تستند إلى XAML. يعد هذا أحد المشروعات المهمة تقنيًا في تاريخ .NET ، حيث يتيح لك إنشاء واجهات مشتركة بين الأنظمة الأساسية استنادًا إلى نظام WPF. آمل أن تساعد هذه المقالة المؤلفين على تصحيح بعض الأخطاء وإقناعهم باستخدام أجهزة التحليل الثابتة في المستقبل.

حول أفالونيا UI


يوفر مشروع Avalonia UI (المعروف سابقًا باسم Perspex) القدرة على إنشاء واجهات المستخدم التي تعمل على Windows و Linux و MacOS. هناك أيضًا دعم تجريبي لنظامي Android و iOS في الوقت الحالي. لا يعد Avalonia UI غلافًا على الأغلفة ، ولكنه يشير إلى واجهة برمجة التطبيقات الأصلية. على عكس Xamarin Forms ، الذي يلتف غلاف Xamarin. في أحد مقاطع الفيديو التجريبية ، أدهشني القدرة على جلب التحكم إلى وحدة تحكم دبيان. بالإضافة إلى ذلك ، يوفر المشروع ميزات أكثر للتصميم والتصميم مقارنةً بمصممي الواجهة التقليدية ، وذلك بفضل استخدام علامة XAML.

تتضمن المشاريع التي تستخدم بالفعل Avalonia UI AvalonStudio (IDE عبر النظام الأساسي لتطوير في C # و C / C ++) و Core2D (محرر المخططات والرسوم البيانية ثنائية الأبعاد). كمشروع تجاري ، يمكنك إحضار Wasabi Wallet ( محفظة Bitcoin).

تعد مكافحة الحاجة إلى وجود مكتبات متعددة مختلفة عند إنشاء تطبيق مشترك بين المنصات ذا أهمية كبيرة. أردنا أن نساعد المشروع ، وقمت بتنزيله وفحصته مع محللنا. آمل أن يهتم المؤلفون بهذه المقالة وإجراء التغييرات اللازمة على الكود ، أو ربما تقديم تحليل ثابت منتظم في عملية التطوير. للقيام بذلك ، يمكنهم الاستفادة من خيار الترخيص المجاني لبرنامج PVS-Studio للمشاريع مفتوحة المصدر. الاستخدام المنتظم للمحلل الثابت يساعد على تجنب العديد من المشاكل ويقلل من تكلفة اكتشاف وإصلاح العديد من الأخطاء.

نتائج التحقق من الصحة


تحذير PVS-Studio: V3001 هناك تعبيرات فرعية متطابقة 'controlledFlags' إلى اليسار وإلى يمين المشغل '^'. WindowImpl.cs 975TwitterClientMessageHandler.cs 52

private void UpdateWMStyles(Action change) { .... var style = (WindowStyles)GetWindowLong(....); .... style = style | controlledFlags ^ controlledFlags; .... } 

سأبدأ رمزياً مع تشخيصات C # الأولى لدينا. اكتشف المحلل استخدامًا غريبًا لمشغل OR bitwise. اسمحوا لي أن أشرح على الأرقام ما يحدث هنا:

التعبير

 1100 0011 | 1111 0000 ^ 1111 0000 

مشابه لهذا:

 1100 0011 | 0000 0000 

يحتوي OR ("^") الحصري على أولوية أعلى من OR ("|"). على الأرجح ، تم تضمين ترتيب مختلف للعمليات هنا. في هذه الحالة ، يجب أن تأخذ التعبير الأول بين قوسين:

 private void UpdateWMStyles(Action change) { .... style = (style | controlledFlags) ^ controlledFlags; .... } 

قبل التحذيرين التاليين يجب أن أعترف: الإيجابيات الخاطئة. هذا بسبب استخدام API العمومي ، طريقة TransformToVisual . في حالتنا ، VisualRoot هو دائمًا الوالد المرئي . لم أفهم هذا عند تحليل الرد ، كما أخبرني مؤلف المشروع بعد كتابة المقال. لذا فإن التعديلات المقترحة في المقالة ليست للحماية من السقوط الفعلي ، بل ضد المراجعة المحتملة التي تخالف هذا المنطق.

PVS-Studio Warning: V3080 dereference null of value value method. النظر في التفتيش: 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; } 

في الواقع ، هناك عودة فارغة .

هذه الطريقة لديها 6 مكالمات. في ثلاث حالات ، يتم التحقق من القيمة ، ويكشف PVS-Studio في البقية عن إمكانية إلغاء التسجيل وإصدار تحذيرات. ذكرت الأول أعلاه ، والتحذيران الآخران هنا:

  • V3080 dereference ممكن. النظر في تفتيش "ع". VisualExtensions.cs 35
  • V3080 dereference ممكن. النظر في تفتيش "controlPoint". المشهد

أقترح إصلاحه عن طريق القياس عن طريق إضافة Nullable <Struct> .HasValue تحقق داخل الأسلوب PointToClient :

 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 Warning: V3080 dereference null of value value method. النظر في التفتيش: 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 إرجاع قيمة خالية كقيمة افتراضية لأنواع المرجع:

 public static IVisual FindCommonVisualAncestor(this IVisual visual, IVisual target) { Contract.Requires<ArgumentNullException>(visual != null); return ....FirstOrDefault(); } 

يتم استخدام طريقة TransformToVisual في تسعة أماكن ؛ وهناك عمليات تدقيق في سبعة. التحذير الأول الذي يتم استخدامه دون التحقق أعلى ، والأخير هنا:

V3080 dereference ممكن. النظر في تفتيش "تحويل". MouseDevice.cs 80

تحذير PVS-Studio: V3022 Expression صحيح دائمًا. ربما يجب استخدام عامل التشغيل "&&" هنا. NavigationDirection.cs 89

 public static bool IsDirectional(this NavigationDirection direction) { return direction > NavigationDirection.Previous || direction <= NavigationDirection.PageDown; } 

الاختيار غريب. في تعداد NavigationDirection ، هناك 9 أنواع و PageDown هو الأخير منها. ربما لم يكن هذا هو الحال دائمًا ، أم أنه تأمين ضد ظهور خيارات الإحالة الجديدة للسودان. يبدو لي أن الفحص الأول يكفي هنا. سأترك القرار لمؤلفي المشروع.

تحذير PVS-Studio: V3066 تم تمرير ترتيب غير صحيح للوسيطات إلى مُنشئ "SelectionChangedEventArgs": "removeSelectedItems" و "AddedSelectedItems". DataGridSelectedItemsCollection.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)); 

المنشئ نفسه SelectChangedEventArgs.

تحذيرات 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 وفي طريقة التحديث ، يكون للوسيطتين 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". DataGridSortDescription.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 . الواجهة IEnumerable ، التي يتم من خلالها توريث الوسيطة seq ، لها أسلوب 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 لم يتم استخدام معلمة مُنشئ "الهواتف". 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 خاصية لكل وسيطة ، ولا يتم تعيين أي قيمة في أي مكان في الهواتف . التحرير واضح ، لن أحضره.

تحذير PVS-Studio: V3080 dereference null. النظر في تفتيش "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 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". 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; } 

الأسلوب يعود دائما صحيح . ربما تم تغيير الغرض من الطريقة منذ كتابة التوقيع ، ولكن على الأرجح هذا خطأ. هذا هو أيضًا فئة التحكم التي تم نسخها من Microsoft ، بناءً على التعليق في بداية الفصل. في رأيي ، DataGrid عادةً ما يكون أحد أكثر عناصر التحكم غير المستقرة ، ويبدو لي أنه يستحق الدراسة ، هل من الضروري تأكيد التمرير إذا لم يكن مستوفياً للشروط؟

استنتاج


تم تقديم بعض هذه الأخطاء في المشروع ليس من قِبل مطوري Avalonia UI أنفسهم ، ولكن بواسطة رمز تم نسخه من عناصر تحكم WPF. ومع ذلك ، بالنسبة لمستخدم الواجهة ، لا يلعب مصدر الخطأ عادةً دورًا. واجهة تعطل أو خلل يفسد رأي البرنامج بأكمله.

في المقالة التي ذكرتها الحاجة إلى تكوين المحلل ، توجد إيجابيات كاذبة لا مفر منها نظرًا لمبادئ تشغيل خوارزميات التحليل الثابت. أي شخص مطلع على مشكلة التوقف يدرك القيود الرياضية عند العمل مع كود آخر. لكن في هذه الحالة ، نتحدث عن تعطيل تشخيص واحد من بين حوالي مائة ونصف. لذلك نحن لا نتحدث عن فقدان المعنى في التحليل الثابت (أو السؤال لا يستحق كل هذا العناء). بالإضافة إلى ذلك ، كان من الممكن أن يكون لهذا التشخيص استجابات جيدة ، من الصعب العثور عليه بين مجموعة من الإيجابيات الخاطئة.

تأكد من ملاحظة الجودة العالية لرمز المشروع! آمل أن يحافظ المطورون على سرعة ومستوى جودة الكود. لسوء الحظ ، كلما كبر المشروع ، زاد عدد الأخطاء فيه. تتمثل إحدى الطرق الممكنة لتقليل الأخطاء في تكوين CI \ CD بشكل صحيح ، من خلال الاتصال بالتحليلات الثابتة والديناميكية. وإذا كنت ترغب في تبسيط العمل مع المشروعات الكبيرة وتقليل مقدار الوقت اللازم للتصحيح ، قم بتنزيل وتجربة PVS-Studio!



إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فالرجاء استخدام الرابط الخاص بالترجمة: Alexander Senichkin. مساهمتنا الصغيرة في نضال أفالونيا واجهة المستخدم من أجل منصات أقل .

Source: https://habr.com/ru/post/ar481598/


All Articles