Adding analyzer project

Contains two analyzers with fixes

* Use AnsiConsole over System.Console
* Favor local instance over static implementation
This commit is contained in:
Phil Scott 2021-06-20 19:47:19 -04:00 committed by Patrik Svensson
parent d015a4966f
commit 4f293d887d
20 changed files with 974 additions and 0 deletions

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Spectre.Console\Spectre.Console.csproj" />
<ProjectReference Include="..\..\..\src\Spectre.Console.Analyzer\Spectre.Console.Analyzer.csproj"
PrivateAssets="all"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,32 @@
using Spectre.Console;
namespace AnalyzerTester
{
class Program
{
static void Main(string[] args)
{
AnsiConsole.WriteLine("Hello World!");
}
}
class Dependency
{
private readonly IAnsiConsole _ansiConsole;
public Dependency(IAnsiConsole ansiConsole)
{
_ansiConsole = ansiConsole;
}
public void DoIt()
{
_ansiConsole.WriteLine("Hey mom!");
}
public void DoIt(IAnsiConsole thisConsole)
{
thisConsole.WriteLine("Hey mom!");
}
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.CodeAnalysis.Diagnostics;
namespace Spectre.Console.Analyzer
{
/// <summary>
/// Base class for Spectre analyzers.
/// </summary>
public abstract class BaseAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(AnalyzeCompilation);
}
/// <summary>
/// Analyze compilation.
/// </summary>
/// <param name="compilationStartContext">Compilation Start Analysis Context.</param>
protected abstract void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext);
}
}

View File

@ -0,0 +1,89 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Spectre.Console.Analyzer
{
/// <summary>
/// Analyzer to suggest using available instances of AnsiConsole over the static methods.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class FavorInstanceAnsiConsoleOverStaticAnalyzer : BaseAnalyzer
{
private static readonly DiagnosticDescriptor _diagnosticDescriptor =
Descriptors.S1010_FavorInstanceAnsiConsoleOverStatic;
/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(_diagnosticDescriptor);
/// <inheritdoc />
protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext)
{
compilationStartContext.RegisterOperationAction(
context =>
{
var ansiConsoleType = context.Compilation.GetTypeByMetadataName("Spectre.Console.AnsiConsole");
// if this operation isn't an invocation against one of the System.Console methods
// defined in _methods then we can safely stop analyzing and return;
var invocationOperation = (IInvocationOperation)context.Operation;
if (!Equals(invocationOperation.TargetMethod.ContainingType, ansiConsoleType))
{
return;
}
if (!HasFieldAnsiConsole(invocationOperation.Syntax) &&
!HasParameterAnsiConsole(invocationOperation.Syntax))
{
return;
}
var methodSymbol = invocationOperation.TargetMethod;
var displayString = SymbolDisplay.ToDisplayString(
methodSymbol,
SymbolDisplayFormat.CSharpShortErrorMessageFormat
.WithParameterOptions(SymbolDisplayParameterOptions.None)
.WithGenericsOptions(SymbolDisplayGenericsOptions.None));
context.ReportDiagnostic(
Diagnostic.Create(
_diagnosticDescriptor,
invocationOperation.Syntax.GetLocation(),
displayString));
}, OperationKind.Invocation);
}
private static bool HasParameterAnsiConsole(SyntaxNode syntaxNode)
{
return syntaxNode
.Ancestors().OfType<MethodDeclarationSyntax>()
.First()
.ParameterList.Parameters
.Any(i => i.Type.NormalizeWhitespace().ToString() == "IAnsiConsole");
}
private static bool HasFieldAnsiConsole(SyntaxNode syntaxNode)
{
var isStatic = syntaxNode
.Ancestors()
.OfType<MethodDeclarationSyntax>()
.First()
.Modifiers.Any(i => i.Kind() == SyntaxKind.StaticKeyword);
return syntaxNode
.Ancestors().OfType<ClassDeclarationSyntax>()
.First()
.Members
.OfType<FieldDeclarationSyntax>()
.Any(i =>
i.Declaration.Type.NormalizeWhitespace().ToString() == "IAnsiConsole" &&
(!isStatic ^ i.Modifiers.Any(modifier => modifier.Kind() == SyntaxKind.StaticKeyword)));
}
}
}

View File

@ -0,0 +1,63 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
namespace Spectre.Console.Analyzer
{
/// <summary>
/// Analyzer to enforce the use of AnsiConsole over System.Console for known methods.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UseSpectreInsteadOfSystemConsoleAnalyzer : BaseAnalyzer
{
private static readonly DiagnosticDescriptor _diagnosticDescriptor =
Descriptors.S1000_UseAnsiConsoleOverSystemConsole;
private static readonly string[] _methods = { "WriteLine", "Write" };
/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(_diagnosticDescriptor);
/// <inheritdoc />
protected override void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext)
{
compilationStartContext.RegisterOperationAction(
context =>
{
// if this operation isn't an invocation against one of the System.Console methods
// defined in _methods then we can safely stop analyzing and return;
var invocationOperation = (IInvocationOperation)context.Operation;
var systemConsoleType = context.Compilation.GetTypeByMetadataName("System.Console");
if (!Equals(invocationOperation.TargetMethod.ContainingType, systemConsoleType))
{
return;
}
var methodName = System.Array.Find(_methods, i => i.Equals(invocationOperation.TargetMethod.Name));
if (methodName == null)
{
return;
}
var methodSymbol = invocationOperation.TargetMethod;
var displayString = SymbolDisplay.ToDisplayString(
methodSymbol,
SymbolDisplayFormat.CSharpShortErrorMessageFormat
.WithParameterOptions(SymbolDisplayParameterOptions.None)
.WithGenericsOptions(SymbolDisplayGenericsOptions.None));
context.ReportDiagnostic(
Diagnostic.Create(
_diagnosticDescriptor,
invocationOperation.Syntax.GetLocation(),
displayString));
}, OperationKind.Invocation);
}
}
}

View File

@ -0,0 +1,8 @@
namespace Spectre.Console.Analyzer
{
internal static class Constants
{
internal const string StaticInstance = "AnsiConsole";
internal const string SpectreConsole = "Spectre.Console";
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Concurrent;
using Microsoft.CodeAnalysis;
using static Microsoft.CodeAnalysis.DiagnosticSeverity;
using static Spectre.Console.Analyzer.Descriptors.Category;
namespace Spectre.Console.Analyzer
{
/// <summary>
/// Code analysis descriptors.
/// </summary>
public static class Descriptors
{
internal enum Category
{
Usage, // 1xxx
}
private static readonly ConcurrentDictionary<Category, string> _categoryMapping = new();
private static DiagnosticDescriptor Rule(string id, string title, Category category, DiagnosticSeverity defaultSeverity, string messageFormat, string? description = null)
{
var helpLink = $"https://spectreconsole.net/spectre.console.analyzers/rules/{id}";
const bool IsEnabledByDefault = true;
return new DiagnosticDescriptor(id, title, messageFormat, _categoryMapping.GetOrAdd(category, c => c.ToString()), defaultSeverity, IsEnabledByDefault, description, helpLink);
}
/// <summary>
/// Gets definitions of diagnostics Spectre1000.
/// </summary>
public static DiagnosticDescriptor S1000_UseAnsiConsoleOverSystemConsole { get; } =
Rule(
"Spectre1000",
"Use AnsiConsole instead of System.Console",
Usage,
Warning,
"Use AnsiConsole instead of System.Console");
/// <summary>
/// Gets definitions of diagnostics Spectre1010.
/// </summary>
public static DiagnosticDescriptor S1010_FavorInstanceAnsiConsoleOverStatic { get; } =
Rule(
"Spectre1010",
"Favor the use of the instance of AnsiConsole over the static helper.",
Usage,
Info,
"Favor the use of the instance of AnsiConsole over the static helper.");
}
}

View File

@ -0,0 +1,115 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Spectre.Console.Analyzer.CodeActions
{
/// <summary>
/// Code action to change calls to System.Console to AnsiConsole.
/// </summary>
public class SwitchToAnsiConsoleAction : CodeAction
{
private readonly Document _document;
private readonly InvocationExpressionSyntax _originalInvocation;
/// <summary>
/// Initializes a new instance of the <see cref="SwitchToAnsiConsoleAction"/> class.
/// </summary>
/// <param name="document">Document to change.</param>
/// <param name="originalInvocation">The method to change.</param>
/// <param name="title">Title of the fix.</param>
public SwitchToAnsiConsoleAction(Document document, InvocationExpressionSyntax originalInvocation, string title)
{
_document = document;
_originalInvocation = originalInvocation;
Title = title;
}
/// <inheritdoc />
public override string Title { get; }
/// <inheritdoc />
public override string EquivalenceKey => Title;
/// <inheritdoc />
protected override async Task<Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
{
var originalCaller = ((MemberAccessExpressionSyntax)_originalInvocation.Expression).Name.ToString();
var syntaxTree = await _document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var root = (CompilationUnitSyntax)await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
// If there is an ansiConsole passed into the method then we'll use it.
// otherwise we'll check for a field level instance.
// if neither of those exist we'll fall back to the static param.
var ansiConsoleParameterDeclaration = GetAnsiConsoleParameterDeclaration();
var ansiConsoleFieldIdentifier = GetAnsiConsoleFieldDeclaration();
var ansiConsoleIdentifier = ansiConsoleParameterDeclaration ??
ansiConsoleFieldIdentifier ??
Constants.StaticInstance;
// Replace the System.Console call with a call to the identifier above.
var newRoot = root.ReplaceNode(
_originalInvocation,
GetImportedSpectreCall(originalCaller, ansiConsoleIdentifier));
// If we are calling the static instance and Spectre isn't imported yet we should do so.
if (ansiConsoleIdentifier == Constants.StaticInstance && root.Usings.ToList().All(i => i.Name.ToString() != Constants.SpectreConsole))
{
newRoot = newRoot.AddUsings(Syntax.SpectreUsing);
}
return _document.WithSyntaxRoot(newRoot);
}
private string? GetAnsiConsoleParameterDeclaration()
{
return _originalInvocation
.Ancestors().OfType<MethodDeclarationSyntax>()
.First()
.ParameterList.Parameters
.FirstOrDefault(i => i.Type.NormalizeWhitespace().ToString() == "IAnsiConsole")
?.Identifier.Text;
}
private string? GetAnsiConsoleFieldDeclaration()
{
// let's look to see if our call is in a static method.
// if so we'll only want to look for static IAnsiConsoles
// and vice-versa if we aren't.
var isStatic = _originalInvocation
.Ancestors()
.OfType<MethodDeclarationSyntax>()
.First()
.Modifiers.Any(i => i.Kind() == SyntaxKind.StaticKeyword);
return _originalInvocation
.Ancestors().OfType<ClassDeclarationSyntax>()
.First()
.Members
.OfType<FieldDeclarationSyntax>()
.FirstOrDefault(i =>
i.Declaration.Type.NormalizeWhitespace().ToString() == "IAnsiConsole" &&
(!isStatic ^ i.Modifiers.Any(modifier => modifier.Kind() == SyntaxKind.StaticKeyword)))
?.Declaration.Variables.First().Identifier.Text;
}
private ExpressionSyntax GetImportedSpectreCall(string originalCaller, string ansiConsoleIdentifier)
{
return ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(ansiConsoleIdentifier),
IdentifierName(originalCaller)))
.WithArgumentList(
_originalInvocation.ArgumentList))
.Expression;
}
}
}

View File

@ -0,0 +1,35 @@
using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Spectre.Console.Analyzer.CodeActions;
namespace Spectre.Console.Analyzer.FixProviders
{
/// <summary>
/// Fix provider to change System.Console calls to AnsiConsole calls.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public class StaticAnsiConsoleToInstanceFix : CodeFixProvider
{
/// <inheritdoc />
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
Descriptors.S1010_FavorInstanceAnsiConsoleOverStatic.Id);
/// <inheritdoc />
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
/// <inheritdoc />
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var methodDeclaration = root.FindNode(context.Span).FirstAncestorOrSelf<InvocationExpressionSyntax>();
context.RegisterCodeFix(
new SwitchToAnsiConsoleAction(context.Document, methodDeclaration, "Convert static AnsiConsole calls to local instance."),
context.Diagnostics);
}
}
}

View File

@ -0,0 +1,35 @@
using System.Collections.Immutable;
using System.Composition;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Spectre.Console.Analyzer.CodeActions;
namespace Spectre.Console.Analyzer.FixProviders
{
/// <summary>
/// Fix provider to change System.Console calls to AnsiConsole calls.
/// </summary>
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public class SystemConsoleToAnsiConsoleFix : CodeFixProvider
{
/// <inheritdoc />
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(
Descriptors.S1000_UseAnsiConsoleOverSystemConsole.Id);
/// <inheritdoc />
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
/// <inheritdoc />
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
var methodDeclaration = root.FindNode(context.Span).FirstAncestorOrSelf<InvocationExpressionSyntax>();
context.RegisterCodeFix(
new SwitchToAnsiConsoleAction(context.Document, methodDeclaration, "Convert static call to AnsiConsole to Spectre.Console.AnsiConsole"),
context.Diagnostics);
}
}
}

View File

@ -0,0 +1,10 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
namespace Spectre.Console.Analyzer
{
internal static class Syntax
{
public static readonly UsingDirectiveSyntax SpectreUsing = UsingDirective(QualifiedName(IdentifierName("Spectre"), IdentifierName("Console")));
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>true</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="2.9.8" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.6.1" />
</ItemGroup>
<ItemGroup>
<None Update="tools\*.ps1" CopyToOutputDirectory="Always" Pack="true" PackagePath="tools" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,39 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Spectre.Console.Analyzer;
using Xunit;
using AnalyzerVerify =
Spectre.Console.Tests.CodeAnalyzers.SpectreAnalyzerVerifier<
Spectre.Console.Analyzer.FavorInstanceAnsiConsoleOverStaticAnalyzer>;
namespace Spectre.Console.Tests.CodeAnalyzers.Analyzers
{
public class FavorInstanceAnsiConsoleOverStaticAnalyzerTests
{
private static readonly DiagnosticResult _expectedDiagnostics = new(
Descriptors.S1010_FavorInstanceAnsiConsoleOverStatic.Id,
DiagnosticSeverity.Info);
[Fact]
public async void Console_Write_Has_Warning()
{
const string Source = @"
using Spectre.Console;
class TestClass
{
IAnsiConsole _ansiConsole = AnsiConsole.Console;
void TestMethod()
{
_ansiConsole.Write(""this is fine"");
AnsiConsole.Write(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(11, 9))
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,53 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Spectre.Console.Analyzer;
using Xunit;
using AnalyzerVerify =
Spectre.Console.Tests.CodeAnalyzers.SpectreAnalyzerVerifier<
Spectre.Console.Analyzer.UseSpectreInsteadOfSystemConsoleAnalyzer>;
namespace Spectre.Console.Tests.CodeAnalyzers.Analyzers
{
public class UseSpectreInsteadOfSystemConsoleAnalyzerTests
{
private static readonly DiagnosticResult _expectedDiagnostics = new(
Descriptors.S1000_UseAnsiConsoleOverSystemConsole.Id,
DiagnosticSeverity.Warning);
[Fact]
public async void Console_Write_Has_Warning()
{
const string Source = @"
using System;
class TestClass {
void TestMethod()
{
Console.Write(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(7, 9))
.ConfigureAwait(false);
}
[Fact]
public async void Console_WriteLine_Has_Warning()
{
const string Source = @"
using System;
class TestClass
{
void TestMethod() {
Console.WriteLine(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyAnalyzerAsync(Source, _expectedDiagnostics.WithLocation(7, 9))
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.VisualStudio.Composition;
using Spectre.Console.Analyzer;
using Spectre.Console.Analyzer.FixProviders;
namespace Spectre.Console.Tests.CodeAnalyzers
{
internal static class CodeFixProviderDiscovery
{
private static readonly Lazy<IExportProviderFactory> _exportProviderFactory;
static CodeFixProviderDiscovery()
{
_exportProviderFactory = new Lazy<IExportProviderFactory>(
() =>
{
var discovery = new AttributedPartDiscovery(Resolver.DefaultInstance, isNonPublicSupported: true);
var parts = Task.Run(() => discovery.CreatePartsAsync(typeof(SystemConsoleToAnsiConsoleFix).Assembly)).GetAwaiter().GetResult();
var catalog = ComposableCatalog.Create(Resolver.DefaultInstance).AddParts(parts);
var configuration = CompositionConfiguration.Create(catalog);
var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration);
return runtimeComposition.CreateExportProviderFactory();
},
LazyThreadSafetyMode.ExecutionAndPublication);
}
public static IEnumerable<CodeFixProvider> GetCodeFixProviders(string language)
{
var exportProvider = _exportProviderFactory.Value.CreateExportProvider();
var exports = exportProvider.GetExports<CodeFixProvider, LanguageMetadata>();
return exports.Where(export => export.Metadata.Languages.Contains(language)).Select(export => export.Value);
}
private class LanguageMetadata
{
public LanguageMetadata(IDictionary<string, object> data)
{
if (!data.TryGetValue(nameof(ExportCodeFixProviderAttribute.Languages), out var languages))
{
languages = Array.Empty<string>();
}
Languages = ((string[])languages).ToImmutableArray();
}
public ImmutableArray<string> Languages { get; }
}
}
}

View File

@ -0,0 +1,54 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Spectre.Console.Analyzer;
using Xunit;
using AnalyzerVerify =
Spectre.Console.Tests.CodeAnalyzers.SpectreAnalyzerVerifier<
Spectre.Console.Analyzer.FavorInstanceAnsiConsoleOverStaticAnalyzer>;
namespace Spectre.Console.Tests.CodeAnalyzers.Fixes
{
public class UseInstanceOfStaticAnsiConsoleTests
{
private static readonly DiagnosticResult _expectedDiagnostic = new(
Descriptors.S1010_FavorInstanceAnsiConsoleOverStatic.Id,
DiagnosticSeverity.Info);
[Fact]
public async Task SystemConsole_replaced_with_AnsiConsole()
{
const string Source = @"
using Spectre.Console;
class TestClass
{
IAnsiConsole _ansiConsole = AnsiConsole.Console;
void TestMethod()
{
_ansiConsole.Write(""this is fine"");
AnsiConsole.Write(""Hello, World"");
}
}";
const string FixedSource = @"
using Spectre.Console;
class TestClass
{
IAnsiConsole _ansiConsole = AnsiConsole.Console;
void TestMethod()
{
_ansiConsole.Write(""this is fine"");
_ansiConsole.Write(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,152 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Spectre.Console.Analyzer;
using Xunit;
using AnalyzerVerify =
Spectre.Console.Tests.CodeAnalyzers.SpectreAnalyzerVerifier<
Spectre.Console.Analyzer.UseSpectreInsteadOfSystemConsoleAnalyzer>;
namespace Spectre.Console.Tests.CodeAnalyzers.Fixes
{
public class UseSpectreInsteadOfSystemConsoleFixTests
{
private static readonly DiagnosticResult _expectedDiagnostic = new(
Descriptors.S1000_UseAnsiConsoleOverSystemConsole.Id,
DiagnosticSeverity.Warning);
[Fact]
public async Task SystemConsole_replaced_with_AnsiConsole()
{
const string Source = @"
using System;
class TestClass
{
void TestMethod()
{
Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
void TestMethod()
{
AnsiConsole.WriteLine(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource)
.ConfigureAwait(false);
}
[Fact]
public async Task SystemConsole_replaced_with_imported_AnsiConsole()
{
const string Source = @"
using System;
class TestClass
{
void TestMethod()
{
Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
void TestMethod()
{
AnsiConsole.WriteLine(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(8, 9), FixedSource)
.ConfigureAwait(false);
}
[Fact]
public async Task SystemConsole_replaced_with_field_AnsiConsole()
{
const string Source = @"
using System;
using Spectre.Console;
class TestClass
{
IAnsiConsole _ansiConsole;
void TestMethod()
{
Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
IAnsiConsole _ansiConsole;
void TestMethod()
{
_ansiConsole.WriteLine(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
}
[Fact]
public async Task SystemConsole_replaced_with_static_field_AnsiConsole()
{
const string Source = @"
using System;
using Spectre.Console;
class TestClass
{
static IAnsiConsole _ansiConsole;
static void TestMethod()
{
Console.WriteLine(""Hello, World"");
}
}";
const string FixedSource = @"
using System;
using Spectre.Console;
class TestClass
{
static IAnsiConsole _ansiConsole;
static void TestMethod()
{
_ansiConsole.WriteLine(""Hello, World"");
}
}";
await AnalyzerVerify
.VerifyCodeFixAsync(Source, _expectedDiagnostic.WithLocation(11, 9), FixedSource)
.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
namespace Spectre.Console.Tests.CodeAnalyzers
{
public static class SpectreAnalyzerVerifier<TAnalyzer>
where TAnalyzer : DiagnosticAnalyzer, new()
{
public static Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
=> VerifyCodeFixAsync(source, new[] { expected }, fixedSource);
private static Task VerifyCodeFixAsync(string source, IEnumerable<DiagnosticResult> expected, string fixedSource)
{
// Roslyn fixers always use \r\n for newlines, regardless of OS environment settings, so we normalize
// the source as it typically comes from multi-line strings with varying newlines.
if (Environment.NewLine != "\r\n")
{
source = source.Replace(Environment.NewLine, "\r\n");
fixedSource = fixedSource.Replace(Environment.NewLine, "\r\n");
}
var test = new Test
{
TestCode = source,
FixedCode = fixedSource,
};
test.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
public static Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
{
var test = new Test
{
TestCode = source,
CompilerDiagnostics = CompilerDiagnostics.All,
ReferenceAssemblies = CodeAnalyzerHelper.CurrentSpectre,
TestBehaviors = TestBehaviors.SkipGeneratedCodeCheck,
};
test.ExpectedDiagnostics.AddRange(expected);
return test.RunAsync();
}
// Code fix tests support both analyzer and code fix testing. This test class is derived from the code fix test
// to avoid the need to maintain duplicate copies of the customization work.
private class Test : CSharpCodeFixTest<TAnalyzer, EmptyCodeFixProvider, XUnitVerifier>
{
public Test()
{
ReferenceAssemblies = CodeAnalyzerHelper.CurrentSpectre;
TestBehaviors |= TestBehaviors.SkipGeneratedCodeCheck;
}
protected override IEnumerable<CodeFixProvider> GetCodeFixProviders()
{
var analyzer = new TAnalyzer();
foreach (var provider in CodeFixProviderDiscovery.GetCodeFixProviders(Language))
{
if (analyzer.SupportedDiagnostics.Any(diagnostic => provider.FixableDiagnosticIds.Contains(diagnostic.Id)))
{
yield return provider;
}
}
}
}
}
internal static class CodeAnalyzerHelper
{
internal static ReferenceAssemblies CurrentSpectre { get; }
static CodeAnalyzerHelper()
{
CurrentSpectre = ReferenceAssemblies.Net.Net50.AddAssemblies(ImmutableArray.Create(typeof(AnsiConsole).Assembly.Location.Replace(".dll", string.Empty)));
}
}
}

View File

@ -17,6 +17,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" Version="1.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" Version="1.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.6.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="Spectre.Verify.Extensions" Version="0.3.0" />
@ -29,6 +32,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Spectre.Console.Analyzer\Spectre.Console.Analyzer.csproj" />
<ProjectReference Include="..\Spectre.Console.Testing\Spectre.Console.Testing.csproj" />
<ProjectReference Include="..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>

View File

@ -90,6 +90,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "..\examples\Share
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Live", "..\examples\Console\Live\Live.csproj", "{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnalyzerTester", "..\examples\Console\AnalyzerTester\AnalyzerTester.csproj", "{D2B32355-D99F-480B-92BF-9FAABE79ADD4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectre.Console.Analyzer", "Spectre.Console.Analyzer\Spectre.Console.Analyzer.csproj", "{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -472,6 +476,30 @@ Global
{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|x64.Build.0 = Release|Any CPU
{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|x86.ActiveCfg = Release|Any CPU
{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1}.Release|x86.Build.0 = Release|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Debug|x64.ActiveCfg = Debug|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Debug|x64.Build.0 = Debug|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Debug|x86.ActiveCfg = Debug|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Debug|x86.Build.0 = Debug|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Release|Any CPU.Build.0 = Release|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Release|x64.ActiveCfg = Release|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Release|x64.Build.0 = Release|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Release|x86.ActiveCfg = Release|Any CPU
{D2B32355-D99F-480B-92BF-9FAABE79ADD4}.Release|x86.Build.0 = Release|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Debug|x64.ActiveCfg = Debug|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Debug|x64.Build.0 = Debug|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Debug|x86.ActiveCfg = Debug|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Debug|x86.Build.0 = Debug|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Release|Any CPU.Build.0 = Release|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Release|x64.ActiveCfg = Release|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Release|x64.Build.0 = Release|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Release|x86.ActiveCfg = Release|Any CPU
{26006ACD-F19D-4C2A-8864-FE0D6C15B58C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -508,6 +536,7 @@ Global
{A0C772BA-C5F4-451D-AA7A-4045F2FA0201} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{8428A7DD-29FC-4417-9CA0-B90D34B26AB2} = {A0C772BA-C5F4-451D-AA7A-4045F2FA0201}
{E607AA2A-A4A6-48E4-8AAB-B0EB74EACAA1} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{D2B32355-D99F-480B-92BF-9FAABE79ADD4} = {A0C772BA-C5F4-451D-AA7A-4045F2FA0201}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}