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

Add diagnostic and code fix verification helpers #131

Closed
wants to merge 9 commits into from
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<OutputPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\..\..\bin\CSharp\ConvertToConditional.Test'))</OutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.6.0" />
<PackageReference Include="Microsoft.CodeAnalysis" Version="2.6.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ protected Document CreateDocument(string code)
// find these assemblies in the running process
string[] simpleNames = { "mscorlib", "System.Core", "System" };

#if !NETSTANDARD1_5
IEnumerable<PortableExecutableReference> references = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => simpleNames.Contains(a.GetName().Name, StringComparer.OrdinalIgnoreCase))
.Select(a => MetadataReference.CreateFromFile(a.Location));
#else
IEnumerable<PortableExecutableReference> references = Enumerable.Empty<PortableExecutableReference>();
#endif

return new AdhocWorkspace().CurrentSolution
.AddProject(projectId, "TestProject", "TestProject", LanguageName)
Expand Down
61 changes: 61 additions & 0 deletions samples/Shared/UnitTestFramework/CodeFixVerifier`2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Roslyn.UnitTestFramework
{
public static class CodeFixVerifier<TAnalyzer, TCodeFix>
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new()
{
public static DiagnosticResult[] EmptyDiagnosticResults
=> DiagnosticVerifier<TAnalyzer>.EmptyDiagnosticResults;

public static DiagnosticResult Diagnostic(string diagnosticId = null)
=> DiagnosticVerifier<TAnalyzer>.Diagnostic(diagnosticId);

public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
=> DiagnosticVerifier<TAnalyzer>.Diagnostic(descriptor);

public static DiagnosticResult CompilerError(string errorIdentifier)
=> DiagnosticVerifier<TAnalyzer>.CompilerError(errorIdentifier);

public static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expected, CancellationToken cancellationToken)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the cancellation token or make it optional? Only keep it if there is a use case for testing cancellation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➡️ Should be fixed now 👍

=> DiagnosticVerifier<TAnalyzer>.VerifyCSharpDiagnosticAsync(source, expected, cancellationToken);

public static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] expected, CancellationToken cancellationToken)
=> DiagnosticVerifier<TAnalyzer>.VerifyCSharpDiagnosticAsync(source, expected, cancellationToken);

public static Task VerifyCSharpFixAsync(string source, DiagnosticResult expected, string fixedSource, CancellationToken cancellationToken)
=> VerifyCSharpFixAsync(source, new[] { expected }, fixedSource, cancellationToken);

public static Task VerifyCSharpFixAsync(string source, DiagnosticResult[] expected, string fixedSource, CancellationToken cancellationToken)
{
CSharpTest test = new CSharpTest
{
TestCode = source,
FixedCode = fixedSource,
};

test.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync(cancellationToken);
}

public class CSharpTest : DiagnosticVerifier<TAnalyzer>.CSharpTest
{
protected override IEnumerable<CodeFixProvider> GetCodeFixProviders()
=> new[] { new TCodeFix() };
}

public class VisualBasicTest : DiagnosticVerifier<TAnalyzer>.VisualBasicTest
{
protected override IEnumerable<CodeFixProvider> GetCodeFixProviders()
=> new[] { new TCodeFix() };
}
}
}
23 changes: 23 additions & 0 deletions samples/Shared/UnitTestFramework/CustomDiagnosticVerifier`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Roslyn.UnitTestFramework
{
public static class CustomDiagnosticVerifier<TAnalyzer>
where TAnalyzer : DiagnosticAnalyzer, new()
{
public static DiagnosticResult[] EmptyDiagnosticResults
=> DiagnosticVerifier<TAnalyzer>.EmptyDiagnosticResults;

public static DiagnosticResult Diagnostic(string diagnosticId = null)
=> DiagnosticVerifier<TAnalyzer>.Diagnostic(diagnosticId);

public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
=> DiagnosticVerifier<TAnalyzer>.Diagnostic(descriptor);

public static DiagnosticResult CompilerError(string errorIdentifier)
=> DiagnosticVerifier<TAnalyzer>.CompilerError(errorIdentifier);
}
}
176 changes: 176 additions & 0 deletions samples/Shared/UnitTestFramework/DiagnosticResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

namespace Roslyn.UnitTestFramework
{
/// <summary>
/// Structure that stores information about a <see cref="Diagnostic"/> appearing in a source.
/// </summary>
public struct DiagnosticResult
{
private const string DefaultPath = "Test0.cs";

private static readonly object[] EmptyArguments = new object[0];

private ImmutableArray<FileLinePositionSpan> _spans;
private string _message;

public DiagnosticResult(string id, DiagnosticSeverity severity)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is severity required here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor is rarely required. Severity does not need to be specified in any of the three primary locations where this is constructed:

  1. By calling Diagnostic()
  2. By calling CompilerError("CS0000")
  3. Starting with commit 56d48d9, by using markup syntax in the input and/or output

: this()
{
Id = id;
Severity = severity;
}

public DiagnosticResult(DiagnosticDescriptor descriptor)
: this()
{
Id = descriptor.Id;
Severity = descriptor.DefaultSeverity;
MessageFormat = descriptor.MessageFormat;
}

public ImmutableArray<FileLinePositionSpan> Spans
{
get
{
return _spans.IsDefault ? ImmutableArray<FileLinePositionSpan>.Empty : _spans;
}
}

public DiagnosticSeverity Severity
{
get;
private set;
}

public string Id
{
get;
private set;
}

public string Message
{
get
{
if (_message != null)
{
return _message;
}

if (MessageFormat != null)
{
return string.Format(MessageFormat.ToString(), MessageArguments ?? EmptyArguments);
}

return null;
}
}

public LocalizableString MessageFormat
{
get;
private set;
}

public object[] MessageArguments
{
get;
private set;
}

public bool HasLocation
{
get
{
return (_spans != null) && (_spans.Length > 0);
}
}

public DiagnosticResult WithSeverity(DiagnosticSeverity severity)
{
DiagnosticResult result = this;
result.Severity = severity;
return result;
}

public DiagnosticResult WithArguments(params object[] arguments)
{
DiagnosticResult result = this;
result.MessageArguments = arguments;
return result;
}

public DiagnosticResult WithMessage(string message)
{
DiagnosticResult result = this;
result._message = message;
return result;
}

public DiagnosticResult WithMessageFormat(LocalizableString messageFormat)
{
DiagnosticResult result = this;
result.MessageFormat = messageFormat;
return result;
}

public DiagnosticResult WithLocation(int line, int column)
{
return WithLocation(DefaultPath, line, column);
}

public DiagnosticResult WithLocation(string path, int line, int column)
{
LinePosition linePosition = new LinePosition(line, column);

return AppendSpan(new FileLinePositionSpan(path, linePosition, linePosition));
}

public DiagnosticResult WithSpan(int startLine, int startColumn, int endLine, int endColumn)
{
return WithSpan(DefaultPath, startLine, startColumn, endLine, endColumn);
}

public DiagnosticResult WithSpan(string path, int startLine, int startColumn, int endLine, int endColumn)
{
return AppendSpan(new FileLinePositionSpan(path, new LinePosition(startLine, startColumn), new LinePosition(endLine, endColumn)));
}

public DiagnosticResult WithLineOffset(int offset)
{
DiagnosticResult result = this;
ImmutableArray<FileLinePositionSpan>.Builder spansBuilder = result._spans.ToBuilder();
for (int i = 0; i < result._spans.Length; i++)
{
LinePosition newStartLinePosition = new LinePosition(result._spans[i].StartLinePosition.Line + offset, result._spans[i].StartLinePosition.Character);
LinePosition newEndLinePosition = new LinePosition(result._spans[i].EndLinePosition.Line + offset, result._spans[i].EndLinePosition.Character);

spansBuilder[i] = new FileLinePositionSpan(result._spans[i].Path, newStartLinePosition, newEndLinePosition);
}

result._spans = spansBuilder.MoveToImmutable();
return result;
}

private DiagnosticResult AppendSpan(FileLinePositionSpan span)
{
ImmutableArray<FileLinePositionSpan> newSpans = _spans.Add(span);

// clone the object, so that the fluent syntax will work on immutable objects.
return new DiagnosticResult
{
Id = Id,
_message = _message,
MessageFormat = MessageFormat,
MessageArguments = MessageArguments,
Severity = Severity,
_spans = newSpans,
};
}
}
}
79 changes: 79 additions & 0 deletions samples/Shared/UnitTestFramework/DiagnosticVerifier`1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Roslyn.UnitTestFramework
{
public static class DiagnosticVerifier<TAnalyzer>
where TAnalyzer : DiagnosticAnalyzer, new()
{
public static DiagnosticResult[] EmptyDiagnosticResults { get; } = { };

public static DiagnosticResult Diagnostic(string diagnosticId = null)
{
TAnalyzer analyzer = new TAnalyzer();
ImmutableArray<DiagnosticDescriptor> supportedDiagnostics = analyzer.SupportedDiagnostics;
if (diagnosticId is null)
{
return Diagnostic(supportedDiagnostics.Single());
}
else
{
return Diagnostic(supportedDiagnostics.Single(i => i.Id == diagnosticId));
}
}

public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
{
return new DiagnosticResult(descriptor);
}

public static DiagnosticResult CompilerError(string errorIdentifier)
{
return new DiagnosticResult(errorIdentifier, DiagnosticSeverity.Error);
}

public static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult expected, CancellationToken cancellationToken)
=> VerifyCSharpDiagnosticAsync(source, new[] { expected }, cancellationToken);

public static Task VerifyCSharpDiagnosticAsync(string source, DiagnosticResult[] expected, CancellationToken cancellationToken)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be IEnumerable<DiagnosticResult. Also cancellation token needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be IEnumerable<DiagnosticResult>

I'll file a follow-up issue to consider this.

{
CSharpTest test = new CSharpTest
{
TestCode = source,
};

test.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync(cancellationToken);
}

public class CSharpTest : GenericAnalyzerTest
{
public override string Language => LanguageNames.CSharp;

protected override IEnumerable<DiagnosticAnalyzer> GetDiagnosticAnalyzers()
=> new[] { new TAnalyzer() };

protected override IEnumerable<CodeFixProvider> GetCodeFixProviders()
=> Enumerable.Empty<CodeFixProvider>();
}

public class VisualBasicTest : GenericAnalyzerTest
{
public override string Language => LanguageNames.VisualBasic;

protected override IEnumerable<DiagnosticAnalyzer> GetDiagnosticAnalyzers()
=> new[] { new TAnalyzer() };

protected override IEnumerable<CodeFixProvider> GetCodeFixProviders()
=> Enumerable.Empty<CodeFixProvider>();
}
}
}
Loading