roslyn-sdk: Special test scenario with new unit test framework fails
I’ve got a scenario where my test case has an error and a warning - i provide a codefix or the warning. After the warning is fixed, the error is fixed automatically too.
If i expect both diagnostics get this:
If i expect just one diagnostic i get this:
The old Inherited CodeFixTest version makes the test pass just fine.
this is my test method:
[TestMethod]
public async Task MixedMethodsWithPartialFix()
{
var test = @"namespace ConsoleApplication1
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.IO;
public class TestClass
{
private string _workspaceFile;
public async Task<Configuration[]> LoadStorageContentAsync()
{
var fileInfo = new FileInfo(_workspaceFile);
if (!fileInfo.Exists)
{
Log.Error($""No configuration storage located at { fileInfo.FullName}."");
return Array.Empty<Configuration>();
}
using (var stream = new StreamReader(new FileStream(fileInfo.FullName, FileMode.Open)))
{
try
{
var storage = new Storage();
return Task.FromResult(storage.Configurations.ToArray());
}
catch (Exception e)
{
Log.Error(e);
return Array.Empty<Configuration>();
}
}
}
public class Log
{
public static void Error(Exception p0)
{
throw new NotImplementedException();
}
public static void Error(string p0)
{
throw new NotImplementedException();
}
}
public class Storage
{
public Configuration[] Configurations { get; set; }
}
public class Configuration
{
public Guid Id { get; set; }
public string ConfigurationName { get; set; }
}
}
}";
var fixtest = @"namespace ConsoleApplication1
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.IO;
public class TestClass
{
private string _workspaceFile;
public Task<Configuration[]> LoadStorageContentAsync()
{
var fileInfo = new FileInfo(_workspaceFile);
if (!fileInfo.Exists)
{
Log.Error($""No configuration storage located at { fileInfo.FullName}."");
return Task.FromResult(Array.Empty<Configuration>());
}
using (var stream = new StreamReader(new FileStream(fileInfo.FullName, FileMode.Open)))
{
try
{
var storage = new Storage();
return Task.FromResult(storage.Configurations.ToArray());
}
catch (Exception e)
{
Log.Error(e);
return Task.FromResult(Array.Empty<Configuration>());
}
}
}
public class Log
{
public static void Error(Exception p0)
{
throw new NotImplementedException();
}
public static void Error(string p0)
{
throw new NotImplementedException();
}
}
public class Storage
{
public Configuration[] Configurations { get; set; }
}
public class Configuration
{
public Guid Id { get; set; }
public string ConfigurationName { get; set; }
}
}
}";
// await new CodeFixTest<EmptyDiagnosticAnalyzer,
// Amusoft.CodeAnalysis.Analyzers.CS1998.FixByWrappingInTaskResult>()
// {
// CompilerDiagnostics = CompilerDiagnostics.Errors | CompilerDiagnostics.Warnings,
// TestState =
// {
// Sources = { test },
// ExpectedDiagnostics =
// {
// CompilerWarning("CS1591").WithSpan(11, 15, 11, 24),
// CompilerWarning("CS0649").WithSpan(13, 18, 13, 32),
// CompilerWarning("CS1591").WithSpan(15, 38, 15, 61),
// CompilerWarning("CS1998").WithLocation(15,38),
// CompilerError("CS4016").WithSpan(29, 13, 29, 62).WithArguments("ConsoleApplication1.TestClass.Configuration[]"),
//
// CompilerWarning("CS1591").WithSpan(39, 16, 39, 19),
// CompilerWarning("CS1591").WithSpan(41, 23, 41, 28),
// CompilerWarning("CS1591").WithSpan(46, 23, 46, 28),
// CompilerWarning("CS1591").WithSpan(52, 16, 52, 23),
// CompilerWarning("CS1591").WithSpan(54, 27, 54, 41),
// CompilerWarning("CS1591").WithSpan(56, 16, 56, 29),
// CompilerWarning("CS1591").WithSpan(58, 16, 58, 18),
// CompilerWarning("CS1591").WithSpan(59, 18, 59, 35),
// },
// },
// FixedState =
// {
// ExpectedDiagnostics =
// {
// CompilerWarning("CS1591").WithSpan(11, 15, 11, 24),
// CompilerWarning("CS0649").WithSpan(13, 18, 13, 32),
// CompilerWarning("CS1591").WithSpan(15, 32, 15, 55),
// CompilerWarning("CS1591").WithSpan(39, 16, 39, 19),
// CompilerWarning("CS1591").WithSpan(41, 23, 41, 28),
// CompilerWarning("CS1591").WithSpan(46, 23, 46, 28),
// CompilerWarning("CS1591").WithSpan(52, 16, 52, 23),
// CompilerWarning("CS1591").WithSpan(54, 27, 54, 41),
// CompilerWarning("CS1591").WithSpan(56, 16, 56, 29),
// CompilerWarning("CS1591").WithSpan(58, 16, 58, 18),
// CompilerWarning("CS1591").WithSpan(59, 18, 59, 35),
// },
// Sources = { fixtest }
// },
// }.RunAsync();
var expected = new[]
{
CompilerWarning("CS1998").WithLocation(15, 38),
// CompilerError("CS4016").WithSpan(29, 13, 29, 62)
// .WithArguments("ConsoleApplication1.TestClass.Configuration[]"),
};
await Verifier.VerifyCodeFixAsync(test, expected, fixtest);
}
This is my CodeFix:
[ExportCodeFixProvider(LanguageNames.CSharp, Name = "CS1998-FixByWrappingInTaskResult"), Shared]
public class FixByWrappingInTaskResult : CodeFixProviderBase
{
/// <inheritdoc />
protected override string DiagnosticId { get; } = "CS1998";
/// <inheritdoc />
protected override string GetEquivalenceKey(SyntaxNode rootNode)
{
return "CS1998-FixByWrappingInTaskResult";
}
/// <inheritdoc />
protected override string GetTitle(SyntaxNode rootNode)
{
return Resources.MessageFormat_CS1998_FixByWrappingInTaskResult;
}
/// <inheritdoc />
protected override async Task<SyntaxNode> FixedDiagnosticAsync(SyntaxNode rootNode, SyntaxNode diagnosticNode,
CodeFixContext context,
CancellationToken cancellationToken)
{
var semanticModel = await context.Document.GetSemanticModelAsync(cancellationToken)
.ConfigureAwait(false);
if (diagnosticNode is MethodDeclarationSyntax methodDeclarationSyntax)
{
var controlFlowAnalysis = semanticModel.AnalyzeControlFlow(methodDeclarationSyntax.Body);
if (controlFlowAnalysis.ReturnStatements.Length > 0)
{
var documentEditor = await DocumentEditor.CreateAsync(context.Document, cancellationToken)
.ConfigureAwait(false);
RemoveAsyncFromMethod(documentEditor, methodDeclarationSyntax, semanticModel);
foreach (var returnStatement in controlFlowAnalysis.ReturnStatements)
{
if (returnStatement is ReturnStatementSyntax returnStatementSyntax)
{
if (!ShouldRewrite(returnStatementSyntax))
continue;
documentEditor.ReplaceNode(returnStatementSyntax, RewriteExit(returnStatementSyntax));
}
}
return await documentEditor.GetChangedDocument().GetSyntaxRootAsync(cancellationToken);
}
}
return rootNode;
}
private static void RemoveAsyncFromMethod(DocumentEditor documentEditor, MethodDeclarationSyntax methodDeclarationSyntax, SemanticModel semanticModel)
{
documentEditor.SetModifiers(methodDeclarationSyntax, DeclarationModifiers.From(semanticModel.GetDeclaredSymbol(methodDeclarationSyntax)).WithAsync(false));
}
private bool ShouldRewrite(ReturnStatementSyntax returnStatementSyntax)
{
if (returnStatementSyntax.Expression is InvocationExpressionSyntax invocationExpressionSyntax)
{
if (invocationExpressionSyntax.Expression is MemberAccessExpressionSyntax memberAccessExpressionSyntax)
{
if (memberAccessExpressionSyntax.Name.Identifier.Text.Equals("FromResult")
&& memberAccessExpressionSyntax.Expression is IdentifierNameSyntax expressionIdentifierName
&& expressionIdentifierName.Identifier.Text.Equals("Task"))
{
return false;
}
}
}
return true;
}
private SyntaxNode RewriteExit(ReturnStatementSyntax returnStatement)
{
return returnStatement.WithExpression(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("Task"),
IdentifierName("FromResult")))
.WithArgumentList(
ArgumentList(
SingletonSeparatedList<ArgumentSyntax>(
Argument(
returnStatement.Expression))))
);
}
}
/// <summary>
/// <inheritdoc />
/// </summary>
public abstract class CodeFixProviderBase : CodeFixProvider
{
public sealed override FixAllProvider GetFixAllProvider()
{
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
return WellKnownFixAllProviders.BatchFixer;
}
/// <inheritdoc />
public override ImmutableArray<string> FixableDiagnosticIds => new[] { DiagnosticId }.ToImmutableArray();
protected abstract string DiagnosticId { get; }
protected const string TitleAnnotation = "META:CodeFixTitleAnnotation";
protected const string MemberAnnotation = "META:MemberAnnotation";
protected const string TypeAnnotation = "META:TypeAnnotation";
protected abstract string GetEquivalenceKey(SyntaxNode rootNode);
protected abstract string GetTitle(SyntaxNode rootNode);
protected virtual async Task<Document> GetFixedDocumentAsync(CodeFixContext context,
CancellationToken cancellationToken, Diagnostic diagnostic)
{
var rootNode = await context.Document.GetSyntaxRootAsync(cancellationToken)
.ConfigureAwait(false);
var fixedRoot = await FixedDiagnosticAsync(rootNode, rootNode.FindNode(diagnostic.Location.SourceSpan), context, cancellationToken)
.ConfigureAwait(false);
return context.Document
.WithSyntaxRoot(fixedRoot);
}
protected abstract Task<SyntaxNode> FixedDiagnosticAsync(SyntaxNode rootNode, SyntaxNode diagnosticNode,
CodeFixContext context, CancellationToken cancellationToken);
/// <inheritdoc />
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var originalRoot = await context.Document.GetSyntaxRootAsync(context.CancellationToken)
.ConfigureAwait(false);
foreach (var diagnostic in context.Diagnostics)
{
if (!diagnostic.Id.Equals(DiagnosticId, StringComparison.OrdinalIgnoreCase))
continue;
var fixedDocument = await GetFixedDocumentAsync(context, context.CancellationToken, diagnostic)
.ConfigureAwait(false);
var fixedRoot = await fixedDocument.GetSyntaxRootAsync(context.CancellationToken)
.ConfigureAwait(false);
if(fixedRoot.IsEquivalentTo(originalRoot))
continue;
context.RegisterCodeFix(CodeAction.Create(GetTitle(fixedRoot), c => Task.FromResult(fixedDocument), GetEquivalenceKey(fixedRoot)), diagnostic);
}
}
protected string GetAnnotationValue(SyntaxNode rootNode, string annotation, string defaultValue = "unknown annotation")
{
return rootNode.GetAnnotations(annotation).FirstOrDefault(d => d.Kind == annotation)?.Data ?? defaultValue;
}
}
@sharwell Willing to contribute again if this is an option. Unless you want to take care of it yourself of course 😃
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Comments: 16 (16 by maintainers)
I’m not a big fan of this right now, for two reasons:
I don’t see this as a particular advantage. I find the inherited types quite convenient for tailoring the test framework to the majority characteristics of an analyzer library.