Skip to content

Commit

Permalink
GH-164 - throws used in async method diagnostic
Browse files Browse the repository at this point in the history
  • Loading branch information
tpodolak committed Dec 5, 2021
1 parent 3eadc24 commit 892034e
Show file tree
Hide file tree
Showing 16 changed files with 573 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers;

namespace NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal class AsyncThrowsAnalyzer : AbstractAsyncThrowsAnalyzer<SyntaxKind, InvocationExpressionSyntax>
{
public AsyncThrowsAnalyzer()
: base(CSharp.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance)
{
}

protected override SyntaxKind InvocationExpressionKind => SyntaxKind.InvocationExpression;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,7 @@ internal class AbstractDiagnosticDescriptorsProvider<T> : IDiagnosticDescriptors
public DiagnosticDescriptor ReceivedUsedInReceivedInOrder { get; } = DiagnosticDescriptors<T>.ReceivedUsedInReceivedInOrder;

public DiagnosticDescriptor AsyncCallbackUsedInReceivedInOrder { get; } = DiagnosticDescriptors<T>.AsyncCallbackUsedInReceivedInOrder;

public DiagnosticDescriptor AsyncThrows { get; } = DiagnosticDescriptors<T>.AsyncThrows;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Shared.Extensions;

namespace NSubstitute.Analyzers.Shared.DiagnosticAnalyzers
{
internal abstract class
AbstractAsyncThrowsAnalyzer<TSyntaxKind, TInvocationExpressionSyntax> : AbstractDiagnosticAnalyzer
where TInvocationExpressionSyntax : SyntaxNode
where TSyntaxKind : struct
{
private readonly ISubstitutionNodeFinder<TInvocationExpressionSyntax> _substitutionNodeFinder;
private readonly Action<SyntaxNodeAnalysisContext> _analyzeInvocationAction;

protected abstract TSyntaxKind InvocationExpressionKind { get; }

protected AbstractAsyncThrowsAnalyzer(
IDiagnosticDescriptorsProvider diagnosticDescriptorsProvider,
ISubstitutionNodeFinder<TInvocationExpressionSyntax> substitutionNodeFinder)
: base(diagnosticDescriptorsProvider)
{
_substitutionNodeFinder = substitutionNodeFinder;
SupportedDiagnostics = ImmutableArray.Create(DiagnosticDescriptorsProvider.AsyncThrows);

_analyzeInvocationAction = AnalyzeInvocation;
}

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }

protected override void InitializeAnalyzer(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(_analyzeInvocationAction, InvocationExpressionKind);
}

private void AnalyzeInvocation(SyntaxNodeAnalysisContext syntaxNodeContext)
{
var invocationExpression = syntaxNodeContext.Node;
var methodSymbolInfo = syntaxNodeContext.SemanticModel.GetSymbolInfo(invocationExpression);

if (methodSymbolInfo.Symbol?.Kind != SymbolKind.Method)
{
return;
}

var methodSymbol = (IMethodSymbol)methodSymbolInfo.Symbol;

if (!methodSymbol.IsThrowLikeMethod())
{
return;
}

var substitutedExpression = _substitutionNodeFinder.FindForStandardExpression(
(TInvocationExpressionSyntax)invocationExpression,
methodSymbol);

if (substitutedExpression == null)
{
return;
}

var semanticModel = syntaxNodeContext.SemanticModel.GetSymbolInfo(substitutedExpression);

if (!(semanticModel.Symbol is IMethodSymbol substituteMethodSymbol))
{
return;
}

var typeByMetadataName = syntaxNodeContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task");

if (!HasTaskReturnType(substituteMethodSymbol, typeByMetadataName))
{
return;
}

syntaxNodeContext.ReportDiagnostic(
Diagnostic.Create(DiagnosticDescriptorsProvider.AsyncThrows, invocationExpression.GetLocation()));
}

private static bool HasTaskReturnType(IMethodSymbol methodSymbol, ISymbol typeByMetadataName)
{
return methodSymbol.ReturnType.Equals(typeByMetadataName) ||
(methodSymbol.ReturnType.BaseType != null && methodSymbol.ReturnType.BaseType.Equals(typeByMetadataName));
}
}
}
7 changes: 7 additions & 0 deletions src/NSubstitute.Analyzers.Shared/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ internal class DiagnosticDescriptors<T>
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

public static DiagnosticDescriptor AsyncThrows { get; } = CreateDiagnosticDescriptor(
name: nameof(AsyncThrows),
id: DiagnosticIdentifiers.AsyncThrows,
category: DiagnosticCategory.Usage.GetDisplayName(),
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

private static DiagnosticDescriptor CreateDiagnosticDescriptor(
string name, string id, string category, DiagnosticSeverity defaultSeverity, bool isEnabledByDefault)
{
Expand Down
1 change: 1 addition & 0 deletions src/NSubstitute.Analyzers.Shared/DiagnosticIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ internal class DiagnosticIdentifiers
public const string UnusedReceived = "NS5000";
public const string ReceivedUsedInReceivedInOrder = "NS5001";
public const string AsyncCallbackUsedInReceivedInOrder = "NS5002";
public const string AsyncThrows = "NS5003";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,7 @@ internal interface IDiagnosticDescriptorsProvider
DiagnosticDescriptor ReceivedUsedInReceivedInOrder { get; }

DiagnosticDescriptor AsyncCallbackUsedInReceivedInOrder { get; }

DiagnosticDescriptor AsyncThrows { get; }
}
}
18 changes: 18 additions & 0 deletions src/NSubstitute.Analyzers.Shared/Resources.Designer.cs

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

9 changes: 9 additions & 0 deletions src/NSubstitute.Analyzers.Shared/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -409,4 +409,13 @@
<value>Non-virtual setup specification.</value>
<comment>The title of the diagnostic.</comment>
</data>
<data name="AsyncThrowsDescription" xml:space="preserve">
<value>Sync throws used.</value>
</data>
<data name="AsyncThrowsMessageFormat" xml:space="preserve">
<value>Sync throws used.</value>
</data>
<data name="AsyncThrowsTitle" xml:space="preserve">
<value>Sync throws used.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
using NSubstitute.Analyzers.Shared.DiagnosticAnalyzers;

namespace NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers
{
[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
internal class AsyncThrowsAnalyzer : AbstractAsyncThrowsAnalyzer<SyntaxKind, InvocationExpressionSyntax>
{
public AsyncThrowsAnalyzer()
: base(VisualBasic.DiagnosticDescriptorsProvider.Instance, SubstitutionNodeFinder.Instance)
{
}

protected override SyntaxKind InvocationExpressionKind => SyntaxKind.InvocationExpression;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.CSharp;
using NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers;
using NSubstitute.Analyzers.Shared;
using NSubstitute.Analyzers.Tests.Shared.DiagnosticAnalyzers;
using Xunit;

namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.AsyncThrowsAnalyzerTests
{
public abstract class AsyncThrowsDiagnosticVerifier : CSharpDiagnosticVerifier, IAsyncThrowsDiagnosticVerifier
{
protected DiagnosticDescriptor AsyncThrowsDescriptor => DiagnosticDescriptors<DiagnosticDescriptorsProvider>.AsyncThrows;

protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; } = new AsyncThrowsAnalyzer();

[Theory]
[InlineData("Throws")]
[InlineData("ThrowsForAnyArgs")]
public abstract Task ReportsDiagnostic_WhenUsedWithNonGenericAsyncMethod(string method);

[Theory]
[InlineData("Throws")]
[InlineData("ThrowsForAnyArgs")]
public abstract Task ReportsDiagnostic_WhenUsedWithGenericAsyncMethod(string method);

[Theory]
[InlineData("Throws")]
[InlineData("ThrowsForAnyArgs")]
public abstract Task ReportsNoDiagnostic_WhenUsedWithSyncMethod(string method);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Threading.Tasks;

namespace NSubstitute.Analyzers.Tests.CSharp.DiagnosticAnalyzerTests.AsyncThrowsAnalyzerTests
{
public class ThrowsAsExtensionMethodTests : AsyncThrowsDiagnosticVerifier
{
public override async Task ReportsDiagnostic_WhenUsedWithNonGenericAsyncMethod(string method)
{
var source = $@"using System;
using System.Threading.Tasks;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
namespace MyNamespace
{{
public interface IFoo
{{
Task Bar();
}}
public class FooTests
{{
public void Test()
{{
var substitute = NSubstitute.Substitute.For<IFoo>();
[|substitute.Bar().{method}(new Exception())|];
}}
}}
}}";

await VerifyDiagnostic(source, AsyncThrowsDescriptor);
}

public override async Task ReportsDiagnostic_WhenUsedWithGenericAsyncMethod(string method)
{
var source = $@"using System;
using System.Threading.Tasks;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
namespace MyNamespace
{{
public interface IFoo
{{
Task<object> Bar();
}}
public class FooTests
{{
public void Test()
{{
var substitute = NSubstitute.Substitute.For<IFoo>();
[|substitute.Bar().{method}(new Exception())|];
}}
}}
}}";

await VerifyDiagnostic(source, AsyncThrowsDescriptor);
}

public override async Task ReportsNoDiagnostic_WhenUsedWithSyncMethod(string method)
{
var source = $@"using System;
using System.Threading.Tasks;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
namespace MyNamespace
{{
public interface IFoo
{{
object Bar();
}}
public class FooTests
{{
public void Test()
{{
var substitute = NSubstitute.Substitute.For<IFoo>();
substitute.Bar().{method}(new Exception());
}}
}}
}}";

await VerifyNoDiagnostic(source);
}
}
}
Loading

0 comments on commit 892034e

Please sign in to comment.