Artikel ini mengulas hasil pemeriksaan kedua proyek Orchard dengan analisa statis PVS-Studio. Orchard adalah sistem pengelola konten sumber terbuka yang dikirimkan sebagai bagian dari Galeri Open Source ASP.NET di bawah Yayasan Outercurve nirlaba. Pemeriksaan hari ini sangat menarik karena baik proyek maupun penganalisa telah berjalan jauh sejak pemeriksaan pertama, dan kali ini kita akan melihat pesan diagnostik baru dan beberapa bug yang bagus.
Tentang Orchard CMSKami
memeriksa Orchard tiga tahun lalu. Alat analisis C # PVS-Studio telah sangat berkembang sejak saat itu: kami telah meningkatkan analisis aliran data, menambahkan analisis antar-prosedur dan diagnostik baru, dan memperbaiki sejumlah kesalahan positif. Lebih dari itu, cek kedua mengungkapkan bahwa pengembang Orchard telah memperbaiki semua bug yang dilaporkan dalam artikel pertama, yang berarti kami telah mencapai tujuan kami, yaitu membuat mereka membuat kode mereka lebih baik.
Saya berharap mereka akan memperhatikan artikel ini juga dan melakukan perbaikan yang diperlukan atau, lebih baik, mengadopsi PVS-Studio untuk digunakan secara teratur. Sebagai pengingat, kami menyediakan pengembang
open-source dengan lisensi gratis. Ngomong-ngomong, ada
opsi lain yang bisa dinikmati proyek berpemilik juga.
Kode sumber Orchard tersedia untuk diunduh di
sini . Deskripsi proyek lengkap ada di
sini . Jika Anda belum memiliki salinan PVS-Studio, Anda dapat
mengunduh versi uji coba. Saya menggunakan PVS-Studio 7.05 Beta dan akan menyertakan beberapa peringatannya di artikel ini. Saya harap ulasan ini akan meyakinkan Anda bahwa PVS-Studio adalah alat yang berguna. Perlu diingat bahwa ini dimaksudkan untuk digunakan
secara teratur .
Hasil analisisBerikut adalah beberapa angka dari cek pertama Orchard sehingga Anda tidak perlu beralih di antara dua artikel untuk perbandingan.
Selama pemeriksaan sebelumnya, "kami melakukan analisis terhadap semua file kode sumber (3739 item) dengan ekstensi .cs. Secara total ada 214.564 baris kode. Hasil cek itu 137 peringatan. Untuk lebih tepatnya, ada 39 peringatan tingkat pertama (tinggi). Ada juga peringatan tingkat 60 (menengah) kedua. "
Versi Orchard saat ini terdiri dari 2.767 file .cs, yaitu sekitar seribu file lebih kecil. Perampingan dan penggantian nama repositori menunjukkan bahwa pengembang telah mengisolasi inti proyek (
commit 966 ), yang merupakan 108.287 LOC. Penganalisa mengeluarkan 153 peringatan: 33 tingkat pertama dan 70 tingkat kedua. Kami biasanya tidak memasukkan peringatan tingkat ketiga, dan saya akan tetap berpegang pada tradisi.
Pesan diagnostik PVS-Studio: V3110 Kemungkinan rekursi tak terbatas di dalam metode 'TryValidateModel'. PrefixedModuleUpdater.cs 48
public bool TryValidateModel(object model, string prefix) { return TryValidateModel(model, Prefix(prefix)); }
Mari kita mulai dengan bug rekursi yang tak terbatas, seperti yang kita lakukan di artikel pertama. Kali ini niat pasti pengembang tidak jelas, tetapi saya perhatikan bahwa metode
TryValidateModel memiliki versi kelebihan beban dengan satu parameter:
public bool TryValidateModel(object model) { return _updateModel.TryValidateModel(model); }
Saya pikir, seperti halnya pada versi overload, pengembang bermaksud memanggil metode tersebut melalui
_updateModel. Kompiler tidak memperhatikan kesalahan;
_updateModel bertipe
IUpdateModel , dan kelas saat ini juga mengimplementasikan antarmuka ini. Karena metode ini tidak menyertakan pemeriksaan terhadap
StackOverflowException , itu mungkin tidak pernah dipanggil, meskipun saya tidak akan mengandalkan itu. Jika asumsi saya benar, versi tetap akan terlihat seperti ini:
public bool TryValidateModel(object model, string prefix) { return _updateModel.TryValidateModel(model, Prefix(prefix)); }
Pesan diagnostik PVS-Studio: V3008 Variabel 'konten' diberi nilai dua kali berturut-turut. Mungkin ini sebuah kesalahan. Periksa baris: 197, 190. DynamicCacheTagHelper.cs 197
public override async Task ProcessAsync(....) { .... IHtmlContent content; .... try { content = await output.GetChildContentAsync(); } finally { _cacheScopeManager.ExitScope(); } content = await ProcessContentAsync(output, cacheContext); .... }
Penganalisa mendeteksi dua tugas untuk
konten variabel lokal
. GetChildContentAsync adalah metode pustaka yang terlalu jarang digunakan bagi kami untuk mengambil kesulitan untuk memeriksa dan membubuhi keterangan. Jadi, saya khawatir, baik kita maupun analis tidak tahu apa-apa tentang objek pengembalian metode dan efek sampingnya. Tapi kami tahu pasti bahwa menetapkan nilai kembali ke
konten tidak masuk akal jika tidak digunakan lebih lanjut dalam kode. Mungkin itu hanya operasi yang berlebihan daripada kesalahan. Saya tidak bisa mengatakan bagaimana tepatnya ini harus diperbaiki, jadi saya serahkan kepada pengembang.
Pesan diagnostik PVS-Studio: V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'itemTag'. CoreShapes.cs 92
public async Task<IHtmlContent> List(....string ItemTag....) { .... string itemTagName = null; if (ItemTag != "-") { itemTagName = string.IsNullOrEmpty(ItemTag) ? "li" : ItemTag; } var index = 0; foreach (var item in items) { var itemTag = String.IsNullOrEmpty(itemTagName) ? null : ....; .... itemTag.InnerHtml.AppendHtml(itemContent); listTag.InnerHtml.AppendHtml(itemTag); ++index; } return listTag; }
Alat analisis mendeteksi dereferensi
itemTag yang tidak aman. Cuplikan ini adalah contoh yang baik tentang bagaimana alat analisis statis berbeda dari pengembang manusia yang melakukan tinjauan kode. Metode ini memiliki parameter bernama
ItemTag dan variabel lokal bernama
itemTag . Tidak perlu memberi tahu Anda itu membuat perbedaan besar ke kompiler! Ini adalah dua variabel yang berbeda, meskipun terkait. Cara mereka terkait adalah melalui variabel ketiga,
itemTagName. Berikut urutan langkah menuju kemungkinan pengecualian: jika argumen
ItemTag sama dengan "-", tidak ada nilai yang akan ditetapkan ke
itemTagName , jadi itu akan tetap menjadi referensi nol, dan jika itu adalah referensi nol, maka variabel lokal
itemTag akan berubah menjadi referensi nol juga. Menurut pendapat saya, lebih baik untuk memiliki pengecualian yang dilemparkan mengikuti cek string.
public async Task<IHtmlContent> List(....string ItemTag....) { .... string itemTagName = null; if (ItemTag != "-") { itemTagName = string.IsNullOrEmpty(ItemTag) ? "li" : ItemTag; } var index = 0; foreach (var item in items) { var itemTag = ....; if(String.IsNullOrEmpty(itemTag)) throw .... .... itemTag.InnerHtml.AppendHtml(itemContent); listTag.InnerHtml.AppendHtml(itemTag); ++index; } return listTag; }
Pesan diagnostik PVS-Studio: V3095 Objek 'remoteClient' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 49, 51. ImportRemoteInstanceController.cs 49
public async Task<IActionResult> Import(ImportViewModel model) { .... var remoteClient = remoteClientList.RemoteClients.FirstOrDefault(....); var apiKey = Encoding.UTF8.GetString(....(remoteClient.ProtectedApiKey)); if (remoteClient == null || ....) { .... } .... }
Penganalisa mendeteksi dereference dari
remoteClient diikuti oleh pemeriksaan nol beberapa baris kemudian. Ini memang potensi
NullReferenceException karena metode
FirstOrDefault dapat mengembalikan nilai default (yang
nol untuk tipe referensi). Saya kira cuplikan ini dapat diperbaiki dengan hanya memindahkan pemeriksaan sehingga yang mendahului operasi dereference:
public async Task<IActionResult> Import(ImportViewModel model) { .... var remoteClient = remoteClientList.RemoteClients.FirstOrDefault(....); if (remoteClient != null) var apiKey = UTF8.GetString(....remoteClient.ProtectedApiKey); else if (....) { .... } .... }
Atau mungkin itu harus diperbaiki dengan mengganti
FirstOrDefault dengan
First dan menghapus cek sama sekali.
Peringatan oleh PVS-Studio 7.05 Beta:Sekarang, kami telah memberi anotasi pada semua metode
orDefault LINQ . Informasi ini akan digunakan oleh diagnostik baru yang sedang kami kerjakan: mendeteksi kasus-kasus di mana nilai-nilai yang dikembalikan oleh metode-metode ini direferensikan tanpa pemeriksaan sebelumnya. Setiap metode
orDefault memiliki rekanan yang melempar pengecualian jika tidak ada elemen yang cocok yang ditemukan. Pengecualian ini akan lebih membantu dalam melacak masalah daripada
NullReferenceException abstrak.
Saya tidak bisa tidak membagikan hasil yang saya dapatkan dari diagnostik ini pada proyek Orchard. Ada 27 tempat yang berpotensi berbahaya. Inilah beberapa di antaranya:
ContentTypesAdminNodeNavigationBuilder.cs 71:
var treeBuilder = treeNodeBuilders.Where(....).FirstOrDefault(); await treeBuilder.BuildNavigationAsync(childNode, builder, treeNodeBuilders);
ListPartDisplayDriver.cs 217:
var contentTypePartDefinition = ....Parts.FirstOrDefault(....); return contentTypePartDefinition.Settings....;
ContentTypesAdminNodeNavigationBuilder.cs 113:
var typeEntry = node.ContentTypes.Where(....).FirstOrDefault(); return AddPrefixToClasses(typeEntry.IconClass);
Pesan diagnostik PVS-Studio: V3080 Kemungkinan null dereference nilai pengembalian metode. Pertimbangkan untuk memeriksa: CreateScope (). SetupService.cs 136
public async Task<string> SetupInternalAsync(SetupContext context) { .... using (var shellContext = await ....) { await shellContext.CreateScope().UsingAsync(....); } .... }
Penganalisa menyebutkan dereferensi nilai yang dikembalikan oleh metode
CreateScope .
CreateScope adalah metode kecil, jadi inilah implementasinya yang lengkap:
public ShellScope CreateScope() { if (_placeHolder) { return null; } var scope = new ShellScope(this);
Seperti yang Anda lihat, ada dua kasus di mana ia dapat mengembalikan
nol . Analisator tidak tahu cabang mana dari aliran eksekusi yang akan mengikuti, jadi ia bermain aman dan melaporkan kode sebagai mencurigakan. Jika saya menulis kode seperti itu, saya akan segera menulis cek nol.
Mungkin pendapat saya bias, tetapi saya percaya bahwa setiap metode asinkron harus dilindungi dari
NullReferenceException sebanyak mungkin karena debugging hal-hal seperti itu jauh dari menyenangkan.
Dalam kasus khusus ini, metode
CreateScope disebut empat kali: dua panggilan itu disertai dengan pemeriksaan dan dua lainnya tidak. Dua panggilan terakhir (tanpa pemeriksaan) tampaknya merupakan klon copy-tempel (kelas yang sama, metode yang sama, cara yang sama untuk mendereferensi hasil yang dipanggil dengan MenggunakanAsync) Yang pertama dari dua panggilan itu ditunjukkan di atas, dan Anda mungkin yakin yang kedua memicu peringatan yang sama:
V3080 Kemungkinan nol dereferensi nilai pengembalian metode. Pertimbangkan untuk memeriksa: CreateScope (). SetupService.cs 192
Pesan diagnostik PVS-Studio: V3127 Dua fragmen kode serupa ditemukan. Mungkin, ini adalah kesalahan ketik dan variabel 'AccessTokenSecret' harus digunakan sebagai ganti 'ConsumerSecret' TwitterClientMessageHandler.cs 52
public async Task ConfigureOAuthAsync(HttpRequestMessage request) { .... if (!string.IsNullOrWhiteSpace(settings.ConsumerSecret)) settings.ConsumerSecret = protrector.Unprotect(settings.ConsumerSecret); if (!string.IsNullOrWhiteSpace(settings.ConsumerSecret)) settings.AccessTokenSecret = protrector.Unprotect(settings.AccessTokenSecret); .... }
Itu kesalahan salin-tempel yang klasik.
ConsumerSecret diperiksa dua kali, sementara
AccessTokenSecret tidak diperiksa sama sekali. Jelas, ini diperbaiki sebagai berikut:
public async Task ConfigureOAuthAsync(HttpRequestMessage request) { .... if (!string.IsNullOrWhiteSpace(settings.ConsumerSecret)) settings.ConsumerSecret = protrector.Unprotect(settings.ConsumerSecret); if (!string.IsNullOrWhiteSpace(settings.AccessTokenSecret)) settings.AccessTokenSecret = protrector.Unprotect(settings.AccessTokenSecret); .... }
Pesan diagnostik PVS-Studio: V3139 Dua atau lebih cabang kasus melakukan tindakan yang sama. SerialDocumentExecuter.cs 23
Bug salin-tempel lainnya. Untuk kejelasan, inilah implementasi kelas yang lengkap (ini kecil).
public class SerialDocumentExecuter : DocumentExecuter { private static IExecutionStrategy ParallelExecutionStrategy = new ParallelExecutionStrategy(); private static IExecutionStrategy SerialExecutionStrategy = new SerialExecutionStrategy(); private static IExecutionStrategy SubscriptionExecutionStrategy = new SubscriptionExecutionStrategy(); protected override IExecutionStrategy SelectExecutionStrategy(....) { switch (context.Operation.OperationType) { case OperationType.Query: return SerialExecutionStrategy; case OperationType.Mutation: return SerialExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; } } }
Alat analisis tidak menyukai dua cabang
kasus yang identik. Memang, kelas memiliki tiga entitas, sedangkan pernyataan switch hanya mengembalikan dua dari mereka. Jika perilaku ini dimaksudkan dan entitas ketiga sebenarnya tidak dimaksudkan untuk digunakan, kode dapat ditingkatkan dengan menghapus cabang ketiga setelah menggabungkan keduanya sebagai berikut:
switch (context.Operation.OperationType) { case OperationType.Query: case OperationType.Mutation: return SerialExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; }
Jika ini adalah bug salin-tempel, bidang pengembalian duplikat pertama harus diperbaiki sebagai berikut:
switch (context.Operation.OperationType) { case OperationType.Query: return ParallelExecutionStrategy; case OperationType.Mutation: return SerialExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; }
Atau harus cabang kasus kedua. Saya tidak tahu detail proyek dan karena itu tidak dapat menentukan korelasi antara nama-nama jenis operasi dan strategi.
switch (context.Operation.OperationType) { case OperationType.Query: return SerialExecutionStrategy; case OperationType.Mutation: return ParallelExecutionStrategy; case OperationType.Subscription: return SubscriptionExecutionStrategy; default: throw ....; }
Pesan diagnostik PVS-Studio: V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'permintaan'. GraphQLMiddleware.cs 148
private async Task ExecuteAsync(HttpContext context....) { .... GraphQLRequest request = null; .... if (HttpMethods.IsPost(context.Request.Method)) { .... } else if (HttpMethods.IsGet(context.Request.Method)) { .... request = new GraphQLRequest(); .... } var queryToExecute = request.Query; .... }
Variabel
permintaan diberikan nilai yang berbeda dari
nol beberapa kali di blok
if pertama, tetapi setiap kali dengan kondisi bersarang. Termasuk semua kondisi itu akan membuat contoh terlalu lama, jadi kita hanya akan pergi dengan yang pertama, yang memeriksa jenis metode http
IsGet atau
IsPost . Kelas
Microsoft.AspNetCore.Http.HttpMethods memiliki sembilan metode statis untuk memeriksa jenis kueri. Oleh karena itu, meneruskan, misalnya,
Hapus atau
Set kueri ke metode
ExecuteAsync akan mengarah pada peningkatan
NullReferenceException . Sekalipun metode seperti itu saat ini tidak didukung sama sekali, akan lebih bijak jika menambahkan cek pelempar pengecualian. Bagaimanapun, persyaratan sistem dapat berubah. Berikut ini contoh cek semacam itu:
private async Task ExecuteAsync(HttpContext context....) { .... if (request == null) throw ....; var queryToExecute = request.Query; .... }
Pesan diagnostik PVS-Studio: V3080 Kemungkinan null dereference nilai pengembalian metode. Pertimbangkan untuk memeriksa: Dapatkan <ContentPart> (...). ContentPartHandlerCoordinator.cs 190
Sebagian besar peringatan
V3080 lebih nyaman untuk dilihat dalam lingkungan pengembangan karena Anda memerlukan navigasi metode, penyorotan jenis, dan suasana IDE yang ramah. Saya mencoba mengurangi teks contoh sebanyak mungkin agar tetap dapat dibaca. Tetapi jika saya tidak melakukannya dengan benar atau jika Anda ingin menguji kemampuan pemrograman Anda dan mencari tahu sendiri, saya sarankan memeriksa hasil diagnostik ini pada proyek open-source atau hanya kode Anda sendiri.
public override async Task LoadingAsync(LoadContentContext context) { .... context.ContentItem.Get<ContentPart>(typePartDefinition.Name) .Weld(fieldName, fieldActivator.CreateInstance()); .... }
Penganalisa melaporkan baris ini. Mari kita lihat metode
Get :
public static TElement Get<TElement>(this ContentElement contentElement....) where TElement : ContentElement { return (TElement)contentElement.Get(typeof(TElement), name); }
Itu menyebut versi kelebihannya. Mari kita periksa juga:
public static ContentElement Get(this ContentElement contentElement....) { .... var elementData = contentElement.Data[name] as JObject; if (elementData == null) { return null; } .... }
Ternyata jika kita mendapatkan entitas tipe yang tidak kompatibel dengan
JObject dari
Data menggunakan pengindeks
nama , metode
Get akan mengembalikan
nol . Saya tidak tahu pasti seberapa mungkin itu karena jenis-jenis ini dari perpustakaan
Newtonsoft.Json , yang saya belum banyak bekerja. Tetapi pembuat kode curiga bahwa elemen yang dicari mungkin tidak ada, jadi kita harus mengingatnya ketika mengakses hasil operasi baca juga. Secara pribadi, saya akan memiliki pengecualian yang dilemparkan di
Dapatkan pertama jika kami percaya simpul harus ada, atau tambahkan cek sebelum dereferensi jika ketidakhadiran simpul tidak mengubah logika keseluruhan (misalnya, kami mendapatkan nilai default).
Solusi 1:
public static ContentElement Get(this ContentElement contentElement....) { .... var elementData = contentElement.Data[name] as JObject; if (elementData == null) { throw.... } .... }
Solusi 2:
public override async Task LoadingAsync(LoadContentContext context) { .... context.ContentItem.Get<ContentPart>(typePartDefinition.Name) ?.Weld(fieldName, fieldActivator.CreateInstance()); .... }
Pesan diagnostik PVS-Studio: V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'hasil'. ContentQueryOrchardRazorHelperExtensions.cs 19
public static async Task<IEnumerable<ContentItem>> ContentQueryAsync(....) { var results = await orchardHelper.QueryAsync(queryName, parameters); .... foreach (var result in results) { .... } .... }
Ini adalah contoh yang cukup sederhana dibandingkan dengan yang sebelumnya. Analisator curiga bahwa metode
QueryAsync mungkin mengembalikan referensi nol. Inilah implementasi metode ini:
public static async Task<IEnumerable> QueryAsync(....) { .... var query = await queryManager.GetQueryAsync(queryName); if (query == null) { return null; } .... }
Karena
GetQueryAsync adalah metode antarmuka, Anda tidak dapat memastikan setiap implementasi, terutama jika kami menganggap bahwa proyek ini juga menyertakan versi berikut:
public async Task<Query> GetQueryAsync(string name) { var document = await GetDocumentAsync(); if(document.Queries.TryGetValue(name, out var query)) { return query; } return null; }
Beberapa panggilan ke fungsi eksternal dan akses cache membuat analisis
GetDocumentAsync sulit, jadi anggap saja pemeriksaan diperlukan - terlebih lagi karena metode ini merupakan metode yang tidak sinkron.
public static async Task<IEnumerable<ContentItem>> ContentQueryAsync(....) { var results = await orchardHelper.QueryAsync(queryName, parameters); if(results == null) throw ....; .... foreach (var result in results) { .... } .... }
KesimpulanSaya tidak bisa tidak menyebutkan kualitas kode Orchard yang tinggi! Benar, ada beberapa cacat lain, yang tidak saya diskusikan di sini, tetapi saya menunjukkan semua bug yang paling parah. Tentu saja, ini bukan untuk mengatakan bahwa memeriksa kode sumber Anda sekali dalam tiga tahun sudah cukup. Anda akan mendapatkan hasil maksimal dari analisis statis jika Anda menggunakannya
secara teratur karena ini adalah cara Anda dijamin untuk menangkap dan memperbaiki bug pada tahap pengembangan paling awal, di mana perbaikan bug termurah dan termudah.
Meskipun pemeriksaan satu kali tidak banyak membantu, saya tetap mendorong Anda untuk mengunduh
PVS-Studio dan mencobanya di proyek Anda: siapa tahu, mungkin Anda akan menemukan beberapa bug yang menarik juga.