web-dev-qa-db-fra.com

Comment savoir quelles parties du code ne sont jamais utilisées?

J'ai du code C++ hérité que je suis supposé supprimer du code inutilisé. Le problème est que la base de code est volumineuse.

Comment puis-je savoir quel code n'est jamais appelé/jamais utilisé?

309
user63898

Il existe deux variétés de code inutilisé:

  • le local, c’est-à-dire que dans certaines fonctions, certains chemins ou variables sont inutilisés (ou utilisés mais de manière non significative, comme écrit mais jamais lu)
  • le global: fonctions jamais appelées, objets globaux jamais accédés

Pour le premier type, un bon compilateur peut aider:

  • -Wunused _ (GCC, Clang ) devrait avertir des variables inutilisées, l'analyseur non utilisé de Clang a même été incrémenté pour avertir des variables qui ne sont jamais lues (même si elles sont utilisées).
  • -Wunreachable-code _ (ancien GCC, supprimé en 201 ) devrait avertir des blocs locaux qui ne sont jamais accédés (cela se produit avec des retours précoces ou des conditions toujours considérées comme vraies)
  • à ma connaissance, il n’existe aucune option permettant d’avertir les blocs inutilisés catch, car le compilateur ne peut généralement pas prouver qu’aucune exception ne sera levée.

Pour le second type, c'est beaucoup plus difficile. Statiquement, il nécessite une analyse complète du programme. Même si l'optimisation du temps de liaison peut réellement supprimer le code mort, le programme a été tellement transformé au moment de son exécution qu'il est presque impossible de transmettre des informations significatives à l'utilisateur.

Il y a donc deux approches:

  • La théorie consiste à utiliser un analyseur statique. Un logiciel qui examinera tout le code à la fois en détail et trouvera tous les chemins de flux. En pratique, je n'en connais aucun qui fonctionnerait ici.
  • Le pragmatique consiste à utiliser une heuristique: utilisez un outil de couverture de code (dans la chaîne GNU, il s'agit de gcov. Notez que des indicateurs spécifiques doivent être passés lors de la compilation pour fonctionner correctement) Vous exécutez l’outil de couverture de code avec un bon ensemble d’entrées variées (vos tests unitaires ou vos tests de non-régression), le code mort est nécessairement compris dans le code non atteint ... et vous pouvez donc commencer à partir de cet endroit.

Si le sujet vous intéresse énormément et que vous avez le temps et l'envie de créer vous-même un outil, je vous conseillerais d'utiliser les bibliothèques Clang pour créer un tel outil.

  1. Utilisez la bibliothèque Clang pour obtenir un AST (arbre de syntaxe abstraite)
  2. Effectuer une analyse par balisage à partir des points d'entrée

Comme Clang analysera le code pour vous et effectuera la résolution de la surcharge, vous n’aurez pas à vous préoccuper des règles du langage C++ et vous pourrez vous concentrer sur le problème à résoudre.

Cependant, ce type de technique ne peut pas identifier les remplacements virtuels inutilisés, car ils pourraient être appelés par un code tiers que vous ne pouvez pas raisonner.

195
Matthieu M.

Dans le cas de fonctions entières inutilisées (et de variables globales inutilisées), GCC peut réellement effectuer la majeure partie du travail, à condition que vous utilisiez GCC et GNU ld.

Lors de la compilation de la source, utilisez -ffunction-sections et -fdata-sections, puis en reliant utiliser -Wl,--gc-sections,--print-gc-sections. L'éditeur de liens va maintenant lister toutes les fonctions qui pourraient être supprimées car elles n'ont jamais été appelées et tous les globals qui n'ont jamais été référencés.

(Bien sûr, vous pouvez aussi sauter le --print-gc-sections partie et laissez l’éditeur de liens supprimer les fonctions en mode silencieux, mais conservez-les dans le source.)

Note: cela ne trouvera que des fonctions complètes non utilisées, cela ne fera rien au sujet du code mort dans les fonctions. Les fonctions appelées depuis le code mort dans les fonctions en direct seront également conservées.

Certaines fonctionnalités spécifiques à C++ poseront également des problèmes, notamment:

  • Fonctions virtuelles. Sans savoir quelles sous-classes existent et lesquelles sont réellement instanciées au moment de l'exécution, vous ne pouvez pas savoir quelles fonctions virtuelles doivent exister dans le programme final. L'éditeur de liens n'a pas assez d'informations à ce sujet, il devra donc les conserver toutes.
  • Globals avec constructeurs et leurs constructeurs. En général, l'éditeur de liens ne peut pas savoir que le constructeur d'un global n'a pas d'effets secondaires, il doit donc l'exécuter. Évidemment, cela signifie que le global lui-même doit également être conservé.

Dans les deux cas, tout ce qui est utilisé par une fonction virtuelle ou un constructeur de variable globale doit également être conservé.

Une mise en garde supplémentaire est que si vous construisez une bibliothèque partagée, les paramètres par défaut de GCC exporteront chaque fonction de la bibliothèque partagée, ce qui entraînera son exportation. "utilisé" en ce qui concerne l'éditeur de liens. Pour résoudre ce problème, vous devez définir le paramètre par défaut de masquage de symboles au lieu d’exporter (en utilisant par exemple -fvisibility=hidden), puis sélectionnez explicitement les fonctions exportées que vous devez exporter.

34
olsner

Eh bien, si vous utilisez g ++, vous pouvez utiliser cet indicateur -Wunused

Selon la documentation:

Avertir chaque fois qu'une variable est utilisée en dehors de sa déclaration, chaque fois qu'une fonction est déclarée statique mais non définie, chaque fois qu'une étiquette est déclarée mais non utilisée et chaque fois qu'une instruction calcule un résultat explicitement non utilisé.

http://docs.freebsd.org/info/gcc/gcc.info.Warning_Options.html

Edit : Voici un autre indicateur utile -Wunreachable-code Selon la documentation:

Cette option est destinée à avertir lorsque le compilateur détecte qu'au moins une ligne entière de code source ne sera jamais exécutée, car certaines conditions ne sont jamais satisfaites ou parce qu'elles suivent une procédure qui ne retourne jamais.

Mise à jour : J'ai trouvé un sujet similaire Détection de code mort dans un projet C/C++ hérité

25
UmmaGumma

Je pense que vous recherchez un outil couverture de code . Un outil de couverture de code analysera votre code en cours d’exécution et vous indiquera quelles lignes de code ont été exécutées et combien de fois, ainsi que celles qui ne l’ont pas été.

Vous pouvez essayer de donner une chance à cet outil de couverture de code open source: TestCocoon - outil de couverture de code pour C/C++ et C #.

18
Carlos V

La vraie réponse est la suivante: Vous ne pouvez jamais vraiment savoir avec certitude.

Au moins, dans les cas non triviaux, vous ne pouvez pas être sûr d'avoir tout compris. Considérez ce qui suit à partir de article de Wikipedia sur le code inaccessible :

double x = sqrt(2);
if (x > 5)
{
  doStuff();
}

Comme Wikipedia le note correctement, un compilateur intelligent peut être capable d’attraper quelque chose comme ça. Mais considérons une modification:

int y;
cin >> y;
double x = sqrt((double)y);

if (x != 0 && x < 1)
{
  doStuff();
}

Est-ce que le compilateur va attraper ça? Peut être. Mais pour cela, il faudra faire plus que lancer sqrt contre une valeur scalaire constante. Il va falloir comprendre que (double)y sera toujours un entier (facile), puis comprendra la plage mathématique de sqrt pour l'ensemble des entiers (durs). Un compilateur très sophistiqué pourrait peut-être faire cela pour la fonction sqrt ou pour chaque fonction de math.h, ou pour toute fonction à entrée fixe dont le domaine peut être représenté en dehors. Cela devient très, très complexe et la complexité est pratiquement illimitée. Vous pouvez continuer à ajouter des couches de sophistication à votre compilateur, mais il y aura toujours un moyen d'insérer du code inaccessible pour un ensemble d'entrées donné.

Et puis il y a les ensembles d'entrées qui ne sont jamais entrés. Des entrées qui n'auraient aucun sens dans la vie réelle, ou qui seraient bloquées par la logique de validation ailleurs. Le compilateur n'a aucun moyen de connaître ces informations.

Le résultat final est que, même si les outils logiciels mentionnés par d’autres sont extrêmement utiles, vous ne saurez jamais si vous avez tout saisi, à moins que vous ne lisiez le code manuellement par la suite. Même alors, vous ne serez jamais certain de n'avoir rien manqué.

La seule vraie solution, à mon humble avis, est d’être aussi vigilant que possible, d’utiliser l’automatisation à votre disposition, de procéder à une refactorisation dans la mesure du possible, et de rechercher en permanence des moyens d’améliorer votre code. Bien sûr, c’est une bonne idée de le faire quand même.

15
Justin Morgan

Je ne l'ai pas utilisé moi-même, mais cppcheck , prétend retrouver des fonctions inutilisées. Cela ne résoudra probablement pas le problème, mais ce pourrait être un début.

12
Mr Shark

Vous pouvez essayer d'utiliser PC-lint/FlexeLint de Gimple Software . Il prétend à

trouver des macros, typedef's, classes, membres, déclarations, etc. inutilisés dans tout le projet

Je l'ai utilisé pour l'analyse statique et l'ai trouvé très bien, mais je dois admettre que je ne l'ai pas utilisé pour rechercher spécifiquement du code mort.

9
Tony

Marquez autant de fonctions et de variables publiques que privées ou protégées sans causer d'erreur de compilation. Essayez de refactoriser le code. En rendant les fonctions privées et, dans une certaine mesure, protégées, vous réduisez votre zone de recherche car les fonctions privées ne peuvent être appelées que de la même classe (sauf en cas de macro stupide ou d’autres astuces pour contourner la restriction d’accès. Si tel était le cas, je vous le recommanderais. trouver un nouvel emploi). Il est beaucoup plus facile de déterminer que vous n'avez pas besoin d'une fonction privée, car seule la classe sur laquelle vous travaillez actuellement peut appeler cette fonction. Cette méthode est plus facile si votre base de code a de petites classes et est couplée de manière lâche. Si votre base de code n'a pas de petites classes ou a un couplage très étroit, je suggère de les nettoyer en premier.

La prochaine étape consistera à marquer toutes les fonctions publiques restantes et à créer un graphe d’appel pour déterminer la relation entre les classes. À partir de cet arbre, essayez de déterminer quelle partie de la branche peut être coupée.

L'avantage de cette méthode est que vous pouvez le faire sur une base par module, il est donc facile de continuer à passer votre testeur sans avoir beaucoup de temps sans avoir une longue période de temps lorsque le code est cassé.

4
Lie Ryan

Mon approche normale pour trouver des choses inutilisées est

  1. assurez-vous que le système de compilation gère correctement le suivi des dépendances
  2. configurez un deuxième moniteur, avec une fenêtre de terminal plein écran, exécutant des versions répétées et affichant le premier écran de sortie. watch "make 2>&1" a tendance à faire le tour sous Unix.
  3. exécuter une opération de recherche et remplacement sur l'ensemble de l'arborescence source, en ajoutant "//?" au début de chaque ligne
  4. corrige la première erreur signalée par le compilateur en supprimant le "//?" dans les lignes correspondantes.
  5. Répétez jusqu'à ce qu'il n'y ait plus d'erreur.

C'est un processus un peu long, mais il donne de bons résultats.

4
Simon Richter

Je n'ai vraiment utilisé aucun outil qui fasse une telle chose ... Mais pour autant que j'ai pu le constater dans toutes les réponses, personne n'a jamais dit que ce problème n'était pas calculable.

Qu'est-ce que je veux dire par là? Que ce problème ne puisse être résolu par aucun algorithme jamais installé sur un ordinateur. Ce théorème (qu'un tel algorithme n'existe pas) est un corollaire du problème de Halting de Turing.

Tous les outils que vous utiliserez ne sont pas des algorithmes mais des méthodes heuristiques (c.-à-d. Pas des algorithmes exacts). Ils ne vous donneront pas exactement tout le code qui n'est pas utilisé.

3
geekazoid

Si vous êtes sous Linux, vous voudrez peut-être examiner callgrind, un outil d'analyse de programme C/C++ faisant partie de la suite valgrind, qui contient également des outils de contrôle des fuites de mémoire et autres. erreurs de mémoire (que vous devriez également utiliser). Il analyse une instance en cours de votre programme et produit des données sur son graphe d'appels et sur les coûts de performance des noeuds du graphe d'appels. Il est généralement utilisé pour l'analyse des performances, mais il génère également un graphe d'appels pour vos applications, afin que vous puissiez voir quelles fonctions sont appelées, ainsi que leurs appelants.

Ceci est évidemment complémentaire aux méthodes statiques mentionnées ailleurs dans la page, et ne sera utile que pour éliminer les classes, méthodes et fonctions totalement inutilisées - cela n'aidera pas à retrouver le code mort à l'intérieur des méthodes qui sont réellement appelées.

3
Adam Higuera

Une solution consiste à utiliser un débogueur et la fonctionnalité du compilateur consistant à éliminer le code machine non utilisé lors de la compilation.

Une fois que le code machine a été éliminé, le débogueur ne vous laissera pas mettre de pause sur la ligne correspondante du code source. Donc, vous mettez des points d'arrêt partout et démarrez le programme et inspectez les points d'arrêt - ceux qui sont dans l'état "aucun code chargé pour cette source" correspondent au code éliminé - soit ce code n'est jamais appelé, soit il a été mis en ligne et vous devez effectuer un minimum analyse pour trouver lequel de ces deux est arrivé.

Au moins, c'est comme ça que ça marche dans Visual Studio et je suppose que d'autres outils peuvent également le faire.

C'est beaucoup de travail, mais je suppose plus rapide que d'analyser manuellement tout le code.

2
sharptooth

Cela dépend de la plate-forme que vous utilisez pour créer votre application.

Par exemple, si vous utilisez Visual Studio, vous pouvez utiliser un outil tel que .NET ANTS Profiler , capable d'analyser et de profiler votre code. De cette façon, vous devriez rapidement savoir quelle partie de votre code est réellement utilisée. Eclipse a également des plugins équivalents.

Sinon, si vous avez besoin de savoir quelle fonction de votre application est réellement utilisée par votre utilisateur final et si vous pouvez libérer votre application facilement, vous pouvez utiliser un fichier journal pour un audit.

Pour chaque fonction principale, vous pouvez suivre son utilisation et, au bout de quelques jours/semaines, récupérez simplement ce fichier journal et consultez-le.

1
AUS

CppDepend est un outil commercial capable de détecter les types, méthodes et champs inutilisés, et bien plus encore. Il est disponible pour Windows et Linux (mais n’a actuellement aucune prise en charge 64 bits) et est fourni avec un essai de 2 semaines.

Clause de non-responsabilité: je n'y travaille pas, mais je possède une licence pour cet outil (ainsi que NDepend , qui est une alternative plus puissante pour le code .NET).

Pour ceux qui sont curieux, voici un exemple de règle intégrée (personnalisable) pour la détection de méthodes mortes, écrite en CQLinq :

// <Name>Potentially dead Methods</Name>
warnif count > 0
// Filter procedure for methods that should'nt be considered as dead
let canMethodBeConsideredAsDeadProc = new Func<IMethod, bool>(
    m => !m.IsPublic &&       // Public methods might be used by client applications of your Projects.
         !m.IsEntryPoint &&            // Main() method is not used by-design.
         !m.IsClassConstructor &&      
         !m.IsVirtual &&               // Only check for non virtual method that are not seen as used in IL.
         !(m.IsConstructor &&          // Don't take account of protected ctor that might be call by a derived ctors.
           m.IsProtected) &&
         !m.IsGeneratedByCompiler
)

// Get methods unused
let methodsUnused = 
   from m in JustMyCode.Methods where 
   m.NbMethodsCallingMe == 0 && 
   canMethodBeConsideredAsDeadProc(m)
   select m

// Dead methods = methods used only by unused methods (recursive)
let deadMethodsMetric = methodsUnused.FillIterative(
   methods => // Unique loop, just to let a chance to build the hashset.
              from o in new[] { new object() }
              // Use a hashet to make Intersect calls much faster!
              let hashset = methods.ToHashSet()
              from m in codeBase.Application.Methods.UsedByAny(methods).Except(methods)
              where canMethodBeConsideredAsDeadProc(m) &&
                    // Select methods called only by methods already considered as dead
                    hashset.Intersect(m.MethodsCallingMe).Count() == m.NbMethodsCallingMe
              select m)

from m in JustMyCode.Methods.Intersect(deadMethodsMetric.DefinitionDomain)
select new { m, m.MethodsCallingMe, depth = deadMethodsMetric[m] }
1
Roman Boiko

J’ai demandé à un ami de me poser cette question aujourd’hui, et j’ai jeté un coup d’œil aux développements prometteurs de Clang, par exemple ASTMatcher s et le Static Analyzer qui pourrait avoir une visibilité suffisante lors de la compilation lors de la compilation pour déterminer les sections de code mort, mais j'ai trouvé ceci:

https://blog.flameeyes.eu/2008/01/today-how-to-identify-unused-exported-functions-and-variables

C'est à peu près une description complète de la façon d'utiliser quelques drapeaux GCC apparemment conçus dans le but d'identifier des symboles non référencés!

0
Steven Lu

Je ne pense pas que cela puisse être fait automatiquement.

Même avec les outils de couverture de code, vous devez fournir suffisamment de données d'entrée à exécuter.

Peut être un outil d'analyse statique très complexe et coûteux, tel que de Coverity ou compilateur LLVM pourrait être une aide.

Mais je ne suis pas sûr et je préférerais un examen manuel du code.

[~ # ~] mis à jour [~ # ~]

Bien .. ne supprimant que les variables inutilisées, les fonctions inutilisées n’est pas difficile cependant.

[~ # ~] mis à jour [~ # ~]

Après avoir lu d'autres réponses et commentaires, je suis plus convaincu que cela ne peut pas être fait.

Vous devez connaître le code pour avoir une mesure significative de la couverture de code et si vous savez que beaucoup de modifications manuelles seront plus rapides que de préparer/exécuter/réviser les résultats de couverture.

0
9dan

Le problème général de savoir si une fonction sera appelée est NP-Complete. De manière générale, vous ne pouvez pas savoir à l'avance si une fonction sera appelée car vous ne saurez pas si une machine de Turing s'arrêtera un jour. Vous pouvez obtenir si un chemin (statique) va de main () à la fonction que vous avez écrite, mais cela ne garantit pas qu'il sera appelé.

0
Luis Colorado