Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Add a code analyzer to fix certain Span<T> usage #2206

Merged
merged 4 commits into from
Apr 16, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions samples/SpanUsage/SpanUsage.sln
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}"
Copy link
Member

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?

Copy link
Member Author

@ahsonkhan ahsonkhan Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: 1. Add to corefxlab.sln

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?

Copy link
Member

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.

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
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();
}
}
}

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;
}
}
}
}
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
}
}

Loading