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

Commit 48c1f63

Browse files
authored
Add a code analyzer to fix certain Span<T> usage (#2206)
* Add a code analyzer to fix certain Span<T> usage * Add license header to all source files * Update tests * Update the VSIX description
1 parent 992826c commit 48c1f63

15 files changed

+1550
-0
lines changed

samples/SpanUsage/SpanUsage.sln

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.27604.0
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpanUsage", "SpanUsage\SpanUsage\SpanUsage.csproj", "{A99829A8-F185-4044-904F-211077DD11B8}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpanUsage.Test", "SpanUsage\SpanUsage.Test\SpanUsage.Test.csproj", "{C1C5D4D1-058B-41FC-8738-38A56178C390}"
9+
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpanUsage.Vsix", "SpanUsage\SpanUsage.Vsix\SpanUsage.Vsix.csproj", "{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}"
11+
EndProject
12+
Global
13+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
14+
Debug|Any CPU = Debug|Any CPU
15+
Release|Any CPU = Release|Any CPU
16+
EndGlobalSection
17+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
18+
{A99829A8-F185-4044-904F-211077DD11B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19+
{A99829A8-F185-4044-904F-211077DD11B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
20+
{A99829A8-F185-4044-904F-211077DD11B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
21+
{A99829A8-F185-4044-904F-211077DD11B8}.Release|Any CPU.Build.0 = Release|Any CPU
22+
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23+
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Debug|Any CPU.Build.0 = Debug|Any CPU
24+
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Release|Any CPU.ActiveCfg = Release|Any CPU
25+
{C1C5D4D1-058B-41FC-8738-38A56178C390}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{A6F3E9A4-DD45-4C93-8B49-AFA4D587AC59}.Release|Any CPU.Build.0 = Release|Any CPU
30+
EndGlobalSection
31+
GlobalSection(SolutionProperties) = preSolution
32+
HideSolutionNode = FALSE
33+
EndGlobalSection
34+
GlobalSection(ExtensibilityGlobals) = postSolution
35+
SolutionGuid = {8B59ECEA-7478-4C05-9301-ABCA0CA3DAF4}
36+
EndGlobalSection
37+
EndGlobal
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeActions;
7+
using Microsoft.CodeAnalysis.Formatting;
8+
using Microsoft.CodeAnalysis.Simplification;
9+
using System.Collections.Generic;
10+
using System.Linq;
11+
using System.Threading;
12+
13+
namespace TestHelper
14+
{
15+
/// <summary>
16+
/// Diagnostic Producer class with extra methods dealing with applying codefixes
17+
/// All methods are static
18+
/// </summary>
19+
public abstract partial class CodeFixVerifier : DiagnosticVerifier
20+
{
21+
/// <summary>
22+
/// Apply the inputted CodeAction to the inputted document.
23+
/// Meant to be used to apply codefixes.
24+
/// </summary>
25+
/// <param name="document">The Document to apply the fix on</param>
26+
/// <param name="codeAction">A CodeAction that will be applied to the Document.</param>
27+
/// <returns>A Document with the changes from the CodeAction</returns>
28+
private static Document ApplyFix(Document document, CodeAction codeAction)
29+
{
30+
var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result;
31+
var solution = operations.OfType<ApplyChangesOperation>().Single().ChangedSolution;
32+
return solution.GetDocument(document.Id);
33+
}
34+
35+
/// <summary>
36+
/// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection.
37+
/// 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,
38+
/// this method may not necessarily return the new one.
39+
/// </summary>
40+
/// <param name="diagnostics">The Diagnostics that existed in the code before the CodeFix was applied</param>
41+
/// <param name="newDiagnostics">The Diagnostics that exist in the code after the CodeFix was applied</param>
42+
/// <returns>A list of Diagnostics that only surfaced in the code after the CodeFix was applied</returns>
43+
private static IEnumerable<Diagnostic> GetNewDiagnostics(IEnumerable<Diagnostic> diagnostics, IEnumerable<Diagnostic> newDiagnostics)
44+
{
45+
var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
46+
var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
47+
48+
int oldIndex = 0;
49+
int newIndex = 0;
50+
51+
while (newIndex < newArray.Length)
52+
{
53+
if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id)
54+
{
55+
++oldIndex;
56+
++newIndex;
57+
}
58+
else
59+
{
60+
yield return newArray[newIndex++];
61+
}
62+
}
63+
}
64+
65+
/// <summary>
66+
/// Get the existing compiler diagnostics on the inputted document.
67+
/// </summary>
68+
/// <param name="document">The Document to run the compiler diagnostic analyzers on</param>
69+
/// <returns>The compiler diagnostics that were found in the code</returns>
70+
private static IEnumerable<Diagnostic> GetCompilerDiagnostics(Document document)
71+
{
72+
return document.GetSemanticModelAsync().Result.GetDiagnostics();
73+
}
74+
75+
/// <summary>
76+
/// Given a document, turn it into a string based on the syntax root
77+
/// </summary>
78+
/// <param name="document">The Document to be converted to a string</param>
79+
/// <returns>A string containing the syntax of the Document after formatting</returns>
80+
private static string GetStringFromDocument(Document document)
81+
{
82+
var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result;
83+
var root = simplifiedDoc.GetSyntaxRootAsync().Result;
84+
root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace);
85+
return root.GetText().ToString();
86+
}
87+
}
88+
}
89+
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
using System;
7+
8+
namespace TestHelper
9+
{
10+
/// <summary>
11+
/// Location where the diagnostic appears, as determined by path, line number, and column number.
12+
/// </summary>
13+
public struct DiagnosticResultLocation
14+
{
15+
public DiagnosticResultLocation(string path, int line, int column)
16+
{
17+
if (line < -1)
18+
{
19+
throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1");
20+
}
21+
22+
if (column < -1)
23+
{
24+
throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1");
25+
}
26+
27+
this.Path = path;
28+
this.Line = line;
29+
this.Column = column;
30+
}
31+
32+
public string Path { get; }
33+
public int Line { get; }
34+
public int Column { get; }
35+
}
36+
37+
/// <summary>
38+
/// Struct that stores information about a Diagnostic appearing in a source
39+
/// </summary>
40+
public struct DiagnosticResult
41+
{
42+
private DiagnosticResultLocation[] locations;
43+
44+
public DiagnosticResultLocation[] Locations
45+
{
46+
get
47+
{
48+
if (this.locations == null)
49+
{
50+
this.locations = new DiagnosticResultLocation[] { };
51+
}
52+
return this.locations;
53+
}
54+
55+
set
56+
{
57+
this.locations = value;
58+
}
59+
}
60+
61+
public DiagnosticSeverity Severity { get; set; }
62+
63+
public string Id { get; set; }
64+
65+
public string Message { get; set; }
66+
67+
public string Path
68+
{
69+
get
70+
{
71+
return this.Locations.Length > 0 ? this.Locations[0].Path : "";
72+
}
73+
}
74+
75+
public int Line
76+
{
77+
get
78+
{
79+
return this.Locations.Length > 0 ? this.Locations[0].Line : -1;
80+
}
81+
}
82+
83+
public int Column
84+
{
85+
get
86+
{
87+
return this.Locations.Length > 0 ? this.Locations[0].Column : -1;
88+
}
89+
}
90+
}
91+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.CodeAnalysis.Text;
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Collections.Immutable;
12+
using System.Linq;
13+
14+
namespace TestHelper
15+
{
16+
/// <summary>
17+
/// Class for turning strings into documents and getting the diagnostics on them
18+
/// All methods are static
19+
/// </summary>
20+
public abstract partial class DiagnosticVerifier
21+
{
22+
private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
23+
private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location);
24+
private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location);
25+
private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location);
26+
27+
internal static string DefaultFilePathPrefix = "Test";
28+
internal static string CSharpDefaultFileExt = "cs";
29+
internal static string VisualBasicDefaultExt = "vb";
30+
internal static string TestProjectName = "TestProject";
31+
32+
#region Get Diagnostics
33+
34+
/// <summary>
35+
/// 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.
36+
/// </summary>
37+
/// <param name="sources">Classes in the form of strings</param>
38+
/// <param name="language">The language the source classes are in</param>
39+
/// <param name="analyzer">The analyzer to be run on the sources</param>
40+
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
41+
private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer)
42+
{
43+
return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language));
44+
}
45+
46+
/// <summary>
47+
/// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it.
48+
/// The returned diagnostics are then ordered by location in the source document.
49+
/// </summary>
50+
/// <param name="analyzer">The analyzer to run on the documents</param>
51+
/// <param name="documents">The Documents that the analyzer will be run on</param>
52+
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
53+
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
54+
{
55+
var projects = new HashSet<Project>();
56+
foreach (var document in documents)
57+
{
58+
projects.Add(document.Project);
59+
}
60+
61+
var diagnostics = new List<Diagnostic>();
62+
foreach (var project in projects)
63+
{
64+
var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer));
65+
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
66+
foreach (var diag in diags)
67+
{
68+
if (diag.Location == Location.None || diag.Location.IsInMetadata)
69+
{
70+
diagnostics.Add(diag);
71+
}
72+
else
73+
{
74+
for (int i = 0; i < documents.Length; i++)
75+
{
76+
var document = documents[i];
77+
var tree = document.GetSyntaxTreeAsync().Result;
78+
if (tree == diag.Location.SourceTree)
79+
{
80+
diagnostics.Add(diag);
81+
}
82+
}
83+
}
84+
}
85+
}
86+
87+
var results = SortDiagnostics(diagnostics);
88+
diagnostics.Clear();
89+
return results;
90+
}
91+
92+
/// <summary>
93+
/// Sort diagnostics by location in source document
94+
/// </summary>
95+
/// <param name="diagnostics">The list of Diagnostics to be sorted</param>
96+
/// <returns>An IEnumerable containing the Diagnostics in order of Location</returns>
97+
private static Diagnostic[] SortDiagnostics(IEnumerable<Diagnostic> diagnostics)
98+
{
99+
return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
100+
}
101+
102+
#endregion
103+
104+
#region Set up compilation and documents
105+
/// <summary>
106+
/// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it.
107+
/// </summary>
108+
/// <param name="sources">Classes in the form of strings</param>
109+
/// <param name="language">The language the source code is in</param>
110+
/// <returns>A Tuple containing the Documents produced from the sources and their TextSpans if relevant</returns>
111+
private static Document[] GetDocuments(string[] sources, string language)
112+
{
113+
if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic)
114+
{
115+
throw new ArgumentException("Unsupported Language");
116+
}
117+
118+
var project = CreateProject(sources, language);
119+
var documents = project.Documents.ToArray();
120+
121+
if (sources.Length != documents.Length)
122+
{
123+
throw new InvalidOperationException("Amount of sources did not match amount of Documents created");
124+
}
125+
126+
return documents;
127+
}
128+
129+
/// <summary>
130+
/// Create a Document from a string through creating a project that contains it.
131+
/// </summary>
132+
/// <param name="source">Classes in the form of a string</param>
133+
/// <param name="language">The language the source code is in</param>
134+
/// <returns>A Document created from the source string</returns>
135+
protected static Document CreateDocument(string source, string language = LanguageNames.CSharp)
136+
{
137+
return CreateProject(new[] { source }, language).Documents.First();
138+
}
139+
140+
/// <summary>
141+
/// Create a project using the inputted strings as sources.
142+
/// </summary>
143+
/// <param name="sources">Classes in the form of strings</param>
144+
/// <param name="language">The language the source code is in</param>
145+
/// <returns>A Project created out of the Documents created from the source strings</returns>
146+
private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp)
147+
{
148+
string fileNamePrefix = DefaultFilePathPrefix;
149+
string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt;
150+
151+
var projectId = ProjectId.CreateNewId(debugName: TestProjectName);
152+
153+
var solution = new AdhocWorkspace()
154+
.CurrentSolution
155+
.AddProject(projectId, TestProjectName, TestProjectName, language)
156+
.AddMetadataReference(projectId, CorlibReference)
157+
.AddMetadataReference(projectId, SystemCoreReference)
158+
.AddMetadataReference(projectId, CSharpSymbolsReference)
159+
.AddMetadataReference(projectId, CodeAnalysisReference);
160+
161+
int count = 0;
162+
foreach (var source in sources)
163+
{
164+
var newFileName = fileNamePrefix + count + "." + fileExt;
165+
var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName);
166+
solution = solution.AddDocument(documentId, newFileName, SourceText.From(source));
167+
count++;
168+
}
169+
return solution.GetProject(projectId);
170+
}
171+
#endregion
172+
}
173+
}
174+

0 commit comments

Comments
 (0)