Kontribusi kecil untuk perang melawan platform kebun binatang UI Avalonia

Gambar 2

Artikel ini adalah hasil dari pengecekan proyek Avalonia UI menggunakan analisa statis PVS-Studio. Avalonia UI adalah open-source, cross-platform platform antarmuka pengguna berbasis XAML. Ini adalah salah satu proyek signifikan secara teknologi dalam sejarah .NET, karena memungkinkan Anda untuk membuat antarmuka lintas-platform berdasarkan pada sistem WPF. Saya harap artikel ini akan membantu penulis memperbaiki beberapa kesalahan dan meyakinkan mereka untuk menggunakan analisa statis di masa depan.

Tentang Avalonia UI


Proyek Avalonia UI (sebelumnya bernama Perspex) menyediakan kemampuan untuk membuat antarmuka pengguna yang berjalan pada Windows, Linux, dan MacOS. Ada juga dukungan eksperimental untuk Android dan iOS saat ini. Avalonia UI bukan pembungkus pembungkus, tetapi mengacu pada API asli. Berbeda dengan Formulir Xamarin, yang membungkus pembungkus Xamarin. Di salah satu video demo, saya dikejutkan oleh kemampuan untuk membawa kontrol ke konsol Debian. Selain itu, proyek ini menawarkan lebih banyak fitur untuk tata letak dan desain daripada desainer antarmuka konvensional, berkat penggunaan markup XAML.

Proyek yang sudah menggunakan Avalonia UI termasuk AvalonStudio (IDE lintas-platform untuk pengembangan di C # dan C / C ++) dan Core2D (editor diagram dan diagram 2D). Sebagai proyek komersial, Anda dapat membawa Wasabi Wallet (dompet Bitcoin).

Perjuangan melawan kebutuhan untuk memiliki beberapa perpustakaan yang berbeda saat membuat aplikasi lintas platform sangat penting. Kami ingin membantu proyek, dan saya mengunduh proyek dan memeriksanya dengan penganalisa kami. Saya berharap bahwa penulis akan memperhatikan artikel ini dan membuat perubahan yang diperlukan pada kode, atau mungkin memperkenalkan analisis statis reguler ke dalam proses pengembangan. Untuk melakukan ini, mereka dapat mengambil keuntungan dari opsi lisensi bebas PVS-Studio untuk proyek sumber terbuka. Penggunaan penganalisis statis secara teratur membantu menghindari banyak masalah dan mengurangi biaya untuk mendeteksi dan memperbaiki banyak kesalahan.

Hasil Validasi


PVS-Studio Warning: V3001 Ada sub-ekspresi yang identik 'controlledFag' di sebelah kiri dan di sebelah kanan operator '^'. WindowImpl.cs 975TwitterClientMessageHandler.cs 52

private void UpdateWMStyles(Action change) { .... var style = (WindowStyles)GetWindowLong(....); .... style = style | controlledFlags ^ controlledFlags; .... } 

Saya akan mulai secara simbolis dengan diagnostik C # pertama kami. Analyzer mendeteksi penggunaan aneh dari operator bitwise OR. Mari saya jelaskan pada angka apa yang terjadi di sini:

ekspresi

 1100 0011 | 1111 0000 ^ 1111 0000 

mirip dengan ini:

 1100 0011 | 0000 0000 

ATAU eksklusif ("^") memiliki prioritas lebih tinggi daripada ATAU bitwise ("|"). Kemungkinan besar, urutan operasi yang berbeda tersirat di sini. Dalam hal ini, Anda harus menggunakan ekspresi pertama dalam tanda kurung:

 private void UpdateWMStyles(Action change) { .... style = (style | controlledFlags) ^ controlledFlags; .... } 

Sebelum dua peringatan berikutnya saya harus mengakui: positif palsu. Ini disebabkan oleh penggunaan API publik, metode TransformToVisual . Dalam kasus kami, VisualRoot selalu menjadi induk dari visual . Saya tidak mengerti ini ketika menganalisis respons, penulis proyek mengatakan kepada saya setelah menulis artikel. Jadi pengeditan yang diusulkan dalam artikel ini bukan untuk perlindungan terhadap kejatuhan yang sebenarnya, tetapi terhadap revisi potensial yang merusak logika ini.

PVS-Studio Warning: V3080 Kemungkinan null dereference dari nilai pengembalian metode. Pertimbangkan untuk memeriksa: TranslatePoint (...). VisualExtensions.cs 23

 public static Point PointToClient(this IVisual visual, PixelPoint point) { var rootPoint = visual.VisualRoot.PointToClient(point); return visual.VisualRoot.TranslatePoint(rootPoint, visual).Value; } 

Metode kecil. Penganalisa menganggap bahwa mendereferensi hasil panggilan ke TranslatePoint tidak aman. Lihatlah metode ini:

 public static Point? TranslatePoint(this IVisual visual, Point point, IVisual relativeTo) { var transform = visual.TransformToVisual(relativeTo); if (transform.HasValue) { return point.Transform(transform.Value); } return null; } 

Memang, ada null kembali.

Metode ini memiliki 6 panggilan. Dalam tiga kasus, nilainya diperiksa, dan selebihnya PVS-Studio mendeteksi potensi dereferencing dan mengeluarkan peringatan. Saya mengutip yang pertama di atas, dan dua peringatan lainnya ada di sini:

  • V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'p'. VisualExtensions.cs 35
  • V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'controlPoint'. Scene.cs 176

Saya mengusulkan untuk memperbaikinya dengan analogi dengan menambahkan <Struct> Nullable. Pemeriksaan nilai dalam metode PointToClient :

 public static Point PointToClient(this IVisual visual, PixelPoint point) { var rootPoint = visual.VisualRoot.PointToClient(point); if (rootPoint.HasValue) return visual.VisualRoot.TranslatePoint(rootPoint, visual).Value; else throw ....; } 

PVS-Studio Warning: V3080 Kemungkinan null dereference dari nilai pengembalian metode. Pertimbangkan untuk memeriksa: TransformToVisual (...). ViewportManager.cs 381

Kasus yang sangat mirip dengan contoh sebelumnya:

 private void OnEffectiveViewportChanged(TransformedBounds? bounds) { .... var transform = _owner.GetVisualRoot().TransformToVisual(_owner).Value; .... } 

Metode TransformToVisual terlihat seperti ini:

 public static Matrix? TransformToVisual(this IVisual from, IVisual to) { var common = from.FindCommonVisualAncestor(to); if (common != null) { .... } return null; } 

Omong-omong, metode FindCommonVisualAncestor memang dapat mengembalikan nol sebagai nilai default untuk jenis referensi:

 public static IVisual FindCommonVisualAncestor(this IVisual visual, IVisual target) { Contract.Requires<ArgumentNullException>(visual != null); return ....FirstOrDefault(); } 

Metode TransformToVisual digunakan di sembilan tempat, ada cek di tujuh. Peringatan pertama untuk digunakan tanpa memeriksa lebih tinggi, dan yang terakhir ada di sini:

V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'transformasi'. MouseDevice.cs 80

Peringatan PVS-Studio: Ekspresi V3022 selalu benar. Mungkin operator '&&' harus digunakan di sini. NavigationDirection.cs 89

 public static bool IsDirectional(this NavigationDirection direction) { return direction > NavigationDirection.Previous || direction <= NavigationDirection.PageDown; } 

Pemeriksaan aneh. Dalam enumerasi NavigationDirection , ada 9 jenis dan PageDown adalah yang terakhir dari mereka. Mungkin ini tidak selalu terjadi, atau apakah asuransi terhadap penampilan SUDDEN dari opsi rujukan baru. Menurut saya cek pertama sudah cukup di sini. Saya akan menyerahkan keputusan kepada penulis proyek.

Peringatan PVS-Studio: V3066 Kemungkinan urutan argumen yang salah diteruskan ke konstruktor 'SelectionChangedEventArgs': 'dihapusSelectedItems' dan 'ditambahkanSelectedItems'. DataGridSelectedItemsCollection.cs 338

 internal SelectionChangedEventArgs GetSelectionChangedEventArgs() { .... return new SelectionChangedEventArgs (DataGrid.SelectionChangedEvent, removedSelectedItems, addedSelectedItems) { Source = OwningGrid }; } 

Dalam hal ini, penganalisa menyarankan bahwa argumen kedua dan ketiga dari konstruktor bingung. Mari kita lihat konstruktor yang disebut:

 public SelectionChangedEventArgs(RoutedEvent routedEvent, IList addedItems, IList removedItems) : base(routedEvent) { AddedItems = addedItems; RemovedItems = removedItems; } 

Dua wadah tipe IList diterima, mudah dicampur. Dilihat oleh komentar di awal kelas, ini adalah kesalahan dalam kode kontrol yang disalin dari Microsoft dan dimodifikasi di bawah Avalonia. Tetapi bagi saya itu bermanfaat untuk memperbaiki urutan argumen metode ini, setidaknya untuk tidak mencari kemungkinan kesalahan dalam diri Anda ketika laporan bug tiba.

Penganalisa menemukan tiga kesalahan serupa:

Peringatan PVS-Studio: V3066 Kemungkinan urutan argumen yang salah diteruskan ke 'konstruktor SelectionChangedEventArgs': 'dihapus' dan 'ditambahkan'. AutoCompleteBox.cs 707

 OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added)); 

Pilihan konstruktor yang samaChangedEventArgs.

Peringatan PVS-Studio V3066 :

  • Kemungkinan urutan argumen yang salah diteruskan ke konstruktor 'ItemsRepeaterElementIndexChangedEventArgs': 'oldIndex' dan 'newIndex'. ItemsRepeater.cs 532
  • Kemungkinan urutan argumen yang salah diteruskan ke metode 'Perbarui': 'oldIndex' dan 'newIndex'. ItemsRepeater.cs 536

Dua operasi dalam satu metode panggilan acara.

 internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex) { if (ElementIndexChanged != null) { if (_elementIndexChangedArgs == null) { _elementIndexChangedArgs = new ItemsRepeaterElementIndexChangedEventArgs(element, oldIndex, newIndex); } else { _elementIndexChangedArgs.Update(element, oldIndex, newIndex); } ..... } } 

Penganalisa melihat bahwa di ItemsRepeaterElementIndexChangedEventArgs dan dalam metode Pembaruan , argumen oldIndex dan newIndex memiliki urutan berbeda:

 internal ItemsRepeaterElementIndexChangedEventArgs( IControl element, int newIndex, int oldIndex) { Element = element; NewIndex = newIndex; OldIndex = oldIndex; } internal void Update(IControl element, int newIndex, int oldIndex) { Element = element; NewIndex = newIndex; OldIndex = oldIndex; } 

Mungkin kodenya ditulis oleh programmer yang berbeda, dan untuk yang satu lebih penting apa yang terjadi, dan untuk yang lain - apa yang akan terjadi :)

Seperti dalam kasus sebelumnya, Anda tidak harus mengeditnya segera, Anda perlu memeriksa apakah memang ada kesalahan.

PVS-Studio Warning: V3004 Pernyataan 'then' setara dengan pernyataan 'else'. DataGridSortDescription.cs 235

 public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq) { if (_descending) { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } else { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } } 

Implementasi metode ThenBy yang sangat menarik. Antarmuka IEnumerable , dari mana argumen seq diwarisi , memiliki metode ThenBy ; Saya kira penggunaannya tersirat. Seperti ini:

 public override IOrderedEnumerable<object> ThenBy(IOrderedEnumerable<object> seq) { if (_descending) { return seq.ThenByDescending(o => GetValue(o), InternalComparer); } else { return seq.ThenBy(o => GetValue(o), InternalComparer); } } 

Peringatan PVS-Studio: V3106 Nilai indeks negatif yang mungkin. Nilai indeks 'indeks' bisa mencapai -1. Animator.cs 68

 protected T InterpolationHandler(double animationTime, T neutralValue) { .... if (kvCount > 2) { if (animationTime <= 0.0) { .... } else if (animationTime >= 1.0) { .... } else { int index = FindClosestBeforeKeyFrame(animationTime); firstKeyframe = _convertedKeyframes[index]; } .... } .... } 

Penganalisa percaya bahwa indeks dapat memiliki nilai -1. Variabel diperoleh dari metode FindClosestBeforeKeyFrame , lihat:

 private int FindClosestBeforeKeyFrame(double time) { for (int i = 0; i < _convertedKeyframes.Count; i++) if (_convertedKeyframes[i].Cue.CueValue > time) return i - 1; throw new Exception("Index time is out of keyframe time range."); } 

Seperti yang dapat kita lihat, kondisi diperiksa dalam loop dan nilai iterator sebelumnya dikembalikan. Kondisi ini cukup sulit untuk diverifikasi, dan saya tidak bisa mengatakan dengan tepat apa itu CueValue , tetapi menurut uraiannya dibutuhkan nilai dari 0,0 hingga 1,0. Kita dapat mengatakan sesuatu tentang waktu , ini adalah animationTime dari metode panggilan, jelas lebih dari nol dan kurang dari satu. Jika tidak, eksekusi program akan berjalan di cabang yang berbeda. Jika ini adalah metode yang dipanggil untuk membuat animasi, maka situasinya terlihat seperti bug mengambang yang bagus. Saya akan menambahkan cek untuk hasil FindClosestBeforeKeyFrame jika penanganan khusus diperlukan dalam kasus ini. Atau - jika elemen pertama tidak memenuhi beberapa kondisi lain - hapus dari loop. Tidak tahu bagaimana semua ini harus bekerja, saya akan memilih opsi kedua sebagai contoh koreksi:

 private int FindClosestBeforeKeyFrame(double time) { for (int i = 1; i < _convertedKeyframes.Count; i++) if (_convertedKeyframes[i].Cue.CueValue > time) return i - 1; throw new Exception("Index time is out of keyframe time range."); } 

Peringatan PVS-Studio: V3117 Parameter konstruktor 'ponsel' tidak digunakan. Country.cs 25

 public Country(string name, string region, int population, int area, double density, double coast, double? migration, double? infantMorality, int gdp, double? literacy, double? phones, double? birth, double? death) { Name = name; Region = region; Population = population; Area = area; PopulationDensity = density; CoastLine = coast; NetMigration = migration; InfantMortality = infantMorality; GDP = gdp; LiteracyPercent = literacy; BirthRate = birth; DeathRate = death; } 

Contoh yang baik dari perbedaan antara operasi penganalisa dan tinjauan kode manual. Tiga belas argumen konstruktor, satu tidak digunakan. Bahkan, Visual Studio juga mencatat argumen yang tidak digunakan, tetapi pada peringatan tingkat ketiga (mereka sering dinonaktifkan). Dalam hal ini, ini adalah kesalahan yang jelas, karena kelas juga memiliki tiga belas properti untuk setiap argumen, dan tidak ada nilai yang diberikan di mana pun di Ponsel . Pengeditan sudah jelas, saya tidak akan membawanya.

Peringatan PVS-Studio: V3080 Kemungkinan null dereference. Pertimbangkan untuk memeriksa 'tabItem'. TabItemContainerGenerator.cs 22

 protected override IControl CreateContainer(object item) { var tabItem = (TabItem)base.CreateContainer(item); tabItem.ParentTabControl = Owner; .... } 

Penganalisa menganggap berbahaya untuk melakukan dereferensi hasil pemanggilan CreateContainer .

Lihatlah metode ini:

 protected override IControl CreateContainer(object item) { var container = item as T; if (item == null) { return null; } else if (container != null) { return container; } else { .... return result; } } 

Penganalisis dapat melihat penugasan nol ke variabel bahkan ketika melewati nilai melalui rantai lima puluh metode. Tapi dia tidak bisa mengatakan apakah eksekusi akan dilakukan di utas ini setidaknya sekali. Ya, dan saya juga tidak bisa, secara umum ... Panggilan metode hilang di antara metode yang ditimpa dan virtual. Jadi saya sarankan agar aman dengan cek tambahan:

 protected override IControl CreateContainer(object item) { var tabItem = (TabItem)base.CreateContainer(item); if(tabItem == null) return null; tabItem.ParentTabControl = Owner; .... } 

Peringatan PVS-Studio: V3142 Kode yang tidak dapat dideteksi terdeteksi. Mungkin saja ada kesalahan. DevTools.xaml.cs 91

Saya tidak akan menulis banyak kode di sini untuk membuat intrik, saya akan mengatakan segera: peringatan itu salah. Penganalisa melihat panggilan ke metode yang melempar pengecualian tanpa syarat. Ini dia:

 public static void Load(object obj) { throw new XamlLoadException($"No precompiled XAML found for {obj.GetType()}, make sure to specify x:Class and include your XAML file as AvaloniaResource"); } 

Mustahil untuk tidak memperhatikan tiga puluh lima peringatan (!) Tentang kode yang tidak dapat dijangkau yang terletak setelah panggilan ke metode ini. Saya bertanya kepada salah satu pengembang proyek: bagaimana cara kerjanya? Dan mereka memberi tahu saya cara mengganti panggilan dari satu metode dengan panggilan yang lain menggunakan perpustakaan Mono.Cecil . Ini memungkinkan Anda untuk mengganti panggilan secara langsung dalam kode IL.

Penganalisa tidak mendukung perpustakaan ini, oleh karena itu, kami memiliki banyak kesalahan positif, jadi lebih baik untuk menonaktifkan diagnostik ini pada proyek ini. Agak memalukan untuk mengakui, sayalah yang melakukan diagnosis ini ... tetapi, seperti alat apa pun, analisis statis perlu dikonfigurasikan.

Misalnya, kami sedang mengembangkan diagnostik tentang konversi jenis yang tidak aman. Dan itu memberikan operasi kurang sedikit dari seribu pada proyek game, di mana kontrol mengetik dilakukan di sisi mesin.

Peringatan PVS-Studio: V3009 Aneh bahwa metode ini selalu mengembalikan nilai yang sama dan 'benar'. DataGridRows.cs 412

 internal bool ScrollSlotIntoView(int slot, bool scrolledHorizontally) { if (.....) { .... if (DisplayData.FirstScrollingSlot < slot && DisplayData.LastScrollingSlot > slot) { return true; } else if (DisplayData.FirstScrollingSlot == slot && slot != -1) { .... return true; } .... } .... return true; } 

Metode ini selalu mengembalikan true . Mungkin tujuan metode ini telah berubah sejak tanda tangan ditulis, tetapi kemungkinan besar ini adalah kesalahan. Ini juga kelas kontrol yang disalin dari Microsoft, dilihat dari komentar di awal kelas. Menurut pendapat saya, DataGrid biasanya merupakan salah satu kontrol yang paling tidak stabil, dan bagi saya itu layak untuk dipikirkan apakah perlu untuk mengkonfirmasi gulir jika tidak memenuhi persyaratan?

Kesimpulan


Beberapa kesalahan ini diperkenalkan ke dalam proyek bukan oleh pengembang UI Avalonia sendiri, tetapi oleh kode yang disalin dari kontrol WPF. Namun, untuk pengguna antarmuka, sumber kesalahan biasanya tidak masalah. Antarmuka yang macet atau tidak berfungsi merusak pendapat seluruh program.

Dalam artikel yang saya sebutkan kebutuhan untuk mengkonfigurasi analisa, ada positif palsu yang tidak dapat dihindari karena prinsip-prinsip pengoperasian algoritma analisis statis. Siapa pun yang terbiasa dengan masalah berhenti menyadari keterbatasan matematika ketika bekerja dengan kode lain. Tetapi dalam kasus ini kita berbicara tentang menonaktifkan satu diagnostik dari hampir seratus setengah. Jadi kita tidak berbicara tentang hilangnya makna dalam analisis statis (atau pertanyaan itu tidak sepadan). Selain itu, diagnosis ini mungkin memiliki respons yang baik, hanya saja lebih sulit untuk menemukan di antara massa positif palsu.

Pastikan untuk mencatat kualitas tinggi dari kode proyek! Saya berharap para pengembang akan menjaga kecepatan dan tingkat kualitas kode. Sayangnya, semakin besar proyeknya, semakin banyak bug di dalamnya. Salah satu cara yang mungkin untuk mengurangi bug adalah dengan mengkonfigurasi CI \ CD dengan benar, dengan koneksi analisis statis dan dinamis. Dan jika Anda ingin menyederhanakan pekerjaan dengan proyek besar dan mengurangi jumlah waktu yang dibutuhkan untuk debugging, unduh dan coba PVS-Studio!



Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Alexander Senichkin. Kontribusi Kecil Kami untuk Avalonia UI's Fight for Fewer Platforms .

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


All Articles