Basic ModelMaker Design Critic
Last changed 9 October 2003
Home | Papers | ModelMaker | Basic Design Critic
 

Creating a Simple Design Critic for ModelMaker

Abstract

ModelMaker is a fabulous round-trip UML tools for Borland's Delphi. This paper describes how I created a custom design critic.

Introduction

One ModelMaker feature that I've started to become attached to is the design critic, which reports on various suspicious items in the model. The supplied critics for documentation are very simple. One reports classes, interfaces, and members that have nothing in the documentation box, and the other reports classes, interfaces, and members that have an empty "one-liner" documentation field.

I have the need to ensure that classes, interfaces, and members have some form of documentation, i.e. they must have either the documentation box or the "one-liner" field filled in. This is something that neither of the existing critics can do.

Credits

Thanks to Gerrit Beuze for his gracious assistance.
Thanks to Rithban for sharing code and ideas on the identical problem space.

Basics

Design critics are created within the ModelMaker Tools API framework, which allows one to create plug-ins for ModelMaker. From my limited knowledge, it appears that they may be the simplest form of plug-in.

Plug-ins are DLLs placed in the Experts directory under the place where ModelMaker was installed. When one starts ModelMaker, it automatically examines the DLLs and loads them if they have the correct entry points.

Notes

Note the repeated references to the demo code. This refers to the project that can be found in MMExperDemo.zip file in the Experts directory.

Step One: New DLL Project

  1. Create a new DLL project. I called mine "SomeDoc" since the critic ensures that there is some documentation.
  2. Add MMToolsApi to the project uses clause.
  3. Add the three required entry points. Note that we've not created the critic's class yet, so the second line in the InitializeExpert class is commented out for now. Export the three entry points with the correct name.

    (All of this code is a stripped-down version of the demo code.)

        procedure InitializeExpert(const Srv: IMMToolServices); stdcall;
        begin
          // (Required action. See demo code.)
          MMToolServices := Srv;
          // Add design critic
          //TODO: Create critic class and uncomment this next line.
          //MMToolServices.CriticManager.AddCritic(TsdSomeDocCritic.Create);
          // Sync with parent window, (Required action. See demo code.)
          Application.Handle := Srv.GetParentHandle;
        end;
    
        procedure FinalizeExpert; stdcall;
        begin
        end;
    
        function ExpertVersion: LongInt; stdcall;
        begin
          // (Required action. See demo code.)
          Result := MMToolsApiVersion;
        end;
    
        exports
          InitializeExpert name MMExpertEntryProcName,
          FinalizeExpert name MMExpertExitProcName,
          ExpertVersion name MMExpertVersionProcName;
        end.
        

Step Two: Create Critic Class

  1. Create a new unit in Delphi, and save the new file. The Delphi wizard will add the unit to the project's uses clause.
  2. Add the MMToolsApi and MMCriticsBase units to the interface uses clause.
        unit sdCritic;
    
        interface
    
        uses
          MMToolsApi,
          MMCriticsBase;
        
  3. In the new unit, create a new class that descends from TMMDesignCritic. This class is the design critic.
        type
          TsdSomeDocCritic = class(TMMDesignCritic)
          end;
        
  4. Add a constructor. In the constructor, assign a category to the Category property. (As far as I can tell, the value is arbitrary.)
        type
          TsdSomeDocCritic = class(TMMDesignCritic)
          public
            constructor Create;
          end;
    
        implementation
    
        constructor TsdSomeDocCritic.Create;
        begin
          inherited Create;
          Category := 'Documentation';
        end;
        
    ModelMaker displays this value in the Critic Manager thus:

    Illustration of where the Critic Manager displays the Category property.

  5. Override the GetAuthor, GetCriticName, GetDescription, and GetHeadline public methods. The GetCriticName method should return something unique from what I understand in the documentation.
        type
          TsdSomeDocCritic = class(TMMDesignCritic)
          public
            constructor Create;
            function GetAuthor: WideString; override; safecall;
            function GetCriticName: WideString; override; safecall;
            function GetDescription: WideString; override; safecall;
            function GetHeadLine: WideString; override; safecall;
          end;
    
        implementation
    
        constructor TsdSomeDocCritic.Create;
        begin
          inherited Create;
          Category := 'Documentation';
        end;
    
        function TsdSomeDocCritic.GetAuthor: WideString;
        begin
          Result := 'IFM Services, LLC';
        end;
    
        function TsdSomeDocCritic.GetCriticName: WideString;
        begin
          Result := 'Non-zero Documentation Critic';
        end;
    
        function TsdSomeDocCritic.GetDescription: WideString;
        begin
          Result := 'This critic ensures that classes and members have one-line or multi-line documentation (inclusive or).';
        end;
    
        function TsdSomeDocCritic.GetHeadLine: WideString;
        begin
          Result := 'Check for some documentation';
        end;
        
    ModelMaker displays the GetAuthor, GetDescription, and GetHeadline methods in the Critic Manager thus:

    Illustration of where the Critic Manager displays the Category property.

Step Three: Finish Project File

Now that there's a class, we can fix up the TODO item left in the project file:

procedure InitializeExpert(const Srv: IMMToolServices); stdcall;
begin
  // (Required action. See demo code.)
  MMToolServices := Srv;
  // Add design critic
  MMToolServices.CriticManager.AddCritic(TsdSomeDocCritic.Create);
  // Sync with parent window, (Required action. See demo code.)
  Application.Handle := Srv.GetParentHandle;
end;

Step Four: Add the Guts

At this point we should have a valid design critic that does absolutely nothing. Now we add a method that examines each class, interface, and member, ensuring each has some form of documentation. Any class, interface, member that has no documentation at all will be reported back to ModelMaker.

  1. Override the Refresh method.
        type
          TsdSomeDocCritic = class(TMMDesignCritic)
          public
            constructor Create;
            function GetAuthor: WideString; override; safecall;
            function GetCriticName: WideString; override; safecall;
            function GetDescription: WideString; override; safecall;
            function GetHeadLine: WideString; override; safecall;
            procedure Refresh; override; safecall;
          end;
        
  2. Add MMEngineDefs to the unit implementation uses clause. The MMEngineDefs unit contains various type and constant definitions we need to examine elements in the ModelMaker project.
        implementation
    
        uses
          MMEngineDefs;
        
  3. The rest of this is straightforward, but is easier to show in code than to describe in text. Examine the sample implementation's comments. The basic idea is the following steps:
    1. Loop to examine each class and interface. Note that ModelMaker represents both classes and interfaces though the IMMClassBase interface. This allows us to attack both classes and interfaces in a simple manner.
    2. Skip placeholder classes and interfaces, e.g. IUnknown and TObject.
    3. Check the documentation for the class or interface. If it is completely empty, report it to ModelMaker.
    4. Loop to examine each member.
    5. Check the documentation for the member. If it is completely empty, report it to ModelMaker.
        procedure TsdSomeDocCritic.Refresh;
    
          procedure AddMessage(aEntity :IInterface; aMessage :WideString);
          var
            lNewMessage: IMMMessage;
          begin
            // This method reports a problem to ModelMaker
            // (Taken directly from demo.)
            lNewMessage := MMToolServices.MessageServer.CreateMessage(CriticID, MMCriticsContainer);
            lNewMessage.HeadLine := aMessage;
            lNewMessage.Priority := Priority;
            lNewMessage.Category := Category;
            lNewMessage.ReferToEntity(aEntity);
          end;
    
        var
          i, j: Integer;
          lCodeModel: IMMCodeModel;
          lClass: IMMClassBase;
          lClassName: WideString;
          lMember: IMMMember;
          lMemberName :WideString;
        begin
          // All of the setup and teardown code came from the demo.
          lCodeModel := MMToolServices.CodeModel;
          MMToolServices.MessageServer.BeginUpdate;
          try
            MMToolServices.MessageServer.DeleteOwner(CriticID);
            //
            // Examine each class
            //
            for i := 0 to Pred(lCodeModel.ClassCount) do
            begin
              lClass := lCodeModel.Classes[i];
              if not Assigned(lClass) or not lClass.Valid then Continue;
              //
              // Skip placeholder classes completely
              //
              if lClass.Options[classPlaceHolder] then Continue;
              //
              // Check class' documentation.
              //
              lClassName := lClass.Name;
              if (lClass.OneLiner='') and (lClass.Documentation='') then
                AddMessage(lClass, Format('Class %s has no documentation', [lClassName]));
              //
              // Examine each class member
              //
              for j := 0 to Pred(lClass.MemberCount) do
              begin
                lMember := lClass.Members[j];
                if not Assigned(lMember) or not lMember.Valid then Continue;
                // 
                // Check member's documentation.
                //
                lMemberName := lMember.Name;
                if (lMember.OneLiner='') and (lMember.Documentation='') then
                  AddMessage(lMember, Format('Member %s.%s has no documentation', [lClassName, lMemberName]));
              end;
            end;
          finally
            MMToolServices.MessageServer.EndUpdate;
          end;
        end;
        

Step Five: Adding the Critic to ModelMaker

The following simple steps add the critic to ModelMaker.

  1. Shut down ModelMaker if it is running.
  2. Copy the DLL to the Experts directory.
  3. Start ModelMaker.

To use, open the Messages window, click on Critics, and press the refresh button.

Additional Topic: Debugging

Debugging a critic is the same as debugging any DLL in Delphi.

  1. Go to the Run | Run Parameters menu item.
  2. For Host Application, select the ModelMaker executable, e.g. C:\Program Files\ModelMaker\ModelMaker\6.1\bin\mm6.exe.
  3. Add the -nosplash command line parameter in the lower edit box for convenience.
  4. Add the appropriate breakpoints or perform other relevant debugging actions as one would with any other application.
Happy bug slaying!

Download

Sample source files are here.

Sample source files for the enhanced version are here.

Enhancements

I've since enhanced this critic to be a little more intelligent, and to handle the specific needs of the project. These enhancements have been pretty straightforward so far. For example:

  1. Ignore constructors and destructors. Their purpose should be obvious.
  2. Ignore property implementation methods (accessors and mutators, also known as "getters" and "setters"). Their purpose should be obvious also.
  3. Make special class factory "coclass" documentation special, as we document the class function(s) themselves. The coclass is rather obvious and doesn't need its own documentation. Note that this is a special quirk for our own specific situation.

Here is code to modify the TsdSomeDocCritic.Refresh method (above) that will ignore constructors, destructors, property fields, and property methods:

var
..
  lField: IMMField;
  lMethod :IMMMethod;
...
      //
      // Examine each class member
      //
      for j := 0 to Pred(lClass.MemberCount) do
      begin
        lMember := lClass.Members[j];
        if not Assigned(lMember) or not lMember.Valid then Continue;
        // 
        if lMember.MemberType=cpMethod then
        begin
          lMethod := lMember as IMMMethod;
          // Skip constructors and destructors 
          if lMethod.MethodKind in [mkConstructor, mkDestructor] then
            Continue;
          // Skip property methods
          if lMethod.Options[methodAccessMethod] then
            Continue;
        end
        else if lMember.MemberType=cpField then
        begin
          lField := lMember as IMMField;
          // Skip property fields
          if lField.Options[fieldReserved3] then
            Continue;
        end;
        //
        // Check member's documentation.
        //
        lMemberName := lMember.Name;
        if (lMember.OneLiner='') and (lMember.Documentation='') then
          AddMessage(lMember, Format('Member %s.%s has no documentation', [lClassName, lMemberName]));
      end;

Final Word

This web page was designed to give a short tutorial. I make no guarantee that any of this information is correct. If this does work, or if you break your computer trying to use this information, that's the risk you take.

Comments Welcome

If you have comments, wish to report a mistake, or make any other suggestion, please send e-mail. I will try to respond as soon as possible.

Home | Papers | ModelMaker | Basic Design Critic
Copyright © 2003 James Knowles
All rights reserved.