Blog de Amazon Web Services (AWS)

Usar Amazon CodeGuru Reviewer para encontrar inconsistencias de código

En este blog vamos a presentar el detector de inconsistencias para Java en Amazon CodeGuru Reviewer. CodeGuru Reviewer analiza automáticamente pull requests (creadas en repostorios soportados tales como AWS CodeCommit, GitHub, GitHub Enterprise, y Bitbucket) y genera recomendaciones para mejorar la calidad del código. Para más información, ver Automating code reviews and application profiling with Amazon CodeGuru.

El principio de Inconsistencia

El software es repetitivo, por lo que es posible extraer especificaciones de uso desde el análisis de grandes bases de código. Aunque las especificaciones obtenidas son habitualmente correctas, no suelen ser bugs los que ocasionen los incumplimientos de dichas especificaciones. Esto es porque el uso del código es contextual y, por lo tanto, se requiere de análisis contextual para detectar incumplimientos válidos de dichas especificaciones. Es más, la aplicación de especificaciones sin contexto puede conducir a muchos falsos positivos.

El principio de inconsistencia esta basado en la siguiente observación: Si bien, normalmente se permiten las desviaciones de especificaciones entre repositorios, las desviaciones dentro de dichos repositorios (o paquetes) deben ser analizadas por los desarrolladores ya que estas sí suelen ser causadas por bugs o “code smells” (código que huele).

Consideremos el siguiente ejemplo. Asumimos que, dentro de un repositorio o paquete, observamos 10 ocurrencias de esta declaración:

private static final String stage = AppConfig.getDomain().toLowerCase();

Y también asumimos que hay una ocurrencia de la siguiente declaración en el mismo paquete:

private static final String newStage = AppConfig.getDomain();

La inconsistencia puede ser descrita como: en este paquete, hay una llamada API de toLowerCase() seguida de AppConfig.getDomain() que convierte el string generado por AppConfig.getDomain() a su correspondencia en minúsculas. Sin embargo, en uno de los casos no aparece dicha llamada. Esto debe ser examinado.

En resumen, el detector de inconsistencias puede aprender desde un único paquete (léase como: tu propia base de código) y puede identificar patrones de uso en el código, tales como patrones de logging, manejo de excepciones, ejecución de null checks, etc. A continuación, se marcan los patrones de uso que no son consistentes con el resto. Mientras que las violaciones marcadas son frecuentemente bugs o “code smells”, es posible que, en algunos casos, la mayoría de los patrones de uso sean anti-patrones y las violaciones sean en realidad sean falsos positivos. La extracción y detección son ejecutadas al vuelo durante el análisis de código de forma que no se almacenan ni el código ni los patrones. Esto asegura preservar la privacidad de los requerimientos del código del cliente.

Recomendaciones

Quizás la característica mas distinguida del detector de inconsistencias es que está basada en aprender del propio código del cliente mientras ejecuta el análisis al vuelo sin guardar el código. Esto es diferente de las reglas predefinidas o los algoritmos de Machine Learning que tienen como objetivo defectos específicos de código tales como concurrencia. Estas aproximaciones también almacenan modelos o patrones aprendidos. Por lo tanto, los detectores basados en el principio de inconsistencia cubren un mayor rango de problemas en el código.

Nuestra aproximación tiene la intención de detectar automáticamente inconsistencias en partes del código que son anormales y que se desvían de los patrones típicos de código dentro de un mismo paquete (asumiendo que el comportamiento típico es el adecuado). Esta aproximación de detección de anomalías no requiere un conocimiento previo y minimiza el esfuerzo de los desarrolladores a la hora de predefinir conjuntos de reglas, ya que puede, implícita y automáticamente, inferir reglas tales como patrones en el código desde el propio código fuente. Si bien las inconsistencias pueden ser detectadas en aspectos muy específicos de programas (tales como sincronización, excepciones, logging, etc.) CodeGuru Reviewer puede detectar inconsistencias en múltiples aspectos del programa. Las detecciones no están limitadas a defectos específicos de código, sino que cubren diversas categorías de problemas en el código. Estos defectos no tienen porque ser necesariamente bugs, también pueden ser code smells que deben ser evitados para un mejor mantenimiento y legibilidad del mismo. CodeGuru Reviewer puede detectar hasta 12 tipos de inconsistencias de código para Java. Estos detectores identifican problemas como typos; mensajes, logging o declaraciones inconsistentes, falta de llamadas API, null checks, precondiciones, excepciones, etc. Ahora echemos un vistazo a varios ejemplos reales de código (aunque puede que no cubran todos los tipos de hallazgos).

Typo

Los typos frecuentemente aparecen en mensajes de código, logs, rutas, nombres de variables y otros strings. Incluso typos simples pueden ser difíciles de encontrar pudiendo causar errores graves en el código. En el ejemplo (real) de más abajo, el desarrollador está estableciendo una localización para un fichero de configuración.

context.setConfigLocation("com.amazom.daas.customerinfosearch.config");

El detector de inconsistencias detecta un typo en la ruta amazomamazon. Un fallo en la identificación y posterior solución de este typo hará que no se pueda encontrar el fichero de configuración. En este caso el desarrollador solucionó el typo. En otros casos, ciertos typos son personalizaciones del código del consumidor y pueden ser difíciles de encontrar por los vocabularios o reglas predefinidas. En el siguiente ejemplo. El desarrollador establece un “bean name” como string.

public static final String COOKIE_SELECTOR_BEAN_NAME = "com.amazon.um.webapp.config.CookiesSelectorService";

El detector de inconsistencia aprende de toda la base de código e identifica que um debería ser uam, ya que uam aparece en múltiples ocasiones a lo largo del código, haciendo el uso de um anormal. El desarrollador respondió “¡Genial!” por este hallazgo y lo solucionó.

Logging Inconsistente

En el ejemplo de mas abajo, el desarrollador está dejando en el log una excepción relativa a la falta de un campo, ID_FIELD.

log.info ("Missing {} from data {}", ID_FIELD, data);

El detector de inconsistencia aprende de patrones en el código del desarrollador e identifica que, en otros 47 casos similares de la base de código, el código del desarrollador usa un nivel de log error en lugar de info, como se muestra abajo.

log.error ("Missing {} from data {}", ID_FIELD, data);

Utilizar el nivel apropiado de log puede mejorar el proceso de depurado de las aplicaciones y asegura que los desarrolladores esten al tanto de todos los errores potenciales en sus aplicaciones. El mensaje de CodeGuru Reviewer fue: “The detector found 47 occurrences of code with logging similar to the selected lines. However, the logging level in the selected lines is not consistent with the other occurrences. We recommend you check if this is intentional. The following are some examples in your code of the expected level of logging in similar occurrences: log.error (“Missing {} from data {}”, ID_FIELD, data); …”

El desarrollador acepta la recomendación para hacer el nivel de log consistente e implementa el correspondiente cambio para aumentar el nivel de info a error.

Además de niveles de error, el detector de inconsistencia identifica fragmentos de log potencialmente perdidos y se asegura que el mensaje de log es consistente en todo el código base.

Declaración Inconsistente

El detector de inconsistencia también identifica inconsistencias en declaraciones de variables o métodos. Un ejemplo típico es olvidar una palabra reservada final para algunas funciones o variables. Mientras que una variable no siempre necesita ser declarada como final, la inconsistencia determina ciertas variables como final infiriéndolo de otras apariciones de variables. En el ejemplo de mas abajo, el desarrollador declara un método con la variable request.

protected ExecutionContext setUpExecutionContext(ActionRequest request) {

Sin conocimiento previo, la inconsistencia aprende de otras 9 ocurrencias similares del mismo código base y determina que request debe tener una palabra reservada final. El desarrollador responde diciendo “¡Buen trabajo!”, y ejecuta el cambio.

API ausente

La falta de una llamada API es un típico bug en el código, y su identificación es muy valorada por los clientes. El detector de inconsistencia puede encontrar errores a través del principio de inconsistencia. En el ejemplo siguiente, el desarrollador borra entidades obsoletas en el objeto document.

private void deleteEntities(Document document) {

...

for (Entity returnEntity : oldReturnCollection) {

document.deleteEntity(returnEntity);

}

}

El detector de inconsistencia aprende de otros 4 métodos similares en el código base del desarrollador y determina que la API document.deleteEntity() esta fuertemente asociada con otra API document.acceptChanges(), que habitualmente es llamada después de document.deleteEntity() con el fin de asegurar que el documento puede aceptar otros cambios, pero que en este caso no aparece. Por tanto el detector de inconsistencias recomienda añadir la llamada document.acceptChanges(). Otro revisor humano está de acuerdo con la sugerencia de CodeGuru. El desarrollador hace la corrección correspondiente como se muestra más abajo.

private void deleteEntities(Document document) {

...

for (Entity returnEntity : oldReturnCollection) {

document.deleteEntity(returnEntity);

}

document.acceptChanges();

}

Null Check

Consideremos el siguiente fragmento de código:

String ClientOverride = System.getProperty(MB_CLIENT_OVERRIDE_PROPERTY);

if (ClientOverride != null) {

log.info("Found Client override {}", ClientOverride);

config.setUnitTestOverride(ConfigKeys.Client, ClientOverride);

}

String ServerOverride = System.getProperty(MB_SERVER_OVERRIDE_PROPERTY);

if (ClientOverride != null) {

log.info("Found Server override {}", ServerOverride);

config.setUnitTestOverride(ConfigKeys.ServerMode, ServerOverride);

}

CodeGuru Reviewer indica que a la salida generada por System.getProperty(MB_SERVER_OVERRIDE_PROPERTY) le falta un null-check. Un null-check aparece en la siguiente línea. Sin embargo, si miramos detenidamente, ese null-check no está aplicado a la salida correcta. Es un error de copy-paste y en lugar de comprobar en ServerOverride lo hace para ClientOverride. El desarrollador arregla el código tal y como sugiere la recomendación.

Manejo de Excepciones

El siguiente ejemplo muestra una inconsistencia referente al manejo de excepciones.

Detección:

try {

if (null != PriceString) {

final Double Price = Double.parseDouble(PriceString);

if (Price.isNaN() || Price <= 0 || Price.isInfinite()) {

throw new InvalidParameterException(

ExceptionUtil.getExceptionMessageInvalidParam(PRICE_PARAM));

}

}

} catch (NumberFormatException ex) {

throw new InvalidParameterException(ExceptionUtil.getExceptionMessageInvalidParam(PRICE_PARAM));

}

Bloque de Código soportado:

try {

final Double spotPrice = Double.parseDouble(launchTemplateOverrides.getSpotPrice());

if (spotPrice.isNaN() || spotPrice <= 0 || spotPrice.isInfinite()) {

throw new InvalidSpotFleetRequestConfigException(

ExceptionUtil.getExceptionMessageInvalidParam(LAUNCH_TEMPLATE_OVERRIDE_SPOT_PRICE_PARAM));

}

} catch (NumberFormatException ex) {

throw new InvalidSpotFleetRequestConfigException(

ExceptionUtil.getExceptionMessageInvalidParam(LAUNCH_TEMPLATE_OVERRIDE_SPOT_PRICE_PARAM));

}

Hay 4 ocurrencias de manejo de excepciones arrojadas por Double.parseDouble usando catch con InvalidSpotFleetRequestConfigException(). El fragmento de código detectado con un contexto sorprendentemente similar simplemente usa InvalidParameterException(), el cual no ofrece tanto detalle como la excepción customizada. El desarrollador responde con “Vaya, la detección parece correcta” y ejecuta la revisión.

Respuestas a los hallazgos del Detector de Inconsistencias

Los repositorios grandes normalmente son desarrollados por más de un desarrollador al mismo tiempo. Por tanto, mantener la consistencia para el código que sirve al mismo propósito, así como el estilo de código, etc. puede ser pasado por alto fácilmente, especialmente para repositorios con meses o años de antigüedad. Como resultado, el detector de inconsistencias en CodeGuru Reviewer ha marcado numerosos hallazgos y alertado a los desarrolladores en Amazon de cientos de cuestiones de inconsistencia antes de que lleguen a producción.

El detector de inconsistencias ha alcanzado un alto índice de aceptación por los desarrolladores, con feedback y opiniones muy positivas tales como: “lo arreglaré. No me di cuenta ya que esto estaba copiado de otro sitio”, “Vaya, lo corregiré y usaré un formato consistente.”, “Interesante. Ahora se puede aprender de nuestro propio código. IA esta intentado quedarse con nuestros trabajos, jajaja”, y “si!, es sorprendente”. Así mismo, los desarrolladores también están de acuerdo en que los hallazgos encontrados son esenciales y deben ser solucionados.

Conclusión

Los errores de inconsistencia son difíciles de detectar o replicar durante la fase de testeo y pueden impactar la disponibilidad de servicios en producción. Por eso, es importante detectar estos errores automáticamente y lo antes posible dentro del ciclo de vida del desarrollo de software, por ejemplo, en pull requests o escaneos de código. El detector de inconsistencias de CodeGuru Reviewer combina algoritmos de análisis estático de código con Machine Learning y minado de datos para resaltar y sugerir solo las desviaciones con alta fiabilidad. Tiene una alta aceptación y ha alertado a los desarrolladores de Amazon de cientos de inconsistencias antes de alcanzar producción.


Este blogpost es una traducción por Juan Pablo Denegri (Head of Enterprise Solutions Architectture, Iberia) del original en Inglés.