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
9 changes: 7 additions & 2 deletions .github/workflows/justdummies-mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ jobs:
# generator is red.
fail-fast: false
matrix:
# The two JustDummies packages this repository SHIPS: the generator and its xUnit v3 adapter
# (ADR-0039). The FirstClassErrors libraries live in `mutation.yml`. See ADR-0043.
# What the JustDummies packages SHIP: the generator, its xUnit v3 adapter (ADR-0039), and the
# analyzers packed inside the generator package (ADR-0044). The FirstClassErrors libraries live
# in `mutation.yml`. See ADR-0043.
include:
- name: justdummies
config: build/stryker/justdummies.json
- name: justdummies-xunit
config: build/stryker/justdummies-xunit.json
- name: justdummies-analyzers
config: build/stryker/justdummies-analyzers.json
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down Expand Up @@ -196,6 +199,8 @@ jobs:
config: build/stryker/justdummies.json
- name: justdummies-xunit
config: build/stryker/justdummies-xunit.json
- name: justdummies-analyzers
config: build/stryker/justdummies-analyzers.json
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down
30 changes: 30 additions & 0 deletions FirstClassErrors.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JustDummies.Xunit.UnitTests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JustDummies.PropertyTests", "JustDummies.PropertyTests\JustDummies.PropertyTests.csproj", "{A09FC4C6-1470-4768-A1C0-2D4E42788FCF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JustDummies.Analyzers", "JustDummies.Analyzers\JustDummies.Analyzers.csproj", "{CD456AD9-0736-4333-89D8-4EF160185CDD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JustDummies.Analyzers.UnitTests", "JustDummies.Analyzers.UnitTests\JustDummies.Analyzers.UnitTests.csproj", "{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -373,6 +377,30 @@ Global
{A09FC4C6-1470-4768-A1C0-2D4E42788FCF}.Release|x64.Build.0 = Release|Any CPU
{A09FC4C6-1470-4768-A1C0-2D4E42788FCF}.Release|x86.ActiveCfg = Release|Any CPU
{A09FC4C6-1470-4768-A1C0-2D4E42788FCF}.Release|x86.Build.0 = Release|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Debug|x64.ActiveCfg = Debug|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Debug|x64.Build.0 = Debug|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Debug|x86.ActiveCfg = Debug|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Debug|x86.Build.0 = Debug|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Release|Any CPU.Build.0 = Release|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Release|x64.ActiveCfg = Release|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Release|x64.Build.0 = Release|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Release|x86.ActiveCfg = Release|Any CPU
{CD456AD9-0736-4333-89D8-4EF160185CDD}.Release|x86.Build.0 = Release|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Debug|x64.ActiveCfg = Debug|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Debug|x64.Build.0 = Debug|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Debug|x86.ActiveCfg = Debug|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Debug|x86.Build.0 = Debug|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Release|Any CPU.Build.0 = Release|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Release|x64.ActiveCfg = Release|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Release|x64.Build.0 = Release|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Release|x86.ActiveCfg = Release|Any CPU
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -403,6 +431,8 @@ Global
{BF67DBAE-EBE1-45CE-86E5-B2E9E3D4EF6B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{75FD5E2C-A871-4659-8A01-13461FED84EB} = {B7C3D08D-EFC5-4F5D-8DE4-5B7938354DBB}
{A09FC4C6-1470-4768-A1C0-2D4E42788FCF} = {B7C3D08D-EFC5-4F5D-8DE4-5B7938354DBB}
{CD456AD9-0736-4333-89D8-4EF160185CDD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{09EED96D-C8A7-44B2-BAEB-2E91A1ABFC8D} = {B7C3D08D-EFC5-4F5D-8DE4-5B7938354DBB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4988972E-3E0D-4F48-8656-0E67ECE994BF}
Expand Down
55 changes: 55 additions & 0 deletions JustDummies.Analyzers.UnitTests/AnalyzerTestHarness.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Collections.Immutable;

using JustDummies;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;

namespace JustDummies.Analyzers.UnitTests;

/// <summary>
/// Minimal in-process harness: compiles a C# snippet against the running runtime plus the JustDummies core, runs a
/// single analyzer over it, and returns the analyzer diagnostics. Deliberately dependency-free (no
/// Microsoft.CodeAnalysis.Testing) so it composes cleanly with xUnit v3 and NFluent — the same shape as
/// FirstClassErrors' harness.
/// </summary>
internal static class AnalyzerTestHarness {

private static readonly ImmutableArray<MetadataReference> References = BuildReferences();

public static async Task<ImmutableArray<Diagnostic>> GetDiagnosticsAsync(DiagnosticAnalyzer analyzer, string source) {
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, new CSharpParseOptions(LanguageVersion.Latest));

CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName: "JustDummies.Analyzers.TestSnippet",
syntaxTrees: new[] { syntaxTree },
references: References,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

CompilationWithAnalyzers withAnalyzers = compilation.WithAnalyzers(ImmutableArray.Create(analyzer));

return await withAnalyzers.GetAnalyzerDiagnosticsAsync();
}

private static ImmutableArray<MetadataReference> BuildReferences() {
List<MetadataReference> references = new();

// Reference the running runtime's assemblies so snippets resolve System types without pinning a ref pack.
string trustedAssemblies = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string ?? string.Empty;
foreach (string path in trustedAssemblies.Split(Path.PathSeparator)) {
if (string.IsNullOrEmpty(path)) { continue; }
try {
references.Add(MetadataReference.CreateFromFile(path));
} catch {
// Skip any native or otherwise unloadable entry in the TPA list.
}
}

// The JustDummies core, so Any.Reproducibly / Any.ReproduciblyAsync resolve inside the snippet.
references.Add(MetadataReference.CreateFromFile(typeof(Any).Assembly.Location));

return references.ToImmutableArray();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;

using NFluent;

namespace JustDummies.Analyzers.UnitTests;

public class Jd001AsyncBodyPassedToReproduciblyTests {

[Fact]
public async Task Reports_an_async_lambda_passed_to_Reproducibly() {
const string source = """
using JustDummies;
using System.Threading.Tasks;

public static class Sample {
public static void M() {
Any.Reproducibly(async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new AsyncBodyPassedToReproduciblyAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(1);
Check.That(diagnostics[0].Id).IsEqualTo("JD001");
Check.That(diagnostics[0].GetMessage()).Contains("ReproduciblyAsync");
}

[Fact]
public async Task Reports_an_async_lambda_passed_to_the_seeded_Reproducibly() {
const string source = """
using JustDummies;
using System.Threading.Tasks;

public static class Sample {
public static void M() {
Any.Reproducibly(42, async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new AsyncBodyPassedToReproduciblyAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(1);
Check.That(diagnostics[0].Id).IsEqualTo("JD001");
}

[Fact]
public async Task Does_not_report_a_synchronous_lambda() {
const string source = """
using JustDummies;

public static class Sample {
public static void M() {
Any.Reproducibly(() => { var _ = Any.Int32().Generate(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new AsyncBodyPassedToReproduciblyAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(0);
}

[Fact]
public async Task Does_not_report_a_Reproducibly_that_is_not_Any() {
// A same-named method on another type must not trip the rule — the analyzer keys on JustDummies.Any.
const string source = """
using System;
using System.Threading.Tasks;

public static class Other {
public static void Reproducibly(Action body) { }
}

public static class Sample {
public static void M() {
Other.Reproducibly(async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new AsyncBodyPassedToReproduciblyAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(0);
}

[Fact]
public async Task Does_not_report_an_async_lambda_passed_to_ReproduciblyAsync() {
const string source = """
using JustDummies;
using System.Threading.Tasks;

public static class Sample {
public static async Task M() {
await Any.ReproduciblyAsync(async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new AsyncBodyPassedToReproduciblyAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(0);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;

using NFluent;

namespace JustDummies.Analyzers.UnitTests;

public class Jd002DiscardedReproduciblyAsyncResultTests {

[Fact]
public async Task Reports_when_the_task_is_discarded_as_a_statement() {
const string source = """
using JustDummies;
using System.Threading.Tasks;

public static class Sample {
public static void M() {
Any.ReproduciblyAsync(async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new DiscardedReproduciblyAsyncResultAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(1);
Check.That(diagnostics[0].Id).IsEqualTo("JD002");
Check.That(diagnostics[0].GetMessage()).Contains("ReproduciblyAsync");
}

[Fact]
public async Task Reports_when_the_task_is_assigned_to_a_discard() {
const string source = """
using JustDummies;
using System.Threading.Tasks;

public static class Sample {
public static void M() {
_ = Any.ReproduciblyAsync(async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new DiscardedReproduciblyAsyncResultAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(1);
Check.That(diagnostics[0].Id).IsEqualTo("JD002");
}

[Fact]
public async Task Does_not_report_a_ReproduciblyAsync_that_is_not_Any() {
// A same-named method on another type must not trip the rule — the analyzer keys on JustDummies.Any.
const string source = """
using System;
using System.Threading.Tasks;

public static class Other {
public static Task ReproduciblyAsync(Func<Task> body) => Task.CompletedTask;
}

public static class Sample {
public static void M() {
Other.ReproduciblyAsync(async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new DiscardedReproduciblyAsyncResultAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(0);
}

[Fact]
public async Task Does_not_report_when_the_task_is_awaited() {
const string source = """
using JustDummies;
using System.Threading.Tasks;

public static class Sample {
public static async Task M() {
await Any.ReproduciblyAsync(async () => { await Task.Yield(); });
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new DiscardedReproduciblyAsyncResultAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(0);
}

[Fact]
public async Task Does_not_report_when_the_task_is_captured() {
const string source = """
using JustDummies;
using System.Threading.Tasks;

public static class Sample {
public static Task M() {
Task task = Any.ReproduciblyAsync(async () => { await Task.Yield(); });

return task;
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new DiscardedReproduciblyAsyncResultAnalyzer(), source);

Check.That(diagnostics.Length).IsEqualTo(0);
}

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

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NFluent" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.v3" />
</ItemGroup>

<ItemGroup>
<!-- ReferenceOutputAssembly so the analyzer's DiagnosticAnalyzer types are usable from tests (plain library
reference — the analyzer is NOT run over this test project). -->
<ProjectReference Include="..\JustDummies.Analyzers\JustDummies.Analyzers.csproj" />
<!-- The core is referenced so JustDummies.Any resolves inside the snippets the harness compiles. -->
<ProjectReference Include="..\JustDummies\JustDummies.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions JustDummies.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Loading
Loading