Skip to content

Commit 2cd87bf

Browse files
authored
Merge pull request #2431 from retailcoder/next
fixed handling of recursive functions in FunctionReturnValueNotUsed a…
2 parents c8a34d8 + 312829d commit 2cd87bf

9 files changed

+134
-30
lines changed

RetailCoder.VBE/Inspections/FunctionReturnValueNotUsedInspection.cs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public override IEnumerable<InspectionResultBase> GetInspectionResults()
3838

3939
private IEnumerable<FunctionReturnValueNotUsedInspectionResult> GetInterfaceMemberIssues(IEnumerable<Declaration> interfaceMembers)
4040
{
41-
return from interfaceMember in interfaceMembers
41+
return (from interfaceMember in interfaceMembers
4242
let implementationMembers =
4343
UserDeclarations.FindInterfaceImplementationMembers(interfaceMember.IdentifierName).ToList()
4444
where interfaceMember.DeclarationType == DeclarationType.Function &&
@@ -52,13 +52,14 @@ private IEnumerable<FunctionReturnValueNotUsedInspectionResult> GetInterfaceMemb
5252
implementationMember.Selection), implementationMember))
5353
select
5454
new FunctionReturnValueNotUsedInspectionResult(this, interfaceMember.Context,
55-
interfaceMember.QualifiedName, implementationMemberIssues, interfaceMember);
55+
interfaceMember.QualifiedName, implementationMemberIssues, interfaceMember)).ToList();
5656
}
5757

5858
private IEnumerable<FunctionReturnValueNotUsedInspectionResult> GetNonInterfaceIssues(IEnumerable<Declaration> nonInterfaceFunctions)
5959
{
6060
var returnValueNotUsedFunctions = nonInterfaceFunctions.Where(function => function.DeclarationType == DeclarationType.Function && !IsReturnValueUsed(function));
6161
var nonInterfaceIssues = returnValueNotUsedFunctions
62+
.Where(function => !IsRecursive(function))
6263
.Select(function =>
6364
new FunctionReturnValueNotUsedInspectionResult(
6465
this,
@@ -68,14 +69,15 @@ private IEnumerable<FunctionReturnValueNotUsedInspectionResult> GetNonInterfaceI
6869
return nonInterfaceIssues;
6970
}
7071

72+
private bool IsRecursive(Declaration function)
73+
{
74+
return function.References.Any(usage => usage.ParentScoping.Equals(function) && IsIndexExprOrCallStmt(usage));
75+
}
76+
7177
private bool IsReturnValueUsed(Declaration function)
7278
{
7379
foreach (var usage in function.References)
7480
{
75-
if (IsReturnStatement(function, usage))
76-
{
77-
continue;
78-
}
7981
if (IsAddressOfCall(usage))
8082
{
8183
continue;
@@ -84,7 +86,7 @@ private bool IsReturnValueUsed(Declaration function)
8486
{
8587
continue;
8688
}
87-
if (IsCallStmt(usage))
89+
if (IsCallStmt(usage)) // IsIndexExprOrCallStmt(usage))
8890
{
8991
continue;
9092
}
@@ -96,6 +98,10 @@ private bool IsReturnValueUsed(Declaration function)
9698
{
9799
continue;
98100
}
101+
if (IsReturnStatement(function, usage))
102+
{
103+
continue;
104+
}
99105
return true;
100106
}
101107
return false;
@@ -116,6 +122,11 @@ private bool IsReturnStatement(Declaration function, IdentifierReference assignm
116122
return assignment.ParentScoping.Equals(function) && assignment.Declaration.Equals(function);
117123
}
118124

125+
private bool IsIndexExprOrCallStmt(IdentifierReference usage)
126+
{
127+
return IsCallStmt(usage) || IsIndexExprContext(usage);
128+
}
129+
119130
private bool IsCallStmt(IdentifierReference usage)
120131
{
121132
var callStmt = ParserRuleContextHelper.GetParent<VBAParser.CallStmtContext>(usage.Context);
@@ -128,8 +139,22 @@ private bool IsCallStmt(IdentifierReference usage)
128139
{
129140
return true;
130141
}
131-
bool isUsedAsArgumentThusReturnValueIsUsed = ParserRuleContextHelper.HasParent(usage.Context, argumentList);
132-
return !isUsedAsArgumentThusReturnValueIsUsed;
142+
return !ParserRuleContextHelper.HasParent(usage.Context, argumentList);
143+
}
144+
145+
private bool IsIndexExprContext(IdentifierReference usage)
146+
{
147+
var indexExpr = ParserRuleContextHelper.GetParent<VBAParser.IndexExprContext>(usage.Context);
148+
if (indexExpr == null)
149+
{
150+
return false;
151+
}
152+
var argumentList = indexExpr.argumentList();
153+
if (argumentList == null)
154+
{
155+
return true;
156+
}
157+
return !ParserRuleContextHelper.HasParent(usage.Context, argumentList);
133158
}
134159

135160
private bool IsLet(IdentifierReference usage)

RetailCoder.VBE/Inspections/NonReturningFunctionInspection.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ public override IEnumerable<InspectionResultBase> GetInspectionResults()
4949

5050
return unassigned
5151
.Select(issue =>
52-
new NonReturningFunctionInspectionResult(this, new QualifiedContext<ParserRuleContext>(issue.QualifiedName, issue.Context),
53-
interfaceImplementationMembers.Select(m => m.Scope).Contains(issue.Scope), issue));
52+
new NonReturningFunctionInspectionResult(this, new QualifiedContext<ParserRuleContext>(issue.QualifiedName, issue.Context), issue,
53+
canConvertToProcedure: !IsRecursive(issue) && !interfaceImplementationMembers.Select(m => m.Scope).Contains(issue.Scope)));
5454
}
5555

5656
private bool IsReturningUserDefinedType(Declaration member)
@@ -70,6 +70,46 @@ private bool IsUserDefinedTypeAssigned(Declaration member)
7070
return result;
7171
}
7272

73+
private bool IsRecursive(Declaration function)
74+
{
75+
return function.References.Any(usage => usage.ParentScoping.Equals(function) && IsIndexExprOrCallStmt(usage));
76+
}
77+
78+
private bool IsIndexExprOrCallStmt(IdentifierReference usage)
79+
{
80+
return IsCallStmt(usage) || IsIndexExprContext(usage);
81+
}
82+
83+
private bool IsCallStmt(IdentifierReference usage)
84+
{
85+
var callStmt = ParserRuleContextHelper.GetParent<VBAParser.CallStmtContext>(usage.Context);
86+
if (callStmt == null)
87+
{
88+
return false;
89+
}
90+
var argumentList = CallStatement.GetArgumentList(callStmt);
91+
if (argumentList == null)
92+
{
93+
return true;
94+
}
95+
return !ParserRuleContextHelper.HasParent(usage.Context, argumentList);
96+
}
97+
98+
private bool IsIndexExprContext(IdentifierReference usage)
99+
{
100+
var indexExpr = ParserRuleContextHelper.GetParent<VBAParser.IndexExprContext>(usage.Context);
101+
if (indexExpr == null)
102+
{
103+
return false;
104+
}
105+
var argumentList = indexExpr.argumentList();
106+
if (argumentList == null)
107+
{
108+
return true;
109+
}
110+
return !ParserRuleContextHelper.HasParent(usage.Context, argumentList);
111+
}
112+
73113
/// <summary>
74114
/// A visitor that visits a member's body and returns <c>true</c> if any <c>LET</c> statement (assignment) is assigning the specified <c>name</c>.
75115
/// </summary>

RetailCoder.VBE/Inspections/ProcedureNotUsedInspection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ private bool IsIgnoredDeclaration(IEnumerable<Declaration> declarations, Declara
7777
{
7878
var enumerable = classes as IList<Declaration> ?? classes.ToList();
7979
var result = !ProcedureTypes.Contains(declaration.DeclarationType)
80-
|| declaration.References.Any(r => !r.IsAssignment)
80+
|| declaration.References.Any(r => !r.IsAssignment && !r.ParentScoping.Equals(declaration)) // recursive calls don't count
8181
|| handlers.Contains(declaration)
8282
|| IsPublicModuleMember(modules, declaration)
8383
|| IsClassLifeCycleHandler(enumerable, declaration)

RetailCoder.VBE/Inspections/Resources/InspectionsUI.Designer.cs

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

RetailCoder.VBE/Inspections/Resources/InspectionsUI.fr.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@
375375
<value>Le paramètre '{0}' passé par valeur est assigné</value>
376376
</data>
377377
<data name="FunctionReturnValueNotUsedInspectionMeta" xml:space="preserve">
378-
<value>Un membre est implémenté comme une fonction, mais utilisé comme une procédure. Considérez convertir la fonction en procédure.</value>
378+
<value>Un membre est implémenté comme une fonction, mais utilisé comme une procédure. À moins que la fonction soit récursive, considérez la convertir en procédure. Si la fonction est récursive, aucun appelant externe n'utilise sa valeur de retour.</value>
379379
</data>
380380
<data name="IdentifierNotUsedInspectionResultFormat" xml:space="preserve">
381381
<value>{0} '{1}' n'est pas utilisé(e).</value>

RetailCoder.VBE/Inspections/Resources/InspectionsUI.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ If the parameter can be null, ignore this inspection result; passing a null valu
379379
<value>Parameter '{0}' is passed ByVal and assigned a value</value>
380380
</data>
381381
<data name="FunctionReturnValueNotUsedInspectionMeta" xml:space="preserve">
382-
<value>A member is written as a function, but used as a procedure. Consider converting the 'Function' into a 'Sub'.</value>
382+
<value>A member is written as a function, but used as a procedure. Unless the function is recursive, consider converting the 'Function' into a 'Sub'. If the function is recursive, none of its external callers are using the returned value.</value>
383383
</data>
384384
<data name="IdentifierNotUsedInspectionResultFormat" xml:space="preserve">
385385
<value>{0} '{1}' is not used</value>

RetailCoder.VBE/Inspections/Results/FunctionReturnValueNotUsedInspectionResult.cs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
namespace Rubberduck.Inspections.Results
1414
{
15+
16+
1517
public class FunctionReturnValueNotUsedInspectionResult : InspectionResultBase
1618
{
1719
private readonly IEnumerable<QuickFixBase> _quickFixes;
@@ -20,8 +22,9 @@ public FunctionReturnValueNotUsedInspectionResult(
2022
IInspection inspection,
2123
ParserRuleContext context,
2224
QualifiedMemberName qualifiedName,
23-
Declaration target)
24-
: this(inspection, context, qualifiedName, new List<Tuple<ParserRuleContext, QualifiedSelection, Declaration>>(), target)
25+
Declaration target,
26+
bool allowConvertToProcedure = true)
27+
: this(inspection, context, qualifiedName, new List<Tuple<ParserRuleContext, QualifiedSelection, Declaration>>(), target, allowConvertToProcedure)
2528
{
2629
}
2730

@@ -30,17 +33,25 @@ public FunctionReturnValueNotUsedInspectionResult(
3033
ParserRuleContext context,
3134
QualifiedMemberName qualifiedName,
3235
IEnumerable<Tuple<ParserRuleContext, QualifiedSelection, Declaration>> children,
33-
Declaration target)
36+
Declaration target, bool allowConvertToProcedure = true)
3437
: base(inspection, qualifiedName.QualifiedModuleName, context, target)
3538
{
36-
var root = new ConvertToProcedureQuickFix(context, QualifiedSelection, target);
37-
var compositeFix = new CompositeCodeInspectionFix(root);
38-
children.ToList().ForEach(child => compositeFix.AddChild(new ConvertToProcedureQuickFix(child.Item1, child.Item2, child.Item3)));
39-
_quickFixes = new QuickFixBase[]
39+
var ignoreOnce = new IgnoreOnceQuickFix(Context, QualifiedSelection, Inspection.AnnotationName);
40+
if (allowConvertToProcedure)
4041
{
41-
compositeFix,
42-
new IgnoreOnceQuickFix(Context, QualifiedSelection, Inspection.AnnotationName)
43-
};
42+
var root = new ConvertToProcedureQuickFix(context, QualifiedSelection, target);
43+
var compositeFix = new CompositeCodeInspectionFix(root);
44+
children.ToList().ForEach(child => compositeFix.AddChild(new ConvertToProcedureQuickFix(child.Item1, child.Item2, child.Item3)));
45+
_quickFixes = new QuickFixBase[]
46+
{
47+
compositeFix,
48+
ignoreOnce
49+
};
50+
}
51+
else
52+
{
53+
_quickFixes = new[] {ignoreOnce};
54+
}
4455
}
4556

4657
public override IEnumerable<QuickFixBase> QuickFixes { get { return _quickFixes; } }

RetailCoder.VBE/Inspections/Results/NonReturningFunctionInspectionResult.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,17 @@ public sealed class NonReturningFunctionInspectionResult : InspectionResultBase
1616

1717
public NonReturningFunctionInspectionResult(IInspection inspection,
1818
QualifiedContext<ParserRuleContext> qualifiedContext,
19-
bool isInterfaceImplementation,
20-
Declaration target)
19+
Declaration target, bool canConvertToProcedure)
2120
: base(inspection, qualifiedContext.ModuleName, qualifiedContext.Context, target)
2221
{
23-
_quickFixes = isInterfaceImplementation
22+
_quickFixes = canConvertToProcedure
2423
? new QuickFixBase[]
2524
{
25+
new ConvertToProcedureQuickFix(Context, QualifiedSelection, target),
2626
new IgnoreOnceQuickFix(Context, QualifiedSelection, Inspection.AnnotationName),
2727
}
2828
: new QuickFixBase[]
2929
{
30-
new ConvertToProcedureQuickFix(Context, QualifiedSelection, target),
3130
new IgnoreOnceQuickFix(Context, QualifiedSelection, Inspection.AnnotationName),
3231
};
3332
}

RubberduckTests/Inspections/FunctionReturnValueNotUsedInspectionTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,35 @@ Public Sub Baz()
332332
Assert.AreEqual(0, inspectionResults.Count());
333333
}
334334

335+
[TestMethod]
336+
public void FunctionReturnValueNotUsed_DoesNotReturnResult_RecursiveFunction()
337+
{
338+
const string inputCode =
339+
@"Public Function Factorial(ByVal n As Long) As Long
340+
If n <= 1 Then
341+
Factorial = 1
342+
Else
343+
Factorial = Factorial(n - 1) * n
344+
End If
345+
End Function";
346+
347+
//Arrange
348+
var builder = new MockVbeBuilder();
349+
IVBComponent component;
350+
var vbe = builder.BuildFromSingleStandardModule(inputCode, out component);
351+
var mockHost = new Mock<IHostApplication>();
352+
mockHost.SetupAllProperties();
353+
var parser = MockParser.Create(vbe.Object, new RubberduckParserState(new Mock<ISinks>().Object));
354+
355+
parser.Parse(new CancellationTokenSource());
356+
if (parser.State.Status >= ParserState.Error) { Assert.Inconclusive("Parser Error"); }
357+
358+
var inspection = new FunctionReturnValueNotUsedInspection(parser.State);
359+
var inspectionResults = inspection.GetInspectionResults();
360+
361+
Assert.AreEqual(0, inspectionResults.Count());
362+
}
363+
335364
[TestMethod]
336365
public void FunctionReturnValueNotUsed_DoesNotReturnResult_ArgumentFunctionCall()
337366
{

0 commit comments

Comments
 (0)