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
238 changes: 238 additions & 0 deletions JustDummies.Analyzers.UnitTests/Jd023Jd024ScalarChainTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;

using NFluent;

namespace JustDummies.Analyzers.UnitTests;

public class Jd023ScalarChainAdmitsNoValueTests {

[Theory]
[InlineData("Any.Int32().Between(1, 10).MultipleOf(20)")]
[InlineData("Any.Int32().GreaterThan(10).LessThan(3)")]
[InlineData("Any.Int32().Positive().LessThan(-5)")]
[InlineData("Any.Int32().Positive().Negative()")]
[InlineData("Any.Int32().Zero().NonZero()")]
[InlineData("Any.Int32().OneOf(5).Except(5)")]
[InlineData("Any.Int64().GreaterThanOrEqualTo(10).LessThanOrEqualTo(9)")]
public async Task Reports_a_chain_that_admits_no_value(string expression) {
string source = $$"""
using JustDummies;

public static class Sample {
public static void M() {
_ = {{expression}};
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

[Theory]
[InlineData("Any.Int32().Between(1, 10).MultipleOf(5)")]
[InlineData("Any.Int32().Positive().LessThan(100)")]
[InlineData("Any.Int32().Between(1, 10).Except(5)")]
[InlineData("Any.Int32().OneOf(1, 2, 3).Except(2)")]
[InlineData("Any.Int32().GreaterThan(-100).LessThan(100).MultipleOf(7)")]
public async Task Does_not_report_a_satisfiable_chain(string expression) {
string source = $$"""
using JustDummies;

public static class Sample {
public static void M() {
_ = {{expression}};
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

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

public static class Sample {
public static void M(int bound) {
_ = Any.Int32().GreaterThan(bound).LessThan(3);
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

[Fact]
public async Task Does_not_report_a_non_integer_generator() {
// The model is integer arithmetic; a floating-point domain does not behave like one.
const string source = """
using JustDummies;

public static class Sample {
public static void M() {
_ = Any.Double().Positive().LessThan(-5);
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

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

public static class Check2 {
public static void ThatCode(Func<object> code) { }
}

public static class Sample {
public static void M() {
Check2.ThatCode(() => Any.Int32().Positive().Negative());
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

}

public class Jd024ConstraintWithNoEffectTests {

[Fact]
public async Task Reports_an_exclusion_the_domain_could_never_produce() {
// The dangerous case: the author excluded a sentinel the generator was never going to draw. It silently
// misses, and starts mattering the day someone widens the range.
const string source = """
using JustDummies;

public static class Sample {
public static void M() {
_ = Any.Int32().Between(1, 10).Except(20);
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

Check.That(diagnostics.Length).IsEqualTo(1);
Check.That(diagnostics[0].Id).IsEqualTo("JD024");
Check.That(diagnostics[0].GetMessage()).Contains("removes no value");
}

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

public static class Sample {
public static void M() {
_ = Any.Int32().Positive().GreaterThan(-5);
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

Check.That(diagnostics.Length).IsEqualTo(1);
Check.That(diagnostics[0].Id).IsEqualTo("JD024");
Check.That(diagnostics[0].GetMessage()).Contains("already implied");
}

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

public static class Sample {
public static void M() {
_ = Any.Int32().Between(1, 10).Except(5);
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

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

public static class Sample {
public static void M() {
_ = Any.Int32().Positive().GreaterThan(100);
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

}

public class Jd023ScalarChainRepresentableExtremesTests {

[Theory]
[InlineData("Any.Int64().LessThanOrEqualTo(long.MinValue)")]
[InlineData("Any.Int64().GreaterThanOrEqualTo(long.MaxValue)")]
[InlineData("Any.Int32().LessThanOrEqualTo(int.MinValue)")]
public async Task Does_not_report_a_bound_at_a_representable_extreme(string expression) {
// Live in JustDummies.UnitTests/AnySignedIntegerTests.cs, which asserts these generate exactly that value.
// The first version used -long.MaxValue as its "unbounded" sentinel, which made long.MinValue
// unrepresentable and condemned a legal chain.
string source = $$"""
using JustDummies;

public static class Sample {
public static void M() {
_ = {{expression}};
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

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

public static class Sample {
public static void M() {
_ = Any.Int64().GreaterThan(long.MaxValue);
}
}
""";

ImmutableArray<Diagnostic> diagnostics = await AnalyzerTestHarness.GetDiagnosticsAsync(new ScalarChainAdmitsNoValueAnalyzer(), source, "JD023", "JD024");

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

}
2 changes: 2 additions & 0 deletions JustDummies.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ JD019 | JustDummies.Reproducibility | Disabled | CommittedReplaySeedAnalyzer
JD020 | JustDummies.Reproducibility | Info | SharedStaticAnyContextAnalyzer
JD021 | JustDummies.Reproducibility | Warning | BlankReplaySnippetAnalyzer
JD022 | JustDummies.Reproducibility | Info | ParallelDrawWithoutPerItemSeedAnalyzer
JD023 | JustDummies.Constraints | Warning | ScalarChainAdmitsNoValueAnalyzer
JD024 | JustDummies.Constraints | Info | ScalarChainAdmitsNoValueAnalyzer
20 changes: 20 additions & 0 deletions JustDummies.Analyzers/Descriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,24 @@ internal static class Descriptors {
description: "The ambient seed scope flows with the execution context, so a scope opened around a parallel loop reaches every worker and their draws interleave: neither the sequence nor the multiset is stable across runs. A scope opened inside the loop body gives each unit of work its own sequence, and the whole run replays — the shape the library's documentation names.",
helpLinkUri: HelpLinks.For(DiagnosticIds.ParallelDrawWithoutPerItemSeed));

public static readonly DiagnosticDescriptor ScalarChainAdmitsNoValue = new(
id: DiagnosticIds.ScalarChainAdmitsNoValue,
title: "The declared scalar constraints admit no value",
messageFormat: "No value satisfies this chain once {0} is applied",
category: DiagnosticCategories.Constraints,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "The constant constraints narrow the domain to nothing, so the chain throws a ConflictingAnyConstraintException the moment the arrange line runs. The library computes this with one emptiness test over bounds, lattice and allow-list; this rule runs the same test over the constants written at the call site, and stays silent for every argument it cannot fold.",
helpLinkUri: HelpLinks.For(DiagnosticIds.ScalarChainAdmitsNoValue));

public static readonly DiagnosticDescriptor ConstraintWithNoEffect = new(
id: DiagnosticIds.ConstraintWithNoEffect,
title: "A constraint narrows nothing",
messageFormat: "This constraint changes nothing: {0}",
category: DiagnosticCategories.Constraints,
defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true,
description: "The constraint is legal and inert: the domain it produces is the one that already existed. This is the only member of the constraint family the run time NEVER reports — every other contradiction throws eventually and loudly, while an inert constraint leaves the test green and exercising a domain the author did not write. The dangerous case is an exclusion of a sentinel the generator could never draw: it silently misses, and starts mattering the day someone widens the range.",
helpLinkUri: HelpLinks.For(DiagnosticIds.ConstraintWithNoEffect));

}
3 changes: 3 additions & 0 deletions JustDummies.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ internal static class DiagnosticIds {
public const string BlankReplaySnippet = "JD021";
public const string ParallelDrawWithoutPerItemSeed = "JD022";

public const string ScalarChainAdmitsNoValue = "JD023";
public const string ConstraintWithNoEffect = "JD024";

}
Loading
Loading