Skip to content

RubberduckParserState

Max Dörner edited this page Jul 15, 2017 · 7 revisions

Parser state is accessed by various features, from inspections to refactorings and toolwindows: it's constructor-injected in singleton scope by Ninject, so any class that needs to work with parser state simply needs to have a RubberduckParserState constructor parameter.

The parser state raises events to indicate a change in state; code that requires a specific parser state to work should register the StateChanged and/or ModuleStateChanged event, and determine what to do depending on current state.

ParserState enum

Any given module is always in one of the following states:

public enum ParserState
{
    /// <summary>
    /// Parse was requested but hasn't started yet.
    /// </summary>
    Pending,
    /// <summary>
    /// Parser state is in sync with the actual code in the VBE.
    /// </summary>
    Ready,
    /// <summary>
    /// Code from modified modules is being parsed.
    /// </summary>
    Parsing,
    /// <summary>
    /// Parse tree is waiting to be walked for identifier resolution.
    /// </summary>
    Parsed,
    /// <summary>
    /// Resolving identifier references.
    /// </summary>
    Resolving,
    /// <summary>
    /// Parsing could not be completed for one or more modules.
    /// </summary>
    Error
}

The RubberduckParserState itself also uses this enum to indicate "global state": when all modules are in the same state, the parser state itself is in that state. When any module is in Parsing, Resolving or Error state, then the parser state itself is in that state.

Features that require resolved identifier usages (inspections, refactorings, etc.) should wait for a global Ready state; features that merely enable navigation and/or comments (e.g. code explorer), don't need to wait for the resolver to complete and can wait for a global Parsed state instead.


Declarations

When the parser is in a Parsed state, all modules have successfully parsed, and all parse trees have been successfully walked and all declarations have been identified.

Use the AllDeclarations property to return all Declaration objects.

    /// <summary>
    /// Gets a copy of the collected declarations, including the built-in ones.
    /// </summary>
    public IEnumerable<Declaration> AllDeclarations { get { return _declarations.Keys.ToList(); } }

Built-in declarations

Rubberduck creates a vast amount of Declaration objects to represent built-in modules and members of the VBA standard library, as well as some from host application object models, such as the Excel object model. A lot of features don't need to account for these built-in declarations.

Use the AllUserDeclarations property to retun all Declaration objects that only involve user code.

    /// <summary>
    /// Gets a copy of the collected declarations, excluding the built-in ones.
    /// </summary>
    public IEnumerable<Declaration> AllUserDeclarations { get { return _declarations.Keys.Where(e => !e.IsBuiltIn).ToList(); } }

What's a declaration anyway?

Shortly put: any identifier that can be referred to in VBA code. In other words, it's a project, it's a module, a class, a property getter, a function, a variable, a constant, an event, its parameters, line labels, ..literally everything.

Declarations have a References collection that, once identifier resolution is completed, contains all IdentifierReference objects that point to it.

By inspecting the declarations and their references, we can programmatically determine whether a variable is used or not, what scope they're declared in and how visible they are, and where and how they're used.


Refreshing the parser state

As soon as we programmatically modify a code module, all declarations and usages point to the wrong locations and need to be refreshed. While we only need to re-parse the modified module, we cannot be sure that identifier references were added or removed, that belong to declarations that aren't in the module we modified - for example if we have a DoSomething method in Class1 and we modified Module1 to add a call to Class1.DoSomething, the declaration for DoSomething needs its references updated. Hence, even when we parse a single module, the resolver needs to re-process every single parse tree.

Code that has a RubberduckParserState dependency can trigger a global re-parse by calling the OnParseRequested method on the parser state:

_state.OnParseRequested();

To trigger the re-parsing of a specific component, pass the modified VBComponent as a parameter:

_state.OnParseRequested(modifiedModule);

The parser processes everything asynchronously, so the IDE/UI should not be affected by the intense processing going on in the background.

Careful!

Requesting a re-parse will change the parser state, and all features that have code to execute when parser state changes, will run that code. For this reason, the code that runs on the UI thread upon a change in parser state should be kept to a minimum, to avoid blocking the UI. Consider implementing extensive work on a background thread.

Clone this wiki locally