-
Notifications
You must be signed in to change notification settings - Fork 509
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3704 from sharwell/generate-tests
Generate and validate derived test classes
- Loading branch information
Showing
400 changed files
with
1,721 additions
and
1,301 deletions.
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
167 changes: 167 additions & 0 deletions
167
StyleCop.Analyzers/StyleCop.Analyzers.PrivateAnalyzers/DerivedTestGenerator.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,167 @@ | ||
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. | ||
// Licensed under the MIT License. See LICENSE in the project root for license information. | ||
|
||
namespace StyleCop.Analyzers.PrivateAnalyzers | ||
{ | ||
using System; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Text.RegularExpressions; | ||
using Microsoft.CodeAnalysis; | ||
|
||
[Generator] | ||
internal sealed class DerivedTestGenerator : IIncrementalGenerator | ||
{ | ||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
var testData = context.CompilationProvider.Select((compilation, cancellationToken) => | ||
{ | ||
var currentAssemblyName = compilation.AssemblyName ?? string.Empty; | ||
if (!Regex.IsMatch(currentAssemblyName, @"^StyleCop\.Analyzers\.Test\.CSharp\d+$")) | ||
{ | ||
// This is not a test project where derived test classes are expected | ||
return null; | ||
} | ||
|
||
var currentVersion = int.Parse(currentAssemblyName["StyleCop.Analyzers.Test.CSharp".Length..]); | ||
var currentTestString = "CSharp" + currentVersion; | ||
var previousTestString = currentVersion switch | ||
{ | ||
7 => string.Empty, | ||
_ => "CSharp" + (currentVersion - 1).ToString(), | ||
}; | ||
var previousAssemblyName = previousTestString switch | ||
{ | ||
"" => "StyleCop.Analyzers.Test", | ||
_ => "StyleCop.Analyzers.Test." + previousTestString, | ||
}; | ||
|
||
return new TestData(previousTestString, previousAssemblyName, currentTestString, currentAssemblyName); | ||
}); | ||
|
||
var testTypes = context.CompilationProvider.Combine(testData).SelectMany((compilationAndTestData, cancellationToken) => | ||
{ | ||
var (compilation, testData) = compilationAndTestData; | ||
if (testData is null) | ||
{ | ||
return ImmutableArray<string>.Empty; | ||
} | ||
|
||
var previousAssembly = compilation.Assembly.Modules.First().ReferencedAssemblySymbols.First( | ||
symbol => symbol.Identity.Name == testData.PreviousAssemblyName); | ||
if (previousAssembly is null) | ||
{ | ||
return ImmutableArray<string>.Empty; | ||
} | ||
|
||
var collector = new TestClassCollector(testData.PreviousTestString); | ||
var previousTests = collector.Visit(previousAssembly); | ||
return previousTests.ToImmutableArray(); | ||
}); | ||
|
||
context.RegisterSourceOutput( | ||
testTypes.Combine(testData), | ||
(context, testTypeAndData) => | ||
{ | ||
var (testType, testData) = testTypeAndData; | ||
if (testData is null) | ||
{ | ||
throw new InvalidOperationException("Not reachable"); | ||
} | ||
|
||
string expectedTest; | ||
if (testData.PreviousTestString is "") | ||
{ | ||
expectedTest = testType.Replace(testData.PreviousAssemblyName, testData.CurrentAssemblyName).Replace("UnitTests", testData.CurrentTestString + "UnitTests"); | ||
} | ||
else | ||
{ | ||
expectedTest = testType.Replace(testData.PreviousTestString, testData.CurrentTestString); | ||
} | ||
|
||
var lastDot = testType.LastIndexOf('.'); | ||
var baseNamespaceName = testType["global::".Length..lastDot]; | ||
var baseTypeName = testType[(lastDot + 1)..]; | ||
|
||
lastDot = expectedTest.LastIndexOf('.'); | ||
var namespaceName = expectedTest["global::".Length..lastDot]; | ||
var typeName = expectedTest[(lastDot + 1)..]; | ||
var content = | ||
$@"// <auto-generated/> | ||
#nullable enable | ||
namespace {namespaceName}; | ||
using {baseNamespaceName}; | ||
public partial class {typeName} | ||
: {baseTypeName} | ||
{{ | ||
}} | ||
"; | ||
|
||
context.AddSource( | ||
typeName + ".cs", | ||
content); | ||
}); | ||
} | ||
|
||
private sealed record TestData(string PreviousTestString, string PreviousAssemblyName, string CurrentTestString, string CurrentAssemblyName); | ||
|
||
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; | ||
} | ||
} | ||
} | ||
} | ||
} |
156 changes: 156 additions & 0 deletions
156
StyleCop.Analyzers/StyleCop.Analyzers.PrivateAnalyzers/IncludeTestClassesAnalyzer.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,156 @@ | ||
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. | ||
// 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+$")) | ||
{ | ||
// 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 = currentVersion switch | ||
{ | ||
7 => string.Empty, | ||
_ => "CSharp" + (currentVersion - 1).ToString(), | ||
}; | ||
var previousAssemblyName = previousTestString switch | ||
{ | ||
"" => "StyleCop.Analyzers.Test", | ||
_ => "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; | ||
} | ||
} | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
StyleCop.Analyzers/StyleCop.Analyzers.PrivateAnalyzers/PrivateDiagnosticIds.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,13 @@ | ||
// Copyright (c) Tunnel Vision Laboratories, LLC. All Rights Reserved. | ||
// Licensed under the MIT License. See LICENSE in the project root for license information. | ||
|
||
namespace StyleCop.Analyzers.PrivateAnalyzers | ||
{ | ||
internal static class PrivateDiagnosticIds | ||
{ | ||
/// <summary> | ||
/// SP0001: Include all test classes. | ||
/// </summary> | ||
public const string SP0001 = nameof(SP0001); | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
....Analyzers/StyleCop.Analyzers.PrivateAnalyzers/StyleCop.Analyzers.PrivateAnalyzers.csproj
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,31 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netstandard2.0</TargetFramework> | ||
<!-- RS2008: Enable analyzer release tracking --> | ||
<NoWarn>$(NoWarn),RS2008</NoWarn> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<CodeAnalysisRuleSet>..\StyleCop.Analyzers.ruleset</CodeAnalysisRuleSet> | ||
</PropertyGroup> | ||
|
||
<PropertyGroup> | ||
<SignAssembly>true</SignAssembly> | ||
<AssemblyOriginatorKeyFile>..\..\build\keys\StyleCopAnalyzers.snk</AssemblyOriginatorKeyFile> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" /> | ||
<PackageReference Include="TunnelVisionLabs.LanguageTypes.SourceGenerator" Version="0.1.20-beta" /> | ||
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" PrivateAssets="all" /> | ||
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[3.1.0]" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<!-- The .generated file is excluded by default, but we want to show the items in Solution Explorer so we included it as None --> | ||
<None Include="Lightup\.generated\**" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Oops, something went wrong.