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:
- Get a list of units from a DPR file.
- 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.

It did not work Access violation on line
PreambleSize := TEncoding.GetBufferEncoding(Buffer, Encoding, TEncoding.Default);
Compiled on Delphi xe
changed
Content.Read(Buffer, 0, Content.Size); to Content.Read(Buffer, Content.Size);
Try to change it to Content.Read(Buffer[0], Content.Size);
OK fixed
Roman, I am trying to make use of DelphiAST, but have no training with formal parser development, so it is a bit overwhelming all at once. Is it always the case that you would produce the tree in XML, and then use DOM? Or is it possible to collect information directly form the parser? Right now, I am focused on two main goals, the first is to collect the uses clauses, and the second is to collect a list of all public or published members from each module in a large project.