
Sekali-sekali kita kembali ke proyek yang sebelumnya telah kita periksa menggunakan PVS-Studio, yang menghasilkan deskripsi mereka di berbagai artikel. Dua alasan membuat comeback ini menarik bagi kami. Pertama, kesempatan untuk menilai kemajuan alat analisis kami. Kedua, memantau umpan balik dari penulis proyek untuk artikel kami dan laporan kesalahan, yang biasanya kami berikan kepada mereka. Tentu saja, kesalahan dapat diperbaiki tanpa partisipasi kami. Namun, selalu menyenangkan ketika upaya kami membantu membuat proyek lebih baik. Roslyn tidak terkecuali. Artikel sebelumnya tentang pemeriksaan proyek ini tanggal kembali ke 23 Desember 2015. Ini cukup lama, mengingat kemajuan yang telah dibuat oleh analis kami sejak saat itu. Karena inti C # dari analisa PVS-Studio didasarkan pada Roslyn, itu memberi kami minat tambahan dalam proyek ini. Sebagai hasilnya, kami sama tertarik dengan mustard tentang kualitas kode proyek ini. Sekarang mari kita coba sekali lagi dan cari tahu beberapa masalah baru dan menarik (tapi mari kita berharap tidak ada yang signifikan) yang dapat ditemukan oleh PVS-Studio.
Banyak pembaca kami mungkin sangat menyadari Roslyn (atau .NET Compiler Platform). Singkatnya, ini adalah kumpulan kompiler open source dan API untuk analisis kode bahasa C # dan Visual Basic .NET dari Microsoft. Kode sumber proyek tersedia di
GitHub .
Saya tidak akan memberikan penjelasan rinci tentang platform ini dan saya akan merekomendasikan memeriksa artikel oleh rekan saya Sergey Vasiliev "
Pengantar Roslyn dan penggunaannya dalam pengembangan program " untuk semua pembaca yang tertarik. Dari artikel ini, Anda dapat mengetahui tidak hanya tentang fitur arsitektur Roslyn, tetapi bagaimana tepatnya kami menggunakan platform ini.
Seperti yang saya sebutkan sebelumnya, sudah lebih dari 3 tahun sejak kolega saya Andrey Karpov menulis artikel terakhir tentang cek Roslyn "
New Year PVS-Studio 6.00 Release: Scanning Roslyn ". Sejak itu, penganalisa C # PVS-Studio telah mendapatkan banyak fitur baru. Sebenarnya, artikel Andrey adalah uji kasus, karena pada saat itu penganalisa C # baru saja ditambahkan di PVS-Studio. Meskipun demikian, kami berhasil mendeteksi kesalahan dalam proyek Roslyn, yang tentu saja berkualitas tinggi. Jadi apa yang telah berubah dalam alat analisis untuk kode C # pada saat ini yang memungkinkan kami melakukan analisis yang lebih mendalam?
Sejak itu, inti dan infrastruktur telah berkembang. Kami menambahkan dukungan untuk Visual Studio 2017 dan Roslyn 2.0, dan integrasi mendalam dengan MSBuild. Artikel oleh rekan saya Paul Eremeev "
Dukungan Visual Studio 2017 dan Roslyn 2.0 di PVS-Studio: kadang-kadang itu tidak mudah untuk menggunakan solusi yang sudah jadi seperti yang tampak " menggambarkan pendekatan kami untuk integrasi dengan MSBuild dan alasan untuk keputusan ini.
Saat ini kami secara aktif bekerja untuk pindah ke Roslyn 3.0 dengan cara yang sama seperti kami awalnya mendukung Visual Studio 2017. Ini membutuhkan penggunaan toolset kami sendiri, termasuk dalam distribusi PVS-Studio sebagai "rintisan", yang merupakan MSBuild kosong File .exe. Terlepas dari kenyataan bahwa itu terlihat seperti "penopang" (MSBuild API tidak terlalu ramah untuk digunakan kembali dalam proyek-proyek pihak ketiga karena portabilitas perpustakaan rendah), pendekatan semacam itu telah membantu kami untuk secara relatif mulus mengatasi beberapa pembaruan Roslyn dalam hal Visual Studio 2017. Sampai sekarang ini membantu (bahkan dengan beberapa tantangan) untuk melewati pembaruan Visual Studio 2019 dan mempertahankan kompatibilitas mundur penuh dan kinerja untuk sistem dengan versi MSBuild yang lebih lama.
Inti penganalisa juga telah mengalami sejumlah perbaikan. Salah satu fitur utama adalah analisis interprocedural lengkap dengan pertimbangan nilai-nilai metode input dan output, mengevaluasi (tergantung pada parameter ini) jangkauan cabang eksekusi dan titik pengembalian.
Kami sedang dalam perjalanan untuk menyelesaikan tugas pemantauan parameter di dalam metode (misalnya, dereferensi yang berpotensi berbahaya) bersama dengan menyimpan anotasi otomatis mereka. Untuk diagnostik yang menggunakan mekanisme aliran data, ini akan memungkinkan mempertimbangkan situasi berbahaya, terjadi ketika melewati parameter dalam suatu metode. Sebelum ini, ketika menganalisis tempat berbahaya seperti itu, peringatan tidak dibuat, karena kami tidak bisa tahu tentang semua nilai input yang mungkin dalam metode seperti itu. Sekarang kita dapat mendeteksi bahaya, karena di semua tempat memanggil metode ini, parameter input ini akan diperhitungkan.
Catatan: Anda dapat membaca tentang mekanisme analisa dasar, seperti aliran data dan lainnya dalam artikel "
Teknologi yang digunakan dalam penganalisa kode PVS-Studio untuk menemukan bug dan potensi kerentanan ".
Analisis antar-prosedur dalam PVS-Studio C # tidak dibatasi oleh parameter input, maupun kedalaman. Satu-satunya batasan adalah metode virtual di kelas, terbuka untuk warisan, serta masuk ke rekursi (analisis berhenti ketika tersandung panggilan berulang metode yang sudah dievaluasi). Dengan demikian, metode rekursif itu sendiri pada akhirnya akan dievaluasi dengan asumsi bahwa nilai pengembalian rekursi tidak diketahui.
Fitur baru yang hebat lainnya dalam penganalisis C # telah mempertimbangkan kemungkinan dereferensi penunjuk yang berpotensi nol. Sebelum itu, penganalisa mengeluh tentang kemungkinan pengecualian referensi nol, dipastikan bahwa di semua cabang eksekusi nilai variabel akan menjadi nol. Tentu saja, itu salah dalam beberapa kasus, itu sebabnya diagnostik
V3080 sebelumnya disebut referensi nol potensial.
Sekarang penganalisa menyadari fakta bahwa variabel bisa nol di salah satu cabang eksekusi (misalnya, di bawah kondisi
if tertentu). Jika ia melihat akses ke variabel seperti itu tanpa centang, itu akan mengeluarkan peringatan V3080, tetapi pada tingkat kepastian yang lebih rendah, daripada jika ia melihat nol di semua cabang. Seiring dengan peningkatan analisis antarproedural, mekanisme semacam itu memungkinkan menemukan kesalahan yang sangat sulit dideteksi. Berikut ini sebuah contoh - bayangkan rantai panjang pemanggilan metode, yang terakhir tidak Anda kenal. Dalam keadaan tertentu, ia mengembalikan nol di blok
tangkap , tetapi Anda belum terlindungi dari ini, karena Anda belum tahu. Dalam hal ini, penganalisa hanya mengeluh, ketika ia benar-benar melihat penugasan nol. Dalam pandangan kami, ini secara kualitatif membedakan pendekatan kami dari fitur C # 8.0 sebagai referensi jenis yang dapat dibatalkan, yang, pada kenyataannya, terbatas pada pengaturan pemeriksaan untuk null untuk setiap metode. Namun, kami menyarankan alternatif - untuk melakukan pemeriksaan hanya di tempat-tempat di mana null benar-benar dapat terjadi, dan penganalisa kami sekarang dapat mencari kasus-kasus tersebut.
Jadi, jangan menunda poin utama terlalu lama dan menyalahkan orang lain - menganalisis hasil pemeriksaan Roslyn. Pertama, mari kita pertimbangkan kesalahan yang ditemukan karena fitur-fitur yang dijelaskan di atas. Singkatnya, ada cukup banyak peringatan untuk kode Roslyn kali ini. Saya pikir ini terkait dengan fakta bahwa platform sangat aktif berkembang (pada titik ini basis kode sekitar 2.770.000 baris tidak termasuk kosong), dan kami belum menganalisis proyek ini lama. Namun demikian, tidak ada begitu banyak kesalahan kritis, sedangkan mereka yang paling menarik untuk artikel ini. Seperti biasa, saya mengecualikan tes dari cek, ada cukup banyak di Roslyn.
Saya akan mulai dengan kesalahan V3080 tingkat menengah kepastian, di mana penganalisa telah mendeteksi kemungkinan akses dengan referensi nol, tetapi tidak dalam semua kasus yang mungkin (kode cabang).
Kemungkinan null dereference - MediumV3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'saat ini'. CSharpSyntaxTreeFactoryService.PositionalSyntaxReference.cs 70
private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....))
Mari kita pertimbangkan metode
GetNode . Alat analisis menyarankan akses dengan referensi nol dimungkinkan dalam kondisi blok
sementara . Variabel diberikan nilai dalam tubuh blok
sementara , yang merupakan hasil dari metode
AsNode . Dalam beberapa kasus, nilai ini akan menjadi
nol . Contoh yang baik dari analisis antar prosedur dalam aksi.
Sekarang mari kita pertimbangkan kasus serupa, di mana analisis antar-prosedur dilakukan melalui dua pemanggilan metode.
V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'direktori'. CommonCommandLineParser.cs 911
private IEnumerable<CommandLineSourceFile> ExpandFileNamePattern(string path, string baseDirectory, ....) { string directory = PathUtilities.GetDirectoryName(path); .... var resolvedDirectoryPath = (directory.Length == 0) ?
Variabel
direktori dalam tubuh metode
ExpandFileNamePattern mendapatkan nilai dari metode
GetDirectoryName (string) . Itu, pada gilirannya, mengembalikan hasil metode
GetDirectoryName kelebihan
(string, bool) yang nilainya bisa
nol . Karena
direktori variabel digunakan tanpa pemeriksaan pendahuluan untuk null dalam tubuh metode
ExpandFileNamePattern - kita dapat menyatakan penganalisa dengan benar tentang mengeluarkan peringatan. Ini adalah konstruksi yang berpotensi tidak aman.
Fragmen kode lain dengan kesalahan V3080, lebih tepatnya, dengan dua kesalahan, dikeluarkan untuk satu baris kode. Analisis antar-prosedur tidak diperlukan di sini.
V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'spanStartLocation'. TestWorkspace.cs 574
V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa '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,
Variabel
spanStartLocation dan
spanEndLocationExclusive adalah dari tipe
int nullable dan diinisialisasi dengan
nol . Lebih lanjut di sepanjang kode mereka dapat diberi nilai, tetapi hanya dalam kondisi tertentu. Dalam beberapa kasus, nilainya tetap
nol . Setelah itu, variabel-variabel ini diakses dengan referensi tanpa pemeriksaan awal untuk null, yang menunjukkan penganalisa.
Kode Roslyn mengandung cukup banyak kesalahan seperti itu, lebih dari 100. Seringkali pola kesalahan ini sama. Ada beberapa jenis metode umum, yang berpotensi mengembalikan
nol . Hasil dari metode ini digunakan di banyak tempat, kadang-kadang melalui puluhan panggilan metode menengah atau pemeriksaan tambahan. Penting untuk dipahami bahwa kesalahan ini tidak fatal, tetapi mereka berpotensi menyebabkan akses dengan referensi nol. Meskipun mendeteksi kesalahan seperti itu cukup menantang. Itu sebabnya dalam beberapa kasus seseorang harus mempertimbangkan kode refactoring, dalam hal ini jika
null kembali, metode ini akan mengeluarkan pengecualian. Jika tidak, Anda dapat mengamankan kode Anda hanya dengan pemeriksaan umum yang cukup melelahkan dan terkadang tidak dapat diandalkan. Bagaimanapun, jelas bahwa setiap kasus spesifik memerlukan solusi berdasarkan spesifikasi proyek.
Catatan Kebetulan, bahwa pada titik tertentu tidak ada kasus seperti itu (input data), ketika metode mengembalikan
nol dan tidak ada kesalahan aktual. Namun, kode semacam itu masih tidak dapat diandalkan, karena semuanya dapat berubah ketika memperkenalkan beberapa perubahan kode.
Untuk menjatuhkan subjek
V3080 , mari kita lihat kesalahan yang jelas dari tingkat kepastian tinggi, ketika akses dengan referensi nol adalah yang paling mungkin atau bahkan tak terhindarkan.
Kemungkinan null dereference - TinggiV3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa '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; } .... }
Karena kesalahan ketik dalam kondisi (
&& digunakan sebagai ganti operator
|| ), kode bekerja secara berbeda dari yang dimaksudkan dan akses ke variabel
collectionType.Type akan dieksekusi ketika itu
nol . Kondisi tersebut harus diperbaiki sebagai berikut:
if (collectionType.Type == null || collectionType.Type.TypeKind == TypeKind.Error) ....
Ngomong-ngomong, hal-hal dapat terungkap dengan cara lain: di bagian pertama kondisi operator
== dan
! = Kacau
. Maka kode yang benar akan terlihat seperti ini:
if (collectionType.Type != null && collectionType.Type.TypeKind == TypeKind.Error) ....
Versi kode ini kurang logis, tetapi juga memperbaiki kesalahan. Solusi akhir ada pada penulis proyek untuk memutuskan.
Kesalahan serupa lainnya.
V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'tindakan'. 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}'."); } .... }
Kesalahan dibuat saat membuat pesan untuk pengecualian. Ini diikuti oleh upaya untuk mengakses properti
action.DisplayText melalui variabel
tindakan , yang dikenal sebagai
nol .
Di sinilah kesalahan V3080 terakhir dari tingkat Tinggi.
V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'tipe'. ObjectFormatterHelpers.cs 91
private static bool IsApplicableAttribute( TypeInfo type, TypeInfo targetType, string targetTypeName) { return type != null && AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName; }
Metodenya cukup kecil, jadi saya kutip sepenuhnya. Kondisi di blok
kembali salah. Dalam beberapa kasus, ketika mengakses
type.FullName , pengecualian dapat terjadi. Saya akan menggunakan tanda kurung untuk memperjelas (mereka tidak akan mengubah perilaku):
return (type != null && AreEquivalent(targetType, type)) || (targetTypeName != null && type.FullName == targetTypeName);
Menurut operasi yang diutamakan, kode akan bekerja persis seperti ini. Jika variabel
tipe adalah
nol , kita akan jatuh ke cek lain, di mana kita akan menggunakan referensi
tipe nol, setelah memeriksa variabel
targetTypeName untuk
null . Kode mungkin diperbaiki, misalnya, sebagai berikut:
return type != null && (AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName);
Saya pikir, itu sudah cukup untuk meninjau kesalahan V3080. Sekarang saatnya untuk melihat hal-hal menarik lainnya yang berhasil ditemukan oleh alat analisa PVS-Studio.
Salah ketikV3005 Variabel 'SourceCodeKind' ditugaskan untuk dirinya sendiri. DynamicFileInfo.cs 17
internal sealed class DynamicFileInfo { .... public DynamicFileInfo( string filePath, SourceCodeKind sourceCodeKind, TextLoader textLoader, IDocumentServiceProvider documentServiceProvider) { FilePath = filePath; SourceCodeKind = SourceCodeKind;
Karena kegagalan penamaan variabel, kesalahan ketik dibuat di konstruktor kelas
DynamicFileInfo . Bidang
SourceCodeKind diberi nilainya sendiri alih-alih menggunakan parameter
sourceCodeKind . Untuk meminimalkan kemungkinan kesalahan tersebut, kami sarankan Anda menggunakan awalan garis bawah ke nama parameter dalam kasus tersebut. Berikut adalah contoh versi kode yang diperbaiki:
public DynamicFileInfo( string _filePath, SourceCodeKind _sourceCodeKind, TextLoader _textLoader, IDocumentServiceProvider _documentServiceProvider) { FilePath = _filePath; SourceCodeKind = _sourceCodeKind; TextLoader = _textLoader; DocumentServiceProvider = _documentServiceProvider; }
KetidaksengajaanV3006 Objek telah dibuat tetapi tidak sedang digunakan. Kata kunci 'melempar' bisa hilang: lempar InvalidOperationException (FOO) baru. ProjectBuildManager.cs 61
~ProjectBuildManager() { if (_batchBuildStarted) { new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } }
Di bawah kondisi tertentu, destructor harus melempar eksepsi, tetapi itu tidak terjadi ketika objek eksepsi hanya dibuat. Kata kunci
lemparan tidak terjawab. Ini adalah versi kode yang diperbaiki:
~ProjectBuildManager() { if (_batchBuildStarted) { throw new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } }
Masalah dengan destruktor di C # dan melemparkan pengecualian dari mereka adalah topik untuk diskusi lain, yang berada di luar cakupan artikel ini.
Ketika hasilnya tidak pentingMetode, yang menerima nilai yang sama dalam semua kasus, memicu sejumlah peringatan
V3009 . Dalam beberapa kasus bisa jadi tidak kritis atau nilai kembaliannya tidak dicentang dalam kode panggilan. Saya melewatkan peringatan seperti itu. Tetapi beberapa cuplikan kode tampak mencurigakan. Ini salah satunya:
V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama dari 'benar'. GoToDefinitionCommandHandler.cs 62
internal bool TryExecuteCommand(....) { .... using (context.OperationContext.AddScope(....)) { if (....) { return true; } } .... return true; }
Metode
TryExecuteCommand mengembalikan apa pun kecuali
benar . Dalam melakukannya, dalam kode panggilan nilai yang dikembalikan terlibat dalam beberapa pemeriksaan.
public bool ExecuteCommand(....) { .... if (caretPos.HasValue && TryExecuteCommand(....)) { .... } .... }
Sulit untuk mengatakan dengan tepat sejauh mana perilaku seperti itu berbahaya. Tetapi jika hasilnya tidak diperlukan, mungkin jenis nilai kembali harus diubah menjadi batal dan orang harus mengedit kecil dalam metode panggilan. Ini akan membuat kode lebih mudah dibaca dan aman.
Peringatan penganalisa serupa:
- V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama dari 'benar'. CommentUncommentSelectionCommandHandler.cs 86
- V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama dari 'benar'. RenameTrackingTaggerProvider.RenameTrackingCommitter.cs 99
- V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama dari 'benar'. JsonRpcClient.cs 138
- V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama dari 'benar'. AbstractFormatEngine.OperationApplier.cs 164
- V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama 'salah'. TriviaDataFactory.CodeShapeAnalyzer.cs 254
- V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama dari 'benar'. ObjectList.cs 173
- V3009 Aneh bahwa metode ini selalu mengembalikan satu dan nilai yang sama dari 'benar'. ObjectList.cs 249
Memeriksa hal yang salahV3019 Mungkin variabel yang salah dibandingkan dengan nol setelah konversi jenis menggunakan kata kunci 'sebagai'. Periksa variabel 'nilai', 'valueToSerialize'. RoamingVisualStudioProfileOptionPersister.cs 277
public bool TryPersist(OptionKey optionKey, object value) { .... var valueToSerialize = value as NamingStylePreferences; if (value != null) { value = valueToSerialize.CreateXElement().ToString(); } .... }
Variabel
nilai dilemparkan ke tipe
NamingStylePreferences . Masalahnya ada di cek yang mengikuti ini. Bahkan jika variabel
nilai bukan nol, itu tidak menjamin bahwa casting tipe berhasil dan
valueToSerialize tidak mengandung
nol . Kemungkinan melempar pengecualian
NullReferenceException . Kode perlu diperbaiki sebagai berikut:
var valueToSerialize = value as NamingStylePreferences; if (valueToSerialize != null) { value = valueToSerialize.CreateXElement().ToString(); }
Bug serupa lainnya:
V3019 Mungkin variabel yang salah dibandingkan dengan nol setelah konversi jenis menggunakan kata kunci 'sebagai'. Periksa variabel 'columnState', 'columnState2'. StreamingFindUsagesPresenter.cs 181
private void SetDefinitionGroupingPriority(....) { .... foreach (var columnState in ....) { var columnState2 = columnState as ColumnState2; if (columnState?.Name ==
Variabel
columnState dilemparkan ke jenis
ColumnState2 . Namun, hasil operasi, yang merupakan variabel
columnState2, tidak diperiksa untuk
nol lebih lanjut. Sebagai gantinya, variabel
columnState diperiksa menggunakan operator
null bersyarat. Mengapa kode ini berbahaya? Sama seperti pada contoh sebelumnya, casting dengan operator
as mungkin gagal dan variabel akan menjadi
nol yang akan menghasilkan pengecualian. Ngomong-ngomong, kesalahan ketik bisa disalahkan di sini. Lihatlah kondisi di blok
if .
Mungkin, bukannya
columnState? .Nama penulis ingin menulis
columnState2? .Name . Sangat mungkin, mengingat nama variabel yang agak salah,
columnState dan
columnState2.Cek berlebihanCukup banyak peringatan (lebih dari 100) dikeluarkan pada konstruksi yang tidak kritis, tetapi berpotensi tidak aman terkait dengan cek yang berlebihan. Misalnya, ini salah satunya.
Ekspresi
V3022 'navInfo == null' selalu salah. 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)
Mungkin tidak ada bug yang sebenarnya di sini. Itu hanya alasan yang bagus untuk menunjukkan "analisis antar-proses + analisis aliran data" yang bekerja dengan baik. Alat analisis menyarankan cek kedua
navInfo == null berlebihan. Memang, sebelum itu nilai yang ditetapkan untuk
navInfo akan diperoleh dari metode
libraryService.NavInfoFactory.CreateForProject , yang akan membangun dan mengembalikan objek baru dari kelas
NavInfo . Tidak mungkin itu akan mengembalikan
nol . Di sini muncul pertanyaan, mengapa analis tidak mengeluarkan peringatan untuk cek pertama
navInfo == null ? Ada beberapa alasan. Pertama, jika variabel
simbol nol , nilai
navInfo akan tetap menjadi referensi nol juga. Kedua, bahkan jika
navInfo mendapatkan nilai dari metode
ibraryService.NavInfoFactory.CreateForSymbol , nilai ini juga bisa menjadi
nol . Jadi, pemeriksaan pertama
navInfo == null benar-benar diperlukan.
Cek tidak mencukupiSekarang situasi sebaliknya dari yang dibahas di atas. Beberapa peringatan
V3042 dipicu untuk kode, di mana akses dengan referensi nol dimungkinkan. Bahkan satu atau dua cek kecil bisa memperbaiki semuanya.
Mari kita pertimbangkan fragmen kode lain yang menarik, yang memiliki dua kesalahan seperti itu.
V3042 Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'penerima' Binder_Expressions.cs 7770
V3042 Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'penerima' Binder_Expressions.cs 7776
private BoundExpression GetReceiverForConditionalBinding( ExpressionSyntax binding, DiagnosticBag diagnostics) { .... BoundExpression receiver = this.ConditionalReceiverExpression; if (receiver?.Syntax !=
Variabel
penerima mungkin nol. Penulis kode mengetahui hal ini, karena ia menggunakan operator
null bersyarat dalam kondisi blok
if untuk mengakses
receiver ? .
Sintaks . Selanjutnya variabel
penerima digunakan tanpa pemeriksaan untuk mengakses
receiver. Jenis ,
penerima .
Sintaks dan
penerima .
Memiliki Kesalahan . Kesalahan ini harus diperbaiki:
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; }
Kita juga harus memastikan bahwa konstruktor mendukung mendapatkan nilai
nol untuk parameternya atau kita perlu melakukan refactoring tambahan.
Kesalahan serupa lainnya:
- V3042 Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'mengandungType' SyntaxGeneratorExtensions_Negate.cs 240
- V3042 Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'ekspresi' ExpressionSyntaxExtensions.cs 349
- V3042 Kemungkinan NullReferenceException. '?.' dan '.' operator digunakan untuk mengakses anggota objek 'ekspresi' ExpressionSyntaxExtensions.cs 349
Kesalahan dalam kondisiV3057 Fungsi 'Substring' dapat menerima nilai '-1' sementara nilai non-negatif diharapkan. Periksa argumen kedua. CommonCommandLineParser.cs 109
internal static bool TryParseOption(....) { .... if (colon >= 0) { name = arg.Substring(1, colon - 1); value = arg.Substring(colon + 1); } .... }
Dalam hal jika variabel
usus besar adalah 0, yang baik-baik saja sesuai dengan kondisi dalam kode, metode
Substring akan melempar pengecualian. Ini harus diperbaiki:
if (colon > 0)
Kemungkinan salah ketikV3065 Parameter 't2' tidak digunakan di dalam tubuh metode. 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; }); .... }
Ekspresi lambda menerima dua parameter: t1 dan t2. Namun, hanya t1 yang digunakan. Itu terlihat mencurigakan, dengan mempertimbangkan fakta betapa mudahnya melakukan kesalahan ketika menggunakan variabel dengan nama-nama tersebut.
KetidaksengajaanV3083 Doa yang tidak aman dari acara 'TagsChanged', NullReferenceException dimungkinkan. Pertimbangkan menugaskan acara ke variabel lokal sebelum menjalankannya. PreviewUpdater.Tagger.cs 37
public void OnTextBufferChanged() { if (PreviewUpdater.SpanToShow != default) { if (TagsChanged != null) { var span = _textBuffer.CurrentSnapshot.GetFullSpan(); TagsChanged(this, new SnapshotSpanEventArgs(span));
Acara
TagsChanged dipanggil dengan cara yang tidak aman. Antara memeriksa
nol dan menjalankan acara, seseorang dapat berhenti berlangganan darinya, maka pengecualian akan dilemparkan. Selanjutnya, operasi lain dilakukan di tubuh blok
if tepat sebelum memanggil acara. Saya menyebut kesalahan ini "Kelalaian", karena acara ini ditangani lebih hati-hati di tempat lain, sebagai berikut:
private void OnTrackingSpansChanged(bool leafChanged) { var handler = TagsChanged; if (handler != null) { var snapshot = _buffer.CurrentSnapshot; handler(this, new SnapshotSpanEventArgs(snapshot.GetFullSpan())); } }
Penggunaan variabel
penangan tambahan mencegah masalah. Dalam metode
OnTextBufferChanged, kita harus mengedit agar dapat menangani acara dengan aman.
Rentang berpotonganV3092 Range intersection dimungkinkan dalam ekspresi bersyarat. Contoh: if (A> 0 && A <5) {...} lain jika (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 { .... } }
Untuk pemahaman yang lebih baik, izinkan saya menulis ulang kode ini, mengubah nama konstanta dengan nilai aktualnya:
internal void EmitLongConstant(long value) { if (value >= -2147483648 && value <= 2147483648) { .... } else if (value >= 0 && value <= 4294967295) { .... } else { .... } }
Mungkin, tidak ada kesalahan nyata, tetapi kondisinya terlihat aneh. Bagian kedua (
selain itu ) akan dieksekusi hanya untuk rentang dari 2147483648 +1 untuk 4294967295.
Beberapa peringatan serupa lainnya:
- V3092 Range intersection dimungkinkan dalam ekspresi bersyarat. Contoh: if (A> 0 && A <5) {...} lain jika (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 109
- V3092 Range intersection dimungkinkan dalam ekspresi bersyarat. Contoh: if (A> 0 && A <5) {...} lain jika (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 66
Lebih lanjut tentang cek nol (atau kurang dari itu)Beberapa kesalahan
V3095 pada pemeriksaan variabel untuk null tepat setelah penggunaannya. Yang pertama ambigu, mari kita pertimbangkan kodenya.
V3095 Objek 'displayName' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 498, 503. FusionAssemblyIdentity.cs 498
internal static IAssemblyName ToAssemblyNameObject(string displayName) { if (displayName.IndexOf('\0') >= 0) { return null; } Debug.Assert(displayName != null); .... }
Diasumsikan bahwa
displayName referensi bisa nol. Untuk ini, cek
Debug.Assert dilakukan. Tidak jelas mengapa ia pergi setelah menggunakan string. Ini juga harus diperhitungkan bahwa untuk konfigurasi yang berbeda dari Debug, kompiler akan menghapus
Debug . Masukkan sama sekali
. Apakah itu berarti mendapatkan referensi nol hanya mungkin untuk Debug? Jika tidak demikian, mengapa penulis melakukan pemeriksaan
string. IsNullOrEmpty (string) , misalnya. Ini adalah pertanyaan untuk pembuat kode.
Kesalahan berikut lebih jelas.
V3095 Objek 'scriptArgsOpt' digunakan sebelum diverifikasi terhadap null. Periksa baris: 321, 325. CommonCommandLineParser.cs 321
internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt.Add(arg);
Saya pikir kode ini tidak memerlukan penjelasan apa pun. Biarkan saya memberi Anda versi tetap:
internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt?.Add(arg); continue; } if (scriptArgsOpt != null) { .... } .... } }
Dalam kode Roslyn, ada 15 kesalahan serupa lainnya:
- V3095 Objek 'LocalFunctions' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 289, 317. ControlFlowGraphBuilder.RegionBuilder.cs 289
- V3095 Objek 'resolution.OverloadResolutionResult' digunakan sebelum diverifikasi terhadap null. Periksa baris: 579, 588. Binder_Invocation.cs 579
- V3095 Objek 'resolution.MethodGroup' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 592, 621. Binder_Invocation.cs 592
- V3095 Objek 'touchFilesLogger' digunakan sebelum diverifikasi terhadap null. Periksa baris: 111, 126. CSharpCompiler.cs 111
- V3095 Objek 'newExceptionRegionsOpt' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 736, 743. AbstrakEditAndLanjutkanAnalyzer.cs 736
- V3095 Objek 'simbol' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 422, 427. AbstractGenerateConstructorService.Editor.cs 422
- V3095 Objek '_state.BaseTypeOrInterfaceOpt' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 132, 140. AbstractGenerateTypeService.GenerateNamedType.cs 132
- V3095 Objek 'elemen' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 232, 233. ProjectUtil.cs 232
- V3095 Objek 'bahasa' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 22, 28. ExportCodeCleanupProvider.cs 22
- V3095 Objek 'memberType' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 183, 184. SyntaxGeneratorExtensions_CreateGetHashCodeMethod.cs 183
- V3095 Objek 'validTypeDeclarations' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 223, 228. SyntaxTreeExtensions.cs 223
- V3095 Objek 'teks' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 376, 385. MSBuildWorkspace.cs 376
- V3095 Objek 'nameOrMemberAccessExpression' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 206, 223. CSharpGenerateTypeService.cs 206
- V3095 Objek 'simpleName' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 83, 85. CSharpGenerateMethodService.cs 83
- V3095 Objek 'opsi' digunakan sebelum diverifikasi terhadap nol. Periksa baris: 23, 28. OptionKey.cs 23
Mari kita pertimbangkan kesalahan
V3105 . Di sini operator
nol bersyarat digunakan ketika menginisialisasi variabel, tetapi selanjutnya variabel digunakan tanpa memeriksa
nol .
Dua peringatan menunjukkan kesalahan berikut:
V3105 Variabel 'documentId' digunakan setelah ditugaskan melalui operator kondisional nol. NullReferenceException dimungkinkan. CodeLensReferencesService.cs 138
V3105 Variabel 'documentId' digunakan setelah ditugaskan melalui operator kondisional nol. NullReferenceException dimungkinkan. CodeLensReferencesService.cs 139
private static async Task<ReferenceLocationDescriptor> GetDescriptorOfEnclosingSymbolAsync(....) { .... var documentId = solution.GetDocument(location.SourceTree)?.Id; return new ReferenceLocationDescriptor( .... documentId.ProjectId.Id, documentId.Id, ....); }
Variabel
documentId dapat diinisialisasi dengan
nol . Akibatnya, membuat objek
ReferenceLocationDescriptor akan menghasilkan melempar pengecualian. Kode harus diperbaiki:
return new ReferenceLocationDescriptor( .... documentId?.ProjectId.Id, documentId?.Id, ....);
Pengembang juga harus mencakup kemungkinan variabel, diteruskan ke konstruktor, menjadi
nol.Kesalahan serupa lainnya dalam kode:
- V3105 Variabel 'simbol' digunakan setelah ditugaskan melalui operator kondisional nol. NullReferenceException dimungkinkan. SymbolFinder_Hierarchy.cs 44
- V3105 Variabel 'simbol' digunakan setelah ditugaskan melalui operator kondisional nol. NullReferenceException dimungkinkan. SymbolFinder_Hierarchy.cs 51
Prioritas dan tanda kurungV3123 Mungkin
operator '?:' Bekerja dengan cara yang berbeda dari yang diharapkan. Prioritasnya lebih rendah daripada prioritas operator lain dalam kondisinya. 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); }
Kondisi di blok kembali dievaluasi bukan seperti yang dimaksudkan pengembang. Diasumsikan bahwa kondisi pertama adalah
_kind == other._kin d, (ini sebabnya setelah kondisi ini ada jeda baris), dan setelah itu blok kondisi dengan operator "
? " Akan dievaluasi secara berurutan. Faktanya, kondisi pertama adalah
_kind == other._kind && (_oldNode == null) . Ini disebabkan oleh fakta bahwa operator
&& memiliki prioritas lebih tinggi daripada operator "
? ". Untuk memperbaikinya, pengembang harus mengambil semua ekspresi operator "
? " Dalam tanda kurung:
return _kind == other._kind && ((_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode)) && ((_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode));
Itu menyimpulkan deskripsi saya tentang kesalahan yang ditemukan.
KesimpulanTerlepas dari sejumlah besar kesalahan, yang saya berhasil temukan, dalam hal ukuran kode proyek Roslyn (2.770.000 baris), itu tidak terlalu banyak. Seperti yang ditulis Andrey dalam artikel sebelumnya, saya juga siap untuk mengakui kualitas tinggi dari proyek ini.
Saya ingin mencatat bahwa pemeriksaan kode sesekali tidak ada hubungannya dengan metodologi analisis statis dan hampir tidak membantu. Analisis statis harus diterapkan secara teratur, dan tidak berdasarkan kasus per kasus. Dengan cara ini, banyak kesalahan akan diperbaiki pada tahap paling awal, dan karenanya biaya untuk memperbaikinya akan sepuluh kali lebih sedikit. Gagasan ini dituangkan secara lebih rinci dalam
catatan kecil ini, silakan, periksa.
Anda dapat memeriksa sendiri beberapa kesalahan di proyek ini dan di yang lain. Untuk melakukan ini, Anda hanya perlu
mengunduh dan mencoba penganalisa kami.