Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 12 additions & 0 deletions AspNetCore.Analyzer.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project>

<Import Project="$(MSBuildThisFileDirectory)Roslyn.props" />

<ItemGroup>
<Compile Include="..\TUnit.AspNetCore.Analyzers\**\*.cs" Exclude="..\TUnit.AspNetCore.Analyzers\obj\**\*.cs" />
<EmbeddedResource Include="..\TUnit.AspNetCore.Analyzers\**\*.resx" />
<AdditionalFiles Include="..\TUnit.AspNetCore.Analyzers\AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="..\TUnit.AspNetCore.Analyzers\AnalyzerReleases.Unshipped.md" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
<IsPackable>false</IsPackable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<PropertyGroup Condition="$(MSBuildProjectName.StartsWith('TUnit.AspNetCore.Analyzers.Roslyn'))">
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<AssemblyName>TUnit.AspNetCore.Analyzers</AssemblyName>
<RootNamespace>TUnit.AspNetCore.Analyzers</RootNamespace>
<IsPackable>false</IsPackable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<PropertyGroup>
<CurrentYear>$([System.DateTime]::Now.ToString("yyyy"))</CurrentYear>
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
<PackageVersion Include="Verify" Version="31.9.0" />
<PackageVersion Include="Verify.NUnit" Version="31.9.0" />
<PackageVersion Include="TUnit" Version="1.6.5" />
<PackageVersion Include="TUnit.AspNetCore" Version="1.6.5" />
<PackageVersion Include="TUnit.Core" Version="1.6.5" />
<PackageVersion Include="TUnit.Assertions" Version="1.6.5" />
<PackageVersion Include="Verify.TUnit" Version="31.9.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RoslynVersion>4.14</RoslynVersion>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)..\AspNetCore.Analyzer.props" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RoslynVersion>4.4</RoslynVersion>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)..\AspNetCore.Analyzer.props" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RoslynVersion>4.7</RoslynVersion>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)..\AspNetCore.Analyzer.props" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\TestProject.props" />

<PropertyGroup>
<!-- ASP.NET Core only supports .NET 8.0+ -->
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" VersionOverride="4.8.0" />
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TUnit.AspNetCore.Analyzers\TUnit.AspNetCore.Analyzers.csproj" />
<ProjectReference Include="..\TUnit.Engine\TUnit.Engine.csproj" />
<ProjectReference Include="..\TUnit.Core\TUnit.Core.csproj" />
</ItemGroup>

<Import Project="..\TestProject.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;

namespace TUnit.AspNetCore.Analyzers.Tests.Verifiers;

public static partial class CSharpAnalyzerVerifier<TAnalyzer>
where TAnalyzer : DiagnosticAnalyzer, new()
{
public class Test : CSharpAnalyzerTest<TAnalyzer, LineEndingNormalizingVerifier>
{
public Test()
{
ReferenceAssemblies.AddAssemblies(ReferenceAssemblies.Net.Net60.Assemblies);
SolutionTransforms.Add((solution, projectId) =>
{
var project = solution.GetProject(projectId);

if (project is null)
{
return solution;
}

var compilationOptions = project.CompilationOptions;

if (compilationOptions is null)
{
return solution;
}

if (compilationOptions is CSharpCompilationOptions cSharpCompilationOptions)
{
compilationOptions =
cSharpCompilationOptions.WithNullableContextOptions(NullableContextOptions.Enable);
}

if (project.ParseOptions is not CSharpParseOptions parseOptions)
{
return solution;
}

compilationOptions = compilationOptions
.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions
.SetItems(CSharpVerifierHelper.NullableWarnings)
// Suppress analyzer release tracking warnings - we're testing TUnit analyzers, not release tracking
.SetItem("RS2007", ReportDiagnostic.Suppress)
.SetItem("RS2008", ReportDiagnostic.Suppress));

solution = solution.WithProjectCompilationOptions(projectId, compilationOptions)
.WithProjectParseOptions(projectId, parseOptions
.WithLanguageVersion(LanguageVersion.Preview));

return solution;
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;

namespace TUnit.AspNetCore.Analyzers.Tests.Verifiers;

public static partial class CSharpAnalyzerVerifier<TAnalyzer>
where TAnalyzer : DiagnosticAnalyzer, new()
{
/// <inheritdoc cref="Microsoft.CodeAnalysis.Diagnostic"/>
public static DiagnosticResult Diagnostic()
=> CSharpAnalyzerVerifier<TAnalyzer, DefaultVerifier>.Diagnostic();

/// <inheritdoc cref="AnalyzerVerifier{TAnalyzer, TTest, TVerifier}.Diagnostic(string)"/>
public static DiagnosticResult Diagnostic(string diagnosticId)
=> CSharpAnalyzerVerifier<TAnalyzer, DefaultVerifier>.Diagnostic(diagnosticId);

/// <inheritdoc cref="AnalyzerVerifier{TAnalyzer, TTest, TVerifier}.Diagnostic(DiagnosticDescriptor)"/>
public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
=> CSharpAnalyzerVerifier<TAnalyzer, DefaultVerifier>.Diagnostic(descriptor);

/// <inheritdoc cref="AnalyzerVerifier{TAnalyzer, TTest, TVerifier}.VerifyAnalyzerAsync(string, DiagnosticResult[])"/>
public static Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, params DiagnosticResult[] expected)
{
return VerifyAnalyzerAsync(source, _ => { }, expected);
}

/// <inheritdoc cref="AnalyzerVerifier{TAnalyzer, TTest, TVerifier}.VerifyAnalyzerAsync(string, DiagnosticResult[])"/>
public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, Action<Test> configureTest, params DiagnosticResult[] expected)
{
var test = new Test
{
TestCode = source,
ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
TestState =
{
AdditionalReferences =
{
typeof(TUnit.Core.TUnitAttribute).Assembly.Location,
},
},
};

test.ExpectedDiagnostics.AddRange(expected);

configureTest(test);

await test.RunAsync(CancellationToken.None);
}
}
32 changes: 32 additions & 0 deletions TUnit.AspNetCore.Analyzers.Tests/Verifiers/CSharpVerifierHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TUnit.AspNetCore.Analyzers.Tests.Verifiers;

internal static class CSharpVerifierHelper
{
/// <summary>
/// By default, the compiler reports diagnostics for nullable reference types at
/// <see cref="DiagnosticSeverity.Warning"/>, and the analyzer test framework defaults to only validating
/// diagnostics at <see cref="DiagnosticSeverity.Error"/>. This map contains all compiler diagnostic IDs
/// related to nullability mapped to <see cref="ReportDiagnostic.Error"/>, which is then used to enable all
/// of these warnings for default validation during analyzer and code fix tests.
/// </summary>
internal static ImmutableDictionary<string, ReportDiagnostic> NullableWarnings { get; } = GetNullableWarningsFromCompiler();

private static ImmutableDictionary<string, ReportDiagnostic> GetNullableWarningsFromCompiler()
{
string[] args = ["/warnaserror:nullable", "-p:LangVersion=preview"];
var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory);
var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions;

// Workaround for https://github.com/dotnet/roslyn/issues/41610
nullableWarnings = nullableWarnings
.SetItem("CS8632", ReportDiagnostic.Error)
.SetItem("CS8669", ReportDiagnostic.Error)
.SetItem("CS8652", ReportDiagnostic.Suppress);

return nullableWarnings;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;

namespace TUnit.AspNetCore.Analyzers.Tests.Verifiers;

/// <summary>
/// A custom verifier that normalizes line endings to LF before comparison to support cross-platform testing.
/// This prevents tests from failing due to differences between Windows (CRLF) and Unix (LF) line endings.
/// By normalizing to LF (the universal standard), tests pass consistently on all platforms.
/// </summary>
public class LineEndingNormalizingVerifier : IVerifier
{
private readonly DefaultVerifier _defaultVerifier = new();

public void Empty<T>(string collectionName, IEnumerable<T> collection)
{
_defaultVerifier.Empty(collectionName, collection);
}

public void Equal<T>(T expected, T actual, string? message = null)
{
// Normalize line endings for string comparisons
if (expected is string expectedString && actual is string actualString)
{
var normalizedExpected = NormalizeLineEndings(expectedString);
var normalizedActual = NormalizeLineEndings(actualString);
_defaultVerifier.Equal(normalizedExpected, normalizedActual, message);
}
else
{
_defaultVerifier.Equal(expected, actual, message);
}
}

public void True(bool assert, string? message = null)
{
_defaultVerifier.True(assert, message);
}

public void False(bool assert, string? message = null)
{
_defaultVerifier.False(assert, message);
}

[DoesNotReturn]
public void Fail(string? message = null)
{
_defaultVerifier.Fail(message);
}

public void LanguageIsSupported(string language)
{
_defaultVerifier.LanguageIsSupported(language);
}

public void NotEmpty<T>(string collectionName, IEnumerable<T> collection)
{
_defaultVerifier.NotEmpty(collectionName, collection);
}

public void SequenceEqual<T>(IEnumerable<T> expected, IEnumerable<T> actual, IEqualityComparer<T>? equalityComparer = null, string? message = null)
{
// Normalize line endings for string sequence comparisons
if (typeof(T) == typeof(string))
{
var normalizedExpected = expected.Cast<string>().Select(NormalizeLineEndings).Cast<T>();
var normalizedActual = actual.Cast<string>().Select(NormalizeLineEndings).Cast<T>();
_defaultVerifier.SequenceEqual(normalizedExpected, normalizedActual, equalityComparer, message);
}
else
{
_defaultVerifier.SequenceEqual(expected, actual, equalityComparer, message);
}
}

public IVerifier PushContext(string context)
{
// Create a new verifier that wraps the result of PushContext on the default verifier
return new LineEndingNormalizingVerifierWithContext(_defaultVerifier.PushContext(context));
}

private static string NormalizeLineEndings(string value)
{
// Normalize all line endings to LF (Unix) for cross-platform consistent comparison
// LF is the universal standard and prevents Windows/Linux test mismatches
return value.Replace("\r\n", "\n");
}

/// <summary>
/// Internal helper class to wrap a verifier with context
/// </summary>
private class LineEndingNormalizingVerifierWithContext : LineEndingNormalizingVerifier
{
private readonly IVerifier _wrappedVerifier;

public LineEndingNormalizingVerifierWithContext(IVerifier wrappedVerifier)
{
_wrappedVerifier = wrappedVerifier;
}
}
}
Loading
Loading