diff --git a/RetailCoder.VBE/Inspections/MemberNotOnInterfaceInspection.cs b/RetailCoder.VBE/Inspections/MemberNotOnInterfaceInspection.cs new file mode 100644 index 0000000000..133df614c7 --- /dev/null +++ b/RetailCoder.VBE/Inspections/MemberNotOnInterfaceInspection.cs @@ -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 InterestingTypes = new List + { + 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 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(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().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); + } + } +} diff --git a/RetailCoder.VBE/Inspections/Resources/InspectionsUI.Designer.cs b/RetailCoder.VBE/Inspections/Resources/InspectionsUI.Designer.cs index 730ce46b56..b01d3eac29 100644 --- a/RetailCoder.VBE/Inspections/Resources/InspectionsUI.Designer.cs +++ b/RetailCoder.VBE/Inspections/Resources/InspectionsUI.Designer.cs @@ -591,6 +591,33 @@ public static string MakeSingleLineParameterQuickFix { } } + /// + /// Looks up a localized string similar to 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.. + /// + public static string MemberNotOnInterfaceInspectionMeta { + get { + return ResourceManager.GetString("MemberNotOnInterfaceInspectionMeta", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member does not exist on interface. + /// + public static string MemberNotOnInterfaceInspectionName { + get { + return ResourceManager.GetString("MemberNotOnInterfaceInspectionName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Member '{0}' is not declared on the interface for type '{1}'.. + /// + public static string MemberNotOnInterfaceInspectionResultFormat { + get { + return ResourceManager.GetString("MemberNotOnInterfaceInspectionResultFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to An annotation parameter is missing or incorrectly specified. The correct syntax is : '@Annotation([parameter])\nExample: '@Folder("Parent.Child"). /// diff --git a/RetailCoder.VBE/Inspections/Resources/InspectionsUI.resx b/RetailCoder.VBE/Inspections/Resources/InspectionsUI.resx index 586ba61d22..8b0881af61 100644 --- a/RetailCoder.VBE/Inspections/Resources/InspectionsUI.resx +++ b/RetailCoder.VBE/Inspections/Resources/InspectionsUI.resx @@ -615,4 +615,14 @@ If the parameter can be null, ignore this inspection result; passing a null valu Add to whitelist + + 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. + + + Member does not exist on interface + + + Member '{0}' is not declared on the interface for type '{1}'. + {0} Member used, {1} type being accessed. + \ No newline at end of file diff --git a/RetailCoder.VBE/Inspections/Results/MemberNotOnInterfaceInspectionResult.cs b/RetailCoder.VBE/Inspections/Results/MemberNotOnInterfaceInspectionResult.cs new file mode 100644 index 0000000000..32239bfa3a --- /dev/null +++ b/RetailCoder.VBE/Inspections/Results/MemberNotOnInterfaceInspectionResult.cs @@ -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 QuickFixes + { + get { return new List { new IgnoreOnceQuickFix(_member, QualifiedSelection, Inspection.AnnotationName) }; } + } + + public override string Description + { + get { return string.Format(InspectionsUI.MemberNotOnInterfaceInspectionResultFormat, _member.GetText(), _asTypeDeclaration.IdentifierName); } + } + } +} diff --git a/RetailCoder.VBE/Rubberduck.csproj b/RetailCoder.VBE/Rubberduck.csproj index 9bed8ab93f..351131c95c 100644 --- a/RetailCoder.VBE/Rubberduck.csproj +++ b/RetailCoder.VBE/Rubberduck.csproj @@ -367,6 +367,7 @@ + True @@ -381,6 +382,7 @@ + diff --git a/Rubberduck.Parsing/ComReflection/ComCoClass.cs b/Rubberduck.Parsing/ComReflection/ComCoClass.cs index 7bb7e99444..a056ac854d 100644 --- a/Rubberduck.Parsing/ComReflection/ComCoClass.cs +++ b/Rubberduck.Parsing/ComReflection/ComCoClass.cs @@ -15,6 +15,11 @@ public class ComCoClass : ComType, IComTypeWithMembers private readonly Dictionary _interfaces = new Dictionary(); private readonly List _events = new List(); + public bool IsExtensible + { + get { return _interfaces.Keys.Any(i => i.IsExtensible); } + } + public ComInterface DefaultInterface { get; private set; } public IEnumerable EventInterfaces diff --git a/Rubberduck.Parsing/ComReflection/ComInterface.cs b/Rubberduck.Parsing/ComReflection/ComInterface.cs index 46ff32a177..3352343c0c 100644 --- a/Rubberduck.Parsing/ComReflection/ComInterface.cs +++ b/Rubberduck.Parsing/ComReflection/ComInterface.cs @@ -7,6 +7,7 @@ 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 { @@ -14,7 +15,9 @@ namespace Rubberduck.Parsing.ComReflection public class ComInterface : ComType, IComTypeWithMembers { private readonly List _inherited = new List(); - private readonly List _members = new List(); + private readonly List _members = new List(); + + public bool IsExtensible { get; private set; } public IEnumerable InheritedInterfaces { @@ -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; diff --git a/Rubberduck.Parsing/ComReflection/ComType.cs b/Rubberduck.Parsing/ComReflection/ComType.cs index c953e90af6..1ab3b5804e 100644 --- a/Rubberduck.Parsing/ComReflection/ComType.cs +++ b/Rubberduck.Parsing/ComReflection/ComType.cs @@ -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 @@ -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) @@ -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); } } } diff --git a/Rubberduck.Parsing/Symbols/ClassModuleDeclaration.cs b/Rubberduck.Parsing/Symbols/ClassModuleDeclaration.cs index c837ec0dc3..8aada50512 100644 --- a/Rubberduck.Parsing/Symbols/ClassModuleDeclaration.cs +++ b/Rubberduck.Parsing/Symbols/ClassModuleDeclaration.cs @@ -77,6 +77,7 @@ public ClassModuleDeclaration(ComCoClass coClass, Declaration parent, QualifiedM .ToList(); _supertypes = new HashSet(); _subtypes = new HashSet(); + IsExtensible = coClass.IsExtensible; } public ClassModuleDeclaration(ComInterface intrface, Declaration parent, QualifiedModuleName module, @@ -87,7 +88,10 @@ public ClassModuleDeclaration(ComInterface intrface, Declaration parent, Qualifi intrface.Name, true, new List(), - attributes) { } + attributes) + { + IsExtensible = intrface.IsExtensible; + } public static IEnumerable GetSupertypes(Declaration type) { @@ -105,6 +109,8 @@ public static bool HasDefaultMember(Declaration type) return classModule != null && classModule.DefaultMember != null; } + public bool IsExtensible { get; set; } + private bool? _isExposed; /// /// Gets an attribute value indicating whether a class is exposed to other projects. diff --git a/Rubberduck.Parsing/Symbols/Declaration.cs b/Rubberduck.Parsing/Symbols/Declaration.cs index 0207c30505..a998396ac1 100644 --- a/Rubberduck.Parsing/Symbols/Declaration.cs +++ b/Rubberduck.Parsing/Symbols/Declaration.cs @@ -318,7 +318,7 @@ public IEnumerable References { get { - return _references.Union(_memberCalls); + return _references; } set { @@ -326,19 +326,6 @@ public IEnumerable References } } - private ConcurrentBag _memberCalls = new ConcurrentBag(); - public IEnumerable MemberCalls - { - get - { - return _memberCalls.ToList(); - } - set - { - _memberCalls = new ConcurrentBag(value); - } - } - private readonly IEnumerable _annotations; public IEnumerable Annotations { get { return _annotations ?? new List(); } } @@ -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; /// /// Gets a Selection representing the position of the declaration in the code module. diff --git a/Rubberduck.Parsing/Symbols/DeclarationFinder.cs b/Rubberduck.Parsing/Symbols/DeclarationFinder.cs index 8e2ffb5ee0..85bdd0977f 100644 --- a/Rubberduck.Parsing/Symbols/DeclarationFinder.cs +++ b/Rubberduck.Parsing/Symbols/DeclarationFinder.cs @@ -13,6 +13,7 @@ public class DeclarationFinder private readonly IDictionary _comments; private readonly IDictionary _annotations; private readonly IDictionary> _undeclared; + private readonly AnnotationService _annotationService; private readonly IReadOnlyList _declarations; private readonly IDictionary _declarationsByName; @@ -35,6 +36,7 @@ public DeclarationFinder( .ToDictionary(grouping => grouping.Key.IdentifierName, grouping => grouping.ToArray()); _undeclared = new Dictionary>(); + _annotationService = new AnnotationService(this); } public IEnumerable Undeclared @@ -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) diff --git a/Rubberduck.Parsing/Symbols/SerializableDeclaration.cs b/Rubberduck.Parsing/Symbols/SerializableDeclaration.cs index a6143d8a63..cf496d64d4 100644 --- a/Rubberduck.Parsing/Symbols/SerializableDeclaration.cs +++ b/Rubberduck.Parsing/Symbols/SerializableDeclaration.cs @@ -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 Attributes { get; set; } @@ -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; } @@ -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: diff --git a/RubberduckTests/Inspections/MemberNotOnInterfaceInspectionTests.cs b/RubberduckTests/Inspections/MemberNotOnInterfaceInspectionTests.cs new file mode 100644 index 0000000000..bafb0f8699 --- /dev/null +++ b/RubberduckTests/Inspections/MemberNotOnInterfaceInspectionTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Rubberduck.API; +using Rubberduck.Inspections; +using Rubberduck.Parsing.Symbols; +using Rubberduck.Parsing.VBA; +using Rubberduck.VBEditor; +using Rubberduck.VBEditor.Application; +using Rubberduck.VBEditor.Events; +using Rubberduck.VBEditor.SafeComWrappers; +using RubberduckTests.Mocks; +using Accessibility = Rubberduck.Parsing.Symbols.Accessibility; +using ParserState = Rubberduck.Parsing.VBA.ParserState; + +namespace RubberduckTests.Inspections +{ + [TestClass] + public class MemberNotOnInterfaceInspectionTests + { + private static void AddTestBuiltInLibrary(RubberduckParserState state) + { + var projectName = new QualifiedModuleName("TestLib", string.Empty, "TestLib"); + var library = new ProjectDeclaration(projectName.QualifyMemberName("TestLib"), "TestLib", true); + state.AddDeclaration(library); + + var attributes = new Attributes(); + attributes.AddPredeclaredIdTypeAttribute(); + attributes.AddGlobalClassAttribute(); + + var extensible = new ClassModuleDeclaration(projectName.QualifyMemberName("Extensible"), + library, + "Extensible", + true, + null, + attributes, + true) { IsExtensible = true }; + state.AddDeclaration(extensible); + + var nonExtensible = new ClassModuleDeclaration(projectName.QualifyMemberName("NonExtensible"), + library, + "NonExtensible", + true, + null, + attributes, + true); + state.AddDeclaration(nonExtensible); + + var member = new SubroutineDeclaration(extensible.QualifiedName.QualifiedModuleName.QualifyMemberName("ExtensibleMember"), + extensible, + extensible, + string.Empty, + Accessibility.Global, + null, + Selection.Home, + true, + null, + new Attributes()); + state.AddDeclaration(member); + state.CoClasses.TryAdd(new List { member.IdentifierName }, extensible); + + member = new SubroutineDeclaration(nonExtensible.QualifiedName.QualifiedModuleName.QualifyMemberName("NonExtensibleMember"), + nonExtensible, + nonExtensible, + string.Empty, + Accessibility.Global, + null, + Selection.Home, + true, + null, + new Attributes()); + state.AddDeclaration(member); + state.CoClasses.TryAdd(new List { member.IdentifierName }, nonExtensible); + } + +// [TestMethod] +// [TestCategory("Inspections")] +// public void MemberNotOnInterface_ReturnsResult_GlobalReference() +// { +// const string inputCode = +//@"Sub MemberTest() +// Extensible.Foo +//End Sub"; + +// //Arrange +// var builder = new MockVbeBuilder(); +// var project = builder.ProjectBuilder("VBAProject", ProjectProtection.Unprotected) +// .AddComponent("Codez", ComponentType.StandardModule, inputCode) +// .Build(); +// var vbe = builder.AddProject(project).Build(); + +// var mockHost = new Mock(); +// mockHost.SetupAllProperties(); +// var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock().Object)); + +// AddTestBuiltInLibrary(parser.State); + +// parser.Parse(new CancellationTokenSource()); +// if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); } + +// var inspection = new MemberNotOnInterfaceInspection(parser.State); +// var inspectionResults = inspection.GetInspectionResults(); + +// Assert.AreEqual(1, inspectionResults.Count()); +// } + +// [TestMethod] +// [TestCategory("Inspections")] +// public void MemberNotOnInterface_DoesNotReturnResult_DeclaredMember() +// { +// const string inputCode = +//@"Sub MemberTest() +// Extensible.ExtensibleMember +//End Sub"; + +// //Arrange +// var builder = new MockVbeBuilder(); +// var project = builder.ProjectBuilder("VBAProject", ProjectProtection.Unprotected) +// .AddComponent("Codez", ComponentType.StandardModule, inputCode) +// .Build(); +// var vbe = builder.AddProject(project).Build(); + +// var mockHost = new Mock(); +// mockHost.SetupAllProperties(); +// var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock().Object)); + +// parser.Parse(new CancellationTokenSource()); +// if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); } + +// AddTestBuiltInLibrary(parser.State); + +// var inspection = new MemberNotOnInterfaceInspection(parser.State); +// var inspectionResults = inspection.GetInspectionResults(); + +// Assert.IsFalse(inspectionResults.Any()); +// } + +// [TestMethod] +// [TestCategory("Inspections")] +// public void MemberNotOnInterface_DoesNotReturnResult_NonExtensible() +// { +// const string inputCode = +//@"Sub MemberTest() +// NonExtensible.Foo +//End Sub"; + +// //Arrange +// var builder = new MockVbeBuilder(); +// var project = builder.ProjectBuilder("VBAProject", ProjectProtection.Unprotected) +// .AddComponent("Codez", ComponentType.StandardModule, inputCode) +// .Build(); +// var vbe = builder.AddProject(project).Build(); + +// var mockHost = new Mock(); +// mockHost.SetupAllProperties(); +// var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock().Object)); + +// parser.Parse(new CancellationTokenSource()); +// if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); } + +// AddTestBuiltInLibrary(parser.State); + +// var inspection = new MemberNotOnInterfaceInspection(parser.State); +// var inspectionResults = inspection.GetInspectionResults(); + +// Assert.IsFalse(inspectionResults.Any()); +// } + } +} diff --git a/RubberduckTests/Inspections/UndeclaredVariableInspectionTests.cs b/RubberduckTests/Inspections/UndeclaredVariableInspectionTests.cs new file mode 100644 index 0000000000..ee0de45fda --- /dev/null +++ b/RubberduckTests/Inspections/UndeclaredVariableInspectionTests.cs @@ -0,0 +1,142 @@ +using System.Linq; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Rubberduck.Inspections; +using Rubberduck.Parsing.VBA; +using Rubberduck.VBEditor.Application; +using Rubberduck.VBEditor.Events; +using Rubberduck.VBEditor.SafeComWrappers; +using RubberduckTests.Mocks; + +namespace RubberduckTests.Inspections +{ + [TestClass] + public class UndeclaredVariableInspectionTests + { + [TestMethod] + [TestCategory("Inspections")] + public void UndeclaredVariable_ReturnsResult() + { + const string inputCode = +@"Sub Test() + a = 42 + Debug.Print a +End Sub"; + + //Arrange + var builder = new MockVbeBuilder(); + var project = builder.ProjectBuilder("VBAProject", ProjectProtection.Unprotected) + .AddComponent("MyClass", ComponentType.ClassModule, inputCode) + .Build(); + var vbe = builder.AddProject(project).Build(); + + var mockHost = new Mock(); + mockHost.SetupAllProperties(); + var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock().Object)); + + parser.Parse(new CancellationTokenSource()); + if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); } + + var inspection = new UndeclaredVariableInspection(parser.State); + var inspectionResults = inspection.GetInspectionResults(); + + Assert.AreEqual(1, inspectionResults.Count()); + } + + [TestMethod] + [TestCategory("Inspections")] + public void UndeclaredVariable_ReturnsNoResultIfDeclaredLocally() + { + const string inputCode = +@"Sub Test() + Dim a As Long + a = 42 + Debug.Print a +End Sub"; + + //Arrange + var builder = new MockVbeBuilder(); + var project = builder.ProjectBuilder("VBAProject", ProjectProtection.Unprotected) + .AddComponent("MyClass", ComponentType.ClassModule, inputCode) + .Build(); + var vbe = builder.AddProject(project).Build(); + + var mockHost = new Mock(); + mockHost.SetupAllProperties(); + var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock().Object)); + + parser.Parse(new CancellationTokenSource()); + if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); } + + var inspection = new UndeclaredVariableInspection(parser.State); + var inspectionResults = inspection.GetInspectionResults(); + + Assert.IsFalse(inspectionResults.Any()); + } + + [TestMethod] + [TestCategory("Inspections")] + public void UndeclaredVariable_ReturnsNoResultIfDeclaredModuleScope() + { + const string inputCode = +@"Private a As Long + +Sub Test() + a = 42 + Debug.Print a +End Sub"; + + //Arrange + var builder = new MockVbeBuilder(); + var project = builder.ProjectBuilder("VBAProject", ProjectProtection.Unprotected) + .AddComponent("MyClass", ComponentType.ClassModule, inputCode) + .Build(); + var vbe = builder.AddProject(project).Build(); + + var mockHost = new Mock(); + mockHost.SetupAllProperties(); + var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock().Object)); + + parser.Parse(new CancellationTokenSource()); + if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); } + + var inspection = new UndeclaredVariableInspection(parser.State); + var inspectionResults = inspection.GetInspectionResults(); + + Assert.IsFalse(inspectionResults.Any()); + } + + //https://github.com/rubberduck-vba/Rubberduck/issues/2525 + [TestMethod] + [TestCategory("Inspections")] + public void UndeclaredVariable_ReturnsNoResultIfAnnotated() + { + const string inputCode = +@"Sub Test() + '@Ignore UndeclaredVariable + a = 42 + Debug.Print a +End Sub"; + + //Arrange + var builder = new MockVbeBuilder(); + var project = builder.ProjectBuilder("VBAProject", ProjectProtection.Unprotected) + .AddComponent("MyClass", ComponentType.ClassModule, inputCode) + .Build(); + var vbe = builder.AddProject(project).Build(); + + var mockHost = new Mock(); + mockHost.SetupAllProperties(); + var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock().Object)); + + parser.Parse(new CancellationTokenSource()); + if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); } + + var inspection = new UndeclaredVariableInspection(parser.State); + var inspectionResults = inspection.GetInspectionResults(); + + Assert.IsFalse(inspectionResults.Any()); + } + } +} diff --git a/RubberduckTests/RubberduckTests.csproj b/RubberduckTests/RubberduckTests.csproj index ddb94302d7..4120864e47 100644 --- a/RubberduckTests/RubberduckTests.csproj +++ b/RubberduckTests/RubberduckTests.csproj @@ -90,6 +90,8 @@ + +