Here is yet another piece of Rubberduck code, this time the nasty ProcedureNotUsedInspection
class, whose role is to identify all procedures that are never called anywhere, and to issue a ProcedureNotUsedInspectionResult
for each one.
using System.Collections.Generic;
using System.Linq;
using Rubberduck.Parsing;
using Rubberduck.Parsing.Grammar;
using Rubberduck.Parsing.Symbols;
namespace Rubberduck.Inspections
{
public class ProcedureNotUsedInspection : IInspection
{
public ProcedureNotUsedInspection()
{
Severity = CodeInspectionSeverity.Hint;
}
public string Name { get { return InspectionNames.ProcedureNotUsed_; } }
public CodeInspectionType InspectionType { get { return CodeInspectionType.CodeQualityIssues; } }
public CodeInspectionSeverity Severity { get; set; }
public IEnumerable<CodeInspectionResultBase> GetInspectionResults(VBProjectParseResult parseResult)
{
var handlers = parseResult.Declarations.Items.Where(item => item.DeclarationType == DeclarationType.Control)
.SelectMany(control => parseResult.Declarations.FindEventHandlers(control));
var issues = parseResult.Declarations.Items
.Where(item => !IsIgnoredProcedure(parseResult.Declarations, item, handlers))
.Select(issue => new IdentifierNotUsedInspectionResult(string.Format(Name, issue.IdentifierName), Severity, issue.Context, issue.QualifiedName.QualifiedModuleName));
return issues;
}
private bool IsIgnoredProcedure(Declarations declarations, Declaration declaration, IEnumerable<Declaration> handlers)
{
var result =
declaration.DeclarationType != DeclarationType.Procedure
|| handlers.Contains(declaration)
|| declaration.References.Any()
|| IsPublicModuleMember(declarations, declaration)
|| IsClassLifeCycleHandler(declarations, declaration)
|| IsInterfaceMember(declarations, declaration);
return result;
}
/// <remarks>
/// We cannot determine whether exposed members of standard modules are called or not,
/// so we assume they are instead of flagging them as "never called".
/// </remarks>
private bool IsPublicModuleMember(Declarations declarations, Declaration procedure)
{
var parent = declarations.Items.SingleOrDefault(item =>
item.IdentifierName == procedure.ComponentName &&
(item.DeclarationType == DeclarationType.Module));
return parent != null && (procedure.Accessibility == Accessibility.Implicit
|| procedure.Accessibility == Accessibility.Public);
}
private static readonly string[] ClassLifeCycleHandlers =
{
"Class_Initialize",
"Class_Terminate"
};
private bool IsClassLifeCycleHandler(Declarations declarations, Declaration procedure)
{
var parent = declarations.Items.SingleOrDefault(item =>
item.IdentifierName == procedure.ComponentName &&
(item.DeclarationType == DeclarationType.Class));
return parent != null && ClassLifeCycleHandlers.Contains(procedure.IdentifierName);
}
/// <remarks>
/// Interface implementation members are private, they're not called from an object
/// variable reference of the type of the procedure's class, and whether they're called or not,
/// they have to be implemented anyway, so removing them would break the code.
/// Best just ignore them.
/// </remarks>
private bool IsInterfaceMember(Declarations declarations, Declaration procedure)
{
// get the procedure's parent module
var parent = declarations.Items.SingleOrDefault(item =>
item.IdentifierName == procedure.ComponentName &&
(item.DeclarationType == DeclarationType.Class));
if (parent == null)
{
return false;
}
var classes = declarations.Items.Where(item => item.DeclarationType == DeclarationType.Class);
var interfaces = classes.Where(item => item.References.Any(reference =>
reference.Context.Parent is VBAParser.ImplementsStmtContext));
if (interfaces.Select(i => i.ComponentName).Contains(procedure.ComponentName))
{
return true;
}
// todo: find a way to avoid running this for every procedure in a class
var result = GetImplementedInterfaceMembers(declarations, procedure.ComponentName)
.Contains(procedure.IdentifierName);
return result;
}
private IEnumerable<string> GetImplementedInterfaceMembers(Declarations declarations, string componentName)
{
var classes = declarations.Items.Where(item => item.DeclarationType == DeclarationType.Class);
var interfaces = classes.Where(item => item.References.Any(reference =>
reference.Context.Parent is VBAParser.ImplementsStmtContext
&& reference.QualifiedModuleName.ModuleName == componentName));
var members = interfaces.SelectMany(declarations.FindMembers)
.Select(member => member.ComponentName + "_" + member.IdentifierName);
return members;
}
}
}
As you can see there are a number of constraints and assumptions that we need to make here:
- Any
Public
procedure that can be called from Excel as a user-defined function or a "macro" is ignored, because we can't tell whether it's actually used or not. - Event handlers and class life cycle handlers are called by VBA itself, so they're ignored too.
- An interface can only be recognized as such if it's implemented somewhere.
Now, the main issue I have is one of performance - I need to figure out a way to reorganize the code in this inspection so as to avoid doing redundant checks like I'm doing; I'd like this code to be as efficient as possible, and I know it isn't - there are redundancies with detecting interface members, and I'm "finding" interface members way too many times (a module's members aren't going to change between two iterations!).
How can I clean this up without breaking it?
procedure
could be a sub or function? \$\endgroup\$Function
procs for now. \$\endgroup\$