SFDX CLI Scanner, la navaja suiza para el análisis de código estático en Salesforce

Salesforce acaba de publicar un plugin para su CLI, denominado CLI Scanner.

En este artículo veremos qué es, para qué sirve, cómo utilizarlo y sus fortalezas y puntos de mejora.

¿Qué es?

  • Salesforce CLI Scanner es un plug-in de la CLI de Salesforce (vaya frase!!). Esto implica que es invocable en la línea de comandos, y por tanto automatizable en un pipeline de CI/CD, o en tipo shell script, etc.
  • Es un agregador, es decir, ejecuta motores de análisis estático de código, y ofrece los resultados obtenidos (incumplimientos en base a las reglas de los motores).
  • Permite seleccionar las opciones para que el equipo de proyecto pueda limitar el ámbito de actuación.
  • Permite añadir/eliminar reglas de los motores usados.

En el esquema de la documentación de Salesforce, vemos como el agregador tiene como inputs: el código fuente, las aplica las reglas de todos los motores de los que tiene constancia en su catálogo, y devuelve los incumplimientos que los diversos motores hayan encontrado.

En esta primera versión del Scanner, tenemos los 2 motores de reglas más usados por los desarrolladores:

  • PMD (v6.22.0)
  • ESlint (6.8.0)
Esquema alto nivel Salesforce CLI Scanner

Funciones del Scanner CLI

Sus funciones son básicamente 5 actualmente:

  1. Ejecutar un análisis de código fuente. Permite la selección de las categorías de validación (subconjuntos de reglas de un mismo ámbito) que queremos ejecutar, los ficheros de código fuente que queremos analizar, el formato de salida, etc.
  2. Listar todas las reglas disponibles en el catálogo. Obtenemos la lista de reglas que por lenguaje y/o por categoría a la que pertenecen. Al final de este artículo he incluido el listado de todas las reglas disponibles.
  3. Obtener la descripción y las características de una regla. Obtiene los atributos, como nombre, descripción, categoría, etc., de una regla concreta.
  4. Añadir una regla al catálogo. Mis ex-compañeros de un banco español muy conocido, estarán felices de esta capacidad ya que habían ampliado el conjunto de reglas de PMD.
  5. Eliminar una regla existente del catálogo.

Video demostrativo: instalación y uso básico

En el siguiente video intento mostrarte como instalar y como usarla, por ejemplo para analizar la aplicación Dreamhouse.

Primersos pasos: instalación del plugin, listado de reglas,

Ejemplos

Veamos algunos ejemplos para una mayor comprensión:

sfdx scanner:run --format table --target TestPropertyController.cls: analiza el fichero respecto a todas las reglas del catálogo (solo serán aplicadas las de Apex), la salida la solicito en formato tabla en la salida estándar.

sfdx scanner:run --format table --target messageService.js --category "Design": analiza el fichero javascript indicado respecto a las reglas que pertenecen a la categoría Design de Javascript

sfdx scanner:rule:describe --rulename ExcessiveClassLength: nos detalla las características de la regla, nos detalla en que circunstacia aplica, etc.:

Feedback – lo que más me ha gustado

  • Esta herramienta simplifica todo el proceso de instalación de PMD y ESLint para automatización en una pipeline de CI/CD. Aunque antes ya era posible con las herramientas de PMD y Eslint, la facilidad de instalar/actualizar y usar el plugin es genial.
  • Los requerimientos son básicos, y la instalación trivial, con tener la JDK 8 o superior y la CLI es suficiente. Ambos requerimientos son totalmente automatizables, pueden ser satisfechos al vuelo en un contenedor por ejemplo para tener escaneos de usar y tirar, completamente automatizado.
  • Su mantenimiento al ser un plugin será realmente sencilla.
  • Permite que un equipo de desarrollo unifique una herramienta más de manera sencilla mediante el uso de un plugin.
  • Las opciones de utilizar JSON como formato de salida y la opción --output me parecen super interesantes.
  • La idea de un agregador, como ejecutor de motores me ha gustado mucho, ya que sienta las bases de una herramienta muy potente y abierta, en lugar de reinventar la rueda.
  • Es una herramienta open source, podemos adaptarla y moldearla a nuestras necesidades.

Feedback – posibilidades de mejora

Esta es la 1a versión del scanner, y me parece una idea muy buena, de cara a las posibles ampliaciones con nuevos motores.

Aun así, en esta mi primera toma de contacto, estas son las pequeñas debilidades/inconvenientes que he encontrado:

  • Conozco algunos proyectos que estandarizan una versión muy concreta de PMD, con lo que si la versión que está dentro del motor no es la misma, deben añadir y elimina las reglas que no coincidan entre versiones. Aunque como es automatizables será un esfuerzo inicial que valga la pena
  • Si el fichero de código fuente no existe, no aparece ningún mensaje de error:
  • No es posible analizar todos los ficheros de una aplicación ya que no hay buscar recursiva en directorios. Sin ser ningún experto, te paso el script que he utilizado para analizar el proyecto Dreamhouse desde su raíz
#! /bin/bash

#Directorio del proyecto
SourceCodeDir="/mnt/DATOS/workspace/dreamhouse-lwc"

#Analisis de ficheros Apex
find $SourceCodeDir -name "*.cls" -print0 | while read -d $'\0' file
do
echo -e "\nANALIZANDO $file"

#Aquí puedes limitar qué categorias, especificar el formato de salida etc.
sfdx scanner:run --format table --target $file
done

#Analisis de ficheros Javascript

find $SourceCodeDir -name "*.js" -print0 | while read -d $'\0' file
do
echo -e "\nANALIZANDO $file"

#Idem
sfdx scanner:run --format table --target $file
done
  • Aparecen muchos errores debido a que no se reconocen las anotaciones propias y otras características de LWC.
  • Hecho en falta la salida en formato HTML directamente.

Conclusiones

Este scanner establece las bases para un análisis estático de código con diversos motores, especialmente útil en un pipeline CI/CD, y que puede ser usado desde el primer día dado que cuenta con los 3 motores más usados actualmente.

Links interesantes

Anuncio disponibilidad de SFDX CLI Scanner: https://developer.salesforce.com/blogs/2020/10/improve-your-code-quality-with-the-salesforce-cli-scanner.html

Documentación oficial SFDX CLI Scanner: https://forcedotcom.github.io/sfdx-scanner/

Qué son los Plugins para la Salesforce CLI: https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_plugins.meta/sfdx_cli_plugins/cli_plugins_intro_what_is_plugin.htm

Aplicación Dreamhouse – como instalarla: https://github.com/trailheadapps/dreamhouse-lwc#installing-dreamhouse-using-a-developer-edition-org-or-a-trailhead-playground

Video – Análisis Código Estástico con PMD: https://youtu.be/I03y7xKQdCE

Repositorio de código del proyecto SFDX-Scanner: https://github.com/forcedotcom/sfdx-scanner

Artículos anterior sobre Análisis de código estático con PMD: Análisis de código Estático con PMD

Apéndice – Listado de reglas por lenguaje

Reglas para APEX y visualforce

Nombre ReglaLenguajeCategoría
VfCsrf visualforceSecurity
VfUnescapeEl visualforce Security
ApexAssertionsShouldIncludeMessage ApexBest Practices
ApexUnitTestClassShouldHaveAsserts ApexBest Practices
ApexUnitTestMethodShouldHaveIsTestAnnotation ApexBest Practices
ApexUnitTestShouldNotUseSeeAllDataTrue ApexBest Practices
AvoidGlobalModifier ApexBest Practices
AvoidLogicInTrigger ApexBest Practices
DebugsShouldUseLoggingLevel ApexBest Practices
AvoidDmlStatementsInLoops ApexPerformance
AvoidSoqlInLoops ApexPerformance
AvoidSoslInLoops ApexPerformance
ApexBadCrypto ApexSecurity
ApexCRUDViolation ApexSecurity
ApexCSRF ApexSecurity
ApexDangerousMethods ApexSecurity
ApexInsecureEndpoint ApexSecurity
ApexOpenRedirect ApexSecurity
ApexSharingViolations ApexSecurity
ApexSOQLInjection ApexSecurity
ApexSuggestUsingNamedCred ApexSecurity
ApexXSSFromEscapeFalse ApexSecurity
ApexXSSFromURLParam ApexSecurity
ClassNamingConventions ApexCode Style
IfElseStmtsMustUseBraces ApexCode Style
IfStmtsMustUseBraces ApexCode Style
FieldNamingConventions ApexCode Style
ForLoopsMustUseBraces ApexCode Style
FormalParameterNamingConventions ApexCode Style
LocalVariableNamingConventions ApexCode Style
MethodNamingConventions ApexCode Style
OneDeclarationPerLine ApexCode Style
PropertyNamingConventions ApexCode Style
VariableNamingConventions ApexCode Style
WhileLoopsMustUseBraces ApexCode Style
AvoidDeeplyNestedIfStmts ApexDesign
CyclomaticComplexity ApexDesign
CognitiveComplexity ApexDesign
ExcessiveParameterList ApexDesign
ExcessivePublicCount ApexDesign
NcssConstructorCount ApexDesign
NcssMethodCount ApexDesign
NcssTypeCount ApexDesign
StdCyclomaticComplexity ApexDesign
TooManyFields ApexDesign
ApexCSRF apexError Prone
AvoidDirectAccessTriggerMap apexError Prone
AvoidHardcodingId apexError Prone
EmptyCatchBlock apexError Prone
EmptyIfStmt apexError Prone
EmptyStatementBlock apexError Prone
EmptyTryOrFinallyBlock apexError Prone
EmptyWhileStmt apexError Prone
MethodWithSameNameAsEnclosingClass apexError Prone
AvoidNonExistentAnnotations apexError Prone
TestMethodsMustBeInTestClasses apexError Prone
ApexDoc Apex

Para Javascript

Nombre ReglaLenguajeCategoria
constructor-super javascriptECMAScript 6eslint
for-direction javascriptPossible Errorseslint
getter-return javascriptPossible Errorseslint
no-async-promise-executor javascriptPossible Errorseslint
no-case-declarations javascriptBest Practiceseslint
no-class-assign javascriptECMAScript 6eslint
no-compare-neg-zero javascriptPossible Errorseslint
no-cond-assign javascriptPossible Errorseslint
no-const-assign javascriptECMAScript 6eslint
no-constant-condition javascriptPossible Errorseslint
no-control-regex javascriptPossible Errorseslint
no-debugger javascriptPossible Errorseslint
no-delete-var javascriptVariableseslint
no-dupe-args javascriptPossible Errorseslint
no-dupe-class-members javascriptECMAScript 6eslint
no-dupe-keys javascriptPossible Errorseslint
no-duplicate-case javascriptPossible Errorseslint
no-empty javascriptPossible Errorseslint
no-empty-character-class javascriptPossible Errorseslint
no-empty-pattern javascriptBest Practiceseslint
no-ex-assign javascriptPossible Errorseslint
no-extra-boolean-cast javascriptPossible Errorseslint
no-extra-semi javascriptPossible Errorseslint
no-fallthrough javascriptBest Practiceseslint
no-func-assign javascriptPossible Errorseslint
no-global-assign javascriptBest Practiceseslint
no-inner-declarations javascriptPossible Errorseslint
no-invalid-regexp javascriptPossible Errorseslint
no-irregular-whitespace javascriptPossible Errorseslint
no-misleading-character-class javascriptPossible Errorseslint
no-mixed-spaces-and-tabs javascriptStylistic Issueseslint
no-new-symbol javascriptECMAScript 6eslint
no-obj-calls javascriptPossible Errorseslint
no-octal javascriptBest Practiceseslint
no-prototype-builtins javascriptPossible Errorseslint
no-redeclare javascriptBest Practiceseslint
no-regex-spaces javascriptPossible Errorseslint
no-self-assign javascriptBest Practiceseslint
no-shadow-restricted-names javascriptVariableseslint
no-sparse-arrays javascriptPossible Errorseslint
no-this-before-super javascriptECMAScript 6eslint
no-undef javascriptVariableseslint
no-unexpected-multiline javascriptPossible Errorseslint
no-unreachable javascriptPossible Errorseslint
no-unsafe-finally javascriptPossible Errorseslint
no-unsafe-negation javascriptPossible Errorseslint
no-unused-labels javascriptBest Practiceseslint
no-unused-vars javascriptVariableseslint
no-useless-catch javascriptBest Practiceseslint
no-useless-escape javascriptBest Practiceseslint
no-with javascriptBest Practiceseslint
require-yield javascriptECMAScript 6eslint
use-isnan javascriptPossible Errorseslint
valid-typeof javascriptPossible Errorseslint
constructor-super javascriptECMAScript 6eslint
for-direction javascriptPossible Errorseslint
getter-return javascriptPossible Errorseslint
no-async-promise-executor javascriptPossible Errorseslint
no-case-declarations javascriptBest Practiceseslint
no-class-assign javascriptECMAScript 6eslint
no-compare-neg-zero javascriptPossible Errorseslint
no-cond-assign javascriptPossible Errorseslint
no-const-assign javascriptECMAScript 6eslint
no-constant-condition javascriptPossible Errorseslint
no-control-regex javascriptPossible Errorseslint
no-debugger javascriptPossible Errorseslint
no-delete-var javascriptVariableseslint
no-dupe-args javascriptPossible Errorseslint
no-dupe-class-members javascriptECMAScript 6eslint
no-dupe-keys javascriptPossible Errorseslint
no-duplicate-case javascriptPossible Errorseslint
no-empty javascriptPossible Errorseslint
no-empty-character-class javascriptPossible Errorseslint
no-empty-pattern javascriptBest Practiceseslint
no-ex-assign javascriptPossible Errorseslint
no-extra-boolean-cast javascriptPossible Errorseslint
no-extra-semi javascriptPossible Errorseslint
no-fallthrough javascriptBest Practiceseslint
no-func-assign javascriptPossible Errorseslint
no-global-assign javascriptBest Practiceseslint
no-inner-declarations javascriptPossible Errorseslint
no-invalid-regexp javascriptPossible Errorseslint
no-irregular-whitespace javascriptPossible Errorseslint
no-misleading-character-class javascriptPossible Errorseslint
no-mixed-spaces-and-tabs javascriptStylistic Issueseslint
no-new-symbol javascriptECMAScript 6eslint
no-obj-calls javascriptPossible Errorseslint
no-octal javascriptBest Practiceseslint
no-prototype-builtins javascriptPossible Errorseslint
no-redeclare javascriptBest Practiceseslint
no-regex-spaces javascriptPossible Errorseslint
no-self-assign javascriptBest Practiceseslint
no-shadow-restricted-names javascriptVariableseslint
no-sparse-arrays javascriptPossible Errorseslint
no-this-before-super javascriptECMAScript 6eslint
no-undef javascriptVariableseslint
no-unexpected-multiline javascriptPossible Errorseslint
no-unreachable javascriptPossible Errorseslint
no-unsafe-finally javascriptPossible Errorseslint
no-unsafe-negation javascriptPossible Errorseslint
no-unused-labels javascriptBest Practiceseslint
no-unused-vars javascriptVariableseslint
no-useless-catch javascriptBest Practiceseslint
no-useless-escape javascriptBest Practiceseslint
no-with javascriptBest Practiceseslint
require-yield javascriptECMAScript 6eslint
vuse-isnan javascriptPossible Errorseslint
valid-typeof javascriptPossible Errorseslint
@lwc/lwc/no-api-reassignments javascriptLWCeslint
@lwc/lwc/no-async-operation javascriptLWCeslint
@lwc/lwc/no-deprecated javascriptLWCeslint
@lwc/lwc/no-inner-html javascriptLWCeslint
@lwc/lwc/no-leading-uppercase-api-name javascriptLWCeslint
@lwc/lwc/no-leaky-event-listeners javascriptLWCeslint
@lwc/lwc/valid-api javascriptLWCeslint
@lwc/lwc/valid-track javascriptLWCeslint
@lwc/lwc/valid-wire javascriptLWCeslint

Para Typescript

Nombre ReglaLenguajeCategoria
constructor-super typescriptECMAScript 6eslint
for-direction typescriptPossible Errorseslint
getter-return typescriptPossible Errorseslint
no-async-promise-executor typescriptPossible Errorseslint
no-case-declarations typescriptBest Practiceseslint
no-class-assign typescriptECMAScript 6eslint
no-compare-neg-zero typescriptPossible Errorseslint
no-cond-assign typescriptPossible Errorseslint
no-const-assign typescriptECMAScript 6eslint
no-constant-condition typescriptPossible Errorseslint
no-control-regex typescriptPossible Errorseslint
no-debugger typescriptPossible Errorseslint
no-delete-var typescriptVariableseslint
no-dupe-args typescriptPossible Errorseslint
no-dupe-class-members typescriptECMAScript 6eslint
no-dupe-keys typescriptPossible Errorseslint
no-duplicate-case typescriptPossible Errorseslint
no-empty typescriptPossible Errorseslint
no-empty-character-class typescriptPossible Errorseslint
no-empty-pattern typescriptBest Practiceseslint
no-ex-assign typescriptPossible Errorseslint
no-extra-boolean-cast typescriptPossible Errorseslint
no-extra-semi typescriptPossible Errorseslint
no-fallthrough typescriptBest Practiceseslint
no-func-assign typescriptPossible Errorseslint
no-global-assign typescriptBest Practiceseslint
no-inner-declarations typescriptPossible Errorseslint
no-invalid-regexp typescriptPossible Errorseslint
no-irregular-whitespace typescriptPossible Errorseslint
no-misleading-character-class typescriptPossible Errorseslint
no-mixed-spaces-and-tabs typescriptStylistic Issueseslint
no-new-symbol typescriptECMAScript 6eslint
no-obj-calls typescriptPossible Errorseslint
no-octal typescriptBest Practiceseslint
no-prototype-builtins typescriptPossible Errorseslint
no-redeclare typescriptBest Practiceseslint
no-regex-spaces typescriptPossible Errorseslint
no-self-assign typescriptBest Practiceseslint
no-shadow-restricted-names typescriptVariableseslint
no-sparse-arrays typescriptPossible Errorseslint
no-this-before-super typescriptECMAScript 6eslint
no-undef typescriptVariableseslint
no-unexpected-multiline typescriptPossible Errorseslint
no-unreachable typescriptPossible Errorseslint
no-unsafe-finally typescriptPossible Errorseslint
no-unsafe-negation typescriptPossible Errorseslint
no-unused-labels typescriptBest Practiceseslint
no-unused-vars typescriptVariableseslint
no-useless-catch typescriptBest Practiceseslint
no-useless-escape typescriptBest Practiceseslint
no-with typescriptBest Practiceseslint
require-yield typescriptECMAScript 6eslint
use-isnan typescriptPossible Errorseslint
valid-typeof typescriptPossible Errorseslint
@typescript-eslint/adjacent-overload-signatures typescriptBest Practiceseslint
@typescript-eslint/await-thenable typescriptBest Practiceseslint
@typescript-eslint/ban-ts-ignore typescriptBest Practiceseslint
@typescript-eslint/ban-types typescriptBest Practiceseslint
@typescript-eslint/camelcase typescriptStylistic Issueseslint
@typescript-eslint/class-name-casing typescriptBest Practiceseslint
@typescript-eslint/consistent-type-assertions typescriptBest Practiceseslint
@typescript-eslint/explicit-function-return-type typescriptStylistic Issueseslint
@typescript-eslint/interface-name-prefix typescriptStylistic Issueseslint
@typescript-eslint/member-delimiter-style typescriptStylistic Issueseslint
@typescript-eslint/no-array-constructor typescriptStylistic Issueseslint
@typescript-eslint/no-empty-function typescriptBest Practiceseslint
@typescript-eslint/no-empty-interface typescriptBest Practiceseslint
@typescript-eslint/no-explicit-any typescriptBest Practiceseslint
@typescript-eslint/no-for-in-array typescriptBest Practiceseslint
@typescript-eslint/no-inferrable-types typescriptBest Practiceseslint
@typescript-eslint/no-misused-new typescriptBest Practiceseslint
@typescript-eslint/no-misused-promises typescriptBest Practiceseslint
@typescript-eslint/no-namespace typescriptBest Practiceseslint
@typescript-eslint/no-non-null-assertion typescriptStylistic Issueseslint
@typescript-eslint/no-this-alias typescriptBest Practiceseslint
@typescript-eslint/no-unnecessary-type-assertion typescriptBest Practiceseslint
@typescript-eslint/no-unused-vars typescriptVariableseslint
@typescript-eslint/no-use-before-define typescriptVariableseslint
@typescript-eslint/no-var-requires typescriptBest Practiceseslint
@typescript-eslint/prefer-includes typescriptBest Practiceseslint
@typescript-eslint/prefer-namespace-keyword typescriptBest Practiceseslint
@typescript-eslint/prefer-regexp-exec typescriptBest Practiceseslint
@typescript-eslint/prefer-string-starts-ends-with typescriptBest Practiceseslint
@typescript-eslint/require-await typescriptBest Practiceseslint
@typescript-eslint/triple-slash-reference typescriptBest Practiceseslint
@typescript-eslint/type-annotation-spacing typescriptStylistic Issueseslint
@typescript-eslint/unbound-method typescriptBest Practiceseslint