Skip to content
Merged
83 changes: 83 additions & 0 deletions RetailCoder.VBE/Inspections/MemberNotOnInterfaceInspection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Antlr4.Runtime;
using Rubberduck.Inspections.Abstract;
using Rubberduck.Inspections.Resources;
using Rubberduck.Inspections.Results;
using Rubberduck.Parsing;
using Rubberduck.Parsing.Grammar;
using Rubberduck.Parsing.Symbols;
using Rubberduck.Parsing.VBA;
using Rubberduck.VBEditor;

namespace Rubberduck.Inspections
{
public sealed class MemberNotOnInterfaceInspection : InspectionBase
{
private static readonly List<Type> InterestingTypes = new List<Type>
{
typeof(VBAParser.MemberAccessExprContext),
typeof(VBAParser.WithMemberAccessExprContext),
typeof(VBAParser.DictionaryAccessExprContext),
typeof(VBAParser.WithDictionaryAccessExprContext)
};

public MemberNotOnInterfaceInspection(RubberduckParserState state, CodeInspectionSeverity defaultSeverity = CodeInspectionSeverity.Warning)
: base(state, defaultSeverity)
{
}

public override string Meta { get { return InspectionsUI.MemberNotOnInterfaceInspectionMeta; } }
public override string Description { get { return InspectionsUI.MemberNotOnInterfaceInspectionName; } }
public override CodeInspectionType InspectionType { get { return CodeInspectionType.CodeQualityIssues; } }

public override IEnumerable<InspectionResultBase> GetInspectionResults()
{
var targets = Declarations.Where(decl => decl.AsTypeDeclaration != null &&
decl.AsTypeDeclaration.DeclarationType == DeclarationType.ClassModule &&
((ClassModuleDeclaration)decl.AsTypeDeclaration).IsExtensible &&
decl.References.Any(usage => InterestingTypes.Contains(usage.Context.Parent.GetType())))
.ToList();

//Unfortunately finding implemented members is fairly expensive, so group by the return type.
return (from access in targets.GroupBy(t => t.AsTypeDeclaration)
let typeDeclaration = access.Key
let typeMembers = new HashSet<string>(BuiltInDeclarations.Where(d => d.ParentDeclaration != null && d.ParentDeclaration.Equals(typeDeclaration))
.Select(d => d.IdentifierName)
.Distinct())
from references in access.Select(usage => usage.References.Where(r => InterestingTypes.Contains(r.Context.Parent.GetType())))
from reference in references.Where(r => !r.IsInspectionDisabled(AnnotationName))
let identifier = reference.Context.Parent.GetChild(reference.Context.Parent.ChildCount - 1)
where !typeMembers.Contains(identifier.GetText())
let pseudoDeclaration = CreatePseudoDeclaration((ParserRuleContext) identifier, reference)
where !pseudoDeclaration.Annotations.Any()
select new MemberNotOnInterfaceInspectionResult(this, pseudoDeclaration, (ParserRuleContext) identifier, typeDeclaration))
.Cast<InspectionResultBase>().ToList();
}

//Builds a throw-away Declaration for the indentifiers found by the inspection. These aren't (and shouldn't be) created by the parser.
//Used to pass to the InspectionResult to make it selectable.
private static Declaration CreatePseudoDeclaration(ParserRuleContext context, IdentifierReference reference)
{
return new Declaration(
new QualifiedMemberName(reference.QualifiedModuleName, context.GetText()),
null,
null,
string.Empty,
string.Empty,
false,
false,
Accessibility.Implicit,
DeclarationType.Variable,
context,
context.GetSelection(),
false,
null,
true,
null,
null,
true);
}
}
}
27 changes: 27 additions & 0 deletions RetailCoder.VBE/Inspections/Resources/InspectionsUI.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions RetailCoder.VBE/Inspections/Resources/InspectionsUI.resx
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,14 @@ If the parameter can be null, ignore this inspection result; passing a null valu
<data name="WhiteListIdentifierQuickFix" xml:space="preserve">
<value>Add to whitelist</value>
</data>
<data name="MemberNotOnInterfaceInspectionMeta" xml:space="preserve">
<value>A member access is being used that is not declared on the object's interface. This is most likely an error. If the member access is using the object's extensible interface, consider using a non-extensible equivalent to allow compile time checks that will avoid the possibility of a run-time error 438.</value>
</data>
<data name="MemberNotOnInterfaceInspectionName" xml:space="preserve">
<value>Member does not exist on interface</value>
</data>
<data name="MemberNotOnInterfaceInspectionResultFormat" xml:space="preserve">
<value>Member '{0}' is not declared on the interface for type '{1}'.</value>
<comment>{0} Member used, {1} type being accessed.</comment>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using Antlr4.Runtime;
using Rubberduck.Inspections.Abstract;
using Rubberduck.Inspections.QuickFixes;
using Rubberduck.Inspections.Resources;
using Rubberduck.Parsing.Symbols;

namespace Rubberduck.Inspections.Results
{
public class MemberNotOnInterfaceInspectionResult : InspectionResultBase
{
private readonly ParserRuleContext _member;
private readonly Declaration _asTypeDeclaration;

public MemberNotOnInterfaceInspectionResult(IInspection inspection, Declaration target, ParserRuleContext member, Declaration asTypeDeclaration)
: base(inspection, target)
{
_member = member;
_asTypeDeclaration = asTypeDeclaration;
}

public override IEnumerable<QuickFixBase> QuickFixes
{
get { return new List<QuickFixBase> { new IgnoreOnceQuickFix(_member, QualifiedSelection, Inspection.AnnotationName) }; }
}

public override string Description
{
get { return string.Format(InspectionsUI.MemberNotOnInterfaceInspectionResultFormat, _member.GetText(), _asTypeDeclaration.IdentifierName); }
}
}
}
2 changes: 2 additions & 0 deletions RetailCoder.VBE/Rubberduck.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
<Compile Include="Common\UndocumentedAttribute.cs" />
<Compile Include="Inspections\HungarianNotationInspection.cs" />
<Compile Include="Inspections\ImplicitDefaultMemberAssignmentInspection.cs" />
<Compile Include="Inspections\MemberNotOnInterfaceInspection.cs" />
<Compile Include="Inspections\QuickFixes\AddIdentifierToWhiteListQuickFix.cs" />
<Compile Include="Inspections\Resources\InspectionsUI.Designer.cs">
<AutoGen>True</AutoGen>
Expand All @@ -381,6 +382,7 @@
<Compile Include="Inspections\QuickFixes\RenameDeclarationQuickFix.cs" />
<Compile Include="Inspections\QuickFixes\RemoveUnusedParameterQuickFix.cs" />
<Compile Include="Inspections\QuickFixes\ReplaceGlobalModifierQuickFix.cs" />
<Compile Include="Inspections\Results\MemberNotOnInterfaceInspectionResult.cs" />
<Compile Include="Inspections\Results\ObjectVariableNotSetInspectionResult.cs" />
<Compile Include="Inspections\Results\MultipleFolderAnnotationsInspectionResult.cs" />
<Compile Include="Inspections\QuickFixes\MoveFieldCloserToUsageQuickFix.cs" />
Expand Down
5 changes: 5 additions & 0 deletions Rubberduck.Parsing/ComReflection/ComCoClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public class ComCoClass : ComType, IComTypeWithMembers
private readonly Dictionary<ComInterface, bool> _interfaces = new Dictionary<ComInterface, bool>();
private readonly List<ComInterface> _events = new List<ComInterface>();

public bool IsExtensible
{
get { return _interfaces.Keys.Any(i => i.IsExtensible); }
}

public ComInterface DefaultInterface { get; private set; }

public IEnumerable<ComInterface> EventInterfaces
Expand Down
6 changes: 5 additions & 1 deletion Rubberduck.Parsing/ComReflection/ComInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@
using TYPEATTR = System.Runtime.InteropServices.ComTypes.TYPEATTR;
using FUNCDESC = System.Runtime.InteropServices.ComTypes.FUNCDESC;
using CALLCONV = System.Runtime.InteropServices.ComTypes.CALLCONV;
using TYPEFLAGS = System.Runtime.InteropServices.ComTypes.TYPEFLAGS;

namespace Rubberduck.Parsing.ComReflection
{
[DebuggerDisplay("{Name}")]
public class ComInterface : ComType, IComTypeWithMembers
{
private readonly List<ComInterface> _inherited = new List<ComInterface>();
private readonly List<ComMember> _members = new List<ComMember>();
private readonly List<ComMember> _members = new List<ComMember>();

public bool IsExtensible { get; private set; }

public IEnumerable<ComInterface> InheritedInterfaces
{
Expand All @@ -41,6 +44,7 @@ public ComInterface(ITypeLib typeLib, ITypeInfo info, TYPEATTR attrib, int index

private void GetImplementedInterfaces(ITypeInfo info, TYPEATTR typeAttr)
{
IsExtensible = !typeAttr.wTypeFlags.HasFlag(TYPEFLAGS.TYPEFLAG_FNONEXTENSIBLE);
for (var implIndex = 0; implIndex < typeAttr.cImplTypes; implIndex++)
{
int href;
Expand Down
7 changes: 2 additions & 5 deletions Rubberduck.Parsing/ComReflection/ComType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ public interface IComType : IComBase
bool IsAppObject { get; }
bool IsPreDeclared { get; }
bool IsHidden { get; }
bool IsRestricted { get; }
bool IsExtensible { get; }
bool IsRestricted { get; }
}

public interface IComTypeWithMembers : IComType
Expand All @@ -30,7 +29,6 @@ public abstract class ComType : ComBase, IComType
public bool IsPreDeclared { get; private set; }
public bool IsHidden { get; private set; }
public bool IsRestricted { get; private set; }
public bool IsExtensible { get; private set; }

protected ComType(ITypeInfo info, TYPEATTR attrib)
: base(info)
Expand All @@ -51,8 +49,7 @@ private void SetFlagsFromTypeAttr(TYPEATTR attrib)
IsAppObject = attrib.wTypeFlags.HasFlag(TYPEFLAGS.TYPEFLAG_FAPPOBJECT);
IsPreDeclared = attrib.wTypeFlags.HasFlag(TYPEFLAGS.TYPEFLAG_FPREDECLID);
IsHidden = attrib.wTypeFlags.HasFlag(TYPEFLAGS.TYPEFLAG_FHIDDEN);
IsRestricted = attrib.wTypeFlags.HasFlag(TYPEFLAGS.TYPEFLAG_FRESTRICTED);
IsExtensible = !attrib.wTypeFlags.HasFlag(TYPEFLAGS.TYPEFLAG_FNONEXTENSIBLE);
IsRestricted = attrib.wTypeFlags.HasFlag(TYPEFLAGS.TYPEFLAG_FRESTRICTED);
}
}
}
8 changes: 7 additions & 1 deletion Rubberduck.Parsing/Symbols/ClassModuleDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public ClassModuleDeclaration(ComCoClass coClass, Declaration parent, QualifiedM
.ToList();
_supertypes = new HashSet<Declaration>();
_subtypes = new HashSet<Declaration>();
IsExtensible = coClass.IsExtensible;
}

public ClassModuleDeclaration(ComInterface intrface, Declaration parent, QualifiedModuleName module,
Expand All @@ -87,7 +88,10 @@ public ClassModuleDeclaration(ComInterface intrface, Declaration parent, Qualifi
intrface.Name,
true,
new List<IAnnotation>(),
attributes) { }
attributes)
{
IsExtensible = intrface.IsExtensible;
}

public static IEnumerable<Declaration> GetSupertypes(Declaration type)
{
Expand All @@ -105,6 +109,8 @@ public static bool HasDefaultMember(Declaration type)
return classModule != null && classModule.DefaultMember != null;
}

public bool IsExtensible { get; set; }

private bool? _isExposed;
/// <summary>
/// Gets an attribute value indicating whether a class is exposed to other projects.
Expand Down
25 changes: 1 addition & 24 deletions Rubberduck.Parsing/Symbols/Declaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -318,27 +318,14 @@ public IEnumerable<IdentifierReference> References
{
get
{
return _references.Union(_memberCalls);
return _references;
}
set
{
_references = new ConcurrentBag<IdentifierReference>(value);
}
}

private ConcurrentBag<IdentifierReference> _memberCalls = new ConcurrentBag<IdentifierReference>();
public IEnumerable<IdentifierReference> MemberCalls
{
get
{
return _memberCalls.ToList();
}
set
{
_memberCalls = new ConcurrentBag<IdentifierReference>(value);
}
}

private readonly IEnumerable<IAnnotation> _annotations;
public IEnumerable<IAnnotation> Annotations { get { return _annotations ?? new List<IAnnotation>(); } }

Expand Down Expand Up @@ -406,16 +393,6 @@ public void AddReference(
annotations));
}

public void AddMemberCall(IdentifierReference reference)
{
if (reference == null || reference.Declaration == null || reference.Declaration.Context == reference.Context)
{
return;
}

_memberCalls.Add(reference);
}

private readonly Selection _selection;
/// <summary>
/// Gets a <c>Selection</c> representing the position of the declaration in the code module.
Expand Down
10 changes: 9 additions & 1 deletion Rubberduck.Parsing/Symbols/DeclarationFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class DeclarationFinder
private readonly IDictionary<QualifiedModuleName, CommentNode[]> _comments;
private readonly IDictionary<QualifiedModuleName, IAnnotation[]> _annotations;
private readonly IDictionary<QualifiedMemberName, IList<Declaration>> _undeclared;
private readonly AnnotationService _annotationService;

private readonly IReadOnlyList<Declaration> _declarations;
private readonly IDictionary<string, Declaration[]> _declarationsByName;
Expand All @@ -35,6 +36,7 @@ public DeclarationFinder(
.ToDictionary(grouping => grouping.Key.IdentifierName, grouping => grouping.ToArray());

_undeclared = new Dictionary<QualifiedMemberName, IList<Declaration>>();
_annotationService = new AnnotationService(this);
}

public IEnumerable<Declaration> Undeclared
Expand Down Expand Up @@ -294,7 +296,13 @@ public Declaration FindMemberEnclosingProcedure(Declaration enclosingProcedure,

public Declaration OnUndeclaredVariable(Declaration enclosingProcedure, string identifierName, ParserRuleContext context)
{
var undeclaredLocal = new Declaration(new QualifiedMemberName(enclosingProcedure.QualifiedName.QualifiedModuleName, identifierName), enclosingProcedure, enclosingProcedure, "Variant", string.Empty, false, false, Accessibility.Implicit, DeclarationType.Variable, context, context.GetSelection(), false, null, false, null, null, true);
var annotations = _annotationService.FindAnnotations(enclosingProcedure.QualifiedName.QualifiedModuleName, context.Start.Line);
var undeclaredLocal =
new Declaration(
new QualifiedMemberName(enclosingProcedure.QualifiedName.QualifiedModuleName, identifierName),
enclosingProcedure, enclosingProcedure, "Variant", string.Empty, false, false,
Accessibility.Implicit, DeclarationType.Variable, context, context.GetSelection(), false, null,
false, annotations, null, true);

var hasUndeclared = _undeclared.ContainsKey(enclosingProcedure.QualifiedName);
if (hasUndeclared)
Expand Down
9 changes: 8 additions & 1 deletion Rubberduck.Parsing/Symbols/SerializableDeclaration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ public SerializableDeclaration(Declaration declaration)
IsByRefParam = param.IsByRef;
IsParamArray = param.IsParamArray;
}

var canExtend = declaration as ClassModuleDeclaration;
if (canExtend != null)
{
IsExtensible = canExtend.IsExtensible;
}
}

public List<SerializableMemberAttribute> Attributes { get; set; }
Expand All @@ -197,6 +203,7 @@ public SerializableDeclaration(Declaration declaration)
public bool IsBuiltIn { get; set; }
public bool IsSelfAssigned { get; set; }
public bool IsWithEvents { get; set; }
public bool IsExtensible { get; set; }
public Accessibility Accessibility { get; set; }
public DeclarationType DeclarationType { get; set; }

Expand All @@ -218,7 +225,7 @@ public Declaration Unwrap(Declaration parent)
case DeclarationType.Project:
return new ProjectDeclaration(QualifiedMemberName, IdentifierName, true);
case DeclarationType.ClassModule:
return new ClassModuleDeclaration(QualifiedMemberName, parent, IdentifierName, true, annotations, attributes);
return new ClassModuleDeclaration(QualifiedMemberName, parent, IdentifierName, true, annotations, attributes) { IsExtensible = IsExtensible };
case DeclarationType.ProceduralModule:
return new ProceduralModuleDeclaration(QualifiedMemberName, parent, IdentifierName, true, annotations, attributes);
case DeclarationType.Procedure:
Expand Down
Loading