-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathIncludeTestClassesAnalyzer.cs
More file actions
148 lines (126 loc) · 5.96 KB
/
Copy pathIncludeTestClassesAnalyzer.cs
File metadata and controls
148 lines (126 loc) · 5.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Copyright (c) Contributors to the New StyleCop Analyzers project.
// Licensed under the MIT License. See LICENSE in the project root for license information.
namespace StyleCop.Analyzers.PrivateAnalyzers;
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class IncludeTestClassesAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Descriptor =
new(PrivateDiagnosticIds.SP0001, "Include all test classes", "Expected test class '{0}' was not found", "Correctness", DiagnosticSeverity.Warning, isEnabledByDefault: true, customTags: new[] { "CompilationEnd" });
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Descriptor);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterCompilationStartAction(context =>
{
var assemblyName = context.Compilation.AssemblyName ?? string.Empty;
if (!Regex.IsMatch(assemblyName, @"^StyleCop\.Analyzers\.Test\.CSharp\d+$") || assemblyName.EndsWith("CSharp6"))
{
// This is not a test project where derived test classes are expected
return;
}
// Map actual test class in current project to base type
var testClasses = new ConcurrentDictionary<string, string>();
context.RegisterSymbolAction(
context =>
{
var namedType = (INamedTypeSymbol)context.Symbol;
if (namedType.TypeKind != TypeKind.Class)
{
return;
}
testClasses[namedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)] = namedType.BaseType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? string.Empty;
},
SymbolKind.NamedType);
context.RegisterCompilationEndAction(context =>
{
var currentVersion = int.Parse(assemblyName["StyleCop.Analyzers.Test.CSharp".Length..]);
var currentTestString = "CSharp" + currentVersion;
var previousTestString = "CSharp" + (currentVersion - 1).ToString();
var previousAssemblyName = "StyleCop.Analyzers.Test." + previousTestString;
var previousAssembly = context.Compilation.Assembly.Modules.First().ReferencedAssemblySymbols.First(
symbol => symbol.Identity.Name == previousAssemblyName);
if (previousAssembly is null)
{
return;
}
var reportingLocation = context.Compilation.SyntaxTrees.FirstOrDefault()?.GetLocation(new TextSpan(0, 0)) ?? Location.None;
var collector = new TestClassCollector(previousTestString);
var previousTests = collector.Visit(previousAssembly);
foreach (var previousTest in previousTests)
{
string expectedTest;
if (previousTestString is "")
{
expectedTest = previousTest.Replace(previousAssemblyName, assemblyName).Replace("UnitTests", currentTestString + "UnitTests");
}
else
{
expectedTest = previousTest.Replace(previousTestString, currentTestString);
}
if (testClasses.TryGetValue(expectedTest, out var actualTest)
&& actualTest == previousTest)
{
continue;
}
context.ReportDiagnostic(Diagnostic.Create(Descriptor, reportingLocation, expectedTest));
}
});
});
}
private sealed class TestClassCollector : SymbolVisitor<ImmutableSortedSet<string>>
{
private readonly string testString;
public TestClassCollector(string testString)
{
this.testString = testString;
}
public override ImmutableSortedSet<string> Visit(ISymbol? symbol)
=> base.Visit(symbol) ?? throw new InvalidOperationException("Not reachable");
public override ImmutableSortedSet<string>? DefaultVisit(ISymbol symbol)
=> ImmutableSortedSet<string>.Empty;
public override ImmutableSortedSet<string> VisitAssembly(IAssemblySymbol symbol)
{
return this.Visit(symbol.GlobalNamespace);
}
public override ImmutableSortedSet<string> VisitNamespace(INamespaceSymbol symbol)
{
var result = ImmutableSortedSet<string>.Empty;
foreach (var member in symbol.GetMembers())
{
result = result.Union(this.Visit(member)!);
}
return result;
}
public override ImmutableSortedSet<string> VisitNamedType(INamedTypeSymbol symbol)
{
if (this.testString is "")
{
if (symbol.Name.EndsWith("UnitTests"))
{
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}
else
{
return ImmutableSortedSet<string>.Empty;
}
}
else if (symbol.Name.Contains(this.testString))
{
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
}
else
{
return ImmutableSortedSet<string>.Empty;
}
}
}
}