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: 10 additions & 2 deletions JustDummies.Analyzers.UnitTests/AnalyzerTestHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,22 @@ internal static class AnalyzerTestHarness {

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

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

CSharpCompilationOptions options = new(OutputKind.DynamicallyLinkedLibrary);
if (enabledDiagnosticIds.Length > 0) {
// Force otherwise opt-in (isEnabledByDefault: false) rules on for the test, as an .editorconfig would.
ImmutableDictionary<string, ReportDiagnostic>.Builder specific = ImmutableDictionary.CreateBuilder<string, ReportDiagnostic>();
foreach (string id in enabledDiagnosticIds) { specific[id] = ReportDiagnostic.Warn; }
options = options.WithSpecificDiagnosticOptions(specific.ToImmutable());
}

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;

using NFluent;

namespace JustDummies.Analyzers.UnitTests;

public class Jd011GeneratorWhereValueExpectedTests {

[Fact]
public async Task Reports_a_generator_bound_to_an_object_parameter() {
// The shape that matters: an assertion helper taking object inspects the recipe, not the value.
const string source = """
using JustDummies;

public static class Assert {
public static void NotNull(object value) { }
}

public static class Sample {
public static void M() {
Assert.NotNull(Any.String().NonEmpty());
}
}
""";

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

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

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

public static class Sample {
public static object[] Row() {
return new object[] { Any.Int32().Positive(), 1 };
}
}
""";

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

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

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

public static class Sample {
public static object M() {
object boxed = Any.Int32().Positive();

return boxed;
}
}
""";

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

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

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

public static class Sample {
public static bool M(string expected) {
return Any.String().NonEmpty().Equals(expected);
}
}
""";

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

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

[Fact]
public async Task Does_not_report_ReferenceEquals_between_two_generators() {
// How an immutability test proves a constraint returned a new generator. Generate() would destroy it.
// This shape is live in JustDummies.PropertyTests/ScalarIntervalProperties.cs.
const string source = """
using JustDummies;

public static class Sample {
public static bool M() {
AnyInt32 original = Any.Int32().Between(1, 10);
AnyInt32 narrowed = original.GreaterThanOrEqualTo(10);

return !ReferenceEquals(original, narrowed);
}
}
""";

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

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

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

public static class Sample {
public static bool M() {
AnyInt32 first = Any.Int32();
AnyInt32 second = Any.Int32();

return first.Equals(second);
}
}
""";

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

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

[Fact]
public async Task Does_not_report_a_throws_assertion_binding_to_Func_of_object() {
// Assert.Throws<T>(() => chain) binds to Func<object>, producing a real generator-to-object conversion at
// 88+ existing sites in this repository.
const string source = """
using System;
using JustDummies;

public static class Assert {
public static T Throws<T>(Func<object> code) where T : Exception => null!;
}

public static class Sample {
public static void M() {
Assert.Throws<ArgumentException>(() => Any.String().WithLength(3).StartingWith("ORD-"));
}
}
""";

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

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

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

public static class Assert {
public static void NotNull(object value) { }
}

public static class Sample {
public static void M() {
Assert.NotNull(Any.String().NonEmpty().Generate());
}
}
""";

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

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

[Fact]
public async Task Is_disabled_by_default() {
// The severity choice is the finding, not an incidental: dogfooding produced no true positive and two false
// ones, so the rule ships opt-in (ADR-0059's follow-up).
DiagnosticDescriptor descriptor = new GeneratorWhereValueExpectedAnalyzer().SupportedDiagnostics[0];

Check.That(descriptor.IsEnabledByDefault).IsFalse();
Check.That(descriptor.DefaultSeverity).IsEqualTo(DiagnosticSeverity.Warning);
}

}
105 changes: 105 additions & 0 deletions JustDummies.Analyzers.UnitTests/Jd012GeneratorPooledAsValueTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;

using NFluent;

namespace JustDummies.Analyzers.UnitTests;

public class Jd012GeneratorPooledAsValueTests {

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

public static class Sample {
public static void M() {
IAny<AnyInt32> pool = Any.OneOf(Any.Int32().Positive(), Any.Int32().Negative());
}
}
""";

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

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

[Fact]
public async Task Reports_a_pool_of_generators_on_a_seeded_context() {
// OneOf and ElementOf are mirrored on AnyContext; a rule keyed on Any alone would miss half the surface.
const string source = """
using JustDummies;

public static class Sample {
public static void M() {
AnyContext context = Any.WithSeed(1234);
IAny<AnyInt32> pool = context.OneOf(context.Int32(), context.Int32());
}
}
""";

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

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

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

public static class Sample {
public static void M() {
IAny<int> pool = Any.OneOf(1, 2, 3);
}
}
""";

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

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

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

public static class Sample {
public static void M() {
IAny<int> pool = Any.OneOf(Any.Int32().Positive().Generate(), Any.Int32().Negative().Generate());
}
}
""";

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

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

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

public static class Other {
public static T OneOf<T>(params T[] values) => values[0];
}

public static class Sample {
public static void M() {
AnyInt32 chosen = Other.OneOf(Any.Int32(), Any.Int32());
}
}
""";

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

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

}
Loading
Loading