|
1 |
| -using System.Collections.Generic; |
2 |
| -using System.Linq; |
3 |
| -using Antlr4.Runtime; |
| 1 | +using System.Linq; |
4 | 2 | using Rubberduck.Inspections.Abstract;
|
5 |
| -using Rubberduck.Inspections.Results; |
6 | 3 | using Rubberduck.Parsing;
|
7 | 4 | using Rubberduck.Parsing.Grammar;
|
8 |
| -using Rubberduck.Parsing.Inspections.Abstract; |
9 | 5 | using Rubberduck.Resources.Inspections;
|
10 | 6 | using Rubberduck.Parsing.Symbols;
|
11 | 7 | using Rubberduck.Parsing.VBA;
|
12 |
| -using Rubberduck.Parsing.VBA.Parsing; |
| 8 | +using Rubberduck.Parsing.VBA.DeclarationCaching; |
13 | 9 |
|
14 | 10 | namespace Rubberduck.Inspections.Concrete
|
15 | 11 | {
|
@@ -39,75 +35,85 @@ namespace Rubberduck.Inspections.Concrete
|
39 | 35 | /// End Function
|
40 | 36 | /// ]]>
|
41 | 37 | /// </example>
|
42 |
| - public sealed class ProcedureCanBeWrittenAsFunctionInspection : InspectionBase, IParseTreeInspection |
| 38 | + /// <example hasResults="false"> |
| 39 | + /// <![CDATA[ |
| 40 | + /// Option Explicit |
| 41 | + /// Public Function DoSomething(ByVal arg As Long) As Long |
| 42 | + /// ' ... |
| 43 | + /// DoSomething = 42 |
| 44 | + /// End Function |
| 45 | + /// ]]> |
| 46 | + /// </example> |
| 47 | + public sealed class ProcedureCanBeWrittenAsFunctionInspection : DeclarationInspectionBase |
43 | 48 | {
|
44 | 49 | public ProcedureCanBeWrittenAsFunctionInspection(IDeclarationFinderProvider declarationFinderProvider)
|
45 |
| - : base(declarationFinderProvider) |
46 |
| - { |
47 |
| - Listener = new SingleByRefParamArgListListener(); |
48 |
| - } |
| 50 | + : base(declarationFinderProvider, new []{DeclarationType.Procedure}, new []{DeclarationType.LibraryProcedure, DeclarationType.PropertyLet, DeclarationType.PropertySet}) |
| 51 | + {} |
49 | 52 |
|
50 |
| - public CodeKind TargetKindOfCode => CodeKind.CodePaneCode; |
51 |
| - public IInspectionListener Listener { get; } |
52 |
| - |
53 |
| - //FIXME This should really be a declaration inspection. |
54 |
| - |
55 |
| - protected override IEnumerable<IInspectionResult> DoGetInspectionResults() |
| 53 | + protected override bool IsResultDeclaration(Declaration declaration, DeclarationFinder finder) |
56 | 54 | {
|
57 |
| - if (!Listener.Contexts().Any()) |
| 55 | + if (!(declaration is ModuleBodyElementDeclaration member) |
| 56 | + || member.IsInterfaceImplementation |
| 57 | + || member.IsInterfaceMember |
| 58 | + || finder.FindEventHandlers().Contains(member) |
| 59 | + || member.Parameters.Count(param => param.IsByRef && !param.IsParamArray) != 1) |
58 | 60 | {
|
59 |
| - return Enumerable.Empty<IInspectionResult>(); |
| 61 | + return false; |
60 | 62 | }
|
61 | 63 |
|
62 |
| - var finder = DeclarationFinderProvider.DeclarationFinder; |
63 |
| - |
64 |
| - var userDeclarations = finder.AllUserDeclarations.ToList(); |
65 |
| - var builtinHandlers = finder.FindEventHandlers().ToList(); |
| 64 | + var parameter = member.Parameters.First(param => param.IsByRef && !param.IsParamArray); |
| 65 | + var parameterReferences = parameter.References.ToList(); |
66 | 66 |
|
67 |
| - var contextLookup = userDeclarations.Where(decl => decl.Context != null).ToDictionary(decl => decl.Context); |
| 67 | + return parameterReferences.Any(reference => IsAssignment(reference, finder)); |
| 68 | + } |
68 | 69 |
|
69 |
| - var ignored = new HashSet<Declaration>(finder.FindAllInterfaceMembers() |
70 |
| - .Concat(finder.FindAllInterfaceImplementingMembers()) |
71 |
| - .Concat(builtinHandlers) |
72 |
| - .Concat(userDeclarations.Where(item => item.IsWithEvents))); |
| 70 | + private static bool IsAssignment(IdentifierReference reference, DeclarationFinder finder) |
| 71 | + { |
| 72 | + return reference.IsAssignment |
| 73 | + || IsUsageAsAssignedToByRefArgument(reference, finder); |
| 74 | + } |
73 | 75 |
|
74 |
| - return Listener.Contexts() |
75 |
| - .Where(context => context.Context.Parent is VBAParser.SubStmtContext |
76 |
| - && HasArgumentReferencesWithIsAssignmentFlagged(context)) |
77 |
| - .Select(GetSubStmtParentDeclaration) |
78 |
| - .Where(decl => decl != null && |
79 |
| - !ignored.Contains(decl) && |
80 |
| - userDeclarations.Where(item => item.IsWithEvents) |
81 |
| - .All(withEvents => !finder.FindHandlersForWithEventsField(withEvents).Any()) && |
82 |
| - !builtinHandlers.Contains(decl)) |
83 |
| - .Select(result => new DeclarationInspectionResult(this, |
84 |
| - string.Format(InspectionResults.ProcedureCanBeWrittenAsFunctionInspection, result.IdentifierName), |
85 |
| - result)); |
| 76 | + private static bool IsUsageAsAssignedToByRefArgument(IdentifierReference reference, DeclarationFinder finder) |
| 77 | + { |
| 78 | + var argExpression = ImmediateArgumentExpressionContext(reference); |
86 | 79 |
|
87 |
| - bool HasArgumentReferencesWithIsAssignmentFlagged(QualifiedContext<ParserRuleContext> context) |
| 80 | + if (argExpression == null) |
88 | 81 | {
|
89 |
| - return contextLookup.TryGetValue(context.Context.GetChild<VBAParser.ArgContext>(), out Declaration decl) |
90 |
| - && decl.References.Any(rf => rf.IsAssignment); |
| 82 | + return false; |
91 | 83 | }
|
92 | 84 |
|
93 |
| - Declaration GetSubStmtParentDeclaration(QualifiedContext<ParserRuleContext> context) |
| 85 | + var parameter = finder.FindParameterOfNonDefaultMemberFromSimpleArgumentNotPassedByValExplicitly(argExpression, reference.QualifiedModuleName); |
| 86 | + |
| 87 | + if (parameter == null) |
94 | 88 | {
|
95 |
| - return contextLookup.TryGetValue(context.Context.Parent as VBAParser.SubStmtContext, out Declaration decl) |
96 |
| - ? decl |
97 |
| - : null; |
| 89 | + //We have no idea what parameter it is passed to as argument. So, we have to err on the safe side and assume it is not passed by reference. |
| 90 | + return false; |
98 | 91 | }
|
| 92 | + |
| 93 | + //We go only one level deep and make a conservative check to avoid a potentially costly recursion. |
| 94 | + return parameter.IsByRef |
| 95 | + && parameter.References.Any(paramReference => paramReference.IsAssignment); |
99 | 96 | }
|
100 | 97 |
|
101 |
| - public class SingleByRefParamArgListListener : InspectionListenerBase |
| 98 | + private static VBAParser.ArgumentExpressionContext ImmediateArgumentExpressionContext(IdentifierReference reference) |
102 | 99 | {
|
103 |
| - public override void ExitArgList(VBAParser.ArgListContext context) |
104 |
| - { |
105 |
| - var args = context.arg(); |
106 |
| - if (args != null && args.All(a => a.PARAMARRAY() == null && a.LPAREN() == null) && args.Count(a => a.BYREF() != null || (a.BYREF() == null && a.BYVAL() == null)) == 1) |
107 |
| - { |
108 |
| - SaveContext(context); |
109 |
| - } |
110 |
| - } |
| 100 | + var context = reference.Context; |
| 101 | + //The context is either already a simpleNameExprContext or an IdentifierValueContext used in a sub-rule of some other lExpression alternative. |
| 102 | + var lExpressionNameContext = context is VBAParser.SimpleNameExprContext simpleName |
| 103 | + ? simpleName |
| 104 | + : context.GetAncestor<VBAParser.LExpressionContext>(); |
| 105 | + |
| 106 | + //To be an immediate argument and, thus, assignable by ref, the structure must be argumentExpression -> expression -> lExpression. |
| 107 | + return lExpressionNameContext? |
| 108 | + .Parent? |
| 109 | + .Parent as VBAParser.ArgumentExpressionContext; |
| 110 | + } |
| 111 | + |
| 112 | + protected override string ResultDescription(Declaration declaration) |
| 113 | + { |
| 114 | + return string.Format( |
| 115 | + InspectionResults.ProcedureCanBeWrittenAsFunctionInspection, |
| 116 | + declaration.IdentifierName); |
111 | 117 | }
|
112 | 118 | }
|
113 | 119 | }
|
0 commit comments