Análisis de código Estático con PMD

Desde hace ya mucho, para los lenguajes de programación más comunes, disponemos de analizadores de código estático.

Existen varias herramientas en el mercado, para el análisis de código Apex (Checkmarx utilizada por Salesforce, Sonarqube, Cosechan, Codacy, etc.) pero desde abril de 2016, Robert Sösemann portó Apex a PMD, una de las herramientas más conocidas de análisis de código y con licencia Open Source (Hiper-Kudos para Robert), con años de experiencia como una de las soluciones líderes en Java.

PMD para Apex y Visualforce (aún incipiente) no requiere de ninguna infraestructura adicional, solo descargar los binarios y ejecutarlos, en nuestro ordenador, en el servidor, con la JDK instalada etc.

Las capacidades de PMD, su licenciamiento, versatilidad, extensibilidad, integración con los IDEs y con  herramientas de CI habituales (Maven, Ant, Gradle, etc.), y comunidad facilita enormemente su adopción.

Por ello, si no lo conoces, creo que puede ser muy útil, y es una herramienta muy a tener en cuenta para utilizar en nuestros proyectos. Te animo a leer el artículo por si te puede ayudar.

Qué es PMD

PMD se define como An extensible cross-language static code analyzer, y en pocas palabras, es una herramienta Open Source que permite realizar análisis de código estático de varios lenguajes, entre ellos Apex por la aportación inicial de Robert Sösemann.

Desde entonces, varias empresas, como la propia Salesforce,  aportan código, adaptaciones del lenguaje, nuevas reglas, etc.

Anuncio de la disponibilidad de PMD con Apex:

Definición del propio autor del port sobre PMD, que creo es bastante auto-explicativo:

En el momento de escritura de este artículo (Julio 2018) la versión actual de PMD es la 6.5.

Funcionamiento básico de PMD

Desde el punto de vista del usuario,típicamente usamos PMD de 4 formas distintas:

  1. Mediante linea de comandos (CLI)
  2. Integrado en un entorno de desarrollo como Intellij, Eclipse, Visual Studio Code, Welkin Suite, etc.
  3. Formando parte de un sistema de Integración Continua como Maven, Ant, Gradle, etc.
  4. Formando parte de un producto comercial en Cloud u OnPremise como Codacy, Code-Scan, etc.

Las características principales de PMD son :

  • Requiere de una lista de reglas, que alguien ha definido y programado previamente para detectar incumplimientos.
  • Estas reglas detectan situaciones que queremos anunciar, asignando un mensaje de error/advertencia/información, y un valor de prioridad/severidad.
  • Al invocar a PMD sobre una estructura de ficheros, la herramienta examina fichero a fichero, y va aplicando las reglas y produciendo los resultados de los incumplimientos positivos encontrados.
  • El conjunto de reglas definido se denomina ruleset. De la calidad de las rules, de la selección que hayamos realizado para nuestro proyecto, de como las entendamos, y del proceso de mejora continua que hayamos pensado, dependerá el éxito que tendremos en el uso de PMD.
  • El informe resultado, puede ser obtenido en varios formatos: fichero plano, XML, HTML, dentro de una herramienta, etc., aunque idealmente debería ser visible a cualquiera, sin secretismos, y estudiado entre todos para entender los incumplimientos y preparar medidas correctoras.

Ruleset definido por la comunidad y como está organizados

Existe un ruleset ofrecido por la comunidad para PMD-Apex, clasificado en las siguientes áreas:

  1. Best Practices: engloba las reglas que examinan el uso de buenas prácticas aceptadas mayoritariamente.
  2. Code Style: engloba las reglas que examinan el código para que comprueban el cumplimiento de cierto estilo de código.
  3. Design: reglas que nos permiten descubrir problemáticas de diseño (cada uno entiende diseño de manera distinta).
  4. Error Prone: reglas que permiten detectar situaciones propicias a errores en tiempo de ejecución o confusas.
  5. Performance: reglas que permiten detectar código que no permite la ejecución bajo condiciones de rendimiento aceptables.
  6. Security: reglas que detectan potenciales problemas de seguridad.
  7. Additional rulesets: lo que viene a ser un cajón de sastre para compatibilidad con versiones anteriores.

A continuación, con el siguiente gráfico, podemos observar el peso de cada área, en cuanto a la tipología de las reglas:

Distribución de rules por Área de análisis

Hagamos un repaso rápido de cada una de las reglas, y así obtener una idea de lo que pretenden cubrir en cada caso.

Reglas asociadas a Best Practices

  1. ApexUnitTestClassShouldHaveAsserts: todo Test debe tener una sentencia Assert.
  2. ApexUnitTestShouldNotUseSeeAllDataTrue: un test no debería usar la anotación @isTest(seeAllData=true) ya que la ejecución del test dependerá de los datos existentes en la sandbox.
  3. AvoidGlobalModifier: no deben usarse class con el acceso global, ya que no pueden ser modificadas ni eliminadas, muy importante para los que desarrollan managed packages.
  4. AvoidLogicInTrigger: evitar lógica de negocio en los trigger. Si esta no te queda clara te recomiendo todas las 4 entradas anteriores de este mismo blog ;-).

Reglas asociadas al área de Code Style

  1. ClassNamingConventions: el nombre de las clases debe empezar por una letra mayúscula.
  2. ForLoopsMustUseBraces: los bucles for deben tener siempre las llaves de apertura y cierre de bloque.
  3. IfElseStmtsMustUseBraces: análogo a la anterior para sentencias if-else.
  4. IfStmtsMustUseBraces: análogo a la anterior para sentencias if.
  5. MethodNamingConventions: el nombre de los métodos debe empezar por una letra minúscula.
  6. VariableNamingConventions: las variables de tipo final deben ser nombradas con nombres completo en mayúsculas, y el resto de variables no debe incluir guiones bajos en su nombre.
  7. WhileLoopsMustUseBraces: análogo a los anteriores respecto a las claves.

Reglas asociadas al área de Design

  1. AvoidDeeplyNestedIfStmts: detecta situaciones de anidación de if-then que son dificiles de entender y mantener.
  2. CyclomaticComplexity: examina la complejidad de un método, enumerando sus  puntos de decisión, estos son, los puntos donde el código puede saltar a otro sitio, típicamente las operaciones que realizan saltos son: if, while, for, case, etc. Para ello se asignan los siguientes rangos y se produce un aviso para los métodos cuya complejidad sea alta (que deberían ser seccionados en métodos auxiliares y disminuir así la complejidad). Los rangos establecidos son:
    1. Puntuación  1-4: complejidad baja
    2. Puntuación 5-7: complejidad moderada
    3. Puntuación 8-10: complejidad alta
    4. Puntuación 11+: complejidad muy alta
  3. ExcessiveClassLength: excesiva longitud de una clase.  El valor por defecto, asignado de longitud es de 1000 líneas de código
  4. ExcessiveParameterList: métodos con numerosos parámetros son difíciles de mantener. El valor por defecto, es de 4 parámetros.
  5. ExcessivePublicCount: las clases con numerosos miembros y métodos públicos, requieren de tests complejos, y son difíciles de mantener. El valor por defecto, de la suma de atributos y métodos públicos es de 25.
  6.  NcssConstructorCount: evalúa el número de líneas de un constructor de una clase, sin comentar los comentarios. Si el constructor contiene demasiadas líneas, debería ser troceado en otros métodos. El valor por defecto de número de líneas máximo es de 20.
  7. NcssMethodCount: análogo al anterior para métodos. El valor por defecto de número de líneas máximo es de 60.
  8. NcssTypeCount: análogo al anterior pero para la definición de tipos. El valor por defecto de número de líneas máximo es de 700.
  9. StdCyclomaticComplexity: deprecada, se usa la regla CyclomaticComplexity en su lugar.
  10. TooManyFields: aquellas clases con demasiados miembros son difíciles de comprender. El valor por defecto de número máximo de campos es de 20.

Ahora ya sabemos que entienden por Diseño 😉

Reglas asociadas al área de Error Prone

  1. AvoidDirectAccessTriggerMap: evitar acceder directamente a los list Trigger.old y Trigger.new para hacerlo en modo Bulk.
  2. AvoidHardcodingId: evitar la utilización de IDs en el código (hardcoded).
  3. AvoidNonExistentAnnotations: comprueba el uso de anotaciones deprecadas
  4. EmptyCatchBlock: detecta bloques catch que no implementan código de resolución de una excepción capturada.
  5. EmptyIfStmt: análoga a la anterior para bloques if.
  6. EmptyStatementBlock: análoga a la anterior para un bloque funcional cualquiera.
  7. EmptyTryOrFinallyBlock: análoga que la anterior para bloques Finally.
  8. EmptyWhileStmt: análoga a la anterior para bloques de iterados por while.
  9. MethodWithSameNameAsEnclosingClass: comprueba la existencia de métodos cuyo nombre sea el mismo que el de la clase.

Reglas asociadas al área de Performance

  1. AvoidDmlStatementsInLoops: valida la inserción de sentencias DML dentro de bloques iterativos.
  2. AvoidSoqlInLoops: valida la inserción de sentencias SOQL dentro de bloques iterativos.
  3. AvoidSoslInLoops: valida la inserción de sentencias SOSL dentro de bloques iterativos.

Reglas asociadas al área de Security

  1. ApexBadCrypto: valida que se utilizan IVs y keys para las llamadas crypto.
  2. ApexCRUDViolation: valida que se comprueban los permisos de acceso antes de ejecutar una operación SOQL/SOSL/DML.
  3. ApexCSRF: valida que no se llevan a cabo operaciones DML en los constructores ni métodos Init.
  4. ApexDangerousMethods: valida que no se realizan llamadas a métodos peligrosos, actualmente comprueba disableTriggerCRUDSecurity() y llamadas a System.debug que contengan parámetros que puedan exponer datos sensibles.
  5. ApexInsecureEndpoint: valida la invocación de Callouts mediante https.
  6. ApexOpenRedirect: valida la redirección hacia ubicaciones inseguras.
  7. ApexSharingViolations: valida la declaración de clases sin without sharing especificado.
  8. ApexSOQLInjection: valida el uso de operaciones DML no confiables o con variables sin escape.
  9. ApexSuggestUsingNamedCred: valida la inclusión de credenciales para la invocación de Callouts.
  10. ApexXSSFromEscapeFalse: valida que las invocaciones a addError se realicen con escape habilitado, para evitar ataques XSS.
  11. ApexXSSFromURLParam: valida que todos los parámetros obtenidos de una URL son obtenidos sin riesgo para ataque XSS.
RulesLimitsSalesforce
Dado el carácter Multi-tenant, Salesforce requiere del establecimiento de límites para su correcto funcionamiento. El uso de reglas que validen las buenas prácticas establecidas, nos ahorrará tiempo y malas ratos.

El Ruleset: listado de las reglas a aplicar

El ruleset, es un fichero XML, que define el conjunto de reglas que aplicaremos al ejecutar un análisis de código.

En la siguiente porción de código vemos la definición de un ruleset:

  • Un ruleset tiene atributos (description) y  la secuencia de las rules que lo componen.
  • Cada regla contiene su referencia, atributos (como por ejemplo el name y la priority) y propiedades (formadas por claves-valor que determinan cierto comportamiento de la regla).
<?xml version="1.0" encoding="UTF-8"?>
<ruleset xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Default ruleset para desarrolladores del proyecto X" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">

<description>Ruleset durante etapa de desarrollo</description>

   <rule ref="category/apex/design.xml/ExcessiveClassLength" message="Evitar clases super largas de código (líneas de código)">
      <priority>3</priority>
      <properties>
         <property name="minimum" value="1000" />
      </properties>
   </rule>
   <rule ref="category/apex/design.xml/ExcessiveParameterList" message="Evitar métodos con una lista numerosa de parámetros">
      <priority>3</priority>
      <properties>
         <property name="minimum" value="4" />
      </properties>
   </rule>
...
</ruleset>

Como hemos comentado justo al iniciar el artículo, la comunidad ofrece un ruleset definido para Apex PMD,  pero es fundamental, que en nuestro proyecto (cliente, proveedor, equipo de QA, etc.) acordemos nuestro ruleset, o si tenemos suficiente expertise, construirnos y añadir nuestras propias reglas.

Configuración de nuestro Ruleset

Veamos ahora como configurar nuestro propio ruleset. Para ello, utilizaremos como base las reglas definidas por la comunidad en el ruleset por defecto. El esqueleto básico del ruleset es:

<?xml version="1.0"?>

<ruleset name="Custom Rules" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>
        Nuestro propio ruleset
    </description>
    <!-- Definición de reglas y parámetros -->
</ruleset>

Añadir una regla o un conjunto de reglas

Podemos añadir reglas predefinidas o un conjunto completo de reglas directamente (de las que hemos visto antes, dado que estamos trabajando con las pre-definidas por PMD):

<!-- Así añadimos una única regla: En este caso la ExcessiveParameterList que pertenece al bloque de Design -->
<rule ref="category/apex/design.xml/ExcessiveParameterList" />
<!-- Y así añadimos una conjunto completo de reglas: En este caso, para seguir con el ejemplo, añado todas las del bloque de Design -->
<rule ref="category/apex/design.xml" />
<!-- Podemos tunear mediante el uso de exclude, de manera que ahora las añadíriamos todas menos ExcessiveParameterList -->
<rule ref="category/apex/design.xml/ExcessiveParameterList" >
  <exclude name="ExcessiveParameterList"/>
</rule>

Usando Patterns, podemos simplificar de manera radical el ruleset, veamos ejemplos básicos:

<?xml version="1.0"?>
<ruleset name="myruleset" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
   <description>Incluimos 2</description>

   <!-- Mediante exclude-pattern e include-pattern referencimos patrones basándonos en los nombres de las referencias reglas-->

   <exclude-pattern>category/apex/design.xml/Excessive*</exclude-pattern>
   <include-pattern>category/apex/design.xml/Ncss*</include-pattern>   
   <include-pattern>category/apex/security.xml/Apex*</include-pattern>

   <!-- A partir de aquí definimos la reglas individuales -->

</ruleset>

Nota: el esquema es exactamente igual para el resto de lenguajes soportados por PMD!!

Modificar Atributos y Propiedades de una regla para adecuarla a nuestros criterios

Las reglas pueden contener propiedades pre-definidas, pero quizás, en nuestro proyecto queremos modificar los atributos estándar, como por ejemplo:

  • El mensaje que aparezca al incumplir una regla
  • El valor sobre la Prioridad que deseamos asignar a ese positivo

Aquí va un ejemplo muy sencillo de cómo hacerlo:

  • Modificamos la regla EmptyIfStmt para que refleje un texto distinto, y además para que en Visual Studio Code se muestre, como Error en lugar de solo Warning.
  • Para ello modifico la Prioridad a 1.

En el ruleset dejo la regla así:

<rule ref="category/apex/errorprone.xml/EmptyIfStmt" message="Esteve por dios, implementa los IFs">
      <priority>1</priority>
</rule>

El resultado es el esperado, por ejemplo en VSCode, vemos, como se muestra el mensaje que hemos tuneado y el incumplimiento se muestra como grave:

Para Intellij aparece de forma parecida:

Como evitar que ciertas partes del código se examinen

Por los motivos que sean, podemos requerir que ciertas partes del código sean ignoradas por PMD. Mediante la anotación @SuppressWarnings podemos ignorar todas o algunas reglas en una clase.

En este primer ejemplo, voy a suprimir la comprobación de todas las reglas en la clase:


// Ignorar cualquier validación para PMD
@SuppressWarnings('PMD')
public class ForceGraells {
    void leer() {}
}

En este otro ejemplo, voy a suprimir solo la comprobación de 2 reglas concretas:


// Ignorar 2 reglas concretas
@SuppressWarnings('PMD.ExcessiveParameterList,  
                   PMD.EmptyStatementBlock')
public class ForceGraells {
    void leer(String a, String b, String c, String d, String e) {
        //sin cuerpo
    }
}

Veamos un ejemplo de como usar la anotación @SuppressWarnings y su resultado en Intellij:

Para los más experimentados, comentar que el comentario //NOPMD no está soportado en Apex (enlace al issue de la discusión en el Repositorio oficial), dado que el parseador utilizado actualmente elimina los comentarios (veremos en futuras versiones como evoluciona este aspecto, ya que sería de gran utilidad, bien usado 😉 )

Detector de código copiado

Finalmente, dentro del mismo bundle de herramientas, el proyecto PMD ofrece una herramienta para detección de código duplicado, que puede ser muy útil para detectar esas situaciones que nos pueden haber pasado por alto.

Mediante varios parámetros de configuración podemos afinar la búsqueda de segmentos de código duplicado. A continuación adjunto un ejemplo del resultado ofrecido por cpd:

Found a 8 line (16 tokens) duplication in the following files: 
Starting at line 3 of /home/egraells/Dropbox/workspace/triggerClaseDinamica/triggerClaseDinamica/force-app/main/default/classes/ForceGraells.cls
Starting at line 12 of /home/egraells/Dropbox/workspace/triggerClaseDinamica/triggerClaseDinamica/force-app/main/default/classes/ForceGraells.cls

    public void leer(){

        boolean durmiendo = false;
        if (!durmiendo){
            //System.debug('Este codigo nunca se ejecuta');
        }   
        //Petete: una sólida cultura es la herencia más segura
    }
=====================================================================

Recomendaciones personales

  • Para evaluar fast&furious PMD para Apex utilízalo dentro de tu IDE, hay plugins para casi todos ellos y así simplificarás el proceso.
  • Utiliza la  opción -cache para acelerar el análisis.
  • Utiliza con exquisita precaución la opción @SuppressWarnings. Debe haber una buena razón acordada.
  • Puedes empezar modificando el  ruleset comunitario, para cambiar textos y prioridades, y partir de ahí con confianza, tunearlo.

Conclusiones

El análisis de código estático es imprescindible en cualquier proyecto (***).

Existen otras herramientas en el mercado muy conocidas, como SonarQube, Code-Scan, Clayton, Codacy, Checkmarx, etc., que proporcionan funcionalidades adicionales, pero que requieren de más infraestructura  y/o costes adicionales (aunque te animo por supuesto a evaluarlas igualmente).

Por ello,  si áun no estás usando ninguna, creo que PMD es la herramienta ideal para iniciarte, dado que con el respaldo de la comunidad y de la propia Salesforce, permiten a cualquier desarollador usarlo sin coste y en muy breve tiempo.

Espero que te sirva y si te encuentras a Robert por la calle, no olvides agradecérselo!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.