Add API Analyzer Assembly (#906)
* Start on API analyzers * Finish GuildAccessAnalyzer * Update build script (will this do?) * Correct slashes * Extrapolate DerivesFromModuleBase() to an extension method * Quick refactoring * Add doc file
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.DotNet.PlatformAbstractions;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
|
||||
namespace System
|
||||
{
|
||||
/// <summary> Polyfill of the AppDomain class from full framework. </summary>
|
||||
internal class AppDomain
|
||||
{
|
||||
public static AppDomain CurrentDomain { get; private set; }
|
||||
|
||||
private AppDomain()
|
||||
{
|
||||
}
|
||||
|
||||
static AppDomain()
|
||||
{
|
||||
CurrentDomain = new AppDomain();
|
||||
}
|
||||
|
||||
public Assembly[] GetAssemblies()
|
||||
{
|
||||
var rid = RuntimeEnvironment.GetRuntimeIdentifier();
|
||||
var ass = DependencyContext.Default.GetRuntimeAssemblyNames(rid);
|
||||
|
||||
return ass.Select(xan => Assembly.Load(xan)).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
111
test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs
Normal file
111
test/Discord.Net.Tests/AnalyzerTests/GuildAccessTests.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Discord.Analyzers;
|
||||
using TestHelper;
|
||||
using Xunit;
|
||||
|
||||
namespace Discord
|
||||
{
|
||||
public partial class AnalyserTests
|
||||
{
|
||||
public class GuildAccessTests : DiagnosticVerifier
|
||||
{
|
||||
[Fact]
|
||||
public void VerifyDiagnosticWhenLackingRequireContext()
|
||||
{
|
||||
string source = @"using System;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class TestModule : ModuleBase<ICommandContext>
|
||||
{
|
||||
[Command(""test"")]
|
||||
public Task TestCmd() => ReplyAsync(Context.Guild.Name);
|
||||
}
|
||||
}";
|
||||
var expected = new DiagnosticResult()
|
||||
{
|
||||
Id = "DNET0001",
|
||||
Locations = new[] { new DiagnosticResultLocation("Test0.cs", line: 10, column: 45) },
|
||||
Message = "Command method 'TestCmd' is accessing 'Context.Guild' but is not restricted to Guild contexts.",
|
||||
Severity = DiagnosticSeverity.Warning
|
||||
};
|
||||
VerifyCSharpDiagnostic(source, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyDiagnosticWhenWrongRequireContext()
|
||||
{
|
||||
string source = @"using System;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class TestModule : ModuleBase<ICommandContext>
|
||||
{
|
||||
[Command(""test""), RequireContext(ContextType.Group)]
|
||||
public Task TestCmd() => ReplyAsync(Context.Guild.Name);
|
||||
}
|
||||
}";
|
||||
var expected = new DiagnosticResult()
|
||||
{
|
||||
Id = "DNET0001",
|
||||
Locations = new[] { new DiagnosticResultLocation("Test0.cs", line: 10, column: 45) },
|
||||
Message = "Command method 'TestCmd' is accessing 'Context.Guild' but is not restricted to Guild contexts.",
|
||||
Severity = DiagnosticSeverity.Warning
|
||||
};
|
||||
VerifyCSharpDiagnostic(source, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyNoDiagnosticWhenRequireContextOnMethod()
|
||||
{
|
||||
string source = @"using System;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public class TestModule : ModuleBase<ICommandContext>
|
||||
{
|
||||
[Command(""test""), RequireContext(ContextType.Guild)]
|
||||
public Task TestCmd() => ReplyAsync(Context.Guild.Name);
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyCSharpDiagnostic(source, Array.Empty<DiagnosticResult>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyNoDiagnosticWhenRequireContextOnClass()
|
||||
{
|
||||
string source = @"using System;
|
||||
using System.Threading.Tasks;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public class TestModule : ModuleBase<ICommandContext>
|
||||
{
|
||||
[Command(""test"")]
|
||||
public Task TestCmd() => ReplyAsync(Context.Guild.Name);
|
||||
}
|
||||
}";
|
||||
|
||||
VerifyCSharpDiagnostic(source, Array.Empty<DiagnosticResult>());
|
||||
}
|
||||
|
||||
protected override DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
=> new GuildAccessAnalyzer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
using Microsoft.CodeAnalysis.Simplification;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Diagnostic Producer class with extra methods dealing with applying codefixes
|
||||
/// All methods are static
|
||||
/// </summary>
|
||||
public abstract partial class CodeFixVerifier : DiagnosticVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply the inputted CodeAction to the inputted document.
|
||||
/// Meant to be used to apply codefixes.
|
||||
/// </summary>
|
||||
/// <param name="document">The Document to apply the fix on</param>
|
||||
/// <param name="codeAction">A CodeAction that will be applied to the Document.</param>
|
||||
/// <returns>A Document with the changes from the CodeAction</returns>
|
||||
private static Document ApplyFix(Document document, CodeAction codeAction)
|
||||
{
|
||||
var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result;
|
||||
var solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
|
||||
return solution.GetDocument(document.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection.
|
||||
/// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row,
|
||||
/// this method may not necessarily return the new one.
|
||||
/// </summary>
|
||||
/// <param name="diagnostics">The Diagnostics that existed in the code before the CodeFix was applied</param>
|
||||
/// <param name="newDiagnostics">The Diagnostics that exist in the code after the CodeFix was applied</param>
|
||||
/// <returns>A list of Diagnostics that only surfaced in the code after the CodeFix was applied</returns>
|
||||
private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic> diagnostics, IEnumerable<Diagnostic> newDiagnostics)
|
||||
{
|
||||
var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
|
||||
var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
|
||||
|
||||
int oldIndex = 0;
|
||||
int newIndex = 0;
|
||||
|
||||
while (newIndex < newArray.Length)
|
||||
{
|
||||
if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id)
|
||||
{
|
||||
++oldIndex;
|
||||
++newIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return newArray[newIndex++];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the existing compiler diagnostics on the inputted document.
|
||||
/// </summary>
|
||||
/// <param name="document">The Document to run the compiler diagnostic analyzers on</param>
|
||||
/// <returns>The compiler diagnostics that were found in the code</returns>
|
||||
private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document)
|
||||
{
|
||||
return document.GetSemanticModelAsync().Result.GetDiagnostics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a document, turn it into a string based on the syntax root
|
||||
/// </summary>
|
||||
/// <param name="document">The Document to be converted to a string</param>
|
||||
/// <returns>A string containing the syntax of the Document after formatting</returns>
|
||||
private static string GetStringFromDocument(Document document)
|
||||
{
|
||||
var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result;
|
||||
var root = simplifiedDoc.GetSyntaxRootAsync().Result;
|
||||
root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
|
||||
return root.GetText().ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Location where the diagnostic appears, as determined by path, line number, and column number.
|
||||
/// </summary>
|
||||
public struct DiagnosticResultLocation
|
||||
{
|
||||
public DiagnosticResultLocation(string path, int line, int column)
|
||||
{
|
||||
if (line < -1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
|
||||
}
|
||||
|
||||
if (column < -1)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
|
||||
}
|
||||
|
||||
this.Path = path;
|
||||
this.Line = line;
|
||||
this.Column = column;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public int Line { get; }
|
||||
public int Column { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Struct that stores information about a Diagnostic appearing in a source
|
||||
/// </summary>
|
||||
public struct DiagnosticResult
|
||||
{
|
||||
private DiagnosticResultLocation[] locations;
|
||||
|
||||
public DiagnosticResultLocation[] Locations
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.locations == null)
|
||||
{
|
||||
this.locations = new DiagnosticResultLocation[] { };
|
||||
}
|
||||
return this.locations;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.locations = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DiagnosticSeverity Severity { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string Path
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Path : "";
|
||||
}
|
||||
}
|
||||
|
||||
public int Line
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Line : -1;
|
||||
}
|
||||
}
|
||||
|
||||
public int Column
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Locations.Length > 0 ? this.Locations[0].Column : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Discord;
|
||||
using Discord.Commands;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for turning strings into documents and getting the diagnostics on them
|
||||
/// All methods are static
|
||||
/// </summary>
|
||||
public abstract partial class DiagnosticVerifier
|
||||
{
|
||||
private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location);
|
||||
private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location);
|
||||
private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).GetTypeInfo().Assembly.Location);
|
||||
private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).GetTypeInfo().Assembly.Location);
|
||||
//private static readonly MetadataReference DiscordNetReference = MetadataReference.CreateFromFile(typeof(IDiscordClient).GetTypeInfo().Assembly.Location);
|
||||
//private static readonly MetadataReference DiscordCommandsReference = MetadataReference.CreateFromFile(typeof(CommandAttribute).GetTypeInfo().Assembly.Location);
|
||||
private static readonly Assembly DiscordCommandsAssembly = typeof(CommandAttribute).GetTypeInfo().Assembly;
|
||||
|
||||
internal static string DefaultFilePathPrefix = "Test";
|
||||
internal static string CSharpDefaultFileExt = "cs";
|
||||
internal static string VisualBasicDefaultExt = "vb";
|
||||
internal static string TestProjectName = "TestProject";
|
||||
|
||||
#region Get Diagnostics
|
||||
|
||||
/// <summary>
|
||||
/// Given classes in the form of strings, their language, and an IDiagnosticAnlayzer to apply to it, return the diagnostics found in the string after converting it to a document.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source classes are in</param>
|
||||
/// <param name="analyzer">The analyzer to be run on the sources</param>
|
||||
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
|
||||
private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer)
|
||||
{
|
||||
return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
|
||||
/// The returned diagnostics are then ordered by location in the source document.
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer to run on the documents</param>
|
||||
/// <param name="documents">The Documents that the analyzer will be run on</param>
|
||||
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
|
||||
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
|
||||
{
|
||||
var projects = new HashSet<Project>();
|
||||
foreach (var document in documents)
|
||||
{
|
||||
projects.Add(document.Project);
|
||||
}
|
||||
|
||||
var diagnostics = new List<Diagnostic>();
|
||||
foreach (var project in projects)
|
||||
{
|
||||
var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
|
||||
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
|
||||
foreach (var diag in diags)
|
||||
{
|
||||
if (diag.Location == Location.None || diag.Location.IsInMetadata)
|
||||
{
|
||||
diagnostics.Add(diag);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < documents.Length; i++)
|
||||
{
|
||||
var document = documents[i];
|
||||
var tree = document.GetSyntaxTreeAsync().Result;
|
||||
if (tree == diag.Location.SourceTree)
|
||||
{
|
||||
diagnostics.Add(diag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var results = SortDiagnostics(diagnostics);
|
||||
diagnostics.Clear();
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sort diagnostics by location in source document
|
||||
/// </summary>
|
||||
/// <param name="diagnostics">The list of Diagnostics to be sorted</param>
|
||||
/// <returns>An IEnumerable containing the Diagnostics in order of Location</returns>
|
||||
private static Diagnostic[] SortDiagnostics(IEnumerable<Diagnostic> diagnostics)
|
||||
{
|
||||
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Set up compilation and documents
|
||||
/// <summary>
|
||||
/// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Tuple containing the Documents produced from the sources and their TextSpans if relevant</returns>
|
||||
private static Document[] GetDocuments(string[] sources, string language)
|
||||
{
|
||||
if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic)
|
||||
{
|
||||
throw new ArgumentException("Unsupported Language");
|
||||
}
|
||||
|
||||
var project = CreateProject(sources, language);
|
||||
var documents = project.Documents.ToArray();
|
||||
|
||||
if (sources.Length != documents.Length)
|
||||
{
|
||||
throw new Exception("Amount of sources did not match amount of Documents created");
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Document from a string through creating a project that contains it.
|
||||
/// </summary>
|
||||
/// <param name="source">Classes in the form of a string</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Document created from the source string</returns>
|
||||
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
|
||||
{
|
||||
return CreateProject(new[] { source }, language).Documents.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a project using the inputted strings as sources.
|
||||
/// </summary>
|
||||
/// <param name="sources">Classes in the form of strings</param>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <returns>A Project created out of the Documents created from the source strings</returns>
|
||||
private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
|
||||
{
|
||||
string fileNamePrefix = DefaultFilePathPrefix;
|
||||
string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
|
||||
|
||||
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
|
||||
|
||||
var solution = new AdhocWorkspace()
|
||||
.CurrentSolution
|
||||
.AddProject(projectId, TestProjectName, TestProjectName, language)
|
||||
.AddMetadataReference(projectId, CorlibReference)
|
||||
.AddMetadataReference(projectId, SystemCoreReference)
|
||||
.AddMetadataReference(projectId, CSharpSymbolsReference)
|
||||
.AddMetadataReference(projectId, CodeAnalysisReference)
|
||||
.AddMetadataReferences(projectId, Transitive(DiscordCommandsAssembly));
|
||||
|
||||
int count = 0;
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var newFileName = fileNamePrefix + count + "." + fileExt;
|
||||
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
|
||||
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
|
||||
count++;
|
||||
}
|
||||
return solution.GetProject(projectId);
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="MetadataReference"/> for <paramref name="assembly"/> and all assemblies referenced by <paramref name="assembly"/>
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly.</param>
|
||||
/// <returns><see cref="MetadataReference"/>s.</returns>
|
||||
private static IEnumerable<MetadataReference> Transitive(Assembly assembly)
|
||||
{
|
||||
foreach (var a in RecursiveReferencedAssemblies(assembly))
|
||||
{
|
||||
yield return MetadataReference.CreateFromFile(a.Location);
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<Assembly> RecursiveReferencedAssemblies(Assembly a, HashSet<Assembly> assemblies = null)
|
||||
{
|
||||
assemblies = assemblies ?? new HashSet<Assembly>();
|
||||
if (assemblies.Add(a))
|
||||
{
|
||||
foreach (var referencedAssemblyName in a.GetReferencedAssemblies())
|
||||
{
|
||||
var referencedAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SingleOrDefault(x => x.GetName() == referencedAssemblyName) ??
|
||||
Assembly.Load(referencedAssemblyName);
|
||||
RecursiveReferencedAssemblies(referencedAssembly, assemblies);
|
||||
}
|
||||
}
|
||||
|
||||
return assemblies;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CodeActions;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
//using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Superclass of all Unit tests made for diagnostics with codefixes.
|
||||
/// Contains methods used to verify correctness of codefixes
|
||||
/// </summary>
|
||||
public abstract partial class CodeFixVerifier : DiagnosticVerifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the codefix being tested (C#) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
/// <returns>The CodeFixProvider to be used for CSharp code</returns>
|
||||
protected virtual CodeFixProvider GetCSharpCodeFixProvider()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the codefix being tested (VB) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
/// <returns>The CodeFixProvider to be used for VisualBasic code</returns>
|
||||
protected virtual CodeFixProvider GetBasicCodeFixProvider()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# codefix when applied on the inputted string as a source
|
||||
/// </summary>
|
||||
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
|
||||
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
|
||||
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
|
||||
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
|
||||
protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
|
||||
{
|
||||
VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a VB codefix when applied on the inputted string as a source
|
||||
/// </summary>
|
||||
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
|
||||
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
|
||||
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
|
||||
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
|
||||
protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false)
|
||||
{
|
||||
VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// General verifier for codefixes.
|
||||
/// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes.
|
||||
/// Then gets the string after the codefix is applied and compares it with the expected result.
|
||||
/// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true.
|
||||
/// </summary>
|
||||
/// <param name="language">The language the source code is in</param>
|
||||
/// <param name="analyzer">The analyzer to be applied to the source code</param>
|
||||
/// <param name="codeFixProvider">The codefix to be applied to the code wherever the relevant Diagnostic is found</param>
|
||||
/// <param name="oldSource">A class in the form of a string before the CodeFix was applied to it</param>
|
||||
/// <param name="newSource">A class in the form of a string after the CodeFix was applied to it</param>
|
||||
/// <param name="codeFixIndex">Index determining which codefix to apply if there are multiple</param>
|
||||
/// <param name="allowNewCompilerDiagnostics">A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied</param>
|
||||
private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics)
|
||||
{
|
||||
var document = CreateDocument(oldSource, language);
|
||||
var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
|
||||
var compilerDiagnostics = GetCompilerDiagnostics(document);
|
||||
var attempts = analyzerDiagnostics.Length;
|
||||
|
||||
for (int i = 0; i < attempts; ++i)
|
||||
{
|
||||
var actions = new List<CodeAction>();
|
||||
var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None);
|
||||
codeFixProvider.RegisterCodeFixesAsync(context).Wait();
|
||||
|
||||
if (!actions.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (codeFixIndex != null)
|
||||
{
|
||||
document = ApplyFix(document, actions.ElementAt((int)codeFixIndex));
|
||||
break;
|
||||
}
|
||||
|
||||
document = ApplyFix(document, actions.ElementAt(0));
|
||||
analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document });
|
||||
|
||||
var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
|
||||
|
||||
//check if applying the code fix introduced any new compiler diagnostics
|
||||
if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any())
|
||||
{
|
||||
// Format and get the compiler diagnostics again so that the locations make sense in the output
|
||||
document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace));
|
||||
newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document));
|
||||
|
||||
Assert.True(false,
|
||||
string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n",
|
||||
string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())),
|
||||
document.GetSyntaxRootAsync().Result.ToFullString()));
|
||||
}
|
||||
|
||||
//check if there are analyzer diagnostics left after the code fix
|
||||
if (!analyzerDiagnostics.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//after applying all of the code fixes, compare the resulting string to the inputted one
|
||||
var actual = GetStringFromDocument(document);
|
||||
Assert.Equal(newSource, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
//using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Xunit;
|
||||
|
||||
namespace TestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Superclass of all Unit Tests for DiagnosticAnalyzers
|
||||
/// </summary>
|
||||
public abstract partial class DiagnosticVerifier
|
||||
{
|
||||
#region To be implemented by Test classes
|
||||
/// <summary>
|
||||
/// Get the CSharp analyzer being tested - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetCSharpDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the Visual Basic analyzer being tested (C#) - to be implemented in non-abstract class
|
||||
/// </summary>
|
||||
protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Verifier wrappers
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# DiagnosticAnalyzer when applied on the single inputted string as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="source">A class in the form of a string to run the analyzer on</param>
|
||||
/// <param name="expected"> DiagnosticResults that should appear after the analyzer is run on the source</param>
|
||||
protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a VB DiagnosticAnalyzer when applied on the single inputted string as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="source">A class in the form of a string to run the analyzer on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the source</param>
|
||||
protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a C# DiagnosticAnalyzer when applied on the inputted strings as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
protected void VerifyCSharpDiagnostic(string[] sources, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(sources, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to test a VB DiagnosticAnalyzer when applied on the inputted strings as a source
|
||||
/// Note: input a DiagnosticResult for each Diagnostic expected
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] expected)
|
||||
{
|
||||
VerifyDiagnostics(sources, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// General method that gets a collection of actual diagnostics found in the source after the analyzer is run,
|
||||
/// then verifies each of them.
|
||||
/// </summary>
|
||||
/// <param name="sources">An array of strings to create source documents from to run the analyzers on</param>
|
||||
/// <param name="language">The language of the classes represented by the source strings</param>
|
||||
/// <param name="analyzer">The analyzer to be run on the source code</param>
|
||||
/// <param name="expected">DiagnosticResults that should appear after the analyzer is run on the sources</param>
|
||||
private void VerifyDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expected)
|
||||
{
|
||||
var diagnostics = GetSortedDiagnostics(sources, language, analyzer);
|
||||
VerifyDiagnosticResults(diagnostics, analyzer, expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Actual comparisons and verifications
|
||||
/// <summary>
|
||||
/// Checks each of the actual Diagnostics found and compares them with the corresponding DiagnosticResult in the array of expected results.
|
||||
/// Diagnostics are considered equal only if the DiagnosticResultLocation, Id, Severity, and Message of the DiagnosticResult match the actual diagnostic.
|
||||
/// </summary>
|
||||
/// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param>
|
||||
/// <param name="analyzer">The analyzer that was being run on the sources</param>
|
||||
/// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param>
|
||||
private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
|
||||
{
|
||||
int expectedCount = expectedResults.Count();
|
||||
int actualCount = actualResults.Count();
|
||||
|
||||
if (expectedCount != actualCount)
|
||||
{
|
||||
string diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE.";
|
||||
|
||||
Assert.True(false,
|
||||
string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", expectedCount, actualCount, diagnosticsOutput));
|
||||
}
|
||||
|
||||
for (int i = 0; i < expectedResults.Length; i++)
|
||||
{
|
||||
var actual = actualResults.ElementAt(i);
|
||||
var expected = expectedResults[i];
|
||||
|
||||
if (expected.Line == -1 && expected.Column == -1)
|
||||
{
|
||||
if (actual.Location != Location.None)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}",
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, actual.Location, expected.Locations.First());
|
||||
var additionalLocations = actual.AdditionalLocations.ToArray();
|
||||
|
||||
if (additionalLocations.Length != expected.Locations.Length - 1)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n",
|
||||
expected.Locations.Length - 1, additionalLocations.Length,
|
||||
FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
for (int j = 0; j < additionalLocations.Length; ++j)
|
||||
{
|
||||
VerifyDiagnosticLocation(analyzer, actual, additionalLocations[j], expected.Locations[j + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (actual.Id != expected.Id)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Id, actual.Id, FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.Severity != expected.Severity)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
|
||||
if (actual.GetMessage() != expected.Message)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to VerifyDiagnosticResult that checks the location of a diagnostic and compares it with the location in the expected DiagnosticResult.
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that was being run on the sources</param>
|
||||
/// <param name="diagnostic">The diagnostic that was found in the code</param>
|
||||
/// <param name="actual">The Location of the Diagnostic found in the code</param>
|
||||
/// <param name="expected">The DiagnosticResultLocation that should have been found</param>
|
||||
private static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Location actual, DiagnosticResultLocation expected)
|
||||
{
|
||||
var actualSpan = actual.GetLineSpan();
|
||||
|
||||
Assert.True(actualSpan.Path == expected.Path || (actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test.")),
|
||||
string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic)));
|
||||
|
||||
var actualLinePosition = actualSpan.StartLinePosition;
|
||||
|
||||
// Only check line position if there is an actual line in the real diagnostic
|
||||
if (actualLinePosition.Line > 0)
|
||||
{
|
||||
if (actualLinePosition.Line + 1 != expected.Line)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
|
||||
// Only check column position if there is an actual column position in the real diagnostic
|
||||
if (actualLinePosition.Character > 0)
|
||||
{
|
||||
if (actualLinePosition.Character + 1 != expected.Column)
|
||||
{
|
||||
Assert.True(false,
|
||||
string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n",
|
||||
expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic)));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Formatting Diagnostics
|
||||
/// <summary>
|
||||
/// Helper method to format a Diagnostic into an easily readable string
|
||||
/// </summary>
|
||||
/// <param name="analyzer">The analyzer that this verifier tests</param>
|
||||
/// <param name="diagnostics">The Diagnostics to be formatted</param>
|
||||
/// <returns>The Diagnostics formatted as a string</returns>
|
||||
private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] diagnostics)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
for (int i = 0; i < diagnostics.Length; ++i)
|
||||
{
|
||||
builder.AppendLine("// " + diagnostics[i].ToString());
|
||||
|
||||
var analyzerType = analyzer.GetType();
|
||||
var rules = analyzer.SupportedDiagnostics;
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule != null && rule.Id == diagnostics[i].Id)
|
||||
{
|
||||
var location = diagnostics[i].Location;
|
||||
if (location == Location.None)
|
||||
{
|
||||
builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(location.IsInSource,
|
||||
$"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n");
|
||||
|
||||
string resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt";
|
||||
var linePosition = diagnostics[i].Location.GetLineSpan().StartLinePosition;
|
||||
|
||||
builder.AppendFormat("{0}({1}, {2}, {3}.{4})",
|
||||
resultMethodName,
|
||||
linePosition.Line + 1,
|
||||
linePosition.Character + 1,
|
||||
analyzerType.Name,
|
||||
rule.Id);
|
||||
}
|
||||
|
||||
if (i != diagnostics.Length - 1)
|
||||
{
|
||||
builder.Append(',');
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<ProjectReference Include="../../src/Discord.Net.Commands/Discord.Net.Commands.csproj" />
|
||||
<ProjectReference Include="../../src/Discord.Net.Core/Discord.Net.Core.csproj" />
|
||||
<ProjectReference Include="../../src/Discord.Net.Rest/Discord.Net.Rest.csproj" />
|
||||
<ProjectReference Include="../../src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Akavache" Version="5.0.0" />
|
||||
|
||||
Reference in New Issue
Block a user