@@ -669,6 +669,11 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
669669 LogStream : TStreamWriter;
670670 Watch : TStopwatch;
671671 ElapsedMs : Int64;
672+ // Phase-Tracking fuer Crash-Diagnose. Wird an Schluesselstellen aktualisiert
673+ // und im Outer-Handler bei "Analyseabbruch: ..." mit-geloggt. Variable-
674+ // Assignment ist quasi-kostenlos; kein Logging im Happy-Path.
675+ LastPhase : string;
676+ LastFile : string;
672677
673678 procedure LogLine (const S: string);
674679 begin
@@ -692,6 +697,8 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
692697 // Best-effort: kein ConfigDir = kein Log, der Scan laeuft trotzdem.
693698 end ;
694699 Parser := nil ;
700+ LastPhase := ' init' ;
701+ LastFile := ' ' ;
695702 // Eine gemeinsame try-finally klammer fuer LogStream UND Parser - so leakt
696703 // weder bei Parser-Create-OOM der LogStream noch umgekehrt.
697704 try
@@ -704,6 +711,12 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
704711 LogStream := nil ;
705712 end ;
706713
714+ // Outer-Diagnose-Try: bei Exception VOR re-raise die letzte Phase + das
715+ // aktuelle File ins Log schreiben. Sonst sieht der Caller-Outer-Handler
716+ // nur "Analyseabbruch: <Message>" ohne Kontext.
717+ try
718+
719+
707720 // AST-File-Cache: pro .pas einmal parsen, von beiden Pre-Indizes UND
708721 // dem Main-Loop wiederverwendet. Spart 2 von 3 Parser-Durchlaeufen pro
709722 // File (perf_analyse.md Hot-Spot 🅐).
@@ -731,6 +744,7 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
731744 var IndexFiles: TStringList := IndexFileList;
732745 if IndexFiles = nil then IndexFiles := FileList;
733746
747+ LastPhase := ' Pre-Index: DfmRepoIndex.Build' ;
734748 gDfmRepoIndex := TDfmRepoIndex.Create;
735749 try
736750 gDfmRepoIndex.Build(IndexFiles);
@@ -744,20 +758,24 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
744758 // hier dennoch aufgebaut weil andere Konsumenten (Tests, mORMot-Cross-
745759 // Check) ihn lesen; kann perspektivisch entfallen wenn keiner mehr
746760 // referenziert.
761+ LastPhase := ' Pre-Index: SymbolRefIndex.Build' ;
747762 gSymbolRefIndex := TSymbolReferenceIndex.Create;
748763 try
749764 gSymbolRefIndex.Build(IndexFiles);
750765 except
751766 FreeAndNil(gSymbolRefIndex);
752767 end ;
753768
769+ LastPhase := ' Parser.Create + Main-Loop start' ;
754770 Parser := TParser2.Create;
755771 Total := FileList.Count;
756772 for i := 0 to Total - 1 do
757773 begin
758774 SafeProgress(i + 1 , Total);
759775
760776 FileName := FileList[i];
777+ LastFile := FileName;
778+ LastPhase := Format(' File %d/%d: pre-check' , [i + 1 , Total]);
761779
762780 // Leerer Dateiname → ignorieren (defensiv)
763781 if Trim(FileName) = ' ' then
@@ -811,6 +829,7 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
811829 // OwnsRoot=False wenn der Cache das Root besitzt (er freet es bei
812830 // Evict). True wenn lokal geparst -> nach AST-Verarbeitung selbst free.
813831 var OwnsRoot := False;
832+ LastPhase := Format(' File %d/%d: Parse/Acquire' , [i + 1 , Total]);
814833 try
815834 try
816835 // Cache-Pfad bevorzugen: Pre-Indizes haben das Root schon erzeugt.
@@ -891,6 +910,7 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
891910 // Beide Gruppen respektieren LeakyClassExcludes. Die INI bleibt
892911 // unangetastet; der User entscheidet handisch welche Klasse er in
893912 // [Detectors] LeakyClasses uebernimmt.
913+ LastPhase := Format(' File %d/%d: AutoDiscovery' , [i + 1 , Total]);
894914 if AutoDiscoverCustomClasses then
895915 begin
896916 var Instantiable : TArray<string>;
@@ -922,9 +942,11 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
922942 // Detektoren - so liegen sie im Output sortierbar zusammen.
923943 // No-op wenn TCustomRuleDetector.LoadFromYaml nicht aufgerufen
924944 // wurde (HasRules = False).
945+ LastPhase := Format(' File %d/%d: CustomRules' , [i + 1 , Total]);
925946 if TCustomRuleDetector.HasRules then
926947 TCustomRuleDetector.AnalyzeFile(FileName, Results);
927948
949+ LastPhase := Format(' File %d/%d: RunAllDetectors' , [i + 1 , Total]);
928950 RunAllDetectors(Root, FileName, Results, AIncludeUsesCheck,
929951 procedure(const Name : string; ElapsedMs: Int64)
930952 var
@@ -968,16 +990,42 @@ class procedure TStaticAnalyzer2.ParseLeaks(FileList: TStringList;
968990 CaptResults.Add(F);
969991 end );
970992 finally
993+ LastPhase := Format(' File %d/%d: finally Root.Free/Evict' , [i + 1 , Total]);
971994 if OwnsRoot then
972995 Root.Free
973996 else if Assigned(gAstFileCache) then
974997 gAstFileCache.Evict(FileName); // Memory-Peak bremsen
998+ LastPhase := Format(' File %d/%d: finally gFileTextCache.Clear' , [i + 1 , Total]);
975999 // Text-Cache fuer die abgearbeitete Datei freigeben - die File-Scan-
9761000 // Detektoren haben sie konsumiert, niemand brauchts mehr.
9771001 if Assigned(gFileTextCache) then
9781002 gFileTextCache.Clear;
9791003 end ;
9801004 end ;
1005+ LastPhase := ' Main-Loop fertig, post-process' ;
1006+
1007+ // Outer-Diagnose-Try Schliessung: bei Exception VOR re-raise die letzte
1008+ // Phase + das aktuelle File ins Log schreiben. So sieht der Caller-Outer-
1009+ // Handler (AnalyzeLeaksRecursive etc.) statt nur "Analyseabbruch: <msg>"
1010+ // im scan.log einen klaren Trail "letzte Phase: ... aktuelles File: ...".
1011+ except
1012+ on EAbort do raise;
1013+ on E: Exception do
1014+ begin
1015+ LogLine(' ' );
1016+ LogLine(Format(' === ABBRUCH: %s: %s ===' ,
1017+ [E.ClassName, E.Message]));
1018+ LogLine(Format(' === letzte Phase: %s ===' , [LastPhase]));
1019+ if LastFile <> ' ' then
1020+ LogLine(Format(' === aktuelles File: %s ===' , [LastFile]))
1021+ else
1022+ LogLine(' === aktuelles File: (n/a - Crash vor Main-Loop) ===' );
1023+ // Re-raise damit der Caller-Outer-Handler sein "Analyseabbruch: ..."
1024+ // SARIF/UI-Finding wie bisher anlegt. Im scan.log steht jetzt der
1025+ // Kontext direkt vor dieser Meldung.
1026+ raise;
1027+ end ;
1028+ end ;
9811029 finally
9821030 // LogStream auch bei EAbort sauber schliessen, danach Parser.
9831031 // Eine gemeinsame Klammer verhindert dass Parser-Create-OOM den LogStream
0 commit comments