التحقق من شفرة المصدر لروزلين

PVS- ستوديو ضد روزلين

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

ربما تكون Roslyn (أو برنامج .NET Compiler Platform) مألوفة لدى العديد من قرائنا. باختصار ، هي مجموعة من المجمعين المفتوحين وواجهة برمجة التطبيقات (APIs) لتحليل الكود للغات Microsoft C # و Visual Basic .NET. الكود المصدري للمشروع متاح على جيثب .

لن أقدم وصفًا مفصلاً لهذه المنصة ، لكنني أوصي لكل مهتم بمقال زميلي سيرجي فاسيلييف ، " مقدمة لروزلين. استخدام أدوات التحليل الثابتة لتطوير ". من هذه المقالة ، يمكنك التعرف ليس فقط على ميزات بنية Roslyn ، ولكن أيضًا كيف نستخدم هذه المنصة بالضبط.

كما ذكرت سابقًا ، مرت أكثر من ثلاث سنوات منذ كتابة المقال الأخير لزميلي أندريه كاربوف حول اختبار روزلين " إصدار السنة الجديدة من PVS-Studio 6.00: التحقق من روسلين ". خلال هذا الوقت ، اكتسب محلل C # PVS-Studio العديد من الميزات الجديدة. بشكل عام ، كانت مقالة Andrey نوعًا من "كرة الاختبار" ، لأنه تم إضافة محلل C # إلى PVS-Studio فقط. على الرغم من هذا ، حتى في ذلك الوقت ، في مشروع عالي الجودة دون قيد أو شرط ، تمكنت Roslyn من العثور على أخطاء مثيرة للاهتمام. ما الذي تغير في محلل كود C # حتى الآن ، مما قد يسمح بإجراء تحليل أعمق؟

على مر الزمن الماضي ، تم تطوير كلاً من مركز التحليل والبنية التحتية. تمت إضافة الدعم لـ Visual Studio 2017 و Roslyn 2.0 ، بالإضافة إلى التكامل العميق مع MSBuild. يمكنك قراءة المزيد حول مقاربتنا للتكامل مع MSBuild وعن الأسباب التي دفعتنا إلى قبولها في المقالة التي كتبها زميلي Pavel Yeremeyev ، " دعم Visual Studio 2017 و Roslyn 2.0 في PVS-Studio: أحيانًا لا يكون استخدام الحلول الجاهزة أمرًا سهلاً كما يبدو في لمحة ".

نعمل الآن بنشاط على الانتقال إلى Roslyn 3.0 وفقًا لنفس المخطط الذي دعمناه في البداية لـ Visual Studio 2017 ، أي من خلال مجموعة الأدوات الخاصة بنا ، والتي تأتي في مجموعة توزيع PVS-Studio مع "كعب روتين" في شكل ملف MSBuild.exe فارغ. على الرغم من حقيقة أنه يبدو وكأنه "عكاز" (MSBuild API ليست ودية للغاية لإعادة استخدامها في مشاريع patry الثالث بسبب انخفاض قابلية نقل المكتبات) ، فقد ساعدنا هذا النهج بالفعل على استرجاع العديد من تحديثات Roslyn غير المؤلمة أثناء حياة Visual Studio 2017 ، دون أي ألم. والآن ، على الرغم من وجود الكثير من التراكبات ، يمكنك البقاء على قيد الحياة حتى الترقية إلى Visual Studio 2019 ، بالإضافة إلى الحفاظ على التوافق والأداء الكامل للخلف على الأنظمة ذات الإصدارات الأقدم من MSBuild.

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

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

ملاحظة: يمكنك التعرف على آليات التحليل الرئيسية ، مثل تدفق البيانات وغيرها ، من المقالة " التقنيات المستخدمة في محلل كود PVS-Studio لإيجاد الأخطاء ونقاط الضعف المحتملة ".

لا يقتصر تحليل Interprocedural في PVS-Studio C # إما على معلمات الإدخال أو العمق. القيد الوحيد هو الأساليب الافتراضية في الفئات التي لم يتم إغلاقها للميراث والسقوط في التكرار (سنتوقف عندما رأينا في المكدس مكالمة متكررة إلى طريقة محسوبة بالفعل). علاوة على ذلك ، سيتم في النهاية حساب الطريقة العودية نفسها بافتراض أن القيمة المرجعة للتكرار الذاتي غير معروفة.

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

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

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

سأبدأ بأخطاء V3080 ، بمستوى درجة الأهمية المتوسطة ، حيث اكتشف المحلل الوصول المحتمل عبر رابط صفري ، ولكن ليس في جميع الحالات الممكنة (فروع الكود).

ممكن dereference فارغة - متوسطة

V3080 dereference ممكن. النظر في تفتيش "الحالية". CSharpSyntaxTreeFactoryService.PositionalSyntaxReference.cs 70

private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....)) // <= { .... var nodeOrToken = current.ChildThatContainsPosition(....); .... current = nodeOrToken.AsNode(); // <= } .... } public SyntaxNode AsNode() { if (_token != null) { return null; } return _nodeOrParent; } 

النظر في الأسلوب 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) ? // <= baseDirectory : FileUtilities.ResolveRelativePath(directory, baseDirectory); .... } public static string GetDirectoryName(string path) { return GetDirectoryName(path, IsUnixLikePlatform); } internal static string GetDirectoryName(string path, bool isUnixLike) { if (path != null) { .... } return null; } 

يحصل متغير الدليل في نص أسلوب ExpandFileNamePattern على القيمة من أسلوب GetDirectoryName (string) . سيؤدي ذلك بدوره إلى إرجاع نتيجة 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, // <= spanEndLocationExclusive.Value - // <= spanStartLocation.Value)); } } .... } 

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

يحتوي رمز 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; } 

الطريقة صغيرة ، لذلك أعطي رمزها بالكامل. الشرط في كتلة الإرجاع غير صحيح. في بعض الحالات ، من الممكن رمي NullReferenceException عند الوصول إلى type.FullName . أستخدم الأقواس (لن يغيروا السلوك هنا) لتوضيح الموقف:

 return (type != null && AreEquivalent(targetType, type)) || (targetTypeName != null && type.FullName == targetTypeName); 

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

 return type != null && (AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName); 

أعتقد أن هذا هو المكان الذي يمكنك من خلاله إكمال دراسة أخطاء V3080 ومعرفة ما استطاع محلل PVS-Studio المثير للاهتمام العثور عليه في كود Roslyn.

الخطأ المطبعي

V3005 يتم تعيين متغير "SourceCodeKind" لنفسه. DynamicFileInfo.cs 17

 internal sealed class DynamicFileInfo { .... public DynamicFileInfo( string filePath, SourceCodeKind sourceCodeKind, TextLoader textLoader, IDocumentServiceProvider documentServiceProvider) { FilePath = filePath; SourceCodeKind = SourceCodeKind; // <= TextLoader = textLoader; DocumentServiceProvider = documentServiceProvider; } .... } 

بسبب اسم متغير غير ناجح ، تم إجراء خطأ مطبعي في مُنشئ فئة 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 بإرجاع true فقط ، ولا شيء غير صحيح . في الوقت نفسه ، يتم تضمين قيمة الإرجاع في بعض الاختبارات في رمز الاتصال:

 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 == // <= StandardTableColumnDefinitions2.Definition) { newColumns.Add(new ColumnState2( columnState2.Name, // <= ....)); } .... } .... } 

متغير العمودستيت من النوع ColumnState2 . ومع ذلك ، لا يتم التحقق من نتيجة العملية ، العمود المتغير State2 ، لاغية . بدلاً من ذلك ، يتم التحقق من المتغير columnState باستخدام العبارة الفارغة الشرطية. ما هو خطر هذا الرمز؟ كما في المثال السابق ، قد تفشل كتابة casting باستخدام العامل as ، وسيكون متغير columnState2 خاليًا ، مما يؤدي إلى مزيد من الاستثناء. بالمناسبة ، قد يكون السبب هو الخطأ المطبعي. لاحظ الشرط في كتلة if . ربما بدلاً من columnState؟ .Name أرادوا كتابة columnState2؟ .Name . من المحتمل جدًا أن يُعطى كل من هذا الاسم المتغير المؤسف عمودتي STATE و 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) // <= { return true; } .... } public IVsNavInfo CreateForSymbol(....) { .... return null; } public IVsNavInfo CreateForProject(....) { return new NavInfo(....); } 

ربما ليس هناك خطأ حقيقي هنا. مجرد سبب وجيه لإظهار مزيج من التقنيات "تحليل interprocedural + تحليل تدفق البيانات" في العمل. يعتبر المحلل أن الفحص الثاني navInfo == null لا لزوم له. بالفعل ، قبل ذلك ، سيتم الحصول على قيمة تعيين navInfo من أسلوب libraryService.NavInfoFactory.CreateForProject ، الذي سيقوم بإنشاء وإرجاع كائن جديد من فئة NavInfo . ولكن ليس لاغيا بأي شكل من الأشكال. السؤال هو ، لماذا لم يولد المحلل تحذيراً للاختبار الأول navInfo == لاغٍ ؟ هناك تفسير لهذا. أولاً ، إذا تبين أن متغير الرمز فارغ ، فستظل قيمة navInfo مرجعًا فارغًا. ثانياً ، حتى إذا حصلت navInfo على القيمة من أسلوب libraryService.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 != // <= 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; } 

متغير المتلقي يمكن أن تكون فارغة . يعرف مؤلف الكود هذا لأنه يستخدم عامل التشغيل الفارغ الشرطي في حالة الكتلة if للوصول إلى المستقبل؟ .Syntax . كذلك في الكود ، يتم استخدام المتلقي المتغير دون أي اختبارات للوصول إلى المتلقي. النوع ، المتلقي . سينتاكس والمتلقي . هذه الأخطاء تحتاج إلى إصلاح:

 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; } 

تحتاج أيضًا إلى التأكد من أن مُنشئ BoundConditionalReceiver يدعم الحصول على قيم فارغة للمعلمات الخاصة به ، أو إجراء إعادة معالجة إضافية.

أخطاء مماثلة أخرى:

  • 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 سترمي ArgumentOutOfRangeException . التصحيح المطلوب:

 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); .... } 

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

الخطأ التالي هو أكثر وضوحا.

V3095 تم استخدام كائن 'scriptArgsOpt' قبل أن يتم التحقق منه مقابل لاغ. خطوط التحقق: 321 ، 325. CommonCommandLineParser.cs 321

 internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt.Add(arg); // <= continue; } if (scriptArgsOpt != null) { .... } .... } } 

أعتقد أن هذا الجزء من الكود لا يتطلب أي تفسير. سأقدم النسخة الصحيحة:

 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' قبل أن يتم التحقق منه مقابل خالية. Check lines: 223, 228. SyntaxTreeExtensions.cs 223
  • V3095 The 'text' object was used before it was verified against null. Check lines: 376, 385. MSBuildWorkspace.cs 376
  • V3095 The 'nameOrMemberAccessExpression' object was used before it was verified against null. Check lines: 206, 223. CSharpGenerateTypeService.cs 206
  • V3095 The 'simpleName' object was used before it was verified against null. Check lines: 83, 85. CSharpGenerateMethodService.cs 83
  • V3095 The 'option' object was used before it was verified against null. Check lines: 23, 28. OptionKey.cs 23

النظر في أخطاء V3105 . نستخدم هنا عامل التشغيل الفارغ الشرطي عند تهيئة المتغير ، وفيما يلي في الكود يتم استخدام المتغير دون التحقق من المساواة الفارغة .

يتم الإشارة إلى الخطأ التالي فورًا بتحذيرين.

V3105 تم استخدام متغير 'documentId' بعد أن تم تعيينه من خلال معامل null-conditional. NullReferenceException ممكن. CodeLensReferencesService.cs 138

V3105 تم استخدام المتغير 'documentId' بعد تعيينه من خلال عامل التشغيل الشرطي. 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 المتغير إلى قيمة خالية . نتيجة لذلك ، سينتهي إنشاء 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 سطر) ، فسيكون هذا مبلغًا صغيرًا جدًا. مثل أندريه في المقال السابق ، أنا مستعد أيضًا للاعتراف بالجودة العالية لهذا المشروع.

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

يمكنك البحث بشكل مستقل عن بعض الأخطاء الأخرى سواء في المشروع المعني أو في أي شيء آخر. للقيام بذلك ، تحتاج فقط إلى تحميل وتجربة محلل لدينا.



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

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


All Articles