Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
fix(mocks): propagate [Obsolete] and null-forgiving raise dispatch (#…
  • Loading branch information
JohnVerheij committed Apr 19, 2026
commit eb62c8a9ad84c317780ac2bb9f557f24a931cee3
109 changes: 109 additions & 0 deletions TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,115 @@ void M()
return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_With_Obsolete_Members()
{
// Regression for #5626: members marked [Obsolete] on the source interface or
// base class previously caused CS0612/CS0618/CS0672 warnings to leak from the
// generated mock into consumer builds (a blocker for TreatWarningsAsErrors).
// The fix copies the [Obsolete] attribute onto every generated forward and
// override, since a member marked [Obsolete] may freely call other obsolete
// members without warning.
var source = """
using System;
using System.Threading.Tasks;
using TUnit.Mocks;

public interface IDialogService
{
[Obsolete("Use ShowAsync(options) instead")]
string? Show(string? title, string? message);

[Obsolete]
Task<string?> ShowPanel<TData>(TData? data) where TData : class;

[Obsolete("Removed", true)]
event EventHandler<string?>? Opened;

string? Greeting { [Obsolete] get; [Obsolete] set; }

// Asymmetric accessor cases: only the marked accessor should carry [Obsolete]
// on the generated forward, otherwise the unmarked accessor would gain a
// spurious CS0618 warning at the consumer call site.
string? Headline { [Obsolete] get; set; }
string? Subtitle { get; [Obsolete] set; }

// Exercises the message-escape path: embedded quotes and backslashes
// must round-trip through the generated attribute literal verbatim.
[Obsolete("Use \"NewMethod\" in C:\\New\\Path")]
string? WithTrickyChars();
}

public abstract class BaseDialog
{
[Obsolete]
public virtual string? Compute(string? input) => input;
}

public class TestUsage
{
void M()
{
var dialog = Mock.Of<IDialogService>();
var partial = Mock.Of<BaseDialog>();
}
}
""";

AssertGeneratedCodeHasNoNullableWarnings(source);
return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_FluentUI_Shape_Nullable_Warnings()
{
// Investigation for the CS8600/CS8604 portion of #5626. The reporter claimed
// these warnings fire against Microsoft.FluentUI.AspNetCore.Components.IDialogService.
// These are the exact patterns from FluentUI that earlier synthetic repros missed:
// unconstrained Task<T?> returns, class-constrained generics with nullable returns,
// and events with mixed-nullability delegate type arguments.
var source = """
using System;
using System.Threading.Tasks;
using TUnit.Mocks;

public interface IDialogReference
{
// Unconstrained generic + Task<T?> return: classic CS8600 trigger.
Task<T?> GetReturnValueAsync<T>();
}

public interface IDialogContentComponent { }
public interface IDialogContentComponent<TContent> : IDialogContentComponent { TContent Content { get; set; } }
public class DialogParameters { }
public class DialogParameters<TContent> : DialogParameters where TContent : class { public TContent Content { get; set; } = default!; }

public partial interface IDialogService
{
// Nullable return + class-constrained generic
Task<IDialogReference?> UpdateDialogAsync<TData>(string id, DialogParameters<TData> parameters) where TData : class;

// Non-nullable return + different generic-constraint shape
Task<IDialogReference> ShowDialogAsync<TDialog>(object data, DialogParameters parameters) where TDialog : IDialogContentComponent;

// Event with mixed nullable/non-nullable delegate type arguments
event Action<IDialogReference, Type?, DialogParameters, object>? OnShow;
}

public class TestUsage
{
void M()
{
var dialog = Mock.Of<IDialogService>();
var refs = Mock.Of<IDialogReference>();
}
}
""";

AssertGeneratedCodeHasNoNullableWarnings(source);
return VerifyGeneratorOutput(source);
}

[Test]
public Task Interface_Inheriting_Nested_Generic_IEnumerable()
{
Expand Down
41 changes: 37 additions & 4 deletions TUnit.Mocks.SourceGenerator.Tests/SnapshotTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,40 @@ protected static IReadOnlyList<Diagnostic> GetGeneratedCompilationErrors(
string source,
IEnumerable<MetadataReference>? additionalReferences = null,
CSharpParseOptions? parseOptions = null)
{
var (_, diagnostics) = RunGeneratorWithCompilationDiagnostics(source, additionalReferences, parseOptions);
return diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
}

/// <summary>
/// Compiles the generator output under nullable-enabled context and asserts that no
/// CS86xx-family nullable warnings (CS8600, CS8602, CS8603, CS8604, CS8618, CS8625)
/// are emitted. Used as a regression guard for #5626 and #5424/#5425/#5251.
/// Other compiler diagnostics (e.g. CS1520 from C# 14 extension() blocks not parseable
/// by the test-pinned Roslyn) are intentionally not asserted because they're
/// limitations of the test infrastructure, not the generator output.
/// </summary>
protected static void AssertGeneratedCodeHasNoNullableWarnings(
string source,
IEnumerable<MetadataReference>? additionalReferences = null,
CSharpParseOptions? parseOptions = null)
{
var (_, diagnostics) = RunGeneratorWithCompilationDiagnostics(source, additionalReferences, parseOptions);
var nullableWarnings = diagnostics
.Where(d => d.Severity >= DiagnosticSeverity.Warning && d.Id.StartsWith("CS86", StringComparison.Ordinal))
.ToList();
if (nullableWarnings.Count > 0)
{
var messages = string.Join(Environment.NewLine, nullableWarnings.Select(d => d.ToString()));
throw new InvalidOperationException(
$"Generated code emits {nullableWarnings.Count} CS86xx nullable warning(s):{Environment.NewLine}{messages}");
}
}

private static (Compilation Compilation, IReadOnlyList<Diagnostic> Diagnostics) RunGeneratorWithCompilationDiagnostics(
string source,
IEnumerable<MetadataReference>? additionalReferences,
CSharpParseOptions? parseOptions)
{
parseOptions ??= CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview);
var syntaxTree = CSharpSyntaxTree.ParseText(source, parseOptions);
Expand All @@ -156,7 +190,8 @@ protected static IReadOnlyList<Diagnostic> GetGeneratedCompilationErrors(
"TestAssembly",
[syntaxTree],
refs,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithNullableContextOptions(NullableContextOptions.Enable));

var generator = new MockGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create([generator.AsSourceGenerator()], parseOptions: parseOptions);
Expand All @@ -169,9 +204,7 @@ protected static IReadOnlyList<Diagnostic> GetGeneratedCompilationErrors(
throw new InvalidOperationException($"Generator produced errors:{Environment.NewLine}{errorMessages}");
}

return outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error)
.ToList();
return (outputCompilation, outputCompilation.GetDiagnostics());
}

private static async Task VerifySnapshot(
Expand Down
Loading