unit VA508Classes;

interface
  uses SysUtils, Classes, Contnrs, StrUtils, Windows, HRParser, HRParserPas, Forms, Dialogs;

type
  TFormData = class
  private
    FFileName: string;
    FlcFormClassName: string;
    FInheritedForm: boolean;
    FParent: TFormData;
    FManagerComponentName: string;
    FInheritedManager: boolean;
    FFormClassName: string;
    FEmptyManager: boolean; 
    procedure SetFormClassName(const Value: string);
  public
    function HasManager: boolean;
    function HasParent: boolean;
    property FormClassName: string read FFormClassName write SetFormClassName;
    property lcFormClassName: string read FlcFormClassName;
    property EmptyManager: boolean read FEmptyManager write FEmptyManager; 
    property FileName: string read FFileName write FFileName;
    property Parent: TFormData read FParent write FParent;
    property InheritedForm: boolean read FInheritedForm write FInheritedForm;
    property InheritedManager: boolean read FInheritedManager write FInheritedManager;
    property ManagerComponentName: string read FManagerComponentName write FManagerComponentName;
  end;

  EVA508AccessibilityException = class(Exception);

  TParentChildErrorCode = (pcNoParentManager, pcValidRelationship,
                           pcNoInheritence, pcNoChildComponent, pcEmptyManagerComponent,
                           pcOtherChildComponent, pcInheritedNoParent);
const
  TParentChildPassCodes = [pcNoParentManager, pcValidRelationship];
  TParentChildFailCodes = [pcNoInheritence, pcNoChildComponent, pcEmptyManagerComponent,
                           pcOtherChildComponent, pcInheritedNoParent];
  TAutoFixFailCodes = [pcNoInheritence, pcEmptyManagerComponent, pcNoChildComponent, pcInheritedNoParent];

type
  TParentChildFormTracker = class
  private
    FData: TObjectList;
    function FindForm(AFormClassName: String): TFormData;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Clear;
    procedure AddForm(AFileName, AFormClassName, AManagerComponentName: string;
                      AEmptyManager: boolean; AInheritedForm, AInheritedManager: boolean);
    procedure AddLink(ParentFormClassName, ChildFormClassName: string);
    function FormCount: integer;
    function GetFormData(index: integer): TFormData;
    function ParentChildErrorStatus(index: integer): TParentChildErrorCode;
    function ParentChildErrorDescription(index: integer): string;
  end;

 TUnitSection = (usUnknown, usInterface, usImplementation);
  TTokenState = (tsNormal, tsPendingEqualChar, tsPendingClassSymbol, tsPendingParenChar,
                  tsPendingClassName, tsPendingEndOfClass);

  TVA508Parser = class
  private
    FClassName: String;
    FParentClass: String;
    FPendingParentClass: string;
    FParser: THRParserPas;
    FToken: THRToken;
    FLastLine: integer;
    FLastPos: integer;
    FTokenName: String;
    FState: TTokenState;
    FUnitSection: TUnitSection;
    FDone: boolean;
    FIsSymbol: boolean;
    FIsChar: boolean;
    procedure ParseToken;
  public
    function GetParentClassName(ClassName, FileName: String;
                                InStream: TStream; var OutStream: TStream): String;
    function LastLineRead: integer;
    function LastPosition: integer;
  end;

procedure VA508ComponentCreationCheck(AComponent, AOwner: TComponent;
            AllowDataModules: boolean; ManagerRequired: boolean);
procedure VA508ComponentDestructionCheck(AComponent: TComponent);

const
  NO_OWNER_ERROR = 'Cannot create a %s component without an owner';

implementation

uses
  VA508AccessibilityManager, VA508ImageListLabeler;

const
  MANAGER_CLASS_REQUIRED = 'Cannot create a %s component without a ' + #13#10 +
                           '%s component on the same form';
  OTHER_COMPONENTS_DELETED = 'Deleting this %s component also deletes all' + #13#10 +
                             'A %s components on this form';
  OWNER_NOT_ALLOWED = 'You may not place a %s component on a %s';
  OWNER_REQUIREMENTS = '%s component can only be added to a %s';
  HAS_EXISTING_MANAGER_ERROR = '%s alread has a %s component';

function HasAnotherAccessibilityManager(Root, AComponent: TComponent): boolean;
var
  i: integer;
  comp: TComponent;
begin
  Result := false;
  for i := 0 to AComponent.ComponentCount-1 do
  begin
    comp := AComponent.Components[i];
    if (comp <> Root) and (comp is TVA508AccessibilityManager) then
    begin
      Result := true;
      exit;
    end;
    if HasAnotherAccessibilityManager(Root, AComponent.Components[i]) then
    begin
      Result := true;
      exit;
    end;
  end;
end;

procedure VA508ComponentCreationCheck(AComponent, AOwner: TComponent;
            AllowDataModules: boolean; ManagerRequired: boolean);
var
  msg: string;

  procedure EnsureManager;
  var
    i: integer;
    error: boolean;
  begin
    if (csDesigning in AOwner.ComponentState) and (not (csLoading in AOwner.ComponentState)) then
    begin
      error := TRUE;
      for i := 0 to AOwner.ComponentCount-1 do
      begin
        if AOwner.Components[i] is TVA508AccessibilityManager then
        begin
          error := FALSE;
          break;
        end;
      end;
      if error then
      begin
        raise EVA508AccessibilityException.CreateFmt(MANAGER_CLASS_REQUIRED,
                                [AComponent.ClassName, TVA508AccessibilityManager.ClassName]);
      end;
    end;
  end;

begin
  if not assigned(AOwner) then
    raise EVA508AccessibilityException.CreateFmt(NO_OWNER_ERROR, [AComponent.ClassName]);
  if (AOwner is TDataModule) then
  begin
    if AllowDataModules then
      exit
    else
      raise EVA508AccessibilityException.CreateFmt(OWNER_NOT_ALLOWED, [AComponent.ClassName, TDataModule.ClassName]);
  end;
  if not (AOwner is TCustomForm) then
  begin
    msg := 'Form';
    if AllowDataModules then
      msg := msg + ' or a Data Module';
    raise EVA508AccessibilityException.CreateFmt(OWNER_REQUIREMENTS, [AComponent.ClassName, msg]);
  end;
  if ManagerRequired then
    EnsureManager
  else
  begin
    if HasAnotherAccessibilityManager(AComponent, AOwner) then
      raise EVA508AccessibilityException.Create(Format(HAS_EXISTING_MANAGER_ERROR,
            [AOwner.ClassName, AComponent.ClassName]));
  end;
end;

procedure VA508ComponentDestructionCheck(AComponent: TComponent);
var
  i: integer;
  list: TObjectList;
  msg: string;
  ComponentAccessFound, ImageListLabelerFound: boolean;
  Owner: TComponent;

begin
  if not assigned(AComponent) then exit;
  Owner := AComponent.Owner;
  if not assigned(Owner) then exit;
  if HasAnotherAccessibilityManager(AComponent, Owner) then exit;
  if (csDesigning in AComponent.ComponentState) and (not (csDestroying in Owner.ComponentState)) then
  begin
    list := TObjectList.Create;
    try
      ComponentAccessFound := FALSE;
      ImageListLabelerFound := FALSE;
      for I := 0 to Owner.ComponentCount-1 do
      begin
        if Owner.Components[i] is TVA508ComponentAccessibility then
        begin
          ComponentAccessFound := TRUE;
          list.Add(Owner.Components[i]);
        end
        else
        if Owner.Components[i] is TVA508ImageListLabeler then
        begin
          ImageListLabelerFound := TRUE;
          list.Add(Owner.Components[i]);
        end
      end;
      msg := '';
      if ImageListLabelerFound then
        msg := TVA508ImageListLabeler.ClassName;
      if ComponentAccessFound then
      begin
        if msg <> '' then
          msg := msg + ' and ';
        msg := msg + TVA508ComponentAccessibility.ClassName;
      end;
      if msg <> '' then
      begin
        MessageDlg(Format(OTHER_COMPONENTS_DELETED, [AComponent.ClassName, msg]), mtWarning, [mbOK], 0);
      end;
    finally
      list.Free;
    end;
  end;
end;

{ TFormData }

function TFormData.HasManager: boolean;
begin
  Result := ManagerComponentName <> '';
end;

function TFormData.HasParent: boolean;
begin
  Result := assigned(Parent);
end;

procedure TFormData.SetFormClassName(const Value: string);
begin
  FFormClassName := Value;
  FlcFormClassName := lowerCase(Value);
end;

{ TParentChildFormTracker }

procedure TParentChildFormTracker.AddForm(AFileName, AFormClassName, AManagerComponentName: string;
                                AEmptyManager: boolean; AInheritedForm, AInheritedManager: boolean);
var
  data: TFormData;
begin
  if FindForm(AFormClassName) = nil then
  begin
    Data := TFormData.Create;
    data.FileName := AFileName;
    data.FormClassName := AFormClassName;
    data.ManagerComponentName := AManagerComponentName;
    data.Parent := nil;
    data.InheritedForm := AInheritedForm;
    data.InheritedManager := AInheritedManager;
    data.EmptyManager := AEmptyManager;
    FData.Add(data);
  end;
end;

procedure TParentChildFormTracker.AddLink(ParentFormClassName, ChildFormClassName: string);
var
  child,parent: TFormData;
begin
  child := FindForm(ChildFormClassName);
  parent := FindForm(ParentFormClassName);
  if assigned(child) and assigned(parent) then
    child.Parent := parent;
end;

procedure TParentChildFormTracker.Clear;
begin
  FData.Clear;
end;

constructor TParentChildFormTracker.Create;
begin
  FData := TObjectList.Create;
end;

destructor TParentChildFormTracker.Destroy;
begin
  FData.Free;
  inherited;
end;

function TParentChildFormTracker.FindForm(AFormClassName: String): TFormData;
var
  i: integer;
  name: string;
begin
  name := lowercase(AFormClassName);
  Result := nil;
  for i := 0 to FData.Count - 1 do
  begin
    if GetFormData(i).lcFormClassName = Name then
    begin
      Result := GetFormData(i);
      exit;
    end;
  end;
end;

function TParentChildFormTracker.FormCount: integer;
begin
  Result := FData.Count;
end;

function TParentChildFormTracker.GetFormData(index: integer): TFormData;
begin
  Result := TFormData(FData[index]);
end;

function TParentChildFormTracker.ParentChildErrorDescription(index: integer): string;
var
  code: TParentChildErrorCode;
  parent: TFormData;
  child: TFormData;
begin
  code := ParentChildErrorStatus(index);
  Result := '';
  if code in [pcNoParentManager, pcValidRelationship] then exit;
  child := GetFormData(index);
  parent := child.Parent;
  case code of
    pcNoInheritence: Result := 'Form ' + child.FormClassName + ' descends from form ' + parent.FormClassName +
                                ' but uses the word "object" instead of "inherited" in the .dfm file.';
    pcNoChildComponent, pcEmptyManagerComponent: Result := 'Form ' + child.FormClassName +
                                ' .dfm file needs to be rebuilt.    To fix manually, view the form as text, then as a form, ' +
                                ' make sure the form is in a modified state, and save it.';
    pcOtherChildComponent: Result := 'Form ' + child.FormClassName + ' has two ' + TVA508AccessibilityManager.ClassName +
                                ' components, one from an inherited form, and one on the form.' +
                                '  Remove the component on the form and use the inherited component';
    pcInheritedNoParent: Result := 'Form ' + child.FormClassName + ' has a ' + TVA508AccessibilityManager.ClassName +
                                ' component, ' + child.ManagerComponentName +
                                ', that was inherited from a parent form, but ' + child.ManagerComponentName +
                                ' has been deleted from the parent form.  To Remove the component, view the form as text, then as a form, ' +
                                ' make sure the form is in a modified state, and save it.  Or you can add the ' +
                                TVA508AccessibilityManager.ClassName + ' component back onto the parent form.';
    else Result := '';
  end;
end;

function TParentChildFormTracker.ParentChildErrorStatus(
  index: integer): TParentChildErrorCode;
var
  parent: TFormData;
  child: TFormData;
  bad: boolean;

begin
  Result := pcNoParentManager;
  child := GetFormData(index);
  if not assigned(child) then exit;

  bad := false;
  if child.InheritedManager then
  begin
    bad := not child.HasParent;
    if not bad then
      bad := not child.InheritedForm;
    if not bad then
      bad := not child.Parent.HasManager;
  end;

  try
    if not child.HasParent then exit;
    parent := child.Parent;
    if not parent.HasManager then exit;
    if child.InheritedForm then
    begin
      if child.HasManager then
      begin
        if (parent.ManagerComponentName = child.ManagerComponentName) and
            (child.InheritedManager) then
        begin
          if child.EmptyManager then
            Result := pcEmptyManagerComponent
          else
            Result := pcValidRelationship
        end
        else
          Result := pcOtherChildComponent
      end
      else
        Result := pcNoChildComponent;
    end
    else
      Result := pcNoInheritence;
  finally
    if bad and (Result = pcNoParentManager) then
      Result := pcInheritedNoParent;
  end;
end;

const
  INTERFACE_NAME      = 'interface';
  IMPLEMENTATION_NAME = 'implementation';

  CLASS_NAME = 'class';
  LEFT_PAREN  = '(';
  RIGHT_PAREN = ')';
  COMMA       = ',';
  EQUALS      = '=';

{ TVA508Parser }

function TVA508Parser.GetParentClassName(ClassName, FileName: String;
                                InStream: TStream; var OutStream: TStream): String;
begin
  FClassName := lowerCase(ClassName);
  FParentClass := '';
  FState := tsNormal;
  FUnitSection := usUnknown;
  FDone := false;

  if(assigned(FParser)) then
    FParser.Free;
  FParser := THRParserPas.Create;
  FLastLine := 0;
  FLastPos := 0;
  if assigned(InStream) then
    FParser.Source := InStream
  else
    FParser.Source := TFileStream.Create(FileName, fmOpenRead, fmShareDenyNone);
  try
    while (not FDone) and (FParser.NextToken.TokenType <> HR_TOKEN_EOF) do
    begin
      FToken := FParser.Token;
      FLastLine := FToken.Line;
      FLastPos := FToken.SourcePos;
      ParseToken;
    end;
  finally
    if assigned(InStream) then
    begin
      InStream.Free;
      OutStream := nil;
    end
    else
      OutStream := FParser.Source;
    FreeAndNil(FParser);
  end;
  Result := FParentClass;  
end;

function TVA508Parser.LastLineRead: integer;
begin
  Result := FLastLine;
end;

function TVA508Parser.LastPosition: integer;
begin
  Result := FLastPos + 1;
end;

procedure TVA508Parser.ParseToken;

  function IgnoreToken: boolean;
  begin
    if(FUnitSection = usImplementation) then
    begin
      Result := TRUE;
      exit;
    end;
    case FToken.TokenType of
      HR_TOKEN_TEXT_SPACE, HR_TOKEN_PAS_COMMENT_SLASH,
      HR_TOKEN_PAS_COMMENT_BRACE_OPEN, HR_TOKEN_PAS_COMMENT_BRACE,
      HR_TOKEN_PAS_COMMENT_BRACKET_OPEN, HR_TOKEN_PAS_COMMENT_BRACKET:
        Result := TRUE;
      else
        Result := FALSE;
    end;
  end;

  function InvalidSection: boolean;
  var
    changed: boolean;
  begin
    changed := false;
    if FIsSymbol then
    begin
      if FTokenName = INTERFACE_NAME then
      begin
        FUnitSection := usInterface;
        changed := true;
      end
      else if FTokenName = IMPLEMENTATION_NAME then
      begin
        FUnitSection := usImplementation;
        FDone := TRUE;
        changed := true;
      end;
    end;
    Result := (FUnitSection <> usInterface);
    if changed then
      FState := tsNormal;
  end;

begin
  if(IgnoreToken) then exit;

  FTokenName := LowerCase(FToken.Token);
  FIsSymbol  := (FToken.TokenType = HR_TOKEN_TEXT_SYMBOL);
  FIsChar    := (FToken.TokenType = HR_TOKEN_CHAR);

  if(InvalidSection) then exit;
  case FState of
    tsNormal:               if FIsSymbol and (FTokenName = FClassName) then
                              FState := tsPendingEqualChar;
    tsPendingEqualChar:     if FIsChar and (FTokenName = EQUALS) then
                              FState := tsPendingClassSymbol
                            else
                              FState := tsNormal;
    tsPendingClassSymbol:   if FIsSymbol and (FTokenName = CLASS_NAME) then
                              FState := tsPendingParenChar
                            else
                              FState := tsNormal;
    tsPendingParenChar:     if FIsChar and (FTokenName = LEFT_PAREN) then
                              FState := tsPendingClassName
                            else
                              FState := tsNormal;
    tsPendingClassName:     if FIsSymbol then
                            begin
                              FPendingParentClass := FToken.Token;
                              FState := tsPendingEndOfClass;
                            end
                            else
                              FState := tsNormal;
    tsPendingEndOfClass:    begin
                              if FIsChar and ((FTokenName = RIGHT_PAREN) or
                                  (FTokenName = COMMA)) then
                              begin
                                FParentClass := FPendingParentClass;
                                FDone := TRUE;
                              end;
                              FState := tsNormal;
                            end;
  else
    FState := tsNormal;
  end;
end;


end.
