Skip to content

Commit 0548969

Browse files
authored
Merge pull request #3704 from sharwell/generate-tests
Generate and validate derived test classes
2 parents 57b15ad + d1d8ee9 commit 0548969

File tree

400 files changed

+1721
-1301
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

400 files changed

+1721
-1301
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.CodeGeneration/StyleCop.Analyzers.CodeGeneration.csproj

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
<Project Sdk="Microsoft.NET.Sdk">
33

44
<PropertyGroup>
5-
<TargetFrameworks>netstandard2.0</TargetFrameworks>
6-
<Nullable>enable</Nullable>
5+
<TargetFramework>netstandard2.0</TargetFramework>
76
</PropertyGroup>
87

98
<PropertyGroup>
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.PrivateAnalyzers
5+
{
6+
using System;
7+
using System.Collections.Immutable;
8+
using System.Linq;
9+
using System.Text.RegularExpressions;
10+
using Microsoft.CodeAnalysis;
11+
12+
[Generator]
13+
internal sealed class DerivedTestGenerator : IIncrementalGenerator
14+
{
15+
public void Initialize(IncrementalGeneratorInitializationContext context)
16+
{
17+
var testData = context.CompilationProvider.Select((compilation, cancellationToken) =>
18+
{
19+
var currentAssemblyName = compilation.AssemblyName ?? string.Empty;
20+
if (!Regex.IsMatch(currentAssemblyName, @"^StyleCop\.Analyzers\.Test\.CSharp\d+$"))
21+
{
22+
// This is not a test project where derived test classes are expected
23+
return null;
24+
}
25+
26+
var currentVersion = int.Parse(currentAssemblyName["StyleCop.Analyzers.Test.CSharp".Length..]);
27+
var currentTestString = "CSharp" + currentVersion;
28+
var previousTestString = currentVersion switch
29+
{
30+
7 => string.Empty,
31+
_ => "CSharp" + (currentVersion - 1).ToString(),
32+
};
33+
var previousAssemblyName = previousTestString switch
34+
{
35+
"" => "StyleCop.Analyzers.Test",
36+
_ => "StyleCop.Analyzers.Test." + previousTestString,
37+
};
38+
39+
return new TestData(previousTestString, previousAssemblyName, currentTestString, currentAssemblyName);
40+
});
41+
42+
var testTypes = context.CompilationProvider.Combine(testData).SelectMany((compilationAndTestData, cancellationToken) =>
43+
{
44+
var (compilation, testData) = compilationAndTestData;
45+
if (testData is null)
46+
{
47+
return ImmutableArray<string>.Empty;
48+
}
49+
50+
var previousAssembly = compilation.Assembly.Modules.First().ReferencedAssemblySymbols.First(
51+
symbol => symbol.Identity.Name == testData.PreviousAssemblyName);
52+
if (previousAssembly is null)
53+
{
54+
return ImmutableArray<string>.Empty;
55+
}
56+
57+
var collector = new TestClassCollector(testData.PreviousTestString);
58+
var previousTests = collector.Visit(previousAssembly);
59+
return previousTests.ToImmutableArray();
60+
});
61+
62+
context.RegisterSourceOutput(
63+
testTypes.Combine(testData),
64+
(context, testTypeAndData) =>
65+
{
66+
var (testType, testData) = testTypeAndData;
67+
if (testData is null)
68+
{
69+
throw new InvalidOperationException("Not reachable");
70+
}
71+
72+
string expectedTest;
73+
if (testData.PreviousTestString is "")
74+
{
75+
expectedTest = testType.Replace(testData.PreviousAssemblyName, testData.CurrentAssemblyName).Replace("UnitTests", testData.CurrentTestString + "UnitTests");
76+
}
77+
else
78+
{
79+
expectedTest = testType.Replace(testData.PreviousTestString, testData.CurrentTestString);
80+
}
81+
82+
var lastDot = testType.LastIndexOf('.');
83+
var baseNamespaceName = testType["global::".Length..lastDot];
84+
var baseTypeName = testType[(lastDot + 1)..];
85+
86+
lastDot = expectedTest.LastIndexOf('.');
87+
var namespaceName = expectedTest["global::".Length..lastDot];
88+
var typeName = expectedTest[(lastDot + 1)..];
89+
var content =
90+
$@"// <auto-generated/>
91+
92+
#nullable enable
93+
94+
namespace {namespaceName};
95+
96+
using {baseNamespaceName};
97+
98+
public partial class {typeName}
99+
: {baseTypeName}
100+
{{
101+
}}
102+
";
103+
104+
context.AddSource(
105+
typeName + ".cs",
106+
content);
107+
});
108+
}
109+
110+
private sealed record TestData(string PreviousTestString, string PreviousAssemblyName, string CurrentTestString, string CurrentAssemblyName);
111+
112+
private sealed class TestClassCollector : SymbolVisitor<ImmutableSortedSet<string>>
113+
{
114+
private readonly string testString;
115+
116+
public TestClassCollector(string testString)
117+
{
118+
this.testString = testString;
119+
}
120+
121+
public override ImmutableSortedSet<string> Visit(ISymbol? symbol)
122+
=> base.Visit(symbol) ?? throw new InvalidOperationException("Not reachable");
123+
124+
public override ImmutableSortedSet<string>? DefaultVisit(ISymbol symbol)
125+
=> ImmutableSortedSet<string>.Empty;
126+
127+
public override ImmutableSortedSet<string> VisitAssembly(IAssemblySymbol symbol)
128+
{
129+
return this.Visit(symbol.GlobalNamespace);
130+
}
131+
132+
public override ImmutableSortedSet<string> VisitNamespace(INamespaceSymbol symbol)
133+
{
134+
var result = ImmutableSortedSet<string>.Empty;
135+
foreach (var member in symbol.GetMembers())
136+
{
137+
result = result.Union(this.Visit(member)!);
138+
}
139+
140+
return result;
141+
}
142+
143+
public override ImmutableSortedSet<string> VisitNamedType(INamedTypeSymbol symbol)
144+
{
145+
if (this.testString is "")
146+
{
147+
if (symbol.Name.EndsWith("UnitTests"))
148+
{
149+
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
150+
}
151+
else
152+
{
153+
return ImmutableSortedSet<string>.Empty;
154+
}
155+
}
156+
else if (symbol.Name.Contains(this.testString))
157+
{
158+
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
159+
}
160+
else
161+
{
162+
return ImmutableSortedSet<string>.Empty;
163+
}
164+
}
165+
}
166+
}
167+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.PrivateAnalyzers;
5+
6+
using System;
7+
using System.Collections.Concurrent;
8+
using System.Collections.Immutable;
9+
using System.Linq;
10+
using System.Text.RegularExpressions;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.Diagnostics;
13+
using Microsoft.CodeAnalysis.Text;
14+
15+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
16+
internal sealed class IncludeTestClassesAnalyzer : DiagnosticAnalyzer
17+
{
18+
private static readonly DiagnosticDescriptor Descriptor =
19+
new(PrivateDiagnosticIds.SP0001, "Include all test classes", "Expected test class '{0}' was not found", "Correctness", DiagnosticSeverity.Warning, isEnabledByDefault: true, customTags: new[] { "CompilationEnd" });
20+
21+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Descriptor);
22+
23+
public override void Initialize(AnalysisContext context)
24+
{
25+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
26+
context.EnableConcurrentExecution();
27+
28+
context.RegisterCompilationStartAction(context =>
29+
{
30+
var assemblyName = context.Compilation.AssemblyName ?? string.Empty;
31+
if (!Regex.IsMatch(assemblyName, @"^StyleCop\.Analyzers\.Test\.CSharp\d+$"))
32+
{
33+
// This is not a test project where derived test classes are expected
34+
return;
35+
}
36+
37+
// Map actual test class in current project to base type
38+
var testClasses = new ConcurrentDictionary<string, string>();
39+
40+
context.RegisterSymbolAction(
41+
context =>
42+
{
43+
var namedType = (INamedTypeSymbol)context.Symbol;
44+
if (namedType.TypeKind != TypeKind.Class)
45+
{
46+
return;
47+
}
48+
49+
testClasses[namedType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)] = namedType.BaseType?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? string.Empty;
50+
},
51+
SymbolKind.NamedType);
52+
53+
context.RegisterCompilationEndAction(context =>
54+
{
55+
var currentVersion = int.Parse(assemblyName["StyleCop.Analyzers.Test.CSharp".Length..]);
56+
var currentTestString = "CSharp" + currentVersion;
57+
var previousTestString = currentVersion switch
58+
{
59+
7 => string.Empty,
60+
_ => "CSharp" + (currentVersion - 1).ToString(),
61+
};
62+
var previousAssemblyName = previousTestString switch
63+
{
64+
"" => "StyleCop.Analyzers.Test",
65+
_ => "StyleCop.Analyzers.Test." + previousTestString,
66+
};
67+
68+
var previousAssembly = context.Compilation.Assembly.Modules.First().ReferencedAssemblySymbols.First(
69+
symbol => symbol.Identity.Name == previousAssemblyName);
70+
if (previousAssembly is null)
71+
{
72+
return;
73+
}
74+
75+
var reportingLocation = context.Compilation.SyntaxTrees.FirstOrDefault()?.GetLocation(new TextSpan(0, 0)) ?? Location.None;
76+
var collector = new TestClassCollector(previousTestString);
77+
var previousTests = collector.Visit(previousAssembly);
78+
foreach (var previousTest in previousTests)
79+
{
80+
string expectedTest;
81+
if (previousTestString is "")
82+
{
83+
expectedTest = previousTest.Replace(previousAssemblyName, assemblyName).Replace("UnitTests", currentTestString + "UnitTests");
84+
}
85+
else
86+
{
87+
expectedTest = previousTest.Replace(previousTestString, currentTestString);
88+
}
89+
90+
if (testClasses.TryGetValue(expectedTest, out var actualTest)
91+
&& actualTest == previousTest)
92+
{
93+
continue;
94+
}
95+
96+
context.ReportDiagnostic(Diagnostic.Create(Descriptor, reportingLocation, expectedTest));
97+
}
98+
});
99+
});
100+
}
101+
102+
private sealed class TestClassCollector : SymbolVisitor<ImmutableSortedSet<string>>
103+
{
104+
private readonly string testString;
105+
106+
public TestClassCollector(string testString)
107+
{
108+
this.testString = testString;
109+
}
110+
111+
public override ImmutableSortedSet<string> Visit(ISymbol? symbol)
112+
=> base.Visit(symbol) ?? throw new InvalidOperationException("Not reachable");
113+
114+
public override ImmutableSortedSet<string>? DefaultVisit(ISymbol symbol)
115+
=> ImmutableSortedSet<string>.Empty;
116+
117+
public override ImmutableSortedSet<string> VisitAssembly(IAssemblySymbol symbol)
118+
{
119+
return this.Visit(symbol.GlobalNamespace);
120+
}
121+
122+
public override ImmutableSortedSet<string> VisitNamespace(INamespaceSymbol symbol)
123+
{
124+
var result = ImmutableSortedSet<string>.Empty;
125+
foreach (var member in symbol.GetMembers())
126+
{
127+
result = result.Union(this.Visit(member)!);
128+
}
129+
130+
return result;
131+
}
132+
133+
public override ImmutableSortedSet<string> VisitNamedType(INamedTypeSymbol symbol)
134+
{
135+
if (this.testString is "")
136+
{
137+
if (symbol.Name.EndsWith("UnitTests"))
138+
{
139+
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
140+
}
141+
else
142+
{
143+
return ImmutableSortedSet<string>.Empty;
144+
}
145+
}
146+
else if (symbol.Name.Contains(this.testString))
147+
{
148+
return ImmutableSortedSet.Create(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
149+
}
150+
else
151+
{
152+
return ImmutableSortedSet<string>.Empty;
153+
}
154+
}
155+
}
156+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace StyleCop.Analyzers.PrivateAnalyzers
5+
{
6+
internal static class PrivateDiagnosticIds
7+
{
8+
/// <summary>
9+
/// SP0001: Include all test classes.
10+
/// </summary>
11+
public const string SP0001 = nameof(SP0001);
12+
}
13+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
4+
<PropertyGroup>
5+
<TargetFramework>netstandard2.0</TargetFramework>
6+
<!-- RS2008: Enable analyzer release tracking -->
7+
<NoWarn>$(NoWarn),RS2008</NoWarn>
8+
</PropertyGroup>
9+
10+
<PropertyGroup>
11+
<CodeAnalysisRuleSet>..\StyleCop.Analyzers.ruleset</CodeAnalysisRuleSet>
12+
</PropertyGroup>
13+
14+
<PropertyGroup>
15+
<SignAssembly>true</SignAssembly>
16+
<AssemblyOriginatorKeyFile>..\..\build\keys\StyleCopAnalyzers.snk</AssemblyOriginatorKeyFile>
17+
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
21+
<PackageReference Include="TunnelVisionLabs.LanguageTypes.SourceGenerator" Version="0.1.20-beta" />
22+
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" PrivateAssets="all" />
23+
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[3.1.0]" />
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<!-- The .generated file is excluded by default, but we want to show the items in Solution Explorer so we included it as None -->
28+
<None Include="Lightup\.generated\**" />
29+
</ItemGroup>
30+
31+
</Project>

0 commit comments

Comments
 (0)