diff --git a/examples/Console/AnalyzerTester/AnalyzerTester.csproj b/examples/Console/AnalyzerTester/AnalyzerTester.csproj
new file mode 100644
index 0000000..76cf494
--- /dev/null
+++ b/examples/Console/AnalyzerTester/AnalyzerTester.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net5.0
+
+
+
+
+
+
+
+
diff --git a/examples/Console/AnalyzerTester/Program.cs b/examples/Console/AnalyzerTester/Program.cs
new file mode 100644
index 0000000..ed5b945
--- /dev/null
+++ b/examples/Console/AnalyzerTester/Program.cs
@@ -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!");
+ }
+ }
+}
diff --git a/src/Spectre.Console.Analyzer/Analyzers/BaseAnalyzer.cs b/src/Spectre.Console.Analyzer/Analyzers/BaseAnalyzer.cs
new file mode 100644
index 0000000..5886b10
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Analyzers/BaseAnalyzer.cs
@@ -0,0 +1,25 @@
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Spectre.Console.Analyzer
+{
+ ///
+ /// Base class for Spectre analyzers.
+ ///
+ public abstract class BaseAnalyzer : DiagnosticAnalyzer
+ {
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(AnalyzeCompilation);
+ }
+
+ ///
+ /// Analyze compilation.
+ ///
+ /// Compilation Start Analysis Context.
+ protected abstract void AnalyzeCompilation(CompilationStartAnalysisContext compilationStartContext);
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Analyzers/FavorInstanceAnsiConsoleOverStaticAnalyzer.cs b/src/Spectre.Console.Analyzer/Analyzers/FavorInstanceAnsiConsoleOverStaticAnalyzer.cs
new file mode 100644
index 0000000..c9a7620
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Analyzers/FavorInstanceAnsiConsoleOverStaticAnalyzer.cs
@@ -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
+{
+ ///
+ /// Analyzer to suggest using available instances of AnsiConsole over the static methods.
+ ///
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class FavorInstanceAnsiConsoleOverStaticAnalyzer : BaseAnalyzer
+ {
+ private static readonly DiagnosticDescriptor _diagnosticDescriptor =
+ Descriptors.S1010_FavorInstanceAnsiConsoleOverStatic;
+
+ ///
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(_diagnosticDescriptor);
+
+ ///
+ 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()
+ .First()
+ .ParameterList.Parameters
+ .Any(i => i.Type.NormalizeWhitespace().ToString() == "IAnsiConsole");
+ }
+
+ private static bool HasFieldAnsiConsole(SyntaxNode syntaxNode)
+ {
+ var isStatic = syntaxNode
+ .Ancestors()
+ .OfType()
+ .First()
+ .Modifiers.Any(i => i.Kind() == SyntaxKind.StaticKeyword);
+
+ return syntaxNode
+ .Ancestors().OfType()
+ .First()
+ .Members
+ .OfType()
+ .Any(i =>
+ i.Declaration.Type.NormalizeWhitespace().ToString() == "IAnsiConsole" &&
+ (!isStatic ^ i.Modifiers.Any(modifier => modifier.Kind() == SyntaxKind.StaticKeyword)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Analyzers/UseSpectreInsteadOfSystemConsoleAnalyzer.cs b/src/Spectre.Console.Analyzer/Analyzers/UseSpectreInsteadOfSystemConsoleAnalyzer.cs
new file mode 100644
index 0000000..5dbe6c0
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Analyzers/UseSpectreInsteadOfSystemConsoleAnalyzer.cs
@@ -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
+{
+ ///
+ /// Analyzer to enforce the use of AnsiConsole over System.Console for known methods.
+ ///
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class UseSpectreInsteadOfSystemConsoleAnalyzer : BaseAnalyzer
+ {
+ private static readonly DiagnosticDescriptor _diagnosticDescriptor =
+ Descriptors.S1000_UseAnsiConsoleOverSystemConsole;
+
+ private static readonly string[] _methods = { "WriteLine", "Write" };
+
+ ///
+ public override ImmutableArray SupportedDiagnostics =>
+ ImmutableArray.Create(_diagnosticDescriptor);
+
+ ///
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Constants.cs b/src/Spectre.Console.Analyzer/Constants.cs
new file mode 100644
index 0000000..6c14084
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Constants.cs
@@ -0,0 +1,8 @@
+namespace Spectre.Console.Analyzer
+{
+ internal static class Constants
+ {
+ internal const string StaticInstance = "AnsiConsole";
+ internal const string SpectreConsole = "Spectre.Console";
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Descriptors.cs b/src/Spectre.Console.Analyzer/Descriptors.cs
new file mode 100644
index 0000000..3d4d242
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Descriptors.cs
@@ -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
+{
+ ///
+ /// Code analysis descriptors.
+ ///
+ public static class Descriptors
+ {
+ internal enum Category
+ {
+ Usage, // 1xxx
+ }
+
+ private static readonly ConcurrentDictionary _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);
+ }
+
+ ///
+ /// Gets definitions of diagnostics Spectre1000.
+ ///
+ public static DiagnosticDescriptor S1000_UseAnsiConsoleOverSystemConsole { get; } =
+ Rule(
+ "Spectre1000",
+ "Use AnsiConsole instead of System.Console",
+ Usage,
+ Warning,
+ "Use AnsiConsole instead of System.Console");
+
+ ///
+ /// Gets definitions of diagnostics Spectre1010.
+ ///
+ 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.");
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Fixes/CodeActions/SwitchToAnsiConsoleAction.cs b/src/Spectre.Console.Analyzer/Fixes/CodeActions/SwitchToAnsiConsoleAction.cs
new file mode 100644
index 0000000..c1c9e77
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Fixes/CodeActions/SwitchToAnsiConsoleAction.cs
@@ -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
+{
+ ///
+ /// Code action to change calls to System.Console to AnsiConsole.
+ ///
+ public class SwitchToAnsiConsoleAction : CodeAction
+ {
+ private readonly Document _document;
+ private readonly InvocationExpressionSyntax _originalInvocation;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Document to change.
+ /// The method to change.
+ /// Title of the fix.
+ public SwitchToAnsiConsoleAction(Document document, InvocationExpressionSyntax originalInvocation, string title)
+ {
+ _document = document;
+ _originalInvocation = originalInvocation;
+ Title = title;
+ }
+
+ ///
+ public override string Title { get; }
+
+ ///
+ public override string EquivalenceKey => Title;
+
+ ///
+ protected override async Task 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()
+ .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()
+ .First()
+ .Modifiers.Any(i => i.Kind() == SyntaxKind.StaticKeyword);
+
+ return _originalInvocation
+ .Ancestors().OfType()
+ .First()
+ .Members
+ .OfType()
+ .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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Fixes/FixProviders/StaticAnsiConsoleToInstanceFix.cs b/src/Spectre.Console.Analyzer/Fixes/FixProviders/StaticAnsiConsoleToInstanceFix.cs
new file mode 100644
index 0000000..53a8b74
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Fixes/FixProviders/StaticAnsiConsoleToInstanceFix.cs
@@ -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
+{
+ ///
+ /// Fix provider to change System.Console calls to AnsiConsole calls.
+ ///
+ [ExportCodeFixProvider(LanguageNames.CSharp)]
+ [Shared]
+ public class StaticAnsiConsoleToInstanceFix : CodeFixProvider
+ {
+ ///
+ public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(
+ Descriptors.S1010_FavorInstanceAnsiConsoleOverStatic.Id);
+
+ ///
+ public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ var methodDeclaration = root.FindNode(context.Span).FirstAncestorOrSelf();
+ context.RegisterCodeFix(
+ new SwitchToAnsiConsoleAction(context.Document, methodDeclaration, "Convert static AnsiConsole calls to local instance."),
+ context.Diagnostics);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Fixes/FixProviders/SystemConsoleToAnsiConsoleFix.cs b/src/Spectre.Console.Analyzer/Fixes/FixProviders/SystemConsoleToAnsiConsoleFix.cs
new file mode 100644
index 0000000..530ebe0
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Fixes/FixProviders/SystemConsoleToAnsiConsoleFix.cs
@@ -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
+{
+ ///
+ /// Fix provider to change System.Console calls to AnsiConsole calls.
+ ///
+ [ExportCodeFixProvider(LanguageNames.CSharp)]
+ [Shared]
+ public class SystemConsoleToAnsiConsoleFix : CodeFixProvider
+ {
+ ///
+ public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(
+ Descriptors.S1000_UseAnsiConsoleOverSystemConsole.Id);
+
+ ///
+ public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ var methodDeclaration = root.FindNode(context.Span).FirstAncestorOrSelf();
+ context.RegisterCodeFix(
+ new SwitchToAnsiConsoleAction(context.Document, methodDeclaration, "Convert static call to AnsiConsole to Spectre.Console.AnsiConsole"),
+ context.Diagnostics);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Fixes/Syntax.cs b/src/Spectre.Console.Analyzer/Fixes/Syntax.cs
new file mode 100644
index 0000000..cc0e6e3
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Fixes/Syntax.cs
@@ -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")));
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Analyzer/Spectre.Console.Analyzer.csproj b/src/Spectre.Console.Analyzer/Spectre.Console.Analyzer.csproj
new file mode 100644
index 0000000..aa0e6b0
--- /dev/null
+++ b/src/Spectre.Console.Analyzer/Spectre.Console.Analyzer.csproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ true
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Spectre.Console.Tests/CodeAnalyzer/Analyzers/UseInstanceAnsiConsoleTests.cs b/src/Spectre.Console.Tests/CodeAnalyzer/Analyzers/UseInstanceAnsiConsoleTests.cs
new file mode 100644
index 0000000..5c57e7b
--- /dev/null
+++ b/src/Spectre.Console.Tests/CodeAnalyzer/Analyzers/UseInstanceAnsiConsoleTests.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/CodeAnalyzer/Analyzers/UseSpectreInsteadOfSystemConsoleAnalyzerTests.cs b/src/Spectre.Console.Tests/CodeAnalyzer/Analyzers/UseSpectreInsteadOfSystemConsoleAnalyzerTests.cs
new file mode 100644
index 0000000..f63ee88
--- /dev/null
+++ b/src/Spectre.Console.Tests/CodeAnalyzer/Analyzers/UseSpectreInsteadOfSystemConsoleAnalyzerTests.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/CodeAnalyzer/CodeFixProviderDiscovery.cs b/src/Spectre.Console.Tests/CodeAnalyzer/CodeFixProviderDiscovery.cs
new file mode 100644
index 0000000..daecc51
--- /dev/null
+++ b/src/Spectre.Console.Tests/CodeAnalyzer/CodeFixProviderDiscovery.cs
@@ -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 _exportProviderFactory;
+
+ static CodeFixProviderDiscovery()
+ {
+ _exportProviderFactory = new Lazy(
+ () =>
+ {
+ 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 GetCodeFixProviders(string language)
+ {
+ var exportProvider = _exportProviderFactory.Value.CreateExportProvider();
+ var exports = exportProvider.GetExports();
+ return exports.Where(export => export.Metadata.Languages.Contains(language)).Select(export => export.Value);
+ }
+
+ private class LanguageMetadata
+ {
+ public LanguageMetadata(IDictionary data)
+ {
+ if (!data.TryGetValue(nameof(ExportCodeFixProviderAttribute.Languages), out var languages))
+ {
+ languages = Array.Empty();
+ }
+
+ Languages = ((string[])languages).ToImmutableArray();
+ }
+
+ public ImmutableArray Languages { get; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/CodeAnalyzer/Fixes/UseInstanceOfStaticAnsiConsoleTests.cs b/src/Spectre.Console.Tests/CodeAnalyzer/Fixes/UseInstanceOfStaticAnsiConsoleTests.cs
new file mode 100644
index 0000000..74a13e1
--- /dev/null
+++ b/src/Spectre.Console.Tests/CodeAnalyzer/Fixes/UseInstanceOfStaticAnsiConsoleTests.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/CodeAnalyzer/Fixes/UseSpectreInsteadOfSystemConsoleFixTests.cs b/src/Spectre.Console.Tests/CodeAnalyzer/Fixes/UseSpectreInsteadOfSystemConsoleFixTests.cs
new file mode 100644
index 0000000..0524313
--- /dev/null
+++ b/src/Spectre.Console.Tests/CodeAnalyzer/Fixes/UseSpectreInsteadOfSystemConsoleFixTests.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/CodeAnalyzer/SpectreAnalyzerVerifier.cs b/src/Spectre.Console.Tests/CodeAnalyzer/SpectreAnalyzerVerifier.cs
new file mode 100644
index 0000000..2f00a09
--- /dev/null
+++ b/src/Spectre.Console.Tests/CodeAnalyzer/SpectreAnalyzerVerifier.cs
@@ -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
+ 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 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
+ {
+ public Test()
+ {
+ ReferenceAssemblies = CodeAnalyzerHelper.CurrentSpectre;
+ TestBehaviors |= TestBehaviors.SkipGeneratedCodeCheck;
+ }
+
+ protected override IEnumerable 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)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj
index f667424..49f69fc 100644
--- a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj
+++ b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj
@@ -17,6 +17,9 @@
+
+
+
@@ -29,6 +32,7 @@
+
diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln
index 1b6df2e..d59b8d9 100644
--- a/src/Spectre.Console.sln
+++ b/src/Spectre.Console.sln
@@ -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}