{ *************************************************************************** * * * This source is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This code is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * General Public License for more details. * * * * A copy of the GNU General Public License is available on the World * * Wide Web at . You can also * * obtain it by writing to the Free Software Foundation, * * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * * *************************************************************************** Author: Mattias Gaertner Abstract: Parsing fpc message files. FPC prints message IDs with -vq } (* For example: general_t_compilername=01000_T_Compiler: $1 % When the \var{-vt} switch is used, this line tells you what compiler % is used. __=__ *) unit CodeToolsFPCMsgs; {$mode objfpc}{$H+} {off $DEFINE VerboseFPCMsgFile} interface uses Classes, SysUtils, FileProcs, AVL_Tree; type TfmiSpecialItem = ( fmisiNone, fmisiFatal, fmisiError, fmisiWarning, fmisiNote, fmisiHint ); TfmiSpecialItems = set of TfmiSpecialItem; { TFPCMsgItem } TFPCMsgItem = class public Part: string; // e.g. 'general', 'unit', 'link' Typ: string; // e.g. 'f','e','w','n','h','i','l','u','t','c','d','x','o' TxtIdentifier: string; // identifier ID: integer; // positive number ShownTyp: string; // e.g. shown Typ, can be different from Typ Pattern: string; // Text with placeholders $1 .. $9 PatternEndSpace: string; Comment: string; // multi line Index: integer; // index in list function GetName(WithID: boolean = true): string; function PatternFits(aMsg: string): integer; // >=0 fits function GetTrimmedComment(NoLineBreaks, NoLatex: boolean): string; end; { TFPCMsgFile } TFPCMsgFile = class private fSpecialItems: array[TfmiSpecialItem] of TFPCMsgItem; FItems: TFPList; // list of TFPCMsgItem fSortedForID: TAVLTree; // tree of TFPCMsgItem sorted for ID fItemById: array of TFPCMsgItem; fNodeMgr: TAVLTreeNodeMemManager; function GetItems(Index: integer): TFPCMsgItem; procedure CreateArray; function GetSpecialItems(Index: TfmiSpecialItem): TFPCMsgItem; public constructor Create; destructor Destroy; override; procedure LoadFromFile(const Filename: string); procedure LoadFromList(List: TStrings); virtual; procedure LoadFromText(s: string); virtual; procedure Clear; virtual; function Count: integer; property Items[Index: integer]: TFPCMsgItem read GetItems; default; function FindWithID(ID: integer): TFPCMsgItem; function FindWithMessage(Msg: string): TFPCMsgItem; function GetMsgText(Item: TFPCMsgItem): string; // prepends msg type (e.g. Error:) function PatternFits(Item: TFPCMsgItem; aMsg: string): integer; // >=0 fits property SpecialItems[Index: TfmiSpecialItem]: TFPCMsgItem read GetSpecialItems; function MsgTypToSpecialItem(const Typ: string): TFPCMsgItem; end; function CompareFPCMsgId(item1, item2: Pointer): integer; function CompareIDWithFPCMsgId(PtrID, Item: Pointer): integer; type TFPCMsgRange = record StartPos: integer; EndPos: integer; end; PFPCMsgRange = ^TFPCMsgRange; { TFPCMsgRanges } TFPCMsgRanges = class private FCount: integer; FCapacity: integer; public Ranges: PFPCMsgRange; property Count: integer read FCount; property Capacity: integer read FCapacity; procedure Add(StartPos, EndPos: integer); procedure Clear(FreeMemory: boolean = false); destructor Destroy; override; end; procedure ExtractFPCMsgParameters(const Mask, Txt: string; var Ranges: TFPCMsgRanges); function dbgs(i: TfmiSpecialItem): string; overload; implementation function CompareFPCMsgId(item1, item2: Pointer): integer; var Msg1: TFPCMsgItem absolute item1; Msg2: TFPCMsgItem absolute item2; begin if Msg1.IDMsg2.ID then exit(1) else exit(0); end; function CompareIDWithFPCMsgId(PtrID, Item: Pointer): integer; var Msg: TFPCMsgItem absolute Item; ID: LongInt; begin ID:=PInteger(PtrID)^; if IDMsg.ID then exit(1) else exit(0); end; procedure ExtractFPCMsgParameters(const Mask, Txt: string; var Ranges: TFPCMsgRanges); { Examples: Mask: bla$1blo Txt: blatestblo Result:=['test'] } function FindEndOfNextMatch(MaskStartPos, MaskEndPos, TxtStartPos: PChar): PChar; var TxtPos: PChar; MaskPos: PChar; begin while TxtStartPos^<>#0 do begin TxtPos:=TxtStartPos; MaskPos:=MaskStartPos; while (MaskPos'$') then break; inc(MaskPos); inc(TxtPos); end; while MaskPos^='$' do begin // skip variable in mask inc(MaskPos); while MaskPos^ in ['0'..'9','A'..'Z','a'..'z','_'] do inc(MaskPos); // get next pattern in mask MaskStartPos:=MaskPos; while (MaskPos^<>#0) and (MaskPos^<>'$') do inc(MaskPos); if MaskPos^=#0 then begin // variable at end of mask Ranges.Add(TxtPos-BaseTxtPos,length(Txt)+1); exit; end; // search pattern in txt TxtEndPos:=FindEndOfNextMatch(MaskStartPos,MaskPos,TxtPos); if TxtEndPos=nil then exit; Ranges.Add(TxtPos-BaseTxtPos,TxtEndPos-BaseTxtPos); TxtPos:=TxtEndPos; end; end; function dbgs(i: TfmiSpecialItem): string; begin case i of fmisiFatal: Result:='fatal'; fmisiError: Result:='error'; fmisiWarning: Result:='warning'; fmisiNote: Result:='note'; fmisiHint: Result:='hint'; else Result:='?'; end; end; { TFPCMsgItem } function TFPCMsgItem.GetName(WithID: boolean): string; begin Result:=Part+'_'; if Typ<>'' then Result:=Result+Typ+'_'; Result:=Result+TxtIdentifier; if WithID then Result:=Result+'='+IntToStr(ID); end; function TFPCMsgItem.PatternFits(aMsg: string): integer; var PatStartPos: PChar; PatEndPos: PChar; MsgFitPos: PChar; MatchLen: Integer; MsgPos: PChar; PatPos: PChar; MsgStartPos: PChar; PatLen: Integer; begin Result:=-1; // Pattern is for example "$1 lines compiled, $2 sec$3" if (aMsg='') or (Pattern='') then exit; // aMsg can start with a filename => hard to tell where the message starts // the Pattern is always at the end of aMsg => quick check the end PatLen:=length(Pattern); if (PatLen>=2) and ((Pattern[PatLen-1]<>'$') or (not (Pattern[PatLen] in ['0'..'9']))) then begin // the pattern does not have a placeholder at the end // => the tail must be the pattern => check tail PatStartPos:=PChar(Pattern); PatEndPos:=@Pattern[PatLen]; MsgPos:=@aMsg[length(aMsg)]; MsgStartPos:=PChar(aMsg); while (PatEndPos^=MsgPos^) do begin if PatEndPos=PatStartPos then begin // pattern has no placeholders and whole pattern fits Result:=PatLen; exit; end; dec(PatEndPos); if (PatEndPos^ in ['0'..'9']) and (PatEndPos[-1]='$') then begin // pattern behind last placeholder fits // => a full check is needed break; end; if MsgPos=MsgStartPos then begin // pattern does not fit exit(-1); end; dec(MsgPos); end; end; PatEndPos:=PChar(Pattern); MsgFitPos:=PChar(aMsg); MatchLen:=0; repeat PatStartPos:=PatEndPos; // get next pattern between placeholders repeat if PatEndPos^=#0 then break; if (PatEndPos^='$') and (PatEndPos[1] in ['0'..'9']) then break; inc(PatEndPos); until false; if PatEndPos<>PatStartPos then begin // search pattern in Pattern repeat MsgPos:=MsgFitPos; PatPos:=PatStartPos; while (MsgPos^=PatPos^) and (PatPos check next inc(MsgFitPos); until MsgFitPos^=#0; if PatPos does not fit inc(MatchLen,PatEndPos-PatStartPos); // pattern fits, search the rest of the patterns behind this position MsgFitPos:=MsgPos; end; if PatEndPos^=#0 then begin // whole pattern fits Result:=MatchLen; exit; end; // skip placeholder $d inc(PatEndPos,2); until false; end; function TFPCMsgItem.GetTrimmedComment(NoLineBreaks, NoLatex: boolean): string; var i: Integer; StartPos: Integer; begin Result:=Comment; if NoLatex then begin // remove tags i:=1; while i100000 then begin debugln(['TFPCMsgFile.CreateArray WARNING: MaxID ',MaxID,' too high: ',Item.Pattern]); exit; end; //debugln(['TFPCMsgFile.CreateArray Max=',MaxID]); SetLength(fItemById,MaxID+1); for i:=0 to length(fItemById)-1 do fItemById[i]:=nil; for i:=0 to FItems.Count-1 do begin Item:=TFPCMsgItem(FItems[i]); //debugln(['TFPCMsgFile.CreateArray ',Item.ID,' ',copy(Item.Pattern,1,20),'..',copy(Item.Pattern,length(Item.Pattern)-19,20)]); fItemById[Item.ID]:=Item; end; end; function TFPCMsgFile.GetSpecialItems(Index: TfmiSpecialItem): TFPCMsgItem; begin Result:=fSpecialItems[Index]; end; constructor TFPCMsgFile.Create; begin inherited Create; FItems:=TFPList.Create; fSortedForID:=TAVLTree.Create(@CompareFPCMsgId); fNodeMgr:=TAVLTreeNodeMemManager.Create; fSortedForID.SetNodeManager(fNodeMgr); end; destructor TFPCMsgFile.Destroy; begin Clear; FreeAndNil(FItems); FreeAndNil(fSortedForID); FreeAndNil(fNodeMgr); inherited Destroy; end; procedure TFPCMsgFile.LoadFromFile(const Filename: string); var sl: TStringList; begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromFile START ',Filename]); {$ENDIF} sl:=TStringList.Create; try sl.LoadFromFile(UTF8ToSys(Filename)); LoadFromList(sl); finally sl.Free; end; end; procedure TFPCMsgFile.LoadFromList(List: TStrings); function ReadTilChar(var p: PChar; EndChar: char; out s: string): boolean; var c: Char; StartPos: PChar; begin StartPos:=p; repeat c:=p^; if c=#0 then exit(false); if c=EndChar then begin break; end; inc(p); until false; if p=StartPos then exit(false); SetLength(s,p-StartPos); System.Move(StartPos^,s[1],length(s)); inc(p); Result:=true; end; function ReadItem(var Line: integer; const s: string): TFPCMsgItem; // __=__ // option and wpo are different: // _=__ // and // _=_[ // ...] // var p: PChar; Part: string; Typ: string; TxtID: string; ShownTyp: string; IDStr: string; ID: LongInt; Msg: string; h: string; i: Integer; MsgEndSpace: String; begin Result:=nil; p:=PChar(s); if not ReadTilChar(p,'_',Part) then begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromList invalid , line ',Line,': "',s,'"']); {$ENDIF} exit; end; if (Part='option') or (Part='wpo') then Typ:='' else if not ReadTilChar(p,'_',Typ) then begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromList invalid , line ',Line,': "',s,'"']); {$ENDIF} exit; end else if (length(Typ)<>1) or (not (Typ[1] in ['f','e','w','n','h','i','l','u','t','c','d','x','o'])) then begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromList invalid , line ',Line,': "',s,'"']); {$ENDIF} exit; end; if not ReadTilChar(p,'=',TxtID) then begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromList invalid , line ',Line,': "',s,'"']); {$ENDIF} exit; end; if not ReadTilChar(p,'_',IDStr) then begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromList invalid id, line ',Line,': "',s,'"']); {$ENDIF} exit; end; ID:=StrToIntDef(IDStr,-1); if ID<0 then begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromList invalid id, line ',Line,': "',s,'"']); {$ENDIF} exit; end; ShownTyp:=''; if p<>'[' then begin if not ReadTilChar(p,'_',ShownTyp) then begin {$IFDEF VerboseFPCMsgFile} debugln(['TFPCMsgFile.LoadFromList invalid urgency, line ',Line,': "',s,'"']); {$ENDIF} exit; end; Msg:=p; end else begin // multi line message Msg:=''; repeat inc(Line); if Line>=List.Count then exit; h:=List[Line]; //debugln(['ReadItem ID=',ID,' h=',h]); if (h<>'') and (h[1]=']') then break; Msg:=Msg+h+LineEnding; until false; end; i:=length(Msg); while (i>=1) and (Msg[i] in [' ',#9,#10,#13]) do dec(i); if inil then begin if Item.Comment<>'' then Item.Comment:=Item.Comment+LineEnding; Item.Comment:=Item.Comment+copy(s,2,length(s)); end; end else begin Item:=ReadItem(Line,s); if Item<>nil then begin //debugln(['TFPCMsgFile.LoadFromList ',Item.ID,' ',Item.Pattern]); Item.Index:=FItems.Count; FItems.Add(Item); fSortedForID.Add(Item); case Item.ID of 1012: fSpecialItems[fmisiFatal]:=Item; 1013: fSpecialItems[fmisiError]:=Item; 1014: fSpecialItems[fmisiWarning]:=Item; 1015: fSpecialItems[fmisiNote]:=Item; 1016: fSpecialItems[fmisiHint]:=Item; end; end; end; inc(Line); end; CreateArray; end; procedure TFPCMsgFile.LoadFromText(s: string); var sl: TStringList; begin //debugln(['TFPCMsgFile.LoadFromText START']); sl:=TStringList.Create; try sl.Text:=s; LoadFromList(sl); finally sl.Free; end; end; procedure TFPCMsgFile.Clear; var i: Integer; s: TfmiSpecialItem; begin for s:=Low(fSpecialItems) to high(fSpecialItems) do fSpecialItems[s]:=nil; SetLength(fItemById,0); fSortedForID.Clear; for i:=0 to FItems.Count-1 do TObject(FItems[i]).Free; FItems.Clear; end; function TFPCMsgFile.Count: integer; begin Result:=FItems.Count; end; function TFPCMsgFile.FindWithID(ID: integer): TFPCMsgItem; var Node: TAVLTreeNode; begin //debugln(['TFPCMsgFile.FindWithID ',ID,' Max=',length(fItemById)]); if (ID>=0) and (IDnil then Result:=TFPCMsgItem(Node.Data) else Result:=nil; end; function TFPCMsgFile.FindWithMessage(Msg: string): TFPCMsgItem; var MsgID: Integer; Item: TFPCMsgItem; i: Integer; p: PChar; BestMatchLen: Integer; MatchLen: Integer; begin Result:=nil; if Msg='' then exit; Msg:=Trim(Msg); if Msg='' then exit; p:=PChar(Msg); // skip time [0.000] if (p^='[') and (p[1] in ['0'..'9']) then begin inc(p,2); while p^ in ['0'..'9','.'] do inc(p); if p^<>']' then exit; // not a fpc message inc(p); while p^ in [' '] do inc(p); end; // read message ID (000) MsgID:=0; if (p^='(') and (p[1] in ['0'..'9']) then begin inc(p); while p^ in ['0'..'9','.'] do begin if MsgID>1000000 then exit; // not a fpc message MsgID:=MsgID*10+ord(p^)-ord('0'); inc(p); end; if p^<>')' then exit; // not a fpc message inc(p); while p^ in [' '] do inc(p); Result:=FindWithID(MsgID); exit; end; // search a message pattern that fits the Msg BestMatchLen:=-1; for i:=0 to Count-1 do begin Item:=Items[i]; if Item.Pattern='' then continue; MatchLen:=PatternFits(Item,Msg); if MatchLen>BestMatchLen then begin BestMatchLen:=MatchLen; Result:=Item; end; end; end; function TFPCMsgFile.GetMsgText(Item: TFPCMsgItem): string; var si: TFPCMsgItem; begin if Item=nil then exit(''); Result:=Item.Pattern; si:=MsgTypToSpecialItem(Item.Typ); if si<>nil then Result:=si.Pattern+' '+Result; end; function TFPCMsgFile.PatternFits(Item: TFPCMsgItem; aMsg: string): integer; var si: TFPCMsgItem; begin Result:=Item.PatternFits(aMsg); if Result<0 then exit; // some messages have two types // => check typ si:=MsgTypToSpecialItem(Item.Typ); if si<>nil then begin if System.Pos(si.Pattern,aMsg)>0 then inc(Result,length(si.Pattern)); end; end; function TFPCMsgFile.MsgTypToSpecialItem(const Typ: string): TFPCMsgItem; begin Result:=nil; if length(Typ)<>1 then exit; case Typ[1] of 'f': Result:=fSpecialItems[fmisiFatal]; 'e': Result:=fSpecialItems[fmisiError]; 'w': Result:=fSpecialItems[fmisiWarning]; 'n': Result:=fSpecialItems[fmisiNote]; 'h': Result:=fSpecialItems[fmisiHint]; end; end; { TFPCMsgRanges } procedure TFPCMsgRanges.Add(StartPos, EndPos: integer); begin if Count=Capacity then begin if Capacity<8 then fCapacity:=8 else fCapacity:=Capacity*2; ReAllocMem(Ranges,Capacity*SizeOf(TFPCMsgRange)); end; Ranges[FCount].StartPos:=StartPos; Ranges[FCount].EndPos:=EndPos; inc(FCount); end; procedure TFPCMsgRanges.Clear(FreeMemory: boolean); begin FCount:=0; if not FreeMemory then begin ReAllocMem(Ranges,0); FCapacity:=0; end; end; destructor TFPCMsgRanges.Destroy; begin Clear(true); inherited Destroy; end; end.