Vor nicht allzu langer Zeit habe ich die Ausgabe des IntelliJ IDEA Static Analyzer für Java-Code untersucht und bin auf einen interessanten Fall gestoßen. Da das entsprechende Codefragment nicht Open Source ist, habe ich es anonymisiert und von externen Abhängigkeiten gelöst. Wir nehmen an, dass er so aussah:
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; }
Der Code ist wie Code, etwas wird transformiert, etwas wird getan, aber der statische Analysator mochte es nicht. Hier sehen wir zwei Warnungen:

Im Ausdruck res.isEmpty()
sagt res.isEmpty()
IDE, dass die Bedingung immer wahr ist, und im cur
die Zuweisung bedeutungslos zu sein, da derselbe Wert bereits in dieser Variablen liegt. Es ist leicht zu erkennen, dass das Zuordnungsproblem eine direkte Folge des ersten Problems ist. Wenn res.isEmpty()
wirklich immer wahr ist, wird die Zeichenfolge auf reduziert
if (cur == -1) cur = -1;
Das ist in der Tat überflüssig. Aber warum ist der Ausdruck immer wahr? Immerhin ist res
eine Liste, die im selben Zyklus gefüllt wird. Die Anzahl der Iterationen der Schleife und die Verzweigungen, in die wir gehen, hängen von den Eingabeparametern ab, die die IDE nicht kennen kann. Wir könnten ein Element hinzufügen, das bei der vorherigen Iteration res
, und dann wird die Liste nicht leer sein.
Ich habe diesen Code zum ersten Mal gesehen und viel Zeit damit verbracht, mich mit diesem Fall zu befassen. Zuerst war ich fast davon überzeugt, dass ich auf einen Fehler im Analysegerät gestoßen bin, und ich musste ihn beheben. Mal sehen, ob das so ist.
Zunächst markieren wir alle Zeilen, in denen sich der Status der Methode ändert. Dies ist eine Änderung der Variablen cur
oder eine Änderung der res
Liste:
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);
Die Zeilen 'A'
und 'B'
( 'B'
ist der erste Zweig der bedingten Anweisung) ändern die Variable cur
, 'D'
ändert die Liste und 'C'
(der zweite Zweig der bedingten Anweisung) ändert sowohl die Liste als auch die Variable cur
. Es ist uns wichtig, ob cur
-1 darin liegt und ob die Liste leer ist. Das heißt, Sie müssen vier Zustände überwachen:

String 'A'
ändert cur
wenn vorher -1
war. Und wir wissen nicht, ob das Ergebnis -1
oder nicht. Daher sind zwei Optionen möglich:

String 'B'
funktioniert auch nur, wenn cur
-1
. Gleichzeitig tut sie, wie wir bereits bemerkt haben, im Prinzip nichts. Wir beachten aber trotzdem diese Rippe, um das Bild zu vervollständigen:

Die Zeichenfolge 'C'
arbeitet wie die vorherigen mit cur == -1
und ändert sie willkürlich (wie 'A'
). Gleichzeitig kann es jedoch die nicht leere Liste res
in leer res
oder nicht leer lassen, wenn mehr als ein Element vorhanden ist.

Schließlich vergrößert die Zeichenfolge 'D'
die Liste: Sie kann leer zu nicht leer oder nicht leer werden. Es kann nicht leer zu leer machen:

Was gibt uns das? Gar nichts. Es ist völlig unverständlich, warum die Bedingung res.isEmpty()
immer wahr ist.
Tatsächlich haben wir falsch angefangen. In diesem Fall reicht es nicht aus, den Status jeder Variablen separat zu überwachen. Hier spielen korrelierte Zustände eine wichtige Rolle. Glücklicherweise haben wir aufgrund der Tatsache, dass 2+2 = 2*2
, auch nur vier davon:

Mit einem doppelten Rand habe ich den Ausgangszustand markiert, den wir bei der Eingabe der Methode haben. Versuchen Sie es erneut. 'A'
ändert oder speichert cur
für res
, res
ändert sich nicht:

'B'
funktioniert nur mit cur == -1 && res.isEmpty()
und macht nichts. Hinzufügen:

'C'
funktioniert nur mit cur == -1 && !res.isEmpty()
. Gleichzeitig ändern sich cur
und res
willkürlich: Nach 'C'
landen wir in einem beliebigen Zustand:

Schließlich kann 'D'
in cur != -1 && res.isEmpty()
und die Liste nicht leer machen oder in cur != -1 && !res.isEmpty()
und dort bleiben:

Auf den ersten Blick scheint es nur noch schlimmer geworden zu sein: Das Diagramm ist komplizierter geworden, und es ist nicht klar, wie man es verwendet. Tatsächlich stehen wir jedoch einer Lösung nahe. Die Pfeile zeigen nun den gesamten möglichen Ablauf der Ausführung unserer Methode. Da wir wissen, von welchem Zustand wir angefangen haben, machen wir einen Spaziergang entlang der Pfeile:

Und hier wird eine sehr interessante Sache offenbart. Wir können nicht in die untere linke Ecke gelangen. Und da wir nicht darauf eingehen können, können wir keinen der Pfeile 'C'
entlang gehen. Das heißt, die Zeile 'C'
wirklich nicht erreichbar und das 'B'
kann ausgeführt werden. Dies ist nur möglich, wenn die Bedingung res.isEmpty()
tatsächlich immer wahr ist! Die IntelliJ IDEA ist völlig korrekt. Entschuldigung, Analysator, vergebens dachte ich, Sie wären fehlerhaft. Du bist einfach so schlau, dass es für mich, einen normalen Menschen, schwierig ist, dich einzuholen.
Unser Analysegerät verfügt über keine „Hype“ -Technologien für künstliche Intelligenz, sondern verwendet die Ansätze der Kontrollflussanalyse und Datenflussanalyse, die nicht weniger als ein halbes Jahrhundert alt sind. Trotzdem zieht er manchmal sehr nicht triviale Schlussfolgerungen. Dies ist jedoch verständlich: Lange Zeit ist es besser, Diagramme zu erstellen und mit Maschinen darauf zu laufen als mit Menschen. Es gibt ein wichtiges ungelöstes Problem: Es reicht nicht aus, nur einer Person mitzuteilen, dass sie einen Programmfehler hat. Das Siliziumgehirn muss dem Biologischen erklären, warum es dies beschlossen hat und damit das biologische Gehirn es versteht. Wenn jemand brillante Ideen dazu hat, würde ich mich freuen, von Ihnen zu hören. Wenn Sie bereit sind, Ihre Ideen selbst umzusetzen, wird unser Team die Zusammenarbeit mit Ihnen nicht ablehnen!
Einer der Abnahmetests liegt vor Ihnen: Für dieses Beispiel sollte automatisch eine Erklärung generiert werden. Es kann ein Text, eine Grafik, ein Baum, ein Bild mit Siegeln sein - alles, was nur eine Person verstehen könnte.
Die Frage bleibt offen, was der Autor der Methode gemeint hat und wie der Code eigentlich aussehen soll. Die Verantwortlichen des Subsystems haben mir mitgeteilt, dass dieser Teil etwas aufgegeben ist, und sie selbst wissen nicht, wie sie ihn reparieren sollen, oder es ist besser, ihn insgesamt zu entfernen.