نعود مرة أخرى إلى المشروعات التي قمنا بفحصها مسبقًا باستخدام PVS-Studio ، مما يؤدي إلى أوصافها في مقالات مختلفة. سببين تجعل هذه العودة مثيرة بالنسبة لنا. أولاً ، الفرصة لتقييم تقدم محللنا. ثانياً ، مراقبة تعليقات مؤلفي المشروع على مقالتنا وتقرير الأخطاء ، والتي نوفرها لهم عادة. بالطبع ، يمكن تصحيح الأخطاء دون مشاركتنا. ومع ذلك ، من الجيد دائمًا أن تساعد جهودنا في تحسين المشروع. كان روسلين ليست استثناء. يعود المقال السابق حول التحقق من هذا المشروع إلى 23 ديسمبر 2015. إنه وقت طويل جدًا ، نظرًا للتقدم الذي أحرزه محللنا منذ ذلك الوقت. نظرًا لأن C # الأساسية لمحلل PVS-Studio يعتمد على Roslyn ، فإنه يعطينا اهتمامًا إضافيًا في هذا المشروع. نتيجة لذلك ، نحن حريصون مثل الخردل على جودة رمز هذا المشروع. الآن دعنا نختبره مرة أخرى ونكتشف بعض المشكلات الجديدة والمثيرة للاهتمام (ولكن دعونا نأمل ألا يكون هناك أي شيء مهم) يمكن لـ PVS-Studio العثور عليه.
من المحتمل أن يكون العديد من قرائنا على دراية جيدة بـ Roslyn (أو .NET Compiler Platform). باختصار ، هي عبارة عن مجموعة من برامج التحويل البرمجي مفتوحة المصدر وواجهة برمجة التطبيقات لتحليل التعليمات البرمجية للغات C # و Visual Basic .NET من Microsoft. الكود المصدري للمشروع متاح على
جيثب .
لن أقدم وصفًا تفصيليًا لهذه المنصة وسأوصي بمراجعة المقال بواسطة زميلي سيرجي فاسيلييف "
مقدمة لروزلين واستخدامه في تطوير البرنامج " لجميع القراء المهتمين. من هذه المقالة ، يمكنك معرفة ليس فقط حول ميزات بنية Roslyn ، ولكن كيف نستخدم هذه المنصة بالضبط.
كما ذكرت سابقًا ، لقد مر أكثر من 3 سنوات منذ أن كتب زميلي أندري كاربوف آخر مقالة حول اختبار روزلين "إصدار
السنة الجديدة PVS-Studio 6.00: Scanning Roslyn ". منذ ذلك الحين ، حصل محلل C # PVS-Studio على العديد من الميزات الجديدة. في الواقع ، كانت مقالة أندريه حالة اختبار ، حيث تمت إضافة محلل C # في PVS-Studio. على الرغم من ذلك ، تمكنا من اكتشاف الأخطاء في مشروع Roslyn ، والذي كان بالتأكيد ذا جودة عالية. إذن ما الذي تغير في محلل كود C # في هذه اللحظة والذي سيتيح لنا إجراء تحليل أكثر تعمقا؟
منذ ذلك الحين ، تم تطوير البنية الأساسية والبنية التحتية. أضفنا دعمًا لبرنامج Visual Studio 2017 و Roslyn 2.0 ، وتكاملًا عميقًا مع MSBuild. تصف مقالة زميلي بول إريمييف "
دعم Visual Studio 2017 و Roslyn 2.0 في PVS-Studio: في بعض الأحيان أنه ليس من السهل استخدام حلول جاهزة كما قد يبدو " يصف نهجنا للتكامل مع MSBuild وأسباب هذا القرار.
في الوقت الحالي ، نعمل بنشاط على الانتقال إلى الإصدار Roslyn 3.0 بنفس الطريقة التي دعمنا بها في البداية Visual Studio 2017. يتطلب استخدام مجموعة أدواتنا الخاصة ، المضمنة في توزيع PVS-Studio كـ "كعب روتين" ، وهو MSBuild فارغ ملف exe. على الرغم من حقيقة أن الأمر يبدو وكأنه "عكاز" (MSBuild API غير سهل للغاية لإعادة استخدامه في مشاريع الجهات الخارجية بسبب انخفاض قابلية نقل المكتبات) ، فقد ساعدتنا هذه الطريقة بالفعل في التغلب على تحديثات Roslyn المتعددة بسهولة من حيث Visual Studio 2017. حتى الآن ، كان يساعد (حتى مع بعض التحديات) على المرور عبر تحديث Visual Studio 2019 والحفاظ على توافق وأداء كامل للخلف للأنظمة مع إصدارات MSBuild الأقدم.
خضع المحلل الأساسي أيضًا إلى عدد من التحسينات. واحدة من الميزات الرئيسية هي تحليل interprocedural الكامل مع النظر في قيم أساليب المدخلات والمخرجات ، وتقييم (اعتمادا على هذه المعلمات) قابلية الوصول إلى فروع التنفيذ ونقاط العودة.
نحن في طريقنا لاستكمال مهمة مراقبة المعلمات داخل الأساليب (على سبيل المثال ، dereferences يحتمل أن تكون خطرة) إلى جانب حفظ التعليقات التوضيحية التلقائية الخاصة بهم. بالنسبة للتشخيص الذي يستخدم آلية تدفق البيانات ، سيتيح ذلك مراعاة المواقف الخطيرة التي تحدث عند تمرير معلمة في إحدى الطرق. قبل ذلك ، عند تحليل مثل هذه الأماكن الخطرة ، لم يتم إنشاء تحذير ، حيث لم نتمكن من معرفة جميع قيم المدخلات الممكنة في مثل هذه الطريقة. الآن يمكننا اكتشاف الخطر ، كما هو الحال في جميع أماكن استدعاء هذه الطريقة ، سيتم أخذ معلمات الإدخال هذه في الاعتبار.
ملاحظة: يمكنك قراءة آليات التحليل الأساسية ، مثل تدفق البيانات وغيرها في المقالة "
التقنيات المستخدمة في محلل كود PVS-Studio للعثور على الأخطاء ونقاط الضعف المحتملة ".
يقتصر تحليل Interprocedural في PVS-Studio C # لا من المعلمات الإدخال ، ولا العمق. القيد الوحيد هو الأساليب الافتراضية في الفصول ، مفتوحة للميراث ، وكذلك الدخول في العودية (يتوقف التحليل عندما يتعثر عند استدعاء متكرر للطريقة التي تم تقييمها بالفعل). عند القيام بذلك ، سيتم في النهاية تقييم الطريقة العودية نفسها على افتراض أن القيمة المرجعة للتكرار غير معروفة.
أصبحت ميزة جديدة رائعة أخرى في محلل C # تأخذ في الاعتبار التباين المحتمل لمؤشر لاغٍ. وقبل ذلك ، اشتكى المحلل من وجود استثناء مرجعي محتمل ، مع التأكد من أن القيمة المتغيرة في جميع فروع التنفيذ ستكون فارغة. بالطبع ، كان من الخطأ في بعض الحالات ، وهذا هو السبب في أن تشخيص
V3080 قد سبق أن دعا مرجع فارغ.
الآن يدرك المحلل حقيقة أن المتغير قد يكون فارغًا في أحد فروع التنفيذ (على سبيل المثال ، تحت شرط معين
إذا ). إذا لاحظت الوصول إلى هذا المتغير بدون فحص ، فسيصدر تحذير V3080 ، ولكن بمستوى أقل من اليقين ، مما إذا كان لاغياً في جميع الفروع. جنبا إلى جنب مع تحسين تحليل interprocedural ، مثل هذه الآلية تسمح بالبحث عن الأخطاء التي يصعب اكتشافها. إليك مثال على ذلك - تخيل سلسلة طويلة من مكالمات الطريقة ، وآخرها غير مألوف بالنسبة لك. في ظل ظروف معينة ، يتم إرجاع قيمة فارغة في كتلة
catch ، لكنك لم تقم بالحماية من ذلك ، لأنك ببساطة غير معروف. في هذه الحالة ، يشكو المحلل فقط ، عندما يرى بالضبط مهمة فارغة. في رأينا ، إنه يميز نوعًا ما نهجنا عن ميزة C # 8.0 كمرجع نوع لاغٍ ، والذي ، في الواقع ، يقتصر على إعداد عمليات التحقق من عدم وجود قيمة لكل طريقة. ومع ذلك ، فإننا نقترح البديل - لإجراء عمليات فحص فقط في الأماكن التي يمكن أن تحدث فيها أي قيمة فعلية ، ويمكن الآن للمحلل الخاص بنا البحث عن مثل هذه الحالات.
لذلك ، دعونا لا نؤخر النقطة الرئيسية لفترة طويلة ونذهب إلى اقتحام اللوم - تحليل نتائج فحص روسلين. أولاً ، دعنا نأخذ في الاعتبار الأخطاء التي تم العثور عليها بسبب الميزات الموضحة أعلاه. باختصار ، كان هناك الكثير من التحذيرات لرمز Roslyn هذه المرة. أعتقد أن الأمر مرتبط بحقيقة أن النظام الأساسي يتطور بشكل نشط للغاية (في هذه المرحلة ، يبلغ حجم قاعدة الشفرة حوالي 2،770،000 سطر باستثناء فارغ) ، ولم نقم بتحليل هذا المشروع لفترة طويلة. ومع ذلك ، لا توجد الكثير من الأخطاء الحرجة ، في حين أنها ذات أهمية كبرى لهذه المقالة. كالعادة ، استبعدت الاختبارات من الشيك ، فهناك الكثير منها في روسلين.
سأبدأ بأخطاء V3080 ذات المستوى المتوسط من اليقين ، والتي اكتشف فيها المحلل إمكانية الوصول الممكنة عن طريق مرجع خالي ، ولكن ليس في جميع الحالات الممكنة (فروع الكود).
ممكن dereference فارغة - متوسطةV3080 dereference ممكن. النظر في تفتيش "الحالية". CSharpSyntaxTreeFactoryService.PositionalSyntaxReference.cs 70
private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....))
دعنا نفكر في طريقة
GetNode . يشير المحلل إلى أن الوصول بواسطة مرجع خالي ممكن في حالة الكتلة
أثناء . يتم تعيين المتغير قيمة في نص الكتلة
أثناء ، وهو نتيجة لطريقة
AsNode . في بعض الحالات ، ستكون هذه القيمة
فارغة . مثال جيد للتحليل interprocedural في العمل.
الآن دعونا ننظر في حالة مماثلة ، حيث تم إجراء تحليل interprocedural عبر مكالمات الطريقة اثنين.
V3080 dereference ممكن. النظر في فحص "الدليل". CommonCommandLineParser.cs 911
private IEnumerable<CommandLineSourceFile> ExpandFileNamePattern(string path, string baseDirectory, ....) { string directory = PathUtilities.GetDirectoryName(path); .... var resolvedDirectoryPath = (directory.Length == 0) ?
يحصل متغير
الدليل في
نص أسلوب
ExpandFileNamePattern على القيمة من الأسلوب
GetDirectoryName (سلسلة) . هذا ، بدوره ، إرجاع نتيجة الأسلوب overloaded
GetDirectoryName (سلسلة ، منطقي) الذي يمكن أن تكون قيمتها
فارغة . نظرًا لاستخدام
الدليل المتغير بدون فحص أولي
للإلغاء في النص الأساسي للطريقة
ExpandFileNamePattern - يمكننا إعلان المحلل تصحيحًا حول إصدار التحذير. هذا هو البناء يحتمل أن تكون غير آمنة.
جزء رمز آخر مع خطأ V3080 ، وبشكل أكثر دقة ، مع وجود خطأين ، تم إصداره لسطر واحد من التعليمات البرمجية. لم يكن تحليل interprocedural مطلوبًا هنا.
V3080 dereference ممكن. النظر في فحص "spanStartLocation". TestWorkspace.cs 574
V3080 dereference ممكن. النظر في فحص "spanEndLocationExclusive". TestWorkspace.cs 574
private void MapMarkupSpans(....) { .... foreach (....) { .... foreach (....) { .... int? spanStartLocation = null; int? spanEndLocationExclusive = null; foreach (....) { if (....) { if (spanStartLocation == null && positionInMarkup <= markupSpanStart && ....) { .... spanStartLocation = ....; } if (spanEndLocationExclusive == null && positionInMarkup <= markupSpanEndExclusive && ....) { .... spanEndLocationExclusive = ....; break; } .... } .... } tempMappedMarkupSpans[key]. Add(new TextSpan( spanStartLocation.Value,
المتغيرات
spanStartLocation و
spanEndLocationExclusive من النوع
int nullable ويتم تهيئتها بواسطة
null . علاوة على ذلك ، يمكن تخصيص قيم لها ، ولكن فقط في ظل ظروف معينة. في بعض الحالات ، تظل قيمتها
فارغة . بعد ذلك ، يتم الوصول إلى هذه المتغيرات بالرجوع دون التحقق الأولي من وجود قيمة خالية ، وهو ما يشير إليه المحلل.
يحتوي رمز Roslyn على الكثير من هذه الأخطاء ، أكثر من 100. غالبًا ما يكون نمط هذه الأخطاء هو نفسه. هناك نوع من الطريقة العامة ، والتي من المحتمل أن تكون
خالية . يتم استخدام نتيجة هذه الطريقة في العديد من الأماكن ، وأحيانًا من خلال العشرات من مكالمات الطريقة الوسيطة أو عمليات الفحص الإضافية. من المهم أن نفهم أن هذه الأخطاء ليست قاتلة ، ولكن يمكن أن تؤدي إلى الوصول إليها بالإشارة فارغة. في حين أن اكتشاف مثل هذه الأخطاء هو تحد كبير. لهذا السبب في بعض الحالات ، ينبغي للمرء أن يفكر في إعادة صياغة التعليمات البرمجية ، وفي هذه الحالة إذا تم إرجاع قيمة
خالية ، فإن الطريقة ستلقي استثناء. خلاف ذلك ، يمكنك تأمين التعليمات البرمجية الخاصة بك فقط مع الشيكات العامة التي هي متعبة للغاية وغير موثوق بها في بعض الأحيان. على أي حال ، من الواضح أن كل حالة محددة تتطلب حلاً يعتمد على مواصفات المشروع.
المذكرة. يحدث ذلك ، أنه في مرحلة معينة لا توجد مثل هذه الحالات (بيانات الإدخال) ، عندما تُرجع الطريقة
فارغة ولا يوجد خطأ فعلي. ومع ذلك ، لا يزال هذا الرمز غير موثوق به ، لأن كل شيء يمكن أن يتغير عند تقديم بعض التغييرات في الكود.
لإسقاط موضوع
V3080 ، دعونا نلقي نظرة على الأخطاء الواضحة لمستوى اليقين العالي ، عندما يكون الوصول عن طريق مرجع فارغ هو
الاحتمال المحتمل أو الحتمي.
ممكن dereference فارغة - عاليةV3080 dereference ممكن. النظر في فحص 'collectionType.Type'. AbstractConvertForToForEachCodeRefactoringProvider.cs 137
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { .... var collectionType = semanticModel.GetTypeInfo(....); if (collectionType.Type == null && collectionType.Type.TypeKind == TypeKind.Error) { return; } .... }
بسبب الخطأ المطبعي في الشرط (يتم استخدام
&& بدلاً من العامل
|| ) ، يعمل الرمز بشكل مختلف عن المقصود وسيتم تنفيذ الوصول إلى
collectionType.Type عندما يكون
فارغًا . يجب تصحيح الحالة على النحو التالي:
if (collectionType.Type == null || collectionType.Type.TypeKind == TypeKind.Error) ....
بالمناسبة ، قد تتكشف الأشياء بطريقة أخرى: في الجزء الأول من الحالة ، يكون العاملون
== و
! = مضطربون
. عندها سيبدو الرمز الصحيح كما يلي:
if (collectionType.Type != null && collectionType.Type.TypeKind == TypeKind.Error) ....
هذا الإصدار من الكود أقل منطقية ، لكنه يصحح الخطأ أيضًا. الحل النهائي يكمن في أن يقرر مؤلفو المشروع.
خطأ مماثل آخر.
V3080 dereference ممكن. النظر في تفتيش "العمل". TextViewWindow_InProc.cs 372
private Func<IWpfTextView, Task> GetLightBulbApplicationAction(....) { .... if (action == null) { throw new InvalidOperationException( $"Unable to find FixAll in {fixAllScope.ToString()} code fix for suggested action '{action.DisplayText}'."); } .... }
يتم الخطأ عند إنشاء الرسالة للاستثناء. يتبع ذلك محاولة الوصول إلى خاصية
action.DisplayText عبر متغير
الإجراء ، والذي يُعرف بأنه
لاغٍ .
هنا يأتي آخر خطأ V3080 من المستوى العالي.
V3080 dereference ممكن. النظر في فحص "النوع". ObjectFormatterHelpers.cs 91
private static bool IsApplicableAttribute( TypeInfo type, TypeInfo targetType, string targetTypeName) { return type != null && AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName; }
هذه الطريقة صغيرة جدًا ، لذلك أشير إليها تمامًا. الشرط في كتلة
الإرجاع غير صحيح. في بعض الحالات ، عند الوصول إلى
type.FullName ، قد يحدث استثناء. سأستخدم الأقواس لتوضيح الأمر (لن يغيروا السلوك):
return (type != null && AreEquivalent(targetType, type)) || (targetTypeName != null && type.FullName == targetTypeName);
وفقًا لأسبقية العمليات ، سيعمل الرمز تمامًا مثل هذا. في حالة ما إذا كان متغير
النوع خاليًا ، فسوف نقع في الاختيار الآخر ، حيث سنستخدم مرجع
نوع null ، بعد التحقق من متغير
targetTypeName للإلغاء . قد تكون الشفرة ثابتة ، على سبيل المثال ، كما يلي:
return type != null && (AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName);
أعتقد أنها كافية لمراجعة أخطاء V3080. حان الوقت الآن لرؤية أشياء أخرى مثيرة للاهتمام تمكن محلل PVS-Studio من العثور عليها.
الخطأ المطبعيV3005 يتم تعيين متغير "SourceCodeKind" لنفسه. DynamicFileInfo.cs 17
internal sealed class DynamicFileInfo { .... public DynamicFileInfo( string filePath, SourceCodeKind sourceCodeKind, TextLoader textLoader, IDocumentServiceProvider documentServiceProvider) { FilePath = filePath; SourceCodeKind = SourceCodeKind;
بسبب فشل تسمية المتغيرات ، تم إجراء خطأ مطبعي في مُنشئ فئة
DynamicFileInfo . يتم تعيين الحقل
SourceCodeKind القيمة الخاصة به بدلاً من استخدام المعلمة
sourceCodeKind . لتقليل احتمال حدوث مثل هذه الأخطاء ، نوصي باستخدام بادئة تسطير أسفل السطر لأسماء المعلمات في مثل هذه الحالات. فيما يلي مثال على نسخة مصححة من الكود:
public DynamicFileInfo( string _filePath, SourceCodeKind _sourceCodeKind, TextLoader _textLoader, IDocumentServiceProvider _documentServiceProvider) { FilePath = _filePath; SourceCodeKind = _sourceCodeKind; TextLoader = _textLoader; DocumentServiceProvider = _documentServiceProvider; }
غفلةV3006 تم
تكوين العنصر ولكن لا يتم استخدامه. قد تكون الكلمة الأساسية "رمي" مفقودة: رمي InvalidOperationException (FOO) جديد. ProjectBuildManager.cs 61
~ProjectBuildManager() { if (_batchBuildStarted) { new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } }
تحت شرط معين ، يجب أن يلقي المدمر استثناءً ، لكنه لا يحدث بينما يتم إنشاء كائن الاستثناء ببساطة. الكلمة المفتاحية كانت مفقودة. هنا هو الإصدار الصحيح من الكود:
~ProjectBuildManager() { if (_batchBuildStarted) { throw new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } }
المشكلة مع المدمرات في C # ورمي استثناءات منها موضوع لمناقشة أخرى ، وهو ما يتجاوز نطاق هذه المقالة.
عندما تكون النتيجة ليست مهمةأدت الطرق ، التي تلقت نفس القيمة في جميع الحالات ، إلى إطلاق عدد معين من تحذيرات
V3009 . في بعض الحالات ، قد لا يكون الأمر حرجًا أو لا يتم تحديد قيمة الإرجاع في رمز الاتصال. لقد تخطيت هذه التحذيرات. ولكن يبدو أن بعض مقتطفات الشفرة كانت مشبوهة. هنا واحد منهم:
V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". GoToDefinitionCommandHandler.cs 62
internal bool TryExecuteCommand(....) { .... using (context.OperationContext.AddScope(....)) { if (....) { return true; } } .... return true; }
الأسلوب
TryExecuteCommand بإرجاع شيء ولكن
صحيح . عند القيام بذلك ، في رمز الاتصال ، يتم إرجاع القيمة التي تم إرجاعها في بعض عمليات الفحص.
public bool ExecuteCommand(....) { .... if (caretPos.HasValue && TryExecuteCommand(....)) { .... } .... }
من الصعب أن نقول بالضبط إلى أي مدى مثل هذا السلوك خطير. ولكن إذا لم تكن هناك حاجة للنتيجة ، فربما يجب تغيير نوع القيمة المرتدة للإلغاء ويجب إجراء تعديلات صغيرة في طريقة الاتصال. هذا سيجعل الكود أكثر قابلية للقراءة وآمنة.
تحذيرات محلل مماثلة:
- V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". CommentUncommentSelectionCommandHandler.cs 86
- V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". RenameTrackingTaggerProvider.RenameTrackingCommitter.cs 99
- V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". JsonRpcClient.cs 138
- V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". AbstractFormatEngine.OperationApplier.cs 164
- V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "false". TriviaDataFactory.CodeShapeAnalyzer.cs 254
- V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". ObjectList.cs 173
- V3009 من الغريب أن هذه الطريقة تُرجع دائمًا نفس القيمة "صواب". ObjectList.cs 249
فحص الشيء الخطأV3019 ربما تتم مقارنة متغير غير صحيح
بالقيمة الخالية بعد التحويل باستخدام كلمة "as". تحقق المتغيرات "القيمة" ، "valueToSerialize". RoamingVisualStudioProfileOptionPersister.cs 277
public bool TryPersist(OptionKey optionKey, object value) { .... var valueToSerialize = value as NamingStylePreferences; if (value != null) { value = valueToSerialize.CreateXElement().ToString(); } .... }
يتم تحويل متغير
القيمة إلى نوع
NamingStylePreferences . المشكلة هي في الاختيار الذي يتبع هذا. حتى إذا كان متغير
القيمة غير فارغ ، فإنه لا يضمن نجاح عملية كتابة
الكتابة وأن
valueToSerialize لا يحتوي على قيمة
خالية . ممكن رمي الاستثناء
NullReferenceException . يحتاج الرمز إلى تصحيح كما يلي:
var valueToSerialize = value as NamingStylePreferences; if (valueToSerialize != null) { value = valueToSerialize.CreateXElement().ToString(); }
خطأ آخر مماثل:
V3019 ربما تتم مقارنة متغير غير صحيح بالقيمة الخالية بعد التحويل باستخدام كلمة "as". تحقق من المتغيرات "columnState" ، "columnState2". StreamingFindUsagesPresenter.cs 181
private void SetDefinitionGroupingPriority(....) { .... foreach (var columnState in ....) { var columnState2 = columnState as ColumnState2; if (columnState?.Name ==
يتم
تحويل متغير
columnState إلى النوع
ColumnState2 . ومع ذلك ، لا يتم التحقق من نتيجة العملية ، والتي هي
العمود المتغير
State2 ، لمزيد من الإبهام . بدلاً من ذلك ، يتم فحص متغير
columnState باستخدام عامل التشغيل
الفارغ الشرطي. لماذا هذا الرمز خطير؟ تمامًا كما في المثال السابق ، قد تفشل عملية الإرسال باستخدام عامل التشغيل وسيصبح المتغير
خاليًا مما سيؤدي إلى استثناء. بالمناسبة ، قد يكون الخطأ المطبعي هو السبب هنا. ألقِ نظرة على الحالة في كتلة
if .
ربما ، بدلاً من
columnState؟ .Name أراد المؤلف أن يكتب
columnState2؟ .Name . من المحتمل جدًا ، مع الأخذ في الاعتبار أسماء المتغيرات الخاطئة
columnState و
columnState2.الشيكات الزائدةتم إصدار عدد كبير جدًا من التحذيرات (أكثر من 100) على إنشاءات غير حرجة ، لكن يحتمل أن تكون غير آمنة تتعلق بالتحققات المتكررة. على سبيل المثال ، هذا واحد منهم.
تعبير
V3022 'navInfo == null' غير صحيح دائمًا. AbstractSyncClassViewCommandHandler.cs 101
public bool ExecuteCommand(....) { .... IVsNavInfo navInfo = null; if (symbol != null) { navInfo = libraryService.NavInfoFactory.CreateForSymbol(....); } if (navInfo == null) { navInfo = libraryService.NavInfoFactory.CreateForProject(....); } if (navInfo == null)
قد لا يكون هناك خطأ حقيقي هنا. انها مجرد سبب وجيه لإظهار "تحليل interprocedural + تحليل تدفق البيانات" العمل في السحب. يقترح المحلل أن الاختيار الثاني
navInfo == null لا لزوم له. بالفعل ، قبل ذلك ، سيتم الحصول على القيمة المعينة لـ
navInfo من
libraryService.NavInfoFactory.CreateForProject ، والتي ستقوم ببناء وإرجاع كائن جديد من فئة
NavInfo . بأي حال من الأحوال سوف يعود
فارغة . هنا يطرح السؤال ، لماذا لم يصدر المحلل تحذيراً
للاختبار الأول
navInfo == null ؟ هناك بعض الاسباب أولاً ، إذا كان متغير
الرمز فارغًا ،
فستظل قيمة
navInfo مرجعًا فارغًا أيضًا. ثانياً ، حتى إذا
حصلت navInfo على القيمة من الأسلوب
ibraryService.NavInfoFactory.CreateForSymbol ، يمكن أن تكون هذه القيمة
فارغة أيضًا. وبالتالي ، فإن الاختيار الأول
navInfo == null ضروري بالفعل.
الشيكات غير كافيةالآن الوضع العكسي من ما نوقش أعلاه. تم تشغيل العديد من تحذيرات
V3042 للرمز ، والتي يمكن الوصول إليها عن طريق مرجع فارغة. يمكن حتى واحد أو اثنين من الشيكات الصغيرة إصلاح كل شيء.
دعنا نفكر في جزء آخر من التعليمات البرمجية المهمة ، والذي يحتوي على اثنين من هذه الأخطاء.
V3042 ممكن NullReferenceException. "؟" و "." تستخدم عوامل التشغيل للوصول إلى أعضاء كائن "المتلقي" Binder_Expressions.cs 7770
V3042 ممكن NullReferenceException. "؟" و "." يتم استخدام عوامل التشغيل للوصول إلى أعضاء كائن "المتلقي" Binder_Expressions.cs 7776
private BoundExpression GetReceiverForConditionalBinding( ExpressionSyntax binding, DiagnosticBag diagnostics) { .... BoundExpression receiver = this.ConditionalReceiverExpression; if (receiver?.Syntax !=
قد يكون متغير
المتلقي فارغًا. يعرف مؤلف الكود هذا ، لأنه يستخدم عامل التشغيل
الفارغ الشرطي في
حالة كتلة
if للوصول إلى
المتلقي ؟ .
بناء الجملة . كذلك يتم استخدام متغير
المتلقي دون أي اختبارات للوصول إلى
المتلقي. النوع ،
المتلقي .
سينتاكس والمتلقي .
الأخطاء . يجب تصحيح هذه الأخطاء:
private BoundExpression GetReceiverForConditionalBinding( ExpressionSyntax binding, DiagnosticBag diagnostics) { .... BoundExpression receiver = this.ConditionalReceiverExpression; if (receiver?.Syntax != GetConditionalReceiverSyntax(conditionalAccessNode)) { receiver = BindConditionalAccessReceiver(conditionalAccessNode, diagnostics); } var receiverType = receiver?.Type; if (receiverType?.IsNullableType() == true) { .... } receiver = new BoundConditionalReceiver(receiver?.Syntax, 0, receiverType ?? CreateErrorType(), hasErrors: receiver?.HasErrors) { WasCompilerGenerated = true }; return receiver; }
علينا أيضًا أن نتأكد من أن المُنشئ يدعم الحصول على قيم
فارغة للمعايير الخاصة به أو أننا بحاجة إلى تنفيذ عمليات إعادة بناء إضافية.
أخطاء مماثلة أخرى:
- V3042 ممكن NullReferenceException. "؟" و "." عوامل التشغيل المستخدمة للوصول إلى أعضاء كائن "containType" SyntaxGeneratorExtensions_Negate.cs 240
- V3042 ممكن NullReferenceException. "؟" و "." يتم استخدام عوامل التشغيل للوصول إلى أعضاء كائن 'تعبير' ExpressionSyntaxExtensions.cs 349
- V3042 ممكن NullReferenceException. "؟" و "." يتم استخدام عوامل التشغيل للوصول إلى أعضاء كائن 'تعبير' ExpressionSyntaxExtensions.cs 349
خطأ في الحالةV3057 قد تتلقى الدالة "Substring" القيمة "-1" بينما من المتوقع أن تكون القيمة غير السالبة. تفقد الحجة الثانية. CommonCommandLineParser.cs 109
internal static bool TryParseOption(....) { .... if (colon >= 0) { name = arg.Substring(1, colon - 1); value = arg.Substring(colon + 1); } .... }
في حالة ما إذا كان متغير
النقطتين يساوي 0 ، وهو أمر جيد وفقًا للشرط الوارد في الكود ، فإن طريقة
Substring ستلقي استثناءً. هذا يجب أن يكون ثابتا:
if (colon > 0)
الخطأ المطبعي ممكنV3065 لا يتم استخدام المعلمة 't2' داخل
هيكل الطريقة. CSharpCodeGenerationHelpers.cs 84
private static TypeDeclarationSyntax ReplaceUnterminatedConstructs(....) { .... var updatedToken = lastToken.ReplaceTrivia(lastToken.TrailingTrivia, (t1, t2) => { if (t1.Kind() == SyntaxKind.MultiLineCommentTrivia) { var text = t1.ToString(); .... } else if (t1.Kind() == SyntaxKind.SkippedTokensTrivia) { return ReplaceUnterminatedConstructs(t1); } return t1; }); .... }
يقبل تعبير lambda معلمتين: t1 و t2. ومع ذلك ، يتم استخدام t1 فقط. يبدو الأمر مشبوهًا ، مع الأخذ في الاعتبار مدى سهولة ارتكاب خطأ عند استخدام المتغيرات التي تحمل هذه الأسماء.
غفلةV3083 الاحتجاج غير الآمن للحدث "TagsChanged" ، NullReferenceException ممكن. النظر في تعيين الحدث إلى متغير محلي قبل استدعاء ذلك. PreviewUpdater.Tagger.cs 37
public void OnTextBufferChanged() { if (PreviewUpdater.SpanToShow != default) { if (TagsChanged != null) { var span = _textBuffer.CurrentSnapshot.GetFullSpan(); TagsChanged(this, new SnapshotSpanEventArgs(span));
يتم استدعاء الحدث
TagsChanged بطريقة غير آمنة. بين التحقق من عدم
الصلاحية واستدعاء الحدث ، يجوز لأي شخص إلغاء الاشتراك منه ، ثم يتم طرح استثناء. علاوة على ذلك ، يتم تنفيذ عمليات أخرى في نص كتلة
if مباشرة قبل استدعاء الحدث. اتصلت بهذا الخطأ "الإهمال" ، لأنه يتم التعامل مع هذا الحدث بعناية أكبر في أماكن أخرى ، كما يلي:
private void OnTrackingSpansChanged(bool leafChanged) { var handler = TagsChanged; if (handler != null) { var snapshot = _buffer.CurrentSnapshot; handler(this, new SnapshotSpanEventArgs(snapshot.GetFullSpan())); } }
استخدام متغير
معالج إضافي يمنع المشكلة. في أسلوب
OnTextBufferChanged ، يتعين على المرء إجراء تعديلات من أجل معالجة الحدث بأمان.
نطاقات متقاطعةV3092 التقاطعات المدى ممكنة ضمن التعبيرات الشرطية. مثال: if (A> 0 && A <5) {...} وإلا إذا (A> 3 && A <9) {...}. ILBuilderEmit.cs 677
internal void EmitLongConstant(long value) { if (value >= int.MinValue && value <= int.MaxValue) { .... } else if (value >= uint.MinValue && value <= uint.MaxValue) { .... } else { .... } }
لفهم أفضل ، اسمح لي بإعادة كتابة هذا الرمز ، مع تغيير أسماء الثوابت بقيمها الفعلية:
internal void EmitLongConstant(long value) { if (value >= -2147483648 && value <= 2147483648) { .... } else if (value >= 0 && value <= 4294967295) { .... } else { .... } }
ربما ، لا يوجد خطأ حقيقي ، لكن الحالة تبدو غريبة. سيتم تنفيذ الجزء الثاني (
وإلا إذا كان ) للمجال من 2147483648 + 1 إلى 4294967295.
آخر تحذيران مماثلان:
- V3092 التقاطعات المدى ممكنة ضمن التعبيرات الشرطية. مثال: if (A> 0 && A <5) {...} وإلا إذا (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 109
- V3092 التقاطعات المدى ممكنة ضمن التعبيرات الشرطية. مثال: if (A> 0 && A <5) {...} وإلا إذا (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 66
المزيد عن الشيكات الخالية (أو عدم وجودها)اثنين من الأخطاء
V3095 على التحقق من متغير
لإلغاء الحق بعد استخدامه. الأول غامض ، لننظر في الكود.
V3095 تم استخدام كائن 'displayName' قبل أن يتم التحقق منه مقابل خالية. خطوط التحقق: 498 ، 503. FusionAssemblyIdentity.cs 498
internal static IAssemblyName ToAssemblyNameObject(string displayName) { if (displayName.IndexOf('\0') >= 0) { return null; } Debug.Assert(displayName != null); .... }
من المفترض أن يكون اسم العرض المرجعي فارغًا. لهذا ، تم إجراء التحقق
Debug.Assert . ليس من الواضح لماذا يذهب بعد استخدام سلسلة. يجب أيضًا أخذ ذلك في الاعتبار أنه بالنسبة للتكوينات المختلفة عن Debug ، سيقوم المترجم بإزالة
Debug.Assert على الإطلاق
. هل يعني ذلك أن الحصول على مرجع فارغ ممكن فقط لـ Debug؟ إذا لم يكن الأمر كذلك ، فلماذا قام المؤلف بالتحقق من
السلسلة . على سبيل المثال ،
IsNullOrEmpty (string) . هذا هو السؤال لمؤلفي الكود.
الخطأ التالي هو أكثر وضوحا.
V3095 تم استخدام كائن 'scriptArgsOpt' قبل أن يتم التحقق منه مقابل لاغ. خطوط التحقق: 321 ، 325. CommonCommandLineParser.cs 321
internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt.Add(arg);
أعتقد أن هذا الرمز لا يحتاج إلى أي تفسيرات. اسمحوا لي أن أقدم لكم النسخة الثابتة:
internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt?.Add(arg); continue; } if (scriptArgsOpt != null) { .... } .... } }
في كود Roslyn ، كان هناك 15 أخطاء أخرى مماثلة:
- V3095 تم استخدام كائن "LocalFunctions" قبل أن يتم التحقق منه مقابل لاغٍ. خطوط التحقق: 289 ، 317. ControlFlowGraphBuilder.RegionBuilder.cs 289
- V3095 تم استخدام عنصر 'resolution.OverloadResolutionResult' قبل أن يتم التحقق منه ضد القيمة الخالية. خطوط التحقق: 579 ، 588. Binder_Invocation.cs 579
- V3095 تم استخدام كائن 'resolution.MethodGroup' قبل أن يتم التحقق منه مقابل لاغٍ. خطوط التحقق: 592 ، 621. Binder_Invocation.cs 592
- V3095 تم استخدام كائن "touchedFilesLogger" قبل أن يتم التحقق منه مقابل لاغٍ. خطوط التحقق: 111 ، 126. CSharpCompiler.cs 111
- V3095 تم استخدام كائن 'newExceptionRegionsOpt' قبل أن يتم التحقق منه مقابل لاغٍ. خطوط التحقق: 736 ، 743. AbstractEditAndContinueAnalyzer.cs 736
- V3095 تم استخدام عنصر 'الرمز' قبل أن يتم التحقق منه ضد قيمة خالية. خطوط التحقق: 422 ، 427. AbstractGenerateConstructorService.Editor.cs 422
- V3095 تم استخدام العنصر '_state.BaseTypeOrInterfaceOpt' قبل أن يتم التحقق منه مقابل خالية. خطوط التحقق: 132 ، 140. AbstractGenerateTypeService.GenerateNamedType.cs 132
- V3095 تم استخدام عنصر "العنصر" قبل أن يتم التحقق منه ضد قيمة خالية. خطوط التحقق: 232 ، 233. ProjectUtil.cs 232
- V3095 تم استخدام عنصر "اللغات" قبل أن يتم التحقق منه ضد قيمة خالية. خطوط التحقق: 22 ، 28. ExportCodeCleanupProvider.cs 22
- V3095 تم استخدام كائن 'memberType' قبل أن يتم التحقق منه مقابل خالية. خطوط التحقق: 183 ، 184. SyntaxGeneratorExtensions_CreateGetHashCodeMethod.cs 183
- V3095 تم استخدام كائن 'validTypeDeclarations' قبل أن يتم التحقق منه مقابل خالية. خطوط التحقق: 223 ، 228. SyntaxTreeExtensions.cs 223
- V3095 تم استخدام كائن "النص" قبل أن يتم التحقق منه مقابل لاغٍ. خطوط التحقق: 376 ، 385. MSBuildWorkspace.cs 376
- V3095 تم استخدام عنصر 'nameOrMemberAccessExpression' قبل أن يتم التحقق منه ضد قيمة خالية. خطوط التحقق: 206 ، 223. CSharpGenerateTypeService.cs 206
- V3095 تم استخدام كائن 'simpleName' قبل أن يتم التحقق منه ضد قيمة خالية. خطوط التحقق: 83 ، 85. CSharpGenerateMethodService.cs 83
- V3095 تم استخدام كائن 'option' قبل أن يتم التحقق منه ضد قيمة خالية. خطوط التحقق: 23 ، 28. OptionKey.cs 23
دعونا
نفكر في أخطاء
V3105 . هنا يتم استخدام عامل التشغيل
الفارغ الشرطي عند تهيئة المتغير ، ولكن يتم استخدام المتغير بدون التحقق من وجود قيمة
خالية .
يشير تحذيران إلى الخطأ التالي:
V3105 تم استخدام متغير 'documentId' بعد أن تم تعيينه من خلال معامل null-conditional. NullReferenceException ممكن. CodeLensReferencesService.cs 138
V3105 تم استخدام متغير 'documentId' بعد أن تم تعيينه من خلال معامل null-conditional. NullReferenceException ممكن. CodeLensReferencesService.cs 139
private static async Task<ReferenceLocationDescriptor> GetDescriptorOfEnclosingSymbolAsync(....) { .... var documentId = solution.GetDocument(location.SourceTree)?.Id; return new ReferenceLocationDescriptor( .... documentId.ProjectId.Id, documentId.Id, ....); }
يمكن تهيئة المتغير
documentId بواسطة
null . نتيجة لذلك ، سيؤدي إنشاء كائن
ReferenceLocationDescriptor إلى طرح استثناء. يجب إصلاح الكود:
return new ReferenceLocationDescriptor( .... documentId?.ProjectId.Id, documentId?.Id, ....);
يجب على المطورين أيضًا تغطية احتمال أن تكون المتغيرات ، التي يتم تمريرها إلى المنشئ ،
خالية.أخطاء أخرى مماثلة في الكود:
- V3105 تم استخدام متغير "الرمز" بعد تعيينه من خلال عامل التشغيل الشرطي. NullReferenceException ممكن. SymbolFinder_Hierarchy.cs 44
- V3105 تم استخدام متغير "الرمز" بعد تعيينه من خلال عامل التشغيل الشرطي. NullReferenceException ممكن. SymbolFinder_Hierarchy.cs 51
الأولويات والأقواسV3123 ربما يعمل المشغل '؟: بطريقة مختلفة عما كان متوقعًا. أولويتها أقل من أولوية المشغلين الآخرين في حالتها. Edit.cs 70
public bool Equals(Edit<TNode> other) { return _kind == other._kind && (_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode) && (_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode); }
يتم تقييم الشرط في كتلة الإرجاع وليس كما يقصد المطور. كان من المفترض أن يكون الشرط الأول هو
_kind == other._kin d ، (لهذا السبب بعد هذا الشرط ، هناك فاصل أسطر) ، وبعد ذلك سيتم تقييم كتل الشروط مع المشغل "
؟ " بالتتابع. في الواقع ، الشرط الأول هو
_kind == other._kind && (_oldNode == null) . هذا يرجع إلى حقيقة أن المشغل
&& له أولوية أعلى من المشغل "
؟ ". لإصلاح ذلك ، يجب أن يأخذ المطور جميع تعبيرات المشغل "
؟ " بين قوسين:
return _kind == other._kind && ((_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode)) && ((_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode));
بهذا يخلص وصفي للأخطاء الموجودة.
استنتاجعلى الرغم من العدد الكبير من الأخطاء التي تمكنت من العثور عليها ، من حيث حجم رمز مشروع Roslyn (2،770.000 سطر) ، فإنه ليس كثيرًا. كما كتب أندري في مقال سابق ، أنا مستعد أيضًا للاعتراف بالجودة العالية لهذا المشروع.
أود أن أشير إلى أن عمليات التحقق من الشفرة العرضية هذه لا علاقة لها بمنهجية التحليل الثابت ولا تكاد تكون مفيدة. يجب تطبيق التحليل الثابت بانتظام ، وليس على أساس كل حالة على حدة. بهذه الطريقة ، سيتم تصحيح العديد من الأخطاء في المراحل المبكرة ، وبالتالي ستكون تكلفة إصلاحها أقل بعشر مرات. هذه الفكرة مبينة بمزيد من التفصيل في هذه
المذكرة الصغيرة ، من فضلك ، تحقق منها.
يمكنك التحقق من بعض الأخطاء في كل من هذا المشروع والآخر. للقيام بذلك ، تحتاج فقط إلى
تحميل وتجربة محلل لدينا.