{-----------------------------------------------------------------------------
The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/MPL-1.1.html

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is: UsesParser.pas, released on 2006-02-20.

The Initial Developer of the Original Code is Florent Ouchet [ouchet dott florent att laposte dott net]
Portions created by Florent Ouchet are Copyright (C) 2006 Florent Ouchet.
All Rights Reserved.

Contributor(s):

You may retrieve the latest version of this file at the Project JEDI's JVCL home page,
located at http://jvcl.sourceforge.net

Known Issues:
-----------------------------------------------------------------------------}
// $Id: UsesParser.pas 10610 2006-05-19 13:35:08Z elahn $

unit UsesParser;

interface

uses
  SysUtils, Classes;

type
  TUsesParser = class (TObject)
  private
    FFileContent: string;
    FDefines: TStrings;
    FIncludeDirs: TStrings;
    FUsesList: TStrings;
    procedure SetDefines(const Value: TStrings);
    procedure SetFileContent(const Value: string);
    procedure SetIncludeDirs(const Value: TStrings);
  public
    constructor Create; reintroduce;
    destructor Destroy; override;

    function ParseUses: Boolean;
    function LoadFromFile(const FileName: string): Boolean;

    property FileContent: string read FFileContent write SetFileContent;
    property Defines: TStrings read FDefines write SetDefines;
    property IncludeDirs: TStrings read FIncludeDirs write SetIncludeDirs;
    property UsesList: TStrings read FUsesList;
  end;

implementation

uses
  JclStrings, JclFileUtils;

function ParseFile(const AFileContent: string;
  ADefines, AIncludeDirs, UsesList: TStrings; var IfDefCount: Integer;
  var InUsesSection: Boolean): Boolean;
  function GetNextWord(Ptr: PChar): string;
  var
    PtrStart: PChar;
  begin
    while not (Ptr^ in [AnsiNull, 'a'..'z', 'A'..'Z', '_', '0'..'9']) do
      Inc(Ptr);
    PtrStart := Ptr;
    while Ptr^ in ['a'..'z', 'A'..'Z', '_', '.', '0'..'9'] do
      Inc(Ptr);
    SetString(Result, PtrStart, Ptr - PtrStart);
  end;
  procedure Define(const Symbol: string);
  var
    Index: Integer;
  begin
    for Index := 0 to ADefines.Count - 1 do
      if SameText(Symbol, ADefines.Strings[Index]) then
        Exit;
    ADefines.Add(Symbol);
  end;
  procedure Undef(const Symbol: string);
  var
    Index: Integer;
  begin
    for Index := ADefines.Count - 1 downto 0 do
      if SameText(Symbol, ADefines.Strings[Index]) then
        ADefines.Delete(Index);
  end;
  function IfDef(const Symbol: string): Boolean;
  var
    Index: Integer;
  begin
    Result := True;
    for Index := 0 to ADefines.Count - 1 do
      if SameText(Symbol, ADefines.Strings[Index]) then
        Exit;
    Result := False;
  end;
  function ParseInclude(const Symbol: string): Boolean;
  var
    AFileStream: TFileStream;
    BFileContent: string;
    Index: Integer;
    AFileName, BFileName: string;
  begin
    Result := False;
    AFileName := AnsiDequotedStr(AnsiDequotedStr(Symbol, ''''), '"');
    for Index := 0 to AIncludeDirs.Count - 1 do
    begin
      BFileName := PathAddSeparator(AIncludeDirs.Strings[Index]) + AFileName;
      if FileExists(BFileName) then
      begin
        AFileStream := TFileStream.Create(BFileName, fmOpenRead or fmShareDenyWrite);
        try
          if AFileStream.Size > MaxInt then
            raise ERangeError.CreateFmt('File %s too big', [BFileName]);
          SetLength(BFileContent, AFileStream.Size);
          AFileStream.Read(BFileContent[1], AFileStream.Size);
          Result := ParseFile(BFileContent, ADefines, AIncludeDirs, UsesList, IfDefCount, InUsesSection);
        finally
          AFileStream.Free;
        end;
        Exit;
      end;
    end;
  end;
var
  Ptr, PtrStartWord: PChar;
  WordAtPtr: string;
begin
  Result := True;
  InUsesSection := False;

  Ptr := PChar(AFileContent);
  while True do
  begin
    case Ptr^ of
      AnsiNull :
        Exit;
      AnsiForwardSlash :
        if Ptr[1] = '/' then
          while not (Ptr^ in [AnsiNull, AnsiCarriageReturn, AnsiLineFeed]) do
            Inc(Ptr);
      '{' :
        begin
          if Ptr[1] = '$' then
          begin
            WordAtPtr := GetNextWord(@Ptr[1]);
            if SameText(WordAtPtr, 'IFDEF') then
            begin
              // inside $IFDEF
              if (IfDefCount > 0) or not IfDef(GetNextWord(@Ptr[7])) then
                Inc(IfDefCount);
            end
            else if SameText(WordAtPtr, 'IFNDEF') then
            begin
              // inside $IFNDEF
              if (IfDefCount > 0) or IfDef(GetNextWord(@Ptr[8])) then
                Inc(IfDefCount);
            end
            else if SameText(WordAtPtr, 'ELSE') then
            begin
              // inside $ELSE
              if IfDefCount = 1 then
                IfDefCount := 0
              else if IfDefCount = 0 then
                IfDefCount := 1;
            end
            else if SameText(WordAtPtr, 'ENDIF') then
            begin
              if IfDefCount > 0 then
                Dec(IfDefCount);
              // inside $ENDIF
            end
            else if SameText(WordAtPtr, 'DEFINE') then
            begin
              // inside $DEFINE
              if IfDefCount = 0 then
                Define(GetNextWord(@Ptr[8]));
            end
            else if SameText(WordAtPtr, 'UNDEF') then
            begin
              // inside $UNDEF
              if IfDefCount = 0 then
                Undef(GetNextWord(@Ptr[7]));
            end
            else if SameText(WordAtPtr, 'INCLUDE') then
            begin
              // inside $INCLUDE
              if IfDefCount = 0 then
              begin
                Result := ParseInclude(GetNextWord(@Ptr[9]));
                if not Result then
                  Exit;
              end;
            end
            else if SameText(WordAtPtr, 'I') then
            begin
              // inside $I
              if IfDefCount = 0 then
              begin
                Result := ParseInclude(GetNextWord(@Ptr[3]));
                if not Result then
                  Exit;
              end;
            end; // TODO: $IF + condition
          end;

          while not (Ptr^ in [AnsiNull, '}']) do
            Inc(Ptr);
        end;
      AnsiSingleQuote :
        begin
          Inc(Ptr);
          while not (Ptr^ in [AnsiNull, AnsiCarriageReturn, AnsiLineFeed, AnsiSingleQuote]) do
            Inc(Ptr);
        end;
      '(' :
        if Ptr[1] = '*' then
        begin
          Inc(Ptr, 2);
          while Ptr^ <> AnsiNull do
            if (Ptr^ = '*') and (Ptr[1] = ')') then
          begin
            Inc(Ptr, 2);
            Break;
          end
          else
            Inc(Ptr);
        end;
      'a'..'z',
      'A'..'Z',
      '_' :
        begin
          PtrStartWord := Ptr;
          while Ptr^ in ['a'..'z', 'A'..'Z', '_', '0'..'9'] do
            Inc(Ptr);
          SetString(WordAtPtr, PtrStartWord, Ptr - PtrStartWord);
          if SameText(WordAtPtr, 'uses') and (IfDefCount = 0) then
            InUsesSection := True
          else if InUsesSection and (UsesList.IndexOf(WordAtPtr) = -1)
            and (IfDefCount = 0) then
            UsesList.Add(WordAtPtr);
        end;
      ';' :
        if IfDefCount = 0 then
          InUsesSection := False;
    end;
    case Ptr^ of
      AnsiNull :
        Exit;
      ';' :
        if IfDefCount = 0 then
          InUsesSection := False;
    end;
    Inc(Ptr);
  end;
end;

//=== TUsesParser ============================================================

constructor TUsesParser.Create;
begin
  inherited Create;
  FDefines := TStringList.Create;
  FIncludeDirs := TStringList.Create;
  FUsesList := TStringList.Create;
end;

destructor TUsesParser.Destroy;
begin
  FDefines.Free;
  FIncludeDirs.Free;
  FUsesList.Free;
  inherited Destroy;
end;

function TUsesParser.LoadFromFile(const FileName: string): Boolean;
var
  AFileStream: TFileStream;
begin
  SetLength(FFileContent, 0);
  if FileExists(FileName) then
  begin
    AFileStream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
    try
      if AFileStream.Size > MaxInt then
        raise ERangeError.CreateFmt('File %s is too big', [FileName]);
      SetLength(FFileContent, AFileStream.Size);
      AFileStream.Read(FFileContent[1], AFileStream.Size);
    finally
      AFileStream.Free;
    end;
  end;

  Result := Length(FileContent) > 0;
end;

function TUsesParser.ParseUses: Boolean;
var
  IfDefCount: Integer;
  InUsesSection: Boolean;
begin
  IfDefCount := 0;
  InUsesSection := False;
  Result := ParseFile(FileContent, Defines, IncludeDirs, UsesList, IfDefCount, InUsesSection);
end;

procedure TUsesParser.SetDefines(const Value: TStrings);
begin
  FDefines.Assign(Value);
end;

procedure TUsesParser.SetFileContent(const Value: string);
begin
  FFileContent := Value;
end;

procedure TUsesParser.SetIncludeDirs(const Value: TStrings);
begin
  FIncludeDirs.Assign(Value);
end;

end.
