Il s'agit du deuxième article sur l'utilisation de l'analyseur statique PVS-Studio dans les systèmes CI cloud, et cette fois, nous examinerons la plate-forme Azure DevOps - une solution cloud CI \ CD de Microsoft. En tant que projet analysé cette fois, considérez ShareX.
Nous aurons besoin de trois composants. Le premier est l'analyseur statique PVS-Studio. Le second est Azure DevOps, avec lequel nous intégrerons l'analyseur. Le troisième est un projet que nous allons vérifier pour démontrer les capacités de PVS-Studio lorsque vous travaillez dans le cloud. Commençons donc.
PVS-Studio est un analyseur de code statique pour la recherche d'erreurs et de défauts de sécurité. Effectue une analyse de code en C, C ++, C # et Java.
Azure DevOps . La plate-forme Azure DevOps comprend des outils tels que Azure Pipeline, Azure Board, Azure Artifacts et autres pour accélérer le processus de création de logiciels et améliorer sa qualité.
ShareX est une application gratuite qui vous permet de capturer et d'enregistrer n'importe quelle partie de l'écran. Le projet est écrit en C # et est idéal pour montrer comment exécuter l'analyseur statique. Le code source du projet
est disponible sur GitHub .
La sortie de la commande cloc pour le projet ShareX:
En d'autres termes, le projet est petit, mais tout à fait suffisant pour démontrer le travail de PVS-Studio en conjonction avec une plateforme cloud.
Mettons en place
Pour commencer dans Azure DevOps, cliquez sur le
lien et cliquez sur le bouton «Commencer gratuitement avec GitHub».
Donner à l'application Microsoft l'accès aux données du compte GitHub.
Pour terminer l'inscription, vous devrez créer un compte Microsoft.
Après l'enregistrement, créez un projet:
Ensuite, nous devons aller dans la section «Pipelines» - «Builds» et créer un nouveau pipeline de construction
À la question où se trouve notre code, nous répondrons - GitHub.
Nous autorisons l'application Azure Pipelines et sélectionnons le référentiel avec le projet pour lequel nous allons configurer le lancement de l'analyseur statique
Dans la fenêtre de sélection du modèle, sélectionnez «Starter pipeline».
Nous pouvons exécuter une analyse statique du code de projet de deux manières: en utilisant des agents hébergés par Microsoft ou auto-hébergés.
Dans la première version, nous utiliserons des agents hébergés par Microsoft. Ces agents sont des machines virtuelles ordinaires qui démarrent lorsque nous démarrons notre pipeline et sont supprimées après la fin de la tâche. L'utilisation de tels agents vous permet de ne pas perdre de temps à les prendre en charge et à les mettre à jour, mais impose certaines restrictions, par exemple l'impossibilité d'installer des logiciels supplémentaires utilisés pour construire le projet.
Remplacez notre configuration par défaut par ce qui suit pour utiliser des agents hébergés par Microsoft:
# # master- trigger: - master # # , Docker-, # Windows Server 1803 pool: vmImage: 'win1803' container: microsoft/dotnet-framework:4.7.2-sdk-windowsservercore-1803 steps: # - task: PowerShell@2 inputs: targetType: 'inline' script: 'Invoke-WebRequest -Uri https://files.viva64.com/PVS-Studio_setup.exe -OutFile PVS-Studio_setup.exe' - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | # nuget restore .\ShareX.sln # , md .\PVSTestResults # PVS-Studio_setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /COMPONENTS=Core # "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" credentials -u $(PVS_USERNAME) -n $(PVS_KEY) # html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog # - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults
Remarque: selon la
documentation , le conteneur utilisé doit être mis en cache dans l'image de la machine virtuelle, mais au moment de la rédaction de cet article ne fonctionne pas et le conteneur est téléchargé à chaque démarrage de la tâche, ce qui affecte négativement le temps d'exécution.
Enregistrez le pipeline et créez les variables qui seront utilisées pour créer le fichier de licence. Pour ce faire, ouvrez la fenêtre d'édition du pipeline et dans le coin supérieur droit cliquez sur le bouton «Variables».
Ajoutez deux variables -
PVS_USERNAME et
PVS_KEY , contenant respectivement le nom d'utilisateur et la clé de licence. Lors de la création de la variable
PVS_KEY ,
n'oubliez pas de cocher l'élément "Garder cette valeur secrète" pour crypter la valeur de la variable avec une clé RSA 2048 bits, ainsi que pour supprimer la sortie de la valeur de la variable dans le journal d'exécution de la tâche.
Nous enregistrons les variables et démarrons le pipeline avec le bouton «Exécuter».
La deuxième option pour exécuter l'analyse consiste à utiliser un agent auto-hébergé. Les agents auto-hébergés sont des agents que nous configurons et gérons nous-mêmes. Ces agents offrent plus de possibilités d'installation de logiciels, ce qui est nécessaire pour l'assemblage et les tests de notre produit logiciel.
Avant d'utiliser de tels agents, ils doivent être configurés conformément aux
instructions et un analyseur statique doit être installé et
configuré .
Pour démarrer la tâche sur un agent auto-hébergé, nous remplaçons la configuration par défaut proposée par la suivante:
# # master- trigger: - master # self-hosted 'MyPool' pool: 'MyPool' steps: - task: CmdLine@2 inputs: workingDirectory: $(System.DefaultWorkingDirectory) script: | # nuget restore .\ShareX.sln # , md .\PVSTestResults # html. "C:\Program Files (x86)\PVS-Studio\PVS-Studio_Cmd.exe" -t .\ShareX.sln -o .\PVSTestResults\ShareX.plog "C:\Program Files (x86)\PVS-Studio\PlogConverter.exe" -t html -o .\PVSTestResults\ .\PVSTestResults\ShareX.plog # - task: PublishBuildArtifacts@1 inputs: pathToPublish: PVSTestResults artifactName: PVSTestResults
Après avoir terminé la tâche, l'archive avec les rapports de l'analyseur peut être téléchargée sur l'onglet Résumé, ou nous pouvons utiliser l'extension
Envoyer un courrier électronique , qui vous permet de configurer l'envoi d'e-mails, ou chercher un outil plus pratique pour nous sur la place de
marché .
À propos des résultats d'analyse
Examinons maintenant certaines des erreurs détectées dans le projet vérifié - ShareX.
Contrôles redondantsPour s'échauffer, commençons par de simples failles dans le code, à savoir avec des contrôles redondants:
private void PbThumbnail_MouseMove(object sender, MouseEventArgs e) { .... IDataObject dataObject = new DataObject(DataFormats.FileDrop, new string[] { Task.Info.FilePath }); if (dataObject != null) { Program.MainForm.AllowDrop = false; dragBoxFromMouseDown = Rectangle.Empty; pbThumbnail.DoDragDrop(dataObject, DragDropEffects.Copy | DragDropEffects.Move); Program.MainForm.AllowDrop = true; } .... }
Avertissement PVS-Studio :
V3022 [CWE-571] L'expression 'dataObject! = Null' est toujours vraie. TaskThumbnailPanel.cs 415
Faites attention à vérifier la variable
dataObject pour
null . Pourquoi est-elle ici?
dataObject ne peut tout simplement pas être
null dans ce cas, car il est initialisé avec une référence à l'objet créé. En conséquence, nous avons une vérification redondante. Est-ce critique? Non. Semble concis? Non. Cette vérification est clairement mieux supprimée afin de ne pas encombrer le code.
Jetons un coup d'œil à un autre morceau de code, auquel vous pouvez faire des commentaires similaires:
private static Image GetDIBImage(MemoryStream ms) { .... try { .... return new Bitmap(bmp); .... } finally { if (gcHandle != IntPtr.Zero) { GCHandle.FromIntPtr(gcHandle).Free(); } } .... } private static Image GetImageAlternative() { .... using (MemoryStream ms = dataObject.GetData(format) as MemoryStream) { if (ms != null) { try { Image img = GetDIBImage(ms); if (img != null) { return img; } } catch (Exception e) { DebugHelper.WriteException(e); } } } .... }
Avertissement PVS-Studio :
V3022 [CWE-571] L'expression 'img! = Null' est toujours vraie. ClipboardHelpers.cs 289
La méthode
GetImageAlternative vérifie à nouveau que la variable
img n'est pas
nulle immédiatement après la création d'une nouvelle instance de la classe
Bitmap . La différence avec l'exemple précédent ici est que pour initialiser la variable
img , nous n'utilisons pas le constructeur, mais la méthode
GetDIBImage . L'auteur du code suppose qu'une exception peut se produire dans cette méthode, mais déclare uniquement
try et
finalement bloque, en omettant
catch . Par conséquent, si une exception se produit, la méthode appelante
- GetImageAlternative - ne recevra pas de référence à un objet de type
Bitmap, mais sera forcée de gérer l'exception dans son propre
bloc catch . Dans ce cas, la variable
img ne sera pas initialisée, et le thread d'exécution n'atteindra même pas le contrôle
img! = Null , mais tombera immédiatement dans le
bloc catch . Par conséquent, l'analyseur a indiqué une validation redondante.
Considérez l'exemple d'avertissement suivant avec le code
V3022 :
private void btnCopyLink_Click(object sender, EventArgs e) { .... if (lvClipboardFormats.SelectedItems.Count == 0) { url = lvClipboardFormats.Items[0].SubItems[1].Text; } else if (lvClipboardFormats.SelectedItems.Count > 0) { url = lvClipboardFormats.SelectedItems[0].SubItems[1].Text; } .... }
Avertissement PVS-Studio :
V3022 [CWE-571] L'expression 'lvClipboardFormats.SelectedItems.Count> 0' est toujours vraie. AfterUploadForm.cs 155
Regardons la deuxième expression conditionnelle. Là, nous vérifions la valeur de la propriété
Count en lecture seule. Cette propriété affiche le nombre d'éléments dans une instance de la collection
SelectedItems . La condition n'est satisfaite que si la propriété
Count est supérieure à zéro. Tout irait bien, mais ce n'est que dans l'
instruction if externe que le
compte est déjà vérifié. Une instance de la collection
SelectedItems ne peut pas avoir le nombre d'éléments inférieur à zéro, par conséquent,
Count prend une valeur égale à zéro ou supérieure à zéro. Étant donné que nous avons déjà effectué une vérification dans la première
instruction if que
Count est zéro et qu'il s'est avéré être faux, cela n'a aucun sens d'écrire une autre vérification sur la branche else que
Count est supérieur à zéro.
Le dernier exemple d'erreur numéro
V3022 est le fragment de code suivant:
private void DrawCursorGraphics(Graphics g) { .... int cursorOffsetX = 10, cursorOffsetY = 10, itemGap = 10, itemCount = 0; Size totalSize = Size.Empty; int magnifierPosition = 0; Bitmap magnifier = null; if (Options.ShowMagnifier) { if (itemCount > 0) totalSize.Height += itemGap; .... } .... }
PVS-Studio Warning :
V3022 Expression 'itemCount> 0' est toujours false. RegionCaptureForm.cs 1100.
L'analyseur a remarqué que la condition
itemCount> 0 sera toujours fausse, car une déclaration un peu plus élevée est effectuée et la variable
itemCount est
définie sur zéro en même temps. Jusqu'à la condition même, cette variable n'est utilisée nulle part et ne change pas, par conséquent, l'analyseur a tiré la bonne conclusion sur l'expression conditionnelle, dont la valeur est toujours fausse.
Eh bien, regardons maintenant quelque chose de vraiment intéressant.
La meilleure façon de comprendre le bogue est de visualiser le bogue.Il nous semble qu'une erreur assez intéressante a été trouvée à cet endroit:
public static void Pixelate(Bitmap bmp, int pixelSize) { .... float r = 0, g = 0, b = 0, a = 0; float weightedCount = 0; for (int y2 = y; y2 < yLimit; y2++) { for (int x2 = x; x2 < xLimit; x2++) { ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight; } } .... ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount)); .... }
Je ne veux pas révéler immédiatement toutes les cartes et montrer ce que notre analyseur a trouvé ici, alors reportons ce moment pour un court instant.
Par le nom de la méthode, il est facile de deviner ce qu'elle fait - vous lui soumettez une image ou un fragment de l'image en entrée, et elle effectue sa pixellisation. Le code de la méthode est assez long, donc nous ne le donnerons pas ici dans son intégralité, mais essayez simplement d'expliquer son algorithme et d'expliquer quel type de bogue PVS-Studio a trouvé ici.
Cette méthode accepte deux paramètres en entrée: un objet de type
Bitmap et une valeur de type
int , qui indique la taille des pixels. L'algorithme de fonctionnement est assez simple:
1) On décompose le fragment de l'image reçue à l'entrée en carrés de côté égal à la taille de la pixellisation. Par exemple, si nous avons une taille de pixelisation de 15, nous obtenons alors un carré contenant 15x15 = 225 pixels.
2) Ensuite, nous contournons chaque pixel de ce carré et accumulons les valeurs des champs
Rouge ,
Vert ,
Bleu et
Alpha en variables intermédiaires, et en multipliant précédemment la valeur de couleur correspondante et la valeur de canal alpha par la variable
pixelWeight , obtenue en divisant la valeur
Alpha par 255 (la variable
Alpha a type
octet ). De plus, lors de la traversée de pixels, nous additionnons les valeurs enregistrées dans
pixelWeight dans une variable appelée
weightedCount .
L'extrait de code qui effectue les étapes ci-dessus est le suivant:
ColorBgra color = unsafeBitmap.GetPixel(x2, y2); float pixelWeight = color.Alpha / 255; r += color.Red * pixelWeight; g += color.Green * pixelWeight; b += color.Blue * pixelWeight; a += color.Alpha * pixelWeight; weightedCount += pixelWeight;
Par ailleurs, notez que si la valeur de la variable
Alpha est zéro,
pixelWeight n'ajoutera aucune valeur à la variable
weightedCount pour ce pixel. Nous en aurons besoin à l'avenir.
3) Après avoir contourné tous les pixels du carré actuel, nous pouvons composer la couleur «moyenne» générale pour ce carré. Le code qui effectue ces actions est le suivant:
ColorBgra averageColor = new ColorBgra((byte)(b / weightedCount), (byte)(g / weightedCount), (byte)(r / weightedCount), (byte)(a / pixelCount));
4) Maintenant que nous avons la couleur finale et l'
écrivons dans la variable
averageColor , nous pouvons à nouveau faire le tour de chaque pixel du carré et lui affecter une valeur de
averageColor .
5) Nous revenons à l'étape 2 tant qu'il reste des carrés bruts.
Encore une fois, nous notons que la variable
weightedCount n'est pas égale au nombre de tous les pixels au carré. Par exemple, si un pixel absolument transparent est rencontré dans l'image (la valeur est nulle sur le canal alpha), alors la variable
pixelWeight sera nulle pour ce pixel (
0/255 = 0), par conséquent, ce pixel ne contribuera pas à la formation de la valeur de la variable
weightedCount . C'est logique - cela n'a aucun sens de prendre en compte les couleurs d'un pixel absolument transparent.
Tout semble assez raisonnable - la pixellisation devrait fonctionner correctement. Et cela fonctionne vraiment bien. Ce n'est tout simplement pas pour les images png qui ont des pixels avec des valeurs dans le canal alpha inférieures à 255 et différentes de zéro. Faites attention à l'image pixélisée ci-dessous:
Avez-vous vu la pixellisation? Et nous ne le sommes pas. Eh bien, maintenant, révélons cette petite intrigue et expliquons où exactement le bogue est caché dans cette méthode. L'erreur s'est glissée dans la ligne de calcul de la valeur de la variable
pixelWeight :
float pixelWeight = color.Alpha / 255;
Le fait est que l'auteur du code, déclarant la variable
pixelWeight comme float , a impliqué que lors de la division du champ
Alpha par 255, en plus de zéro et un, des nombres fractionnaires devraient être obtenus. C'est là que réside le problème, puisque la variable
Alpha est de type
octet , et lorsque nous la divisons par 255, nous obtenons une valeur entière, et alors seulement elle sera implicitement transtypée pour
flotter , par conséquent, la partie fractionnaire est perdue.
L'incapacité à pixelliser des images PNG présentant un certain degré de transparence est facile à expliquer. Étant donné que les valeurs du canal alpha pour ces pixels se situent dans la plage 0 <Alpha <255, lors de la division de la variable
Alpha par 255, nous obtiendrons toujours 0. Par conséquent, les valeurs des
variables pixelWeight ,
r ,
g ,
b ,
a ,
weightedCount sont également toujours sera nul. Par conséquent, notre couleur moyenne
averageColor sera avec des valeurs nulles sur tous les canaux: rouge - 0, bleu - 0, vert - 0, alpha - 0. En remplissant le carré avec cette couleur, nous ne changeons pas la couleur d'origine des pixels, car
averageColor est absolument transparent . Pour corriger cette erreur, il vous suffit de convertir explicitement le champ
Alpha en type
flottant . La ligne de code corrigée peut ressembler à ceci:
float pixelWeight = (float)color.Alpha / 255;
Et il est temps de citer le message que PVS-Studio a donné au code incorrect:
Avertissement PVS-Studio :
V3041 [CWE-682] L'expression a été implicitement
convertie du type 'int' en type 'float'. Pensez à utiliser un transtypage de type explicite pour éviter la perte d'une partie fractionnaire. Un exemple: double A = (double) (X) / Y;. ImageHelpers.cs 1119.
Et à titre de comparaison, nous donnons une capture d'écran d'une image vraiment pixélisée obtenue sur une version fixe de l'application:
Potentiel NullReferenceException public static bool AddMetadata(Image img, int id, string text) { .... pi.Value = bytesText; if (pi != null) { img.SetPropertyItem(pi); return true; } .... }
Avertissement PVS-Studio: V3095 [CWE-476] L'objet 'pi' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 801, 803. ImageHelpers.cs 801
Ce fragment de code montre que son auteur s'attendait à ce que la variable
pi soit
nulle , c'est pourquoi la vérification
pi! = Null est effectuée avant d'appeler la méthode
SetPropertyItem . Il est étrange qu'avant cette vérification, un tableau d'octets soit affecté à la propriété
pi.Value , car si
pi est
nul , une exception de type
NullReferenceException sera levée.
Une situation similaire a été observée ailleurs:
private static void Task_TaskCompleted(WorkerTask task) { .... task.KeepImage = false; if (task != null) { if (task.RequestSettingUpdate) { Program.MainForm.UpdateCheckStates(); } .... } .... }
Avertissement PVS-Studio: V3095 [CWE-476] L'objet 'tâche' a été utilisé avant d'être vérifié par rapport à null. Vérifiez les lignes: 268, 270. TaskManager.cs 268
PVS-Studio a trouvé une autre erreur similaire. La signification est toujours la même, il n'y a donc pas grand besoin de donner un fragment de code, nous nous limitons au message de l'analyseur.
Avertissement PVS-Studio: V3095 [CWE-476] L'objet 'Config.PhotobucketAccountInfo' a été utilisé avant d'être vérifié par rapport à null. Vérifier les lignes: 216, 219. UploadersConfigForm.cs 216
La même valeur de retourUn morceau de code suspect a été découvert dans la méthode
EvalWindows de la classe
WindowsList , qui retourne
true en toutes circonstances:
public class WindowsList { public List<IntPtr> IgnoreWindows { get; set; } .... public WindowsList() { IgnoreWindows = new List<IntPtr>(); } public WindowsList(IntPtr ignoreWindow) : this() { IgnoreWindows.Add(ignoreWindow); } .... private bool EvalWindows(IntPtr hWnd, IntPtr lParam) { if (IgnoreWindows.Any(window => hWnd == window)) { return true;
Avertissement PVS-Studio: V3009 Il est étrange que cette méthode renvoie toujours une seule et même valeur de «vrai». WindowsList.cs 82
Il semble logique que si un pointeur avec la même valeur que
hWnd était trouvé dans la liste avec le nom
IgnoreWindows , alors la méthode devrait retourner
false .
La liste
IgnoreWindows peut être remplie soit en appelant le constructeur
WindowsList (IntPtr ignoreWindow) , soit directement via l'accès à la propriété, car elle est publique. D'une manière ou d'une autre, selon Visual Studio, pour le moment, dans le code, cette liste n'est en aucun cas remplie. C'est un autre endroit étrange de cette méthode.
Appel non sécurisé aux gestionnaires d'événements protected void OnNewsLoaded() { if (NewsLoaded != null) { NewsLoaded(this, EventArgs.Empty); } }
Avertissement PVS-Studio: V3083 [CWE-367] L'appel non sécurisé de l'événement 'NewsLoaded', NullReferenceException est possible. Pensez à affecter un événement à une variable locale avant de l'invoquer. NewsListControl.cs 111
Dans ce cas, la situation désagréable suivante peut se produire: après avoir vérifié la variable
NewsLoaded pour une inégalité
nulle , la méthode qui traite l'événement peut être désabonnée, par exemple, dans un autre thread, et lorsque nous entrons dans le corps de l'
instruction conditionnelle
if , la variable
NewsLoaded sera déjà est
nul . Tenter d'appeler des abonnés sur un événement
NewsLoaded qui est
null lèvera une
exception NullReferenceException . Il est beaucoup plus sûr d'utiliser l'opérateur conditionnel nul et de réécrire le code ci-dessus comme suit:
protected void OnNewsLoaded() { NewsLoaded?.Invoke(this, EventArgs.Empty); }
L'analyseur a indiqué
68 endroits plus similaires. Nous ne les décrirons pas ici - le modèle de l'appel d'événement en eux est similaire.
Retourne null de ToStringIl n'y a pas si longtemps, à partir d'un
article intéressant d'un collègue
, j'ai découvert que Microsoft ne recommandait pas de retourner
null à partir d'une méthode
ToString remplacée. PVS-Studio en est bien conscient:
public override string ToString() { lock (loggerLock) { if (sbMessages != null && sbMessages.Length > 0) { return sbMessages.ToString(); } return null; } }
Avertissement PVS-Studio: V3108 Il n'est pas recommandé de renvoyer 'null' à partir de la méthode 'ToSting ()'. Logger.cs 167
Pourquoi convient-il si je n'utilise pas? public SeafileCheckAccInfoResponse GetAccountInfo() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "account/info/?format=json"); .... }
PVS-Studio Warning: V3008 La variable 'url' se voit attribuer des valeurs deux fois de suite. C'est peut-être une erreur. Vérifiez les lignes: 197, 196. Seafile.cs 197
Comme vous pouvez le voir dans l'exemple, lors de la déclaration de la variable
url , une valeur renvoyée par la méthode
FixPrefix lui est attribuée. Dans la ligne suivante, nous «broyons» la valeur résultante, même sans l'utiliser nulle part. Nous obtenons quelque chose de similaire au "code mort" - il fait le travail, il n'affecte pas le résultat final. Cette erreur est probablement le résultat du copier-coller, car de tels fragments de code se trouvent dans 9 autres méthodes.
Pour un exemple, nous donnons deux méthodes avec une première ligne similaire:
public bool CheckAuthToken() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "auth/ping/?format=json"); .... } .... public bool CheckAPIURL() { string url = URLHelpers.FixPrefix(APIURL); url = URLHelpers.CombineURL(APIURL, "ping/?format=json"); .... }
Total
Comme nous pouvons le voir, la complexité de la configuration de la vérification automatique par l'analyseur ne dépend pas du système CI sélectionné - en seulement 15 minutes et en quelques clics de souris, nous configurons la vérification de notre code de projet avec un analyseur statique.
En conclusion, nous vous suggérons de
télécharger et d'essayer l'analyseur sur vos projets.

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Oleg Andreev, Ilya Gainulin.
PVS-Studio dans les nuages: Azure DevOps .