Vérification du code source de Roslyn

PVS-Studio vs Roslyn

De temps en temps, nous revenons à des projets que nous avions précédemment testés avec PVS-Studio et avons écrit des articles à ce sujet. Il y a deux raisons à cela. Tout d'abord, pour comprendre à quel point notre analyseur est devenu meilleur. Deuxièmement, pour vérifier si les auteurs du projet ont prêté attention à notre article, ainsi qu'au rapport d'erreur que nous leur fournissons habituellement. Bien sûr, les erreurs peuvent être corrigées sans notre participation. Mais c'est toujours agréable quand exactement nos efforts contribuent à améliorer un projet. Roslyn ne faisait pas exception. Un précédent article de synthèse sur ce projet remonte au 23 décembre 2015. C'est assez long, compte tenu du chemin parcouru par notre analyseur dans son développement pendant cette période. Pour nous personnellement, Roslyn présente également un intérêt supplémentaire du fait que le cœur de l'analyseur C # PVS-Studio est basé sur celui-ci. Par conséquent, nous sommes très intéressés par la qualité du code de ce projet. Nous organiserons un deuxième contrôle et découvrirons ce qui est nouveau et intéressant (mais espérons que rien d'important) PVS-Studio puisse y trouver.

Roslyn (ou la plate-forme du compilateur .NET) est probablement familier à beaucoup de nos lecteurs. En bref, il s'agit d'un ensemble de compilateurs open source et d'API pour l'analyse de code pour les langages C # et Visual Basic .NET de Microsoft. Le code source du projet est disponible sur GitHub .

Je ne donnerai pas une description détaillée de cette plate-forme, mais je recommanderais à tous ceux qui sont intéressés un article de mon collègue Sergey Vasiliev, " Introduction à Roslyn. Utiliser des outils d'analyse statique pour développer ". À partir de cet article, vous pouvez en apprendre non seulement sur les fonctionnalités de l'architecture Roslyn, mais également sur la façon dont nous utilisons exactement cette plate-forme.

Comme je l'ai mentionné plus tôt, plus de trois ans se sont écoulés depuis la rédaction du dernier article de mon collègue Andrei Karpov sur le chèque de Roslyn " Sortie du Nouvel An de PVS-Studio 6.00: vérification de Roslyn ". Pendant ce temps, l'analyseur C # PVS-Studio a acquis de nombreuses nouvelles fonctionnalités. En général, l'article d'Andrey était une sorte de «test ball», car l'analyseur C # n'était alors ajouté qu'à PVS-Studio. Malgré cela, même alors, dans un projet de qualité inconditionnelle, Roslyn a réussi à trouver des erreurs intéressantes. Qu'est-ce qui a changé dans l'analyseur pour le code C # jusqu'à présent, ce qui permettra potentiellement une analyse plus approfondie?

Au cours des dernières années, le cœur de l'analyseur et l'infrastructure se sont développés. Un support a été ajouté pour Visual Studio 2017 et Roslyn 2.0, ainsi qu'une intégration approfondie avec MSBuild. Vous pouvez en savoir plus sur notre approche de l'intégration avec MSBuild et sur les raisons qui nous ont amenés à l'accepter dans l'article de mon collègue Pavel Yeremeyev, " Prise en charge de Visual Studio 2017 et Roslyn 2.0 dans PVS-Studio: parfois, l'utilisation de solutions toutes faites n'est pas aussi simple qu'il n'y paraît en un coup d'œil . "

Maintenant, nous travaillons activement sur la transition vers Roslyn 3.0 selon le même schéma que nous avons initialement pris en charge Visual Studio 2017, c'est-à-dire via notre propre ensemble d'outils, qui est fourni dans le kit de distribution PVS-Studio avec un «stub» sous la forme d'un fichier MSBuild.exe vide. Malgré le fait qu'elle ressemble à une "béquille" (l'API MSBuild n'est pas très conviviale pour une réutilisation dans des projets de troisième génération en raison de la faible portabilité des bibliothèques), cette approche nous a déjà aidé à revivre plusieurs mises à jour de Roslyn relativement sans douleur pendant la durée de Visual Studio 2017. et maintenant, bien qu'avec de nombreuses superpositions, survivez à la mise à niveau vers Visual Studio 2019, tout en conservant une compatibilité descendante et des performances complètes sur les systèmes avec des versions plus anciennes de MSBuild.

Le cœur de l'analyseur a également subi un certain nombre d'améliorations. L'une des principales innovations est une analyse interprocédurale à part entière, prenant en compte les valeurs d'entrée et de sortie des méthodes, prenant en compte, en fonction de ces paramètres, l'accessibilité des branches d'exécution et des points de retour.

La tâche de suivi des paramètres à l'intérieur des méthodes est déjà presque terminée, tout en préservant les annotations automatiques de ce qui se passe avec ces paramètres (par exemple, un déréférencement potentiellement dangereux). Cela permettra à tout diagnostic utilisant le mécanisme de flux de données de prendre en compte les situations dangereuses qui se produisent lors du passage d'un paramètre à une méthode. Auparavant, lors de l'analyse de ces endroits dangereux, aucun avertissement n'était généré, car nous ne pouvions pas connaître toutes les valeurs d'entrée possibles pour une telle méthode. Nous pouvons maintenant détecter le danger, car dans tous les endroits où cette méthode est appelée, ces paramètres d'entrée seront pris en compte.

Remarque: vous pouvez vous familiariser avec les principaux mécanismes de l'analyseur, tels que le flux de données et autres, à partir de l'article " Technologies utilisées dans l'analyseur de code PVS-Studio pour trouver les erreurs et les vulnérabilités potentielles ".

L'analyse interprocédurale dans PVS-Studio C # n'est pas limitée par les paramètres d'entrée ou la profondeur. La seule limitation est les méthodes virtuelles dans les classes qui n'ont pas été fermées pour l'héritage et qui tombent dans la récursivité (arrêtons quand nous avons vu sur la pile un appel répété à une méthode déjà calculée). De plus, la méthode récursive elle-même sera finalement calculée en supposant que la valeur de retour de son auto-récursivité est inconnue.

Une autre grande innovation dans l'analyseur C # est la possibilité de déréférencer un pointeur potentiellement nul. Auparavant, l'analyseur jurait à une éventuelle exception de référence nulle s'il était sûr que dans toutes les branches d'exécution, la valeur de la variable serait nulle. Bien sûr, il se trompait parfois, de sorte que le diagnostic du V3080 était auparavant appelé référence nulle potentielle.

L'analyseur se souvient maintenant que la variable peut être nulle dans l'une des branches d'exécution (par exemple, sous une certaine condition dans if). S'il voit l'accès à une telle variable sans vérification, il recevra un message V3080, mais à un niveau d'importance moindre que s'il voit null dans toutes les branches. En combinaison avec une analyse interprocédurale améliorée, un tel mécanisme permet de trouver des erreurs très difficiles à détecter. Un exemple est une longue chaîne d'appels de méthode, dont le dernier n'est pas familier et qui, par exemple, renvoie null dans certaines circonstances, mais vous ne vous en êtes pas protégé car vous ne le saviez tout simplement pas. Dans ce cas, l'analyseur ne jure que lorsqu'il voit précisément l'affectation de null. À notre avis, cela distingue qualitativement notre approche d'une telle innovation de C # 8.0 en tant que type de référence nullable, qui, en fait, se résume à définir des contrôles nuls dans chaque méthode. Nous offrons une alternative - pour faire des vérifications uniquement là où la valeur nulle peut vraiment arriver, et notre analyseur est maintenant capable de rechercher de telles situations.

Alors, sans plus tarder, passons au «débriefing» - analysant les résultats du contrôle Roslyn. Voyons d'abord les erreurs trouvées grâce aux innovations décrites ci-dessus. En général, plusieurs avertissements ont été émis pour le code Roslyn cette fois. Je pense que cela est dû au fait que la plate-forme se développe très activement (la base de code s'élève actuellement à environ 2 770 000 lignes de code, à l'exclusion des lignes vides), et nous n'avons pas fait d'analyse de ce projet depuis longtemps. Cependant, il n'y a pas autant d'erreurs critiques, à savoir qu'elles intéressent l'article. Et oui, il y a pas mal de tests à Roslyn que j'ai, comme d'habitude, exclus des tests.

Je vais commencer par les erreurs V3080, avec un niveau de criticité moyen, dans lequel l'analyseur a détecté un accès possible via une liaison nulle, mais pas dans tous les cas possibles (branches de code).

Déréférence nulle possible - Moyenne

V3080 Déréférence nulle possible. Envisagez d'inspecter «actuel». CSharpSyntaxTreeFactoryService.PositionalSyntaxReference.cs 70

private SyntaxNode GetNode(SyntaxNode root) { var current = root; .... while (current.FullSpan.Contains(....)) // <= { .... var nodeOrToken = current.ChildThatContainsPosition(....); .... current = nodeOrToken.AsNode(); // <= } .... } public SyntaxNode AsNode() { if (_token != null) { return null; } return _nodeOrParent; } 

Considérez la méthode GetNode . L'analyseur considère que l'accès par référence nulle est possible dans l'état du bloc while . Dans le corps du bloc while , la variable courante se verra attribuer une valeur - le résultat de l'exécution de la méthode AsNode . Et cette valeur dans certains cas sera nulle . Un bon exemple d'analyse interprocédurale en action.

Considérons maintenant un cas similaire dans lequel l'analyse interprocédurale a été effectuée via deux appels de méthode.

V3080 Déréférence nulle possible. Pensez à inspecter le «répertoire». CommonCommandLineParser.cs 911

 private IEnumerable<CommandLineSourceFile> ExpandFileNamePattern(string path, string baseDirectory, ....) { string directory = PathUtilities.GetDirectoryName(path); .... var resolvedDirectoryPath = (directory.Length == 0) ? // <= baseDirectory : FileUtilities.ResolveRelativePath(directory, baseDirectory); .... } public static string GetDirectoryName(string path) { return GetDirectoryName(path, IsUnixLikePlatform); } internal static string GetDirectoryName(string path, bool isUnixLike) { if (path != null) { .... } return null; } 

La variable de répertoire dans le corps de la méthode ExpandFileNamePattern obtient la valeur de la méthode GetDirectoryName (chaîne) . Cela, à son tour, renverra le résultat de la méthode GetDirectoryName surchargée (chaîne, bool) , dont la valeur peut être nulle . Étant donné que plus loin dans le corps de la méthode ExpandFileNamePattern , la variable de répertoire est utilisée sans vérifier d'abord l' égalité nulle , nous pouvons parler de la légitimité de l'avertissement par l'analyseur. Il s'agit d'une conception potentiellement dangereuse.

Un autre morceau de code avec l'erreur V3080, plus précisément, immédiatement avec deux erreurs émises pour une ligne de code. Ici, l'analyse interprocédurale n'était pas nécessaire.

V3080 Déréférence nulle possible. Pensez à inspecter «spanStartLocation». TestWorkspace.cs 574

V3080 Déréférence nulle possible. Pensez à inspecter «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, // <= spanEndLocationExclusive.Value - // <= spanStartLocation.Value)); } } .... } 

Les variables spanStartLocation et spanEndLocationExclusive sont de type nullable int et sont initialisées à null . Plus loin dans le code, des valeurs peuvent leur être attribuées, mais uniquement si certaines conditions sont remplies. Dans certains cas, leur valeur restera égale à null . Plus loin dans le code, ces variables sont accessibles par référence sans d'abord vérifier l' égalité nulle , comme l'indique l'analyseur.

Le code Roslyn contient un certain nombre de ces erreurs, plus de 100. Souvent, le modèle de ces erreurs est le même. Il existe une méthode générale qui retourne potentiellement null . Le résultat de cette méthode est utilisé dans un grand nombre d'endroits, parfois via des dizaines d'appels de méthodes intermédiaires ou des vérifications supplémentaires. Il est important de comprendre que ces erreurs ne sont pas fatales, mais elles peuvent potentiellement conduire à un accès via un lien nul. Et détecter de telles erreurs est très difficile. Par conséquent, dans certains cas, vous devez envisager de refactoriser le code, dans lequel une exception serait levée si la méthode retournait null . Sinon, vous ne pouvez sécuriser votre code qu'avec des contrôles totaux, ce qui est assez fastidieux et peu fiable. Bien entendu, dans chaque cas, la décision doit être prise en fonction des caractéristiques du projet.

Remarque Il arrive qu'à l'heure actuelle il n'y a pas de situations (données d'entrée) dans lesquelles la méthode retourne null et il n'y a pas de véritable erreur. Cependant, un tel code n'est toujours pas fiable, car tout peut changer lorsque des modifications sont apportées au code.

Pour fermer le sujet avec V3080 , jetons un coup d'œil aux erreurs évidentes avec le niveau de confiance élevé, lorsque l'accès via un lien nul est plus probable ou même inévitable.

Déréférence nulle possible - Élevée

V3080 Déréférence nulle possible. Pensez à inspecter '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; } .... } 

En raison d'une faute de frappe dans la condition (au lieu de l'opérateur || que nous avons utilisé && ), le code ne fonctionne pas du tout comme prévu, et la variable collectionType.Type sera accessible si elle est nulle . La condition doit être corrigée comme suit:

 if (collectionType.Type == null || collectionType.Type.TypeKind == TypeKind.Error) .... 

Soit dit en passant, la deuxième variante du développement des événements est également possible: dans la première partie, les conditions sont mélangées par les opérateurs == et ! = . Le code corrigé serait alors:

 if (collectionType.Type != null && collectionType.Type.TypeKind == TypeKind.Error) .... 

Cette version du code est moins logique, mais corrige également l'erreur. La décision finale appartient aux auteurs du projet.

Une autre erreur similaire.

V3080 Déréférence nulle possible. Envisagez d'inspecter «l'action». 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}'."); } .... } 

Une erreur a été commise lors de la rédaction d'un message pour une exception. Dans le même temps, une tentative d'accès à la propriété action.DisplayText est effectuée via la variable d' action , qui est évidemment nulle .

Et la dernière erreur est le V3080 High Level.

V3080 Déréférence nulle possible. Envisagez d'inspecter le «type». ObjectFormatterHelpers.cs 91

 private static bool IsApplicableAttribute( TypeInfo type, TypeInfo targetType, string targetTypeName) { return type != null && AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName; } 

La méthode est petite, donc je donne tout son code. La condition dans le bloc de retour est incorrecte. Dans certains cas, il est possible de lever une NullReferenceException lors de l'accès à type.FullName . J'utilise des crochets (ils ne changeront pas de comportement ici) pour clarifier la situation:

 return (type != null && AreEquivalent(targetType, type)) || (targetTypeName != null && type.FullName == targetTypeName); 

C'est ainsi que, selon la priorité des opérations, ce code fonctionnera. Si la variable de type est null , nous entrons dans une vérification else, où, en nous assurant que la variable targetTypeName est null , nous utilisons la référence de type null. Vous pouvez corriger le code, par exemple, comme ceci:

 return type != null && (AreEquivalent(targetType, type) || targetTypeName != null && type.FullName == targetTypeName); 

Je pense que c'est là que vous pouvez terminer l'étude des erreurs V3080 et voir ce que l'analyseur PVS-Studio intéressant a réussi à trouver dans le code Roslyn.

Typo

V3005 La variable 'SourceCodeKind' est assignée à elle-même. DynamicFileInfo.cs 17

 internal sealed class DynamicFileInfo { .... public DynamicFileInfo( string filePath, SourceCodeKind sourceCodeKind, TextLoader textLoader, IDocumentServiceProvider documentServiceProvider) { FilePath = filePath; SourceCodeKind = SourceCodeKind; // <= TextLoader = textLoader; DocumentServiceProvider = documentServiceProvider; } .... } 

En raison d'un nom de variable non réussi, une faute de frappe a été effectuée dans le constructeur de la classe DynamicFileInfo . Le champ SourceCodeKind se voit attribuer sa propre valeur, au lieu d'utiliser le paramètre sourceCodeKind . Pour minimiser la probabilité de telles erreurs, il est recommandé d'utiliser le préfixe de soulignement pour les noms de paramètres dans de tels cas. Je vais donner un exemple d'une version corrigée du code:

 public DynamicFileInfo( string _filePath, SourceCodeKind _sourceCodeKind, TextLoader _textLoader, IDocumentServiceProvider _documentServiceProvider) { FilePath = _filePath; SourceCodeKind = _sourceCodeKind; TextLoader = _textLoader; DocumentServiceProvider = _documentServiceProvider; } 

Insouciance

V3006 L'objet a été créé mais il n'est pas utilisé. Le mot clé 'throw' peut être manquant: lancez une nouvelle exception InvalidOperationException (FOO). ProjectBuildManager.cs 61

 ~ProjectBuildManager() { if (_batchBuildStarted) { new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } } 

Sous une certaine condition, le destructeur doit lever une exception, mais cela ne se produit pas et l'objet exception est simplement créé. Le mot clé throw a été omis. Version corrigée du code:

 ~ProjectBuildManager() { if (_batchBuildStarted) { throw new InvalidOperationException("ProjectBuilderManager.Stop() not called."); } } 

La question de travailler avec des destructeurs en C # et d'en lever des exceptions est un sujet pour une discussion séparée, qui dépasse le cadre de cet article.

Quand le résultat n'est pas important

Un certain nombre d'avertissements V3009 ont été reçus pour les méthodes qui retournent la même valeur dans tous les cas. Parfois, cela n'est pas critique, ou le code retour n'est tout simplement pas vérifié dans le code appelant. J'ai raté de tels avertissements. Mais quelques morceaux de code me semblaient suspects. Je citerai l'un d'eux:

V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'true'. GoToDefinitionCommandHandler.cs 62

 internal bool TryExecuteCommand(....) { .... using (context.OperationContext.AddScope(....)) { if (....) { return true; } } .... return true; } 

La méthode TryExecuteCommand renvoie uniquement true et rien que true . Dans le même temps, la valeur de retour est impliquée dans certaines vérifications du code appelant:

 public bool ExecuteCommand(....) { .... if (caretPos.HasValue && TryExecuteCommand(....)) { .... } .... } 

Il est difficile de dire à quel point ce comportement est dangereux. Mais si le résultat n'est pas nécessaire, il peut être utile de remplacer le type de retour par void et d'apporter des modifications minimales à la méthode d'appel. Cela rendra le code plus compréhensible et plus sûr.

Autres avertissements similaires:

  • V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'true'. CommentUncommentSelectionCommandHandler.cs 86
  • V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'true'. RenameTrackingTaggerProvider.RenameTrackingCommitter.cs 99
  • V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'true'. JsonRpcClient.cs 138
  • V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'true'. AbstractFormatEngine.OperationApplier.cs 164
  • V3009 Il est étrange que cette méthode retourne toujours une seule et même valeur de 'false'. TriviaDataFactory.CodeShapeAnalyzer.cs 254
  • V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'true'. ObjectList.cs 173
  • V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de 'true'. ObjectList.cs 249

Non vérifié

V3019 Il est possible qu'une variable incorrecte soit comparée à null après la conversion de type à l'aide du mot clé 'as'. Vérifiez les variables 'value', 'valueToSerialize'. RoamingVisualStudioProfileOptionPersister.cs 277

 public bool TryPersist(OptionKey optionKey, object value) { .... var valueToSerialize = value as NamingStylePreferences; if (value != null) { value = valueToSerialize.CreateXElement().ToString(); } .... } 

La valeur de la variable est de type NamingStylePreferences . Le problème vient de cette vérification. Même si la variable de valeur n'est pas nulle, cela ne garantit pas que la conversion de type a réussi et que valueToSerialize ne contient pas null . Une exception NullReferenceException peut être levée . Le code doit être corrigé comme suit:

 var valueToSerialize = value as NamingStylePreferences; if (valueToSerialize != null) { value = valueToSerialize.CreateXElement().ToString(); } 

Et encore une erreur similaire.

V3019 Il est possible qu'une variable incorrecte soit comparée à null après la conversion de type à l'aide du mot clé 'as'. Vérifiez les variables 'columnState', 'columnState2'. StreamingFindUsagesPresenter.cs 181

 private void SetDefinitionGroupingPriority(....) { .... foreach (var columnState in ....) { var columnState2 = columnState as ColumnState2; if (columnState?.Name == // <= StandardTableColumnDefinitions2.Definition) { newColumns.Add(new ColumnState2( columnState2.Name, // <= ....)); } .... } .... } 

La variable columnState est de type ColumnState2 . Toutefois, le résultat de l'opération, la variable columnState2 , n'est pas davantage vérifié pour null . Au lieu de cela, la variable columnState est vérifiée à l'aide de l'instruction null conditionnelle. Quel est le danger de ce code? Comme dans l'exemple précédent, la conversion de type à l'aide de l'opérateur as peut échouer et la variable columnState2 sera nulle , ce qui générera une exception. Soit dit en passant, une faute de frappe peut être à blâmer. Notez la condition dans le bloc if . Peut-être qu'au lieu de columnState? .Name, ils voulaient écrire columnState2? .Name . Cela est très probable compte tenu des noms de variables plutôt malheureux columnState et columnState2.

Contrôles redondants

Un nombre assez élevé d'avertissements (plus de 100) ont été émis pour des constructions non critiques, mais potentiellement dangereuses associées à des contrôles redondants. Par exemple, je vais en donner un.

V3022 L' expression 'navInfo == null' est toujours fausse. 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) // <= { return true; } .... } public IVsNavInfo CreateForSymbol(....) { .... return null; } public IVsNavInfo CreateForProject(....) { return new NavInfo(....); } 

Il n'y a peut-être pas de véritable erreur ici. Juste une bonne raison de démontrer la combinaison des technologies «analyse interprocédurale + analyse de flux de données» en action. L'analyseur considère que la deuxième vérification navInfo == null est redondante. En effet, avant cela, la valeur d'affectation de navInfo sera obtenue à partir de la méthode libraryService.NavInfoFactory.CreateForProject , qui va construire et retourner un nouvel objet de la classe NavInfo . Mais pas nul en aucune façon. La question est, pourquoi l'analyseur n'a-t-il pas généré d' avertissement pour la première vérification navInfo == null ? Il y a une explication à cela. Premièrement, si la variable de symbole s'avère nulle , la valeur de navInfo restera une référence nulle. Deuxièmement, même si navInfo obtient la valeur de la méthode libraryService.NavInfoFactory.CreateForSymbol , cette valeur peut être nulle . La première vérification de navInfo == null est donc vraiment nécessaire.

Pas assez de contrôles

Et maintenant, la situation est l'inverse de ce qui précède. Plusieurs avertissements V3042 ont été reçus pour le code accessible par une référence nulle. De plus, seulement un ou deux petits chèques pouvaient tout réparer.

Considérez un morceau de code intéressant qui contient deux de ces erreurs.

V3042 Exception NullReferenceException possible. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'récepteur' Binder_Expressions.cs 7770

V3042 Exception NullReferenceException possible. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'récepteur' Binder_Expressions.cs 7776

 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; } 

La variable récepteur peut être nulle . L'auteur du code le sait car il utilise l'opérateur nul conditionnel dans la condition du bloc if pour accéder au récepteur? .Syntax . Plus loin dans le code, le récepteur variable est utilisé sans aucun contrôle pour accéder à receiver.Type , receiver.Syntax et receiver.HasErrors . Ces erreurs doivent être corrigées:

 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; } 

Vous devez également vous assurer que le constructeur BoundConditionalReceiver prend en charge l'obtention de valeurs nulles pour ses paramètres, ou effectuer un refactoring supplémentaire.

Autres erreurs similaires:

  • V3042 Exception NullReferenceException possible. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'containerType' SyntaxGeneratorExtensions_Negate.cs 240
  • V3042 Exception NullReferenceException possible. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'expression' ExpressionSyntaxExtensions.cs 349
  • V3042 Exception NullReferenceException possible. Le '?.' et '.' les opérateurs sont utilisés pour accéder aux membres de l'objet 'expression' ExpressionSyntaxExtensions.cs 349

Erreur de condition

V3057 La fonction 'Sous-chaîne' pourrait recevoir la valeur '-1' alors qu'une valeur non négative est attendue. Inspectez le deuxième argument. CommonCommandLineParser.cs 109

 internal static bool TryParseOption(....) { .... if (colon >= 0) { name = arg.Substring(1, colon - 1); value = arg.Substring(colon + 1); } .... } 

Si la variable deux - points est 0, ce qui autorise une condition dans le code, la méthode Substring lèvera une ArgumentOutOfRangeException . Correction requise:

 if (colon > 0) 

La faute de frappe est possible

V3065 Le paramètre 't2' n'est pas utilisé dans le corps de la méthode. 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; }); .... } 

Deux paramètres sont passés à l'expression lambda: t1 et t2. Cependant, seul t1 est utilisé. Cela semble suspect, compte tenu de la facilité avec laquelle il est possible de se tromper lors de l'utilisation de variables portant ces noms.

Insouciance

V3083 Invocation non sûre de l'événement 'TagsChanged', NullReferenceException est possible. Pensez à affecter un événement à une variable locale avant de l'invoquer. PreviewUpdater.Tagger.cs 37

 public void OnTextBufferChanged() { if (PreviewUpdater.SpanToShow != default) { if (TagsChanged != null) { var span = _textBuffer.CurrentSnapshot.GetFullSpan(); TagsChanged(this, new SnapshotSpanEventArgs(span)); // <= } } } 

L'événement TagsChanged est déclenché de manière non sécurisée. Entre la vérification de l' égalité nulle et l'appel d'un événement, ils peuvent avoir le temps de se désinscrire, puis une exception sera levée. De plus, dans le corps du bloc if , juste avant l'appel de l'événement, d'autres opérations sont effectuées. J'ai appelé cette erreur «inattention», car à d'autres endroits du code, ils fonctionnent avec plus de précision avec cet événement, par exemple, comme ceci:

 private void OnTrackingSpansChanged(bool leafChanged) { var handler = TagsChanged; if (handler != null) { var snapshot = _buffer.CurrentSnapshot; handler(this, new SnapshotSpanEventArgs(snapshot.GetFullSpan())); } } 

L'utilisation de la variable de gestionnaire facultative élimine le problème. Dans la méthode OnTextBufferChanged , vous devez apporter des modifications pour la même opération sûre avec l'événement.

Gammes d'intersection

V3092 Les intersections de plage sont possibles dans les expressions conditionnelles. Exemple: if (A> 0 && A <5) {...} else if (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 { .... } } 

Pour une meilleure compréhension, je vais réécrire ce fragment de code, en remplaçant les noms constants par leurs valeurs réelles:

 internal void EmitLongConstant(long value) { if (value >= -2147483648 && value <= 2147483648) { .... } else if (value >= 0 && value <= 4294967295) { .... } else { .... } } 

Probablement, il n'y a pas de véritable erreur, mais la condition semble étrange. La deuxième partie ( sinon si ) sera effectuée uniquement pour la plage de valeurs de 2147483648 + 1 à 4294967295.

Quelques avertissements supplémentaires:

  • V3092 Les intersections de plage sont possibles dans les expressions conditionnelles. Exemple: if (A> 0 && A <5) {...} else if (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 109
  • V3092 Les intersections de plage sont possibles dans les expressions conditionnelles. Exemple: if (A> 0 && A <5) {...} else if (A> 3 && A <9) {...}. LocalRewriter_Literal.cs 66

En savoir plus sur les contrôles d'égalité nulle (ou leur absence)

Quelques erreurs V3095 sur la vérification de la null d' une variable après l' avoir utilisée. Le premier est ambigu, considérez le code.

V3095 L'objet 'displayName' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 498, 503. FusionAssemblyIdentity.cs 498

 internal static IAssemblyName ToAssemblyNameObject(string displayName) { if (displayName.IndexOf('\0') >= 0) { return null; } Debug.Assert(displayName != null); .... } 

Il est supposé que la référence displayName peut être nulle. Pour ce faire, vérifiez Debug.Assert . On ne sait pas pourquoi il va après avoir utilisé la chaîne. Il convient également de noter que pour d'autres configurations autres que Debug, le compilateur supprimera Debug.Assert du code. Est-ce à dire que ce n'est que pour le débogage qu'il est possible d'obtenir une référence nulle? Et si ce n'est pas le cas, alors pourquoi ne pas vérifier string.IsNullOrEmpty (string) , par exemple. Ce sont des questions pour les auteurs du code.

L'erreur suivante est plus évidente.

V3095 L'objet 'scriptArgsOpt' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 321, 325. CommonCommandLineParser.cs 321

 internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt.Add(arg); // <= continue; } if (scriptArgsOpt != null) { .... } .... } } 

Je pense que ce morceau de code ne nécessite aucune explication. Je vais donner la version corrigée:

 internal void FlattenArgs(...., List<string> scriptArgsOpt, ....) { .... while (args.Count > 0) { .... if (parsingScriptArgs) { scriptArgsOpt?.Add(arg); continue; } if (scriptArgsOpt != null) { .... } .... } } 

Le code Roslyn a trouvé 15 autres erreurs de ce type:

  • V3095 L'objet 'LocalFunctions' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 289, 317. ControlFlowGraphBuilder.RegionBuilder.cs 289
  • V3095 L'objet 'resolution.OverloadResolutionResult' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 579, 588. Binder_Invocation.cs 579
  • V3095 L'objet 'resolution.MethodGroup' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 592, 621. Binder_Invocation.cs 592
  • V3095 L'objet 'touchedFilesLogger' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 111, 126. CSharpCompiler.cs 111
  • V3095 L'objet 'newExceptionRegionsOpt' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 736, 743. AbstractEditAndContinueAnalyzer.cs 736
  • V3095 L'objet 'symbole' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 422, 427. AbstractGenerateConstructorService.Editor.cs 422
  • V3095 L'objet '_state.BaseTypeOrInterfaceOpt' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 132, 140. AbstractGenerateTypeService.GenerateNamedType.cs 132
  • V3095 L'objet 'element' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 232, 233. ProjectUtil.cs 232
  • V3095 L'objet 'languages' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 22, 28. ExportCodeCleanupProvider.cs 22
  • V3095 L'objet 'memberType' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 183, 184. SyntaxGeneratorExtensions_CreateGetHashCodeMethod.cs 183
  • V3095 L'objet 'validTypeDeclarations' a été utilisé avant d'être vérifié par rapport à null. Check lines: 223, 228. SyntaxTreeExtensions.cs 223
  • V3095 The 'text' object was used before it was verified against null. Check lines: 376, 385. MSBuildWorkspace.cs 376
  • V3095 The 'nameOrMemberAccessExpression' object was used before it was verified against null. Check lines: 206, 223. CSharpGenerateTypeService.cs 206
  • V3095 The 'simpleName' object was used before it was verified against null. Check lines: 83, 85. CSharpGenerateMethodService.cs 83
  • V3095 The 'option' object was used before it was verified against null. Check lines: 23, 28. OptionKey.cs 23

Considérez les erreurs de V3105 . Ici, nous utilisons l'opérateur nul conditionnel lors de l'initialisation de la variable, et ci-après dans le code, la variable est utilisée sans vérifier l' égalité nulle .

L'erreur suivante est signalée immédiatement par deux avertissements.

V3105 La variable 'documentId' a été utilisée après son affectation via un opérateur conditionnel nul. NullReferenceException est possible. CodeLensReferencesService.cs 138

V3105 La variable 'documentId' a été utilisée après avoir été affectée via l'opérateur de condition nulle. NullReferenceException est possible. CodeLensReferencesService.cs 139

 private static async Task<ReferenceLocationDescriptor> GetDescriptorOfEnclosingSymbolAsync(....) { .... var documentId = solution.GetDocument(location.SourceTree)?.Id; return new ReferenceLocationDescriptor( .... documentId.ProjectId.Id, documentId.Id, ....); } 

La variable documentId peut être initialisée à null . Par conséquent, la création du ReferenceLocationDescriptor finira par lever une exception. Le code doit être corrigé:

 return new ReferenceLocationDescriptor( .... documentId?.ProjectId.Id, documentId?.Id, ....); 

De plus, plus loin dans le code, il est nécessaire de prévoir la possibilité d'égalité des variables nulles passées au constructeur.

Autres erreurs similaires dans le code:

  • V3105 La variable 'symbol' a été utilisée après avoir été affectée via un opérateur conditionnel nul. NullReferenceException est possible. SymbolFinder_Hierarchy.cs 44
  • V3105 La variable 'symbol' a été utilisée après avoir été affectée via un opérateur conditionnel nul. NullReferenceException est possible. SymbolFinder_Hierarchy.cs 51

Priorités et crochets

V3123 L'opérateur «?:» Fonctionne peut-être d'une manière différente de celle attendue. Sa priorité est inférieure à la priorité des autres opérateurs dans son état. 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); } 

La condition dans le bloc de retour n'est pas du tout calculée comme le pensait le développeur. Il a été supposé que la première condition serait _kind == other._kin d (par conséquent, l'habillage de ligne a été effectué après cette condition), puis les blocs de condition avec l'opérateur " ? " Seraient calculés séquentiellement . En fait, la première condition sera _kind == other._kind && (_oldNode == null) . Cela est dû au fait que l'opérateur && a une priorité plus élevée que l'opérateur " ? ". Pour corriger l'erreur, il est nécessaire de mettre entre crochets toutes les expressions de l'opérateur " ? ":

 return _kind == other._kind && ((_oldNode == null) ? other._oldNode == null : _oldNode.Equals(other._oldNode)) && ((_newNode == null) ? other._newNode == null : _newNode.Equals(other._newNode)); 

Ceci conclut la description des erreurs trouvées.

Conclusions

Malgré le nombre important d'erreurs que j'ai pu détecter, en termes de taille du code du projet Roslyn (2 770 000 lignes), ce sera un montant assez faible. Comme Andrei dans l'article précédent, je suis également prêt à reconnaître la haute qualité de ce projet.

Je tiens à noter que ces vérifications de code occasionnelles n'ont rien à voir avec la méthodologie de l'analyse statique et n'apportent pratiquement aucun avantage. L'analyse statique doit être appliquée régulièrement, et non au cas par cas. Ensuite, de nombreuses erreurs seront corrigées dès les premières étapes et, par conséquent, le coût de leur correction sera dix fois moins élevé. Cette idée est décrite plus en détail dans ce petit article , que je vous demande de vous familiariser avec.

Vous pouvez rechercher indépendamment d'autres erreurs à la fois dans le projet considéré et dans tout autre. Pour ce faire, il vous suffit de télécharger et d'essayer notre analyseur.



Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Sergey Khrenov. Vérification du code source de Roslyn .

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


All Articles