Jouez "osu!", N'oubliez pas les erreurs

Image 1

Nous accueillons tous les amateurs d'exotisme et de peu d'erreurs dans le code. Aujourd'hui sur le banc d'essai PVS-Studio est un invité assez rare - un jeu en C #. À savoir, osu! Comme d'habitude: recherchez les erreurs, réfléchissez, jouez.

Le jeu


Osu! - Un jeu de rythme musical open source. À en juger par les informations du site Web du jeu , il est assez populaire, car plus de 15 millions de joueurs inscrits sont déclarés. Le projet se caractérise par un gameplay gratuit, un design coloré avec la possibilité de personnaliser les cartes, des fonctionnalités avancées pour compiler un classement en ligne des joueurs, la présence du multijoueur, un large éventail de compositions musicales. Je ne décrirai pas le jeu en détail, les intéressés trouveront facilement toutes les informations sur le réseau. Par exemple, ici .

Je suis plus intéressé par le code source du projet, qui peut être téléchargé depuis GitHub . Un nombre important de validations (plus de 24 mille) dans le référentiel attire immédiatement l'attention, ce qui indique le développement actif du projet, qui se poursuit à ce jour (le jeu est sorti en 2007, mais le travail a probablement commencé plus tôt). Dans le même temps, il n'y a pas beaucoup de code source - 1813 fichiers .cs qui contiennent 135 000 lignes de code, à l'exclusion de celles vides. Ce code contient des tests que je ne prends généralement pas en compte dans les contrôles. Le code de test est contenu dans 306 fichiers .cs et, par conséquent, 25 000 lignes de code, à l'exclusion des lignes vides. Il s'agit d'un petit projet: à titre de comparaison, le cœur C # de l'analyseur PVS-Studio contient environ 300 000 lignes de code.

Au total, pour vérifier le jeu pour les erreurs, j'ai utilisé des projets non-test contenant 1507 fichiers de code source et 110 mille lignes. Cependant, le résultat m'a partiellement plu, car il y a eu plusieurs erreurs intéressantes dont je m'empresse de vous parler.

Erreurs


V3001 Il existe des sous-expressions identiques 'result == HitResult.Perfect' à gauche et à droite de '||' opérateur. DrawableHoldNote.cs 266

protected override void CheckForResult(....) { .... ApplyResult(r => { if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) result = HitResult.Good; .... }); } 

Un bon exemple de programmation orientée copier-coller. Un terme comique que mon collègue Valery Komarov a récemment utilisé (introduit) dans son article " Top 10 des erreurs dans les projets Java pour 2019 ".

Ainsi, deux contrôles identiques se succèdent. L'une des vérifications devrait très probablement contenir une autre constante d' énumération HitResult :

 public enum HitResult { None, Miss, Meh, Ok, Good, Great, Perfect, } 

Quelle constante particulière devait être utilisée, ou la deuxième vérification n'est-elle pas nécessaire du tout? Questions auxquelles seul le développeur peut répondre. Dans tous les cas, une erreur a été commise qui fausse la logique du programme.

V3001 Il existe des sous-expressions identiques «famille! = GetFamilyString (TournamentTypeface.Aquatico)» à gauche et à droite de l'opérateur «&&». TournamentFont.cs 64

 public static string GetWeightString(string family, FontWeight weight) { .... if (weight == FontWeight.Regular && family != GetFamilyString(TournamentTypeface.Aquatico) && family != GetFamilyString(TournamentTypeface.Aquatico)) weightString = string.Empty; .... } 

Et encore une fois copier-coller. J'ai formaté le code, donc l'erreur est facilement perceptible. Dans la version originale, la condition entière était écrite sur une seule ligne. Il est également difficile de dire comment le code peut être corrigé. L'énumération TournamentTypeface contient une seule constante:

 public enum TournamentTypeface { Aquatico } 

La condition peut avoir utilisé la variable familiale deux fois par erreur, mais ce n'est pas exact.

V3009 [CWE-393] Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'false'. KeyCounterAction.cs 19

 public bool OnPressed(T action, bool forwards) { if (!EqualityComparer<T>.Default.Equals(action, Action)) return false; IsLit = true; if (forwards) Increment(); return false; } 

La méthode retournera toujours false . Pour de telles erreurs, je vérifie généralement le code appelant, car ils n'utilisent souvent la valeur de retour nulle part, il n'y a pas d'erreur (sauf pour le style de programmation laid). Dans ce cas, je suis tombé sur le code suivant:

 public bool OnPressed(T action) => Target.Children .OfType<KeyCounterAction<T>>() .Any(c => c.OnPressed(action, Clock.Rate >= 0)); 

Comme vous pouvez le voir, le résultat renvoyé par la méthode OnPressed est utilisé. Et comme il est toujours faux , le résultat de l'appel OnPressed sera également toujours faux . Je pense que vous devriez vérifier à nouveau ce code, car il y a une forte probabilité d'erreur.

Une autre erreur similaire:

  • V3009 [CWE-393] Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'false'. KeyCounterAction.cs 30

V3042 [CWE-476] Possible NullReferenceException. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'val.NewValue' TournamentTeam.cs 41

 public TournamentTeam() { Acronym.ValueChanged += val => { if (....) FlagName.Value = val.NewValue.Length >= 2 // <= ? val.NewValue?.Substring(0, 2).ToUpper() : string.Empty; }; .... } 

Dans la condition de l'opérateur ?: , La variable val.NewValue n'est pas sûre. L'analyseur a fait une telle conclusion, car dans la branche alors, une option sûre est utilisée pour accéder à la variable via l' instruction d' accès conditionnel val.NewValue? .Substring (....) .

Une autre erreur similaire:

  • V3042 [CWE-476] Possible NullReferenceException. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'val.NewValue' TournamentTeam.cs 48

V3042 [CWE-476] Possible NullReferenceException. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'api' SetupScreen.cs 77

 private void reload() { .... new ActionableInfo { Label = "Current User", ButtonText = "Change Login", Action = () => { api.Logout(); // <= .... }, Value = api?.LocalUser.Value.Username, .... }, .... } private class ActionableInfo : LabelledDrawable<Drawable> { .... public Action Action; .... } 

Ce code est moins clair, mais je pense que l'erreur est toujours là. Créez un objet de type ActionableInfo . Le champ Action est initialisé avec un lambda, dans le corps duquel il n'est pas sûr de travailler avec une API de référence potentiellement nulle. L'analyseur a considéré ce modèle comme une erreur, car lors de l'initialisation du paramètre Value , la variable api fonctionne en toute sécurité. J'ai appelé l'erreur ambiguë, car le code lambda suppose une exécution retardée et, peut-être, le développeur garantit en quelque sorte une valeur non nulle du lien api . Mais ce n'est qu'une hypothèse, car le corps lambda ne contient aucun signe de travail sûr avec le lien (contrôles préliminaires, par exemple).

V3066 [CWE-683] Ordre incorrect possible des arguments passés à la méthode 'Atan2': 'diff.X' et 'diff.Y'. SliderBall.cs 182

 public void UpdateProgress(double completionProgress) { .... Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); .... } 

L'analyseur soupçonnait qu'en travaillant avec la méthode Atan2 de la classe Math , le développeur mélangeait l'ordre des arguments. Annonce Atan2 :

 // Parameters: // y: // The y coordinate of a point. // // x: // The x coordinate of a point. public static double Atan2(double y, double x); 

Comme vous pouvez le voir, les valeurs ont été transférées dans l'ordre inverse. Je ne peux pas juger si c'est une erreur, car la méthode UpdateProgress contient beaucoup de calculs non triviaux. Je note juste le fait d'un problème possible dans le code.

V3080 [CWE-476] Déréférence nulle possible. Pensez à inspecter «Beatmap». WorkingBeatmap.cs 57

 protected virtual Track GetVirtualTrack() { .... var lastObject = Beatmap.HitObjects.LastOrDefault(); .... } 

L'analyseur a souligné le danger d'accès via le lien nul Beatmap . Voici ce que c'est:

 public IBeatmap Beatmap { get { try { return LoadBeatmapAsync().Result; } catch (TaskCanceledException) { return null; } } } 

Eh bien, l'analyseur a raison.

Pour plus d'informations sur la façon dont PVS-Studio détecte ces erreurs, ainsi que sur les innovations C # 8.0 liées à des sujets similaires (travail avec des références potentiellement nulles), consultez l'article " Types de référence nullables en C # 8.0 et analyse statique ".

V3083 [CWE-367] L'appel non sécurisé de l'événement 'ObjectConverted', NullReferenceException est possible. Pensez à affecter un événement à une variable locale avant de l'invoquer. BeatmapConverter.cs 82

 private List<T> convertHitObjects(....) { .... if (ObjectConverted != null) { converted = converted.ToList(); ObjectConverted.Invoke(obj, converted); } .... } 

Erreur non critique et assez courante. Entre la vérification de la nullité de l'événement et son invocation, ils peuvent se désinscrire de l'événement, ce qui entraînera le plantage du programme.
L'un des correctifs:

 private List<T> convertHitObjects(....) { .... converted = converted.ToList(); ObjectConverted?.Invoke(obj, converted); .... } 

V3095 [CWE-476] L'objet 'colonnes' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 141, 142. SquareGraph.cs 141

 private void redrawProgress() { for (int i = 0; i < ColumnCount; i++) columns[i].State = i <= progress ? ColumnState.Lit : ColumnState.Dimmed; columns?.ForceRedraw(); } 

Il n'est pas sûr de contourner la collection de colonnes dans une boucle. Dans le même temps, le développeur a supposé que le lien des colonnes pouvait être nul, car plus tard dans le code, un opérateur d'accès conditionnel était utilisé pour accéder à la collection.

V3119 L' appel de l'événement ignoré 'OnNewResult' peut entraîner un comportement imprévisible. Envisagez d'implémenter explicitement les accesseurs d'événements ou utilisez un mot clé «scellé». DrawableRuleset.cs 256

 private void addHitObject(TObject hitObject) { .... drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); .... } public override event Action<JudgementResult> OnNewResult; 

L'analyseur avertit des dangers liés à l'utilisation d'un événement ignoré ou virtuel. Quel est exactement le danger - je suggère de lire la description du diagnostic. Aussi, à un moment donné, j'ai écrit un article sur ce sujet, « Événements virtuels en C #: quelque chose s'est mal passé ».

Une autre construction dangereuse similaire dans le code:

  • V3119 L'appel d'un événement ignoré peut entraîner un comportement imprévisible. Envisagez d'implémenter explicitement les accesseurs d'événements ou utilisez un mot clé «scellé». DrawableRuleset.cs 257

V3123 [CWE-783] Peut-être le '??' L'opérateur fonctionne d'une manière différente de celle attendue. Sa priorité est inférieure à la priorité des autres opérateurs dans sa partie gauche. OsuScreenStack.cs 45

 private void onScreenChange(IScreen prev, IScreen next) { parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * ((IOsuScreen)next)?.BackgroundParallaxAmount ?? 1.0f; } 

Pour une meilleure compréhension du problème - je vais donner un exemple synthétique de la façon dont le code fonctionne maintenant:

 x = (c * a) ?? b; 

L'erreur a été commise du fait que l'opérateur * a une priorité plus élevée que l'opérateur ??. Version corrigée du code (crochets ajoutés):
 private void onScreenChange(IScreen prev, IScreen next) { parallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * (((IOsuScreen)next)?.BackgroundParallaxAmount ?? 1.0f); } 

Une autre erreur similaire dans le code:

V3123 [CWE-783] Peut-être le '??' L'opérateur fonctionne d'une manière différente de celle attendue. Sa priorité est inférieure à la priorité des autres opérateurs dans sa partie gauche. FramedReplayInputHandler.cs 103

 private bool inImportantSection { get { .... return IsImportant(frame) && Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; } } 

Ici, comme dans le fragment de code précédent, la priorité des opérateurs n'a pas été prise en compte. Maintenant, l'expression passée à la méthode Math.Abs est évaluée comme suit:

 (a – b) ?? 0 

Code corrigé:
 private bool inImportantSection { get { .... return IsImportant(frame) && Math.Abs(CurrentTime – (NextFrame?.Time ?? 0)) <= AllowedImportantTimeSpan; } } 

V3142 [CWE-561] Code inaccessible détecté. Il est possible qu'une erreur soit présente. DrawableHoldNote.cs 214

 public override bool OnPressed(ManiaAction action) { if (!base.OnPressed(action)) return false; if (Result.Type == HitResult.Miss) // <= holdNote.hasBroken = true; .... } 

L'analyseur prétend que le code du gestionnaire OnPressed , en commençant par la deuxième instruction if , est inaccessible. Cela découle de l'hypothèse que la première condition est toujours vraie, c'est-à-dire que la méthode base.OnPressed retournera toujours false . Jetez un œil à la méthode base.OnPressed :

 public virtual bool OnPressed(ManiaAction action) { if (action != Action.Value) return false; return UpdateResult(true); } 

Nous passons à la méthode UpdateResult :
 protected bool UpdateResult(bool userTriggered) { if (Time.Elapsed < 0) return false; if (Judged) return false; .... return Judged; } 

Notez que l'implémentation de la propriété Judged n'est pas importante ici, car la logique de la méthode UpdateResult implique que la dernière instruction de retour est équivalente à ceci:

 return false; 

Ainsi, la méthode UpdateResult retournera toujours false , ce qui entraînera une erreur avec du code inaccessible dans le code au-dessus de la pile.

V3146 [CWE-476] Déréférence nulle possible de 'jeu de règles'. Le 'FirstOrDefault' peut renvoyer la valeur nulle par défaut. APILegacyScoreInfo.cs 24

 public ScoreInfo CreateScoreInfo(RulesetStore rulesets) { var ruleset = rulesets.GetRuleset(OnlineRulesetID); var mods = Mods != null ? ruleset.CreateInstance() // <= .GetAllMods().Where(....) .ToArray() : Array.Empty<Mod>(); .... } 

L'analyseur considère que l'appel de ruleset.CreateInstance () n'est pas sûr. La variable d'ensemble de règles obtient précédemment la valeur suite à l'appel de GetRuleset :

 public RulesetInfo GetRuleset(int id) => AvailableRulesets.FirstOrDefault(....); 

Comme vous pouvez le voir, l'avertissement de l'analyseur est justifié, car la chaîne d'appel contient FirstOrDefault , qui peut retourner null .

Conclusion


En général, le projet de jeu "osu!" Satisfait d'un petit nombre d'erreurs. Néanmoins, je recommande aux développeurs de faire attention aux problèmes détectés. Et laissez le jeu continuer de plaire à ses fans.

Et pour ceux qui aiment approfondir le code, je vous rappelle que l'analyseur PVS-Studio, qui est facile à télécharger sur le site officiel, sera d'une bonne aide. Je note également que les vérifications de projet ponctuelles, telles que celles décrites ci-dessus, n'ont rien à voir avec l'utilisation d'un analyseur statique dans le travail réel. Une efficacité maximale dans la lutte contre les erreurs ne peut être atteinte qu'avec une utilisation régulière de l'outil à la fois sur le serveur de build et directement sur l'ordinateur du développeur (analyse incrémentale). L'objectif maximum est d'empêcher les erreurs de pénétrer dans le système de contrôle de version, en corrigeant les défauts déjà au stade de l'écriture du code.

Bonne chance et succès!

Les références


Il s'agit de la première publication en 2020. Profitant de cette opportunité, je fournirai des liens vers des articles sur la vérification des projets C # l'année dernière:




Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Sergey Khrenov. Jouez "osu!", Mais attention aux bugs .

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


All Articles