Il y a quelques jours, Microsoft a rendu public le code source de leur calculatrice Windows. Calculatrice est une application qui est traditionnellement livrée avec toutes les versions de Windows. Un certain nombre de projets Microsoft sont devenus open source au cours des dernières années, mais cette fois, l'actualité a été couverte même par des médias non informatiques dès le premier jour. Eh bien, c'est un programme populaire mais minuscule en C ++. Malgré sa taille, nous avons quand même réussi à trouver un certain nombre de fragments suspects dans son code en utilisant l'analyseur statique PVS-Studio.
Présentation
Je ne pense pas que nous ayons besoin d'introduire Calculator car vous ne trouveriez guère un utilisateur Windows qui ne sait pas ce que c'est. Désormais, n'importe qui peut télécharger le code source de l'application depuis
GitHub et suggérer leurs améliorations.
La fonction suivante, par exemple, a déjà attiré l'attention de la communauté:
void TraceLogger::LogInvalidInputPasted(....) { if (!GetTraceLoggingProviderEnabled()) return;
LoggingFields fields{}; fields.AddString(L"Mode", NavCategory::GetFriendlyName(mode)->Data()); fields.AddString(L"Reason", reason); fields.AddString(L"PastedExpression", pastedExpression); fields.AddString(L"ProgrammerNumberBase", GetProgrammerType(...).c_str()); fields.AddString(L"BitLengthType", GetProgrammerType(bitLengthType).c_str()); LogTelemetryEvent(EVENT_NAME_INVALID_INPUT_PASTED, fields); }
Cette fonction enregistre le texte du presse-papiers et l'envoie apparemment aux serveurs Microsoft. Ce post, cependant, ne concerne pas cette fonction, mais vous verrez certainement de nombreux extraits suspects.
Nous avons utilisé l'analyseur statique
PVS-Studio pour vérifier le code source de Calculator. Comme il n'est pas écrit en C ++ standard, beaucoup de nos lecteurs réguliers doutaient qu'une telle vérification soit possible, mais nous l'avons fait. L'analyseur prend en charge C ++ / CLI et C ++ / CX, et même si certains diagnostics ont produit quelques faux positifs, nous n'avons rencontré aucun problème critique qui gênerait le travail de PVS-Studio.
Pour rappel, au cas où vous auriez manqué les informations sur les autres fonctionnalités de notre outil, PVS-Studio prend en charge non seulement C et C ++ mais aussi C # et Java.
Comparaison de chaînes incorrecte
V547 L' expression 'm_resolvedName == L "en-US"' est toujours fausse. Pour comparer des chaînes, vous devez utiliser la fonction wcscmp (). Calculator LocalizationSettings.h 180
wchar_t m_resolvedName[LOCALE_NAME_MAX_LENGTH]; Platform::String^ GetEnglishValueFromLocalizedDigits(....) const { if (m_resolvedName == L"en-US") { return ref new Platform::String(localizedString.c_str()); } .... }
Lors de l'affichage des rapports de l'analyseur, je trie les avertissements par code de diagnostic dans l'ordre croissant, et celui-ci, qui constitue un exemple assez vivant, s'est avéré être le premier de la liste.
Vous voyez, l'exemple ci-dessus montre une comparaison incorrecte des chaînes. Le programmeur compare en fait des pointeurs au lieu de valeurs de chaîne en comparant l'adresse d'un tableau de caractères à celle d'un littéral de chaîne. Ces pointeurs ne sont jamais égaux, la condition est donc toujours fausse également. Pour une comparaison correcte des chaînes, on devrait utiliser la fonction
wcscmp , par exemple.
À propos, pendant que j'écrivais cet article, le tableau de caractères
m_resolvedName a été corrigé dans le fichier d'en-tête et est devenu une chaîne complète de type
std :: wstring , de sorte que la comparaison peut être effectuée correctement maintenant. Au moment où vous lirez cet article, de nombreux autres bugs seront probablement corrigés aussi grâce aux passionnés et aux critiques comme celle-ci.
Fuite de mémoire en code natif
V773 La fonction a été
fermée sans relâcher le pointeur 'temp'. Une fuite de mémoire est possible. CalcViewModel StandardCalculatorViewModel.cpp 529
void StandardCalculatorViewModel::HandleUpdatedOperandData(Command cmdenum) { .... wchar_t* temp = new wchar_t[100]; .... if (commandIndex == 0) { delete [] temp; return; } .... length = m_selectedExpressionLastData->Length() + 1; if (length > 50) { return; } .... String^ updatedData = ref new String(temp); UpdateOperand(m_tokenPosition, updatedData); displayExpressionToken->Token = updatedData; IsOperandUpdatedUsingViewModel = true; displayExpressionToken->CommandIndex = commandIndex; }
Le pointeur
temporaire fait référence à un tableau alloué dynamiquement de 100 éléments. Malheureusement, la mémoire n'est libérée que dans une partie de la fonction, tandis que tous les autres se retrouvent avec une fuite de mémoire. Ce n'est pas trop mal, mais il est toujours considéré comme un bogue dans le code C ++.
Exception insaisissable
Les classes
V702 doivent toujours être dérivées de std :: exception (et similaires) en tant que «public» (aucun mot-clé n'a été spécifié, donc le compilateur le définit par défaut sur «privé»). CalcManager CalcException.h 4
class CalcException : std::exception { public: CalcException(HRESULT hr) { m_hr = hr; } HRESULT GetException() { return m_hr; } private: HRESULT m_hr; };
L'analyseur a détecté une classe dérivée de la classe
std :: exception à l'aide du modificateur
private (qui est par défaut si aucun autre modificateur n'est spécifié). Le problème avec ce code est que le gestionnaire ignorera l'exception de type
CalcException lorsqu'il
tentera d'attraper une
exception std :: générique car l'héritage privé interdit la conversion de type implicite.
Jour manqué
V719 L'instruction switch ne couvre pas toutes les valeurs de l'énumération 'DateUnit': Day. CalcViewModel DateCalculator.cpp 279
public enum class _Enum_is_bitflag_ DateUnit { Year = 0x01, Month = 0x02, Week = 0x04, Day = 0x08 }; Windows::Globalization::Calendar^ m_calendar; DateTime DateCalculationEngine::AdjustCalendarDate(Windows::Foundation::DateTime date, DateUnit dateUnit, int difference) { m_calendar→SetDateTime(date); switch (dateUnit) { case DateUnit::Year: { .... m_calendar->AddYears(difference); m_calendar->ChangeCalendarSystem(currentCalendarSystem); break; } case DateUnit::Month: m_calendar->AddMonths(difference); break; case DateUnit::Week: m_calendar->AddWeeks(difference); break; } return m_calendar->GetDateTime(); }
Il est suspect que l'instruction switch n'ait pas de cas
DateUnit :: Day . Pour cette raison, la valeur du jour ne sera pas ajoutée au calendrier (la variable
m_calendar ), bien que le calendrier ait la méthode
AddDays .
Autres cas suspects avec une autre énumération:
- V719 L'instruction switch ne couvre pas toutes les valeurs de l'énumération 'eANGLE_TYPE': ANGLE_RAD. CalcManager trans.cpp 109
- V719 L'instruction switch ne couvre pas toutes les valeurs de l'énumération 'eANGLE_TYPE': ANGLE_RAD. CalcManager trans.cpp 204
- V719 L'instruction switch ne couvre pas toutes les valeurs de l'énumération 'eANGLE_TYPE': ANGLE_RAD. CalcManager trans.cpp 276
Comparaison suspecte de nombres réels
V550 Une comparaison étrange et précise: ratio == seuil. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B) <Epsilon. Calculatrice AspectRatioTrigger.cpp 80
void AspectRatioTrigger::UpdateIsActive(Size sourceSize) { double numerator, denominator; .... bool isActive = false; if (denominator > 0) { double ratio = numerator / denominator; double threshold = abs(Threshold); isActive = ((ratio > threshold) || (ActiveIfEqual && (ratio == threshold))); } SetActive(isActive); }
L'analyseur a souligné le
taux d' expression suspecte
== seuil . Ces variables sont de type
double et, par conséquent, pourraient difficilement être comparées avec précision à l'aide de l'opérateur égal régulier. Par ailleurs, la valeur de la variable
ratio est le résultat d'une opération de division.
Un code comme celui-ci semble particulièrement étrange dans une application comme Calculatrice. J'inclus une liste complète des avertissements de ce type au cas où:
- V550 Une comparaison précise étrange. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B) <Epsilon. CalcManager UnitConverter.cpp 752
- V550 Une comparaison étrange et précise: stod (roundString)! = 0.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B)> Epsilon. CalcManager UnitConverter.cpp 778
- V550 Une comparaison précise étrange. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B) <Epsilon. CalcManager UnitConverter.cpp 790
- V550 Une comparaison étrange et précise: stod (roundString)! = 0.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B)> Epsilon. CalcManager UnitConverter.cpp 820
- V550 Une comparaison étrange et précise: table de conversion [m_toType] .ratio == 1.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B) <Epsilon. CalcManager UnitConverter.cpp 980
- V550 Une comparaison étrange et précise: conversionTable [m_toType] .offset == 0.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B) <Epsilon. CalcManager UnitConverter.cpp 980
- V550 Une comparaison étrange et précise: returnValue! = 0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B)> Epsilon. CalcManager UnitConverter.cpp 1000
- V550 Une comparaison étrange et précise: sizeToUse! = 0.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B)> Epsilon. CalcViewModel LocalizationService.cpp 270
- V550 Une comparaison étrange et précise: sizeToUse! = 0.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B)> Epsilon. CalcViewModel LocalizationService.cpp 289
- V550 Une comparaison étrange et précise: sizeToUse! = 0.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B)> Epsilon. CalcViewModel LocalizationService.cpp 308
- V550 Une comparaison étrange et précise: sizeToUse! = 0.0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B)> Epsilon. CalcViewModel LocalizationService.cpp 327
- V550 Une comparaison précise étrange: stod (stringToLocalize) == 0. Il est probablement préférable d'utiliser une comparaison avec une précision définie: fabs (A - B) <Epsilon. CalcViewModel UnitConverterViewModel.cpp 388
Séquence de fonctions suspectes
V1020 La fonction s'est terminée sans appeler la fonction 'TraceLogger :: GetInstance (). LogNewWindowCreationEnd'. Vérifier les lignes: 396, 375. Calculatrice App.xaml.cpp 396
void App::OnAppLaunch(IActivatedEventArgs^ args, String^ argument) { .... if (!m_preLaunched) { auto newCoreAppView = CoreApplication::CreateNewView(); newCoreAppView->Dispatcher->RunAsync(....([....]() { TraceLogger::GetInstance().LogNewWindowCreationBegin(....);
Diagnostic
V1020 inspecte les blocs de code et recherche les branches avec un appel de fonction manquant à l'aide de l'heuristique.
L'extrait ci-dessus contient un bloc avec les appels aux fonctions
LogNewWindowCreationBegin et
LogNewWindowCreationEnd . Ceci est suivi par un autre bloc où la fonction
LogNewWindowCreationEnd est appelée uniquement si certaines conditions sont remplies, ce qui semble très suspect.
Tests peu fiables
V621 Envisagez d'inspecter l'opérateur «pour». Il est possible que la boucle soit mal exécutée ou ne soit pas exécutée du tout. CalculatorUnitTests UnitConverterViewModelUnitTests.cpp 500
public enum class NumbersAndOperatorsEnum { .... Add = (int) CM::Command::CommandADD,
L'analyseur a détecté une boucle
for qui ne s'exécute pas du tout, ce qui signifie que les tests ne s'exécutent pas non plus. La valeur initiale du
bouton de compteur de boucle (93) est supérieure à la valeur finale (0) dès le début.
V760 Deux blocs de texte identiques ont été trouvés. Le deuxième bloc commence à la ligne 688. CalculatorUnitTests UnitConverterViewModelUnitTests.cpp 683
TEST_METHOD(TestSwitchAndReselectCurrentlyActiveValueDoesNothing) { shared_ptr<UnitConverterMock> mock = make_shared<UnitConverterMock>(); VM::UnitConverterViewModel vm(mock); const WCHAR * vFrom = L"1", *vTo = L"234"; vm.UpdateDisplay(vFrom, vTo); vm.Value2Active = true;
Un autre test suspect. L'analyseur a détecté deux fragments de code identiques s'exécutant immédiatement l'un après l'autre. Il semble que ce code ait été écrit en utilisant la technique du copier-coller et le programmeur a oublié de modifier les copies.
V601 La valeur 'false' est implicitement
convertie en type entier. Inspectez le deuxième argument. CalculatorUnitTests CalcInputTest.cpp 352
Rational CalcInput::ToRational(uint32_t radix, int32_t precision) { .... } TEST_METHOD(ToRational) { .... auto rat = m_calcInput.ToRational(10, false); .... }
La fonction
ToRational est appelée avec la valeur booléenne
false , tandis que le paramètre correspondant est de type
int32_t et est appelé
précision .
J'ai décidé de suivre la valeur dans le code et j'ai vu qu'elle était ensuite passée à la fonction
StringToRat :
PRAT StringToRat(...., int32_t precision) { .... }
puis à
StringToNumber :
PNUMBER StringToNumber(...., int32_t precision) { .... stripzeroesnum(pnumret, precision); .... }
Voici le corps de la fonction cible:
bool stripzeroesnum(_Inout_ PNUMBER pnum, long starting) { MANTTYPE *pmant; long cdigits; bool fstrip = false; pmant=pnum->mant; cdigits=pnum->cdigit; if ( cdigits > starting )
La variable de
précision s'appelle désormais
starting et participe à l'expression
cdigits> starting , ce qui est très suspect car
false a été transmis comme valeur d'origine.
Redondance
V560 Une partie de l'expression conditionnelle est toujours vraie: NumbersAndOperatorsEnum :: None! = Op. CalcViewModel UnitConverterViewModel.cpp 991
void UnitConverterViewModel::OnPaste(String^ stringToPaste, ViewMode mode) { .... NumbersAndOperatorsEnum op = MapCharacterToButtonId(*it, canSendNegate); if (NumbersAndOperatorsEnum::None != op)
La variable
op a déjà été comparée à la valeur
NumbersAndOperatorsEnum :: None , de sorte que la vérification en double peut être supprimée.
V728 Un contrôle excessif peut être simplifié. Le '(A && B) || (! A &&! B) 'est équivalente à l'expression' bool (A) == bool (B) '. Calculatrice Calculator.xaml.cpp 239
void Calculator::AnimateCalculator(bool resultAnimate) { if (App::IsAnimationEnabled()) { m_doAnimate = true; m_resultAnimate = resultAnimate; if (((m_isLastAnimatedInScientific && IsScientific) || (!m_isLastAnimatedInScientific && !IsScientific)) && ((m_isLastAnimatedInProgrammer && IsProgrammer) || (!m_isLastAnimatedInProgrammer && !IsProgrammer))) { this->OnStoryboardCompleted(nullptr, nullptr); } } }
Cette énorme expression conditionnelle comptait à l'origine 218 caractères, mais je l'ai divisée en plusieurs lignes à des fins de démonstration. Il peut être réécrit dans une version beaucoup plus courte et, surtout, plus claire:
if ( m_isLastAnimatedInScientific == IsScientific && m_isLastAnimatedInProgrammer == IsProgrammer) { this->OnStoryboardCompleted(nullptr, nullptr); }
V524 Il est étrange que le corps de la fonction 'ConvertBack' soit entièrement équivalent au corps de la fonction 'Convert'. Calculator BooleanNegationConverter.cpp 24
Object^ BooleanNegationConverter::Convert(....) { (void) targetType;
L'analyseur a détecté deux fonctions implémentées de manière identique. Comme leurs noms,
Convert et
ConvertBack , le suggèrent, ils étaient censés faire des choses différentes, mais les développeurs devraient en savoir plus.
Conclusion
Je suppose que chaque projet Microsoft qui a été rendu open-source nous a donné l'occasion de montrer l'importance de l'analyse statique - même sur des projets aussi petits que Calculator. Les grandes entreprises, telles que Microsoft, Google, Amazon, etc., emploient beaucoup de développeurs talentueux, mais ce sont toujours des humains qui font des erreurs. Les outils d'analyse statique sont l'un des meilleurs moyens d'aider toute équipe de développeurs à améliorer la qualité de leurs produits.
Bienvenue pour télécharger
PVS-Studio et l'essayer sur votre propre «calculatrice». :-)