How to get a list of used units with DelphiAST

Getting a list of used units from the source code can be useful in many code automation tasks. There are a number of tools that can help you to investigate what dependencies you have in your project. In this post I will show how you can write your own simple dependency analysis tool using DelphiAST library.

This demo program will count how many times each unit is used by other units and will consist of two steps:

  1. Get a list of units from a DPR file.
  2. Go through this list, analyze uses statements and count used units.

To keep demo as simple as possible, it will ignore a unit if its path was not mentioned in a DPR.

As Delphi strings and chars are Unicode, before starting to parse any file we have to convert it to Unicode. This function reads a file to a string, converting it to Unicode if needed.

function TForm1.ReadFileToString(const FileName: string): string;
var
  Buffer: TBytes;
  Content: TMemoryStream;
  Encoding: TEncoding;
  PreambleSize: Integer;
begin
  Encoding := nil;

  Content := TMemoryStream.Create;
  try
    Content.LoadFromFile(FileName);

    SetLength(Buffer, Content.Size);
    Content.Read(Buffer, 0, Content.Size);

    PreambleSize := TEncoding.GetBufferEncoding(Buffer, Encoding, TEncoding.Default);

    Result := Encoding.GetString(Buffer, PreambleSize, Content.Size - PreambleSize);
  finally
    Content.Free;
  end;
end;

And now we’re getting to the fun part, to parsing.

function TForm1.Parse(const Content: string): string;
var
  ASTBuilder: TPasSyntaxTreeBuilder;
  StringStream: TStringStream;
  SyntaxTree: TSyntaxNode;
begin
  Result := '';

  StringStream := TStringStream.Create(Content, TEncoding.Unicode);
  try
    StringStream.Position := 0;

    ASTBuilder := TPasSyntaxTreeBuilder.Create;
    try
      ASTBuilder.InitDefinesDefinedByCompiler;

      SyntaxTree := ASTBuilder.Run(StringStream);
      try
        Result := TSyntaxTreeWriter.ToXML(SyntaxTree);
      finally
        SyntaxTree.Free;
      end;
    finally
      ASTBuilder.Free;
    end;
  finally
    StringStream.Free;
  end;
end;

This function returns parser’s output as an XML in a string. The coolest fact about XML is that it makes possible to query the syntax tree using XPath. You will see some examples below.

Let’s see how a DPR syntax tree looks like.

<UNIT line="1" col="1" name="UnitCounter">
  <USES line="3" col="1">
    <UNIT line="4" col="3" name="Vcl.Forms"/>
    <UNIT line="5" col="3" name="frmMain" path="frmMain.pas">
      <EXPRESSION line="5" col="14">
        <LITERAL line="5" col="35" type="string" value="frmMain.pas"/>
      </EXPRESSION>
    </UNIT>
  </USES>
  <STATEMENTS end_line="14" begin_line="10" end_col="1" begin_col="3">
    <CALL line="10" col="3">
      <DOT>
        <IDENTIFIER line="10" col="3" name="Application"/>
        <IDENTIFIER line="10" col="15" name="Initialize"/>
      </DOT>
    </CALL>
    <ASSIGN line="11" col="3">
      <LHS>
        <DOT>
          <IDENTIFIER line="11" col="3" name="Application"/>
          <IDENTIFIER line="11" col="15" name="MainFormOnTaskbar"/>
        </DOT>
      </LHS>
      <RHS>
        <EXPRESSION line="11" col="36">
          <IDENTIFIER line="11" col="36" name="True"/>
        </EXPRESSION>
      </RHS>
    </ASSIGN>
    <CALL line="12" col="3">
      <CALL>
        <DOT>
          <IDENTIFIER line="12" col="3" name="Application"/>
          <IDENTIFIER line="12" col="15" name="CreateForm"/>
        </DOT>
        <EXPRESSIONS>
          <EXPRESSION line="12" col="26">
            <IDENTIFIER line="12" col="26" name="TForm1"/>
          </EXPRESSION>
          <EXPRESSION line="12" col="34">
            <IDENTIFIER line="12" col="34" name="Form1"/>
          </EXPRESSION>
        </EXPRESSIONS>
      </CALL>
    </CALL>
    <CALL line="13" col="3">
      <DOT>
        <IDENTIFIER line="13" col="3" name="Application"/>
        <IDENTIFIER line="13" col="15" name="Run"/>
      </DOT>
    </CALL>
  </STATEMENTS>
</UNIT>

Definetely, we are interested in UNIT/USES subtree.

Thats how it looks like in Delphi, it couldn’t be easier.

procedure TForm1.GetFileList(const RootFolder, ParsedDpr: string;
  const Files: TStringList);
var
  XmlDoc: IXMLDOMDocument2;
  UnitNodes: IXMLDOMNodeList;
  PathAttrNode: IXMLDOMNode;
  I: Integer;
  FileName: string;
begin
  Files.Clear;

  XmlDoc := CoDOMDocument60.Create;
  XmlDoc.SetProperty('SelectionLanguage', 'XPath');
  XmlDoc.validateOnParse := False;
  XmlDoc.preserveWhiteSpace := False;
  XmlDoc.resolveExternals := False;
  XmlDoc.loadXML(ParsedDpr);

  // select all units nodes
  UnitNodes := XmlDoc.documentElement.selectNodes('/UNIT/USES/UNIT');
  for I := 0 to UnitNodes.length - 1 do
  begin
    // if 'path' attribute exists, then use it
    PathAttrNode := UnitNodes.item[I].attributes.getNamedItem('path');
    if Assigned(PathAttrNode) then
      FileName := PathAttrNode.nodeValue
    else
      FileName := UnitNodes.item[I].attributes.getNamedItem('name').nodeValue + '.pas';

    FileName := ExpandFileName(TPath.Combine(RootFolder, FileName));

    // ignore units that placed somewhere on the search path
    if FileExists(FileName) then
      Files.Add(FileName);
  end;
end;

Now, when we have a list of units file names we can start to calculate.

That’s how a unit syntax tree looks like. I have skipped not interesting but too long parts. You can use DelphiAST demo application to see a full syntax tree for any unit.

<UNIT line="1" col="1" name="frmMain">
  <INTERFACE line="3" col="1">
    <USES line="5" col="1">
      <UNIT line="6" col="3" name="Winapi.Windows"/>
      <UNIT line="6" col="19" name="Winapi.Messages"/>
      <UNIT line="6" col="36" name="System.SysUtils"/>
      <UNIT line="6" col="53" name="System.Variants"/>
      <UNIT line="6" col="70" name="System.Classes"/>
      <UNIT line="6" col="86" name="Vcl.Graphics"/>
      <UNIT line="7" col="3" name="Vcl.Controls"/>
      <UNIT line="7" col="17" name="Vcl.Forms"/>
      <UNIT line="7" col="28" name="Vcl.Dialogs"/>
      <UNIT line="7" col="41" name="Vcl.StdCtrls"/>
      <UNIT line="7" col="55" name="Generics.Collections"/>
    </USES>
    <SKIPPED>..</SKIPPED>
  </INTERFACE>
  <IMPLEMENTATION line="27" col="1">
    <USES line="29" col="1">
      <UNIT line="30" col="3" name="DelphiAST"/>
      <UNIT line="30" col="14" name="DelphiAST.Classes"/>
      <UNIT line="30" col="33" name="DelphiAST.Writer"/>
      <UNIT line="30" col="51" name="MSXML2_TLB"/>
      <UNIT line="30" col="63" name="IOUtils"/>
    </USES>
    <SKIPPED>..</SKIPPED>
  </IMPLEMENTATION>
</UNIT>

We are interested in particular name attribute of /UNIT/INTERFACE/USES/UNIT and /UNIT/IMPLEMENTATION/USES/UNIT nodes.

procedure TForm1.CalculateUnits(const ParsedUnit: string;
  const Stats: TDictionary<string, Integer>);
var
  XmlDoc: IXMLDOMDocument2;
  UnitNodes: IXMLDOMNodeList;
  I: Integer;
  UnitName: string;
begin
  XmlDoc := CoDOMDocument60.Create;
  XmlDoc.SetProperty('SelectionLanguage', 'XPath');
  XmlDoc.validateOnParse := False;
  XmlDoc.preserveWhiteSpace := False;
  XmlDoc.resolveExternals := False;
  XmlDoc.loadXML(ParsedUnit);

  UnitNodes := XmlDoc.documentElement.selectNodes('/UNIT/INTERFACE/USES/UNIT/@name|/UNIT/IMPLEMENTATION/USES/UNIT/@name');

  for I := 0 to UnitNodes.length - 1 do
  begin
    UnitName := UnitNodes.item[I].nodeValue;

    if not Stats.ContainsKey(UnitName) then
      Stats.Add(UnitName, 0);

    Stats[UnitName] := Stats[UnitName] + 1;
  end;
end;

And last, but not least, let’s put all parts together.

var
  Units: TStringList;
  FileName: string;
  Stats: TDictionary<string, Integer>;
  UnitCounter: TPair<string, Integer>;
begin
  if OpenDialog.Execute then
  begin
    Units := TStringList.Create;
    try
      GetFileList(ExtractFilePath(OpenDialog.FileName),
        Parse(ReadFileToString(OpenDialog.FileName)), Units);

      Stats := TDictionary<string, Integer>.Create;
      try
        for FileName in Units do
          CalculateUnits(Parse(ReadFileToString(FileName)), Stats);

        memUnits.Clear;
        for UnitCounter in Stats do
          memUnits.Lines.Add(UnitCounter.Key + ' : ' + IntToStr(UnitCounter.Value));
      finally
        Stats.Free;
      end;
    finally
      Units.Free;
    end;
  end;

That’s it. You can see an example of its output on the screenshot in the beginning of this post.

You can download the full source code here, DelphiAST is available on GitHub. This application is much less complicated than this kind of application is usually expected to be. DelphiAST is a powerful tool and it is a must have if you are going to automatically process your Delphi source code.