Task:
My goal is to find all numbered lines in procedures of my Code Modules. The CodeModule.Find method can be used to check for search terms (target parameter).
Syntax:
object.Find(target, startline, startcol, endline, endcol [, wholeword] [, matchcase] [, patternsearch])
The referring help site https://msdn.microsoft.com/en-us/library/aa443952(v=vs.60).aspx states: parameter patternsearch: Optional. A Boolean value specifying whether or not the target string is a regular expression pattern. If True, the target string is a regular expression pattern. False is the default.
As explained above the find method allows a regex pattern search, which I would like to use in order to identify numbered lines in a precise way: digits followed by a tab. The example below therefore defines a search string s and sets the last parameter PatternSearch in the .Find method to True.
Problem AFAIK a valid regex definition could be
s = "[0-9]{1,4}[ \t]"
but that doesn't show anything, not even an error.
In order to show at least any results, I defined the search term
s = "[0-9]*[ \t]*)"
in the calling example procedure ListNumberedLines showing erratic results.
Question
Is there any possibility to use a valid regex patternsearch in the CodeModule.Find method?
Example code
Option Explicit
' ==============
' Example Search
' ==============
Sub ListNumberedLines()
' Declare search pattern string s
Dim S As String
10 S = "[0-9]*[ \t]*)"
20 Debug.Print "Search Term: " & S
30 Call findWordInModules(S)
End Sub
Public Sub findWordInModules(ByVal sSearchTerm As String)
' Purpose: find modules ('components') with lines containing a search term
' Method: .CodeModule.Find with last parameter patternsearch set to True
' Based on https://www.devhut.net/2016/02/24/vba-find-term-in-vba-modulescode/
' VBComponent requires reference to Microsoft Visual Basic for Applications Extensibility
' or keep it as is and use Late Binding instead
' Declare module variable oComponent
Dim oComponent As Object 'VBComponent
For Each oComponent In Application.VBE.ActiveVBProject.VBComponents
If oComponent.CodeModule.Find(sSearchTerm, 1, 1, -1, -1, False, False, True) = True Then
Debug.Print "Module: " & oComponent.Name 'Name of the current module in which the term was found (at least once)
'Need to execute a recursive listing of where it is found in the module since it could be found more than once
Call listLinesinModuleWhereFound(oComponent, sSearchTerm)
End If
Next oComponent
End Sub
Sub listLinesinModuleWhereFound(ByVal oComponent As Object, ByVal sSearchTerm As String)
' Purpose: list module lines containing a search term
' Method: .CodeModule.Find with last parameter patternsearch set to True
Dim lTotalNoLines As Long 'total number of lines within the module being examined
Dim lLineNo As Long 'will return the line no where the term is found
lLineNo = 1
With oComponent ' Module
lTotalNoLines = .CodeModule.CountOfLines
Do While .CodeModule.Find(sSearchTerm, lLineNo, 1, -1, -1, False, False, True) = True
Debug.Print vbTab & "Zl. " & lLineNo & "|" & _
Trim(.CodeModule.Lines(lLineNo, 1)) 'Remove any padding spaces
lLineNo = lLineNo + 1 'Restart the search at the next line looking for the next occurence
Loop
End With
End Sub
For the longest time, Rubberduck was struggling with properly/formally parsing line numbers - our work-around was to remove them (replacing them with spaces) before feeding the code module contents to our parser.
Recently we've managed to formally define line numbers:
And
lineNumberLabel
is defined as:(full Antlr4 grammar here)
Notice the predicate
{_input.La(-1) == NEWLINE}?
, which force the parser rule to only match astatementLabelDefinition
at the start of a line - a logical line of code.You see VBA code has physical code lines, like what you're getting from the
CodeModule
's contents. But VBA code also has a concept of logical code lines, and it turns out that is all the parser cares about.This would trip any typical regex:
There's only 1 logical line of code between the signature and the
End Sub
token, but a simpleFind
will happily consider that42
as a "line number" ...which it isn't - it's the argument passed toDebug.Print
, in the same instruction, on the same logical code line, but on the next physical code line.And you can't be dealing with logical code lines without first pre-processing your input, to take line continuation tokens into account.
And in order to do that, you need to
actually parse the instructions you're seeing - at least know where they start and where they end... and that's no small undertaking!see ThunderFrame's answerThe VBIDE API is extremely limited, and won't be helpful for that.
TL;DR: You can't parse VBA code with regular expressions alone. So,
nope. Sorry!you need a much more complex regex pattern than that - see ThunderFrame's answer.As @MatsMug says, parsing VBA with Regex is
hardimpossible, but line-numbers are a simpler case, and should be findable with regex alone.Fortunately, line numbers can only appear within a procedure body (including before the
End Sub/Function/Property
statement), so we know they'll never be the first line of your code.Unfortunately, you can prefix a line-label with 0 or more line continuations:
Furthermore, a line number isn't always followed by a space - it can be followed by an instruction separator, giving the line-number the appearance of a line-label:
And if you're code is evil, you might encounter a negative line-number (entered by using hex notation - which VBE dutifully pretty prints back to the code-pane with a leading space and a negative number):
And we also need to be able to identify numbers that appear on a continued line, that aren't line-numbers:
So, here's some evil line-numbering, with a mix of all of those edge-cases:
And here's some regex that identifies the line-numbers:
And here's a syntax highlight from regex101:
Conclusion regarding CodeModule.Find via search pattern
Firstly, CodeModule.Find doesn't help via search pattern and its possible use is intransparent. I agree that the VBIDE API is extremely limited and that there exist excellent professional tools which I highly recommand for any programmer :-)
Consequence: Work around via XML
Secondly I prefer household remedies if possible, so I tried to find an alternative solution using only the helpful parts of VBIDE.
Method That is why I tried a simple xml conversation of the CodeModule.Lines allowing a flexible search within logical lines. Instead of using regular expressions in requesting the xml data, I demonstrate a method to find leading numbers via a well defined XPath search (loop thru node list), thus resolving most problems shown by @ThunderFrame. The search string in function showErls is defined as "line[substring(translate(.,'0123456789','¹¹¹¹¹¹¹¹¹¹'),1,1)="¹"]"
Furthermore function 'lineNumber' returns the logical line number within the module. Note: To keep it simple, the search is restrained to one module only (user defined constant MYMODULE) and code avoids any regex.
Work around code - main sub
Sub procedures