Video Summary and Transcription
Tibor Blanesy de Sonar presenta técnicas avanzadas para el linting con ESLint, incluyendo el uso de ControlFlowGraph para detectar errores en el código. El algoritmo se basa en el análisis de vivacidad, que identifica las variables vivas en cualquier punto del programa. Además, la charla cubre el cálculo de conjuntos de bloques utilizando la diferencia entre el conjunto de salida y el conjunto de eliminación unido con genset.
1. Técnicas Avanzadas para Linting con ESLint
¡Hola! Mi nombre es Tibor Blanesy, trabajo en el análisis estático de JavaScript en Sonar, y en esta charla me gustaría mostrarte algunas técnicas más avanzadas para el linting con ESLint. Echemos un vistazo a una función que devuelve un rango de números entre dos valores pasados como argumento. Si no se proporciona el argumento, asumirá que el rango debe comenzar desde cero. Utilizaremos una representación del código llamada ControlFlowGraph para detectar errores en el código. La base del algoritmo es el análisis de vivacidad, que nos dice qué variables están vivas en cualquier punto del programa.
Mi nombre es Tibor Blanesy, trabajo en el análisis estático de JavaScript en Sonar, y en esta charla me gustaría mostrarte algunas técnicas más avanzadas para el linting con ESLint. Echemos un vistazo a la siguiente función, que encontré en el código base de VS Code. Esta función devuelve un rango de números entre dos valores pasados como argumento.
Si no se proporciona el argumento, asumirá que el rango debe comenzar desde cero. Cuando utilizamos herramientas de análisis estático, como SonarQube, nos mostrará rápidamente que hay un problema con este código. Por alguna razón, el valor asignado a la variable 'from' nunca se utiliza más adelante en el código. La lógica que maneja los argumentos está duplicada.
Este tipo de errores, cuando el valor asignado a la variable no se utiliza, se llama un almacenamiento muerto. SonarQube proporciona la siguiente explicación de por qué esto es un problema. Un almacenamiento muerto ocurre cuando una variable local se le asigna un valor que no es leído por ninguna instrucción posterior. Calcular o recuperar un valor solo para luego sobrescribirlo o desecharlo podría indicar un error grave en el código. Incluso si no es un problema, es como mínimo un desperdicio de recursos. Por lo tanto, se deben utilizar valores sobrecalculados. En los próximos minutos intentaré explicar cómo se pueden detectar este tipo de errores con el análisis estático.
Primero, utilizaremos una representación del código llamada ControlFlowGraph. En esta representación, un nodo llamado bloques básicos contiene solo declaraciones que se ejecutan secuencialmente. Los saltos se representan como flechas entre los bloques. Así que aquí tenemos un ControlFlowGraph para la función que mostré anteriormente. Solo mostraremos una parte del grafo, que es relevante para el problema, para mantenerlo pequeño. En la siguiente diapositiva, tengo el mismo ControlFlowGraph anotado en rojo, con eventos que son proporcionados por ESLint cuando escribimos una regla personalizada. La API de ESLint proporciona dos objetos, CodePath, que representa el flujo de control de toda la función, y ControlPathSegment para cada bloque básico. Luego, ESLint dispara eventos para el inicio y fin de CodePath, y para el inicio y fin de cada bloque básico, que es un CodePathSegment.
Entonces, en el código, lo que escribiremos es el siguiente objeto, que contiene un controlador de eventos para los eventos de CodePath. No tenemos tiempo para entrar en detalles de implementación, pero en las siguientes diapositivas, describiré rápidamente los conceptos básicos del algoritmo. La base del algoritmo es el análisis de vivacidad, que nos dice qué variables están vivas en cualquier punto del programa. La variable está viva cuando el valor que contiene podría ser necesario en el futuro. Para cada bloque básico, calcularemos cuatro conjuntos de variables. El conjunto de inicio con las variables que se leen en el bloque básico, el conjunto de eliminación, que contiene las variables que se escriben en el bloque básico, el conjunto de entrada con las variables que están vivas al comienzo del bloque, y el conjunto de salida con las variables que están vivas al final del bloque. Para calcular estos conjuntos, utilizaremos las siguientes dos reglas. El conjunto de salida del bloque actual es la unión de todos los conjuntos de entrada de sus sucesores.
2. Cálculo de Conjuntos de Bloques
El conjunto de entrada del bloque actual es la diferencia entre el conjunto de salida y el conjunto de eliminación, unido con genset. Calcularemos los valores de estos conjuntos comenzando desde el fondo del grafo y moviéndonos hacia los predecesores de cada bloque.
El conjunto de entrada del bloque actual es la diferencia entre el conjunto de salida y el conjunto de eliminación, unido con genset. Ahora pasaré por los bloques básicos de la función que mostré anteriormente, y calcularemos los valores de estos conjuntos. Así que comenzaremos desde el fondo del grafo para calcular los conjuntos de este bloque básico. Así que asumiremos que 'from' y 'to' se leen más adelante en la función, por lo que genset se establece en 'from' y 'to', y ignoraremos que hay algo escrito, por lo que el conjunto de eliminación estará vacío. A partir de esto, podemos calcular el conjunto de entrada como 'from' y 'to', y ahora nos moveremos a los predecesores de este bloque. Como el conjunto de entrada del bloque sucesor es 'from' y 'to', podemos decir que el conjunto de salida también es 'from' y 'to', siguiendo la primera regla. En este bloque básico, estamos leyendo el valor de 'arc', por lo que genset es 'arc' y estamos escribiendo los valores de 'from' y 'to', por lo que el conjunto de eliminación es 'from' y 'to'. A partir de esto, podemos calcular el conjunto de entrada como la diferencia entre el conjunto de salida y el conjunto de eliminación. Como estos conjuntos son iguales, la diferencia será vacía. El conjunto de entrada es la unión de vacío con genset, que contiene 'arc'. Por lo tanto, el conjunto de entrada será 'arc'. Ahora nos movemos a otro bloque donde nuevamente el conjunto de salida se establece como el conjunto de entrada del bloque sucesor, por lo que es 'from' y 'to'. Estamos leyendo el valor de 'arc', por lo que genset es 'arc' y estamos escribiendo el valor de 'from'. A partir de esto, podemos calcular que el conjunto de entrada es 'arc' y 'to'. Cuando nos movemos al predecesor de estos dos bloques, podemos establecer el conjunto de salida como la unión del conjunto de entrada de los bloques sucesores, por lo que es 'arc' y 'to'. Estamos leyendo el valor de 'to', por lo que genset es 'to' y no estamos escribiendo ninguna variable, por lo que el conjunto de eliminación está vacío. A partir de esto, podemos calcular el conjunto de entrada como la diferencia entre el conjunto de salida y el conjunto de eliminación, que es 'arc' y 'to', y unirlo con 'to', lo cual no cambia nada. A partir de aquí, nos movemos al predecesor de este bloque, por lo que sabemos que el conjunto de salida es 'arc' y 'to', estamos leyendo el valor de 'arc', por lo que genset es 'arc', estamos escribiendo el valor de 'from', por lo que el conjunto de eliminación contiene 'from', y a partir de esto podemos calcular el conjunto de entrada como 'arc' y 'to'. En el siguiente bloque, la situación es casi idéntica, el conjunto de salida es 'arc' y 'to', estamos escribiendo el valor de 'from', por lo que el conjunto de eliminación es 'from', no estamos leyendo ninguna variable, por lo que genset está vacío. A partir de esto, calculamos el conjunto de entrada como 'arc' y 'to'. Y ahora nos movemos al primer bloque donde el conjunto de salida es 'arc' y 'to', estamos leyendo el valor de 'to', por lo que genset es 'to', no estamos escribiendo ninguna variable, por lo que el conjunto de eliminación está vacío, por lo tanto, podemos calcular el conjunto de entrada como 'arc' y 'to'. Así que ahora hemos calculado los valores de estos bloques básicos. Cada vez que calculamos el conjunto de entrada del bloque, necesitamos recalcular los predecesores de los bloques. Esto es especialmente importante si hay bucles en el grafo, porque necesitaremos continuar hasta que los conjuntos no cambien más. Una vez que esto esté terminado, podemos analizar los valores calculados y detectar los problemas. Generaremos un problema para cada variable que forma parte del conjunto de eliminación, lo que significa que se está escribiendo, pero no forma parte del conjunto de salida del bloque, lo que significa que el valor nunca se necesita en los bloques posteriores. Por lo tanto, en este caso, en nuestra función, 'from()' se escribe al principio, pero no se lee en ningún bloque posterior porque se vuelve a escribir. Por eso generaremos un problema en esta llamada a 'from()'. Si quieres, puedes echar un vistazo a la implementación completa de este algoritmo en nuestro analizador de JavaScript, que es un proyecto de código abierto en GitHub, y el algoritmo se encuentra en la regla 18.54. Este analizador se utiliza en SonarQube, SonarCloud o SonarLink. Espero que hayas encontrado interesante esta charla, y gracias por tu atención. ¡Adiós!
Comments