Accueil > Non classé > Développer ses propres règles PMD pour Sonar

Développer ses propres règles PMD pour Sonar

PMD est un analyseur de code source. Il permet de détecter le code inutile, trop complexe… Il est par exemple intégré à Sonar, qui l’utilise conjointement avec FindBugs (plutôt destiné, comme son nom l’indique, à détecter les sources potentielles de bugs) et de Checkstyle (qui vérifie le respect de règles de codage).

PMD est intégré à Sonar avec un ensemble de règles prédéfinies. Il est cependant possible d’écrire soi-même des règles de détection puis de packager ces nouvelles règles sous la forme d’un plugin pour Sonar. C’est ce que nous allons faire dans cet article.

Abstract Syntax Tree

Voyons tout d’abord comment fonctionne PMD lors de l’analyse du code source.

Avant d’appliquer les règles sur le code source à analyser, PMD réalise un premier traitement : il parse le code source afin de le représenter sous la forme d’une arborescence, appelée Abstract Syntax Tree (AST). Chaque élément du code Java (import, déclaration, boucle…) correspond à un nœud de l’arbre et est identifié comme une instance d’une des classes AST de PMD. C’est sur cet arbre que PMD va effectuer son analyse, et non sur le code source original.

Dans cet article, nous supposerons que nous voulons détecter les déclarations d’instances d’interface java.sql.Connection dans des boucles.

Deux possibilités sont proposées pour écrire des règles :

  • la détection de patterns dans le code via une expression XPath
  • la détection de problèmes via l’implémentation d’une classe Java.

Développement de règles en XPath

Principe

La première solution pour écrire une règle PMD consiste donc à représenter la violation à rechercher sous la forme d’une expression XPath. Cette expression sera ensuite appliquée par PMD à l’AST pour identifier les violations dans le code source.

Développement

Nous allons commencer par écrire un cas de test, c’est-à dire une classe Java qui ne respecte pas la règle que nous allons développer. La classe suivante, par exemple, viole la règle :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.excilys.tuto.pmd.sample;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public final class MySample {

    private MySample() {
    }

    public static void main(String args[]) {
        for (int i = 0; i < 1; i++) {
            Connection c = null;
            try {
                c = DriverManager.getConnection("jdbc:...");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (c != null) {
                    try {
                        c.close();
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

L'objectif est donc de détecter la ligne 14 (Connection c = null;) de ce code source comme étant une violation.

Une fois ce cas identifié, il faut construire l’AST correspondant, afin d’identifier le pattern à détecter. Heureusement, PMD est livré avec un petit utilitaire (Designer) en Swing (bin/designer.bat) permettant de visualiser cet arbre. Copiez-collez le code contenant la violation dans le panel “Source code” puis cliquez sur “Go”, l’AST apparaît dans l’onglet “AbstractSyntaxTree”.

Dans l’arbre produit, nous pouvons identifier la portion de code contenant le pattern à détecter :

sonar1_1

Il s’agit donc de détecter les nœuds LocalVariableDeclaration, possédant un ReferenceType d’interface Connection, et descendant d’un ForStatement.

En XPath, cela nous donne l’expression suivante :
//ForStatement//LocalVariableDeclaration/Type/ReferenceType/ClassOrInterfaceType[@Image='Connection']

Saisissons cette requête dans le champ XPath Query et cliquons sur “Go” afin de l’exécuter. Le résultat s’affiche dans le panel du bas, il indique que la ligne 14 matche notre expression XPath et constitue donc une violation par rapport à notre règle :

ASTClassOrInterfaceType at line 14 column 25

Configuration

Nous avons maintenant à notre disposition une règle XPath permettant de détecter les violations qui nous intéressent. Voyons ensuite comment configurer cette règle pour qu’elle soit prise en compte par PMD lors de son analyse.

Pour cela, un fichier de configuration, rulesets.xml, doit être créé et placé dans les ressources du projet.
Il décrit les règles qui constituent notre extension et permet d’attribuer à chaque règle divers messages d’information et une priorité, qui seront affichés à l’utilisateur lors de l’exécution de PMD. Notre règle sera décrite de la façon suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0"?>
<ruleset name="PMD extensions" xmlns="http://pmd.sf.net/ruleset/1.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
        xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">

    <description>A sample ruleset.</description>

    <rule name="AvoidDeclaringConnectionsInLoopsXPath"
         message="Avoid declaring java.sql.Connection objects in loops."
         class="net.sourceforge.pmd.rules.XPathRule">
        <description>
            Avoid declaring java.sql.Connection objects in loops.
        </description>

        <properties>
            <property name="xpath">
                <value>
                    <![CDATA[
       //ForStatement//LocalVariableDeclaration/Type/ReferenceType/ClassOrInterfaceType[@Image='Connection']
       ]]>
                </value>
            </property>
        </properties>
    </rule>

</ruleset>

La règle est définie comme étant de classe net.sourceforge.pmd.rules.XPathRule, qui est la classe proposée par PMD pour détecter des patterns suivant une expression XPath.
Comme son nom l’indique, la propriété xPath de la classe XPathRule est initialisée avec notre expression XPath.

Test

Pour tester cette règle, nous pouvons invoquer la classe net.sourceforge.pmd.PMD, qui propose une méthode main permettant d’appliquer les règles définies dans un fichier rulesets.xml à un ou plusieurs fichiers à tester.

Dans notre cas, nous pouvons tester la règle en lançant la commande suivante :
java net.sourceforge.pmd.PMD MySample.java xml rulesets.xml
où :

  • MySample.java est le fichier contenant le code source à analyser
  • xml est le format du rapport produit par PMD (il est également possible de générer du HTML…)
  • rulesets.xml est le fichier contenant la définition de nos règles.

Nous obtenons en sortie le rapport suivant :

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<pmd version="4.2.5" timestamp="2013-01-22T21:18:41.488">
    <file name="/home/francois/dev/workspace/workspace_sonar/pmd/src/main/java/com/excilys/tuto/pmd/sample/MySample.java">
        <violation beginline="14" endline="14" begincolumn="25" endcolumn="34" rule="AvoidDeclaringConnectionsInLoopsXPath" ruleset="PMD extensions" package="com.excilys.tuto.pmd.sample" method="main"
                  priority="5">
            Avoid declaring java.sql.Connection objects in loops.
        </violation>
    </file>
</pmd>

qui permet de vérifier que l’anomalie a bien été détectée par notre nouvelle règle.

L’avantage de l’utilisation de XPath est que la règle est rapide à écrire et à configurer. Cependant, ce mode possède des limites : il permet de détecter uniquement des patterns se trouvant dans une même classe. Il n’est par exemple pas possible avec cette solution de construire des règles parcourant plusieurs fichiers sources du projet (par exemple, compter le nombre total d’appels à une méthode dans un projet).

Nous verrons en fin d’article comment déployer cette règle dans Sonar, mais voyons auparavant comment nous aurions pu l’écrire en Java.

Développement de règles en Java

Principe

Il est également possible d’écrire des règles PMD en Java, en utilisant l’API proposée par PMD pour manipuler l’AST.

L’API PMD utilise le design pattern visitor : une règle Java écrite en utilisant cette API contient des méthodes qui sont exécutées lors de la rencontre de nœuds particuliers lors de l’inspection de l’AST. En effet, lors de l’exécution d’une règle Java, PMD parcourt chaque nœud de l’AST de manière séquentielle et exécute sur celui-ci la méthode qui a éventuellement été définie dans la règle pour ce type de nœud.
Il est par exemple possible d’écrire des méthodes qui seront exécutées lorsque PMD rencontrera un statement d’import, une déclaration de variable, une boucle for…

Concrètement, la règle doit hériter de la classe net.sourceforge.pmd.AbstractJavaRule. Cette classe propose notamment un ensemble de méthodes nommées visit, qu’il faut surcharger pour exécuter un traitement particulier lors de la détection d’un certain type de nœud. Par exemple :

  • la méthode Object visit(ASTImportStatement node, Object data) sera appelée lors de la visite d’un nœud de type ASTImportStatement (c’est-à-dire une ligne de la forme import mon.package.MaClasse;).
  • la méthode Object visit(ASTForStatement node, Object data) sera appelée lors de la visite d’un nœud de type ASTForStatement, c’est-à-dire une boucle for : for(int i = 0; i &lt;=1; i++).
    etc…

La construction d’une règle en Java consiste donc à implémenter une classe héritant de AbstractJavaRule et surchargeant les méthodes visit(XXX node, Object data) pour chaque type de nœud sur lequel un élément doit être inspecté.

Développement

Dans notre cas, nous souhaitons détecter les nœuds de type ASTLocalVariableDeclaration, nous allons donc surcharger la méthode visit(ASTLocalVariableDeclaration node, Object data). Dans cette méthode, nous allons tester si la déclaration de variable locale détectée se trouve dans une boucle et est de classe java.sql.Connection :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.excilys.tuto.pmd;

import net.sourceforge.pmd.AbstractJavaRule;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.ast.ASTDoStatement;
import net.sourceforge.pmd.ast.ASTForStatement;
import net.sourceforge.pmd.ast.ASTLocalVariableDeclaration;
import net.sourceforge.pmd.ast.ASTWhileStatement;

public class AvoidDeclaringConnectionsInLoopsJava extends AbstractJavaRule {

    public Object visit(ASTLocalVariableDeclaration node, Object data) {

        if (!(node.getParentsOfType(ASTWhileStatement.class).isEmpty()
                && node.getParentsOfType(ASTDoStatement.class).isEmpty()
                && node.getParentsOfType(ASTForStatement.class).isEmpty())) {
            if (java.sql.Connection.class.equals(node.getTypeNode().getType())) {
                addViolation((RuleContext) data, node.getTypeNode());
            }
        }
        return super.visit(node, data);
    }
}

Notez le paramètre (RuleContext) data présent dans l’appel à addViolation. Un objet de classe RuleContext contient des informations sur le contexte d’exécution de PMD, ainsi que des informations permettant d’identifier le fichier source sur lequel la violation a été détectée.
Si on ne passe pas le bon RuleContext à la méthode addViolation, cela aura donc pour effet d’associer la violation détectée avec tous les fichiers Java du projet. On se retrouverait dans notre cas avec une violation “Déclaration d’un objet Connection dans une boucle” détectée sur la ligne 14 de tous les fichiers Java du projet, alors qu’elle ne concernait que la ligne 14 du fichier MySample.java.

Configuration

La configuration de la règle dans le fichier rulesets.xml est similaire à la précédente, excepté le fait que la classe de la règle n’est plus net.sourceforge.pmd.rules.XPathRule mais com.excilys.tuto.pmd.AvoidDeclaringConnectionsInLoopsJava, et que nous n’avons plus d’expression XPath à déclarer :

1
2
3
4
5
6
7
<rule name="AvoidDeclaringConnectionsInLoopsJava"
     message="Avoid declaring java.sql.Connection objects in loops."
     class="com.excilys.tuto.pmd.AvoidDeclaringConnectionsInLoopsJava">
    <description>
        Avoid declaring java.sql.Connection objects in loops.
    </description>
</rule>

Test

De la même façon que ce que nous avons fait pour le test de la règle au format XPath, la règle Java peut être testée en lançant la commande suivante :

java net.sourceforge.pmd.PMD MySample.java xml rulesets.xml

Le rapport XML produit par cette commande est le même que celui que nous avions obtenu avec la version XPath de notre règle, ce qui permet de vérifier que la règle que nous venons d’écrire détecte l’anomalie.

Déploiement de la règle dans Sonar

Packaging

Pour que cette règle soit prise en compte par Sonar, nous devons ajouter plusieurs éléments au projet :

  • une classe RuleRepository héritant de org.sonar.api.rules.RuleRepository. Cette classe a pour but d’indiquer quel(s) est(sont) le(s) fichier(s) rulesets.xml contenant les règles à prendre en compte, ainsi que diverses options de configuration, par exemple la définition du langage sur lequel ces règles s’appliqueront.
  • une classe héritant de org.sonar.api.SonarPlugin. Cette classe permet à Sonar d’identifier notre projet comme étant un plugin. Elle définit le(s) RuleRepository constituant le plugin.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package com.excilys.tuto.pmd;

    import org.sonar.api.SonarPlugin;

    import java.util.Arrays;
    import java.util.List;

    public class PmdExtensionPlugin extends SonarPlugin {

        /**
         * List extension repositories constituting this plugin
         */

        public List getExtensions() {
            return Arrays.asList(PmdExtensionRepository.class);
        }
    }
  • un fichier extensions.xml, qui permet de recenser les règles à prendre en compte dans le fichier rulesets.xml, en indiquant pour chacune une clé, une priorité (qui peut être différente de celle précédemment définie pour PMD dans rulesets.xml)…
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?xml version="1.0"?>
    <rules>

        <rule>
            <key>AvoidDeclaringConnectionsInLoopsJava</key>
            <name>Avoid declaring connections in loops (Java).</name>
            <description>Avoid declaring connections in loops.</description>

            <configKey>com/excilys/tuto/pmd/rulesets.xml/AvoidDeclaringConnectionsInLoopsJava</configKey>

            <priority>BLOCKER</priority>
        </rule>
       
        <rule>
            <key>AvoidDeclaringConnectionsInLoopsXPath</key>
            <name>Avoid declaring connections in loops (XPath).</name>
            <description>Avoid declaring connections in loops.</description>

            <configKey>com/excilys/tuto/pmd/rulesets.xml/AvoidDeclaringConnectionsInLoopsXPath</configKey>

            <priority>BLOCKER</priority>
        </rule>
       
    </rules>

Sonar propose un plugin Maven (sonar-packaging-maven-plugin) permettant de packager le projet sous la forme d’un plugin Sonar. Une fois les éléments ci-dessus créés, il ne nous reste plus qu’à ajouter ce plugin au POM de notre projet et d’indiquer que le projet sera packagé sous la forme d’un plugin pour créer le JAR :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.excilys.tuto</groupId>
    <artifactId>excilys-pmd</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>sonar-plugin</packaging>

    <name>excilys-pmd</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>pmd</groupId>
            <artifactId>pmd</artifactId>
            <version>4.2.5</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.codehaus.sonar</groupId>
            <artifactId>sonar-plugin-api</artifactId>
            <version>2.10</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.5</source>
                    <target>1.5</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.codehaus.sonar</groupId>
                <artifactId>sonar-packaging-maven-plugin</artifactId>
                <version>1.1</version>
                <extensions>true</extensions>
                <configuration>
                    <pluginClass>com.excilys.tuto.pmd.PmdExtensionPlugin</pluginClass>
                    <excludes>
                        <exclude>**/sample/*.*</exclude>
                    </excludes>
                    <basePlugin>pmd</basePlugin>
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>

Déploiement du plugin et configuration de Sonar

Le répertoire de Sonar destiné à recevoir les plugins est <sonar-root>/extensions/plugins. Il suffit donc de copier le JAR précédemment obtenu à cet emplacement puis de redémarrer Sonar (sonar.sh restart) pour installer le plugin.

Une fois Sonar redémarré, connectons-nous en admin sur la console de Sonar (par défaut : http://localhost:9000), puis ouvrons la page Settings / Quality profiles. Sonar affiche ici la liste des profils disponibles pour l’analyse du code source. Nous allons modifier le profil “Sonar way” (activé par défaut) pour qu’il prenne en compte notre règle.

Recherchons les deux règles que nous venons d’écrire puis activons-les en cochant les checkbox correspondantes.

sonar2

Exécutons alors mvn sonar:sonar sur notre projet, les violations correspondant aux règles que nous avons définies sont visibles dans la console de Sonar :

sonar3_1

Conclusion

Cet article ne présente que quelques facettes des possibilités offertes par PMD, le framework est cependant riche et permet de définir des règles plus étendues que les exemples donnés ici. Pour en savoir plus, je vous invite à visiter le site de PMD : http://pmd.sourceforge.net/

Share
Categories: Non classé Tags: , ,
  1. 31/05/2013 à 08:20 | #1

    La même pour les règles Checkstyle, article écrit il y a qques temps déjà : http://blog.tchassagnette.com/2010/09/22/checkstyle-ecriture-de-regles-personnalisees/

  2. Rémi
    31/05/2013 à 08:32 | #2

    Article interressant!
    Mais existe-t’il la possibilité de faire la même chose sur la détection de librairie? Pour en interdire un ou deux en particulier dans un projet?

  3. François MARLIAC
    02/06/2013 à 18:59 | #3

    @Rémi
    Oui c’est possible ; pour cela, il faudrait détecter l’utilisation des packages de cette librairie dans le code. Deux types de nœuds devraient être détectés par cette règle :
    – les imports de packages de cette librairie (import mon.package.MaClasse);
    – les déclarations de variables dont le Type est issu d’un package de cette librairie (mon.package.MaClasse monInstance).

    La règle suivante contient des exemples de recherche sur les imports et les types de noeuds :
    http://pmd.sourceforge.net/pmd-4.3/xref/net/sourceforge/pmd/typeresolution/rules/imports/UnusedImports.html.

  1. Pas encore de trackbacks