Análise estática do IntelliJ IDEA versus mente humana

Há pouco tempo, estudei a saída do analisador estático IntelliJ IDEA para código Java e deparei-me com um caso interessante. Como o fragmento de código correspondente não é de código aberto, eu o anonimizei e o desamarrei das dependências externas. Assumimos que ele se parecia com isso:


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; } 

O código é como código, algo está sendo transformado, algo está sendo feito, mas o analisador estático não gostou. Aqui vemos dois avisos:


Captura de tela do código


Na expressão res.isEmpty() IDE diz que a condição é sempre verdadeira e, cur que a atribuição não tem sentido, pois o mesmo valor já está nessa variável. É fácil ver que o problema de atribuição é uma conseqüência direta do primeiro problema. Se res.isEmpty() realmente sempre verdadeiro, a sequência será reduzida para


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

Isso é realmente supérfluo. Mas por que a expressão é sempre verdadeira? Afinal, res é uma lista, é preenchida no mesmo ciclo. O número de iterações do loop e em quais ramificações entramos dependem dos parâmetros de entrada que o IDE não pode conhecer. Poderíamos adicionar um elemento para res na iteração anterior e, em seguida, a lista não ficará vazia.


Vi esse código pela primeira vez e passei muito tempo lidando com esse caso. No começo, eu estava quase convencido de que encontrei um bug no analisador e teria que corrigi-lo. Vamos ver se é assim.


Primeiro, marcaremos todas as linhas em que o estado do método muda. Esta é uma alteração na variável cur ou uma alteração na 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; } 

As linhas 'A' e 'B' ( 'B' é o primeiro ramo da instrução condicional) alteram a variável cur , 'D' altera a lista e 'C' (o segundo ramo da instrução condicional) altera a lista e a variável cur . É importante para nós se cur -1 está e se a lista está vazia. Ou seja, você precisa monitorar quatro estados:



A string 'A' muda cur se houver -1 antes disso. E não sabemos se o resultado será -1 ou não. Portanto, duas opções são possíveis:



A string 'B' também funciona apenas se cur for -1 . Ao mesmo tempo, como já vimos, em princípio, ela não faz nada. Mas notamos, no entanto, esta nervura para completar o quadro:



A string 'C' , como as anteriores, trabalha com cur == -1 e a altera arbitrariamente (como 'A' ). Mas, ao mesmo tempo, ele ainda pode transformar as res não vazias da lista em vazias ou deixar vazias se houver mais de um elemento.



Por fim, a string 'D' aumenta o tamanho da lista: pode ficar vazia para não-vazia ou aumentar não-vazia. Não pode transformar não vazio em vazio:



O que isso nos dá? Nada mesmo. É completamente incompreensível que a condição res.isEmpty() sempre verdadeira.


De fato, começamos errado. Nesse caso, não é suficiente monitorar o estado de cada variável separadamente. Aqui os estados correlatos desempenham um papel importante. Felizmente, devido ao fato de 2+2 = 2*2 , também temos apenas quatro deles:



Com uma borda dupla, marquei o estado inicial que temos ao inserir o método. Bem, tente novamente. 'A' altera ou salva cur para qualquer res , res não muda:



'B' funciona apenas com cur == -1 && res.isEmpty() e não faz nada. Adicionar:



'C' funciona apenas com cur == -1 && !res.isEmpty() . Ao mesmo tempo, cur e res mudam arbitrariamente: depois de 'C' , terminamos em qualquer estado:



Finalmente, 'D' pode começar em cur != -1 && res.isEmpty() e tornar a lista não vazia, ou começar em cur != -1 && !res.isEmpty() e permanecer lá:



À primeira vista, parece que só piorou: o gráfico ficou mais complicado e não está claro como usá-lo. Mas, de fato, estamos perto de uma solução. As setas agora mostram todo o fluxo possível de execução do nosso método. Como sabemos de que estado começamos, vamos dar um passeio pelas setas:



E aqui uma coisa muito interessante é revelada. Não podemos chegar ao canto inferior esquerdo. E como não podemos entrar nisso, isso significa que não podemos andar com nenhuma das setas 'C' . Ou seja, a linha 'C' realmente inatingível e o 'B' pode ser executado. Isso só é possível se a condição res.isEmpty() realmente sempre verdadeira! A IntelliJ IDEA está completamente correta. Desculpe, analisador, em vão eu pensei que você era buggy. Você é tão inteligente que é difícil para mim, uma pessoa comum, alcançá-lo.


Nosso analisador não possui nenhuma tecnologia "hype" de inteligência artificial, mas usa as abordagens de análise de fluxo de controle e análise de fluxo de dados, que têm menos de meio século. No entanto, ele às vezes tira conclusões não triviais. No entanto, isso é compreensível: por um longo tempo, é melhor criar gráficos e andar neles com máquinas do que com pessoas. Há um importante problema não resolvido: não basta dizer a uma pessoa que ela tem um erro no programa. O cérebro de silício deve explicar ao biológico por que o decidiu e para que o cérebro biológico entenda. Se alguém tiver idéias brilhantes sobre como fazer isso, terei prazer em ouvir de você. Se você estiver pronto para realizar suas idéias, nossa equipe não se recusará a cooperar com você!


Um dos testes de aceitação está à sua frente: para este exemplo, uma explicação deve ser gerada automaticamente. Pode ser um texto, um gráfico, uma árvore, uma imagem com selos - qualquer coisa, se apenas uma pessoa pudesse entender.


A questão permanece em aberto, o que o autor do método quis dizer e como o código deveria ser realmente. Os responsáveis ​​pelo subsistema me informaram que essa parte está um pouco abandonada e eles mesmos não sabem como consertá-la ou é melhor removê-la completamente.

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


All Articles