This repository has been archived by the owner on Aug 2, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 346
Add a code analyzer to fix certain Span<T> usage #2206
Merged
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
| ||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio 15 | ||
VisualStudioVersion = 15.0.27604.0 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpanUsage", "SpanUsage\SpanUsage\SpanUsage.csproj", "{A99829A8-F185-4044-904F-211077DD11B8}" | ||
EndProject | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpanUsage.Test", "SpanUsage\SpanUsage.Test\SpanUsage.Test.csproj", "{C1C5D4D1-058B-41FC-8738-38A56178C390}" | ||
EndProject | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpanUsage.Vsix", "SpanUsage\SpanUsage.Vsix\SpanUsage.Vsix.csproj", "{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{A99829A8-F185-4044-904F-211077DD11B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{A99829A8-F185-4044-904F-211077DD11B8}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{A99829A8-F185-4044-904F-211077DD11B8}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{A99829A8-F185-4044-904F-211077DD11B8}.Release|Any CPU.Build.0 = Release|Any CPU | ||
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Release|Any CPU.Build.0 = Release|Any CPU | ||
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {8B59ECEA-7478-4C05-9301-ABCA0CA3DAF4} | ||
EndGlobalSection | ||
EndGlobal |
85 changes: 85 additions & 0 deletions
85
samples/SpanUsage/SpanUsage/SpanUsage.Test/Helpers/CodeFixVerifier.Helper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} | ||
} | ||
|
87 changes: 87 additions & 0 deletions
87
samples/SpanUsage/SpanUsage/SpanUsage.Test/Helpers/DiagnosticResult.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} | ||
} |
170 changes: 170 additions & 0 deletions
170
samples/SpanUsage/SpanUsage/SpanUsage.Test/Helpers/DiagnosticVerifier.Helper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Text; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
|
||
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).Assembly.Location); | ||
private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); | ||
private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); | ||
private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); | ||
|
||
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 IDiagnosticAnalyzer 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 InvalidOperationException("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); | ||
|
||
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 | ||
} | ||
} | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it really a sample?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be considered a sample code analyzer for span. I wanted to start it there, and move it to the solution. Would you be fine with adding it to the solution?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@terrajobst is working on a package for many .NET analyzers. I think this analyzer might be a good fit for such package.
Having said that, I am totally fine with adding it to this samples project short term.