-
Notifications
You must be signed in to change notification settings - Fork 257
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
Changes from 4 commits
36e487d
e1ff60d
7ed9da1
c0f8098
4a95fa0
180fb4b
56d48d9
8d4de6e
497cb50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
=> 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() }; | ||
} | ||
} | ||
} |
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); | ||
} | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is severity required here? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
: 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, | ||
}; | ||
} | ||
} | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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>(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Should be fixed now 👍