Nothing to declare

Somewhere in the first batch of issues/to-do’s we created when we started Rubberduck on GitHub (Issue# 33 actually), there was the intention to create a tool that could locate undeclared variables, because even if you and I use Option Explicit and declare all our variables, we have brothers and sisters that have to deal with code bases that don’t.

So we tried… but Rubberduck simply couldn’t do this with the 1.x resolver: identifiers that couldn’t be resolved were countless, running an inspection that would pop a result for every single one of them would have crippled our poor little duckling… so we postponed it.

The 2.0 resolver however, thinks quite literally like VBA itself, and knows about all available types, members, globals, locals, events, enums and whatnot, not just in the VBA project, but also in every referenced COM library: if something returns a type other than Variant or Object, Rubberduck knows about it.

The role of the resolver is simple: while the parse tree of a module is being traversed, every time an identifier is encountered it attempts to determine which declaration is being referred to. If the resolver finds a corresponding declaration, an IdentifierReference is created and added to the Declaration instance. And when the resolver can’t resolve the identifier (i.e. locate the exact declaration the identifier is referring to), a null reference was returned and, unless you have detailed logging enabled, nothing notable happens.

As of the last build, instead of “doing nothing” when a reference to variable can’t be resolved to the declaration of that variable, we create a declaration on the spot: so the first appearance of a variable in an executable statement becomes the “declaration”.

We create an implicit Variant variable declaration to work with, and then this happens:

hhp2m

With a Declaration object for an undeclared variable, any further reference to the same implicit variable would simply resolve to that declaration – this means other Rubberduck features like find all references and refactor/rename can now be used with undeclared variables too.

Rubberduck is now seeing the whole picture, with or without Option Explicit.

The introduce local variable quick-fix simply inserts a “Dim VariableName As Variant” line immediately above the first use in the procedure, where VariableName is the unresolved identifier name. The variable is made an explicit Variant, …because there’s another inspection that could fire up a result if we added an implicit Variant.

The quick-fix doesn’t assume an indentation level – makes me wonder if we should run the indenter on the procedure after applying a quick-fix… but that’s another discussion.

To be continued…

Advertisements

2.0: Stabilizing

The latest release packs a number of fundamental changes that affected every single part of Rubberduck. These changes are the result of the payment of a massive technical debt: from the very beginning, the very idea of wrapping the VBIDE API was deemed a massive and not-quite-needed undertaking – and so many parts of Rubberduck had direct dependencies on the VBA extensibility type library.

VBProjects, VBComponents, CodePanes, CommandBars, everything.

The problem is that, with COM resources, you need to release what you use – and cleaning up COM resources through .NET interop isn’t exactly trivial. Of course it looks pretty simple if you look at the ShutdownAddIn method in Rubberduck’s core:

try
{
    _ide.Release();
}
catch (Exception e)
{
    _logger.Error(e);
}

This Release method is exposed by the ISafeComWrapper interface, which is itself derived from a very general-purpose INullObjectWrapper interface:

public interface ISafeComWrapper : INullObjectWrapper
{
    void Release();
}

public interface ISafeComWrapper<out T> : ISafeComWrapper
{
    new T Target { get; }
}

public interface INullObjectWrapper
{
    object Target { get; }
    bool IsWrappingNullReference { get; }
}

Together, these simple interfaces form the down payment on the technical debt that has been plaguing Rubberduck from its early beginnings: the next step was to derive wrapper interfaces for every single type in the API. So instead of depending on a Microsoft.Vbe.Interop.CodePane, our code would be depending on a Rubberduck.VBEditor.SafeComWrappers.Abstract.ICodePane interface:

public interface ICodePane : ISafeComWrapper, IEquatable<ICodePane>
{
    IVBE VBE { get; }
    ICodePanes Collection { get; }
    IWindow Window { get; }
    int TopLine { get; set; }
    int CountOfVisibleLines { get; }
    ICodeModule CodeModule { get; }
    CodePaneView CodePaneView { get; }
    Selection GetSelection();
    QualifiedSelection? GetQualifiedSelection();
    void SetSelection(int startLine, int startColumn, int endLine, int endColumn);
    void SetSelection(Selection selection);
    void Show();
}

These abstract wrapper interfaces can then be implemented by concrete wrapper types that directly deal with the Microsoft interop types – so we could get rid of a number of extension methods, and move them directly into the wrapper types… and this time we’re not limited to the VBA extensibility API:

icodepaneimplementations

Every wrapper type must implement the Release method, where it cleans up after itself and its child objects – here the CodePanes wrapper releases its CodePane children before it releases itself:

public override void Release()
{
    if (!IsWrappingNullReference)
    {
        for (var i = 1; i <= Count; i++)
        {
            this[i].Release();
        }
        Marshal.ReleaseComObject(Target);
    }
}

This way, the entry point can call IVBE.Release, and every COM object ever accessed in the lifetime of Rubberduck is sure to get properly cleaned up, with one single method call.


Everywhere in Rubberduck, dependencies on Microsoft.Vbe.Interop were replaced with dependencies on Rubberduck.VBEditor.SafeComWrappers.Abstract, and then we were a step closer to a stable release.

But Rubberduck still kept crashing its host application on exit, and sometimes even at startup: something else wasn’t right, the call stack was pointing to an event listener picking up ItemAdded and ItemRemoved events fired from the References class.

Rubberduck implements COM event sinks to listen to IDE events such as “a module was renamed” or “a new project was added”, or “a project was closed”; I disabled them all. I also disabled the initial parse, just in case.

 

And Rubberduck kept crashing the host on exit, still.

The wrapper type that implemented the IReferences wrapper interface was registering handlers for ItemAdded and ItemRemoved; I removed them.

And Rubberduck did not crash on exit anymore.

So I reinstated the keyboard hook: still no crash.

So I reinstated the event sinks: still no crash.

So I reinstated the initial parse… and it crashed.

Initializing a VBE add-in is… complicated: the VBE hasn’t completely constructed itself at that time, and projects are still loading – the event sink was picking up ProjectAdded and ProjectRenamed events at startup (did you know your VBA projects aren’t “natively” called “VBAProject1”, but literally renamed that way at startup?), and trying to parse projects in the IDE at that point was just asking for trouble.

So I disabled the initial parse. No crash at startup, no crash at shut down. I moved the sinks’ event registration code out of constructors and into a dedicated method meant to be called once the IDE is ready for it… and forgot to call it.

So v2.0.9 does not parse automatically at startup, and doesn’t automatically know when a module or project is renamed, added, or removed.

But it doesn’t crash either. At least, not on my machine running 64-bit Office 2010 on Win10 x64… but since 2.0.9 was issued earlier today, we’ve found a race condition preventing proper teardown of the Rubberduck menu, and there are still a few more things to fix before we can call 2.0 “stable”.

But we’re much closer today than we were a month ago.

To be continued…