هذا المقال عبارة عن مراجعة للأخطاء الموجودة في مشروع 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 مزيدًا من إمكانات التصميم والتخطيط مقارنةً ببناة UI الأخرى.
على سبيل المثال لا الحصر ، يتم استخدام Avalonia UI في
AvalonStudio (IDE عبر النظام الأساسي لتطوير برامج C # و C / C ++) و
Core2D (محرر مخطط ثنائي الأبعاد).
يعد Wasabi Wallet (
محفظة bitcoin) مثالًا على البرامج التجارية التي تستخدم Avalonia UI.
من الأهمية بمكان الكفاح ضد ضرورة الاحتفاظ بمجموعة من المكتبات عند إنشاء تطبيق مشترك بين الأنظمة الأساسية. أردنا مساعدة مؤلفي Avalonia UI في ذلك ، لذلك قمت بتنزيل الكود المصدري للمشروع والتحقق منه مع محللنا. آمل أن يروا هذه المقالة وأن يقوموا بالإصلاحات المقترحة وأن يبدأوا في استخدام التحليل الثابت بانتظام كجزء من عملية التطوير الخاصة بهم. يمكن القيام بذلك بسهولة بفضل خيار الترخيص المجاني لـ PVS-Studio المتاح للمطورين ذوي المصادر المفتوحة. يساعد استخدام التحليل الثابت على أساس منتظم في تجنب الكثير من المشكلات وجعل اكتشاف الأخطاء وإصلاحها أرخص بكثير.
نتائج التحليل
رسالة تشخيص PVS-Studio: V3001 هناك تعبيرات فرعية متطابقة "التحكم في
الإشارات " إلى اليسار وإلى يمين المشغل "^". 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 ("^") أعلى من تلك الموجودة في bitwise OR ("|"). ربما لا ينوي المبرمج هذا الطلب. يمكن إصلاح الشفرة عن طريق تضمين التعبير الأول بين قوسين:
private void UpdateWMStyles(Action change) { .... style = (style | controlledFlags) ^ controlledFlags; .... }
بالنسبة للتحذيرين التاليين ، علي أن أعترف: هذه إيجابيات كاذبة. كما ترى ، فإن المطورين يستخدمون واجهة برمجة التطبيقات العامة للأسلوب
TransformToVisual . في هذه الحالة ، يعد
VisualRoot دائمًا عنصرًا أساسيًا لـ
Visual . لم أفهم ذلك عند فحص التحذير ؛ فقط بعد أن أنهيت المقال ، أخبرني أحد مؤلفي المشروع بذلك. لذلك ، تهدف الإصلاحات المقترحة أدناه فعليًا إلى حماية التعليمات البرمجية من التعديلات المحتملة التي تخرق هذا المنطق بدلاً من التعطل الفعلي.
رسالة تشخيص PVS-Studio: V3080 dereference خالية محتملة لقيمة إرجاع الطريقة. النظر في التفتيش: 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; }
في الواقع ، يمكن أن يعود
لاغيا .
تسمى هذه الطريقة ست مرات: ثلاث مرات مع التحقق من القيمة التي تم إرجاعها ، والأخرى الثلاثة بدون فحص ، وبالتالي إطلاق التحذير حول dereference المحتملة. الأول هو واحد أعلاه ، وهنا الاثنان الآخران:
- 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: V3080 dereference خالية محتملة لقيمة إرجاع الطريقة. النظر في التفتيش: 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 تسع مرات ، مع سبعة اختبارات فقط. المكالمة الأولى مع dereference غير آمنة هي المذكورة أعلاه ، وهنا هي الثانية:
V3080 dereference ممكن. النظر في تفتيش "تحويل". MouseDevice.cs 80
رسالة تشخيص PVS-Studio: التعبير
V3022 صحيح دائمًا. ربما يجب استخدام عامل التشغيل "&&" هنا. 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));
إنه نفس مُنشئ
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); } ..... } }
لاحظ المحلل أن الوسائط
oldIndex و
newIndex مكتوبة بترتيب مختلف في كلا الطريقتين
ItemsRepeaterElementIndexChangedEventArgs و
Update :
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 الذي تم تمريره إلى طريقة الاتصال ، وهو بالتأكيد أكبر من الصفر وأقل من واحد. إذا لم يكن الأمر كذلك ، فسيتبع التنفيذ فرعًا مختلفًا. إذا تم استخدام هذه الأساليب للرسوم المتحركة ، فإن هذا الموقف يشبه إلى حد كبير Heisenbug. أوصي بالتحقق من القيمة التي يتم إرجاعها بواسطة
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; } }
يمكن لـ PVS-Studio تتبع مهمة
لاغية حتى من خلال سلسلة من خمسين طريقة ، لكن لا يمكن القول ما إذا كان التنفيذ سيتبع هذا الفرع على الإطلاق. ولا يمكنني ، بالنسبة لهذا الأمر ... فقد الاتصالات بين الأساليب المبسطة والظاهرية ، لذلك أقترح ببساطة كتابة فحص إضافي فقط في حالة:
protected override IControl CreateContainer(object item) { var tabItem = (TabItem)base.CreateContainer(item); if(tabItem == null) return; 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.
لا يدعم محللنا هذه المكتبة ، وبالتالي الكم الهائل من الإيجابيات الخاطئة. هذا يعني أنه يجب إيقاف تشغيل هذا التشخيص عند فحص Avalonia UI. يبدو الأمر محرجًا بعض الشيء ، لكن يجب أن أعترف بأنني من صنع هذا التشخيص ... لكن ، مثل أي أداة أخرى ، يحتاج المحلل الثابت إلى ضبط دقيق.
على سبيل المثال ، نحن نعمل حاليًا على تشخيص اكتشاف تحويلات النوع غير الآمن. وتنتج حوالي ألف ايجابيات كاذبة في مشروع لعبة حيث يتم فحص النوع على جانب المحرك.
رسالة تشخيص 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; }
طريقة إرجاع
صحيح في كل وقت. ربما تم تغيير الغرض منه منذ كتابته لأول مرة ، لكنه يبدو أشبه خطأ. اذا حكمنا من خلال التعليق في بداية الفصل ، هذه فئة تحكم أخرى مستعارة من مايكروسوفت. إذا سألتني ، يعد
DataGrid أحد أقل أدوات التحكم ثباتًا ، لذلك قد لا يكون من الجيد تأكيد التمرير عندما لا يفي بالشروط.
استنتاج
تم استعارة بعض الأخطاء الموضحة أعلاه بالإضافة إلى الشفرة التي تم نسخها من عناصر تحكم WPF ، ولا علاقة لمؤلفي Avalonia UI بها. لكن ذلك لا يحدث فرقًا بالنسبة للمستخدم: تترك واجهة تعطل أو خلل انطباعًا سيئًا للجودة الكلية للبرنامج.
ذكرت ضرورة ضبط المحلل: الإيجابيات الخاطئة لا يمكن تجنبها بسبب مبادئ العمل الكامنة وراء خوارزميات التحليل الثابت. يعرف أولئك الذين يعرفون
مشكلة التوقف أن هناك قيودًا رياضية في معالجة جزء من التعليمات البرمجية مع أداة أخرى. في هذه الحالة ، رغم ذلك ، نتحدث عن تعطيل تشخيص واحد من بين مائة ونصف تقريبًا. لذلك ، لا توجد مشكلة في فقدان المعنى في حالة التحليل الثابت. إضافة إلى ذلك ، يمكن لهذا التشخيص أيضًا أن ينتج تحذيرات تشير إلى أخطاء حقيقية ، لكن سيكون من الصعب ملاحظة تلك الأطنان من الإيجابيات الخاطئة.
يجب أن أذكر الجودة الرائعة لمشروع Avalonia UI! آمل أن يبقيه المطورون بهذه الطريقة. لسوء الحظ ، فإن عدد الأخطاء ينمو حتما جنبا إلى جنب مع حجم البرنامج. يعد التوليف الحكيم لأنظمة CI \ CD ، مدعومًا بالتحليل الثابت والديناميكي ، أحد الطرق للحفاظ على الأخطاء. وإذا كنت ترغب في جعل تطوير المشاريع الكبيرة أسهل وقضاء وقت أقل في تصحيح الأخطاء ، قم
بتنزيل وتجربة PVS-Studio!