Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-163 - better argument matcher enclosing expression detection #166

Merged
merged 2 commits into from
Sep 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ namespace NSubstitute.Analyzers.CSharp.CodeFixProviders
internal sealed class NonSubstitutableMemberArgumentMatcherSuppressDiagnosticsCodeFixProvider
: AbstractNonSubstitutableMemberArgumentMatcherSuppressDiagnosticsCodeFixProvider
{
protected override ImmutableArray<ImmutableArray<int>> AllowedAncestorPaths { get; } = NonSubstitutableMemberArgumentMatcherAnalyzer.AllowedPaths;
protected override ImmutableHashSet<int> MaybeAllowedArgMatcherAncestors { get; } = NonSubstitutableMemberArgumentMatcherAnalyzer.MaybeAllowedAncestors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,24 @@ namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class NonSubstitutableMemberArgumentMatcherAnalyzer : AbstractNonSubstitutableMemberArgumentMatcherAnalyzer<SyntaxKind, InvocationExpressionSyntax>
{
internal static ImmutableArray<ImmutableArray<int>> AllowedPaths { get; } = ImmutableArray.Create(
ImmutableArray.Create(
(int)SyntaxKind.Argument,
(int)SyntaxKind.ArgumentList,
(int)SyntaxKind.InvocationExpression),
ImmutableArray.Create(
(int)SyntaxKind.Argument,
(int)SyntaxKind.BracketedArgumentList,
(int)SyntaxKind.ElementAccessExpression),
ImmutableArray.Create(
(int)SyntaxKind.CastExpression,
(int)SyntaxKind.Argument,
(int)SyntaxKind.ArgumentList,
(int)SyntaxKind.InvocationExpression),
ImmutableArray.Create(
(int)SyntaxKind.AsExpression,
(int)SyntaxKind.Argument,
(int)SyntaxKind.ArgumentList,
(int)SyntaxKind.InvocationExpression),
ImmutableArray.Create(
(int)SyntaxKind.CastExpression,
(int)SyntaxKind.Argument,
(int)SyntaxKind.BracketedArgumentList,
(int)SyntaxKind.ElementAccessExpression),
ImmutableArray.Create(
(int)SyntaxKind.AsExpression,
(int)SyntaxKind.Argument,
(int)SyntaxKind.BracketedArgumentList,
(int)SyntaxKind.ElementAccessExpression),
ImmutableArray.Create((int)SyntaxKind.AddAssignmentExpression));
internal static ImmutableHashSet<int> MaybeAllowedAncestors { get; } = ImmutableHashSet.Create(
(int)SyntaxKind.InvocationExpression,
(int)SyntaxKind.ElementAccessExpression,
(int)SyntaxKind.AddAssignmentExpression,
(int)SyntaxKind.ObjectCreationExpression);

private static ImmutableArray<ImmutableArray<int>> IgnoredPaths { get; } = ImmutableArray.Create(
ImmutableArray.Create(
(int)SyntaxKind.EqualsValueClause,
(int)SyntaxKind.VariableDeclarator),
ImmutableArray.Create(
(int)SyntaxKind.CastExpression,
(int)SyntaxKind.EqualsValueClause,
(int)SyntaxKind.VariableDeclarator),
ImmutableArray.Create(
(int)SyntaxKind.AsExpression,
(int)SyntaxKind.EqualsValueClause,
(int)SyntaxKind.VariableDeclarator));
private static ImmutableHashSet<int> IgnoredAncestors { get; } =
ImmutableHashSet.Create(
(int)SyntaxKind.VariableDeclarator);

public NonSubstitutableMemberArgumentMatcherAnalyzer()
: base(NonSubstitutableMemberAnalysis.Instance, NSubstitute.Analyzers.CSharp.DiagnosticDescriptorsProvider.Instance)
{
}

protected override ImmutableArray<ImmutableArray<int>> AllowedAncestorPaths { get; } = AllowedPaths;
protected override ImmutableHashSet<int> MaybeAllowedArgMatcherAncestors { get; } = MaybeAllowedAncestors;

protected override ImmutableArray<ImmutableArray<int>> IgnoredAncestorPaths { get; } = IgnoredPaths;
protected override ImmutableHashSet<int> IgnoredArgMatcherAncestors { get; } = IgnoredAncestors;

protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using NSubstitute.Analyzers.Shared.Extensions;

namespace NSubstitute.Analyzers.Shared.CodeFixProviders
{
internal abstract class AbstractNonSubstitutableMemberArgumentMatcherSuppressDiagnosticsCodeFixProvider : AbstractSuppressDiagnosticsCodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticIdentifiers.NonSubstitutableMemberArgumentMatcherUsage);

protected abstract ImmutableArray<ImmutableArray<int>> AllowedAncestorPaths { get; }
protected abstract ImmutableHashSet<int> MaybeAllowedArgMatcherAncestors { get; }

protected override IEnumerable<ISymbol> GetSuppressibleSymbol(SemanticModel model, SyntaxNode syntaxNode, ISymbol symbol)
{
var ancestorNode = syntaxNode.GetAncestorNode(AllowedAncestorPaths);
var ancestorNode = syntaxNode.Ancestors()
.FirstOrDefault(ancestor => MaybeAllowedArgMatcherAncestors.Contains(ancestor.RawKind));

if (ancestorNode == null)
{
return Enumerable.Empty<ISymbol>();
}

if (model.GetSymbolInfo(ancestorNode).Symbol is IMethodSymbol methodSymbol && methodSymbol.MethodKind == MethodKind.Constructor)
{
return Enumerable.Empty<ISymbol>();
}

return base.GetSuppressibleSymbol(model, ancestorNode, model.GetSymbolInfo(ancestorNode).Symbol);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared.Extensions;
Expand All @@ -14,9 +15,9 @@ internal abstract class AbstractNonSubstitutableMemberArgumentMatcherAnalyzer<TS

private readonly INonSubstitutableMemberAnalysis _nonSubstitutableMemberAnalysis;

protected abstract ImmutableArray<ImmutableArray<int>> AllowedAncestorPaths { get; }
protected abstract ImmutableHashSet<int> MaybeAllowedArgMatcherAncestors { get; }

protected abstract ImmutableArray<ImmutableArray<int>> IgnoredAncestorPaths { get; }
protected abstract ImmutableHashSet<int> IgnoredArgMatcherAncestors { get; }

protected abstract TSyntaxKind InvocationExpressionKind { get; }

Expand Down Expand Up @@ -59,16 +60,15 @@ private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext)

private void AnalyzeArgLikeMethod(SyntaxNodeAnalysisContext syntaxNodeContext, TInvocationExpressionSyntax argInvocationExpression)
{
// find allowed enclosing expression
var enclosingExpression = FindAllowedEnclosingExpression(argInvocationExpression);
var enclosingExpression = FindMaybeAllowedEnclosingExpression(argInvocationExpression);

// if Arg is used with not allowed expression, find if it is used in ignored ones eg. var x = Arg.Any
// as variable might be used later on
if (enclosingExpression == null)
{
var maybeIgnoredEnclosingExpression = FindMaybeIgnoredEnclosingExpression(argInvocationExpression);
var ignoredEnclosingExpression = FindIgnoredEnclosingExpression(argInvocationExpression);

if (maybeIgnoredEnclosingExpression == null)
if (ignoredEnclosingExpression == null)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptorsProvider.NonSubstitutableMemberArgumentMatcherUsage,
Expand Down Expand Up @@ -108,14 +108,16 @@ private void AnalyzeArgLikeMethod(SyntaxNodeAnalysisContext syntaxNodeContext, T
}
}

private SyntaxNode FindAllowedEnclosingExpression(TInvocationExpressionSyntax invocationExpression)
{
return invocationExpression.GetAncestorNode(AllowedAncestorPaths);
}
private SyntaxNode FindMaybeAllowedEnclosingExpression(TInvocationExpressionSyntax invocationExpression) =>
FindEnclosingExpression(invocationExpression, MaybeAllowedArgMatcherAncestors);

private SyntaxNode FindIgnoredEnclosingExpression(TInvocationExpressionSyntax invocationExpressionSyntax) =>
FindEnclosingExpression(invocationExpressionSyntax, IgnoredArgMatcherAncestors);

private SyntaxNode FindMaybeIgnoredEnclosingExpression(TInvocationExpressionSyntax invocationExpressionSyntax)
private static SyntaxNode FindEnclosingExpression(TInvocationExpressionSyntax invocationExpression, ImmutableHashSet<int> ancestors)
{
return invocationExpressionSyntax.GetAncestorNode(IgnoredAncestorPaths);
return invocationExpression.Ancestors()
.FirstOrDefault(ancestor => ancestors.Contains(ancestor.RawKind));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ namespace NSubstitute.Analyzers.VisualBasic.CodeFixProviders
[ExportCodeFixProvider(LanguageNames.VisualBasic)]
internal sealed class NonSubstitutableMemberArgumentMatcherSuppressDiagnosticsCodeFixProvider : AbstractNonSubstitutableMemberArgumentMatcherSuppressDiagnosticsCodeFixProvider
{
protected override ImmutableArray<ImmutableArray<int>> AllowedAncestorPaths { get; } = NonSubstitutableMemberArgumentMatcherAnalyzer.AllowedPaths;
protected override ImmutableHashSet<int> MaybeAllowedArgMatcherAncestors { get; } = NonSubstitutableMemberArgumentMatcherAnalyzer.MaybeAllowedAncestors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,53 +10,22 @@ namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers
[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
internal sealed class NonSubstitutableMemberArgumentMatcherAnalyzer : AbstractNonSubstitutableMemberArgumentMatcherAnalyzer<SyntaxKind, InvocationExpressionSyntax>
{
internal static ImmutableArray<ImmutableArray<int>> AllowedPaths { get; } = ImmutableArray.Create(
ImmutableArray.Create(
(int)SyntaxKind.SimpleArgument,
(int)SyntaxKind.ArgumentList,
(int)SyntaxKind.InvocationExpression),
ImmutableArray.Create(
(int)SyntaxKind.TryCastExpression,
(int)SyntaxKind.SimpleArgument,
(int)SyntaxKind.ArgumentList,
(int)SyntaxKind.InvocationExpression),
ImmutableArray.Create(
(int)SyntaxKind.DirectCastExpression,
(int)SyntaxKind.SimpleArgument,
(int)SyntaxKind.ArgumentList,
(int)SyntaxKind.InvocationExpression),
ImmutableArray.Create(
(int)SyntaxKind.CTypeExpression,
(int)SyntaxKind.SimpleArgument,
(int)SyntaxKind.ArgumentList,
(int)SyntaxKind.InvocationExpression),
ImmutableArray.Create((int)SyntaxKind.AddHandlerStatement));
internal static ImmutableHashSet<int> MaybeAllowedAncestors { get; } = ImmutableHashSet.Create(
(int)SyntaxKind.InvocationExpression,
(int)SyntaxKind.AddHandlerStatement,
(int)SyntaxKind.ObjectCreationExpression);

private static ImmutableArray<ImmutableArray<int>> IgnoredPaths { get; } = ImmutableArray.Create(
ImmutableArray.Create(
(int)SyntaxKind.EqualsValue,
(int)SyntaxKind.VariableDeclarator),
ImmutableArray.Create(
(int)SyntaxKind.TryCastExpression,
(int)SyntaxKind.EqualsValue,
(int)SyntaxKind.VariableDeclarator),
ImmutableArray.Create(
(int)SyntaxKind.DirectCastExpression,
(int)SyntaxKind.EqualsValue,
(int)SyntaxKind.VariableDeclarator),
ImmutableArray.Create(
(int)SyntaxKind.CTypeExpression,
(int)SyntaxKind.EqualsValue,
(int)SyntaxKind.VariableDeclarator));
private static ImmutableHashSet<int> IgnoredAncestors { get; } = ImmutableHashSet.Create(
(int)SyntaxKind.VariableDeclarator);

public NonSubstitutableMemberArgumentMatcherAnalyzer()
: base(NonSubstitutableMemberAnalysis.Instance, NSubstitute.Analyzers.VisualBasic.DiagnosticDescriptorsProvider.Instance)
{
}

protected override ImmutableArray<ImmutableArray<int>> AllowedAncestorPaths { get; } = AllowedPaths;
protected override ImmutableHashSet<int> MaybeAllowedArgMatcherAncestors { get; } = MaybeAllowedAncestors;

protected override ImmutableArray<ImmutableArray<int>> IgnoredAncestorPaths { get; } = IgnoredPaths;
protected override ImmutableHashSet<int> IgnoredArgMatcherAncestors { get; } = IgnoredAncestors;

protected override SyntaxKind InvocationExpressionKind { get; } = SyntaxKind.InvocationExpression;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ public static IEnumerable<object[]> MisusedArgTestCases
yield return new object[] { "[|Arg.Compat.Is(1)|]" };
yield return new object[] { "(int)[|Arg.Compat.Is(1)|]" };
yield return new object[] { "[|Arg.Compat.Is(1)|] as int?" };
yield return new object[] { "[|Arg.Do<int>(_ => {})|]" };
yield return new object[] { "[|Arg.Compat.Do<int>(_ => {})|]" };
yield return new object[] { "[|Arg.Do<int>(__ => {})|]" };
yield return new object[] { "[|Arg.Compat.Do<int>(__ => {})|]" };
yield return new object[] { "[|Arg.Invoke()|]" };
yield return new object[] { "[|Arg.Compat.Invoke()|]" };
yield return new object[] { "[|Arg.InvokeDelegate<int>()|]" };
Expand All @@ -147,11 +147,15 @@ public static IEnumerable<object[]> CorrectlyUsedArgTestCases
yield return new object[] { "Arg.Is(1)" };
yield return new object[] { "(int)Arg.Is(1)" };
yield return new object[] { "Arg.Is(1) as int?" };
yield return new object[] { "Arg.Is((int x) => x > 0)" };
yield return new object[] { "true ? Arg.Is((int x) => x > 0) : 0" };
yield return new object[] { "Arg.Compat.Is(1)" };
yield return new object[] { "(int)Arg.Compat.Is(1)" };
yield return new object[] { "Arg.Compat.Is(1) as int?" };
yield return new object[] { "Arg.Do<int>(_ => {})" };
yield return new object[] { "Arg.Compat.Do<int>(_ => {})" };
yield return new object[] { "Arg.Compat.Is((int x) => x > 0) " };
yield return new object[] { "true ? Arg.Compat.Is((int x) => x > 0) : 0" };
yield return new object[] { "Arg.Do<int>(__ => {})" };
yield return new object[] { "Arg.Compat.Do<int>(__ => {})" };
yield return new object[] { "Arg.Invoke()" };
yield return new object[] { "Arg.Compat.Invoke()" };
yield return new object[] { "Arg.InvokeDelegate<int>()" };
Expand All @@ -171,8 +175,8 @@ public static IEnumerable<object[]> CorrectlyUsedArgTestCasesWithoutCasts
yield return new object[] { "Arg.Compat.Any<int>()" };
yield return new object[] { "Arg.Is(1)" };
yield return new object[] { "Arg.Compat.Is(1)" };
yield return new object[] { "Arg.Do<int>(_ => {})" };
yield return new object[] { "Arg.Compat.Do<int>(_ => {})" };
yield return new object[] { "Arg.Do<int>(__ => {})" };
yield return new object[] { "Arg.Compat.Do<int>(__ => {})" };
yield return new object[] { "Arg.Invoke()" };
yield return new object[] { "Arg.Compat.Invoke()" };
yield return new object[] { "Arg.InvokeDelegate<int>()" };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ public class FooTests
public void Test()
{{
var substitute = NSubstitute.Substitute.For<IFoo>();
var x = substitute[{arg}];
var _ = substitute[{arg}];
}}
}}
}}";
Expand All @@ -325,7 +325,7 @@ public class FooTests
public void Test()
{{
var substitute = NSubstitute.Substitute.For<Foo>();
var x = substitute[{arg}];
var _ = substitute[{arg}];
}}
}}
}}";
Expand Down Expand Up @@ -362,6 +362,7 @@ public void Test()
public override async Task ReportsNoDiagnostics_WhenUsingUnfortunatelyNamedMethod(string arg)
{
var source = $@"using System;
using System.Linq.Expressions;
using NSubstitute;

namespace MyNamespace
Expand Down Expand Up @@ -391,6 +392,11 @@ public static T Is<T>(T value)
return default(T);
}}

public static T Is<T>(Expression<Predicate<T>> predicate)
{{
return default(T);
}}

public static Action Invoke()
{{
return default(Action);
Expand Down Expand Up @@ -418,6 +424,11 @@ public static T Is<T>(T value)
return default(T);
}}

public static T Is<T>(Expression<Predicate<T>> predicate)
{{
return default(T);
}}

public static Action Invoke()
{{
return default(Action);
Expand Down Expand Up @@ -575,7 +586,7 @@ public class FooTests
public void Test()
{{
var substitute = NSubstitute.Substitute.For<Foo>();
var x = substitute.FooBar({arg});
var _ = substitute.FooBar({arg});
}}
}}
}}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,14 @@ public static IEnumerable<object[]> CorrectlyUsedArgTestCases
yield return new object[] { "TryCast(Arg.Is(1), Object)" };
yield return new object[] { "CType(Arg.Is(1), Integer)" };
yield return new object[] { "DirectCast(Arg.Is(1), Integer)" };
yield return new object[] { "Arg.Is(Function(ByVal x As Integer) x > 0)" };
yield return new object[] { "If(True, Arg.Is(Function(ByVal x As Integer) x > 0), 0)" };
yield return new object[] { "Arg.Compat.Is(1)" };
yield return new object[] { "TryCast(Arg.Compat.Is(1), Object)" };
yield return new object[] { "CType(Arg.Compat.Is(1), Integer)" };
yield return new object[] { "DirectCast(Arg.Compat.Is(1), Integer)" };
yield return new object[] { "Arg.Compat.Is(Function(ByVal x As Integer) x > 0)" };
yield return new object[] { "If(True, Arg.Compat.Is(Function(ByVal x As Integer) x > 0), 0)" };
yield return new object[] { "Arg.Invoke()" };
yield return new object[] { "Arg.Compat.Invoke()" };
yield return new object[] { "Arg.InvokeDelegate(Of Integer)()" };
Expand Down
Loading