IntelliJ IDEA Análisis estático vs mente humana

No hace mucho tiempo, estudié el resultado del analizador estático IntelliJ IDEA para código Java y encontré un caso interesante. Como el fragmento de código correspondiente no es de código abierto, lo anonimé y lo desaté de las dependencias externas. Suponemos que se veía así:


private static List<Integer> process(Map<String, Integer> options, List<String> inputs) { List<Integer> res = new ArrayList<>(); int cur = -1; for (String str : inputs) { if (str.startsWith("-")) if (options.containsKey(str)) { if (cur == -1) cur = options.get(str); } else if (options.containsKey("+" + str)) { if (cur == -1) cur = res.isEmpty() ? -1 : res.remove(res.size() - 1); if (cur != -1) res.add(cur + str.length()); } } return res; } 

El código es como el código, se está transformando algo, se está haciendo algo, pero al analizador estático no le gustó. Aquí vemos dos advertencias:


Captura de pantalla de código


En la expresión res.isEmpty() IDE dice que la condición siempre es verdadera, y en cur que la asignación no tiene sentido, ya que el mismo valor ya se encuentra en esta variable. Es fácil ver que el problema de asignación es una consecuencia directa del primer problema. Si res.isEmpty() realmente siempre cierto, entonces la cadena se reduce a


 if (cur == -1) cur = -1; 

Esto es de hecho superfluo. Pero, ¿por qué la expresión siempre es verdadera? Después de todo, res es una lista, se completa en el mismo ciclo. El número de iteraciones del bucle y en qué ramas entramos depende de los parámetros de entrada que el IDE no puede conocer. Podríamos agregar un elemento a res en la iteración anterior, y luego la lista no estará vacía.


Vi este código por primera vez y pasé mucho tiempo para tratar este caso. Al principio, estaba casi convencido de que me encontré con un error en el analizador y que tendría que solucionarlo. A ver si esto es así.


Primero, marcaremos todas las líneas donde cambia el estado del método. Este es un cambio en la variable cur o un cambio en la lista res :


 private static List<Integer> process(Map<String, Integer> options, List<String> inputs) { List<Integer> res = new ArrayList<>(); int cur = -1; for (String str : inputs) { if (str.startsWith("-")) if (options.containsKey(str)) { if (cur == -1) cur = options.get(str); // A } else if (options.containsKey("+" + str)) { if (cur == -1) cur = res.isEmpty() ? -1 : // B res.remove(res.size() - 1); // C if (cur != -1) res.add(cur + str.length()); // D } } return res; } 

Las líneas 'A' y 'B' ( 'B' es la primera rama de la declaración condicional) cambian la variable cur , 'D' cambia la lista y 'C' (la segunda rama de la declaración condicional) cambia tanto la lista como la variable cur . Nos importa si cur -1 se encuentra y si la lista está vacía. Es decir, debe monitorear cuatro estados:



La cadena 'A' cambia cur si hubo -1 antes de eso. Y no sabemos si el resultado será -1 o no. Por lo tanto, hay dos opciones posibles:



La cadena 'B' también funciona solo si cur es -1 . Al mismo tiempo, como ya hemos notado, en principio, ella no hace nada. Pero notamos, sin embargo, esta costilla para completar la imagen:



La cadena 'C' , como las anteriores, funciona con cur == -1 y la cambia arbitrariamente (como 'A' ). Pero al mismo tiempo, todavía puede convertir las res lista no vacías en vacías, o dejarlas no vacías si hubiera más de un elemento.



Finalmente, la cadena 'D' aumenta el tamaño de la lista: puede volverse vacía a no vacía, o aumentar no vacía. No puede convertir nada vacío en vacío:



¿Qué nos da esto? Nada en absoluto Es completamente incomprensible por qué la condición res.isEmpty() siempre res.isEmpty() verdadera.


De hecho, empezamos mal. En este caso, no es suficiente monitorear el estado de cada variable por separado. Aquí los estados correlacionados juegan un papel importante. Afortunadamente, debido al hecho de que 2+2 = 2*2 , también tenemos solo cuatro de ellos:



Con un borde doble, marqué el estado inicial que tenemos al ingresar al método. Bueno, inténtalo de nuevo. 'A' cambia o guarda el cur para cualquier res , la res no cambia:



'B' solo funciona con cur == -1 && res.isEmpty() y no hace nada. Añadir:



'C' solo funciona con cur == -1 && !res.isEmpty() . Al mismo tiempo, tanto cur como res cambian arbitrariamente: después de 'C' terminamos en cualquier estado:



Finalmente, 'D' puede comenzar en cur != -1 && res.isEmpty() y hacer que la lista no esté vacía, o comenzar en cur != -1 && !res.isEmpty() y permanecer allí:



A primera vista, parece que solo ha empeorado: el gráfico se ha vuelto más complicado y no está claro cómo usarlo. Pero, de hecho, estamos cerca de una solución. Las flechas ahora muestran todo el flujo posible de ejecución de nuestro método. Como sabemos desde qué estado comenzamos, demos un paseo por las flechas:



Y aquí se revela algo muy interesante. No podemos llegar a la esquina inferior izquierda. Y como no podemos entrar en eso, eso significa que no podemos caminar a lo largo de ninguna de las flechas 'C' . Es decir, la línea 'C' realmente inalcanzable y la 'B' puede ejecutarse. ¡Esto solo es posible si la condición res.isEmpty() siempre cierta! La IntelliJ IDEA es completamente correcta. Lo siento, analizador, en vano pensé que estabas con errores. Eres tan inteligente que es difícil para mí, una persona común, ponerte al día contigo.


Nuestro analizador no tiene ninguna tecnología de inteligencia artificial "exagerada", pero utiliza los enfoques de análisis de flujo de control y análisis de flujo de datos, que tienen no menos de medio siglo de antigüedad. Sin embargo, a veces saca conclusiones muy triviales. Sin embargo, esto es comprensible: durante mucho tiempo es mejor construir gráficos y caminar sobre ellos con máquinas que con personas. Hay un problema importante sin resolver: no basta con decirle a una persona que tiene un error de programa. El cerebro de silicio debe explicarle al biológico por qué lo decidió, y para que el cerebro biológico lo entienda. Si alguien tiene ideas brillantes sobre cómo hacer esto, me alegrará saber de usted. ¡Si está listo para realizar sus ideas usted mismo, nuestro equipo no se negará a cooperar con usted!


Una de las pruebas de aceptación está ante usted: para este ejemplo, se debe generar una explicación automáticamente. Puede ser un texto, un gráfico, un árbol, una imagen con sellos, cualquier cosa, si solo una persona pudiera entender.


La pregunta sigue abierta, qué significa el autor del método y cómo debería ser el código. Los responsables del subsistema me informaron que esta parte está algo abandonada, y ellos mismos no saben cómo solucionarlo o es mejor eliminarlo por completo.

Source: https://habr.com/ru/post/472932/


All Articles