Skip to content

Architecture Overview

Max Dörner edited this page Nov 8, 2018 · 8 revisions

The entry point of the Rubberduck project is the COM-visible _Extension class, which implements the IDTExtensibility2 COM interface. Code at the entry point is kept to a minimum:

private void Startup()
{
    try
    {
        var currentDomain = AppDomain.CurrentDomain;
        currentDomain.UnhandledException += HandlAppDomainException;
        currentDomain.AssemblyResolve += LoadFromSameFolder;

        _container = new WindsorContainer().Install(new RubberduckIoCInstaller(_vbe, _addin, _initialSettings));
        
        _app = _container.Resolve<App>();
        _app.Startup();

        _isInitialized = true;
    }
    catch (Exception e)
    {
        _logger.Log(LogLevel.Fatal, e, "Startup sequence threw an unexpected exception.");
#if DEBUG
        throw;
#else
        throw new Exception("Rubberduck's startup sequence threw an unexpected exception. Please check the Rubberduck logs for more information and report an issue if necessary");
#endif
    }
}

This is the application's composition root, where our IoC container resolves the dependencies of every class involved in creating an instance of the App class. There shouldn't be much reasons to ever modify this code.

The preferred approach for a class in Rubberduck to take dependencies is by constructor injection, i.e. through parameters in the constructor. In production, these get automatically resolved by the IoC container. Note that injecting base types like string requires special registration in the container.

For dependencies that need to get created later than the object using them, a factory should get constructor injected to provide the object later.


Commands and Menus

Every feature eventually requires some way for the user to get them to run. Sometimes a feature can be launched from the main "Rubberduck" menu, two or more context menus, and an inspection quick-fix. Our architecture solves this problem by implementing commands.

Commands

Implementing a command is easy: derive a new class from CommandBase and override the Execute method. In its simplest form, a command could look like this:

public class AboutCommand : CommandBase
{
    public override void Execute(object parameter)
    {
        using (var window = new AboutWindow())
        {
            window.ShowDialog();
        }
    }
}

The base implementation for CanExecute simply returns true; override it to provide the logic that determines whether a command should be enabled or not - the WPF/XAML UI will use this logic to enable/disable the corresponding UI elements.

A command that has dependencies, should receive them as abstractions in its constructor - Ninject automatically takes care of injecting the concrete implementations.

Refactoring commands

The refactorings have common behavior and dependencies that have been abstracted into a RefactorCommandBase base class that refactoring commands should derive from:

public abstract class RefactorCommandBase : CommandBase
{
    protected readonly IVBE Vbe;

    protected RefactorCommandBase(IVBE vbe)
        : base (LogManager.GetCurrentClassLogger())
    {
        Vbe = vbe;
    }

    protected void HandleInvalidSelection(object sender, EventArgs e)
    {
        System.Windows.Forms.MessageBox.Show(RubberduckUI.ExtractMethod_InvalidSelectionMessage, RubberduckUI.ExtractMethod_Caption, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }
}
}

Hence, refactoring commands should take at least a VBE dependency:

public class RefactorImplementInterfaceCommand : RefactorCommandBase
{
    private readonly RubberduckParserState _state;
    private readonly IRewritingManager _rewritingManager;
    private readonly IMessageBox _msgBox;

    public RefactorImplementInterfaceCommand(IVBE vbe, RubberduckParserState state, IMessageBox msgBox, IRewritingManager rewritingManager)
        : base(vbe)
    {
        _state = state;
        _rewritingManager = rewritingManager;
        _msgBox = msgBox;
    }

    protected override bool EvaluateCanExecute(object parameter)
    {
        
        var selection = Vbe.GetActiveSelection();        

        if (!selection.HasValue)
        {
            return false;
        }

        var targetInterface = _state.DeclarationFinder.FindInterface(selection.Value);
        
        var targetClass = _state.DeclarationFinder.Members(selection.Value.QualifiedName)
            .SingleOrDefault(declaration => declaration.DeclarationType == DeclarationType.ClassModule);

        return targetInterface != null && targetClass != null
            && !_state.IsNewOrModified(targetInterface.QualifiedModuleName)
            && !_state.IsNewOrModified(targetClass.QualifiedModuleName);
        
    }

    protected override void OnExecute(object parameter)
    {
        using (var pane = Vbe.ActiveCodePane)
        {
            if (pane.IsWrappingNullReference)
            {
                return;
            }
        }
        var refactoring = new ImplementInterfaceRefactoring(Vbe, _state, _msgBox, _rewritingManager);
        refactoring.Refactor();
    }
}

The parser state is also a common dependency, since it exposes the processed VBA code. See the RubberduckParserState page for more information about this specific class.

Another dependency common to the refactorings is the IRewritingManager, which allows to check out IRewriteSessions, in which the the actual application of the refactoring to the code happens.


Menus

One way of executing commands, is to associate them with menu items. The easiest way to implement this, is to derive a new class from the CommandMenuItemBase abstract class, and pass the concrete command type to the base constructor - here's the simple AboutCommandMenuItem implementation:

public class AboutCommandMenuItem : CommandMenuItemBase
{
    public AboutCommandMenuItem(AboutCommand command) : base(command)
    {
    }

    public override string Key => "RubberduckMenu_About";
    public override bool BeginGroup => true;
    public override int DisplayOrder => (int)RubberduckMenuItemDisplayOrder.About;

    public override bool EvaluateCanExecute(RubberduckParserState state)
    {
        return true;
    }
}

The name of the type isn't a coincidence that it looks very much like the name of the corresponding command class.

Naming Convention for CommandMenuItemBase implementations

The naming convention for command menu items is as follows:

 [CommandClassName]MenuItem

This convention used to be employed to guide the component registration by convention in the IoC container, but is only a convention to enhance readability, now.

Classes derived from CommandMenuItemBase simply need to override base members to alter behavior.

  • Key property must return a string representing the resource key that contains the localized caption to use. This property is abstract and must therefore be overridden in all derived types.
  • BeginGroup is virtual and only needs to be overridden when you want the menu item to begin a group; when this property returns true, the menu item is rendered with a separator line immediately above it.
  • DisplayOrder is also virtual and should be overridden to control the display order of menu items.
  • Image and Mask are virtual and return null by default; they should be overridden when a menu item should be rendered with an icon. The Mask is a black & white version of the Image bitmap, where everything that should be transparent is white, and everything that should be in color, is black.

Controlling display order

Rather than hard-coding "magic" int values into each implementation, use an enum type: the order of the enum members will determine the order of the menu items. For example, the main RubberduckMenu uses this RubberduckMenuItemDisplayOrder enum:

public enum RubberduckMenuItemDisplayOrder
{
    UnitTesting,
    Refactorings,
    Navigate,
    CodeInspections,
    SourceControl,
    Options,
    About
}

This makes the code much cleaner, and makes it much easier to change how menus look like.

Parent menus

Menus can have sub-items, which can have sub-items themselves: the "parent" items have no command associated to them, so they're not derived from CommandMenuItemBase. Instead, they are subclasses of the ParentMenuItemBase abstract class.

Here's the RefactoringsParentMenu implementation:

public class RefactoringsParentMenu : ParentMenuItemBase
{
    public RefactoringsParentMenu(IEnumerable<IMenuItem> items)
        : base("RubberduckMenu_Refactor", items)
    {
    }

    public override int DisplayOrder => (int)RubberduckMenuItemDisplayOrder.Refactorings;
}

There's pretty much no code, in every single implementation: only the DisplayOrder and BeginGroup properties can be overridden, and the Key is passed to the base constructor along with the child items.

Every parent menu must receive an IEnumerable<IMenuItem> constructor parameter that contains its child items. To be able to exactly specify which submenues and commands are available on each menu, corresponding functions exist in the IoC registration class RubberduckIoCInstaller:

private Type[] NavigateMenuItems()
{
    return new[]
    {
        typeof(CodeExplorerCommandMenuItem),
        typeof(RegexSearchReplaceCommandMenuItem),               
        typeof(FindSymbolCommandMenuItem),
        typeof(FindAllReferencesCommandMenuItem),
        typeof(FindAllImplementationsCommandMenuItem)
    };
}

These methods determine what goes where. The order of the items in this code does not impact the actual order of menu items in the rendered menus - that's controlled by the DisplayOrder value of each item.


Inspections

All code inspections derive from the InspectionBase class or its derived class ParseTreeInspectionBase. By convention, their names end with Inspection, and their GetInspectionResults implementation return objects whose name is the same as the inspection class, ending with InspectionResult.

Here is the VariableNotUsedInspection implementation:

public sealed class VariableNotUsedInspection : InspectionBase
{
    public VariableNotUsedInspection(RubberduckParserState state) : base(state) { }

    protected override IEnumerable<IInspectionResult> DoGetInspectionResults()
    {
        var declarations = State.DeclarationFinder.UserDeclarations(DeclarationType.Variable)
            .Where(declaration =>
                !declaration.IsWithEvents
                && !IsIgnoringInspectionResultFor(declaration, AnnotationName)
                && !declaration.References.Any());

        return declarations.Select(issue => 
            new DeclarationInspectionResult(this,
                                 string.Format(InspectionResults.IdentifierNotUsedInspection, issue.DeclarationType.ToLocalizedString(), issue.IdentifierName),
                                 issue,
                                 new QualifiedContext<ParserRuleContext>(issue.QualifiedName.QualifiedModuleName, ((dynamic)issue.Context).identifier())));
    }
}

All inspections should be sealed classes derived from InspectionBase, passing the RubberduckParserState into the base constructor.

A noteworthy exception are Inspections that have to work on the IParseTree produced by the ANTLR parser instead of the resulting Declarations. They should derive from ParseTreeInspectionBase instead, which enables passing the ParseTreeResults generated in the Inspector for further analysis. The actual inspection work for these inspections is performed in the Inspector where the IParseTree in the ParserState is rewalked once for all IParseTreeInspections (to improve performance).

The Description property returns a format string that is used for all inspection results as a description - for example, the description for VariableNotUsedInspection returns a localized string similar to "Variable '{0}' is not used.".

The InspectionType property determines the type of inspection - this can be any of the CodeInspectionType enum values:

public enum CodeInspectionType
{
    RubberduckOpportunities,
    LanguageOpportunities,
    MaintainabilityAndReadabilityIssues,
    CodeQualityIssues,
    Performance,
}

The constructor must assign the default Severity level. Most inspections should default to Warning.

Guidelines for default severity level

  • Don't use DoNotShow. That level effectively disables an inspection, and that should be done by the user. The exception is when implementing opposit pairs of inspections. Then the one we do not want to promote can default to DoNotShow.
  • Don't use Hint level either. Leave that level for the user to use for inspections deemed of lower priority, but that they don't want to disable.
  • Do use Warning if you're unsure.
  • Only default an inspection to Error severity for inspections that can detect bugs and runtime errors. For example, an object variable is assigned without the Set keyword.
  • Consider defaulting to Warning severity an inspection that could potentially detect a bug, but not a runtime error. For example, a function that doesn't return a value - it's definitely a smell worth looking into, but not necessarily a bug.
  • Consider defaulting to Suggestion severity an inspection when the quick-fix involves a refactoring.
Clone this wiki locally