Skip to content

Commit

Permalink
Ensure ForwardCancellationTokenAnalyzer treats usages of default expr…
Browse files Browse the repository at this point in the history
…essions properly (release-8.0) (#6617)

* Ensure ForwardCancellationTokenAnalyzer treats usages of default expressions properly

* Use nameof(CancellationToken)

* namespace
  • Loading branch information
DavidBoike committed Dec 2, 2022
1 parent c7efa69 commit 9a0fd52
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,79 @@ public class Bar {}");
public class ForwardCancellationTokenTestsCSharp8 : ForwardCancellationTokenTests
{
protected override LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp8;

[TestCase("default(CancellationToken)")]
[TestCase("default")]
[TestCase("CancellationToken.None")]
[TestCase("new CancellationToken(true)")]
[TestCase("context.CancellationToken")]
public Task DefaultTokenParameters(string parameterValue) => Assert(ForwardCancellationTokenAnalyzer.DiagnosticId, @"
using System;
using System.Threading;
using System.Threading.Tasks;
using NServiceBus;
public class Foo : IHandleMessages<TestMessage>
{
public async Task Handle(TestMessage message, IMessageHandlerContext context)
{
await Test(""hi"", 1, DateTime.Now, " + parameterValue + @");
await Test(default(string), default(int), default(DateTime), " + parameterValue + @");
await Test(default, default, default, " + parameterValue + @");
await [|Test(""hi"", 1, DateTime.Now)|];
await [|Test(default(string), default(int), default(DateTime))|];
await [|Test(default, default, default)|];
}
Task Test(string s, int i, DateTime d, CancellationToken token = default)
{
System.Console.WriteLine($""{{s}} {{i}} {{d}}"");
return Task.Delay(i, token);
}
}
public class TestMessage : ICommand {}
");

#if NET // IAsyncEnumerable requires package Microsoft.Bcl.AsyncInterfaces on .NET Framework

[TestCase("AsyncEnumerator(context.CancellationToken)")]
[TestCase("AsyncEnumerator(CancellationToken.None)")]
[TestCase("AsyncEnumerator(default(CancellationToken))")]
[TestCase("AsyncEnumerator(default)")]
[TestCase("[|AsyncEnumerator()|]")]
[TestCase("[|AsyncEnumerator()|].WithCancellation(context.CancellationToken)")]
[TestCase("[|AsyncEnumerator()|].WithCancellation(CancellationToken.None)")]
[TestCase("[|AsyncEnumerator()|].WithCancellation(default(CancellationToken))")]
[TestCase("[|AsyncEnumerator()|].WithCancellation(default)")]
public Task AwaitForeach(string asyncCall) => Assert(ForwardCancellationTokenAnalyzer.DiagnosticId, @"
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using NServiceBus;
using System.Threading;
using System.Threading.Tasks;
public class Foo : IHandleMessages<TestMessage>
{
public async Task Handle(TestMessage message, IMessageHandlerContext context)
{
await foreach (int item in " + asyncCall + @")
{
Console.WriteLine(item);
}
}
static async IAsyncEnumerable<int> AsyncEnumerator([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(0, cancellationToken);
yield return i;
}
}
}
public class TestMessage : ICommand {}
");
#endif
}

public class ForwardCancellationTokenTestsCSharp9 : ForwardCancellationTokenTestsCSharp8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ static AnalyzerTestFixture()
MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).GetTypeInfo().Assembly.Location),
#if NET
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Console").Location),
MetadataReference.CreateFromFile(Assembly.Load("System.Private.CoreLib").Location),
#endif
MetadataReference.CreateFromFile(typeof(EndpointConfiguration).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(IUniformSession).GetTypeInfo().Assembly.Location));
Expand Down
43 changes: 35 additions & 8 deletions src/NServiceBus.Core.Analyzer/ForwardCancellationTokenAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -165,32 +166,58 @@ static bool LooksLikeCancellationToken(ExpressionSyntax expression, string calle
var memberName = memberAccess.Name.Identifier.ValueText;

// Is context.CancellationToken
if (refName == callerCancellableContextParam && memberName == "CancellationToken")
if (refName == callerCancellableContextParam && memberName == nameof(CancellationToken))
{
return true;
}

if (refName == "CancellationToken" && memberName == "None")
if (refName == nameof(CancellationToken) && memberName == "None")
{
return true;
}
}
}

if (expression is ObjectCreationExpressionSyntax objectCreation &&
objectCreation.Type is IdentifierNameSyntax objectCreationTypeName &&
objectCreationTypeName.Identifier.ValueText == "CancellationToken")
// new CancellationToken(...)
if (expression is ObjectCreationExpressionSyntax objectCreation && TypeSyntaxLooksLikeCancellationToken(objectCreation.Type))
{
return true;
}

// default(CancellationToken)
if (expression is DefaultExpressionSyntax defaultSyntax && TypeSyntaxLooksLikeCancellationToken(defaultSyntax.Type))
{
return true;
}

// Note: `default` on its own is a LiteralExpressionSyntax, not a DefaultExpressionSyntax, so when this is used
// as a parameter we can't tell whether it's a CancellationToken at the lexical level and must allow IsCancellationToken
// to make the determination by consulting the semantic model
return false;
}

static bool TypeSyntaxLooksLikeCancellationToken(TypeSyntax typeSyntax) =>
typeSyntax is IdentifierNameSyntax identifierSyntax && identifierSyntax.Identifier.ValueText == nameof(CancellationToken);

static bool IsCancellationToken(ExpressionSyntax expressionSyntax, SyntaxNodeAnalysisContext context, INamedTypeSymbol cancellationTokenType)
{
var expressionSymbol = context.SemanticModel.GetSymbolInfo(expressionSyntax, context.CancellationToken).Symbol;
return expressionSymbol.GetTypeSymbolOrDefault()?.Equals(cancellationTokenType, SymbolEqualityComparer.IncludeNullability) ?? false;
var typeSymbol = GetTypeSymbolFromExpression(expressionSyntax, context);
return typeSymbol?.Equals(cancellationTokenType, SymbolEqualityComparer.IncludeNullability) ?? false;
}

static ITypeSymbol GetTypeSymbolFromExpression(ExpressionSyntax expression, SyntaxNodeAnalysisContext context)
{
if (expression is LiteralExpressionSyntax) // `default` as a parameter
{
var typeInfo = context.SemanticModel.GetTypeInfo(expression, context.CancellationToken);
return typeInfo.Type;
}
else
{
var symbolInfo = context.SemanticModel.GetSymbolInfo(expression, context.CancellationToken);
var expressionSymbol = symbolInfo.Symbol;
return expressionSymbol.GetTypeSymbolOrDefault();
}
}

static IMethodSymbol GetRecommendedMethod(
Expand Down Expand Up @@ -247,7 +274,7 @@ static bool IsCancellationToken(ExpressionSyntax expressionSyntax, SyntaxNodeAna

// if adding a cancellation token to the args will not put it in the right place
static string GetRequiredArgName(IMethodSymbol recommendedMethod, IParameterSymbol extraParam, SeparatedSyntaxList<ArgumentSyntax> args) =>
!recommendedMethod.Parameters[args.Count].Equals(extraParam, SymbolEqualityComparer.IncludeNullability) ? extraParam.Name : null;
args.Count < recommendedMethod.Parameters.Length && !recommendedMethod.Parameters[args.Count].Equals(extraParam, SymbolEqualityComparer.IncludeNullability) ? extraParam.Name : null;

static INamedTypeSymbol GetCalledType(InvocationExpressionSyntax call, IMethodSymbol calledMethod, SyntaxNodeAnalysisContext context)
{
Expand Down

0 comments on commit 9a0fd52

Please sign in to comment.