I have an issue opened on GitHub since mid-November, to refactor & DRY up the Rubberduck Parser
module (the idea is to model the code in a VBA project or code module, so it can be browsed and inspected programmatically).
Yes, I've written this ........crap. I'm guilty of Ctrl+C/Ctrl+V coding here, of which I'm very unproud, but I'm not sure if it's because I messed up or because it's just the way it has to be (please, no!).
[ComVisible(false)]
public class Parser
{
private readonly IEnumerable<ISyntax> _grammar;
public Parser(IEnumerable<ISyntax> grammar)
{
_grammar = grammar;
}
public SyntaxTreeNode Parse(VBProject project)
{
var nodes = new List<SyntaxTreeNode>();
try
{
var components = project.VBComponents.Cast<VBComponent>().ToList();
foreach (var component in components)
{
var lineCount = component.CodeModule.CountOfLines;
if (lineCount <= 0)
{
continue;
}
var code = component.CodeModule.Lines[1, lineCount];
var isClassModule = component.Type == vbext_ComponentType.vbext_ct_ClassModule
|| component.Type == vbext_ComponentType.vbext_ct_Document
|| component.Type == vbext_ComponentType.vbext_ct_MSForm;
nodes.Add(Parse(project.Name, component.Name, code, isClassModule));
}
}
catch
{
// todo: handle exception like a chief
Debug.Assert(false);
}
return new ProjectNode(project, nodes);
}
/// <summary>
/// Converts VBA code into a <see cref="SyntaxTreeNode"/>.
/// </summary>
/// <param name="projectName">The name of the VBA Project, used for scoping public nodes.</param>
/// <param name="componentName">The name of the module, used for scoping private nodes.</param>
/// <param name="code">The code to parse.</param>
/// <returns></returns>
public SyntaxTreeNode Parse(string projectName, string componentName, string code, bool isClassModule)
{
var content = SplitLogicalCodeLines(projectName, componentName, code);
var memberNodes = ParseModuleMembers(projectName, componentName, content).ToList();
var result = new ModuleNode(projectName, componentName, memberNodes, isClassModule);
return result;
}
A bit of VBA theory here. In VBA, a code line can be "continued" using a "line continuation character", like this:
Dim foo As Integer, _
bar As String, _
baz As SomethingElse
The code module will give me 3 lines for the above, but it's really 1 "logical" code line, with one instruction that declares 3 variables. The SplitLogicalCodeLines
method scans the VBA source code and returns a LogicalCodeLine
for each, well, logical code line:
private IEnumerable<LogicalCodeLine> SplitLogicalCodeLines(string projectName, string componentName, string content)
{
const string lineContinuationMarker = "_";
var lines = content.Split('\n').Select(line => line.Replace("\r", string.Empty)).ToList();
var logicalLine = new StringBuilder();
var startLine = 0;
var isContinuing = false;
for (var index = 0; index < lines.Count; index++)
{
if (!isContinuing)
{
startLine = index + 1;
}
var line = lines[index];
if (line.EndsWith(lineContinuationMarker))
{
isContinuing = true;
logicalLine.Append(line.Remove(line.Length - 1));
}
else
{
logicalLine.Append(line);
yield return new LogicalCodeLine(projectName, componentName, startLine, index + 1, logicalLine.ToString());
logicalLine.Clear();
isContinuing = false;
}
}
}
Each logical code line can contain a number of instructions, each separated by an instruction separator character - in VBA that's the colon (:
). I've implemented that part in the LogicalCodeLine
structure, and there's a little issue with properly splitting instructions when an instruction contains a string literal that contains a colon, so I'm not including that struct
for review.
So far so good? Here's the annoying/offending code: ParseCodeBlock
is pretty much a carbon copy of ParseModuleMembers
- this is what I'd like to clean up:
private IEnumerable<SyntaxTreeNode> ParseModuleMembers(string publicScope, string localScope, IEnumerable<LogicalCodeLine> logicalCodeLines)
{
var currentLocalScope = localScope;
var lines = logicalCodeLines.ToArray();
for (var index = 0; index < lines.Length; index++)
{
var line = lines[index];
if (string.IsNullOrWhiteSpace(line.Content))
{
continue;
}
var instructions = line.SplitInstructions();
foreach (var instruction in instructions)
{
var parsed = false;
foreach (var syntax in _grammar.Where(s => !s.IsChildNodeSyntax))
{
SyntaxTreeNode node;
if (!syntax.IsMatch(publicScope, currentLocalScope, instruction, out node))
{
continue;
}
if (syntax.Type.HasFlag(SyntaxType.HasChildNodes))
{
var codeBlockNode = node as CodeBlockNode;
if (codeBlockNode != null)
{
if (node is ProcedureNode)
{
currentLocalScope = localScope + "." + (node as ProcedureNode).Identifier.Name;
yield return ParseProcedure(publicScope, currentLocalScope, node as ProcedureNode, lines, ref index);
currentLocalScope = localScope;
parsed = true;
break;
}
yield return ParseCodeBlock(publicScope, currentLocalScope, codeBlockNode, lines, ref index);
currentLocalScope = localScope;
parsed = true;
break;
}
}
yield return node;
parsed = true;
}
if (!parsed)
{
yield return new ExpressionNode(instruction, currentLocalScope);
}
}
}
}
ParseCodeBlock
is very similar:
private SyntaxTreeNode ParseCodeBlock(string publicScope, string localScope, CodeBlockNode codeBlockNode, IEnumerable<LogicalCodeLine> logicalLines, ref int index)
{
var ifBlockNode = codeBlockNode as IfBlockNode;
if (ifBlockNode != null && !string.IsNullOrEmpty(ifBlockNode.Expression.Value))
{
return codeBlockNode;
}
var result = codeBlockNode;
var grammar = result.ChildSyntaxType == null
? _grammar.Where(syntax => !syntax.IsChildNodeSyntax).ToList()
: _grammar.Where(syntax => syntax.IsChildNodeSyntax && syntax.GetType() == result.ChildSyntaxType).ToList();
var logicalCodeLines = logicalLines as LogicalCodeLine[] ?? logicalLines.ToArray();
var lines = logicalCodeLines.ToArray();
var currentIndex = ++index;
while (currentIndex < lines.Length && !result.EndOfBlockMarkers.Any(marker => lines[currentIndex].Content.Trim().StartsWith(marker)))
{
var line = lines[currentIndex];
if (string.IsNullOrWhiteSpace(line.Content))
{
currentIndex++;
continue;
}
var instructions = line.SplitInstructions();
foreach (var instruction in instructions)
{
var parsed = false;
foreach (var syntax in grammar)
{
SyntaxTreeNode node;
if (!syntax.IsMatch(publicScope, localScope, instruction, out node))
{
continue;
}
var childNode = node as CodeBlockNode;
if (childNode != null)
{
node = ParseCodeBlock(publicScope, localScope, childNode, logicalCodeLines, ref currentIndex);
}
result.AddNode(node);
parsed = true;
break;
}
if (!parsed)
{
result.AddNode(new ExpressionNode(instruction, localScope));
}
}
if (lines[currentIndex + 1].Content.Trim().StartsWith(ReservedKeywords.Else))
{
break;
}
currentIndex++;
}
index = currentIndex;
return result;
}
And, cherry on top, ParseProcedure
is just as annoying:
private SyntaxTreeNode ParseProcedure(string publicScope, string localScope, ProcedureNode procedureNode, IEnumerable<LogicalCodeLine> logicalLines, ref int index)
{
var result = procedureNode;
var grammar = VBAGrammar.GetGrammarSyntax().Where(s => !s.IsChildNodeSyntax).ToList();
var logicalCodeLines = logicalLines as LogicalCodeLine[] ?? logicalLines.ToArray();
var lines = logicalCodeLines.ToArray();
var currentIndex = ++index;
while (currentIndex < lines.Length && !result.EndOfBlockMarkers.Any(marker => lines[currentIndex].Content.Trim().StartsWith(marker)))
{
var line = lines[currentIndex];
if (string.IsNullOrWhiteSpace(line.Content))
{
currentIndex++;
continue;
}
var instructions = line.SplitInstructions();
foreach (var instruction in instructions)
{
var parsed = false;
foreach (var syntax in grammar)
{
SyntaxTreeNode node;
if (!syntax.IsMatch(publicScope, localScope, instruction, out node))
{
continue;
}
if (node.HasChildNodes)
{
var childNode = node as CodeBlockNode;
if (childNode != null)
{
node = ParseCodeBlock(publicScope, localScope, childNode, logicalCodeLines, ref currentIndex);
}
}
result.AddNode(node);
parsed = true;
break;
}
if (!parsed)
{
result.AddNode(new ExpressionNode(instruction, localScope));
}
}
currentIndex++;
}
index = currentIndex;
return result;
}
}
How do I clean up this mess and keep all my parser tests passing?
Some additional context:
- A procedure is a code block that defines a scope.
- A code block has child nodes and a block ending marker, and wouldn't define a scope unless it's a
ProcedureNode
. - Anything that can't be parsed, is because the syntax hasn't been implemented yet - the code generates an
ExpressionNode
when that happens. Eventually the parser will only produce anExpresionNode
for expressions - for now I can use the Rubberduck Code Explorer and see exactly what fails to parse correctly and whatISyntax
implementations need to be worked on: