
عندما قررنا البحث عن أخطاء في مشروع Azure SDK for .NET ، فوجئنا بسرورها بحجمه. "قلنا ثلاثة ملايين ونصف المليون سطر من التعليمات البرمجية" ، قلنا ، ندرس إحصائيات المشروع. هذا هو مقدار ما يمكنك أن تجد هناك. ولكن للأسف ، آه. تحول المشروع ليكون سرا. ما هي خصوصية المشروع وكيف تم اختباره - اقرأ في هذا المقال.
عن المشروع
أكتب هذه المقالة في متابعة لمقالتي السابقة ، والتي كانت أيضًا عن مشروع متعلق بـ Microsoft Azure:
Azure PowerShell: "غير ضار بشكل أساسي" . لذلك ، هذه المرة كنت أعول على عدد كبير من الأخطاء المتنوعة والمثيرة للاهتمام. في الواقع ، بالنسبة لتحليل ثابت ، فإن حجم المشروع عادة ما يكون مهمًا ، لا سيما مع الفحوصات لمرة واحدة للمشروع بأكمله. نعم ، في الممارسة العملية لا يفعلون ذلك عادة. وإذا فعلوا ، ثم فقط في مرحلة تنفيذ محلل. في الوقت نفسه ، لا يفهم أحد على الفور العدد الهائل من العمليات (هناك عدد كبير من التحذيرات هو المعيار عندما يبدأ المحلل في هذا الوضع) ، ولكن ببساطة يضعهم في دين تقني باستخدام آليات كبح الرسائل وتخزينها في قواعد البيانات الخاصة (كبت جماعي). نحن منخرطون في عمليات التفتيش لمرة واحدة لأغراض البحث. لذلك ، المشاريع الكبيرة للدراسة هي دائما أفضل من المشاريع الصغيرة.
ومع ذلك ، أظهر مشروع Azure SDK for .NET على الفور فشله كمقعد اختبار. حتى حجمها المثير للإعجاب لم يساعد ، بل بالأحرى تعقيد العمل. السبب موضح في إحصائيات المشروع التالية:
- ملفات المصدر .cs (لا تشمل الاختبارات): 16،500
- حلول Visual Studio (.sln): 163
- أسطر الكود غير الفارغة: 3،462،000
- التي ولدت تلقائيا: حوالي 3.3 مليون دولار
- مستودع المشروع متاح على جيثب .
يتم إنشاء حوالي 95 ٪ من الشفرة تلقائيًا ، ويتم تكرار جزء كبير من هذا الرمز عدة مرات. عادةً ما يكون التحقق من مثل هذه المشروعات باستخدام محلل ثابت أمرًا مستغرقًا للوقت ولا فائدة منه ، نظرًا لوجود الكثير من العمل ، ولكنه غير منطقي (للوهلة الأولى) ورمز متكرر. هذا يؤدي إلى عدد كبير من الإيجابيات الخاطئة.
تم استخدام عدد كبير من حلول Visual Studio (163) كرز على الكعكة ، وفقًا لذلك تم "تلطيخ" هذه الكتلة من الكود. حتى أتحقق من الكود المتبقي (لا يتم إنشاؤه تلقائيًا) اضطررت إلى بذل بعض الجهود. لقد ساعد ذلك في وضع جميع الشفرات التي تم إنشاؤها تلقائيًا في مجلدات الحلول الفرعية على طول المسار النسبي "<مجلد الحلول> \ src \ Generated". أيضًا ، يحتوي كل ملف .cs من هذا الرمز على تعليق خاص في علامة
<auto-generated> :
من أجل نقاء التجربة ، راجعت عشوائيًا حوالي عشرة حلول تم اختيارها عشوائيًا باستخدام شفرة تم إنشاؤها تلقائيًا. ستكون النتائج أقل.
لذلك ، على الرغم من قلة الكود "الصادق" المتبقي ، إلا أنني تمكنت من العثور على عدد معين من الأخطاء هناك. هذه المرة لن أقدم رحلات بترتيب أرقام تشخيصات PVS-Studio. بدلاً من ذلك ، سوف أقوم بتجميع الإجابات حسب الحلول التي تم اكتشافها فيها.
دعونا نرى ما وجدته في Azure SDK لرمز .NET.
Microsoft.Azure.Management.Advisor
هذا مجرد واحد من العديد من الحلول التي تحتوي على رمز تم إنشاؤه تلقائيًا. كما قلت أعلاه ، تم اختبار نحو عشرة من هذه الحلول بشكل انتقائي. وفي كل مكان كانت الرسائل هي نفسها ، وكما هو متوقع ، عديمة الفائدة. اسمحوا لي أن أقدم لكم مثالين على هذه الردود.
V3022 التعبير "بيانات الاعتماد! =
لاغ " صحيح دائمًا. AdvisorManagementClient.cs 204
من الواضح أن الكود لا لزوم له ، والتحقق من
مؤهلات الاعتماد! = لا قيمة لها. لكن الرمز يعمل. و autogenerated. لذلك - لا شكاوى.
تعبير
V3022 '_queryParameters.Count> 0' غير صحيح دائمًا. ConfigurationsOperations.cs 871
ومرة أخرى ، على ما يبدو خالية من تصميم المنطق. لسبب ما ، يتحققون من حجم القائمة
الفارغة التي تم إنشاؤها للتو. في الواقع - كل شيء في محله. هذا الاختبار لا معنى له الآن ، ولكن إذا قام المولد بإعداد إنشاء قائمة ، على سبيل المثال ، بناءً على مجموعة أخرى ، فسيكون التحقق بالفعل منطقيًا. لذلك - مرة أخرى ، لا توجد شكاوى ضد الكود ، بالنظر إلى مصدره ، بالطبع.
لكل حل برمز تم إنشاؤه تلقائيًا ، تم تلقي مئات التحذيرات المشابهة. بالنظر إلى عدم جدواها ، أعتقد أنه لا جدوى من النقاش الإضافي حول هذه الإيجابيات. الأخطاء الحقيقية فقط في الكود "العادي" سيتم النظر فيها أدناه.
Azure.Core
V3001 هناك
تعبيرات فرعية مماثلة "buffer.Length" إلى اليسار وإلى يمين المشغل "<". AzureBaseBuffersExtensions.cs 30
public static async Task WriteAsync(...., ReadOnlyMemory<byte> buffer, ....) { byte[]? array = null; .... if (array == null || buffer.Length < buffer.Length)
حدث خطأ في الحالة ، ربما كنتيجة لاستخدام لصق النسخ. استنادا إلى حقيقة أن مزيد من في
المخزن المؤقت للرمز
يتم نسخها إلى
مجموعة ، يجب أن يبدو الاختيار:
if (array == null || array.Length < buffer.Length)
ولكن ، كما أقول دائمًا ، يجب على مؤلف الرمز إصلاح مثل هذه الأخطاء.
V3083 الاحتجاج غير الآمن للحدث "_onChange" ، NullReferenceException ممكن. النظر في تعيين الحدث إلى متغير محلي قبل استدعاء ذلك. ClientOptionsMonitor.cs 44
private event Action<TOptions, string> _onChange; .... private void InvokeChanged(....) { .... if (_onChange != null) { _onChange.Invoke(options, name); } }
غير حرجة ، ولكن خطأ. بين التحقق من هذا الحدث للمساواة
الفارغة واستدعائه ، يمكن إلغاء اشتراك الحدث. ثم
يصبح متغير
_onChange خاليًا وسيتم طرح استثناء. يحتاج الرمز إلى إعادة كتابة بأمان أكبر. على سبيل المثال ، مثل هذا:
private void InvokeChanged(....) { .... _onChange?.Invoke(options, name); }
Azure.Messaging.EventHubs
V3080 dereference ممكن. النظر في فحص "eventPropertyValue". AmqpMessageConverter.cs 650
private static bool TryCreateEventPropertyForAmqpProperty( object amqpPropertyValue, out object eventPropertyValue) { eventPropertyValue = null; .... switch (GetTypeIdentifier(amqpPropertyValue)) { case AmqpProperty.Type.Byte: .... case AmqpProperty.Type.String: eventPropertyValue = amqpPropertyValue; return true; .... } .... switch (amqpPropertyValue) { case AmqpSymbol symbol: eventPropertyValue = ....; break; case byte[] array: eventPropertyValue = ....; break; case ArraySegment<byte> segment when segment.Count == segment.Array.Length: eventPropertyValue = ....; break; case ArraySegment<byte> segment: .... eventPropertyValue = ....; break; case DescribedType described when (described.Descriptor is AmqpSymbol): eventPropertyValue = ....; break; default: var exception = new SerializationException( string.Format(...., eventPropertyValue.GetType().FullName));
دعنا نتتبع ما يحدث لقيمة متغير
eventPropertyValue في مقتطف الكود أعلاه. في بداية الطريقة ، يكون المتغير
فارغًا . علاوة على ذلك ، في أحد شروط
التبديل الأول ، تتم تهيئة المتغير ، وبعد ذلك يتم إنهاء الطريقة. تحتوي كتلة
التبديل الثانية على الكثير من الشروط ، حيث يتلقى المتغير أيضًا بعض القيمة الجديدة في كل منها. ولكن في الكتلة
الافتراضية ،
يتم استخدام متغير
eventPropertyValue ببساطة دون أي تحقق ، وهذا خطأ ، لأن المتغير في هذه اللحظة
لاغٍ .
V3066 تم ترتيب الوسيطات غير الصحيحة المحتملة إلى مُنشئ "EventHubConsumer": "partitionId" و "ConsumerGroup". TrackOneEventHubClient.cs 394
public override EventHubConsumer CreateConsumer(....) { return new EventHubConsumer ( new TrackOneEventHubConsumer(....), TrackOneClient.EventHubName, partitionId,
يشتبه المحلل أنه عند استدعاء مُنشئ فئة
EventHubConsumer ، تم خلط ترتيب الوسيطتين الثالثة والرابعة. دعونا نلقي نظرة على إعلان المنشئ:
internal EventHubConsumer(TransportEventHubConsumer transportConsumer, string eventHubName, string consumerGroup, // <= 3 string partitionId, // <= 4 EventPosition eventPosition, EventHubConsumerOptions consumerOptions, EventHubRetryPolicy retryPolicy) { .... }
يتم خلط الحجج حقا. أعتقد كيف تم ارتكاب هذا الخطأ. ربما يكون الخطأ هو التنسيق الضعيف للرمز. إلقاء نظرة أخرى على
إعلان منشئ
EventHubConsumer . نظرًا لحقيقة أن المعلمة الأولى
transportConsumer موجودة على نفس سطر اسم الفئة ، أثناء النظر في الكود لفترة وجيزة ، قد يبدو أن المعلمة
partitionId في المركز الثالث وليست في المرتبة الرابعة (لا توجد تعليقاتي مع الأرقام التسلسلية للمعلمات في الكود الأصلي ).
هذا مجرد تخمين ، لكنني أود تغيير تنسيق كود إعلان المنشئ إلى هذا:
internal EventHubConsumer ( TransportEventHubConsumer transportConsumer, string eventHubName, string consumerGroup, string partitionId, EventPosition eventPosition, EventHubConsumerOptions consumerOptions, EventHubRetryPolicy retryPolicy) { .... }
Azure.Storage
V3112 خلل في مقارنات مماثلة. من الممكن أن يكون هناك خطأ مطبعي موجود داخل التعبير "ContentLanguage == other.ContentEncoding". BlobSasBuilder.cs 410
public struct BlobSasBuilder : IEquatable<BlobSasBuilder> { .... public bool Equals(BlobSasBuilder other) => BlobName == other.BlobName && CacheControl == other.CacheControl && BlobContainerName == other.BlobContainerName && ContentDisposition == other.ContentDisposition && ContentEncoding == other.ContentEncoding &&
خطأ ارتكبه الغفلة. من الصعب جدًا العثور على خطأ مشابه في مراجعة الكود. خيار الاختيار الصحيح:
.... ContentEncoding == other.ContentEncoding && ContentLanguage == other.ContentLanguage && ....
V3112 خلل في مقارنات مماثلة. من الممكن أن يكون هناك خطأ مطبعي موجود داخل التعبير "ContentLanguage == other.ContentEncoding". FileSasBuilder.cs 265
public struct FileSasBuilder : IEquatable<FileSasBuilder> { .... public bool Equals(FileSasBuilder other) => CacheControl == other.CacheControl && ContentDisposition == other.ContentDisposition && ContentEncoding == other.ContentEncoding
بالضبط نفس الخطأ في قطعة مماثلة جدا من التعليمات البرمجية. ربما تم نسخ الكود وتعديله جزئيًا. لكن الخطأ بقي.
Microsoft.Azure.Batch
V3053 تعبير مفرط. فحص سلاسل "IList" و "قائمة". PropertyData.cs 157
V3053 تعبير مفرط. فحص substrings "قائمة" و "IReadOnlyList". PropertyData.cs 158
public class PropertyData { .... public bool IsTypeCollection => this.Type.Contains("IList") || this.Type.Contains("IEnumerable") || this.Type.Contains("List") ||
أصدر المحلل تحذيرين حول فحوصات لا معنى لها أو خاطئة. في الحالة الأولى ، يبدو البحث عن السلسلة الفرعية "List" بعد البحث عن "IList" أمرًا ضروريًا. في الواقع ، الشرط:
this.Type.Contains("IList") || this.Type.Contains("List")
يمكن استبداله بهذا:
this.Type.Contains("List")
في الحالة الثانية ، لا معنى للبحث عن السلسلة الفرعية "IReadOnlyList" ، حيث يتم في وقت سابق البحث عن السلسلة الفرعية الأقصر "قائمة".
هناك أيضًا احتمال أن تكون العناصر الفرعية للبحث قد ارتكبت أخطاء ويجب أن يكون هناك شيء آخر. في أي حال ، لا يمكن تقديم الإصدار الصحيح من تصحيح الحالة ، مع مراعاة كلتا الملاحظة ، إلا بواسطة مؤلف الرمز.
V3095 تم استخدام عنصر 'httpRequest.Content.Headers' قبل أن يتم التحقق منه ضد قيمة خالية. خطوط التحقق: 76 ، 79. BatchSharedKeyCredential.cs 76
public override Task ProcessHttpRequestAsync( HttpRequestMessage httpRequest, ....) { .... signature.Append(httpRequest.Content != null && httpRequest.Content.Headers.Contains("Content-Language") ? .... : ....; long? contentLength = httpRequest.Content?.Headers?.ContentLength; .... }
في البداية ،
يتم استخدام المتغير
httpRequest.Content.Headers دون أي عمليات تدقيق ، ولكن لاحقًا في الكود يتم الوصول إلى هذا المتغير باستخدام عامل الوصول المشروط.
V3125 تم استخدام كائن "omPropertyData" بعد أن تم التحقق منه مقابل خالية. خطوط التحقق: 156 ، 148. CodeGenerationUtilities.cs 156
private static string GetProtocolCollectionToObjectModelCollectionString( ...., PropertyData omPropertyData, ....) { if (IsMappedEnumPair(omPropertyData?.GenericTypeParameter, ....)) { .... } if (IsTypeComplex(omPropertyData.GenericTypeParameter)) .... }
الوضع العكسي. تحتوي كتلة واحدة من التعليمات البرمجية على خيار وصول آمن إلى ارتباط
omPropertyData المحتمل أنه لاغٍ. علاوة على ذلك في الكود مع نفس الرابط يعملون دون أي شيكات.
V3146 التراجع المحتمل الفارغ لـ "القيمة". يمكن لـ 'FirstOrDefault' إرجاع القيمة الخالية الافتراضية. BatchSharedKeyCredential.cs 127
public override Task ProcessHttpRequestAsync(HttpRequestMessage httpRequest, ....) { .... foreach (string canonicalHeader in customHeaders) { string value = httpRequest.Headers. GetValues(canonicalHeader).FirstOrDefault(); value = value.Replace('\n', ' ').Replace('\r', ' ').TrimStart(); .... } .... }
نتيجة لطريقة
FirstOrDefault ، إذا فشل البحث ، فسيتم إرجاع القيمة الافتراضية لنوع
السلسلة ، أي أنها
لاغية . سيتم تعيين
القيمة لمتغير
القيمة ، والذي يتم استخدامه لاحقًا في الكود مع طريقة
الاستبدال بدون أي عمليات تحقق. يجب جعل الرمز أكثر أمانًا. على سبيل المثال ، مثل هذا:
foreach (string canonicalHeader in customHeaders) { string value = httpRequest.Headers. GetValues(canonicalHeader).FirstOrDefault(); value = value?.Replace('\n', ' ').Replace('\r', ' ').TrimStart(); .... }
Microsoft.Azure.ServiceBus
V3121 تم
إعلان التعداد "BlocksUsing" باستخدام سمة "الإشارات" ، لكنه لا يقوم بتعيين أي مُهيئات لتجاوز القيم الافتراضية. Fx.cs 69
static class Fx { .... public static class Tag { .... [Flags] public enum BlocksUsing { MonitorEnter, MonitorWait, ManualResetEvent, AutoResetEvent, AsyncResult, IAsyncResult, PInvoke, InputQueue, ThreadNeutralSemaphore, PrivatePrimitive, OtherInternalPrimitive, OtherFrameworkPrimitive, OtherInterop, Other, NonBlocking, } .... } .... }
يتم تعريف التعداد مع سمة
العلامات . في هذه الحالة ، يتم ترك قيم الثوابت افتراضيًا (
MonitorEnter = 0 ،
MonitorWait = 1 ،
ManualResetEvent = 2 ، وهكذا). قد يؤدي ذلك إلى حقيقة أنه عند محاولة استخدام
مجموعة من العلامات ، على سبيل المثال ، الثوابت الثانية والثالثة
MonitorWait (= 1) |
ManualResetEvent (= 2) ، لن يتم استلام قيمة فريدة ، ولكن ثابت بقيمة 3 افتراضيًا (
AutoResetEvent ). قد يكون هذا بمثابة مفاجأة لرمز الاتصال. إذا كان
مخطط تعداد
BlocksUsing مخططًا بالفعل لاستخدامه في تحديد مجموعات
الأعلام (حقل بت) ، فعليك إعطاء قيم الثوابت مساوٍ لقوى اثنين:
[Flags] public enum BlocksUsing { MonitorEnter = 1, MonitorWait = 2, ManualResetEvent = 4, AutoResetEvent = 8, AsyncResult = 16, IAsyncResult = 32, PInvoke = 64, InputQueue = 128, ThreadNeutralSemaphore = 256, PrivatePrimitive = 512, OtherInternalPrimitive = 1024, OtherFrameworkPrimitive = 2048, OtherInterop = 4096, Other = 8192, NonBlocking = 16384, }
V3125 تم استخدام عنصر "الجلسة" بعد أن تم التحقق منه ضد قيمة خالية. خطوط الفحص: 69 ، 68. AmqpLinkCreator.cs 69
public async Task<Tuple<AmqpObject, DateTime>> CreateAndOpenAmqpLinkAsync() { .... AmqpSession session = null; try {
انتبه للعمل مع متغير
الجلسة في
كتلة catch . يتم
استدعاء أسلوب
إحباط بأمان من خلال بيان الوصول الشرطي. ولكن بعد ذلك يقومون بإجراء مكالمة غير آمنة إلى الأسلوب
GetInnerException . في هذه الحالة ، بدلاً من طرح استثناء من النوع المتوقع ، قد يتم طرح
NullReferenceException . يحتاج الرمز إلى إصلاح. يعتمد أسلوب
AmqpExceptionHelper.GetClientException تمرير قيمة
فارغة للمعلمة
innerException :
public static Exception GetClientException( Exception exception, string referenceId = null, Exception innerException = null, bool connectionError = false) { .... }
لذلك ، يكفي استخدام عامل الوصول المشروط عند استدعاء
session.GetInnerException () :
public async Task<Tuple<AmqpObject, DateTime>> CreateAndOpenAmqpLinkAsync() { .... AmqpSession session = null; try {
استنتاج
كما ترون ، لا يضمن حجم المشروع الكبير دائمًا عددًا كبيرًا من الأخطاء. ولكن لا حاجة للاسترخاء - يمكنك دائمًا العثور على شيء ما. حتى في مثل هذا المشروع المعقد مثل Azure SDK for .NET. نعم ، هذا يتطلب بذل جهود إضافية ، ولكن النتيجة ستكون أكثر متعة. وحتى لا تضطر إلى بذل جهود مفرطة ، نوصي باستخدام التحليل الثابت ، وفي مكان العمل للمطورين عند كتابة رمز جديد. هذا هو النهج الأكثر فعالية.
قم بتنزيل وتجربة PVS-Studio في العمل. حظا سعيدا في الحرب ضد الحشرات!

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