Qualité de
code Java








Eric Bui
Régis Clavel
Matthieu Damerose
Sébastien Grivolat
Hakim Maadadi
Christophe Safra
Arnaud Schulz
Jérôme Valentin
ESSI3 Juin 1999


Retour au CV.

Manuel utilisateur.

Diapositives.

Télécharger les diapositives
(zip d'un fichier PowerPoint).


Sebastien.Grivolat@free.fr

Sommaire

Remarque : Vous revenez au sommaire par les titres !

Retour au CV.

QUELQUES TERMES TECHNIQUES

Introduction

1. Généralités
1.1. Analyse rapide du sujet
1.2. Solutions proposées
1.3. Solution adoptée
2. Architecture
2.1. Classes de base
2.2. La classe ROOT
2.3. La classe TRAITEMENT
2.4. Nos classes de nœuds : une classe pour chaque nœud récupéré
2.5. Modification à apporter pour ajouter un nouveau traitement
3. Fonctionnalités
3.1. Commentaires
3.2. Corps des méthodes vide
3.3. Variables non utilisées
3.4. Méthodes identiques
3.5. Méthodes inutiles
3.6. Arguments inutiles
3.7. Utilisation inutile du "cast"
3.8. Imports utiles et package inutiles
3.9. Classes sans constructeur
3.10. Présence de super
3.11. Présence des accesseurs
3.12. Vérification du nommage
3.13. Variables de même nom - masquage
3.14. Classe utilisable
3.15. Fermeture transitive
3.16. Variables en dur
3.17. Profondeur d’une classe
3.18. Usage de la réflexivité
3.19. Couplage
3.20. Nombre de membres par classe
4. Autres phases du développement
4.1. I.H.M.
4.2. Tests globaux
4.3. Modifications faites au compilateur
5. Répartition du travail

6. Problèmes rencontrés

Conclusion

Retour au CV.




QUELQUES TERMES TECHNIQUES


Environnement : Structure gérée par le compilateur contenant l'environnement de la compilation : l'ensemble des classes, les environnements fils.

AST : Abstract Syntax Tree, Arbre de Syntaxe Abstraite.

Nœud : Elément de l'AST.

ROOT : Nous rajoutons cette classe afin d'enrichir l'AST.

Parser : Programme qui découpe le code source en mot pour l’analyser.

Compilation : Analyse du code écrit par le programmeur pour produire du langage machine compréhensible et exécutable par l'ordinateur.

Mémoire vive : Mémoire rapide et relativement peu étendue dans laquelle l'ordinateur exécute les programmes.

Machine virtuelle : Simulation logicielle d'un ordinateur.

Garbage collector : Partie de la machine virtuelle chargée de récupérer la mémoire encore occupée par des objets qui ne sont plus utilisés.

Fichiers exécutables et "class", code natif et byte code :
  • exécutable : fichier d'un programme qui s'exécute sur l'ordinateur, à opposer aux fichiers texte qui contient des informations brutes.
  • class : fichier produit par compilateur et exécutable par la machine virtuelle.
  • code natif : fichier exécutable écrit pour un ordinateur spécifique, il sera différent entre un Mac et un PC.
  • byte code : fichier exécutable écrit pour la machine virtuelle il sera le même pour tout type d'ordinateur.





  • Introduction

    Le but de ce projet est la réalisation d’un outil de vérification de qualité de code pour le langage Java. De tels outils sont de plus en plus nécessaires afin de contrôler la qualité du code et ainsi, fournir un code propre et bien commenté permettant une maintenance et une réutilisabilité accrue du programme.

    Il intègre deux types de fonctionnalités: des fonctions de métriques sur le code et des fonctions d’analyse.

    C’est donc dans cet objectif que s’intègre notre outil d’analyse de code Java. Pour cela, il nous faut définir des critères à respecter afin que le code soit correct. Pour les critères, nous nous sommes inspirés des conseils de nos professeurs lors des cours ainsi que de EnvyQA, un outil existant pour le langage Smalltalk. Il offre à l’utilisateur, la possibilité de définir des règles qui doivent être vérifiées dans le code.

    Nous avons ainsi déterminé une vingtaine de points à vérifier concernant:

  • Les commentaires.
  • Les éléments inutiles.
  • La présence d’éléments nécessaires.
  • Les problèmes de nommage.
  • Nous avons aussi choisi de donner des informations plus générales:

  • A des fins de distribution du code.
  • A des fins de statistiques.
  • Un tel outil permet une analyse du code source fourni par l’utilisateur. Afin d’éviter tous les problèmes liés aux éventuelles erreurs de programmation, un des prérequis de notre programme est de lui fournir des programmes syntaxiquement et sémantiquement corrects, c'est à dire compilables.

    Nous allons donc dans un premier temps voir les différentes solutions possibles pour implémenter un tel logiciel (§1), détailler les structures à rajouter qui seront nécessaires (§2), les différents traitements qui seront effectués sur le code fourni (§3), et les autres aspects du développement (§4). Enfin, nous aborderons la répartition du travail dans le groupe (§5) pour conclure par les problèmes que nous avons rencontrés (§6).




    1. Généralités

    Nous allons dans un premier temps expliquer le cheminement qui nous a conduit à la solution technique utilisée dans notre logiciel.

    1.1. Analyse rapide du sujet

    Notre projet est basé sur l'analyse de code, il nous est donc nécessaire d'utiliser les techniques de compilation étudiées en cours.

    Mais quelle est exactement la phase de compilation que nous utiliserons ?

    La réponse est fonction des traitements, cependant l'analyse nécessite un parser dans tous les cas. Pour les traitements les plus complexes, masquage des variables par exemple, il nous faudra également un type checking.

    Ensuite nous avons dû décider quelles fonctions il nous serait possible de coder en fonction du temps accordé. Donc nous avons évalué la complexité et le temps pour l'implémentation de chaque fonctionnalité ( les problèmes de répartition seront évoqués au §5 ).

    Finalement nous avons décidé de la présentation des résultats, des entrées sorties de notre outil.

    1.2. Solutions proposées

  • Création d'un parser et d'un type checker.
  • Récupération et modification du code d'un compilateur Java :
  • L'avantage majeur est la présence du type checker, ainsi que la création de l'AST.
  • Mais le code source doit être disponible avec l'autorisation de la modifier.
  • De même la structure de l'AST ne sera probablement pas adaptée à nos traitements.
  • 1.3. Solution adoptée

    Afin d'analyser le code, deux solutions se présentaient à nous :
  • Création d'un parser Java à l'aide d'un programme dédié tel JavaCC ou Yacc pour Java mais, cela n'aurait pas suffit pour tous les traitements (nécessité d'avoir un type checking).
  • Récupération des sources d'un compilateur déjà existant.
  • Nous avons choisi la deuxième solution du fait d'impératif de temps et aussi de complexité de programmation, en effet ce projet ne dure que trois semaines et il nous en aurait fallut au moins deux pour faire un petit analyseur, qui plus est non optimisé et possiblement buggé. Nous avons donc décidé de réutiliser les sources d'un compilateur Java.

    En effet un compilateur réalise exactement le travail que nous voulons. Il lit le programme, le découpe en unité lexicale ayant un sens pour le langage de programmation concerné. Il construit une représentation intermédiaire, sous forme d'arbre abstrait, du programme avec laquelle il vérifie la sémantique du programme.
    Puis il parcourt cet arbre abstrait pour transformer le texte saisi par le programmeur en code compréhensible par la machine.

    Nous avons donc récupéré la partie avant du compilateur qui réalise la lecture du programme, la création de l'arbre et aussi l'analyse sémantique avec le type checking. Nous utiliserons ensuite l'arbre produit pour faire notre analyse du code.

    Nous avons ensuite dû déterminer de quelle manière nous allions utiliser l'arbre afin de modifier le moins possible le compilateur pour assurer la portabilité de notre outil.

    Ainsi pour palier aux inconvénients de cette solution, nous avons aussi plusieurs possibilités :
  • Récupérer toutes les structures construites par le compilateur. Puis à partir de ces données construire une structure adaptée à nos traitements.
  • Modifier le code du compilateur pour implémenter directement nos traitements.
  • Ou bien, trapper les objets construits par le compilateur.
  • De façon événementielle suivant le modèle observeur observable sur des classes : ceci est impossible en Java.
  • Modifier les constructeurs, solution plus raisonnable mais tous les constructeurs sont à changer.
  • Ajouter un super classe et utiliser l'introspection.



  • 2. Architecture

    Voyons tout d'abord quelle est l'architecture que nous avons mise en place :

    Afin de stocker les données nécessaires aux traitements du code, nous avons envisagé deux solutions :
  • La première reposant sur des parcours de l'AST nécessitant donc une modification de celui ci.
  • La seconde consistant en l'implémentation de structures orthogonales remplies à la construction de l'arbre syntaxique.
  • Après étude des deux solutions, nous avons finalement retenu la seconde, expliquée ci après.
    Celle-ci nécessite moins de modifications et apporte plus de portabilité. De plus elle n'implique pas obligatoirement de parcours de l'AST d'où un gain en complexité.
    L'idée majeure consiste à faire dériver la classe Node d'une classe ROOT, et d'utiliser des surclasses aux classes du compilateur ceci afin de rendre les modifications faites aux classes existantes minimales.
    De plus cette méthode permet d'améliorer la portabilité vers un autre compilateur.

    classes
    Architecture de notre programme

    L'architecture se présente ainsi :
  • Ajout d'un Super Nœud, ROOT, au-dessus de l'AST : cette classe contiendra un vecteur de traitements.
  • Création de certaines classes, utiles pour nos traitements, dont le nom correspondra au nom d'un nœud de l'AST concaténé avec JavaQA. Ces classes disposeront de méthodes statiques isInterestedBy<Ti>(), qui répondront vrai si le nœud est intéressé par le traitement Ti, et ce pour les n traitements.
  • Toutes ces classes dériveront d'une classe Node_JavaQA, qui répondra faux à tous les traitements. Leurs descendants n'auront donc à surcharger que les méthodes devant répondre vrai. Ceci facilitera l'ajout de nouveaux traitements. De plus, chacune de ces classes disposera d'une référence sur le nœud original de l'AST.
  • Création d'une classe abstraite Traitement, contenant un vecteur d'instance de classe Node_JavaQA.
  • Chaque traitement effectué sur le code par notre logiciel, sera définit dans une classe dérivant de la classe Traitement. Il possèdera un tableau d'objet de type Node_JavaQA.

  • Détails du fonctionnement :
    L'élément notable est la super classe commune aux classes Node et ClassDefinition nommée Root. Elle a pour but la détection de la création de chaque nouvelle instance de ses sous classes, afin de créer un proxy pour ces dernières. Le proxy est ici modélisé par l'association entre les classes JavaQA_Node et Node.

    La création d'une instance d'une classe proxy s'effectue "automatiquement" par le chaînage des constructeurs. Dans la classe Root, le code constructeur, grâce à l'introspection, crée une instance de la classe proxy correspondante à la classe de "this". En effet dans le constructeur de la classe Root, grâce à l'introspection, une instance de la classe proxy correspondante est créée. Cela est modélisé sur le diagramme de séquence ci-dessous :

    diagramme
    Diagramme de séquence lors de l'appel d'un constructeur


    Nous allons maintenant nous intéresser aux différentes classes nécessaires à l'implémentation d'une telle architecture.

    2.1. Classes de base

    Durée : - 2 jours -

    Description : Mise en place des classes nécessaires à l'enrichissement de l'AST (classes proxy).

    Détails du traitement : Création des classes ROOT, Traitement et JavaQA.

    Structure nécessaire : Voir ci dessous.

    2.2. La classe ROOT

    private static Vector // contient la liste des traitements
    public static addTraitement ()
    public static ajouterNoeudAuxTraitement ()
    public static lanceTraitement ()
    public static recupResult ()

    2.3. La classe TRAITEMENT

    vecteur nœuds // la liste des nœuds auxquels appliquer le traitement
    addNoeud ()
    startTraitement ()
    recupResult ()

    2.4. Nos classes de nœuds : une classe pour chaque nœud récupéré

    NodeJavaQA
    IsInterestedByT1() return no;
    IsInterestedByT2() return no;
    ...
    IsInterestedByTn() return no;

    XXX étant le nom d'un nœud existant, comme WhileStatement :
    XXXJavaQA
    // classe qui hérite de NodeJavaQA
    IsInterestedByTi() return true; // Si XXX est concernée par le traitement i.

    Si le nœud courant n'est pas surchargé, la classe Traitement récupère une exception du type ClassNotFound.

    2.5. Modification à apporter pour ajouter un nouveau traitement

    Voici les différentes étapes à suivre pour ajouter un nouveau traitement à notre logiciel :
  • Faire hériter le nouveau traitement de la classe Traitement et le placer dans le package javaqa.traitement.
  • Ajouter dans le package javaqa.noeudjavaqa les classes XXXJavaQA nécessaires, celles-ci héritant de JavaQA.
  • Ajouter dans la classe JavaQA la méthode isInterestedByYYYTraitement() retournant false.
  • Ajout du traitement dans l'interface.
  • Faire prendre en charge le nouveau traitement dans la classe javaqa.util.InterfaceLiaisonTraitement.

  • 3. Fonctionnalités

    Dans cette partie, nous allons présenter, pour chacune des fonctionnalités le temps nécessaire pour les mettre en place, une brève description de leur utilité, les détails concernant l'implémentation, les structures essentielles et enfin le type de traitement : analyse de code ou métrique.

    Remarque : nous ne pouvons effectuer aucun traitement à la construction car le nœud n'est pas entièrement élaboré puisque la création du proxy se fait à l'appel du super constructeur, et alors l'instance pour laquelle le proxy est crée n'est pas complètement initialisée.

    3.1. Commentaires

    Durée : - 3 jours -

    Description : Ce traitement recherche la présence de commentaires au format JavaDoc. Il retourne la liste de toutes les méthodes et classes qui ne sont pas commentées.

    Remarque : Nous limitons ce traitement aux commentaires du format JavaDoc, puisque le parser du compilateur choisit pour notre développement ne conserve que ce type de commentaires. Les autres commentaires sont ignorés dans les fichiers sources. Notre première priorité étant de toucher un minimum au code du compilateur, nous avons décidé de ne pas rajouter la récupération des autres formats de commentaires, cela rend impossible tout traitement dessus.

    Détails du traitement :
  • Parcours de la liste des méthodes à partir de l'environnement.
  • Vérification de la présence de commentaires Javadoc dans les classes et les méthodes (méthode getDocumentation() ).


  • Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux classes et méthodes.

    Type de traitement : Analyse de code.

    3.2. Corps des méthodes vide

    Durée : - 2 jours -

    Description : Ce traitement permet de vérifier si le code source dispose de méthodes dont le corps est vide.

    Détails du traitement :
  • Parcours de l'environnement.
  • Sur toutes les classes : récupérer la liste des méthodes (classe MethodeExpression).
  • Sur toutes les méthodes vérifier les champs Field, implémentation.


  • Structure nécessaire : Aucune, les méthodes sont accessibles via l'environnement.

    Type de traitement : Analyse de code.

    3.3. Variables non utilisées

    Durée : - 3 jours -

    Description : Recherche, dans toutes les méthodes, des variables déclarées mais non utilisées par la suite.

    Détails du traitement :
  • Pour chaque déclaration de variable, nous l'ajoutons dans notre liste.
  • Appel de isUsed () dans tree.LocalMember.
  • Renvoi des variables non marquées.


  • Structure nécessaire : Pour accéder aux variables, nous utilisons le nœud VarDeclarationStatementJavaQA afin de créer une liste de déclarations de variables.

    Type de traitement : Analyse de code.

    3.4. Méthodes identiques

    Durée : - 6 jours -

    Description : Recherche des méthodes qui ont des noms courts égaux et des noms longs différents dans la même hiérarchie et lorsque cette recherche est fructueuse on vérifie si les corps des méthodes sont identiques.

    Détails du traitement :
  • Vérifier la signature.
  • Utilisation d'une méthode Egal() implémentée dans les classes Root et MemberDefinition afin de pouvoir comparer les corps des méthodes.


  • Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux classes.

    Type de traitement : Analyse de code.

    3.5. Méthodes inutiles

    Durée : - 1,5 jours -

    Description : Recherche des méthodes jamais appelées.

    Détails du traitement :
  • Liste des déclarations des méthodes à partir de l'environnement.
  • Liste des appels des méthodes par le nœud MethodeExpression.
  • Intersection des deux ensembles.


  • Structure nécessaire : Nous avons besoin Il d'une nouvelle classe MethodExpressionJavaQA pour avoir la liste de MethodExpression.

    Type de traitement : Analyse de code.

    3.6. Arguments inutiles

    Durée : - 2 jours -

    Description : Recherche pour toutes les méthodes des arguments définis mais non utilisés dans le corps de la méthode.

    Détails du traitement :
  • On dispose de la liste des arguments d'une méthode à partir de l'environnement (méthode getArguments()).
  • Si elles ont été lues ou plus d'une fois affectées => utilisée(méthode isUsed()).


  • Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux méthodes et à leurs arguments.

    Type de traitement : Analyse de code.

    3.7. Utilisation inutile du "cast"

    Durée : - 3 jours -

    Description : Recherche de tous les casts dans toutes les méthodes et vérification de leur utilité : un cast inutile se caractérise par une tentative de cast sur une expression qui est déjà du bon type.

    Détails du traitement :
  • On gère une liste de tous les objets de type CastExpression.
  • On parcourt ensuite cette liste en vérifiant les types sur l'objet CastExpression.


  • Structure nécessaire : Il faut créer une nouvelle classe CastExpressionJavaQA pour avoir la liste de CastExpression.

    Type de traitement : Analyse de code.

    3.8. Imports utiles et package inutiles

    Durée : - 1.5 jours -

    Description : Ce traitement donne la liste des imports nécessaires pour que le programme soit correct et renvoie la liste des packages non nécessaires à son fonctionnement.

    Détails du traitement :
  • récupération de tous les appels de méthode afin de vérifier s'ils nécessitent un import
  • récupération de toutes les créations de variables
  • comparaison avec la liste des packages inclus pour chaque classe


  • Structure nécessaire : Il faut récupérer tous les appels de méthode et toutes les créations de variables, pour cela on utilise la classe Root ainsi que des nœuds javaqa.

    Type de traitement : Analyse de code.

    3.9. Classes sans constructeur

    Durée : - 2 jours -

    Description : Ce traitement vérifie la présence de constructeur dans chaque classe, et retourne la liste des classes ne vérifiant pas cette contrainte.

    Détails du traitement :
  • Parcours de l'environnement pour atteindre toutes les classes.
  • Sur chaque classe regarder la liste des méthodes.
  • Pour chaque méthode on vérifie si c'est un constructeur (isConstructor()).
  • Renvoi des classes sans constructeurs.


  • Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux classes.

    Type de traitement : Analyse de code.

    3.10. Présence de super

    Durée : - 3/4 jours -

    Description : Ce traitement vérifie la présence de l'appel au constructeur de la classe parente via l'utilisation de "super". Ce traitement retourne la liste de toutes les classes et de tous les constructeurs ne disposant pas de l'appel à la méthode super, considérées ainsi comme mal construites.

    Remarque : Le compilateur rajoute automatiquement l'appel super() à tout constructeur dont la première instruction n'est pas une initialisation.

    Détails du traitement : Le traitement consiste donc à détecter dans l'environnement les constructeurs dont l'initiateur a été rajouté par le compilateur. L'identité des numéros de lignes et d'offsets du constructeur et de sa première instruction (l'initiateur) obtenue par la méthode MemberDefinition.firstConstructor(), permet ce traitement.

    Structure nécessaire : Javaqa.noeudjavaqa.ConstructorTheo
    public class ConstructorTheo {
    protected MemberDefinition co; //le constructeur lié
    public ConstructorTheo(MemberDefinition md) ;
    public MemberDefinition getMemberDefinition () ; // retourne le constructeur lié
    // Dans le cas d'un constructeur on peut descendre a son "fils" qui est super(...) ou this(...)
    public Expression firstConstructor () ;
    public ClassDefinition getClassDefinition () ; // retourne la classe du constructeur lié
    public ClassDeclaration getClassDeclaration () ;
    public String getClassName() ; // retourne le nom de la classe du constructeur lié
    public String getName () ; // retourne le nom (long) du constructeur lié
    public String getSourceName () ; // retourne le nom du fichier contenant le constructeur lié
    // Verifier si "super()" a été rajoute a un constructeur par le compilateur
    // Procede: comparaison des numéros de ligne et d'offset du super() par rapport au constructeur
    public boolean haveAddedSuper () ;
    public String toString() ;
    }

    3.11. Présence des accesseurs

    Durée : - 1,5 jours -

    Description : Ce traitement permet de vérifier la présence d'accesseurs pour toutes les données membres d'une classe, sous la forme getXXX ou setXXX.

    Détails du traitement : A l'aide de l'environnement, on parcourt la liste des données membres et on vérifie dans la liste des méthodes la présence des accesseurs.

    Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux données membres et aux différentes méthodes.

    Type de traitement : Analyse de code.

    3.12. Vérification du nommage

    Durée : - 2/3 jours -

    Description :
    L'utilisateur a la possibilité de définir des formats de nommage pour les noms de ses :
  • Constantes, variables de classe et d'instance.
  • Méthodes, classes et interfaces
  • Ce traitement effectue la vérification sur le code source du respect de ces formats de nommages spécifiés par l'utilisateur.

    Détails du traitement :
  • Vérification des nommages sur les classes, interfaces, méthodes et variables contenues dans l'environnement.
  • Vérification des nommages sur les déclarations de variables trappées.


  • Remarque : Les différentes règles de nommages à vérifier sont indiquées par les paramètres entrés dans javaqa.util.Parametres :
  • private static String nommeClasse;
  • private static String nommeMethode;
  • private static String nommeInterface;
  • private static String nommeVarClasse;
  • private static String nommeVarInst;
  • private static String nommeConst;

    Structure nécessaire : Utilisation de la classe javaqa.traitement.RegexpTheo pour analyser les expressions régulières de type *.
    javaqa.traitement.RegexpTheo est en fait la copie de la classe sun.misc.Regexp dont la méthode "boolean matches(String)" est devenue publique.
    La différenciation des nommages est faite par les classes javaqa.traitement.NommageXXX héritant toutes de javaqa.traitement.Nommage.

    Exemple : public class NommageClass extends Nommage {
    /** Message d'erreur de nommage non vérifié */
    public static final String NOMME = "Nommage de classes non respecte !";
    private NommageClass () ;
    public NommageClass (String ) ;
    public NommageClass (String , boolean) ;
    public NommageClass (String, boolean, boolean ) ;
    public boolean aVerifier (ClassDeclaration) ;
    public boolean aVerifier (ClassDefinition) ;

    /**
    * Verification du nommage
    */
    public boolean verifier (ClassDefinition ) ;
    public boolean verifier (ClassDeclaration ) ;
    }

    public class Nommage {
    protected RegexpTheo regle;
    protected boolean upper_case;
    protected boolean lower_case;
    protected Nommage () ;
    public Nommage (String ) ;
    public Nommage (String, boolean ) ;
    public Nommage (String, boolean, boolean ) ;
    public String getExp ()
    /**
    * test si l'objet o doit etre verifie
    * fonction par defaut
    */
    public boolean aVerifier (Object) ;
    /** matching */
    protected boolean verifierString (String ) ;
    }
    Cette implémentation permet facilement le rajout de règle de nommage. Cependant certaines limitations du langage Java ne permettent pas une utilisation étendue de ces classes. En effet, les Vector et Hastable Java impliquant des casts d'objets stockés remettent en question la justesse de la fonction public boolean aVerifier(Object) d'où le coté peu dynamique de javaqa.traitement.VerifNommageTraitement.

    3.13. Variables de même nom - masquage

    Durée : - 3 jours -

    Description : Ce traitement recherche dans tout le code la présence de variables se masquant les unes les autres. Il retourne la liste de toutes les variables concernées par ce masquage.
    Par exemple la classe B hérite de la classe A, et toutes les deux déclarent une variable x.

    Détails du traitement :
  • Parcours de l'environnement pour connaître les données membres des méthodes, récupération des créations de variables à l'aide de la classe Root.
  • Si une variable a le même nom qu'une donnée membre de la classe ou d'une classe héritée, on le signale.
  • Si un argument d'une méthode a le même nom qu'une donnée membre, on le signale aussi.


  • Structure nécessaire : Nous utilisons l'environnement pour accéder aux données membres et nous utilisons un nœud VarDeclarationStatementJavaQA pour récupérer les déclarations de variables.

    Type de traitement : Analyse de code.

    3.14. Classe utilisable

    Durée : - 1 jours -

    Description : Ce traitement permet à l'utilisateur de disposer de la liste des classes utilisables, c'est à dire contenant une méthode main. De même il prévient de l'absence d'une méthode Main dans un ensemble de classes.

    Détails du traitement :
  • On atteint la liste des classes en parcourant l'environnement.
  • Vérification du nom de chaque méthode.
  • Renvoi des classes contenant une méthode main.


  • Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux classes.

    Type de traitement : Analyse de code.

    3.15. Fermeture transitive

    Durée : - 2 jours -

    Description : Ce traitement permet d'obtenir la liste de toutes les classes qui sont utilisées, à partir d'une classe choisie par l'utilisateur.

    Détails du traitement :
  • Récupération du graphe d'héritage à partir de l'environnement (dépendances).
  • Marquage de la classe choisie par l'utilisateur.
  • Création d'une liste des classes et marquage des classes apparaissant dans le graphe d'héritage.
  • Affichage des classes marquées.


  • Structure nécessaire : Aucune dans le nœud traitement, l'environnement nous permet d'accéder aux classes, mais par contre utilisation d'une structure, pour marquer les méthodes.

    Type de traitement : Analyse de code.

    3.16. Variables en dur

    Durée : - 3 jours -

    Description : Recherche des constantes utilisées par le programmeur au milieu du code. Par exemple if (x == 10).

    Détails du traitement :
  • Opération effectuée uniquement sur les comparaisons.
  • Récupérer le nœud BinaryExpression : ajout dans la liste.
  • Demande isConstant () sur les expressions gauche et droite de chaque nœud.


  • Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux variables.

    Type de traitement : Analyse de code.

    3.17. Profondeur d'une classe

    Durée : - 1 jours -

    Description : Ce traitement retourne la profondeur de toutes les classes du programme choisi par l'utilisateur.

    Détails du traitement : Parcours d'environnement : on parcourt les pères jusqu'à obtenir ascendant = Object à l'aide de la méthode getSuper().

    Structure nécessaire : Aucune, nous utilisons l'environnement pour accéder aux classes.

    Type de traitement : Métrique.

    3.18. Usage de la réflexivité

    Durée : - 3 jours -

    Description : Ce traitement permet de vérifier, si le code source utilise ou non des méthodes provenant du package : java.lang.reflect.

    Détails du traitement :
  • Liste des appels des méthodes (MethodeExpression).
  • vérifier si la méthode fait partie du package java.lang.reflect.


  • Structure nécessaire : Nous utilisons une classe MethodExpressionJavaQA afin de créer une liste des méthodes appelées.

    Type de traitement : Analyse de code.

    3.19. Couplage

    Durée : - 1 jour

    Description : Ce traitement donne la liste des classes couplées. Deux classes sont couplées si une utilise des variables ou des méthodes de l'autre.

    Détails du traitement :
  • Récupération de tous les appels de méthodes et des créations de variables (new).
  • Vérification sur le nom long de l'appartenance de la variable ou de la méthode.


  • Structure nécessaire : Il faut créer la liste des méthodes appelées, la classe Root et des nœuds Javaqa permettant cette création. Donc aucune structure spécifique n'est nécessaire.

    Type de traitement : Analyse de code.

    3.20. Nombre de membres par classe

    Durée : - 1 jour

    Description : Ce traitement permet d'obtenir des statistiques sur les différentes classes d'un projet : le nombre de méthodes et de données membres

    Détails du traitement : Parcours de l'environnement afin de compter pour chaque classe, le nombre de méthodes et de données membres.

    Structure nécessaire : Aucune, un parcours de l'environnement suffit.

    Type de traitement : Métrique.

    4. Autres phases du développement

    4.1. I.H.M.

    Durée : - 6 jours -

    Description : Interface graphique de notre logiciel, permettant à l'utilisateur de choisir des traitements parmi une liste pré établie. L'interface permettra la visualisation des résultats de chaque traitement.

    Détails du traitement :
  • Maquette.
  • Validation par les encadreurs.
  • Branchement avec le noyau.
  • Tests.


  • L'interface graphique que nous avons réalisée, pour notre outil, est composée de 3 parties :
  • Une boite de dialogue, "parcourir", pour sélectionner le fichier source à analyser.
  • Une liste de cases à cocher, sur la gauche, afin que l'utilisateur puisse sélectionner les différents traitements qu'il veut appliquer au programme.
  • Une zone, sur la droite, gérée par des onglets correspondants à la sélection de traitements, pour présenter les résultats au format HTML ou texte.


  • Lors de la sélection des divers traitements, si l'un d'eux a besoin de renseignement précis, une boite de dialogue apparaît afin que l'utilisateur puisse saisir les informations supplémentaires.

    Nous avons commencé par réaliser une maquette de l'interface qui a été validée par les encadreurs. Puis nous avons fini le développement et effectué le branchement des différentes fonctionnalités.
    Une extension possible de l'interface consisterait à offrir la possibilité d'afficher les résultats par classes.

    4.2. Tests globaux

    Au cours de la création des différentes fonctionnalités, nous avons généré des fichiers afin de tester le traitement effectué. Ce jeu de tests nous a aussi permis de contrôler le programme global une fois l'intégration réalisée.

    Il comprend :
  • Un ou plusieurs petits programmes de test pour chacune des fonctionnalités.
  • Un programme plus conséquent, plusieurs centaines de lignes, n'ayant pas été écrit dans le but de tests, afin de vérifier l'efficacité de notre outil en situation réelle.

    Ce jeu de test est disponible dans le sous-répertoire "Test" de notre logiciel.

    Remarque : Nous avons aussi testé JavaQA sur des programmes Java fournis par d'autres élèves de l'école afin d'en éprouver la fiabilité.

    La réalisation de ces tests nous a aussi permis de mesurer les temps de calculs de chacune des fonctionnalités, et d'une manière générale d'estimer le surcoût au travail du compilateur que nous engendrons.

    4.3. Modifications faites au compilateur

    Les modifications apportées aux sources du compilateur sont les suivantes :
  • Supprimer la phase de génération de code dans javac.Main.
  • Obliger le compilateur à recompiler tous les fichiers liés à celui compilé, même ceux dont les fichiers "class" existent déjà dans java.BatchEnvironment.
  • Ajouter une méthode Egal() dans la classe tree.MemberDefinition .
  • Ajout d'accesseurs sur tree.BinaryExpression .
  • Ajout d'accesseur sur tree.VarDeclarationStatement
  • Ajout d'accesseur sur tree.MethdoExpression.
  • Passage en public d'un membre de tree.IdentifierExpression.
  • Passage en public d'un membre de tree.FieldExpression.
  • Passage en public d'un membre de tree.CompoundExpression.
  • Rajouter la dérivation de Root pour java.Node.
  • Rajouter la dérivation de Root pour java.ClassDefinition.

    5. Répartition du travail

    La durée prévisionnelle du projet est de 60 jours. Nous l'avons calculée à partir des estimations de temps que nous avons établi pour chacune des différentes parties du projet. Afin de faciliter la répartition du travail, nous avons décidé de travailler par binôme. Ainsi, nous pouvons travailler à deux sur les points les plus difficiles sans bouleverser le planning. Nous avons réalisé un diagramme de Gant, voir page suivante, afin de représenter les différentes taches à réaliser ainsi que leur durée et la répartition entre les divers binômes.

    La première semaine, nous avons commencé par travailler tous ensemble sur l'architecture du programme, la recherche d'un compilateur et d'un environnement de développement. En effet, nous n'avons pas pût utiliser Visual Age for Java qui, intégrant le jdk1.1.7 dont nous n'avions pas les sources, ne supportait pas le jdk1.2 nécessaire pour notre programme.
    Nous avons ensuite passé plusieurs heures à étudier les sources du compilateur afin de comprendre son fonctionnement et ce qu'il nous serait possible de faire grâce aux fonctionnalités déjà présentes.

    A partir de cette première analyse du compilateur, nous avons pu réaliser une étude de faisabilité pour chacun des traitements que nous nous sommes répartis par binôme.

    Nous avons ensuite installé un logiciel de gestion de source afin de pouvoir travailler tous ensemble avec les mêmes fichiers. Nous avons utilisé l'outil RCS qui permet de travailler dans un répertoire partagé commun à tous, contenant les dernières versions de l'ensemble fichier.
    Ainsi chacun avait toujours à disposition une version stable des autres fichiers, en faisait une copie sur son poste de travail. Nous posions un verrou sur un fichier à modifier et lorsque la nouvelle version de ce fichier etait prête nous la mettions à disposition dans le répertoire partagé, en levant le verrou.
    Dans un projet à plusieurs développeurs, un tel outil est nécessaire, en général les environnements de programmation intègrent une gestion de version.

    Nous avons enfin programmé les différents traitements. Lors de la dernière semaine, nous avons effectué les derniers tests concernant le logiciel puis, nous avons réalisé un kit d'installation, une documentation pour les futurs utilisateurs de notre outil et un rapport de conduite de projet.


    planning Diagramme de Gant de la répartition des tâches.

    6. Problèmes rencontrés

    Nous avons rencontré divers problèmes lors de notre projet, dû à la reprise de code existant, à diverses spécificités du compilateur Java et aussi dû à certaines erreurs de notre part en ce qui concerne l'ajout de certaines fonctions dans les fichiers de base de notre architecture.

    Le premier problème que nous avons rencontré, a été de comprendre le fonctionnement du compilateur et de trouver ce qui été réutilisable. En effet le programme n'est pas des plus simples et nous ne disposons pas toujours facilement des données qu'il nous faudrait. Ainsi, nous avons été obligés pour certains traitement de rajouter des accesseurs sur certaines variables. Par exemple dans BinaryExpression nous avions besoin d'un accès au données membres, l'expression droite et gauche, mais aucun n'accesseur n'existait.

    Certaines fonctions déjà présentes dans le compilateur et que nous pensions réutiliser se sont révélées non appropriées ou inefficaces pour nos traitements nous obligeant de ce fait à créer nos propres fonctions. Par exemple, HasConstructor() n'est pas utilisable pour nous car renvoie toujours "true" dans notre cas.

    Nous nous sommes aussi aperçus dès le début que, lors d'une remontée dans l'arbre, tous les nœuds n'étaient pas créés. Cela a entraîné une modification de Root afin de pallier ce problème et par la suite, le rajout d'une table de hachage afin d'éviter une remontée systématique.

    Nous avons aussi eu des problèmes liés au fait que le compilateur utilisé pour générer les fichiers "class" exécutables ne recompile pas systématiquement tous les fichiers transformés. Ainsi les modifications ne sont pas prises en compte mais le programme s'exécute en utilisant l'ancienne version des fichiers "class", donc sans déclencher une erreur ce qui cache cette non prise en compte des modifications.

    Enfin lors des tests, nous nous sommes rendus compte d'une certaine lenteur du programme lors de son exécution. Ce ralentissement est en fait dû à la construction de l'arbre par le compilateur rendant même négligeable le temps d'exécution de nos traitements. En effet, cette construction demande une importante capacité de mémoire vive qui se trouve reportée sur le disque dur donc ralentit très fortement les calculs. Nous avons donc décidé de rajouter pour chaque traitement une évaluation de la durée de celui ci.

    Les tests sont effectués sur un programme de test (exercice de TP Java) de 300 lignes qui prend normalement 10s pour se compiler avec le jdk1.2 de Sun. Nous avons fait deux mesures de temps car suivant l'instant ou se déclenche le garbage collector, la durée d'un traitement peut varier du simple au double.


    Voici les mesures que nous avons effectuées avec le programme test de 300 lignes :

    Traitement 1er essai (ms) 2ème essai (ms) Moyenne
    Corps des méthodes vides 610 1512 1061
    Vérification nommage 360 300 330
    Classes utilisables 50 80 65
    Fermeture transitive 110 90 100
    Classes sans constructeurs 20 40 30
    Présence de super 1623 1121 1372
    Variables masquées 390 231 310,5
    Commentaires 40 80 60
    Accesseurs 81 130 105,5
    Variables non utilisées 20 10 15
    Méthodes identiques 80 20 50
    Variables en dur 120 110 115
    Méthodes inutiles 230 381 305,5
    Arguments inutiles 40 50 45
    Couplage des classes 110 180 145
    Profondeur d'une classe 20 20 20
    Usage de la Réflexivité 10 30 20
    Nombre de membres par classe 20 41 30.5
    Imports utiles 350 550 450
    Casts inutiles 60 30 45
    Total 4344 5008 4676
    Soit en % du temps total 7.5% 8.78% 8.14%
           
    Constructeur de ROOT 18005 17950 17977,5
           
    Total pour nous 22349 22958 22653,5
    Soit en % du temps total 38.70% 40.26% 39.48%
           
    Programme complet 57743 57022 57382,5

    Nous pouvons constater à la lecture de ce tableau que nos traitements prennent moins de 10% de la totalité du temps mis par notre programme pour analyser le code. Si nous comptons le temps passé dans le constructeur de Root, notre outil utilise 40% du temps d'exécution.

    De plus le compilateur en version exécutable native prend 10s pour compiler notre programme de test et en version "class" java il lui faut 40s.
    En fait, la différence entre le compilateur non modifié (compilation du programme de test en 10s) et notre outil, en terme de performance, vient du fait que le compilateur est fourni sous forme d'exécutable compilé en code natif alors que notre programme est fourni en byte code et utilise la machine virtuelle java pour s'exécuter. Nous pourrions donc nous attendre à une diminution du temps global d'exécution de notre outil de l'ordre de 75% si nous le compilions en code natif.




    Conclusion


    Ce projet nous a permis d'approfondir notre connaissance du langage Java par l'intermédiaire de l'étude de son compilateur et de l'utilisation de techniques de programmation avancées telles l'introspection et la réflexivité qui nous ont été très utiles pour faire certains traitements et sont à la base de notre architecture.

    De plus nous avons aussi appris à concevoir une architecture objet complexe afin de pouvoir s'intégrer facilement au compilateur tout en ne lui apportant que de très petites modifications.
    Tout ceci souligne l'importance de spécifications détaillées en amont du codage : il faut bien cerner le problème, l'analyser, le modéliser, proposer plusieurs solutions, choisir la plus adaptée et, seulement ensuite, l'implémenter.

    Ce projet nous a aussi permis de travailler en groupe et de voir les problèmes de compréhension que nous pouvons rencontrer, dans un groupe assez important, pour expliquer sa propre vision d'un point de l'architecture. Par contre, nous avons la compensation de passer ainsi en revue plus de méthodes possibles pour résoudre un problème et de mieux analyser chacune des solutions proposées.
    De même il est apparu nécessaire de manager, motiver, rassembler et freiner le groupe afin d'obtenir dans des délais raisonnables une solution aux problèmes.

    Enfin nous avons encore plus pris conscience de l'importance de produire du code commenté et respectant certaines règles pour que la reprise ultérieure du programme soit plus aisée. En effet, le code du compilateur, quoique bien commenté, est parfois assez étrange et difficile à comprendre pour une personne qui doit le reprendre et l'utiliser.

    Retour au CV. Retour au Sommaire.